feat: 添加异步导出功能,优化导出图片的下载流程

This commit is contained in:
bighuixiang
2025-07-22 20:39:10 +08:00
parent 3652e0a384
commit 46ef450dfb
4 changed files with 152 additions and 56 deletions

View File

@@ -135,11 +135,9 @@ const lastSelectLayerId = ref(null); // 最后选择的图层ID
// 当前选择的工具 // 当前选择的工具
const activeTool = ref(CanvasConfig.defaultTool); // 默认工具 const activeTool = ref(CanvasConfig.defaultTool); // 默认工具
//监听画布元素宽度是否发生变化 //监听画布元素宽度是否发生变化
let observer = null let observer = null;
let observerTime = null//加入防抖 let observerTime = null; //加入防抖
// 管理器实例 // 管理器实例
let canvasManager = null; let canvasManager = null;
@@ -406,23 +404,43 @@ onMounted(async () => {
nextTick(() => { nextTick(() => {
// 确保所有依赖都已加载完成 // 确保所有依赖都已加载完成
handleCanvasInit(); handleCanvasInit();
requestAnimationFrame(() => {
setTimeout(() => { setTimeout(() => {
// 初始状态下生成所有预览图 // 初始状态下生成所有预览图
canvasManager?.updateAllThumbnails?.(); canvasManager?.updateAllThumbnails?.();
}, 300); }, 700);
});
}); });
observer = new ResizeObserver(entries => { let throttleTimeout = null;
for (let entry of entries) { let lastRunTime = 0;
clearTimeout(observerTime) let trailingTimeout = null;
observerTime = setTimeout(()=>{
nextTick(()=>{ observer = new ResizeObserver((entries) => {
handleWindowResize() const now = Date.now();
}) const throttleDelay = 100;
},100)
// const { width } = entry.contentRect; 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); observer.observe(canvasContainerRef.value);
@@ -478,9 +496,9 @@ onBeforeUnmount(() => {
// 窗口大小变化处理函数 // 窗口大小变化处理函数
function handleWindowResize() { function handleWindowResize() {
console.log(132) console.log(132);
// 使用requestAnimationFrame来防止频繁更新 // 使用requestAnimationFrame来防止频繁更新
requestAnimationFrame(() => { setTimeout(() => {
// 更新画布大小并自动居中所有元素 // 更新画布大小并自动居中所有元素
updateCanvasSize(); updateCanvasSize();

View File

@@ -810,7 +810,7 @@ export class CanvasManager {
* @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1 * @param {Boolean} options.restoreOpacityInRedGreen 红绿图模式下是否恢复透明度为1
* @returns {String} 导出的图片数据URL * @returns {String} 导出的图片数据URL
*/ */
exportImage(options = {}) { async exportImage(options = {}) {
if (!this.exportManager) { if (!this.exportManager) {
console.error("导出管理器未初始化,请确保已设置图层管理器"); console.error("导出管理器未初始化,请确保已设置图层管理器");
throw new Error("导出管理器未初始化"); throw new Error("导出管理器未初始化");
@@ -852,7 +852,7 @@ export class CanvasManager {
} }
} }
return this.exportManager.exportImage(enhancedOptions); return await this.exportManager.exportImage(enhancedOptions);
} catch (error) { } catch (error) {
console.error("CanvasManager导出图片失败:", error); console.error("CanvasManager导出图片失败:", error);
throw error; throw error;

View File

@@ -34,7 +34,6 @@ export class ExportManager {
expPicType = "png", expPicType = "png",
restoreOpacityInRedGreen = true, restoreOpacityInRedGreen = true,
} = options; } = options;
try { try {
// 检查是否为红绿图模式 // 检查是否为红绿图模式
const isRedGreenMode = this.layerManager?.isInRedGreenMode?.() || false; const isRedGreenMode = this.layerManager?.isInRedGreenMode?.() || false;
@@ -86,7 +85,12 @@ export class ExportManager {
* @returns {String} 图片数据URL * @returns {String} 图片数据URL
* @private * @private
*/ */
_exportSpecificLayer(layerId, expPicType, isRedGreenMode, restoreOpacityInRedGreen) { async _exportSpecificLayer(
layerId,
expPicType,
isRedGreenMode,
restoreOpacityInRedGreen
) {
if (!this.layerManager) { if (!this.layerManager) {
throw new Error("图层管理器未初始化"); throw new Error("图层管理器未初始化");
} }
@@ -110,11 +114,19 @@ export class ExportManager {
// 红绿图模式下使用固定尺寸和裁剪 // 红绿图模式下使用固定尺寸和裁剪
if (isRedGreenMode) { 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 * @returns {String} 图片数据URL
* @private * @private
*/ */
_exportMultipleLayers( async _exportMultipleLayers(
layerIdArray, layerIdArray,
expPicType, expPicType,
isContainBg, isContainBg,
@@ -155,11 +167,19 @@ export class ExportManager {
// 红绿图模式下使用固定尺寸和裁剪 // 红绿图模式下使用固定尺寸和裁剪
if (isRedGreenMode) { 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 * @returns {String} 图片数据URL
* @private * @private
*/ */
_exportAllLayers( async _exportAllLayers(
expPicType, expPicType,
isContainBg, isContainBg,
isContainFixed, isContainFixed,
@@ -194,14 +214,22 @@ export class ExportManager {
// 红绿图模式下使用固定尺寸和裁剪 // 红绿图模式下使用固定尺寸和裁剪
if (isRedGreenMode) { if (isRedGreenMode) {
return this._exportWithRedGreenMode(objectsToExport, expPicType, restoreOpacityInRedGreen); return this._exportWithRedGreenMode(
objectsToExport,
expPicType,
restoreOpacityInRedGreen
);
} }
let canvasClipPath = this.canvas.clipPath; let canvasClipPath = this.canvas.clipPath;
if (isCropByBg) { if (isCropByBg) {
const cropWidth = const cropWidth =
this.canvasManager?.canvasWidth?.value || this.canvas?.canvasWidth || this.canvas.width; this.canvasManager?.canvasWidth?.value ||
this.canvas?.canvasWidth ||
this.canvas.width;
const cropHeight = 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({ canvasClipPath = new fabric.Rect({
left: this.canvas.width / 2, left: this.canvas.width / 2,
top: this.canvas.height / 2, top: this.canvas.height / 2,
@@ -219,7 +247,7 @@ export class ExportManager {
canvasClipPath.setCoords(); canvasClipPath.setCoords();
} }
// 普通模式使用画布尺寸 // 普通模式使用画布尺寸
return this._exportWithCanvasSize( return await this._exportWithCanvasSize(
objectsToExport, objectsToExport,
expPicType, expPicType,
restoreOpacityInRedGreen, restoreOpacityInRedGreen,
@@ -303,14 +331,27 @@ export class ExportManager {
* @returns {String} 图片数据URL * @returns {String} 图片数据URL
* @private * @private
*/ */
async _exportObject(obj, expPicType, isRedGreenMode, restoreOpacityInRedGreen) { async _exportObject(
obj,
expPicType,
isRedGreenMode,
restoreOpacityInRedGreen
) {
// 红绿图模式下使用固定尺寸和裁剪 // 红绿图模式下使用固定尺寸和裁剪
if (isRedGreenMode) { 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 (layerIdArray && !layerIdArray.includes(layer.id)) continue;
// 检查图层类型过滤条件 // 检查图层类型过滤条件
if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed)) continue; if (!this._shouldIncludeLayer(layer, isContainBg, isContainFixed))
continue;
if (layer.visible) { if (layer.visible) {
const layerObjects = this._collectObjectsFromLayer(layer); const layerObjects = this._collectObjectsFromLayer(layer);
@@ -472,12 +514,21 @@ export class ExportManager {
* @returns {String} 图片数据URL * @returns {String} 图片数据URL
* @private * @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) { if (!fixedLayerObject) {
console.warn("红绿图模式下未找到固定图层对象,使用画布尺寸"); 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++) { for (let i = 0; i < objectsToExport.length; i++) {
const obj = objectsToExport[i]; const obj = objectsToExport[i];
const cloned = await this._cloneObjectForExport(obj, restoreOpacityInRedGreen && true); const cloned = await this._cloneObjectForExport(
obj,
restoreOpacityInRedGreen && true
);
if (cloned) { if (cloned) {
// 调整对象位置:将原画布坐标转换为以固定图层为原点的相对坐标 // 调整对象位置:将原画布坐标转换为以固定图层为原点的相对坐标
cloned.set({ cloned.set({
@@ -552,7 +606,12 @@ export class ExportManager {
* @returns {String} 图片数据URL * @returns {String} 图片数据URL
* @private * @private
*/ */
async _exportWithCanvasSize(objectsToExport, expPicType, restoreOpacityInRedGreen, maskObject) { async _exportWithCanvasSize(
objectsToExport,
expPicType,
restoreOpacityInRedGreen,
maskObject
) {
// 使用当前画布尺寸 // 使用当前画布尺寸
// const canvasWidth = // const canvasWidth =
// this.canvasManager?.canvasWidth?.value || this.canvas.width; // this.canvasManager?.canvasWidth?.value || this.canvas.width;
@@ -644,7 +703,10 @@ export class ExportManager {
* @returns {Promise<fabric.Object>} 克隆的对象 * @returns {Promise<fabric.Object>} 克隆的对象
* @private * @private
*/ */
_cloneObjectAsync(obj, propertiesToInclude = ["id", "layerId", "layerName", "name"]) { _cloneObjectAsync(
obj,
propertiesToInclude = ["id", "layerId", "layerName", "name"]
) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!obj) { if (!obj) {
resolve(null); resolve(null);
@@ -674,7 +736,11 @@ export class ExportManager {
* @returns {Promise<Object>} 克隆的对象 * @returns {Promise<Object>} 克隆的对象
* @private * @private
*/ */
async _cloneObjectForExport(obj, forceRestoreOpacity = false, removeClipPath = true) { async _cloneObjectForExport(
obj,
forceRestoreOpacity = false,
removeClipPath = true
) {
if (!obj) return null; if (!obj) return null;
try { 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) { if (!clonedClipPath) {
console.warn("无法克隆裁剪对象"); console.warn("无法克隆裁剪对象");

View File

@@ -64,12 +64,20 @@ const editorConfig = {
backgroundColor: "#ffffff", // 画布背景色 backgroundColor: "#ffffff", // 画布背景色
}; };
const exportImage = () => { const exportImage = async () => {
if (canvasEditor.value) { if (canvasEditor.value) {
canvasEditor.value.exportImage({ const base64 = await canvasEditor.value.exportImage({
isContainFixed: true, // 是否导出底图 isContainFixed: true, // 是否导出底图
isContainBg: false, // 是否导出背景 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); // 下载后移除链接元素
} }
}; };