Files
aida_front/src/component/Canvas/CanvasEditor/index.vue

1217 lines
32 KiB
Vue
Raw Normal View History

2025-06-09 10:25:54 +08:00
<script setup>
import {
ref,
onMounted,
onBeforeUnmount,
defineAsyncComponent,
shallowRef,
provide,
2025-06-18 11:05:23 +08:00
defineExpose,
2025-06-22 13:52:28 +08:00
nextTick,
2025-06-09 10:25:54 +08:00
} 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 { MinimapManager } from "./managers/minimap/MinimapManager";
// 导入封装的组件
import ToolsSidebar from "./components/ToolsSidebar.vue";
import HeaderMenu from "./components/HeaderMenu.vue";
2025-06-18 11:05:23 +08:00
import LayersPanel from "./components/LayersPanel/LayersPanel.vue";
2025-06-09 10:25:54 +08:00
import BrushControlPanel from "./components/BrushControlPanel.vue";
import TextEditorPanel from "./components/TextEditorPanel.vue"; // 引入文本编辑面板
import LiquifyPanel from "./components/LiquifyPanel.vue"; // 引入液化编辑面板
import SelectionPanel from "./components/SelectionPanel.vue"; // 引入选区面板
import { OperationType } from "./utils/layerHelper.js";
2025-06-22 13:52:28 +08:00
import { ToolManager } from "./managers/toolManager.js";
2025-06-18 11:05:23 +08:00
import { fabric } from "fabric-with-all";
2025-06-09 10:25:54 +08:00
import { uploadImageAndCreateLayer } from "./utils/imageHelper.js";
// import MinimapPanel from "./components/MinimapPanel.vue";
const KeyboardShortcutHelp = defineAsyncComponent(() =>
import("./components/KeyboardShortcutHelp.vue")
);
const emit = defineEmits(["trigger-red-green-mouseup"]);
const props = defineProps({
canvasJSON: {
type: [Object, String],
default: "", // 默认空
},
config: {
type: Object,
default: () => CanvasConfig, // 默认配置
},
enabledRedGreenMode: {
type: Boolean,
default: false, // 是否启用红绿图模式
},
clothingImageUrl: {
type: String,
default: "", // 衣服底图URL
},
redGreenImageUrl: {
type: String,
default: "", // 红绿图URL
},
2025-06-22 13:52:28 +08:00
clothingImageOpts: {
type: Object,
default: () => {
return {
scaleX: 1,
scaleY: 1,
};
},
},
2025-06-09 10:25:54 +08:00
});
// 引用和状态
const canvasRef = ref(null);
const canvasContainerRef = shallowRef(null);
const imageUploadRef = ref(null);
const currentZoom = ref(100);
// 画布设置
const canvasWidth = ref(CanvasConfig.width);
const canvasHeight = ref(CanvasConfig.height);
const canvasColor = ref(CanvasConfig.backgroundColor);
2025-06-18 11:05:23 +08:00
const layerWidth = ref(CanvasConfig.layerWidth);
2025-06-09 10:25:54 +08:00
const brushSize = ref(CanvasConfig.brushSize); // 画笔大小
const canvasManagerLoaded = ref(false); // 画布是否加载完成
// 红绿图模式状态
const isRedGreenMode = ref(false);
2025-06-18 11:05:23 +08:00
const isShowLayerPanel = ref(true); // 是否显示图层面板
provide("isShowLayerPanel", isShowLayerPanel); // 提供红绿图模式状态给子组件
2025-06-09 10:25:54 +08:00
// 小地图设置
// const minimapEnabled = ref(true);
// const minimapManager = ref(null);
// 图层和元素管理
const layers = ref([]);
const activeLayerId = ref(null);
const activeElementId = ref(null);
// 当前选择的工具
const activeTool = ref(CanvasConfig.defaultTool); // 默认工具
// 管理器实例
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;
}
// 工具选择处理
function handleToolSelect(tool) {
activeTool.value = tool;
// toolManager.setActiveTool(tool); // 更新工具管理器中的当前工具 普通模式,不可撤回操作
2025-06-18 11:05:23 +08:00
toolManager.setToolWithCommand(tool, {
undoable: props.enabledRedGreenMode ? false : true, // 普通模式下工具选择不可撤销
}); // 命令模式 可撤回操作
2025-06-09 10:25:54 +08:00
}
function toggleMinimap(enabled) {
// minimapEnabled.value = enabled;
// if (minimapManager.value) {
// minimapManager.value.setVisibility(enabled);
// }
}
// 初始化画布
onMounted(async () => {
// 如果启用了红绿图模式,设置画布大小为默认值
if (props.enabledRedGreenMode) {
canvasHeight.value = canvasContainerRef.value.clientWidth;
canvasWidth.value = canvasContainerRef.value.clientHeight;
}
2025-06-18 11:05:23 +08:00
2025-06-09 10:25:54 +08:00
// 创建管理器实例
canvasManager = new CanvasManager(canvasRef.value, {
2025-06-18 11:05:23 +08:00
width: canvasContainerRef.value.clientWidth,
2025-06-09 10:25:54 +08:00
height: canvasContainerRef.value.clientHeight,
// backgroundColor: canvasColor.value,
currentZoom,
layers,
canvasWidth,
canvasHeight,
canvasColor,
enabledRedGreenMode: props.enabledRedGreenMode,
});
canvasManager.canvas.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.value,
2025-06-18 11:05:23 +08:00
isRedGreenMode: props.enabledRedGreenMode,
2025-06-09 10:25:54 +08:00
layers,
activeLayerId,
canvasManager, // 添加对 canvasManager 的引用
commandManager, // 添加对命令管理器的引用
});
// commandManager.setLayerManager(layerManager); // 设置命令管理器需要访问的图层数据
// 设置缩略图管理器需要访问的图层数据
// canvasManager.layers = layers;
if (props.canvasJSON) {
// 如果传入了初始JSON数据加载到画布上
if (typeof props.canvasJSON === "string") {
try {
canvasManager.loadJSON(props.canvasJSON);
} catch (error) {
console.error("加载画布JSON失败:", error);
// 初始化图层 - 确保创建背景层
layerManager.initializeLayers();
}
} else if (typeof props.canvasJSON === "object") {
canvasManager.loadJSON(JSON.stringify(props.canvasJSON));
}
} else {
// 初始化图层 - 确保创建背景层
layerManager.initializeLayers();
}
// 创建工具管理器实例
toolManager = new ToolManager({
canvas: canvasManager.canvas, // fabric.js 画布实例
commandManager, // 命令管理器实例,用于撤销/重做
canvasManager, // 画布管理器实例
layerManager,
activeTool, // 响应式引用,存储当前选中的工具
brushSize: brushSize.value, // 可选,初始画笔大小
});
// 初始化文本编辑功能
toolManager.setupTextEditingEvents();
toolManager.setFileUploadHandler(triggerImageUpload); // 设置快捷图片上传处理函数
layerManager.setToolManager(toolManager); // 将工具管理器传递给图层管理器
canvasManager.setToolManager(toolManager); // 将工具管理器传递给画布管理器
canvasManager.setLayerManager(layerManager);
2025-06-18 11:05:23 +08:00
canvasManager.setCommandManager(commandManager); // 将命令管理器传递给画布管理器
2025-06-09 10:25:54 +08:00
// 初始化快捷键管理器
keyboardManager = new KeyboardManager({
canvas: canvasManager.canvas,
commandManager,
layerManager,
toolManager,
});
// 绑定快捷键事件
keyboardManager.init();
provide("canvasManager", canvasManager); // 提供给子组件使用
provide("layerManager", layerManager); // 提供给子组件使用
provide("commandManager", commandManager); // 提供给子组件使用
provide("toolManager", toolManager); // 提供给子组件使用
provide("keyboardManager", keyboardManager); // 提供给子组件使用
provide("activeTool", activeTool); // 提供给子组件使用
provide("liquifyManager", () => liquifyManager); // 提供液化管理器
provide("layers", layers); // 提供图层数据
// 绑定画布操作事件
canvasManager.setupCanvasEvents(activeElementId, layerManager);
canvasManagerLoaded.value = true;
// 初始化网格设置
// toggleGridVisibility(gridEnabled.value);
// 初始化小地图
// minimapManager.value = new MinimapManager(canvasManager.canvas);
2025-06-22 13:52:28 +08:00
setTimeout(() => {
// 初始状态下生成所有预览图
canvasManager.updateAllThumbnails();
}, 300);
2025-06-09 10:25:54 +08:00
// 使用window的resize事件代替ResizeObserver
// 只有当窗口大小变化时才更新画布尺寸
window.addEventListener("resize", handleWindowResize);
// 初始化液化管理器
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.enabledRedGreenMode &&
props.clothingImageUrl &&
props.redGreenImageUrl
) {
canvasManager.canvas.fill = "#fff"; // 设置画布背景色为白色 // 初始化红绿图模式管理器
redGreenModeManager = new RedGreenModeManager({
canvas: canvasManager.canvas,
canvasManager,
layerManager,
toolManager,
commandManager,
});
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);
}
}
2025-06-22 13:52:28 +08:00
// 如果初始化有默认底图,设置底图 - 红绿图模式不通过初始化重置底图了
if (!isRedGreenMode.value && props.clothingImageUrl) {
nextTick(() => {
setTimeout(() => {
try {
canvasManager?.changeFixedImage?.(props.clothingImageUrl, {
undoable: false, // 不可撤销操作
...(props?.clothingImageOpts || {}),
});
} catch (error) {
console.error("更换底图失败:", error);
}
}, 92); // 延迟 确保更新底图完成
});
this.canvasManager?.centerBackgroundLayer?.(
this.canvas.width,
this.canvas.height
);
}
2025-06-09 10:25:54 +08:00
// 初始设置
handleWindowResize(); // 设置画布大小
}
});
onBeforeUnmount(() => {
// if (import.meta.hot) {
// // 热更新
// console.log("onBeforeUnmount 开发环境热更新不卸载组件...");
// return; // 开发环境下不卸载组件
// }
2025-06-09 10:25:54 +08:00
console.log("onBeforeUnmount 组件卸载,清理资源...");
// canvasManager?.dispose?.();
// commandManager?.dispose?.();
// layerManager?.dispose?.();
// keyboardManager?.dispose?.();
// toolManager?.dispose?.();
// liquifyManager?.dispose?.();
// selectionManager?.dispose?.();
// redGreenModeManager?.dispose?.();
2025-06-09 10:25:54 +08:00
// minimapManager?.dispose?.();
canvasManager = null;
commandManager = null;
layerManager = null;
keyboardManager = null;
toolManager = null;
liquifyManager = null;
selectionManager = null;
redGreenModeManager = null;
2025-06-09 10:25:54 +08:00
// 移除window resize事件监听
window.removeEventListener("resize", handleWindowResize);
});
// 窗口大小变化处理函数
function handleWindowResize() {
// 使用requestAnimationFrame来防止频繁更新
requestAnimationFrame(() => {
// 更新画布大小并自动居中所有元素
updateCanvasSize();
// 确保显示的缩放信息是最新的
currentZoom.value = Math.round(canvasManager.canvas.getZoom() * 100);
});
}
function resetZoom() {
canvasManager.resetZoom();
}
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;
2025-06-18 11:05:23 +08:00
// 普通模式下,更新画布大小,这会同时重置视图和居中所有元素
canvasManager.setCanvasSize(containerWidth, containerHeight);
// // 如果启用了红绿图模式,使用 layerManager 的缩放方法
// if (props.enabledRedGreenMode && layerManager) {
// layerManager.resizeCanvasWithScale(containerWidth, containerHeight, {
// undoable: false, // 可撤销操作
// });
// } else {
// // 普通模式下,更新画布大小,这会同时重置视图和居中所有元素
// canvasManager.setCanvasSize(containerWidth, containerHeight);
// }
2025-06-09 10:25:54 +08:00
}
}
function updateCanvasColor() {
canvasManager.setCanvasColor(canvasColor.value);
}
2025-06-22 13:52:28 +08:00
async function addLayer() {
await layerManager.createLayer();
2025-06-09 10:25:54 +08:00
}
function setActiveLayer(layerId) {
2025-06-18 11:05:23 +08:00
if (layerId !== activeLayerId.value) {
layerManager.setActiveLayer(layerId, {
undoable: true, // 可撤销
});
2025-06-09 10:25:54 +08:00
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 removeLayer(layerId) {
// Check if this is the last layer - prevent deletion
if (layers.value.length <= 2) {
console.warn(
"Cannot delete the last layer. At least one layer must remain."
);
return;
}
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();
}
}
layerManager.removeLayer(layerId);
}
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);
});
}
function handleAddText() {
if (toolManager && canvasManager && canvasManager.canvas) {
// 在画布中央创建文本
const canvasCenter = canvasManager.canvas.getCenter();
toolManager.createText(canvasCenter.left, canvasCenter.top);
}
}
// 红绿图模式切换处理
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("子图层排序失败");
}
}
}
// 提供外部ref实例方法
defineExpose({
getCanvasManager: () => canvasManager, // 获取画布管理器实例
canvasManagerLoaded,
// 加载新数据到画布
2025-06-18 11:05:23 +08:00
loadJSON: (json, calllBack) => {
2025-06-09 10:25:54 +08:00
try {
2025-06-18 11:05:23 +08:00
if (json) canvasManager?.loadJSON?.(json, calllBack);
2025-06-09 10:25:54 +08:00
return true;
} catch (error) {
console.error("加载画布JSON失败:", error);
return false;
}
},
// 获取当前画布的JSON数据
getJSON: () => {
return canvasManager?.getJSON?.();
},
// (更换底图,不可撤销,不可操作)
changeFixedImage: (url, opts) => {
return canvasManager?.changeFixedImage?.(url, opts);
},
//图片url或者base64 可选图层ID 不传默认新建图层
addImageToLayer: (url, layerId) => {
return canvasManager.addImageToLayer(url, layerId);
},
//图片url或者base64数组 可选图层ID 不传默认新建图层
addMultipleImagesToLayer: (urls, layerId) => {
return canvasManager?.addMultipleImages(urls, layerId);
},
// 导出图片
exportImage: ({
isContainBg = false, // 是否包含背景图层
isContainFixed = false, // 是否包含固定图层
layerId = "", // 导出具体图层ID
layerIdArray = [], // 导出多个图层ID数组
expPicType = "png", // 导出图片类型 JPG 或 PNG ,SVG
} = {}) => {
return canvasManager.exportImage({
isContainBg,
isContainFixed,
layerId,
layerIdArray,
expPicType,
});
},
2025-06-18 11:05:23 +08:00
/**
* 移动图层位置
* @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);
},
2025-06-09 10:25:54 +08:00
});
</script>
<template>
<div class="app-container">
<!-- 头部菜单组件 -->
2025-06-18 11:05:23 +08:00
<div class="header-menu">
<HeaderMenu
v-if="canvasManagerLoaded"
:activeTool="activeTool"
:canvasWidth="canvasWidth"
:canvasHeight="canvasHeight"
:canvasColor="canvasColor"
:brushSize="brushSize"
:enabledRedGreenMode="enabledRedGreenMode"
@update:canvasWidth="canvasWidth = $event"
@update:canvasHeight="canvasHeight = $event"
@update:canvasColor="canvasColor = $event"
@update:brushSize="brushSize = $event"
@canvas-size-change="updateCanvasSize"
@canvas-color-change="updateCanvasColor"
/>
</div>
2025-06-09 10:25:54 +08:00
<div class="main-content">
<!-- :minimapEnabled="minimapEnabled" -->
<!-- 工具栏组件 -->
2025-06-18 11:05:23 +08:00
<div style="min-width: 58px">
<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"
/>
</div>
2025-06-09 10:25:54 +08:00
<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"
/>
<!-- 文本编辑面板 -->
<TextEditorPanel
2025-06-18 11:05:23 +08:00
v-if="canvasManagerLoaded && !enabledRedGreenMode"
2025-06-09 10:25:54 +08:00
:canvas="canvasManager?.canvas"
:commandManager="commandManager"
/>
<!-- 液化编辑面板 -->
<LiquifyPanel
2025-06-18 11:05:23 +08:00
v-if="canvasManagerLoaded && !enabledRedGreenMode"
2025-06-09 10:25:54 +08:00
:canvas="canvasManager?.canvas"
:commandManager="commandManager"
:liquifyManager="liquifyManager"
:layerManager="layerManager"
:activeTool="activeTool"
/>
<!-- 选区面板 -->
<SelectionPanel
2025-06-18 11:05:23 +08:00
v-if="canvasManagerLoaded && !enabledRedGreenMode"
2025-06-09 10:25:54 +08:00
:canvas="canvasManager?.canvas"
:commandManager="commandManager"
:selectionManager="selectionManager"
:layerManager="layerManager"
:toolManager="toolManager"
:activeTool="activeTool"
/>
<div class="zoom-info">
缩放: {{ currentZoom }}%
<button class="reset-zoom" @click="resetZoom">重置视图</button>
<button
class="help-btn"
@click="toggleShortcutHelp"
title="查看快捷键和触控操作"
>
?
</button>
</div>
</div>
<!-- 图层面板组件 -->
2025-06-18 11:05:23 +08:00
<!-- v-if="canvasManagerLoaded && !enabledRedGreenMode" -->
<transition name="fade">
2025-06-22 13:52:28 +08:00
<div class="layers-panel" v-if="isShowLayerPanel">
2025-06-18 11:05:23 +08:00
<LayersPanel
2025-06-22 13:52:28 +08:00
v-if="canvasManagerLoaded"
2025-06-18 11:05:23 +08:00
:activeLayerId="activeLayerId"
:activeElementId="activeElementId"
:thumbnailManager="canvasManager.thumbnailManager"
@add-layer="addLayer"
@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>
2025-06-09 10:25:54 +08:00
</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"
/>
</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: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 77;
2025-06-18 11:05:23 +08:00
& > .header-menu {
height: 52px;
overflow: hidden;
display: flex;
align-items: center;
padding: 0 20px;
border-bottom: 1px solid #e0e0e0;
background-color: #ffffff;
}
2025-06-09 10:25:54 +08:00
}
.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: 10px;
right: 10px;
background: rgba(255, 255, 255, 0.7);
padding: 5px 10px;
border-radius: 4px;
font-size: 14px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
color: #666;
}
.zoom-hint {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.8);
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
color: #666;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.reset-zoom {
margin-left: 10px;
cursor: pointer;
padding: 2px 5px;
font-size: 12px;
border: 1px solid #ddd;
background: #f8f8f8;
border-radius: 3px;
}
.footer-actions {
display: flex;
justify-content: center;
gap: 10px;
padding: 15px;
border-top: 1px solid #e0e0e0;
}
.share-btn,
.export-btn {
padding: 8px 20px;
border: none;
border-radius: 20px;
background-color: #000;
color: #fff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.share-btn:hover,
.export-btn:hover {
opacity: 0.9;
}
button {
font-size: 13px;
transition: all 0.2s;
}
button:hover {
background: #e6e6e6;
}
.help-btn {
margin-left: 10px;
cursor: pointer;
width: 24px;
height: 24px;
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: 600px;
max-height: 90vh;
overflow-y: auto;
background-color: #fff;
border-radius: 8px;
padding: 20px;
}
.close-modal {
position: absolute;
top: 10px;
right: 10px;
width: 30px;
height: 30px;
border-radius: 50%;
border: none;
background-color: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
cursor: pointer;
}
/* 网格控制面板样式 */
.grid-controls {
position: absolute;
bottom: 115px;
right: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 4px;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
gap: 8px;
z-index: 5;
}
.layers-panel {
2025-06-18 11:05:23 +08:00
position: absolute;
right: 20px;
top: 10px;
2025-06-09 10:25:54 +08:00
transition: width 0.3s ease;
background: #fff;
2025-06-18 11:05:23 +08:00
width: 350px;
max-height: 85vh;
box-shadow: 0 4px 20px 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: -9px;
right: 6px;
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 10px solid rgba(255, 255, 255, 0.95); /* 与面板背景色一致 */
filter: drop-shadow(0 -1px 1px rgba(0, 0, 0, 0.05));
z-index: 1;
}
2025-06-09 10:25:54 +08:00
}
/* 添加触控设备的样式调整 */
@media (pointer: coarse) {
.tool-btn {
width: 44px;
height: 44px;
font-size: 18px;
}
.layers-panel {
width: 280px;
}
.layer-item,
.element-item {
padding: 12px 8px;
}
.element-action-btn,
.element-delete-btn {
width: 24px;
height: 24px;
}
.help-btn {
width: 32px;
height: 32px;
}
.modal-content {
width: 90%;
padding: 16px;
}
.close-modal {
width: 40px;
height: 40px;
font-size: 24px;
}
}
2025-06-18 11:05:23 +08:00
// 淡入淡出动画
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s, transform 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(10px);
}
2025-06-09 10:25:54 +08:00
</style>