Kaynağa Gözat

first commit

liuhairui 2 hafta önce
ebeveyn
işleme
85c0aea0c7
1 değiştirilmiş dosya ile 192 ekleme ve 26 silme
  1. 192 26
      src/view/TemplateManagement/TemplateDesign.vue

+ 192 - 26
src/view/TemplateManagement/TemplateDesign.vue

@@ -3,18 +3,29 @@
     <!-- 左侧工具栏 -->
     <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-upload
+            class="custom-upload"
+            drag
             :show-file-list="false"
             :before-upload="beforeUpload"
             :http-request="handleUpload"
-            accept="image/*"
+            accept="image/jpeg,image/png,image/jpg,image/webp"
             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-button type="success" :icon="Plus" style="width: 100%;" @click="addTextLayer">
@@ -153,6 +164,9 @@
           </div>
         </template>
           </div>
+          
+          <!-- 保存模版按钮 -->
+          <el-divider />
         </el-tab-pane>
         
         <el-tab-pane label="素材选择" name="material" :label-class="'material-tab'">
@@ -174,14 +188,26 @@
                   <el-icon><Plus /></el-icon>
                   <span>添加</span>
                 </div>
-            </div>
+              </div>
           </div>
         </el-tab-pane>
       </el-tabs>
+      </div>
+
+      <!-- 固定在底部的保存模版按钮 -->
+      <el-divider />
+      <el-button
+        type="primary"
+        :icon="Document"
+        class="save-template-btn"
+        @click="saveTemplate"
+      >
+        保存模版
+      </el-button>
     </div>
     
     <!-- 中间画布区域 -->
-    <div class="canvas-area">
+    <div class="canvas-area" ref="canvasAreaRef">
       <div class="canvas-wrapper">
         <div 
           ref="canvasRef"
@@ -193,6 +219,7 @@
           @mousedown="handleCanvasMouseDown"
           @mousemove="handleCanvasMouseMove"
           @mouseup="handleCanvasMouseUp"
+          @mouseleave="handleCanvasMouseUp"
           @wheel="handleCanvasWheel"
         >
           <div
@@ -304,12 +331,13 @@
 </template>
 
 <script setup>
-import { ref, computed, reactive, onMounted } from 'vue'
+import { ref, computed, reactive, onMounted, onUnmounted } from 'vue'
 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 { Material_List } from '@/api/mes/job'
 
 const canvasRef = ref(null)
+const canvasAreaRef = ref(null)
 const layerListRef = ref(null)
 const currentTool = ref('select')
 const activeTab = ref('design') // 'design' 或 'material'
@@ -398,10 +426,10 @@ const handleUpload = (options) => {
         id: ++layerIdCounter,
         name: file.name,
         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,
         opacity: 100,
         visible: true,
@@ -509,10 +537,10 @@ const addMaterialToCanvas = (material) => {
       id: ++layerIdCounter,
       name: `素材 ${layerIdCounter}`,
       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,
       opacity: 100,
       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(() => {
   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) => {
   if (!layer.visible) {
     return { display: 'none' }
@@ -812,7 +911,8 @@ const handleLayerMouseDown = (e, layer) => {
 }
 
 const handleCanvasMouseDown = (e) => {
-  if (e.target === canvasRef.value) {
+  // 确保 canvasRef 已经初始化
+  if (canvasRef.value && e.target === canvasRef.value) {
     selectedLayerId.value = null
   }
 }
@@ -931,17 +1031,19 @@ const handleCanvasWheel = (e) => {
 <style scoped>
 .template-design-container {
   display: flex;
-  height: calc(100vh - 50px);
+  height: calc(100vh - 140px);
   background-color: #f5f5f5;
 }
 
 .toolbar {
-  width: 220px;
+  width: 300px;
   background-color: #fff;
   border-right: 1px solid #ddd;
   padding: 8px 12px;
   display: flex;
   flex-direction: column;
+  box-sizing: border-box;
+  overflow: hidden; /* 外层高度锁死,内部区域单独滚动 */
 }
 
 .toolbar h3 {
@@ -1008,8 +1110,8 @@ const handleCanvasWheel = (e) => {
 .materials-list-full {
   flex: 1;
   display: grid;
-  grid-template-columns: repeat(2, 1fr);
-  gap: 8px;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 6px;
   overflow-y: auto;
   padding: 4px;
   background-color: #f9f9f9;
@@ -1021,9 +1123,9 @@ const handleCanvasWheel = (e) => {
   position: relative;
   aspect-ratio: 1;
   cursor: pointer;
-  border-radius: 6px;
+  border-radius: 8px;
   overflow: hidden;
-  border: 2px solid #ddd;
+  border: 1px solid #e5e5e5;
   transition: all 0.2s;
 }
 
@@ -1274,7 +1376,6 @@ const handleCanvasWheel = (e) => {
   padding: 8px 12px;
   display: flex;
   flex-direction: column;
-  overflow: hidden;
 }
 
 .layer-panel h3 {
@@ -1395,7 +1496,6 @@ const handleCanvasWheel = (e) => {
   flex: 1;
   font-size: 11px;
   color: #333;
-  overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
 }
@@ -1411,6 +1511,12 @@ const handleCanvasWheel = (e) => {
   margin: 6px 0;
 }
 
+.save-template-btn {
+  width: 100%;
+  margin-top: 4px;
+  flex-shrink: 0;
+}
+
 /* 隐藏滚动条但保持功能 */
 .layer-list::-webkit-scrollbar,
 .layer-info::-webkit-scrollbar {
@@ -1458,7 +1564,6 @@ const handleCanvasWheel = (e) => {
   grid-template-columns: repeat(2, 1fr);
   gap: 6px;
   max-height: 200px;
-  overflow-y: auto;
   padding: 4px;
   background-color: #f9f9f9;
   border-radius: 4px;
@@ -1527,4 +1632,65 @@ const handleCanvasWheel = (e) => {
 .materials-list::-webkit-scrollbar-track {
   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>