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

1718 lines
47 KiB
Vue
Raw Normal View History

2025-06-09 10:25:54 +08:00
<script setup>
2026-02-06 11:11:04 +08:00
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)
2026-02-06 13:05:19 +08:00
const loading = ref(true)
const loading_ = ref(true)
2026-02-06 11:11:04 +08:00
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
2026-01-16 13:02:26 +08:00
}
2026-02-06 11:11:04 +08:00
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 = ""
2026-02-06 13:05:19 +08:00
await layerManager?.sortLayers()
2026-02-06 11:11:04 +08:00
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)
}
})
2025-06-09 10:25:54 +08:00
</script>
<template>
2026-02-06 11:11:04 +08:00
<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">
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
2026-02-06 11:11:04 +08:00
<!-- 快捷键帮助模态框 -->
<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>
2025-06-09 10:25:54 +08:00
</template>
<style scoped lang="less">
2026-02-06 11:11:04 +08:00
* {
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);
}
2025-06-09 10:25:54 +08:00
</style>