Sfoglia il codice sorgente

feat(picture-editor): 添加图片裁剪背景样式设置功能

- 添加背景色和透明度设置选项,支持颜色选择器和透明度滑块
- 实现裁剪背景对象的创建、恢复和清理功能
- 保存裁剪设置到图片对象,支持编辑状态恢复
- 优化裁剪设置UI,添加描边颜色和宽度配置
- 调整界面布局,添加数值输入框替代纯滑块控制
- 修复圆角矩形初始化时宽高不一致问题
- 增加画布序列化时裁剪设置的保存字段
panqiuyao 6 giorni fa
parent
commit
8ac16b56d2

+ 91 - 3
frontend/src/views/components/PictureEditor/mixin/edit/index.js

@@ -19,6 +19,8 @@ const DEFAULT_CLIP_SETTINGS = {
   svgUrl: '',
   svgWidth: 100,
   svgHeight: 100,
+  fillColor: '#FFFFFF', // 背景色
+  fillOpacity: 0.5, // 背景透明度
 };
 
 export default  {
@@ -152,8 +154,13 @@ export default  {
 
           // 优先使用默认值
           let imageSettings = { ...DEFAULT_CLIP_SETTINGS };
-          // 若已有 clipPath,合并其中的属性
-          if (this.editLayer.clipPath) {
+
+          // 如果图片对象有保存的裁剪设置,优先使用
+          if (this.editLayer.clipSettings) {
+            imageSettings = { ...imageSettings, ...this.editLayer.clipSettings };
+          }
+          // 否则从 clipPath 恢复基本几何属性
+          else if (this.editLayer.clipPath) {
             imageSettings = {
               ...imageSettings,
               width: this.editLayer.clipPath.width || imageSettings.width,
@@ -172,6 +179,11 @@ export default  {
           // 更新 clipSettings,用于 UI 显示
           this.clipSettings = imageSettings;
 
+          // 如果有裁剪设置,恢复背景对象
+          if (imageSettings.shape && this.editLayer.clipSettings) {
+            this.createClipBackground();
+          }
+
           // 同步描边图形(如果存在)
           if (this.editLayer.strokeObj) {
             this.fcanvas.add(markRaw(this.editLayer.strokeObj));
@@ -461,6 +473,12 @@ export default  {
         // 应用剪裁区域
         this.editLayer.set({ clipPath: clipObj });
 
+        // 保存裁剪设置到图片对象
+        this.editLayer.clipSettings = { ...this.clipSettings };
+
+        // 创建背景对象(方案二:独立背景对象)
+        this.createClipBackground();
+
         // 更新画布
         this.updateClipStroke()
         this.updateCanvasState();
@@ -471,6 +489,64 @@ export default  {
         this.$message.error('剪裁设置错误: ' + error.message);
       }
     },
+
+    // 创建裁剪背景对象(方案二:独立背景对象)
+    createClipBackground() {
+      if (!this.judge() || !this.clipSettings.shape) return;
+
+      // 移除现有的背景对象
+      if (this.editLayer.clipBackgroundObj) {
+        this.fcanvas.remove(this.editLayer.clipBackgroundObj);
+        this.editLayer.clipBackgroundObj = null;
+      }
+
+      const { shape, width, height, radius, offsetX, offsetY, rectRadius, fillColor, fillOpacity, strokeColor, strokeWidth } = this.clipSettings;
+
+      let bgObj;
+
+      if (shape === 'rect') {
+        bgObj = new fabric.Rect({
+          width: width,
+          height: height,
+          left: offsetX,
+          top: offsetY,
+          rx: rectRadius,
+          ry: rectRadius,
+          fill: fillColor,
+          stroke: strokeColor,
+          strokeWidth: strokeWidth,
+          opacity: fillOpacity,
+          selectable: false,
+          evented: false,
+          absolutePositioned: true
+        });
+      } else if (shape === 'circle') {
+        bgObj = new fabric.Circle({
+          radius: radius,
+          left: offsetX,
+          top: offsetY,
+          fill: fillColor,
+          stroke: strokeColor,
+          strokeWidth: strokeWidth,
+          opacity: fillOpacity,
+          selectable: false,
+          evented: false,
+          absolutePositioned: true
+        });
+      }
+
+      if (bgObj) {
+        // 绑定到当前对象
+        this.editLayer.clipBackgroundObj = bgObj;
+
+        // 添加到画布
+        this.fcanvas.add(markRaw(bgObj));
+
+        // 确保层级正确:背景在最下面,图片在上面
+        this.fcanvas.sendToBack(bgObj);
+        this.fcanvas.bringToFront(this.editLayer);
+      }
+    },
     // 切换剪裁形状,自动填充默认值(位置跟随图层,尺寸为画布一半)
     onClipShapeChange(shape){
       if(!this.editLayer || this.editLayer.type !== 'image'){
@@ -488,7 +564,7 @@ export default  {
       const canvasW = this.fcanvas?.width || obj.canvas?.width || 800
       const canvasH = this.fcanvas?.height || obj.canvas?.height || 800
       const initWidth = Math.max(10, Math.round(canvasW / 2))
-      const initHeight = Math.max(10, Math.round(canvasH / 2))
+      const initHeight = initWidth // 高度和宽度保持一致
       const initRadius = Math.max(5, Math.round(Math.min(canvasW, canvasH) / 4))
       const initSettings = {
         shape,
@@ -601,6 +677,18 @@ export default  {
         this.editLayer.strokeObj = null;
       }
 
+      // 移除背景对象(如果存在)
+      if (this.editLayer.clipBackgroundObj) {
+        this.fcanvas.remove(this.editLayer.clipBackgroundObj);
+        this.editLayer.clipBackgroundObj = null;
+      }
+
+      // 清除保存的裁剪设置
+      delete this.editLayer.clipSettings;
+
+      // 重置UI设置
+      this.clipSettings = { ...DEFAULT_CLIP_SETTINGS };
+
       // 更新画布状态
       this.updateCanvasState();
       this.fcanvas.requestRenderAll();

+ 132 - 12
frontend/src/views/components/PictureEditor/mixin/edit/module/tools.js

@@ -25,13 +25,14 @@ export default  {
             <el-slider
               size="small"
               v-model="opacityValue"
-              style="width:80%"
+              style="width:90px"
               :step="0.01"
               :min="0"
               :max="1"
               :show-tooltip="false"
               @input="setOpacity"
             ></el-slider>
+        <span class="mar-left-20">{{ Math.round(opacityValue * 100) }}%</span>
 
           </div>
       `
@@ -83,7 +84,67 @@ cutout(){
   <!--      <el-option label="SVG" value="svg"></el-option>-->
       </el-select>
     </div>
-    
+
+    <!-- 背景色和描边设置 -->
+    <template v-if="clipSettings.shape">
+      <div class="title_two mar-top-15">样式</div>
+
+      <!-- 背景色 -->
+      <div class="flex left mar-top-10">
+        <div class="label fs-14 c-333 te-l" style="width: 50px;">背景</div>
+        <el-color-picker
+          v-model="clipSettings.fillColor"
+          @change="applyClipPath"
+          size="small"
+          :predefine="['#FFFFFF', '#000000', '#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF', 'rgba(255,255,255,0.5)', 'rgba(0,0,0,0.5)']"
+        />
+        <span class="mar-left-10">{{ clipSettings.fillColor || '无' }}</span>
+      </div>
+
+      <!-- 背景透明度 -->
+      <div class="flex left mar-top-10">
+        <div class="label fs-14 c-333 te-l" style="width: 50px;">透明度</div>
+        <el-slider
+          v-model="clipSettings.fillOpacity"
+          @input="applyClipPath"
+          :min="0"
+          :max="1"
+          :step="0.1"
+          style="width:120px;"
+          :show-tooltip="false"
+        />
+        <span class="mar-left-20">{{ Math.round(clipSettings.fillOpacity * 100) }}%</span>
+      </div>
+
+      <!-- 描边 -->
+      <div class="flex left mar-top-10">
+        <div class="label fs-14 c-333 te-l" style="width: 50px;">描边</div>
+        <el-color-picker
+          v-model="clipSettings.strokeColor"
+          @change="applyClipPath"
+          size="small"
+          :predefine="['#FFFFFF', '#000000', '#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF']"
+        />
+        <span class="mar-left-10">{{ clipSettings.strokeColor || '无' }}</span>
+      </div>
+
+      <!-- 描边宽度 -->
+      <div class="flex left mar-top-10">
+        <div class="label fs-14 c-333 te-l" style="width: 50px;">宽度</div>
+        <el-input-number
+          v-model="clipSettings.strokeWidth"
+          @change="applyClipPath"
+          :min="0"
+          :max="20"
+          :step="1"
+          size="small"
+          style="width:80px;"
+          controls-position="right"
+        />
+        <span class="mar-left-10 fs-12 c-999">px</span>
+      </div>
+    </template>
+
     <template v-if="clipSettings.shape">
       <div v-if="clipSettings.shape === 'svg'" class="flex left" style="margin-left:10px">
         <!-- 固定地址输入框 -->
@@ -108,12 +169,23 @@ cutout(){
                   :step="1"
                   :min="1"
                   :max="canvas.width"
-                  style="width:120px;"
+                  style="width:80px;"
                   :show-tooltip="false"
                    @input="applyClipPath"
 
                 ></el-slider>
-                <span class="mar-left-10">{{clipSettings.offsetX}}</span>
+                <el-input-number
+                   v-model="clipSettings.offsetX"
+                  @change="applyClipPath"
+                  :min="1"
+                  :max="5000"
+                  :step="1"
+                  size="small"
+                  :controls="false"
+                  class="mar-left-10"
+                  style="width:70px;"
+                />
+        <span class="mar-left-10 fs-12 c-999">px</span>
                 
       </div>
       
@@ -125,12 +197,24 @@ cutout(){
                   :step="1"
                   :min="1"
                   :max="canvas.height"
-                  style="width:120px;"
+                  style="width:80px;"
                   :show-tooltip="false"
                    @input="applyClipPath"
 
                 ></el-slider>
-                <span class="mar-left-10">{{clipSettings.offsetY}}</span>
+                <el-input-number
+                   v-model="clipSettings.offsetY"
+                  @change="applyClipPath"
+                  :min="1"
+                  :max="5000"
+                  :step="1"
+                  size="small"
+                  :controls="false"
+                  class="mar-left-10"
+                  style="width:70px;"
+                />
+        <span class="mar-left-10 fs-12 c-999">px</span>
+                
       </div>
       <div v-if="clipSettings.shape === 'svg'" class="flex left" style="margin-left:10px">
       <el-input-number v-model="clipSettings.svgWidth" @input="applyClipPath" :step="1" :min="1" size="small" style="width:80px"/>
@@ -149,12 +233,24 @@ cutout(){
                   :step="1"
                   :min="1"
                   :max="canvas.width"
-                  style="width:120px;"
+                  style="width:80px;"
                   :show-tooltip="false"
                    @input="applyClipPath"
 
                 ></el-slider>
-                <span class="mar-left-10">{{clipSettings.width}}</span>
+                    
+                <el-input-number
+                   v-model="clipSettings.width"
+                  @change="applyClipPath"
+                  :min="1"
+                  :max="5000"
+                  :step="1"
+                  size="small"
+                  :controls="false"
+                  class="mar-left-10"
+                  style="width:70px;"
+                />
+        <span class="mar-left-10 fs-12 c-999">px</span>
         
       </div>
       <div  class="flex left mar-top-10 ">
@@ -166,12 +262,24 @@ cutout(){
                   :step="1"
                   :min="1"
                   :max="canvas.height"
-                  style="width:120px;"
+                  style="width:80px;"
                   :show-tooltip="false"
                    @input="applyClipPath"
 
                 ></el-slider>
-                <span class="mar-left-10">{{clipSettings.height}}</span>
+                
+            <el-input-number
+               v-model="clipSettings.height"
+              @change="applyClipPath"
+              :min="1"
+              :max="5000"
+              :step="1"
+              size="small"
+              :controls="false"
+              class="mar-left-10"
+              style="width:70px;"
+            />
+        <span class="mar-left-10 fs-12 c-999">px</span>
       </div>
       <div  class="flex left mar-top-10 ">
         <div class="label fs-14 c-333 te-l"  style="width: 35px;">圆角</div>
@@ -189,12 +297,24 @@ cutout(){
                   :step="1"
                   :min="1"
         :max="canvas.width / 2"
-                  style="width:120px;"
+                  style="width:80px;"
                   :show-tooltip="false"
                    @input="applyClipPath"
 
                 ></el-slider>
-                <span class="mar-left-10">{{clipSettings.radius}}</span>
+                
+              <el-input-number
+                 v-model="clipSettings.radius"
+                @change="applyClipPath"
+                :min="1"
+                :max="5000"
+                :step="1"
+                size="small"
+                :controls="false"
+                class="mar-left-10"
+                style="width:70px;"
+              />
+        <span class="mar-left-10 fs-12 c-999">px</span>
       </div>
 
     <!-- 描边参数 -->

+ 52 - 2
frontend/src/views/components/marketingEdit/index.vue

@@ -146,7 +146,8 @@ export default {
         this.init()
         this.scrollToCanvas(newValue)
       })
-    }
+    },
+
   },
   mounted() {
     // 延迟初始化,等待父组件设置数据
@@ -212,6 +213,8 @@ export default {
         this.getLayers()
         // 恢复选中状态
         this.undoAfterSelectLayers()
+        // 恢复裁剪背景对象
+        this.restoreClipBackgroundObjects()
         this.fcanvas.renderAll()
       } catch (e) {
         console.warn('[marketingEdit] setTpl failed', e)
@@ -260,10 +263,18 @@ export default {
         height: this.this_canvas.height
       }))
       this.fcanvasId = canvasId
+
+      // 清理可能存在的裁剪背景对象
+      this.clearAllClipBackgroundObjects()
+
       const hydrateCanvas = () => {
         // this.minimapInit()
         this.actionInit()
         this.layerInit();
+
+        // 恢复裁剪背景对象
+        this.restoreClipBackgroundObjects();
+
         this.loading = false // 加载完成
 
         // 在所有初始化完成后,保存当前画布状态作为初始状态
@@ -532,6 +543,45 @@ export default {
         if (obj.textAlign === undefined) obj.textAlign = 'left'
       })
     },
+
+    // 清理所有裁剪背景对象
+    clearAllClipBackgroundObjects() {
+      if (!this.fcanvas) return;
+
+      // 遍历所有画布对象,移除背景对象
+      this.fcanvas.getObjects().forEach(obj => {
+        if (obj.type === 'image' && obj.clipBackgroundObj) {
+          this.fcanvas.remove(obj.clipBackgroundObj);
+          obj.clipBackgroundObj = null;
+        }
+      });
+    },
+
+    // 恢复裁剪背景对象
+    restoreClipBackgroundObjects() {
+      if (!this.fcanvas) return;
+
+      // 遍历所有画布对象
+      this.fcanvas.getObjects().forEach(obj => {
+        // 只处理图片对象
+        if (obj.type === 'image' && obj.clipSettings && obj.clipSettings.shape) {
+          // 临时保存当前编辑层
+          const currentEditLayer = this.editLayer;
+
+          // 设置当前对象为编辑层
+          this.editLayer = obj;
+
+          // 恢复clipSettings到组件状态
+          this.clipSettings = { ...obj.clipSettings };
+
+          // 重新创建背景对象
+          this.createClipBackground();
+
+          // 恢复原来的编辑层
+          this.editLayer = currentEditLayer;
+        }
+      });
+    },
     addCanvas(){
 
       this.canvasForm.type = 'add'
@@ -879,7 +929,7 @@ export default {
       }
 
       if(!this.fcanvas) return
-      const json = JSON.stringify(this.fcanvas.toJSON(['name','sort','mtr','id','selectable','erasable','data-key','data-type','data-value']))
+      const json = JSON.stringify(this.fcanvas.toJSON(['name','sort','mtr','id','selectable','erasable','data-key','data-type','data-value','clipSettings']))
       let preview = canvasData.preview || ''
       try{
         preview = this.fcanvas.toDataURL({