Browse Source

feat(marketingEdit): 更新画布预览图片并优化滚动定位逻辑

- 更新了两个画布模板的 preview 图片链接
- 修改了 canvas_json 中文本框的字体和对齐方式
- 新增 scrollToCanvas 方法实现画布间平滑滚动定位
- 调整画布容器样式,将 overflow 设置为 visible
- 优化 canvas-switch 按钮位置和样式表现
- 在新增画布后触发滚动到当前画布的逻辑
- 为画布元素添加 ref 引用以支持精确定位
panqiuyao 1 week ago
parent
commit
bf462ed207

+ 2 - 2
frontend/src/views/Home/index.vue

@@ -14,7 +14,7 @@
     <!-- 左侧图片区域 -->
     <div class="image-container left-image" @click="goCheck" v-log="{ describe: { action: '点击拍照检查入口' } }">
       <img src="@/assets/images/home/left.png" alt="拍摄产品并处理图像" class="zoom-on-hover" />
-      <div class="overlay-text">新加模板</div>
+      <div class="overlay-text">模板列表</div>
     </div>
 
     <!-- 右侧图片区域 -->
@@ -75,7 +75,7 @@ function socketConnect(){
 
 const goCheck = async () => {
   router.push({
-    name: 'addTpl'
+    name: 'tpl'
   });
 };
 

+ 37 - 8
frontend/src/views/components/PictureEditor/mixin/edit/index.js

@@ -143,7 +143,10 @@ export default  {
           if (this.editLayer.strokeObj) {
             this.fcanvas.add(markRaw(this.editLayer.strokeObj));
           }
-          this.updateClipPreview && this.updateClipPreview()
+          // 延迟调用 updateClipPreview,确保对象已完全加载
+          this.$nextTick(() => {
+            this.updateClipPreview && this.updateClipPreview()
+          })
           break;
       }
     },
