|
@@ -3,18 +3,29 @@
|
|
|
<!-- 左侧工具栏 -->
|
|
<!-- 左侧工具栏 -->
|
|
|
<div class="toolbar">
|
|
<div class="toolbar">
|
|
|
<!-- 标签页切换 -->
|
|
<!-- 标签页切换 -->
|
|
|
- <el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
|
|
|
|
|
|
|
+ <div class="toolbar-tabs">
|
|
|
|
|
+ <el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
|
|
|
<el-tab-pane label="模版设计" name="design">
|
|
<el-tab-pane label="模版设计" name="design">
|
|
|
<el-upload
|
|
<el-upload
|
|
|
|
|
+ class="custom-upload"
|
|
|
|
|
+ drag
|
|
|
:show-file-list="false"
|
|
:show-file-list="false"
|
|
|
:before-upload="beforeUpload"
|
|
:before-upload="beforeUpload"
|
|
|
:http-request="handleUpload"
|
|
:http-request="handleUpload"
|
|
|
- accept="image/*"
|
|
|
|
|
|
|
+ accept="image/jpeg,image/png,image/jpg,image/webp"
|
|
|
multiple
|
|
multiple
|
|
|
>
|
|
>
|
|
|
- <el-button type="primary" :icon="Upload" style="width: 100%; margin-bottom: 8px;">
|
|
|
|
|
- 上传素材图
|
|
|
|
|
- </el-button>
|
|
|
|
|
|
|
+ <div class="upload-main">
|
|
|
|
|
+ <div class="upload-inner-button">
|
|
|
|
|
+ <el-icon class="custom-upload-icon">
|
|
|
|
|
+ <Upload />
|
|
|
|
|
+ </el-icon>
|
|
|
|
|
+ <span class="upload-inner-text">上传素材图</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="el-upload__tip">
|
|
|
|
|
+ 拖拽或点击上传JPEG/JPG/PNG 10M以内
|
|
|
|
|
+ </div>
|
|
|
</el-upload>
|
|
</el-upload>
|
|
|
|
|
|
|
|
<el-button type="success" :icon="Plus" style="width: 100%;" @click="addTextLayer">
|
|
<el-button type="success" :icon="Plus" style="width: 100%;" @click="addTextLayer">
|
|
@@ -153,6 +164,9 @@
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 保存模版按钮 -->
|
|
|
|
|
+ <el-divider />
|
|
|
</el-tab-pane>
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
<el-tab-pane label="素材选择" name="material" :label-class="'material-tab'">
|
|
<el-tab-pane label="素材选择" name="material" :label-class="'material-tab'">
|
|
@@ -174,14 +188,26 @@
|
|
|
<el-icon><Plus /></el-icon>
|
|
<el-icon><Plus /></el-icon>
|
|
|
<span>添加</span>
|
|
<span>添加</span>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</el-tab-pane>
|
|
</el-tab-pane>
|
|
|
</el-tabs>
|
|
</el-tabs>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 固定在底部的保存模版按钮 -->
|
|
|
|
|
+ <el-divider />
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ :icon="Document"
|
|
|
|
|
+ class="save-template-btn"
|
|
|
|
|
+ @click="saveTemplate"
|
|
|
|
|
+ >
|
|
|
|
|
+ 保存模版
|
|
|
|
|
+ </el-button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 中间画布区域 -->
|
|
<!-- 中间画布区域 -->
|
|
|
- <div class="canvas-area">
|
|
|
|
|
|
|
+ <div class="canvas-area" ref="canvasAreaRef">
|
|
|
<div class="canvas-wrapper">
|
|
<div class="canvas-wrapper">
|
|
|
<div
|
|
<div
|
|
|
ref="canvasRef"
|
|
ref="canvasRef"
|
|
@@ -193,6 +219,7 @@
|
|
|
@mousedown="handleCanvasMouseDown"
|
|
@mousedown="handleCanvasMouseDown"
|
|
|
@mousemove="handleCanvasMouseMove"
|
|
@mousemove="handleCanvasMouseMove"
|
|
|
@mouseup="handleCanvasMouseUp"
|
|
@mouseup="handleCanvasMouseUp"
|
|
|
|
|
+ @mouseleave="handleCanvasMouseUp"
|
|
|
@wheel="handleCanvasWheel"
|
|
@wheel="handleCanvasWheel"
|
|
|
>
|
|
>
|
|
|
<div
|
|
<div
|
|
@@ -304,12 +331,13 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
-import { ref, computed, reactive, onMounted } from 'vue'
|
|
|
|
|
|
|
+import { ref, computed, reactive, onMounted, onUnmounted } from 'vue'
|
|
|
import { ElMessage } from 'element-plus'
|
|
import { ElMessage } from 'element-plus'
|
|
|
import { Pointer, Rank, ArrowUp, ArrowDown, Delete, View, Hide, Lock, Unlock, Plus, Document, Picture, Refresh, Upload } from '@element-plus/icons-vue'
|
|
import { Pointer, Rank, ArrowUp, ArrowDown, Delete, View, Hide, Lock, Unlock, Plus, Document, Picture, Refresh, Upload } from '@element-plus/icons-vue'
|
|
|
import { Material_List } from '@/api/mes/job'
|
|
import { Material_List } from '@/api/mes/job'
|
|
|
|
|
|
|
|
const canvasRef = ref(null)
|
|
const canvasRef = ref(null)
|
|
|
|
|
+const canvasAreaRef = ref(null)
|
|
|
const layerListRef = ref(null)
|
|
const layerListRef = ref(null)
|
|
|
const currentTool = ref('select')
|
|
const currentTool = ref('select')
|
|
|
const activeTab = ref('design') // 'design' 或 'material'
|
|
const activeTab = ref('design') // 'design' 或 'material'
|
|
@@ -398,10 +426,10 @@ const handleUpload = (options) => {
|
|
|
id: ++layerIdCounter,
|
|
id: ++layerIdCounter,
|
|
|
name: file.name,
|
|
name: file.name,
|
|
|
url: e.target.result,
|
|
url: e.target.result,
|
|
|
- x: (canvasWidth.value - width) / 2,
|
|
|
|
|
- y: (canvasHeight.value - height) / 2,
|
|
|
|
|
- width: width,
|
|
|
|
|
- height: height,
|
|
|
|
|
|
|
+ x: Math.round((canvasWidth.value - width) / 2),
|
|
|
|
|
+ y: Math.round((canvasHeight.value - height) / 2),
|
|
|
|
|
+ width: Math.round(width),
|
|
|
|
|
+ height: Math.round(height),
|
|
|
rotation: 0,
|
|
rotation: 0,
|
|
|
opacity: 100,
|
|
opacity: 100,
|
|
|
visible: true,
|
|
visible: true,
|
|
@@ -509,10 +537,10 @@ const addMaterialToCanvas = (material) => {
|
|
|
id: ++layerIdCounter,
|
|
id: ++layerIdCounter,
|
|
|
name: `素材 ${layerIdCounter}`,
|
|
name: `素材 ${layerIdCounter}`,
|
|
|
url: material.material_url,
|
|
url: material.material_url,
|
|
|
- x: (canvasWidth.value - width) / 2,
|
|
|
|
|
- y: (canvasHeight.value - height) / 2,
|
|
|
|
|
- width: width,
|
|
|
|
|
- height: height,
|
|
|
|
|
|
|
+ x: Math.round((canvasWidth.value - width) / 2),
|
|
|
|
|
+ y: Math.round((canvasHeight.value - height) / 2),
|
|
|
|
|
+ width: Math.round(width),
|
|
|
|
|
+ height: Math.round(height),
|
|
|
rotation: 0,
|
|
rotation: 0,
|
|
|
opacity: 100,
|
|
opacity: 100,
|
|
|
visible: true,
|
|
visible: true,
|
|
@@ -529,10 +557,81 @@ const addMaterialToCanvas = (material) => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 组件挂载时获取素材库数据
|
|
// 组件挂载时获取素材库数据
|
|
|
|
|
+// 全局点击事件处理函数:仅在中间画布区域内点击空白处时取消选中
|
|
|
|
|
+const handleGlobalClick = (e) => {
|
|
|
|
|
+ const areaEl = canvasAreaRef.value
|
|
|
|
|
+ if (!areaEl) return
|
|
|
|
|
+
|
|
|
|
|
+ // 是否在中间画布区域内点击
|
|
|
|
|
+ const isInCanvasArea = areaEl.contains(e.target)
|
|
|
|
|
+ // 检查是否点击了图层
|
|
|
|
|
+ const isLayerClick = e.target.closest('.layer')
|
|
|
|
|
+ // 检查是否点击了画布
|
|
|
|
|
+ const isCanvasClick = e.target === canvasRef.value || canvasRef.value?.contains(e.target)
|
|
|
|
|
+
|
|
|
|
|
+ // 只有在中间画布区域内,并且既没有点击图层也没有点击画布,才取消选择
|
|
|
|
|
+ if (isInCanvasArea && !isLayerClick && !isCanvasClick) {
|
|
|
|
|
+ selectedLayerId.value = null
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
fetchMaterials()
|
|
fetchMaterials()
|
|
|
|
|
+ // 添加全局点击事件监听器
|
|
|
|
|
+ document.addEventListener('click', handleGlobalClick)
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+onUnmounted(() => {
|
|
|
|
|
+ // 移除全局点击事件监听器
|
|
|
|
|
+ document.removeEventListener('click', handleGlobalClick)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+// 保存模版
|
|
|
|
|
+const saveTemplate = () => {
|
|
|
|
|
+ // 构建模版数据
|
|
|
|
|
+ const templateData = {
|
|
|
|
|
+ canvasWidth: canvasWidth.value,
|
|
|
|
|
+ canvasHeight: canvasHeight.value,
|
|
|
|
|
+ canvasRatio: canvasRatio.value,
|
|
|
|
|
+ layers: layers.value.map(layer => ({
|
|
|
|
|
+ id: layer.id,
|
|
|
|
|
+ name: layer.name,
|
|
|
|
|
+ type: layer.type || 'image',
|
|
|
|
|
+ url: layer.url,
|
|
|
|
|
+ text: layer.text,
|
|
|
|
|
+ x: layer.x,
|
|
|
|
|
+ y: layer.y,
|
|
|
|
|
+ width: layer.width,
|
|
|
|
|
+ height: layer.height,
|
|
|
|
|
+ rotation: layer.rotation,
|
|
|
|
|
+ opacity: layer.opacity,
|
|
|
|
|
+ visible: layer.visible,
|
|
|
|
|
+ locked: layer.locked,
|
|
|
|
|
+ fontFamily: layer.fontFamily,
|
|
|
|
|
+ fontSize: layer.fontSize,
|
|
|
|
|
+ color: layer.color,
|
|
|
|
|
+ backgroundColor: layer.backgroundColor,
|
|
|
|
|
+ fontWeight: layer.fontWeight,
|
|
|
|
|
+ fontStyle: layer.fontStyle,
|
|
|
|
|
+ textDecoration: layer.textDecoration,
|
|
|
|
|
+ lineHeight: layer.lineHeight,
|
|
|
|
|
+ letterSpacing: layer.letterSpacing,
|
|
|
|
|
+ textAlign: layer.textAlign,
|
|
|
|
|
+ originalWidth: layer.originalWidth,
|
|
|
|
|
+ originalHeight: layer.originalHeight
|
|
|
|
|
+ }))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 这里可以根据实际需求发送请求到后端保存模版
|
|
|
|
|
+ // 暂时使用本地存储示例
|
|
|
|
|
+ console.log('保存模版数据:', templateData)
|
|
|
|
|
+
|
|
|
|
|
+ // 示例: 保存到本地存储
|
|
|
|
|
+ localStorage.setItem('templateDesign', JSON.stringify(templateData))
|
|
|
|
|
+
|
|
|
|
|
+ ElMessage.success('模版保存成功!')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const getLayerStyle = (layer) => {
|
|
const getLayerStyle = (layer) => {
|
|
|
if (!layer.visible) {
|
|
if (!layer.visible) {
|
|
|
return { display: 'none' }
|
|
return { display: 'none' }
|
|
@@ -812,7 +911,8 @@ const handleLayerMouseDown = (e, layer) => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const handleCanvasMouseDown = (e) => {
|
|
const handleCanvasMouseDown = (e) => {
|
|
|
- if (e.target === canvasRef.value) {
|
|
|
|
|
|
|
+ // 确保 canvasRef 已经初始化
|
|
|
|
|
+ if (canvasRef.value && e.target === canvasRef.value) {
|
|
|
selectedLayerId.value = null
|
|
selectedLayerId.value = null
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -931,17 +1031,19 @@ const handleCanvasWheel = (e) => {
|
|
|
<style scoped>
|
|
<style scoped>
|
|
|
.template-design-container {
|
|
.template-design-container {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- height: calc(100vh - 50px);
|
|
|
|
|
|
|
+ height: calc(100vh - 140px);
|
|
|
background-color: #f5f5f5;
|
|
background-color: #f5f5f5;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.toolbar {
|
|
.toolbar {
|
|
|
- width: 220px;
|
|
|
|
|
|
|
+ width: 300px;
|
|
|
background-color: #fff;
|
|
background-color: #fff;
|
|
|
border-right: 1px solid #ddd;
|
|
border-right: 1px solid #ddd;
|
|
|
padding: 8px 12px;
|
|
padding: 8px 12px;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ overflow: hidden; /* 外层高度锁死,内部区域单独滚动 */
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.toolbar h3 {
|
|
.toolbar h3 {
|
|
@@ -1008,8 +1110,8 @@ const handleCanvasWheel = (e) => {
|
|
|
.materials-list-full {
|
|
.materials-list-full {
|
|
|
flex: 1;
|
|
flex: 1;
|
|
|
display: grid;
|
|
display: grid;
|
|
|
- grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
- gap: 8px;
|
|
|
|
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
|
|
+ gap: 6px;
|
|
|
overflow-y: auto;
|
|
overflow-y: auto;
|
|
|
padding: 4px;
|
|
padding: 4px;
|
|
|
background-color: #f9f9f9;
|
|
background-color: #f9f9f9;
|
|
@@ -1021,9 +1123,9 @@ const handleCanvasWheel = (e) => {
|
|
|
position: relative;
|
|
position: relative;
|
|
|
aspect-ratio: 1;
|
|
aspect-ratio: 1;
|
|
|
cursor: pointer;
|
|
cursor: pointer;
|
|
|
- border-radius: 6px;
|
|
|
|
|
|
|
+ border-radius: 8px;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
- border: 2px solid #ddd;
|
|
|
|
|
|
|
+ border: 1px solid #e5e5e5;
|
|
|
transition: all 0.2s;
|
|
transition: all 0.2s;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1274,7 +1376,6 @@ const handleCanvasWheel = (e) => {
|
|
|
padding: 8px 12px;
|
|
padding: 8px 12px;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
- overflow: hidden;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.layer-panel h3 {
|
|
.layer-panel h3 {
|
|
@@ -1395,7 +1496,6 @@ const handleCanvasWheel = (e) => {
|
|
|
flex: 1;
|
|
flex: 1;
|
|
|
font-size: 11px;
|
|
font-size: 11px;
|
|
|
color: #333;
|
|
color: #333;
|
|
|
- overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
text-overflow: ellipsis;
|
|
|
white-space: nowrap;
|
|
white-space: nowrap;
|
|
|
}
|
|
}
|
|
@@ -1411,6 +1511,12 @@ const handleCanvasWheel = (e) => {
|
|
|
margin: 6px 0;
|
|
margin: 6px 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.save-template-btn {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/* 隐藏滚动条但保持功能 */
|
|
/* 隐藏滚动条但保持功能 */
|
|
|
.layer-list::-webkit-scrollbar,
|
|
.layer-list::-webkit-scrollbar,
|
|
|
.layer-info::-webkit-scrollbar {
|
|
.layer-info::-webkit-scrollbar {
|
|
@@ -1458,7 +1564,6 @@ const handleCanvasWheel = (e) => {
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
gap: 6px;
|
|
gap: 6px;
|
|
|
max-height: 200px;
|
|
max-height: 200px;
|
|
|
- overflow-y: auto;
|
|
|
|
|
padding: 4px;
|
|
padding: 4px;
|
|
|
background-color: #f9f9f9;
|
|
background-color: #f9f9f9;
|
|
|
border-radius: 4px;
|
|
border-radius: 4px;
|
|
@@ -1527,4 +1632,65 @@ const handleCanvasWheel = (e) => {
|
|
|
.materials-list::-webkit-scrollbar-track {
|
|
.materials-list::-webkit-scrollbar-track {
|
|
|
background-color: transparent;
|
|
background-color: transparent;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+/* 自定义上传样式,改成卡片风格 */
|
|
|
|
|
+.custom-upload {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.custom-upload :deep(.el-upload-dragger) {
|
|
|
|
|
+ padding: 20px 16px;
|
|
|
|
|
+ background-color: #ffffff;
|
|
|
|
|
+ border: 1px dashed #dcdfe6;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.custom-upload :deep(.upload-main) {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.custom-upload :deep(.upload-inner-button) {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ padding: 6px 16px;
|
|
|
|
|
+ border-radius: 999px;
|
|
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
|
|
+ background-color: #ffffff;
|
|
|
|
|
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.custom-upload-icon {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.custom-upload :deep(.upload-inner-text) {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #333333;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.custom-upload :deep(.el-upload__text) {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #333333;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.custom-upload :deep(.el-upload__tip) {
|
|
|
|
|
+ font-size: 12px !important;
|
|
|
|
|
+ color: #888888 !important;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|