浏览代码

feat(marketingEdit): 实现多画布管理与固定宽度支持

- 新增画布堆栈展示结构,支持多个画布切换编辑
- 设置画布宽度固定为800px,并添加提示信息
- 实现画布状态快照保存与恢复功能
- 添加画布实例销毁逻辑,防止内存泄漏
- 优化画布初始化流程,支持从JSON数据加载
- 调整画布容器样式,改善视觉呈现效果
- 增加画布选择事件处理与样式计算方法
- 修改画布表单默认值及校验逻辑
- 更新组件模板结构与类名定义
- 添加beforeDestroy钩子确保组件卸载时清理资源
panqiuyao 2 周之前
父节点
当前提交
41cafe7ed7

+ 201 - 30
frontend/src/views/components/marketingEdit/index.vue

@@ -9,6 +9,8 @@ import layerMixins from '@/views/components/PictureEditor/mixin/layer/index'
 import colorMixins from '@/views/components/PictureEditor/mixin/color/index'
 import editMixins from '@/views/components/PictureEditor/mixin/edit/index'
 import { uploadBaseImg } from '@/apis/other'
+
+const FIXED_CANVAS_WIDTH = 800
 export default {
   name: 'marketingImageEditor',
   mixins: [ viewMixins,actionsMixins,layerMixins,colorMixins,editMixins],
@@ -38,20 +40,26 @@ export default {
     },
 
   },
