浏览代码

feat(editor): 增强文本图层编辑功能与响应式支持

- 为文本与图片元素添加 name 属性以支持标识
- 在下拉菜单中显示当前画布名称
- 重构文本配置模块,使用 layerState 管理样式状态
- 新增透明度控制滑块并优化其数据绑定逻辑
- 引入 reactive 以增强对象响应性
- 添加 setLayerAttr 和 setOpacity 方法统一处理属性更新
- 更新字体、颜色、行高、字距等组件绑定至 layerState
- 修复 editLayer 属性更新后 UI 不同步的问题
- 优化文本填充默认值逻辑,确保有默认颜色 #000
- 清理冗余代码注释,提升代码可读性
panqiuyao 6 天之前
父节点
当前提交
05a789df04

+ 1 - 1
frontend/src/views/components/PictureEditor/mixin/actions/index.js

@@ -268,8 +268,8 @@ export default  {
       let id = this.getNextLayersId()
       this.fcanvas.discardActiveObject()
       this.fcanvas.add(markRaw(textbox.set({
-        ...options,
         name:"text",
+        ...options,
         sort,
         id,
         splitByGrapheme:true,

+ 77 - 10
frontend/src/views/components/PictureEditor/mixin/edit/index.js

@@ -3,7 +3,7 @@ import Teleport from '@/components/Teleport'
 
 import * as TextboxConfig from './module/TextboxConfig'
 import fabric from "../../js/fabric-adapter";
-import {markRaw} from "vue";
+import {markRaw,reactive} from "vue";
 // 将默认的 clipSettings 提取为常量
 const DEFAULT_CLIP_SETTINGS = {
   shape: '',
@@ -30,6 +30,14 @@ export default  {
       TextboxConfig:TextboxConfig,
       options:TextboxConfig.fontFamily,
       fontFamilyStyle:"",
+      opacityValue: 1,
+      layerState: {
+        fontSize: 16,
+        lineHeight: 1.2,
+        charSpacing: 0,
+        fill: '#000000',
+        textAlign: 'left',
+      },
       shadowText:{
         x:0,
         y:0,
@@ -51,6 +59,7 @@ export default  {
       if(this.selected && this.selected.length === 1){
         object = this.selected[0]
       }
+      // return reactive(object)
       return object
     },
     fontFamily(){
@@ -68,6 +77,17 @@ export default  {
   watch: {
     editLayer(){
       this.fontFamilyStyle = this.editLayer.fontFamily
+      this.opacityValue = (this.editLayer && typeof this.editLayer.opacity === 'number')
+        ? this.editLayer.opacity
+        : 1
+      this.layerState = {
+        ...this.layerState,
+        fontSize: this.editLayer?.fontSize ?? 16,
+        lineHeight: this.editLayer?.lineHeight ?? 1,
+        charSpacing: this.editLayer?.charSpacing ?? 0,
+        fill: this.editLayer?.fill ?? '#000000',
+        textAlign: this.editLayer?.textAlign ?? 'left',
+      }
       switch (this.editLayer.type){
         case "textbox":
           if(this.editLayer.shadow){
@@ -118,6 +138,19 @@ export default  {
           break;
       }
     },
+    selected(){
+      this.opacityValue = (this.editLayer && typeof this.editLayer.opacity === 'number')
+        ? this.editLayer.opacity
+        : 1
+      this.layerState = {
+        ...this.layerState,
+        fontSize: this.editLayer?.fontSize ?? 16,
+        lineHeight: this.editLayer?.lineHeight ?? 1,
+        charSpacing: this.editLayer?.charSpacing ?? 0,
+        fill: this.editLayer?.fill ?? '#000000',
+        textAlign: this.editLayer?.textAlign ?? 'left',
+      }
+    }
   },
   created() {
   },
@@ -177,25 +210,59 @@ export default  {
       if(!this.judge()) return;
       let {label,value,action } = data
       action = action || 'set'
+      // 构造待设置的参数;仅当 label 为 fill 且当前无填充时兜底 #000
       let params = value
-      if(label){
-        params = {}
-        params[label] = value
-      }
-      if(!this.editLayer.fill || label !== 'fill'){
-        params = {
-          fill:'#000'
+      if (label) {
+        params = { [label]: value }
+        if (label === 'fill' && !this.editLayer.fill) {
+          params.fill = value || '#000'
         }
       }
+
       this.editLayer[action](params)
-      if(Array.isArray(this.editLayer._objects)){
-        this.editLayer._objects?.map(item=>{
+      if (Array.isArray(this.editLayer._objects)){
+        this.editLayer._objects?.forEach(item=>{
           item[action](params)
         })
       }
+
+      // 同步常用文本状态到 layerState,避免 UI 视图不同步
+      if (['fontSize','lineHeight','charSpacing','fill','textAlign'].includes(label)) {
+        this.layerState = {
+          ...this.layerState,
+          [label]: value,
+        }
+      }
+
       this.updateCanvasState()
       this.fcanvas.requestRenderAll()
     },
+    // 通用设置图层属性,解决 markRaw 不响应问题
+    setLayerAttr(name, val){
+      if(!this.editLayer) return
+      this.layerState = {
+        ...this.layerState,
+        [name]: val,
+      }
+      this.editLayer.set(name, val)
+      if(Array.isArray(this.editLayer._objects)){
+        this.editLayer._objects.forEach(o => o.set(name, val))
+      }
+      this.updateCanvasState()
+      this.fcanvas && this.fcanvas.requestRenderAll()
+    },
+    // 设置透明度(解决 Vue3 markRaw 不响应)
+    setOpacity(val){
+      if(!this.editLayer) return
+      const num = Number(val)
+      this.opacityValue = num
+      this.editLayer.set('opacity', num)
+      if(Array.isArray(this.editLayer._objects)){
+        this.editLayer._objects.forEach(o => o.set('opacity', num))
+      }
+      this.updateCanvasState()
+      this.fcanvas && this.fcanvas.requestRenderAll()
+    },
     async setFontFamily(val,item=null){
       console.log(val)
       console.log(item)

+ 14 - 8
frontend/src/views/components/PictureEditor/mixin/edit/module/textbox.js

@@ -34,26 +34,32 @@ const textbox = () => {
 
           <div class="flex left mar-top-10">
             <div class="edit-font-size flex left position-r" style="margin-right: 10px">
-                <el-select v-model="editLayer.fontSize" :clearable="false"
+                <el-select
+                     :model-value="layerState.fontSize"
+                     @update:model-value="val=>setLayerAttr('fontSize', val)"
+                     :clearable="false"
                      style="width: 80px"
-                 @change="(val)=>editObj({label:'fontSize',value:val})">
+                 >
                      <el-option v-for="item,index in TextboxConfig.fontSize" :key="index" :value="item">{{item}}</el-option>
                  </el-select>
                  <div class="bq fs-14 c-999">px</div>
              </div>
-              <el-color-picker class="mar-left-10" :value="editLayer.fill"
-               @change="(val)=>editObj({label:'fill',value:val})"></el-color-picker>
+             <el-color-picker
+               class="mar-left-10"
+               :model-value="layerState.fill"
+               @change="(val)=>setLayerAttr('fill', val)"
+             ></el-color-picker>
           </div>
           <div class="flex left">
                <div class="label title_two">行高</div>
                  <el-input-number
-                 v-model="editLayer.lineHeight"
+                 v-model="layerState.lineHeight"
                   controls-position="right"
                   :precision="2"
                   :step="0.01"
                   class="mar-left-10"
                   style="width: 100px"
-                   @change="(val)=>editObj({label:'lineHeight',value:val} )"
+                   @change="(val)=>setLayerAttr('lineHeight',val)"
                   :min="0.01"
                    :max="100"/>
           </div>
@@ -62,13 +68,13 @@ const textbox = () => {
                <div class="label title_two">字距</div>
 
                  <el-input-number
-                 v-model="editLayer.charSpacing"
+                 v-model="layerState.charSpacing"
                   style="width: 100px"
                   class="mar-left-10"
                   controls-position="right"
                   :precision="1"
                   :step="1"
-                   @change="(val)=>editObj({label:'charSpacing',value:val})"/> 
+                   @change="(val)=>setLayerAttr('charSpacing',val)"/> 
           </div>
 
           <div class="flex line-20 mar-top-10">

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

@@ -22,18 +22,17 @@ export default  {
     return `
           <div class="title_two">透明度</div>
           <div class="flex left">
-         <!-- <el-input v-model="editLayer.opacity"/>-->
-                <el-slider
-                 size="small"
-                  v-model="editLayer.opacity"
-                  style="width:80%"
-                  :step="0.01"
-                  :min="0.01"
-                  :max="1"
-                  :show-tooltip="false"
-                   @input.native="(val)=>editObj({label:'opacity',value:val} )"
-
-                ></el-slider>
+            <span :key="opacityValue">{{ opacityValue }}</span>
+            <el-slider
+              size="small"
+              v-model="opacityValue"
+              style="width:80%"
+              :step="0.01"
+              :min="0"
+              :max="1"
+              :show-tooltip="false"
+              @input="setOpacity"
+            ></el-slider>
 
           </div>
       `

+ 2 - 0
frontend/src/views/components/marketingEdit/tpl/add.js

@@ -46,6 +46,7 @@ let add = () => {
                                   top:50,
                                   'data-key':item.key,
                                   'data-type':'text',
+                                   name:item.key,
                                   'data-value':item.value,
                                   text:item.value
                               })"
@@ -94,6 +95,7 @@ let add = () => {
                                   top:50,
                                     maxWidth:fcanvas.width*0.8,
                                     maxHeight:fcanvas.height*0.8,
+                                    name:item.key,
                                   'data-type':'img',
                                   'data-key':item.key,
                                   'data-value':item.value,

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

@@ -12,6 +12,7 @@ export  default function tpl(){
            
                 
                     <el-dropdown v-if="!isEmpty">
+                       <span v-if="this_canvas.name">当前画布:</span>
                       <el-button>
                         {{this_canvas.name}}<el-icon class="el-icon--right"><arrow-down /></el-icon>
                       </el-button>