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