feat: 裁剪组裁剪跟随选择组移动
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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; // 确保空组也有足够的拖拽区域
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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内部创建 -->
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user