|
|
@@ -0,0 +1,225 @@
|
|
|
+<template>
|
|
|
+ <div
|
|
|
+ ref="boxRef"
|
|
|
+ style="width: 100%; height: 100%; position: relative"
|
|
|
+ @mousedown="handleMouseDown"
|
|
|
+ @mousemove="handleMouseMove"
|
|
|
+ @mouseup="handleMouseUp"
|
|
|
+ >
|
|
|
+ <div class="workspace flex flex-col">
|
|
|
+ <div class="workspace-top">
|
|
|
+ <DesignerCanvas :state="state" ref="canvasRef" @changeState="handleSetState" />
|
|
|
+ <Scaleplate :state="state" />
|
|
|
+ </div>
|
|
|
+ <div class="workspace-bottom flex justify-between items-center">
|
|
|
+ <div class="bottom-left">
|
|
|
+ <span style="margin-right: 12px">画布尺寸:</span>
|
|
|
+ <span>画布自适应:</span>
|
|
|
+ </div>
|
|
|
+ <div class="bottom-right">
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ type="text"
|
|
|
+ :disabled="state.scale <= 0.1"
|
|
|
+ @click="handleSizeChange(Number(state.scale) - 0.1)"
|
|
|
+ >
|
|
|
+ <LuMinusCircle />
|
|
|
+ </el-button>
|
|
|
+ <el-autocomplete
|
|
|
+ size="small"
|
|
|
+ style="width: 120px"
|
|
|
+ :options="sizeOptions"
|
|
|
+ :value="(state.scale * 100).toFixed(0) + '%'"
|
|
|
+ @change="handleSizeChange"
|
|
|
+ />
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ type="text"
|
|
|
+ :disabled="state.scale >= 4"
|
|
|
+ @click="handleSizeChange(Number(state.scale) + 0.1)"
|
|
|
+ >
|
|
|
+ <LuPlusCircle />
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="selectBox" ref="selectBoxRef"></div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import type { StageState } from './type'
|
|
|
+
|
|
|
+import { ref, reactive } from 'vue'
|
|
|
+
|
|
|
+import { LuMinusCircle, LuPlusCircle } from 'vue-icons-plus/lu'
|
|
|
+import Scaleplate from './Scaleplate.vue'
|
|
|
+import DesignerCanvas from './DesignerCanvas.vue'
|
|
|
+import { throttle } from 'lodash'
|
|
|
+
|
|
|
+const canvasRef = ref<{ getPosition: HTMLElement['getBoundingClientRect'] } | null>()
|
|
|
+const state = reactive<StageState>({
|
|
|
+ scale: 1,
|
|
|
+ width: 1280,
|
|
|
+ height: 720,
|
|
|
+ originX: 0,
|
|
|
+ originY: 0,
|
|
|
+ viewportWidth: 0,
|
|
|
+ viewportHeight: 0,
|
|
|
+ centerX: 0,
|
|
|
+ centerY: 0,
|
|
|
+ scrollX: 0,
|
|
|
+ scrollY: 0,
|
|
|
+ wrapperWidth: 0,
|
|
|
+ wrapperHeight: 0
|
|
|
+})
|
|
|
+
|
|
|
+const sizeOptions = [
|
|
|
+ { value: 0.1, label: '10%' },
|
|
|
+ { value: 0.25, label: '25%' },
|
|
|
+ { value: 0.5, label: '50%' },
|
|
|
+ { value: 0.75, label: '75%' },
|
|
|
+ { value: 1, label: '100%' },
|
|
|
+ { value: 1.25, label: '125%' },
|
|
|
+ { value: 1.5, label: '150%' },
|
|
|
+ { value: 2, label: '200%' },
|
|
|
+ { value: 3, label: '300%' },
|
|
|
+ { value: 4, label: '400%' },
|
|
|
+ { value: 0, label: '适应大小' }
|
|
|
+]
|
|
|
+
|
|
|
+// 修改状态
|
|
|
+const handleSetState = (newState: Partial<StageState>) => {
|
|
|
+ Object.entries(newState).forEach(([key, value]) => {
|
|
|
+ state[key] = value
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const handleSizeChange = (val: any) => {
|
|
|
+ if (Number.isFinite(val)) {
|
|
|
+ // 为0时为自动适应大小
|
|
|
+ if (val === 0) {
|
|
|
+ state.scale = 0
|
|
|
+ return
|
|
|
+ }
|
|
|
+ state.scale = (val as number) < 0.1 ? 0.1 : val
|
|
|
+ }
|
|
|
+ if (typeof val === 'string') {
|
|
|
+ const n = +(val + '').replace('%', '')
|
|
|
+ if (Number.isNaN(n) && n >= 10 && n <= 400) {
|
|
|
+ state.scale = (n / 100).toFixed(2) as unknown as number
|
|
|
+ } else {
|
|
|
+ state.scale = 0.1
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* ====================处理框选多个组件======================== */
|
|
|
+const selectBoxRef = ref<HTMLElement | null>(null)
|
|
|
+const boxRef = ref<HTMLElement | null>(null)
|
|
|
+let isMouseDown = ref(false)
|
|
|
+let startX = 0 // 框选起始x坐标
|
|
|
+let startY = 0 // 框选起始y坐标
|
|
|
+let workspaceLeft = 0
|
|
|
+let workspaceTop = 0
|
|
|
+// 鼠标按下
|
|
|
+const handleMouseDown = (e: MouseEvent) => {
|
|
|
+ if (
|
|
|
+ e?.target?.closest('.edit-box') ||
|
|
|
+ e?.target?.closest('.component-content') ||
|
|
|
+ e?.target?.closest('.component-wrapper') ||
|
|
|
+ e?.target?.closest('.scaleplate-horizontal') ||
|
|
|
+ e?.target?.closest('.scaleplate-vertical') ||
|
|
|
+ e?.target?.closest('.refer-line-img') ||
|
|
|
+ e?.target?.closest('.workspace-bottom') ||
|
|
|
+ e?.target?.closest('.refer-line')
|
|
|
+ ) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const { top, left } = boxRef.value!.getBoundingClientRect()
|
|
|
+ const { clientX, clientY } = e
|
|
|
+ isMouseDown.value = true
|
|
|
+ workspaceLeft = left
|
|
|
+ workspaceTop = top
|
|
|
+ startX = clientX - left
|
|
|
+ startY = clientY - top
|
|
|
+ selectBoxRef.value!.style.display = 'block'
|
|
|
+ selectBoxRef.value!.style.left = `${startX}px`
|
|
|
+ selectBoxRef.value!.style.top = `${startY}px`
|
|
|
+}
|
|
|
+/* 鼠标移动 */
|
|
|
+const handleMouseMove = throttle((e: MouseEvent) => {
|
|
|
+ if (!isMouseDown.value) return
|
|
|
+ const { clientX, clientY } = e
|
|
|
+ const width = clientX - workspaceLeft - startX
|
|
|
+ const height = clientY - workspaceTop - startY
|
|
|
+
|
|
|
+ const left = width > 0 ? startX : clientX - workspaceLeft
|
|
|
+ const top = height > 0 ? startY : clientY - workspaceTop
|
|
|
+
|
|
|
+ selectBoxRef.value!.style.width = `${Math.abs(width)}px`
|
|
|
+ selectBoxRef.value!.style.height = `${Math.abs(height)}px`
|
|
|
+ selectBoxRef.value!.style.left = `${left}px`
|
|
|
+ selectBoxRef.value!.style.top = `${top}px`
|
|
|
+}, 50)
|
|
|
+/* 鼠标抬起 */
|
|
|
+const handleMouseUp = (e: MouseEvent) => {
|
|
|
+ if (!isMouseDown || (startX === 0 && startY === 0)) return
|
|
|
+
|
|
|
+ isMouseDown.value = false
|
|
|
+ selectBoxRef.value!.style.display = 'none'
|
|
|
+ selectBoxRef.value!.style.width = '0'
|
|
|
+ selectBoxRef.value!.style.height = '0'
|
|
|
+
|
|
|
+ const { clientX, clientY } = e
|
|
|
+ const { left: stageLeft = 0, top: stageTop = 0 } = canvasRef.value?.getPosition() || {}
|
|
|
+
|
|
|
+ // 框选的起始位置
|
|
|
+ const x1 = (Math.min(startX + workspaceLeft, clientX) - stageLeft) / state.scale
|
|
|
+ const y1 = (Math.min(startY + workspaceTop, clientY) - stageTop) / state.scale
|
|
|
+ // 框选的结束位置
|
|
|
+ const x2 = (Math.max(startX + workspaceLeft, clientX) - stageLeft) / state.scale
|
|
|
+ const y2 = (Math.max(startY + workspaceTop, clientY) - stageTop) / state.scale
|
|
|
+ handleSelectComponent(x1, y1, x2, y2)
|
|
|
+
|
|
|
+ // 清空临时数据
|
|
|
+ startX = startY = workspaceLeft = workspaceTop = 0
|
|
|
+}
|
|
|
+/* 处理框选的组件 */
|
|
|
+const handleSelectComponent = (startX: number, startY: number, endX: number, endY: number) => {
|
|
|
+ // todo 框选组件
|
|
|
+}
|
|
|
+/* ====================处理框选多个组件======================== */
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="less" scoped>
|
|
|
+.workspace {
|
|
|
+ height: 100%;
|
|
|
+ user-select: none;
|
|
|
+ &-top {
|
|
|
+ flex: 1;
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+ &-bottom {
|
|
|
+ padding: 0 20px;
|
|
|
+ height: 40px;
|
|
|
+ background: var(--background-primary);
|
|
|
+ border-top: solid 1px var(--border-color);
|
|
|
+ font-size: 12px;
|
|
|
+ color: #666;
|
|
|
+ }
|
|
|
+}
|
|
|
+.selectBox {
|
|
|
+ display: none;
|
|
|
+ position: absolute;
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+ z-index: 9999;
|
|
|
+ border: solid 1px #2c76bd;
|
|
|
+ box-sizing: border-box;
|
|
|
+ background-color: #2c76bd90;
|
|
|
+ width: 0;
|
|
|
+ height: 0;
|
|
|
+}
|
|
|
+</style>
|