import Teleport from '@/components/Teleport' import * as TextboxConfig from './module/TextboxConfig' import fabric from "../../js/fabric-adapter"; import {markRaw,reactive} from "vue"; // 将默认的 clipSettings 提取为常量 const DEFAULT_CLIP_SETTINGS = { shape: '', width: 100, height: 100, radius: 50, offsetX: 50, offsetY: 50, showClipStroke: true, strokeColor: '#000000', strokeWidth: 1, rectRadius: 0, svgUrl: '', svgWidth: 100, svgHeight: 100, fillColor: '#FFFFFF', // 背景色 fillOpacity: 0.5, // 背景透明度 }; export default { components: { Teleport, }, data() { return { TextboxConfig:TextboxConfig, options:TextboxConfig.fontFamily, fontFamilyStyle:"", opacityValue: 1, clipPreview: { dataUrl: '', maxOffsetX: 0, maxOffsetY: 0, maxWidth: 0, maxHeight: 0, maxRadius: 0, }, layerState: { fontSize: 16, lineHeight: 1.2, charSpacing: 0, fill: '#000000', textAlign: 'left', }, shadowText:{ x:0, y:0, vague:0, color:'#000', }, clipSettings: { ...DEFAULT_CLIP_SETTINGS, }, strokeObj: null, // 用于引用当前描边图形 imageSize: { width: 100, height: 100, keepRatio: true, originalRatio: 1, // 原始宽高比 lastAdjusted: 'width' // 最后调整的维度 ('width' 或 'height') }, // 响应式的文字内容状态(用于解决markRaw后的响应式问题) reactiveTextContent: '' } }, props: { }, computed: { editLayer(){ let object = {} if(this.selected && this.selected.length === 1){ object = this.selected[0] } // return reactive(object) return object }, fontFamily(){ let obj = {} this.TextboxConfig.fontFamily.map(item=>{ obj[item.name] = item }) return obj }, shadow(){ let shadowText = this.shadowText return `${shadowText.x}px ${shadowText.y}px ${shadowText.vague}px ${shadowText.color} ` } }, watch: { editLayer(){ // 自动加载文字对象的字体 if (this.editLayer && (this.editLayer.type === 'textbox' || this.editLayer.type === 'text')) { const fontFamily = this.editLayer.fontFamily if (fontFamily && fontFamily !== 'Arial') { // 如果是自定义字体,自动加载 this.setFontFamily(fontFamily, this.editLayer).catch(err => { console.warn('自动加载字体失败:', err) }) } } this.fontFamilyStyle = this.editLayer.fontFamily this.opacityValue = (this.editLayer && typeof this.editLayer.opacity === 'number') ? this.editLayer.opacity : 1 this.layerState = { ...this.layerState, text: this.editLayer?.text ?? '', 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', } // 同步响应式文字内容状态 this.reactiveTextContent = this.editLayer?.text ?? ''; switch (this.editLayer.type){ case "textbox": if(this.editLayer.shadow){ this.shadowText = { x:this.editLayer.shadow.offsetX || 0, y:this.editLayer.shadow.offsetY || 0, vague:this.editLayer.shadow.blur || 0, color:this.editLayer.shadow.color || '#000', } }else{ this.shadowText = { x:0, y:0, vague:0, color:'#000' } } break; case "image": // 计算原始宽高比(基于图片的原始尺寸) const originalWidth = this.editLayer.width || 100; const originalHeight = this.editLayer.height || 100; const originalRatio = originalWidth / originalHeight; // 更新图片尺寸显示 this.imageSize = { width: Math.round(this.editLayer.getScaledWidth() || originalWidth), height: Math.round(this.editLayer.getScaledHeight() || originalHeight), keepRatio: true, originalRatio: originalRatio, lastAdjusted: 'width' }; // 优先使用默认值 let imageSettings = { ...DEFAULT_CLIP_SETTINGS }; // 如果图片对象有保存的裁剪设置,优先使用 if (this.editLayer.clipSettings) { imageSettings = { ...imageSettings, ...this.editLayer.clipSettings }; } // 否则从 clipPath 恢复基本几何属性 else if (this.editLayer.clipPath) { imageSettings = { ...imageSettings, width: this.editLayer.clipPath.width || imageSettings.width, height: this.editLayer.clipPath.height || imageSettings.height, offsetX: this.editLayer.clipPath.left || imageSettings.offsetX, offsetY: this.editLayer.clipPath.top || imageSettings.offsetY, shape: this.editLayer.clipPath.type || imageSettings.shape, }; } // 如果没有剪裁路径,则 shape 为 "" if (!this.editLayer.clipPath) { imageSettings.shape = ''; } // 更新 clipSettings,用于 UI 显示 this.clipSettings = imageSettings; // 如果有裁剪设置,恢复背景对象 if (imageSettings.shape && this.editLayer.clipSettings) { this.createClipBackground(); } // 为图片对象添加尺寸变化监听 if (this.editLayer.type === 'image') { // 移除之前可能的监听器 this.editLayer.off('scaling', this.updateImageSizeFromCanvas); // 添加新的监听器 this.editLayer.on('scaling', this.updateImageSizeFromCanvas); } // 同步描边图形(如果存在) if (this.editLayer.strokeObj) { this.fcanvas.add(markRaw(this.editLayer.strokeObj)); } this.updateClipPreview && this.updateClipPreview() break; } }, selected(){ this.opacityValue = (this.editLayer && typeof this.editLayer.opacity === 'number') ? this.editLayer.opacity : 1 this.layerState = { ...this.layerState, text: this.editLayer?.text ?? '', 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', } // 同步响应式文字内容状态 this.reactiveTextContent = this.editLayer?.text ?? ''; // 更新图片尺寸显示 if (this.editLayer && this.editLayer.type === 'image') { const originalWidth = this.editLayer.width || 100; const originalHeight = this.editLayer.height || 100; const originalRatio = originalWidth / originalHeight; this.imageSize = { width: Math.round(this.editLayer.getScaledWidth() || originalWidth), height: Math.round(this.editLayer.getScaledHeight() || originalHeight), keepRatio: this.imageSize.keepRatio, // 保持当前的保持比例设置 originalRatio: originalRatio, lastAdjusted: this.imageSize.lastAdjusted || 'width' }; } } }, created() { }, mounted() { }, methods:{ judge(){ return this.editLayer?.id }, //翻转 Flip(type){ if(!this.judge()) return; switch (type){ case 'X': this.editLayer.set({ flipX:!this.editLayer.flipX, }) break; case 'Y': this.editLayer.set({ flipY:!this.editLayer.flipY, }) break; } this.updateCanvasState() this.fcanvas.requestRenderAll() }, //合并组 setGroup(){ const group = this.fcanvas.getActiveObject().toGroup(); this.fcanvas.requestRenderAll(); this.selected = [group] if(this.getLayers) this.getLayers() this.updateCanvasState() }, //取消组 unGroup(){ if (!this.fcanvas.getActiveObject()) { return; } if (this.fcanvas.getActiveObject().type !== 'group') { return; } this.fcanvas.getActiveObject().toActiveSelection(); this.selected = this.fcanvas.getActiveObject()._objects this.fcanvas.requestRenderAll(); if(this.getLayers) this.getLayers() this.updateCanvasState() }, //编辑文字 editObj(data = { label:'', value:'', action:'set', }){ if(!this.judge()) return; let {label,value,action } = data action = action || 'set' // 构造待设置的参数;仅当 label 为 fill 且当前无填充时兜底 #000 let params = value 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?.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) console.log(this.fontFamily) if(!item && !this.judge()) return; let font = this.fontFamily[val] console.log(font) if(!font) return; await loadFont(font.name,font.src) let obj = item || this.editLayer if(typeof obj.set !== 'function') return; obj.set({'fontFamily': val}); obj.setSelectionStyles({'fontFamily': val}); this.fontFamilyStyle = val this.updateCanvasState() await this.$nextTick() this.fcanvas.requestRenderAll() async function loadFont(_fontName, _fontUrl) { return new Promise((resolve, reject) => { if (checkFont(_fontName)) { console.log('已有字体:', _fontName) return resolve(true) } let prefont = new FontFace( _fontName, 'url(' + _fontUrl + ')' ); prefont.load().then(function (loaded_face) { document.fonts.add(loaded_face); // document.body.style.fontFamily = (_fontFamily ? _fontFamily + ',' : _fontFamily) + _fontName; console.log('字体加载成功', loaded_face, document.fonts) return resolve(true) }).catch(function (error) { console.log('字体加载失败', error) return reject(error) }) }) } function checkFont(name){ let values = document.fonts.values(); let isHave=false; let item = values.next(); while(!item.done&&!isHave) { let fontFace=item.value; if(fontFace.family==name) { isHave=true; } item=values.next(); } return isHave; } }, remoteMethod(query){ if (query !== '') { this.options = this.TextboxConfig.fontFamily.filter(item => { return item.localFile.toLowerCase() .indexOf(query.toLowerCase()) > -1; }); } else { this.options = this.TextboxConfig.fontFamily } }, async applyClipPath() { if (!this.judge()) return; const { shape, width, height, radius, offsetX, offsetY, rectRadius } = this.clipSettings; if (shape === '') { // 选择“无”时,直接清除剪裁 this.clearClipPath(); return; } try { // 创建纯剪裁区域(不带描边) let clipObj; if (shape === 'svg') { // 使用 fabric.js 加载远程 SVG clipObj = await new Promise((resolve, reject) => { fabric.loadSVGFromURL( this.clipSettings.svgUrl, (loadedObjects, options) => { console.log('============='); // 创建组合对象 const svgGroup = fabric.util.groupSVGElements(loadedObjects, options); console.log(svgGroup); // 设置尺寸 svgGroup.scaleToWidth(this.clipSettings.svgWidth); console.log(this.clipSettings.svgWidth); // svgGroup.scaleToHeight(this.clipSettings.svgHeight); svgGroup.set({ absolutePositioned: true, left: offsetX, top: offsetY, }) resolve(svgGroup); } ) }); }else if (shape === 'rect') { if (width <= 0 || height <= 0) throw new Error('尺寸必须大于0'); clipObj = new fabric.Rect({ width: width, height: height, left: offsetX, top: offsetY, rx: rectRadius, ry: rectRadius, absolutePositioned: true }); } else if(shape === 'circle') { if (radius <= 0) throw new Error('半径必须大于0'); clipObj = new fabric.Circle({ radius: radius, left: offsetX, top: offsetY, absolutePositioned: true }); } // 应用剪裁区域 this.editLayer.set({ clipPath: clipObj }); // 保存裁剪设置到图片对象 this.editLayer.clipSettings = { ...this.clipSettings }; // 创建背景对象(方案二:独立背景对象) this.createClipBackground(); // 更新画布 this.updateClipStroke() this.updateCanvasState(); this.fcanvas.requestRenderAll(); this.updateClipPreview && this.updateClipPreview(true) } catch (error) { console.error('剪裁区域创建失败:', error); 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'){ this.clipSettings.shape = shape return } if(!shape){ this.clipSettings.shape = '' this.clearClipPath() this.updateClipPreview && this.updateClipPreview(true) return } const obj = this.editLayer const bounds = obj.getBoundingRect(true) 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 = initWidth // 高度和宽度保持一致 const initRadius = Math.max(5, Math.round(Math.min(canvasW, canvasH) / 4)) const initSettings = { shape, offsetX: Math.round(bounds.left), offsetY: Math.round(bounds.top), width: initWidth, height: initHeight, radius: initRadius, rectRadius: this.clipSettings.rectRadius || 0, } this.clipSettings = { ...this.clipSettings, ...initSettings } this.applyClipPath() }, // 预览辅助,若用户保持旧样式可忽略 dataUrl updateClipPreview(force=false){ if(!this.editLayer || this.editLayer.type !== 'image') return const obj = this.editLayer const bounds = obj.getBoundingRect(true) this.clipPreview = { ...this.clipPreview, maxOffsetX: Math.max(0, Math.floor(bounds.width)), maxOffsetY: Math.max(0, Math.floor(bounds.height)), maxWidth: Math.max(10, Math.floor(bounds.width)), maxHeight: Math.max(10, Math.floor(bounds.height)), maxRadius: Math.max(5, Math.floor(Math.min(bounds.width, bounds.height)/2)), } if(force || this.clipPreview.dataUrl === ''){ try{ // clone 可能是异步的(带 filters 时),使用回调形式避免 callback is not a function obj.clone((cloned)=>{ if(!cloned) return 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 failed', e) } } }, updateClipStroke() { if (!this.judge() || !this.clipSettings.showClipStroke) return; return;; const { shape, width, height, radius, offsetX, offsetY, strokeColor, strokeWidth, rectRadius } = this.clipSettings; // 移除当前对象已有的描边图形(如果存在) if (this.editLayer.strokeObj) { this.fcanvas.remove(this.editLayer.strokeObj); this.editLayer.strokeObj = null; } // 创建新的描边图形 let strokeObj; if (shape === 'rect') { strokeObj = new fabric.Rect({ width: width, height: height, left: offsetX, top: offsetY, rx: rectRadius, ry: rectRadius, stroke: strokeColor, sort:this.editLayer.sort, strokeWidth: strokeWidth, fill: null, selectable: false, evented: false }); } else { strokeObj = new fabric.Circle({ radius: radius, left: offsetX, top: offsetY, sort:this.editLayer.sort, stroke: strokeColor, strokeWidth: strokeWidth, fill: null, selectable: false, evented: false }); } // 将描边图形绑定到当前对象 this.editLayer.strokeObj = strokeObj; this.fcanvas.add(markRaw(strokeObj)); this.fcanvas.requestRenderAll(); }, removeClipStroke() { if (this.clipStrokeObject) { this.fcanvas.remove(this.clipStrokeObject); this.clipStrokeObject = null; } }, clearClipPath() { if (!this.judge()) return; // 清除剪裁路径 this.editLayer.set({ clipPath: null }); // 移除描边图形(如果存在) if (this.editLayer.strokeObj) { this.fcanvas.remove(this.editLayer.strokeObj); 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(); }, // 更新图片尺寸 updateImageSize(adjustedField) { if (!this.judge() || this.editLayer.type !== 'image') return; const { width, height, keepRatio } = this.imageSize; const originalWidth = this.editLayer.width; const originalHeight = this.editLayer.height; if (keepRatio) { // 保持宽高比的情况下,使用Fabric.js内置的scaleToWidth/scaleToHeight // 这些函数本身就会保持比例 if (adjustedField === 'width') { this.editLayer.scaleToWidth(width); // 更新UI显示 this.imageSize.height = Math.round(this.editLayer.getScaledHeight()); } else { this.editLayer.scaleToHeight(height); // 更新UI显示 this.imageSize.width = Math.round(this.editLayer.getScaledWidth()); } } else { // 不保持比例的情况下,直接设置scaleX和scaleY const scaleX = width / originalWidth; const scaleY = height / originalHeight; this.editLayer.set({ scaleX: scaleX, scaleY: scaleY }); } // 重新计算边界和位置 this.editLayer.setCoords(); // 更新画布状态(撤销功能) this.updateCanvasState(); this.fcanvas.requestRenderAll(); // 更新剪裁预览(如果有剪裁) this.updateClipPreview && this.updateClipPreview(true); } } }