From 46ef450dfb8e74f64a304c9f96652a875b677d63 Mon Sep 17 00:00:00 2001 From: bighuixiang <472705331@qq.com> Date: Tue, 22 Jul 2025 20:39:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=BC=82=E6=AD=A5?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=E5=9B=BE=E7=89=87=E7=9A=84=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/component/Canvas/CanvasEditor/index.vue | 74 ++++++----- .../CanvasEditor/managers/CanvasManager.js | 4 +- .../CanvasEditor/managers/ExportManager.js | 118 ++++++++++++++---- src/component/Canvas/canvasExample.vue | 12 +- 4 files changed, 152 insertions(+), 56 deletions(-) diff --git a/src/component/Canvas/CanvasEditor/index.vue b/src/component/Canvas/CanvasEditor/index.vue index c0113973..63fc9628 100644 --- a/src/component/Canvas/CanvasEditor/index.vue +++ b/src/component/Canvas/CanvasEditor/index.vue @@ -135,11 +135,9 @@ const lastSelectLayerId = ref(null); // 最后选择的图层ID // 当前选择的工具 const activeTool = ref(CanvasConfig.defaultTool); // 默认工具 - //监听画布元素宽度是否发生变化 -let observer = null -let observerTime = null//加入防抖 - +let observer = null; +let observerTime = null; //加入防抖 // 管理器实例 let canvasManager = null; @@ -406,29 +404,49 @@ onMounted(async () => { nextTick(() => { // 确保所有依赖都已加载完成 handleCanvasInit(); - requestAnimationFrame(() => { - setTimeout(() => { - // 初始状态下生成所有预览图 - canvasManager?.updateAllThumbnails?.(); - }, 300); - }); + setTimeout(() => { + // 初始状态下生成所有预览图 + canvasManager?.updateAllThumbnails?.(); + }, 700); }); - - observer = new ResizeObserver(entries => { - for (let entry of entries) { - clearTimeout(observerTime) - observerTime = setTimeout(()=>{ - nextTick(()=>{ - handleWindowResize() - }) - },100) - // const { width } = entry.contentRect; - } - }); - observer.observe(canvasContainerRef.value); + + let throttleTimeout = null; + let lastRunTime = 0; + let trailingTimeout = null; + + observer = new ResizeObserver((entries) => { + const now = Date.now(); + const throttleDelay = 100; + + if (!throttleTimeout) { + // 立即执行一次 + handleWindowResize(); + layerManager?.updateLayersObjectsInteractivity?.(); + setTimeout(() => { + layerManager?.updateLayersObjectsInteractivity?.(); + }); + lastRunTime = now; + + throttleTimeout = setTimeout(() => { + throttleTimeout = null; + }, throttleDelay); + } else { + // 如果在节流期间有新的变化,则重置尾触发 + clearTimeout(trailingTimeout); + trailingTimeout = setTimeout(() => { + handleWindowResize(); + layerManager?.updateLayersObjectsInteractivity?.(); + setTimeout(() => { + layerManager?.updateLayersObjectsInteractivity?.(); + }); + lastRunTime = Date.now(); + }, throttleDelay); + } + }); + observer.observe(canvasContainerRef.value); // 使用window的resize事件代替ResizeObserver // 只有当窗口大小变化时才更新画布尺寸 -// window.addEventListener("resize", handleWindowResize); + // window.addEventListener("resize", handleWindowResize); }); watchEffect(() => { @@ -472,15 +490,15 @@ onBeforeUnmount(() => { redGreenModeManager = null; // 移除window resize事件监听 -// window.removeEventListener("resize", handleWindowResize); - observer.unobserve(canvasContainerRef.value); + // window.removeEventListener("resize", handleWindowResize); + observer.unobserve(canvasContainerRef.value); }); // 窗口大小变化处理函数 function handleWindowResize() { - console.log(132) + console.log(132); // 使用requestAnimationFrame来防止频繁更新 - requestAnimationFrame(() => { + setTimeout(() => { // 更新画布大小并自动居中所有元素 updateCanvasSize(); diff --git a/src/component/Canvas/CanvasEditor/managers/CanvasManager.js b/src/component/Canvas/CanvasEditor/managers/CanvasManager.js index ed8327a3..7ffdbe50 100644 --- a/src/component/Canvas/CanvasEditor/managers/CanvasManager.js +++ b/src/component/Canvas/CanvasEditor/managers/CanvasManager.js @@ -810,7 +810,7 @@ export class CanvasManager { * @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 * @returns {String} 导出的图片数据URL */ - exportImage(options = {}) { + async exportImage(options = {}) { if (!this.exportManager) { console.error("导出管理器未初始化,请确保已设置图层管理器"); throw new Error("导出管理器未初始化"); @@ -852,7 +852,7 @@ export class CanvasManager { } } - return this.exportManager.exportImage(enhancedOptions); + return await this.exportManager.exportImage(enhancedOptions); } catch (error) { console.error("CanvasManager导出图片失败:", error); throw error; diff --git a/src/component/Canvas/CanvasEditor/managers/ExportManager.js b/src/component/Canvas/CanvasEditor/managers/ExportManager.js index 9e9dc282..0e6c21bd 100644 --- a/src/component/Canvas/CanvasEditor/managers/ExportManager.js +++ b/src/component/Canvas/CanvasEditor/managers/ExportManager.js @@ -34,7 +34,6 @@ export class ExportManager { expPicType = "png", restoreOpacityInRedGreen = true, } = options; - try { // 检查是否为红绿图模式 const isRedGreenMode = this.layerManager?.isInRedGreenMode?.() || false; @@ -86,7 +85,12 @@ export class ExportManager { * @returns {String} 图片数据URL * @private */ - _exportSpecificLayer(layerId, expPicType, isRedGreenMode, restoreOpacityInRedGreen) { + async _exportSpecificLayer( + layerId, + expPicType, + isRedGreenMode, + restoreOpacityInRedGreen + ) { if (!this.layerManager) { throw new Error("图层管理器未初始化"); } @@ -110,11 +114,19 @@ export class ExportManager { // 红绿图模式下使用固定尺寸和裁剪 if (isRedGreenMode) { - return this._exportWithRedGreenMode(objectsToExport, expPicType, restoreOpacityInRedGreen); + return this._exportWithRedGreenMode( + objectsToExport, + expPicType, + restoreOpacityInRedGreen + ); } // 普通模式使用画布尺寸 - return this._exportWithCanvasSize(objectsToExport, expPicType, restoreOpacityInRedGreen); + return await this._exportWithCanvasSize( + objectsToExport, + expPicType, + restoreOpacityInRedGreen + ); } /** @@ -128,7 +140,7 @@ export class ExportManager { * @returns {String} 图片数据URL * @private */ - _exportMultipleLayers( + async _exportMultipleLayers( layerIdArray, expPicType, isContainBg, @@ -155,11 +167,19 @@ export class ExportManager { // 红绿图模式下使用固定尺寸和裁剪 if (isRedGreenMode) { - return this._exportWithRedGreenMode(objectsToExport, expPicType, restoreOpacityInRedGreen); + return this._exportWithRedGreenMode( + objectsToExport, + expPicType, + restoreOpacityInRedGreen + ); } // 普通模式使用画布尺寸 - return this._exportWithCanvasSize(objectsToExport, expPicType, restoreOpacityInRedGreen); + return await this._exportWithCanvasSize( + objectsToExport, + expPicType, + restoreOpacityInRedGreen + ); } /** @@ -172,7 +192,7 @@ export class ExportManager { * @returns {String} 图片数据URL * @private */ - _exportAllLayers( + async _exportAllLayers( expPicType, isContainBg, isContainFixed, @@ -194,14 +214,22 @@ export class ExportManager { // 红绿图模式下使用固定尺寸和裁剪 if (isRedGreenMode) { - return this._exportWithRedGreenMode(objectsToExport, expPicType, restoreOpacityInRedGreen); + return this._exportWithRedGreenMode( + objectsToExport, + expPicType, + restoreOpacityInRedGreen + ); } let canvasClipPath = this.canvas.clipPath; if (isCropByBg) { const cropWidth = - this.canvasManager?.canvasWidth?.value || this.canvas?.canvasWidth || this.canvas.width; + this.canvasManager?.canvasWidth?.value || + this.canvas?.canvasWidth || + this.canvas.width; const cropHeight = - this.canvasManager?.canvasHeight?.value || this.canvas?.canvasHeight || this.canvas.height; + this.canvasManager?.canvasHeight?.value || + this.canvas?.canvasHeight || + this.canvas.height; canvasClipPath = new fabric.Rect({ left: this.canvas.width / 2, top: this.canvas.height / 2, @@ -219,7 +247,7 @@ export class ExportManager { canvasClipPath.setCoords(); } // 普通模式使用画布尺寸 - return this._exportWithCanvasSize( + return await this._exportWithCanvasSize( objectsToExport, expPicType, restoreOpacityInRedGreen, @@ -303,14 +331,27 @@ export class ExportManager { * @returns {String} 图片数据URL * @private */ - async _exportObject(obj, expPicType, isRedGreenMode, restoreOpacityInRedGreen) { + async _exportObject( + obj, + expPicType, + isRedGreenMode, + restoreOpacityInRedGreen + ) { // 红绿图模式下使用固定尺寸和裁剪 if (isRedGreenMode) { - return this._exportWithRedGreenMode([obj], expPicType, restoreOpacityInRedGreen); + return this._exportWithRedGreenMode( + [obj], + expPicType, + restoreOpacityInRedGreen + ); } // 普通模式使用画布尺寸 - return this._exportWithCanvasSize([obj], expPicType, restoreOpacityInRedGreen); + return await this._exportWithCanvasSize( + [obj], + expPicType, + restoreOpacityInRedGreen + ); } /** @@ -333,7 +374,8 @@ export class ExportManager { if (layerIdArray && !layerIdArray.includes(layer.id)) continue; // 检查图层类型过滤条件 - if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed)) continue; + if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed)) + continue; if (layer.visible) { const layerObjects = this._collectObjectsFromLayer(layer); @@ -472,12 +514,21 @@ export class ExportManager { * @returns {String} 图片数据URL * @private */ - async _exportWithRedGreenMode(objectsToExport, expPicType, restoreOpacityInRedGreen) { + async _exportWithRedGreenMode( + objectsToExport, + expPicType, + restoreOpacityInRedGreen + ) { // 获取固定图层对象(衣服底图)作为参考 - const fixedLayerObject = this._getFixedLayerObject() ?? this.canvas.clipPath; + const fixedLayerObject = + this._getFixedLayerObject() ?? this.canvas.clipPath; if (!fixedLayerObject) { console.warn("红绿图模式下未找到固定图层对象,使用画布尺寸"); - return this._exportWithCanvasSize(objectsToExport, expPicType, restoreOpacityInRedGreen); + return await this._exportWithCanvasSize( + objectsToExport, + expPicType, + restoreOpacityInRedGreen + ); } // 获取固定图层对象的边界矩形(包含位置、尺寸、缩放等信息) @@ -514,7 +565,10 @@ export class ExportManager { // 克隆并添加所有对象到临时画布,需要调整位置相对于固定图层 for (let i = 0; i < objectsToExport.length; i++) { const obj = objectsToExport[i]; - const cloned = await this._cloneObjectForExport(obj, restoreOpacityInRedGreen && true); + const cloned = await this._cloneObjectForExport( + obj, + restoreOpacityInRedGreen && true + ); if (cloned) { // 调整对象位置:将原画布坐标转换为以固定图层为原点的相对坐标 cloned.set({ @@ -552,7 +606,12 @@ export class ExportManager { * @returns {String} 图片数据URL * @private */ - async _exportWithCanvasSize(objectsToExport, expPicType, restoreOpacityInRedGreen, maskObject) { + async _exportWithCanvasSize( + objectsToExport, + expPicType, + restoreOpacityInRedGreen, + maskObject + ) { // 使用当前画布尺寸 // const canvasWidth = // this.canvasManager?.canvasWidth?.value || this.canvas.width; @@ -644,7 +703,10 @@ export class ExportManager { * @returns {Promise} 克隆的对象 * @private */ - _cloneObjectAsync(obj, propertiesToInclude = ["id", "layerId", "layerName", "name"]) { + _cloneObjectAsync( + obj, + propertiesToInclude = ["id", "layerId", "layerName", "name"] + ) { return new Promise((resolve, reject) => { if (!obj) { resolve(null); @@ -674,7 +736,11 @@ export class ExportManager { * @returns {Promise} 克隆的对象 * @private */ - async _cloneObjectForExport(obj, forceRestoreOpacity = false, removeClipPath = true) { + async _cloneObjectForExport( + obj, + forceRestoreOpacity = false, + removeClipPath = true + ) { if (!obj) return null; try { @@ -808,7 +874,11 @@ export class ExportManager { } // 克隆对象作为裁剪路径 - const clonedClipPath = await this._cloneObjectForExport(clipObject, false, false); + const clonedClipPath = await this._cloneObjectForExport( + clipObject, + false, + false + ); if (!clonedClipPath) { console.warn("无法克隆裁剪对象"); diff --git a/src/component/Canvas/canvasExample.vue b/src/component/Canvas/canvasExample.vue index 84c17991..5f5aa084 100644 --- a/src/component/Canvas/canvasExample.vue +++ b/src/component/Canvas/canvasExample.vue @@ -64,12 +64,20 @@ const editorConfig = { backgroundColor: "#ffffff", // 画布背景色 }; -const exportImage = () => { +const exportImage = async () => { if (canvasEditor.value) { - canvasEditor.value.exportImage({ + const base64 = await canvasEditor.value.exportImage({ isContainFixed: true, // 是否导出底图 isContainBg: false, // 是否导出背景 }); + + // 模拟下载图片 + const link = document.createElement("a"); + link.href = base64; + link.download = "canvas_image.png"; // 设置下载文件名 + document.body.appendChild(link); + link.click(); // 触发下载 + document.body.removeChild(link); // 下载后移除链接元素 } };