@@ -457,7 +460,23 @@ export default  {
     updateClipPreview(force=false){
       if(!this.editLayer || this.editLayer.type !== 'image') return
       const obj = this.editLayer
+      
+      // 检查对象是否已完全加载(对于图片对象)
+      if (obj.type === 'image' && obj._element && !obj._element.complete) {
+        // 如果图片还在加载中,等待加载完成后再执行
+        const onLoad = () => {
+          obj.off('loaded', onLoad)
+          this.updateClipPreview(force)
+        }
+        obj.on('loaded', onLoad)
+        return
+      }
+      
       const bounds = obj.getBoundingRect(true)
+      if (!bounds || bounds.width <= 0 || bounds.height <= 0) {
+        return // 无效的边界,跳过预览
+      }
+      
       this.clipPreview = {
         ...this.clipPreview,
         maxOffsetX: Math.max(0, Math.floor(bounds.width)),
@@ -468,13 +487,23 @@ export default  {
       }
       if(force || this.clipPreview.dataUrl === ''){
         try{
-          const cloned = obj.clone()
-          const tmp = new fabric.Canvas(document.createElement('canvas'), { width: Math.min(200, bounds.width), height: Math.min(200, bounds.height) })
-          cloned.scaleToWidth(tmp.width)
-          tmp.add(cloned)
-          tmp.renderAll()
-          this.clipPreview.dataUrl = tmp.toDataURL({ format:'png', multiplier:1 })
-          tmp.dispose()
+          // clone 方法在对象有 filters 时是异步的,需要使用回调
+          obj.clone((cloned) => {
+            if (!cloned) {
+              console.warn('clip preview: clone failed')
+              return
+            }
+            try {
+              const tmp = new fabric.Canvas(document.createElement('canvas'), { width: Math.min(200, bounds.width), height: Math.min(200, bounds.height) })
+              cloned.scaleToWidth(tmp.width)
+              tmp.add(cloned)
+              tmp.renderAll()
+              this.clipPreview.dataUrl = tmp.toDataURL({ format:'png', multiplier:1 })
+              tmp.dispose()
+            } catch(e) {
+              console.warn('clip preview render failed', e)
+            }
+          })
         }catch(e){
           console.warn('clip preview failed', e)
         }

+ 231 - 6
frontend/src/views/components/marketingEdit/index.vue

@@ -72,6 +72,7 @@ export default {
         color:"#fff",
         bg_color:'#fff',
         visible:false,
+        canvas_type: 'normal', // 画布类型:'normal'(普通画布), 'model'(模特图), 'scene'(场景图)
         multi_goods_mode: '', // 多货号模式:''(默认单货号), 'single'(一个货号多角度), 'multiple'(多个货号同角度)
         max_goods_count: null, // 多货号模式下,最多可追加多少个货号
       }
@@ -83,13 +84,75 @@ export default {
     isEmpty(){
       return this.data.length === 0
     },
+    // 按 sort 字段排序后的画布数据
+    sortedData(){
+      return [...this.data].sort((a, b) => {
+        const sortA = a.sort !== undefined ? a.sort : (a._sortIndex !== undefined ? a._sortIndex : 999999)
+        const sortB = b.sort !== undefined ? b.sort : (b._sortIndex !== undefined ? b._sortIndex : 999999)
+        return sortA - sortB
+      })
+    },
+    // 根据排序后的数据获取当前画布
     this_canvas(){
-      return this.data[this.index]
+      const sorted = this.sortedData
+      return sorted[this.index] || this.data[this.index]
+    },
+    // 获取当前画布在原始数据中的索引
+    currentCanvasIndex(){
+      const sorted = this.sortedData
+      const currentCanvas = sorted[this.index]
+      if(!currentCanvas) return this.index
+      return this.data.findIndex(item => item === currentCanvas)
     }
   },
+  methods: {
+    // 辅助:通过排序后的索引,找到 data 中的真实索引
+    getDataIndexBySortedIndex(sortedIdx){
+      if(sortedIdx === undefined || sortedIdx === null) return -1
+      const sorted = this.sortedData
+      const item = sorted[sortedIdx]
+      if(!item) return -1
+      if(item._uid){
+        const found = this.data.findIndex(d => d._uid === item._uid)
+        if(found !== -1) return found
+      }
+      const foundByRef = this.data.findIndex(d => d === item)
+      return foundByRef !== -1 ? foundByRef : -1
+    },
+  },
   watch: {
     index(newValue, oldValue) {
       if (this.isEmpty) return
+      const sorted = this.sortedData
+      const targetCanvas = sorted[newValue]
+      // 场景图和模特图不能被激活,如果切换到这些类型,自动切换到下一个普通画布
+      if(targetCanvas && (targetCanvas.canvas_type === 'model' || targetCanvas.canvas_type === 'scene')){
+        // 查找下一个普通画布
+        let nextNormalIndex = -1
+        for(let i = newValue + 1; i < sorted.length; i++){
+          if(sorted[i].canvas_type === 'normal'){
+            nextNormalIndex = i
+            break
+          }
+        }
+        // 如果后面没有,往前找
+        if(nextNormalIndex === -1){
+          for(let i = newValue - 1; i >= 0; i--){
+            if(sorted[i].canvas_type === 'normal'){
+              nextNormalIndex = i
+              break
+            }
+          }
+        }
+        // 如果找到了普通画布,切换到它;否则保持原索引
+        if(nextNormalIndex !== -1){
+          this.$emit('update:index', nextNormalIndex)
+        } else {
+          // 如果没有普通画布,恢复到之前的索引
+          this.$emit('update:index', oldValue)
+        }
+        return
+      }
       this.saveCanvasSnapshot(oldValue)
       this.destroyCanvasInstance()
       this.$nextTick(() => {
@@ -99,6 +162,15 @@ export default {
     }
   },
   mounted() {
+    // 初始化 sort 和唯一 _uid 字段(如果不存在)
+    this.data.forEach((item, idx) => {
+      if(item.sort === undefined){
+        item.sort = idx
+      }
+      if(!item._uid){
+        item._uid = `canvas-${Date.now()}-${Math.random().toString(16).slice(2)}-${idx}`
+      }
+    })
     if(this.$route?.name === 'editTpl'){
       this.loadTemplate()
     }
@@ -111,7 +183,17 @@ export default {
   methods: {
     loadTemplate(){
       if(this.hasLoadedTpl) return
-      this.data.splice(0, this.data.length, ...JSON.parse(JSON.stringify(canvas)))
+      const templateData = JSON.parse(JSON.stringify(canvas))
+      // 为每个画布初始化 sort 和唯一 _uid 字段
+      templateData.forEach((item, idx) => {
+        if(item.sort === undefined){
+          item.sort = idx
+        }
+        if(!item._uid){
+          item._uid = `canvas-${Date.now()}-${Math.random().toString(16).slice(2)}-${idx}`
+        }
+      })
+      this.data.splice(0, this.data.length, ...templateData)
       this.$emit('update:index', 0)
       this.hasLoadedTpl = true
     },
@@ -169,6 +251,24 @@ export default {
         return;
       }
 
+      // 模特图和场景图不需要初始化 canvas
+      const canvasType = this.this_canvas?.canvas_type || 'normal'
+      if (canvasType === 'model' || canvasType === 'scene') {
+        // 如果当前是场景图/模特图,自动切换到普通画布
+        const sorted = this.sortedData
+        let nextNormalIndex = -1
+        for(let i = 0; i < sorted.length; i++){
+          if(sorted[i].canvas_type === 'normal'){
+            nextNormalIndex = i
+            break
+          }
+        }
+        if(nextNormalIndex !== -1 && nextNormalIndex !== this.index){
+          this.$emit('update:index', nextNormalIndex)
+        }
+        return
+      }
+
 /*
       //画布下不存在模板OSS地址
       if(!this.this_canvas){
@@ -232,6 +332,7 @@ export default {
       this.canvasForm.width = FIXED_CANVAS_WIDTH
       this.canvasForm.height = 1024
       this.canvasForm.bg_color = '#fff'
+      this.canvasForm.canvas_type = 'normal'
       this.canvasForm.multi_goods_mode = ''
       this.canvasForm.max_goods_count = null
       this.canvasForm.visible = true;
@@ -243,24 +344,53 @@ export default {
       this.canvasForm.width = FIXED_CANVAS_WIDTH
       this.canvasForm.height = this.this_canvas.height
       this.canvasForm.bg_color = this.this_canvas.bg_color
+      this.canvasForm.canvas_type = this.this_canvas.canvas_type || 'normal'
       this.canvasForm.multi_goods_mode = this.this_canvas.multi_goods_mode || ''
       this.canvasForm.max_goods_count = this.this_canvas.max_goods_count || null
       this.canvasForm.visible = true;
     },
+    handleCanvasTypeChange(type){
+      if(this.canvasForm.type !== 'add') return
+      if(type === 'model'){
+        this.canvasForm.name = '模特图'
+      }else if(type === 'scene'){
+        this.canvasForm.name = '场景图'
+      }else if(!this.canvasForm.name || this.canvasForm.name === '模特图' || this.canvasForm.name === '场景图'){
+        this.canvasForm.name = '画布_'+new Date().getTime().toString().substr(8)+Math.round(100)
+      }
+    },
     submitCanvasInfo() {
       // 假设 this.canvasForm 包含最新的 width, height 和 color
 
       if(this.canvasForm.type === 'add'){
         this.saveCanvasSnapshot()
+        // 计算新的 sort 值(取当前最大 sort + 1,或使用数组长度)
+        const maxSort = this.data.length > 0 
+          ? Math.max(...this.data.map(item => item.sort !== undefined ? item.sort : (item._sortIndex !== undefined ? item._sortIndex : 0))) 
+          : -1
+        // 若是模特图/场景图,名称默认改为对应名称
+        let canvasName = this.canvasForm.name
+        if(this.canvasForm.canvas_type === 'model'){
+          if(!canvasName || canvasName.startsWith('画布_') || canvasName === '场景图'){
+            canvasName = '模特图'
+          }
+        }else if(this.canvasForm.canvas_type === 'scene'){
+          if(!canvasName || canvasName.startsWith('画布_') || canvasName === '模特图'){
+            canvasName = '场景图'
+          }
+        }
         this.data.push({
                tpl_url:"",
                image_path:"",
-               name:this.canvasForm.name,
+               name:canvasName,
                width:FIXED_CANVAS_WIDTH,
                height:this.canvasForm.height,
                bg_color:this.canvasForm.bg_color,
+               canvas_type: this.canvasForm.canvas_type || 'normal',
+               _uid: `canvas-${Date.now()}-${Math.random().toString(16).slice(2)}`,
                canvas_json:'',
                preview:'',
+               sort: maxSort + 1,
                multi_goods_mode: this.canvasForm.multi_goods_mode || '',
                max_goods_count: this.canvasForm.multi_goods_mode ? (this.canvasForm.max_goods_count || null) : null,
         })
@@ -331,6 +461,7 @@ export default {
         this.data[this.index].width = FIXED_CANVAS_WIDTH
         this.data[this.index].height = newHeight
         this.data[this.index].bg_color = this.canvasForm.bg_color
+        this.data[this.index].canvas_type = this.canvasForm.canvas_type || 'normal'
         this.data[this.index].multi_goods_mode = this.canvasForm.multi_goods_mode || ''
         this.data[this.index].max_goods_count = this.canvasForm.multi_goods_mode ? (this.canvasForm.max_goods_count || null) : null
         this.canvasForm.visible = false;
@@ -344,9 +475,50 @@ export default {
     },
     handleSelectCanvas(index){
       if(index === this.index) return
+      // 场景图和模特图不能被选中/激活
+      const sorted = this.sortedData
+      const targetCanvas = sorted[index]
+      if(targetCanvas && (targetCanvas.canvas_type === 'model' || targetCanvas.canvas_type === 'scene')){
+        return
+      }
       this.saveCanvasSnapshot()
       this.$emit('update:index', index)
     },
+    handleMoveCanvas(currentIndex, direction){
+      // 先保存当前画布快照
+      this.saveCanvasSnapshot()
+      
+      const sorted = this.sortedData
+      if(currentIndex < 0 || currentIndex >= sorted.length) return
+      
+      let targetIndex
+      if(direction === 'up' && currentIndex > 0){
+        targetIndex = currentIndex - 1
+      } else if(direction === 'down' && currentIndex < sorted.length - 1){
+        targetIndex = currentIndex + 1
+      } else {
+        return // 无法移动
+      }
+      
+      // 获取要移动的画布和目标位置的画布
+      const movedCanvas = sorted[currentIndex]
+      const targetCanvas = sorted[targetIndex]
+      
+      // 交换 sort 值
+      const tempSort = movedCanvas.sort !== undefined ? movedCanvas.sort : currentIndex
+      const targetSort = targetCanvas.sort !== undefined ? targetCanvas.sort : targetIndex
+      
+      // 更新 sort 字段
+      movedCanvas.sort = targetSort
+      targetCanvas.sort = tempSort
+      
+      // 如果当前激活的是被移动的画布,更新索引
+      if(this.index === currentIndex){
+        this.$emit('update:index', targetIndex)
+      } else if(this.index === targetIndex){
+        this.$emit('update:index', currentIndex)
+      }
+    },
     handleDeleteCanvas(targetIndex){
       if(this.data.length <= 1){
         this.$message.warning('至少需要保留一个画布')
@@ -434,6 +606,19 @@ export default {
       })
     },
     canvasBodyStyle(item){
+      const canvasType = item?.canvas_type || 'normal'
+      // 模特图和场景图显示为正方形
+      if(canvasType === 'model' || canvasType === 'scene'){
+        return {
+          width: `${FIXED_CANVAS_WIDTH}px`,
+          height: `${FIXED_CANVAS_WIDTH}px`,
+          margin: '0 auto',
+          display: 'flex',
+          alignItems: 'center',
+          justifyContent: 'center'
+        }
+      }
+      // 普通画布
       const height = Number(item?.height)
       const width = Number(item?.width) || FIXED_CANVAS_WIDTH
       const style = {
@@ -468,9 +653,11 @@ export default {
           width: item.width,
           height: item.height,
           bg_color: item.bg_color,
+          canvas_type: item.canvas_type || 'normal',
           name: item.name || '',
           tpl_url: item.tpl_url || '',
           image_path: item.image_path || '',
+          sort: item.sort !== undefined ? item.sort : idx,
           multi_goods_mode: item.multi_goods_mode || '',
           max_goods_count: item.max_goods_count || null,
         }
@@ -511,9 +698,11 @@ export default {
        return { bundles }
     },
     saveCanvasSnapshot(targetIndex){
-      const snapshotIndex = typeof targetIndex === 'number' ? targetIndex : this.index
-      if(!this.fcanvas || snapshotIndex === undefined || snapshotIndex === null) return
-      const canvasData = this.data[snapshotIndex]
+      // targetIndex 为排序后的索引,需要映射到 data 的真实索引
+      const sortedIdx = typeof targetIndex === 'number' ? targetIndex : this.index
+      const dataIndex = this.getDataIndexBySortedIndex(sortedIdx)
+      if(!this.fcanvas || dataIndex === undefined || dataIndex === null || dataIndex < 0) return
+      const canvasData = this.data[dataIndex]
       if(!canvasData) return
       const json = JSON.stringify(this.fcanvas.toJSON(['name','sort','mtr','id','selectable','erasable','data-key','data-type','data-value']))
       let preview = canvasData.preview || ''
@@ -753,6 +942,42 @@ export default {
   border-bottom: 1px solid #f0f0f0;
 }
 
+.canvas-special-placeholder {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #f5f5f5;
+  border: 2px dashed #d0d0d0;
+  border-radius: 4px;
+  
+  &.model {
+    border-color: #68BCA5;
+    background: rgba(104, 188, 165, 0.05);
+  }
+  
+  &.scene {
+    border-color: #409EFF;
+    background: rgba(64, 158, 255, 0.05);
+  }
+}
+
+.canvas-special-placeholder .special-placeholder-content {
+  font-size: 24px;
+  font-weight: bold;
+  color: #666;
+  text-align: center;
+}
+
+.canvas-special-placeholder.model .special-placeholder-content {
+  color: #68BCA5;
+}
+
+.canvas-special-placeholder.scene .special-placeholder-content {
+  color: #409EFF;
+}
+
 .fixed-width-tip {
   line-height: 32px;
   color: #666;

+ 58 - 18
frontend/src/views/components/marketingEdit/tpl/header.js

@@ -19,28 +19,52 @@ export  default function tpl(){
                       <template #dropdown>
                         <el-dropdown-menu>
                           <el-dropdown-item
-                            v-for="(item,idx) in data"
-                            :key="(item.id || item.tpl_url || idx) + '-' + idx"
+                            v-for="(item,idx) in sortedData"
+                            :key="item._uid || item.id || (item.tpl_url || '') + '-' + idx"
                             @click.native.stop
                           >
                             <div style="display: flex; align-items: center; justify-content: space-between; width: 100%;">
                               <span 
-                                :class="idx === index ? 'c-primary' : ''" 
-                                style="flex: 1; cursor: pointer;"
+                                :class="(idx === index && item.canvas_type !== 'model' && item.canvas_type !== 'scene') ? 'c-primary' : ''" 
+                                :style="item.canvas_type === 'model' || item.canvas_type === 'scene' ? 'flex: 1; cursor: not-allowed; color: #999;' : 'flex: 1; cursor: pointer;'"
                                 @click="handleSelectCanvas(idx)"
                               >
                                 {{item.name || ('画布 ' + (idx + 1))}}
+                                <span v-if="item.canvas_type === 'model'" style="color: #68BCA5; margin-left: 4px;">(模特图)</span>
+                                <span v-if="item.canvas_type === 'scene'" style="color: #409EFF; margin-left: 4px;">(场景图)</span>
                               </span>
-                              <el-button
-                                type="danger"
-                                text
-                                size="small"
-                                :disabled="data.length === 1"
-                                @click="handleDeleteCanvas(idx)"
-                                style="margin-left: 10px; padding: 0 8px;"
-                              >
-                                删除
-                              </el-button>
+                              <div style="display: flex; align-items: center; gap: 4px;">
+                                <el-button
+                                  text
+                                  size="small"
+                                  :disabled="idx === 0"
+                                  @click="handleMoveCanvas(idx, 'up')"
+                                  style="padding: 0 4px;"
+                                  title="上移"
+                                >
+                                  ↑
+                                </el-button>
+                                <el-button
+                                  text
+                                  size="small"
+                                  :disabled="idx === sortedData.length - 1"
+                                  @click="handleMoveCanvas(idx, 'down')"
+                                  style="padding: 0 4px;"
+                                  title="下移"
+                                >
+                                  ↓
+                                </el-button>
+                                <el-button
+                                  type="danger"
+                                  text
+                                  size="small"
+                                  :disabled="data.length === 1"
+                                  @click="handleDeleteCanvas(idx)"
+                                  style="margin-left: 4px; padding: 0 8px;"
+                                >
+                                  删除
+                                </el-button>
+                              </div>
                             </div>
                           </el-dropdown-item>
                         </el-dropdown-menu>
@@ -55,19 +79,35 @@ export  default function tpl(){
             <!-- 新增:调整画布尺寸弹窗 -->
             <el-dialog :title="canvasForm.type === 'add' ? '新增画布' : '编辑画布'" v-model="canvasForm.visible" append-to-body width="700px">
               <el-form :model="canvasForm" label-width="100px">
+                <el-form-item label="画布类型">
+                  <el-radio-group
+                    v-model="canvasForm.canvas_type"
+                    :disabled="canvasForm.type === 'edit'"
+                    @change="handleCanvasTypeChange"
+                  >
+                    <el-radio label="normal">普通画布</el-radio>
+                    <el-radio label="model">模特图</el-radio>
+                    <el-radio label="scene">场景图</el-radio>
+                  </el-radio-group>
+                  <div style="font-size: 12px; color: #999; margin-top: 8px; line-height: 1.6;">
+                    <div v-if="canvasForm.canvas_type === 'normal'">普通画布,可编辑内容</div>
+                    <div v-else-if="canvasForm.canvas_type === 'model'">模特图占位,用于前台/后端生成时替换</div>
+                    <div v-else-if="canvasForm.canvas_type === 'scene'">场景图占位,用于前台/后端生成时替换</div>
+                  </div>
+                </el-form-item>
                 <el-form-item label="画布名称">
                   <el-input v-model="canvasForm.name" placeholder="请输入画布名称"></el-input>
                 </el-form-item>
           <!--      <el-form-item label="宽度">
                   <div class="fixed-width-tip">800(固定)</div>
                 </el-form-item>-->
-                <el-form-item label="高度">
+                <el-form-item v-if="canvasForm.canvas_type === 'normal'" label="高度">
                   <el-input v-model.number="canvasForm.height" placeholder="请输入高度"></el-input>
                 </el-form-item>
-                <el-form-item label="背景颜色">
+                <el-form-item v-if="canvasForm.canvas_type === 'normal'" label="背景颜色">
                     <el-color-picker v-model="canvasForm.bg_color" />
                 </el-form-item>
-                <el-form-item label="多货号模式">
+                <el-form-item v-if="canvasForm.canvas_type === 'normal'" label="多货号模式">
                   <el-radio-group v-model="canvasForm.multi_goods_mode">
                     <el-radio label="">默认(单货号)</el-radio>
                     <el-radio label="single">一个货号多角度</el-radio>
@@ -80,7 +120,7 @@ export  default function tpl(){
                   </div>
                 </el-form-item>
                 <el-form-item
-                  v-if="canvasForm.multi_goods_mode === 'single' || canvasForm.multi_goods_mode === 'multiple'"
+                  v-if="canvasForm.canvas_type === 'normal' && (canvasForm.multi_goods_mode === 'single' || canvasForm.multi_goods_mode === 'multiple')"
                   label="最多货号数量"
                 >
                   <el-input-number

+ 32 - 15
frontend/src/views/components/marketingEdit/tpl/view.js

@@ -8,17 +8,18 @@ export default function() {
       <div v-if="!isEmpty" class="canvas-stack">
         <div
           class="canvas-stack_item"
-          v-for="(item, idx) in data"
-          :key="(item.id || item.tpl_url || idx) + '-' + idx"
+          v-for="(item, idx) in sortedData"
+          :key="item._uid || item.id || (item.tpl_url || '') + '-' + idx"
           :ref="'canvasItem-' + idx"
           :style="{backgroundColor: item.bg_color || '#fff'}"
         >
           <div
             class="canvas-stack_body"
-            :class="idx === index ? 'active' : 'inactive'"
+            :class="(idx === index && item.canvas_type !== 'model' && item.canvas_type !== 'scene') ? '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"
@@ -28,18 +29,34 @@ export default function() {
             >
               {{ idx === index ? '正在编辑' : '切换画布' }}
             </el-button>
-            <div v-if="idx !== index"  class="canvas-stack_placeholder">
-              <img v-if="item.preview" :src="item.preview" alt="">
-              <img v-else-if="item.image_path" :src="item.image_path" alt="">
-              <img v-else-if="item.tpl_url" :src="item.tpl_url" alt="">
-              <span v-else>点击“切换到此画布”开始编辑</span>
-            </div>
-            <template  v-else >
-              <canvas
-                :id="'marketing-canvas-' + idx"
-                :width="item.width"
-                :height="item.height"
-              />
+            <!-- 模特图/场景图占位 -->
+            <template v-if="item.canvas_type === 'model' || item.canvas_type === 'scene'">
+              <div class="canvas-special-placeholder" :class="item.canvas_type">
+                <div class="special-placeholder-content">
+                  {{ item.canvas_type === 'model' ? '模特图' : '场景图' }}
+                </div>
+              </div>
+            </template>
+            <!-- 普通画布 -->
+            <template v-else>
+              <div 
+                v-if="idx !== index"  
+                class="canvas-stack_placeholder"
+                @click="handleSelectCanvas(idx)"
+                style="cursor: pointer;"
+              >
+                <img v-if="item.preview" :src="item.preview" alt="">
+                <img v-else-if="item.image_path" :src="item.image_path" alt="">
+                <img v-else-if="item.tpl_url" :src="item.tpl_url" alt="">
+                <span v-else>点击"切换到此画布"开始编辑</span>
+              </div>
+              <template  v-else >
+                <canvas
+                  :id="'marketing-canvas-' + idx"
+                  :width="item.width"
+                  :height="item.height"
+                />
+              </template>
             </template>
           </div>
         </div>