2025-06-09 10:25:54 +08:00
|
|
|
<script setup>
|
|
|
|
|
import { ref, inject, computed, onMounted, onUnmounted } from "vue";
|
|
|
|
|
import { OperationType } from "../utils/layerHelper";
|
|
|
|
|
|
2025-06-23 15:56:01 +08:00
|
|
|
const emit = defineEmits([
|
|
|
|
|
"tool-selected",
|
|
|
|
|
"trigger-image-upload",
|
|
|
|
|
"add-text",
|
|
|
|
|
"undo",
|
|
|
|
|
"redo",
|
|
|
|
|
"toggle-minimap",
|
|
|
|
|
"zoom-in",
|
|
|
|
|
"zoom-out",
|
|
|
|
|
"toggle-red-green-mode",
|
|
|
|
|
"undo-redo-status-changed",
|
|
|
|
|
]);
|
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
const props = defineProps({
|
|
|
|
|
activeTool: String,
|
|
|
|
|
minimapEnabled: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: true,
|
|
|
|
|
},
|
|
|
|
|
isRedGreenMode: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
default: false,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const commandManager = inject("commandManager");
|
|
|
|
|
|
|
|
|
|
// 撤销/重做按钮状态
|
|
|
|
|
const canUndo = ref(false);
|
|
|
|
|
const canRedo = ref(false);
|
|
|
|
|
|
|
|
|
|
// 监听命令管理器状态变化
|
|
|
|
|
commandManager.setChangeCallback((info) => {
|
|
|
|
|
canUndo.value = info.canUndo;
|
|
|
|
|
canRedo.value = info.canRedo;
|
2025-06-23 15:56:01 +08:00
|
|
|
|
|
|
|
|
emit("undo-redo-status-changed", {
|
|
|
|
|
canUndo: canUndo.value,
|
|
|
|
|
canRedo: canRedo.value,
|
|
|
|
|
commandManager,
|
|
|
|
|
});
|
2025-06-09 10:25:54 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 撤销/重做操作
|
|
|
|
|
const undoFun = () => commandManager.undo();
|
|
|
|
|
const redoFun = () => commandManager.redo();
|
|
|
|
|
|
|
|
|
|
// 普通模式工具列表
|
|
|
|
|
const normalToolsList = ref([
|
|
|
|
|
{
|
|
|
|
|
id: "undo",
|
|
|
|
|
title: "Undo",
|
|
|
|
|
action: undo,
|
|
|
|
|
icon: { name: "CUndo", size: "20" },
|
|
|
|
|
class: "undo-btn",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "redo",
|
|
|
|
|
title: "Redo",
|
|
|
|
|
action: redo,
|
|
|
|
|
icon: { name: "CRedo", size: "20" },
|
|
|
|
|
class: "redo-btn",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: OperationType.DRAW,
|
|
|
|
|
title: "Drawing",
|
|
|
|
|
action: () => selectTool(OperationType.DRAW),
|
|
|
|
|
icon: { name: "CBrush", size: "24" },
|
|
|
|
|
class: "draw-btn",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: OperationType.ERASER,
|
|
|
|
|
title: "Eraser",
|
|
|
|
|
action: () => selectTool(OperationType.ERASER),
|
|
|
|
|
icon: { name: "CEraser", size: "22" },
|
|
|
|
|
class: "eraser-btn",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: OperationType.PAN,
|
|
|
|
|
title: "Pan",
|
|
|
|
|
action: () => selectTool(OperationType.PAN),
|
|
|
|
|
icon: { name: "CHand", size: "28" },
|
|
|
|
|
class: "hand-btn",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: OperationType.SELECT,
|
|
|
|
|
title: "Select",
|
|
|
|
|
action: () => selectTool(OperationType.SELECT),
|
|
|
|
|
icon: { name: "CSelect", size: "28" },
|
|
|
|
|
class: "select-btn",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: OperationType.LIQUIFY,
|
|
|
|
|
title: "Liquefying",
|
|
|
|
|
action: () => selectTool(OperationType.LIQUIFY),
|
|
|
|
|
icon: { name: "CLiquefying", size: "32" },
|
|
|
|
|
class: "liquify-btn",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: OperationType.LASSO,
|
|
|
|
|
title: "Lasso",
|
|
|
|
|
action: () => selectTool(OperationType.LASSO),
|
|
|
|
|
icon: { name: "CLasso", size: "28" },
|
|
|
|
|
class: "lasso-btn",
|
|
|
|
|
activeList: [
|
|
|
|
|
OperationType.LASSO,
|
|
|
|
|
OperationType.LASSO_RECTANGLE,
|
|
|
|
|
OperationType.AREA_CUSTOM,
|
|
|
|
|
OperationType.AREA_RECTANGLE,
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "zoomIn",
|
|
|
|
|
title: "Zoom In",
|
|
|
|
|
action: zoomIn,
|
|
|
|
|
icon: { name: "CZoomIn", size: "30" },
|
|
|
|
|
class: "zoom-in-btn",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "zoomOut",
|
|
|
|
|
title: "Zoom Out",
|
|
|
|
|
action: zoomOut,
|
|
|
|
|
icon: { name: "CZoomOut", size: "26" },
|
|
|
|
|
class: "zoom-out-btn",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "upload",
|
|
|
|
|
title: "Upload Image",
|
|
|
|
|
action: triggerImageUpload,
|
|
|
|
|
icon: { name: "CUpload", size: "26" },
|
|
|
|
|
class: "upload-btn",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "addText",
|
|
|
|
|
title: "Add Text",
|
|
|
|
|
action: () => addText(),
|
|
|
|
|
icon: { name: "CFont", size: "20" },
|
|
|
|
|
class: "text-btn",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 红绿图模式工具列表
|
|
|
|
|
const redGreenToolsList = ref([
|
|
|
|
|
{
|
|
|
|
|
id: "undo",
|
|
|
|
|
title: "Undo",
|
|
|
|
|
action: undo,
|
|
|
|
|
icon: { name: "CUndo", size: "20" },
|
|
|
|
|
class: "undo-btn",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "redo",
|
|
|
|
|
title: "Redo",
|
|
|
|
|
action: redo,
|
|
|
|
|
icon: { name: "CRedo", size: "20" },
|
|
|
|
|
class: "redo-btn",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: OperationType.RED_BRUSH,
|
|
|
|
|
title: "Red Brush (R)",
|
2025-06-18 11:05:23 +08:00
|
|
|
action: () => selectTool(OperationType.RED_BRUSH, true),
|
2025-06-09 10:25:54 +08:00
|
|
|
icon: { name: "CBrush", size: "24" },
|
|
|
|
|
class: "red-brush-btn",
|
|
|
|
|
style: { color: "#FF0000" },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: OperationType.GREEN_BRUSH,
|
|
|
|
|
title: "Green Brush (G)",
|
2025-06-18 11:05:23 +08:00
|
|
|
action: () => selectTool(OperationType.GREEN_BRUSH, true),
|
2025-06-09 10:25:54 +08:00
|
|
|
icon: { name: "CBrush", size: "24" },
|
|
|
|
|
class: "green-brush-btn",
|
|
|
|
|
style: { color: "#00AA00" },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: OperationType.ERASER,
|
|
|
|
|
title: "Eraser (E)",
|
2025-06-18 11:05:23 +08:00
|
|
|
action: () => selectTool(OperationType.ERASER, true),
|
2025-06-09 10:25:54 +08:00
|
|
|
icon: { name: "CEraser", size: "22" },
|
|
|
|
|
class: "eraser-btn",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "zoomIn",
|
|
|
|
|
title: "Zoom In",
|
|
|
|
|
action: zoomIn,
|
|
|
|
|
icon: { name: "CZoomIn", size: "30" },
|
|
|
|
|
class: "zoom-in-btn",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "zoomOut",
|
|
|
|
|
title: "Zoom Out",
|
|
|
|
|
action: zoomOut,
|
|
|
|
|
icon: { name: "CZoomOut", size: "26" },
|
|
|
|
|
class: "zoom-out-btn",
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 根据模式选择工具列表
|
|
|
|
|
const toolsList = computed(() => {
|
|
|
|
|
return props.isRedGreenMode ? redGreenToolsList.value : normalToolsList.value;
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-18 11:05:23 +08:00
|
|
|
function selectTool(tool, isRedGreenMode = false) {
|
|
|
|
|
emit("tool-selected", tool, isRedGreenMode);
|
2025-06-09 10:25:54 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function triggerImageUpload() {
|
|
|
|
|
emit("trigger-image-upload");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addText() {
|
|
|
|
|
emit("add-text");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function undo() {
|
|
|
|
|
if (!canUndo.value) return;
|
|
|
|
|
undoFun();
|
|
|
|
|
emit("undo", {
|
|
|
|
|
canUndo: canUndo.value,
|
|
|
|
|
canRedo: canRedo.value,
|
|
|
|
|
commandManager,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function redo() {
|
|
|
|
|
if (!canRedo.value) return;
|
|
|
|
|
emit("redo", {
|
|
|
|
|
canUndo: canUndo.value,
|
|
|
|
|
canRedo: canRedo.value,
|
|
|
|
|
commandManager,
|
|
|
|
|
});
|
|
|
|
|
redoFun();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleMinimap() {
|
|
|
|
|
emit("toggle-minimap", !props.minimapEnabled);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function zoomIn() {
|
|
|
|
|
emit("zoom-in");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function zoomOut() {
|
|
|
|
|
emit("zoom-out");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleRedGreenMode() {
|
|
|
|
|
emit("toggle-red-green-mode");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 键盘快捷键处理
|
|
|
|
|
function handleKeyDown(event) {
|
|
|
|
|
// 在红绿图模式下处理特定快捷键
|
|
|
|
|
if (props.isRedGreenMode) {
|
|
|
|
|
const key = event.key.toUpperCase();
|
|
|
|
|
|
|
|
|
|
// 当处于输入状态时不触发快捷键
|
|
|
|
|
if (
|
|
|
|
|
event.target.tagName === "INPUT" ||
|
|
|
|
|
event.target.tagName === "TEXTAREA"
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (key) {
|
|
|
|
|
case "R":
|
2025-06-18 11:05:23 +08:00
|
|
|
selectTool(OperationType.RED_BRUSH, true);
|
2025-06-09 10:25:54 +08:00
|
|
|
event.preventDefault();
|
|
|
|
|
break;
|
|
|
|
|
case "G":
|
2025-06-18 11:05:23 +08:00
|
|
|
selectTool(OperationType.GREEN_BRUSH, true);
|
2025-06-09 10:25:54 +08:00
|
|
|
event.preventDefault();
|
|
|
|
|
break;
|
|
|
|
|
case "E":
|
2025-06-18 11:05:23 +08:00
|
|
|
selectTool(OperationType.ERASER, true);
|
2025-06-09 10:25:54 +08:00
|
|
|
event.preventDefault();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
// 添加键盘事件监听
|
|
|
|
|
window.addEventListener("keydown", handleKeyDown);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
// 移除键盘事件监听
|
|
|
|
|
window.removeEventListener("keydown", handleKeyDown);
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div class="tools-sidebar">
|
|
|
|
|
<div
|
|
|
|
|
v-for="tool in toolsList"
|
|
|
|
|
:key="tool.id"
|
|
|
|
|
:class="[
|
|
|
|
|
'tool-btn',
|
|
|
|
|
tool.class,
|
|
|
|
|
{
|
|
|
|
|
active:
|
|
|
|
|
tool.id === activeTool ||
|
|
|
|
|
tool.id === activeTool.toLowerCase() ||
|
|
|
|
|
tool?.activeList?.includes(activeTool),
|
|
|
|
|
disabled:
|
|
|
|
|
(tool.id === 'undo' && !canUndo) ||
|
|
|
|
|
(tool.id === 'redo' && !canRedo),
|
|
|
|
|
},
|
|
|
|
|
]"
|
|
|
|
|
:style="tool.style"
|
|
|
|
|
@click="tool.action"
|
|
|
|
|
>
|
|
|
|
|
<SvgIcon :name="tool.icon.name" :size="tool.icon.size"></SvgIcon>
|
|
|
|
|
<div class="tool-tooltip">{{ tool.title }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.tools-sidebar {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
padding: 15px 10px;
|
|
|
|
|
border-right: 1px solid #e0e0e0;
|
|
|
|
|
background-color: #ffffff;
|
|
|
|
|
user-select: none;
|
2025-06-18 11:05:23 +08:00
|
|
|
min-width: 58px;
|
|
|
|
|
height: 100%;
|
2025-06-09 10:25:54 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tool-btn {
|
|
|
|
|
position: relative;
|
|
|
|
|
width: 36px;
|
|
|
|
|
height: 36px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
background: none;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
color: #333;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tool-btn:hover {
|
|
|
|
|
background-color: #f0f0f0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tool-btn:hover .tool-tooltip {
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tool-btn.active {
|
|
|
|
|
background-color: #e6f7ff;
|
|
|
|
|
color: #1890ff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tool-btn.disabled {
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
color: #e0e0e0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tool-tooltip {
|
|
|
|
|
display: none;
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: 100%;
|
|
|
|
|
top: 50%;
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
background-color: rgba(0, 0, 0, 0.7);
|
|
|
|
|
color: white;
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
margin-left: 8px;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
z-index: 10;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tool-tooltip:before {
|
|
|
|
|
content: "";
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 50%;
|
|
|
|
|
right: 100%;
|
|
|
|
|
margin-top: -5px;
|
|
|
|
|
border-width: 5px;
|
|
|
|
|
border-style: solid;
|
|
|
|
|
border-color: transparent rgba(0, 0, 0, 0.7) transparent transparent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.red-green-mode {
|
|
|
|
|
background-color: #fff4f4;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mode-indicator {
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
padding: 8px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
background-color: #ffcccc;
|
|
|
|
|
color: #a33;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mode-label {
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mode-hint {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
}
|
|
|
|
|
</style>
|