|
|
@@ -9,14 +9,18 @@
|
|
|
</el-icon>
|
|
|
</el-button>
|
|
|
<div class="sidebar-title">组件</div>
|
|
|
- <template v-for="item in editorContent.elements.slice().sort((a, b) => b.depth - a.depth)"
|
|
|
+ <template v-for="(item, idx) in editorContent.elements.slice().sort((a, b) => b.depth - a.depth)"
|
|
|
:key="item.depth + '-' + item.type">
|
|
|
<div class="sidebar-item component-item"
|
|
|
:class="{ 'selected': selectedComponent === item || (item.type === 'canvas' && selectedComponent && selectedComponent.type === 'canvas') }"
|
|
|
- @click="item.type === 'canvas' ? selectCanvasFromSidebar() : selectComponent(item)">
|
|
|
+ @click="item.type === 'canvas' ? selectCanvasFromSidebar() : selectComponent(item)" draggable="true"
|
|
|
+ @dragstart="onSidebarDragStart(item, idx)" @dragover.prevent="onSidebarDragOver(item, idx, $event)"
|
|
|
+ @drop.prevent="onSidebarDrop(item, idx, $event)">
|
|
|
<div class="component-icon-text">
|
|
|
<template v-if="item.type === 'canvas'">画布</template>
|
|
|
<template v-else-if="item.type === 'text'">文本</template>
|
|
|
+ <template v-else-if="item.type === 'scrollingText'">滚动文本</template>
|
|
|
+ <template v-else-if="item.type === 'mediaAsset'">媒资</template>
|
|
|
<!-- 未来可扩展图片等类型 -->
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -27,6 +31,8 @@
|
|
|
<div class="main-editor">
|
|
|
<div class="toolbar">
|
|
|
<div class="toolbar-item" draggable="true" @dragstart="onToolbarDragStart('text')">文本</div>
|
|
|
+ <div class="toolbar-item" draggable="true" @dragstart="onToolbarDragStart('scrollingText')">滚动文本</div>
|
|
|
+ <div class="toolbar-item" draggable="true" @dragstart="onToolbarDragStart('mediaAsset')">媒资</div>
|
|
|
<!-- 可扩展更多组件 -->
|
|
|
</div>
|
|
|
<div class="editor-canvas" ref="editorCanvasRef" @dragover.prevent @drop="onCanvasDrop">
|
|
|
@@ -45,8 +51,28 @@
|
|
|
zIndex: 10
|
|
|
}" @mousedown="onElementMouseDown($event, item)">
|
|
|
<TextBoard v-if="item.type === 'text'" :text="item.text" :color="item.color" :font-size="item.fontSize"
|
|
|
- :font-weight="item.fontWeight" :align="item.align" @click.stop="selectComponent(item)"
|
|
|
- :class="{ selected: selectedComponent === item }" />
|
|
|
+ :font-weight="item.fontWeight" :align="item.align" :width="item.width" :height="item.height"
|
|
|
+ :selected="selectedComponent === item" @resize="
|
|
|
+ ({ width, height }) => {
|
|
|
+ item.width = width;
|
|
|
+ item.height = height;
|
|
|
+ }
|
|
|
+ " @click.stop="selectComponent(item)" :class="{ selected: selectedComponent === item }" />
|
|
|
+ <ScrollingTextBoard v-if="item.type === 'scrollingText'" :text="item.text" :color="item.color"
|
|
|
+ :font-size="item.fontSize" :font-weight="item.fontWeight" :align="item.align" :width="item.width"
|
|
|
+ :height="item.height" :speed="item.speed" :selected="selectedComponent === item" @resize="
|
|
|
+ ({ width, height }) => {
|
|
|
+ item.width = width;
|
|
|
+ item.height = height;
|
|
|
+ }
|
|
|
+ " @click.stop="selectComponent(item)" :class="{ selected: selectedComponent === item }" />
|
|
|
+ <MediaAssetBoard v-if="item.type === 'mediaAsset'" :width="item.width" :height="item.height"
|
|
|
+ :media-id="item.mediaId" :selected="selectedComponent === item" @resize="
|
|
|
+ ({ width, height }) => {
|
|
|
+ item.width = width;
|
|
|
+ item.height = height;
|
|
|
+ }
|
|
|
+ " @click.stop="selectComponent(item)" :class="{ selected: selectedComponent === item }" />
|
|
|
<!-- 未来可扩展更多类型 -->
|
|
|
</div>
|
|
|
</template>
|
|
|
@@ -85,10 +111,52 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
+// 拖拽排序相关
|
|
|
+const sidebarDrag = ref<{ item: any; idx: number } | null>(null);
|
|
|
+
|
|
|
+function onSidebarDragStart(item: any, idx: number) {
|
|
|
+ sidebarDrag.value = { item, idx };
|
|
|
+}
|
|
|
+function onSidebarDragOver(targetItem: any, targetIdx: number, e: DragEvent) {
|
|
|
+ e.preventDefault();
|
|
|
+}
|
|
|
+function onSidebarDrop(targetItem: any, targetIdx: number, e: DragEvent) {
|
|
|
+ if (!sidebarDrag.value) return;
|
|
|
+ const elements = editorContent.value.elements;
|
|
|
+ // 排序前先按 depth 降序
|
|
|
+ const sorted = elements.slice().sort((a, b) => b.depth - a.depth);
|
|
|
+ const fromIdx = sorted.findIndex((el) => el === sidebarDrag.value!.item);
|
|
|
+ const toIdx = sorted.findIndex((el) => el === targetItem);
|
|
|
+ if (fromIdx === -1 || toIdx === -1 || fromIdx === toIdx) {
|
|
|
+ sidebarDrag.value = null;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 交换depth
|
|
|
+ const fromDepth = sorted[fromIdx].depth;
|
|
|
+ const toDepth = sorted[toIdx].depth;
|
|
|
+ sorted[fromIdx].depth = toDepth;
|
|
|
+ sorted[toIdx].depth = fromDepth;
|
|
|
+ // 重新赋值到原数组
|
|
|
+ for (let i = 0; i < sorted.length; i++) {
|
|
|
+ const origIdx = elements.findIndex((el) => el === sorted[i]);
|
|
|
+ if (origIdx !== -1) elements[origIdx].depth = sorted[i].depth;
|
|
|
+ }
|
|
|
+ sidebarDrag.value = null;
|
|
|
+}
|
|
|
+
|
|
|
import { ref, onMounted, computed, nextTick } from 'vue';
|
|
|
import CanvasBoard from './component/CanvasBoard.vue';
|
|
|
import TextBoard from './component/TextBoard.vue';
|
|
|
+import ScrollingTextBoard from './component/ScrollingTextBoard.vue';
|
|
|
+import MediaAssetBoard from './component/MediaAssetBoard.vue';
|
|
|
import { canvasPropNameMap } from './component/propNameMaps';
|
|
|
+// 拖拽类型
|
|
|
+const dragType = ref<string | null>(null);
|
|
|
+// 获取最大 depth
|
|
|
+function getMaxDepth() {
|
|
|
+ if (!editorContent.value.elements.length) return 0;
|
|
|
+ return Math.max(...editorContent.value.elements.map((el: any) => el.depth || 0));
|
|
|
+}
|
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
|
import { ElMessage } from 'element-plus';
|
|
|
import { ArrowLeft } from '@element-plus/icons-vue';
|
|
|
@@ -119,10 +187,20 @@ function showEditableProp(key: string) {
|
|
|
}
|
|
|
|
|
|
// 获取属性中文名
|
|
|
+import { textPropNameMap, scrollingTextPropNameMap, mediaAssetPropNameMap } from './component/propNameMaps';
|
|
|
function getPropLabel(key: string) {
|
|
|
if (selectedComponent.value?.type === 'canvas') {
|
|
|
return canvasPropNameMap[key] || key;
|
|
|
}
|
|
|
+ if (selectedComponent.value?.type === 'text') {
|
|
|
+ return textPropNameMap[key] || key;
|
|
|
+ }
|
|
|
+ if (selectedComponent.value?.type === 'scrollingText') {
|
|
|
+ return scrollingTextPropNameMap[key] || key;
|
|
|
+ }
|
|
|
+ if (selectedComponent.value?.type === 'mediaAsset') {
|
|
|
+ return mediaAssetPropNameMap[key] || key;
|
|
|
+ }
|
|
|
return key;
|
|
|
}
|
|
|
|
|
|
@@ -241,38 +319,61 @@ function onElementMouseUp() {
|
|
|
}
|
|
|
|
|
|
function onToolbarDragStart(type: string) {
|
|
|
- dragComponentType.value = type;
|
|
|
+ dragType.value = type;
|
|
|
}
|
|
|
|
|
|
function onCanvasDrop(e: DragEvent) {
|
|
|
- if (!dragComponentType.value) return;
|
|
|
- // 计算相对画布的坐标(此处简单居中,后续可完善为鼠标点)
|
|
|
- const canvas = editorContent.value.elements.find((el: any) => el.type === 'canvas');
|
|
|
- let x = 50,
|
|
|
- y = 50;
|
|
|
- if (canvas && editorCanvasRef.value) {
|
|
|
- // 计算鼠标在容器中的位置,考虑缩放
|
|
|
- const rect = editorCanvasRef.value.getBoundingClientRect();
|
|
|
- const scale = canvasScale.value || 1;
|
|
|
- x = (e.clientX - rect.left) / scale - (canvas.x || 0);
|
|
|
- y = (e.clientY - rect.top) / scale - (canvas.y || 0);
|
|
|
- }
|
|
|
- if (dragComponentType.value === 'text') {
|
|
|
- editorContent.value.elements.push({
|
|
|
+ if (!dragType.value) return;
|
|
|
+ const rect = editorCanvasRef.value?.getBoundingClientRect();
|
|
|
+ const x = e.clientX - (rect?.left || 0);
|
|
|
+ const y = e.clientY - (rect?.top || 0);
|
|
|
+ if (dragType.value === 'text') {
|
|
|
+ const newText = {
|
|
|
type: 'text',
|
|
|
text: '新文本',
|
|
|
color: '#222',
|
|
|
fontSize: 24,
|
|
|
fontWeight: 'normal',
|
|
|
align: 'center',
|
|
|
- x,
|
|
|
- y,
|
|
|
+ x: x,
|
|
|
+ y: y,
|
|
|
width: 200,
|
|
|
height: 40,
|
|
|
- depth: editorContent.value.elements.length
|
|
|
- });
|
|
|
+ depth: getMaxDepth() + 1
|
|
|
+ };
|
|
|
+ editorContent.value.elements.push(newText);
|
|
|
+ nextTick(() => selectComponent(newText));
|
|
|
+ } else if (dragType.value === 'scrollingText') {
|
|
|
+ const newScrollingText = {
|
|
|
+ type: 'scrollingText',
|
|
|
+ text: '新滚动文本',
|
|
|
+ color: '#222',
|
|
|
+ fontSize: 24,
|
|
|
+ fontWeight: 'normal',
|
|
|
+ align: 'center',
|
|
|
+ speed: 50,
|
|
|
+ x: x,
|
|
|
+ y: y,
|
|
|
+ width: 300,
|
|
|
+ height: 40,
|
|
|
+ depth: getMaxDepth() + 1
|
|
|
+ };
|
|
|
+ editorContent.value.elements.push(newScrollingText);
|
|
|
+ nextTick(() => selectComponent(newScrollingText));
|
|
|
+ } else if (dragType.value === 'mediaAsset') {
|
|
|
+ const newMediaAsset = {
|
|
|
+ type: 'mediaAsset',
|
|
|
+ mediaId: '',
|
|
|
+ x: x,
|
|
|
+ y: y,
|
|
|
+ width: 120,
|
|
|
+ height: 120,
|
|
|
+ depth: getMaxDepth() + 1
|
|
|
+ };
|
|
|
+ editorContent.value.elements.push(newMediaAsset);
|
|
|
+ nextTick(() => selectComponent(newMediaAsset));
|
|
|
}
|
|
|
- dragComponentType.value = null;
|
|
|
+ dragType.value = null;
|
|
|
}
|
|
|
|
|
|
const containerSize = ref({ width: 0, height: 0 });
|
|
|
@@ -403,19 +504,19 @@ const goBack = () => {
|
|
|
|
|
|
.toolbar {
|
|
|
display: flex;
|
|
|
- flex-direction: column;
|
|
|
+ flex-direction: row;
|
|
|
align-self: flex-start;
|
|
|
- align-items: flex-start;
|
|
|
- justify-content: stretch;
|
|
|
- width: 100px;
|
|
|
+ align-items: center;
|
|
|
height: 50px;
|
|
|
+ width: 450px;
|
|
|
margin-left: 5%;
|
|
|
margin-top: -5%;
|
|
|
margin-bottom: 1%;
|
|
|
background: #fafbfc;
|
|
|
border-radius: 8px;
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
|
|
- /* padding: 6px 0; */
|
|
|
+ gap: 16px;
|
|
|
+ padding: 0 16px;
|
|
|
}
|
|
|
|
|
|
.toolbar-item {
|