feat: 裁剪组裁剪跟随选择组移动

This commit is contained in:
bighuixiang
2025-07-14 01:00:23 +08:00
parent 96e13cb22a
commit 24e9ba8ae5
80 changed files with 2052 additions and 4292 deletions

View File

@@ -43,11 +43,7 @@
>
+
</button>
<button
class="control-btn remove"
@click="removeMemorizedSize"
v-if="canRemoveSize"
>
<button class="control-btn remove" @click="removeMemorizedSize" v-if="canRemoveSize">
-
</button>
</div>
@@ -57,16 +53,9 @@
<!-- 1.这里加上过渡动画 颜色选择器 - 仅在特定工具下显示 -->
<transition name="color-picker-fade" mode="out-in">
<div
v-if="showColorPicker"
class="color-picker-container"
key="color-picker"
>
<div v-if="showColorPicker" class="color-picker-container" key="color-picker">
<label for="color-picker" class="current-color-label">
<div
class="current-color"
:style="{ backgroundColor: brushColor }"
></div>
<div class="current-color" :style="{ backgroundColor: brushColor }"></div>
</label>
<input
type="color"
@@ -115,9 +104,7 @@
></div>
</div>
<div class="tooltip-content">
<div class="tooltip-text">
{{ Math.round(brushOpacity * 100) }}%
</div>
<div class="tooltip-text">{{ Math.round(brushOpacity * 100) }}%</div>
<div class="tooltip-controls">
<button
class="control-btn add"
@@ -470,7 +457,9 @@ watch(
background: rgba(255, 255, 255, 0.8);
border-radius: 5px;
padding: 15px 3px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.05);
box-shadow:
0 4px 20px rgba(0, 0, 0, 0.1),
0 0 0 1px rgba(0, 0, 0, 0.05);
z-index: 8;
backdrop-filter: blur(2px);
color: #333;
@@ -478,7 +467,9 @@ watch(
-webkit-user-select: none;
// 添加高度过渡动画
transition: height 0.3s ease-out, min-height 0.3s ease-out;
transition:
height 0.3s ease-out,
min-height 0.3s ease-out;
// overflow: hidden;
transform: translate3d(0, -50%, 0); // 确保使用3D变换以提高性能
@@ -492,8 +483,7 @@ watch(
display: flex;
justify-content: center;
align-items: center;
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% /
10px 10px;
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% / 10px 10px;
border-radius: 6px;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
margin-bottom: 5px;
@@ -521,8 +511,7 @@ watch(
left: 0;
width: 100%;
height: 100%;
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% /
10px 10px;
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% / 10px 10px;
}
.opacity-color {
@@ -617,13 +606,17 @@ watch(
height: 32px;
border-radius: 8px;
border: 2px solid #fff;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
box-shadow:
0 2px 6px rgba(0, 0, 0, 0.15),
0 0 0 1px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
cursor: pointer;
&:hover {
transform: scale(1.05);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.1);
box-shadow:
0 4px 8px rgba(0, 0, 0, 0.2),
0 0 0 1px rgba(0, 0, 0, 0.1);
}
}
@@ -683,7 +676,9 @@ watch(
// 淡入淡出动画
.brush-control-canel-fade-enter-active,
.brush-control-canel-fade-leave-active {
transition: opacity 0.3s, transform 0.3s;
transition:
opacity 0.3s,
transform 0.3s;
}
.brush-control-canel-fade-enter-from,
.brush-control-canel-fade-leave-to {
@@ -694,7 +689,9 @@ watch(
// 颜色选择器过渡动画
.color-picker-fade-enter-active,
.color-picker-fade-leave-active {
transition: opacity 0.25s ease-out, transform 0.25s ease-out;
transition:
opacity 0.25s ease-out,
transform 0.25s ease-out;
}
.color-picker-fade-enter-from,
.color-picker-fade-leave-to {
@@ -705,7 +702,9 @@ watch(
// 透明度滑块过渡动画
.opacity-slider-fade-enter-active,
.opacity-slider-fade-leave-active {
transition: opacity 0.25s ease-out, transform 0.25s ease-out;
transition:
opacity 0.25s ease-out,
transform 0.25s ease-out;
}
.opacity-slider-fade-enter-from,
.opacity-slider-fade-leave-to {

View File

@@ -12,25 +12,16 @@
v-for="brush in brushStore.state.availableBrushes"
:key="brush.id"
@click="setBrushTypeWithCommand(brush.id)"
:class="[
'brush-type-item',
{ active: brushStore.state.type === brush.id },
]"
:class="['brush-type-item', { active: brushStore.state.type === brush.id }]"
>
<div
class="brush-preview"
:style="getBrushPreviewStyle(brush)"
></div>
<div class="brush-preview" :style="getBrushPreviewStyle(brush)"></div>
<span class="brush-name">{{ brush.name }}</span>
</div>
</div>
</div>
<!-- 动态渲染笔刷可配置属性 -->
<template
v-for="(properties, category) in propertiesByCategory"
:key="category"
>
<template v-for="(properties, category) in propertiesByCategory" :key="category">
<div class="brush-section">
<div class="section-header">
<span>{{ category }}</span>
@@ -43,11 +34,7 @@
<!-- 针对每个属性根据其类型渲染合适的控件 -->
<div class="property-list">
<div
v-for="prop in properties"
:key="prop.id"
class="property-item"
>
<div v-for="prop in properties" :key="prop.id" class="property-item">
<!-- 滑块控件 -->
<template v-if="prop.type === 'slider'">
<div class="slider-property">
@@ -61,13 +48,7 @@
<input
type="range"
:value="prop.value"
@input="
(e) =>
handlePropertyChange(
prop.id,
parseFloat(e.target.value)
)
"
@input="(e) => handlePropertyChange(prop.id, parseFloat(e.target.value))"
:min="prop.min || 0"
:max="prop.max || 100"
:step="prop.step || 1"
@@ -93,25 +74,19 @@
<div class="color-property">
<div class="color-header">
<span>{{ prop.name }}</span>
<div
class="color-preview"
:style="{ backgroundColor: prop.value }"
></div>
<div class="color-preview" :style="{ backgroundColor: prop.value }"></div>
</div>
<div class="color-row">
<input
type="color"
:value="prop.value"
@input="
(e) => handlePropertyChange(prop.id, e.target.value)
"
@input="(e) => handlePropertyChange(prop.id, e.target.value)"
class="color-picker"
/>
<!-- 如果是主颜色显示最近使用的颜色 -->
<div v-if="prop.id === 'color'" class="recent-colors">
<div
v-for="(color, index) in brushStore.state
.recentColors"
v-for="(color, index) in brushStore.state.recentColors"
:key="index"
class="color-item"
:style="{ backgroundColor: color }"
@@ -130,9 +105,7 @@
<input
type="checkbox"
:checked="prop.value"
@change="
(e) => handlePropertyChange(prop.id, e.target.checked)
"
@change="(e) => handlePropertyChange(prop.id, e.target.checked)"
:id="`toggle-${prop.id}`"
/>
<label :for="`toggle-${prop.id}`"></label>
@@ -146,9 +119,7 @@
<span>{{ prop.name }}</span>
<select
:value="prop.value"
@change="
(e) => handlePropertyChange(prop.id, e.target.value)
"
@change="(e) => handlePropertyChange(prop.id, e.target.value)"
class="property-select"
>
<option
@@ -225,10 +196,7 @@
<!-- 上传的纹理缓存区域 -->
<!-- 自定义纹理上传按钮 -->
<div
class="texture-item upload-item"
@click="triggerTextureUpload"
>
<div class="texture-item upload-item" @click="triggerTextureUpload">
<div class="upload-icon">
<span>+</span>
</div>
@@ -275,9 +243,7 @@
<input
type="text"
:value="prop.value"
@input="
(e) => handlePropertyChange(prop.id, e.target.value)
"
@input="(e) => handlePropertyChange(prop.id, e.target.value)"
class="property-input"
/>
</div>
@@ -360,13 +326,7 @@
<input
type="checkbox"
:checked="brushStore.state.shadowEnabled"
@change="
(e) =>
handleShadowPropertyChange(
'shadowEnabled',
e.target.checked
)
"
@change="(e) => handleShadowPropertyChange('shadowEnabled', e.target.checked)"
id="shadow-enabled"
/>
<label for="shadow-enabled"></label>
@@ -390,13 +350,7 @@
<input
type="color"
:value="brushStore.state.shadowColor"
@input="
(e) =>
handleShadowPropertyChange(
'shadowColor',
e.target.value
)
"
@input="(e) => handleShadowPropertyChange('shadowColor', e.target.value)"
class="color-picker"
/>
</div>
@@ -408,20 +362,14 @@
<div class="slider-property">
<div class="slider-header">
<span>{{ $t("阴影宽度") }}</span>
<span class="slider-value"
>{{ brushStore.state.shadowWidth }}px</span
>
<span class="slider-value">{{ brushStore.state.shadowWidth }}px</span>
</div>
<div class="slider-container">
<input
type="range"
:value="brushStore.state.shadowWidth"
@input="
(e) =>
handleShadowPropertyChange(
'shadowWidth',
parseFloat(e.target.value)
)
(e) => handleShadowPropertyChange('shadowWidth', parseFloat(e.target.value))
"
:min="0"
:max="50"
@@ -435,8 +383,7 @@
:key="preset"
@click="handleShadowPropertyChange('shadowWidth', preset)"
:class="{
active:
Math.abs(brushStore.state.shadowWidth - preset) < 0.1,
active: Math.abs(brushStore.state.shadowWidth - preset) < 0.1,
}"
>
{{ preset }}
@@ -450,9 +397,7 @@
<div class="slider-property">
<div class="slider-header">
<span>{{ $t("阴影X偏移") }}</span>
<span class="slider-value"
>{{ brushStore.state.shadowOffsetX }}px</span
>
<span class="slider-value">{{ brushStore.state.shadowOffsetX }}px</span>
</div>
<div class="slider-container">
<input
@@ -460,10 +405,7 @@
:value="brushStore.state.shadowOffsetX"
@input="
(e) =>
handleShadowPropertyChange(
'shadowOffsetX',
parseFloat(e.target.value)
)
handleShadowPropertyChange('shadowOffsetX', parseFloat(e.target.value))
"
:min="-50"
:max="50"
@@ -475,13 +417,9 @@
<button
v-for="preset in [-10, -5, 0, 5, 10]"
:key="preset"
@click="
handleShadowPropertyChange('shadowOffsetX', preset)
"
@click="handleShadowPropertyChange('shadowOffsetX', preset)"
:class="{
active:
Math.abs(brushStore.state.shadowOffsetX - preset) <
0.1,
active: Math.abs(brushStore.state.shadowOffsetX - preset) < 0.1,
}"
>
{{ preset }}
@@ -495,9 +433,7 @@
<div class="slider-property">
<div class="slider-header">
<span>{{ $t("阴影Y偏移") }}</span>
<span class="slider-value"
>{{ brushStore.state.shadowOffsetY }}px</span
>
<span class="slider-value">{{ brushStore.state.shadowOffsetY }}px</span>
</div>
<div class="slider-container">
<input
@@ -505,10 +441,7 @@
:value="brushStore.state.shadowOffsetY"
@input="
(e) =>
handleShadowPropertyChange(
'shadowOffsetY',
parseFloat(e.target.value)
)
handleShadowPropertyChange('shadowOffsetY', parseFloat(e.target.value))
"
:min="-50"
:max="50"
@@ -520,13 +453,9 @@
<button
v-for="preset in [-10, -5, 0, 5, 10]"
:key="preset"
@click="
handleShadowPropertyChange('shadowOffsetY', preset)
"
@click="handleShadowPropertyChange('shadowOffsetY', preset)"
:class="{
active:
Math.abs(brushStore.state.shadowOffsetY - preset) <
0.1,
active: Math.abs(brushStore.state.shadowOffsetY - preset) < 0.1,
}"
>
{{ preset }}
@@ -544,14 +473,8 @@
class="shadow-preview-element"
:style="{
backgroundColor: brushStore.state.color,
width: `${Math.max(
20,
Math.min(60, brushStore.state.size)
)}px`,
height: `${Math.max(
20,
Math.min(60, brushStore.state.size)
)}px`,
width: `${Math.max(20, Math.min(60, brushStore.state.size))}px`,
height: `${Math.max(20, Math.min(60, brushStore.state.size))}px`,
boxShadow: brushStore.state.shadowEnabled
? `${brushStore.state.shadowOffsetX}px ${brushStore.state.shadowOffsetY}px ${brushStore.state.shadowWidth}px ${brushStore.state.shadowColor}`
: 'none',
@@ -567,11 +490,7 @@
<div class="brush-section">
<div class="section-header">
<span>笔刷预设</span>
<button
class="save-preset-btn"
@click="saveCurrentAsPreset"
title="保存当前设置为预设"
>
<button class="save-preset-btn" @click="saveCurrentAsPreset" title="保存当前设置为预设">
<i class="save-icon">+</i>
</button>
</div>
@@ -694,9 +613,7 @@ const filteredTextures = computed(() => {
if (selectedCategory.value === "全部") {
return textureLibrary;
}
return textureLibrary.filter(
(texture) => texture.category === selectedCategory.value
);
return textureLibrary.filter((texture) => texture.category === selectedCategory.value);
});
// 从材质库选择材质
@@ -909,8 +826,7 @@ function getBrushPreviewStyle(brush) {
case "spray":
return {
...baseStyle,
backgroundImage:
"radial-gradient(circle, currentColor 1px, transparent 1px)",
backgroundImage: "radial-gradient(circle, currentColor 1px, transparent 1px)",
backgroundSize: "4px 4px",
backgroundPosition: "center",
opacity: 0.8,
@@ -931,8 +847,7 @@ function getBrushPreviewStyle(brush) {
case "rainbow":
return {
...baseStyle,
background:
"linear-gradient(90deg, red, orange, yellow, green, blue, indigo, violet)",
background: "linear-gradient(90deg, red, orange, yellow, green, blue, indigo, violet)",
height: "3px",
};
case "texture":
@@ -957,10 +872,7 @@ function applyPresetWithCommand(presetIndex) {
// 保存当前设置为预设
function saveCurrentAsPreset() {
// 简单实现,可以后续优化为弹窗输入名称
const name = prompt(
"请输入预设名称:",
`预设 ${BrushStore.state.presets.length + 1}`
);
const name = prompt("请输入预设名称:", `预设 ${BrushStore.state.presets.length + 1}`);
if (name) {
const presetIndex = BrushStore.saveCurrentAsPreset(name);
// 应用新创建的预设(可选)
@@ -1740,7 +1652,9 @@ const brushStore = BrushStore;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.05);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.1),
0 0 0 1px rgba(0, 0, 0, 0.05);
animation: modalSlideUp 0.3s ease;
}
@@ -1930,12 +1844,7 @@ const brushStore = BrushStore;
content: "";
flex: 1;
height: 1px;
background: linear-gradient(
to right,
transparent,
rgba(0, 0, 0, 0.08),
transparent
);
background: linear-gradient(to right, transparent, rgba(0, 0, 0, 0.08), transparent);
}
.uploaded-textures-divider span {
@@ -2022,8 +1931,7 @@ const brushStore = BrushStore;
}
.shadow-preview-box {
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% /
10px 10px;
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% / 10px 10px;
border-radius: 8px;
padding: 30px;
display: flex;

View File

@@ -1,13 +1,5 @@
<script setup>
import {
inject,
ref,
provide,
onMounted,
computed,
watch,
onUnmounted,
} from "vue";
import { inject, ref, provide, onMounted, computed, watch, onUnmounted } from "vue";
import { OperationType } from "../utils/layerHelper";
import BrushPanel from "./BrushPanel.vue";
import { BrushStore } from "../store/BrushStore";
@@ -50,9 +42,7 @@ const lastColor = ref("#ffffff");
// return props.activeTool === OperationType.DRAW;
// });
function updateCanvasSize(
{ width, height } = { width: props.width, height: props.height }
) {
function updateCanvasSize({ width, height } = { width: props.width, height: props.height }) {
if (!layerManager) {
console.warn("LayerManager 未初始化,无法调整背景层尺寸");
return;
@@ -366,11 +356,7 @@ onMounted(() => {
<!-- 绘图工具设置 -->
<div class="canvas-settings gap-20" v-if="!props.enabledRedGreenMode">
<div
class="btn"
:class="{ active: showBrushPanel }"
@click="toggleBrushPanel"
>
<div class="btn" :class="{ active: showBrushPanel }" @click="toggleBrushPanel">
<!-- <span class="setting-label">笔刷:</span>/ -->
<div class="brush-selector">
<SvgIcon name="CBrushTop" size="22"></SvgIcon>
@@ -385,11 +371,7 @@ onMounted(() => {
<!-- <span class="brush-dropdown"></span> -->
</div>
<!-- 笔刷面板 -->
<div
v-if="showBrushPanel"
class="brush-panel-container"
ref="brushPanelRef"
>
<div v-if="showBrushPanel" class="brush-panel-container" ref="brushPanelRef">
<Teleport to="body">
<BrushPanel />
</Teleport>

View File

@@ -16,9 +16,7 @@ onMounted(() => {
platform.value = {
isMac: keyboardManager.platform === "mac",
isIOS: keyboardManager.platform === "ios",
isIPad:
keyboardManager.platform === "ios" &&
/iPad/.test(window.navigator.userAgent),
isIPad: keyboardManager.platform === "ios" && /iPad/.test(window.navigator.userAgent),
isTouchDevice: keyboardManager.isTouchDevice,
isWindows: keyboardManager.platform === "windows",
isAndroid: keyboardManager.platform === "android",
@@ -58,10 +56,10 @@ function convertShortcuts(managerShortcuts) {
increaseBrushSize: "增加笔触大小",
decreaseBrushSize: "减小笔触大小",
toggleTempTool: "临时切换工具",
newLayer: "新建图层",
groupLayers: "组合图层",
ungroupLayers: "取消组合",
mergeLayers: "合并图层",
// newLayer: "新建图层",
// groupLayers: "组合图层",
// ungroupLayers: "取消组合",
// mergeLayers: "合并图层",
};
// 工具ID到显示名称的映射
@@ -69,11 +67,11 @@ function convertShortcuts(managerShortcuts) {
select: "选择模式",
draw: "绘画模式",
eraser: "橡皮擦模式",
eyedropper: "吸色工具",
// eyedropper: "吸色工具",
pan: "移动画布",
lasso: "套索工具",
area_custom: "自由选区工具",
wave: "波浪工具",
// area_custom: "自由选区工具",
// wave: "波浪工具",
liquify: "液化工具",
};
@@ -82,11 +80,7 @@ function convertShortcuts(managerShortcuts) {
let actionDisplay = actionDisplayMap[shortcut.action] || shortcut.action;
// 特殊处理工具选择
if (
shortcut.action === "selectTool" &&
shortcut.param &&
toolDisplayMap[shortcut.param]
) {
if (shortcut.action === "selectTool" && shortcut.param && toolDisplayMap[shortcut.param]) {
actionDisplay = toolDisplayMap[shortcut.param];
}
@@ -144,12 +138,12 @@ function generateDefaultShortcuts() {
mac: "Delete 或 ⌫",
touch: "长按选中元素后点击删除",
},
{
action: "全选",
windows: "Ctrl+A",
mac: "⌘+A",
touch: "无",
},
// {
// action: "全选",
// windows: "Ctrl+A",
// mac: "⌘+A",
// touch: "无",
// },
{
action: "复制",
windows: "Ctrl+C",
@@ -198,12 +192,12 @@ function generateDefaultShortcuts() {
mac: "E",
touch: "点击橡皮擦工具",
},
{
action: "吸色工具",
windows: "I",
mac: "I",
touch: "点击吸色工具",
},
// {
// action: "吸色工具",
// windows: "I",
// mac: "I",
// touch: "点击吸色工具",
// },
{
action: "增加笔触大小",
windows: "]",
@@ -251,39 +245,24 @@ function getShortcutForCurrentPlatform(shortcut) {
// 按分类获取快捷键
function getShortcutsByCategory(category) {
const categoryMap = {
basic: [
"撤销",
"重做",
"全选",
"复制",
"粘贴",
"剪切",
"删除选中元素",
"上传图片",
],
// basic: ["撤销", "重做", "全选", "复制", "粘贴", "剪切", "删除选中元素", "上传图片"],
basic: ["撤销", "重做", "复制", "粘贴", "剪切", "删除选中元素", "上传图片"],
view: ["缩放画布", "移动画布"],
tools: [
"绘画模式",
"选择模式",
"橡皮擦模式",
"吸色工具",
// "吸色工具",
"套索工具",
"自由选区工具",
"波浪工具",
// "自由选区工具",
// "波浪工具",
"液化工具",
],
brush: [
"增加笔触大小",
"减小笔触大小",
"增加材质图片大小",
"减小材质图片大小",
],
layer: ["新建图层", "组合图层", "取消组合", "合并图层"],
brush: ["增加笔触大小", "减小笔触大小", "增加材质图片大小", "减小材质图片大小"],
// layer: ["新建图层", "组合图层", "取消组合", "合并图层"],
};
return shortcuts.value.filter((s) =>
categoryMap[category]?.includes(s.action)
);
return shortcuts.value.filter((s) => categoryMap[category]?.includes(s.action));
}
</script>
@@ -313,10 +292,7 @@ function getShortcutsByCategory(category) {
</tr>
</thead>
<tbody>
<tr
v-for="item in getShortcutsByCategory('basic')"
:key="item.action"
>
<tr v-for="item in getShortcutsByCategory('basic')" :key="item.action">
<td>{{ item.action }}</td>
<td>{{ getShortcutForCurrentPlatform(item) }}</td>
</tr>
@@ -334,10 +310,7 @@ function getShortcutsByCategory(category) {
</tr>
</thead>
<tbody>
<tr
v-for="item in getShortcutsByCategory('view')"
:key="item.action"
>
<tr v-for="item in getShortcutsByCategory('view')" :key="item.action">
<td>{{ item.action }}</td>
<td>{{ getShortcutForCurrentPlatform(item) }}</td>
</tr>
@@ -355,10 +328,7 @@ function getShortcutsByCategory(category) {
</tr>
</thead>
<tbody>
<tr
v-for="item in getShortcutsByCategory('tools')"
:key="item.action"
>
<tr v-for="item in getShortcutsByCategory('tools')" :key="item.action">
<td>{{ item.action }}</td>
<td>{{ getShortcutForCurrentPlatform(item) }}</td>
</tr>
@@ -376,10 +346,7 @@ function getShortcutsByCategory(category) {
</tr>
</thead>
<tbody>
<tr
v-for="item in getShortcutsByCategory('brush')"
:key="item.action"
>
<tr v-for="item in getShortcutsByCategory('brush')" :key="item.action">
<td>{{ item.action }}</td>
<td>{{ getShortcutForCurrentPlatform(item) }}</td>
</tr>
@@ -387,7 +354,7 @@ function getShortcutsByCategory(category) {
</table>
</div>
<div class="shortcuts-category">
<!-- <div class="shortcuts-category">
<h3>图层操作</h3>
<table class="shortcuts-table">
<thead>
@@ -397,16 +364,13 @@ function getShortcutsByCategory(category) {
</tr>
</thead>
<tbody>
<tr
v-for="item in getShortcutsByCategory('layer')"
:key="item.action"
>
<tr v-for="item in getShortcutsByCategory('layer')" :key="item.action">
<td>{{ item.action }}</td>
<td>{{ getShortcutForCurrentPlatform(item) }}</td>
</tr>
</tbody>
</table>
</div>
</div> -->
<div class="touch-tips" v-if="platform.isTouchDevice">
<h3>触控设备提示</h3>

View File

@@ -145,11 +145,7 @@ const handleItemMouseEnter = (item, index, event) => {
// 处理鼠标在菜单项内移动
const handleItemMouseMove = (item, index, event) => {
// 如果当前菜单项有子菜单但子菜单未显示,则显示子菜单
if (
item.children &&
item.children.length > 0 &&
hoveredItem.value !== index
) {
if (item.children && item.children.length > 0 && hoveredItem.value !== index) {
const element = event.target.closest(".context-menu-item");
showSubmenu(item, index, element);
}
@@ -261,10 +257,7 @@ onUnmounted(() => {
>
<template v-for="(item, index) in items" :key="index">
<!-- 分隔线 -->
<div
v-if="item.type === 'divider'"
class="context-menu-divider"
></div>
<div v-if="item.type === 'divider'" class="context-menu-divider"></div>
<!-- 菜单项 -->
<div
@@ -294,38 +287,24 @@ onUnmounted(() => {
<span class="context-menu-shortcut" v-if="item.shortcut">
{{ item.shortcut }}
</span>
<span
class="context-menu-arrow"
v-if="item.children && item.children.length > 0"
>
<span class="context-menu-arrow" v-if="item.children && item.children.length > 0">
<SvgIcon name="CRight" size="12" />
</span>
<!-- 子菜单 -->
<transition name="context-submenu">
<div
v-if="
item.children &&
item.children.length > 0 &&
hoveredItem === index
"
v-if="item.children && item.children.length > 0 && hoveredItem === index"
class="context-submenu"
:class="{
'submenu-left':
submenuPositions.get(index)?.direction === 'left',
'submenu-left': submenuPositions.get(index)?.direction === 'left',
}"
@mouseenter="handleSubmenuMouseEnter(index)"
@mouseleave="handleSubmenuMouseLeave"
>
<template
v-for="(subItem, subIndex) in item.children"
:key="subIndex"
>
<template v-for="(subItem, subIndex) in item.children" :key="subIndex">
<!-- 子菜单分隔线 -->
<div
v-if="subItem.type === 'divider'"
class="context-menu-divider"
></div>
<div v-if="subItem.type === 'divider'" class="context-menu-divider"></div>
<!-- 子菜单项 -->
<div
@@ -342,9 +321,7 @@ onUnmounted(() => {
:name="subItem.icon"
size="14"
:style="{
transform: subItem.inverIcon
? `rotate(90deg)`
: 'none',
transform: subItem.inverIcon ? `rotate(90deg)` : 'none',
}"
/>
</span>

View File

@@ -250,13 +250,7 @@ function handleTouchEnd(event) {
function handleUpdateChildLayers(newChildren) {
// 更新当前组图层的children数组
console.log(
"更新子图层顺序:",
"父图层ID:",
props.layer.id,
"新顺序:",
newChildren
);
console.log("更新子图层顺序:", "父图层ID:", props.layer.id, "新顺序:", newChildren);
emit("update-child-layers", props.layer.id, newChildren);
}
@@ -383,21 +377,13 @@ function findParentLayerId() {
>
<!-- 拖拽手柄 -->
<div class="layer-drag-handle" :title="$t('拖拽排序')">
<SvgIcon
v-if="!isHidenDragHandle"
:name="isChild ? 'CSort' : 'CSort'"
:size="32"
></SvgIcon>
<SvgIcon v-if="!isHidenDragHandle" :name="isChild ? 'CSort' : 'CSort'" :size="32"></SvgIcon>
</div>
<!-- 图层头部 -->
<div class="layer-header">
<!-- 多选复选框 -->
<div
v-if="isMultiSelectMode && !isChild"
class="layer-checkbox"
@click.stop
>
<div v-if="isMultiSelectMode && !isChild" class="layer-checkbox" @click.stop>
<Checkbox :checked="isSelected" @change="handleCheckboxChange" />
</div>
@@ -453,12 +439,7 @@ function findParentLayerId() {
>
<SvgIcon name="CLock" :size="18"></SvgIcon>
</span>
<span
v-else
class="status-icon"
:title="$t('未锁定')"
@click.stop="handleToggleLock"
>
<span v-else class="status-icon" :title="$t('未锁定')" @click.stop="handleToggleLock">
<SvgIcon name="CUnLock" :size="18"></SvgIcon>
</span>

View File

@@ -96,8 +96,7 @@ const handleRootLayersSort = (event) => {
// 获取被拖拽的图层ID
const draggedLayerId =
item.getAttribute("data-layer-id") ||
props.sortableRootLayers[oldIndex]?.id;
item.getAttribute("data-layer-id") || props.sortableRootLayers[oldIndex]?.id;
if (!draggedLayerId) {
console.error("❌ 无法获取被拖拽的图层ID");
@@ -150,25 +149,14 @@ const handleRootLayersSort = (event) => {
if (props.isChild) {
// 子图层事件处理
// 确保排序只影响当前组图层的children而不是全局layers
emit(
"child-layers-sort",
event,
props.sortableRootLayers,
props.parentLayerId
);
emit("child-layers-sort", event, props.sortableRootLayers, props.parentLayerId);
} else {
emit("root-layers-sort", event);
}
};
// 验证跨层级移动的有效性
const validateCrossLevelMove = (
layerId,
fromType,
toType,
fromParentId,
toParentId
) => {
const validateCrossLevelMove = (layerId, fromType, toType, fromParentId, toParentId) => {
// 查找图层
const layer = findLayerInHierarchy(layerId, fromType, fromParentId);
@@ -250,9 +238,7 @@ const handleDragRemove = (event) => {
const canDeleteComputed = computed(() => {
// 如果是子图层,检查父图层是否可以删除
if (props.isChild) {
const parentLayer = props.layers.find(
(layer) => layer.id === props.parentLayerId
);
const parentLayer = props.layers.find((layer) => layer.id === props.parentLayerId);
return parentLayer?.children?.length > 1;
}
// 否则直接返回根图层的可删除状态
@@ -305,23 +291,14 @@ const canDeleteComputed = computed(() => {
:is-editing="editingLayerId === layer.id"
:editing-name="editingLayerName"
:can-delete="
canDeleteComputed &&
!layer.isBackground &&
!layer.isFixed &&
!layer.locked
canDeleteComputed && !layer.isBackground && !layer.isFixed && !layer.locked
"
:expanded-group-ids="expandedGroupIds"
@click="(...args) => forwardEvent('layer-click', ...args)"
@double-click="
(...args) => forwardEvent('layer-double-click', ...args)
"
@double-click="(...args) => forwardEvent('layer-double-click', ...args)"
@context-menu="(...args) => forwardEvent('context-menu', ...args)"
@checkbox-change="
(...args) => forwardEvent('checkbox-change', ...args)
"
@toggle-visibility="
(...args) => forwardEvent('toggle-visibility', ...args)
"
@checkbox-change="(...args) => forwardEvent('checkbox-change', ...args)"
@toggle-visibility="(...args) => forwardEvent('toggle-visibility', ...args)"
@toggle-lock="(...args) => forwardEvent('toggle-lock', ...args)"
@delete="(...args) => forwardEvent('delete', ...args)"
@edit-confirm="(...args) => forwardEvent('edit-confirm', ...args)"
@@ -330,18 +307,10 @@ const canDeleteComputed = computed(() => {
@touch-start="(...args) => forwardEvent('touch-start', ...args)"
@touch-move="(...args) => forwardEvent('touch-move', ...args)"
@touch-end="(...args) => forwardEvent('touch-end', ...args)"
@update:editing-name="
(...args) => forwardEvent('update:editing-name', ...args)
"
@toggle-group-expanded="
(...args) => forwardEvent('toggle-group-expanded', ...args)
"
@toggle-child-visibility="
(...args) => forwardEvent('toggle-child-visibility', ...args)
"
@toggle-child-lock="
(...args) => forwardEvent('toggle-child-lock', ...args)
"
@update:editing-name="(...args) => forwardEvent('update:editing-name', ...args)"
@toggle-group-expanded="(...args) => forwardEvent('toggle-group-expanded', ...args)"
@toggle-child-visibility="(...args) => forwardEvent('toggle-child-visibility', ...args)"
@toggle-child-lock="(...args) => forwardEvent('toggle-child-lock', ...args)"
@delete-child="(...args) => forwardEvent('delete-child', ...args)"
@rename-child="(...args) => forwardEvent('rename-child', ...args)"
/>
@@ -370,16 +339,10 @@ const canDeleteComputed = computed(() => {
:parentLayerId="layer.id"
:group-name="groupName"
@layer-click="(...args) => forwardEvent('layer-click', ...args)"
@layer-double-click="
(...args) => forwardEvent('layer-double-click', ...args)
"
@layer-double-click="(...args) => forwardEvent('layer-double-click', ...args)"
@context-menu="(...args) => forwardEvent('context-menu', ...args)"
@checkbox-change="
(...args) => forwardEvent('checkbox-change', ...args)
"
@toggle-visibility="
(...args) => forwardEvent('toggle-visibility', ...args)
"
@checkbox-change="(...args) => forwardEvent('checkbox-change', ...args)"
@toggle-visibility="(...args) => forwardEvent('toggle-visibility', ...args)"
@toggle-lock="(...args) => forwardEvent('toggle-lock', ...args)"
@delete="(...args) => forwardEvent('delete', ...args)"
@edit-confirm="(...args) => forwardEvent('edit-confirm', ...args)"
@@ -388,33 +351,17 @@ const canDeleteComputed = computed(() => {
@touch-start="(...args) => forwardEvent('touch-start', ...args)"
@touch-move="(...args) => forwardEvent('touch-move', ...args)"
@touch-end="(...args) => forwardEvent('touch-end', ...args)"
@update:editing-name="
(...args) => forwardEvent('update:editing-name', ...args)
"
@root-layers-sort="
(...args) => forwardEvent('root-layers-sort', ...args)
"
@child-layers-sort="
(...args) => forwardEvent('child-layers-sort', ...args)
"
@cross-level-move="
(...args) => forwardEvent('cross-level-move', ...args)
"
@select-child-layer="
(...args) => forwardEvent('select-child-layer', ...args)
"
@start-child-layer-edit="
(...args) => forwardEvent('start-child-layer-edit', ...args)
"
@child-context-menu="
(...args) => forwardEvent('child-context-menu', ...args)
"
@update:editing-name="(...args) => forwardEvent('update:editing-name', ...args)"
@root-layers-sort="(...args) => forwardEvent('root-layers-sort', ...args)"
@child-layers-sort="(...args) => forwardEvent('child-layers-sort', ...args)"
@cross-level-move="(...args) => forwardEvent('cross-level-move', ...args)"
@select-child-layer="(...args) => forwardEvent('select-child-layer', ...args)"
@start-child-layer-edit="(...args) => forwardEvent('start-child-layer-edit', ...args)"
@child-context-menu="(...args) => forwardEvent('child-context-menu', ...args)"
@toggle-child-visibility="
(...args) => forwardEvent('toggle-child-visibility', ...args)
"
@toggle-child-lock="
(...args) => forwardEvent('toggle-child-lock', ...args)
"
@toggle-child-lock="(...args) => forwardEvent('toggle-child-lock', ...args)"
@finish-child-layer-edit="
(...args) => forwardEvent('finish-child-layer-edit', ...args)
"
@@ -424,9 +371,7 @@ const canDeleteComputed = computed(() => {
@child-layer-edit-keydown="
(...args) => forwardEvent('child-layer-edit-keydown', ...args)
"
@toggle-group-expanded="
(...args) => forwardEvent('toggle-group-expanded', ...args)
"
@toggle-group-expanded="(...args) => forwardEvent('toggle-group-expanded', ...args)"
@delete-child="(...args) => forwardEvent('delete-child', ...args)"
@rename-child="(...args) => forwardEvent('rename-child', ...args)"
/>
@@ -517,7 +462,9 @@ const canDeleteComputed = computed(() => {
// 跨层级拖拽目标区域高亮
.sortable-layers {
position: relative;
transition: background-color 0.2s ease, border-color 0.2s ease;
transition:
background-color 0.2s ease,
border-color 0.2s ease;
border-radius: 4px;
min-height: 40px; // 确保空组也有足够的拖拽区域

View File

@@ -72,26 +72,21 @@ const contextMenuItems = ref([]);
// 计算属性:可排序的根级图层(排除背景层和固定层)
const sortableRootLayers = computed(() => {
if (!layers) return [];
return layers.value.filter(
(layer) => !layer.parentId && !layer.isFixed && !layer.isBackground
);
return layers.value.filter((layer) => !layer.parentId && !layer.isFixed && !layer.isBackground);
});
// 计算属性:不可排序的固定图层(背景层和固定层)
const fixedLayers = computed(() => {
if (!layers) return [];
return layers.value.filter((layer) => {
if (props.showFixedLayer)
return !layer.parentId && (layer.isFixed || layer.isBackground);
if (props.showFixedLayer) return !layer.parentId && (layer.isFixed || layer.isBackground);
return !layer.parentId && layer.isBackground; // 只显示背景层,不显示固定层 - 固定层用来做红绿图模式 和 放模特
});
});
// 计算属性:获取当前选中的图层
const selectedLayers = computed(() => {
return sortableRootLayers.value.filter((layer) =>
selectedLayerIds.value.includes(layer.id)
);
return sortableRootLayers.value.filter((layer) => selectedLayerIds.value.includes(layer.id));
});
// 计算属性:获取当前是否激活子图层
@@ -224,9 +219,7 @@ function toggleLayerSelection(layer, event) {
}
const isShift = event.shiftKey;
const layerIndex = sortableRootLayers.value.findIndex(
(l) => l.id === layer.id
);
const layerIndex = sortableRootLayers.value.findIndex((l) => l.id === layer.id);
console.log("isShift", isShift);
if (isShift && lastSelectedIndex.value !== -1) {
@@ -386,9 +379,7 @@ async function ungroupSelectedLayer() {
try {
const childLayerIds = await layerManager?.ungroupLayers(groupLayer.id);
console.log(
`✅ 成功解组图层组: ${groupLayer.name}, 子图层: ${childLayerIds}`
);
console.log(`✅ 成功解组图层组: ${groupLayer.name}, 子图层: ${childLayerIds}`);
// 清除选择状态
clearSelection();
@@ -406,9 +397,7 @@ function deleteSelectedLayers() {
}
// 检查是否包含不能删除的图层
const undeletableLayers = selectedLayers.filter(
(layer) => layer.isBackground || layer.isFixed
);
const undeletableLayers = selectedLayers.filter((layer) => layer.isBackground || layer.isFixed);
if (undeletableLayers.length > 0) {
console.warn("选择的图层中包含背景层或固定层,无法删除");
@@ -417,10 +406,7 @@ function deleteSelectedLayers() {
// 检查删除后是否还有足够的普通图层
const remainingNormalLayers = layers.value.filter(
(layer) =>
!layer.isBackground &&
!layer.isFixed &&
!selectedLayerIds.value.includes(layer.id)
(layer) => !layer.isBackground && !layer.isFixed && !selectedLayerIds.value.includes(layer.id)
).length;
if (remainingNormalLayers < 1) {
@@ -460,9 +446,7 @@ function startEditing(layer) {
// 下一帧聚焦输入框并选中文本
nextTick(() => {
const inputElement = document.querySelector(
`input[data-layer-id="${layer.id}"]`
);
const inputElement = document.querySelector(`input[data-layer-id="${layer.id}"]`);
if (inputElement) {
inputElement.focus();
inputElement.select();
@@ -494,41 +478,41 @@ function handleEditKeydown(event) {
}
}
// 获取图层缩略图URL
function getLayerThumbnail(layerId) {
if (props.thumbnailManager) {
return props.thumbnailManager.getLayerThumbnail(layerId);
}
return null;
}
// 获取图层缩略图URL - 弃用
// function getLayerThumbnail(layerId) {
// if (props.thumbnailManager) {
// return props.thumbnailManager.getLayerThumbnail(layerId);
// }
// return null;
// }
// 获取图层类型图标
function getLayerTypeIcon(layer) {
if (!layer) return "🖼️";
// function getLayerTypeIcon(layer) {
// if (!layer) return "🖼️";
if (isGroupLayer(layer)) {
return "📁";
}
// if (isGroupLayer(layer)) {
// return "📁";
// }
if (layer.fabricObject) {
switch (layer.fabricObject.type) {
case "image":
return "🖼️";
case "text":
return "📝";
case "rect":
return "▢";
case "circle":
return "⬤";
case "path":
return "✎";
default:
return "⬤";
}
}
// if (layer.fabricObject) {
// switch (layer.fabricObject.type) {
// case "image":
// return "🖼️";
// case "text":
// return "📝";
// case "rect":
// return "▢";
// case "circle":
// return "⬤";
// case "path":
// return "✎";
// default:
// return "⬤";
// }
// }
return "🖼️";
}
// return "🖼️";
// }
// 获取图层的子图层
function getChildLayers(parentId) {
@@ -557,22 +541,22 @@ const toggleGroupExpanded = (groupId) => {
expandedGroupIds.value = new Set(expandedGroupIds.value);
};
// 渲染单个图层项(递归组件)
function renderLayerItem(layer, index) {
if (!layer) return null;
// // 渲染单个图层项(递归组件)
// function renderLayerItem(layer, index) {
// if (!layer) return null;
const isGroup = isGroupLayerType(layer);
const children = isGroup ? getChildLayers(layer.id) : [];
// const isGroup = isGroupLayerType(layer);
// const children = isGroup ? getChildLayers(layer.id) : [];
return {
id: layer.id,
name: layer.name,
isGroup: isGroup,
children: children,
fabricObject: layer.fabricObject,
visible: layer.visible,
};
}
// return {
// id: layer.id,
// name: layer.name,
// isGroup: isGroup,
// children: children,
// fabricObject: layer.fabricObject,
// visible: layer.visible,
// };
// }
// 处理图层点击事件
function handleLayerClick(layer, event) {
@@ -580,12 +564,7 @@ function handleLayerClick(layer, event) {
event.stopPropagation();
// 如果按住修饰键,执行多选逻辑
if (
event.ctrlKey ||
event.metaKey ||
event.shiftKey ||
isMultiSelectMode.value
) {
if (event.ctrlKey || event.metaKey || event.shiftKey || isMultiSelectMode.value) {
toggleLayerSelection(layer, event);
} else {
// 普通点击:进入单选模式
@@ -595,11 +574,7 @@ function handleLayerClick(layer, event) {
// 如果不是多选模式,才可激活图层
// 1.如果是组,则设置组下的第一个子图层为活动图层
// 2.否则直接设置活动图层
if (
isGroupLayerType(layer) &&
layer.children &&
layer.children.length > 0
) {
if (isGroupLayerType(layer) && layer.children && layer.children.length > 0) {
// 如果是组图层,设置第一个子图层为活动图层
layerManager?.setAllActiveGroupLayerCanvasObject?.(layer);
setActiveLayer(layer.children[0].id, { parentId: layer.id });
@@ -609,9 +584,7 @@ function handleLayerClick(layer, event) {
layerManager?.updateLayersObjectsInteractivity();
}
}
lastSelectedIndex.value = sortableRootLayers.value.findIndex(
(l) => l.id === layer.id
);
lastSelectedIndex.value = sortableRootLayers.value.findIndex((l) => l.id === layer.id);
}
}
@@ -675,10 +648,7 @@ function buildContextMenuItems(layer) {
{
label: "栅格化图层",
icon: "CPicture",
disabled:
layer.isBackground ||
layer.isFixed ||
!layerManager?.canRasterizeLayer?.(layer.id),
disabled: layer.isBackground || layer.isFixed || !layerManager?.canRasterizeLayer?.(layer.id),
action: () => {
rasterizeLayer(layer.id);
hideContextMenu();
@@ -688,10 +658,7 @@ function buildContextMenuItems(layer) {
{
label: "导出图层",
icon: "CExport",
disabled:
layer.isBackground ||
layer.isFixed ||
!layerManager?.canRasterizeLayer?.(layer.id),
disabled: layer.isBackground || layer.isFixed || !layerManager?.canRasterizeLayer?.(layer.id),
action: () => {
exportLayerToImage(layer.id);
hideContextMenu();
@@ -755,10 +722,7 @@ function buildContextMenuItems(layer) {
label: "置顶",
icon: "CBottom",
inverIcon: true, // 倒置图标
disabled:
layer.isBackground ||
layer.isFixed ||
!layerManager?.canMoveToTop?.(layer.id),
disabled: layer.isBackground || layer.isFixed || !layerManager?.canMoveToTop?.(layer.id),
action: () => {
moveLayerToTop(layer.id);
hideContextMenu();
@@ -767,10 +731,7 @@ function buildContextMenuItems(layer) {
{
label: "向上移动",
icon: "CUp",
disabled:
layer.isBackground ||
layer.isFixed ||
!layerManager?.canMoveToTop?.(layer.id),
disabled: layer.isBackground || layer.isFixed || !layerManager?.canMoveToTop?.(layer.id),
action: () => {
moveLayerUp(layer.id);
hideContextMenu();
@@ -780,9 +741,7 @@ function buildContextMenuItems(layer) {
label: "向下移动",
icon: "CDown",
disabled:
layer.isBackground ||
layer.isFixed ||
!layerManager?.canMoveToBottom?.(layer.id),
layer.isBackground || layer.isFixed || !layerManager?.canMoveToBottom?.(layer.id),
action: () => {
moveLayerDown(layer.id);
hideContextMenu();
@@ -792,9 +751,7 @@ function buildContextMenuItems(layer) {
label: "置底",
icon: "CBottom",
disabled:
layer.isBackground ||
layer.isFixed ||
!layerManager?.canMoveToBottom?.(layer.id),
layer.isBackground || layer.isFixed || !layerManager?.canMoveToBottom?.(layer.id),
action: () => {
moveLayerToBottom(layer.id);
hideContextMenu();
@@ -856,18 +813,13 @@ function canDeleteLayers() {
if (selectedLayers.length === 0) return false;
// 检查是否包含不能删除的图层
const undeletableLayers = selectedLayers.filter(
(layer) => layer.isBackground || layer.isFixed
);
const undeletableLayers = selectedLayers.filter((layer) => layer.isBackground || layer.isFixed);
if (undeletableLayers.length > 0) return false;
// 检查删除后是否还有足够的普通图层
const remainingNormalLayers = layers.value.filter(
(layer) =>
!layer.isBackground &&
!layer.isFixed &&
!selectedLayerIds.value.includes(layer.id)
(layer) => !layer.isBackground && !layer.isFixed && !selectedLayerIds.value.includes(layer.id)
).length;
return remainingNormalLayers >= 1;
@@ -904,9 +856,7 @@ function startChildLayerEdit(childLayer) {
childLayer.tempName = childLayer.name;
nextTick(() => {
const inputElement = document.querySelector(
`input[data-child-layer-id="${childLayer.id}"]`
);
const inputElement = document.querySelector(`input[data-child-layer-id="${childLayer.id}"]`);
if (inputElement) {
inputElement.focus();
inputElement.select();
@@ -943,8 +893,7 @@ function handleChildLayerEditKeydown(event, childLayer) {
// 选中子图层
function selectChildLayer(layerId, parentId) {
if (!isMultiSelectMode.value)
layerManager?.setActiveLayer(layerId, { parentId });
if (!isMultiSelectMode.value) layerManager?.setActiveLayer(layerId, { parentId });
}
// 子图层右键菜单处理
@@ -991,8 +940,7 @@ function buildChildLayerContextMenuItems(childLayer) {
{
label: childLayer.visible ? "隐藏图层" : "显示图层",
icon: childLayer.visible ? "CUnEye" : "CEye",
action: () =>
toggleChildLayerVisibility(childLayer.id, childLayer.parentId),
action: () => toggleChildLayerVisibility(childLayer.id, childLayer.parentId),
},
{ type: "divider" },
// 移出组
@@ -1045,9 +993,7 @@ async function handleRootLayersSort(event) {
try {
layerManager?.reorderLayers(oldIndex, newIndex, layerId);
console.log(
`✅ 图层排序命令执行成功: ${layerToMove.name} (${oldIndex} -> ${newIndex})`
);
console.log(`✅ 图层排序命令执行成功: ${layerToMove.name} (${oldIndex} -> ${newIndex})`);
} catch (error) {
console.error("❌ 图层排序命令执行失败:", error);
emit("reorder-layers", {
@@ -1085,12 +1031,7 @@ async function handleChildLayersSort(event, childrenLayers, parentId) {
const layerToMove = childrenLayers[oldIndex];
if (!layerToMove) {
console.error(
"❌ 找不到要移动的子图层oldIndex:",
oldIndex,
"childLayers:",
childrenLayers
);
console.error("❌ 找不到要移动的子图层oldIndex:", oldIndex, "childLayers:", childrenLayers);
return;
}
@@ -1105,9 +1046,7 @@ async function handleChildLayersSort(event, childrenLayers, parentId) {
try {
layerManager?.moveLayerToIndex({ parentId, oldIndex, newIndex, layerId });
console.log(
`✅ 子图层排序命令执行成功: ${layerToMove.name} (${oldIndex} -> ${newIndex})`
);
console.log(`✅ 子图层排序命令执行成功: ${layerToMove.name} (${oldIndex} -> ${newIndex})`);
} catch (error) {
console.error("❌ 子图层排序命令执行失败:", error);
emit("reorder-child-layers", {
@@ -1168,11 +1107,7 @@ async function mergeGroupLayer(groupId) {
const childLayerId = await layerManager.mergeGroupLayers(groupId);
if (childLayerId) {
const groupLayer = layers.value.find((l) => l.id === groupId);
console.log(
`✅ 成功合并组图层: ${
groupLayer?.name || groupId
}, 生成 ${childLayerId} 图层`
);
console.log(`✅ 成功合并组图层: ${groupLayer?.name || groupId}, 生成 ${childLayerId} 图层`);
} else {
console.warn("合并组图层失败");
}
@@ -1284,8 +1219,7 @@ async function handleCrossLevelMove(moveData) {
目标图层不是组图层: "只能将图层移动到组图层中",
};
const userMessage =
errorMessages[error.message] || `移动失败: ${error.message}`;
const userMessage = errorMessages[error.message] || `移动失败: ${error.message}`;
// 这里可以触发一个全局的错误提示组件
// 暂时使用console.warn实际项目中应该替换为适当的提示方式
@@ -1315,9 +1249,7 @@ async function executeDirectMove(moveData) {
} else if (fromContainerType === "child" && fromParentId) {
sourceParent = layers.value.find((layer) => layer.id === fromParentId);
if (sourceParent && sourceParent.children) {
draggedLayer = sourceParent.children.find(
(child) => child.id === layerId
);
draggedLayer = sourceParent.children.find((child) => child.id === layerId);
}
}
@@ -1385,9 +1317,7 @@ async function moveRootToGroup(draggedLayer, toParentId, newIndex) {
}
// 从顶级图层数组中移除
const rootIndex = layers.value.findIndex(
(layer) => layer.id === draggedLayer.id
);
const rootIndex = layers.value.findIndex((layer) => layer.id === draggedLayer.id);
if (rootIndex !== -1) {
layers.value.splice(rootIndex, 1);
}
@@ -1432,9 +1362,7 @@ async function moveGroupToRoot(draggedLayer, fromParentId, newIndex) {
} else {
// 回退方案:手动更新图层关系
// 从源父图层的children中移除
const childIndex = sourceParent.children.findIndex(
(child) => child.id === draggedLayer.id
);
const childIndex = sourceParent.children.findIndex((child) => child.id === draggedLayer.id);
if (childIndex !== -1) {
sourceParent.children.splice(childIndex, 1);
}
@@ -1461,12 +1389,7 @@ async function moveGroupToRoot(draggedLayer, fromParentId, newIndex) {
}
// 在不同组之间移动
async function moveGroupToGroup(
draggedLayer,
fromParentId,
toParentId,
newIndex
) {
async function moveGroupToGroup(draggedLayer, fromParentId, toParentId, newIndex) {
console.log("🔄 在不同组间移动:", {
layerId: draggedLayer.id,
fromParentId,
@@ -1488,18 +1411,11 @@ async function moveGroupToGroup(
// 使用 layerManager 的方法在组间移动
if (layerManager?.moveLayerBetweenGroups) {
await layerManager.moveLayerBetweenGroups(
draggedLayer.id,
fromParentId,
toParentId,
newIndex
);
await layerManager.moveLayerBetweenGroups(draggedLayer.id, fromParentId, toParentId, newIndex);
} else {
// 回退方案:手动更新图层关系
// 从源父图层中移除
const childIndex = sourceParent.children.findIndex(
(child) => child.id === draggedLayer.id
);
const childIndex = sourceParent.children.findIndex((child) => child.id === draggedLayer.id);
if (childIndex !== -1) {
sourceParent.children.splice(childIndex, 1);
@@ -1585,19 +1501,11 @@ async function moveGroupToGroup(
>
<SvgIcon name="CPlusTop" size="16"></SvgIcon>
</div>
<div
class="add-layer-btn action-btn"
@click="addLayer"
:title="$t('添加图层')"
>
<div class="add-layer-btn action-btn" @click="addLayer" :title="$t('添加图层')">
<SvgIcon name="CPlus" size="16"></SvgIcon>
</div>
<div
class="select-all-btn action-btn"
@click="selectAllLayers"
:title="$t('全选图层')"
>
<div class="select-all-btn action-btn" @click="selectAllLayers" :title="$t('全选图层')">
<SvgIcon name="CCheckbox" size="16"></SvgIcon>
</div>
</div>
@@ -1668,15 +1576,11 @@ async function moveGroupToGroup(
:editing-name="editingLayerName"
:can-delete="false"
:isHidenDragHandle="true"
@toggle-visibility="
(...args) => forwardEvent('toggle-layer-visibility', ...args)
"
@toggle-visibility="(...args) => forwardEvent('toggle-layer-visibility', ...args)"
@edit-confirm="(...args) => forwardEvent('edit-confirm', ...args)"
@edit-cancel="(...args) => forwardEvent('edit-cancel', ...args)"
@edit-keydown="(...args) => forwardEvent('edit-keydown', ...args)"
@update:editing-name="
(...args) => forwardEvent('update:editing-name', ...args)
"
@update:editing-name="(...args) => forwardEvent('update:editing-name', ...args)"
/>
</div>

View File

@@ -374,10 +374,7 @@ onMounted(() => {
});
// 创建状态管理器
stateManager.value = new LiquifyStateManager(
props.canvas,
realtimeUpdater.value
);
stateManager.value = new LiquifyStateManager(props.canvas, realtimeUpdater.value);
}
// 监听画布事件
@@ -446,12 +443,7 @@ function showPanel(event) {
return;
}
console.log(
"显示液化面板,目标对象:",
targetObj.type,
"图层ID:",
targetLayerIdValue
);
console.log("显示液化面板,目标对象:", targetObj.type, "图层ID:", targetLayerIdValue);
// 更新面板状态,但保持当前模式不变
// 只有在首次显示面板或面板已关闭时才重置模式
@@ -521,8 +513,8 @@ function showPanel(event) {
const status = props.liquifyManager.getStatus
? props.liquifyManager.getStatus()
: props.liquifyManager.enhancedManager
? props.liquifyManager.enhancedManager.getStatus()
: {};
? props.liquifyManager.enhancedManager.getStatus()
: {};
webglAvailable.value = status.isWebGLAvailable || false;
@@ -553,8 +545,8 @@ function showPanel(event) {
const status = props.liquifyManager.getStatus
? props.liquifyManager.getStatus()
: props.liquifyManager.enhancedManager
? props.liquifyManager.enhancedManager.getStatus()
: {};
? props.liquifyManager.enhancedManager.getStatus()
: {};
webglAvailable.value = status.isWebGLAvailable || false;
@@ -613,11 +605,7 @@ function setTargetObject(obj) {
// 确保对象有唯一ID
if (!obj.id && !obj.objectId && !obj.uid) {
obj.id =
"liquify_target_" +
Date.now() +
"_" +
Math.random().toString(36).substr(2, 9);
obj.id = "liquify_target_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9);
}
targetObject.value = obj;
@@ -685,9 +673,7 @@ function updateParam(paramName, value) {
`set${paramName.charAt(0).toUpperCase() + paramName.slice(1)}`
] === "function"
) {
props.liquifyManager[
`set${paramName.charAt(0).toUpperCase() + paramName.slice(1)}`
](value);
props.liquifyManager[`set${paramName.charAt(0).toUpperCase() + paramName.slice(1)}`](value);
} else {
console.warn(`❌ 液化管理器不支持设置参数: ${paramName}`);
}
@@ -770,19 +756,9 @@ async function getCurrentImageData(targetObject) {
tempCtx.drawImage(element, 0, 0, tempCanvas.width, tempCanvas.height);
// 获取ImageData
const imageData = tempCtx.getImageData(
0,
0,
tempCanvas.width,
tempCanvas.height
);
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
console.log(
"✅ 成功获取当前图像状态,尺寸:",
imageData.width,
"x",
imageData.height
);
console.log("✅ 成功获取当前图像状态,尺寸:", imageData.width, "x", imageData.height);
return imageData;
} catch (error) {
console.warn("获取当前图像数据失败:", error);
@@ -861,9 +837,7 @@ async function handleMouseDown(event) {
const imageCoords = _convertFabricCoordsToImageCoords(pointer.x, pointer.y);
if (imageCoords) {
props.liquifyManager.startLiquifyOperation(imageCoords.x, imageCoords.y);
console.log(
`开始液化操作,图像坐标: (${imageCoords.x}, ${imageCoords.y})`
);
console.log(`开始液化操作,图像坐标: (${imageCoords.x}, ${imageCoords.y})`);
}
}
@@ -959,27 +933,15 @@ async function handleMouseUp() {
// 尝试从实时更新器获取当前图像数据
if (realtimeUpdater.value) {
try {
const currentImageElement =
realtimeUpdater.value.getTargetObject()?._element;
const currentImageElement = realtimeUpdater.value.getTargetObject()?._element;
if (currentImageElement) {
// 从当前图像元素创建ImageData
const tempCanvas = document.createElement("canvas");
tempCanvas.width = initialImageData.value.width;
tempCanvas.height = initialImageData.value.height;
const tempCtx = tempCanvas.getContext("2d");
tempCtx.drawImage(
currentImageElement,
0,
0,
tempCanvas.width,
tempCanvas.height
);
finalImageData = tempCtx.getImageData(
0,
0,
tempCanvas.width,
tempCanvas.height
);
tempCtx.drawImage(currentImageElement, 0, 0, tempCanvas.width, tempCanvas.height);
finalImageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
console.log(
"✅ 从实时更新器获取最终图像数据成功,尺寸:",
@@ -1000,19 +962,8 @@ async function handleMouseUp() {
tempCanvas.width = initialImageData.value.width;
tempCanvas.height = initialImageData.value.height;
const tempCtx = tempCanvas.getContext("2d");
tempCtx.drawImage(
currentTarget._element,
0,
0,
tempCanvas.width,
tempCanvas.height
);
finalImageData = tempCtx.getImageData(
0,
0,
tempCanvas.width,
tempCanvas.height
);
tempCtx.drawImage(currentTarget._element, 0, 0, tempCanvas.width, tempCanvas.height);
finalImageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
console.log(
"✅ 从目标对象获取最终图像数据成功,尺寸:",
@@ -1097,14 +1048,7 @@ async function applyLiquifyAtPoint(x, y) {
return;
}
console.log(
"原始坐标:",
x,
y,
"转换后图像坐标:",
imageCoords.x,
imageCoords.y
);
console.log("原始坐标:", x, y, "转换后图像坐标:", imageCoords.x, imageCoords.y);
// 获取当前参数
const params = {
@@ -1169,8 +1113,7 @@ async function applyLiquifyAtPoint(x, y) {
targetObject.value = updatedObject;
// 如果对象有新的ID也要更新ID
if (updatedObject.id || updatedObject.objectId || updatedObject.uid) {
targetObjectId.value =
updatedObject.id || updatedObject.objectId || updatedObject.uid;
targetObjectId.value = updatedObject.id || updatedObject.objectId || updatedObject.uid;
}
}
}
@@ -1184,8 +1127,7 @@ async function applyLiquifyAtPoint(x, y) {
if (avgOperationTime.value === 0) {
avgOperationTime.value = operationTime;
} else {
avgOperationTime.value =
0.7 * avgOperationTime.value + 0.3 * operationTime;
avgOperationTime.value = 0.7 * avgOperationTime.value + 0.3 * operationTime;
}
// 将性能指标传递给状态管理器
@@ -1265,7 +1207,7 @@ async function _updateImageOnCanvas(imageData) {
}
} else {
// 拖拽结束后进行完整的对象替换
await _replaceTargetObjectWithNewImage(dataURL);
// await _replaceTargetObjectWithNewImage(dataURL);
}
} catch (error) {
console.error("更新画布图像时出错:", error);
@@ -1325,9 +1267,7 @@ async function _replaceTargetWithNewImage(dataURL) {
if (layer.type === "background" || layer.isBackground) {
layer.fabricObject = img;
} else if (layer.fabricObjects) {
const objIndex = layer.fabricObjects.findIndex(
(obj) => obj.id === img.id
);
const objIndex = layer.fabricObjects.findIndex((obj) => obj.id === img.id);
if (objIndex !== -1) {
layer.fabricObjects[objIndex] = img;
}
@@ -1364,10 +1304,7 @@ function _convertFabricCoordsToImageCoords(fabricX, fabricY) {
const matrix = fabric.util.invertTransform(transform);
// 应用逆变换,将画布坐标转换为对象本地坐标
const localPoint = fabric.util.transformPoint(
new fabric.Point(fabricX, fabricY),
matrix
);
const localPoint = fabric.util.transformPoint(new fabric.Point(fabricX, fabricY), matrix);
// 获取图像的原始尺寸(未缩放前)
const imageWidth = originalImageData.value.width;
@@ -1403,12 +1340,7 @@ function _convertFabricCoordsToImageCoords(fabricX, fabricY) {
);
// 检查坐标是否在图像范围内
if (
imageX < 0 ||
imageX >= imageWidth ||
imageY < 0 ||
imageY >= imageHeight
) {
if (imageX < 0 || imageX >= imageWidth || imageY < 0 || imageY >= imageHeight) {
console.warn(
`坐标超出图像范围: (${imageX}, ${imageY}), 图像尺寸: ${imageWidth}x${imageHeight}`
);
@@ -1607,7 +1539,9 @@ function stopPressTimer() {
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s, transform 0.3s;
transition:
opacity 0.3s,
transform 0.3s;
}
.fade-enter-from,
.fade-leave-to {

View File

@@ -68,9 +68,7 @@ onBeforeUnmount(() => {
<div class="minimap-container">
<div class="minimap-header">
<span>画布小地图</span>
<button class="minimap-refresh" @click="forceRefresh" title="刷新小地图">
</button>
<button class="minimap-refresh" @click="forceRefresh" title="刷新小地图"></button>
</div>
<div class="minimap-content" ref="minimapContainerRef">
<!-- 不再需要直接提供canvas引用由MinimapManager内部创建 -->

View File

@@ -10,30 +10,21 @@
<div class="tool-types">
<div
:class="[
'tool-btn',
{ active: selectionType === OperationType.LASSO },
]"
:class="['tool-btn', { active: selectionType === OperationType.LASSO }]"
@click="setSelectionType(OperationType.LASSO)"
>
<svg-icon name="CFree" size="26" />
<span>{{ $t("手绘") }}</span>
</div>
<div
:class="[
'tool-btn',
{ active: selectionType === OperationType.LASSO_RECTANGLE },
]"
:class="['tool-btn', { active: selectionType === OperationType.LASSO_RECTANGLE }]"
@click="setSelectionType(OperationType.LASSO_RECTANGLE)"
>
<svg-icon name="CRectangle" size="32" />
<span>{{ $t("矩形") }}</span>
</div>
<div
:class="[
'tool-btn',
{ active: selectionType === OperationType.LASSO_ELLIPSE },
]"
:class="['tool-btn', { active: selectionType === OperationType.LASSO_ELLIPSE }]"
@click="setSelectionType(OperationType.LASSO_ELLIPSE)"
>
<svg-icon name="CEllipse" size="30" />
@@ -159,9 +150,7 @@
<div class="dialog-container">
<div class="dialog-header">
<h3>{{ $t("选择填充颜色") }}</h3>
<button class="close-dialog-btn" @click="cancelColorPicker">
×
</button>
<button class="close-dialog-btn" @click="cancelColorPicker">×</button>
</div>
<div class="dialog-content">
<input type="color" v-model="fillColor" class="color-picker" />
@@ -249,6 +238,7 @@ onMounted(() => {
checkSelectionStatus();
// 设置选区状态变化的回调
// eslint-disable-next-line vue/no-mutating-props
props.selectionManager.onSelectionChanged = () => {
checkSelectionStatus();
};
@@ -321,8 +311,7 @@ function setSelectionType(type) {
*/
function checkSelectionStatus() {
hasSelection.value =
props.selectionManager &&
props.selectionManager.getSelectionObject() !== null;
props.selectionManager && props.selectionManager.getSelectionObject() !== null;
// 同步羽化值
if (hasSelection.value) {
@@ -836,7 +825,9 @@ function confirmColorPicker() {
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s, transform 0.3s;
transition:
opacity 0.3s,
transform 0.3s;
}
.fade-enter-from,
.fade-leave-to {

View File

@@ -1,10 +1,6 @@
<!-- filepath: /Users/aaron/work/pc/air/canvasEdit/src/components/CanvasEditor/components/TextEditorPanel.vue -->
<template>
<div
v-if="visible"
class="text-editor-panel"
:class="{ 'is-active': visible }"
>
<div v-if="visible" class="text-editor-panel" :class="{ 'is-active': visible }">
<div class="text-editor-panel-header">
<div class="header-btn import-btn">编辑文本样式</div>
<div class="header-actions">
@@ -185,10 +181,7 @@
<div class="bg-header">字体色</div>
<div class="bg-options">
<div class="style-btn color-btn" @click="openColorPicker('text')">
<div
class="style-icon color-icon"
:style="{ backgroundColor: textColor }"
></div>
<div class="style-icon color-icon" :style="{ backgroundColor: textColor }"></div>
</div>
</div>
</div>
@@ -196,22 +189,15 @@
<div class="background-controls">
<div class="bg-header">背景色</div>
<div class="bg-options">
<div
class="style-btn color-btn"
@click="openColorPicker('background')"
>
<div class="style-btn color-btn" @click="openColorPicker('background')">
<div
class="style-icon color-icon"
:style="{
backgroundColor: hasTransparentBg
? 'transparent'
: backgroundColor,
backgroundColor: hasTransparentBg ? 'transparent' : backgroundColor,
backgroundImage: hasTransparentBg
? 'linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc), linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc)'
: 'none',
backgroundSize: hasTransparentBg
? '8px 8px, 8px 8px'
: 'auto',
backgroundSize: hasTransparentBg ? '8px 8px, 8px 8px' : 'auto',
backgroundPosition: hasTransparentBg ? '0 0, 4px 4px' : '0 0',
}"
></div>
@@ -232,18 +218,11 @@
<div v-if="showColorPicker" class="color-picker-modal">
<div class="color-picker-container">
<div class="color-picker-header">
<span>{{
colorPickerMode === "text" ? "选择文字颜色" : "选择背景颜色"
}}</span>
<span>{{ colorPickerMode === "text" ? "选择文字颜色" : "选择背景颜色" }}</span>
<div class="close-color-picker" @click="closeColorPicker">×</div>
</div>
<div class="color-picker-content">
<input
type="color"
v-model="currentColor"
@change="updateColor"
class="color-input"
/>
<input type="color" v-model="currentColor" @change="updateColor" class="color-input" />
<div class="color-presets">
<div
v-for="(color, index) in colorPresets"
@@ -253,9 +232,7 @@
@click="selectPresetColor(color)"
></div>
</div>
<div class="confirm-color-btn" @click="confirmColorSelection">
确定
</div>
<div class="confirm-color-btn" @click="confirmColorSelection">确定</div>
</div>
</div>
</div>
@@ -316,7 +293,7 @@ export default {
// 新增属性
const charSpacingPercent = ref(0);
const textSpacing = ref(0);
const baseline = ref(0);
// const baseline = ref(0);
const showColorPicker = ref(false);
const colorPickerMode = ref("text"); // 'text' 或 'background'
const currentColor = ref("#000000");
@@ -406,8 +383,7 @@ export default {
textColor.value = textObject.value.fill || "#000000";
backgroundColor.value = textObject.value.textBackgroundColor || "#ffffff";
hasTransparentBg.value = !textObject.value.textBackgroundColor;
opacity.value =
textObject.value.opacity !== undefined ? textObject.value.opacity : 1;
opacity.value = textObject.value.opacity !== undefined ? textObject.value.opacity : 1;
// 样式
fontWeight.value = textObject.value.fontWeight || "normal";
@@ -422,7 +398,7 @@ export default {
// 转换字符间距为百分比显示
charSpacingPercent.value = charSpacing.value / 10;
textSpacing.value = charSpacingPercent.value; // 暂用相同值
baseline.value = 0; // Fabric.js没有直接支持基线偏移用0初始化
// baseline.value = 0; // Fabric.js没有直接支持基线偏移用0初始化
};
const selectFont = (fontName) => {
@@ -459,8 +435,7 @@ export default {
// 颜色选择器相关功能
const openColorPicker = (mode) => {
colorPickerMode.value = mode;
currentColor.value =
mode === "text" ? textColor.value : backgroundColor.value;
currentColor.value = mode === "text" ? textColor.value : backgroundColor.value;
showColorPicker.value = true;
};
@@ -602,12 +577,12 @@ export default {
executeCommand(command);
};
// 基线更新 (Fabric.js没有直接支持可能需要自定义实现)
const updateBaseline = () => {
console.log("基线调整功能待实现", baseline.value);
// 注意Fabric.js 5没有直接支持基线调整
// 可能需要通过自定义处理或CSS方式实现
};
// // 基线更新 (Fabric.js没有直接支持可能需要自定义实现)
// const updateBaseline = () => {
// // console.log("基线调整功能待实现", baseline.value);
// // 注意Fabric.js 5没有直接支持基线调整
// // 可能需要通过自定义处理或CSS方式实现
// };
// 透明度更新
const updateOpacity = () => {
@@ -662,7 +637,7 @@ export default {
lineHeight,
charSpacingPercent,
textSpacing,
baseline,
// baseline,
showColorPicker,
colorPickerMode,
currentColor,
@@ -690,7 +665,7 @@ export default {
updateCharSpacing,
updateTextSpacing,
updateLineHeight,
updateBaseline,
// updateBaseline,
updateOpacity,
executeCommand,
};

View File

@@ -259,10 +259,7 @@ function handleKeyDown(event) {
const key = event.key.toUpperCase();
// 当处于输入状态时不触发快捷键
if (
event.target.tagName === "INPUT" ||
event.target.tagName === "TEXTAREA"
) {
if (event.target.tagName === "INPUT" || event.target.tagName === "TEXTAREA") {
return;
}
@@ -312,10 +309,7 @@ const handleToolClick = (tool) => {
/>
<!-- 自定义工具栏按钮插槽 -->
<slot
name="customTools"
:tool-button-props="{ activeTool, canUndo, canRedo }"
/>
<slot name="customTools" :tool-button-props="{ activeTool, canUndo, canRedo }" />
</div>
</template>

View File

@@ -7,10 +7,7 @@
@touchstart.prevent="startTouchSliding"
@click="handleClick"
>
<div
class="slider-fill"
:style="{ height: `${displayPercentage}%` }"
></div>
<div class="slider-fill" :style="{ height: `${displayPercentage}%` }"></div>
<div
class="slider-thumb"
:style="{
@@ -154,9 +151,7 @@ function clearHideTooltipTimer() {
// 计算tooltip的可见性优先使用props中的showTooltip否则使用内部状态
const tooltipVisible = computed(() => {
return props.showTooltip !== undefined
? props.showTooltip
: internalShowTooltip.value;
return props.showTooltip !== undefined ? props.showTooltip : internalShowTooltip.value;
});
// 更新tooltip状态的方法
@@ -250,8 +245,7 @@ function updateValueFromMousePosition(event) {
}
} else {
// 检查是否可以进入吸附状态
const snapPercentage =
(props.snapThreshold / trackHeight) * (props.max - props.min);
const snapPercentage = (props.snapThreshold / trackHeight) * (props.max - props.min);
// 检查是否接近预设值,增加吸附判定范围
for (const preset of props.presets) {
@@ -417,8 +411,7 @@ function updateValueFromTouchPosition(event) {
}
} else {
// 检查是否可以进入吸附状态
const snapPercentage =
(props.snapThreshold / trackHeight) * (props.max - props.min);
const snapPercentage = (props.snapThreshold / trackHeight) * (props.max - props.min);
// 检查是否接近预设值,增加吸附判定范围
for (const preset of props.presets) {
@@ -500,9 +493,7 @@ onMounted(() => {
// 添加全局点击事件监听
if (props.closeOnOutsideClick) {
// 使用 setTimeout 确保点击事件在其他处理程序之后执行
window.addEventListener("click", (e) =>
setTimeout(() => handleOutsideClick(e), 0)
);
window.addEventListener("click", (e) => setTimeout(() => handleOutsideClick(e), 0));
}
});
@@ -616,7 +607,9 @@ onBeforeUnmount(() => {
background: rgba(255, 255, 255, 0.95);
border-radius: 10px;
padding: 10px;
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.05);
box-shadow:
0 3px 12px rgba(0, 0, 0, 0.1),
0 0 0 1px rgba(0, 0, 0, 0.05);
min-width: 120px;
z-index: 10;
@@ -664,7 +657,9 @@ onBeforeUnmount(() => {
// 淡入淡出动画
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s, transform 0.3s;
transition:
opacity 0.3s,
transform 0.3s;
}
.fade-enter-from,
.fade-leave-to {