1020 lines
27 KiB
Vue
1020 lines
27 KiB
Vue
|
|
<script setup>
|
|||
|
|
import {
|
|||
|
|
ref,
|
|||
|
|
onMounted,
|
|||
|
|
onBeforeUnmount,
|
|||
|
|
defineAsyncComponent,
|
|||
|
|
shallowRef,
|
|||
|
|
provide,
|
|||
|
|
} 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";
|
|||
|
|
import LayersPanel from "./components/LayersPanel.vue";
|
|||
|
|
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";
|
|||
|
|
import { ToolManager } from "./managers/ToolManager.js";
|
|||
|
|
//import { fabric } from "fabric-with-all";
|
|||
|
|
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
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 引用和状态
|
|||
|
|
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);
|
|||
|
|
const layerWidth = ref(CanvasConfig.layerWidth); // 假设侧边栏宽度为 250px
|
|||
|
|
const brushSize = ref(CanvasConfig.brushSize); // 画笔大小
|
|||
|
|
const canvasManagerLoaded = ref(false); // 画布是否加载完成
|
|||
|
|
|
|||
|
|
// 红绿图模式状态
|
|||
|
|
const isRedGreenMode = ref(false);
|
|||
|
|
|
|||
|
|
// 小地图设置
|
|||
|
|
// 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); // 更新工具管理器中的当前工具 普通模式,不可撤回操作
|
|||
|
|
toolManager.setToolWithCommand(tool); // 命令模式 可撤回操作
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
// 创建管理器实例
|
|||
|
|
canvasManager = new CanvasManager(canvasRef.value, {
|
|||
|
|
width: canvasContainerRef.value.clientWidth - layerWidth.value, // 初始化的时候需要减去侧边栏宽度
|
|||
|
|
height: canvasContainerRef.value.clientHeight,
|
|||
|
|
// backgroundColor: canvasColor.value,
|
|||
|
|
currentZoom,
|
|||
|
|
layers,
|
|||
|
|
canvasWidth,
|
|||
|
|
canvasHeight,
|
|||
|
|
canvasColor,
|
|||
|
|
enabledRedGreenMode: props.enabledRedGreenMode,
|
|||
|
|
});
|
|||
|
|
console.log(canvasManager,canvasManager.thumbnailManager)
|
|||
|
|
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,
|
|||
|
|
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);
|
|||
|
|
|
|||
|
|
// 初始化快捷键管理器
|
|||
|
|
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);
|
|||
|
|
|
|||
|
|
// setTimeout(() => {
|
|||
|
|
// // historyManager.saveCanvasState();
|
|||
|
|
// // 初始状态下生成所有预览图
|
|||
|
|
// canvasManager.updateAllThumbnails();
|
|||
|
|
// }, 500);
|
|||
|
|
|
|||
|
|
// 使用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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 初始设置
|
|||
|
|
handleWindowResize(); // 设置画布大小
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
onBeforeUnmount(() => {
|
|||
|
|
console.log("onBeforeUnmount 组件卸载,清理资源...");
|
|||
|
|
canvasManager?.dispose?.();
|
|||
|
|
commandManager?.dispose?.();
|
|||
|
|
layerManager?.dispose?.();
|
|||
|
|
keyboardManager?.dispose?.();
|
|||
|
|
toolManager?.dispose?.();
|
|||
|
|
liquifyManager?.dispose?.();
|
|||
|
|
selectionManager?.dispose?.();
|
|||
|
|
redGreenModeManager?.dispose?.();
|
|||
|
|
// minimapManager?.dispose?.();
|
|||
|
|
|
|||
|
|
// 移除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;
|
|||
|
|
|
|||
|
|
// 如果启用了红绿图模式,使用 layerManager 的缩放方法
|
|||
|
|
if (props.enabledRedGreenMode && layerManager) {
|
|||
|
|
layerManager.resizeCanvasWithScale(containerWidth, containerHeight);
|
|||
|
|
} else {
|
|||
|
|
// 普通模式下,更新画布大小,这会同时重置视图和居中所有元素
|
|||
|
|
canvasManager.setCanvasSize(containerWidth, containerHeight);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateCanvasColor() {
|
|||
|
|
canvasManager.setCanvasColor(canvasColor.value);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function addLayer() {
|
|||
|
|
layerManager.createLayer();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setActiveLayer(layerId) {
|
|||
|
|
if (activeElementId.value && canvasManager && canvasManager.canvas) {
|
|||
|
|
const activeObject = canvasManager.canvas.getActiveObject();
|
|||
|
|
if (activeObject) {
|
|||
|
|
canvasManager.canvas.discardActiveObject();
|
|||
|
|
canvasManager.canvas.renderAll();
|
|||
|
|
}
|
|||
|
|
activeElementId.value = null;
|
|||
|
|
}
|
|||
|
|
layerManager.setActiveLayer(layerId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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,
|
|||
|
|
// 加载新数据到画布
|
|||
|
|
loadJSON: (json) => {
|
|||
|
|
try {
|
|||
|
|
if (json) canvasManager?.loadJSON?.(json);
|
|||
|
|
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,
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<div class="app-container">
|
|||
|
|
<!-- 头部菜单组件 -->
|
|||
|
|
<HeaderMenu
|
|||
|
|
v-if="canvasManagerLoaded"
|
|||
|
|
:activeTool="activeTool"
|
|||
|
|
:canvasWidth="canvasWidth"
|
|||
|
|
:canvasHeight="canvasHeight"
|
|||
|
|
:canvasColor="canvasColor"
|
|||
|
|
:brushSize="brushSize"
|
|||
|
|
@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 class="main-content">
|
|||
|
|
<!-- :minimapEnabled="minimapEnabled" -->
|
|||
|
|
<!-- 工具栏组件 -->
|
|||
|
|
<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
|
|||
|
|
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
|
|||
|
|
v-if="canvasManagerLoaded"
|
|||
|
|
:canvas="canvasManager?.canvas"
|
|||
|
|
:commandManager="commandManager"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<!-- 液化编辑面板 -->
|
|||
|
|
<LiquifyPanel
|
|||
|
|
v-if="canvasManagerLoaded"
|
|||
|
|
:canvas="canvasManager?.canvas"
|
|||
|
|
:commandManager="commandManager"
|
|||
|
|
:liquifyManager="liquifyManager"
|
|||
|
|
:layerManager="layerManager"
|
|||
|
|
:activeTool="activeTool"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<!-- 选区面板 -->
|
|||
|
|
<SelectionPanel
|
|||
|
|
v-if="canvasManagerLoaded"
|
|||
|
|
: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>
|
|||
|
|
|
|||
|
|
<!-- 图层面板组件 -->
|
|||
|
|
<LayersPanel
|
|||
|
|
class="layers-panel"
|
|||
|
|
:style="{ width: layerWidth + 'px' }"
|
|||
|
|
v-if="canvasManagerLoaded && !enabledRedGreenMode"
|
|||
|
|
: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>
|
|||
|
|
|
|||
|
|
<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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.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 {
|
|||
|
|
transition: width 0.3s ease;
|
|||
|
|
background: #fff;
|
|||
|
|
width: 250px;
|
|||
|
|
flex: none;
|
|||
|
|
}
|
|||
|
|
/* 添加触控设备的样式调整 */
|
|||
|
|
@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;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|