瀏覽代碼

feat(marketingEdit): 新增画布排序与类型区分功能

- 在 canvas.json 中为每个画布配置增加 canvas_type 字段,用于标识画布类型(normal/model/scene)
- 更新模板 header.js 和 view.js,添加画布上下移动按钮及事件处理逻辑
- 样式文件 header.scss 中新增相关样式支持画布操作按钮布局
- 在 index.vue 中实现画布拖拽、移动、重载以及查找可编辑画布的方法
- 调整画布切换逻辑,自动跳过不可编辑的模特图/场景图画布
- 引入并整合新的样式文件以优化界面展示效果
panqiuyao 5 天之前
父節點
當前提交
5b4547b5f6

File diff suppressed because it is too large
+ 1 - 1
frontend/src/views/components/marketingEdit/canvas.json


+ 99 - 27
frontend/src/views/components/marketingEdit/index.vue

@@ -91,25 +91,16 @@ export default {
   watch: {
     index(newValue, oldValue) {
       if (this.isEmpty) return
+      const redirectIndex = this.findEditableCanvas(newValue)
       const target = this.data[newValue]
       // 模特图/场景图不进入编辑,自动跳到下一个普通画布
-      if(target && (target.canvas_type === 'model' || target.canvas_type === 'scene')){
-        let nextIdx = -1
-        for(let i = newValue + 1; i < this.data.length; i++){
-          if(this.data[i].canvas_type !== 'model' && this.data[i].canvas_type !== 'scene'){
-            nextIdx = i; break
-          }
-        }
-        if(nextIdx === -1){
-          for(let i = newValue - 1; i >= 0; i--){
-            if(this.data[i].canvas_type !== 'model' && this.data[i].canvas_type !== 'scene'){
-              nextIdx = i; break
-            }
-          }
-        }
+      if(
+        target &&
+        (target.canvas_type === 'model' || target.canvas_type === 'scene')
+      ){
         // 如果没有可切换的普通画布,保持当前索引,避免递归更新
-        if(nextIdx !== -1 && nextIdx !== newValue){
-          this.$emit('update:index', nextIdx)
+        if(redirectIndex !== -1 && redirectIndex !== newValue){
+          this.$emit('update:index', redirectIndex)
         }
         return
       }
@@ -200,6 +191,10 @@ export default {
       // 模特图/场景图不初始化 fabric,直接跳过
       const canvasType = this.this_canvas?.canvas_type || 'normal'
       if (canvasType === 'model' || canvasType === 'scene') {
+        const redirectIndex = this.findEditableCanvas(this.index)
+        if(redirectIndex !== -1 && redirectIndex !== this.index){
+          this.$emit('update:index', redirectIndex)
+        }
         // 模特/场景图不需要 fabric,保持占位
         return
       }
@@ -520,6 +515,7 @@ export default {
           name: item.name || '',
           tpl_url: item.tpl_url || '',
           image_path: item.image_path || '',
+          canvas_type: item.canvas_type || 'normal',
           multi_goods_mode: item.multi_goods_mode || '',
           max_goods_count: item.max_goods_count || null,
         }
@@ -594,6 +590,56 @@ export default {
       }
       this.data.splice(snapshotIndex, 1, updated)
     },
+    handleMoveCanvas(idx, direction){
+      const offset = direction === 'up' ? -1 : 1
+      const targetIdx = idx + offset
+      if(targetIdx < 0 || targetIdx >= this.data.length) return
+      this.saveCanvasSnapshot()
+      const currentCanvas = this.data[this.index]
+      const [moved] = this.data.splice(idx, 1)
+      this.data.splice(targetIdx, 0, moved)
+      this.$nextTick(() => {
+        const newIndex = this.data.indexOf(currentCanvas)
+        if(newIndex !== -1 && newIndex !== this.index){
+          this.$emit('update:index', newIndex)
+        }else{
+          this.reloadCurrentCanvas(false)
+        }
+        this.reloadAllNormalCanvases()
+      })
+    },
+    reloadCurrentCanvas(scroll = true){
+      if(this.isEmpty) return
+      this.destroyCanvasInstance()
+      const canvasType = this.this_canvas?.canvas_type || 'normal'
+      if(canvasType === 'model' || canvasType === 'scene'){
+        return
+      }
+      this.$nextTick(() => {
+        this.init()
+        if(scroll){
+          this.scrollToCanvas(this.index)
+        }
+      })
+    },
+    reloadAllNormalCanvases(){
+      // 简单策略:重新加载当前画布即可,避免重复显示;其他画布在切换时再加载
+      this.reloadCurrentCanvas(false)
+    },
+    findEditableCanvas(startIndex = 0){
+      if(this.isEmpty) return -1
+      const total = this.data.length
+      const isEditable = (item) =>
+        item && item.canvas_type !== 'model' && item.canvas_type !== 'scene'
+
+      for(let i = startIndex; i < total; i++){
+        if(isEditable(this.data[i])) return i
+      }
+      for(let i = startIndex - 1; i >= 0; i--){
+        if(isEditable(this.data[i])) return i
+      }
+      return -1
+    },
     destroyCanvasInstance(){
       if(!this.fcanvas && !this.fcanvasId) return
       const canvasEl = this.fcanvas && this.fcanvas.getElement ? this.fcanvas.getElement() : document.getElementById(this.fcanvasId)
@@ -631,6 +677,10 @@ export default {
 }
 </script>
 
+<style lang="scss" >
+@import './tpl/header.scss';
+
+</style>
 <style lang="scss" scoped>
 @import '@/styles/fcanvas.scss';
 .picture-editor-wrap {
@@ -641,7 +691,6 @@ export default {
   bottom:0;
   background: #e6e6e6;
   overflow: auto;
-
   .picture-editor-wrap_canvas {
     position: relative;
     margin-top: 85px;
@@ -659,7 +708,6 @@ export default {
   @import '../PictureEditor/mixin/layer/index.scss';
   @import '../PictureEditor/mixin/color/index.scss';
   @import '../PictureEditor/mixin/edit/index.scss';
-  @import './tpl/header.scss';
 
 
   .picture-editor-empty {
@@ -764,21 +812,32 @@ export default {
   height: 100% !important;
 }
 
-.canvas-stack_switch {
+.canvas-stack_controls {
   position: absolute;
-  top: 0px;
-  right: -25px;
-  width: 25px;
+  top: 10px;
+  right: -30px;
+  display: flex;
+  flex-direction: column;
+  align-items: stretch;
+  gap: 6px;
+  width: 30px;
+}
+
+.canvas-stack_switch {
+  width: 100%;
   height: 60px;
   background: rgba(104, 188, 165, 0.1);
   border-color: rgba(104, 188, 165, 0.3);
   color: #68BCA5;
-  z-index: 10;
-  display: block;
-  padding: 0px;
   white-space: break-spaces;
-  border-top-left-radius: 0px;
-  border-bottom-left-radius: 0px;
+  padding: 5px !important;
+  ::v-deep {
+    span {
+      display: block;
+      width: 100%;
+      height: 100%;
+    }
+  }
 }
 
 .canvas-stack_body.active .canvas-stack_switch {
@@ -787,6 +846,19 @@ export default {
   color: #fff;
 }
 
+.canvas-stack_sort {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.canvas-stack_sort .el-button {
+  width: 100%;
+  padding: 0;
+  height: 28px;
+  margin-left: 0px !important;
+}
+
 .canvas-stack_placeholder {
   height: 100%;
   width: 100%;

+ 24 - 5
frontend/src/views/components/marketingEdit/tpl/header.js

@@ -17,27 +17,46 @@ export  default function tpl(){
                         {{this_canvas.name}}<el-icon class="el-icon--right"><arrow-down /></el-icon>
                       </el-button>
                       <template #dropdown>
-                        <el-dropdown-menu>
+                        <el-dropdown-menu
+                          @dragover.prevent
+                          @drop.prevent="handleCanvasDrop(data.length)"
+                        >
                           <el-dropdown-item
                             v-for="(item,idx) in data"
                             :key="(item.id || item.tpl_url || idx) + '-' + idx"
                             @click.native.stop
                           >
-                            <div style="display: flex; align-items: center; justify-content: space-between; width: 100%;">
+                            <div class="canvas-dropdown-item">
                               <span 
                                 :class="idx === index ? 'c-primary' : ''" 
-                                style="flex: 1; cursor: pointer;"
+                                class="canvas-name"
                                 @click="handleSelectCanvas(idx)"
                               >
                                 {{item.name || ('画布 ' + (idx + 1))}}
                               </span>
+                                <el-button
+                                  text
+                                  size="small"
+                                  icon="ArrowUp"
+                                  class="canvas-sort-btn"
+                                  @click.stop="handleMoveCanvas(idx, 'up')"
+                                  :disabled="idx === 0"
+                                />
+                                <el-button
+                                  text
+                                  size="small"
+                                  class="canvas-sort-btn"
+                                  icon="ArrowDown"
+                                  @click.stop="handleMoveCanvas(idx, 'down')"
+                                  :disabled="idx === data.length - 1"
+                                />
                               <el-button
                                 type="danger"
                                 text
                                 size="small"
                                 :disabled="data.length === 1"
-                                @click="handleDeleteCanvas(idx)"
-                                style="margin-left: 10px; padding: 0 8px;"
+                                @click.stop="handleDeleteCanvas(idx)"
+                                class="canvas-delete-btn"
                               >
                                 删除
                               </el-button>

+ 27 - 0
frontend/src/views/components/marketingEdit/tpl/header.scss

@@ -10,3 +10,30 @@
   line-height: 30px;
 }
 
+.canvas-dropdown-item {
+  display: flex;
+  align-items: center;
+  width: 300px;
+  gap: 6px;
+}
+
+
+.canvas-dropdown-item .canvas-name {
+  flex: 1;
+  cursor: pointer;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  margin-right: 10px;
+}
+
+.canvas-dropdown-item .canvas-delete-btn {
+  padding: 0 8px;
+  .el-button { margin-left: 5px !important;}
+}
+
+.canvas-sort-btn {
+  padding: 0px 5px 0px 10px !important;
+  width: 25px;
+}
+

+ 29 - 11
frontend/src/views/components/marketingEdit/tpl/view.js

@@ -18,17 +18,35 @@ export default function() {
             :class="idx === index ? 'active' : 'inactive'"
             :style="canvasBodyStyle(item)"
           >
-            <el-button
-              v-if="item.canvas_type !== 'model' && item.canvas_type !== 'scene'"
-              size="small"
-              class="canvas-stack_switch"
-              type="primary"
-              plain
-              :disabled="idx === index"
-              @click="handleSelectCanvas(idx)"
-            >
-              {{ idx === index ? '正在编辑' : '切换画布' }}
-            </el-button>
+            <div class="canvas-stack_controls">
+              <el-button
+                v-if="item.canvas_type !== 'model' && item.canvas_type !== 'scene'"
+                size="small"
+                class="canvas-stack_switch"
+                type="primary"
+                plain
+                :disabled="idx === index"
+                @click="handleSelectCanvas(idx)"
+              >
+                {{ idx === index ? '正在编辑' : '切换画布' }}
+              </el-button>
+              <div class="canvas-stack_sort">
+                <el-button
+                  text
+                  size="small"
+                  icon="ArrowUp"
+                  @click.stop="handleMoveCanvas(idx, 'up')"
+                  :disabled="idx === 0"
+                />
+                <el-button
+                  text
+                  size="small"
+                  icon="ArrowDown"
+                  @click.stop="handleMoveCanvas(idx, 'down')"
+                  :disabled="idx === data.length - 1"
+                />
+              </div>
+            </div>
             <!-- 模特图/场景图占位 -->
             <template v-if="item.canvas_type === 'model' || item.canvas_type === 'scene'">
               <div class="canvas-special-placeholder" :class="item.canvas_type">

Some files were not shown because too many files changed in this diff