Files
aida_front/src/component/Canvas/CanvasEditor/index.vue
2025-12-17 14:49:17 +08:00

1671 lines
46 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import {
ref,
onMounted,
onBeforeUnmount,
defineAsyncComponent,
shallowRef,
provide,
defineExpose,
nextTick,
watchEffect,
} from "vue";
import { CanvasManager } from "./managers/CanvasManager";
import { LayerManager } from "./managers/LayerManager";
import { CommandManager } from "./managers/command/CommandManager";
import { KeyboardManager } from "./managers/events/KeyboardManager.js";
import CanvasConfig from "./config/canvasConfig.js";
import { LiquifyManager } from "./managers/liquify/LiquifyManager";
import { SelectionManager } from "./managers/selection/SelectionManager";
import { RedGreenModeManager } from "./managers/RedGreenModeManager";
import texturePresetManager from "./managers/brushes/TexturePresetManager";
import { BrushStore } from "./store/BrushStore";
import cuowuImg from "@/assets/images/homePage/cuowu.svg";
import { Https } from "@/tool/https";
import SelectImages from "@/component/common/SelectImages.vue";
import CropImage from "./components/CropImage.vue";
import { UrlToFile } from "@/tool/util";
// import { MinimapManager } from "./managers/minimap/MinimapManager";
// 导入封装的组件
import ToolsSidebar from "./components/ToolsSidebar.vue";
import HeaderMenu from "./components/HeaderMenu.vue";
import LayersPanel from "./components/LayersPanel/LayersPanel.vue";
import BrushControlPanel from "./components/BrushControlPanel.vue";
import TextEditorPanel from "./components/TextEditorPanel.vue"; // 引入文本编辑面板
import LiquifyPanel from "./components/LiquifyPanel.vue"; // 引入液化编辑面板
import SelectMenuPanel from "./components/SelectMenuPanel.vue"; // 引入选择工具菜单组件
import SelectionPanel from "./components/SelectionPanel.vue"; // 引入选区面板
import { LayerType, OperationType } from "./utils/layerHelper.js";
import { ToolManager } from "./managers/ToolManager.js";
import { fabric } from "fabric-with-all";
import {
uploadImageAndCreateLayer,
loadImageUrlToLayer,
loadImage,
} from "./utils/imageHelper.js";
// import MinimapPanel from "./components/MinimapPanel.vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const KeyboardShortcutHelp = defineAsyncComponent(() =>
import("./components/KeyboardShortcutHelp.vue")
);
const emit = defineEmits([
"trigger-red-green-mouseup", // 红绿图模式鼠标抬起事件
"changeCanvas", // 画布变更事件
"canvasInit", // 画布初始化事件
"trigger-library", // 触发打开Library选择图片事件
]);
const props = defineProps({
canvasJSON: {
type: [Object, String],
default: "", // 默认空
},
config: {
type: Object,
default: () => CanvasConfig, // 默认配置
},
showLayersPanel: {
type: Boolean,
default: true, // 是否显示图层面板
},
enabledRedGreenMode: {
type: Boolean,
default: false, // 是否启用红绿图模式
},
clothingImageUrl: {
type: String,
default: "", // 衣服底图URL
},
redGreenImageUrl: {
type: String,
default: "", // 红绿图URL
},
clothingImageOpts: {
type: Object,
default: () => {
return {
scaleX: 1,
scaleY: 1,
};
},
},
isFixedErasable: {
type: Boolean,
default: false, // 是否允许擦除固定图层
},
isBackgroundErasable: {
type: Boolean,
default: false, // 是否允许擦除背景图层
},
isBackgroundChangeable: {
type: Boolean,
default: true, // 是否允许修改背景图层
},
showFixedLayer: {
type: Boolean,
default: false, // 是否显示固定图层
},
isGeneral: {
// 从generalMiniCanvas来的
type: Boolean,
default: false,
},
isEdit: {
// 从design点击喜欢过的图片,再点击顶部的编辑图标
type: Boolean,
default: false,
},
hideCanvas: {
type: Boolean,
default: false, // 是否隐藏画布-隐藏关闭部分功能
},
});
// 引用和状态
const canvasRef = ref(null);
const canvasContainerRef = shallowRef(null);
const imageUploadRef = ref(null);
const currentZoom = ref(100);
const appContainerRef = ref(null);
// 画布设置
const canvasWidth = ref(props.config.width);
const canvasHeight = ref(props.config.height);
const canvasColor = ref(props.config.backgroundColor);
// const layerWidth = ref(CanvasConfig.layerWidth);
const brushSize = ref(CanvasConfig.brushSize); // 画笔大小
const canvasManagerLoaded = ref(false); // 画布是否加载完成
// 红绿图模式状态
const isRedGreenMode = ref(false);
const isShowLayerPanel = ref(false); // 是否显示图层面板
provide("isShowLayerPanel", isShowLayerPanel); // 提供红绿图模式状态给子组件
// 小地图设置
// const minimapEnabled = ref(true);
// const minimapManager = ref(null);
// 图层和元素管理
const layers = ref([]);
const activeLayerId = ref(null);
const activeElementId = ref(null);
const lastSelectLayerId = ref(null); // 最后选择的图层ID
// 当前选择的工具
const activeTool = ref(CanvasConfig.defaultTool); // 默认工具
//监听画布元素宽度是否发生变化
let observer = null;
let observerTime = null; //加入防抖
// 管理器实例
let canvasManager = null;
let layerManager = null;
let commandManager = null;
let keyboardManager = null;
let toolManager = null;
let liquifyManager = null;
let selectionManager = null;
let redGreenModeManager = null;
// 快捷键帮助模态框状态
const showShortcutHelp = ref(false);
function toggleShortcutHelp() {
showShortcutHelp.value = !showShortcutHelp.value;
}
watch(()=>props.hideCanvas, (newVal)=>{
console.log("==========是否隐藏画布",newVal)
if(newVal){
keyboardManager?.removeEvents()
}else {
keyboardManager?.init()
}
})
// 工具选择处理
function handleToolSelect(tool) {
activeTool.value = tool;
// toolManager.setActiveTool(tool); // 更新工具管理器中的当前工具 普通模式,不可撤回操作
toolManager.setToolWithCommand(tool, {
undoable: props.enabledRedGreenMode ? false : true, // 普通模式下工具选择不可撤销
}); // 命令模式 可撤回操作
}
// 触发组件初始化事件
function handleCanvasInit(isLoadJson = false) {
emit("canvasInit", {
isLoadJson: isLoadJson ?? !!props.canvasJSON, // 是否加载了JSON数据
isRedGreenMode: isRedGreenMode.value,
layers,
activeLayerId,
canvasManager,
layerManager,
commandManager,
toolManager,
keyboardManager,
liquifyManager,
selectionManager,
redGreenModeManager,
});
}
function toggleMinimap(enabled) {
// minimapEnabled.value = enabled;
// if (minimapManager.value) {
// minimapManager.value.setVisibility(enabled);
// }
}
// 初始化画布
onMounted(async () => {
// 设置BrushStore的全局引用供BaseBrush使用
if (typeof window !== "undefined") {
window.BrushStore = BrushStore;
}
// 如果启用了红绿图模式,设置画布大小为默认值
if (props.enabledRedGreenMode) {
canvasHeight.value = canvasContainerRef.value.clientWidth;
canvasWidth.value = canvasContainerRef.value.clientHeight;
}
// 创建管理器实例
canvasManager = new CanvasManager(canvasRef.value, {
width: canvasContainerRef.value.clientWidth,
height: canvasContainerRef.value.clientHeight,
// backgroundColor: canvasColor.value,
currentZoom,
layers,
lastSelectLayerId,
canvasWidth,
canvasHeight,
canvasColor,
enabledRedGreenMode: props.enabledRedGreenMode,
isFixedErasable: props.isFixedErasable,
});
canvasManager.canvas.activeLayerId = activeLayerId;
canvasManager.activeLayerId = activeLayerId;
canvasManager.canvas.activeElementId = activeElementId;
// 创建命令管理器
commandManager = new CommandManager({
canvas: canvasManager.canvas,
autoSaveState: true,
});
// 创建图层管理器
layerManager = new LayerManager({
canvas: canvasManager.canvas,
canvasWidth: canvasWidth.value,
canvasHeight: canvasHeight.value,
backgroundColor: canvasColor,
isRedGreenMode: props.enabledRedGreenMode,
lastSelectLayerId,
layers,
activeLayerId,
canvasManager, // 添加对 canvasManager 的引用
commandManager, // 添加对命令管理器的引用
t, // 国际化函数
});
// commandManager.setLayerManager(layerManager); // 设置命令管理器需要访问的图层数据
// 设置缩略图管理器需要访问的图层数据
// canvasManager.layers = layers;
// 创建工具管理器实例
toolManager = new ToolManager({
canvas: canvasManager.canvas, // fabric.js 画布实例
commandManager, // 命令管理器实例,用于撤销/重做
canvasManager, // 画布管理器实例
layerManager,
activeTool, // 响应式引用,存储当前选中的工具
brushSize: brushSize.value, // 可选,初始画笔大小
t, // 国际化函数
});
// 初始化文本编辑功能
toolManager.setupTextEditingEvents();
toolManager.setFileUploadHandler(triggerImageUpload); // 设置快捷图片上传处理函数
layerManager.setToolManager(toolManager); // 将工具管理器传递给图层管理器
canvasManager.setToolManager(toolManager); // 将工具管理器传递给画布管理器
canvasManager.setLayerManager(layerManager);
canvasManager.setCommandManager(commandManager); // 将命令管理器传递给画布管理器
// 初始化快捷键管理器
keyboardManager = new KeyboardManager({
canvas: canvasManager.canvas,
commandManager,
layerManager,
toolManager,
isRedGreenMode,
pasteText: (text) => {
// console.log("粘贴的文本:", text);
handleAddText(text);
},
pasteImage: (file) => {
// console.log("粘贴的图片:", file);
uploadImageAndCreateLayer({
file,
layerManager,
toolManager,
canvas: canvasManager.canvas,
});
},
});
// 绑定快捷键事件
keyboardManager.init();
// 绑定画布操作事件
canvasManager.setupCanvasEvents(activeElementId, layerManager);
canvasManager.setupCanvasInitEvent(handleCanvasInit); // 绑定画布初始化事件
provide("canvasManager", canvasManager); // 提供给子组件使用
provide("layerManager", layerManager); // 提供给子组件使用
provide("commandManager", commandManager); // 提供给子组件使用
provide("toolManager", toolManager); // 提供给子组件使用
provide("keyboardManager", keyboardManager); // 提供给子组件使用
provide("activeTool", activeTool); // 提供给子组件使用
provide("liquifyManager", () => liquifyManager); // 提供液化管理器
provide("texturePresetManager", texturePresetManager); // 提供纹理预设管理器
provide("layers", layers); // 提供图层数据
provide("lastSelectLayerId", lastSelectLayerId); // 提供最后选择的图层ID
// 初始化网格设置
// toggleGridVisibility(gridEnabled.value);
// 初始化小地图
// minimapManager.value = new MinimapManager(canvasManager.canvas);
// 初始化液化管理器
liquifyManager = new LiquifyManager({
canvas: canvasManager.canvas,
layerManager,
});
// 将liquifyManager设置到canvasManager中确保ToolManager能访问到它
canvasManager.setLiquifyManager(liquifyManager);
// 初始化选区管理器
selectionManager = new SelectionManager({
canvas: canvasManager.canvas,
layerManager,
});
canvasManager.setSelectionManager(selectionManager);
if (props.canvasJSON) {
// 如果传入了初始JSON数据加载到画布上
if (typeof props.canvasJSON === "string") {
try {
await canvasManager.loadJSON(props.canvasJSON);
} catch (error) {
console.error("加载画布JSON失败:", error);
// 初始化图层 - 确保创建背景层
await layerManager.initializeLayers();
}
} else if (typeof props.canvasJSON === "object") {
await canvasManager.loadJSON(JSON.stringify(props.canvasJSON));
}
} else {
// 初始化图层 - 确保创建背景层
await layerManager.initializeLayers();
}
if (
props.enabledRedGreenMode &&
props.clothingImageUrl &&
props.redGreenImageUrl
) {
canvasManager.canvas.fill = "#fff"; // 设置画布背景色为白色 // 初始化红绿图模式管理器
redGreenModeManager = new RedGreenModeManager({
canvas: canvasManager.canvas,
canvasManager,
layerManager,
toolManager,
commandManager,
clothingImageOpts: props.clothingImageOpts,
});
canvasManager.setRedGreenModeManager(redGreenModeManager);
// 如果提供了图片URL立即初始化红绿图模式
if (props.clothingImageUrl && props.redGreenImageUrl) {
try {
await redGreenModeManager.initialize({
clothingImageUrl: props.clothingImageUrl,
redGreenImageUrl: props.redGreenImageUrl,
onImageGenerated: (imageData) => {
console.log("红绿图生成:", imageData);
// 这里可以添加图片生成后的回调处理
emit("trigger-red-green-mouseup", imageData);
},
});
// 设置红绿图模式状态
isRedGreenMode.value = true;
console.log("红绿图模式已自动启用");
} catch (error) {
console.error("红绿图模式初始化失败:", error);
}
}
// 初始设置
handleWindowResize(); // 设置画布大小
} else if (!isRedGreenMode.value && props.clothingImageUrl) {
try {
await canvasManager?.changeFixedImage?.(props.clothingImageUrl, {
undoable: false, // 不可撤销操作
...(props?.clothingImageOpts || {}),
});
} catch (error) {
console.error("更换底图失败:", error);
}
canvasManager?.centerBackgroundLayer?.(
canvasManager.canvas.width,
canvasManager.canvas.height
);
}
// // 设置固定图层是否可擦除
// canvasManager.setFixedLayerErasable({
// type: "isFixed",
// flag: !props.isFixedErasable, // 设置操作类型为可擦除
// });
// // 设置背景图层是否可擦除
// canvasManager.setFixedLayerErasable({
// type: "isBackground",
// flag: !props.isBackgroundErasable, // 设置操作类型为可擦除
// });
canvasManagerLoaded.value = true;
// 添加删除按钮
// if(!fabric.Object.prototype.controls.deleteControl)addRemoveBtn(removeLayer)
addRemoveBtn(removeLayer);
// 触发组件初始化事件
nextTick(() => {
// 确保所有依赖都已加载完成
handleCanvasInit();
setTimeout(() => {
// 初始状态下生成所有预览图
canvasManager?.updateAllThumbnails?.();
}, 700);
});
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);
if (props.config.initZoom) {
const width = canvasManager.width;
const height = canvasManager.height;
const cwidth = props.config.width;
const cheight = props.config.height;
let zoom = Math.min(1, width / cwidth, height / cheight);
if (zoom < 1) zoom -= 0.05;
setZoom(zoom); // 设置画布缩放
}
});
watchEffect(() => {
// 设置固定图层是否可擦除
if (canvasManagerLoaded.value) {
canvasManager?.setFixedLayerErasable({
type: "isFixed",
flag: !props.isFixedErasable, // 设置操作类型为可擦除
});
// 设置背景图层是否可擦除
canvasManager?.setFixedLayerErasable({
type: "isBackground",
flag: !props.isBackgroundErasable, // 设置操作类型为可擦除
});
}
});
onBeforeUnmount(() => {
// if (import.meta.hot) {
// // 热更新
// console.log("onBeforeUnmount 开发环境热更新不卸载组件...");
// return; // 开发环境下不卸载组件
// }
console.log("onBeforeUnmount 组件卸载,清理资源...");
canvasManager?.dispose?.();
commandManager?.dispose?.();
layerManager?.dispose?.();
keyboardManager?.dispose?.();
toolManager?.dispose?.();
liquifyManager?.dispose?.();
selectionManager?.dispose?.();
redGreenModeManager?.dispose?.();
// minimapManager?.dispose?.();
canvasManager = null;
commandManager = null;
layerManager = null;
keyboardManager = null;
toolManager = null;
liquifyManager = null;
selectionManager = null;
redGreenModeManager = null;
// fabric.Object.prototype.controls.deleteControl = undefined;
// 移除window resize事件监听
// window.removeEventListener("resize", handleWindowResize);
observer.unobserve(canvasContainerRef.value);
});
// 窗口大小变化处理函数
function handleWindowResize() {
console.log(132);
// 使用requestAnimationFrame来防止频繁更新
setTimeout(() => {
// 更新画布大小并自动居中所有元素
updateCanvasSize();
// 确保显示的缩放信息是最新的
currentZoom.value = Math.round(canvasManager.canvas.getZoom() * 100);
});
}
function resetZoom() {
canvasManager.resetZoom();
}
function setZoom(zoom) {
setTimeout(() => {
if (!canvasManager) return;
const newZoom = Math.max(zoom, 0.1);
// 使用画布中心作为缩放点
const centerPoint = {
x: canvasManager.canvas.width / 2,
y: canvasManager.canvas.height / 2,
};
canvasManager.animateZoom(centerPoint, newZoom);
});
}
function zoomIn() {
if (!canvasManager) return;
const currentZoom = canvasManager.canvas.getZoom();
const newZoom = Math.min(currentZoom * 1.2, 20); // 增加20%最大20倍
// 使用画布中心作为缩放点
const centerPoint = {
x: canvasManager.canvas.width / 2,
y: canvasManager.canvas.height / 2,
};
canvasManager.animateZoom(centerPoint, newZoom);
}
function zoomOut() {
if (!canvasManager) return;
const currentZoom = canvasManager.canvas.getZoom();
const newZoom = Math.max(currentZoom / 1.2, 0.1); // 减少20%最小0.1倍
// 使用画布中心作为缩放点
const centerPoint = {
x: canvasManager.canvas.width / 2,
y: canvasManager.canvas.height / 2,
};
canvasManager.animateZoom(centerPoint, newZoom);
}
function updateCanvasSize() {
if (canvasManager && canvasContainerRef.value) {
const containerWidth = canvasContainerRef.value.clientWidth;
const containerHeight = canvasContainerRef.value.clientHeight;
// 普通模式下,更新画布大小,这会同时重置视图和居中所有元素
canvasManager.setCanvasSize(containerWidth, containerHeight);
// // 如果启用了红绿图模式,使用 layerManager 的缩放方法
// if (props.enabledRedGreenMode && layerManager) {
// layerManager.resizeCanvasWithScale(containerWidth, containerHeight, {
// undoable: false, // 可撤销操作
// });
// } else {
// // 普通模式下,更新画布大小,这会同时重置视图和居中所有元素
// canvasManager.setCanvasSize(containerWidth, containerHeight);
// }
}
}
function updateCanvasColor() {
canvasManager.setCanvasColor(canvasColor.value);
}
function createLayerName(){
const layer = t("Canvas.layer")
// 检查图层名称是否已存在
let layerIndex = 1;
let layerName = `${layer + " " + layerIndex}`;
while (layerManager.getLayerByName(layerName)) {
layerIndex++;
layerName = `${layer} ${layerIndex}`;
}
return layerName;
}
async function addLayer() {
await layerManager.createLayer(createLayerName());
}
async function addTopLayer() {
await layerManager.createLayer(createLayerName(), LayerType.EMPTY, {
insertTop: true,
});
}
function setActiveLayer(layerId) {
if (layerId !== activeLayerId.value) {
layerManager.setActiveLayer(layerId, {
undoable: true, // 可撤销
});
const activeObject = canvasManager.canvas.getActiveObject();
if (activeObject) {
canvasManager.canvas.discardActiveObject();
canvasManager.canvas.renderAll();
}
}
}
function toggleLayerVisibility(layerId) {
layerManager.toggleLayerVisibility(layerId, activeElementId.value);
}
function moveLayerUp(layerId) {
// 使用命令管理器执行移动图层命令,传递正确的方向参数
layerManager.moveLayer(layerId, "up");
}
function moveLayerDown(layerId) {
// 使用命令管理器执行移动图层命令,传递正确的方向参数
layerManager.moveLayer(layerId, "down");
}
function addRemoveBtn(fun) {
//添加删除按钮
const deleteIcon = cuowuImg;
// 创建删除图片元素
let deleteImg = document.createElement("img");
deleteImg.src = deleteIcon;
function renderIcon(icon) {
return function (ctx, left, top, styleOverride, fabricObject) {
var size = this.cornerSize;
ctx.save();
ctx.translate(left, top);
ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));
ctx.drawImage(icon, -size / 3, -size / 3, size / 1.5, size / 1.5);
ctx.restore();
};
}
fabric.Object.prototype.controls.deleteControl = new fabric.Control({
x: 0.5,
y: -0.5,
offsetY: -16,
offsetX: 16,
cursorStyle: "pointer",
mouseUpHandler: deleteFun,
render: renderIcon(deleteImg),
cornerSize: 24,
});
}
function deleteFun() {
removeLayer(layerManager.activeLayerId.value);
}
function removeLayer(layerId) {
// Check if this is the last layer - prevent deletion
var isChild = false;
var parentLength = 0;
layers.value.forEach((layer) => {
if(layer.children.some(v => v.id == layerId)){
isChild = true;
parentLength = layer.children.length;
}
})
if(isChild && parentLength == 1 || layers.value.length <= 3){
console.warn(
"Cannot delete the last layer. At least one layer must remain."
);
return;
}
layerManager.removeLayer(layerId);
// 此处删除画布上内容导致撤回操作无效(多余)
// if (canvasManager && canvasManager.canvas) {
// const layerToRemove = layers.value.find((l) => l.id === layerId);
// if (layerToRemove) {
// const elementIds = layerToRemove?.fabricObjects?.map((e) => e.id);
// elementIds.forEach((elementId) => {
// const objectToRemove = canvasManager.canvas
// .getObjects()
// .find((obj) => obj.id === elementId);
// if (objectToRemove) {
// canvasManager.canvas.remove(objectToRemove);
// }
// });
// if (activeLayerId.value === layerId) {
// activeElementId.value = null;
// }
// canvasManager.canvas.renderAll();
// }
// }
}
function triggerImageUpload() {
imageUploadRef.value.click();
}
function handleImageUpload(event) {
const file = event.target.files[0];
if (!file) return;
// 导入新的图片处理辅助函数
// 显示加载中状态
const loadingMessage = "正在处理图片...";
console.log(loadingMessage);
uploadImageAndCreateLayer({
file,
layerManager,
toolManager,
canvas: canvasManager.canvas,
})
.then((layerId) => {
console.log(`图片上传成功,已创建图层: ${layerId}`);
// 清空文件输入,允许再次选择相同的文件
if (imageUploadRef.value) {
imageUploadRef.value.value = "";
}
})
.catch((error) => {
console.error("图片上传失败:", error);
});
}
const selectImages = ref(null);
const handleImageSelect = (data) => {
UrlToFile(data.url, data.name).then((file) => {
handleImageUpload({ target: { files: [file] } });
});
};
function triggerLibrary() {
// console.log('CanvasEditor', '打开收藏')
if (props.isGeneral || props.isEdit) {
selectImages.value.init();
} else {
emit("trigger-library");
}
}
function handleAddText(text) {
if (toolManager && canvasManager && canvasManager.canvas) {
// 在画布中央创建文本
const canvasCenter = canvasManager.canvas.getCenter();
toolManager.createText(canvasCenter.left, canvasCenter.top, text);
}
}
// 红绿图模式切换处理
function toggleRedGreenMode() {
if (!redGreenModeManager) return;
isRedGreenMode.value = !isRedGreenMode.value;
if (isRedGreenMode.value) {
redGreenModeManager.enterRedGreenMode();
// 切换到红色画笔工具
handleToolSelect("redBrush");
} else {
redGreenModeManager.exitRedGreenMode();
// 恢复到默认工具
handleToolSelect(CanvasConfig.defaultTool);
}
}
// 红绿图模式工具切换处理
function handleRedGreenToolSelect(tool) {
if (!isRedGreenMode.value) return;
switch (tool) {
case "redBrush":
case "greenBrush":
case "eraser":
handleToolSelect(tool);
break;
default:
// 在红绿图模式下,只允许特定工具
break;
}
}
// 处理图层拖拽排序
function handleLayersReorder(reorderData) {
const { oldIndex, newIndex, layerId } = reorderData;
if (layerManager && layerManager.reorderLayers) {
const success = layerManager.reorderLayers(oldIndex, newIndex, layerId);
if (success) {
console.log(
`图层 ${layerId} 已从位置 ${oldIndex} 移动到位置 ${newIndex}`
);
// 更新画布渲染顺序
if (canvasManager) {
canvasManager.canvas.renderAll();
}
} else {
console.warn("图层排序失败");
}
}
}
// 处理子图层拖拽排序
function handleChildLayersReorder(reorderData) {
const { parentId, oldIndex, newIndex, layerId } = reorderData;
if (layerManager && layerManager.reorderChildLayers) {
const success = layerManager.reorderChildLayers(
parentId,
oldIndex,
newIndex,
layerId
);
if (success) {
console.log(
`子图层 ${layerId} 在父图层 ${parentId} 中已从位置 ${oldIndex} 移动到位置 ${newIndex}`
);
// 更新画布渲染顺序
if (canvasManager) {
canvasManager.canvas.renderAll();
}
} else {
console.warn("子图层排序失败");
}
}
}
// 处理画布变更事件
const changeCanvas = async (command) => {
const commandData = {
isChange: command.canUndo || command.canRedo, // 是否有可撤销或可重做的操作
...command, // 传递完整的命令数据
};
emit("changeCanvas", commandData);
if ((command.canUndo || command.canRedo) && props.enabledRedGreenMode) {
setTimeout(async () => {
const imageData = await canvasManager.exportImage({
restoreOpacityInRedGreen: true, // 恢复红绿图模式下的透明度
isCropByBg: true,
});
emit("trigger-red-green-mouseup", imageData);
}, 100);
}
};
const cropImageRef = ref(null);
const cropImage = (url) => {
return cropImageRef.value.open(url)
};
provide("cropImage", cropImage); // 提供给子组件使用
// 处理画布容器的拖放事件
const isDragOver = ref(false);
const canvasDragover = (e) => {
e.preventDefault();
if (isRedGreenMode.value) return;
const types = e.dataTransfer.types;
isDragOver.value = types.includes("Files");
};
// 处理画布容器的拖离事件
const canvasDragleave = (e) => {
e.preventDefault();
if (isRedGreenMode.value) return;
isDragOver.value = false;
};
// 处理画布容器的拖放事件
const canvasDragdrop = (e) => {
e.preventDefault();
if (isRedGreenMode.value) return;
isDragOver.value = false;
const files = e.dataTransfer.files;
for (const file of files) {
if (file.type.startsWith("image/")) {
handleImageUpload({ target: { files: [file] } });
}
}
};
// 提供外部ref实例方法
defineExpose({
layers, // 图层数据
activeTool, // 当前选中的工具
getCanvasManager: () => canvasManager, // 获取画布管理器实例
// type : isBackground isFixed flag: 是否可擦除图层
setFixedLayerErasable: ({ type = "isFixed", flag = false }) => {
canvasManager?.setFixedLayerErasable({
type,
flag, // 设置操作类型为可擦除
});
}, // 获取fabric画布实例
canvasManagerLoaded,
// 加载新数据到画布
loadJSON: (json, calllBack) => {
try {
if (json) canvasManager?.loadJSON?.(json, calllBack);
return true;
} catch (error) {
console.error("加载画布JSON失败:", error);
return false;
}
},
// 获取当前画布的JSON数据
getJSON: () => {
return canvasManager?.getJSON?.();
},
// (更换底图,不可撤销,不可操作)
changeFixedImage: (url, opts) => {
return canvasManager?.changeFixedImage?.(url, {
...(props?.clothingImageOpts || {}),
...opts,
});
},
//图片url或者base64
addImageToLayer: async (
url,
{ layerId, undoable, ...optios } = { layerId: null, undoable: true } // 可选参数 layerId 指定图层 将内容添加到指定图层 undoable 是否可撤销 false不可撤销 默认可撤销
) => {
if (!url) return Promise.reject(new Error("图片URL不能为空"));
if (layerId) {
const fabricImage = await loadImage(url);
// 如果指定了图层ID确保图层存在
return await canvasManager?.addImageToLayer?.({
targetLayerId: layerId,
fabricImage,
undoable, // 是否可撤销操作
...optios,
});
}
// 未指定图层ID默认添加到新的图层
return await loadImageUrlToLayer(
{
imageUrl: url,
layerManager,
canvas: canvasManager.canvas,
toolManager,
},
{ undoable, ...optios }
);
},
// 导出图片
exportImage: ({
isContainBg = false, // 是否包含背景图层
isContainFixed = false, // 是否包含固定图层
isCropByBg = false, // 是否使用背景大小裁剪 // 如果为true则导出时裁剪到背景图层大小
layerId = "", // 导出具体图层ID
layerIdArray = [], // 导出多个图层ID数组
expPicType = "png", // 导出图片类型 JPG 或 PNG ,SVG
isEnhanceImg, // 是否是增强图片
} = {}) => {
return canvasManager.exportImage({
isContainBg,
isContainFixed,
isCropByBg,
layerId,
layerIdArray,
expPicType,
isEnhanceImg,
});
},
/**
* 移动图层位置
* @param {string} layerId 图层ID
* @param {string} direction 移动方向,'up'或'down'
* @returns {boolean} 是否移动成功
*/
moveLayer(layerId, direction) {
if (!layerManager) return false;
const result = layerManager.moveLayer(layerId, direction);
// 使用高级排序重建画布顺序
if (result) {
layerManager.forceRebuildCanvasOrder();
}
return result;
},
/**
* 拖拽排序图层
* @param {number} oldIndex 原索引
* @param {number} newIndex 新索引
* @param {string} layerId 图层ID
* @returns {boolean} 是否排序成功
*/
reorderLayers(oldIndex, newIndex, layerId) {
if (!layerManager) return false;
// 优先使用高级排序功能
if (layerManager.layerSort) {
return layerManager.advancedReorderLayers(oldIndex, newIndex, layerId);
} else {
// 降级到基础排序
return layerManager.reorderLayers(oldIndex, newIndex, layerId);
}
},
/**
* 智能排序图层
* 根据对象类型和位置自动调整图层顺序
* @param {Array<string>} targetLayerIds 要排序的图层ID数组null表示排序所有普通图层
* @returns {boolean} 是否排序成功
*/
smartSortLayers(targetLayerIds = null) {
if (!layerManager) return false;
return layerManager.smartSortLayers(targetLayerIds);
},
/**
* 优化图层结构
* 清理空图层、重新排序等
* @returns {Object} 优化结果统计
*/
optimizeLayerStructure() {
if (!layerManager)
return { removedEmptyLayers: 0, mergedLayers: 0, reorderedLayers: 0 };
return layerManager.optimizeLayerStructure();
},
/**
* 强制重建画布对象顺序
* 当图层顺序发生变化后调用此方法确保画布对象顺序正确
*/
forceRebuildCanvasOrder() {
if (!layerManager) return;
layerManager.forceRebuildCanvasOrder();
},
/**
* 验证画布对象顺序是否正确
* @returns {boolean} 顺序是否正确
*/
validateObjectOrder() {
if (!layerManager) return true;
return layerManager.validateObjectOrder();
},
/**
* 批量重新排序多个图层
* @param {Array} reorderOperations 排序操作数组 [{layerId, oldIndex, newIndex}]
* @returns {boolean} 是否全部操作成功
*/
batchReorderLayers(reorderOperations) {
if (!layerManager) return false;
return layerManager.batchReorderLayers(reorderOperations);
},
/**
* 切换图层可见性
* @param {string} layerId 图层ID
* @returns {boolean} 更新后的可见性状态
*/
toggleLayerVisibility(layerId) {
if (!layerManager) return false;
return layerManager.toggleLayerVisibility(layerId);
},
/**
* 获取图层可见性状态
* @param {string} layerId 图层ID
* @returns {boolean} 图层是否可见
*/
getLayerVisibility(layerId) {
if (!layerManager) return false;
return layerManager.getLayerVisibility(layerId);
},
});
</script>
<template>
<div
ref="appContainerRef"
class="app-container"
@dragover.stop="canvasDragover"
@dragleave.stop="canvasDragleave"
@drop.stop="canvasDragdrop"
>
<!-- 头部菜单组件 -->
<div class="header-menu">
<HeaderMenu
v-if="canvasManagerLoaded"
:activeTool="activeTool"
:canvasWidth="canvasWidth"
:canvasHeight="canvasHeight"
:canvasColor="canvasColor"
:brushSize="brushSize"
:enabledRedGreenMode="enabledRedGreenMode"
:showLayersPanel="showLayersPanel"
:isBackgroundChangeable="isBackgroundChangeable"
@update:canvasWidth="canvasWidth = $event"
@update:canvasHeight="canvasHeight = $event"
@update:canvasColor="canvasColor = $event"
@update:brushSize="brushSize = $event"
@canvas-size-change="updateCanvasSize"
@canvas-color-change="updateCanvasColor"
>
<!-- 菜单扩展插槽 -->
<template #existsImageList>
<slot name="existsImageList" />
</template>
</HeaderMenu>
</div>
<div class="main-content">
<!-- :minimapEnabled="minimapEnabled" -->
<!-- 工具栏组件 -->
<div style="min-width: 5.8rem">
<ToolsSidebar
v-if="canvasManagerLoaded"
:activeTool="activeTool"
:isRedGreenMode="isRedGreenMode"
@tool-selected="handleToolSelect"
@red-green-tool-selected="handleRedGreenToolSelect"
@toggle-red-green-mode="toggleRedGreenMode"
@trigger-image-upload="triggerImageUpload"
@add-text="handleAddText"
@zoom-in="zoomIn"
@zoom-out="zoomOut"
@undo-redo-status-changed="changeCanvas"
@trigger-library="triggerLibrary"
>
<template #customToolsTop="{ toolTopProps }">
<slot name="customToolsTop" :tool-button-props="toolTopProps" />
</template>
<!-- 扩展插槽 -->
<template #customToolsBottom="{ toolButtonProps }">
<slot
name="customToolsBottom"
:tool-button-props="toolButtonProps"
/>
</template>
</ToolsSidebar>
</div>
<div
class="canvas-container"
:class="{ 'background-grid': !enabledRedGreenMode }"
ref="canvasContainerRef"
>
<canvas ref="canvasRef"></canvas>
<!-- 小地图组件 -->
<!-- <MinimapPanel v-if="minimapEnabled" :minimapManager="minimapManager" /> -->
<!-- 笔刷控制面板 -->
<BrushControlPanel
v-if="canvasManagerLoaded"
:activeTool="activeTool"
/>
<!-- 液化编辑面板 -->
<LiquifyPanel
v-if="canvasManagerLoaded && !enabledRedGreenMode"
:canvas="canvasManager && canvasManager.canvas"
:commandManager="commandManager"
:liquifyManager="liquifyManager"
:layerManager="layerManager"
:activeTool="activeTool"
/>
<!-- 选区面板 -->
<SelectionPanel
v-if="canvasManagerLoaded && !enabledRedGreenMode"
:canvas="canvasManager && canvasManager.canvas"
:commandManager="commandManager"
:selectionManager="selectionManager"
:layerManager="layerManager"
:toolManager="toolManager"
:activeTool="activeTool"
/>
<!-- 选择工具菜单组件 -->
<SelectMenuPanel
v-if="canvasManagerLoaded && !enabledRedGreenMode"
:canvas="canvasManager && canvasManager.canvas"
:commandManager="commandManager"
:selectionManager="selectionManager"
:layerManager="layerManager"
:toolManager="toolManager"
:activeTool="activeTool"
/>
<!-- 文本编辑面板 -->
<TextEditorPanel
v-if="canvasManagerLoaded && !enabledRedGreenMode"
:canvas="canvasManager && canvasManager.canvas"
:commandManager="commandManager"
/>
<div class="zoom-info">
{{ t("Canvas.Scale") }}: {{ currentZoom }}%
<button class="reset-zoom" @click="resetZoom">
{{ $t("Canvas.ResetLayer") }}
</button>
<button
class="help-btn"
@click="toggleShortcutHelp"
:title="$t('Canvas.Help')"
>
?
</button>
</div>
</div>
<!-- 图层面板组件 -->
<!-- v-if="canvasManagerLoaded && !enabledRedGreenMode" -->
<transition name="fade">
<div
class="layers-panel"
v-if="isShowLayerPanel && !enabledRedGreenMode && showLayersPanel"
>
<LayersPanel
v-if="canvasManagerLoaded"
:activeLayerId="activeLayerId"
:activeElementId="activeElementId"
:thumbnailManager="canvasManager.thumbnailManager"
:showFixedLayer="showFixedLayer"
@add-layer="addLayer"
@add-top-layer="addTopLayer"
@set-active-layer="setActiveLayer"
@toggle-layer-visibility="toggleLayerVisibility"
@move-layer-up="moveLayerUp"
@move-layer-down="moveLayerDown"
@remove-layer="removeLayer"
@layers-reorder="handleLayersReorder"
@child-layers-reorder="handleChildLayersReorder"
/>
</div>
</transition>
<!-- 裁剪图片组件 -->
<CropImage ref="cropImageRef" />
</div>
<!-- <div class="footer-actions">
<button class="share-btn">Share</button>
<button class="export-btn">Export</button>
</div> -->
<!-- 快捷键帮助模态框 -->
<div
v-if="showShortcutHelp"
class="modal-overlay"
@click="showShortcutHelp = false"
>
<div class="modal-content" @click.stop>
<button class="close-modal" @click="showShortcutHelp = false">×</button>
<KeyboardShortcutHelp />
</div>
</div>
<input
type="file"
ref="imageUploadRef"
accept="image/*"
style="display: none"
@change="handleImageUpload"
/>
<SelectImages
ref="selectImages"
full-data
radio
@select="handleImageSelect"
:api="Https.httpUrls.queryLibraryPage"
isLibrary
/>
<!-- 上传图片遮罩 -->
<div v-show="isDragOver" class="dragover-tip"></div>
</div>
</template>
<style scoped lang="less">
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.app-container {
display: flex;
flex-direction: column;
/* height: 100vh; */
background-color: #ffffff;
font-family: pingfang_medium, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 100;
& > .header-menu {
height: 5.2rem;
overflow: hidden;
display: flex;
align-items: center;
padding: 0 2rem;
border-bottom: 1px solid #e0e0e0;
background-color: #ffffff;
}
>.dragover-tip{
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 1.6rem;
z-index: 9999;
pointer-events: none;
}
}
.main-content {
display: flex;
flex: 1;
overflow: hidden;
position: relative;
}
.canvas-container {
flex: 1;
position: relative;
/* overflow: auto; */
/* background-color: #f8f8f8; */
:deep(.canvas-container) {
position: absolute !important;
}
}
.canvas-container canvas {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.background-grid {
--offsetX: 0px;
--offsetY: 0px;
--size: 8px;
--color: #dedcdc;
background-image: -webkit-linear-gradient(
45deg,
var(--color) 25%,
transparent 0,
transparent 75%,
var(--color) 0
),
-webkit-linear-gradient(45deg, var(--color) 25%, transparent 0, transparent
75%, var(--color) 0);
background-image: linear-gradient(
45deg,
var(--color) 25%,
transparent 0,
transparent 75%,
var(--color) 0
),
linear-gradient(
45deg,
var(--color) 25%,
transparent 0,
transparent 75%,
var(--color) 0
);
background-position: var(--offsetX) var(--offsetY),
calc(var(--size) + var(--offsetX)) calc(var(--size) + var(--offsetY));
background-size: calc(var(--size) * 2) calc(var(--size) * 2);
}
.zoom-info {
position: absolute;
bottom: 1rem;
right: 1rem;
background: rgba(255, 255, 255, 0.7);
padding: 0.5rem 1rem;
border-radius: 0.4rem;
font-size: 1.4rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
color: #666;
display: flex;
}
.zoom-hint {
position: absolute;
top: 1rem;
left: 1rem;
background: rgba(255, 255, 255, 0.8);
padding: 0.5rem 1rem;
border-radius: 0.4rem;
font-size: 1.2rem;
color: #666;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.reset-zoom {
margin-left: 1rem;
cursor: pointer;
padding: 0.2rem 0.5rem;
font-size: 1.2rem;
border: 1px solid #ddd;
background: #f8f8f8;
border-radius: 0.3rem;
}
.footer-actions {
display: flex;
justify-content: center;
gap: 1rem;
padding: 1.5rem;
border-top: 1px solid #e0e0e0;
}
.share-btn,
.export-btn {
padding: 0.8rem 2rem;
border: none;
border-radius: 2rem;
background-color: #000;
color: #fff;
font-size: 1.4rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.share-btn:hover,
.export-btn:hover {
opacity: 0.9;
}
button {
font-size: 1.3rem;
transition: all 0.2s;
}
button:hover {
background: #e6e6e6;
}
.help-btn {
margin-left: 1rem;
cursor: pointer;
width: 2.4rem;
height: 2.4rem;
border-radius: 50%;
background-color: #f0f0f0;
border: 1px solid #ddd;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
}
.help-btn:hover {
background-color: #e0e0e0;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
position: relative;
width: 80%;
max-width: 60rem;
max-height: 90vh;
overflow-y: auto;
background-color: #fff;
border-radius: 0.8rem;
padding: 2rem;
}
.close-modal {
position: absolute;
top: 1rem;
right: 1rem;
width: 3rem;
height: 3rem;
border-radius: 50%;
border: none;
background-color: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
cursor: pointer;
}
/* 网格控制面板样式 */
.grid-controls {
position: absolute;
bottom: 11.5rem;
right: 1rem;
background: rgba(255, 255, 255, 0.9);
padding: 1rem;
border-radius: 0.4rem;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
gap: 0.8rem;
z-index: 5;
}
.layers-panel {
position: absolute;
right: 2rem;
top: 1rem;
transition: width 0.3s ease;
background: #fff;
width: 35rem;
max-height: 90%;
display: flex;
overflow: hidden;
box-shadow: 0 0.4rem 2rem rgba(0, 0, 0, 0.1); /* 添加阴影效果 */
backdrop-filter: blur(2px); /* 添加模糊效果 */
-webkit-backdrop-filter: blur(2px);
background-color: rgba(255, 255, 255, 0.95); /* 改为白色背景 */
z-index: 1000; /* 确保面板在最上层 */
border: 1px solid #e0e0e0;
/* 添加指向整个面板的倒三角 */
&::before {
content: "";
position: absolute;
top: -0.9rem;
right: 0.6rem;
width: 0;
height: 0;
border-left: 1rem solid transparent;
border-right: 1rem solid transparent;
border-bottom: 1rem solid rgba(255, 255, 255, 0.95); /* 与面板背景色一致 */
filter: drop-shadow(0 -1px 1px rgba(0, 0, 0, 0.05));
z-index: 1;
}
}
/* 添加触控设备的样式调整 */
@media (pointer: coarse) {
.tool-btn {
width: 4.4rem;
height: 4.4rem;
font-size: 1.8rem;
}
.layers-panel {
width: 28rem;
}
.layer-item,
.element-item {
padding: 1.2rem 0.8rem;
}
.element-action-btn,
.element-delete-btn {
width: 2.4rem;
height: 2.4rem;
}
.help-btn {
width: 3.2rem;
height: 3.2rem;
}
.modal-content {
width: 90%;
padding: 1.6rem;
}
.close-modal {
width: 4rem;
height: 4rem;
font-size: 2.4rem;
}
}
// 淡入淡出动画
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s, transform 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(1rem);
}
</style>