提交代码

This commit is contained in:
guanj
2025-09-25 11:34:55 +08:00
commit 448b8df85b
188 changed files with 21433 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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')
})
})

View File

@@ -0,0 +1,5 @@
<template>
<div class="w-1/1 h-1/1 select-none">
<slot></slot>
</div>
</template>

View 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>

View 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>

View File

@@ -0,0 +1,3 @@
import MtDzr from './index.vue'
export default MtDzr

View 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>

View 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
})
}
})

View 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
}

View 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 //网格大小
}

View 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
}

View File

@@ -0,0 +1 @@
export type MouseTouchEvent = MouseEvent | TouchEvent

View 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: '回调函数中获取当前触发事件图形的信息'
}
])
}
})

View 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;
}

View 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>

View 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>

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View 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>

View File

@@ -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>

View 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>

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View 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

View 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>

View 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>

View File

@@ -0,0 +1,6 @@
export interface IAreaBinfo {
width: number
height: number
top: number
left: number
}

View 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>

View 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>

View 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[]
}

View 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
}
}

View 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)
})
}

View 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')
}

View File

@@ -0,0 +1,3 @@
import MtEdit from './index.vue'
export default MtEdit

View 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>

View 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
})

View 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
}
})
}
})

View 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
})

View 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
})
}
})

View 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
}
})

View 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)
}
})

View 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 //要显示的标题,一般用中文表示
}

View 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}')`)
}
}

View File

@@ -0,0 +1,3 @@
import MtPreview from './index.vue'
export default MtPreview

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View 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>

View 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>