+  beforeDestroy() {
+    this.saveCanvasSnapshot()
+    this.destroyCanvasInstance()
+  },
   data() {
     return {
       disabled:false,
 
-      fcanvas: {},
+      fcanvas: null,
+      fcanvasId: '',
       scale: 1,
       sceneTplImg:"",//生成的时候记录下来,用户重做
 
 
       canvasForm:{
         type:'add',
-        width: '1024',
+        width: FIXED_CANVAS_WIDTH,
         height: '1024',
         color:"#fff",
+        bg_color:'#fff',
         visible:false,
       }
 
@@ -67,7 +75,13 @@ export default {
     }
   },
   watch: {
-
+    index(newValue, oldValue) {
+      if (this.isEmpty) return
+      this.saveCanvasSnapshot(oldValue)
+      this.$nextTick(() => {
+        this.init()
+      })
+    }
   },
   mounted() {
     this.init()
@@ -88,18 +102,28 @@ export default {
       }
 
 
-
+/*
       //画布下不存在模板OSS地址
-      if(this.this_canvas.tpl_url){
+      if(!this.this_canvas){
+        this.$emit('update:index', 0)
         return;
       }
+
+
+      if(this.this_canvas.tpl_url){
+        return;
+      }*/
       this.$emit('canvasStyle:update',{
         width: this.this_canvas.width,
         height: this.this_canvas.height
       })
       await this.viewInit()
       await  this.$nextTick()
-      this.fcanvas = new fabric.Canvas('marketing-canvas', {
+      const canvasId = `marketing-canvas-${this.index}`
+      const canvasEl = document.getElementById(canvasId)
+      if (!canvasEl) return
+      this.destroyCanvasInstance()
+      this.fcanvas = new fabric.Canvas(canvasId, {
         backgroundColor: this.this_canvas.bg_color,
         containerClass:"fcanvas",
         // 元素对象被选中时保持在当前z轴,不会跳到最顶层
@@ -107,28 +131,43 @@ export default {
         width: this.this_canvas.width,
         height: this.this_canvas.height
       })
-      //保留最初的状态
-      this.updateCanvasState()
-      // this.minimapInit()
-      this.actionInit()
-      this.layerInit();
-      await  this.$nextTick()
-      this.$emit('init')
+      this.fcanvasId = canvasId
+      const hydrateCanvas = () => {
+        this.updateCanvasState()
+        // this.minimapInit()
+        this.actionInit()
+        this.layerInit();
+        this.$nextTick(() => {
+          this.$emit('init')
+        })
+      }
+      if(this.this_canvas.canvas_json){
+        const jsonData = typeof this.this_canvas.canvas_json === 'string'
+          ? this.this_canvas.canvas_json
+          : JSON.stringify(this.this_canvas.canvas_json)
+        this.fcanvas.loadFromJSON(jsonData, () => {
+          this.fcanvas.renderAll()
+          hydrateCanvas()
+        })
+      }else{
+        hydrateCanvas()
+      }
 
     },
     addCanvas(){
 
       this.canvasForm.type = 'add'
       this.canvasForm.name = '画布_'+new Date().getTime().toString().substr(8)+Math.round(100)
-      this.canvasForm.width = 1024
+      this.canvasForm.width = FIXED_CANVAS_WIDTH
       this.canvasForm.height = 1024
       this.canvasForm.bg_color = '#fff'
       this.canvasForm.visible = true;
     },
     handleAdjustCanvas() {
+      if(!this.this_canvas) return;
       this.canvasForm.type = 'edit'
       this.canvasForm.name = this.this_canvas.name
-      this.canvasForm.width = this.this_canvas.width
+      this.canvasForm.width = FIXED_CANVAS_WIDTH
       this.canvasForm.height = this.this_canvas.height
       this.canvasForm.bg_color = this.this_canvas.bg_color
       this.canvasForm.visible = true;
@@ -137,36 +176,50 @@ export default {
       // 假设 this.canvasForm 包含最新的 width, height 和 color
 
       if(this.canvasForm.type === 'add'){
+        this.saveCanvasSnapshot()
         this.data.push({
                tpl_url:"",
                image_path:"",
                name:this.canvasForm.name,
-               width:this.canvasForm.width,
+               width:FIXED_CANVAS_WIDTH,
                height:this.canvasForm.height,
                bg_color:this.canvasForm.bg_color,
+               canvas_json:'',
         })
-        this.$emit('index:update',this.data.length - 1)
+        const nextIndex = this.data.length - 1
+        this.$emit('update:index',nextIndex)
+        if(nextIndex === this.index){
+          this.$nextTick(() => {
+            this.init()
+          })
+        }
 /*        this.index = this.data.length - 1*/
         this.canvasForm.visible = false;
-        this.init();
       }else{
 
+        if(!this.this_canvas){
+          this.canvasForm.visible = false;
+          return;
+        }
+
 
-        const newWidth = this.canvasForm.width;
+        const newWidth = FIXED_CANVAS_WIDTH;
         const newHeight = this.canvasForm.height;
-        const newColor = this.canvasForm.color;
+        const newColor = this.canvasForm.bg_color;
 
         // 更新 fcanvas 的宽度和高度
-        if(newWidth !== this.this_canvas.width)this.fcanvas.setWidth(newWidth);
-        if(newHeight !== this.this_canvas.height)this.fcanvas.setHeight(newHeight);
-        if(newColor !== this.this_canvas.bg_color)this.fcanvas.setBackgroundColor(newColor);
+        if(this.fcanvas){
+          if(newWidth !== this.this_canvas.width)this.fcanvas.setWidth(newWidth);
+          if(newHeight !== this.this_canvas.height)this.fcanvas.setHeight(newHeight);
+          if(newColor !== this.this_canvas.bg_color)this.fcanvas.setBackgroundColor(newColor);
+          // 重新渲染以应用更改
+          this.fcanvas.renderAll();
+        }
 
         this.data[this.index].name = this.canvasForm.name
-        this.data[this.index].width = this.canvasForm.width
+        this.data[this.index].width = FIXED_CANVAS_WIDTH
         this.data[this.index].height = this.canvasForm.height
         this.data[this.index].bg_color = this.canvasForm.bg_color
-        // 重新渲染以应用更改
-        this.fcanvas.renderAll();
         this.canvasForm.visible = false;
       }
 
@@ -175,6 +228,49 @@ export default {
     resizeCanvas(width, height) {
       // TODO: 实现具体的画布调整逻辑
       console.log('调整画布尺寸为:', width, 'x', height);
+    },
+    handleSelectCanvas(index){
+      if(index === this.index) return
+      this.saveCanvasSnapshot()
+      this.$emit('update:index', index)
+    },
+    canvasBodyStyle(item){
+      const height = Number(item?.height)
+      if(!height || Number.isNaN(height)){
+        return {
+          minHeight: '200px'
+        }
+      }
+      return {
+        height: `${height}px`
+      }
+    },
+    saveCanvasSnapshot(targetIndex){
+      const snapshotIndex = typeof targetIndex === 'number' ? targetIndex : this.index
+      if(!this.fcanvas || snapshotIndex === undefined || snapshotIndex === null) return
+      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)
+      }
+    },
+    destroyCanvasInstance(){
+      if(!this.fcanvas) return
+      try{
+        this.fcanvas.dispose()
+      }catch(err){
+        console.warn('[marketingEdit] dispose canvas failed', err)
+      }finally{
+        this.fcanvas = null
+        this.fcanvasId = ''
+      }
     }
   }
 }
@@ -194,10 +290,11 @@ export default {
   .picture-editor-wrap_canvas {
     position: relative;
     margin-top: 85px;
+    box-sizing: border-box;
     .picture-editor-canvas {
-      height: calc(100vh - 85px);
+      min-height: calc(100vh - 85px);
       display: flex;
-      align-items: center;
+      align-items: flex-start;
       justify-content: center;
     }
 
@@ -229,8 +326,6 @@ export default {
     background:#fff;
     box-shadow: 0px 2px 4px 0px rgba(170,177,255,0.54);
 
-    .el-button {
-    }
     .icon {
       margin-right: 5px;
     }
@@ -258,5 +353,81 @@ export default {
 }
 
 
+.canvas-stack {
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+  padding-bottom: 40px;
+}
+
+.canvas-stack_item {
+  width: 100%;
+  border-radius: 8px;
+  box-shadow: 0 6px 16px rgba(0,0,0,0.06);
+  padding: 16px;
+  box-sizing: border-box;
+}
+
+.canvas-stack_header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
+}
+
+.canvas-stack_title {
+  display: flex;
+  gap: 12px;
+  font-size: 16px;
+  font-weight: 600;
+}
+
+.canvas-stack_title .size {
+  font-size: 13px;
+  color: #666;
+}
+
+.canvas-stack_body {
+  background: #fff;
+  border-radius: 6px;
+  padding: 12px;
+  box-sizing: border-box;
+}
+
+.canvas-stack_body canvas {
+  display: block;
+  margin: 0 auto;
+}
+
+.canvas-stack_placeholder {
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+  color: #888;
+  font-size: 14px;
+  text-align: center;
+}
+
+.canvas-stack_placeholder img {
+  max-width: 100%;
+  border-radius: 4px;
+  margin-bottom: 8px;
+}
+
+.canvas-stack_empty {
+  width: 100%;
+  text-align: center;
+  padding: 80px 0;
+  color: #666;
+}
+
+.fixed-width-tip {
+  line-height: 32px;
+  color: #666;
+}
+
 
 </style>

+ 8 - 2
frontend/src/views/components/marketingEdit/tpl/header.js

@@ -17,7 +17,13 @@ export  default function tpl(){
                       </el-button>
                       <template #dropdown>
                         <el-dropdown-menu>
-                          <el-dropdown-item v-for="item,index in data"><span :class="select == index ? 'c-primary' : ''">{{item.name}}</span></el-dropdown-item>
+                          <el-dropdown-item
+                            v-for="(item,idx) in data"
+                            :key="(item.id || item.tpl_url || idx) + '-' + idx"
+                            @click.native="handleSelectCanvas(idx)"
+                          >
+                            <span :class="idx === index ? 'c-primary' : ''">{{item.name || ('画布 ' + (idx + 1))}}</span>
+                          </el-dropdown-item>
                         </el-dropdown-menu>
                       </template>
                   </el-dropdown>
@@ -33,7 +39,7 @@ export  default function tpl(){
                   <el-input v-model.number="canvasForm.name" placeholder="请输入宽度"></el-input>
                 </el-form-item>
                 <el-form-item label="宽度">
-                  <el-input v-model.number="canvasForm.width" placeholder="请输入宽度"></el-input>
+                  <div class="fixed-width-tip">800(固定)</div>
                 </el-form-item>
                 <el-form-item label="高度">
                   <el-input v-model.number="canvasForm.height" placeholder="请输入高度"></el-input>

+ 1 - 1
frontend/src/views/components/marketingEdit/tpl/index.js

@@ -8,7 +8,7 @@ import header from "./header";
 export  default function tpl(){
   return `
     <transition name="fade">
-      <div ref="wrap" class="picture-editor-wrap flex">
+      <div ref="wrap" class="picture-editor-wrap flex top">
       
           <!--头部-->
           ${header()}

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

@@ -5,9 +5,45 @@ import shanyibu from '@/views/components/PictureEditor/images/shanyibu.png'
 export default function() {
   return `
     <div class="picture-editor-canvas">
-       <div  v-if="!isEmpty" style="width: calc(100% + 8px); height: 100%; overflow: auto">
-            <canvas id="marketing-canvas" />
+      <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"
+          :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>
+            <el-button
+              size="small"
+              type="text"
+              @click="handleSelectCanvas(idx)"
+            >
+              {{ 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="">
+              <img v-else-if="item.tpl_url" :src="item.tpl_url" alt="">
+              <span v-else>点击“切换到此画布”开始编辑</span>
+            </div>
+          </div>
+        </div>
       </div>
+      <div v-else class="canvas-stack_empty">暂无画布,请先新增。</div>
     </div>
   `
 }