Prechádzať zdrojové kódy

feat(marketingEdit): 优化画布切换与预览功能

- 新增画布销毁逻辑,确保组件卸载时正确清理资源
- 为画布数据增加 preview 字段用于预览图展示
- 改进 canvasBodyStyle 方法支持宽度设置并保持响应式
- 在 saveCanvasSnapshot 中生成并保存画布预览图
- 调整模板结构将操作按钮移入画布区域
- 更新样式以提升视觉一致性并增强交互反馈
- 使用预览图替代原始图像路径提高加载性能
- 重构 DOM 操作逻辑防止内存泄漏和节点残留问题
panqiuyao 2 týždňov pred
rodič
commit
cce8e136ff

+ 96 - 43
frontend/src/views/components/marketingEdit/index.vue

@@ -78,6 +78,7 @@ export default {
     index(newValue, oldValue) {
       if (this.isEmpty) return
       this.saveCanvasSnapshot(oldValue)
+      this.destroyCanvasInstance()
       this.$nextTick(() => {
         this.init()
       })
@@ -185,6 +186,7 @@ export default {
                height:this.canvasForm.height,
                bg_color:this.canvasForm.bg_color,
                canvas_json:'',
+               preview:''
         })
         const nextIndex = this.data.length - 1
         this.$emit('update:index',nextIndex)
@@ -236,14 +238,17 @@ export default {
     },
     canvasBodyStyle(item){
       const height = Number(item?.height)
-      if(!height || Number.isNaN(height)){
-        return {
-          minHeight: '200px'
-        }
+      const width = Number(item?.width) || FIXED_CANVAS_WIDTH
+      const style = {
+        width: `${width}px`,
+        margin: '0 auto'
       }
-      return {
-        height: `${height}px`
+      if(!height || Number.isNaN(height)){
+        style.minHeight = '200px'
+      }else{
+        style.height = `${height}px`
       }
+      return style
     },
     saveCanvasSnapshot(targetIndex){
       const snapshotIndex = typeof targetIndex === 'number' ? targetIndex : this.index
@@ -251,23 +256,50 @@ export default {
       const canvasData = this.data[snapshotIndex]
       if(!canvasData) return
       const json = JSON.stringify(this.fcanvas.toJSON(['name','sort','mtr','id','selectable','erasable','data-key','data-value']))
-      if(Object.prototype.hasOwnProperty.call(canvasData, 'canvas_json')){
-        canvasData.canvas_json = json
-      }else{
-        const updated = {
-          ...canvasData,
-          canvas_json: json
-        }
-        this.data.splice(snapshotIndex, 1, updated)
+      let preview = canvasData.preview || ''
+      try{
+        preview = this.fcanvas.toDataURL({
+          format: 'png',
+          multiplier: 1,
+          enableRetinaScaling: true
+        })
+      }catch (err){
+        console.warn('[marketingEdit] snapshot preview failed', err)
       }
+      const updated = {
+        ...canvasData,
+        canvas_json: json,
+        preview
+      }
+      this.data.splice(snapshotIndex, 1, updated)
     },
     destroyCanvasInstance(){
-      if(!this.fcanvas) return
+      if(!this.fcanvas && !this.fcanvasId) return
+      const canvasEl = this.fcanvas && this.fcanvas.getElement ? this.fcanvas.getElement() : document.getElementById(this.fcanvasId)
+      const wrapper = canvasEl && canvasEl.parentNode && canvasEl.parentNode.classList?.contains('fcanvas')
+        ? canvasEl.parentNode
+        : null
+      const parentNode = wrapper ? wrapper.parentNode : (canvasEl ? canvasEl.parentNode : null)
+      const nextSibling = wrapper ? wrapper.nextSibling : (canvasEl ? canvasEl.nextSibling : null)
       try{
-        this.fcanvas.dispose()
+        this.fcanvas && this.fcanvas.dispose()
       }catch(err){
         console.warn('[marketingEdit] dispose canvas failed', err)
       }finally{
+        if(wrapper && parentNode){
+          parentNode.removeChild(wrapper)
+        }else if(canvasEl && parentNode){
+          parentNode.removeChild(canvasEl)
+        }
+        if(parentNode && this.fcanvasId){
+          const placeholder = document.createElement('canvas')
+          placeholder.id = this.fcanvasId
+          placeholder.width = Number(this.this_canvas?.width) || FIXED_CANVAS_WIDTH
+          placeholder.height = Number(this.this_canvas?.height) || 0
+          placeholder.style.width = '100%'
+          placeholder.style.height = '100%'
+          parentNode.insertBefore(placeholder, nextSibling || null)
+        }
         this.fcanvas = null
         this.fcanvasId = ''
       }
@@ -357,51 +389,69 @@ export default {
   width: 100%;
   display: flex;
   flex-direction: column;
-  gap: 20px;
-  padding-bottom: 40px;
+  gap: 0;
 }
 
 .canvas-stack_item {
   width: 100%;
-  border-radius: 8px;
-  box-shadow: 0 6px 16px rgba(0,0,0,0.06);
-  padding: 16px;
+  border-radius: 0;
+  box-shadow: none;
+  padding: 0;
   box-sizing: border-box;
+  position: relative;
+  border-bottom: 1px solid #f0f0f0;
 }
 
-.canvas-stack_header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 12px;
+.canvas-stack_body {
+  background: transparent;
+  border-radius: 0;
+  padding: 0;
+  box-sizing: border-box;
+  position: relative;
+  min-height: 200px;
+  overflow: hidden;
+  transition: background 0.2s, box-shadow 0.2s, border 0.2s;
 }
 
-.canvas-stack_title {
-  display: flex;
-  gap: 12px;
-  font-size: 16px;
-  font-weight: 600;
+.canvas-stack_body.active {
+  background: #f6fbff;
+  box-shadow: inset 0 0 0 2px $primary1;
 }
 
-.canvas-stack_title .size {
-  font-size: 13px;
-  color: #666;
+.canvas-stack_body canvas {
+  display: block;
+  margin: 0 auto;
 }
 
-.canvas-stack_body {
-  background: #fff;
-  border-radius: 6px;
-  padding: 12px;
-  box-sizing: border-box;
+.canvas-stack_body .fcanvas {
+  width: 100% !important;
+  height: 100% !important;
 }
 
-.canvas-stack_body canvas {
-  display: block;
-  margin: 0 auto;
+.canvas-stack_body .upper-canvas,
+.canvas-stack_body .lower-canvas {
+  width: 100% !important;
+  height: 100% !important;
+}
+
+.canvas-stack_switch {
+  position: absolute;
+  top: 12px;
+  right: 12px;
+  background: rgba($primary1, 0.1);
+  border-color: rgba($primary1, 0.3);
+  color: $primary1;
+}
+
+.canvas-stack_body.active .canvas-stack_switch {
+  background: $primary1;
+  border-color: $primary1;
+  color: #fff;
 }
 
 .canvas-stack_placeholder {
   height: 100%;
+  width: 100%;
   display: flex;
   align-items: center;
   justify-content: center;
@@ -409,6 +459,8 @@ export default {
   color: #888;
   font-size: 14px;
   text-align: center;
+  padding: 40px 0;
+  box-sizing: border-box;
 }
 
 .canvas-stack_placeholder img {
@@ -420,8 +472,9 @@ export default {
 .canvas-stack_empty {
   width: 100%;
   text-align: center;
-  padding: 80px 0;
+  padding: 40px 0;
   color: #666;
+  border-bottom: 1px solid #f0f0f0;
 }
 
 .fixed-width-tip {

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

@@ -12,34 +12,34 @@ export default function() {
           :key="(item.id || item.tpl_url || idx) + '-' + idx"
           :style="{backgroundColor: item.bg_color || '#fff'}"
         >
-          <div class="canvas-stack_header">
-            <div class="canvas-stack_title">
-              <span class="name">{{ item.name || ('画布 ' + (idx + 1)) }}</span>
-              <span class="size">{{ item.width }} x {{ item.height }}</span>
-            </div>
+          <div
+            class="canvas-stack_body"
+            :class="idx === index ? 'active' : 'inactive'"
+            :style="canvasBodyStyle(item)"
+          >
             <el-button
               size="small"
-              type="text"
+              class="canvas-stack_switch"
+              type="primary"
+              plain
+              :disabled="idx === index"
               @click="handleSelectCanvas(idx)"
             >
-              {{ idx === index ? '当前画布' : '切换到此画布' }}
+              {{ idx === index ? '正在编辑' : '切换画布' }}
             </el-button>
-          </div>
-          <div
-            class="canvas-stack_body"
-            :style="canvasBodyStyle(item)"
-          >
-            <canvas
-              v-if="idx === index"
-              :id="'marketing-canvas-' + idx"
-              :width="item.width"
-              :height="item.height"
-            />
-            <div v-else class="canvas-stack_placeholder">
-              <img v-if="item.image_path" :src="item.image_path" alt="">
+            <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>
           </div>
         </div>
       </div>