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, }; export default { components: { Teleport, }, data() { return { 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, vague:0, color:'#000', }, clipSettings: { ...DEFAULT_CLIP_SETTINGS, }, strokeObj: null // 用于引用当前描边图形 } }, 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(){ 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){ 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": // 优先使用默认值 let imageSettings = { ...DEFAULT_CLIP_SETTINGS }; // 若已有 clipPath,合并其中的属性 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 (this.editLayer.strokeObj) { this.fcanvas.add(markRaw(this.editLayer.strokeObj)); } 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() { }, 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 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.updateClipStroke() this.updateCanvasState(); this.fcanvas.requestRenderAll(); } catch (error) { console.error('剪裁区域创建失败:', error); this.$message.error('剪裁设置错误: ' + error.message); } }, 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; } // 更新画布状态 this.updateCanvasState(); this.fcanvas.requestRenderAll(); } } }