| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759 |
- 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);
- }
- }
- }
|