1428 lines
39 KiB
Vue
1428 lines
39 KiB
Vue
<script setup>
|
||
import {
|
||
ref,
|
||
onMounted,
|
||
onBeforeUnmount,
|
||
defineAsyncComponent,
|
||
shallowRef,
|
||
provide,
|
||
defineExpose,
|
||
nextTick,
|
||
watchEffect,
|
||
} from "vue";
|
||
import { CanvasManager } from "./managers/CanvasManager";
|
||
import { LayerManager } from "./managers/LayerManager";
|
||
import { CommandManager } from "./managers/command/CommandManager";
|
||
import { KeyboardManager } from "./managers/events/KeyboardManager.js";
|
||
import CanvasConfig from "./config/canvasConfig.js";
|
||
import { LiquifyManager } from "./managers/liquify/LiquifyManager";
|
||
import { SelectionManager } from "./managers/selection/SelectionManager";
|
||
import { RedGreenModeManager } from "./managers/RedGreenModeManager";
|
||
import texturePresetManager from "./managers/brushes/TexturePresetManager";
|
||
import { BrushStore } from "./store/BrushStore";
|
||
|
||
// import { MinimapManager } from "./managers/minimap/MinimapManager";
|
||
|
||
// 导入封装的组件
|
||
import ToolsSidebar from "./components/ToolsSidebar.vue";
|
||
import HeaderMenu from "./components/HeaderMenu.vue";
|
||
import LayersPanel from "./components/LayersPanel/LayersPanel.vue";
|
||
import BrushControlPanel from "./components/BrushControlPanel.vue";
|
||
import TextEditorPanel from "./components/TextEditorPanel.vue"; // 引入文本编辑面板
|
||
import LiquifyPanel from "./components/LiquifyPanel.vue"; // 引入液化编辑面板
|
||
import SelectionPanel from "./components/SelectionPanel.vue"; // 引入选区面板
|
||
import { LayerType, OperationType } from "./utils/layerHelper.js";
|
||
import { ToolManager } from "./managers/toolManager.js";
|
||
// import { fabric } from "fabric-with-all";
|
||
import {
|
||
uploadImageAndCreateLayer,
|
||
loadImageUrlToLayer,
|
||
loadImage,
|
||
} from "./utils/imageHelper.js";
|
||
// import MinimapPanel from "./components/MinimapPanel.vue";
|
||
import { useI18n } from "vue-i18n";
|
||
const { t } = useI18n();
|
||
const KeyboardShortcutHelp = defineAsyncComponent(() =>
|
||
import("./components/KeyboardShortcutHelp.vue")
|
||
);
|
||
|
||
const emit = defineEmits([
|
||
"trigger-red-green-mouseup", // 红绿图模式鼠标抬起事件
|
||
"changeCanvas", // 画布变更事件
|
||
"canvasInit", // 画布初始化事件
|
||
]);
|
||
|
||
const props = defineProps({
|
||
canvasJSON: {
|
||
type: [Object, String],
|
||
default: "", // 默认空
|
||
},
|
||
config: {
|
||
type: Object,
|
||
default: () => CanvasConfig, // 默认配置
|
||
},
|
||
showLayersPanel: {
|
||
type: Boolean,
|
||
default: true, // 是否显示图层面板
|
||
},
|
||
enabledRedGreenMode: {
|
||
type: Boolean,
|
||
default: false, // 是否启用红绿图模式
|
||
},
|
||
clothingImageUrl: {
|
||
type: String,
|
||
default: "", // 衣服底图URL
|
||
},
|
||
redGreenImageUrl: {
|
||
type: String,
|
||
default: "", // 红绿图URL
|
||
},
|
||
clothingImageOpts: {
|
||
type: Object,
|
||
default: () => {
|
||
return {
|
||
scaleX: 1,
|
||
scaleY: 1,
|
||
};
|
||
},
|
||
},
|
||
isFixedErasable: {
|
||
type: Boolean,
|
||
default: false, // 是否允许擦除固定图层
|
||
},
|
||
isBackgroundErasable: {
|
||
type: Boolean,
|
||
default: false, // 是否允许擦除背景图层
|
||
},
|
||
|
||
showFixedLayer: {
|
||
type: Boolean,
|
||
default: false, // 是否显示固定图层
|
||
},
|
||
});
|
||
|
||
// 引用和状态
|
||
const canvasRef = ref(null);
|
||
const canvasContainerRef = shallowRef(null);
|
||
const imageUploadRef = ref(null);
|
||
const currentZoom = ref(100);
|
||
|
||
// 画布设置
|
||
const canvasWidth = ref(props.config.width);
|
||
const canvasHeight = ref(props.config.height);
|
||
const canvasColor = ref(props.config.backgroundColor);
|
||
// const layerWidth = ref(CanvasConfig.layerWidth);
|
||
const brushSize = ref(CanvasConfig.brushSize); // 画笔大小
|
||
const canvasManagerLoaded = ref(false); // 画布是否加载完成
|
||
|
||
// 红绿图模式状态
|
||
const isRedGreenMode = ref(false);
|
||
|
||
const isShowLayerPanel = ref(true); // 是否显示图层面板
|
||
|
||
provide("isShowLayerPanel", isShowLayerPanel); // 提供红绿图模式状态给子组件
|
||
|
||
// 小地图设置
|
||
// const minimapEnabled = ref(true);
|
||
// const minimapManager = ref(null);
|
||
|
||
// 图层和元素管理
|
||
const layers = ref([]);
|
||
const activeLayerId = ref(null);
|
||
const activeElementId = ref(null);
|
||
const lastSelectLayerId = ref(null); // 最后选择的图层ID
|
||
|
||
// 当前选择的工具
|
||
const activeTool = ref(CanvasConfig.defaultTool); // 默认工具
|
||
|
||
//监听画布元素宽度是否发生变化
|
||
let observer = null;
|
||
let observerTime = null; //加入防抖
|
||
|
||
// 管理器实例
|
||
let canvasManager = null;
|
||
let layerManager = null;
|
||
let commandManager = null;
|
||
let keyboardManager = null;
|
||
let toolManager = null;
|
||
let liquifyManager = null;
|
||
let selectionManager = null;
|
||
let redGreenModeManager = null;
|
||
|
||
// 快捷键帮助模态框状态
|
||
const showShortcutHelp = ref(false);
|
||
|
||
function toggleShortcutHelp() {
|
||
showShortcutHelp.value = !showShortcutHelp.value;
|
||
}
|
||
|
||
// 工具选择处理
|
||
function handleToolSelect(tool) {
|
||
activeTool.value = tool;
|
||
// toolManager.setActiveTool(tool); // 更新工具管理器中的当前工具 普通模式,不可撤回操作
|
||
toolManager.setToolWithCommand(tool, {
|
||
undoable: props.enabledRedGreenMode ? false : true, // 普通模式下工具选择不可撤销
|
||
}); // 命令模式 可撤回操作
|
||
}
|
||
|
||
// 触发组件初始化事件
|
||
function handleCanvasInit(isLoadJson = false) {
|
||
emit("canvasInit", {
|
||
isLoadJson: isLoadJson ?? !!props.canvasJSON, // 是否加载了JSON数据
|
||
isRedGreenMode: isRedGreenMode.value,
|
||
layers,
|
||
activeLayerId,
|
||
canvasManager,
|
||
layerManager,
|
||
commandManager,
|
||
toolManager,
|
||
keyboardManager,
|
||
liquifyManager,
|
||
selectionManager,
|
||
redGreenModeManager,
|
||
});
|
||
}
|
||
|
||
function toggleMinimap(enabled) {
|
||
// minimapEnabled.value = enabled;
|
||
// if (minimapManager.value) {
|
||
// minimapManager.value.setVisibility(enabled);
|
||
// }
|
||
}
|
||
|
||
// 初始化画布
|
||
onMounted(async () => {
|
||
// 设置BrushStore的全局引用,供BaseBrush使用
|
||
if (typeof window !== "undefined") {
|
||
window.BrushStore = BrushStore;
|
||
}
|
||
|
||
// 如果启用了红绿图模式,设置画布大小为默认值
|
||
if (props.enabledRedGreenMode) {
|
||
canvasHeight.value = canvasContainerRef.value.clientWidth;
|
||
canvasWidth.value = canvasContainerRef.value.clientHeight;
|
||
}
|
||
|
||
// 创建管理器实例
|
||
canvasManager = new CanvasManager(canvasRef.value, {
|
||
width: canvasContainerRef.value.clientWidth,
|
||
height: canvasContainerRef.value.clientHeight,
|
||
// backgroundColor: canvasColor.value,
|
||
currentZoom,
|
||
layers,
|
||
lastSelectLayerId,
|
||
canvasWidth,
|
||
canvasHeight,
|
||
canvasColor,
|
||
enabledRedGreenMode: props.enabledRedGreenMode,
|
||
isFixedErasable: props.isFixedErasable,
|
||
});
|
||
canvasManager.canvas.activeLayerId = activeLayerId;
|
||
canvasManager.canvas.activeElementId = activeElementId;
|
||
|
||
// 创建命令管理器
|
||
commandManager = new CommandManager({
|
||
canvas: canvasManager.canvas,
|
||
autoSaveState: true,
|
||
});
|
||
|
||
// 创建图层管理器
|
||
layerManager = new LayerManager({
|
||
canvas: canvasManager.canvas,
|
||
canvasWidth: canvasWidth.value,
|
||
canvasHeight: canvasHeight.value,
|
||
backgroundColor: canvasColor,
|
||
isRedGreenMode: props.enabledRedGreenMode,
|
||
lastSelectLayerId,
|
||
layers,
|
||
activeLayerId,
|
||
canvasManager, // 添加对 canvasManager 的引用
|
||
commandManager, // 添加对命令管理器的引用
|
||
t, // 国际化函数
|
||
});
|
||
|
||
// commandManager.setLayerManager(layerManager); // 设置命令管理器需要访问的图层数据
|
||
|
||
// 设置缩略图管理器需要访问的图层数据
|
||
// canvasManager.layers = layers;
|
||
|
||
// 创建工具管理器实例
|
||
toolManager = new ToolManager({
|
||
canvas: canvasManager.canvas, // fabric.js 画布实例
|
||
commandManager, // 命令管理器实例,用于撤销/重做
|
||
canvasManager, // 画布管理器实例
|
||
layerManager,
|
||
activeTool, // 响应式引用,存储当前选中的工具
|
||
brushSize: brushSize.value, // 可选,初始画笔大小
|
||
});
|
||
|
||
// 初始化文本编辑功能
|
||
toolManager.setupTextEditingEvents();
|
||
|
||
toolManager.setFileUploadHandler(triggerImageUpload); // 设置快捷图片上传处理函数
|
||
|
||
layerManager.setToolManager(toolManager); // 将工具管理器传递给图层管理器
|
||
canvasManager.setToolManager(toolManager); // 将工具管理器传递给画布管理器
|
||
canvasManager.setLayerManager(layerManager);
|
||
canvasManager.setCommandManager(commandManager); // 将命令管理器传递给画布管理器
|
||
|
||
// 初始化快捷键管理器
|
||
keyboardManager = new KeyboardManager({
|
||
canvas: canvasManager.canvas,
|
||
commandManager,
|
||
layerManager,
|
||
toolManager,
|
||
});
|
||
|
||
// 绑定快捷键事件
|
||
keyboardManager.init();
|
||
// 绑定画布操作事件
|
||
canvasManager.setupCanvasEvents(activeElementId, layerManager);
|
||
canvasManager.setupCanvasInitEvent(handleCanvasInit); // 绑定画布初始化事件
|
||
|
||
provide("canvasManager", canvasManager); // 提供给子组件使用
|
||
provide("layerManager", layerManager); // 提供给子组件使用
|
||
provide("commandManager", commandManager); // 提供给子组件使用
|
||
provide("toolManager", toolManager); // 提供给子组件使用
|
||
provide("keyboardManager", keyboardManager); // 提供给子组件使用
|
||
provide("activeTool", activeTool); // 提供给子组件使用
|
||
provide("liquifyManager", () => liquifyManager); // 提供液化管理器
|
||
provide("texturePresetManager", texturePresetManager); // 提供纹理预设管理器
|
||
provide("layers", layers); // 提供图层数据
|
||
provide("lastSelectLayerId", lastSelectLayerId); // 提供最后选择的图层ID
|
||
|
||
// 初始化网格设置
|
||
// toggleGridVisibility(gridEnabled.value);
|
||
|
||
// 初始化小地图
|
||
// minimapManager.value = new MinimapManager(canvasManager.canvas);
|
||
|
||
// 初始化液化管理器
|
||
liquifyManager = new LiquifyManager({
|
||
canvas: canvasManager.canvas,
|
||
layerManager,
|
||
});
|
||
// 将liquifyManager设置到canvasManager中,确保ToolManager能访问到它
|
||
canvasManager.setLiquifyManager(liquifyManager);
|
||
|
||
// 初始化选区管理器
|
||
selectionManager = new SelectionManager({
|
||
canvas: canvasManager.canvas,
|
||
layerManager,
|
||
});
|
||
canvasManager.setSelectionManager(selectionManager);
|
||
|
||
if (props.canvasJSON) {
|
||
// 如果传入了初始JSON数据,加载到画布上
|
||
if (typeof props.canvasJSON === "string") {
|
||
try {
|
||
await canvasManager.loadJSON(props.canvasJSON);
|
||
} catch (error) {
|
||
console.error("加载画布JSON失败:", error);
|
||
// 初始化图层 - 确保创建背景层
|
||
await layerManager.initializeLayers();
|
||
}
|
||
} else if (typeof props.canvasJSON === "object") {
|
||
await canvasManager.loadJSON(JSON.stringify(props.canvasJSON));
|
||
}
|
||
} else {
|
||
// 初始化图层 - 确保创建背景层
|
||
await layerManager.initializeLayers();
|
||
}
|
||
|
||
if (
|
||
props.enabledRedGreenMode &&
|
||
props.clothingImageUrl &&
|
||
props.redGreenImageUrl
|
||
) {
|
||
canvasManager.canvas.fill = "#fff"; // 设置画布背景色为白色 // 初始化红绿图模式管理器
|
||
redGreenModeManager = new RedGreenModeManager({
|
||
canvas: canvasManager.canvas,
|
||
canvasManager,
|
||
layerManager,
|
||
toolManager,
|
||
commandManager,
|
||
clothingImageOpts: props.clothingImageOpts,
|
||
});
|
||
|
||
canvasManager.setRedGreenModeManager(redGreenModeManager);
|
||
|
||
// 如果提供了图片URL,立即初始化红绿图模式
|
||
if (props.clothingImageUrl && props.redGreenImageUrl) {
|
||
try {
|
||
await redGreenModeManager.initialize({
|
||
clothingImageUrl: props.clothingImageUrl,
|
||
redGreenImageUrl: props.redGreenImageUrl,
|
||
onImageGenerated: (imageData) => {
|
||
console.log("红绿图生成:", imageData);
|
||
// 这里可以添加图片生成后的回调处理
|
||
emit("trigger-red-green-mouseup", imageData);
|
||
},
|
||
});
|
||
|
||
// 设置红绿图模式状态
|
||
isRedGreenMode.value = true;
|
||
|
||
console.log("红绿图模式已自动启用");
|
||
} catch (error) {
|
||
console.error("红绿图模式初始化失败:", error);
|
||
}
|
||
}
|
||
|
||
// 初始设置
|
||
handleWindowResize(); // 设置画布大小
|
||
} else if (!isRedGreenMode.value && props.clothingImageUrl) {
|
||
try {
|
||
await canvasManager?.changeFixedImage?.(props.clothingImageUrl, {
|
||
undoable: false, // 不可撤销操作
|
||
...(props?.clothingImageOpts || {}),
|
||
});
|
||
} catch (error) {
|
||
console.error("更换底图失败:", error);
|
||
}
|
||
|
||
canvasManager?.centerBackgroundLayer?.(
|
||
canvasManager.canvas.width,
|
||
canvasManager.canvas.height
|
||
);
|
||
}
|
||
|
||
// // 设置固定图层是否可擦除
|
||
// canvasManager.setFixedLayerErasable({
|
||
// type: "isFixed",
|
||
// flag: !props.isFixedErasable, // 设置操作类型为可擦除
|
||
// });
|
||
// // 设置背景图层是否可擦除
|
||
// canvasManager.setFixedLayerErasable({
|
||
// type: "isBackground",
|
||
// flag: !props.isBackgroundErasable, // 设置操作类型为可擦除
|
||
// });
|
||
|
||
canvasManagerLoaded.value = true;
|
||
|
||
// 触发组件初始化事件
|
||
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);
|
||
});
|
||
|
||
watchEffect(() => {
|
||
// 设置固定图层是否可擦除
|
||
if (canvasManagerLoaded.value) {
|
||
canvasManager?.setFixedLayerErasable({
|
||
type: "isFixed",
|
||
flag: !props.isFixedErasable, // 设置操作类型为可擦除
|
||
});
|
||
// 设置背景图层是否可擦除
|
||
canvasManager?.setFixedLayerErasable({
|
||
type: "isBackground",
|
||
flag: !props.isBackgroundErasable, // 设置操作类型为可擦除
|
||
});
|
||
}
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
// if (import.meta.hot) {
|
||
// // 热更新 ?
|
||
// console.log("onBeforeUnmount 开发环境热更新不卸载组件...");
|
||
// return; // 开发环境下不卸载组件
|
||
// }
|
||
console.log("onBeforeUnmount 组件卸载,清理资源...");
|
||
// canvasManager?.dispose?.();
|
||
// commandManager?.dispose?.();
|
||
// layerManager?.dispose?.();
|
||
// keyboardManager?.dispose?.();
|
||
// toolManager?.dispose?.();
|
||
// liquifyManager?.dispose?.();
|
||
// selectionManager?.dispose?.();
|
||
// redGreenModeManager?.dispose?.();
|
||
// minimapManager?.dispose?.();
|
||
canvasManager = null;
|
||
commandManager = null;
|
||
layerManager = null;
|
||
keyboardManager = null;
|
||
toolManager = null;
|
||
liquifyManager = null;
|
||
selectionManager = null;
|
||
redGreenModeManager = null;
|
||
|
||
// 移除window resize事件监听
|
||
// window.removeEventListener("resize", handleWindowResize);
|
||
observer.unobserve(canvasContainerRef.value);
|
||
});
|
||
|
||
// 窗口大小变化处理函数
|
||
function handleWindowResize() {
|
||
console.log(132);
|
||
// 使用requestAnimationFrame来防止频繁更新
|
||
setTimeout(() => {
|
||
// 更新画布大小并自动居中所有元素
|
||
updateCanvasSize();
|
||
|
||
// 确保显示的缩放信息是最新的
|
||
currentZoom.value = Math.round(canvasManager.canvas.getZoom() * 100);
|
||
});
|
||
}
|
||
|
||
function resetZoom() {
|
||
canvasManager.resetZoom();
|
||
}
|
||
|
||
function zoomIn() {
|
||
if (!canvasManager) return;
|
||
|
||
const currentZoom = canvasManager.canvas.getZoom();
|
||
const newZoom = Math.min(currentZoom * 1.2, 20); // 增加20%,最大20倍
|
||
|
||
// 使用画布中心作为缩放点
|
||
const centerPoint = {
|
||
x: canvasManager.canvas.width / 2,
|
||
y: canvasManager.canvas.height / 2,
|
||
};
|
||
|
||
canvasManager.animateZoom(centerPoint, newZoom);
|
||
}
|
||
|
||
function zoomOut() {
|
||
if (!canvasManager) return;
|
||
|
||
const currentZoom = canvasManager.canvas.getZoom();
|
||
const newZoom = Math.max(currentZoom / 1.2, 0.1); // 减少20%,最小0.1倍
|
||
|
||
// 使用画布中心作为缩放点
|
||
const centerPoint = {
|
||
x: canvasManager.canvas.width / 2,
|
||
y: canvasManager.canvas.height / 2,
|
||
};
|
||
|
||
canvasManager.animateZoom(centerPoint, newZoom);
|
||
}
|
||
|
||
function updateCanvasSize() {
|
||
if (canvasManager && canvasContainerRef.value) {
|
||
const containerWidth = canvasContainerRef.value.clientWidth;
|
||
const containerHeight = canvasContainerRef.value.clientHeight;
|
||
|
||
// 普通模式下,更新画布大小,这会同时重置视图和居中所有元素
|
||
canvasManager.setCanvasSize(containerWidth, containerHeight);
|
||
|
||
// // 如果启用了红绿图模式,使用 layerManager 的缩放方法
|
||
// if (props.enabledRedGreenMode && layerManager) {
|
||
// layerManager.resizeCanvasWithScale(containerWidth, containerHeight, {
|
||
// undoable: false, // 可撤销操作
|
||
// });
|
||
// } else {
|
||
// // 普通模式下,更新画布大小,这会同时重置视图和居中所有元素
|
||
// canvasManager.setCanvasSize(containerWidth, containerHeight);
|
||
// }
|
||
}
|
||
}
|
||
|
||
function updateCanvasColor() {
|
||
canvasManager.setCanvasColor(canvasColor.value);
|
||
}
|
||
|
||
async function addLayer() {
|
||
await layerManager.createLayer(t("Canvas.EmptyLayer"));
|
||
}
|
||
async function addTopLayer() {
|
||
await layerManager.createLayer("空图层", LayerType.EMPTY, {
|
||
insertTop: true,
|
||
});
|
||
}
|
||
|
||
function setActiveLayer(layerId) {
|
||
if (layerId !== activeLayerId.value) {
|
||
layerManager.setActiveLayer(layerId, {
|
||
undoable: true, // 可撤销
|
||
});
|
||
|
||
const activeObject = canvasManager.canvas.getActiveObject();
|
||
if (activeObject) {
|
||
canvasManager.canvas.discardActiveObject();
|
||
canvasManager.canvas.renderAll();
|
||
}
|
||
}
|
||
}
|
||
|
||
function toggleLayerVisibility(layerId) {
|
||
layerManager.toggleLayerVisibility(layerId, activeElementId.value);
|
||
}
|
||
|
||
function moveLayerUp(layerId) {
|
||
// 使用命令管理器执行移动图层命令,传递正确的方向参数
|
||
layerManager.moveLayer(layerId, "up");
|
||
}
|
||
|
||
function moveLayerDown(layerId) {
|
||
// 使用命令管理器执行移动图层命令,传递正确的方向参数
|
||
layerManager.moveLayer(layerId, "down");
|
||
}
|
||
|
||
function 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("子图层排序失败");
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理画布变更事件
|
||
const changeCanvas = (command) => {
|
||
const commandData = {
|
||
isChange: command.canUndo || command.canRedo, // 是否有可撤销或可重做的操作
|
||
...command, // 传递完整的命令数据
|
||
};
|
||
emit("changeCanvas", commandData);
|
||
};
|
||
|
||
// 提供外部ref实例方法
|
||
defineExpose({
|
||
layers, // 图层数据
|
||
activeTool, // 当前选中的工具
|
||
getCanvasManager: () => canvasManager, // 获取画布管理器实例
|
||
// type : isBackground isFixed flag: 是否可擦除图层
|
||
setFixedLayerErasable: ({ type = "isFixed", flag = false }) => {
|
||
canvasManager?.setFixedLayerErasable({
|
||
type,
|
||
flag, // 设置操作类型为可擦除
|
||
});
|
||
}, // 获取fabric画布实例
|
||
canvasManagerLoaded,
|
||
// 加载新数据到画布
|
||
loadJSON: (json, calllBack) => {
|
||
try {
|
||
if (json) canvasManager?.loadJSON?.(json, calllBack);
|
||
return true;
|
||
} catch (error) {
|
||
console.error("加载画布JSON失败:", error);
|
||
return false;
|
||
}
|
||
},
|
||
// 获取当前画布的JSON数据
|
||
getJSON: () => {
|
||
return canvasManager?.getJSON?.();
|
||
},
|
||
// (更换底图,不可撤销,不可操作)
|
||
changeFixedImage: (url, opts) => {
|
||
return canvasManager?.changeFixedImage?.(url, {
|
||
...(props?.clothingImageOpts || {}),
|
||
...opts,
|
||
});
|
||
},
|
||
//图片url或者base64
|
||
addImageToLayer: async (
|
||
url,
|
||
{ layerId, undoable, ...optios } = { layerId: null, undoable: true } // 可选参数 layerId 指定图层 将内容添加到指定图层 undoable 是否可撤销 false不可撤销 默认可撤销
|
||
) => {
|
||
if (!url) return Promise.reject(new Error("图片URL不能为空"));
|
||
if (layerId) {
|
||
const fabricImage = await loadImage(url);
|
||
// 如果指定了图层ID,确保图层存在
|
||
return await canvasManager?.addImageToLayer?.({
|
||
targetLayerId: layerId,
|
||
fabricImage,
|
||
undoable, // 是否可撤销操作
|
||
...optios,
|
||
});
|
||
}
|
||
|
||
// 未指定图层ID,默认添加到新的图层
|
||
return await loadImageUrlToLayer(
|
||
{
|
||
imageUrl: url,
|
||
layerManager,
|
||
canvas: canvasManager.canvas,
|
||
toolManager,
|
||
},
|
||
{ undoable, ...optios }
|
||
);
|
||
},
|
||
// 导出图片
|
||
exportImage: ({
|
||
isContainBg = false, // 是否包含背景图层
|
||
isContainFixed = false, // 是否包含固定图层
|
||
isCropByBg = false, // 是否使用背景大小裁剪 // 如果为true,则导出时裁剪到背景图层大小
|
||
layerId = "", // 导出具体图层ID
|
||
layerIdArray = [], // 导出多个图层ID数组
|
||
expPicType = "png", // 导出图片类型 JPG 或 PNG ,SVG
|
||
} = {}) => {
|
||
return canvasManager.exportImage({
|
||
isContainBg,
|
||
isContainFixed,
|
||
isCropByBg,
|
||
layerId,
|
||
layerIdArray,
|
||
expPicType,
|
||
});
|
||
},
|
||
/**
|
||
* 移动图层位置
|
||
* @param {string} layerId 图层ID
|
||
* @param {string} direction 移动方向,'up'或'down'
|
||
* @returns {boolean} 是否移动成功
|
||
*/
|
||
moveLayer(layerId, direction) {
|
||
if (!layerManager) return false;
|
||
const result = layerManager.moveLayer(layerId, direction);
|
||
|
||
// 使用高级排序重建画布顺序
|
||
if (result) {
|
||
layerManager.forceRebuildCanvasOrder();
|
||
}
|
||
|
||
return result;
|
||
},
|
||
|
||
/**
|
||
* 拖拽排序图层
|
||
* @param {number} oldIndex 原索引
|
||
* @param {number} newIndex 新索引
|
||
* @param {string} layerId 图层ID
|
||
* @returns {boolean} 是否排序成功
|
||
*/
|
||
reorderLayers(oldIndex, newIndex, layerId) {
|
||
if (!layerManager) return false;
|
||
|
||
// 优先使用高级排序功能
|
||
if (layerManager.layerSort) {
|
||
return layerManager.advancedReorderLayers(oldIndex, newIndex, layerId);
|
||
} else {
|
||
// 降级到基础排序
|
||
return layerManager.reorderLayers(oldIndex, newIndex, layerId);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 智能排序图层
|
||
* 根据对象类型和位置自动调整图层顺序
|
||
* @param {Array<string>} targetLayerIds 要排序的图层ID数组,null表示排序所有普通图层
|
||
* @returns {boolean} 是否排序成功
|
||
*/
|
||
smartSortLayers(targetLayerIds = null) {
|
||
if (!layerManager) return false;
|
||
return layerManager.smartSortLayers(targetLayerIds);
|
||
},
|
||
|
||
/**
|
||
* 优化图层结构
|
||
* 清理空图层、重新排序等
|
||
* @returns {Object} 优化结果统计
|
||
*/
|
||
optimizeLayerStructure() {
|
||
if (!layerManager)
|
||
return { removedEmptyLayers: 0, mergedLayers: 0, reorderedLayers: 0 };
|
||
return layerManager.optimizeLayerStructure();
|
||
},
|
||
|
||
/**
|
||
* 强制重建画布对象顺序
|
||
* 当图层顺序发生变化后调用此方法确保画布对象顺序正确
|
||
*/
|
||
forceRebuildCanvasOrder() {
|
||
if (!layerManager) return;
|
||
layerManager.forceRebuildCanvasOrder();
|
||
},
|
||
|
||
/**
|
||
* 验证画布对象顺序是否正确
|
||
* @returns {boolean} 顺序是否正确
|
||
*/
|
||
validateObjectOrder() {
|
||
if (!layerManager) return true;
|
||
return layerManager.validateObjectOrder();
|
||
},
|
||
|
||
/**
|
||
* 批量重新排序多个图层
|
||
* @param {Array} reorderOperations 排序操作数组 [{layerId, oldIndex, newIndex}]
|
||
* @returns {boolean} 是否全部操作成功
|
||
*/
|
||
batchReorderLayers(reorderOperations) {
|
||
if (!layerManager) return false;
|
||
return layerManager.batchReorderLayers(reorderOperations);
|
||
},
|
||
/**
|
||
* 切换图层可见性
|
||
* @param {string} layerId 图层ID
|
||
* @returns {boolean} 更新后的可见性状态
|
||
*/
|
||
toggleLayerVisibility(layerId) {
|
||
if (!layerManager) return false;
|
||
return layerManager.toggleLayerVisibility(layerId);
|
||
},
|
||
/**
|
||
* 获取图层可见性状态
|
||
* @param {string} layerId 图层ID
|
||
* @returns {boolean} 图层是否可见
|
||
*/
|
||
getLayerVisibility(layerId) {
|
||
if (!layerManager) return false;
|
||
return layerManager.getLayerVisibility(layerId);
|
||
},
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div class="app-container">
|
||
<!-- 头部菜单组件 -->
|
||
<div class="header-menu">
|
||
<HeaderMenu
|
||
v-if="canvasManagerLoaded"
|
||
:activeTool="activeTool"
|
||
:canvasWidth="canvasWidth"
|
||
:canvasHeight="canvasHeight"
|
||
:canvasColor="canvasColor"
|
||
:brushSize="brushSize"
|
||
:enabledRedGreenMode="enabledRedGreenMode"
|
||
:showLayersPanel="showLayersPanel"
|
||
@update:canvasWidth="canvasWidth = $event"
|
||
@update:canvasHeight="canvasHeight = $event"
|
||
@update:canvasColor="canvasColor = $event"
|
||
@update:brushSize="brushSize = $event"
|
||
@canvas-size-change="updateCanvasSize"
|
||
@canvas-color-change="updateCanvasColor"
|
||
>
|
||
<!-- 菜单扩展插槽 -->
|
||
<template #existsImageList>
|
||
<slot name="existsImageList" />
|
||
</template>
|
||
</HeaderMenu>
|
||
</div>
|
||
|
||
<div class="main-content">
|
||
<!-- :minimapEnabled="minimapEnabled" -->
|
||
<!-- 工具栏组件 -->
|
||
<div style="min-width: 5.8rem">
|
||
<ToolsSidebar
|
||
v-if="canvasManagerLoaded"
|
||
:activeTool="activeTool"
|
||
:isRedGreenMode="isRedGreenMode"
|
||
@tool-selected="handleToolSelect"
|
||
@red-green-tool-selected="handleRedGreenToolSelect"
|
||
@toggle-red-green-mode="toggleRedGreenMode"
|
||
@trigger-image-upload="triggerImageUpload"
|
||
@add-text="handleAddText"
|
||
@zoom-in="zoomIn"
|
||
@zoom-out="zoomOut"
|
||
@undo-redo-status-changed="changeCanvas"
|
||
>
|
||
<!-- 扩展插槽 -->
|
||
<template #customTools="{ toolButtonProps }">
|
||
<slot name="customTools" :tool-button-props="toolButtonProps" />
|
||
</template>
|
||
</ToolsSidebar>
|
||
</div>
|
||
|
||
<div
|
||
class="canvas-container"
|
||
:class="{ 'background-grid': !enabledRedGreenMode }"
|
||
ref="canvasContainerRef"
|
||
>
|
||
<canvas ref="canvasRef"></canvas>
|
||
<!-- 小地图组件 -->
|
||
<!-- <MinimapPanel v-if="minimapEnabled" :minimapManager="minimapManager" /> -->
|
||
|
||
<!-- 笔刷控制面板 -->
|
||
<BrushControlPanel
|
||
v-if="canvasManagerLoaded"
|
||
:activeTool="activeTool"
|
||
/>
|
||
|
||
<!-- 文本编辑面板 -->
|
||
<TextEditorPanel
|
||
v-if="canvasManagerLoaded && !enabledRedGreenMode"
|
||
:canvas="canvasManager?.canvas"
|
||
:commandManager="commandManager"
|
||
/>
|
||
|
||
<!-- 液化编辑面板 -->
|
||
<LiquifyPanel
|
||
v-if="canvasManagerLoaded && !enabledRedGreenMode"
|
||
:canvas="canvasManager?.canvas"
|
||
:commandManager="commandManager"
|
||
:liquifyManager="liquifyManager"
|
||
:layerManager="layerManager"
|
||
:activeTool="activeTool"
|
||
/>
|
||
|
||
<!-- 选区面板 -->
|
||
<SelectionPanel
|
||
v-if="canvasManagerLoaded && !enabledRedGreenMode"
|
||
:canvas="canvasManager?.canvas"
|
||
:commandManager="commandManager"
|
||
:selectionManager="selectionManager"
|
||
:layerManager="layerManager"
|
||
:toolManager="toolManager"
|
||
:activeTool="activeTool"
|
||
/>
|
||
|
||
<div class="zoom-info">
|
||
{{ t("Canvas.Scale") }}: {{ currentZoom }}%
|
||
<button class="reset-zoom" @click="resetZoom">
|
||
{{ $t("Canvas.ResetLayer") }}
|
||
</button>
|
||
<button
|
||
class="help-btn"
|
||
@click="toggleShortcutHelp"
|
||
:title="$t('Canvas.Help')"
|
||
>
|
||
?
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 图层面板组件 -->
|
||
<!-- v-if="canvasManagerLoaded && !enabledRedGreenMode" -->
|
||
|
||
<transition name="fade">
|
||
<div
|
||
class="layers-panel"
|
||
v-if="isShowLayerPanel && !enabledRedGreenMode && showLayersPanel"
|
||
>
|
||
<LayersPanel
|
||
v-if="canvasManagerLoaded"
|
||
:activeLayerId="activeLayerId"
|
||
:activeElementId="activeElementId"
|
||
:thumbnailManager="canvasManager.thumbnailManager"
|
||
:showFixedLayer="showFixedLayer"
|
||
@add-layer="addLayer"
|
||
@add-top-layer="addTopLayer"
|
||
@set-active-layer="setActiveLayer"
|
||
@toggle-layer-visibility="toggleLayerVisibility"
|
||
@move-layer-up="moveLayerUp"
|
||
@move-layer-down="moveLayerDown"
|
||
@remove-layer="removeLayer"
|
||
@layers-reorder="handleLayersReorder"
|
||
@child-layers-reorder="handleChildLayersReorder"
|
||
/>
|
||
</div>
|
||
</transition>
|
||
</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: 100;
|
||
& > .header-menu {
|
||
height: 5.2rem;
|
||
overflow: hidden;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 2rem;
|
||
border-bottom: 1px solid #e0e0e0;
|
||
background-color: #ffffff;
|
||
}
|
||
}
|
||
|
||
.main-content {
|
||
display: flex;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
|
||
.canvas-container {
|
||
flex: 1;
|
||
position: relative;
|
||
/* overflow: auto; */
|
||
/* background-color: #f8f8f8; */
|
||
:deep(.canvas-container) {
|
||
position: absolute !important;
|
||
}
|
||
}
|
||
|
||
.canvas-container canvas {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
}
|
||
|
||
.background-grid {
|
||
--offsetX: 0px;
|
||
--offsetY: 0px;
|
||
--size: 8px;
|
||
--color: #dedcdc;
|
||
background-image: -webkit-linear-gradient(
|
||
45deg,
|
||
var(--color) 25%,
|
||
transparent 0,
|
||
transparent 75%,
|
||
var(--color) 0
|
||
),
|
||
-webkit-linear-gradient(45deg, var(--color) 25%, transparent 0, transparent
|
||
75%, var(--color) 0);
|
||
background-image: linear-gradient(
|
||
45deg,
|
||
var(--color) 25%,
|
||
transparent 0,
|
||
transparent 75%,
|
||
var(--color) 0
|
||
),
|
||
linear-gradient(
|
||
45deg,
|
||
var(--color) 25%,
|
||
transparent 0,
|
||
transparent 75%,
|
||
var(--color) 0
|
||
);
|
||
background-position: var(--offsetX) var(--offsetY),
|
||
calc(var(--size) + var(--offsetX)) calc(var(--size) + var(--offsetY));
|
||
background-size: calc(var(--size) * 2) calc(var(--size) * 2);
|
||
}
|
||
|
||
.zoom-info {
|
||
position: absolute;
|
||
bottom: 1rem;
|
||
right: 1rem;
|
||
background: rgba(255, 255, 255, 0.7);
|
||
padding: .5rem 1rem;
|
||
border-radius: .4rem;
|
||
font-size: 1.4rem;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||
color: #666;
|
||
display: flex;
|
||
}
|
||
|
||
.zoom-hint {
|
||
position: absolute;
|
||
top: 1rem;
|
||
left: 1rem;
|
||
background: rgba(255, 255, 255, 0.8);
|
||
padding: .5rem 1rem;
|
||
border-radius: .4rem;
|
||
font-size: 1.2rem;
|
||
color: #666;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.reset-zoom {
|
||
margin-left: 1rem;
|
||
cursor: pointer;
|
||
padding: .2rem .5rem;
|
||
font-size: 1.2rem;
|
||
border: 1px solid #ddd;
|
||
background: #f8f8f8;
|
||
border-radius: .3rem;
|
||
}
|
||
|
||
.footer-actions {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 1rem;
|
||
padding: 1.5rem;
|
||
border-top: 1px solid #e0e0e0;
|
||
}
|
||
|
||
.share-btn,
|
||
.export-btn {
|
||
padding: .8rem 2rem;
|
||
border: none;
|
||
border-radius: 2rem;
|
||
background-color: #000;
|
||
color: #fff;
|
||
font-size: 1.4rem;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.share-btn:hover,
|
||
.export-btn:hover {
|
||
opacity: 0.9;
|
||
}
|
||
|
||
button {
|
||
font-size: 1.3rem;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
button:hover {
|
||
background: #e6e6e6;
|
||
}
|
||
|
||
.help-btn {
|
||
margin-left: 1rem;
|
||
cursor: pointer;
|
||
width: 2.4rem;
|
||
height: 2.4rem;
|
||
border-radius: 50%;
|
||
background-color: #f0f0f0;
|
||
border: 1px solid #ddd;
|
||
font-weight: bold;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.help-btn:hover {
|
||
background-color: #e0e0e0;
|
||
}
|
||
|
||
.modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.modal-content {
|
||
position: relative;
|
||
width: 80%;
|
||
max-width: 60rem;
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
background-color: #fff;
|
||
border-radius: .8rem;
|
||
padding: 2rem;
|
||
}
|
||
|
||
.close-modal {
|
||
position: absolute;
|
||
top: 1rem;
|
||
right: 1rem;
|
||
width: 3rem;
|
||
height: 3rem;
|
||
border-radius: 50%;
|
||
border: none;
|
||
background-color: #f0f0f0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 2rem;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* 网格控制面板样式 */
|
||
.grid-controls {
|
||
position: absolute;
|
||
bottom: 11.5rem;
|
||
right: 1rem;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
padding: 1rem;
|
||
border-radius: .4rem;
|
||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: .8rem;
|
||
z-index: 5;
|
||
}
|
||
|
||
.layers-panel {
|
||
position: absolute;
|
||
right: 2rem;
|
||
top: 1rem;
|
||
transition: width 0.3s ease;
|
||
background: #fff;
|
||
width: 35rem;
|
||
max-height: 85vh;
|
||
box-shadow: 0 .4rem 2rem rgba(0, 0, 0, 0.1); /* 添加阴影效果 */
|
||
backdrop-filter: blur(2px); /* 添加模糊效果 */
|
||
-webkit-backdrop-filter: blur(2px);
|
||
background-color: rgba(255, 255, 255, 0.95); /* 改为白色背景 */
|
||
z-index: 1000; /* 确保面板在最上层 */
|
||
border: 1px solid #e0e0e0;
|
||
/* 添加指向整个面板的倒三角 */
|
||
&::before {
|
||
content: "";
|
||
position: absolute;
|
||
top: -.9rem;
|
||
right: .6rem;
|
||
width: 0;
|
||
height: 0;
|
||
border-left: 1rem solid transparent;
|
||
border-right: 1rem solid transparent;
|
||
border-bottom: 1rem solid rgba(255, 255, 255, 0.95); /* 与面板背景色一致 */
|
||
filter: drop-shadow(0 -1px 1px rgba(0, 0, 0, 0.05));
|
||
z-index: 1;
|
||
}
|
||
}
|
||
/* 添加触控设备的样式调整 */
|
||
@media (pointer: coarse) {
|
||
.tool-btn {
|
||
width: 4.4rem;
|
||
height: 4.4rem;
|
||
font-size: 1.8rem;
|
||
}
|
||
|
||
.layers-panel {
|
||
width: 28rem;
|
||
}
|
||
|
||
.layer-item,
|
||
.element-item {
|
||
padding: 1.2rem .8rem;
|
||
}
|
||
|
||
.element-action-btn,
|
||
.element-delete-btn {
|
||
width: 2.4rem;
|
||
height: 2.4rem;
|
||
}
|
||
|
||
.help-btn {
|
||
width: 3.2rem;
|
||
height: 3.2rem;
|
||
}
|
||
|
||
.modal-content {
|
||
width: 90%;
|
||
padding: 1.6rem;
|
||
}
|
||
|
||
.close-modal {
|
||
width: 4rem;
|
||
height: 4rem;
|
||
font-size: 2.4rem;
|
||
}
|
||
}
|
||
|
||
// 淡入淡出动画
|
||
.fade-enter-active,
|
||
.fade-leave-active {
|
||
transition: opacity 0.3s, transform 0.3s;
|
||
}
|
||
.fade-enter-from,
|
||
.fade-leave-to {
|
||
opacity: 0;
|
||
transform: translateY(1rem);
|
||
}
|
||
</style>
|