提交代码
This commit is contained in:
39
src/components/custom-components/bind-dot-vue/index.vue
Normal file
39
src/components/custom-components/bind-dot-vue/index.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<p
|
||||
class="break-words w-1/1 h-1/1"
|
||||
:class="props.vertical ? 'text-vertical' : ''"
|
||||
:style="{ fontFamily: props.fontFamily, fontSize: props.fontSize + 'px', color: props.fill }"
|
||||
>
|
||||
{{ props.text }}
|
||||
</p>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
fontFamily: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
fontSize: {
|
||||
type: Number,
|
||||
default: 15
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
fill: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
vertical: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.text-vertical {
|
||||
writing-mode: tb;
|
||||
letter-spacing: 5px;
|
||||
}
|
||||
</style>
|
||||
39
src/components/custom-components/bind-index-vue/index.vue
Normal file
39
src/components/custom-components/bind-index-vue/index.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<p
|
||||
class="break-words w-1/1 h-1/1"
|
||||
:class="props.vertical ? 'text-vertical' : ''"
|
||||
:style="{ fontFamily: props.fontFamily, fontSize: props.fontSize + 'px', color: props.fill }"
|
||||
>
|
||||
{{ props.text }}
|
||||
</p>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
fontFamily: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
fontSize: {
|
||||
type: Number,
|
||||
default: 15
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
fill: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
vertical: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.text-vertical {
|
||||
writing-mode: tb;
|
||||
letter-spacing: 5px;
|
||||
}
|
||||
</style>
|
||||
39
src/components/custom-components/card-vue/index.vue
Normal file
39
src/components/custom-components/card-vue/index.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<!-- eslint-disable vue/html-indent -->
|
||||
<template>
|
||||
<div class="w-1/1 h-1/1 flex justify-center items-center custom-card">
|
||||
<el-card
|
||||
class="w-95/100 h-95/100"
|
||||
:shadow="props.shadow as any"
|
||||
:style="{
|
||||
'background-color': props.backGroundColor
|
||||
}"
|
||||
></el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ElCard } from 'element-plus'
|
||||
const props = defineProps({
|
||||
shadow: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
backGroundColor: {
|
||||
type: String,
|
||||
default: '#ffffff'
|
||||
},
|
||||
boxShadow: {
|
||||
type: String,
|
||||
default: '#ffffff'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="less">
|
||||
.custom-card {
|
||||
.el-card.is-always-shadow {
|
||||
box-shadow: v-bind('`0px 0px 12px ${$props.boxShadow}`') !important;
|
||||
}
|
||||
.el-card.is-hover-shadow:hover {
|
||||
box-shadow: v-bind('`0px 0px 12px ${$props.boxShadow}`') !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
71
src/components/custom-components/kv-vue/index.vue
Normal file
71
src/components/custom-components/kv-vue/index.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<table class="w-1/1 h-1/1 kvTable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="kvKey kvKeyValue" colspan="1">{{ props.label }}</td>
|
||||
<td class="kvValue kvKeyValue" colspan="1">{{ props.value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ElDescriptions, ElDescriptionsItem } from 'element-plus'
|
||||
import type { PropType } from 'vue'
|
||||
const props = defineProps({
|
||||
fontFamily: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
fontSize: {
|
||||
type: Number,
|
||||
default: 15
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
labelWidth: {
|
||||
type: Number,
|
||||
default: 50
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
valueWidth: {
|
||||
type: Number,
|
||||
default: 50
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
borderColor: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scroped>
|
||||
.kvTable {
|
||||
border: v-bind('`${props.border?1:0}px solid ${props.borderColor}`');
|
||||
}
|
||||
.kvKeyValue {
|
||||
font-size: v-bind('`${props.fontSize}px`');
|
||||
font-family: v-bind('`${props.fontFamily}`');
|
||||
color: v-bind('`${props.color}`');
|
||||
}
|
||||
.kvKey {
|
||||
width: v-bind('`${props.labelWidth}px`');
|
||||
border-right-width: v-bind('`${props.border?1:0}px`');
|
||||
border-right-style: solid;
|
||||
border-right-color: v-bind('`${props.borderColor}`') !important;
|
||||
}
|
||||
.kvValue {
|
||||
width: v-bind('`${props.valueWidth}px`');
|
||||
}
|
||||
</style>
|
||||
72
src/components/custom-components/now-time-vue/index.vue
Normal file
72
src/components/custom-components/now-time-vue/index.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<!-- eslint-disable vue/html-indent -->
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex">
|
||||
<div class="font-bold" :style="{ color: props.fontColor, fontSize: `${props.dateSize}px` }">
|
||||
{{ date }}
|
||||
</div>
|
||||
<div class="font-bold ml-5px" :style="{ color: props.fontColor, fontSize: `${props.weekSize}px ` }">
|
||||
{{ week }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="font-bold mt-5px ml-5px" :style="{ color: props.fontColor, fontSize: `${props.timeSize}px ` }">
|
||||
{{ time }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, onUnmounted } from 'vue'
|
||||
const props = defineProps({
|
||||
fontColor: {
|
||||
type: String,
|
||||
default: '#000000'
|
||||
},
|
||||
dateSize: {
|
||||
type: Number,
|
||||
default: 12
|
||||
},
|
||||
weekSize: {
|
||||
type: Number,
|
||||
default: 12
|
||||
},
|
||||
timeSize: {
|
||||
type: Number,
|
||||
default: 24
|
||||
}
|
||||
})
|
||||
const now_date = ref(new Date())
|
||||
const timer = ref()
|
||||
const date = computed(() => {
|
||||
const year = now_date.value.getFullYear()
|
||||
const month = now_date.value.getMonth() + 1
|
||||
const day = now_date.value.getDate()
|
||||
const time = year.toString() + '年' + month.toString() + '月' + day.toString() + '日'
|
||||
return time
|
||||
})
|
||||
const week = computed(() => {
|
||||
const d = now_date.value.getDay()
|
||||
const weekday = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
|
||||
const time = weekday[d]
|
||||
return time
|
||||
})
|
||||
const time = computed(() => {
|
||||
const hour = now_date.value.getHours()
|
||||
const minute = now_date.value.getMinutes()
|
||||
const second = now_date.value.getSeconds()
|
||||
const time =
|
||||
(hour < 10 ? '0' + hour.toString() : hour.toString()) +
|
||||
':' +
|
||||
(minute < 10 ? '0' + minute.toString() : minute.toString()) +
|
||||
':' +
|
||||
(second < 10 ? '0' + second.toString() : second.toString())
|
||||
return time
|
||||
})
|
||||
onMounted(() => {
|
||||
timer.value = setInterval(() => {
|
||||
now_date.value = new Date() // 修改数据date
|
||||
}, 500)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer.value)
|
||||
})
|
||||
</script>
|
||||
48
src/components/custom-components/sys-button-vue/index.vue
Normal file
48
src/components/custom-components/sys-button-vue/index.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div style="width: 100%; height: 100%">
|
||||
<button class="w-1/1 h-1/1" :style="getStyle(props.type, props.round)">
|
||||
<el-text>{{ props.text }}</el-text>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ElText } from 'element-plus'
|
||||
import type { PropType } from 'vue'
|
||||
type ButtonType = '' | 'default' | 'success' | 'warning' | 'info' | 'primary' | 'danger'
|
||||
const props = defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
default: '按钮文本'
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<ButtonType>,
|
||||
default: ''
|
||||
},
|
||||
round: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
const getStyle = (type: ButtonType, round: boolean) => {
|
||||
let bg_color = ''
|
||||
let border_radius = '4px'
|
||||
if (type == 'primary') {
|
||||
bg_color = '#409eff'
|
||||
} else if (type == 'success') {
|
||||
bg_color = '#67c23a'
|
||||
} else if (type == 'warning') {
|
||||
bg_color = '#e6a23c'
|
||||
} else if (type == 'danger') {
|
||||
bg_color = '#f56c6c'
|
||||
} else if (type == 'info') {
|
||||
bg_color = '#909399'
|
||||
}
|
||||
if (round) {
|
||||
border_radius = '20px'
|
||||
}
|
||||
return {
|
||||
backgroundColor: bg_color,
|
||||
borderRadius: border_radius
|
||||
}
|
||||
}
|
||||
</script>
|
||||
39
src/components/custom-components/text-vue/index.vue
Normal file
39
src/components/custom-components/text-vue/index.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<p
|
||||
class="break-words w-1/1 h-1/1"
|
||||
:class="props.vertical ? 'text-vertical' : ''"
|
||||
:style="{ fontFamily: props.fontFamily, fontSize: props.fontSize + 'px', color: props.fill }"
|
||||
>
|
||||
{{ props.text }}
|
||||
</p>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
fontFamily: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
fontSize: {
|
||||
type: Number,
|
||||
default: 15
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
fill: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
vertical: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.text-vertical {
|
||||
writing-mode: tb;
|
||||
letter-spacing: 5px;
|
||||
}
|
||||
</style>
|
||||
11
src/components/mt-dzr/__tests__/mt-dzr.spec.ts
Normal file
11
src/components/mt-dzr/__tests__/mt-dzr.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
import { mount } from '@vue/test-utils'
|
||||
import HelloWorld from '../index.vue'
|
||||
|
||||
describe('HelloWorld', () => {
|
||||
it('renders properly', () => {
|
||||
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
|
||||
expect(wrapper.text()).toContain('Hello Vitest')
|
||||
})
|
||||
})
|
||||
5
src/components/mt-dzr/components/render-item.vue
Normal file
5
src/components/mt-dzr/components/render-item.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="w-1/1 h-1/1 select-none">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
190
src/components/mt-dzr/components/resize-handle.vue
Normal file
190
src/components/mt-dzr/components/resize-handle.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-for="(item, key) in points"
|
||||
:key="key"
|
||||
:id="`resize_${key}`"
|
||||
:style="item"
|
||||
class="mt-dzr-resize mt-dzr-resize-point touch-none"
|
||||
@mousedown="e => onMouseDown(e, key)"
|
||||
@touchstart.passive="e => onMouseDown(e, key)"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import type { IDzrPropsModelValue } from '../types'
|
||||
import { dzrStore } from '../store'
|
||||
import {
|
||||
autoDestroyMouseMove,
|
||||
calcGrid,
|
||||
centerToTL,
|
||||
degToRadian,
|
||||
formatData,
|
||||
getLength,
|
||||
getNewStyle,
|
||||
getXY
|
||||
} from '../utils'
|
||||
import type { MouseTouchEvent } from '../utils/types'
|
||||
type ResizeProps = {
|
||||
itemInfo: IDzrPropsModelValue
|
||||
targetDom: HTMLElement | null
|
||||
scaleRatio: number
|
||||
gridAlignSize: number
|
||||
genId: string
|
||||
useProportionalScaling?: boolean
|
||||
}
|
||||
const resizeProps = withDefaults(defineProps<ResizeProps>(), {
|
||||
scaleRatio: 1,
|
||||
gridAlignSize: 1,
|
||||
useProportionalScaling: false
|
||||
})
|
||||
const points = computed(() => {
|
||||
return {
|
||||
tl: {
|
||||
left: '0px',
|
||||
top: '0px',
|
||||
cursor: getCursor(0)
|
||||
},
|
||||
tc: {
|
||||
left: resizeProps.itemInfo.width / 2 + 'px',
|
||||
top: '0px',
|
||||
cursor: getCursor(45)
|
||||
},
|
||||
tr: {
|
||||
left: resizeProps.itemInfo.width + 'px',
|
||||
top: '0px',
|
||||
cursor: getCursor(90)
|
||||
},
|
||||
l: {
|
||||
left: '0px',
|
||||
top: resizeProps.itemInfo.height / 2 + 'px',
|
||||
cursor: getCursor(315)
|
||||
},
|
||||
r: {
|
||||
left: resizeProps.itemInfo.width + 'px',
|
||||
top: resizeProps.itemInfo.height / 2 + 'px',
|
||||
cursor: getCursor(135)
|
||||
},
|
||||
bl: {
|
||||
left: '0px',
|
||||
top: resizeProps.itemInfo.height + 'px',
|
||||
cursor: getCursor(270)
|
||||
},
|
||||
bc: {
|
||||
left: resizeProps.itemInfo.width / 2 + 'px',
|
||||
top: resizeProps.itemInfo.height + 'px',
|
||||
cursor: getCursor(225)
|
||||
},
|
||||
br: {
|
||||
left: resizeProps.itemInfo.width + 'px',
|
||||
top: resizeProps.itemInfo.height + 'px',
|
||||
cursor: getCursor(180)
|
||||
}
|
||||
}
|
||||
})
|
||||
const angle_to_cursor = [
|
||||
{ start: 338, end: 23, cursor: 'nw' },
|
||||
{ start: 23, end: 68, cursor: 'n' },
|
||||
{ start: 68, end: 113, cursor: 'ne' },
|
||||
{ start: 293, end: 338, cursor: 'w' },
|
||||
{ start: 113, end: 158, cursor: 'e' },
|
||||
{ start: 248, end: 293, cursor: 'sw' },
|
||||
{ start: 203, end: 248, cursor: 's' },
|
||||
{ start: 158, end: 203, cursor: 'se' }
|
||||
]
|
||||
/**
|
||||
* 获取旋转之后的光标样式
|
||||
* @param init_angle 初始角度 360/8=45
|
||||
*/
|
||||
const getCursor = (init_angle: number) => {
|
||||
const now_init_angle = (init_angle + resizeProps.itemInfo.angle) % 360
|
||||
const find_cursor = angle_to_cursor.find(f => f.start <= now_init_angle && f.end > now_init_angle)
|
||||
if (!find_cursor) {
|
||||
return 'nw-resize'
|
||||
}
|
||||
return find_cursor.cursor + '-resize'
|
||||
}
|
||||
const emits = defineEmits(['update:itemInfo', 'onResizeDone', 'onResizeMove'])
|
||||
//记录原始位置
|
||||
const dzr_copy_info_value = ref({ ...resizeProps.itemInfo })
|
||||
const onMouseDown = (de: MouseTouchEvent, type: 'tl' | 'tc' | 'tr' | 'l' | 'r' | 'bl' | 'bc' | 'br') => {
|
||||
de.stopPropagation()
|
||||
dzr_copy_info_value.value = { ...resizeProps.itemInfo }
|
||||
const { clientX: de_client_x, clientY: de_client_y } = getXY(de)
|
||||
//计算组件中心点
|
||||
const { width, height, left, top } = resizeProps.itemInfo
|
||||
const centerX = left + width / 2
|
||||
const centerY = top + height / 2
|
||||
//记录原始信息和中心点
|
||||
const rect = {
|
||||
width,
|
||||
height,
|
||||
centerX,
|
||||
centerY,
|
||||
rotateAngle: resizeProps.itemInfo.angle
|
||||
}
|
||||
const onMouseMove = (me: MouseTouchEvent) => {
|
||||
me.preventDefault()
|
||||
dzrStore.showDzrCopy({ ...dzr_copy_info_value.value }, resizeProps.genId)
|
||||
const { clientX: me_client_x, clientY: me_client_y } = getXY(me)
|
||||
// 距离 网格对齐
|
||||
const delta_x = (me_client_x - de_client_x) / resizeProps.scaleRatio
|
||||
const delta_y = (me_client_y - de_client_y) / resizeProps.scaleRatio
|
||||
const alpha = Math.atan2(delta_y, delta_x)
|
||||
const delta_l = getLength(delta_x, delta_y)
|
||||
const beta = alpha - degToRadian(rect.rotateAngle)
|
||||
const deltaW = delta_l * Math.cos(beta)
|
||||
const deltaH = delta_l * Math.sin(beta)
|
||||
// 如果按shift键则等比缩放
|
||||
const ratio = resizeProps.useProportionalScaling || me.shiftKey ? rect.width / rect.height : undefined
|
||||
const {
|
||||
position: { centerX, centerY },
|
||||
size: { width, height }
|
||||
} = getNewStyle(type, { ...rect, rotateAngle: rect.rotateAngle }, deltaW, deltaH, ratio, 1, 1)
|
||||
const pData = centerToTL({
|
||||
centerX,
|
||||
centerY,
|
||||
width,
|
||||
height,
|
||||
angle: resizeProps.itemInfo.angle
|
||||
})
|
||||
const format_data = formatData(pData, centerX, centerY)
|
||||
const new_width = calcGrid(format_data.width, resizeProps.gridAlignSize)
|
||||
const new_height = calcGrid(format_data.height, resizeProps.gridAlignSize)
|
||||
emits('update:itemInfo', {
|
||||
...resizeProps.itemInfo,
|
||||
...format_data,
|
||||
left: calcGrid(format_data.left, resizeProps.gridAlignSize),
|
||||
top: calcGrid(format_data.top, resizeProps.gridAlignSize),
|
||||
width: new_width,
|
||||
height: new_height
|
||||
})
|
||||
emits('onResizeMove', {
|
||||
width: new_width,
|
||||
height: new_height
|
||||
})
|
||||
}
|
||||
autoDestroyMouseMove(onMouseMove, () => {
|
||||
dzrStore.hideDzrCopy()
|
||||
emits('onResizeDone')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.mt-dzr-resize {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.mt-dzr-resize-point {
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
border: 1px solid #59c7f9;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-left: -4px;
|
||||
margin-top: -4px;
|
||||
border-radius: 50%;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
86
src/components/mt-dzr/components/rotate-handle.vue
Normal file
86
src/components/mt-dzr/components/rotate-handle.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="mt-dzr-rotate touch-none" @mousedown="onMouseDown" @touchstart.passive="onMouseDown">
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M929 849a30 30 0 0 1-30-30v-83.137a447.514 447.514 0 0 1-70.921 92.209C722.935 933.225 578.442 975.008 442 953.482a444.917 444.917 0 0 1-241.139-120.591 30 30 0 1 1 37.258-47.01l0.231-0.231A385.175 385.175 0 0 0 442 892.625v-0.006c120.855 22.123 250.206-13.519 343.656-106.975a386.646 386.646 0 0 0 70.6-96.653h-87.247a30 30 0 0 1 0-60H929a30 30 0 0 1 30 30V819a30 30 0 0 1-30 30zM512 392a120 120 0 1 1-120 120 120 120 0 0 1 120-120z m293.005-147.025a29.87 29.87 0 0 1-19.117-6.882l-0.232 0.231A386.5 386.5 0 0 0 689.478 168h-0.011c-145.646-75.182-329.021-51.747-451.117 70.35a386.615 386.615 0 0 0-70.6 96.65H255a30 30 0 0 1 0 60H95a30 30 0 0 1-30-30V205a30 30 0 0 1 60 0v83.129a447.534 447.534 0 0 1 70.923-92.206C317.981 73.866 493.048 37.2 647 85.836v-0.045a444.883 444.883 0 0 1 176.143 105.291 30 30 0 0 1-18.138 53.893z"
|
||||
fill="#06B7FF"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { dzrStore } from '../store'
|
||||
import type { IDzrPropsModelValue } from '../types'
|
||||
import { alignToGrid, autoDestroyMouseMove, getXY } from '../utils'
|
||||
import type { MouseTouchEvent } from '../utils/types'
|
||||
type RotateProps = {
|
||||
itemInfo: IDzrPropsModelValue
|
||||
targetDom: HTMLElement | null
|
||||
genId: string
|
||||
}
|
||||
const rotateProps = withDefaults(defineProps<RotateProps>(), {})
|
||||
const emits = defineEmits(['update:itemInfo', 'onRotateDone', 'onRotateMove'])
|
||||
//记录原始位置
|
||||
const dzr_copy_info_value = ref({ ...rotateProps.itemInfo })
|
||||
const is_mouse_down = ref(false)
|
||||
const onMouseDown = (de: MouseTouchEvent) => {
|
||||
de.stopPropagation()
|
||||
if (!rotateProps.targetDom) {
|
||||
console.error('target_dom is null')
|
||||
return
|
||||
}
|
||||
const target_dom_rect = rotateProps.targetDom.getBoundingClientRect()
|
||||
if (!target_dom_rect) {
|
||||
console.error('boundingClientRect is null')
|
||||
return
|
||||
}
|
||||
const { clientX: de_client_x, clientY: de_client_y } = getXY(de)
|
||||
dzr_copy_info_value.value = { ...rotateProps.itemInfo }
|
||||
dzrStore.hideDzrCopy()
|
||||
//记录旋转前的初始值
|
||||
const init_angle = rotateProps.itemInfo.angle
|
||||
//计算组件中心点位置
|
||||
const center_x = target_dom_rect.left + target_dom_rect.width / 2
|
||||
const center_y = target_dom_rect.top + target_dom_rect.height / 2
|
||||
is_mouse_down.value = true
|
||||
const onMouseMove = (me: MouseTouchEvent) => {
|
||||
if (!is_mouse_down.value) {
|
||||
return
|
||||
}
|
||||
const { clientX: me_client_x, clientY: me_client_y } = getXY(me)
|
||||
dzrStore.showDzrCopy({ ...dzr_copy_info_value.value }, rotateProps.genId)
|
||||
// 旋转前的角度
|
||||
const rotate_before = Math.atan2(de_client_y - center_y, de_client_x - center_x) / (Math.PI / 180)
|
||||
// 旋转后的角度
|
||||
const rotate_after = Math.atan2(me_client_y - center_y, me_client_x - center_x) / (Math.PI / 180)
|
||||
const new_angle = alignToGrid(init_angle + rotate_after - rotate_before)
|
||||
emits('update:itemInfo', {
|
||||
...rotateProps.itemInfo,
|
||||
left: alignToGrid(rotateProps.itemInfo.left),
|
||||
top: alignToGrid(rotateProps.itemInfo.top),
|
||||
angle: new_angle
|
||||
})
|
||||
emits('onRotateMove', {
|
||||
angle: new_angle
|
||||
})
|
||||
}
|
||||
autoDestroyMouseMove(onMouseMove, () => {
|
||||
dzrStore.hideDzrCopy()
|
||||
is_mouse_down.value = false
|
||||
emits('onRotateDone')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.mt-dzr-rotate {
|
||||
position: absolute;
|
||||
cursor: grab;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
transform: translate(-50%, -160%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
font-size: 20px;
|
||||
}
|
||||
</style>
|
||||
0
src/components/mt-dzr/composables/mouse.ts
Normal file
0
src/components/mt-dzr/composables/mouse.ts
Normal file
3
src/components/mt-dzr/index.ts
Normal file
3
src/components/mt-dzr/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import MtDzr from './index.vue'
|
||||
|
||||
export default MtDzr
|
||||
276
src/components/mt-dzr/index.vue
Normal file
276
src/components/mt-dzr/index.vue
Normal file
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<div
|
||||
class="absolute select-none opacity-30"
|
||||
v-if="dzrStore.dzr_copy_info.show && dzrStore.dzr_copy_info.gen_id == gen_id && MtDzrProps.showGhostDom"
|
||||
style="outline: 1px solid #06b7ff"
|
||||
:style="getStyle(dzrStore.dzr_copy_info.value)"
|
||||
>
|
||||
<render-item>
|
||||
<slot></slot>
|
||||
</render-item>
|
||||
</div>
|
||||
<div
|
||||
v-if="!MtDzrProps.hide"
|
||||
:id="MtDzrProps.id"
|
||||
ref="dzrRef"
|
||||
:class="`${MtDzrProps.class} absolute select-none touch-none ${MtDzrProps.lock ? 'opacity-50' : ''} ${
|
||||
MtDzrProps.active && MtDzrProps.modelValue.width != 0 && MtDzrProps.modelValue.height != 0
|
||||
? 'dzr-active'
|
||||
: ''
|
||||
}`"
|
||||
@mousedown="onMouseDown"
|
||||
@touchstart.passive="onMouseDown"
|
||||
@mouseenter="onMouseEnter"
|
||||
@mouseleave="onMouseLeave"
|
||||
@click.right.prevent="onRightClick"
|
||||
:style="getStyle(drag_data_info)"
|
||||
>
|
||||
<render-item>
|
||||
<slot></slot>
|
||||
</render-item>
|
||||
<div v-if="MtDzrProps.resize && !MtDzrProps.lock && MtDzrProps.active && !MtDzrProps.disabled">
|
||||
<resize-handle
|
||||
v-model:item-info="mt_dzr_vmodel"
|
||||
:target-dom="dzrRef"
|
||||
:scale-ratio="MtDzrProps.scaleRatio"
|
||||
:grid-align-size="grid_align_size"
|
||||
:gen-id="gen_id"
|
||||
:use-proportional-scaling="MtDzrProps.useProportionalScaling"
|
||||
@on-resize-done="onResizeDone"
|
||||
@on-resize-move="val => onResizeMove(val)"
|
||||
></resize-handle>
|
||||
</div>
|
||||
<rotate-handle
|
||||
v-if="MtDzrProps.rotate && !MtDzrProps.lock && MtDzrProps.active && !MtDzrProps.disabled"
|
||||
v-model:item-info="mt_dzr_vmodel"
|
||||
:target-dom="dzrRef"
|
||||
:gen-id="gen_id"
|
||||
@on-rotate-done="onRotateDone"
|
||||
@on-rotate-move="val => onRotateMove(val)"
|
||||
></rotate-handle>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { type IDzrProps, type IDzrPropsModelValue } from './types'
|
||||
import { alignToGrid, autoDestroyMouseMove, getXY, randomString } from './utils/index'
|
||||
import ResizeHandle from './components/resize-handle.vue'
|
||||
import RotateHandle from './components/rotate-handle.vue'
|
||||
import renderItem from './components/render-item.vue'
|
||||
import { dzrStore } from './store/index'
|
||||
import type { MouseTouchEvent } from './utils/types'
|
||||
const MtDzrProps = withDefaults(defineProps<IDzrProps>(), {
|
||||
id: randomString(16),
|
||||
modelValue: () => {
|
||||
return {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
angle: 0
|
||||
}
|
||||
},
|
||||
scaleRatio: 1,
|
||||
grid: () => {
|
||||
return {
|
||||
enabled: false,
|
||||
align: false,
|
||||
size: 10
|
||||
}
|
||||
},
|
||||
resize: true,
|
||||
rotate: true,
|
||||
lock: false,
|
||||
active: false,
|
||||
useProportionalScaling: false,
|
||||
showGhostDom: true,
|
||||
hide: false,
|
||||
disabled: false,
|
||||
adsorp_diff: () => {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
}
|
||||
})
|
||||
const MtDzrEmits = defineEmits([
|
||||
'update:modelValue',
|
||||
'mousedown',
|
||||
'onItemMove',
|
||||
'moveMouseUp',
|
||||
'onMouseEnter',
|
||||
'onMouseLeave',
|
||||
'onResizeMove',
|
||||
'onResizeDone',
|
||||
'onRotateDone',
|
||||
'onRightClick',
|
||||
'onRotateMove'
|
||||
])
|
||||
const dzrRef = ref()
|
||||
//
|
||||
const gen_id = randomString(16)
|
||||
|
||||
//记录原始位置
|
||||
const dzr_copy_info_value = ref({ ...MtDzrProps.modelValue })
|
||||
// 拖拽数据
|
||||
const drag_data_info = ref({
|
||||
...MtDzrProps.modelValue
|
||||
})
|
||||
const getStyle = (data: IDzrPropsModelValue) => {
|
||||
const { width, height, left, top, angle } = data
|
||||
return {
|
||||
width: width + 'px',
|
||||
height: height + 'px',
|
||||
left: left + 'px',
|
||||
top: top + 'px',
|
||||
transform: `rotate(${angle}deg)`
|
||||
}
|
||||
}
|
||||
|
||||
//如果网格关闭或者没有开启网格对齐,网格大小为1
|
||||
const grid_align_size = computed(() => (!MtDzrProps.grid.align || !MtDzrProps.grid.enabled ? 1 : MtDzrProps.grid.size))
|
||||
const mt_dzr_vmodel = computed({
|
||||
get: () => drag_data_info.value,
|
||||
set: value => {
|
||||
drag_data_info.value = value
|
||||
MtDzrEmits('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
const onMouseDown = (de: MouseTouchEvent) => {
|
||||
MtDzrEmits('mousedown', de)
|
||||
if (MtDzrProps.lock || MtDzrProps.disabled) {
|
||||
return
|
||||
}
|
||||
//记录最开始点击时鼠标位置和组件的位置
|
||||
const { clientX: de_client_x, clientY: de_client_y } = getXY(de)
|
||||
dzr_copy_info_value.value = { ...MtDzrProps.modelValue }
|
||||
drag_data_info.value = {
|
||||
...MtDzrProps.modelValue
|
||||
}
|
||||
dzrStore.hideDzrCopy()
|
||||
const { left: init_x, top: init_y } = MtDzrProps.modelValue
|
||||
//计算xy轴最小坐标和最大坐标,不要超出父元素
|
||||
const { clientWidth, clientHeight } = dzrRef.value.parentElement
|
||||
const min_left = 0
|
||||
const min_top = 0
|
||||
const max_left = clientWidth - MtDzrProps.modelValue.width
|
||||
const max_top = clientHeight - MtDzrProps.modelValue.height
|
||||
let set_new_left = init_x
|
||||
let set_new_top = init_y
|
||||
const onMouseMove = (me: MouseTouchEvent) => {
|
||||
dzrStore.showDzrCopy({ ...dzr_copy_info_value.value }, gen_id)
|
||||
const { clientX: me_client_x, clientY: me_client_y } = getXY(me)
|
||||
const move_x = (me_client_x - de_client_x) / MtDzrProps.scaleRatio
|
||||
const move_y = (me_client_y - de_client_y) / MtDzrProps.scaleRatio
|
||||
|
||||
const new_left = alignToGrid(init_x + move_x, grid_align_size.value)
|
||||
const new_top = alignToGrid(init_y + move_y, grid_align_size.value)
|
||||
set_new_left = new_left < min_left ? min_left : new_left > max_left ? max_left : new_left
|
||||
set_new_top = new_top < min_top ? min_top : new_top > max_top ? max_top : new_top
|
||||
drag_data_info.value = {
|
||||
...drag_data_info.value,
|
||||
left: set_new_left,
|
||||
top: set_new_top
|
||||
}
|
||||
MtDzrEmits('onItemMove', {
|
||||
move_length: {
|
||||
x: new_left - init_x,
|
||||
y: new_top - init_y
|
||||
},
|
||||
new_lt: {
|
||||
left: set_new_left,
|
||||
top: set_new_top
|
||||
},
|
||||
// 因为是鼠标松开的时候才更新组件数据,所以这里需要把组件的实时binfo返回回去
|
||||
move_binfo: {
|
||||
id: MtDzrProps.id,
|
||||
left: set_new_left,
|
||||
top: set_new_top,
|
||||
width: drag_data_info.value.width,
|
||||
height: drag_data_info.value.height,
|
||||
angle: drag_data_info.value.angle
|
||||
}
|
||||
})
|
||||
nextTick(() => {
|
||||
const adsorp_diff_x = MtDzrProps.adsorp_diff?.x ?? 0
|
||||
const adsorp_diff_y = MtDzrProps.adsorp_diff?.y ?? 0
|
||||
// 当视图渲染之后 根据需要吸附的距离更新数据
|
||||
if (adsorp_diff_x == 0 && adsorp_diff_y == 0) {
|
||||
return
|
||||
}
|
||||
set_new_left += adsorp_diff_x
|
||||
set_new_top += adsorp_diff_y
|
||||
drag_data_info.value = {
|
||||
...drag_data_info.value,
|
||||
left: set_new_left,
|
||||
top: set_new_top
|
||||
}
|
||||
MtDzrEmits('onItemMove', {
|
||||
new_lt: {
|
||||
left: set_new_left,
|
||||
top: set_new_top
|
||||
},
|
||||
// 因为是鼠标松开的时候才更新组件数据,所以这里需要把组件的实时binfo返回回去
|
||||
move_binfo: {
|
||||
id: MtDzrProps.id,
|
||||
left: set_new_left,
|
||||
top: set_new_top,
|
||||
width: drag_data_info.value.width,
|
||||
height: drag_data_info.value.height,
|
||||
angle: drag_data_info.value.angle
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
autoDestroyMouseMove(onMouseMove, () => {
|
||||
nextTick(() => {
|
||||
dzrStore.hideDzrCopy()
|
||||
MtDzrEmits('moveMouseUp')
|
||||
MtDzrEmits('update:modelValue', {
|
||||
...MtDzrProps.modelValue,
|
||||
left: set_new_left,
|
||||
top: set_new_top
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
const onMouseEnter = (e: MouseEvent) => {
|
||||
MtDzrEmits('onMouseEnter', e)
|
||||
}
|
||||
const onMouseLeave = (e: MouseEvent) => {
|
||||
MtDzrEmits('onMouseLeave', e)
|
||||
}
|
||||
const onResizeMove = (val: any) => {
|
||||
MtDzrEmits('onResizeMove', val)
|
||||
}
|
||||
const onResizeDone = () => {
|
||||
MtDzrEmits('onResizeDone')
|
||||
}
|
||||
const onRotateDone = () => {
|
||||
MtDzrEmits('onRotateDone')
|
||||
}
|
||||
const onRotateMove = (val: any) => {
|
||||
MtDzrEmits('onRotateMove', val)
|
||||
}
|
||||
const onRightClick = (e: MouseEvent) => {
|
||||
MtDzrEmits('onRightClick', e)
|
||||
}
|
||||
watch(
|
||||
() => MtDzrProps.modelValue,
|
||||
value => {
|
||||
drag_data_info.value = value
|
||||
},
|
||||
{
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
</script>
|
||||
<style scoped>
|
||||
.dzr {
|
||||
position: absolute;
|
||||
user-select: none;
|
||||
}
|
||||
.dzr-active {
|
||||
outline: 1px solid #06b7ff;
|
||||
}
|
||||
</style>
|
||||
34
src/components/mt-dzr/store/index.ts
Normal file
34
src/components/mt-dzr/store/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { reactive } from 'vue'
|
||||
import type { IDzrCopyInfo, IDzrStore } from './types'
|
||||
import type { IDzrPropsModelValue } from '../types'
|
||||
|
||||
export const dzrStore: IDzrStore = reactive({
|
||||
dzr_copy_info: {
|
||||
gen_id: '',
|
||||
show: false,
|
||||
value: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
angle: 0
|
||||
}
|
||||
},
|
||||
setDzrCopyInfo: (value: IDzrCopyInfo) => {
|
||||
dzrStore.dzr_copy_info = value
|
||||
},
|
||||
showDzrCopy: (value: IDzrPropsModelValue, gen_id: string) => {
|
||||
dzrStore.setDzrCopyInfo({
|
||||
...dzrStore.dzr_copy_info,
|
||||
show: true,
|
||||
value,
|
||||
gen_id
|
||||
})
|
||||
},
|
||||
hideDzrCopy: () => {
|
||||
dzrStore.setDzrCopyInfo({
|
||||
...dzrStore.dzr_copy_info,
|
||||
show: false
|
||||
})
|
||||
}
|
||||
})
|
||||
13
src/components/mt-dzr/store/types.ts
Normal file
13
src/components/mt-dzr/store/types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { IDzrPropsModelValue } from '../types'
|
||||
|
||||
export interface IDzrStore {
|
||||
dzr_copy_info: IDzrCopyInfo
|
||||
setDzrCopyInfo: (value: IDzrCopyInfo) => void
|
||||
showDzrCopy: (value: IDzrPropsModelValue, gen_id: string) => void
|
||||
hideDzrCopy: () => void
|
||||
}
|
||||
export interface IDzrCopyInfo {
|
||||
gen_id: string
|
||||
show: boolean
|
||||
value: IDzrPropsModelValue
|
||||
}
|
||||
31
src/components/mt-dzr/types.ts
Normal file
31
src/components/mt-dzr/types.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export interface IDzrProps {
|
||||
id: string
|
||||
modelValue: IDzrPropsModelValue //位置和大小
|
||||
scaleRatio?: number //画布缩放倍数
|
||||
hide: boolean //隐藏
|
||||
grid?: IDzrPropsGrid //网格配置
|
||||
resize?: boolean //开启缩放
|
||||
rotate?: boolean //开启旋转
|
||||
lock?: boolean //锁定
|
||||
active?: boolean //激活
|
||||
useProportionalScaling?: boolean //开启等比例缩放
|
||||
showGhostDom?: boolean //是否显示幽灵dom
|
||||
class?: string //
|
||||
disabled: boolean //是否禁用
|
||||
adsorp_diff?: {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
}
|
||||
export interface IDzrPropsModelValue {
|
||||
left: number
|
||||
top: number
|
||||
width: number
|
||||
height: number
|
||||
angle: number
|
||||
}
|
||||
export interface IDzrPropsGrid {
|
||||
enabled: boolean //开启网格
|
||||
align: boolean //对齐到网格
|
||||
size: number //网格大小
|
||||
}
|
||||
349
src/components/mt-dzr/utils/index.ts
Normal file
349
src/components/mt-dzr/utils/index.ts
Normal file
@@ -0,0 +1,349 @@
|
||||
import type { IDzrPropsModelValue } from '../types'
|
||||
import type { MouseTouchEvent } from './types'
|
||||
|
||||
/**
|
||||
* 会自动销毁的鼠标移动事件
|
||||
* @param onMousemove
|
||||
*/
|
||||
export const autoDestroyMouseMove = (onMousemove: (e: MouseTouchEvent) => void, mouseUpCallBack?: () => void) => {
|
||||
const onMouseup = () => {
|
||||
document.removeEventListener('mousemove', onMousemove)
|
||||
document.removeEventListener('touchmove', onMousemove)
|
||||
document.removeEventListener('mouseup', onMouseup)
|
||||
document.removeEventListener('touchend', onMouseup)
|
||||
document.removeEventListener('mouseleave', onMouseup)
|
||||
if (mouseUpCallBack) {
|
||||
mouseUpCallBack()
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousemove', onMousemove)
|
||||
document.addEventListener('touchmove', onMousemove)
|
||||
document.addEventListener('mouseup', onMouseup)
|
||||
document.addEventListener('touchend', onMouseup)
|
||||
document.addEventListener('mouseleave', onMouseup)
|
||||
}
|
||||
/**
|
||||
* 根据坐标对齐到网格
|
||||
* @param position 当前坐标
|
||||
* @param grid 网格大小
|
||||
* @returns 对应网格的坐标
|
||||
*/
|
||||
export const alignToGrid = (position: number, grid = 1) => {
|
||||
const integerPart = Math.floor(position / grid)
|
||||
const fractionalPart = position % grid
|
||||
|
||||
if (fractionalPart >= grid / 2) {
|
||||
return (integerPart + 1) * grid
|
||||
} else {
|
||||
return integerPart * grid
|
||||
}
|
||||
}
|
||||
/** 根据移动的距离对齐到网格
|
||||
* @param diff 移动的距离
|
||||
* @param grid 网格大小
|
||||
*/
|
||||
export const calcGrid = (diff: number, grid = 1) => {
|
||||
// 得到每次缩放的余数
|
||||
const r = Math.abs(diff) % grid
|
||||
|
||||
// 正负grid
|
||||
const mulGrid = diff > 0 ? grid : -grid
|
||||
let result = 0
|
||||
// 余数大于grid的1/2
|
||||
if (r > grid / 2) {
|
||||
result = mulGrid * Math.ceil(Math.abs(diff) / grid)
|
||||
} else {
|
||||
result = mulGrid * Math.floor(Math.abs(diff) / grid)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
/**
|
||||
* 获取当前点击坐标 根据pc端和移动端获取
|
||||
* @param e
|
||||
* @returns
|
||||
*/
|
||||
export function getXY(e: MouseTouchEvent) {
|
||||
let clientX = 0,
|
||||
clientY = 0
|
||||
if (isTouchEvent(e)) {
|
||||
const touch = e.targetTouches[0]
|
||||
clientX = touch.pageX
|
||||
clientY = touch.pageY
|
||||
} else {
|
||||
clientX = e.clientX
|
||||
clientY = e.clientY
|
||||
}
|
||||
|
||||
return { clientX, clientY }
|
||||
}
|
||||
|
||||
function isTouchEvent(val: unknown): val is TouchEvent {
|
||||
const typeStr = Object.prototype.toString.call(val)
|
||||
return typeStr.substring(8, typeStr.length - 1) === 'TouchEvent'
|
||||
}
|
||||
|
||||
export const getLength = (x: number, y: number) => Math.sqrt(x * x + y * y)
|
||||
|
||||
export const degToRadian = (deg: number) => (deg * Math.PI) / 180
|
||||
const cos = (deg: number) => Math.cos(degToRadian(deg))
|
||||
const sin = (deg: number) => Math.sin(degToRadian(deg))
|
||||
/**
|
||||
* 计算并返回给定类型变换的新样式。
|
||||
*
|
||||
* @param {string} type - 变换的类型。
|
||||
* @param {any} rect - 矩形对象。
|
||||
* @param {number} deltaW - 宽度变化。
|
||||
* @param {number} deltaH - 高度变化。
|
||||
* @param {number | undefined} ratio - 比例。
|
||||
* @param {number} minWidth - 最小宽度。
|
||||
* @param {number} minHeight - 最小高度。
|
||||
* @returns {Object} 矩形的新位置和大小。
|
||||
*/
|
||||
export const getNewStyle = (
|
||||
type: string,
|
||||
rect: any,
|
||||
deltaW: number,
|
||||
deltaH: number,
|
||||
ratio: number | undefined,
|
||||
minWidth: number,
|
||||
minHeight: number
|
||||
) => {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { width, height, centerX, centerY, rotateAngle } = rect
|
||||
const widthFlag = width < 0 ? -1 : 1
|
||||
const heightFlag = height < 0 ? -1 : 1
|
||||
width = Math.abs(width)
|
||||
height = Math.abs(height)
|
||||
switch (type) {
|
||||
case 'r': {
|
||||
const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth)
|
||||
width = widthAndDeltaW.width
|
||||
deltaW = widthAndDeltaW.deltaW
|
||||
if (ratio) {
|
||||
deltaH = deltaW / ratio
|
||||
height = width / ratio
|
||||
// 左上角固定
|
||||
centerX += (deltaW / 2) * cos(rotateAngle) - (deltaH / 2) * sin(rotateAngle)
|
||||
centerY += (deltaW / 2) * sin(rotateAngle) + (deltaH / 2) * cos(rotateAngle)
|
||||
} else {
|
||||
// 左边固定
|
||||
centerX += (deltaW / 2) * cos(rotateAngle)
|
||||
centerY += (deltaW / 2) * sin(rotateAngle)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'tr': {
|
||||
deltaH = -deltaH
|
||||
const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth)
|
||||
width = widthAndDeltaW.width
|
||||
deltaW = widthAndDeltaW.deltaW
|
||||
const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight)
|
||||
height = heightAndDeltaH.height
|
||||
deltaH = heightAndDeltaH.deltaH
|
||||
if (ratio) {
|
||||
deltaW = deltaH * ratio
|
||||
width = height * ratio
|
||||
}
|
||||
centerX += (deltaW / 2) * cos(rotateAngle) + (deltaH / 2) * sin(rotateAngle)
|
||||
centerY += (deltaW / 2) * sin(rotateAngle) - (deltaH / 2) * cos(rotateAngle)
|
||||
break
|
||||
}
|
||||
case 'br': {
|
||||
const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth)
|
||||
width = widthAndDeltaW.width
|
||||
deltaW = widthAndDeltaW.deltaW
|
||||
const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight)
|
||||
height = heightAndDeltaH.height
|
||||
deltaH = heightAndDeltaH.deltaH
|
||||
if (ratio) {
|
||||
deltaW = deltaH * ratio
|
||||
width = height * ratio
|
||||
}
|
||||
centerX += (deltaW / 2) * cos(rotateAngle) - (deltaH / 2) * sin(rotateAngle)
|
||||
centerY += (deltaW / 2) * sin(rotateAngle) + (deltaH / 2) * cos(rotateAngle)
|
||||
break
|
||||
}
|
||||
case 'bc': {
|
||||
const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight)
|
||||
height = heightAndDeltaH.height
|
||||
deltaH = heightAndDeltaH.deltaH
|
||||
if (ratio) {
|
||||
deltaW = deltaH * ratio
|
||||
width = height * ratio
|
||||
// 左上角固定
|
||||
centerX += (deltaW / 2) * cos(rotateAngle) - (deltaH / 2) * sin(rotateAngle)
|
||||
centerY += (deltaW / 2) * sin(rotateAngle) + (deltaH / 2) * cos(rotateAngle)
|
||||
} else {
|
||||
// 上边固定
|
||||
centerX -= (deltaH / 2) * sin(rotateAngle)
|
||||
centerY += (deltaH / 2) * cos(rotateAngle)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'bl': {
|
||||
deltaW = -deltaW
|
||||
const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth)
|
||||
width = widthAndDeltaW.width
|
||||
deltaW = widthAndDeltaW.deltaW
|
||||
const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight)
|
||||
height = heightAndDeltaH.height
|
||||
deltaH = heightAndDeltaH.deltaH
|
||||
if (ratio) {
|
||||
height = width / ratio
|
||||
deltaH = deltaW / ratio
|
||||
}
|
||||
centerX -= (deltaW / 2) * cos(rotateAngle) + (deltaH / 2) * sin(rotateAngle)
|
||||
centerY -= (deltaW / 2) * sin(rotateAngle) - (deltaH / 2) * cos(rotateAngle)
|
||||
break
|
||||
}
|
||||
case 'l': {
|
||||
deltaW = -deltaW
|
||||
const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth)
|
||||
width = widthAndDeltaW.width
|
||||
deltaW = widthAndDeltaW.deltaW
|
||||
if (ratio) {
|
||||
height = width / ratio
|
||||
deltaH = deltaW / ratio
|
||||
// 右上角固定
|
||||
centerX -= (deltaW / 2) * cos(rotateAngle) + (deltaH / 2) * sin(rotateAngle)
|
||||
centerY -= (deltaW / 2) * sin(rotateAngle) - (deltaH / 2) * cos(rotateAngle)
|
||||
} else {
|
||||
// 右边固定
|
||||
centerX -= (deltaW / 2) * cos(rotateAngle)
|
||||
centerY -= (deltaW / 2) * sin(rotateAngle)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'tl': {
|
||||
deltaW = -deltaW
|
||||
deltaH = -deltaH
|
||||
const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth)
|
||||
width = widthAndDeltaW.width
|
||||
deltaW = widthAndDeltaW.deltaW
|
||||
const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight)
|
||||
height = heightAndDeltaH.height
|
||||
deltaH = heightAndDeltaH.deltaH
|
||||
if (ratio) {
|
||||
width = height * ratio
|
||||
deltaW = deltaH * ratio
|
||||
}
|
||||
centerX -= (deltaW / 2) * cos(rotateAngle) - (deltaH / 2) * sin(rotateAngle)
|
||||
centerY -= (deltaW / 2) * sin(rotateAngle) + (deltaH / 2) * cos(rotateAngle)
|
||||
break
|
||||
}
|
||||
case 'tc': {
|
||||
deltaH = -deltaH
|
||||
const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight)
|
||||
height = heightAndDeltaH.height
|
||||
deltaH = heightAndDeltaH.deltaH
|
||||
if (ratio) {
|
||||
width = height * ratio
|
||||
deltaW = deltaH * ratio
|
||||
// 左下角固定
|
||||
centerX += (deltaW / 2) * cos(rotateAngle) + (deltaH / 2) * sin(rotateAngle)
|
||||
centerY += (deltaW / 2) * sin(rotateAngle) - (deltaH / 2) * cos(rotateAngle)
|
||||
} else {
|
||||
centerX += (deltaH / 2) * sin(rotateAngle)
|
||||
centerY -= (deltaH / 2) * cos(rotateAngle)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
position: {
|
||||
centerX,
|
||||
centerY
|
||||
},
|
||||
size: {
|
||||
width: width * widthFlag,
|
||||
height: height * heightFlag
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 根据给定的参数设置高度和 deltaH 值。
|
||||
*
|
||||
* @param {number} height - 当前的高度值。
|
||||
* @param {number} deltaH - 高度变化值。
|
||||
* @param {number} minHeight - 最小高度值。
|
||||
* @return {object} - 包含更新后的高度和 deltaH 值的对象。
|
||||
*/
|
||||
const setHeightAndDeltaH = (height: number, deltaH: number, minHeight: number) => {
|
||||
const expectedHeight = height + deltaH
|
||||
if (expectedHeight > minHeight) {
|
||||
height = expectedHeight
|
||||
} else {
|
||||
deltaH = minHeight - height
|
||||
height = minHeight
|
||||
}
|
||||
return { height, deltaH }
|
||||
}
|
||||
/**
|
||||
* 设置元素的宽度和deltaW值。
|
||||
*
|
||||
* @param {number} width - 元素的当前宽度。
|
||||
* @param {number} deltaW - 元素宽度的变化量。
|
||||
* @param {number} minWidth - 元素的最小宽度。
|
||||
* @return {Object} - 包含更新后的宽度和deltaW值的对象。
|
||||
*/
|
||||
const setWidthAndDeltaW = (width: number, deltaW: number, minWidth: number) => {
|
||||
const expectedWidth = width + deltaW
|
||||
if (expectedWidth > minWidth) {
|
||||
width = expectedWidth
|
||||
} else {
|
||||
deltaW = minWidth - width
|
||||
width = minWidth
|
||||
}
|
||||
return { width, deltaW }
|
||||
}
|
||||
/**
|
||||
* 根据矩形的中心坐标、尺寸和角度计算左上角的位置。
|
||||
*
|
||||
* @param {object} params - 计算的参数。
|
||||
* @param {number} params.centerX - 矩形的中心点的 x 坐标。
|
||||
* @param {number} params.centerY - 矩形的中心点的 y 坐标。
|
||||
* @param {number} params.width - 矩形的宽度。
|
||||
* @param {number} params.height - 矩形的高度。
|
||||
* @param {number} params.angle - 矩形的旋转角度。
|
||||
* @return {object} - 矩形的左上角位置。
|
||||
*/
|
||||
export const centerToTL = ({ centerX, centerY, width, height, angle }: any): IDzrPropsModelValue => ({
|
||||
top: centerY - height / 2,
|
||||
left: centerX - width / 2,
|
||||
width,
|
||||
height,
|
||||
angle
|
||||
})
|
||||
/**
|
||||
* 格式化数据并返回一个包含更新后尺寸和位置的对象。
|
||||
*
|
||||
* @param {IDzrPropsModelValue} data - 包含宽度和高度的数据。
|
||||
* @param {number} centerX - 中心点的x坐标。
|
||||
* @param {number} centerY - 中心点的y坐标。
|
||||
* @return {object} - 一个包含更新后尺寸和位置的对象。
|
||||
*/
|
||||
export const formatData = (data: IDzrPropsModelValue, centerX: number, centerY: number) => {
|
||||
const { width, height } = data
|
||||
return {
|
||||
width: Math.abs(width),
|
||||
height: Math.abs(height),
|
||||
left: centerX - Math.abs(width) / 2,
|
||||
top: centerY - Math.abs(height) / 2
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 生成随机字符串
|
||||
* @param len 生成个数
|
||||
*/
|
||||
export const randomString = (len?: number) => {
|
||||
len = len || 10
|
||||
const str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
const maxPos = str.length
|
||||
let random_str = ''
|
||||
for (let i = 0; i < len; i++) {
|
||||
random_str += str.charAt(Math.floor(Math.random() * maxPos))
|
||||
}
|
||||
return random_str
|
||||
}
|
||||
1
src/components/mt-dzr/utils/types.ts
Normal file
1
src/components/mt-dzr/utils/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type MouseTouchEvent = MouseEvent | TouchEvent
|
||||
78
src/components/mt-edit/ace-edit.ts
Normal file
78
src/components/mt-edit/ace-edit.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import ace from 'ace-builds'
|
||||
|
||||
import themeMonokaiUrl from 'ace-builds/src-noconflict/theme-monokai?url'
|
||||
ace.config.setModuleUrl('ace/theme/monokai', themeMonokaiUrl)
|
||||
|
||||
import workerBaseUrl from 'ace-builds/src-noconflict/worker-base?url'
|
||||
ace.config.setModuleUrl('ace/mode/base', workerBaseUrl)
|
||||
|
||||
import modeJsonUrl from 'ace-builds/src-noconflict/mode-json?url'
|
||||
ace.config.setModuleUrl('ace/mode/json', modeJsonUrl)
|
||||
import workerJsonUrl from 'ace-builds/src-noconflict/worker-json?url'
|
||||
ace.config.setModuleUrl('ace/mode/json_worker', workerJsonUrl)
|
||||
import snippetsJsonUrl from 'ace-builds/src-noconflict/snippets/json?url'
|
||||
ace.config.setModuleUrl('ace/snippets/json', snippetsJsonUrl)
|
||||
|
||||
import modeJavascriptUrl from 'ace-builds/src-noconflict/mode-javascript?url'
|
||||
ace.config.setModuleUrl('ace/mode/javascript', modeJavascriptUrl)
|
||||
import workerJavascriptUrl from 'ace-builds/src-noconflict/worker-javascript?url'
|
||||
ace.config.setModuleUrl('ace/mode/javascript_worker', workerJavascriptUrl)
|
||||
import snippetsJavascriptUrl from 'ace-builds/src-noconflict/snippets/javascript?url'
|
||||
ace.config.setModuleUrl('ace/snippets/javascript', snippetsJavascriptUrl)
|
||||
|
||||
import 'ace-builds/src-noconflict/ext-language_tools'
|
||||
// ace.require('ace/ext/language_tools');
|
||||
const langTools = ace.require('ace/ext/language_tools')
|
||||
langTools.addCompleter({
|
||||
getCompletions: function (
|
||||
_editor: any,
|
||||
_session: any,
|
||||
_pos: any,
|
||||
prefix: string | any[],
|
||||
callback: (
|
||||
arg0: null,
|
||||
arg1: {
|
||||
name: string //显示的名称
|
||||
value: string //插入的值,
|
||||
score: number //分数
|
||||
meta: string //描述
|
||||
}[]
|
||||
) => void
|
||||
) {
|
||||
if (prefix.length === 0) {
|
||||
callback(null, [])
|
||||
return
|
||||
}
|
||||
callback(null, [
|
||||
{
|
||||
name: '$mtEventCallBack',
|
||||
value: '$mtEventCallBack(type,$item_info.id)',
|
||||
score: 1000,
|
||||
meta: '执行订阅回调函数'
|
||||
},
|
||||
{
|
||||
name: '$mtElMessage',
|
||||
value: '$mtElMessage.success("成功")',
|
||||
score: 1000,
|
||||
meta: '消息提示'
|
||||
},
|
||||
{
|
||||
name: '$mtElMessageBox',
|
||||
value: `$mtElMessageBox.alert('This is a message', 'Title', {
|
||||
confirmButtonText: 'OK',
|
||||
callback: (action) => {
|
||||
console.log(action)
|
||||
},
|
||||
})`,
|
||||
score: 1000,
|
||||
meta: '消息弹出框'
|
||||
},
|
||||
{
|
||||
name: '$item_info',
|
||||
value: '$item_info',
|
||||
score: 1000,
|
||||
meta: '回调函数中获取当前触发事件图形的信息'
|
||||
}
|
||||
])
|
||||
}
|
||||
})
|
||||
37
src/components/mt-edit/assets/css/custom_ani.css
Normal file
37
src/components/mt-edit/assets/css/custom_ani.css
Normal file
@@ -0,0 +1,37 @@
|
||||
@-webkit-keyframes rotate360 {
|
||||
0% {
|
||||
-webkit-transform: rotate3d(0, 0, 1, 0deg);
|
||||
transform: rotate3d(0, 0, 1, 0deg);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: rotate3d(0, 0, 1, 180deg);
|
||||
transform: rotate3d(0, 0, 1, 180deg);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: rotate3d(0, 0, 1, 360deg);
|
||||
transform: rotate3d(0, 0, 1, 360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate360 {
|
||||
0% {
|
||||
-webkit-transform: rotate3d(0, 0, 1, 0deg);
|
||||
transform: rotate3d(0, 0, 1, 0deg);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: rotate3d(0, 0, 1, 180deg);
|
||||
transform: rotate3d(0, 0, 1, 180deg);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: rotate3d(0, 0, 1, 360deg);
|
||||
transform: rotate3d(0, 0, 1, 360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate__rotate360 {
|
||||
-webkit-animation-name: rotate360;
|
||||
animation-name: rotate360;
|
||||
animation-timing-function: linear;
|
||||
-webkit-transform-origin: center;
|
||||
transform-origin: center;
|
||||
}
|
||||
280
src/components/mt-edit/components/add-element/index.vue
Normal file
280
src/components/mt-edit/components/add-element/index.vue
Normal file
@@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<div class="add-element">
|
||||
<el-dialog v-model="open" title="新增图元" width="500px" destroy-on-close @close="closeDialog">
|
||||
<el-form :model="element" ref="ruleFormRef" :rules="rules" label-width="120px">
|
||||
<el-form-item label="图元分类:" prop="elementSonType">
|
||||
<el-select v-model="element.elementSonType" placeholder="请选择图元分类" style="width: 100%">
|
||||
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="组件子类型:" prop="elementSonType">
|
||||
<el-select
|
||||
v-model="element.elementSonType"
|
||||
placeholder="请选择组件子类型"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options1.filter(
|
||||
(item) => item.key == element.elementType
|
||||
)"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item> -->
|
||||
<!-- <el-form-item label="图元状态:" prop="elementForm">
|
||||
<el-select
|
||||
v-if="element.elementSonType == '电力状态图元'"
|
||||
v-model="element.elementForm"
|
||||
placeholder="请选择图元状态"
|
||||
style="width: 100%"
|
||||
@change="
|
||||
(val) => {
|
||||
changeEvent(val, args);
|
||||
}
|
||||
"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in StatusList1"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-else="
|
||||
element.elementSonType == '电力系统基础图元' ||
|
||||
element.elementSonType == '自定义图元'
|
||||
"
|
||||
v-model="element.elementForm"
|
||||
placeholder="请选择图元状态"
|
||||
style="width: 100%"
|
||||
@change="
|
||||
(val) => {
|
||||
changeEvent(val, args);
|
||||
}
|
||||
"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in StatusList"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item> -->
|
||||
<!-- <el-form-item label="组件编码:" prop="elementCode">
|
||||
<el-input
|
||||
v-model="element.elementCode"
|
||||
placeholder="请选择组件编码"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item> -->
|
||||
<el-form-item label="图元名称:" prop="elementName">
|
||||
<el-input v-model="element.elementName" placeholder="请选择组件名称" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="组件标识:" prop="elementMark">
|
||||
<el-input v-model="element.elementMark" placeholder="请选择组件标识" style="width: 100%" />
|
||||
</el-form-item> -->
|
||||
<el-form-item label="上传svg:">
|
||||
<el-upload
|
||||
ref="upload"
|
||||
v-model="fileList"
|
||||
style="width: 415px"
|
||||
action="#"
|
||||
multiple
|
||||
accept="image/svg+xml"
|
||||
:http-request="UploadSvg"
|
||||
:on-remove="handleRemove"
|
||||
:before-upload="beforeAvatarUpload"
|
||||
:limit="1"
|
||||
:on-exceed="handleExceed"
|
||||
>
|
||||
<el-button type="primary">上传</el-button>
|
||||
</el-upload>
|
||||
<el-dialog v-model="dialogVisible">
|
||||
<img w-full :src="dialogImageUrl" alt="Preview Image" />
|
||||
</el-dialog>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="addNewComponent(ruleFormRef)" type="primary">保存</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineProps, getCurrentInstance, onMounted, ref, watch, reactive } from 'vue'
|
||||
import { ElInput, ElFormItem, ElForm, ElDialog, ElUpload, ElMessage, ElButton, ElSelect, ElOption } from 'element-plus'
|
||||
import type { UploadInstance, UploadProps, FormInstance, FormRules, UploadFile, UploadUserFile } from 'element-plus'
|
||||
|
||||
import { addElement, download } from '@/api/index'
|
||||
import { leftAsideStore } from '@/export'
|
||||
|
||||
const instance = getCurrentInstance() // 获取当前组件实例
|
||||
|
||||
const open = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
show: Boolean
|
||||
})
|
||||
|
||||
const element: any = ref({
|
||||
elementCode: '', //组件编码
|
||||
elementForm: '', // 图元状态
|
||||
elementMark: '', //组件标识
|
||||
elementName: '', //图元名称
|
||||
elementSonType: '', //组件子类型 图元分类
|
||||
elementType: '', //组件分类
|
||||
multipartFile: '' // 图元文件
|
||||
})
|
||||
|
||||
interface RuleForm {
|
||||
elementCode: string
|
||||
elementForm: string
|
||||
elementMark: string
|
||||
elementName: string
|
||||
elementSonType: string
|
||||
elementType: string
|
||||
}
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: '电力基础图元',
|
||||
label: '电力基础图元'
|
||||
},
|
||||
{
|
||||
value: '自定义',
|
||||
label: '自定义'
|
||||
}
|
||||
]
|
||||
|
||||
const fileList = ref<UploadFile[]>([]) // 上传文件列表
|
||||
const dialogImageUrl = ref('')
|
||||
const dialogVisible = ref(false) // 上传图片预览
|
||||
|
||||
const handleRemove = (file: UploadFile) => {
|
||||
fileList.value = []
|
||||
}
|
||||
// 文件校验
|
||||
const beforeAvatarUpload: UploadProps['beforeUpload'] = rawFile => {
|
||||
if (rawFile.type !== 'image/svg+xml') {
|
||||
ElMessage.error('只能上传svg格式文件!')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
const handleExceed: UploadProps['onExceed'] = files => {
|
||||
return ElMessage.error('只能上传1个svg文件!')
|
||||
}
|
||||
// 上传svg
|
||||
// const UploadSvg = (params:any) => {
|
||||
// fileList.value.push(params.file);
|
||||
// };
|
||||
|
||||
const UploadSvg = (params: any) => {
|
||||
return new Promise<void>(resolve => {
|
||||
fileList.value.push(params.file)
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
|
||||
const handlePictureCardPreview = (file: UploadFile) => {
|
||||
dialogImageUrl.value = file.url!
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const ruleFormRef = ref<FormInstance>()
|
||||
const rules = reactive<FormRules<RuleForm>>({
|
||||
elementCode: [{ required: true, message: '请输入组件编码', trigger: 'blur' }],
|
||||
elementForm: [{ required: true, message: '请选择图元状态', trigger: 'change' }],
|
||||
elementMark: [{ required: true, message: '请输入组件标识', trigger: 'blur' }],
|
||||
elementName: [{ required: true, message: '请输入图元名称', trigger: 'blur' }],
|
||||
elementSonType: [{ required: true, message: '请选择父类型', trigger: 'change' }],
|
||||
elementType: [{ required: true, message: '请选择组件分类', trigger: 'change' }]
|
||||
})
|
||||
|
||||
const closeDialog = () => {
|
||||
open.value = false
|
||||
if (instance) {
|
||||
const emit = instance.emit
|
||||
emit('update:show', false) // 向父组件发送更新后的状态
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
val => {
|
||||
if (val === true) {
|
||||
open.value = true
|
||||
element.value = {}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 新增保存
|
||||
|
||||
const addNewComponent = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate(valid => {
|
||||
if (valid) {
|
||||
if (fileList.value.length == 0) {
|
||||
ElMessage({
|
||||
message: '请上传svg文件!',
|
||||
type: 'warning'
|
||||
})
|
||||
return Promise.resolve() // 显式返回 Promise<void>
|
||||
}
|
||||
let form = new FormData()
|
||||
|
||||
form.append('elementCode', element.value.elementName)
|
||||
form.append('elementForm', '普通图元')
|
||||
form.append('elementMark', element.value.elementName)
|
||||
form.append('elementName', element.value.elementName)
|
||||
form.append('elementSonType', element.value.elementSonType)
|
||||
form.append('elementType', 'svg文件')
|
||||
form.append('multipartFile', fileList.value[0])
|
||||
|
||||
addElement(form).then((res: any) => {
|
||||
if (res.code == 'A0000') {
|
||||
ElMessage({
|
||||
message: '新增成功',
|
||||
type: 'success'
|
||||
})
|
||||
closeDialog()
|
||||
// 左侧列表渲染新增的图元
|
||||
download({ filePath: res.data.path }).then((Svg: any) => {
|
||||
// 动态添加svg
|
||||
leftAsideStore.svgPush(res.data.elementSonType, [
|
||||
{
|
||||
id: res.data.id,
|
||||
title: res.data.elementName,
|
||||
type: 'svg',
|
||||
thumbnail:
|
||||
'data:image/svg+xml;utf8,' +
|
||||
encodeURIComponent(Svg.replace(/(\sfill=(["']))[^"']*(\2)/g, '$1#000000$3')),
|
||||
svg: Svg.replace(/\sfill=(["'])[^"']*\1/g, ''),
|
||||
props: {
|
||||
fill: {
|
||||
type: 'color',
|
||||
val: '#FF0000',
|
||||
title: '填充色'
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
})
|
||||
} else {
|
||||
ElMessage({
|
||||
message: res.message,
|
||||
type: 'info'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
return Promise.resolve() // 确保所有分支都返回 Promise<void>
|
||||
})
|
||||
}
|
||||
</script>
|
||||
86
src/components/mt-edit/components/context-menu/index.vue
Normal file
86
src/components/mt-edit/components/context-menu/index.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<ul ref="contextMenuRef" class="contextMenu" v-show="show">
|
||||
<li
|
||||
v-for="(item, key) in contextMenuProps.menuInfo.info"
|
||||
:key="item.title"
|
||||
@click="onItemClick(key, item, $event)"
|
||||
>
|
||||
<p :class="item.enable ? '' : 'disabled'">
|
||||
{{ item.title }}
|
||||
<span class="shortcut">{{ item.hot_key }}</span>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuInfoType, IContextMenuDetail, IContextMenuInfo } from '../../store/types'
|
||||
|
||||
type ContextMenuProps = {
|
||||
menuInfo: IContextMenuDetail
|
||||
show: boolean
|
||||
}
|
||||
const contextMenuProps = withDefaults(defineProps<ContextMenuProps>(), {})
|
||||
const emits = defineEmits(['onContextMenuClick'])
|
||||
const onItemClick = (key: ContextMenuInfoType, item: IContextMenuInfo, e: MouseEvent) => {
|
||||
if (!item.enable) {
|
||||
return
|
||||
}
|
||||
emits('onContextMenuClick', key, e)
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.contextMenu {
|
||||
position: fixed;
|
||||
z-index: 99999;
|
||||
background: #ffffff;
|
||||
padding: 5px 0;
|
||||
margin: 0px;
|
||||
display: block;
|
||||
border-radius: 5px;
|
||||
box-shadow: 2px 5px 10px rgba(0, 0, 0, 0.3);
|
||||
left: v-bind('contextMenuProps.menuInfo.left + "px"');
|
||||
top: v-bind('contextMenuProps.menuInfo.top + "px"');
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
width: 115px;
|
||||
text-align: right;
|
||||
float: right;
|
||||
}
|
||||
|
||||
p {
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
padding: 0px 15px 1px 20px;
|
||||
margin: 0;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
p:hover {
|
||||
background-color: #0cf;
|
||||
color: #ffffff;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.disabled:hover {
|
||||
color: #999;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
li.separator {
|
||||
border-top: solid 1px #e3e3e3;
|
||||
padding-top: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<img draggable="false" class="w-1/1 h-1/1" :src="img_url" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
// @ts-nocheck
|
||||
import { onMounted, ref, useSlots, watch } from 'vue'
|
||||
import { renderToString } from 'vue/server-renderer'
|
||||
import { svgToImgSrc } from '../../utils'
|
||||
const img_url = ref('')
|
||||
const slots = useSlots()
|
||||
const setImgUrl = async () => {
|
||||
const slotNodes = slots.default ? slots.default() : []
|
||||
const slotStrings = await renderToString(slotNodes[0])
|
||||
img_url.value = svgToImgSrc(slotStrings)
|
||||
}
|
||||
onMounted(async () => {
|
||||
await setImgUrl()
|
||||
})
|
||||
watch(
|
||||
() => slots.default(),
|
||||
async () => {
|
||||
await setImgUrl()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
</script>
|
||||
51
src/components/mt-edit/components/done-tree/index.vue
Normal file
51
src/components/mt-edit/components/done-tree/index.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<el-tree
|
||||
:data="doneTreeProps.doneJson"
|
||||
:props="defaultProps"
|
||||
@node-click="handleNodeClick"
|
||||
:default-expand-all="true"
|
||||
:expand-on-click-node="false"
|
||||
:highlight-current="true"
|
||||
node-key="id"
|
||||
:current-node-key="current_node_key"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="flex justify-between w-8/10">
|
||||
<div>{{ node.label }}</div>
|
||||
<el-button text circle size="small" class="mr-10px">
|
||||
<el-icon :title="data.hide ? '隐藏' : '显示'" :size="20" @click.stop="changeHide(data)">
|
||||
<svg-analysis :name="data.hide ? 'view-hide' : 'view-show'"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ElTree, ElButton, ElIcon } from 'element-plus'
|
||||
import { computed } from 'vue'
|
||||
import type { IDoneJson } from '@/components/mt-edit/store/types'
|
||||
import SvgAnalysis from '@/components/mt-edit/components/svg-analysis/index.vue'
|
||||
type DoneTree = {
|
||||
doneJson: IDoneJson[]
|
||||
selectedItemsId: string[] //已选中组件的id
|
||||
}
|
||||
const doneTreeProps = withDefaults(defineProps<DoneTree>(), {})
|
||||
|
||||
const emits = defineEmits(['updateSelectedItemsId', 'updateSelectedIdHide'])
|
||||
|
||||
const current_node_key = computed(() => {
|
||||
return doneTreeProps.selectedItemsId.length == 1 ? doneTreeProps.selectedItemsId[0] : ''
|
||||
})
|
||||
const handleNodeClick = (data: IDoneJson) => {
|
||||
emits('updateSelectedItemsId', data.id)
|
||||
}
|
||||
const changeHide = (data: IDoneJson) => {
|
||||
emits('updateSelectedIdHide', data.id)
|
||||
}
|
||||
const defaultProps = {
|
||||
children: 'nochildren',
|
||||
label: 'title'
|
||||
}
|
||||
</script>
|
||||
49
src/components/mt-edit/components/drag-canvas/index.vue
Normal file
49
src/components/mt-edit/components/drag-canvas/index.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="hidden"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MouseTouchEvent } from '../types'
|
||||
type DragCanvasProps = {
|
||||
scaleRatio: number
|
||||
}
|
||||
const dragCanvasProps = withDefaults(defineProps<DragCanvasProps>(), {
|
||||
scaleRatio: 1
|
||||
})
|
||||
const emits = defineEmits(['dragCanvasMouseDown', 'dragCanvasMouseMove', 'dragCanvasMouseUp'])
|
||||
|
||||
const onMouseDown = (de: MouseTouchEvent) => {
|
||||
let move_x = 0
|
||||
let move_y = 0
|
||||
// 记录最开始点击时鼠标位置
|
||||
const d_x = de instanceof MouseEvent ? de.clientX : de.touches[0].pageX
|
||||
const d_y = de instanceof MouseEvent ? de.clientY : de.touches[0].pageY
|
||||
emits('dragCanvasMouseDown', d_x, d_y)
|
||||
const onMouseMove = (e: MouseTouchEvent) => {
|
||||
// 记录鼠标移动的位置
|
||||
const m_x = e instanceof MouseEvent ? e.clientX : e.touches[0].pageX
|
||||
const m_y = e instanceof MouseEvent ? e.clientY : e.touches[0].pageY
|
||||
// 移动的距离
|
||||
move_x = (m_x - d_x) / 1
|
||||
move_y = (m_y - d_y) / 1
|
||||
emits('dragCanvasMouseMove', move_x, move_y)
|
||||
}
|
||||
|
||||
const onMouseUp = () => {
|
||||
document.removeEventListener('mousemove', onMouseMove)
|
||||
document.removeEventListener('mouseup', onMouseUp)
|
||||
document.removeEventListener('touchmove', onMouseMove)
|
||||
document.removeEventListener('touchend', onMouseUp)
|
||||
emits('dragCanvasMouseUp', move_x, move_y)
|
||||
}
|
||||
document.addEventListener('mousemove', onMouseMove)
|
||||
document.addEventListener('mouseup', onMouseUp)
|
||||
document.addEventListener('touchmove', onMouseMove)
|
||||
document.addEventListener('touchend', onMouseUp)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
onMouseDown
|
||||
})
|
||||
</script>
|
||||
<style scoped></style>
|
||||
196
src/components/mt-edit/components/draw-line-render/index.vue
Normal file
196
src/components/mt-edit/components/draw-line-render/index.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<svg
|
||||
:id="lineRenderProps.itemJson.id"
|
||||
class="mt-line-render"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: `${-offset}px`,
|
||||
top: `${-offset}px`,
|
||||
width: `${lineRenderProps.canvasCfg.width + offset}px`,
|
||||
height: `${lineRenderProps.canvasCfg.height + offset}px`
|
||||
}"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
pointer-events="none"
|
||||
>
|
||||
<g>
|
||||
<defs>
|
||||
<marker
|
||||
:id="'markerArrowStart' + lineRenderProps.itemJson.id"
|
||||
viewBox="0 0 10 10"
|
||||
refX="8"
|
||||
refY="5"
|
||||
markerWidth="6"
|
||||
markerHeight="6"
|
||||
orient="auto-start-reverse"
|
||||
>
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" :fill="lineRenderProps.itemJson.props.stroke.val" />
|
||||
</marker>
|
||||
<marker
|
||||
:id="'markerArrowEnd' + lineRenderProps.itemJson.id"
|
||||
viewBox="0 0 10 10"
|
||||
refX="8"
|
||||
refY="5"
|
||||
markerWidth="6"
|
||||
markerHeight="6"
|
||||
orient="auto"
|
||||
>
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" :fill="lineRenderProps.itemJson.props.stroke.val" />
|
||||
</marker>
|
||||
</defs>
|
||||
<path
|
||||
:d="
|
||||
positionArrarToPath(
|
||||
lineRenderProps.itemJson.props.point_position.val,
|
||||
lineRenderProps.itemJson.binfo.left + offset,
|
||||
lineRenderProps.itemJson.binfo.top + offset
|
||||
)
|
||||
"
|
||||
pointer-events="visibleStroke"
|
||||
fill="none"
|
||||
:stroke="
|
||||
lineRenderProps.itemJson.props.ani_type.val === 'electricity'
|
||||
? lineRenderProps.itemJson.props.ani_color.val
|
||||
: lineRenderProps.itemJson.props.stroke.val
|
||||
"
|
||||
:stroke-width="lineRenderProps.itemJson.props['stroke-width'].val"
|
||||
style="cursor: move"
|
||||
stroke-dashoffset="0"
|
||||
:stroke-dasharray="
|
||||
lineRenderProps.itemJson.props.ani_type.val === 'electricity'
|
||||
? lineRenderProps.itemJson.props['stroke-width'].val * 3
|
||||
: 0
|
||||
"
|
||||
:marker-start="
|
||||
lineRenderProps.itemJson.props?.['marker-start']?.val
|
||||
? `url(#markerArrowStart${lineRenderProps.itemJson.id})`
|
||||
: ''
|
||||
"
|
||||
:marker-end="
|
||||
lineRenderProps.itemJson.props?.['marker-end']?.val
|
||||
? `url(#markerArrowEnd${lineRenderProps.itemJson.id})`
|
||||
: ''
|
||||
"
|
||||
class="real"
|
||||
>
|
||||
<animate
|
||||
v-if="lineRenderProps.itemJson.props.ani_type.val === 'electricity'"
|
||||
attributeName="stroke-dashoffset"
|
||||
:from="lineRenderProps.itemJson.props.ani_reverse.val ? 0 : 1000"
|
||||
:to="
|
||||
lineRenderProps.itemJson.props.ani_reverse.val
|
||||
? lineRenderProps.itemJson.props.ani_play.val
|
||||
? 1000
|
||||
: 0
|
||||
: lineRenderProps.itemJson.props.ani_play.val
|
||||
? 0
|
||||
: 1000
|
||||
"
|
||||
:dur="`${
|
||||
lineRenderProps.itemJson.props.ani_dur.val < 1 ? 1 : lineRenderProps.itemJson.props.ani_dur.val
|
||||
}s`"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</path>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { MouseTouchEvent } from '@/components/mt-dzr/utils/types'
|
||||
import type { IDoneJson, IGlobalStoreCanvasCfg, IGlobalStoreGridCfg } from '../../store/types'
|
||||
import { alignToGrid, positionArrarToPath } from '@/components/mt-edit/utils'
|
||||
import { computed } from 'vue'
|
||||
import { configStore } from '../../store/config'
|
||||
type LineRenderProps = {
|
||||
itemJson: IDoneJson
|
||||
canvasCfg: IGlobalStoreCanvasCfg
|
||||
grid: IGlobalStoreGridCfg
|
||||
canvasDom: HTMLElement | null
|
||||
mode: 'pen' | 'pencil'
|
||||
}
|
||||
const lineRenderProps = withDefaults(defineProps<LineRenderProps>(), {
|
||||
mode: 'pen'
|
||||
})
|
||||
const lineRenderEmits = defineEmits(['drawLineEnd'])
|
||||
const offset = configStore.lineRenderOffset
|
||||
//如果网格关闭或者没有开启网格对齐,网格大小为1
|
||||
const grid_align_size = computed(() =>
|
||||
!lineRenderProps.grid.align || !lineRenderProps.grid.enabled ? 1 : lineRenderProps.grid.size
|
||||
)
|
||||
const onMouseDown = (de: MouseTouchEvent, point_index: number, item: { x: number; y: number }) => {
|
||||
de.stopPropagation()
|
||||
// 记录鼠标按下时实际点的坐标
|
||||
const { x: realityX, y: realityY } = item
|
||||
// 记录最开始点击时鼠标位置
|
||||
const d_x = de instanceof MouseEvent ? de.clientX : de.touches[0].pageX
|
||||
const d_y = de instanceof MouseEvent ? de.clientY : de.touches[0].pageY
|
||||
let new_x = 0
|
||||
let new_y = 0
|
||||
const onMouseMove = (e: MouseTouchEvent) => {
|
||||
// 记录鼠标移动的位置
|
||||
const m_x = e instanceof MouseEvent ? e.clientX : e.touches[0].pageX
|
||||
const m_y = e instanceof MouseEvent ? e.clientY : e.touches[0].pageY
|
||||
// 移动的距离
|
||||
const move_x = de.ctrlKey ? 0 : alignToGrid((m_x - d_x) / lineRenderProps.canvasCfg.scale, 1) //感觉对齐网格有点体验不好 所以固定为一了
|
||||
const move_y = de.shiftKey ? 0 : alignToGrid((m_y - d_y) / lineRenderProps.canvasCfg.scale, 1)
|
||||
new_x = realityX + move_x
|
||||
new_y = realityY + move_y
|
||||
if (lineRenderProps.mode == 'pencil') {
|
||||
const new_point_position = lineRenderProps.itemJson.props.point_position.val
|
||||
new_point_position.push({
|
||||
x: new_x,
|
||||
y: new_y
|
||||
})
|
||||
return
|
||||
} else {
|
||||
const new_point_position = lineRenderProps.itemJson.props.point_position.val
|
||||
new_point_position[point_index].x = new_x
|
||||
new_point_position[point_index].y = new_y
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseUp = () => {
|
||||
document.removeEventListener('mousemove', onMouseMove)
|
||||
document.removeEventListener('mouseup', onMouseUp)
|
||||
document.removeEventListener('touchmove', onMouseMove)
|
||||
document.removeEventListener('touchend', onMouseUp)
|
||||
const itemRect = document.querySelector(`#${lineRenderProps.itemJson.id} g .real`)!.getBoundingClientRect()
|
||||
const canvas_area_bounding_info = lineRenderProps.canvasDom!.getBoundingClientRect()
|
||||
const new_left = (itemRect?.left - canvas_area_bounding_info?.left) / lineRenderProps.canvasCfg.scale
|
||||
const new_top = (itemRect?.top - canvas_area_bounding_info?.top) / lineRenderProps.canvasCfg.scale
|
||||
const move_x = new_left - lineRenderProps.itemJson.binfo.left
|
||||
const move_y = new_top - lineRenderProps.itemJson.binfo.top
|
||||
const new_item_json = {
|
||||
...lineRenderProps.itemJson,
|
||||
binfo: {
|
||||
...lineRenderProps.itemJson.binfo,
|
||||
left: new_left,
|
||||
top: new_top,
|
||||
width: itemRect?.width / lineRenderProps.canvasCfg.scale,
|
||||
height: itemRect?.height / lineRenderProps.canvasCfg.scale
|
||||
},
|
||||
props: {
|
||||
...lineRenderProps.itemJson.props,
|
||||
point_position: {
|
||||
...lineRenderProps.itemJson.props.point_position,
|
||||
val: lineRenderProps.itemJson.props.point_position.val.map((m: { x: number; y: number }) => {
|
||||
return {
|
||||
x: m.x - move_x,
|
||||
y: m.y - move_y
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
lineRenderEmits('drawLineEnd', new_item_json)
|
||||
}
|
||||
document.addEventListener('mousemove', onMouseMove)
|
||||
document.addEventListener('mouseup', onMouseUp)
|
||||
document.addEventListener('touchmove', onMouseMove)
|
||||
document.addEventListener('touchend', onMouseUp)
|
||||
}
|
||||
defineExpose({
|
||||
onMouseDown
|
||||
})
|
||||
</script>
|
||||
<style scoped></style>
|
||||
35
src/components/mt-edit/components/export-json/index.vue
Normal file
35
src/components/mt-edit/components/export-json/index.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-ace-editor
|
||||
v-model:value="export_json"
|
||||
lang="json"
|
||||
theme="monokai"
|
||||
style="height: 400px"
|
||||
:options="{
|
||||
useWorker: true,
|
||||
enableBasicAutocompletion: true,
|
||||
enableSnippets: true,
|
||||
enableLiveAutocompletion: true
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { VAceEditor } from 'vue3-ace-editor'
|
||||
import type { IDoneJson, IGlobalStoreCanvasCfg, IGlobalStoreGridCfg } from '../../store/types'
|
||||
import { computed } from 'vue'
|
||||
import { genExportJson } from '../../composables'
|
||||
type ExportProps = {
|
||||
doneJson: IDoneJson[]
|
||||
canvasCfg: IGlobalStoreCanvasCfg
|
||||
gridCfg: IGlobalStoreGridCfg
|
||||
}
|
||||
const exportProps = withDefaults(defineProps<ExportProps>(), {})
|
||||
const export_json = computed({
|
||||
get: () => {
|
||||
const { exportJson } = genExportJson(exportProps.canvasCfg, exportProps.gridCfg, exportProps.doneJson)
|
||||
return JSON.stringify(exportJson, null, 2)
|
||||
},
|
||||
set: () => {}
|
||||
})
|
||||
</script>
|
||||
44
src/components/mt-edit/components/group-render/index.vue
Normal file
44
src/components/mt-edit/components/group-render/index.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="mt-group">
|
||||
<div
|
||||
class="absolute"
|
||||
v-for="item in groupRender.itemJson.children"
|
||||
:key="item.id"
|
||||
:id="item.id"
|
||||
:style="{
|
||||
left: item.binfo.left + '%',
|
||||
top: item.binfo.top + '%',
|
||||
width: item.binfo.width + '%',
|
||||
height: item.binfo.height + '%',
|
||||
transform: `rotate(${item.binfo.angle}deg)`
|
||||
}"
|
||||
>
|
||||
<render-item
|
||||
:item-json="item"
|
||||
:grid="groupRender.grid"
|
||||
:canvas-cfg="groupRender.canvasCfg"
|
||||
:canvas-dom="groupRender.canvasDom"
|
||||
:lock-state="false"
|
||||
></render-item>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import RenderItem from '@/components/mt-edit/components/render-item/index.vue'
|
||||
import type { IDoneJson, IGlobalStoreCanvasCfg, IGlobalStoreGridCfg } from '@/components/mt-edit/store/types'
|
||||
type GroupRender = {
|
||||
itemJson: IDoneJson
|
||||
grid: IGlobalStoreGridCfg
|
||||
canvasCfg: IGlobalStoreCanvasCfg
|
||||
canvasDom: HTMLElement | null
|
||||
}
|
||||
const groupRender = withDefaults(defineProps<GroupRender>(), {})
|
||||
</script>
|
||||
<style scoped>
|
||||
.mt-group {
|
||||
left: v-bind('groupRender.itemJson.binfo.left+"px"');
|
||||
top: v-bind('groupRender.itemJson.binfo.top+"px"');
|
||||
width: v-bind('groupRender.itemJson.binfo.width+"px"');
|
||||
height: v-bind('groupRender.itemJson.binfo.height+"px"');
|
||||
}
|
||||
</style>
|
||||
44
src/components/mt-edit/components/import-json/index.vue
Normal file
44
src/components/mt-edit/components/import-json/index.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-ace-editor
|
||||
v-model:value="import_json"
|
||||
lang="json"
|
||||
theme="monokai"
|
||||
style="height: 400px"
|
||||
:options="{
|
||||
useWorker: true,
|
||||
enableBasicAutocompletion: true,
|
||||
enableSnippets: true,
|
||||
enableLiveAutocompletion: true
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { VAceEditor } from 'vue3-ace-editor'
|
||||
import { ref } from 'vue'
|
||||
import type { IExportJson } from '../types'
|
||||
import { globalStore } from '../../store/global'
|
||||
import { useExportJsonToDoneJson } from '../../composables'
|
||||
const import_json = ref('')
|
||||
const onImport = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const json: IExportJson = JSON.parse(import_json.value)
|
||||
console.log('🚀 ~ onImport ~ json:', json)
|
||||
|
||||
const { canvasCfg, gridCfg, importDoneJson } = useExportJsonToDoneJson(json)
|
||||
|
||||
globalStore.canvasCfg = canvasCfg
|
||||
globalStore.gridCfg = gridCfg
|
||||
globalStore.setGlobalStoreDoneJson(importDoneJson)
|
||||
resolve(true)
|
||||
} catch (error) {
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
defineExpose({
|
||||
onImport
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div class="flex justify-center items-center pt-2">
|
||||
<el-text>Copyright (c) 2025</el-text>
|
||||
|
||||
<el-link class="ml-10px" href="http://www.shining-electric.com/" target="_blank">
|
||||
南京灿能电气自动化股份有限公司
|
||||
</el-link>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ElText, ElLink } from 'element-plus'
|
||||
</script>
|
||||
333
src/components/mt-edit/components/layout/header-panel/index.vue
Normal file
333
src/components/mt-edit/components/layout/header-panel/index.vue
Normal file
@@ -0,0 +1,333 @@
|
||||
<template>
|
||||
<div class="flex justify-between" style="width: 100%; padding-top: 10px">
|
||||
<div class="flex items-center justify-between w-200px">
|
||||
<div class="flex items-center">
|
||||
<!-- <el-image
|
||||
class="w-45px h-45px pl-20px"
|
||||
src="data:image/svg+xml;utf8,%3Csvg%20id%3D%22%E7%BB%84_2%22%20data-name%3D%22%E7%BB%84%202%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22700%22%20height%3D%22700%22%20viewBox%3D%220%200%20700%20700%22%3E%0A%20%20%3Cdefs%3E%0A%20%20%20%20%3Cstyle%3E%0A%20%20%20%20%20%20.cls-1%20%7B%0A%20%20%20%20%20%20%20%20fill%3A%20none%3B%0A%20%20%20%20%20%20%20%20stroke%3A%20%2300ccbd%3B%0A%20%20%20%20%20%20%20%20stroke-width%3A%2020px%3B%0A%20%20%20%20%20%20%20%20fill-rule%3A%20evenodd%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20.cls-2%20%7B%0A%20%20%20%20%20%20%20%20font-size%3A%20100px%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20.cls-2%2C%20.cls-3%20%7B%0A%20%20%20%20%20%20%20%20fill%3A%20%23deea2e%3B%0A%20%20%20%20%20%20%20%20font-weight%3A%20700%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20.cls-3%20%7B%0A%20%20%20%20%20%20%20%20font-size%3A%20120px%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%3C%2Fstyle%3E%0A%20%20%3C%2Fdefs%3E%0A%20%20%3Cg%20id%3D%22%E7%BB%84_1%22%20data-name%3D%22%E7%BB%84%201%22%3E%0A%20%20%20%20%3Cpath%20id%3D%22%E5%BD%A2%E7%8A%B6_1%22%20data-name%3D%22%E5%BD%A2%E7%8A%B6%201%22%20class%3D%22cls-1%22%20d%3D%22M34.518%2C406.156S47.789%2C166%2C187.922%2C211.451c0%2C0%2C55.156%2C15.864%2C91.453%2C45.448%2C0%2C0%2C67.7-42.655%2C141.6%2C0%22%2F%3E%0A%20%20%20%20%3Cpath%20id%3D%22%E5%BD%A2%E7%8A%B6_1_%E6%8B%B7%E8%B4%9D%22%20data-name%3D%22%E5%BD%A2%E7%8A%B6%201%20%E6%8B%B7%E8%B4%9D%22%20class%3D%22cls-1%22%20d%3D%22M665.493%2C406.156s-13.271-240.373-153.4-194.884c0%2C0-55.156%2C15.879-91.452%2C45.489%2C0%2C0-67.7-42.694-141.6%2C0%22%2F%3E%0A%20%20%20%20%3Cpath%20id%3D%22%E5%BD%A2%E7%8A%B6_2%22%20data-name%3D%22%E5%BD%A2%E7%8A%B6%202%22%20class%3D%22cls-1%22%20d%3D%22M117.921%2C306.109s9.026-64.564%2C74.891-32.1%22%2F%3E%0A%20%20%20%20%3Cpath%20id%3D%22%E5%BD%A2%E7%8A%B6_2_%E6%8B%B7%E8%B4%9D%22%20data-name%3D%22%E5%BD%A2%E7%8A%B6%202%20%E6%8B%B7%E8%B4%9D%22%20class%3D%22cls-1%22%20d%3D%22M581.357%2C306.589s-9-65.089-74.668-32.357%22%2F%3E%0A%20%20%20%20%3Ctext%20id%3D%22ao%22%20class%3D%22cls-2%22%20transform%3D%22translate(124.132%20368.651)%20scale(0.823)%22%3Eao%3C%2Ftext%3E%0A%20%20%20%20%3Ctext%20id%3D%22tu%22%20class%3D%22cls-2%22%20transform%3D%22translate(482.948%20368.651)%20scale(0.823)%22%3Etu%3C%2Ftext%3E%0A%20%20%3C%2Fg%3E%0A%20%20%3Ctext%20id%3D%22M_A_O_T_U%22%20data-name%3D%22M%20A%20O%20T%20U%22%20class%3D%22cls-3%22%20x%3D%2245.25%22%20y%3D%22537.03%22%3EM%20A%20O%20T%20U%3C%2Ftext%3E%0A%3C%2Fsvg%3E%0A"
|
||||
/> -->
|
||||
<el-text class="title_text">web组态编辑器</el-text>
|
||||
</div>
|
||||
<el-button text circle size="small" @click="emits('update:leftAside', !headerPanelProps.leftAside)">
|
||||
<el-icon :size="20">
|
||||
<svg-analysis v-if="headerPanelProps.leftAside" name="menu-fold"></svg-analysis>
|
||||
<svg-analysis v-else name="menu-unfold"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="flex justify-between" style="width: calc(100% - 440px)">
|
||||
<div class="flex items-center">
|
||||
<el-button-group>
|
||||
<el-button
|
||||
text
|
||||
circle
|
||||
size="small"
|
||||
:disabled="!headerPanelProps.undoEnabled"
|
||||
@click="emits('onUndoClick')"
|
||||
>
|
||||
<el-icon title="撤销 Ctrl+Z" :size="20">
|
||||
<svg-analysis name="undo"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
text
|
||||
circle
|
||||
size="small"
|
||||
:disabled="!headerPanelProps.redoEnabled"
|
||||
@click="emits('onRedoClick')"
|
||||
>
|
||||
<el-icon title="重做 Ctrl+Y" :size="20">
|
||||
<svg-analysis name="redo"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
<el-divider direction="vertical"></el-divider>
|
||||
<el-button text circle size="small" :disabled="!headerPanelProps.deleteEnabled" @click="onDeleteClick">
|
||||
<el-icon title="删除 delete" :class="``" :size="20">
|
||||
<svg-analysis name="delete"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-divider direction="vertical"></el-divider>
|
||||
<el-button text circle size="small" @click="onTreeClick">
|
||||
<el-icon title="组件树" :size="20">
|
||||
<svg-analysis name="tree-list"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-divider direction="vertical"></el-divider>
|
||||
<el-button-group>
|
||||
<el-button text circle size="small" @click="onImportClick">
|
||||
<el-icon title="导入数据模型" :size="20">
|
||||
<svg-analysis name="import-json"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button text circle size="small" @click="onExportClick">
|
||||
<el-icon title="导出数据模型" :size="20">
|
||||
<svg-analysis name="export-json"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
<el-divider direction="vertical"></el-divider>
|
||||
<el-popover placement="bottom" :width="240" trigger="hover" :disabled="!headerPanelProps.alignEnabled">
|
||||
<template #reference>
|
||||
<el-button text circle size="small" :disabled="!headerPanelProps.alignEnabled">
|
||||
<el-icon title="对齐" :size="20">
|
||||
<svg-analysis name="align"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
<div class="flex justify-center">
|
||||
<el-button-group>
|
||||
<el-button text circle size="small" @click="alignSelected('left')">
|
||||
<el-icon title="左对齐" :size="20">
|
||||
<svg-analysis name="align-left"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button text circle size="small" @click="alignSelected('horizontally')">
|
||||
<el-icon title="水平居中" :size="20">
|
||||
<svg-analysis name="align-horizontally"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button text circle size="small" @click="alignSelected('right')">
|
||||
<el-icon title="右对齐" :size="20">
|
||||
<svg-analysis name="align-right"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button text circle size="small" @click="alignSelected('top')">
|
||||
<el-icon title="上对齐" :size="20">
|
||||
<svg-analysis name="align-top"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button text circle size="small" @click="alignSelected('vertically')">
|
||||
<el-icon title="垂直居中" :size="20">
|
||||
<svg-analysis name="align-vertical"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button text circle size="small" @click="alignSelected('bottom')">
|
||||
<el-icon title="下对齐" :size="20">
|
||||
<svg-analysis name="align-bottom"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button text circle size="small" @click="alignSelected('horizontal-distribution')">
|
||||
<el-icon title="水平分布" :size="20">
|
||||
<svg-analysis name="horizontal-distribution"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button text circle size="small" @click="alignSelected('vertical-distribution')">
|
||||
<el-icon title="垂直分布" :size="20">
|
||||
<svg-analysis name="vertical-distribution"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
</el-popover>
|
||||
<el-divider direction="vertical"></el-divider>
|
||||
<el-button-group>
|
||||
<el-button
|
||||
text
|
||||
circle
|
||||
size="small"
|
||||
:disabled="!headerPanelProps.groupEnabled"
|
||||
@click="onGroupClick"
|
||||
>
|
||||
<el-icon title="组合" :size="20">
|
||||
<svg-analysis name="group"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
text
|
||||
circle
|
||||
size="small"
|
||||
:disabled="!headerPanelProps.unGroupEnabled"
|
||||
@click="onUngroupClick"
|
||||
>
|
||||
<el-icon title="取消组合" :size="20">
|
||||
<svg-analysis name="ungroup"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
<el-divider direction="vertical" v-if="!is_npm_env"></el-divider>
|
||||
<el-button text circle size="small" @click="onDrawLineClick" v-if="!is_npm_env">
|
||||
<el-icon title="连线编辑模式" :size="20" :class="drawline_selected ? 'icon-selected' : ''">
|
||||
<svg-analysis name="pen-line"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="flex justify-center items-center">
|
||||
<el-tag v-if="headerPanelProps.realTimeData.show" size="small">
|
||||
{{ headerPanelProps.realTimeData.text }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="flex items-center mr-20px">
|
||||
<el-button text circle size="small" @click="emits('onReturnClick')">
|
||||
<el-icon title="返回" :size="20">
|
||||
<svg-analysis name="return"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<!-- <el-divider direction="vertical"></el-divider> -->
|
||||
<!-- <el-button text circle size="small" @click="emits('onSaveClick')">
|
||||
<el-icon title="保存" :size="20">
|
||||
<svg-analysis name="save"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button> -->
|
||||
<!-- <el-divider v-if="headerPanelProps.useThumbnail" direction="vertical"></el-divider>
|
||||
<el-button
|
||||
v-if="headerPanelProps.useThumbnail"
|
||||
text
|
||||
circle
|
||||
size="small"
|
||||
@click="emits('onThumbnailClick')"
|
||||
>
|
||||
<el-icon title="生成缩略图" :size="20">
|
||||
<svg-analysis name="thumbnail"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button> -->
|
||||
<el-divider direction="vertical"></el-divider>
|
||||
<el-button text circle size="small" @click="emits('onPreviewClick')">
|
||||
<el-icon title="预览" :size="20">
|
||||
<svg-analysis name="preview"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="emits('onSaveAll')">保存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between w-200px">
|
||||
<el-button text circle size="small" @click="emits('update:rightAside', !headerPanelProps.rightAside)">
|
||||
<el-icon :size="20" style="cursor: pointer">
|
||||
<svg-analysis v-if="headerPanelProps.rightAside" name="menu-unfold"></svg-analysis>
|
||||
<svg-analysis v-else name="menu-fold"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<div class="flex items-center">
|
||||
<el-button text circle size="small" @click="onHelpClick">
|
||||
<el-icon title="帮助" :size="20">
|
||||
<svg-analysis name="help"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button text circle size="small" @click="toggle">
|
||||
<el-icon title="全屏" :size="20">
|
||||
<svg-analysis :name="isFullscreen ? 'exit-full-screen' : 'full-screen'"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-divider direction="vertical"></el-divider>
|
||||
<el-button text circle size="small" @click="changeLockState">
|
||||
<el-icon :title="headerPanelProps.lockState ? '已锁定' : '已解锁'" :size="20">
|
||||
<svg-analysis :name="headerPanelProps.lockState ? 'lock' : 'unlock'"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-divider direction="vertical"></el-divider>
|
||||
<el-button text circle size="small" class="mr-10px">
|
||||
<el-icon :title="isDark ? '切换到日间模式' : '切换到夜间模式'" :size="20" @click="toggleDark()">
|
||||
<svg-analysis :name="isDark ? 'light' : 'dark'"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useDark, useToggle, useFullscreen } from '@vueuse/core'
|
||||
import { ElIcon, ElDivider, ElPopover, ElButton, ElButtonGroup, ElImage, ElText, ElTag } from 'element-plus'
|
||||
import SvgAnalysis from '@/components/mt-edit/components/svg-analysis/index.vue'
|
||||
import type { IRealTimeData } from '@/components/mt-edit/store/types'
|
||||
import { ref } from 'vue'
|
||||
type HeaderPanelProps = {
|
||||
leftAside: boolean
|
||||
rightAside: boolean
|
||||
selectedItemsId: string[] //已选中组件的id
|
||||
groupEnabled: boolean
|
||||
unGroupEnabled: boolean
|
||||
alignEnabled: boolean
|
||||
deleteEnabled: boolean
|
||||
lockState: boolean
|
||||
undoEnabled: boolean
|
||||
redoEnabled: boolean
|
||||
realTimeData: IRealTimeData
|
||||
useThumbnail?: boolean
|
||||
}
|
||||
const headerPanelProps = withDefaults(defineProps<HeaderPanelProps>(), {
|
||||
leftAside: true,
|
||||
rightAside: true,
|
||||
useThumbnail: false,
|
||||
selectedItemsId: () => []
|
||||
})
|
||||
const emits = defineEmits([
|
||||
'update:leftAside',
|
||||
'update:rightAside',
|
||||
'onGroupClick',
|
||||
'onUngroupClick',
|
||||
'onDeleteClick',
|
||||
'onExportClick',
|
||||
'onTreeClick',
|
||||
'alignSelected',
|
||||
'update:lockState',
|
||||
'onHelpClick',
|
||||
'onRedoClick',
|
||||
'onUndoClick',
|
||||
'onImportClick',
|
||||
'onPreviewClick',
|
||||
'onReturnClick',
|
||||
'onSaveClick',
|
||||
'onDrawLineClick',
|
||||
'onThumbnailClick',
|
||||
'onSaveAll'
|
||||
])
|
||||
const isDark = useDark({
|
||||
selector: '#mt-edit'
|
||||
})
|
||||
const { isFullscreen, toggle } = useFullscreen()
|
||||
const toggleDark = useToggle(isDark)
|
||||
const drawline_selected = ref(false)
|
||||
const is_npm_env = ref(import.meta.env.MODE === 'npm')
|
||||
const onGroupClick = () => {
|
||||
emits('onGroupClick')
|
||||
}
|
||||
const onUngroupClick = () => {
|
||||
emits('onUngroupClick')
|
||||
}
|
||||
const onDeleteClick = () => {
|
||||
emits('onDeleteClick')
|
||||
}
|
||||
const onExportClick = () => {
|
||||
emits('onExportClick')
|
||||
}
|
||||
const onTreeClick = () => {
|
||||
emits('onTreeClick')
|
||||
}
|
||||
const alignSelected = (
|
||||
type:
|
||||
| 'left'
|
||||
| 'horizontally'
|
||||
| 'right'
|
||||
| 'top'
|
||||
| 'vertically'
|
||||
| 'bottom'
|
||||
| 'horizontal-distribution'
|
||||
| 'vertical-distribution'
|
||||
) => {
|
||||
emits('alignSelected', type)
|
||||
}
|
||||
const changeLockState = () => {
|
||||
emits('update:lockState', !headerPanelProps.lockState)
|
||||
}
|
||||
const onHelpClick = () => {
|
||||
emits('onHelpClick')
|
||||
}
|
||||
const onImportClick = () => {
|
||||
emits('onImportClick')
|
||||
}
|
||||
const onDrawLineClick = () => {
|
||||
drawline_selected.value = !drawline_selected.value
|
||||
emits('onDrawLineClick', drawline_selected.value)
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.icon-selected {
|
||||
background-color: #ecf5ff;
|
||||
color: #409eff;
|
||||
}
|
||||
.title_text {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
padding-left: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,272 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-button type="primary" style="float: right; margin-right: 5px" size="small" @click="onAddClick">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增图纸
|
||||
</el-button>
|
||||
<el-table
|
||||
:data="dataTrees"
|
||||
style="width: 100%; padding-top: 10px"
|
||||
:show-header="false"
|
||||
highlight-current-row
|
||||
ref="multipleTable"
|
||||
@row-click="onRowClick"
|
||||
empty-text="暂无数据"
|
||||
row-key="id"
|
||||
>
|
||||
<el-table-column prop="name" label="名称" />
|
||||
<el-table-column label="操作" align="center" width="60" #default="scope">
|
||||
<el-tooltip content="修改" placement="top">
|
||||
<el-icon @click.stop="update(scope.$index, scope.row)" style="cursor: pointer">
|
||||
<Edit />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-icon @click.stop="del(scope.$index)" style="margin-left: 5px; cursor: pointer">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="40">
|
||||
<template #default>
|
||||
<el-tooltip content="拖拽" placement="top">
|
||||
<div class="drag-handle">⋮⋮⋮</div>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 新增/修改 -->
|
||||
<el-dialog draggable v-model="dialogFormVisible" :title="dialog_title" width="500px" destroy-on-close>
|
||||
<el-form :model="form" ref="formRef" :rules="rules">
|
||||
<el-form-item label="图纸名称" :label-width="formLabelWidth" prop="name">
|
||||
<el-input v-model="form.name" autocomplete="off" placeholder="请输入图纸名称" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogFormVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveDialog(form)">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, computed, watch, onMounted, nextTick } from 'vue'
|
||||
import {
|
||||
ElButton,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElIcon,
|
||||
ElMessage,
|
||||
ElMessageBox
|
||||
} from 'element-plus'
|
||||
import { useDataStore } from '@/stores/menuList'
|
||||
import { globalStore } from '@/components/mt-edit/store/global'
|
||||
import type { IExportJson } from '@/components/mt-edit/components/types'
|
||||
import { useExportJsonToDoneJson } from '@/components/mt-edit/composables/index'
|
||||
import { cacheStore } from '@/components/mt-edit/store/cache'
|
||||
import { queryPage } from '@/api/index'
|
||||
import Sortable from 'sortablejs'
|
||||
|
||||
const useData = useDataStore()
|
||||
|
||||
// 弹框
|
||||
const dialogFormVisible = ref(false)
|
||||
const dialog_title = ref('新增图纸')
|
||||
const formLabelWidth = '100px'
|
||||
const globalIndex = ref(-1)
|
||||
const multipleTable: any = ref(null)
|
||||
const { pid } = useData // 解构出 myArray 状态
|
||||
|
||||
interface PageQuery {
|
||||
/** 页码 */
|
||||
pageNum: number
|
||||
|
||||
/** 条数 */
|
||||
pageSize: number
|
||||
|
||||
/** 父id */
|
||||
pid: string | number | undefined
|
||||
}
|
||||
|
||||
let form = reactive({
|
||||
name: ''
|
||||
})
|
||||
const formRef = ref()
|
||||
|
||||
const dataTrees = computed(() => useData.dataTree)
|
||||
|
||||
interface PageData {
|
||||
records: any[] // 根据实际字段类型定义
|
||||
}
|
||||
|
||||
// 监听数据变化,数据加载完成后初始化拖拽
|
||||
watch(
|
||||
() => dataTrees.value,
|
||||
newVal => {
|
||||
if (newVal.length > 0) {
|
||||
initSortable()
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
// 在 script setup 中定义 sortableInstance
|
||||
const sortableInstance = ref<any>(null)
|
||||
|
||||
// 修改 initSortable 方法中的实例挂载与销毁逻辑
|
||||
const initSortable = () => {
|
||||
nextTick(() => {
|
||||
const tbody = multipleTable.value.$el.querySelector('.el-table__body-wrapper tbody')
|
||||
if (!tbody) {
|
||||
console.error('未找到 tbody 元素')
|
||||
return
|
||||
}
|
||||
|
||||
// 销毁旧实例
|
||||
if (sortableInstance.value) {
|
||||
sortableInstance.value.destroy()
|
||||
}
|
||||
|
||||
// 创建新实例并保存到 sortableInstance
|
||||
sortableInstance.value = new Sortable(tbody, {
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
onEnd: ({ newIndex, oldIndex }) => {
|
||||
// 确保 newIndex 和 oldIndex 都存在且不相等
|
||||
if (newIndex === undefined || oldIndex === undefined || newIndex === oldIndex) return
|
||||
|
||||
const targetItem = dataTrees.value.splice(oldIndex, 1)[0]
|
||||
dataTrees.value.splice(newIndex, 0, targetItem)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
//form表单校验规则
|
||||
const rules = {
|
||||
name: [{ required: true, message: '图纸名称不能为空', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 使用watch监听myValue的变化
|
||||
watch(
|
||||
() => useData.loading,
|
||||
(newValue, oldValue) => {
|
||||
if (newValue !== oldValue) {
|
||||
// 第一条默认选中
|
||||
if (dataTrees.value.length > 0) {
|
||||
if (dataTrees.value) {
|
||||
multipleTable.value.setCurrentRow(dataTrees.value[0]) // 设置第一行为当前行
|
||||
onRowClick(dataTrees.value[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const onAddClick = () => {
|
||||
Object.assign(form, { name: '' })
|
||||
//打开弹窗
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
|
||||
// 删除功能,传索引行数
|
||||
function del(index: number) {
|
||||
ElMessageBox.confirm('确定删除?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
// splice方法,传两个参数:第几行开始,删除多少条(如果未规定此参数,则删除从 index 开始到原数组结尾的所有元素)
|
||||
dataTrees.value.splice(index, 1)
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: '删除取消'
|
||||
})
|
||||
})
|
||||
}
|
||||
// 点击行
|
||||
const onRowClick = async (row: any) => {
|
||||
useData.placeKid(row.kId)
|
||||
const json: IExportJson = JSON.parse(row.path)
|
||||
const { canvasCfg, gridCfg, importDoneJson } = useExportJsonToDoneJson(json)
|
||||
canvasCfg.drag_offset.x = 0
|
||||
canvasCfg.drag_offset.y = 0
|
||||
canvasCfg.scale = 1
|
||||
globalStore.canvasCfg = canvasCfg
|
||||
globalStore.gridCfg = gridCfg
|
||||
globalStore.setGlobalStoreDoneJson(importDoneJson)
|
||||
cacheStore.addHistory(globalStore.done_json)
|
||||
}
|
||||
|
||||
const saveDialog = async (form: any) => {
|
||||
// 校验表单
|
||||
if (!formRef) return
|
||||
const valid = await formRef.value.validate()
|
||||
if (!valid) return
|
||||
// 提交请求
|
||||
if (globalIndex.value >= 0) {
|
||||
//表示编辑
|
||||
// dataTrees[globalIndex.value] = form;
|
||||
useData.modify(form.kId, form.name)
|
||||
//还原回去
|
||||
globalIndex.value = -1
|
||||
} else {
|
||||
//新增
|
||||
useData.append(form.name)
|
||||
// dataTrees.push(form);
|
||||
}
|
||||
dialogFormVisible.value = false
|
||||
}
|
||||
|
||||
// 修改功能
|
||||
function update(index: number, row: any) {
|
||||
// const newObj = Object.assign({}, row);
|
||||
// form = reactive(newObj);
|
||||
Object.assign(form, row)
|
||||
//把当前编辑的行号赋值给全局保存的行号
|
||||
globalIndex.value = index
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
::v-deep(.el-table--border th.el-table__cell),
|
||||
::v-deep(.el-table td.el-table__cell) {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
::v-deep(.el-table--border .el-table__cell) {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
::v-deep(.el-table--group, .el-table--border) {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.el-table::before {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
::v-deep(.el-table--fit .el-table__inner-wrapper::before) {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
</style>
|
||||
387
src/components/mt-edit/components/layout/left-aside/index.vue
Normal file
387
src/components/mt-edit/components/layout/left-aside/index.vue
Normal file
@@ -0,0 +1,387 @@
|
||||
<template>
|
||||
<div id="mt-left-aside" class="pt-10px h-1/1 box-border p-x-10px">
|
||||
<el-input v-model="search_str" class="pb-10px pr-10px" placeholder="请输入关键字进行搜索"></el-input>
|
||||
<div style="height: calc(100vh - 243px)">
|
||||
<el-scrollbar class="pr-10px" :always="true" :view-style="{ height: '100%', 'overflow-x': 'hidden' }">
|
||||
<el-collapse v-model="active_names">
|
||||
<el-collapse-item
|
||||
v-for="config_item_key in checked_keys"
|
||||
:key="config_item_key"
|
||||
:title="config_item_key"
|
||||
:name="config_item_key"
|
||||
>
|
||||
<div class="flex flex-wrap">
|
||||
<div
|
||||
draggable="true"
|
||||
@dragstart="onDragStart(config_item_key, item.id)"
|
||||
@touchstart.passive="onDragStart(config_item_key, item.id)"
|
||||
class="w-120px h-40px"
|
||||
v-for="(item, index) in getFilteritems(
|
||||
leftAsideProps.leftAsideConfig.get(config_item_key)
|
||||
)"
|
||||
:key="item.id"
|
||||
>
|
||||
<el-tooltip
|
||||
v-model:visible="is_show_tooltip[`${config_item_key}${item.id}`]"
|
||||
placement="right"
|
||||
:width="200"
|
||||
:effect="isDark ? 'dark' : 'light'"
|
||||
:show-arrow="false"
|
||||
:hide-after="0"
|
||||
trigger="hover"
|
||||
:enterable="false"
|
||||
:offset="getOffset(index + 1)"
|
||||
>
|
||||
<div class="flex">
|
||||
<el-image
|
||||
draggable="false"
|
||||
class="w-30px h-30px select-none"
|
||||
:class="isDark ? 'bg-amber-50' : ''"
|
||||
:src="item.thumbnail"
|
||||
/>
|
||||
<span class="textBox">{{ item.title }}</span>
|
||||
</div>
|
||||
|
||||
<template #content>
|
||||
<div class="flex justify-center items-center">
|
||||
<div class="flex flex-col">
|
||||
<el-text>{{ item.title }}</el-text>
|
||||
<el-image
|
||||
class="w-100px h-100px pt-5px"
|
||||
:class="isDark ? 'bg-amber-50' : ''"
|
||||
:src="item.thumbnail"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<div class="h-[calc(10%-1px)] flex justify-center items-center ct-border" style="padding-top: 10px">
|
||||
<el-button class="w-80/100" @click="onManageClick">管理</el-button>
|
||||
</div>
|
||||
<el-dialog v-model="manage_dialog_visiable" title="图库管理" width="50%" destroy-on-close>
|
||||
<div class="flex">
|
||||
<div>
|
||||
<div>
|
||||
<!-- <div class="flex justify-center">
|
||||
<el-checkbox v-model="check_all" :indeterminate="is_indeterminate">全选</el-checkbox>
|
||||
</div> -->
|
||||
<el-scrollbar height="50vh">
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="classify_list"
|
||||
:highlight-current="true"
|
||||
@check-change="handleCheckChange"
|
||||
node-key="label"
|
||||
:default-checked-keys="checked_keys"
|
||||
@node-click="onNodeClick"
|
||||
></el-tree>
|
||||
</el-scrollbar>
|
||||
<!-- <el-upload
|
||||
ref="uploadRef"
|
||||
class="w-24px h-24px"
|
||||
v-model:file-list="up_img_list"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
:show-file-list="false"
|
||||
:on-change="onUpLoadChange"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-button type="primary">本地上传</el-button>
|
||||
</el-upload> -->
|
||||
<el-button type="primary" @click="checkClick()">新增图元</el-button>
|
||||
<add-element :show="openCheck" @update:show="updateOpenCheck"></add-element>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider direction="vertical" class="h-50vh ml-40px"></el-divider>
|
||||
<div v-if="selected_node_key">
|
||||
<el-scrollbar height="50vh" :always="true">
|
||||
<div class="flex flex-wrap">
|
||||
<div
|
||||
v-for="item in leftAsideProps.leftAsideConfig.get(selected_node_key)"
|
||||
:key="item.id"
|
||||
class="w-160px h-160px flex flex-wrap justify-center items-center cursor-pointer relative"
|
||||
@mouseenter="show_del_local_file = item.id"
|
||||
@mouseleave="show_del_local_file = null"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<el-image
|
||||
class="w-60px h-60px"
|
||||
:class="isDark ? 'bg-amber-50' : ''"
|
||||
:src="item.thumbnail"
|
||||
/>
|
||||
<div class="w-160px h-60px flex justify-center items-center">
|
||||
<el-text truncated>{{ item.title }}</el-text>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
selected_node_key !== '数据绑定图元' &&
|
||||
selected_node_key !== '基础图元' &&
|
||||
show_del_local_file == item.id
|
||||
"
|
||||
class="absolute w-160px h-160px left-0 top-0 opacity-80 bg-light-300 flex justify-center items-center"
|
||||
>
|
||||
<el-button type="danger" @click="onDelLocalFile(item)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import {
|
||||
ElInput,
|
||||
ElCollapse,
|
||||
ElCollapseItem,
|
||||
ElButton,
|
||||
ElScrollbar,
|
||||
ElImage,
|
||||
ElTooltip,
|
||||
ElText,
|
||||
ElDialog,
|
||||
ElCheckbox,
|
||||
ElDivider,
|
||||
ElTree,
|
||||
ElUpload,
|
||||
ElMessageBox,
|
||||
type UploadUserFile,
|
||||
ElMessage,
|
||||
type UploadFile
|
||||
} from 'element-plus'
|
||||
import SvgAnalysis from '@/components/mt-edit/components/svg-analysis/index.vue'
|
||||
import { useDark, useLocalStorage } from '@vueuse/core'
|
||||
import type {
|
||||
ILeftAsideConfig,
|
||||
ILeftAsideConfigItem,
|
||||
ILeftAsideConfigItemPublic
|
||||
} from '@/components/mt-edit/store/types'
|
||||
import { globalStore } from '@/components/mt-edit/store/global'
|
||||
import { blobToBase64 } from '@/components/mt-edit/utils'
|
||||
import AddElement from '@/components/mt-edit/components/add-element/index.vue'
|
||||
import { deleteElement } from '@/api/index'
|
||||
import { leftAsideStore } from '@/export'
|
||||
import { useDataStore } from '@/stores/menuList'
|
||||
|
||||
type LeftAsideProps = {
|
||||
leftAsideConfig: ILeftAsideConfig
|
||||
}
|
||||
const leftAsideProps = withDefaults(defineProps<LeftAsideProps>(), {
|
||||
leftAsideConfig: () => new Map<string, ILeftAsideConfigItem[]>()
|
||||
})
|
||||
const isDark = useDark({
|
||||
selector: '#mt-edit'
|
||||
})
|
||||
const uploadRef = ref()
|
||||
// 从本地储存中查被禁用的类别
|
||||
const disable_classify = useLocalStorage<string[]>('mt-disable-classify', [])
|
||||
// 上传的文件也存到本地储存中
|
||||
const local_file = useLocalStorage<ILeftAsideConfigItem[]>('mt-local-file', [])
|
||||
// leftAsideProps.leftAsideConfig.set('本地文件', local_file.value)
|
||||
const treeRef = ref()
|
||||
const is_show_tooltip: Record<string, boolean> = reactive({})
|
||||
const active_names = ref([])
|
||||
const search_str = ref()
|
||||
const manage_dialog_visiable = ref(false)
|
||||
const up_img_list = ref<UploadUserFile[]>([])
|
||||
const show_del_local_file = ref<null | string>(null)
|
||||
const classify_list = computed(() =>
|
||||
[...leftAsideProps.leftAsideConfig.keys()].map(m => {
|
||||
return { label: m }
|
||||
})
|
||||
)
|
||||
const useData = useDataStore()
|
||||
|
||||
// const classify_list = computed(() =>
|
||||
// [...leftAsideProps.leftAsideConfig.keys()]
|
||||
// // 当 useData.graphicDisplay 为 true 时不包含「数据绑定图元」
|
||||
// .filter(key => !(useData.graphicDisplay && key === '数据绑定图元'))
|
||||
// .map(m => {
|
||||
// return { label: m }
|
||||
// })
|
||||
// )
|
||||
|
||||
watch(
|
||||
() => leftAsideProps,
|
||||
newVal => {
|
||||
// checked_keys.value = [...leftAsideProps.leftAsideConfig.keys()]
|
||||
// .filter(key => !(useData.graphicDisplay && key === '数据绑定图元'))
|
||||
// .map(m => {
|
||||
// return { label: m }
|
||||
// })
|
||||
// .map(m => m.label)
|
||||
checked_keys.value = [...leftAsideProps.leftAsideConfig.keys()]
|
||||
.map(m => {
|
||||
return { label: m }
|
||||
})
|
||||
.map(m => m.label)
|
||||
},
|
||||
{ deep: true } // 添加深度监听配置
|
||||
)
|
||||
|
||||
const checked_keys = ref<string[]>(
|
||||
classify_list.value.filter(f => !disable_classify.value.includes(f.label)).map(m => m.label)
|
||||
)
|
||||
|
||||
const selected_node_key = ref()
|
||||
const check_all = computed({
|
||||
get: () => {
|
||||
return classify_list.value.length == checked_keys.value.length
|
||||
},
|
||||
set: val => {
|
||||
if (val) {
|
||||
checked_keys.value = classify_list.value.map(m => m.label)
|
||||
} else {
|
||||
checked_keys.value = []
|
||||
treeRef.value?.setCheckedNodes([])
|
||||
}
|
||||
}
|
||||
})
|
||||
const is_indeterminate = computed(() => {
|
||||
return checked_keys.value.length > 0 && checked_keys.value.length < classify_list.value.length
|
||||
})
|
||||
const getOffset = (index: number) => {
|
||||
return index % 4 == 0 ? 40 : index % 4 == 3 ? 80 : index % 4 == 2 ? 120 : 160
|
||||
}
|
||||
const getFilteritems = (arr: ILeftAsideConfigItemPublic[] | undefined): ILeftAsideConfigItemPublic[] => {
|
||||
if (!arr) {
|
||||
return []
|
||||
}
|
||||
if (search_str.value) {
|
||||
return arr.filter(f => f.title.includes(search_str.value))
|
||||
}
|
||||
return arr
|
||||
}
|
||||
const onDragStart = (config_item_key: string, item_id: string) => {
|
||||
if (!config_item_key || !item_id) {
|
||||
console.error('拖拽初始化失败', config_item_key, item_id)
|
||||
return
|
||||
}
|
||||
is_show_tooltip[`${config_item_key}${item_id}`] = false
|
||||
globalStore.setIntention('create')
|
||||
globalStore.setCreateItemInfo({
|
||||
config_key: config_item_key,
|
||||
item_id
|
||||
})
|
||||
}
|
||||
const onManageClick = () => {
|
||||
manage_dialog_visiable.value = true
|
||||
}
|
||||
const handleCheckChange = (data: { label: string }, checked: boolean, indeterminate: boolean) => {
|
||||
if (checked && !checked_keys.value.includes(data.label)) {
|
||||
checked_keys.value.push(data.label)
|
||||
} else if (!checked) {
|
||||
checked_keys.value = checked_keys.value.filter(f => f !== data.label)
|
||||
}
|
||||
disable_classify.value = classify_list.value.filter(f => !checked_keys.value.includes(f.label)).map(m => m.label)
|
||||
}
|
||||
const onNodeClick = ({ label }: { label: string }) => {
|
||||
selected_node_key.value = label
|
||||
}
|
||||
const onUpLoadChange = (e: UploadFile) => {
|
||||
if (!e.raw!.type.includes('image/')) {
|
||||
ElMessage.error('只能上传图片!')
|
||||
uploadRef.value.clearFiles()
|
||||
up_img_list.value = []
|
||||
return false
|
||||
} else if (e.raw!.size / 1024 / 1024 > 1) {
|
||||
ElMessage.error('不能上传超过1MB的图像!')
|
||||
uploadRef.value.clearFiles()
|
||||
up_img_list.value = []
|
||||
return false
|
||||
}
|
||||
blobToBase64(e.raw!).then(base64 => {
|
||||
const id = e.name.split('.')[0]
|
||||
const config: ILeftAsideConfigItem = {
|
||||
id: id,
|
||||
title: id,
|
||||
type: 'img',
|
||||
thumbnail: base64 as string,
|
||||
props: {},
|
||||
common_animations: {
|
||||
val: '',
|
||||
delay: 'delay-0s',
|
||||
speed: 'slow',
|
||||
repeat: 'infinite'
|
||||
}
|
||||
}
|
||||
const find_index = local_file.value.findIndex(f => f.id == id)
|
||||
if (find_index != -1) {
|
||||
ElMessage.info('存在同名文件,已覆盖!')
|
||||
local_file.value.splice(find_index, 1)
|
||||
}
|
||||
local_file.value.push(config)
|
||||
leftAsideProps.leftAsideConfig.set('本地文件', local_file.value)
|
||||
uploadRef.value.clearFiles()
|
||||
up_img_list.value = []
|
||||
selected_node_key.value = '本地文件'
|
||||
treeRef.value?.setCurrentKey('本地文件')
|
||||
})
|
||||
}
|
||||
|
||||
// 删除
|
||||
function onDelLocalFile(item: ILeftAsideConfigItem) {
|
||||
ElMessageBox.confirm('确定删除?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
deleteElement({ id: item.id }).then((res: any) => {
|
||||
if (res.code == 'A0000') {
|
||||
ElMessage.success('删除成功')
|
||||
// 删除svg
|
||||
leftAsideStore.svgDelete(selected_node_key.value, item.id)
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: '删除取消'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// const onDelLocalFile = (item: ILeftAsideConfigItem) => {
|
||||
// const find_index = local_file.value.findIndex(f => f.id == id)
|
||||
// if (find_index != -1) {
|
||||
// local_file.value.splice(find_index, 1)
|
||||
// }
|
||||
// leftAsideProps.leftAsideConfig.set('本地文件', local_file.value)
|
||||
// }
|
||||
|
||||
// 新增图元
|
||||
const openCheck = ref(false)
|
||||
//打开弹窗按钮
|
||||
function checkClick() {
|
||||
openCheck.value = true
|
||||
}
|
||||
//接收到子页面关闭弹窗的事件,改变openCheck的值,否则在刷新页面之前openCheck会一直是true,导致无法第二次打开弹窗
|
||||
const updateOpenCheck = (value: boolean) => {
|
||||
openCheck.value = value
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
#mt-left-aside .el-collapse-item__header,
|
||||
#mt-left-aside .el-collapse-item__wrap {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.textBox {
|
||||
line-height: 30px;
|
||||
margin-left: 5px;
|
||||
overflow: hidden; /* 隐藏超出宽度的内容 */
|
||||
white-space: nowrap; /* 禁止文本换行 */
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
1514
src/components/mt-edit/components/layout/main-panel/index.vue
Normal file
1514
src/components/mt-edit/components/layout/main-panel/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div id="mt-right-aside" class="px-4">
|
||||
<select-item-setting
|
||||
v-if="globalStore.selected_items_id.length == 1"
|
||||
v-model:item-json="globalStore.done_json[find_index_item_json]"
|
||||
:done-json="globalStore.done_json"
|
||||
@add-history="onAddHistory"
|
||||
>
|
||||
<template v-if="hasDeviceBindSlot" #deviceBind="{ item }">
|
||||
<slot name="deviceBind" :item="item" />
|
||||
</template>
|
||||
</select-item-setting>
|
||||
<page-setting
|
||||
v-else
|
||||
v-model:canvasCfg="globalStore.canvasCfg"
|
||||
v-model:grid-cfg="globalStore.gridCfg"
|
||||
></page-setting>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, useSlots } from 'vue'
|
||||
import PageSetting from './page-setting.vue'
|
||||
import SelectItemSetting from './select-item-setting.vue'
|
||||
import { globalStore } from '@/components/mt-edit/store/global'
|
||||
import { cacheStore } from '@/components/mt-edit/store/cache'
|
||||
const slots = useSlots()
|
||||
const find_index_item_json = computed(() => {
|
||||
return globalStore.done_json.findIndex(f => f.id == globalStore.selected_items_id[0])
|
||||
})
|
||||
const onAddHistory = () => {
|
||||
cacheStore.addHistory(globalStore.done_json)
|
||||
}
|
||||
const hasDeviceBindSlot = computed(() => {
|
||||
return !!slots.deviceBind
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
#mt-right-aside .el-collapse-item__header,
|
||||
#mt-right-aside .el-collapse-item__wrap {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-button type="primary" plain round @click="dialogVisible = true">点击编辑</el-button>
|
||||
<el-dialog v-model="dialogVisible" title="配置编辑" width="60%">
|
||||
<v-ace-editor
|
||||
v-model:value="content"
|
||||
lang="json"
|
||||
theme="monokai"
|
||||
style="height: 400px"
|
||||
:options="{
|
||||
useWorker: true,
|
||||
enableBasicAutocompletion: true,
|
||||
enableSnippets: true,
|
||||
enableLiveAutocompletion: true
|
||||
}"
|
||||
/>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button type="primary" @click="onYesBtnClick">确定</el-button>
|
||||
<el-button type="primary" @click="dialogVisible = false">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { VAceEditor } from 'vue3-ace-editor'
|
||||
import { ElButton, ElDialog } from 'element-plus'
|
||||
import { ref } from 'vue'
|
||||
const props = defineProps({
|
||||
contentObj: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
})
|
||||
const dialogVisible = ref(false)
|
||||
const emits = defineEmits(['update:contentObj'])
|
||||
const content = ref(JSON.stringify(props.contentObj, null, 2))
|
||||
const onYesBtnClick = () => {
|
||||
emits('update:contentObj', JSON.parse(content.value))
|
||||
dialogVisible.value = false
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,356 @@
|
||||
<template>
|
||||
<el-tabs v-model="activeName" class="select-none">
|
||||
<el-tab-pane label="页面" name="page">
|
||||
<el-form label-width="70px" label-position="left">
|
||||
<el-form-item label="画布尺寸" size="small">
|
||||
<el-select v-model="canvas_size" placeholder="请设置画布尺寸">
|
||||
<el-option-group label="自定义">
|
||||
<div class="flex justify-between">
|
||||
<el-input-number
|
||||
v-model="canvas_size_width"
|
||||
size="small"
|
||||
:controls="false"
|
||||
class="w-5/10 pl-5px"
|
||||
></el-input-number>
|
||||
<el-text>*</el-text>
|
||||
<el-input-number
|
||||
v-model="canvas_size_height"
|
||||
size="small"
|
||||
:controls="false"
|
||||
class="w-5/10 pr-5px"
|
||||
></el-input-number>
|
||||
</div>
|
||||
</el-option-group>
|
||||
<el-option-group v-for="group in canvas_size_options" :key="group.label" :label="group.label">
|
||||
<el-option
|
||||
v-for="item in group.options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="缩放倍数" size="small">
|
||||
<el-select v-model="canvas_size_scale" placeholder="请设置缩放比例" size="small">
|
||||
<el-option
|
||||
v-for="item in canvas_size_scale_options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
<div class="flex justify-between px-10px ct-border pt-10px">
|
||||
<el-text>自定义:</el-text>
|
||||
<el-input-number
|
||||
v-model="canvas_size_scale_input"
|
||||
size="small"
|
||||
:step="0.1"
|
||||
:min="0.1"
|
||||
class="mx-5px"
|
||||
></el-input-number>
|
||||
</div>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="背景颜色" size="small">
|
||||
<el-color-picker v-model="canvas_bg_color"></el-color-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="背景图片" size="small">
|
||||
<el-upload
|
||||
ref="canvasBgImgUploadRef"
|
||||
class="w-24px h-24px"
|
||||
v-model:file-list="bg_img_list"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
:show-file-list="false"
|
||||
:on-change="onBgImgChange"
|
||||
accept="image/*"
|
||||
@mouseenter="show_clear_bg_img = true"
|
||||
@mouseleave="show_clear_bg_img = false"
|
||||
>
|
||||
<div class="flex justify-center items-center relative">
|
||||
<img
|
||||
class="w-40px h-40px absolute left-0"
|
||||
v-if="rightAsideProps.canvasCfg.img"
|
||||
:src="rightAsideProps.canvasCfg.img"
|
||||
/>
|
||||
<el-button v-else size="small" class="w-40px h-40px absolute left-0">
|
||||
<el-icon title="上传" :size="20">
|
||||
<svg-analysis name="upload"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<div
|
||||
v-if="rightAsideProps.canvasCfg.img && show_clear_bg_img"
|
||||
class="absolute w-40px h-40px left-0 opacity-80 bg-light-300 flex justify-center items-center"
|
||||
@click.stop="clearBgImg"
|
||||
>
|
||||
<el-icon title="删除" :size="25">
|
||||
<svg-analysis name="delete"></svg-analysis>
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item label="参考线" size="small">
|
||||
<el-switch v-model="canvas_guide"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="吸附" size="small">
|
||||
<el-switch v-model="canvas_adsorp"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="网格" size="small">
|
||||
<el-switch v-model="grid_enabled"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="网格对齐" size="small" v-if="grid_enabled">
|
||||
<el-switch v-model="grid_align"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="网格大小" size="small" v-if="grid_enabled">
|
||||
<el-input-number v-model="grid_size" :min="1"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { IGlobalStoreCanvasCfg, IGlobalStoreGridCfg } from '@/components/mt-edit/store/types'
|
||||
import {
|
||||
ElTabs,
|
||||
ElTabPane,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElOptionGroup,
|
||||
ElSwitch,
|
||||
ElText,
|
||||
ElColorPicker,
|
||||
ElUpload,
|
||||
ElIcon,
|
||||
type UploadUserFile,
|
||||
ElButton,
|
||||
type UploadFile,
|
||||
ElMessage
|
||||
} from 'element-plus'
|
||||
import { computed, ref } from 'vue'
|
||||
import SvgAnalysis from '@/components/mt-edit/components/svg-analysis/index.vue'
|
||||
import { blobToBase64 } from '@/components/mt-edit/utils'
|
||||
type RightAsideProps = {
|
||||
canvasCfg: IGlobalStoreCanvasCfg
|
||||
gridCfg: IGlobalStoreGridCfg
|
||||
}
|
||||
const rightAsideProps = withDefaults(defineProps<RightAsideProps>(), {})
|
||||
const emits = defineEmits(['update:canvasCfg', 'update:gridCfg'])
|
||||
const activeName = ref('page')
|
||||
const canvasBgImgUploadRef = ref()
|
||||
const canvas_size = computed({
|
||||
get: () => {
|
||||
return `${rightAsideProps.canvasCfg.width}*${rightAsideProps.canvasCfg.height}`
|
||||
},
|
||||
set: value => {
|
||||
const [width, height] = value.split('*')
|
||||
emits('update:canvasCfg', {
|
||||
...rightAsideProps.canvasCfg,
|
||||
width: Number(width),
|
||||
height: Number(height)
|
||||
})
|
||||
}
|
||||
})
|
||||
const canvas_size_width = computed({
|
||||
get: () => {
|
||||
return rightAsideProps.canvasCfg.width
|
||||
},
|
||||
set: value => {
|
||||
emits('update:canvasCfg', {
|
||||
...rightAsideProps.canvasCfg,
|
||||
width: value
|
||||
})
|
||||
}
|
||||
})
|
||||
const canvas_size_height = computed({
|
||||
get: () => {
|
||||
return rightAsideProps.canvasCfg.height
|
||||
},
|
||||
set: value => {
|
||||
emits('update:canvasCfg', {
|
||||
...rightAsideProps.canvasCfg,
|
||||
height: value
|
||||
})
|
||||
}
|
||||
})
|
||||
const canvas_size_options = [
|
||||
{
|
||||
label: 'pc端',
|
||||
options: [
|
||||
{
|
||||
value: '1920*1080',
|
||||
label: '1920*1080'
|
||||
},
|
||||
{
|
||||
value: '1600*900',
|
||||
label: '1600*900'
|
||||
},
|
||||
{
|
||||
value: '1366*768',
|
||||
label: '1366*768'
|
||||
},
|
||||
{
|
||||
value: '1280*720',
|
||||
label: '1280*720'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '移动端',
|
||||
options: [
|
||||
{
|
||||
value: '1024*1366',
|
||||
label: '1024*1366'
|
||||
},
|
||||
{
|
||||
value: '768*1024',
|
||||
label: '768*1024'
|
||||
},
|
||||
{
|
||||
value: '480*800',
|
||||
label: '480*800'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
const canvas_size_scale_input = computed({
|
||||
get: () => {
|
||||
return rightAsideProps.canvasCfg.scale
|
||||
},
|
||||
set: value => {
|
||||
emits('update:canvasCfg', {
|
||||
...rightAsideProps.canvasCfg,
|
||||
scale: value
|
||||
})
|
||||
}
|
||||
})
|
||||
const canvas_size_scale = computed({
|
||||
get: () => {
|
||||
return rightAsideProps.canvasCfg.scale
|
||||
},
|
||||
set: value => {
|
||||
emits('update:canvasCfg', {
|
||||
...rightAsideProps.canvasCfg,
|
||||
scale: value
|
||||
})
|
||||
}
|
||||
})
|
||||
const canvas_size_scale_options = [
|
||||
{
|
||||
value: 0.5,
|
||||
label: 0.5
|
||||
},
|
||||
{
|
||||
value: 1,
|
||||
label: 1
|
||||
},
|
||||
{
|
||||
value: 1.5,
|
||||
label: 1.5
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
label: 2
|
||||
}
|
||||
]
|
||||
const canvas_bg_color = computed({
|
||||
get: () => {
|
||||
return rightAsideProps.canvasCfg.color
|
||||
},
|
||||
set: value => {
|
||||
emits('update:canvasCfg', {
|
||||
...rightAsideProps.canvasCfg,
|
||||
color: value
|
||||
})
|
||||
}
|
||||
})
|
||||
const show_clear_bg_img = ref(false)
|
||||
const bg_img_list = ref<UploadUserFile[]>([])
|
||||
const grid_enabled = computed({
|
||||
get: () => {
|
||||
return rightAsideProps.gridCfg.enabled
|
||||
},
|
||||
set: value => {
|
||||
emits('update:gridCfg', {
|
||||
...rightAsideProps.gridCfg,
|
||||
enabled: value
|
||||
})
|
||||
}
|
||||
})
|
||||
const grid_align = computed({
|
||||
get: () => {
|
||||
return rightAsideProps.gridCfg.align
|
||||
},
|
||||
set: value => {
|
||||
emits('update:gridCfg', {
|
||||
...rightAsideProps.gridCfg,
|
||||
align: value
|
||||
})
|
||||
}
|
||||
})
|
||||
const grid_size = computed({
|
||||
get: () => {
|
||||
return rightAsideProps.gridCfg.size
|
||||
},
|
||||
set: value => {
|
||||
emits('update:gridCfg', {
|
||||
...rightAsideProps.gridCfg,
|
||||
size: value
|
||||
})
|
||||
}
|
||||
})
|
||||
const onBgImgChange = (e: UploadFile) => {
|
||||
show_clear_bg_img.value = false
|
||||
if (!e.raw!.type.includes('image/')) {
|
||||
ElMessage.error('只能上传图片!')
|
||||
canvasBgImgUploadRef.value.clearFiles()
|
||||
bg_img_list.value = []
|
||||
return false
|
||||
} else if (e.raw!.size / 1024 / 1024 > 1) {
|
||||
ElMessage.error('不能上传超过1MB的图像!')
|
||||
canvasBgImgUploadRef.value.clearFiles()
|
||||
bg_img_list.value = []
|
||||
return false
|
||||
}
|
||||
blobToBase64(e.raw!).then(base64 => {
|
||||
emits('update:canvasCfg', {
|
||||
...rightAsideProps.canvasCfg,
|
||||
img: base64
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const clearBgImg = () => {
|
||||
canvasBgImgUploadRef.value.clearFiles()
|
||||
emits('update:canvasCfg', {
|
||||
...rightAsideProps.canvasCfg,
|
||||
img: ''
|
||||
})
|
||||
}
|
||||
const canvas_adsorp = computed({
|
||||
get: () => {
|
||||
return rightAsideProps.canvasCfg.adsorp
|
||||
},
|
||||
set: value => {
|
||||
emits('update:canvasCfg', {
|
||||
...rightAsideProps.canvasCfg,
|
||||
adsorp: value
|
||||
})
|
||||
}
|
||||
})
|
||||
const canvas_guide = computed({
|
||||
get: () => {
|
||||
return rightAsideProps.canvasCfg.guide
|
||||
},
|
||||
set: value => {
|
||||
emits('update:canvasCfg', {
|
||||
...rightAsideProps.canvasCfg,
|
||||
guide: value
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<div style="height: 100%">
|
||||
<el-tag
|
||||
closable
|
||||
v-if="select_val"
|
||||
@close="addAnimation('')"
|
||||
@click="drawer_visiable = true"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
{{
|
||||
common_animate_list
|
||||
.map(m => m.children)
|
||||
.reduce((pre, curr) => {
|
||||
return pre.concat(curr)
|
||||
})
|
||||
.find(f => f.value == select_val)?.label
|
||||
}}
|
||||
<el-icon :size="10"><svg-analysis name="setting"></svg-analysis></el-icon>
|
||||
</el-tag>
|
||||
<el-tag v-else type="success" style="cursor: pointer" @click="drawer_visiable = true">新增</el-tag>
|
||||
<el-drawer v-model="drawer_visiable" title="选择动画" direction="ltr">
|
||||
<el-tabs v-model="activeName">
|
||||
<el-tab-pane
|
||||
:label="tab_item.label"
|
||||
:name="tab_item.label"
|
||||
v-for="tab_item in common_animate_list"
|
||||
:key="tab_item.label"
|
||||
>
|
||||
<el-scrollbar height="500px">
|
||||
<div class="flex flex-wrap">
|
||||
<div
|
||||
class="animate"
|
||||
v-for="(animate, index) in tab_item.children"
|
||||
:key="index"
|
||||
@mouseenter="play_index = index"
|
||||
@mouseleave="play_index = null"
|
||||
@click="addAnimation(animate.value)"
|
||||
>
|
||||
<div
|
||||
:class="`${
|
||||
play_index == index
|
||||
? `animate__animated animate__${animate.value} animate__slow animate__infinite`
|
||||
: ''
|
||||
}`"
|
||||
>
|
||||
{{ animate.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ElTag, ElDrawer, ElTabs, ElTabPane, ElScrollbar, ElIcon } from 'element-plus'
|
||||
import { computed, ref } from 'vue'
|
||||
import SvgAnalysis from '@/components/mt-edit/components/svg-analysis/index.vue'
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
const emits = defineEmits(['update:modelValue'])
|
||||
const select_val = computed(() => props.modelValue)
|
||||
const drawer_visiable = ref(false)
|
||||
const activeName = ref('进入')
|
||||
const play_index = ref<null | number>(null)
|
||||
const common_animate_list = [
|
||||
{
|
||||
label: '进入',
|
||||
children: [
|
||||
{ label: '渐显', value: 'fadeIn' },
|
||||
{ label: '向右进入', value: 'fadeInLeft' },
|
||||
{ label: '向左进入', value: 'fadeInRight' },
|
||||
{ label: '向上进入', value: 'fadeInUp' },
|
||||
{ label: '向下进入', value: 'fadeInDown' },
|
||||
{ label: '向右长距进入', value: 'fadeInLeftBig' },
|
||||
{ label: '向左长距进入', value: 'fadeInRightBig' },
|
||||
{ label: '向上长距进入', value: 'fadeInUpBig' },
|
||||
{ label: '向下长距进入', value: 'fadeInDownBig' },
|
||||
{ label: '旋转进入', value: 'rotateIn' },
|
||||
{ label: '左顺时针旋转', value: 'rotateInDownLeft' },
|
||||
{ label: '右逆时针旋转', value: 'rotateInDownRight' },
|
||||
{ label: '左逆时针旋转', value: 'rotateInUpLeft' },
|
||||
{ label: '右逆时针旋转', value: 'rotateInUpRight' },
|
||||
{ label: '弹入', value: 'bounceIn' },
|
||||
{ label: '向右弹入', value: 'bounceInLeft' },
|
||||
{ label: '向左弹入', value: 'bounceInRight' },
|
||||
{ label: '向上弹入', value: 'bounceInUp' },
|
||||
{ label: '向下弹入', value: 'bounceInDown' },
|
||||
{ label: '光速从右进入', value: 'lightSpeedInRight' },
|
||||
{ label: '光速从左进入', value: 'lightSpeedInLeft' },
|
||||
{ label: '光速从右退出', value: 'lightSpeedOutRight' },
|
||||
{ label: '光速从左退出', value: 'lightSpeedOutLeft' },
|
||||
{ label: 'Y轴旋转', value: 'flip' },
|
||||
{ label: '中心X轴旋转', value: 'flipInX' },
|
||||
{ label: '中心Y轴旋转', value: 'flipInY' },
|
||||
{ label: '左长半径旋转', value: 'rollIn' },
|
||||
{ label: '由小变大进入', value: 'zoomIn' },
|
||||
{ label: '左变大进入', value: 'zoomInLeft' },
|
||||
{ label: '右变大进入', value: 'zoomInRight' },
|
||||
{ label: '向上变大进入', value: 'zoomInUp' },
|
||||
{ label: '向下变大进入', value: 'zoomInDown' },
|
||||
{ label: '向右滑动展开', value: 'slideInLeft' },
|
||||
{ label: '向左滑动展开', value: 'slideInRight' },
|
||||
{ label: '向上滑动展开', value: 'slideInUp' },
|
||||
{ label: '向下滑动展开', value: 'slideInDown' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '强调',
|
||||
children: [
|
||||
{ label: '弹跳', value: 'bounce' },
|
||||
{ label: '闪烁', value: 'flash' },
|
||||
{ label: '放大缩小', value: 'pulse' },
|
||||
{ label: '放大缩小弹簧', value: 'rubberBand' },
|
||||
{ label: '左右晃动', value: 'headShake' },
|
||||
{ label: '左右扇形摇摆', value: 'swing' },
|
||||
{ label: '放大晃动缩小', value: 'tada' },
|
||||
{ label: '扇形摇摆', value: 'wobble' },
|
||||
{ label: '左右上下晃动', value: 'jello' },
|
||||
{ label: 'Y轴旋转', value: 'flip' },
|
||||
{ label: '旋转360', value: 'rotate360' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '退出',
|
||||
children: [
|
||||
{ label: '渐隐', value: 'fadeOut' },
|
||||
{ label: '向左退出', value: 'fadeOutLeft' },
|
||||
{ label: '向右退出', value: 'fadeOutRight' },
|
||||
{ label: '向上退出', value: 'fadeOutUp' },
|
||||
{ label: '向下退出', value: 'fadeOutDown' },
|
||||
{ label: '向左长距退出', value: 'fadeOutLeftBig' },
|
||||
{ label: '向右长距退出', value: 'fadeOutRightBig' },
|
||||
{ label: '向上长距退出', value: 'fadeOutUpBig' },
|
||||
{ label: '向下长距退出', value: 'fadeOutDownBig' },
|
||||
{ label: '旋转退出', value: 'rotateOut' },
|
||||
{ label: '左顺时针旋转', value: 'rotateOutDownLeft' },
|
||||
{ label: '右逆时针旋转', value: 'rotateOutDownRight' },
|
||||
{ label: '左逆时针旋转', value: 'rotateOutUpLeft' },
|
||||
{ label: '右逆时针旋转', value: 'rotateOutUpRight' },
|
||||
{ label: '弹出', value: 'bounceOut' },
|
||||
{ label: '向左弹出', value: 'bounceOutLeft' },
|
||||
{ label: '向右弹出', value: 'bounceOutRight' },
|
||||
{ label: '向上弹出', value: 'bounceOutUp' },
|
||||
{ label: '向下弹出', value: 'bounceOutDown' },
|
||||
{ label: '中心X轴旋转', value: 'flipOutX' },
|
||||
{ label: '中心Y轴旋转', value: 'flipOutY' },
|
||||
{ label: '左长半径旋转', value: 'rollOut' },
|
||||
{ label: '由小变大退出', value: 'zoomOut' },
|
||||
{ label: '左变大退出', value: 'zoomOutLeft' },
|
||||
{ label: '右变大退出', value: 'zoomOutRight' },
|
||||
{ label: '向上变大退出', value: 'zoomOutUp' },
|
||||
{ label: '向下变大退出', value: 'zoomOutDown' },
|
||||
{ label: '向左滑动收起', value: 'slideOutLeft' },
|
||||
{ label: '向右滑动收起', value: 'slideOutRight' },
|
||||
{ label: '向上滑动收起', value: 'slideOutUp' },
|
||||
{ label: '向下滑动收起', value: 'slideOutDown' }
|
||||
]
|
||||
}
|
||||
]
|
||||
const addAnimation = (val: string) => {
|
||||
emits('update:modelValue', val)
|
||||
drawer_visiable.value = false
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.animate {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.animate > div {
|
||||
width: 80px;
|
||||
height: 60px;
|
||||
background: #f5f8fb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 12px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
border-radius: 3px;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<el-form label-width="60px" label-position="left">
|
||||
<el-form-item label="动画效果" size="small">
|
||||
<common-animate v-model="item_val"></common-animate>
|
||||
</el-form-item>
|
||||
<el-form-item label="延迟" size="small">
|
||||
<el-select v-model="item_delay">
|
||||
<el-option value="delay-0s" label="无"></el-option>
|
||||
<el-option value="delay-1s" label="1秒"></el-option>
|
||||
<el-option value="delay-2s" label="2秒"></el-option>
|
||||
<el-option value="delay-3s" label="3秒"></el-option>
|
||||
<el-option value="delay-4s" label="4秒"></el-option>
|
||||
<el-option value="delay-5s" label="5秒"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="动画速度" size="small">
|
||||
<el-select v-model="item_speed">
|
||||
<el-option value="slow" label="慢"></el-option>
|
||||
<el-option value="slower" label="最慢"></el-option>
|
||||
<el-option value="fast" label="快"></el-option>
|
||||
<el-option value="faster" label="最快"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="循环次数" size="small">
|
||||
<el-select v-model="item_repeat">
|
||||
<el-option value="repeat-1" label="一次"></el-option>
|
||||
<el-option value="repeat-2" label="两次"></el-option>
|
||||
<el-option value="repeat-3" label="三次"></el-option>
|
||||
<el-option value="infinite" label="无限次"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { ICommonAnimations } from '@/components/mt-edit/store/types'
|
||||
import CommonAnimate from './common-animate.vue'
|
||||
import { computed } from 'vue'
|
||||
import { ElForm, ElFormItem, ElSelect, ElOption } from 'element-plus'
|
||||
type SelectItemAnimateSettingProps = {
|
||||
commonAnimates: ICommonAnimations | undefined
|
||||
}
|
||||
const selectItemAnimateSettingProps = withDefaults(defineProps<SelectItemAnimateSettingProps>(), {})
|
||||
const emits = defineEmits(['update:commonAnimates'])
|
||||
const item_val = computed({
|
||||
get: () => {
|
||||
return selectItemAnimateSettingProps.commonAnimates?.val
|
||||
},
|
||||
set: value => {
|
||||
emits('update:commonAnimates', {
|
||||
...selectItemAnimateSettingProps.commonAnimates,
|
||||
val: value
|
||||
})
|
||||
}
|
||||
})
|
||||
const item_delay = computed({
|
||||
get: () => {
|
||||
return selectItemAnimateSettingProps.commonAnimates?.delay
|
||||
},
|
||||
set: value => {
|
||||
emits('update:commonAnimates', {
|
||||
...selectItemAnimateSettingProps.commonAnimates,
|
||||
delay: value
|
||||
})
|
||||
}
|
||||
})
|
||||
const item_speed = computed({
|
||||
get: () => {
|
||||
return selectItemAnimateSettingProps.commonAnimates?.speed
|
||||
},
|
||||
set: value => {
|
||||
emits('update:commonAnimates', {
|
||||
...selectItemAnimateSettingProps.commonAnimates,
|
||||
speed: value
|
||||
})
|
||||
}
|
||||
})
|
||||
const item_repeat = computed({
|
||||
get: () => {
|
||||
return selectItemAnimateSettingProps.commonAnimates?.repeat
|
||||
},
|
||||
set: value => {
|
||||
emits('update:commonAnimates', {
|
||||
...selectItemAnimateSettingProps.commonAnimates,
|
||||
repeat: value
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,429 @@
|
||||
<template>
|
||||
<div v-if="useData.graphicDisplay == 'zl'">
|
||||
<el-form label-width="60px" label-position="left">
|
||||
<el-form-item label="监测点" size="small" class="mt-10px" v-if="item_title == '绑定监测点' || item_title == '绑定指标'">
|
||||
<div>
|
||||
<el-cascader :key="cascaderKey" :options="treeData"
|
||||
:props="{ value: 'id', label: 'name', children: 'children' }" clearable placeholder="请选择"
|
||||
@change="handleDeptChange" @clear="handleDeptClear" filterable v-model="deptIds"
|
||||
ref="fileRef"></el-cascader>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="指标绑定" size="small" class="mt-10px"
|
||||
v-if="deptIds && deptIds.length > 0 && item_title == '绑定指标'">
|
||||
<div>
|
||||
<el-cascader :key="cascaderKey + 1" v-model="item_uid" filterable :options="treeIndexs"
|
||||
:props="{ value: 'id', label: 'showName', children: 'children' }" @change="handleSelectUID"
|
||||
@clear="handleIndexClear" clearable placeholder="请选择" ref="indexRef" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<!-- 无锡监测点 指标 -->
|
||||
<div v-if="useData.graphicDisplay == 'wx'">
|
||||
<el-form label-width="60px" label-position="left">
|
||||
<el-form-item label="监测点" size="small" class="mt-10px">
|
||||
<div>
|
||||
<el-cascader :key="cascaderKey" :options="treeData_wx"
|
||||
:props="{ value: 'id', label: 'name', children: 'children' }" clearable placeholder="请选择"
|
||||
@change="handleDeptChange" @clear="handleDeptClear" filterable v-model="deptIds"
|
||||
ref="fileRef"></el-cascader>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="指标绑定" size="small" class="mt-10px" v-if="deptIds && deptIds.length > 0">
|
||||
<div>
|
||||
<el-cascader :key="cascaderKey + 1" v-model="item_uid" filterable collapse-tags
|
||||
collapse-tags-tooltip :options="treeIndexs" :props="{
|
||||
value: 'name',
|
||||
label: 'showName',
|
||||
children: 'children',
|
||||
multiple: item_title == '绑定指标' ? false : true
|
||||
}" @change="handleSelectUID" @clear="handleIndexClear" clearable placeholder="请选择"
|
||||
ref="indexRef" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<!-- 显示所选项 -->
|
||||
|
||||
<div v-if="labelString && labelString.length > 0">
|
||||
<el-divider />
|
||||
<el-descriptions title="所选监测点" :column="1">
|
||||
<el-descriptions-item>{{ labelString }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-divider />
|
||||
</div>
|
||||
<div v-if="indexString && indexString.length > 0">
|
||||
<el-descriptions title="所选指标" :column="1" class="tipBox">
|
||||
<el-descriptions-item v-for="value in indexString">{{ value }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ElForm, ElFormItem, ElTreeSelect } from 'element-plus'
|
||||
import { computed, ref, watch, onMounted, reactive, nextTick, watchEffect } from 'vue'
|
||||
import type { IDoneJson } from '@/components/mt-edit/store/types'
|
||||
import { globalStore } from '@/components/mt-edit/store/global'
|
||||
import { lineTree, targetList, eleEpdChooseTree_wx } from '@/api/index'
|
||||
import { useDataStore } from '@/stores/menuList'
|
||||
import { lineTree_wx } from '@/api/index_wx'
|
||||
import { templateRef } from '@vueuse/core'
|
||||
|
||||
type SelectItemSettingProps = {
|
||||
itemJson: IDoneJson | undefined
|
||||
}
|
||||
const selectItemSettingProps = withDefaults(defineProps<SelectItemSettingProps>(), {})
|
||||
|
||||
const item_title = computed(() => selectItemSettingProps.itemJson?.title)
|
||||
|
||||
const flag = ref(false)
|
||||
|
||||
const deptIds = ref([])
|
||||
|
||||
const item_uid = ref([])
|
||||
|
||||
const fileRef = ref()
|
||||
|
||||
const indexRef = ref()
|
||||
|
||||
const labelString = ref('')
|
||||
|
||||
const indexString: any = ref('')
|
||||
|
||||
const disabledTooltip = ref(true)
|
||||
|
||||
const disabledTooltip_1 = ref(true)
|
||||
|
||||
const useData = useDataStore()
|
||||
|
||||
// 添加用于强制刷新的 key
|
||||
const cascaderKey = ref(0)
|
||||
|
||||
const treeData_wx = ref([])
|
||||
|
||||
// 重置级联选择器的方法 关闭面板
|
||||
const resetCascaders = () => {
|
||||
// 清空绑定的数据
|
||||
deptIds.value = []
|
||||
item_uid.value = []
|
||||
|
||||
// 清空显示文本
|
||||
labelString.value = ''
|
||||
indexString.value = ''
|
||||
|
||||
// 强制重新渲染组件
|
||||
cascaderKey.value += 1
|
||||
}
|
||||
|
||||
watch(
|
||||
() => selectItemSettingProps.itemJson?.keyId,
|
||||
(newVal, oldVal) => {
|
||||
if (newVal == 'bind-dot' || newVal == 'bind-index') {
|
||||
flag.value = true
|
||||
} else {
|
||||
flag.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => selectItemSettingProps.itemJson?.id,
|
||||
(newVal, oldVal) => {
|
||||
resetCascaders()
|
||||
// 回显
|
||||
globalStore.done_json.forEach((item: any, index: number) => {
|
||||
if (item.id == newVal) {
|
||||
// 回显
|
||||
if (useData.graphicDisplay == 'wx') {
|
||||
// 无锡多选
|
||||
deptIds.value = item.lineList
|
||||
} else {
|
||||
// 单选
|
||||
deptIds.value = item.lineId
|
||||
}
|
||||
|
||||
item_uid.value = item.UID
|
||||
labelString.value = item.lineName
|
||||
indexString.value = item.UIDNames
|
||||
nextTick(() => {
|
||||
if (item_title.value == '绑定指标' && deptIds.value) {
|
||||
indexList()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (selectItemSettingProps.itemJson?.keyId) {
|
||||
if (
|
||||
selectItemSettingProps.itemJson?.keyId == 'bind-dot' ||
|
||||
selectItemSettingProps.itemJson?.keyId == 'bind-index'
|
||||
) {
|
||||
flag.value = true
|
||||
} else {
|
||||
flag.value = false
|
||||
}
|
||||
}
|
||||
//fetchData()
|
||||
if (useData.graphicDisplay == 'wx') {
|
||||
fetchData_wx()
|
||||
} else {
|
||||
fetchData()
|
||||
}
|
||||
})
|
||||
|
||||
interface TreeOption {
|
||||
name: string
|
||||
id: string | number
|
||||
children?: TreeOption[]
|
||||
}
|
||||
|
||||
const treeData = ref<TreeOption[]>([])
|
||||
|
||||
const treeIndexs = ref<TreeOption[]>([])
|
||||
|
||||
function transformData(data: any[]): TreeOption[] {
|
||||
return data.map(item => ({
|
||||
name: item.name,
|
||||
id: item.id,
|
||||
children: item.children?.length ? transformData(item.children) : undefined
|
||||
}))
|
||||
}
|
||||
|
||||
// 过滤没有监测点的数据
|
||||
function buildLevel3Tree(nodes: any[]) {
|
||||
const result: any[] = []
|
||||
if (!nodes || !nodes.length) return result
|
||||
nodes.forEach(node => {
|
||||
const filteredChildren = buildLevel3Tree(node.children)
|
||||
if (node.level === 3) {
|
||||
result.push({ ...node, children: filteredChildren })
|
||||
} else if (filteredChildren.length > 0) {
|
||||
result.push({ ...node, children: filteredChildren })
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
function transformDataIdex(data: any[]): TreeOption[] {
|
||||
return data.map(item => ({
|
||||
name: item.showName,
|
||||
id: item.id,
|
||||
children: item.children?.length ? transformDataIdex(item.children) : undefined
|
||||
}))
|
||||
}
|
||||
|
||||
// 监测点数据
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await lineTree({})
|
||||
treeData.value = buildLevel3Tree(response.data) // 转换数据格式并赋值给 transformedData
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 无锡
|
||||
const fetchData_wx = async () => {
|
||||
try {
|
||||
const response = await lineTree_wx({})
|
||||
treeData_wx.value = response.data // 转换数据格式并赋值给 transformedData
|
||||
const res = await eleEpdChooseTree_wx()
|
||||
if (res.data) {
|
||||
treeIndexs.value = res.data // 转换数据格式并赋值给 transformedData
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 指标数据
|
||||
const indexList = async () => {
|
||||
try {
|
||||
const lineId = deptIds.value[deptIds.value.length - 1]
|
||||
{
|
||||
const response = await targetList({ lineId: lineId })
|
||||
if (response.data) {
|
||||
treeIndexs.value = response.data // 转换数据格式并赋值给 transformedData
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeptChange = (deptId: []) => {
|
||||
// labelString.value = fileRef.value.getCheckedNodes().pathLabels.join(" / ");
|
||||
item_uid.value = []
|
||||
nextTick(() => {
|
||||
//fileRef.value.getCheckedNodes()[0]?.label 最后一层的值
|
||||
let name = []
|
||||
if (fileRef.value) {
|
||||
const nodes = fileRef.value.getCheckedNodes()
|
||||
name = nodes[0]?.pathLabels || []
|
||||
}
|
||||
|
||||
if (selectItemSettingProps.itemJson) {
|
||||
selectItemSettingProps.itemJson.lineId = deptId[deptId.length - 1]
|
||||
selectItemSettingProps.itemJson.lineList = deptId
|
||||
selectItemSettingProps.itemJson.lineName = name.join(' / ')
|
||||
}
|
||||
|
||||
labelString.value = name.join(' / ')
|
||||
|
||||
if (useData.graphicDisplay == 'zl') {
|
||||
// 配置里面的输入框内容更新
|
||||
if (selectItemSettingProps.itemJson && selectItemSettingProps.itemJson.props) {
|
||||
if (selectItemSettingProps.itemJson?.props.text.type == 'input') {
|
||||
selectItemSettingProps.itemJson.props.text.val = fileRef.value.getCheckedNodes()[0]?.label
|
||||
}
|
||||
}
|
||||
|
||||
if (deptId && item_title.value == '绑定指标') {
|
||||
indexList()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// 给每一个元件绑定下拉框数据
|
||||
const handleSelectUID = (uid: []) => {
|
||||
// 一定要在 nextTick 方法中进行赋值操作
|
||||
nextTick(() => {
|
||||
let name = []
|
||||
let nodes = []
|
||||
if (indexRef.value) {
|
||||
nodes = indexRef.value.getCheckedNodes()
|
||||
console.log('🚀 ~ handleSelectUID ~ indexRef.value.getCheckedNodes():', indexRef.value.getCheckedNodes())
|
||||
name = nodes[0]?.pathLabels || []
|
||||
}
|
||||
if (selectItemSettingProps.itemJson) {
|
||||
selectItemSettingProps.itemJson.UID = uid
|
||||
selectItemSettingProps.itemJson.UIDName =
|
||||
nodes[0].pathNodes[1].data.name + '$' + nodes[0].pathNodes[2].data.name + '$' + nodes[0].data.name //name.join('/')
|
||||
console.log("🚀 ~ handleSelectUID ~ 'nodes[0].pathNodes[0]$' + nodes[0].data.name:", nodes[0])
|
||||
if (is2DArray(uid)) {
|
||||
selectItemSettingProps.itemJson.UIDNames = name.join(' / ')
|
||||
} else {
|
||||
selectItemSettingProps.itemJson.UIDNames = [name.join(' / ')]
|
||||
}
|
||||
}
|
||||
|
||||
// 获取选中的数据名称
|
||||
const nameList = indexRef.value
|
||||
.getCheckedNodes()
|
||||
.map((item: any) => item.text)
|
||||
.filter((text: string) => text !== '' && text !== null && text !== undefined)
|
||||
const unitList = indexRef.value
|
||||
.getCheckedNodes()
|
||||
.map((item: any) => item.data.unit)
|
||||
.filter((text: string) => text !== '' && text !== null && text !== undefined)
|
||||
if (is2DArray(uid)) {
|
||||
if (selectItemSettingProps.itemJson) {
|
||||
selectItemSettingProps.itemJson.UIDNames = nameList
|
||||
selectItemSettingProps.itemJson.unit = unitList
|
||||
indexString.value = selectItemSettingProps.itemJson.UIDNames
|
||||
}
|
||||
} else {
|
||||
indexString.value = nameList
|
||||
// 配置里面的输入框内容更新
|
||||
if (selectItemSettingProps.itemJson && selectItemSettingProps.itemJson.props) {
|
||||
if (selectItemSettingProps.itemJson?.props.text.type == 'input') {
|
||||
// selectItemSettingProps.itemJson.props.text.val = name.join(' / ')
|
||||
let names = name.reverse()
|
||||
let str = ''
|
||||
if (names[1] == '无相别') {
|
||||
name[1] = ''
|
||||
str = names[2] + '-' + names[0] + ':###'
|
||||
} else {
|
||||
str = names[1] + '相' + names[2] + '-' + names[0] + ':###'
|
||||
}
|
||||
selectItemSettingProps.itemJson.props.text.val = str
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 添加监测点清除事件处理函数
|
||||
const handleDeptClear = () => {
|
||||
// 清空监测点相关数据
|
||||
deptIds.value = []
|
||||
item_uid.value = []
|
||||
labelString.value = ''
|
||||
indexString.value = ''
|
||||
|
||||
// 清空 itemJson 中的相关属性
|
||||
if (selectItemSettingProps.itemJson) {
|
||||
selectItemSettingProps.itemJson.lineId = undefined
|
||||
selectItemSettingProps.itemJson.lineList = []
|
||||
selectItemSettingProps.itemJson.lineName = ''
|
||||
selectItemSettingProps.itemJson.UID = []
|
||||
selectItemSettingProps.itemJson.UIDName = ''
|
||||
selectItemSettingProps.itemJson.UIDNames = []
|
||||
|
||||
// 清空配置中的输入框内容
|
||||
if (selectItemSettingProps.itemJson.props && selectItemSettingProps.itemJson.props.text) {
|
||||
selectItemSettingProps.itemJson.props.text.val = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 重置 tooltip 状态
|
||||
disabledTooltip.value = true
|
||||
disabledTooltip_1.value = true
|
||||
|
||||
// 强制重新渲染组件
|
||||
cascaderKey.value += 2
|
||||
}
|
||||
|
||||
// 添加指标绑定清除事件处理函数
|
||||
const handleIndexClear = () => {
|
||||
// 清空指标绑定相关数据
|
||||
item_uid.value = []
|
||||
indexString.value = ''
|
||||
|
||||
// 清空 itemJson 中的相关属性
|
||||
if (selectItemSettingProps.itemJson) {
|
||||
selectItemSettingProps.itemJson.UID = []
|
||||
selectItemSettingProps.itemJson.UIDName = ''
|
||||
selectItemSettingProps.itemJson.UIDNames = []
|
||||
|
||||
// 清空配置中的输入框内容
|
||||
if (selectItemSettingProps.itemJson.props && selectItemSettingProps.itemJson.props.text) {
|
||||
selectItemSettingProps.itemJson.props.text.val = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 重置 tooltip 状态
|
||||
disabledTooltip_1.value = true
|
||||
|
||||
// 强制重新渲染组件
|
||||
cascaderKey.value += 2
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断一个数据是否为二维数组
|
||||
* @param {*} data - 需要判断的数据
|
||||
* @returns {boolean} - 是二维数组返回true,否则返回false
|
||||
*/
|
||||
function is2DArray(data: any) {
|
||||
// 首先判断外层是否为数组
|
||||
if (!Array.isArray(data)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 然后判断内层每个元素是否都为数组
|
||||
return data.every(item => Array.isArray(item))
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.tipBox {
|
||||
:deep(.el-descriptions__body) {
|
||||
height: calc(100vh - 485px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,525 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-button type="primary" class="w-1/1" @click="onAddEvent">添加事件</el-button>
|
||||
<el-form label-width="60px" label-position="left">
|
||||
<el-collapse v-model="activeNames">
|
||||
<el-collapse-item
|
||||
v-for="(event_list_item, event_list_index) in event_list"
|
||||
:key="event_list_item.id"
|
||||
:name="event_list_item.id"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex justify-between items-center w-1/1">
|
||||
<el-text>事件{{ event_list_index + 1 }}</el-text>
|
||||
<el-popconfirm title="删除该事件?" @confirm="onDelEvent(event_list_index)">
|
||||
<template #reference>
|
||||
<el-button text circle size="small" @click.stop>
|
||||
<el-icon title="删除" :class="``" :size="20">
|
||||
<svg-analysis name="delete"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<el-form-item label="事件类型" size="small" class="mt-10px">
|
||||
<el-select placeholder="事件类型" v-model="event_list[event_list_index].type">
|
||||
<el-option
|
||||
v-for="item in event_type"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="事件行为" size="small">
|
||||
<el-select placeholder="事件行为" v-model="event_list[event_list_index].action">
|
||||
<el-option
|
||||
v-for="item in event_action"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="页面跳转"
|
||||
size="small"
|
||||
v-if="event_list[event_list_index].action == 'pageJump'"
|
||||
>
|
||||
<el-select
|
||||
placeholder="页面跳转"
|
||||
v-model="event_list[event_list_index].jump_to"
|
||||
@change="onJumpToChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in dataTree"
|
||||
:key="item.kId"
|
||||
:label="item.name"
|
||||
:value="item.kId"
|
||||
:disabled="item.kId == currentKid"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div v-else>
|
||||
<el-form-item v-if="event_list_item.action == 'changeAttr'" label="属性更改" size="small">
|
||||
<el-button @click="onChangeAttrClick(event_list_item, event_list_index)">
|
||||
点击配置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-else-if="event_list_item.action == 'customCode'"
|
||||
label="代码编写"
|
||||
size="small"
|
||||
>
|
||||
<el-button @click="onCustomCodeClick(event_list_item, event_list_index)">
|
||||
点击编写
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="触发规则" size="small">
|
||||
<el-text>(不填直接触发)</el-text>
|
||||
</el-form-item>
|
||||
<div>
|
||||
<el-text>当</el-text>
|
||||
<el-select
|
||||
placeholder="选择图形"
|
||||
size="small"
|
||||
v-model="event_list[event_list_index].trigger_rule.trigger_id"
|
||||
@change="onTriggerIdChange(event_list_index)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in all_component_options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-text>图形</el-text>
|
||||
<el-select
|
||||
placeholder="选择属性"
|
||||
size="small"
|
||||
v-model="event_list[event_list_index].trigger_rule.trigger_attr"
|
||||
@change="onTriggerAttrChange(event_list_index)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in getAttrById(
|
||||
event_list[event_list_index].trigger_rule.trigger_id
|
||||
)"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-text>属性</el-text>
|
||||
<el-select
|
||||
placeholder="运算符"
|
||||
size="small"
|
||||
v-model="event_list[event_list_index].trigger_rule.operator"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in getOperatorList(
|
||||
getAttrTypeByValue(
|
||||
getAttrById(event_list[event_list_index].trigger_rule.trigger_id),
|
||||
event_list[event_list_index].trigger_rule.trigger_attr
|
||||
)
|
||||
)"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<input-target-value
|
||||
v-model="event_list[event_list_index].trigger_rule.value"
|
||||
:form-type="
|
||||
getAttrTypeByValue(
|
||||
getAttrById(event_list[event_list_index].trigger_rule.trigger_id),
|
||||
event_list[event_list_index].trigger_rule.trigger_attr
|
||||
)
|
||||
"
|
||||
:options="
|
||||
getAttrOptionsByValue(
|
||||
getAttrById(event_list[event_list_index].trigger_rule.trigger_id),
|
||||
event_list[event_list_index].trigger_rule.trigger_attr
|
||||
)
|
||||
"
|
||||
:disabled="event_list[event_list_index].trigger_rule.trigger_attr == undefined"
|
||||
size="small"
|
||||
></input-target-value>
|
||||
<el-text>时</el-text>
|
||||
<div>
|
||||
<el-text>触发该事件</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-form>
|
||||
<el-drawer v-model="drawer_visiable" :title="drawer_title" direction="ltr" size="50%">
|
||||
<el-button class="w-1/1" @click="onAddChangeAttr">新增一组</el-button>
|
||||
<el-form label-width="60px" label-position="left">
|
||||
<div
|
||||
v-for="(change_attr_item, change_attr_index) in drawer_change_attr"
|
||||
:key="change_attr_item.id"
|
||||
class="flex items-center mt-10px"
|
||||
>
|
||||
<el-form-item label="目标图形" size="small" class="m-0 pr-10px w-3/10">
|
||||
<el-select
|
||||
placeholder="选择目标图形"
|
||||
v-model="drawer_change_attr[change_attr_index].target_id"
|
||||
@change="drawer_change_attr[change_attr_index].target_attr = undefined"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in all_component_options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标属性" size="small" class="m-0 pr-10px w-3/10">
|
||||
<el-select
|
||||
placeholder="选择目标属性"
|
||||
v-model="drawer_change_attr[change_attr_index].target_attr"
|
||||
@change="onTargetAttrChange(change_attr_index)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in getAttrById(drawer_change_attr[change_attr_index].target_id)"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="期望值" size="small" class="m-0 pr-10px w-3/10">
|
||||
<input-target-value
|
||||
v-model="drawer_change_attr[change_attr_index].target_value"
|
||||
:form-type="
|
||||
getAttrTypeByValue(
|
||||
getAttrById(drawer_change_attr[change_attr_index].target_id),
|
||||
drawer_change_attr[change_attr_index].target_attr
|
||||
)
|
||||
"
|
||||
:options="
|
||||
getAttrOptionsByValue(
|
||||
getAttrById(drawer_change_attr[change_attr_index].target_id),
|
||||
drawer_change_attr[change_attr_index].target_attr
|
||||
)
|
||||
"
|
||||
:disabled="drawer_change_attr[change_attr_index].target_attr == undefined"
|
||||
></input-target-value>
|
||||
</el-form-item>
|
||||
<el-popconfirm title="删除该配置?" @confirm="onRemoveChangeAttr(change_attr_index)">
|
||||
<template #reference>
|
||||
<el-button text circle size="small" @click.stop>
|
||||
<el-icon title="删除" :class="``" :size="20">
|
||||
<svg-analysis name="delete"></svg-analysis>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
<el-dialog v-model="dialog_visiable" :title="dialog_title" :before-close="onDialogClose">
|
||||
<v-ace-editor
|
||||
v-model:value="dialog_code"
|
||||
lang="javascript"
|
||||
theme="monokai"
|
||||
style="height: 400px"
|
||||
:options="{
|
||||
useWorker: true,
|
||||
enableBasicAutocompletion: true,
|
||||
enableSnippets: true,
|
||||
enableLiveAutocompletion: true
|
||||
}"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ElButton,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElText,
|
||||
ElInput,
|
||||
ElCollapse,
|
||||
ElCollapseItem,
|
||||
ElIcon,
|
||||
ElPopconfirm,
|
||||
ElDrawer,
|
||||
ElDialog
|
||||
} from 'element-plus'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import SvgAnalysis from '@/components/mt-edit/components/svg-analysis/index.vue'
|
||||
import { randomString } from '@/components/mt-edit/utils'
|
||||
import type {
|
||||
DoneJsonEventListAction,
|
||||
DoneJsonEventListType,
|
||||
IDoneJson,
|
||||
IDoneJsonActionChangeAttr,
|
||||
IDoneJsonEventList,
|
||||
ILeftAsideConfigItemPublicPropsType
|
||||
} from '@/components/mt-edit/store/types'
|
||||
import InputTargetValue from './input-target-value.vue'
|
||||
import '@/components/mt-edit/ace-edit'
|
||||
import { VAceEditor } from 'vue3-ace-editor'
|
||||
import { useDataStore } from '@/stores/menuList'
|
||||
|
||||
type SelectItemEventSettingProps = {
|
||||
doneJson: IDoneJson[]
|
||||
itemEvents?: IDoneJsonEventList[]
|
||||
}
|
||||
const selectItemEventSettingProps = withDefaults(defineProps<SelectItemEventSettingProps>(), {
|
||||
itemEvents: () => []
|
||||
})
|
||||
const selectItemEventSettingEmits = defineEmits(['update:itemEvents'])
|
||||
interface IActionChangeAttrTarget {
|
||||
label: string
|
||||
value: string
|
||||
type: ILeftAsideConfigItemPublicPropsType
|
||||
options?: { label: string; value: any }[]
|
||||
}
|
||||
|
||||
const event_type: {
|
||||
label: string
|
||||
value: DoneJsonEventListType
|
||||
}[] = [
|
||||
{
|
||||
label: '单击',
|
||||
value: 'click'
|
||||
},
|
||||
{
|
||||
label: '双击',
|
||||
value: 'dblclick'
|
||||
},
|
||||
{
|
||||
label: '鼠标移入',
|
||||
value: 'mouseover'
|
||||
},
|
||||
{
|
||||
label: '鼠标移出',
|
||||
value: 'mouseout'
|
||||
}
|
||||
]
|
||||
const event_action: {
|
||||
label: string
|
||||
value: DoneJsonEventListAction
|
||||
}[] = [
|
||||
{
|
||||
label: '属性更改',
|
||||
value: 'changeAttr'
|
||||
},
|
||||
{
|
||||
label: '自定义代码',
|
||||
value: 'customCode'
|
||||
},
|
||||
{
|
||||
label: '跳转页面',
|
||||
value: 'pageJump'
|
||||
}
|
||||
]
|
||||
const all_component_options = computed(() => {
|
||||
return selectItemEventSettingProps.doneJson.map(m => {
|
||||
return {
|
||||
label: m.title,
|
||||
value: m.id
|
||||
}
|
||||
})
|
||||
})
|
||||
const activeNames = ref([])
|
||||
const event_list = computed(() => selectItemEventSettingProps.itemEvents)
|
||||
const select_event_index = ref(0)
|
||||
const drawer_visiable = ref(false)
|
||||
const drawer_title = ref('属性更改配置')
|
||||
const drawer_change_attr = ref<IDoneJsonActionChangeAttr[]>([]) // 属性更改配置信息
|
||||
const dialog_visiable = ref(false)
|
||||
const dialog_title = ref('自定义代码编写')
|
||||
const dialog_code = ref('')
|
||||
|
||||
const dataStore = useDataStore()
|
||||
|
||||
const dataTree = computed(() => dataStore.dataTree)
|
||||
|
||||
const currentKid = computed(() => dataStore.identifying)
|
||||
const onAddEvent = () => {
|
||||
event_list.value.push({
|
||||
id: randomString(),
|
||||
type: 'click',
|
||||
action: 'changeAttr',
|
||||
jump_to: '',
|
||||
change_attr: [],
|
||||
custom_code: '',
|
||||
trigger_rule: {
|
||||
trigger_id: undefined,
|
||||
trigger_attr: undefined,
|
||||
operator: undefined,
|
||||
value: null
|
||||
}
|
||||
})
|
||||
}
|
||||
const onDelEvent = (event_list_index: number) => {
|
||||
event_list.value.splice(event_list_index, 1)
|
||||
}
|
||||
const onChangeAttrClick = (item: IDoneJsonEventList, index: number) => {
|
||||
drawer_visiable.value = true
|
||||
drawer_change_attr.value = item.change_attr
|
||||
drawer_title.value = `事件${index + 1}属性更改配置`
|
||||
select_event_index.value = index
|
||||
}
|
||||
const onAddChangeAttr = () => {
|
||||
drawer_change_attr.value.push({
|
||||
id: randomString(),
|
||||
target_id: '',
|
||||
target_attr: undefined,
|
||||
target_value: undefined
|
||||
})
|
||||
}
|
||||
const onRemoveChangeAttr = (change_attr_index: number) => {
|
||||
drawer_change_attr.value.splice(change_attr_index, 1)
|
||||
}
|
||||
const onTargetAttrChange = (change_attr_index: number) => {
|
||||
const type = getAttrTypeByValue(
|
||||
getAttrById(drawer_change_attr.value[change_attr_index].target_id),
|
||||
drawer_change_attr.value[change_attr_index].target_attr
|
||||
)
|
||||
if (type == 'switch') {
|
||||
drawer_change_attr.value[change_attr_index].target_value = false
|
||||
} else if (type == 'number') {
|
||||
drawer_change_attr.value[change_attr_index].target_value = 0
|
||||
} else {
|
||||
drawer_change_attr.value[change_attr_index].target_value = undefined
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 根据id获取属性
|
||||
* @param id
|
||||
*/
|
||||
const getAttrById = (id: string | null | undefined) => {
|
||||
if (!id) {
|
||||
return []
|
||||
}
|
||||
const find_item_json = selectItemEventSettingProps.doneJson.find(f => f.id == id)
|
||||
if (!find_item_json) {
|
||||
return []
|
||||
}
|
||||
const attr: IActionChangeAttrTarget[] = [
|
||||
{
|
||||
label: 'x轴坐标',
|
||||
value: 'binfo.left',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
label: 'y轴坐标',
|
||||
value: 'binfo.top',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
label: '宽度',
|
||||
value: 'binfo.width',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
label: '高度',
|
||||
value: 'binfo.height',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
label: '旋转角度',
|
||||
value: 'binfo.angle',
|
||||
type: 'number'
|
||||
}
|
||||
]
|
||||
for (const props_key in find_item_json.props) {
|
||||
if (find_item_json.props[props_key].disabled) {
|
||||
continue
|
||||
}
|
||||
attr.push({
|
||||
label: find_item_json.props[props_key].title,
|
||||
value: `props.${props_key}.val`,
|
||||
type: find_item_json.props[props_key].type,
|
||||
options: find_item_json.props[props_key].options
|
||||
})
|
||||
}
|
||||
return attr
|
||||
}
|
||||
/**
|
||||
* 根据值获取属性类型
|
||||
* @param attr
|
||||
* @param val
|
||||
*/
|
||||
const getAttrTypeByValue = (attr: IActionChangeAttrTarget[], val: any) => {
|
||||
return attr.find(f => f.value == val)?.type ?? ''
|
||||
}
|
||||
/**
|
||||
* 根据值获取属性选项
|
||||
*/
|
||||
const getAttrOptionsByValue = (attr: IActionChangeAttrTarget[], val: any) => {
|
||||
return attr.find(f => f.value == val)?.options
|
||||
}
|
||||
/**
|
||||
* 根据属性类型获取运算符
|
||||
*/
|
||||
const getOperatorList = (attr_type: ILeftAsideConfigItemPublicPropsType | '') => {
|
||||
if (attr_type == 'number') {
|
||||
return [
|
||||
{
|
||||
label: '大于',
|
||||
value: '>'
|
||||
},
|
||||
{
|
||||
label: '等于',
|
||||
value: '='
|
||||
},
|
||||
{
|
||||
label: '小于',
|
||||
value: '<'
|
||||
},
|
||||
{
|
||||
label: '不等于',
|
||||
value: '!='
|
||||
}
|
||||
]
|
||||
} else if (attr_type == 'color' || attr_type == 'switch' || attr_type == 'select' || attr_type == 'input') {
|
||||
return [
|
||||
{
|
||||
label: '等于',
|
||||
value: '='
|
||||
},
|
||||
{
|
||||
label: '不等于',
|
||||
value: '!='
|
||||
}
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
const onTriggerIdChange = (event_list_index: number) => {
|
||||
event_list.value[event_list_index].trigger_rule.trigger_attr = undefined
|
||||
event_list.value[event_list_index].trigger_rule.operator = undefined
|
||||
event_list.value[event_list_index].trigger_rule.value = null
|
||||
}
|
||||
const onTriggerAttrChange = (event_list_index: number) => {
|
||||
event_list.value[event_list_index].trigger_rule.operator = undefined
|
||||
event_list.value[event_list_index].trigger_rule.value = null
|
||||
}
|
||||
const onCustomCodeClick = (item: IDoneJsonEventList, index: number) => {
|
||||
dialog_visiable.value = true
|
||||
dialog_code.value = item.custom_code
|
||||
dialog_title.value = `事件${index + 1}自定义代码编写`
|
||||
select_event_index.value = index
|
||||
}
|
||||
const onDialogClose = () => {
|
||||
event_list.value[select_event_index.value].custom_code = dialog_code.value
|
||||
dialog_visiable.value = false
|
||||
}
|
||||
watch(event_list.value, val => {
|
||||
selectItemEventSettingEmits('update:itemEvents', val)
|
||||
})
|
||||
|
||||
const onJumpToChange = async (value: string) => {}
|
||||
</script>
|
||||
@@ -0,0 +1,26 @@
|
||||
<!-- 期望值组件 -->
|
||||
<template>
|
||||
<el-input-number v-if="selectItemEventSettingProps.formType == 'number'"></el-input-number>
|
||||
<el-color-picker v-else-if="selectItemEventSettingProps.formType == 'color'"></el-color-picker>
|
||||
<el-switch v-else-if="selectItemEventSettingProps.formType == 'switch'"></el-switch>
|
||||
<el-select v-else-if="selectItemEventSettingProps.formType == 'select'">
|
||||
<el-option v-for="item in select_options" :key="item.value" :label="item.label" :value="item.value"></el-option>
|
||||
</el-select>
|
||||
<el-input v-else></el-input>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { ILeftAsideConfigItemPublicPropsType } from '@/components/mt-edit/store/types'
|
||||
import { ElInputNumber, ElInput, ElColorPicker, ElSwitch, ElSelect, ElOption } from 'element-plus'
|
||||
import { computed } from 'vue'
|
||||
type InputTargetValue = {
|
||||
formType: ILeftAsideConfigItemPublicPropsType | ''
|
||||
options?: {
|
||||
label: string
|
||||
value: any
|
||||
}[]
|
||||
}
|
||||
const selectItemEventSettingProps = withDefaults(defineProps<InputTargetValue>(), {})
|
||||
const select_options = computed(() => {
|
||||
return selectItemEventSettingProps.options ?? []
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div v-for="(attr_item, key) in selectItemPropsSettingProps.propsInfo" :key="key">
|
||||
<el-form-item v-if="!attr_item.disabled" :label="attr_item.title" size="small">
|
||||
<el-select
|
||||
v-if="attr_item.type === 'select' && !attr_item.disabled"
|
||||
v-model="attr_item.val"
|
||||
placeholder="Select"
|
||||
size="small"
|
||||
:disabled="attr_item?.disabled"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in attr_item.options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input-number
|
||||
v-else-if="attr_item.type === 'number' && !attr_item.disabled"
|
||||
v-model="attr_item.val"
|
||||
:disabled="attr_item?.disabled"
|
||||
></el-input-number>
|
||||
<el-input
|
||||
v-else-if="attr_item.type === 'input' && !attr_item.disabled"
|
||||
v-model="attr_item.val"
|
||||
:disabled="attr_item?.disabled || flag"
|
||||
></el-input>
|
||||
<el-input
|
||||
v-else-if="attr_item.type === 'textArea' && !attr_item.disabled"
|
||||
v-model="attr_item.val"
|
||||
:disabled="attr_item?.disabled"
|
||||
type="textarea"
|
||||
></el-input>
|
||||
<el-color-picker
|
||||
v-else-if="attr_item.type === 'color' && !attr_item.disabled"
|
||||
v-model="attr_item.val"
|
||||
:disabled="attr_item?.disabled"
|
||||
></el-color-picker>
|
||||
<el-switch
|
||||
v-else-if="attr_item.type === 'switch' && !attr_item.disabled"
|
||||
v-model="attr_item.val"
|
||||
:disabled="attr_item?.disabled"
|
||||
></el-switch>
|
||||
<div v-else-if="attr_item.type === 'jsonEdit' && !attr_item.disabled">
|
||||
<json-edit v-model:contentObj="attr_item.val"></json-edit>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { ILeftAsideConfigItemPublicProps } from '@/components/mt-edit/store/types'
|
||||
import {
|
||||
ElTabs,
|
||||
ElTabPane,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElOptionGroup,
|
||||
ElSwitch,
|
||||
ElText,
|
||||
ElColorPicker,
|
||||
ElUpload,
|
||||
ElIcon,
|
||||
type UploadUserFile,
|
||||
ElButton,
|
||||
type UploadFile,
|
||||
ElMessage
|
||||
} from 'element-plus'
|
||||
import JsonEdit from './json-edit.vue'
|
||||
import type { IDoneJson } from '@/components/mt-edit/store/types'
|
||||
import { watch, ref } from 'vue'
|
||||
type SelectItemPropsSettingProps = {
|
||||
propsInfo: ILeftAsideConfigItemPublicProps | undefined
|
||||
itemJson: IDoneJson | undefined
|
||||
}
|
||||
const selectItemPropsSettingProps = withDefaults(defineProps<SelectItemPropsSettingProps>(), {})
|
||||
|
||||
const flag = ref(false)
|
||||
|
||||
// 监测点和指标的时候输入框禁用
|
||||
watch(
|
||||
() => selectItemPropsSettingProps.itemJson?.keyId,
|
||||
(newVal, oldVal) => {
|
||||
if (newVal == 'bind-dot' || newVal == 'bind-index') {
|
||||
flag.value = true
|
||||
} else {
|
||||
flag.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<el-tabs v-model="activeName" v-if="selectItemSettingProps.itemJson" class="select-none">
|
||||
<el-tab-pane label="配置" name="config">
|
||||
<el-collapse v-model="activeNames" accordion>
|
||||
<el-collapse-item title="边界和属性" name="1">
|
||||
<el-form label-width="60px" label-position="left">
|
||||
<el-form-item label="标题" size="small">
|
||||
<el-input size="small" v-model="item_title"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="x轴坐标" size="small">
|
||||
<el-input-number
|
||||
size="small"
|
||||
v-model="item_binfo_left"
|
||||
@change="emits('addHistory')"
|
||||
></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="y轴坐标" size="small">
|
||||
<el-input-number
|
||||
size="small"
|
||||
v-model="item_binfo_top"
|
||||
@change="emits('addHistory')"
|
||||
></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="宽度" size="small" v-if="!is_line">
|
||||
<el-input-number
|
||||
size="small"
|
||||
v-model="item_binfo_width"
|
||||
@change="emits('addHistory')"
|
||||
></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="高度" size="small" v-if="!is_line">
|
||||
<el-input-number
|
||||
size="small"
|
||||
v-model="item_binfo_height"
|
||||
@change="emits('addHistory')"
|
||||
></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="旋转角度" size="small" v-if="!is_line">
|
||||
<el-input-number
|
||||
size="small"
|
||||
v-model="item_binfo_angle"
|
||||
@change="emits('addHistory')"
|
||||
></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="隐藏" size="small">
|
||||
<el-switch size="small" v-model="item_hide" @change="emits('addHistory')"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="锁定" size="small">
|
||||
<el-switch size="small" v-model="item_lock" @change="emits('addHistory')"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!item_lock && !is_line" label="可缩放" size="small">
|
||||
<el-switch size="small" v-model="item_resize"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="item_resize && !item_lock && !is_line" label="等比缩放" size="small">
|
||||
<el-switch size="small" v-model="item_use_proportional_scaling"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!item_lock && !is_line" label="可旋转" size="small">
|
||||
<el-switch size="small" v-model="item_rotate"></el-switch>
|
||||
</el-form-item>
|
||||
<select-item-props-setting
|
||||
v-model:propsInfo="item_props"
|
||||
:item-json="selectItemSettingProps.itemJson"
|
||||
></select-item-props-setting>
|
||||
</el-form>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="动画配置" name="2">
|
||||
<select-item-animate-setting
|
||||
v-model:common-animates="item_common_animations"
|
||||
></select-item-animate-setting>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="事件" name="event">
|
||||
<select-item-event-setting
|
||||
:done-json="selectItemSettingProps.doneJson"
|
||||
v-model:item-events="item_events"
|
||||
></select-item-event-setting>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="绑定" name="bind_device">
|
||||
<!-- <slot v-if="hasDeviceBindSlot" name="deviceBind" :item="selectItemSettingProps.itemJson" />
|
||||
<el-empty v-else description="请传递插槽进行设备绑定页面显示" /> -->
|
||||
<select-item-bind-setting :item-json="selectItemSettingProps.itemJson"></select-item-bind-setting>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, useSlots, watch } from 'vue'
|
||||
import {
|
||||
ElTabs,
|
||||
ElTabPane,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElEmpty,
|
||||
ElOption,
|
||||
ElOptionGroup,
|
||||
ElSwitch,
|
||||
ElText,
|
||||
ElColorPicker,
|
||||
ElUpload,
|
||||
ElIcon,
|
||||
type UploadUserFile,
|
||||
ElButton,
|
||||
type UploadFile,
|
||||
ElMessage,
|
||||
ElCollapse,
|
||||
ElCollapseItem
|
||||
} from 'element-plus'
|
||||
import type { IDoneJson } from '@/components/mt-edit/store/types'
|
||||
import SelectItemPropsSetting from './select-item-props-setting.vue'
|
||||
import SelectItemAnimateSetting from './select-item-animate-setting/index.vue'
|
||||
import SelectItemEventSetting from './select-item-event-setting/index.vue'
|
||||
import SelectItemBindSetting from './select-item-bind-setting/index.vue'
|
||||
import { bingStore } from '@/components/mt-edit/store/bind'
|
||||
const activeName = ref('config')
|
||||
const activeNames = ref(['1'])
|
||||
type SelectItemSettingProps = {
|
||||
itemJson: IDoneJson | undefined
|
||||
doneJson: IDoneJson[]
|
||||
}
|
||||
const selectItemSettingProps = withDefaults(defineProps<SelectItemSettingProps>(), {})
|
||||
const emits = defineEmits(['update:itemJson', 'addHistory'])
|
||||
const slots = useSlots()
|
||||
// 自由连线 直角连线都有自定义宽高以及禁止缩放和旋转
|
||||
const is_line = computed(() => selectItemSettingProps.itemJson?.type === 'sys-line')
|
||||
|
||||
const item_title = computed({
|
||||
get: () => {
|
||||
return selectItemSettingProps.itemJson?.title
|
||||
},
|
||||
set: value => {
|
||||
emits('update:itemJson', {
|
||||
...selectItemSettingProps.itemJson,
|
||||
title: value
|
||||
})
|
||||
}
|
||||
})
|
||||
const item_binfo_left = computed({
|
||||
get: () => {
|
||||
return selectItemSettingProps.itemJson?.binfo.left
|
||||
},
|
||||
set: value => {
|
||||
emits('update:itemJson', {
|
||||
...selectItemSettingProps.itemJson,
|
||||
binfo: {
|
||||
...selectItemSettingProps.itemJson?.binfo,
|
||||
left: value
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
const item_binfo_top = computed({
|
||||
get: () => {
|
||||
return selectItemSettingProps.itemJson?.binfo.top
|
||||
},
|
||||
set: value => {
|
||||
emits('update:itemJson', {
|
||||
...selectItemSettingProps.itemJson,
|
||||
binfo: {
|
||||
...selectItemSettingProps.itemJson?.binfo,
|
||||
top: value
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
const item_binfo_width = computed({
|
||||
get: () => {
|
||||
return selectItemSettingProps.itemJson?.binfo.width
|
||||
},
|
||||
set: value => {
|
||||
emits('update:itemJson', {
|
||||
...selectItemSettingProps.itemJson,
|
||||
binfo: {
|
||||
...selectItemSettingProps.itemJson?.binfo,
|
||||
width: value
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
const item_binfo_height = computed({
|
||||
get: () => {
|
||||
return selectItemSettingProps.itemJson?.binfo.height
|
||||
},
|
||||
set: value => {
|
||||
emits('update:itemJson', {
|
||||
...selectItemSettingProps.itemJson,
|
||||
binfo: {
|
||||
...selectItemSettingProps.itemJson?.binfo,
|
||||
height: value
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
const item_binfo_angle = computed({
|
||||
get: () => {
|
||||
return selectItemSettingProps.itemJson?.binfo.angle
|
||||
},
|
||||
set: value => {
|
||||
emits('update:itemJson', {
|
||||
...selectItemSettingProps.itemJson,
|
||||
binfo: {
|
||||
...selectItemSettingProps.itemJson?.binfo,
|
||||
angle: value
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
const item_hide = computed({
|
||||
get: () => {
|
||||
return selectItemSettingProps.itemJson?.hide
|
||||
},
|
||||
set: value => {
|
||||
emits('update:itemJson', {
|
||||
...selectItemSettingProps.itemJson,
|
||||
hide: value
|
||||
})
|
||||
}
|
||||
})
|
||||
const item_lock = computed({
|
||||
get: () => {
|
||||
return selectItemSettingProps.itemJson?.lock
|
||||
},
|
||||
set: value => {
|
||||
emits('update:itemJson', {
|
||||
...selectItemSettingProps.itemJson,
|
||||
lock: value
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const item_resize = computed({
|
||||
get: () => {
|
||||
return selectItemSettingProps.itemJson?.resize
|
||||
},
|
||||
set: value => {
|
||||
emits('update:itemJson', {
|
||||
...selectItemSettingProps.itemJson,
|
||||
resize: value
|
||||
})
|
||||
}
|
||||
})
|
||||
const item_use_proportional_scaling = computed({
|
||||
get: () => {
|
||||
return selectItemSettingProps.itemJson?.use_proportional_scaling
|
||||
},
|
||||
set: value => {
|
||||
emits('update:itemJson', {
|
||||
...selectItemSettingProps.itemJson,
|
||||
use_proportional_scaling: value
|
||||
})
|
||||
}
|
||||
})
|
||||
const item_rotate = computed({
|
||||
get: () => {
|
||||
return selectItemSettingProps.itemJson?.rotate
|
||||
},
|
||||
set: value => {
|
||||
emits('update:itemJson', {
|
||||
...selectItemSettingProps.itemJson,
|
||||
rotate: value
|
||||
})
|
||||
}
|
||||
})
|
||||
const item_props = computed({
|
||||
get: () => {
|
||||
return selectItemSettingProps.itemJson?.props
|
||||
},
|
||||
set: value => {
|
||||
emits('update:itemJson', {
|
||||
...selectItemSettingProps.itemJson,
|
||||
props: value
|
||||
})
|
||||
}
|
||||
})
|
||||
const item_common_animations = computed({
|
||||
get: () => {
|
||||
return selectItemSettingProps.itemJson?.common_animations
|
||||
},
|
||||
set: value => {
|
||||
emits('update:itemJson', {
|
||||
...selectItemSettingProps.itemJson,
|
||||
common_animations: value
|
||||
})
|
||||
}
|
||||
})
|
||||
const item_events = computed({
|
||||
get: () => {
|
||||
return selectItemSettingProps.itemJson?.events
|
||||
},
|
||||
set: value => {
|
||||
emits('update:itemJson', {
|
||||
...selectItemSettingProps.itemJson,
|
||||
events: value
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const item_binds = computed({
|
||||
get: () => {
|
||||
return selectItemSettingProps.itemJson?.bind
|
||||
},
|
||||
set: value => {
|
||||
emits('update:itemJson', {
|
||||
...selectItemSettingProps.itemJson,
|
||||
bind: value
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const hasDeviceBindSlot = computed(() => {
|
||||
return !!slots.deviceBind
|
||||
})
|
||||
</script>
|
||||
21
src/components/mt-edit/components/layout/tabs/index.vue
Normal file
21
src/components/mt-edit/components/layout/tabs/index.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div style="width: 100%">
|
||||
<el-tabs v-model="activeName" class="demo-tabs">
|
||||
<el-tab-pane label="图纸" name="1">
|
||||
<left-aside-list></left-aside-list>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="基本图元" name="2">
|
||||
<left-aside :leftAsideConfig="leftAsideStore.config"></left-aside>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import LeftAside from '@/components/mt-edit/components/layout/left-aside/index.vue'
|
||||
import { ElTabs, ElTabPane } from 'element-plus'
|
||||
import { leftAsideStore } from '@/components/mt-edit/store/left-aside'
|
||||
import LeftAsideList from '@/components/mt-edit/components/layout/left-aside-list/index.vue'
|
||||
|
||||
const activeName = ref('1')
|
||||
</script>
|
||||
524
src/components/mt-edit/components/line-render/index.vue
Normal file
524
src/components/mt-edit/components/line-render/index.vue
Normal file
@@ -0,0 +1,524 @@
|
||||
<template>
|
||||
<svg
|
||||
class="mt-line-render"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: `${-lineRenderProps.itemJson.binfo.left - offset}px`,
|
||||
top: `${-lineRenderProps.itemJson.binfo.top - offset}px`,
|
||||
width: `${lineRenderProps.canvasCfg.width + offset}px`,
|
||||
height: `${lineRenderProps.canvasCfg.height + offset}px`
|
||||
}"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
pointer-events="none"
|
||||
>
|
||||
<g>
|
||||
<defs>
|
||||
<marker
|
||||
:id="'markerArrowStart' + lineRenderProps.itemJson.id"
|
||||
viewBox="0 0 10 10"
|
||||
refX="8"
|
||||
refY="5"
|
||||
markerWidth="6"
|
||||
markerHeight="6"
|
||||
orient="auto-start-reverse"
|
||||
>
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" :fill="lineRenderProps.itemJson.props.stroke.val" />
|
||||
</marker>
|
||||
<marker
|
||||
:id="'markerArrowEnd' + lineRenderProps.itemJson.id"
|
||||
viewBox="0 0 10 10"
|
||||
refX="8"
|
||||
refY="5"
|
||||
markerWidth="6"
|
||||
markerHeight="6"
|
||||
orient="auto"
|
||||
>
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" :fill="lineRenderProps.itemJson.props.stroke.val" />
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<path
|
||||
:d="
|
||||
positionArrarToPath(
|
||||
lineRenderProps.itemJson.props.point_position.val,
|
||||
lineRenderProps.itemJson.binfo.left + offset,
|
||||
lineRenderProps.itemJson.binfo.top + offset
|
||||
)
|
||||
"
|
||||
pointer-events="visibleStroke"
|
||||
fill="none"
|
||||
:stroke="
|
||||
lineRenderProps.itemJson.props.ani_type.val === 'electricity'
|
||||
? lineRenderProps.itemJson.props.ani_color.val
|
||||
: lineRenderProps.itemJson.props.stroke.val
|
||||
"
|
||||
:stroke-width="lineRenderProps.itemJson.props['stroke-width'].val"
|
||||
style="cursor: move"
|
||||
stroke-dashoffset="0"
|
||||
:stroke-dasharray="
|
||||
lineRenderProps.itemJson.props.ani_type.val === 'electricity'
|
||||
? lineRenderProps.itemJson.props['stroke-width'].val * 3
|
||||
: 0
|
||||
"
|
||||
:marker-start="
|
||||
lineRenderProps.itemJson.props?.['marker-start']?.val
|
||||
? `url(#markerArrowStart${lineRenderProps.itemJson.id})`
|
||||
: ''
|
||||
"
|
||||
:marker-end="
|
||||
lineRenderProps.itemJson.props?.['marker-end']?.val
|
||||
? `url(#markerArrowEnd${lineRenderProps.itemJson.id})`
|
||||
: ''
|
||||
"
|
||||
class="real"
|
||||
>
|
||||
<animate
|
||||
v-if="lineRenderProps.itemJson.props.ani_type.val === 'electricity'"
|
||||
attributeName="stroke-dashoffset"
|
||||
:from="lineRenderProps.itemJson.props.ani_reverse.val ? 0 : 1000"
|
||||
:to="
|
||||
lineRenderProps.itemJson.props.ani_reverse.val
|
||||
? lineRenderProps.itemJson.props.ani_play.val
|
||||
? 1000
|
||||
: 0
|
||||
: lineRenderProps.itemJson.props.ani_play.val
|
||||
? 0
|
||||
: 1000
|
||||
"
|
||||
:dur="`${
|
||||
lineRenderProps.itemJson.props.ani_dur.val < 1 ? 1 : lineRenderProps.itemJson.props.ani_dur.val
|
||||
}s`"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</path>
|
||||
<!-- 电流状态不太好选中,所以放个透明的放下面 -->
|
||||
<path
|
||||
:d="
|
||||
positionArrarToPath(
|
||||
lineRenderProps.itemJson.props.point_position.val,
|
||||
lineRenderProps.itemJson.binfo.left + offset,
|
||||
lineRenderProps.itemJson.binfo.top + offset
|
||||
)
|
||||
"
|
||||
pointer-events="visibleStroke"
|
||||
fill="none"
|
||||
stroke="transparent"
|
||||
:stroke-width="lineRenderProps.itemJson.props['stroke-width'].val"
|
||||
style="cursor: move"
|
||||
stroke-dashoffset="0"
|
||||
:marker-start="
|
||||
lineRenderProps.itemJson.props?.['marker-start']?.val
|
||||
? `url(#markerArrowStart${lineRenderProps.itemJson.id})`
|
||||
: ''
|
||||
"
|
||||
:marker-end="
|
||||
lineRenderProps.itemJson.props?.['marker-end']?.val
|
||||
? `url(#markerArrowEnd${lineRenderProps.itemJson.id})`
|
||||
: ''
|
||||
"
|
||||
></path>
|
||||
<!-- 水珠 -->
|
||||
<path
|
||||
v-if="lineRenderProps.itemJson.props.ani_type.val === 'waterdrop'"
|
||||
:d="
|
||||
positionArrarToPath(
|
||||
lineRenderProps.itemJson.props.point_position.val,
|
||||
lineRenderProps.itemJson.binfo.left + offset,
|
||||
lineRenderProps.itemJson.binfo.top + offset
|
||||
)
|
||||
"
|
||||
fill="none"
|
||||
fill-opacity="0"
|
||||
:stroke="lineRenderProps.itemJson.props.ani_color.val"
|
||||
:stroke-width="lineRenderProps.itemJson.props['stroke-width'].val"
|
||||
:stroke-dasharray="lineRenderProps.itemJson.props['stroke-width'].val * 3"
|
||||
stroke-dashoffset="0"
|
||||
stroke-linecap="round"
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
:from="lineRenderProps.itemJson.props.ani_reverse.val ? 0 : 1000"
|
||||
:to="
|
||||
lineRenderProps.itemJson.props.ani_reverse.val
|
||||
? lineRenderProps.itemJson.props.ani_play.val
|
||||
? 1000
|
||||
: 0
|
||||
: lineRenderProps.itemJson.props.ani_play.val
|
||||
? 0
|
||||
: 1000
|
||||
"
|
||||
:dur="`${
|
||||
lineRenderProps.itemJson.props.ani_dur.val < 1 ? 1 : lineRenderProps.itemJson.props.ani_dur.val
|
||||
}s`"
|
||||
repeatCount="indefinite"
|
||||
fill="freeze"
|
||||
/>
|
||||
</path>
|
||||
<!-- 轨迹 -->
|
||||
<circle
|
||||
v-else-if="lineRenderProps.itemJson.props.ani_type.val === 'track'"
|
||||
cx="0"
|
||||
cy="0"
|
||||
:r="lineRenderProps.itemJson.props['stroke-width'].val * 2"
|
||||
:fill="lineRenderProps.itemJson.props.ani_color.val"
|
||||
>
|
||||
<animateMotion
|
||||
:path="
|
||||
positionArrarToPath(
|
||||
lineRenderProps.itemJson.props.point_position.val,
|
||||
lineRenderProps.itemJson.binfo.left + offset,
|
||||
lineRenderProps.itemJson.binfo.top + offset
|
||||
)
|
||||
"
|
||||
:dur="`${
|
||||
lineRenderProps.itemJson.props.ani_dur.val < 1 ? 1 : lineRenderProps.itemJson.props.ani_dur.val
|
||||
}s`"
|
||||
repeatCount="indefinite"
|
||||
></animateMotion>
|
||||
</circle>
|
||||
<g v-if="lineRenderProps.itemJson.active">
|
||||
<circle
|
||||
v-for="(item, index) in addPointPosition"
|
||||
:key="index"
|
||||
pointer-events="fill"
|
||||
:cx="item.x + lineRenderProps.itemJson.binfo.left + offset"
|
||||
:cy="item.y + lineRenderProps.itemJson.binfo.top + offset"
|
||||
r="4"
|
||||
stroke-width="2"
|
||||
:stroke="lineRenderProps.itemJson.props.stroke.val"
|
||||
fill="transparent"
|
||||
class="cursor-crosshair opacity-30"
|
||||
@mousedown="onMouseDown($event, index, 'add', item)"
|
||||
@touchstart.passive="onMouseDown($event, index, 'add', item)"
|
||||
/>
|
||||
</g>
|
||||
<g v-if="lineRenderProps.itemJson.active">
|
||||
<circle
|
||||
v-for="(item, index) in lineRenderProps.itemJson.props.point_position.val"
|
||||
:key="index"
|
||||
pointer-events="fill"
|
||||
:id="`point-${lineRenderProps.itemJson.id}-${index}`"
|
||||
:cx="item.x + lineRenderProps.itemJson.binfo.left + offset"
|
||||
:cy="item.y + lineRenderProps.itemJson.binfo.top + offset"
|
||||
r="4"
|
||||
stroke-width="1"
|
||||
:stroke="lineRenderProps.itemJson.props.stroke.val"
|
||||
fill="#fff"
|
||||
:class="
|
||||
lineRenderProps.mode === 'line-edit' &&
|
||||
index !== 0 &&
|
||||
index !== lineRenderProps.itemJson.props.point_position.val.length - 1
|
||||
? 'cursor-remove'
|
||||
: 'cursor-pointer'
|
||||
"
|
||||
@mousedown="onMouseDown($event, index, 'edit', item)"
|
||||
@touchstart.passive="onMouseDown($event, index, 'edit', item)"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { MouseTouchEvent } from '@/components/mt-dzr/utils/types'
|
||||
import type { IDoneJson, IGlobalStoreCanvasCfg, IGlobalStoreGridCfg } from '../../store/types'
|
||||
import {
|
||||
alignToGrid,
|
||||
getCenterXY,
|
||||
getRealityXY,
|
||||
getRectCenterCoordinate,
|
||||
getRectCoordinate,
|
||||
positionArrarToPath,
|
||||
rotatePoint
|
||||
} from '@/components/mt-edit/utils'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { configStore } from '../../store/config'
|
||||
type LineRenderProps = {
|
||||
itemJson: IDoneJson
|
||||
canvasCfg: IGlobalStoreCanvasCfg
|
||||
grid: IGlobalStoreGridCfg
|
||||
canvasDom: HTMLElement | null
|
||||
doneJson: IDoneJson[]
|
||||
lockState: boolean
|
||||
mode: 'normal' | 'line-edit'
|
||||
}
|
||||
const lineRenderProps = withDefaults(defineProps<LineRenderProps>(), {
|
||||
mode: 'normal'
|
||||
})
|
||||
const lineRenderEmits = defineEmits(['update:itemJson', 'setIntention', 'lineMouseUp'])
|
||||
const offset = configStore.lineRenderOffset
|
||||
//如果网格关闭或者没有开启网格对齐,网格大小为1
|
||||
const grid_align_size = computed(() =>
|
||||
!lineRenderProps.grid.align || !lineRenderProps.grid.enabled ? 1 : lineRenderProps.grid.size
|
||||
)
|
||||
const addPointPosition = ref()
|
||||
const onMouseDown = (
|
||||
de: MouseTouchEvent,
|
||||
point_index: number,
|
||||
type: 'add' | 'edit',
|
||||
item: { x: number; y: number }
|
||||
) => {
|
||||
if (lineRenderProps.lockState) {
|
||||
return
|
||||
}
|
||||
|
||||
de.stopPropagation()
|
||||
// 如果是编辑模式 并且不是第一个点或者不是最后一个点
|
||||
if (
|
||||
lineRenderProps.mode === 'line-edit' &&
|
||||
type == 'edit' &&
|
||||
point_index !== 0 &&
|
||||
point_index !== lineRenderProps.itemJson.props.point_position.val.length - 1
|
||||
) {
|
||||
//删除该点
|
||||
const new_point_position = lineRenderProps.itemJson.props.point_position.val
|
||||
new_point_position.splice(point_index, 1)
|
||||
lineRenderEmits('update:itemJson', {
|
||||
...lineRenderProps.itemJson,
|
||||
props: {
|
||||
...lineRenderProps.itemJson.props,
|
||||
point_position: {
|
||||
...lineRenderProps.itemJson.props.point_position,
|
||||
val: new_point_position
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
// 记录鼠标按下时实际点的坐标
|
||||
const { x: realityX, y: realityY } = item
|
||||
// 记录最开始点击时鼠标位置
|
||||
const d_x = de instanceof MouseEvent ? de.clientX : de.touches[0].pageX
|
||||
const d_y = de instanceof MouseEvent ? de.clientY : de.touches[0].pageY
|
||||
let new_x = 0
|
||||
let new_y = 0
|
||||
let first_click = true
|
||||
const onMouseMove = (e: MouseTouchEvent) => {
|
||||
// 记录鼠标移动的位置
|
||||
const m_x = e instanceof MouseEvent ? e.clientX : e.touches[0].pageX
|
||||
const m_y = e instanceof MouseEvent ? e.clientY : e.touches[0].pageY
|
||||
// 移动的距离
|
||||
const move_x = de.ctrlKey ? 0 : alignToGrid((m_x - d_x) / lineRenderProps.canvasCfg.scale, 1) //感觉对齐网格有点体验不好 所以固定为一了
|
||||
const move_y = de.shiftKey ? 0 : alignToGrid((m_y - d_y) / lineRenderProps.canvasCfg.scale, 1)
|
||||
new_x = realityX + move_x
|
||||
new_y = realityY + move_y
|
||||
if (type === 'add') {
|
||||
item.x = new_x
|
||||
item.y = new_y
|
||||
if (first_click) {
|
||||
const new_point_position = lineRenderProps.itemJson.props.point_position.val
|
||||
new_point_position.splice(point_index + 1, 0, {
|
||||
x: new_x,
|
||||
y: new_y
|
||||
})
|
||||
lineRenderEmits('update:itemJson', {
|
||||
...lineRenderProps.itemJson,
|
||||
props: {
|
||||
...lineRenderProps.itemJson.props,
|
||||
point_position: {
|
||||
...lineRenderProps.itemJson.props.point_position,
|
||||
val: new_point_position
|
||||
}
|
||||
}
|
||||
})
|
||||
first_click = false
|
||||
} else {
|
||||
const new_point_position = lineRenderProps.itemJson.props.point_position.val
|
||||
new_point_position[point_index + 1] = {
|
||||
x: new_x,
|
||||
y: new_y
|
||||
}
|
||||
lineRenderEmits('update:itemJson', {
|
||||
...lineRenderProps.itemJson,
|
||||
props: {
|
||||
...lineRenderProps.itemJson.props,
|
||||
point_position: {
|
||||
...lineRenderProps.itemJson.props.point_position,
|
||||
val: new_point_position
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
if (type === 'edit') {
|
||||
const new_point_position = lineRenderProps.itemJson.props.point_position.val
|
||||
if (point_index === 0) {
|
||||
lineRenderEmits('setIntention', 'adsorbStart')
|
||||
if (lineRenderProps.mode === 'line-edit') {
|
||||
if (first_click) {
|
||||
const new_point_position = lineRenderProps.itemJson.props.point_position.val
|
||||
new_point_position.unshift({
|
||||
x: new_x,
|
||||
y: new_y
|
||||
})
|
||||
lineRenderEmits('update:itemJson', {
|
||||
...lineRenderProps.itemJson,
|
||||
props: {
|
||||
...lineRenderProps.itemJson.props,
|
||||
point_position: {
|
||||
...lineRenderProps.itemJson.props.point_position,
|
||||
val: new_point_position
|
||||
}
|
||||
}
|
||||
})
|
||||
first_click = false
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if (point_index === new_point_position.length - 1) {
|
||||
lineRenderEmits('setIntention', 'adsorbEnd')
|
||||
if (lineRenderProps.mode === 'line-edit') {
|
||||
if (first_click) {
|
||||
const new_point_position = lineRenderProps.itemJson.props.point_position.val
|
||||
new_point_position.push({
|
||||
x: new_x,
|
||||
y: new_y
|
||||
})
|
||||
lineRenderEmits('update:itemJson', {
|
||||
...lineRenderProps.itemJson,
|
||||
props: {
|
||||
...lineRenderProps.itemJson.props,
|
||||
point_position: {
|
||||
...lineRenderProps.itemJson.props.point_position,
|
||||
val: new_point_position
|
||||
}
|
||||
}
|
||||
})
|
||||
point_index += 1
|
||||
first_click = false
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
new_point_position[point_index].x = new_x
|
||||
new_point_position[point_index].y = new_y
|
||||
lineRenderEmits('update:itemJson', {
|
||||
...lineRenderProps.itemJson,
|
||||
props: {
|
||||
...lineRenderProps.itemJson.props,
|
||||
point_position: {
|
||||
...lineRenderProps.itemJson.props.point_position,
|
||||
val: new_point_position
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseUp = () => {
|
||||
document.removeEventListener('mousemove', onMouseMove)
|
||||
document.removeEventListener('mouseup', onMouseUp)
|
||||
document.removeEventListener('touchmove', onMouseMove)
|
||||
document.removeEventListener('touchend', onMouseUp)
|
||||
const itemRect = document.querySelector(`#${lineRenderProps.itemJson.id} g .real`)!.getBoundingClientRect()
|
||||
const canvas_area_bounding_info = lineRenderProps.canvasDom!.getBoundingClientRect()
|
||||
const new_left = (itemRect?.left - canvas_area_bounding_info?.left) / lineRenderProps.canvasCfg.scale
|
||||
const new_top = (itemRect?.top - canvas_area_bounding_info?.top) / lineRenderProps.canvasCfg.scale
|
||||
const move_x = new_left - lineRenderProps.itemJson.binfo.left
|
||||
const move_y = new_top - lineRenderProps.itemJson.binfo.top
|
||||
lineRenderEmits('setIntention', 'none')
|
||||
lineRenderEmits('update:itemJson', {
|
||||
...lineRenderProps.itemJson,
|
||||
binfo: {
|
||||
...lineRenderProps.itemJson.binfo,
|
||||
left: new_left,
|
||||
top: new_top,
|
||||
width: itemRect?.width / lineRenderProps.canvasCfg.scale,
|
||||
height: itemRect?.height / lineRenderProps.canvasCfg.scale
|
||||
},
|
||||
props: {
|
||||
...lineRenderProps.itemJson.props,
|
||||
point_position: {
|
||||
...lineRenderProps.itemJson.props.point_position,
|
||||
val: lineRenderProps.itemJson.props.point_position.val.map((m: { x: number; y: number }) => {
|
||||
return {
|
||||
x: m.x - move_x,
|
||||
y: m.y - move_y
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
document.addEventListener('mousemove', onMouseMove)
|
||||
document.addEventListener('mouseup', onMouseUp)
|
||||
document.addEventListener('touchmove', onMouseMove)
|
||||
document.addEventListener('touchend', onMouseUp)
|
||||
lineRenderEmits('lineMouseUp')
|
||||
}
|
||||
const getCenterPositions = (point_position: { x: number; y: number }[]) => {
|
||||
const res: { x: number; y: number }[] = []
|
||||
//根据当前点坐标计算每两点的中点坐标
|
||||
point_position.forEach((item, index) => {
|
||||
if (index === point_position.length - 1) {
|
||||
return
|
||||
}
|
||||
const center_xy = getCenterXY(item.x, item.y, point_position[index + 1].x, point_position[index + 1].y)
|
||||
res.push(center_xy)
|
||||
})
|
||||
return res
|
||||
}
|
||||
watch(
|
||||
() => lineRenderProps.itemJson.props.point_position.val,
|
||||
(new_val: { x: number; y: number }[]) => {
|
||||
addPointPosition.value = getCenterPositions(new_val)
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
// watch(
|
||||
// () => lineRenderProps.doneJson,
|
||||
// (new_val) => {
|
||||
// const temp_item_json = lineRenderProps.itemJson;
|
||||
// // 处理起点绑定
|
||||
// if (lineRenderProps.itemJson.props.bind_anchors.val.start) {
|
||||
// // 根据id和类型找到锚点坐标
|
||||
// const find_item = new_val.find(
|
||||
// (m) => m.id === lineRenderProps.itemJson.props.bind_anchors.val.start.id
|
||||
// );
|
||||
// if (find_item) {
|
||||
// // 四个角原始坐标
|
||||
// const { topLeft, topRight, bottomLeft, bottomRight } = getRectCoordinate(find_item);
|
||||
// // 四条边中点坐标
|
||||
// const { topCenter, bottomCenter, leftCenter, rightCenter } = getRectCenterCoordinate(
|
||||
// topLeft,
|
||||
// topRight,
|
||||
// bottomLeft,
|
||||
// bottomRight
|
||||
// );
|
||||
// // 旋转中心
|
||||
// const centerX = topCenter.x;
|
||||
// const centerY = leftCenter.y;
|
||||
|
||||
// // 旋转角度(弧度)
|
||||
// const angleRad = (Math.PI / 180) * find_item.binfo.angle;
|
||||
// if (lineRenderProps.itemJson.props.bind_anchors.val.start.type === 'tc') {
|
||||
// const new_tc = rotatePoint(topCenter.x, topCenter.y, centerX, centerY, angleRad);
|
||||
// temp_item_json.props.point_position.val[0] = {
|
||||
// x: new_tc.x - lineRenderProps.itemJson.binfo.left,
|
||||
// y: new_tc.y - lineRenderProps.itemJson.binfo.top
|
||||
// };
|
||||
// } else if (lineRenderProps.itemJson.props.bind_anchors.val.start.type === 'bc') {
|
||||
// const new_bc = rotatePoint(bottomCenter.x, bottomCenter.y, centerX, centerY, angleRad);
|
||||
// temp_item_json.props.point_position.val[0] = {
|
||||
// x: new_bc.x - lineRenderProps.itemJson.binfo.left,
|
||||
// y: new_bc.y - lineRenderProps.itemJson.binfo.top
|
||||
// };
|
||||
// }
|
||||
// } else {
|
||||
// temp_item_json.props.bind_anchors.val.start = null;
|
||||
// }
|
||||
// lineRenderEmits('update:itemJson', temp_item_json);
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
</script>
|
||||
<style scoped>
|
||||
.cursor-remove {
|
||||
cursor:
|
||||
url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPgoJPGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS1kYXNoYXJyYXk9IjIyIiBzdHJva2UtZGFzaG9mZnNldD0iMjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIyIj4KCQk8cGF0aCBkPSJNMTkgNUw1IDE5Ij48YW5pbWF0ZSBmaWxsPSJmcmVlemUiIGF0dHJpYnV0ZU5hbWU9InN0cm9rZS1kYXNob2Zmc2V0IiBiZWdpbj0iMC4zcyIgZHVyPSIwLjNzIiB2YWx1ZXM9IjIyOzAiLz48L3BhdGg+CgkJPHBhdGggZD0iTTUgNUwxOSAxOSI+PGFuaW1hdGUgZmlsbD0iZnJlZXplIiBhdHRyaWJ1dGVOYW1lPSJzdHJva2UtZGFzaG9mZnNldCIgZHVyPSIwLjNzIiB2YWx1ZXM9IjIyOzAiLz48L3BhdGg+Cgk8L2c+Cjwvc3ZnPg==),
|
||||
auto;
|
||||
}
|
||||
</style>
|
||||
71
src/components/mt-edit/components/pattern-grid/index.vue
Normal file
71
src/components/mt-edit/components/pattern-grid/index.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="grid-rect" :style="rectStyle">
|
||||
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern v-if="showSmall" id="smallGrid" :width="grid" :height="grid" patternUnits="userSpaceOnUse">
|
||||
<path :d="`M ${grid} 0 L 0 0 0 ${grid}`" fill="none" :stroke="color.grid" stroke-width="0.5" />
|
||||
</pattern>
|
||||
<pattern id="grid" :width="bigGrid" :height="bigGrid" patternUnits="userSpaceOnUse">
|
||||
<rect v-if="showSmall" :width="bigGrid" :height="bigGrid" fill="url(#smallGrid)" />
|
||||
<path
|
||||
:d="`M ${bigGrid} 0 L 0 0 0 ${bigGrid}`"
|
||||
fill="none"
|
||||
:stroke="color.bigGrid"
|
||||
stroke-width="1"
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#grid)" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useDark } from '@vueuse/core'
|
||||
const isDark = useDark({
|
||||
selector: '#mt-edit'
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
grid: {
|
||||
// 小网格的大小
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
gridCount: {
|
||||
// 小网格的数量,默认为5个
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
showSmall: {
|
||||
// 是否显示小网格
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
// 计算大网格的大小
|
||||
const bigGrid = computed(() => props.grid * props.gridCount)
|
||||
|
||||
// 处理网站皮肤,可忽略
|
||||
const color = computed(() => {
|
||||
const colors = [
|
||||
['#e4e4e4', '#ebebeb'],
|
||||
['#414141', '#363636']
|
||||
]
|
||||
const [bigGrid, grid] = colors[isDark ? 0 : 1]
|
||||
return { bigGrid, grid }
|
||||
})
|
||||
|
||||
const rectStyle = computed(() => ({ '--border-color': color.value.bigGrid }))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.grid-rect {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--border-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
</style>
|
||||
296
src/components/mt-edit/components/render-core/index.vue
Normal file
296
src/components/mt-edit/components/render-core/index.vue
Normal file
@@ -0,0 +1,296 @@
|
||||
<template>
|
||||
<mt-dzr
|
||||
:id="item.id"
|
||||
v-for="(item, index) in renderCoreProps.doneJson"
|
||||
:key="item.id"
|
||||
v-model="item.binfo"
|
||||
:scale-ratio="renderCoreProps.canvasCfg.scale"
|
||||
:grid="renderCoreProps.gridCfg"
|
||||
:resize="item.resize"
|
||||
:rotate="item.rotate"
|
||||
:lock="renderCoreProps.globalLock ? true : item.lock"
|
||||
:active="renderCoreProps.preivewMode ? false : item.active"
|
||||
:useProportionalScaling="item.use_proportional_scaling"
|
||||
:show-ghost-dom="renderCoreProps.showGhostDom"
|
||||
:hide="item.hide"
|
||||
:disabled="renderCoreProps.preivewMode"
|
||||
:adsorp_diff="globalStore.adsorp_diff"
|
||||
@mousedown="onMouseDown(item, $event)"
|
||||
@update:model-value="onUpdateModelValue(item.id, $event)"
|
||||
@on-item-move="(e: any) => onItemMove(e, item.id)"
|
||||
@move-mouse-up="onMoveMouseUp()"
|
||||
@on-mouse-enter="onMouseEnter($event, item)"
|
||||
@on-mouse-leave="onMouseLeave($event, item)"
|
||||
@on-resize-move="onResizeMove($event)"
|
||||
@on-resize-done="onResizeDone(item)"
|
||||
@on-rotate-move="onRotateMove($event)"
|
||||
@on-rotate-done="onRotateDone(item)"
|
||||
@on-right-click="onRightClick($event, item)"
|
||||
:class="`${item.type == 'sys-line' ? 'pointer-events-none' : ''} ${getCommonAni(item)} cursor-pointer`"
|
||||
>
|
||||
<!-- <el-popover
|
||||
v-if="renderCoreProps.preivewMode && renderCoreProps.showPopover"
|
||||
placement="top-start"
|
||||
title="属性信息"
|
||||
:width="200"
|
||||
trigger="hover"
|
||||
>
|
||||
<template #reference>
|
||||
<render-item
|
||||
:item-json="item"
|
||||
:canvas-cfg="renderCoreProps.canvasCfg"
|
||||
:canvas-dom="renderCoreProps.canvasDom"
|
||||
:grid="renderCoreProps.gridCfg"
|
||||
:done-json="renderCoreProps.doneJson"
|
||||
:lock-state="renderCoreProps.globalLock ? true : item.lock"
|
||||
:line-append-enable="renderCoreProps.lineAppendEnable"
|
||||
@update:item-json="onUpdateItemJson(index, $event)"
|
||||
@set-intention="val => renderCoreEmits('setIntention', val)"
|
||||
@line-mouse-up="onLineMouseUp"
|
||||
v-on="renderCoreProps.preivewMode ? eventToVOn(item, externalMethod) : {}"
|
||||
></render-item>
|
||||
</template>
|
||||
<template #default>
|
||||
<div v-for="(prop_item, prop_item_key) in item.props" :key="prop_item_key">
|
||||
<div v-if="!prop_item.disabled">{{ prop_item.title }}:{{ prop_item.val }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover> -->
|
||||
<render-item
|
||||
:item-json="item"
|
||||
:canvas-cfg="renderCoreProps.canvasCfg"
|
||||
:canvas-dom="renderCoreProps.canvasDom"
|
||||
:grid="renderCoreProps.gridCfg"
|
||||
:done-json="renderCoreProps.doneJson"
|
||||
:lock-state="renderCoreProps.globalLock ? true : item.lock"
|
||||
:line-append-enable="renderCoreProps.lineAppendEnable"
|
||||
@update:item-json="onUpdateItemJson(index, $event)"
|
||||
@set-intention="val => renderCoreEmits('setIntention', val)"
|
||||
@line-mouse-up="onLineMouseUp"
|
||||
v-on="renderCoreProps.preivewMode ? eventToVOn(item, externalMethod) : {}"
|
||||
@click="() => renderCoreProps.onElementClick && item.lineId && renderCoreProps.onElementClick(item.lineId)"
|
||||
></render-item>
|
||||
</mt-dzr>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { nextTick, reactive, ref } from 'vue'
|
||||
import MtDzr from '@/components/mt-dzr/index.vue'
|
||||
import RenderItem from '@/components/mt-edit/components/render-item/index.vue'
|
||||
import type {
|
||||
IDoneJson,
|
||||
IDoneJsonBinfo,
|
||||
IGlobalStoreCanvasCfg,
|
||||
IGlobalStoreGridCfg
|
||||
} from '@/components/mt-edit/store/types'
|
||||
import type { MouseTouchEvent } from '../types'
|
||||
import { globalStore } from '../../store/global'
|
||||
import type { onItemMoveParams } from './types'
|
||||
import { eventToVOn } from '../../utils'
|
||||
import { cacheStore } from '../../store/cache'
|
||||
import { getCurrentInstance } from 'vue'
|
||||
import { useExportJsonToDoneJson } from '@/components/mt-edit/composables/index'
|
||||
import TextVue from '@/components/custom-components/text-vue/index.vue'
|
||||
import CardVue from '@/components/custom-components/card-vue/index.vue'
|
||||
import NowTimeVue from '@/components/custom-components/now-time-vue/index.vue'
|
||||
import KvVue from '@/components/custom-components/kv-vue/index.vue'
|
||||
import SysButtonVue from '@/components/custom-components/sys-button-vue/index.vue'
|
||||
import BindDotVue from '@/components/custom-components/bind-dot-vue/index.vue'
|
||||
import BindIndexVue from '@/components/custom-components/bind-index-vue/index.vue'
|
||||
import { ElPopover } from 'element-plus'
|
||||
const instance = getCurrentInstance()
|
||||
const now_include_keys = Object.keys(instance?.appContext?.components as any)
|
||||
if (!now_include_keys.includes('text-vue')) {
|
||||
instance?.appContext.app.component('text-vue', TextVue)
|
||||
}
|
||||
if (!now_include_keys.includes('card-vue')) {
|
||||
instance?.appContext.app.component('card-vue', CardVue)
|
||||
}
|
||||
if (!now_include_keys.includes('now-time-vue')) {
|
||||
instance?.appContext.app.component('now-time-vue', NowTimeVue)
|
||||
}
|
||||
if (!now_include_keys.includes('kv-vue')) {
|
||||
instance?.appContext.app.component('kv-vue', KvVue)
|
||||
}
|
||||
if (!now_include_keys.includes('sys-button-vue')) {
|
||||
instance?.appContext.app.component('sys-button-vue', SysButtonVue)
|
||||
}
|
||||
if (!now_include_keys.includes('bind-dot-vue')) {
|
||||
instance?.appContext.app.component('bind-dot-vue', BindDotVue)
|
||||
}
|
||||
if (!now_include_keys.includes('bind-index-vue')) {
|
||||
instance?.appContext.app.component('bind-index-vue', BindIndexVue)
|
||||
}
|
||||
type RenderCoreProps = {
|
||||
doneJson: IDoneJson[]
|
||||
canvasCfg: IGlobalStoreCanvasCfg
|
||||
gridCfg: IGlobalStoreGridCfg
|
||||
showGhostDom: boolean
|
||||
canvasDom: HTMLElement | null
|
||||
globalLock: boolean
|
||||
preivewMode?: boolean
|
||||
lineAppendEnable?: boolean
|
||||
showPopover?: boolean
|
||||
onElementClick?: (elementId: string) => void
|
||||
}
|
||||
const renderCoreProps = withDefaults(defineProps<RenderCoreProps>(), {
|
||||
doneJson: () => [],
|
||||
showGhostDom: true,
|
||||
preivewMode: false,
|
||||
lineAppendEnable: false,
|
||||
showPopover: true,
|
||||
onElementClick: () => {}
|
||||
})
|
||||
const renderCoreEmits = defineEmits([
|
||||
'update:doneJson',
|
||||
'onMouseDown',
|
||||
'onItemMove',
|
||||
'onMoveMouseUp',
|
||||
'onItemMouseEnter',
|
||||
'onItemMouseLeave',
|
||||
'setIntention',
|
||||
'onItemResizeDone',
|
||||
'onItemRotateDone',
|
||||
'onItemRightClick',
|
||||
'setDoneJson'
|
||||
])
|
||||
// 记录多选的情况除了本次移动组件其他的组件位置信息
|
||||
const other_selected_items_binfo = ref<{ id: string; left: number; top: number }[]>([])
|
||||
const onMouseDown = (item: IDoneJson, e: MouseTouchEvent) => {
|
||||
other_selected_items_binfo.value = globalStore.done_json
|
||||
.filter(m => m.id !== item.id)
|
||||
.map(m => {
|
||||
return {
|
||||
id: m.id,
|
||||
left: m.binfo.left,
|
||||
top: m.binfo.top
|
||||
}
|
||||
})
|
||||
e.stopPropagation()
|
||||
renderCoreEmits('onMouseDown', item, e)
|
||||
}
|
||||
const onUpdateModelValue = (id: string, value: IDoneJsonBinfo) => {
|
||||
renderCoreEmits('update:doneJson', [
|
||||
...renderCoreProps.doneJson.map(m => {
|
||||
if (m.id === id) {
|
||||
return {
|
||||
...m,
|
||||
binfo: value
|
||||
}
|
||||
}
|
||||
return m
|
||||
})
|
||||
])
|
||||
}
|
||||
const onItemMove = (e: any, id: string) => {
|
||||
globalStore.setRealTimeData({
|
||||
show: true,
|
||||
text: `${e.new_lt.left},${e.new_lt.top}`
|
||||
})
|
||||
//如果同时选中多个组件,除去当前正在移动这个,手动更新其它的组件
|
||||
nextTick(() => {
|
||||
// 将所有移动组件的边界信息提供给父组件
|
||||
const item_move_params: onItemMoveParams = {
|
||||
//所有移动组件的信息
|
||||
move_item_bounding_info: globalStore.selected_items_id.map(m => {
|
||||
const { left, top, width, height, right, bottom } = document.getElementById(m)!.getBoundingClientRect()
|
||||
return {
|
||||
id: m,
|
||||
type: globalStore.done_json.find(f => f.id === m)!.type,
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height,
|
||||
right,
|
||||
bottom
|
||||
}
|
||||
}),
|
||||
//当前正在移动的组件的实时坐标信息
|
||||
move_binfo: e.move_binfo
|
||||
}
|
||||
if (globalStore.selected_items_id.length > 1) {
|
||||
// 找到其它的组件的id
|
||||
const other_items_id = globalStore.selected_items_id.filter(m => m !== id)
|
||||
renderCoreEmits('update:doneJson', [
|
||||
...renderCoreProps.doneJson.map(m => {
|
||||
if (other_items_id.includes(m.id)) {
|
||||
//找到初始值
|
||||
const init_pos = other_selected_items_binfo.value.find(f => f.id === m.id)
|
||||
return {
|
||||
...m,
|
||||
binfo: {
|
||||
...m.binfo,
|
||||
left: init_pos?.left + e.move_length.x,
|
||||
top: init_pos?.top + e.move_length.y
|
||||
}
|
||||
}
|
||||
}
|
||||
return m
|
||||
})
|
||||
])
|
||||
}
|
||||
renderCoreEmits('onItemMove', item_move_params)
|
||||
})
|
||||
}
|
||||
const onMoveMouseUp = () => {
|
||||
renderCoreEmits('onMoveMouseUp')
|
||||
globalStore.setRealTimeData({
|
||||
show: false,
|
||||
text: ``
|
||||
})
|
||||
}
|
||||
const getCommonAni = (item: IDoneJson) => {
|
||||
if (!item.common_animations || !item.common_animations.val) {
|
||||
return ``
|
||||
}
|
||||
return `animate__animated animate__${item.common_animations.val} animate__${item.common_animations.speed} animate__${item.common_animations.repeat} animate__${item.common_animations.delay}`
|
||||
}
|
||||
const onUpdateItemJson = (index: number, item: IDoneJson) => {
|
||||
const temp_done_json = [...renderCoreProps.doneJson]
|
||||
temp_done_json[index] = item
|
||||
renderCoreEmits('update:doneJson', temp_done_json)
|
||||
}
|
||||
const onMouseEnter = (e: MouseEvent, item: IDoneJson) => {
|
||||
renderCoreEmits('onItemMouseEnter', e, item)
|
||||
}
|
||||
const onMouseLeave = (e: MouseEvent, item: IDoneJson) => {
|
||||
renderCoreEmits('onItemMouseLeave', e, item)
|
||||
}
|
||||
const onResizeMove = (val: any) => {
|
||||
globalStore.setRealTimeData({
|
||||
show: true,
|
||||
text: `${val?.width}x${val?.height}`
|
||||
})
|
||||
}
|
||||
const onResizeDone = (item: IDoneJson) => {
|
||||
renderCoreEmits('onItemResizeDone', item)
|
||||
globalStore.setRealTimeData({
|
||||
show: false,
|
||||
text: ''
|
||||
})
|
||||
}
|
||||
const onRotateMove = (val: any) => {
|
||||
globalStore.setRealTimeData({
|
||||
show: true,
|
||||
text: `${val?.angle}°`
|
||||
})
|
||||
}
|
||||
const onRotateDone = (item: IDoneJson) => {
|
||||
globalStore.setRealTimeData({
|
||||
show: false,
|
||||
text: ``
|
||||
})
|
||||
renderCoreEmits('onItemRotateDone', item)
|
||||
}
|
||||
const onRightClick = (e: MouseEvent, item: IDoneJson) => {
|
||||
renderCoreEmits('onItemRightClick', e, item)
|
||||
}
|
||||
const externalMethod = (val: any) => {
|
||||
// console.log('调用外面方法;1');
|
||||
renderCoreEmits('setDoneJson', val)
|
||||
}
|
||||
const onLineMouseUp = () => {
|
||||
setTimeout(() => {
|
||||
cacheStore.addHistory(globalStore.done_json)
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
7
src/components/mt-edit/components/render-core/types.ts
Normal file
7
src/components/mt-edit/components/render-core/types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { CacheBoundingBox, IDoneJsonBinfo } from '../../store/types'
|
||||
|
||||
export interface onItemMoveParams {
|
||||
move_item_bounding_info: MoveItemBoundingInfo[]
|
||||
move_binfo: IDoneJsonBinfo & { id: string }
|
||||
}
|
||||
export type MoveItemBoundingInfo = CacheBoundingBox
|
||||
92
src/components/mt-edit/components/render-item/index.vue
Normal file
92
src/components/mt-edit/components/render-item/index.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="w-1/1 h-1/1">
|
||||
<svg-render
|
||||
v-if="item_json.type === 'svg'"
|
||||
draggable="false"
|
||||
:symbol-id="item_json.symbol!.symbol_id"
|
||||
:symbol-str="item_json.symbol!.symbol_str"
|
||||
:width="item_json.symbol!.width"
|
||||
:height="item_json.symbol!.height"
|
||||
:props="item_json.props"
|
||||
></svg-render>
|
||||
<group-render
|
||||
v-else-if="item_json.type === 'group'"
|
||||
:item-json="item_json"
|
||||
:grid="renderItemProps.grid"
|
||||
:canvas-cfg="renderItemProps.canvasCfg"
|
||||
:canvas-dom="renderItemProps.canvasDom"
|
||||
></group-render>
|
||||
<component
|
||||
v-else-if="item_json.type === 'vue'"
|
||||
draggable="false"
|
||||
:is="item_json.tag"
|
||||
v-bind="prosToVBind(item_json.props)"
|
||||
@update:modelValue="(val: any) => onUpdateModelValue(item_json.props, val)"
|
||||
></component>
|
||||
<img v-else-if="item_json.type === 'img'" draggable="false" class="w-1/1 h-1/1" :src="item_json.thumbnail" />
|
||||
<custom-svg-render v-else-if="item_json.type === 'custom-svg'">
|
||||
<component :is="item_json.tag" v-bind="prosToVBind(item_json.props)" :id="item_json.id"></component>
|
||||
</custom-svg-render>
|
||||
<line-render
|
||||
v-else-if="item_json.type === 'sys-line'"
|
||||
v-model:item-json="item_json"
|
||||
:canvas-cfg="renderItemProps.canvasCfg"
|
||||
:grid="renderItemProps.grid"
|
||||
:canvas-dom="renderItemProps.canvasDom"
|
||||
:done-json="renderItemProps.doneJson"
|
||||
:lock-state="renderItemProps.lockState"
|
||||
:mode="renderItemProps.lineAppendEnable ? 'line-edit' : 'normal'"
|
||||
@set-intention="val => emits('setIntention', val)"
|
||||
@line-mouse-up="emits('lineMouseUp')"
|
||||
></line-render>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
IDoneJson,
|
||||
IGlobalStoreCanvasCfg,
|
||||
IGlobalStoreGridCfg,
|
||||
ILeftAsideConfigItemPublicProps
|
||||
} from '../../store/types'
|
||||
import SvgRender from '@/components/mt-edit/components/svg-render/index.vue'
|
||||
import GroupRender from '@/components/mt-edit/components/group-render/index.vue'
|
||||
import { prosToVBind } from '@/components/mt-edit/utils'
|
||||
import LineRender from '@/components/mt-edit/components/line-render/index.vue'
|
||||
import CustomSvgRender from '@/components/mt-edit/components/custom-svg-render/index.vue'
|
||||
import { computed } from 'vue'
|
||||
type RenderItemProps = {
|
||||
itemJson: IDoneJson
|
||||
canvasCfg: IGlobalStoreCanvasCfg
|
||||
grid: IGlobalStoreGridCfg
|
||||
canvasDom: HTMLElement | null
|
||||
doneJson?: IDoneJson[]
|
||||
lockState: boolean
|
||||
lineAppendEnable?: boolean
|
||||
}
|
||||
const renderItemProps = withDefaults(defineProps<RenderItemProps>(), {
|
||||
doneJson: () => [],
|
||||
lineAppendEnable: false
|
||||
})
|
||||
const emits = defineEmits(['update:itemJson', 'setIntention', 'lineMouseUp'])
|
||||
const item_json = computed({
|
||||
get: () => renderItemProps.itemJson,
|
||||
set: value => {
|
||||
emits('update:itemJson', value)
|
||||
}
|
||||
})
|
||||
const onUpdateModelValue = (props: ILeftAsideConfigItemPublicProps, value: any) => {
|
||||
if (props.modelValue) {
|
||||
emits('update:itemJson', {
|
||||
...item_json.value,
|
||||
props: {
|
||||
...item_json.value.props,
|
||||
modelValue: {
|
||||
...item_json.value.props.modelValue,
|
||||
val: value
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
||||
91
src/components/mt-edit/components/selected-area/index.vue
Normal file
91
src/components/mt-edit/components/selected-area/index.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div class="mt-selected-area"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, unref } from 'vue'
|
||||
import { getRealityXY } from '@/components/mt-edit/utils'
|
||||
import type { MouseTouchEvent } from '../types'
|
||||
import type { IAreaBinfo } from './types'
|
||||
type SelectedAreaProps = {
|
||||
scaleRatio: number
|
||||
targetDom: HTMLElement | null
|
||||
}
|
||||
const selectedAreaProps = withDefaults(defineProps<SelectedAreaProps>(), {
|
||||
scaleRatio: 1,
|
||||
targetDom: null
|
||||
})
|
||||
const emits = defineEmits(['selectedAreaMouseUp'])
|
||||
const area_binfo = ref<IAreaBinfo>({
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: 0,
|
||||
left: 0
|
||||
})
|
||||
|
||||
const onMouseDown = (de: MouseTouchEvent) => {
|
||||
// 鼠标按下的位置
|
||||
const { realityX, realityY } = getRealityXY(de, selectedAreaProps.targetDom?.getBoundingClientRect())
|
||||
// 记录最开始点击时鼠标位置
|
||||
const d_x = de instanceof MouseEvent ? de.clientX : de.touches[0].pageX
|
||||
const d_y = de instanceof MouseEvent ? de.clientY : de.touches[0].pageY
|
||||
const onMouseMove = (e: MouseTouchEvent) => {
|
||||
// 记录鼠标移动的位置
|
||||
const m_x = e instanceof MouseEvent ? e.clientX : e.touches[0].pageX
|
||||
const m_y = e instanceof MouseEvent ? e.clientY : e.touches[0].pageY
|
||||
// 移动的距离
|
||||
const move_x = (m_x - d_x) / selectedAreaProps.scaleRatio
|
||||
const move_y = (m_y - d_y) / selectedAreaProps.scaleRatio
|
||||
let left = realityX / selectedAreaProps.scaleRatio,
|
||||
top = realityY / selectedAreaProps.scaleRatio
|
||||
let width = Math.abs(move_x),
|
||||
height = Math.abs(move_y)
|
||||
if (move_x < 0) {
|
||||
left = realityX / selectedAreaProps.scaleRatio - width
|
||||
}
|
||||
if (move_y < 0) {
|
||||
top = realityY / selectedAreaProps.scaleRatio - height
|
||||
}
|
||||
area_binfo.value = {
|
||||
width,
|
||||
height,
|
||||
left,
|
||||
top
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseUp = () => {
|
||||
document.removeEventListener('mousemove', onMouseMove)
|
||||
document.removeEventListener('mouseup', onMouseUp)
|
||||
document.removeEventListener('touchmove', onMouseMove)
|
||||
document.removeEventListener('touchend', onMouseUp)
|
||||
emits('selectedAreaMouseUp', unref(area_binfo))
|
||||
area_binfo.value = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: 0,
|
||||
left: 0
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousemove', onMouseMove)
|
||||
document.addEventListener('mouseup', onMouseUp)
|
||||
document.addEventListener('touchmove', onMouseMove)
|
||||
document.addEventListener('touchend', onMouseUp)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
onMouseDown
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.mt-selected-area {
|
||||
width: v-bind('area_binfo.width + "px"');
|
||||
height: v-bind('area_binfo.height + "px"');
|
||||
top: v-bind('area_binfo.top + "px"');
|
||||
left: v-bind('area_binfo.left + "px"');
|
||||
border: 1px solid #00699a;
|
||||
background-color: #59c7f9;
|
||||
opacity: 0.3;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
6
src/components/mt-edit/components/selected-area/types.ts
Normal file
6
src/components/mt-edit/components/selected-area/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface IAreaBinfo {
|
||||
width: number
|
||||
height: number
|
||||
top: number
|
||||
left: number
|
||||
}
|
||||
19
src/components/mt-edit/components/svg-analysis/index.vue
Normal file
19
src/components/mt-edit/components/svg-analysis/index.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<svg aria-hidden="true">
|
||||
<use :xlink:href="symbolId" v-bind="props.props" />
|
||||
</svg>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
props: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
})
|
||||
const symbolId = computed(() => `#mt-edit-${props.name}`)
|
||||
</script>
|
||||
27
src/components/mt-edit/components/svg-render/index.vue
Normal file
27
src/components/mt-edit/components/svg-render/index.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<img class="w-1/1 h-1/1" :src="url" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { symbolGenSvg, svgToImgSrc, genDomPropstr } from '@/components/mt-edit/utils/index'
|
||||
import type { ILeftAsideConfigItemPublicProps } from '../../store/types'
|
||||
type SvgRenderProps = {
|
||||
symbolId: string
|
||||
symbolStr: string
|
||||
width: string
|
||||
height: string
|
||||
props: ILeftAsideConfigItemPublicProps
|
||||
}
|
||||
const svgRenderProps = withDefaults(defineProps<SvgRenderProps>(), {})
|
||||
const url = computed(() => {
|
||||
return svgToImgSrc(
|
||||
symbolGenSvg(
|
||||
svgRenderProps.symbolId,
|
||||
svgRenderProps.symbolStr,
|
||||
svgRenderProps.width,
|
||||
svgRenderProps.height,
|
||||
genDomPropstr(svgRenderProps.props)
|
||||
)
|
||||
)
|
||||
})
|
||||
</script>
|
||||
13
src/components/mt-edit/components/types.ts
Normal file
13
src/components/mt-edit/components/types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { IDoneJson, IGlobalStoreCanvasCfg, IGlobalStoreGridCfg } from '../store/types'
|
||||
|
||||
export type MouseTouchEvent = MouseEvent | TouchEvent
|
||||
export interface IExportDoneJson extends Omit<IDoneJson, 'props' | 'symbol'> {
|
||||
props: {
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
export interface IExportJson {
|
||||
canvasCfg: IGlobalStoreCanvasCfg
|
||||
gridCfg: IGlobalStoreGridCfg
|
||||
json: IExportDoneJson[]
|
||||
}
|
||||
160
src/components/mt-edit/composables/index.ts
Normal file
160
src/components/mt-edit/composables/index.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import type { IExportDoneJson, IExportJson } from '../components/types'
|
||||
import { leftAsideStore } from '../store/left-aside'
|
||||
import type {
|
||||
IDoneJson,
|
||||
IGlobalStoreCanvasCfg,
|
||||
IGlobalStoreGridCfg,
|
||||
ILeftAsideConfigItem,
|
||||
ILeftAsideConfigItemPublicProps
|
||||
} from '../store/types'
|
||||
import { objectDeepClone } from '../utils'
|
||||
|
||||
export const genExportJson = (
|
||||
canvasCfg: IGlobalStoreCanvasCfg,
|
||||
gridCfg: IGlobalStoreGridCfg,
|
||||
doneJson: IDoneJson[]
|
||||
) => {
|
||||
// 先创建原始的 export_done_json
|
||||
let export_done_json: IExportDoneJson[] = []
|
||||
export_done_json = objectDeepClone<IDoneJson[]>(doneJson).map(m => {
|
||||
if (m.symbol) {
|
||||
delete m.symbol
|
||||
}
|
||||
let new_props = {}
|
||||
for (const key in m.props) {
|
||||
new_props = { ...new_props, ...{ [key]: m.props[key].val } }
|
||||
}
|
||||
return {
|
||||
...m,
|
||||
props: new_props,
|
||||
active: false
|
||||
}
|
||||
})
|
||||
|
||||
// const list = [
|
||||
// 'c53cccb8c65201c192d8c57fbdb4d993-RdNsoqHYOZ',
|
||||
// 'c53cccb8c65201c192d8c57fbdb4d993-O4jAyCBz1A',
|
||||
// 'c53cccb8c65201c192d8c57fbdb4d993-XBd70oZ3kH'
|
||||
// ]
|
||||
|
||||
// const message = [
|
||||
// { id: 'c53cccb8c65201c192d8c57fbdb4d993-RdNsoqHYOZ', text: '发生时刻:2023-07-05 12:00:00' },
|
||||
// { id: 'c53cccb8c65201c192d8c57fbdb4d993-O4jAyCBz1A', text: '传输中1111......' },
|
||||
// { id: 'c53cccb8c65201c192d8c57fbdb4d993-XBd70oZ3kH', text: '发生时刻:2023-07-06 14:20:00' }
|
||||
// ]
|
||||
|
||||
// 查找传输设备图元并添加文字图元
|
||||
// const transportDevices = export_done_json.filter(item =>
|
||||
// // 假设传输设备有特定的标识,比如ID包含特定关键词或type为特定值
|
||||
// // item.title?.includes('传输')
|
||||
// // list.some(id => item.id?.includes(id))
|
||||
// list.includes(item.id)
|
||||
// )
|
||||
|
||||
// 为每个传输设备添加旁边的文字图元
|
||||
const textElementsToAdd: IExportDoneJson[] = []
|
||||
|
||||
// 先删除旧图元
|
||||
// 用于存储需要移除的旧文本图元的 ID
|
||||
const idsToRemove: string[] = []
|
||||
|
||||
// transportDevices.forEach((device, index) => {
|
||||
// // 构造预期的旧文本图元 ID 模式 (基于设备 ID)
|
||||
// const expectedIdPrefix = `auto-text-${device.id}-`
|
||||
|
||||
// // 查找所有与当前设备关联的现有文本图元
|
||||
// const existingTextElements = export_done_json.filter(item => item.id?.startsWith(expectedIdPrefix))
|
||||
|
||||
// // 将这些旧图元的 ID 添加到待删除列表
|
||||
// idsToRemove.push(...existingTextElements.map(item => item.id!))
|
||||
|
||||
// // 获取对应的消息文本
|
||||
// const deviceMessage = message.find(m => m.id === device.id)?.text || '默认提示信息'
|
||||
|
||||
// // 创建新的文本图元
|
||||
// const textElement: IExportDoneJson = {
|
||||
// id: `auto-text-${device.id}-${index}`, // 使用时间戳确保唯一性
|
||||
// title: '动态文字',
|
||||
// type: 'vue',
|
||||
// tag: 'text-vue',
|
||||
// props: {
|
||||
// text: deviceMessage || '默认提示信息', // 添加安全检查
|
||||
// fontFamily: '黑体',
|
||||
// fontSize: 14,
|
||||
// fill: 'red',
|
||||
// vertical: false
|
||||
// },
|
||||
// common_animations: {
|
||||
// val: '',
|
||||
// delay: 'delay-0s',
|
||||
// speed: 'slow',
|
||||
// repeat: 'infinite'
|
||||
// },
|
||||
// binfo: {
|
||||
// left: (device.binfo?.left || 0) + (device.binfo?.width || 0) + 10,
|
||||
// top: (device.binfo?.top || 0) + (device.binfo?.height || 0) / 2 - 10 + (index % 2 === 0 ? 20 : -20), // 偶数下移20px,奇数上移20px
|
||||
// width: 200,
|
||||
// height: 50,
|
||||
// angle: 0
|
||||
// },
|
||||
// resize: true,
|
||||
// rotate: true,
|
||||
// lock: false,
|
||||
// active: false,
|
||||
// hide: false,
|
||||
// UIDName: '',
|
||||
// events: []
|
||||
// }
|
||||
// textElementsToAdd.push(textElement)
|
||||
// })
|
||||
|
||||
// // 从 export_done_json 中移除旧的文本图元
|
||||
// export_done_json = export_done_json.filter(item => !idsToRemove.includes(item.id!))
|
||||
|
||||
// // 合并原始图元和新增的文字图元
|
||||
// export_done_json = [...export_done_json, ...textElementsToAdd]
|
||||
|
||||
const exportJson: IExportJson = {
|
||||
canvasCfg,
|
||||
gridCfg,
|
||||
json: export_done_json
|
||||
}
|
||||
return { exportJson }
|
||||
}
|
||||
export const useExportJsonToDoneJson = (json: IExportJson) => {
|
||||
// 取出所有图形的初始配置
|
||||
let init_configs: ILeftAsideConfigItem[] = []
|
||||
for (const iterator of leftAsideStore.config.values()) {
|
||||
if (iterator.length > 0) {
|
||||
init_configs = [...init_configs, ...iterator]
|
||||
}
|
||||
}
|
||||
const importDoneJson: IDoneJson[] = json.json.map(m => {
|
||||
let props: ILeftAsideConfigItemPublicProps = {}
|
||||
let symbol = undefined
|
||||
// 找到原始的props
|
||||
const find_item = init_configs.find(f => f?.id == m.tag)
|
||||
const find_props = find_item?.props
|
||||
if (find_props) {
|
||||
props = { ...props, ...objectDeepClone(find_props) }
|
||||
}
|
||||
for (const key in m.props) {
|
||||
if (props[key] !== undefined) {
|
||||
props[key].val = m.props[key]
|
||||
}
|
||||
}
|
||||
if (find_item?.symbol) {
|
||||
symbol = find_item.symbol
|
||||
}
|
||||
return {
|
||||
...m,
|
||||
props,
|
||||
symbol
|
||||
}
|
||||
})
|
||||
return {
|
||||
canvasCfg: json.canvasCfg,
|
||||
gridCfg: json.gridCfg,
|
||||
importDoneJson
|
||||
}
|
||||
}
|
||||
200
src/components/mt-edit/composables/sys-line.ts
Normal file
200
src/components/mt-edit/composables/sys-line.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { nextTick } from 'vue'
|
||||
import type { IDoneJson, IDoneJsonBinfo } from '../store/types'
|
||||
import { getRectCenterCoordinate, getRectCoordinate, rotatePoint } from '../utils'
|
||||
|
||||
/**
|
||||
* 更新系统连线实际宽高
|
||||
* @param sys_lines
|
||||
* @param scale
|
||||
*/
|
||||
export const useUpdateSysLineRect = (sys_lines: IDoneJson[], canvasDom: HTMLElement, scale: number) => {
|
||||
sys_lines.forEach(f => {
|
||||
const itemRect = document.querySelector(`#${f.id} g .real`)!.getBoundingClientRect()
|
||||
const canvas_area_bounding_info = canvasDom!.getBoundingClientRect()
|
||||
const new_left = (itemRect?.left - canvas_area_bounding_info?.left) / scale
|
||||
const new_top = (itemRect?.top - canvas_area_bounding_info?.top) / scale
|
||||
const move_x = new_left - f.binfo.left
|
||||
const move_y = new_top - f.binfo.top
|
||||
f.binfo.left = new_left
|
||||
f.binfo.top = new_top
|
||||
f.binfo.width = itemRect?.width / scale
|
||||
f.binfo.height = itemRect?.height / scale
|
||||
f.props.point_position = {
|
||||
...f.props.point_position,
|
||||
val: f.props.point_position.val.map((m: { x: number; y: number }) => {
|
||||
return {
|
||||
x: m.x - move_x,
|
||||
y: m.y - move_y
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 更新系统连线
|
||||
* @param sys_lines 要更新的连线列表
|
||||
* @param done_json 所有组件信息
|
||||
* @param canvasDom 画布dom
|
||||
* @param scale 画布缩放
|
||||
*/
|
||||
export const useUpdateSysLine = (
|
||||
sys_lines: IDoneJson[],
|
||||
done_json: IDoneJson[],
|
||||
canvasDom: HTMLElement,
|
||||
scale: number,
|
||||
move_binfo?: IDoneJsonBinfo & { id: string }
|
||||
) => {
|
||||
const temp_done_json = [...done_json]
|
||||
sys_lines.forEach(f => {
|
||||
if (!f.props.bind_anchors.val.start && !f.props.bind_anchors.val.end) {
|
||||
return
|
||||
}
|
||||
const itemRect = document.querySelector(`#${f.id} g .real`)!.getBoundingClientRect()
|
||||
const canvas_area_bounding_info = canvasDom!.getBoundingClientRect()
|
||||
const new_left = (itemRect?.left - canvas_area_bounding_info?.left) / scale
|
||||
const new_top = (itemRect?.top - canvas_area_bounding_info?.top) / scale
|
||||
|
||||
// 处理起点绑定
|
||||
if (f.props.bind_anchors.val.start) {
|
||||
// 根据id和类型找到锚点坐标
|
||||
const find_item = temp_done_json.find(m => m.id === f.props.bind_anchors.val.start.id)
|
||||
if (find_item) {
|
||||
const b_info = find_item.id === move_binfo?.id ? move_binfo : find_item.binfo
|
||||
// 四个角原始坐标
|
||||
const { topLeft, topRight, bottomLeft, bottomRight } = getRectCoordinate(b_info)
|
||||
// 四条边中点坐标
|
||||
const { topCenter, bottomCenter, leftCenter, rightCenter } = getRectCenterCoordinate(
|
||||
topLeft,
|
||||
topRight,
|
||||
bottomLeft,
|
||||
bottomRight
|
||||
)
|
||||
// 旋转中心
|
||||
const centerX = topCenter.x
|
||||
const centerY = leftCenter.y
|
||||
|
||||
// 旋转角度(弧度)
|
||||
const angleRad = (Math.PI / 180) * find_item.binfo.angle
|
||||
|
||||
if (f.props.bind_anchors.val.start.type === 'tc') {
|
||||
const new_tc = rotatePoint(topCenter.x, topCenter.y, centerX, centerY, angleRad)
|
||||
f.props.point_position.val[0] = {
|
||||
x: new_tc.x - f.binfo.left,
|
||||
y: new_tc.y - f.binfo.top
|
||||
}
|
||||
} else if (f.props.bind_anchors.val.start.type === 'bc') {
|
||||
const new_bc = rotatePoint(bottomCenter.x, bottomCenter.y, centerX, centerY, angleRad)
|
||||
f.props.point_position.val[0] = {
|
||||
x: new_bc.x - f.binfo.left,
|
||||
y: new_bc.y - f.binfo.top
|
||||
}
|
||||
} else if (f.props.bind_anchors.val.start.type === 'lc') {
|
||||
const new_lc = rotatePoint(leftCenter.x, leftCenter.y, centerX, centerY, angleRad)
|
||||
f.props.point_position.val[0] = {
|
||||
x: new_lc.x - f.binfo.left,
|
||||
y: new_lc.y - f.binfo.top
|
||||
}
|
||||
} else if (f.props.bind_anchors.val.start.type === 'rc') {
|
||||
const new_rc = rotatePoint(rightCenter.x, rightCenter.y, centerX, centerY, angleRad)
|
||||
f.props.point_position.val[0] = {
|
||||
x: new_rc.x - f.binfo.left,
|
||||
y: new_rc.y - f.binfo.top
|
||||
}
|
||||
}
|
||||
const move_x = new_left - f.binfo.left
|
||||
const move_y = new_top - f.binfo.top
|
||||
f.binfo = {
|
||||
...f.binfo,
|
||||
left: new_left,
|
||||
top: new_top,
|
||||
width: itemRect?.width / scale,
|
||||
height: itemRect?.height / scale
|
||||
}
|
||||
f.props.point_position = {
|
||||
...f.props.point_position,
|
||||
val: f.props.point_position.val.map((m: { x: number; y: number }) => {
|
||||
return {
|
||||
x: m.x - move_x,
|
||||
y: m.y - move_y
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
f.props.bind_anchors.val.start = null
|
||||
}
|
||||
}
|
||||
// 处理终点绑定
|
||||
if (f.props.bind_anchors.val.end) {
|
||||
// 根据id和类型找到锚点坐标
|
||||
const find_item = temp_done_json.find(m => m.id === f.props.bind_anchors.val.end.id)
|
||||
if (find_item) {
|
||||
const b_info = find_item.id === move_binfo?.id ? move_binfo : find_item.binfo
|
||||
// 四个角原始坐标
|
||||
const { topLeft, topRight, bottomLeft, bottomRight } = getRectCoordinate(b_info)
|
||||
// 四条边中点坐标
|
||||
const { topCenter, bottomCenter, leftCenter, rightCenter } = getRectCenterCoordinate(
|
||||
topLeft,
|
||||
topRight,
|
||||
bottomLeft,
|
||||
bottomRight
|
||||
)
|
||||
// 旋转中心
|
||||
const centerX = topCenter.x
|
||||
const centerY = leftCenter.y
|
||||
|
||||
// 旋转角度(弧度)
|
||||
const angleRad = (Math.PI / 180) * find_item.binfo.angle
|
||||
|
||||
if (f.props.bind_anchors.val.end.type === 'tc') {
|
||||
const new_tc = rotatePoint(topCenter.x, topCenter.y, centerX, centerY, angleRad)
|
||||
f.props.point_position.val[f.props.point_position.val.length - 1] = {
|
||||
x: new_tc.x - f.binfo.left,
|
||||
y: new_tc.y - f.binfo.top
|
||||
}
|
||||
} else if (f.props.bind_anchors.val.end.type === 'bc') {
|
||||
const new_bc = rotatePoint(bottomCenter.x, bottomCenter.y, centerX, centerY, angleRad)
|
||||
f.props.point_position.val[f.props.point_position.val.length - 1] = {
|
||||
x: new_bc.x - f.binfo.left,
|
||||
y: new_bc.y - f.binfo.top
|
||||
}
|
||||
} else if (f.props.bind_anchors.val.end.type === 'lc') {
|
||||
const new_lc = rotatePoint(leftCenter.x, leftCenter.y, centerX, centerY, angleRad)
|
||||
f.props.point_position.val[f.props.point_position.val.length - 1] = {
|
||||
x: new_lc.x - f.binfo.left,
|
||||
y: new_lc.y - f.binfo.top
|
||||
}
|
||||
} else if (f.props.bind_anchors.val.end.type === 'rc') {
|
||||
const new_rc = rotatePoint(rightCenter.x, rightCenter.y, centerX, centerY, angleRad)
|
||||
f.props.point_position.val[f.props.point_position.val.length - 1] = {
|
||||
x: new_rc.x - f.binfo.left,
|
||||
y: new_rc.y - f.binfo.top
|
||||
}
|
||||
}
|
||||
const move_x = new_left - f.binfo.left
|
||||
const move_y = new_top - f.binfo.top
|
||||
f.binfo = {
|
||||
...f.binfo,
|
||||
left: new_left,
|
||||
top: new_top,
|
||||
width: itemRect?.width / scale,
|
||||
height: itemRect?.height / scale
|
||||
}
|
||||
f.props.point_position = {
|
||||
...f.props.point_position,
|
||||
val: f.props.point_position.val.map((m: { x: number; y: number }) => {
|
||||
return {
|
||||
x: m.x - move_x,
|
||||
y: m.y - move_y
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
f.props.bind_anchors.val.end = null
|
||||
}
|
||||
}
|
||||
})
|
||||
// 直接写在这里会损失一部分性能 也可以注释掉下面的 然后根据需求在useUpdateSysLine之后手动调用useUpdateSysLineRect
|
||||
nextTick(() => {
|
||||
useUpdateSysLineRect(sys_lines, canvasDom, scale)
|
||||
})
|
||||
}
|
||||
72
src/components/mt-edit/composables/thumbnail.ts
Normal file
72
src/components/mt-edit/composables/thumbnail.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Canvg } from 'canvg'
|
||||
import html2canvas from 'html2canvas'
|
||||
import { ElMessage } from 'element-plus'
|
||||
export const useGenThumbnail = async (canvas_id: string = 'mtCanvasArea') => {
|
||||
const el = <HTMLElement | null>document.querySelector(`#${canvas_id}`)
|
||||
if (!el) {
|
||||
ElMessage.error('没有找到canvas元素,请检查!')
|
||||
return
|
||||
}
|
||||
// //记录要移除的svg元素
|
||||
const shouldRemoveSvgNodes = []
|
||||
// 获取到所有的SVG 得到一个数组 目前只有自定义连线需要特殊处理 别的元素直接使用html2canvas就可以
|
||||
const svgElements: NodeListOf<HTMLElement> = document.body.querySelectorAll(`#${canvas_id} .mt-line-render`)
|
||||
// 遍历这个数组
|
||||
for (const item of svgElements) {
|
||||
//去除空白字符
|
||||
const svg = item.outerHTML.trim()
|
||||
// 创建一个 canvas DOM元素
|
||||
const canvas = document.createElement('canvas')
|
||||
//设置 canvas 元素的宽高
|
||||
canvas.width = item.getBoundingClientRect().width
|
||||
canvas.height = item.getBoundingClientRect().height
|
||||
const ctx = canvas.getContext('2d')
|
||||
// 将 SVG转化 成 canvas
|
||||
const v = Canvg.fromString(ctx!, svg)
|
||||
await v.render()
|
||||
|
||||
//设置生成 canvas 元素的坐标 保证与原SVG坐标保持一致
|
||||
if (item.style.position) {
|
||||
canvas.style.position += item.style.position
|
||||
canvas.style.left += item.style.left
|
||||
canvas.style.top += item.style.top
|
||||
}
|
||||
|
||||
//添加到需要截图的DOM节点中
|
||||
item.parentNode!.appendChild(canvas)
|
||||
// 删除这个元素
|
||||
shouldRemoveSvgNodes.push(canvas)
|
||||
}
|
||||
|
||||
const width = el.offsetWidth
|
||||
const height = el.offsetHeight
|
||||
const canvas = await html2canvas(el, {
|
||||
useCORS: true,
|
||||
scale: 2,
|
||||
width,
|
||||
height,
|
||||
allowTaint: true,
|
||||
windowHeight: height,
|
||||
logging: false,
|
||||
ignoreElements: element => {
|
||||
if (element.classList.contains('mt-line-render')) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
})
|
||||
// const img_link = document.createElement('a')
|
||||
// img_link.href = canvas.toDataURL('image/png') // 转换后的图片地址
|
||||
// img_link.download = Date.now().toString()
|
||||
// document.body.appendChild(img_link)
|
||||
// // 触发点击
|
||||
// img_link.click()
|
||||
// // 然后移除
|
||||
// document.body.removeChild(img_link)
|
||||
// 移除需要移除掉的svg节点
|
||||
shouldRemoveSvgNodes.forEach(item => {
|
||||
item.remove()
|
||||
})
|
||||
|
||||
return canvas.toDataURL('image/png')
|
||||
}
|
||||
3
src/components/mt-edit/index.ts
Normal file
3
src/components/mt-edit/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import MtEdit from './index.vue'
|
||||
|
||||
export default MtEdit
|
||||
257
src/components/mt-edit/index.vue
Normal file
257
src/components/mt-edit/index.vue
Normal file
@@ -0,0 +1,257 @@
|
||||
<template>
|
||||
<div id="mt-edit" class="relative flex-auto w-1/1 h-1/1 dark">
|
||||
<el-container class="h-1/1">
|
||||
<el-header
|
||||
height="45px"
|
||||
class="dark:bg-myDarkBgColor cb-border p-0 select-none"
|
||||
@mousedown="mainPanelRef?.stopListenerKeyDown()"
|
||||
>
|
||||
<header-panel
|
||||
v-model:leftAside="aside_state.left_show"
|
||||
v-model:rightAside="aside_state.right_show"
|
||||
v-model:lock-state="globalStore.lock"
|
||||
:selected-items-id="globalStore.selected_items_id"
|
||||
:group-enabled="header_group_enabled"
|
||||
:un-group-enabled="header_un_group_enabled"
|
||||
:align-enabled="header_align_enabled"
|
||||
:delete-enabled="header_delete_enabled"
|
||||
:undo-enabled="cacheStore.historyIndex > 0"
|
||||
:redo-enabled="cacheStore.historyIndex < cacheStore.history.length - 1"
|
||||
:real-time-data="globalStore.real_time_data"
|
||||
:use-thumbnail="mtEidtProps.useThumbnail"
|
||||
@on-group-click="mainPanelRef?.createGroupItem"
|
||||
@on-ungroup-click="mainPanelRef?.onUngroup"
|
||||
@on-delete-click="onDeleteClick"
|
||||
@on-export-click="onExportClick"
|
||||
@on-tree-click="done_json_tree_visiable = true"
|
||||
@on-help-click="onHelpClick"
|
||||
@align-selected="onAlignSelected"
|
||||
@on-redo-click="onRedoClick"
|
||||
@on-undo-click="onUndoClick"
|
||||
@on-import-click="onImportClick"
|
||||
@on-return-click="emits('onReturnClick')"
|
||||
@on-save-click="onSaveClick"
|
||||
@on-preview-click="onPreviewClick"
|
||||
@on-thumbnail-click="onThumbnailClick"
|
||||
@on-draw-line-click="onDrawLineClick"
|
||||
@on-save-all="onSaveAll"
|
||||
></header-panel>
|
||||
</el-header>
|
||||
<el-container class="h-[calc(100%-45px-40px)]">
|
||||
<el-aside
|
||||
:width="aside_state.left_show ? '300px' : '0px'"
|
||||
class="dark:bg-myDarkBgColor cr-border mt-edit-aside h-1/1 select-none mt-edit-aside-left"
|
||||
@mousedown="mainPanelRef?.stopListenerKeyDown()"
|
||||
>
|
||||
<tabs></tabs>
|
||||
|
||||
<!-- <left-aside :leftAsideConfig="leftAsideStore.config"></left-aside> -->
|
||||
</el-aside>
|
||||
<el-main class="dark:bg-myMainDarkBgColor" @mousedown="mainPanelRef?.beginListenerKeyDown()">
|
||||
<main-panel
|
||||
ref="mainPanelRef"
|
||||
:group-enabled="header_group_enabled"
|
||||
:un-group-enabled="header_un_group_enabled"
|
||||
:delete-enabled="header_delete_enabled"
|
||||
:line-append-enable="line_append_enable"
|
||||
></main-panel>
|
||||
</el-main>
|
||||
<el-aside
|
||||
:width="aside_state.right_show ? '300px' : '0px'"
|
||||
class="dark:bg-myDarkBgColor cl-border mt-edit-aside select-none"
|
||||
@mousedown="mainPanelRef?.stopListenerKeyDown()"
|
||||
>
|
||||
<right-aside>
|
||||
<template v-if="hasDeviceBindSlot" #deviceBind="{ item }">
|
||||
<slot name="deviceBind" :item="item" />
|
||||
</template>
|
||||
</right-aside>
|
||||
</el-aside>
|
||||
</el-container>
|
||||
<el-footer height="40px" class="dark:bg-myDarkBgColor ct-border select-none">
|
||||
<footer-panel></footer-panel>
|
||||
</el-footer>
|
||||
</el-container>
|
||||
<el-dialog v-model="import_visible" title="数据导入" @close="mainPanelRef?.beginListenerKeyDown()">
|
||||
<import-json ref="importJsonRef"></import-json>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="onImportYes">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="export_visible" title="数据导出" @close="mainPanelRef?.beginListenerKeyDown()">
|
||||
<export-json
|
||||
:done-json="objectDeepClone(globalStore.done_json)"
|
||||
:canvas-cfg="globalStore.canvasCfg"
|
||||
:grid-cfg="globalStore.gridCfg"
|
||||
></export-json>
|
||||
</el-dialog>
|
||||
<el-drawer v-model="done_json_tree_visiable" title="图形结构树" direction="ltr" size="30%">
|
||||
<done-tree
|
||||
:done-json="globalStore.done_json"
|
||||
:selected-items-id="globalStore.selected_items_id"
|
||||
@update-selected-items-id="onTreeUpdateSelectedItemsId"
|
||||
@update-selected-id-hide="onDoneTreeUpdateSelectedIdHide"
|
||||
></done-tree>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import HeaderPanel from '@/components/mt-edit/components/layout/header-panel/index.vue'
|
||||
import LeftAside from '@/components/mt-edit/components/layout/left-aside/index.vue'
|
||||
import MainPanel from '@/components/mt-edit/components/layout/main-panel/index.vue'
|
||||
import RightAside from '@/components/mt-edit/components/layout/right-aside/index.vue'
|
||||
import FooterPanel from '@/components/mt-edit/components/layout/footer-panel/index.vue'
|
||||
import Tabs from '@/components/mt-edit/components/layout/tabs/index.vue'
|
||||
import { leftAsideStore } from '@/components/mt-edit/store/left-aside'
|
||||
import { ElContainer, ElHeader, ElAside, ElMain, ElFooter, ElDialog, ElDrawer, ElButton, ElMessage } from 'element-plus'
|
||||
import { globalStore } from '@/components/mt-edit/store/global'
|
||||
import { computed, reactive, ref, useSlots } from 'vue'
|
||||
import DoneTree from '@/components/mt-edit/components/done-tree/index.vue'
|
||||
import { cacheStore } from './store/cache'
|
||||
import ExportJson from '@/components/mt-edit/components/export-json/index.vue'
|
||||
import ImportJson from '@/components/mt-edit/components/import-json/index.vue'
|
||||
import { objectDeepClone } from './utils'
|
||||
import { genExportJson, useExportJsonToDoneJson } from './composables'
|
||||
import type { IExportJson } from './components/types'
|
||||
import { useDataStore } from '@/stores/menuList'
|
||||
type MtEditProps = {
|
||||
useThumbnail?: boolean
|
||||
}
|
||||
const mtEidtProps = withDefaults(defineProps<MtEditProps>(), {
|
||||
useThumbnail: false
|
||||
})
|
||||
const emits = defineEmits(['onPreviewClick', 'onReturnClick', 'onSaveClick', 'onThumbnailClick', 'onSaveAll'])
|
||||
const slots = useSlots()
|
||||
const mainPanelRef = ref<InstanceType<typeof MainPanel>>()
|
||||
const importJsonRef = ref<InstanceType<typeof ImportJson>>()
|
||||
const aside_state = reactive({
|
||||
left_show: true,
|
||||
right_show: true
|
||||
})
|
||||
const hasDeviceBindSlot = computed(() => {
|
||||
return !!slots.deviceBind
|
||||
})
|
||||
const header_delete_enabled = computed(() => {
|
||||
return globalStore.selected_items_id.length > 0
|
||||
})
|
||||
const header_group_enabled = computed(() => {
|
||||
return globalStore.selected_items_id.length > 1
|
||||
})
|
||||
const header_un_group_enabled = computed(() => {
|
||||
if (globalStore.selected_items_id.length === 1) {
|
||||
const item = globalStore.done_json.find(f => f.id === globalStore.selected_items_id[0])
|
||||
return item?.type === 'group'
|
||||
}
|
||||
return false
|
||||
})
|
||||
const header_align_enabled = computed(() => {
|
||||
const selected_items = globalStore.done_json.filter(
|
||||
f => globalStore.selected_items_id.includes(f.id) && f.type !== 'sys-line'
|
||||
)
|
||||
return selected_items.length > 1
|
||||
})
|
||||
const import_visible = ref(false)
|
||||
const export_visible = ref(false)
|
||||
const done_json_tree_visiable = ref(false)
|
||||
const line_append_enable = ref(false)
|
||||
const onDeleteClick = () => {
|
||||
globalStore.deleteSelectedItems()
|
||||
cacheStore.addHistory(globalStore.done_json)
|
||||
}
|
||||
const onImportClick = () => {
|
||||
import_visible.value = true
|
||||
mainPanelRef.value?.stopListenerKeyDown()
|
||||
}
|
||||
const onExportClick = () => {
|
||||
export_visible.value = true
|
||||
mainPanelRef.value?.stopListenerKeyDown()
|
||||
}
|
||||
const onTreeUpdateSelectedItemsId = (id: string) => {
|
||||
globalStore.setSingleSelect(id)
|
||||
}
|
||||
const onDoneTreeUpdateSelectedIdHide = (id: string) => {
|
||||
const item = globalStore.done_json.find(f => f.id === id)
|
||||
if (item) {
|
||||
item.hide = !item.hide
|
||||
}
|
||||
}
|
||||
const onAlignSelected = (
|
||||
type:
|
||||
| 'left'
|
||||
| 'horizontally'
|
||||
| 'right'
|
||||
| 'top'
|
||||
| 'vertically'
|
||||
| 'bottom'
|
||||
| 'horizontal-distribution'
|
||||
| 'vertical-distribution'
|
||||
) => {
|
||||
mainPanelRef.value?.onAlignSelected(type)
|
||||
}
|
||||
const onHelpClick = () => {
|
||||
window.open('http://mt.yaolm.top')
|
||||
}
|
||||
const onRedoClick = () => {
|
||||
mainPanelRef.value?.onRedo()
|
||||
}
|
||||
const onUndoClick = () => {
|
||||
mainPanelRef.value?.onUndo()
|
||||
}
|
||||
const onImportYes = async () => {
|
||||
const res = await importJsonRef.value?.onImport()
|
||||
if (res) {
|
||||
import_visible.value = false
|
||||
cacheStore.addHistory(globalStore.done_json)
|
||||
} else {
|
||||
ElMessage.error('导入失败,请检查数据格式')
|
||||
}
|
||||
}
|
||||
const onPreviewClick = () => {
|
||||
// 获取导出json
|
||||
const { exportJson } = genExportJson(globalStore.canvasCfg, globalStore.gridCfg, globalStore.done_json)
|
||||
emits('onPreviewClick', exportJson)
|
||||
}
|
||||
|
||||
const onSaveClick = () => {
|
||||
// 获取导出json
|
||||
const { exportJson } = genExportJson(globalStore.canvasCfg, globalStore.gridCfg, globalStore.done_json)
|
||||
emits('onSaveClick', exportJson)
|
||||
}
|
||||
|
||||
const useData = useDataStore()
|
||||
const onSaveAll = () => {
|
||||
let form = new FormData()
|
||||
let blob = new Blob([JSON.stringify(useData.dataTree)], {
|
||||
type: 'application/json'
|
||||
})
|
||||
form.append('multipartFile', blob)
|
||||
form.append('name', useData.name)
|
||||
form.append('pid', useData.pid)
|
||||
emits('onSaveAll', form)
|
||||
}
|
||||
const onThumbnailClick = () => {
|
||||
emits('onThumbnailClick')
|
||||
}
|
||||
const onDrawLineClick = (val: boolean) => {
|
||||
line_append_enable.value = val
|
||||
}
|
||||
const setImportJson = (exportJson: IExportJson) => {
|
||||
const { canvasCfg, gridCfg, importDoneJson } = useExportJsonToDoneJson(exportJson)
|
||||
globalStore.canvasCfg = canvasCfg
|
||||
globalStore.gridCfg = gridCfg
|
||||
globalStore.setGlobalStoreDoneJson(importDoneJson)
|
||||
cacheStore.history[0] = importDoneJson
|
||||
return true
|
||||
}
|
||||
defineExpose({
|
||||
setImportJson
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.mt-edit-aside {
|
||||
transition: width 0.3s;
|
||||
}
|
||||
.mt-edit-aside-left {
|
||||
padding-left: 5px;
|
||||
}
|
||||
</style>
|
||||
244
src/components/mt-edit/store/bind.ts
Normal file
244
src/components/mt-edit/store/bind.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import { reactive } from 'vue'
|
||||
import type { IConfig, ILeftAsideConfigItem } from './types'
|
||||
const sysComponentItems: ILeftAsideConfigItem[] = [
|
||||
// {
|
||||
// id: 'sys-line',
|
||||
// title: '连线',
|
||||
// type: 'sys-line',
|
||||
// thumbnail: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9Im5vbmUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMiIgZD0iTTQgMThhMiAyIDAgMSAwIDQgMGEyIDIgMCAxIDAtNCAwTTE2IDZhMiAyIDAgMSAwIDQgMGEyIDIgMCAxIDAtNCAwTTcuNSAxNi41bDktOSIvPjwvc3ZnPg==`,
|
||||
// props: {
|
||||
// stroke: {
|
||||
// title: '线条颜色',
|
||||
// type: 'color',
|
||||
// val: '#ff0000'
|
||||
// },
|
||||
// 'stroke-width': {
|
||||
// title: '线条宽度',
|
||||
// type: 'number',
|
||||
// val: 2
|
||||
// },
|
||||
// 'marker-start': {
|
||||
// title: '起点箭头',
|
||||
// type: 'switch',
|
||||
// val: false
|
||||
// },
|
||||
// 'marker-end': {
|
||||
// title: '终点箭头',
|
||||
// type: 'switch',
|
||||
// val: true
|
||||
// },
|
||||
// point_position: {
|
||||
// title: '点坐标',
|
||||
// type: 'jsonEdit',
|
||||
// val: [
|
||||
// {
|
||||
// x: 0,
|
||||
// y: 0
|
||||
// },
|
||||
// {
|
||||
// x: 100,
|
||||
// y: 0
|
||||
// }
|
||||
// ],
|
||||
// disabled: true
|
||||
// },
|
||||
// ani_type: {
|
||||
// title: '动画类型',
|
||||
// type: 'select',
|
||||
// val: 'none',
|
||||
// options: [
|
||||
// {
|
||||
// label: '无',
|
||||
// value: 'none'
|
||||
// },
|
||||
// {
|
||||
// label: '电流',
|
||||
// value: 'electricity'
|
||||
// },
|
||||
// {
|
||||
// label: '轨迹',
|
||||
// value: 'track'
|
||||
// },
|
||||
// {
|
||||
// label: '水珠',
|
||||
// value: 'waterdrop'
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// ani_dur: { title: '持续时间', type: 'number', val: 20 },
|
||||
// ani_color: { title: '动画颜色', type: 'color', val: '#0a7ae2' },
|
||||
// ani_reverse: { title: '动画反转', type: 'switch', val: false },
|
||||
// ani_play: { title: '动画播放', type: 'switch', val: true },
|
||||
// bind_anchors: {
|
||||
// title: '锚点绑定',
|
||||
// type: 'jsonEdit',
|
||||
// val: {
|
||||
// start: null,
|
||||
// end: null
|
||||
// },
|
||||
// disabled: true
|
||||
// }
|
||||
// },
|
||||
// common_animations: {
|
||||
// val: '',
|
||||
// delay: 'delay-0s',
|
||||
// speed: 'slow',
|
||||
// repeat: 'infinite'
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// id: 'sys-button-vue',
|
||||
// title: '按钮',
|
||||
// type: 'vue',
|
||||
// thumbnail: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgdmlld0JveD0iMCAwIDIwIDIwIj48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik0yIDhhMyAzIDAgMCAxIDMtM2gxMGEzIDMgMCAwIDEgMyAzdjNhMyAzIDAgMCAxLTMgM0g1YTMgMyAwIDAgMS0zLTNWOFptNyAxLjVhLjUuNSAwIDAgMCAuNS41SDE0YS41LjUgMCAwIDAgMC0xSDkuNWEuNS41IDAgMCAwLS41LjVabS0xIDBhMS41IDEuNSAwIDEgMC0zIDBhMS41IDEuNSAwIDAgMCAzIDBaIi8+PC9zdmc+`,
|
||||
// props: {
|
||||
// text: {
|
||||
// title: '按钮文本',
|
||||
// type: 'input',
|
||||
// val: '按钮文本'
|
||||
// },
|
||||
// type: {
|
||||
// title: '按钮类型',
|
||||
// type: 'select',
|
||||
// val: '',
|
||||
// options: [
|
||||
// {
|
||||
// value: '',
|
||||
// label: '默认'
|
||||
// },
|
||||
// {
|
||||
// value: 'primary',
|
||||
// label: '主要'
|
||||
// },
|
||||
// {
|
||||
// value: 'success',
|
||||
// label: '成功'
|
||||
// },
|
||||
// {
|
||||
// value: 'warning',
|
||||
// label: '警告'
|
||||
// },
|
||||
// {
|
||||
// value: 'danger',
|
||||
// label: '危险'
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// round: {
|
||||
// title: '圆角',
|
||||
// type: 'switch',
|
||||
// val: false
|
||||
// }
|
||||
// },
|
||||
// common_animations: {
|
||||
// val: '',
|
||||
// delay: 'delay-0s',
|
||||
// speed: 'slow',
|
||||
// repeat: 'infinite'
|
||||
// }
|
||||
// },
|
||||
{
|
||||
id: 'bind-dot-vue',
|
||||
keyId: 'bind-dot',
|
||||
title: '绑定监测点',
|
||||
type: 'vue',
|
||||
thumbnail: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+PHBhdGggZmlsbD0ibm9uZSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIzMiIgZD0ibTMyIDQxNS41bDEyMC0zMjBsMTIwIDMyMG0tNDItMTEySDc0bTI1Mi02NGMxMi4xOS0yOC42OSA0MS00OCA3NC00OGgwYzQ2IDAgODAgMzIgODAgODB2MTQ0Ii8+PHBhdGggZmlsbD0ibm9uZSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIzMiIgZD0iTTMyMCAzNTguNWMwIDM2IDI2Ljg2IDU4IDYwIDU4YzU0IDAgMTAwLTI3IDEwMC0xMDZ2LTE1Yy0yMCAwLTU4IDEtOTIgNWMtMzIuNzcgMy44Ni02OCAxOS02OCA1OCIvPjwvc3ZnPg==`,
|
||||
props: {
|
||||
text: {
|
||||
title: '监测内容',
|
||||
type: 'input',
|
||||
val: '绑定监测点'
|
||||
},
|
||||
fontFamily: {
|
||||
title: '字体',
|
||||
type: 'select',
|
||||
val: '黑体',
|
||||
options: [
|
||||
{
|
||||
value: '黑体',
|
||||
label: '黑体'
|
||||
},
|
||||
{
|
||||
value: '宋体',
|
||||
label: '宋体'
|
||||
}
|
||||
]
|
||||
},
|
||||
fontSize: {
|
||||
title: '文字大小',
|
||||
type: 'number',
|
||||
val: 14
|
||||
},
|
||||
fill: {
|
||||
title: '文字颜色',
|
||||
type: 'color',
|
||||
val: '#ff0000'
|
||||
},
|
||||
vertical: {
|
||||
title: '竖排展示',
|
||||
type: 'switch',
|
||||
val: false
|
||||
}
|
||||
},
|
||||
common_animations: {
|
||||
val: '',
|
||||
delay: 'delay-0s',
|
||||
speed: 'slow',
|
||||
repeat: 'infinite'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'bind-index-vue',
|
||||
keyId: 'bind-index',
|
||||
title: '绑定指标',
|
||||
type: 'vue',
|
||||
thumbnail: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+PHBhdGggZmlsbD0ibm9uZSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIzMiIgZD0ibTMyIDQxNS41bDEyMC0zMjBsMTIwIDMyMG0tNDItMTEySDc0bTI1Mi02NGMxMi4xOS0yOC42OSA0MS00OCA3NC00OGgwYzQ2IDAgODAgMzIgODAgODB2MTQ0Ii8+PHBhdGggZmlsbD0ibm9uZSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIzMiIgZD0iTTMyMCAzNTguNWMwIDM2IDI2Ljg2IDU4IDYwIDU4YzU0IDAgMTAwLTI3IDEwMC0xMDZ2LTE1Yy0yMCAwLTU4IDEtOTIgNWMtMzIuNzcgMy44Ni02OCAxOS02OCA1OCIvPjwvc3ZnPg==`,
|
||||
props: {
|
||||
text: {
|
||||
title: '指标内容',
|
||||
type: 'input',
|
||||
val: '绑定指标'
|
||||
},
|
||||
fontFamily: {
|
||||
title: '字体',
|
||||
type: 'select',
|
||||
val: '黑体',
|
||||
options: [
|
||||
{
|
||||
value: '黑体',
|
||||
label: '黑体'
|
||||
},
|
||||
{
|
||||
value: '宋体',
|
||||
label: '宋体'
|
||||
}
|
||||
]
|
||||
},
|
||||
fontSize: {
|
||||
title: '文字大小',
|
||||
type: 'number',
|
||||
val: 14
|
||||
},
|
||||
fill: {
|
||||
title: '文字颜色',
|
||||
type: 'color',
|
||||
val: '#ff0000'
|
||||
},
|
||||
vertical: {
|
||||
title: '竖排展示',
|
||||
type: 'switch',
|
||||
val: false
|
||||
}
|
||||
},
|
||||
common_animations: {
|
||||
val: '',
|
||||
delay: 'delay-0s',
|
||||
speed: 'slow',
|
||||
repeat: 'infinite'
|
||||
}
|
||||
}
|
||||
]
|
||||
export const bingStore: IConfig = reactive({
|
||||
sysComponent: sysComponentItems,
|
||||
lineRenderOffset: 10
|
||||
})
|
||||
33
src/components/mt-edit/store/cache.ts
Normal file
33
src/components/mt-edit/store/cache.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { nextTick, reactive } from 'vue'
|
||||
import type { CacheBoundingBox, ICache, IDoneJson } from './types'
|
||||
import { objectDeepClone } from '../utils'
|
||||
|
||||
export const cacheStore: ICache = reactive({
|
||||
boundingBox: [],
|
||||
setBoundingBox: (val: CacheBoundingBox[]) => {
|
||||
cacheStore.boundingBox = val
|
||||
},
|
||||
adsorbPoint: [],
|
||||
setAdsorbPoint(val) {
|
||||
cacheStore.adsorbPoint = val
|
||||
},
|
||||
copy: [],
|
||||
setCopy(val) {
|
||||
cacheStore.copy = val
|
||||
},
|
||||
history: [[]],
|
||||
historyIndex: 0,
|
||||
addHistory(val: IDoneJson[]) {
|
||||
nextTick(() => {
|
||||
if (cacheStore.historyIndex + 1 < cacheStore.history.length) {
|
||||
cacheStore.history.splice(cacheStore.historyIndex + 1)
|
||||
}
|
||||
cacheStore.history.push(objectDeepClone(val))
|
||||
cacheStore.historyIndex = cacheStore.history.length - 1
|
||||
if (cacheStore.history.length > 20) {
|
||||
cacheStore.history.shift()
|
||||
cacheStore.historyIndex = cacheStore.history.length - 1
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
415
src/components/mt-edit/store/config.ts
Normal file
415
src/components/mt-edit/store/config.ts
Normal file
@@ -0,0 +1,415 @@
|
||||
import { reactive } from 'vue'
|
||||
import type { IConfig, ILeftAsideConfigItem } from './types'
|
||||
const sysComponentItems: ILeftAsideConfigItem[] = [
|
||||
{
|
||||
id: 'sys-line',
|
||||
title: '自由连线',
|
||||
type: 'sys-line',
|
||||
thumbnail: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9Im5vbmUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMiIgZD0iTTQgMThhMiAyIDAgMSAwIDQgMGEyIDIgMCAxIDAtNCAwTTE2IDZhMiAyIDAgMSAwIDQgMGEyIDIgMCAxIDAtNCAwTTcuNSAxNi41bDktOSIvPjwvc3ZnPg==`,
|
||||
props: {
|
||||
stroke: {
|
||||
title: '线条颜色',
|
||||
type: 'color',
|
||||
val: '#ff0000'
|
||||
},
|
||||
'stroke-width': {
|
||||
title: '线条宽度',
|
||||
type: 'number',
|
||||
val: 2
|
||||
},
|
||||
'marker-start': {
|
||||
title: '起点箭头',
|
||||
type: 'switch',
|
||||
val: false
|
||||
},
|
||||
'marker-end': {
|
||||
title: '终点箭头',
|
||||
type: 'switch',
|
||||
val: true
|
||||
},
|
||||
point_position: {
|
||||
title: '点坐标',
|
||||
type: 'jsonEdit',
|
||||
val: [
|
||||
{
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
{
|
||||
x: 100,
|
||||
y: 0
|
||||
}
|
||||
],
|
||||
disabled: true
|
||||
},
|
||||
ani_type: {
|
||||
title: '动画类型',
|
||||
type: 'select',
|
||||
val: 'none',
|
||||
options: [
|
||||
{
|
||||
label: '无',
|
||||
value: 'none'
|
||||
},
|
||||
{
|
||||
label: '电流',
|
||||
value: 'electricity'
|
||||
},
|
||||
{
|
||||
label: '轨迹',
|
||||
value: 'track'
|
||||
},
|
||||
{
|
||||
label: '水珠',
|
||||
value: 'waterdrop'
|
||||
}
|
||||
]
|
||||
},
|
||||
ani_dur: { title: '持续时间', type: 'number', val: 20 },
|
||||
ani_color: { title: '动画颜色', type: 'color', val: '#0a7ae2' },
|
||||
ani_reverse: { title: '动画反转', type: 'switch', val: false },
|
||||
ani_play: { title: '动画播放', type: 'switch', val: true },
|
||||
bind_anchors: {
|
||||
title: '锚点绑定',
|
||||
type: 'jsonEdit',
|
||||
val: {
|
||||
start: null,
|
||||
end: null
|
||||
},
|
||||
disabled: true
|
||||
}
|
||||
},
|
||||
common_animations: {
|
||||
val: '',
|
||||
delay: 'delay-0s',
|
||||
speed: 'slow',
|
||||
repeat: 'infinite'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'sys-line-vertical',
|
||||
title: '自由连线-竖线',
|
||||
type: 'sys-line',
|
||||
thumbnail: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9Im5vbmUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMiIgZD0iTTQgMThhMiAyIDAgMSAwIDQgMGEyIDIgMCAxIDAtNCAwTTE2IDZhMiAyIDAgMSAwIDQgMGEyIDIgMCAxIDAtNCAwTTcuNSAxNi41bDktOSIvPjwvc3ZnPg==`,
|
||||
props: {
|
||||
stroke: {
|
||||
title: '线条颜色',
|
||||
type: 'color',
|
||||
val: '#ff0000'
|
||||
},
|
||||
'stroke-width': {
|
||||
title: '线条宽度',
|
||||
type: 'number',
|
||||
val: 2
|
||||
},
|
||||
'marker-start': {
|
||||
title: '起点箭头',
|
||||
type: 'switch',
|
||||
val: false
|
||||
},
|
||||
'marker-end': {
|
||||
title: '终点箭头',
|
||||
type: 'switch',
|
||||
val: true
|
||||
},
|
||||
point_position: {
|
||||
title: '点坐标',
|
||||
type: 'jsonEdit',
|
||||
val: [
|
||||
{
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
{
|
||||
x: 0,
|
||||
y: 100
|
||||
}
|
||||
],
|
||||
disabled: true
|
||||
},
|
||||
ani_type: {
|
||||
title: '动画类型',
|
||||
type: 'select',
|
||||
val: 'none',
|
||||
options: [
|
||||
{
|
||||
label: '无',
|
||||
value: 'none'
|
||||
},
|
||||
{
|
||||
label: '电流',
|
||||
value: 'electricity'
|
||||
},
|
||||
{
|
||||
label: '轨迹',
|
||||
value: 'track'
|
||||
},
|
||||
{
|
||||
label: '水珠',
|
||||
value: 'waterdrop'
|
||||
}
|
||||
]
|
||||
},
|
||||
ani_dur: { title: '持续时间', type: 'number', val: 20 },
|
||||
ani_color: { title: '动画颜色', type: 'color', val: '#0a7ae2' },
|
||||
ani_reverse: { title: '动画反转', type: 'switch', val: false },
|
||||
ani_play: { title: '动画播放', type: 'switch', val: true },
|
||||
bind_anchors: {
|
||||
title: '锚点绑定',
|
||||
type: 'jsonEdit',
|
||||
val: {
|
||||
start: null,
|
||||
end: null
|
||||
},
|
||||
disabled: true
|
||||
}
|
||||
},
|
||||
common_animations: {
|
||||
val: '',
|
||||
delay: 'delay-0s',
|
||||
speed: 'slow',
|
||||
repeat: 'infinite'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'text-vue',
|
||||
title: '文字',
|
||||
type: 'vue',
|
||||
thumbnail: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+PHBhdGggZmlsbD0ibm9uZSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIzMiIgZD0ibTMyIDQxNS41bDEyMC0zMjBsMTIwIDMyMG0tNDItMTEySDc0bTI1Mi02NGMxMi4xOS0yOC42OSA0MS00OCA3NC00OGgwYzQ2IDAgODAgMzIgODAgODB2MTQ0Ii8+PHBhdGggZmlsbD0ibm9uZSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIzMiIgZD0iTTMyMCAzNTguNWMwIDM2IDI2Ljg2IDU4IDYwIDU4YzU0IDAgMTAwLTI3IDEwMC0xMDZ2LTE1Yy0yMCAwLTU4IDEtOTIgNWMtMzIuNzcgMy44Ni02OCAxOS02OCA1OCIvPjwvc3ZnPg==`,
|
||||
props: {
|
||||
text: {
|
||||
title: '文字内容',
|
||||
type: 'input',
|
||||
val: '文字'
|
||||
},
|
||||
fontFamily: {
|
||||
title: '字体',
|
||||
type: 'select',
|
||||
val: '黑体',
|
||||
options: [
|
||||
{
|
||||
value: '黑体',
|
||||
label: '黑体'
|
||||
},
|
||||
{
|
||||
value: '宋体',
|
||||
label: '宋体'
|
||||
}
|
||||
]
|
||||
},
|
||||
fontSize: {
|
||||
title: '文字大小',
|
||||
type: 'number',
|
||||
val: 14
|
||||
},
|
||||
fill: {
|
||||
title: '文字颜色',
|
||||
type: 'color',
|
||||
val: '#ff0000'
|
||||
},
|
||||
vertical: {
|
||||
title: '竖排展示',
|
||||
type: 'switch',
|
||||
val: false
|
||||
}
|
||||
},
|
||||
common_animations: {
|
||||
val: '',
|
||||
delay: 'delay-0s',
|
||||
speed: 'slow',
|
||||
repeat: 'infinite'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'card-vue',
|
||||
title: '卡片',
|
||||
type: 'vue',
|
||||
thumbnail: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxnIGZpbGw9ImN1cnJlbnRDb2xvciI+PHBhdGggZD0iTTE0LjUgM2EuNS41IDAgMCAxIC41LjV2OWEuNS41IDAgMCAxLS41LjVoLTEzYS41LjUgMCAwIDEtLjUtLjV2LTlhLjUuNSAwIDAgMSAuNS0uNXptLTEzLTFBMS41IDEuNSAwIDAgMCAwIDMuNXY5QTEuNSAxLjUgMCAwIDAgMS41IDE0aDEzYTEuNSAxLjUgMCAwIDAgMS41LTEuNXYtOUExLjUgMS41IDAgMCAwIDE0LjUgMnoiLz48cGF0aCBkPSJNMyA1LjVhLjUuNSAwIDAgMSAuNS0uNWg5YS41LjUgMCAwIDEgMCAxaC05YS41LjUgMCAwIDEtLjUtLjVNMyA4YS41LjUgMCAwIDEgLjUtLjVoOWEuNS41IDAgMCAxIDAgMWgtOUEuNS41IDAgMCAxIDMgOG0wIDIuNWEuNS41IDAgMCAxIC41LS41aDZhLjUuNSAwIDAgMSAwIDFoLTZhLjUuNSAwIDAgMS0uNS0uNSIvPjwvZz48L3N2Zz4=`,
|
||||
props: {
|
||||
shadow: {
|
||||
title: '阴影显示时机',
|
||||
type: 'select',
|
||||
val: 'always',
|
||||
options: [
|
||||
{ label: '总是显示', value: 'always' },
|
||||
{ label: '鼠标悬浮', value: 'hover' },
|
||||
{ label: '不显示', value: 'never' }
|
||||
]
|
||||
},
|
||||
backGroundColor: {
|
||||
title: '背景颜色',
|
||||
type: 'color',
|
||||
val: '#ffffff'
|
||||
},
|
||||
boxShadow: {
|
||||
title: '阴影颜色',
|
||||
type: 'color',
|
||||
val: '#ffffff'
|
||||
}
|
||||
},
|
||||
common_animations: {
|
||||
val: '',
|
||||
delay: 'delay-0s',
|
||||
speed: 'slow',
|
||||
repeat: 'infinite'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'now-time-vue',
|
||||
title: '当前时间',
|
||||
type: 'vue',
|
||||
thumbnail: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+PHBhdGggZmlsbD0ibm9uZSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLXdpZHRoPSIzMiIgZD0iTTI1NiA2NEMxNTAgNjQgNjQgMTUwIDY0IDI1NnM4NiAxOTIgMTkyIDE5MnMxOTItODYgMTkyLTE5MlMzNjIgNjQgMjU2IDY0WiIvPjxwYXRoIGZpbGw9Im5vbmUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMzIiIGQ9Ik0yNTYgMTI4djE0NGg5NiIvPjwvc3ZnPg==`,
|
||||
props: {
|
||||
fontColor: {
|
||||
title: '文字颜色',
|
||||
type: 'color',
|
||||
val: '#000000'
|
||||
},
|
||||
dateSize: {
|
||||
title: '日期文字大小',
|
||||
type: 'number',
|
||||
val: 12
|
||||
},
|
||||
weekSize: {
|
||||
title: '星期文字大小',
|
||||
type: 'number',
|
||||
val: 12
|
||||
},
|
||||
timeSize: {
|
||||
title: '时间文字大小',
|
||||
type: 'number',
|
||||
val: 24
|
||||
}
|
||||
},
|
||||
common_animations: {
|
||||
val: '',
|
||||
delay: 'delay-0s',
|
||||
speed: 'slow',
|
||||
repeat: 'infinite'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'kv-vue',
|
||||
title: '键值对',
|
||||
type: 'vue',
|
||||
thumbnail: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjAgMjAiPjxwYXRoIGZpbGw9ImN1cnJlbnRDb2xvciIgZD0iTTMgNmEzIDMgMCAwIDEgMy0zaDhhMyAzIDAgMCAxIDMgM3Y4YTMgMyAwIDAgMS0zIDNINmEzIDMgMCAwIDEtMy0zem0zLTJhMiAyIDAgMCAwLTIgMnYzLjVoNS41VjR6bTMuNSA2LjVINFYxNGEyIDIgMCAwIDAgMiAyaDMuNXptMSAwVjE2SDE0YTIgMiAwIDAgMCAyLTJ2LTMuNXptNS41LTFWNmEyIDIgMCAwIDAtMi0yaC0zLjV2NS41eiIvPjwvc3ZnPg==`,
|
||||
props: {
|
||||
border: {
|
||||
title: '边框',
|
||||
type: 'switch',
|
||||
val: true
|
||||
},
|
||||
fontFamily: {
|
||||
title: '字体',
|
||||
type: 'select',
|
||||
val: '黑体',
|
||||
options: [
|
||||
{
|
||||
value: '黑体',
|
||||
label: '黑体'
|
||||
},
|
||||
{
|
||||
value: '宋体',
|
||||
label: '宋体'
|
||||
}
|
||||
]
|
||||
},
|
||||
fontSize: {
|
||||
title: '文字大小',
|
||||
type: 'number',
|
||||
val: 14
|
||||
},
|
||||
label: {
|
||||
title: '键名',
|
||||
type: 'input',
|
||||
val: '键名'
|
||||
},
|
||||
labelWidth: {
|
||||
title: '键名宽度',
|
||||
type: 'number',
|
||||
val: 50
|
||||
},
|
||||
value: {
|
||||
title: '键值',
|
||||
type: 'input',
|
||||
val: '键值'
|
||||
},
|
||||
valueWidth: {
|
||||
title: '键值宽度',
|
||||
type: 'number',
|
||||
val: 50
|
||||
},
|
||||
color: {
|
||||
title: '文字颜色',
|
||||
type: 'color',
|
||||
val: '#ff0000'
|
||||
},
|
||||
borderColor: {
|
||||
title: '边框颜色',
|
||||
type: 'color',
|
||||
val: '#000000'
|
||||
}
|
||||
},
|
||||
common_animations: {
|
||||
val: '',
|
||||
delay: 'delay-0s',
|
||||
speed: 'slow',
|
||||
repeat: 'infinite'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'sys-button-vue',
|
||||
title: '按钮',
|
||||
type: 'vue',
|
||||
thumbnail: `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgdmlld0JveD0iMCAwIDIwIDIwIj48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik0yIDhhMyAzIDAgMCAxIDMtM2gxMGEzIDMgMCAwIDEgMyAzdjNhMyAzIDAgMCAxLTMgM0g1YTMgMyAwIDAgMS0zLTNWOFptNyAxLjVhLjUuNSAwIDAgMCAuNS41SDE0YS41LjUgMCAwIDAgMC0xSDkuNWEuNS41IDAgMCAwLS41LjVabS0xIDBhMS41IDEuNSAwIDEgMC0zIDBhMS41IDEuNSAwIDAgMCAzIDBaIi8+PC9zdmc+`,
|
||||
props: {
|
||||
text: {
|
||||
title: '按钮文本',
|
||||
type: 'input',
|
||||
val: '按钮文本'
|
||||
},
|
||||
type: {
|
||||
title: '按钮类型',
|
||||
type: 'select',
|
||||
val: '',
|
||||
options: [
|
||||
{
|
||||
value: '',
|
||||
label: '默认'
|
||||
},
|
||||
{
|
||||
value: 'primary',
|
||||
label: '主要'
|
||||
},
|
||||
{
|
||||
value: 'success',
|
||||
label: '成功'
|
||||
},
|
||||
{
|
||||
value: 'warning',
|
||||
label: '警告'
|
||||
},
|
||||
{
|
||||
value: 'danger',
|
||||
label: '危险'
|
||||
}
|
||||
]
|
||||
},
|
||||
round: {
|
||||
title: '圆角',
|
||||
type: 'switch',
|
||||
val: false
|
||||
}
|
||||
},
|
||||
common_animations: {
|
||||
val: '',
|
||||
delay: 'delay-0s',
|
||||
speed: 'slow',
|
||||
repeat: 'infinite'
|
||||
}
|
||||
}
|
||||
]
|
||||
export const configStore: IConfig = reactive({
|
||||
sysComponent: sysComponentItems,
|
||||
lineRenderOffset: 10
|
||||
})
|
||||
73
src/components/mt-edit/store/context-menu.ts
Normal file
73
src/components/mt-edit/store/context-menu.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { reactive } from 'vue'
|
||||
import type { ContextMenuInfoType, IContextMenu } from './types'
|
||||
|
||||
export const contextMenuStore: IContextMenu = reactive({
|
||||
menuInfo: {
|
||||
display: false,
|
||||
left: 0,
|
||||
top: 0,
|
||||
info: {
|
||||
selectAll: {
|
||||
title: '全选',
|
||||
hot_key: 'Ctrl + A',
|
||||
enable: false
|
||||
},
|
||||
copy: {
|
||||
title: '复制',
|
||||
hot_key: 'Ctrl + C',
|
||||
enable: false
|
||||
},
|
||||
paste: {
|
||||
title: '粘贴',
|
||||
hot_key: 'Ctrl + V',
|
||||
enable: false
|
||||
},
|
||||
delete: {
|
||||
title: '删除',
|
||||
hot_key: 'Delete',
|
||||
enable: false
|
||||
},
|
||||
group: {
|
||||
title: '组合',
|
||||
hot_key: 'Ctrl + G',
|
||||
enable: false
|
||||
},
|
||||
ungroup: {
|
||||
title: '取消组合',
|
||||
hot_key: 'Ctrl + U',
|
||||
enable: false
|
||||
},
|
||||
moveTop: {
|
||||
title: '置顶',
|
||||
hot_key: 'Ctrl + Right',
|
||||
enable: false
|
||||
},
|
||||
moveUp: {
|
||||
title: '上移',
|
||||
hot_key: 'Ctrl + Up',
|
||||
enable: false
|
||||
},
|
||||
moveDown: {
|
||||
title: '下移',
|
||||
hot_key: 'Ctrl + Down',
|
||||
enable: false
|
||||
},
|
||||
moveBottom: {
|
||||
title: '置底',
|
||||
hot_key: 'Ctrl + Left',
|
||||
enable: false
|
||||
}
|
||||
}
|
||||
},
|
||||
setMenuInfo: (val: IContextMenu['menuInfo']) => {
|
||||
contextMenuStore.menuInfo = val
|
||||
},
|
||||
setDisplayItem: (val: ContextMenuInfoType[]) => {
|
||||
for (const key in contextMenuStore.menuInfo.info) {
|
||||
contextMenuStore.menuInfo.info[key as ContextMenuInfoType].enable = false
|
||||
}
|
||||
val.forEach(f => {
|
||||
contextMenuStore.menuInfo.info[f].enable = true
|
||||
})
|
||||
}
|
||||
})
|
||||
104
src/components/mt-edit/store/global.ts
Normal file
104
src/components/mt-edit/store/global.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { reactive } from 'vue'
|
||||
import type { GlobalStoreIntention, IDoneJson, IGlobalStore, IGlobalStoreCreateItemInfo, IRealTimeData } from './types'
|
||||
export const globalStore: IGlobalStore = reactive({
|
||||
intention: 'none',
|
||||
create_item_info: null,
|
||||
selected_items_id: [],
|
||||
done_json: [],
|
||||
canvasCfg: {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
scale: 1,
|
||||
color: '',
|
||||
img: '',
|
||||
guide: true,
|
||||
adsorp: true,
|
||||
adsorp_diff: 5,
|
||||
transform_origin: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
drag_offset: {
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
},
|
||||
gridCfg: {
|
||||
enabled: true,
|
||||
align: true,
|
||||
size: 10
|
||||
},
|
||||
guideCfg: {
|
||||
x: {
|
||||
display: false,
|
||||
top: 0
|
||||
},
|
||||
y: {
|
||||
display: false,
|
||||
left: 0
|
||||
}
|
||||
},
|
||||
lock: false,
|
||||
real_time_data: {
|
||||
show: false,
|
||||
text: ''
|
||||
},
|
||||
adsorp_diff: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
setIntention: (val: GlobalStoreIntention) => {
|
||||
globalStore.intention = val
|
||||
},
|
||||
setCreateItemInfo: (val: IGlobalStoreCreateItemInfo | null) => {
|
||||
globalStore.create_item_info = val
|
||||
},
|
||||
setGlobalStoreDoneJson: (val: IDoneJson[]) => {
|
||||
globalStore.done_json = val
|
||||
},
|
||||
//取消所有组件选中
|
||||
cancelAllSelect: () => {
|
||||
const done_json_temp = [...globalStore.done_json].map(m => {
|
||||
if (m.active) {
|
||||
m.active = false
|
||||
}
|
||||
return m
|
||||
})
|
||||
globalStore.setGlobalStoreDoneJson(done_json_temp)
|
||||
globalStore.selected_items_id = []
|
||||
},
|
||||
//刷新选中的id
|
||||
refreshSelectedItemsId: () => {
|
||||
globalStore.selected_items_id = globalStore.done_json.filter(m => m.active).map(m => m.id)
|
||||
},
|
||||
//删除选中的组件
|
||||
deleteSelectedItems: () => {
|
||||
const done_json_temp = [...globalStore.done_json].filter(m => !globalStore.selected_items_id.includes(m.id))
|
||||
globalStore.setGlobalStoreDoneJson(done_json_temp)
|
||||
globalStore.selected_items_id = []
|
||||
},
|
||||
// 设置单个选中
|
||||
setSingleSelect: (id: string) => {
|
||||
globalStore.done_json.forEach(m => {
|
||||
if (m.id === id) {
|
||||
m.active = true
|
||||
} else {
|
||||
m.active = false
|
||||
}
|
||||
})
|
||||
globalStore.selected_items_id = [id]
|
||||
},
|
||||
setSelectItems: (ids: string[]) => {
|
||||
globalStore.done_json.forEach(m => {
|
||||
if (ids.includes(m.id)) {
|
||||
m.active = true
|
||||
} else {
|
||||
m.active = false
|
||||
}
|
||||
})
|
||||
globalStore.selected_items_id = ids
|
||||
},
|
||||
setRealTimeData: (val: IRealTimeData) => {
|
||||
globalStore.real_time_data = val
|
||||
}
|
||||
})
|
||||
98
src/components/mt-edit/store/left-aside.ts
Normal file
98
src/components/mt-edit/store/left-aside.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { getCurrentInstance, reactive } from 'vue'
|
||||
import type { ILeftAside, ILeftAsideConfigItemPublic, ILeftAsideConfigItem } from './types'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { svgToSymbol } from '../utils'
|
||||
import { configStore } from './config'
|
||||
import { bingStore } from './bind'
|
||||
|
||||
export const leftAsideStore: ILeftAside = reactive({
|
||||
config: new Map<string, ILeftAsideConfigItem[]>([
|
||||
// ['本地文件', []],
|
||||
['基础图元', configStore.sysComponent],
|
||||
['数据绑定图元', bingStore.sysComponent]
|
||||
]),
|
||||
registerConfig: (title: string, config: ILeftAsideConfigItemPublic[]) => {
|
||||
if (title == '本地文件' || title == '基础图元') {
|
||||
ElMessage.info(`title:${title}已被系统占用,请更换名称!`)
|
||||
return
|
||||
}
|
||||
|
||||
if (leftAsideStore.config.has(title)) {
|
||||
ElMessage.info(`title:${title}已存在,已经将其配置覆盖`)
|
||||
}
|
||||
const cfg: ILeftAsideConfigItem[] = config.map(m => {
|
||||
if (m.type == 'svg') {
|
||||
const { symbol_str, width, height } = svgToSymbol(m.svg!, m.id)
|
||||
return {
|
||||
...m,
|
||||
symbol: {
|
||||
symbol_id: m.id,
|
||||
symbol_str,
|
||||
width,
|
||||
height
|
||||
},
|
||||
common_animations: {
|
||||
val: '',
|
||||
delay: 'delay-0s',
|
||||
speed: 'slow',
|
||||
repeat: 'infinite'
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
...m,
|
||||
common_animations: {
|
||||
val: '',
|
||||
delay: 'delay-0s',
|
||||
speed: 'slow',
|
||||
repeat: 'infinite'
|
||||
}
|
||||
}
|
||||
})
|
||||
leftAsideStore.config.set(title, cfg)
|
||||
},
|
||||
svgPush: (title: string, config: ILeftAsideConfigItemPublic[]) => {
|
||||
const targetConfig = leftAsideStore.config.get(title)
|
||||
if (!targetConfig) {
|
||||
console.warn(`未找到标题为 "${title}" 的配置项`)
|
||||
return
|
||||
}
|
||||
|
||||
const cfg: ILeftAsideConfigItem[] = config.map(m => {
|
||||
if (m.type === 'svg') {
|
||||
const { symbol_str, width, height } = svgToSymbol(m.svg!, m.id)
|
||||
return {
|
||||
...m,
|
||||
symbol: {
|
||||
symbol_id: m.id,
|
||||
symbol_str,
|
||||
width,
|
||||
height
|
||||
},
|
||||
common_animations: {
|
||||
val: '',
|
||||
delay: 'delay-0s',
|
||||
speed: 'slow',
|
||||
repeat: 'infinite'
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
...m,
|
||||
common_animations: {
|
||||
val: '',
|
||||
delay: 'delay-0s',
|
||||
speed: 'slow',
|
||||
repeat: 'infinite'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
targetConfig.push(...cfg)
|
||||
},
|
||||
svgDelete: (title: string, id: string) => {
|
||||
const cfg = leftAsideStore.config.get(title)!.filter(m => m.id !== id)
|
||||
console.log('🚀 ~ cfg:', cfg)
|
||||
leftAsideStore.config.set(title, cfg)
|
||||
}
|
||||
})
|
||||
265
src/components/mt-edit/store/types.ts
Normal file
265
src/components/mt-edit/store/types.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
export type ILeftAsideConfig = Map<string, ILeftAsideConfigItem[]>
|
||||
export type ILeftAsideConfigItemPublicPropsType =
|
||||
| 'input'
|
||||
| 'color'
|
||||
| 'select'
|
||||
| 'switch'
|
||||
| 'number'
|
||||
| 'jsonEdit'
|
||||
| 'textArea'
|
||||
// 开放注册配置
|
||||
export type ILeftAsideConfigItemPublicProps = Record<
|
||||
string,
|
||||
{
|
||||
title: string //显示在属性面板的标题
|
||||
type: ILeftAsideConfigItemPublicPropsType //属性的类型决定了修改属性的方式
|
||||
val: any
|
||||
options?: any //比如说修改属性的时候用到了下拉框,这里面就可以放下拉框的选项
|
||||
disabled?: boolean //如果禁用了将不会显示到右侧属性面板里,但是仍然可以通过代码修改属性
|
||||
}
|
||||
>
|
||||
export type ILeftAsideConfigItemPublicType = 'svg' | 'vue' | 'img' | 'custom-svg'
|
||||
export type ILeftAsideConfigItemPrivateType = 'group' | 'sys-line'
|
||||
export interface ILeftAsideConfigItemPublic {
|
||||
id: string //图形的标识 值必须唯一
|
||||
title: string //要显示的标题,一般用中文表示
|
||||
type: ILeftAsideConfigItemPublicType | ILeftAsideConfigItemPrivateType //图形的类型
|
||||
thumbnail: string //显示到左侧时候的缩略图
|
||||
svg?: string //图形的svg代码
|
||||
props: ILeftAsideConfigItemPublicProps
|
||||
keyId?: string
|
||||
use_proportional_scaling?: boolean //是否使用等比例缩放
|
||||
lineId?: string //监测点id
|
||||
lineList?: [] //监测点id 多层
|
||||
lineName?: string
|
||||
UID?: [] //指标id
|
||||
UIDName?: string
|
||||
UIDNames?: string[]
|
||||
unit?: string[]
|
||||
}
|
||||
export interface ILeftAsideConfigItemPrivateSymbol {
|
||||
symbol_id: string
|
||||
symbol_str: string
|
||||
width: string
|
||||
height: string
|
||||
}
|
||||
export interface ICommonAnimations {
|
||||
val: string
|
||||
delay: string
|
||||
speed: string
|
||||
repeat: string
|
||||
}
|
||||
export interface ILeftAsideConfigItemPrivate {
|
||||
symbol?: ILeftAsideConfigItemPrivateSymbol
|
||||
common_animations: ICommonAnimations
|
||||
}
|
||||
export type ILeftAsideConfigItem = ILeftAsideConfigItemPublic & ILeftAsideConfigItemPrivate
|
||||
|
||||
export type GlobalStoreIntention =
|
||||
| 'none'
|
||||
| 'create'
|
||||
| 'beginMulSelect'
|
||||
| 'adsorbStart'
|
||||
| 'adsorbEnd'
|
||||
| 'beginDragCanvas'
|
||||
| 'runDragCanvas'
|
||||
| 'endDragCanvas'
|
||||
| 'showContextMenu'
|
||||
| 'drawSysLineStart'
|
||||
export interface IGlobalStoreCreateItemInfo {
|
||||
config_key: string //也就是折叠面板的值
|
||||
item_id: string //要创建组件的id
|
||||
}
|
||||
|
||||
export interface IGlobalStoreCanvasCfg {
|
||||
width: number
|
||||
height: number
|
||||
scale: number
|
||||
color: string
|
||||
img: string
|
||||
guide: boolean //参考线
|
||||
adsorp: boolean //吸附
|
||||
adsorp_diff: number
|
||||
// 缩放中心
|
||||
transform_origin: {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
// 拖动偏移量
|
||||
drag_offset: {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
}
|
||||
export interface IGlobalStoreGridCfg {
|
||||
enabled: boolean
|
||||
align: boolean
|
||||
size: number
|
||||
}
|
||||
export type DoneJsonEventListType = 'click' | 'dblclick' | 'mouseover' | 'mouseout'
|
||||
export type DoneJsonEventListAction = 'changeAttr' | 'customCode' | 'pageJump'
|
||||
export interface IDoneJsonActionChangeAttr {
|
||||
id: string
|
||||
target_id: string
|
||||
target_attr: string | undefined
|
||||
target_value: any
|
||||
}
|
||||
export interface IDoneJsonEventList {
|
||||
id: string
|
||||
type: DoneJsonEventListType // 事件类型
|
||||
action: DoneJsonEventListAction // 事件行为
|
||||
jump_to?: string //跳转页面
|
||||
change_attr: IDoneJsonActionChangeAttr[] //属性更改
|
||||
custom_code: string
|
||||
trigger_rule: {
|
||||
trigger_id?: string //触发图形的id
|
||||
trigger_attr?: string //触发图形的属性
|
||||
operator?: string //运算符
|
||||
value?: any //期望值
|
||||
}
|
||||
}
|
||||
export interface IDoneJson {
|
||||
id: string //必须唯一
|
||||
title: string //标题
|
||||
type: ILeftAsideConfigItemPublicType | ILeftAsideConfigItemPrivateType //类型 由配置文件决定
|
||||
symbol?: ILeftAsideConfigItemPrivateSymbol //类型是svg的时候需要用这个将svg转换成symbol
|
||||
binfo: IDoneJsonBinfo
|
||||
props: ILeftAsideConfigItemPublicProps
|
||||
resize: boolean //开启缩放
|
||||
rotate: boolean //开启旋转
|
||||
lock: boolean //锁定
|
||||
active: boolean //激活
|
||||
hide: boolean //隐藏
|
||||
common_animations: ICommonAnimations //通用动画
|
||||
use_proportional_scaling?: boolean //使用等比缩放
|
||||
children?: IDoneJson[]
|
||||
tag?: string
|
||||
thumbnail?: string
|
||||
events: IDoneJsonEventList[]
|
||||
bind?: string //绑定事件
|
||||
keyId?: string
|
||||
lineId?: string //监测点id
|
||||
lineList?: [] //监测点id 多层
|
||||
lineName?: string
|
||||
UID?: [] //指标id
|
||||
UIDName?: string
|
||||
UIDNames?: string[]
|
||||
unit?: string[]
|
||||
}
|
||||
//图形边界信息
|
||||
export interface IDoneJsonBinfo {
|
||||
left: number
|
||||
top: number
|
||||
width: number
|
||||
height: number
|
||||
angle: number
|
||||
}
|
||||
export interface CacheBoundingBox {
|
||||
id: string
|
||||
type: ILeftAsideConfigItemPublicType | ILeftAsideConfigItemPrivateType
|
||||
left: number
|
||||
top: number
|
||||
width: number
|
||||
height: number
|
||||
bottom: number
|
||||
right: number
|
||||
}
|
||||
export type AdsorbPointType = 'tc' | 'bc' | 'lc' | 'rc'
|
||||
export interface IContextMenuInfo {
|
||||
title: string
|
||||
hot_key: string
|
||||
enable: boolean
|
||||
}
|
||||
export type ContextMenuInfoType =
|
||||
| 'copy'
|
||||
| 'paste'
|
||||
| 'delete'
|
||||
| 'group'
|
||||
| 'ungroup'
|
||||
| 'selectAll'
|
||||
| 'moveTop'
|
||||
| 'moveUp'
|
||||
| 'moveDown'
|
||||
| 'moveBottom'
|
||||
export interface IContextMenuDetail {
|
||||
left: number
|
||||
top: number
|
||||
info: {
|
||||
[key in ContextMenuInfoType]: IContextMenuInfo
|
||||
}
|
||||
}
|
||||
export interface IRealTimeData {
|
||||
show: boolean
|
||||
text: string
|
||||
}
|
||||
// 全局状态
|
||||
export interface IGlobalStore {
|
||||
intention: GlobalStoreIntention
|
||||
create_item_info: IGlobalStoreCreateItemInfo | null
|
||||
done_json: IDoneJson[]
|
||||
selected_items_id: string[]
|
||||
canvasCfg: IGlobalStoreCanvasCfg
|
||||
gridCfg: IGlobalStoreGridCfg
|
||||
guideCfg: {
|
||||
x: {
|
||||
display: boolean
|
||||
top: number
|
||||
}
|
||||
y: {
|
||||
display: boolean
|
||||
left: number
|
||||
}
|
||||
}
|
||||
lock: boolean
|
||||
real_time_data: IRealTimeData
|
||||
adsorp_diff: {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
setIntention: (val: GlobalStoreIntention) => void
|
||||
setCreateItemInfo: (val: IGlobalStoreCreateItemInfo | null) => void
|
||||
setGlobalStoreDoneJson: (val: IDoneJson[]) => void
|
||||
cancelAllSelect: () => void
|
||||
refreshSelectedItemsId: () => void
|
||||
deleteSelectedItems: () => void
|
||||
setSingleSelect: (id: string) => void
|
||||
setSelectItems: (ids: string[]) => void
|
||||
setRealTimeData: (val: IRealTimeData) => void
|
||||
}
|
||||
// 左侧配置
|
||||
export interface ILeftAside {
|
||||
config: ILeftAsideConfig
|
||||
registerConfig: (title: string, config: ILeftAsideConfigItemPublic[]) => void
|
||||
svgPush: (title: string, config: ILeftAsideConfigItemPublic[]) => void
|
||||
svgDelete: (title: string, id: string) => void
|
||||
}
|
||||
// 缓存配置
|
||||
export interface ICache {
|
||||
boundingBox: CacheBoundingBox[]
|
||||
setBoundingBox: (val: CacheBoundingBox[]) => void
|
||||
adsorbPoint: { type: AdsorbPointType; x: number; y: number; id: string }[]
|
||||
setAdsorbPoint: (val: { type: AdsorbPointType; x: number; y: number; id: string }[]) => void
|
||||
copy: IDoneJson[]
|
||||
setCopy: (val: IDoneJson[]) => void
|
||||
history: IDoneJson[][]
|
||||
historyIndex: number
|
||||
addHistory: (done_json: IDoneJson[]) => void
|
||||
}
|
||||
// 杂项配置
|
||||
export interface IConfig {
|
||||
sysComponent: ILeftAsideConfigItem[]
|
||||
lineRenderOffset: number //因为连线是使用svg进行渲染的,所以需要一个偏移量和div的画布进行重叠
|
||||
}
|
||||
/**
|
||||
* 右键菜单
|
||||
*/
|
||||
export interface IContextMenu {
|
||||
menuInfo: IContextMenuDetail
|
||||
setMenuInfo: (val: IContextMenuDetail) => void
|
||||
setDisplayItem: (val: ContextMenuInfoType[]) => void
|
||||
}
|
||||
|
||||
export interface IDoneJsonBindList {
|
||||
id: string //图形的标识 值必须唯一
|
||||
title: string //要显示的标题,一般用中文表示
|
||||
}
|
||||
853
src/components/mt-edit/utils/index.ts
Normal file
853
src/components/mt-edit/utils/index.ts
Normal file
@@ -0,0 +1,853 @@
|
||||
import type { MoveItemBoundingInfo } from '../components/render-core/types'
|
||||
import type { CacheBoundingBox, IDoneJson, IDoneJsonBinfo, ILeftAsideConfigItemPublicProps } from '../store/types'
|
||||
import { useUpdateSysLine } from '@/components/mt-edit/composables/sys-line'
|
||||
export const createGroupInfo = (
|
||||
selected_items: IDoneJson[],
|
||||
canvas_dom: HTMLElement,
|
||||
scale_ratio: number
|
||||
): IDoneJson => {
|
||||
//定义组合后的组件信息
|
||||
let min_left = Infinity
|
||||
let min_top = Infinity
|
||||
let max_left = -Infinity
|
||||
let max_top = -Infinity
|
||||
//获取画布的信息
|
||||
const canvas_dom_rect = canvas_dom.getBoundingClientRect()
|
||||
selected_items.forEach(item => {
|
||||
// 获取旋转后left和top
|
||||
const itemRect = document.getElementById(item.id!)!.getBoundingClientRect()
|
||||
// 最小left
|
||||
min_left = Math.min(min_left, (itemRect.left - canvas_dom_rect.left) / scale_ratio)
|
||||
// 最大left
|
||||
max_left = Math.max(max_left, (itemRect.right - canvas_dom_rect.left) / scale_ratio)
|
||||
// 最小top
|
||||
min_top = Math.min(min_top, (itemRect.top - canvas_dom_rect.top) / scale_ratio)
|
||||
// 最大top
|
||||
max_top = Math.max(max_top, (itemRect.bottom - canvas_dom_rect.top) / scale_ratio)
|
||||
})
|
||||
//定义组合元素的边界信息
|
||||
const group_binfo = {
|
||||
left: min_left,
|
||||
top: min_top,
|
||||
width: max_left - min_left,
|
||||
height: max_top - min_top,
|
||||
angle: 0
|
||||
}
|
||||
// 计算子元素相对父元素的位置
|
||||
selected_items.forEach(item => {
|
||||
item.binfo.left = item.binfo.left! - min_left
|
||||
item.binfo.top = item.binfo.top! - min_top
|
||||
item.binfo = {
|
||||
width: 100 * (item.binfo.width / group_binfo.width),
|
||||
height: 100 * (item.binfo.height / group_binfo.height),
|
||||
left: 100 * (item.binfo.left / group_binfo.width),
|
||||
top: 100 * (item.binfo.top / group_binfo.height),
|
||||
angle: item.binfo.angle || 0
|
||||
}
|
||||
item.active = false
|
||||
})
|
||||
|
||||
// 组合组件信息
|
||||
return {
|
||||
id: 'group-' + randomString(),
|
||||
title: '组合',
|
||||
type: 'group',
|
||||
binfo: group_binfo,
|
||||
resize: true,
|
||||
rotate: true,
|
||||
lock: false,
|
||||
active: true,
|
||||
hide: false,
|
||||
use_proportional_scaling: true,
|
||||
props: {},
|
||||
common_animations: {
|
||||
val: '',
|
||||
delay: 'delay-0s',
|
||||
speed: 'slow',
|
||||
repeat: 'infinite'
|
||||
},
|
||||
children: [...selected_items],
|
||||
events: [],
|
||||
tag: 'group'
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 取消组合
|
||||
* @param elements 元素列表
|
||||
* @param editorRect 画布react信息
|
||||
* @returns 拆分后的列表
|
||||
*/
|
||||
export const cancelGroup = (
|
||||
selected_item: IDoneJson,
|
||||
canvas_dom: HTMLElement,
|
||||
scale_ratio: number,
|
||||
grid_align_size: number
|
||||
) => {
|
||||
//获取画布的信息
|
||||
const canvas_dom_rect = canvas_dom.getBoundingClientRect()
|
||||
// 获取组合元素的子元素列表
|
||||
const split_items = selected_item.children!.map(item => {
|
||||
// 子组件相对于浏览器视口位置大小
|
||||
const itemRect = document.getElementById(item.id!)!.getBoundingClientRect()
|
||||
// 获取元素的中心点坐标
|
||||
const center = {
|
||||
x: itemRect.left - canvas_dom_rect.left + itemRect.width / 2,
|
||||
y: itemRect.top - canvas_dom_rect.top + itemRect.height / 2
|
||||
}
|
||||
// 拆分后的宽高
|
||||
const width = alignToGrid(selected_item.binfo.width * (item.binfo.width / 100), grid_align_size)
|
||||
const height = alignToGrid(selected_item.binfo.height * (item.binfo.height / 100), grid_align_size)
|
||||
// 根据拆分后的宽高计算边界信息
|
||||
const binfo = {
|
||||
width,
|
||||
height,
|
||||
left: center.x / scale_ratio - width / 2,
|
||||
top: center.y / scale_ratio - height / 2,
|
||||
angle: (item.binfo.angle || 0) + (selected_item.binfo.angle || 0)
|
||||
}
|
||||
//让拆分后的图形处于选中状态
|
||||
return {
|
||||
...item,
|
||||
active: true,
|
||||
binfo
|
||||
}
|
||||
})
|
||||
return split_items
|
||||
}
|
||||
export const svgToSymbol = (svgStr: string, id: string) => {
|
||||
const svgDocument = new DOMParser().parseFromString(svgStr, 'image/svg+xml').children[0]
|
||||
let width = '0'
|
||||
let height = '0'
|
||||
const viewBox = svgDocument.getAttribute('viewBox')
|
||||
const symbol = document.createElementNS('http://www.w3.org/2000/svg', 'symbol')
|
||||
if (viewBox) {
|
||||
const [, , w, h] = viewBox.split(' ')
|
||||
symbol.setAttributeNS(null, 'viewBox', viewBox)
|
||||
width = w
|
||||
height = h
|
||||
} else {
|
||||
width = svgDocument.getAttribute('width') || '0'
|
||||
height = svgDocument.getAttribute('height') || '0'
|
||||
symbol.setAttributeNS(null, 'viewBox', '0 0 ' + width + ' ' + height)
|
||||
}
|
||||
symbol.setAttributeNS(null, 'id', id)
|
||||
symbol.innerHTML = svgDocument.innerHTML
|
||||
.replaceAll('stroke:currentColor', '')
|
||||
.replaceAll('stroke: currentColor', '')
|
||||
.replaceAll('stroke="currentColor"', '')
|
||||
return { symbol_str: symbol.outerHTML, width, height }
|
||||
}
|
||||
export const symbolGenSvg = (
|
||||
symbol_id: string,
|
||||
symbol_str: string,
|
||||
width: string,
|
||||
height: string,
|
||||
props_str: string
|
||||
) => {
|
||||
return `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
version="1.1"
|
||||
width="${width}"
|
||||
height="${height}"
|
||||
>
|
||||
${symbol_str}
|
||||
<use
|
||||
xlink:href="#${symbol_id}"
|
||||
${props_str}
|
||||
x="0"
|
||||
y="0"
|
||||
></use>
|
||||
</svg>
|
||||
`
|
||||
}
|
||||
export const svgToImgSrc = (svgStr: string) => {
|
||||
return 'data:image/svg+xml;utf8,' + encodeURIComponent(svgStr)
|
||||
}
|
||||
/**
|
||||
* 生成dom可用的属性字符串
|
||||
* @param props
|
||||
* @returns
|
||||
*/
|
||||
export const genDomPropstr = (props: ILeftAsideConfigItemPublicProps) => {
|
||||
let res = ''
|
||||
for (const key in props) {
|
||||
res += ` ${key}="${props[key].val}"`
|
||||
}
|
||||
return res
|
||||
}
|
||||
/**
|
||||
* 生成随机字符串
|
||||
* @param len 生成个数
|
||||
*/
|
||||
export const randomString = (len?: number) => {
|
||||
len = len || 10
|
||||
const str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
const maxPos = str.length
|
||||
let random_str = ''
|
||||
for (let i = 0; i < len; i++) {
|
||||
random_str += str.charAt(Math.floor(Math.random() * maxPos))
|
||||
}
|
||||
return random_str
|
||||
}
|
||||
function isTouchEvent(val: unknown): val is TouchEvent {
|
||||
const typeStr = Object.prototype.toString.call(val)
|
||||
return typeStr.substring(8, typeStr.length - 1) === 'TouchEvent'
|
||||
}
|
||||
/**
|
||||
* 获取当前点击坐标 根据pc端和移动端获取
|
||||
* @param e
|
||||
* @returns
|
||||
*/
|
||||
export function getRealityXY(e: DragEvent | TouchEvent | MouseEvent, canvas_dom_rect: DOMRect | undefined) {
|
||||
let realityX = 0,
|
||||
realityY = 0
|
||||
if (isTouchEvent(e)) {
|
||||
const touch = e.targetTouches[0]
|
||||
realityX = canvas_dom_rect ? touch.pageX - canvas_dom_rect.x : 0
|
||||
realityY = canvas_dom_rect ? touch.pageY - canvas_dom_rect.y : 0
|
||||
} else {
|
||||
realityX = canvas_dom_rect ? e.clientX - canvas_dom_rect.x : e.clientX
|
||||
realityY = canvas_dom_rect ? e.clientY - canvas_dom_rect.y : e.clientY
|
||||
}
|
||||
|
||||
return { realityX, realityY }
|
||||
}
|
||||
export const blobToBase64 = (file: Blob) => {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const reader = new FileReader()
|
||||
let img_src: string | ArrayBuffer = ''
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = function () {
|
||||
img_src = reader.result ?? ''
|
||||
}
|
||||
reader.onerror = function (error) {
|
||||
reject(error)
|
||||
}
|
||||
reader.onloadend = function () {
|
||||
resolve(img_src)
|
||||
}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 根据坐标对齐到网格
|
||||
* @param position 当前坐标
|
||||
* @param grid 网格大小
|
||||
* @returns 对应网格的坐标
|
||||
*/
|
||||
export const alignToGrid = (position: number, grid = 1) => {
|
||||
const integerPart = Math.floor(position / grid)
|
||||
const fractionalPart = position % grid
|
||||
|
||||
if (fractionalPart >= grid / 2) {
|
||||
return (integerPart + 1) * grid
|
||||
} else {
|
||||
return integerPart * grid
|
||||
}
|
||||
}
|
||||
/**
|
||||
* json对象深拷贝
|
||||
* @param object
|
||||
* @param default_val
|
||||
* @returns
|
||||
*/
|
||||
export const objectDeepClone = <T>(object: object, default_val: any = {}) => {
|
||||
if (!object) {
|
||||
return default_val as T
|
||||
}
|
||||
return JSON.parse(JSON.stringify(object)) as T
|
||||
}
|
||||
export const prosToVBind = (item: ILeftAsideConfigItemPublicProps) => {
|
||||
let temp = {}
|
||||
for (const key in item) {
|
||||
temp = { ...temp, ...{ [key]: item[key].val } }
|
||||
}
|
||||
return temp
|
||||
}
|
||||
/**
|
||||
* 计算y轴参考线属性和需要吸附的偏移距离
|
||||
* @param cacheStore_boundingBox
|
||||
* @param adsorp_diff
|
||||
* @param move_item_bounding_info
|
||||
*/
|
||||
export const calculateGuideY = (
|
||||
cacheStore_boundingBox: CacheBoundingBox[],
|
||||
adsorp_diff: number,
|
||||
move_item_bounding_info: MoveItemBoundingInfo[],
|
||||
canvas_bounding_info: DOMRect,
|
||||
scale: number
|
||||
) => {
|
||||
for (let index = 0; index < move_item_bounding_info.length; index++) {
|
||||
// 拿到移动时的边界信息
|
||||
const { left, right, width } = move_item_bounding_info[index]
|
||||
// 左左 左中 左右
|
||||
const ll = cacheStore_boundingBox.find(f => Math.abs(f.left - left) < adsorp_diff)
|
||||
const lc = cacheStore_boundingBox.find(f => Math.abs(f.left - (left + width / 2)) < adsorp_diff)
|
||||
const lr = cacheStore_boundingBox.find(f => Math.abs(f.left - right) < adsorp_diff)
|
||||
// 中左 中中 中右
|
||||
const cl = cacheStore_boundingBox.find(f => Math.abs(f.left + f.width / 2 - left) < adsorp_diff)
|
||||
const cc = cacheStore_boundingBox.find(f => Math.abs(f.left + f.width / 2 - (left + width / 2)) < adsorp_diff)
|
||||
const cr = cacheStore_boundingBox.find(f => Math.abs(f.left + f.width / 2 - right) < adsorp_diff)
|
||||
// 右左 右中 右右
|
||||
const rr = cacheStore_boundingBox.find(f => Math.abs(f.right - right) < adsorp_diff)
|
||||
const rc = cacheStore_boundingBox.find(f => Math.abs(f.right - (left + width / 2)) < adsorp_diff)
|
||||
const rl = cacheStore_boundingBox.find(f => Math.abs(f.right - left) < adsorp_diff)
|
||||
if (ll) {
|
||||
return {
|
||||
y_info: {
|
||||
display: true,
|
||||
left: (ll.left - canvas_bounding_info.left) / scale
|
||||
},
|
||||
move_x: ll.left - left
|
||||
}
|
||||
} else if (lc) {
|
||||
return {
|
||||
y_info: {
|
||||
display: true,
|
||||
left: (lc.left - canvas_bounding_info.left) / scale
|
||||
},
|
||||
move_x: lc.left - (left + width / 2)
|
||||
}
|
||||
} else if (lr) {
|
||||
return {
|
||||
y_info: {
|
||||
display: true,
|
||||
left: (lr.left - canvas_bounding_info.left) / scale
|
||||
},
|
||||
move_x: lr.left - right
|
||||
}
|
||||
} else if (cl) {
|
||||
return {
|
||||
y_info: {
|
||||
display: true,
|
||||
left: (cl.left + cl.width / 2 - canvas_bounding_info.left) / scale
|
||||
},
|
||||
move_x: cl.left + cl.width / 2 - left
|
||||
}
|
||||
} else if (cc) {
|
||||
return {
|
||||
y_info: {
|
||||
display: true,
|
||||
left: (cc.left + cc.width / 2 - canvas_bounding_info.left) / scale
|
||||
},
|
||||
move_x: cc.left + cc.width / 2 - (left + width / 2)
|
||||
}
|
||||
} else if (cr) {
|
||||
return {
|
||||
y_info: {
|
||||
display: true,
|
||||
left: (cr.left + cr.width / 2 - canvas_bounding_info.left) / scale
|
||||
},
|
||||
move_x: cr.left + cr.width / 2 - right
|
||||
}
|
||||
} else if (rl) {
|
||||
return {
|
||||
y_info: {
|
||||
display: true,
|
||||
left: (rl.right - canvas_bounding_info.left) / scale
|
||||
},
|
||||
move_x: rl.right - left
|
||||
}
|
||||
} else if (rc) {
|
||||
return {
|
||||
y_info: {
|
||||
display: true,
|
||||
left: (rc.right - canvas_bounding_info.left) / scale
|
||||
},
|
||||
move_x: rc.right - (left + width / 2)
|
||||
}
|
||||
} else if (rr) {
|
||||
return {
|
||||
y_info: {
|
||||
display: true,
|
||||
left: (rr.right - canvas_bounding_info.left) / scale
|
||||
},
|
||||
move_x: rr.right - right
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
y_info: {
|
||||
display: false,
|
||||
left: 0
|
||||
},
|
||||
move_x: 0
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 计算x轴参考线属性和需要吸附的偏移距离
|
||||
* @param cacheStore_boundingBox
|
||||
* @param adsorp_diff
|
||||
* @param move_item_bounding_info
|
||||
*/
|
||||
export const calculateGuideX = (
|
||||
cacheStore_boundingBox: CacheBoundingBox[],
|
||||
adsorp_diff: number,
|
||||
move_item_bounding_info: MoveItemBoundingInfo[],
|
||||
canvas_bounding_info: DOMRect,
|
||||
scale: number
|
||||
) => {
|
||||
for (let index = 0; index < move_item_bounding_info.length; index++) {
|
||||
// 拿到移动时的边界信息
|
||||
const { top, bottom, height } = move_item_bounding_info[index]
|
||||
// 上上 上中 上下
|
||||
const tt = cacheStore_boundingBox.find(f => Math.abs(f.top - top) < adsorp_diff)
|
||||
const tc = cacheStore_boundingBox.find(f => Math.abs(f.top - (top + height / 2)) < adsorp_diff)
|
||||
const tb = cacheStore_boundingBox.find(f => Math.abs(f.top - bottom) < adsorp_diff)
|
||||
// 中上 中中 中下
|
||||
const ct = cacheStore_boundingBox.find(f => Math.abs(f.top + f.height / 2 - top) < adsorp_diff)
|
||||
const cc = cacheStore_boundingBox.find(f => Math.abs(f.top + f.height / 2 - (top + height / 2)) < adsorp_diff)
|
||||
const cb = cacheStore_boundingBox.find(f => Math.abs(f.top + f.height / 2 - bottom) < adsorp_diff)
|
||||
// 下上 下中 下右
|
||||
const bt = cacheStore_boundingBox.find(f => Math.abs(f.bottom - bottom) < adsorp_diff)
|
||||
const bc = cacheStore_boundingBox.find(f => Math.abs(f.bottom - (top + height / 2)) < adsorp_diff)
|
||||
const br = cacheStore_boundingBox.find(f => Math.abs(f.bottom - top) < adsorp_diff)
|
||||
if (tt) {
|
||||
return {
|
||||
x_info: {
|
||||
display: true,
|
||||
top: (tt.top - canvas_bounding_info.top) / scale
|
||||
},
|
||||
move_y: tt.top - top
|
||||
}
|
||||
} else if (tc) {
|
||||
return {
|
||||
x_info: {
|
||||
display: true,
|
||||
top: (tc.top - canvas_bounding_info.top) / scale
|
||||
},
|
||||
move_y: tc.top - (top + height / 2)
|
||||
}
|
||||
} else if (tb) {
|
||||
return {
|
||||
x_info: {
|
||||
display: true,
|
||||
top: (tb.top - canvas_bounding_info.top) / scale
|
||||
},
|
||||
move_y: tb.top - bottom
|
||||
}
|
||||
} else if (ct) {
|
||||
return {
|
||||
x_info: {
|
||||
display: true,
|
||||
top: (ct.top + ct.height / 2 - canvas_bounding_info.top) / scale
|
||||
},
|
||||
move_y: ct.top + ct.height / 2 - top
|
||||
}
|
||||
} else if (cc) {
|
||||
return {
|
||||
x_info: {
|
||||
display: true,
|
||||
top: (cc.top + cc.height / 2 - canvas_bounding_info.top) / scale
|
||||
},
|
||||
move_y: cc.top + cc.height / 2 - (top + height / 2)
|
||||
}
|
||||
} else if (cb) {
|
||||
return {
|
||||
x_info: {
|
||||
display: true,
|
||||
top: (cb.top + cb.height / 2 - canvas_bounding_info.top) / scale
|
||||
},
|
||||
move_y: cb.top + cb.height / 2 - bottom
|
||||
}
|
||||
} else if (br) {
|
||||
return {
|
||||
x_info: {
|
||||
display: true,
|
||||
top: (br.bottom - canvas_bounding_info.top) / scale
|
||||
},
|
||||
move_y: br.bottom - top
|
||||
}
|
||||
} else if (bc) {
|
||||
return {
|
||||
x_info: {
|
||||
display: true,
|
||||
top: (bc.bottom - canvas_bounding_info.top) / scale
|
||||
},
|
||||
move_y: bc.bottom - (top + height / 2)
|
||||
}
|
||||
} else if (bt) {
|
||||
return {
|
||||
x_info: {
|
||||
display: true,
|
||||
top: (bt.bottom - canvas_bounding_info.top) / scale
|
||||
},
|
||||
move_y: bt.bottom - bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
x_info: {
|
||||
display: false,
|
||||
top: 0
|
||||
},
|
||||
move_y: 0
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 坐标数组转换成path路径
|
||||
* @param position_arr
|
||||
* @returns
|
||||
*/
|
||||
export const positionArrarToPath = (position_arr: { x: number; y: number }[], offset_x = 0, offset_y = 0) => {
|
||||
let path_str = ''
|
||||
for (let index = 0; index < position_arr.length; index++) {
|
||||
if (index === 0) {
|
||||
path_str += `M ${position_arr[index].x + offset_x} ${position_arr[index].y + offset_y}`
|
||||
} else {
|
||||
path_str += ` L ${position_arr[index].x + offset_x} ${position_arr[index].y + offset_y}`
|
||||
}
|
||||
}
|
||||
return path_str
|
||||
}
|
||||
/**
|
||||
* 取两点之间坐标
|
||||
* @param x1
|
||||
* @param y1
|
||||
* @param x2
|
||||
* @param y2
|
||||
* @returns
|
||||
*/
|
||||
export const getCenterXY = (x1: number, y1: number, x2: number, y2: number) => {
|
||||
return {
|
||||
x: (x1 + x2) / 2,
|
||||
y: (y1 + y2) / 2
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 计算旋转之后的坐标
|
||||
* @param x 旋转之前x坐标
|
||||
* @param y 旋转之前y坐标
|
||||
* @param centerX 旋转中心x坐标
|
||||
* @param centerY 旋转中心y坐标
|
||||
* @param angleRad 旋转角度
|
||||
* @returns 旋转之后的xy坐标
|
||||
*/
|
||||
export const rotatePoint = (x: number, y: number, centerX: number, centerY: number, angleRad: number) => {
|
||||
const newX = centerX + (x - centerX) * Math.cos(angleRad) - (y - centerY) * Math.sin(angleRad)
|
||||
const newY = centerY + (x - centerX) * Math.sin(angleRad) + (y - centerY) * Math.cos(angleRad)
|
||||
return { x: newX, y: newY }
|
||||
}
|
||||
// 获取四角坐标
|
||||
export const getRectCoordinate = (item: IDoneJsonBinfo) => {
|
||||
const topLeft = { x: item.left, y: item.top }
|
||||
const topRight = { x: item.left + item.width, y: item.top }
|
||||
const bottomLeft = { x: item.left, y: item.top + item.height }
|
||||
const bottomRight = {
|
||||
x: item.left + item.width,
|
||||
y: item.top + item.height
|
||||
}
|
||||
return {
|
||||
topLeft,
|
||||
topRight,
|
||||
bottomLeft,
|
||||
bottomRight
|
||||
}
|
||||
}
|
||||
//获取四条边中点坐标
|
||||
export const getRectCenterCoordinate = (
|
||||
topLeft: { x: any; y: any },
|
||||
topRight: { x: any; y: any },
|
||||
bottomLeft: { x: any; y: any },
|
||||
bottomRight: { x: any; y: any }
|
||||
) => {
|
||||
const topCenter = {
|
||||
x: (topLeft.x + topRight.x) / 2,
|
||||
y: (topLeft.y + topRight.y) / 2
|
||||
}
|
||||
const bottomCenter = {
|
||||
x: (bottomLeft.x + bottomRight.x) / 2,
|
||||
y: (bottomLeft.y + bottomRight.y) / 2
|
||||
}
|
||||
const leftCenter = {
|
||||
x: (topLeft.x + bottomLeft.x) / 2,
|
||||
y: (topLeft.y + bottomLeft.y) / 2
|
||||
}
|
||||
const rightCenter = {
|
||||
x: (topRight.x + bottomRight.x) / 2,
|
||||
y: (topRight.y + bottomRight.y) / 2
|
||||
}
|
||||
return {
|
||||
topCenter,
|
||||
bottomCenter,
|
||||
leftCenter,
|
||||
rightCenter
|
||||
}
|
||||
}
|
||||
|
||||
export const handleAlign = (
|
||||
type:
|
||||
| 'left'
|
||||
| 'horizontally'
|
||||
| 'right'
|
||||
| 'top'
|
||||
| 'vertically'
|
||||
| 'bottom'
|
||||
| 'horizontal-distribution'
|
||||
| 'vertical-distribution',
|
||||
selected_done_json: IDoneJson[],
|
||||
canvasDom: HTMLElement,
|
||||
scale: number,
|
||||
global_done_json: IDoneJson[]
|
||||
) => {
|
||||
switch (type) {
|
||||
case 'left': {
|
||||
// 取出最左边的元素 记录最左边的坐标
|
||||
const left_x = Math.min(...selected_done_json.filter(f => f.type !== 'sys-line').map(m => m.binfo.left))
|
||||
// 将所有元素的坐标都设置成最左边
|
||||
selected_done_json
|
||||
.filter(f => f.type !== 'sys-line')
|
||||
.forEach(m => {
|
||||
m.binfo.left = left_x
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'horizontally': {
|
||||
// 取出第一个元素的中点坐标 将其余元素的中点坐标都设置成这个
|
||||
const center_x =
|
||||
selected_done_json.filter(f => f.type !== 'sys-line')[0].binfo.left +
|
||||
selected_done_json[0].binfo.width / 2
|
||||
selected_done_json
|
||||
.filter(f => f.type !== 'sys-line')
|
||||
.forEach(m => {
|
||||
m.binfo.left = center_x - m.binfo.width / 2
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'right': {
|
||||
// 取出最右边的元素 记录最右边的坐标
|
||||
const right_x = Math.max(
|
||||
...selected_done_json.filter(f => f.type !== 'sys-line').map(m => m.binfo.left + m.binfo.width)
|
||||
)
|
||||
// 将所有元素的坐标都设置成最右边
|
||||
selected_done_json
|
||||
.filter(f => f.type !== 'sys-line')
|
||||
.forEach(m => {
|
||||
m.binfo.left = right_x - m.binfo.width
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'top': {
|
||||
// 取出最上边的元素 记录最上边的坐标
|
||||
const top_y = Math.min(...selected_done_json.filter(f => f.type !== 'sys-line').map(m => m.binfo.top))
|
||||
// 将所有元素的坐标都设置成最上边
|
||||
selected_done_json
|
||||
.filter(f => f.type !== 'sys-line')
|
||||
.forEach(m => {
|
||||
m.binfo.top = top_y
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'vertically': {
|
||||
// 取出第一个元素的中点坐标 将其余元素的中点坐标都设置成这个
|
||||
const center_y =
|
||||
selected_done_json.filter(f => f.type !== 'sys-line')[0].binfo.top +
|
||||
selected_done_json[0].binfo.height / 2
|
||||
selected_done_json
|
||||
.filter(f => f.type !== 'sys-line')
|
||||
.forEach(m => {
|
||||
m.binfo.top = center_y - m.binfo.height / 2
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'bottom': {
|
||||
// 取出最下边的元素 记录最下边的坐标
|
||||
const bottom_y = Math.max(
|
||||
...selected_done_json.filter(f => f.type !== 'sys-line').map(m => m.binfo.top + m.binfo.height)
|
||||
)
|
||||
// 将所有元素的坐标都设置成最下边
|
||||
selected_done_json
|
||||
.filter(f => f.type !== 'sys-line')
|
||||
.forEach(m => {
|
||||
m.binfo.top = bottom_y - m.binfo.height
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'horizontal-distribution': {
|
||||
// 将选中的元素按照水平方向中点坐标从小到大排序
|
||||
selected_done_json.sort((a, b) => a.binfo.left + a.binfo.width / 2 - b.binfo.left + b.binfo.width / 2)
|
||||
const max_info = selected_done_json[selected_done_json.length - 1]
|
||||
const min_info = selected_done_json[0]
|
||||
const point_interval_x =
|
||||
(max_info.binfo.left + max_info.binfo.width / 2 - (min_info.binfo.left + min_info.binfo.width / 2)) /
|
||||
(selected_done_json.length - 1)
|
||||
selected_done_json.forEach((f, index) => {
|
||||
if (index == 0 || index == selected_done_json.length - 1) {
|
||||
return
|
||||
}
|
||||
const new_x = min_info.binfo.left + min_info.binfo.width / 2 + point_interval_x * index
|
||||
f.binfo = {
|
||||
...f.binfo,
|
||||
left: new_x - f.binfo.width / 2
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'vertical-distribution': {
|
||||
// 将选中的元素按照垂直方向中点坐标从小到大排序
|
||||
selected_done_json.sort((a, b) => a.binfo.top + a.binfo.height / 2 - b.binfo.top + b.binfo.height / 2)
|
||||
const max_info = selected_done_json[selected_done_json.length - 1]
|
||||
const min_info = selected_done_json[0]
|
||||
const point_interval_y =
|
||||
(max_info.binfo.top + max_info.binfo.height / 2 - (min_info.binfo.top + min_info.binfo.height / 2)) /
|
||||
(selected_done_json.length - 1)
|
||||
selected_done_json.forEach((f, index) => {
|
||||
if (index == 0 || index == selected_done_json.length - 1) {
|
||||
return
|
||||
}
|
||||
const new_y = min_info.binfo.top + min_info.binfo.height / 2 + point_interval_y * index
|
||||
f.binfo = {
|
||||
...f.binfo,
|
||||
top: new_y - f.binfo.height / 2
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
// 更新绑定连线
|
||||
const sys_lines = global_done_json.filter(f => f.type === 'sys-line')
|
||||
useUpdateSysLine(sys_lines, selected_done_json, canvasDom, scale)
|
||||
return selected_done_json
|
||||
}
|
||||
/**
|
||||
* 设置图形属性
|
||||
* @param id
|
||||
* @param key
|
||||
* @param val
|
||||
* @param json_arr
|
||||
* @returns
|
||||
*/
|
||||
export const setItemAttr = (id: string, key: string, val: any, json_arr: IDoneJson[]) => {
|
||||
return new Promise(res => {
|
||||
const find_item = json_arr.find(f => f.id === id)
|
||||
if (!find_item) {
|
||||
res({
|
||||
status: false,
|
||||
msg: '要设置的id不存在'
|
||||
})
|
||||
}
|
||||
eval(`find_item.${key} = val;`)
|
||||
res({
|
||||
status: true,
|
||||
msg: '操作成功'
|
||||
})
|
||||
})
|
||||
}
|
||||
export const getItemAttr = (id: string, key: string, json_arr: IDoneJson[]) => {
|
||||
const find_item = json_arr.find(f => f.id === id)
|
||||
if (!find_item) {
|
||||
return null
|
||||
}
|
||||
return eval(`find_item.${key}`)
|
||||
}
|
||||
export const previewCompareVal = (val1: any, operator: '>' | '<' | '=' | '!=', val2: any) => {
|
||||
if (operator === '=') {
|
||||
return String(val1) == String(val2)
|
||||
} else if (operator === '>') {
|
||||
return Number(val1) > Number(val2)
|
||||
} else if (operator === '<') {
|
||||
return Number(val1) < Number(val2)
|
||||
} else if (operator === '!=') {
|
||||
return String(val1) != String(val2)
|
||||
}
|
||||
return false
|
||||
}
|
||||
/**
|
||||
* 将事件转换成v-on
|
||||
* @param item
|
||||
* @returns
|
||||
*/
|
||||
export const eventToVOn = (item: IDoneJson, externalMethod: (kid?: string) => void) => {
|
||||
const event_obj: Record<string, string> = {}
|
||||
item.events.forEach(event => {
|
||||
let code_str = ''
|
||||
if (event.action === 'changeAttr') {
|
||||
if (event.change_attr.length < 1) {
|
||||
return
|
||||
}
|
||||
event.change_attr.forEach(attr => {
|
||||
if (!attr.target_id || !attr.target_attr || attr.target_value === undefined) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
!event.trigger_rule ||
|
||||
!event.trigger_rule.trigger_id ||
|
||||
!event.trigger_rule.trigger_attr ||
|
||||
event.trigger_rule.value === undefined ||
|
||||
!event.trigger_rule.operator
|
||||
) {
|
||||
if (typeof attr.target_value == 'boolean') {
|
||||
code_str += `$setItemAttrByID('${attr.target_id}', '${attr.target_attr}', ${attr.target_value});`
|
||||
} else {
|
||||
code_str += `$setItemAttrByID('${attr.target_id}', '${attr.target_attr}', '${attr.target_value}');`
|
||||
}
|
||||
} else {
|
||||
if (typeof attr.target_value == 'boolean') {
|
||||
code_str += `if($previewCompareVal($getItemAttrByID('${event.trigger_rule.trigger_id}', '${event.trigger_rule.trigger_attr}'), '${event.trigger_rule.operator}', '${event.trigger_rule.value}')){$setItemAttrByID('${attr.target_id}', '${attr.target_attr}', ${attr.target_value})};`
|
||||
} else {
|
||||
code_str += `if($previewCompareVal($getItemAttrByID('${event.trigger_rule.trigger_id}', '${event.trigger_rule.trigger_attr}'), '${event.trigger_rule.operator}', '${event.trigger_rule.value}')){$setItemAttrByID('${attr.target_id}', '${attr.target_attr}', '${attr.target_value}')};`
|
||||
}
|
||||
}
|
||||
})
|
||||
} else if (event.action === 'customCode') {
|
||||
if (
|
||||
!event.trigger_rule ||
|
||||
!event.trigger_rule.trigger_id ||
|
||||
!event.trigger_rule.trigger_attr ||
|
||||
event.trigger_rule.value === undefined ||
|
||||
!event.trigger_rule.operator
|
||||
) {
|
||||
code_str += event.custom_code + ';'
|
||||
} else {
|
||||
code_str += `if($previewCompareVal($getItemAttrByID('${event.trigger_rule.trigger_id}', '${event.trigger_rule.trigger_attr}'), '${event.trigger_rule.operator}', '${event.trigger_rule.value}')){${event.custom_code}};`
|
||||
}
|
||||
} else if (event.action === 'pageJump') {
|
||||
// 页面跳转逻辑
|
||||
if (
|
||||
!event.trigger_rule ||
|
||||
!event.trigger_rule.trigger_id ||
|
||||
!event.trigger_rule.trigger_attr ||
|
||||
event.trigger_rule.value === undefined ||
|
||||
!event.trigger_rule.operator
|
||||
) {
|
||||
code_str += event.jump_to
|
||||
}
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(event_obj, event.type)) {
|
||||
event_obj[event.type] = code_str
|
||||
} else {
|
||||
event_obj[event.type] += code_str
|
||||
}
|
||||
})
|
||||
let on_event = {}
|
||||
|
||||
for (const event_key in event_obj) {
|
||||
on_event = {
|
||||
...on_event,
|
||||
...{
|
||||
[event_key]: () => {
|
||||
// 抛出跳转页面的kid出去
|
||||
externalMethod(event_obj[event_key])
|
||||
} //dynamicEvent(event_obj[event_key])(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
return on_event
|
||||
}
|
||||
/**
|
||||
* 创建动态事件,可以根据$item_info获取当前图形信息
|
||||
* @param code_str
|
||||
* @returns
|
||||
*/
|
||||
const dynamicEvent = (code_str: string) => {
|
||||
try {
|
||||
return new Function('$item_info', code_str)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return new Function('$item_info', `console.error('${error}')`)
|
||||
}
|
||||
}
|
||||
3
src/components/mt-preview/index.ts
Normal file
3
src/components/mt-preview/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import MtPreview from './index.vue'
|
||||
|
||||
export default MtPreview
|
||||
1156
src/components/mt-preview/index.vue
Normal file
1156
src/components/mt-preview/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
43
src/components/test/custom-demo/index.vue
Normal file
43
src/components/test/custom-demo/index.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<svg
|
||||
width="557"
|
||||
height="554"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
version="1.1"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
:fill="props.circleFill"
|
||||
d="m279,38.19467a238.80533,238.80533 0 1 1 -238.80533,238.80533a238.80533,238.80533 0 0 1 238.80533,-238.80533m0,-38.528a277.33333,277.33333 0 1 0 277.33333,277.33333a277.33333,277.33333 0 0 0 -277.33333,-277.33333z"
|
||||
id="circle1"
|
||||
/>
|
||||
<path
|
||||
id="line1"
|
||||
:fill="props.pathFill1"
|
||||
d="m436.15733,143.928a14.67733,14.67733 0 0 0 -12.8,7.44533a96,96 0 0 1 -137.10933,24.66134a125.09867,125.09867 0 0 0 -188.032,24.14933a14.208,14.208 0 0 0 -1.28,2.13333a5.80267,5.80267 0 0 0 -0.66133,1.408a14.656,14.656 0 0 0 24.14933,15.68a6.272,6.272 0 0 0 1.28,-1.49333a96.832,96.832 0 0 1 13.248,-16.49067a95.76533,95.76533 0 0 1 124.62933,-9.30133a125.07733,125.07733 0 0 0 189.26934,-26.176a5.824,5.824 0 0 0 0.68266,-1.49333a14.63467,14.63467 0 0 0 -13.44,-20.43734l0.064,-0.08533z"
|
||||
/>
|
||||
<path
|
||||
v-if="props.showLine2"
|
||||
id="line2"
|
||||
:fill="props.pathFill2"
|
||||
d="m440.15733,239.928a14.67733,14.67733 0 0 0 -12.8,7.44533a96,96 0 0 1 -137.10933,24.66134a125.09867,125.09867 0 0 0 -188.032,24.14933a14.208,14.208 0 0 0 -1.28,2.13333a5.80267,5.80267 0 0 0 -0.66133,1.408a14.656,14.656 0 0 0 24.14933,15.68a6.272,6.272 0 0 0 1.28,-1.49333a96.832,96.832 0 0 1 13.248,-16.49067a95.76533,95.76533 0 0 1 124.62933,-9.30133a125.07733,125.07733 0 0 0 189.26934,-26.176a5.824,5.824 0 0 0 0.68266,-1.49333a14.63467,14.63467 0 0 0 -13.44,-20.43734l0.064,-0.08533z"
|
||||
/>
|
||||
<path
|
||||
id="line3"
|
||||
:fill="props.pathFill3"
|
||||
d="m444.15733,336.928a14.67733,14.67733 0 0 0 -12.8,7.44533a96,96 0 0 1 -137.10933,24.66134a125.09867,125.09867 0 0 0 -188.032,24.14933a14.208,14.208 0 0 0 -1.28,2.13333a5.80267,5.80267 0 0 0 -0.66133,1.408a14.656,14.656 0 0 0 24.14933,15.68a6.272,6.272 0 0 0 1.28,-1.49333a96.832,96.832 0 0 1 13.248,-16.49067a95.76533,95.76533 0 0 1 124.62933,-9.30133a125.07733,125.07733 0 0 0 189.26934,-26.176a5.824,5.824 0 0 0 0.68266,-1.49333a14.63467,14.63467 0 0 0 -13.44,-20.43734l0.064,-0.08533z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
id: String,
|
||||
circleFill: String,
|
||||
pathFill1: String,
|
||||
pathFill2: String,
|
||||
pathFill3: String,
|
||||
showLine2: Boolean
|
||||
})
|
||||
</script>
|
||||
18
src/components/test/my-button/index.vue
Normal file
18
src/components/test/my-button/index.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div style="width: 100%; height: 100%">
|
||||
<button class="my-button" style="width: 100%; height: 100%">{{ props.text }}</button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
text: String,
|
||||
bgColor: String,
|
||||
fontFamily: String
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.my-button {
|
||||
background-color: v-bind('props.bgColor');
|
||||
font-family: v-bind('props.fontFamily');
|
||||
}
|
||||
</style>
|
||||
18
src/components/test/my-input/index.vue
Normal file
18
src/components/test/my-input/index.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div style="width: 100%; height: 100%">
|
||||
<el-input v-model="val" style="width: 100%; height: 100%"></el-input>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElInput } from 'element-plus'
|
||||
const props = defineProps(['modelValue'])
|
||||
const emits = defineEmits(['update:modelValue'])
|
||||
const val = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => {
|
||||
emits('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped></style>
|
||||
71
src/components/test/pie-charts/index.vue
Normal file
71
src/components/test/pie-charts/index.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<v-chart class="chart" :option="option" autoresize />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { use } from 'echarts/core'
|
||||
import { SVGRenderer } from 'echarts/renderers'
|
||||
import { PieChart } from 'echarts/charts'
|
||||
import { TitleComponent, TooltipComponent, LegendComponent } from 'echarts/components'
|
||||
import VChart, { THEME_KEY } from 'vue-echarts'
|
||||
import { watch, provide, reactive } from 'vue'
|
||||
use([SVGRenderer, PieChart, TitleComponent, TooltipComponent, LegendComponent])
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '标题'
|
||||
},
|
||||
seriesName: {
|
||||
type: String,
|
||||
default: '详情'
|
||||
},
|
||||
seriesData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const option = reactive({
|
||||
title: {
|
||||
text: props.title,
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b} : {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: props.seriesName,
|
||||
type: 'pie',
|
||||
radius: '55%',
|
||||
center: ['50%', '60%'],
|
||||
data: props.seriesData,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
watch(props, new_val => {
|
||||
option.title.text = new_val.title
|
||||
option.series[0].name = new_val.seriesName
|
||||
option.series[0].data = new_val.seriesData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user