feat: 添加异步导出功能,优化导出图片的下载流程
This commit is contained in:
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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("无法克隆裁剪对象");
|
||||||
|
|||||||
@@ -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); // 下载后移除链接元素
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user