Merge branch 'dev_vite' of https://e.coding.net/aidlabfashion/aida/aida_front into dev_vite
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -22,3 +22,4 @@ dist.rar
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
.eslintrc-auto-import.json
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"serve": "vite",
|
"serve": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview --port 8080 --host 0.0.0.0",
|
||||||
"serve:test": "vite --mode test",
|
"serve:test": "vite --mode test",
|
||||||
"build:test": "vite build --mode test_build",
|
"build:test": "vite build --mode test_build",
|
||||||
"serve:dev": "vite --mode dev",
|
"serve:dev": "vite --mode dev",
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
|
import { createRasterizedImage } from "../utils/SelectionToImage.js";
|
||||||
import { CompositeCommand } from "./Command.js";
|
import { CompositeCommand } from "./Command.js";
|
||||||
import { AddLayerCommand, CreateImageLayerCommand } from "./LayerCommands.js";
|
import { CreateImageLayerCommand } from "./LayerCommands.js";
|
||||||
import { ToolCommand } from "./ToolCommands.js";
|
|
||||||
import { ClearSelectionCommand } from "./SelectionCommands.js";
|
import { ClearSelectionCommand } from "./SelectionCommands.js";
|
||||||
import { createLayer, LayerType, OperationType } from "../utils/layerHelper.js";
|
|
||||||
import { fabric } from "fabric-with-all";
|
import { fabric } from "fabric-with-all";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,7 +26,7 @@ export class LassoCutoutCommand extends CompositeCommand {
|
|||||||
this.executedCommands = [];
|
this.executedCommands = [];
|
||||||
// 高清截图选项
|
// 高清截图选项
|
||||||
this.highResolutionEnabled = options.highResolutionEnabled !== false; // 默认启用
|
this.highResolutionEnabled = options.highResolutionEnabled !== false; // 默认启用
|
||||||
this.baseResolutionScale = options.baseResolutionScale || 4; // 基础分辨率倍数,提高到4倍获得更清晰的图像
|
this.baseResolutionScale = options.baseResolutionScale || 2; // 基础分辨率倍数
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute() {
|
async execute() {
|
||||||
@@ -48,18 +47,26 @@ export class LassoCutoutCommand extends CompositeCommand {
|
|||||||
|
|
||||||
// 确定源图层
|
// 确定源图层
|
||||||
const sourceLayer = this.layerManager.getActiveLayer();
|
const sourceLayer = this.layerManager.getActiveLayer();
|
||||||
if (!sourceLayer || sourceLayer.fabricObjects.length === 0) {
|
if (!sourceLayer) {
|
||||||
console.error("无法执行套索抠图:源图层无效");
|
console.error("无法执行套索抠图:源图层无效");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取源图层的所有对象(包括子图层)
|
||||||
|
const sourceObjects = this._getLayerObjects(sourceLayer);
|
||||||
|
if (sourceObjects.length === 0) {
|
||||||
|
console.error("无法执行套索抠图:源图层没有可见对象");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// 获取选区边界信息用于后续定位
|
// 获取选区边界信息用于后续定位
|
||||||
const selectionBounds = selectionObject.getBoundingRect(true, true);
|
const selectionBounds = selectionObject.getBoundingRect(true, true);
|
||||||
|
|
||||||
// 执行在当前画布上的抠图操作
|
// 使用createRasterizedImage执行抠图操作
|
||||||
this.cutoutImageUrl = await this._performCutout(
|
this.cutoutImageUrl = await this._performCutoutWithRasterized(
|
||||||
sourceLayer,
|
sourceObjects,
|
||||||
selectionObject
|
selectionObject,
|
||||||
|
selectionBounds
|
||||||
);
|
);
|
||||||
if (!this.cutoutImageUrl) {
|
if (!this.cutoutImageUrl) {
|
||||||
console.error("抠图失败");
|
console.error("抠图失败");
|
||||||
@@ -143,7 +150,120 @@ export class LassoCutoutCommand extends CompositeCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在当前画布上执行抠图操作
|
* 获取图层的所有对象(包括子图层,从画布中查找真实对象)
|
||||||
|
* @param {Object} layer 图层对象
|
||||||
|
* @returns {Array} 真实的fabric对象数组
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getLayerObjects(layer) {
|
||||||
|
const objects = [];
|
||||||
|
const canvasObjects = this.canvas.getObjects();
|
||||||
|
|
||||||
|
// 递归获取图层及其子图层的所有对象
|
||||||
|
const collectLayerObjects = (currentLayer) => {
|
||||||
|
// 处理图层的fabricObjects
|
||||||
|
if (
|
||||||
|
currentLayer.fabricObjects &&
|
||||||
|
Array.isArray(currentLayer.fabricObjects)
|
||||||
|
) {
|
||||||
|
currentLayer.fabricObjects.forEach((fabricRef) => {
|
||||||
|
if (fabricRef && fabricRef.id) {
|
||||||
|
// 从画布中查找真实的对象
|
||||||
|
const realObject = canvasObjects.find(
|
||||||
|
(obj) => obj.id === fabricRef.id
|
||||||
|
);
|
||||||
|
if (realObject && realObject.visible) {
|
||||||
|
objects.push(realObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理单个fabricObject(背景图层等)
|
||||||
|
if (currentLayer.fabricObject && currentLayer.fabricObject.id) {
|
||||||
|
const realObject = canvasObjects.find(
|
||||||
|
(obj) => obj.id === currentLayer.fabricObject.id
|
||||||
|
);
|
||||||
|
if (realObject && realObject.visible) {
|
||||||
|
objects.push(realObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归处理子图层
|
||||||
|
if (currentLayer.children && Array.isArray(currentLayer.children)) {
|
||||||
|
currentLayer.children.forEach((childLayer) => {
|
||||||
|
if (childLayer.visible !== false) {
|
||||||
|
// 只处理可见的子图层
|
||||||
|
collectLayerObjects(childLayer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
collectLayerObjects(layer);
|
||||||
|
|
||||||
|
console.log(`从图层 "${layer.name}" 收集到 ${objects.length} 个可见对象`);
|
||||||
|
return objects;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用createRasterizedImage执行抠图操作
|
||||||
|
* @param {Array} sourceObjects 源对象数组
|
||||||
|
* @param {Object} selectionObject 选区对象
|
||||||
|
* @param {Object} selectionBounds 选区边界
|
||||||
|
* @returns {String} 抠图结果的DataURL
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async _performCutoutWithRasterized(
|
||||||
|
sourceObjects,
|
||||||
|
selectionObject,
|
||||||
|
selectionBounds
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
console.log("=== 开始使用createRasterizedImage执行抠图 ===");
|
||||||
|
console.log(`源对象数量: ${sourceObjects.length}`);
|
||||||
|
console.log(`选区边界:`, selectionBounds);
|
||||||
|
|
||||||
|
// 确定缩放因子
|
||||||
|
let scaleFactor = this.baseResolutionScale;
|
||||||
|
if (this.highResolutionEnabled) {
|
||||||
|
const devicePixelRatio = window.devicePixelRatio || 1;
|
||||||
|
scaleFactor = Math.max(scaleFactor, devicePixelRatio * 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用createRasterizedImage生成栅格化图像,将选区作为裁剪路径
|
||||||
|
const rasterizedDataURL = await createRasterizedImage({
|
||||||
|
canvas: this.canvas,
|
||||||
|
fabricObjects: sourceObjects,
|
||||||
|
clipPath: selectionObject, // 使用选区作为裁剪路径而不是遮罩
|
||||||
|
trimWhitespace: true,
|
||||||
|
trimPadding: 2,
|
||||||
|
quality: 1.0,
|
||||||
|
format: "png",
|
||||||
|
scaleFactor: scaleFactor,
|
||||||
|
isReturenDataURL: true, // 返回DataURL
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!rasterizedDataURL) {
|
||||||
|
throw new Error("栅格化生成图像失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`抠图完成,缩放因子: ${scaleFactor}x`);
|
||||||
|
return rasterizedDataURL;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("使用createRasterizedImage执行抠图失败:", error);
|
||||||
|
|
||||||
|
// 如果createRasterizedImage失败,回退到原始方法
|
||||||
|
console.log("回退到原始抠图方法...");
|
||||||
|
return await this._performCutout(
|
||||||
|
{ fabricObjects: sourceObjects },
|
||||||
|
selectionObject
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原始抠图方法(作为备用方案)
|
||||||
* @param {Object} sourceLayer 源图层
|
* @param {Object} sourceLayer 源图层
|
||||||
* @param {Object} selectionObject 选区对象
|
* @param {Object} selectionObject 选区对象
|
||||||
* @returns {String} 抠图结果的DataURL
|
* @returns {String} 抠图结果的DataURL
|
||||||
@@ -151,13 +271,10 @@ export class LassoCutoutCommand extends CompositeCommand {
|
|||||||
*/
|
*/
|
||||||
async _performCutout(sourceLayer, selectionObject) {
|
async _performCutout(sourceLayer, selectionObject) {
|
||||||
try {
|
try {
|
||||||
console.log("=== 开始在当前画布执行抠图 ===");
|
console.log("=== 使用原始方法执行抠图 ===");
|
||||||
|
|
||||||
// 获取选区边界
|
// 获取选区边界
|
||||||
const selectionBounds = selectionObject.getBoundingRect(true, true);
|
const selectionBounds = selectionObject.getBoundingRect(true, true);
|
||||||
console.log(
|
|
||||||
`选区边界: left=${selectionBounds.left}, top=${selectionBounds.top}, width=${selectionBounds.width}, height=${selectionBounds.height}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// 保存画布当前状态
|
// 保存画布当前状态
|
||||||
const originalActiveObject = this.canvas.getActiveObject();
|
const originalActiveObject = this.canvas.getActiveObject();
|
||||||
@@ -175,7 +292,6 @@ export class LassoCutoutCommand extends CompositeCommand {
|
|||||||
const visibleObjects = sourceLayer.fabricObjects.filter(
|
const visibleObjects = sourceLayer.fabricObjects.filter(
|
||||||
(obj) => obj.visible
|
(obj) => obj.visible
|
||||||
);
|
);
|
||||||
console.log(`源图层可见对象数量: ${visibleObjects.length}`);
|
|
||||||
|
|
||||||
if (visibleObjects.length === 0) {
|
if (visibleObjects.length === 0) {
|
||||||
throw new Error("源图层没有可见对象");
|
throw new Error("源图层没有可见对象");
|
||||||
@@ -184,36 +300,19 @@ export class LassoCutoutCommand extends CompositeCommand {
|
|||||||
// 如果只有一个对象且已经是组,直接使用
|
// 如果只有一个对象且已经是组,直接使用
|
||||||
if (visibleObjects.length === 1 && visibleObjects[0].type === "group") {
|
if (visibleObjects.length === 1 && visibleObjects[0].type === "group") {
|
||||||
tempGroup = visibleObjects[0];
|
tempGroup = visibleObjects[0];
|
||||||
console.log("使用现有组对象");
|
|
||||||
} else {
|
} else {
|
||||||
// 创建临时组
|
// 创建临时组
|
||||||
console.log("创建临时组...");
|
|
||||||
|
|
||||||
// 记录原始对象的位置和状态,用于后续恢复
|
|
||||||
originalObjects = visibleObjects.map((obj) => ({
|
|
||||||
object: obj,
|
|
||||||
originalLeft: obj.left,
|
|
||||||
originalTop: obj.top,
|
|
||||||
originalAngle: obj.angle,
|
|
||||||
originalScaleX: obj.scaleX,
|
|
||||||
originalScaleY: obj.scaleY,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 不需要从画布移除原对象,直接创建组
|
|
||||||
// 克隆对象来创建组,避免影响原对象
|
|
||||||
const clonedObjects = [];
|
const clonedObjects = [];
|
||||||
for (const obj of visibleObjects) {
|
for (const obj of visibleObjects) {
|
||||||
const cloned = await this._cloneObject(obj);
|
const cloned = await this._cloneObject(obj);
|
||||||
clonedObjects.push(cloned);
|
clonedObjects.push(cloned);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建组
|
|
||||||
tempGroup = new fabric.Group(clonedObjects, {
|
tempGroup = new fabric.Group(clonedObjects, {
|
||||||
selectable: false,
|
selectable: false,
|
||||||
evented: false,
|
evented: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 添加组到画布
|
|
||||||
this.canvas.add(tempGroup);
|
this.canvas.add(tempGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +326,6 @@ export class LassoCutoutCommand extends CompositeCommand {
|
|||||||
originY: "top",
|
originY: "top",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 应用裁剪路径到组
|
|
||||||
tempGroup.set({
|
tempGroup.set({
|
||||||
clipPath: clipPath,
|
clipPath: clipPath,
|
||||||
});
|
});
|
||||||
@@ -242,23 +340,14 @@ export class LassoCutoutCommand extends CompositeCommand {
|
|||||||
height: selectionBounds.height,
|
height: selectionBounds.height,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 设置高分辨率倍数,用于提高图像清晰度
|
// 设置高分辨率倍数
|
||||||
let highResolutionScale = 1;
|
let highResolutionScale = 1;
|
||||||
|
|
||||||
if (this.highResolutionEnabled) {
|
if (this.highResolutionEnabled) {
|
||||||
// 结合设备像素比和配置的基础倍数,确保在所有设备上都有最佳效果
|
|
||||||
const devicePixelRatio = window.devicePixelRatio || 1;
|
const devicePixelRatio = window.devicePixelRatio || 1;
|
||||||
// 使用更激进的缩放策略,确保高清晰度
|
|
||||||
highResolutionScale = Math.max(
|
highResolutionScale = Math.max(
|
||||||
this.baseResolutionScale,
|
this.baseResolutionScale,
|
||||||
devicePixelRatio * 2
|
devicePixelRatio * 2
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(
|
|
||||||
`设备像素比: ${devicePixelRatio}, 基础倍数: ${this.baseResolutionScale}, 最终放大倍数: ${highResolutionScale}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log("高分辨率渲染已禁用,使用1x倍数");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建用于导出的高分辨率canvas
|
// 创建用于导出的高分辨率canvas
|
||||||
@@ -269,7 +358,6 @@ export class LassoCutoutCommand extends CompositeCommand {
|
|||||||
colorSpace: "srgb",
|
colorSpace: "srgb",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 设置canvas的实际像素尺寸(放大倍数)
|
|
||||||
const actualWidth = Math.round(
|
const actualWidth = Math.round(
|
||||||
renderBounds.width * highResolutionScale
|
renderBounds.width * highResolutionScale
|
||||||
);
|
);
|
||||||
@@ -279,99 +367,38 @@ export class LassoCutoutCommand extends CompositeCommand {
|
|||||||
|
|
||||||
exportCanvas.width = actualWidth;
|
exportCanvas.width = actualWidth;
|
||||||
exportCanvas.height = actualHeight;
|
exportCanvas.height = actualHeight;
|
||||||
|
|
||||||
// 设置canvas的显示尺寸(CSS尺寸,保持与选区一致)
|
|
||||||
exportCanvas.style.width = renderBounds.width + "px";
|
exportCanvas.style.width = renderBounds.width + "px";
|
||||||
exportCanvas.style.height = renderBounds.height + "px";
|
exportCanvas.style.height = renderBounds.height + "px";
|
||||||
|
|
||||||
// 启用最高质量渲染设置
|
|
||||||
exportCtx.imageSmoothingEnabled = true;
|
exportCtx.imageSmoothingEnabled = true;
|
||||||
exportCtx.imageSmoothingQuality = "high";
|
exportCtx.imageSmoothingQuality = "high";
|
||||||
|
|
||||||
// 设置文本渲染质量
|
|
||||||
if (exportCtx.textRenderingOptimization) {
|
|
||||||
exportCtx.textRenderingOptimization = "optimizeQuality";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置线条和图形的渲染质量
|
|
||||||
exportCtx.lineCap = "round";
|
|
||||||
exportCtx.lineJoin = "round";
|
|
||||||
exportCtx.miterLimit = 10;
|
|
||||||
|
|
||||||
// 设置画布背景为透明
|
|
||||||
exportCtx.clearRect(0, 0, actualWidth, actualHeight);
|
exportCtx.clearRect(0, 0, actualWidth, actualHeight);
|
||||||
|
|
||||||
// 获取画布当前的变换矩阵
|
|
||||||
const vpt = this.canvas.viewportTransform;
|
const vpt = this.canvas.viewportTransform;
|
||||||
const zoom = this.canvas.getZoom();
|
const zoom = this.canvas.getZoom();
|
||||||
|
|
||||||
// 保存当前变换状态
|
|
||||||
exportCtx.save();
|
exportCtx.save();
|
||||||
|
|
||||||
// 应用高分辨率缩放
|
|
||||||
exportCtx.scale(highResolutionScale, highResolutionScale);
|
exportCtx.scale(highResolutionScale, highResolutionScale);
|
||||||
|
|
||||||
// 应用偏移,只渲染选区部分
|
|
||||||
exportCtx.translate(-renderBounds.left, -renderBounds.top);
|
exportCtx.translate(-renderBounds.left, -renderBounds.top);
|
||||||
|
|
||||||
// 如果画布有缩放和平移,需要应用相应变换
|
|
||||||
if (zoom !== 1 || vpt[4] !== 0 || vpt[5] !== 0) {
|
if (zoom !== 1 || vpt[4] !== 0 || vpt[5] !== 0) {
|
||||||
exportCtx.transform(vpt[0], vpt[1], vpt[2], vpt[3], vpt[4], vpt[5]);
|
exportCtx.transform(vpt[0], vpt[1], vpt[2], vpt[3], vpt[4], vpt[5]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染被裁剪的组
|
|
||||||
tempGroup.render(exportCtx);
|
tempGroup.render(exportCtx);
|
||||||
|
|
||||||
exportCtx.restore();
|
exportCtx.restore();
|
||||||
|
|
||||||
// 获取结果 - 使用最高质量设置
|
|
||||||
const dataUrl = exportCanvas.toDataURL("image/png", 1.0);
|
const dataUrl = exportCanvas.toDataURL("image/png", 1.0);
|
||||||
|
|
||||||
console.log(
|
|
||||||
`抠图完成,选区尺寸: ${renderBounds.width}x${renderBounds.height}, 实际渲染尺寸: ${actualWidth}x${actualHeight}, 放大倍数: ${highResolutionScale}x`
|
|
||||||
);
|
|
||||||
|
|
||||||
return dataUrl;
|
return dataUrl;
|
||||||
} finally {
|
} finally {
|
||||||
// 清理和恢复
|
// 清理和恢复
|
||||||
if (tempGroup) {
|
if (tempGroup) {
|
||||||
// 移除裁剪路径
|
|
||||||
tempGroup.set({ clipPath: null });
|
tempGroup.set({ clipPath: null });
|
||||||
|
|
||||||
// 如果是我们创建的临时组,需要移除它
|
|
||||||
if (originalObjects.length > 0) {
|
if (originalObjects.length > 0) {
|
||||||
this.canvas.remove(tempGroup);
|
this.canvas.remove(tempGroup);
|
||||||
|
|
||||||
// 恢复原始对象的状态(位置等信息保持不变)
|
|
||||||
originalObjects.forEach(
|
|
||||||
({
|
|
||||||
object,
|
|
||||||
originalLeft,
|
|
||||||
originalTop,
|
|
||||||
originalAngle,
|
|
||||||
originalScaleX,
|
|
||||||
originalScaleY,
|
|
||||||
}) => {
|
|
||||||
// 确保对象仍然在画布上且状态正确
|
|
||||||
if (!this.canvas.getObjects().includes(object)) {
|
|
||||||
this.canvas.add(object);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 恢复原始变换状态(如果需要的话)
|
|
||||||
object.set({
|
|
||||||
left: originalLeft,
|
|
||||||
top: originalTop,
|
|
||||||
angle: originalAngle,
|
|
||||||
scaleX: originalScaleX,
|
|
||||||
scaleY: originalScaleY,
|
|
||||||
});
|
|
||||||
object.setCoords();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 恢复画布状态
|
|
||||||
this.canvas.selection = originalSelection;
|
this.canvas.selection = originalSelection;
|
||||||
if (originalActiveObject) {
|
if (originalActiveObject) {
|
||||||
this.canvas.setActiveObject(originalActiveObject);
|
this.canvas.setActiveObject(originalActiveObject);
|
||||||
@@ -379,7 +406,7 @@ export class LassoCutoutCommand extends CompositeCommand {
|
|||||||
this.canvas.renderAll();
|
this.canvas.renderAll();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("在当前画布执行抠图失败:", error);
|
console.error("原始方法执行抠图失败:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -401,39 +428,14 @@ export class LassoCutoutCommand extends CompositeCommand {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算画布中心位置
|
// 使用选区的位置作为图像位置
|
||||||
const canvasCenter = this.canvas.getCenter();
|
let targetLeft = selectionBounds.left + selectionBounds.width / 2;
|
||||||
|
let targetTop = selectionBounds.top + selectionBounds.height / 2;
|
||||||
|
|
||||||
// 如果有选区边界信息,使用选区的原始位置和尺寸
|
// 设置图像属性,保持选区的原始尺寸
|
||||||
let targetLeft = canvasCenter.left;
|
|
||||||
let targetTop = canvasCenter.top;
|
|
||||||
let targetWidth = img.width;
|
|
||||||
let targetHeight = img.height;
|
|
||||||
|
|
||||||
if (selectionBounds) {
|
|
||||||
// 使用选区的原始位置
|
|
||||||
targetLeft = selectionBounds.left + selectionBounds.width / 2;
|
|
||||||
targetTop = selectionBounds.top + selectionBounds.height / 2;
|
|
||||||
|
|
||||||
// 确保图像显示尺寸与选区尺寸一致
|
|
||||||
targetWidth = selectionBounds.width;
|
|
||||||
targetHeight = selectionBounds.height;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`设置图像位置: left=${targetLeft}, top=${targetTop}, 尺寸: ${targetWidth}x${targetHeight}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算缩放比例以匹配目标尺寸
|
|
||||||
const scaleX = targetWidth / img.width;
|
|
||||||
const scaleY = targetHeight / img.height;
|
|
||||||
|
|
||||||
// 设置图像属性
|
|
||||||
img.set({
|
img.set({
|
||||||
left: targetLeft,
|
left: targetLeft,
|
||||||
top: targetTop,
|
top: targetTop,
|
||||||
scaleX: scaleX,
|
|
||||||
scaleY: scaleY,
|
|
||||||
originX: "center",
|
originX: "center",
|
||||||
originY: "center",
|
originY: "center",
|
||||||
selectable: true,
|
selectable: true,
|
||||||
@@ -455,6 +457,9 @@ export class LassoCutoutCommand extends CompositeCommand {
|
|||||||
// 更新坐标
|
// 更新坐标
|
||||||
img.setCoords();
|
img.setCoords();
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`图像创建完成,位置: (${targetLeft}, ${targetTop}), 尺寸: ${img.width}x${img.height}`
|
||||||
|
);
|
||||||
resolve(img);
|
resolve(img);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -127,7 +127,10 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 4. 确保背景图层大小和衣服地图大小一致
|
// 4. 确保背景图层大小和衣服地图大小一致
|
||||||
await this._setupBackgroundLayer(backgroundLayer, this.clothingImage);
|
const backgroundObject = await this._setupBackgroundLayer(
|
||||||
|
backgroundLayer,
|
||||||
|
this.clothingImage
|
||||||
|
);
|
||||||
|
|
||||||
// 8. 设置普通图层透明度
|
// 8. 设置普通图层透明度
|
||||||
this._setupNormalLayerOpacity(normalLayers); // 这里不需要在这里设置透明度 由图层统一处理
|
this._setupNormalLayerOpacity(normalLayers); // 这里不需要在这里设置透明度 由图层统一处理
|
||||||
@@ -136,7 +139,7 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
|||||||
this.newEmptyLayerId = await this._createAndActivateEmptyLayer();
|
this.newEmptyLayerId = await this._createAndActivateEmptyLayer();
|
||||||
|
|
||||||
// 设置普通图层的裁剪对象为衣服底图
|
// 设置普通图层的裁剪对象为衣服底图
|
||||||
if (this.redGreenImage) {
|
if (backgroundObject) {
|
||||||
// const clipPathImg = this.redGreenImage;
|
// const clipPathImg = this.redGreenImage;
|
||||||
// clipPathImg.set({
|
// clipPathImg.set({
|
||||||
// absolutePositioned: true,
|
// absolutePositioned: true,
|
||||||
@@ -144,7 +147,7 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
|||||||
|
|
||||||
// 克隆衣服底图作为裁剪对象
|
// 克隆衣服底图作为裁剪对象
|
||||||
this.redGreenImageMask = await new Promise((resolve, reject) => {
|
this.redGreenImageMask = await new Promise((resolve, reject) => {
|
||||||
this.redGreenImage.clone((clonedImg) => {
|
backgroundObject.clone((clonedImg) => {
|
||||||
if (!clonedImg) {
|
if (!clonedImg) {
|
||||||
reject(new Error("无法克隆红绿图"));
|
reject(new Error("无法克隆红绿图"));
|
||||||
return;
|
return;
|
||||||
@@ -330,7 +333,7 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
|||||||
*/
|
*/
|
||||||
async _setupBackgroundLayer(backgroundLayer, clothingImage) {
|
async _setupBackgroundLayer(backgroundLayer, clothingImage) {
|
||||||
let backgroundObject = backgroundLayer.fabricObject;
|
let backgroundObject = backgroundLayer.fabricObject;
|
||||||
const { object } = findObjectById(this.canvas, backgroundObject.id);
|
let { object } = findObjectById(this.canvas, backgroundObject.id);
|
||||||
|
|
||||||
if (!object) {
|
if (!object) {
|
||||||
// 创建白色背景矩形
|
// 创建白色背景矩形
|
||||||
@@ -366,6 +369,10 @@ export class BatchInitializeRedGreenModeCommand extends Command {
|
|||||||
fill: "transparent", // 确保背景是透明的
|
fill: "transparent", // 确保背景是透明的
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object.setCoords();
|
||||||
|
|
||||||
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ const props = defineProps({
|
|||||||
canvasColor: String,
|
canvasColor: String,
|
||||||
brushSize: Number,
|
brushSize: Number,
|
||||||
enabledRedGreenMode: Boolean,
|
enabledRedGreenMode: Boolean,
|
||||||
|
showLayersPanel: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true, // 是否显示图层面板
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@@ -337,12 +341,11 @@ onMounted(() => {
|
|||||||
</div> -->
|
</div> -->
|
||||||
|
|
||||||
<!-- 绘图工具设置 -->
|
<!-- 绘图工具设置 -->
|
||||||
<div class="canvas-settings gap-20">
|
<div class="canvas-settings gap-20" v-if="!props.enabledRedGreenMode">
|
||||||
<div
|
<div
|
||||||
class="btn"
|
class="btn"
|
||||||
:class="{ active: showBrushPanel }"
|
:class="{ active: showBrushPanel }"
|
||||||
@click="toggleBrushPanel"
|
@click="toggleBrushPanel"
|
||||||
v-if="!props.enabledRedGreenMode"
|
|
||||||
>
|
>
|
||||||
<!-- <span class="setting-label">笔刷:</span>/ -->
|
<!-- <span class="setting-label">笔刷:</span>/ -->
|
||||||
<div class="brush-selector">
|
<div class="brush-selector">
|
||||||
@@ -369,6 +372,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
v-if="showLayersPanel"
|
||||||
class="btn"
|
class="btn"
|
||||||
:class="{ active: isShowLayerPanel }"
|
:class="{ active: isShowLayerPanel }"
|
||||||
@click="showLayerPanel"
|
@click="showLayerPanel"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, nextTick, onMounted, onUnmounted } from "vue";
|
import { ref, watch, nextTick, onMounted, onUnmounted } from "vue";
|
||||||
import SvgIcon from "../../../SvgIcon/index.vue";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { ref, nextTick, computed, inject } from "vue";
|
import { ref, nextTick, computed, inject } from "vue";
|
||||||
import { Checkbox } from "ant-design-vue";
|
import { Checkbox } from "ant-design-vue";
|
||||||
import { VueDraggable } from "vue-draggable-plus";
|
import { VueDraggable } from "vue-draggable-plus";
|
||||||
import SvgIcon from "../../../SvgIcon/index.vue";
|
|
||||||
import { isGroupLayer } from "../../utils/layerHelper";
|
import { isGroupLayer } from "../../utils/layerHelper";
|
||||||
|
|
||||||
// 设置组件名称,用于递归渲染
|
// 设置组件名称,用于递归渲染
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, nextTick, inject } from "vue";
|
import { computed, ref, nextTick, inject } from "vue";
|
||||||
import { findLayerRecursively, isGroupLayer } from "../../utils/layerHelper";
|
import { findLayerRecursively, isGroupLayer } from "../../utils/layerHelper";
|
||||||
import SvgIcon from "../../../SvgIcon/index.vue";
|
|
||||||
import ContextMenu from "./ContextMenu.vue";
|
import ContextMenu from "./ContextMenu.vue";
|
||||||
import LayerItem from "./LayerItem.vue";
|
import LayerItem from "./LayerItem.vue";
|
||||||
import LayersList from "./LayersList.vue"; // 引入 LayersList 组件
|
import LayersList from "./LayersList.vue"; // 引入 LayersList 组件
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ import SelectionPanel from "./components/SelectionPanel.vue"; // 引入选区面
|
|||||||
import { OperationType } from "./utils/layerHelper.js";
|
import { OperationType } from "./utils/layerHelper.js";
|
||||||
import { ToolManager } from "./managers/toolManager.js";
|
import { ToolManager } from "./managers/toolManager.js";
|
||||||
import { fabric } from "fabric-with-all";
|
import { fabric } from "fabric-with-all";
|
||||||
import { uploadImageAndCreateLayer } from "./utils/imageHelper.js";
|
import {
|
||||||
|
uploadImageAndCreateLayer,
|
||||||
|
loadImageUrlToLayer,
|
||||||
|
} from "./utils/imageHelper.js";
|
||||||
// import MinimapPanel from "./components/MinimapPanel.vue";
|
// import MinimapPanel from "./components/MinimapPanel.vue";
|
||||||
const KeyboardShortcutHelp = defineAsyncComponent(() =>
|
const KeyboardShortcutHelp = defineAsyncComponent(() =>
|
||||||
import("./components/KeyboardShortcutHelp.vue")
|
import("./components/KeyboardShortcutHelp.vue")
|
||||||
@@ -48,6 +51,10 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: () => CanvasConfig, // 默认配置
|
default: () => CanvasConfig, // 默认配置
|
||||||
},
|
},
|
||||||
|
showLayersPanel: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true, // 是否显示图层面板
|
||||||
|
},
|
||||||
enabledRedGreenMode: {
|
enabledRedGreenMode: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false, // 是否启用红绿图模式
|
default: false, // 是否启用红绿图模式
|
||||||
@@ -339,21 +346,29 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (import.meta.hot) {
|
// if (import.meta.hot) {
|
||||||
// 热更新 ?
|
// // 热更新 ?
|
||||||
console.log("onBeforeUnmount 开发环境热更新不卸载组件...");
|
// console.log("onBeforeUnmount 开发环境热更新不卸载组件...");
|
||||||
return; // 开发环境下不卸载组件
|
// return; // 开发环境下不卸载组件
|
||||||
}
|
// }
|
||||||
console.log("onBeforeUnmount 组件卸载,清理资源...");
|
console.log("onBeforeUnmount 组件卸载,清理资源...");
|
||||||
canvasManager?.dispose?.();
|
// canvasManager?.dispose?.();
|
||||||
commandManager?.dispose?.();
|
// commandManager?.dispose?.();
|
||||||
layerManager?.dispose?.();
|
// layerManager?.dispose?.();
|
||||||
keyboardManager?.dispose?.();
|
// keyboardManager?.dispose?.();
|
||||||
toolManager?.dispose?.();
|
// toolManager?.dispose?.();
|
||||||
liquifyManager?.dispose?.();
|
// liquifyManager?.dispose?.();
|
||||||
selectionManager?.dispose?.();
|
// selectionManager?.dispose?.();
|
||||||
redGreenModeManager?.dispose?.();
|
// redGreenModeManager?.dispose?.();
|
||||||
// minimapManager?.dispose?.();
|
// minimapManager?.dispose?.();
|
||||||
|
canvasManager = null;
|
||||||
|
commandManager = null;
|
||||||
|
layerManager = null;
|
||||||
|
keyboardManager = null;
|
||||||
|
toolManager = null;
|
||||||
|
liquifyManager = null;
|
||||||
|
selectionManager = null;
|
||||||
|
redGreenModeManager = null;
|
||||||
|
|
||||||
// 移除window resize事件监听
|
// 移除window resize事件监听
|
||||||
window.removeEventListener("resize", handleWindowResize);
|
window.removeEventListener("resize", handleWindowResize);
|
||||||
@@ -635,9 +650,15 @@ defineExpose({
|
|||||||
changeFixedImage: (url, opts) => {
|
changeFixedImage: (url, opts) => {
|
||||||
return canvasManager?.changeFixedImage?.(url, opts);
|
return canvasManager?.changeFixedImage?.(url, opts);
|
||||||
},
|
},
|
||||||
//图片url或者base64 可选图层ID 不传默认新建图层
|
//图片url或者base64
|
||||||
addImageToLayer: (url, layerId) => {
|
addImageToLayer: async (url) => {
|
||||||
return canvasManager.addImageToLayer(url, layerId);
|
if (!url) return Promise.reject(new Error("图片URL不能为空"));
|
||||||
|
return await loadImageUrlToLayer({
|
||||||
|
imageUrl: url,
|
||||||
|
layerManager,
|
||||||
|
canvas: canvasManager.canvas,
|
||||||
|
toolManager,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
//图片url或者base64数组 可选图层ID 不传默认新建图层
|
//图片url或者base64数组 可选图层ID 不传默认新建图层
|
||||||
addMultipleImagesToLayer: (urls, layerId) => {
|
addMultipleImagesToLayer: (urls, layerId) => {
|
||||||
@@ -760,6 +781,7 @@ defineExpose({
|
|||||||
:canvasColor="canvasColor"
|
:canvasColor="canvasColor"
|
||||||
:brushSize="brushSize"
|
:brushSize="brushSize"
|
||||||
:enabledRedGreenMode="enabledRedGreenMode"
|
:enabledRedGreenMode="enabledRedGreenMode"
|
||||||
|
:showLayersPanel="showLayersPanel"
|
||||||
@update:canvasWidth="canvasWidth = $event"
|
@update:canvasWidth="canvasWidth = $event"
|
||||||
@update:canvasHeight="canvasHeight = $event"
|
@update:canvasHeight="canvasHeight = $event"
|
||||||
@update:canvasColor="canvasColor = $event"
|
@update:canvasColor="canvasColor = $event"
|
||||||
@@ -847,7 +869,10 @@ defineExpose({
|
|||||||
<!-- v-if="canvasManagerLoaded && !enabledRedGreenMode" -->
|
<!-- v-if="canvasManagerLoaded && !enabledRedGreenMode" -->
|
||||||
|
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div class="layers-panel" v-if="isShowLayerPanel">
|
<div
|
||||||
|
class="layers-panel"
|
||||||
|
v-if="isShowLayerPanel && !enabledRedGreenMode && showLayersPanel"
|
||||||
|
>
|
||||||
<LayersPanel
|
<LayersPanel
|
||||||
v-if="canvasManagerLoaded"
|
v-if="canvasManagerLoaded"
|
||||||
:activeLayerId="activeLayerId"
|
:activeLayerId="activeLayerId"
|
||||||
@@ -866,10 +891,10 @@ defineExpose({
|
|||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer-actions">
|
<!-- <div class="footer-actions">
|
||||||
<button class="share-btn">Share</button>
|
<button class="share-btn">Share</button>
|
||||||
<button class="export-btn">Export</button>
|
<button class="export-btn">Export</button>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<!-- 快捷键帮助模态框 -->
|
<!-- 快捷键帮助模态框 -->
|
||||||
<div
|
<div
|
||||||
@@ -991,6 +1016,7 @@ defineExpose({
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
color: #666;
|
color: #666;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zoom-hint {
|
.zoom-hint {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { fabric } from "fabric-with-all";
|
import { fabric } from "fabric-with-all";
|
||||||
import { findObjectById } from "../utils/helper";
|
import { findObjectById } from "../utils/helper";
|
||||||
|
import { createRasterizedImage } from "../utils/SelectionToImage";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 图片导出管理器
|
* 图片导出管理器
|
||||||
@@ -36,7 +37,6 @@ export class ExportManager {
|
|||||||
try {
|
try {
|
||||||
// 检查是否为红绿图模式
|
// 检查是否为红绿图模式
|
||||||
const isRedGreenMode = this.layerManager?.isInRedGreenMode?.() || false;
|
const isRedGreenMode = this.layerManager?.isInRedGreenMode?.() || false;
|
||||||
|
|
||||||
// 如果指定了具体图层ID,导出指定图层
|
// 如果指定了具体图层ID,导出指定图层
|
||||||
if (layerId) {
|
if (layerId) {
|
||||||
return this._exportSpecificLayer(
|
return this._exportSpecificLayer(
|
||||||
@@ -220,7 +220,8 @@ export class ExportManager {
|
|||||||
return this._exportWithCanvasSize(
|
return this._exportWithCanvasSize(
|
||||||
objectsToExport,
|
objectsToExport,
|
||||||
expPicType,
|
expPicType,
|
||||||
restoreOpacityInRedGreen
|
restoreOpacityInRedGreen,
|
||||||
|
this.canvas.clipPath
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,6 +251,14 @@ export class ExportManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (layer.fabricObject) {
|
||||||
|
// 通过ID在画布中查找真实对象
|
||||||
|
const realObj = this._findRealObjectById(layer.fabricObject.id);
|
||||||
|
if (realObj && realObj.visible !== false) {
|
||||||
|
realObjects.push(realObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 递归收集子图层的对象
|
// 递归收集子图层的对象
|
||||||
if (layer.children && layer.children.length > 0) {
|
if (layer.children && layer.children.length > 0) {
|
||||||
for (const childLayer of layer.children) {
|
for (const childLayer of layer.children) {
|
||||||
@@ -569,13 +578,30 @@ export class ExportManager {
|
|||||||
async _exportWithCanvasSize(
|
async _exportWithCanvasSize(
|
||||||
objectsToExport,
|
objectsToExport,
|
||||||
expPicType,
|
expPicType,
|
||||||
restoreOpacityInRedGreen
|
restoreOpacityInRedGreen,
|
||||||
|
maskObject
|
||||||
) {
|
) {
|
||||||
// 使用当前画布尺寸
|
// 使用当前画布尺寸
|
||||||
const canvasWidth = this.canvas.width;
|
// const canvasWidth =
|
||||||
const canvasHeight = this.canvas.height;
|
// this.canvasManager?.canvasWidth?.value || this.canvas.width;
|
||||||
|
// const canvasHeight =
|
||||||
|
// this.canvasManager?.canvasHeight?.value || this.canvas.height;
|
||||||
|
|
||||||
console.log(`普通模式导出,画布尺寸: ${canvasWidth}x${canvasHeight}`);
|
// console.log(`普通模式导出,画布尺寸: ${canvasWidth}x${canvasHeight}`);
|
||||||
|
|
||||||
|
// 使用图层栅格化的方法导出图片
|
||||||
|
const dataURL = await createRasterizedImage({
|
||||||
|
canvas: this.canvas,
|
||||||
|
fabricObjects: objectsToExport,
|
||||||
|
format: expPicType, // 导出格式
|
||||||
|
isReturenDataURL: true, // 返回数据URL
|
||||||
|
maskObject: maskObject ?? null, // 使用裁剪对象
|
||||||
|
trimWhitespace: true, // 裁剪空白
|
||||||
|
trimPadding: 0, // 裁剪边距
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("导出图片数据URL:", dataURL);
|
||||||
|
return dataURL;
|
||||||
|
|
||||||
// 创建与画布相同尺寸的临时画布
|
// 创建与画布相同尺寸的临时画布
|
||||||
const scaleFactor = 2; // 高清导出
|
const scaleFactor = 2; // 高清导出
|
||||||
@@ -593,7 +619,7 @@ export class ExportManager {
|
|||||||
|
|
||||||
tempFabricCanvas.enableRetinaScaling = true;
|
tempFabricCanvas.enableRetinaScaling = true;
|
||||||
tempFabricCanvas.imageSmoothingEnabled = true;
|
tempFabricCanvas.imageSmoothingEnabled = true;
|
||||||
tempFabricCanvas.setZoom(scaleFactor);
|
tempFabricCanvas.setZoom(1);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 克隆并添加所有对象到临时画布
|
// 克隆并添加所有对象到临时画布
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { fabric } from "fabric-with-all";
|
import { fabric } from "fabric-with-all";
|
||||||
import { createLayer, LayerType, OperationType } from "../utils/layerHelper.js";
|
import { createLayer, LayerType, OperationType } from "../utils/layerHelper.js";
|
||||||
import { BatchInitializeRedGreenModeCommand } from "../commands/RedGreenCommands.js";
|
import { BatchInitializeRedGreenModeCommand } from "../commands/RedGreenCommands.js";
|
||||||
|
import { nextTick } from "vue";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 红绿图模式管理器
|
* 红绿图模式管理器
|
||||||
@@ -129,15 +130,17 @@ export class RedGreenModeManager {
|
|||||||
registerRedGreenMouseUpEvent() {
|
registerRedGreenMouseUpEvent() {
|
||||||
this.canvas.on("mouse:up", (event) => {
|
this.canvas.on("mouse:up", (event) => {
|
||||||
// 可以在这里添加更多逻辑,比如生成图片或更新状态
|
// 可以在这里添加更多逻辑,比如生成图片或更新状态
|
||||||
requestAnimationFrame(async () => {
|
nextTick(() => {
|
||||||
if (!this.isInitialized) {
|
requestAnimationFrame(async () => {
|
||||||
console.warn("红绿图模式未初始化,无法处理鼠标事件");
|
if (!this.isInitialized) {
|
||||||
return;
|
console.warn("红绿图模式未初始化,无法处理鼠标事件");
|
||||||
}
|
return;
|
||||||
if (this.onImageGenerated) {
|
}
|
||||||
const imageData = await this.canvasManager.exportImage();
|
if (this.onImageGenerated) {
|
||||||
this.onImageGenerated(imageData);
|
const imageData = await this.canvasManager.exportImage();
|
||||||
}
|
this.onImageGenerated(imageData);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,13 +76,9 @@ export class LayerSort {
|
|||||||
if (layer.isBackground && layer.fabricObject) {
|
if (layer.isBackground && layer.fabricObject) {
|
||||||
// 背景图层对象放在最底层
|
// 背景图层对象放在最底层
|
||||||
zIndexMap.set(layer.fabricObject.id, currentZIndex++);
|
zIndexMap.set(layer.fabricObject.id, currentZIndex++);
|
||||||
} else if (layer.isFixed && layer.fabricObjects) {
|
} else if (layer.isFixed && layer.fabricObject) {
|
||||||
// 固定图层对象
|
// 固定图层对象
|
||||||
layer.fabricObjects.forEach((obj) => {
|
zIndexMap.set(layer.fabricObject.id, currentZIndex++);
|
||||||
if (obj?.id) {
|
|
||||||
zIndexMap.set(obj.id, currentZIndex++);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (!layer.isBackground && !layer.isFixed) {
|
} else if (!layer.isBackground && !layer.isFixed) {
|
||||||
// 普通图层
|
// 普通图层
|
||||||
currentZIndex = this.processLayerObjects(
|
currentZIndex = this.processLayerObjects(
|
||||||
|
|||||||
@@ -207,6 +207,58 @@ export async function addImageToLayer({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从base64或者url加载图片并创建图层
|
||||||
|
* @param {str} imageUrl - base64字符串或图片URL
|
||||||
|
* @param {Object} layerManager - 图层管理器
|
||||||
|
* @param {Object} canvas - fabric.js画布实例
|
||||||
|
* @param {Object} options - 配置选项
|
||||||
|
* @returns {Promise<string>} 新图层ID的Promise
|
||||||
|
*/
|
||||||
|
export function loadImageUrlToLayer(
|
||||||
|
{ imageUrl, layerManager, canvas, toolManager },
|
||||||
|
options = {}
|
||||||
|
) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
if (!imageUrl || !layerManager || !canvas) {
|
||||||
|
reject(new Error("参数无效"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 查找背景图层以获取尺寸
|
||||||
|
const bgLayer = layerManager.layers.value.find(
|
||||||
|
(layer) => layer.isBackground
|
||||||
|
);
|
||||||
|
|
||||||
|
// 设置最大宽高为背景图层的尺寸
|
||||||
|
const maxWidth = bgLayer?.canvasWidth || canvas.width;
|
||||||
|
const maxHeight = bgLayer?.canvasHeight || canvas.height;
|
||||||
|
|
||||||
|
// 加载并处理图片
|
||||||
|
const fabricImage = await loadImage(imageUrl, {
|
||||||
|
maxWidth: maxWidth * 0.8, // 默认图片最大宽度为背景宽度的80%
|
||||||
|
maxHeight: maxHeight * 0.8, // 默认图片最大高度为背景高度的80%
|
||||||
|
canvasWidth: canvas.width,
|
||||||
|
canvasHeight: canvas.height,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建图片图层
|
||||||
|
const layerId = await createImageLayer({
|
||||||
|
layerManager,
|
||||||
|
fabricImage,
|
||||||
|
toolManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve(layerId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("处理图片失败:", error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从File对象加载图片并创建图层
|
* 从File对象加载图片并创建图层
|
||||||
* @param {File} file - 文件对象
|
* @param {File} file - 文件对象
|
||||||
|
|||||||
362
src/component/Canvas/CanvasEditor/utils/selectionToImage.js
Normal file
362
src/component/Canvas/CanvasEditor/utils/selectionToImage.js
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
// 栅格化帮助
|
||||||
|
import { fabric } from "fabric-with-all";
|
||||||
|
/**
|
||||||
|
* 创建栅格化图像
|
||||||
|
* 使用增强版栅格化方法,不受原始画布变换影响
|
||||||
|
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export const createRasterizedImage = async ({
|
||||||
|
canvas, // 画布对象 必填
|
||||||
|
fabricObjects = [], // 要栅格化的对象列表 - 按顺序 必填
|
||||||
|
maskObject = null, // 用于裁剪的对象 - 可选
|
||||||
|
clipPath = null, // 裁剪路径对象 - 可选,优先级高于maskObject
|
||||||
|
trimWhitespace = true, // 是否裁剪空白区域
|
||||||
|
trimPadding = 1, // 裁剪边距
|
||||||
|
quality = 1.0, // 图像质量
|
||||||
|
format = "png", // 图像格式
|
||||||
|
scaleFactor = 1, // 高清倍数 - 默认是画布的高清倍数
|
||||||
|
isReturenDataURL = false, // 是否返回DataURL而不是fabric.Image对象
|
||||||
|
} = {}) => {
|
||||||
|
try {
|
||||||
|
console.log(`📊 开始栅格化 ${fabricObjects.length} 个对象`);
|
||||||
|
|
||||||
|
// 确保有对象需要栅格化
|
||||||
|
if (fabricObjects.length === 0) {
|
||||||
|
console.warn("⚠️ 没有对象需要栅格化,返回空图像");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理裁剪对象,优先使用clipPath
|
||||||
|
const clippingObject = clipPath || maskObject;
|
||||||
|
|
||||||
|
// 高清倍数
|
||||||
|
const currentZoom = canvas.getZoom?.() || 1;
|
||||||
|
|
||||||
|
scaleFactor = Math.max(
|
||||||
|
scaleFactor || canvas?.getRetinaScaling?.(),
|
||||||
|
currentZoom
|
||||||
|
);
|
||||||
|
|
||||||
|
scaleFactor = Math.min(scaleFactor, 3); // 最大不能大于3
|
||||||
|
|
||||||
|
console.log(`高清倍数: ${scaleFactor}, 当前缩放: ${currentZoom}`);
|
||||||
|
|
||||||
|
// 计算绝对边界框(原始尺寸)和相对边界框(当前缩放后的尺寸)
|
||||||
|
const { absoluteBounds, relativeBounds } = calculateBounds(fabricObjects);
|
||||||
|
|
||||||
|
console.log("📏 绝对边界框:", absoluteBounds);
|
||||||
|
console.log("📏 相对边界框:", relativeBounds);
|
||||||
|
|
||||||
|
// 使用绝对边界框创建高质量的离屏渲染
|
||||||
|
const rasterizedImage = await createOffscreenRasterization({
|
||||||
|
canvas,
|
||||||
|
objects: fabricObjects,
|
||||||
|
absoluteBounds,
|
||||||
|
relativeBounds,
|
||||||
|
scaleFactor,
|
||||||
|
clippingObject,
|
||||||
|
trimWhitespace,
|
||||||
|
trimPadding,
|
||||||
|
quality,
|
||||||
|
format,
|
||||||
|
currentZoom,
|
||||||
|
isReturenDataURL,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!rasterizedImage) {
|
||||||
|
console.warn("⚠️ 栅格化图像创建失败,返回空图像");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isReturenDataURL) {
|
||||||
|
console.log("✅ 栅格化图像创建成功,返回DataURL");
|
||||||
|
return rasterizedImage; // 返回DataURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置栅格化图像的属性
|
||||||
|
if (rasterizedImage) {
|
||||||
|
rasterizedImage.set({
|
||||||
|
selectable: true,
|
||||||
|
evented: true,
|
||||||
|
hasControls: true,
|
||||||
|
hasBorders: true,
|
||||||
|
custom: {
|
||||||
|
type: "rasterized",
|
||||||
|
rasterizedAt: new Date().toISOString(),
|
||||||
|
objectCount: fabricObjects.length,
|
||||||
|
absoluteBounds,
|
||||||
|
relativeBounds,
|
||||||
|
originalZoom: currentZoom,
|
||||||
|
hasClipping: !!clippingObject,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ 栅格化图像创建完成`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rasterizedImage;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("创建栅格化图像失败:", error);
|
||||||
|
throw new Error(`栅格化失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算对象的绝对边界框和相对边界框
|
||||||
|
* @param {Array} fabricObjects fabric对象数组
|
||||||
|
* @returns {Object} 包含绝对边界框和相对边界框的对象
|
||||||
|
*/
|
||||||
|
const calculateBounds = (fabricObjects) => {
|
||||||
|
if (fabricObjects.length === 0) {
|
||||||
|
console.warn("⚠️ 没有对象,无法计算边界框");
|
||||||
|
return { absoluteBounds: null, relativeBounds: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
let absoluteBounds = null;
|
||||||
|
let relativeBounds = null;
|
||||||
|
|
||||||
|
fabricObjects.forEach((obj, index) => {
|
||||||
|
// 获取相对边界框(考虑画布缩放和平移)
|
||||||
|
const relativeBound = obj.getBoundingRect();
|
||||||
|
// 获取绝对边界框(原始大小和位置)
|
||||||
|
const absoluteBound = obj.getBoundingRect(true, true);
|
||||||
|
|
||||||
|
console.log(`对象 ${obj.id || index} 边界框比较:`, {
|
||||||
|
relative: relativeBound,
|
||||||
|
absolute: absoluteBound,
|
||||||
|
scaleX: obj.scaleX,
|
||||||
|
scaleY: obj.scaleY,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算绝对边界框的累积范围
|
||||||
|
if (!absoluteBounds) {
|
||||||
|
absoluteBounds = { ...absoluteBound };
|
||||||
|
} else {
|
||||||
|
const right = Math.max(
|
||||||
|
absoluteBounds.left + absoluteBounds.width,
|
||||||
|
absoluteBound.left + absoluteBound.width
|
||||||
|
);
|
||||||
|
const bottom = Math.max(
|
||||||
|
absoluteBounds.top + absoluteBounds.height,
|
||||||
|
absoluteBound.top + absoluteBound.height
|
||||||
|
);
|
||||||
|
|
||||||
|
absoluteBounds.left = Math.min(absoluteBounds.left, absoluteBound.left);
|
||||||
|
absoluteBounds.top = Math.min(absoluteBounds.top, absoluteBound.top);
|
||||||
|
absoluteBounds.width = right - absoluteBounds.left;
|
||||||
|
absoluteBounds.height = bottom - absoluteBounds.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算相对边界框的累积范围
|
||||||
|
if (!relativeBounds) {
|
||||||
|
relativeBounds = { ...relativeBound };
|
||||||
|
} else {
|
||||||
|
const right = Math.max(
|
||||||
|
relativeBounds.left + relativeBounds.width,
|
||||||
|
relativeBound.left + relativeBound.width
|
||||||
|
);
|
||||||
|
const bottom = Math.max(
|
||||||
|
relativeBounds.top + relativeBounds.height,
|
||||||
|
relativeBound.top + relativeBound.height
|
||||||
|
);
|
||||||
|
|
||||||
|
relativeBounds.left = Math.min(relativeBounds.left, relativeBound.left);
|
||||||
|
relativeBounds.top = Math.min(relativeBounds.top, relativeBound.top);
|
||||||
|
relativeBounds.width = right - relativeBounds.left;
|
||||||
|
relativeBounds.height = bottom - relativeBounds.top;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { absoluteBounds, relativeBounds };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建离屏栅格化渲染
|
||||||
|
* @param {Object} options 渲染选项
|
||||||
|
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
|
||||||
|
*/
|
||||||
|
const createOffscreenRasterization = async ({
|
||||||
|
canvas,
|
||||||
|
objects,
|
||||||
|
absoluteBounds,
|
||||||
|
relativeBounds,
|
||||||
|
scaleFactor,
|
||||||
|
clippingObject,
|
||||||
|
trimWhitespace,
|
||||||
|
trimPadding,
|
||||||
|
quality,
|
||||||
|
format,
|
||||||
|
currentZoom,
|
||||||
|
isReturenDataURL,
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
// 创建离屏画布,使用绝对尺寸以保证高质量
|
||||||
|
const offscreenCanvas = new fabric.StaticCanvas();
|
||||||
|
|
||||||
|
// 如果有裁剪对象,使用裁剪对象的边界框
|
||||||
|
let renderBounds = absoluteBounds;
|
||||||
|
if (clippingObject) {
|
||||||
|
const clippingBounds = clippingObject.getBoundingRect(true, true);
|
||||||
|
console.log("🎯 使用裁剪对象边界框:", clippingBounds);
|
||||||
|
renderBounds = clippingBounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置离屏画布尺寸,并应用高清倍数
|
||||||
|
const canvasWidth = Math.ceil(renderBounds.width * scaleFactor);
|
||||||
|
const canvasHeight = Math.ceil(renderBounds.height * scaleFactor);
|
||||||
|
|
||||||
|
offscreenCanvas.setDimensions({
|
||||||
|
width: canvasWidth,
|
||||||
|
height: canvasHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`🎨 离屏画布尺寸: ${canvasWidth}x${canvasHeight}, 缩放: ${scaleFactor}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 克隆对象到离屏画布
|
||||||
|
const clonedObjects = [];
|
||||||
|
for (const obj of objects) {
|
||||||
|
const clonedObj = await cloneObjectAsync(obj);
|
||||||
|
|
||||||
|
// 调整对象位置,相对于渲染边界框的左上角
|
||||||
|
clonedObj.set({
|
||||||
|
left: clonedObj.left - renderBounds.left,
|
||||||
|
top: clonedObj.top - renderBounds.top,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果有裁剪对象,为每个对象设置裁剪路径
|
||||||
|
if (clippingObject) {
|
||||||
|
const clippingPath = await cloneObjectAsync(clippingObject);
|
||||||
|
clippingPath.set({
|
||||||
|
left: clippingPath.left - renderBounds.left,
|
||||||
|
top: clippingPath.top - renderBounds.top,
|
||||||
|
fill: "",
|
||||||
|
stroke: "",
|
||||||
|
absolutePositioned: true,
|
||||||
|
});
|
||||||
|
clonedObj.set({
|
||||||
|
clipPath: clippingPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clonedObjects.push(clonedObj);
|
||||||
|
offscreenCanvas.add(clonedObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染离屏画布
|
||||||
|
offscreenCanvas.renderAll();
|
||||||
|
|
||||||
|
// 生成图像数据
|
||||||
|
const dataURL = offscreenCanvas.toDataURL({
|
||||||
|
format,
|
||||||
|
quality,
|
||||||
|
multiplier: 1, // 已经通过画布尺寸处理了高清倍数
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isReturenDataURL) {
|
||||||
|
return dataURL; // 如果需要返回DataURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理离屏画布
|
||||||
|
offscreenCanvas.dispose();
|
||||||
|
|
||||||
|
// 创建fabric.Image对象
|
||||||
|
const fabricImage = await createFabricImageFromDataURL(dataURL);
|
||||||
|
|
||||||
|
// 设置图像位置为裁剪区域的位置
|
||||||
|
fabricImage.set({
|
||||||
|
left: renderBounds.left + renderBounds.width / 2,
|
||||||
|
top: renderBounds.top + renderBounds.height / 2,
|
||||||
|
originX: "center",
|
||||||
|
originY: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
return fabricImage;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("离屏栅格化失败:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步克隆fabric对象
|
||||||
|
* @param {fabric.Object} obj 要克隆的对象
|
||||||
|
* @returns {Promise<fabric.Object>} 克隆的对象
|
||||||
|
*/
|
||||||
|
const cloneObjectAsync = (obj) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
obj.clone((cloned) => {
|
||||||
|
if (cloned) {
|
||||||
|
resolve(cloned);
|
||||||
|
} else {
|
||||||
|
reject(new Error("对象克隆失败"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从DataURL创建fabric.Image对象
|
||||||
|
* @param {string} dataURL 图像数据URL
|
||||||
|
* @returns {Promise<fabric.Image>} fabric图像对象
|
||||||
|
*/
|
||||||
|
const createFabricImageFromDataURL = (dataURL) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fabric.Image.fromURL(dataURL, (img) => {
|
||||||
|
if (img) {
|
||||||
|
resolve(img);
|
||||||
|
} else {
|
||||||
|
reject(new Error("无法从DataURL创建图像"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算栅格化图像在当前画布上的正确变换
|
||||||
|
* @param {Object} params 计算参数
|
||||||
|
* @returns {Object} 变换属性对象
|
||||||
|
*/
|
||||||
|
const calculateImageTransform = ({
|
||||||
|
absoluteBounds,
|
||||||
|
relativeBounds,
|
||||||
|
currentZoom,
|
||||||
|
scaleFactor,
|
||||||
|
imageWidth,
|
||||||
|
imageHeight,
|
||||||
|
}) => {
|
||||||
|
// 计算缩放比例:相对尺寸 / 绝对尺寸
|
||||||
|
const scaleX = relativeBounds.width / absoluteBounds.width;
|
||||||
|
const scaleY = relativeBounds.height / absoluteBounds.height;
|
||||||
|
|
||||||
|
// 由于我们生成的图像是基于绝对尺寸的高清版本,需要考虑scaleFactor
|
||||||
|
const finalScaleX = scaleX / scaleFactor;
|
||||||
|
const finalScaleY = scaleY / scaleFactor;
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: relativeBounds.left + relativeBounds.width / 2, // 设置为中心点
|
||||||
|
top: relativeBounds.top + relativeBounds.height / 2, // 设置为中心点
|
||||||
|
// scaleX: finalScaleX,
|
||||||
|
// scaleY: finalScaleY,
|
||||||
|
originX: "center",
|
||||||
|
originY: "center",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用遮罩到画布(已弃用,使用clipPath方式)
|
||||||
|
* @param {fabric.Canvas} canvas 目标画布
|
||||||
|
* @param {fabric.Object} maskObject 遮罩对象
|
||||||
|
* @param {Object} bounds 边界框
|
||||||
|
*/
|
||||||
|
const applyMaskToCanvas = async (canvas, maskObject, bounds) => {
|
||||||
|
// 此方法已被clipPath方式替代
|
||||||
|
console.log("⚠️ applyMaskToCanvas已被clipPath方式替代");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getObjectsBounds = (fabricObjects) => {
|
||||||
|
const { absoluteBounds } = calculateBounds(fabricObjects);
|
||||||
|
return absoluteBounds;
|
||||||
|
};
|
||||||
@@ -50,12 +50,18 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
dts: "src/auto-imports.d.ts",
|
dts: "src/auto-imports.d.ts",
|
||||||
|
// 添加 ESLint 支持
|
||||||
|
eslintrc: {
|
||||||
|
enabled: true,
|
||||||
|
filepath: "./.eslintrc-auto-import.json",
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
createSvgIconsPlugin({
|
createSvgIconsPlugin({
|
||||||
// 指定需要缓存的图标文件夹
|
// 指定需要缓存的图标文件夹
|
||||||
iconDirs: [path.resolve(process.cwd(), "src/assets/icons")],
|
iconDirs: [path.resolve(process.cwd(), "src/assets/icons")],
|
||||||
// 指定symbolId格式
|
// 指定symbolId格式
|
||||||
symbolId: "icon-[dir]-[name]",
|
symbolId: "icon-[dir]-[name]",
|
||||||
|
inject: "body-last", // 注入位置优化
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
css: {
|
css: {
|
||||||
@@ -78,6 +84,9 @@ export default defineConfig(({ mode }) => {
|
|||||||
port: mode === "production" ? 8060 : 10086, // 根据环境设置端口
|
port: mode === "production" ? 8060 : 10086, // 根据环境设置端口
|
||||||
open: true, // 自动打开浏览器
|
open: true, // 自动打开浏览器
|
||||||
strictPort: false, // 如果端口已被占用,则尝试下一个可用端口
|
strictPort: false, // 如果端口已被占用,则尝试下一个可用端口
|
||||||
|
hmr: {
|
||||||
|
overlay: true,
|
||||||
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
target: "http://192.168.1.7:5567",
|
target: "http://192.168.1.7:5567",
|
||||||
@@ -100,17 +109,49 @@ export default defineConfig(({ mode }) => {
|
|||||||
sourcemap: false, // 对应vue.config.js中的productionSourceMap: false
|
sourcemap: false, // 对应vue.config.js中的productionSourceMap: false
|
||||||
outDir: "dist",
|
outDir: "dist",
|
||||||
assetsDir: "assets",
|
assetsDir: "assets",
|
||||||
// 分包策略
|
target: "es2015", // 目标浏览器版本
|
||||||
|
minify: "terser", // 使用terser进行压缩
|
||||||
|
cssCodeSplit: true,
|
||||||
|
// reportCompressedSize: false, // 提升构建速度
|
||||||
|
terserOptions: {
|
||||||
|
compress: {
|
||||||
|
drop_console: true, // 删除console
|
||||||
|
drop_debugger: true, // 删除debugger
|
||||||
|
},
|
||||||
|
format: {
|
||||||
|
comments: false, // 删除注释
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 优化分包策略
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
manualChunks: {
|
manualChunks: {
|
||||||
vendor: ["vue", "vue-router", "vuex"],
|
vendor: ["vue", "vue-router"],
|
||||||
antd: ["ant-design-vue"],
|
antd: ["ant-design-vue"],
|
||||||
|
elementPlus: ["element-plus"],
|
||||||
utils: ["axios", "lodash-es"],
|
utils: ["axios", "lodash-es"],
|
||||||
|
// 添加更细粒度的分包
|
||||||
|
icons: ["@ant-design/icons-vue"],
|
||||||
},
|
},
|
||||||
|
// 优化文件命名
|
||||||
|
chunkFileNames: "js/[name]-[hash].js",
|
||||||
|
entryFileNames: "js/[name]-[hash].js",
|
||||||
|
assetFileNames: "[ext]/[name]-[hash].[ext]",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// 优化依赖预构建
|
||||||
|
optimizeDeps: {
|
||||||
|
include: [
|
||||||
|
"vue",
|
||||||
|
"vue-router",
|
||||||
|
"ant-design-vue",
|
||||||
|
"element-plus",
|
||||||
|
"axios",
|
||||||
|
"lodash-es",
|
||||||
|
],
|
||||||
|
exclude: ["@iconify/json"], // 排除大型JSON文件
|
||||||
|
},
|
||||||
// 定义全局常量替换
|
// 定义全局常量替换
|
||||||
define: {
|
define: {
|
||||||
__VUE_OPTIONS_API__: true,
|
__VUE_OPTIONS_API__: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user