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

1590 lines
44 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,
watchEffect,
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 texturePresetManager from "./managers/brushes/TexturePresetManager";
2025-06-29 23:29:47 +08:00
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 { UrlToFile } from "@/tool/util";
2025-06-09 10:25:54 +08:00
// 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 SelectMenuPanel from "./components/SelectMenuPanel.vue"; // 引入选择工具菜单组件
2025-06-09 10:25:54 +08:00
import SelectionPanel from "./components/SelectionPanel.vue"; // 引入选区面板
import { LayerType, OperationType } from "./utils/layerHelper.js";
2025-09-11 15:46:44 +08:00
import { ToolManager } from "./managers/ToolManager.js";
2025-09-01 14:03:30 +08:00
import { fabric } from "fabric-with-all";
import {
uploadImageAndCreateLayer,
loadImageUrlToLayer,
loadImage,
} from "./utils/imageHelper.js";
2025-06-09 10:25:54 +08:00
// import MinimapPanel from "./components/MinimapPanel.vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const KeyboardShortcutHelp = defineAsyncComponent(() =>
import("./components/KeyboardShortcutHelp.vue")
2025-06-09 10:25:54 +08:00
);
const emit = defineEmits([
"trigger-red-green-mouseup", // 红绿图模式鼠标抬起事件
"changeCanvas", // 画布变更事件
"canvasInit", // 画布初始化事件
2025-09-22 15:30:26 +08:00
"trigger-library", // 触发打开Library选择图片事件
]);
2025-06-09 10:25:54 +08:00
const props = defineProps({
canvasJSON: {
type: [Object, String],
default: "", // 默认空
},
config: {
type: Object,
default: () => CanvasConfig, // 默认配置
},
2025-06-23 00:40:45 +08:00
showLayersPanel: {
type: Boolean,
default: true, // 是否显示图层面板
},
2025-06-09 10:25:54 +08:00
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,
};
},
},
isFixedErasable: {
type: Boolean,
default: false, // 是否允许擦除固定图层
},
isBackgroundErasable: {
type: Boolean,
default: false, // 是否允许擦除背景图层
},
showFixedLayer: {
type: Boolean,
default: false, // 是否显示固定图层
},
isGeneral: {
// 从generalMiniCanvas来的
type: Boolean,
default: false,
},
isEdit: {
// 从design点击喜欢过的图片,再点击顶部的编辑图标
type: Boolean,
default: false,
},
2025-11-05 16:28:37 +08:00
hideCanvas: {
type: Boolean,
default: false, // 是否隐藏画布-隐藏关闭部分功能
},
2025-06-09 10:25:54 +08:00
});
// 引用和状态
const canvasRef = ref(null);
const canvasContainerRef = shallowRef(null);
const imageUploadRef = ref(null);
const currentZoom = ref(100);
// 画布设置
2025-06-23 00:40:45 +08:00
const canvasWidth = ref(props.config.width);
const canvasHeight = ref(props.config.height);
const canvasColor = ref(props.config.backgroundColor);
// 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-08-22 10:27:48 +08:00
const isShowLayerPanel = ref(false); // 是否显示图层面板
2025-06-18 11:05:23 +08:00
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 lastSelectLayerId = ref(null); // 最后选择的图层ID
2025-06-09 10:25:54 +08:00
// 当前选择的工具
const activeTool = ref(CanvasConfig.defaultTool); // 默认工具
2025-07-22 18:16:33 +08:00
//监听画布元素宽度是否发生变化
let observer = null;
let observerTime = null; //加入防抖
2025-07-22 18:16:33 +08:00
2025-06-09 10:25:54 +08:00
// 管理器实例
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;
}
2025-11-05 16:28:37 +08:00
watch(()=>props.hideCanvas, (newVal)=>{
console.log("==========是否隐藏画布",newVal)
if(newVal){
keyboardManager?.removeEvents()
}else {
keyboardManager?.init()
}
})
2025-06-09 10:25:54 +08:00
// 工具选择处理
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 handleCanvasInit(isLoadJson = false) {
emit("canvasInit", {
isLoadJson: isLoadJson ?? !!props.canvasJSON, // 是否加载了JSON数据
isRedGreenMode: isRedGreenMode.value,
layers,
activeLayerId,
canvasManager,
layerManager,
commandManager,
toolManager,
keyboardManager,
liquifyManager,
selectionManager,
redGreenModeManager,
});
}
2025-06-09 10:25:54 +08:00
function toggleMinimap(enabled) {
// minimapEnabled.value = enabled;
// if (minimapManager.value) {
// minimapManager.value.setVisibility(enabled);
// }
}
// 初始化画布
onMounted(async () => {
2025-06-29 23:29:47 +08:00
// 设置BrushStore的全局引用供BaseBrush使用
if (typeof window !== "undefined") {
window.BrushStore = BrushStore;
}
2025-06-09 10:25:54 +08:00
// 如果启用了红绿图模式,设置画布大小为默认值
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,
lastSelectLayerId,
2025-06-09 10:25:54 +08:00
canvasWidth,
canvasHeight,
canvasColor,
enabledRedGreenMode: props.enabledRedGreenMode,
isFixedErasable: props.isFixedErasable,
2025-06-09 10:25:54 +08:00
});
canvasManager.canvas.activeLayerId = activeLayerId;
2025-07-24 21:37:21 +08:00
canvasManager.activeLayerId = activeLayerId;
2025-06-09 10:25:54 +08:00
canvasManager.canvas.activeElementId = activeElementId;
// 创建命令管理器
commandManager = new CommandManager({
canvas: canvasManager.canvas,
autoSaveState: true,
});
// 创建图层管理器
layerManager = new LayerManager({
canvas: canvasManager.canvas,
canvasWidth: canvasWidth.value,
canvasHeight: canvasHeight.value,
2025-06-29 23:29:47 +08:00
backgroundColor: canvasColor,
2025-06-18 11:05:23 +08:00
isRedGreenMode: props.enabledRedGreenMode,
lastSelectLayerId,
2025-06-09 10:25:54 +08:00
layers,
activeLayerId,
canvasManager, // 添加对 canvasManager 的引用
commandManager, // 添加对命令管理器的引用
t, // 国际化函数
2025-06-09 10:25:54 +08:00
});
// commandManager.setLayerManager(layerManager); // 设置命令管理器需要访问的图层数据
// 设置缩略图管理器需要访问的图层数据
// canvasManager.layers = layers;
// 创建工具管理器实例
toolManager = new ToolManager({
canvas: canvasManager.canvas, // fabric.js 画布实例
commandManager, // 命令管理器实例,用于撤销/重做
canvasManager, // 画布管理器实例
layerManager,
activeTool, // 响应式引用,存储当前选中的工具
brushSize: brushSize.value, // 可选,初始画笔大小
t, // 国际化函数
2025-06-09 10:25:54 +08:00
});
// 初始化文本编辑功能
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();
// 绑定画布操作事件
canvasManager.setupCanvasEvents(activeElementId, layerManager);
canvasManager.setupCanvasInitEvent(handleCanvasInit); // 绑定画布初始化事件
2025-06-09 10:25:54 +08:00
provide("canvasManager", canvasManager); // 提供给子组件使用
provide("layerManager", layerManager); // 提供给子组件使用
provide("commandManager", commandManager); // 提供给子组件使用
provide("toolManager", toolManager); // 提供给子组件使用
provide("keyboardManager", keyboardManager); // 提供给子组件使用
provide("activeTool", activeTool); // 提供给子组件使用
provide("liquifyManager", () => liquifyManager); // 提供液化管理器
provide("texturePresetManager", texturePresetManager); // 提供纹理预设管理器
2025-06-09 10:25:54 +08:00
provide("layers", layers); // 提供图层数据
provide("lastSelectLayerId", lastSelectLayerId); // 提供最后选择的图层ID
2025-06-09 10:25:54 +08:00
// 初始化网格设置
// 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
) {
2025-06-09 10:25:54 +08:00
canvasManager.canvas.fill = "#fff"; // 设置画布背景色为白色 // 初始化红绿图模式管理器
2025-09-17 14:01:58 +08:00
redGreenModeManager = new RedGreenModeManager({
2025-06-09 10:25:54 +08:00
canvas: canvasManager.canvas,
canvasManager,
layerManager,
toolManager,
commandManager,
clothingImageOpts: props.clothingImageOpts,
2025-06-09 10:25:54 +08:00
});
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(); // 设置画布大小
2025-06-23 00:40:45 +08:00
} else if (!isRedGreenMode.value && props.clothingImageUrl) {
try {
await canvasManager?.changeFixedImage?.(props.clothingImageUrl, {
undoable: false, // 不可撤销操作
...(props?.clothingImageOpts || {}),
});
} catch (error) {
console.error("更换底图失败:", error);
}
2025-06-23 00:40:45 +08:00
canvasManager?.centerBackgroundLayer?.(
canvasManager.canvas.width,
canvasManager.canvas.height
);
2025-06-09 10:25:54 +08:00
}
// // 设置固定图层是否可擦除
// 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);
2025-09-01 14:03:30 +08:00
// 触发组件初始化事件
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, // 设置操作类型为可擦除
});
}
2025-06-09 10:25:54 +08:00
});
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;
// fabric.Object.prototype.controls.deleteControl = undefined;
2025-06-09 10:25:54 +08:00
// 移除window resize事件监听
// window.removeEventListener("resize", handleWindowResize);
observer.unobserve(canvasContainerRef.value);
2025-06-09 10:25:54 +08:00
});
// 窗口大小变化处理函数
function handleWindowResize() {
console.log(132);
2025-06-09 10:25:54 +08:00
// 使用requestAnimationFrame来防止频繁更新
setTimeout(() => {
2025-06-09 10:25:54 +08:00
// 更新画布大小并自动居中所有元素
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);
});
}
2025-06-09 10:25:54 +08:00
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);
}
function createLayerName(){
const layer = t("Canvas.layer")
// 检查图层名称是否已存在
let layerIndex = 1;
let layerName = `${layer + " " + layerIndex}`;
while (layerManager.getLayerByName(layerName)) {
layerIndex++;
layerName = `${layer} ${layerIndex}`;
}
return layerName;
}
2025-06-22 13:52:28 +08:00
async function addLayer() {
await layerManager.createLayer(createLayerName());
}
async function addTopLayer() {
await layerManager.createLayer(createLayerName(), LayerType.EMPTY, {
insertTop: true,
});
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 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,
});
2025-09-01 14:03:30 +08:00
}
function deleteFun() {
removeLayer(layerManager.activeLayerId.value);
2025-09-01 14:03:30 +08:00
}
2025-06-09 10:25:54 +08:00
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."
);
2025-06-09 10:25:54 +08:00
return;
}
2025-06-09 10:25:54 +08:00
layerManager.removeLayer(layerId);
2025-11-05 16:28:37 +08:00
// 此处删除画布上内容导致撤回操作无效(多余)
// 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();
// }
// }
2025-06-09 10:25:54 +08:00
}
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] } });
});
};
2025-09-22 15:30:26 +08:00
function triggerLibrary() {
// console.log('CanvasEditor', '打开收藏')
if (props.isGeneral || props.isEdit) {
selectImages.value.init();
} else {
emit("trigger-library");
}
2025-09-22 15:30:26 +08:00
}
2025-06-09 10:25:54 +08:00
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}`
);
2025-06-09 10:25:54 +08:00
// 更新画布渲染顺序
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
);
2025-06-09 10:25:54 +08:00
if (success) {
console.log(
`子图层 ${layerId} 在父图层 ${parentId} 中已从位置 ${oldIndex} 移动到位置 ${newIndex}`
);
// 更新画布渲染顺序
if (canvasManager) {
canvasManager.canvas.renderAll();
}
} else {
console.warn("子图层排序失败");
}
}
}
// 处理画布变更事件
2025-09-08 14:50:59 +08:00
const changeCanvas = async (command) => {
const commandData = {
isChange: command.canUndo || command.canRedo, // 是否有可撤销或可重做的操作
...command, // 传递完整的命令数据
};
emit("changeCanvas", commandData);
if (command.canUndo || command.canRedo) {
setTimeout(async () => {
const imageData = await canvasManager.exportImage({
restoreOpacityInRedGreen: true, // 恢复红绿图模式下的透明度
isCropByBg: true,
});
emit("trigger-red-green-mouseup", imageData);
}, 100);
2025-09-08 14:50:59 +08:00
}
};
2025-06-09 10:25:54 +08:00
// 提供外部ref实例方法
defineExpose({
layers, // 图层数据
activeTool, // 当前选中的工具
2025-06-09 10:25:54 +08:00
getCanvasManager: () => canvasManager, // 获取画布管理器实例
// type : isBackground isFixed flag: 是否可擦除图层
setFixedLayerErasable: ({ type = "isFixed", flag = false }) => {
canvasManager?.setFixedLayerErasable({
type,
flag, // 设置操作类型为可擦除
});
}, // 获取fabric画布实例
2025-06-09 10:25:54 +08:00
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, {
...(props?.clothingImageOpts || {}),
...opts,
});
2025-06-09 10:25:54 +08:00
},
2025-06-23 00:40:45 +08:00
//图片url或者base64
addImageToLayer: async (
url,
2025-06-29 23:29:47 +08:00
{ layerId, undoable, ...optios } = { layerId: null, undoable: true } // 可选参数 layerId 指定图层 将内容添加到指定图层 undoable 是否可撤销 false不可撤销 默认可撤销
) => {
2025-06-23 00:40:45 +08:00
if (!url) return Promise.reject(new Error("图片URL不能为空"));
if (layerId) {
const fabricImage = await loadImage(url);
// 如果指定了图层ID确保图层存在
2025-06-29 23:29:47 +08:00
return await canvasManager?.addImageToLayer?.({
targetLayerId: layerId,
fabricImage,
undoable, // 是否可撤销操作
2025-06-29 23:29:47 +08:00
...optios,
});
}
// 未指定图层ID默认添加到新的图层
return await loadImageUrlToLayer(
{
imageUrl: url,
layerManager,
canvas: canvasManager.canvas,
toolManager,
},
2025-06-29 23:29:47 +08:00
{ undoable, ...optios }
);
2025-06-09 10:25:54 +08:00
},
// 导出图片
exportImage: ({
isContainBg = false, // 是否包含背景图层
isContainFixed = false, // 是否包含固定图层
isCropByBg = false, // 是否使用背景大小裁剪 // 如果为true则导出时裁剪到背景图层大小
2025-06-09 10:25:54 +08:00
layerId = "", // 导出具体图层ID
layerIdArray = [], // 导出多个图层ID数组
expPicType = "png", // 导出图片类型 JPG 或 PNG ,SVG
isEnhanceImg, // 是否是增强图片
2025-06-09 10:25:54 +08:00
} = {}) => {
return canvasManager.exportImage({
isContainBg,
isContainFixed,
isCropByBg,
2025-06-09 10:25:54 +08:00
layerId,
layerIdArray,
expPicType,
isEnhanceImg,
2025-06-09 10:25:54 +08:00
});
},
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 };
2025-06-18 11:05:23 +08:00
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);
},
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"
2025-06-23 00:40:45 +08:00
:showLayersPanel="showLayersPanel"
2025-06-18 11:05:23 +08:00
@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>
2025-06-18 11:05:23 +08:00
</div>
2025-06-09 10:25:54 +08:00
<div class="main-content">
<!-- :minimapEnabled="minimapEnabled" -->
<!-- 工具栏组件 -->
2025-07-24 20:15:39 +08:00
<div style="min-width: 5.8rem">
2025-06-18 11:05:23 +08:00
<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"
2025-09-22 15:30:26 +08:00
@trigger-library="triggerLibrary"
>
<template #customToolsTop="{ toolTopProps }">
2025-09-12 14:58:07 +08:00
<slot name="customToolsTop" :tool-button-props="toolTopProps" />
</template>
<!-- 扩展插槽 -->
2025-09-12 14:58:07 +08:00
<template #customToolsBottom="{ toolButtonProps }">
<slot
name="customToolsBottom"
:tool-button-props="toolButtonProps"
/>
</template>
</ToolsSidebar>
2025-06-18 11:05:23 +08:00
</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"
/>
2025-06-09 10:25:54 +08:00
<!-- 文本编辑面板 -->
<TextEditorPanel
2025-06-18 11:05:23 +08:00
v-if="canvasManagerLoaded && !enabledRedGreenMode"
:canvas="canvasManager && canvasManager.canvas"
2025-06-09 10:25:54 +08:00
:commandManager="commandManager"
/>
<!-- 液化编辑面板 -->
<LiquifyPanel
2025-06-18 11:05:23 +08:00
v-if="canvasManagerLoaded && !enabledRedGreenMode"
:canvas="canvasManager && canvasManager.canvas"
2025-06-09 10:25:54 +08:00
:commandManager="commandManager"
:liquifyManager="liquifyManager"
:layerManager="layerManager"
:activeTool="activeTool"
/>
<!-- 选区面板 -->
<SelectionPanel
2025-06-18 11:05:23 +08:00
v-if="canvasManagerLoaded && !enabledRedGreenMode"
:canvas="canvasManager && canvasManager.canvas"
2025-06-09 10:25:54 +08:00
: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"
/> -->
2025-06-09 10:25:54 +08:00
<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')"
>
2025-06-09 10:25:54 +08:00
?
</button>
</div>
</div>
<!-- 图层面板组件 -->
2025-06-18 11:05:23 +08:00
<!-- v-if="canvasManagerLoaded && !enabledRedGreenMode" -->
<transition name="fade">
2025-06-23 00:40:45 +08:00
<div
class="layers-panel"
v-if="isShowLayerPanel && !enabledRedGreenMode && showLayersPanel"
>
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"
:showFixedLayer="showFixedLayer"
2025-06-18 11:05:23 +08:00
@add-layer="addLayer"
@add-top-layer="addTopLayer"
2025-06-18 11:05:23 +08:00
@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>
2025-06-23 00:40:45 +08:00
<!-- <div class="footer-actions">
2025-06-09 10:25:54 +08:00
<button class="share-btn">Share</button>
<button class="export-btn">Export</button>
2025-06-23 00:40:45 +08:00
</div> -->
2025-06-09 10:25:54 +08:00
<!-- 快捷键帮助模态框 -->
<div
v-if="showShortcutHelp"
class="modal-overlay"
@click="showShortcutHelp = false"
>
2025-06-09 10:25:54 +08:00
<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
/>
2025-06-09 10:25:54 +08:00
</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;
2025-06-09 10:25:54 +08:00
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
2025-07-24 20:15:39 +08:00
z-index: 100;
2025-06-18 11:05:23 +08:00
& > .header-menu {
2025-07-24 20:15:39 +08:00
height: 5.2rem;
2025-06-18 11:05:23 +08:00
overflow: hidden;
display: flex;
align-items: center;
2025-07-24 20:15:39 +08:00
padding: 0 2rem;
2025-06-18 11:05:23 +08:00
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(
2025-06-09 10:25:54 +08:00
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),
2025-06-09 10:25:54 +08:00
calc(var(--size) + var(--offsetX)) calc(var(--size) + var(--offsetY));
background-size: calc(var(--size) * 2) calc(var(--size) * 2);
}
.zoom-info {
position: absolute;
2025-07-24 20:15:39 +08:00
bottom: 1rem;
right: 1rem;
2025-06-09 10:25:54 +08:00
background: rgba(255, 255, 255, 0.7);
2025-07-24 21:37:21 +08:00
padding: 0.5rem 1rem;
border-radius: 0.4rem;
2025-07-24 20:15:39 +08:00
font-size: 1.4rem;
2025-06-09 10:25:54 +08:00
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
color: #666;
2025-06-23 00:40:45 +08:00
display: flex;
2025-06-09 10:25:54 +08:00
}
.zoom-hint {
position: absolute;
2025-07-24 20:15:39 +08:00
top: 1rem;
left: 1rem;
2025-06-09 10:25:54 +08:00
background: rgba(255, 255, 255, 0.8);
2025-07-24 21:37:21 +08:00
padding: 0.5rem 1rem;
border-radius: 0.4rem;
2025-07-24 20:15:39 +08:00
font-size: 1.2rem;
2025-06-09 10:25:54 +08:00
color: #666;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.reset-zoom {
2025-07-24 20:15:39 +08:00
margin-left: 1rem;
2025-06-09 10:25:54 +08:00
cursor: pointer;
2025-07-24 21:37:21 +08:00
padding: 0.2rem 0.5rem;
2025-07-24 20:15:39 +08:00
font-size: 1.2rem;
2025-06-09 10:25:54 +08:00
border: 1px solid #ddd;
background: #f8f8f8;
2025-07-24 21:37:21 +08:00
border-radius: 0.3rem;
2025-06-09 10:25:54 +08:00
}
.footer-actions {
display: flex;
justify-content: center;
2025-07-24 20:15:39 +08:00
gap: 1rem;
padding: 1.5rem;
2025-06-09 10:25:54 +08:00
border-top: 1px solid #e0e0e0;
}
.share-btn,
.export-btn {
2025-07-24 21:37:21 +08:00
padding: 0.8rem 2rem;
2025-06-09 10:25:54 +08:00
border: none;
2025-07-24 20:15:39 +08:00
border-radius: 2rem;
2025-06-09 10:25:54 +08:00
background-color: #000;
color: #fff;
2025-07-24 20:15:39 +08:00
font-size: 1.4rem;
2025-06-09 10:25:54 +08:00
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.share-btn:hover,
.export-btn:hover {
opacity: 0.9;
}
button {
2025-07-24 20:15:39 +08:00
font-size: 1.3rem;
2025-06-09 10:25:54 +08:00
transition: all 0.2s;
}
button:hover {
background: #e6e6e6;
}
.help-btn {
2025-07-24 20:15:39 +08:00
margin-left: 1rem;
2025-06-09 10:25:54 +08:00
cursor: pointer;
2025-07-24 20:15:39 +08:00
width: 2.4rem;
height: 2.4rem;
2025-06-09 10:25:54 +08:00
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%;
2025-07-24 20:15:39 +08:00
max-width: 60rem;
2025-06-09 10:25:54 +08:00
max-height: 90vh;
overflow-y: auto;
background-color: #fff;
2025-07-24 21:37:21 +08:00
border-radius: 0.8rem;
2025-07-24 20:15:39 +08:00
padding: 2rem;
2025-06-09 10:25:54 +08:00
}
.close-modal {
position: absolute;
2025-07-24 20:15:39 +08:00
top: 1rem;
right: 1rem;
width: 3rem;
height: 3rem;
2025-06-09 10:25:54 +08:00
border-radius: 50%;
border: none;
background-color: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
2025-07-24 20:15:39 +08:00
font-size: 2rem;
2025-06-09 10:25:54 +08:00
cursor: pointer;
}
/* 网格控制面板样式 */
.grid-controls {
position: absolute;
2025-07-24 20:15:39 +08:00
bottom: 11.5rem;
right: 1rem;
2025-06-09 10:25:54 +08:00
background: rgba(255, 255, 255, 0.9);
2025-07-24 20:15:39 +08:00
padding: 1rem;
2025-07-24 21:37:21 +08:00
border-radius: 0.4rem;
2025-06-09 10:25:54 +08:00
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
2025-07-24 21:37:21 +08:00
gap: 0.8rem;
2025-06-09 10:25:54 +08:00
z-index: 5;
}
.layers-panel {
2025-06-18 11:05:23 +08:00
position: absolute;
2025-07-24 20:15:39 +08:00
right: 2rem;
top: 1rem;
2025-06-09 10:25:54 +08:00
transition: width 0.3s ease;
background: #fff;
2025-07-24 20:15:39 +08:00
width: 35rem;
2025-10-16 13:31:24 +08:00
max-height: 90%;
display: flex;
overflow: hidden;
2025-07-24 21:37:21 +08:00
box-shadow: 0 0.4rem 2rem rgba(0, 0, 0, 0.1); /* 添加阴影效果 */
2025-06-18 11:05:23 +08:00
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;
2025-07-24 21:37:21 +08:00
top: -0.9rem;
right: 0.6rem;
2025-06-18 11:05:23 +08:00
width: 0;
height: 0;
2025-07-24 20:15:39 +08:00
border-left: 1rem solid transparent;
border-right: 1rem solid transparent;
border-bottom: 1rem solid rgba(255, 255, 255, 0.95); /* 与面板背景色一致 */
2025-06-18 11:05:23 +08:00
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 {
2025-07-24 20:15:39 +08:00
width: 4.4rem;
height: 4.4rem;
font-size: 1.8rem;
2025-06-09 10:25:54 +08:00
}
.layers-panel {
2025-07-24 20:15:39 +08:00
width: 28rem;
2025-06-09 10:25:54 +08:00
}
.layer-item,
.element-item {
2025-07-24 21:37:21 +08:00
padding: 1.2rem 0.8rem;
2025-06-09 10:25:54 +08:00
}
.element-action-btn,
.element-delete-btn {
2025-07-24 20:15:39 +08:00
width: 2.4rem;
height: 2.4rem;
2025-06-09 10:25:54 +08:00
}
.help-btn {
2025-07-24 20:15:39 +08:00
width: 3.2rem;
height: 3.2rem;
2025-06-09 10:25:54 +08:00
}
.modal-content {
width: 90%;
2025-07-24 20:15:39 +08:00
padding: 1.6rem;
2025-06-09 10:25:54 +08:00
}
.close-modal {
2025-07-24 20:15:39 +08:00
width: 4rem;
height: 4rem;
font-size: 2.4rem;
2025-06-09 10:25:54 +08:00
}
}
2025-06-18 11:05:23 +08:00
// 淡入淡出动画
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s, transform 0.3s;
2025-06-18 11:05:23 +08:00
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
2025-07-24 20:15:39 +08:00
transform: translateY(1rem);
2025-06-18 11:05:23 +08:00
}
2025-06-09 10:25:54 +08:00
</style>