接入画布
This commit is contained in:
@@ -0,0 +1,685 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div v-if="isVisible" class="brush-control-panel">
|
||||
<!-- 笔刷大小控制 -->
|
||||
<VerticalSlider
|
||||
v-model="brushSize"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:presets="sizePresets"
|
||||
:memorized-values="memorizedSizes"
|
||||
:active-threshold="1"
|
||||
custom-class="size-slider"
|
||||
:step="1"
|
||||
v-model:showTooltip="showSizeTooltip"
|
||||
@slide-start="handleSizeSlideStart"
|
||||
@slide-end="handleSizeSlideEnd"
|
||||
@click="showSizeTooltip = true"
|
||||
>
|
||||
<template #tooltip-content>
|
||||
<div class="tooltip-header">
|
||||
<div class="tooltip-title">Size</div>
|
||||
<div class="tooltip-close-btn" @click.stop="closeSizeTooltip">
|
||||
<SvgIcon name="CClose" size="20" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="brush-preview-container">
|
||||
<div
|
||||
class="brush-size-preview"
|
||||
:style="{
|
||||
width: `${Math.min(100, brushSize)}px`,
|
||||
height: `${Math.min(100, brushSize)}px`,
|
||||
backgroundColor: showColorPicker ? brushColor : '#888888',
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<div class="tooltip-content">
|
||||
<div class="tooltip-text">{{ Math.round(brushSize) }}px</div>
|
||||
<div class="tooltip-controls">
|
||||
<button
|
||||
v-if="!memorizedSizes.includes(brushSize)"
|
||||
class="control-btn add"
|
||||
@click="memorizeSize"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
class="control-btn remove"
|
||||
@click="removeMemorizedSize"
|
||||
v-if="canRemoveSize"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VerticalSlider>
|
||||
|
||||
<!-- 颜色选择器 - 仅在特定工具下显示 -->
|
||||
<div v-if="showColorPicker" class="color-picker-container">
|
||||
<label for="color-picker" class="current-color-label">
|
||||
<div
|
||||
class="current-color"
|
||||
:style="{ backgroundColor: brushColor }"
|
||||
></div>
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
id="color-picker"
|
||||
class="system-color-picker"
|
||||
v-model="customColor"
|
||||
@input="setBrushColor(customColor)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 透明度控制 - 仅在特定工具下显示 -->
|
||||
<VerticalSlider
|
||||
v-if="showOpacitySlider"
|
||||
v-model="brushOpacity"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:presets="opacityPresets"
|
||||
:memorized-values="memorizedOpacities"
|
||||
:is-percentage="true"
|
||||
custom-class="opacity-slider"
|
||||
:active-threshold="0.01"
|
||||
:step="0.01"
|
||||
v-model:showTooltip="showOpacityTooltip"
|
||||
@slide-start="handleOpacitySlideStart"
|
||||
@slide-end="handleOpacitySlideEnd"
|
||||
@click="showOpacityTooltip = true"
|
||||
>
|
||||
<template #tooltip-content>
|
||||
<div class="tooltip-header">
|
||||
<div class="tooltip-title">Opacity</div>
|
||||
<div class="tooltip-close-btn" @click.stop="closeOpacityTooltip">
|
||||
<SvgIcon name="CClose" size="20" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="opacity-preview">
|
||||
<div class="opacity-checker"></div>
|
||||
<div
|
||||
class="opacity-color"
|
||||
:style="{
|
||||
backgroundColor: brushColor,
|
||||
opacity: brushOpacity,
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<div class="tooltip-content">
|
||||
<div class="tooltip-text">
|
||||
{{ Math.round(brushOpacity * 100) }}%
|
||||
</div>
|
||||
<div class="tooltip-controls">
|
||||
<button
|
||||
class="control-btn add"
|
||||
v-if="!memorizedOpacities.includes(brushOpacity)"
|
||||
@click="memorizeOpacity"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
class="control-btn remove"
|
||||
@click="removeMemorizedOpacity"
|
||||
v-if="canRemoveOpacity"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VerticalSlider>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
|
||||
import { BrushStore } from "../store/BrushStore";
|
||||
import { OperationType } from "../utils/layerHelper";
|
||||
import { inject } from "vue";
|
||||
import VerticalSlider from "./VerticalSlider.vue";
|
||||
|
||||
const props = defineProps({
|
||||
activeTool: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 工具管理器和画布管理器
|
||||
const toolManager = inject("toolManager");
|
||||
const canvasManager = inject("canvasManager");
|
||||
|
||||
// 可见性控制
|
||||
const isVisible = computed(() => {
|
||||
return [
|
||||
OperationType.DRAW,
|
||||
OperationType.ERASER,
|
||||
OperationType.RED_BRUSH,
|
||||
OperationType.GREEN_BRUSH,
|
||||
].includes(props.activeTool);
|
||||
});
|
||||
|
||||
// 控制颜色选择器的显示
|
||||
const showColorPicker = computed(() => {
|
||||
return props.activeTool === OperationType.DRAW;
|
||||
});
|
||||
|
||||
// 控制透明度滑块的显示
|
||||
const showOpacitySlider = computed(() => {
|
||||
return props.activeTool === OperationType.DRAW;
|
||||
});
|
||||
|
||||
// 笔刷大小相关
|
||||
const brushSize = ref(BrushStore.state.size);
|
||||
const showSizeTooltip = ref(false);
|
||||
const sizePresets = ref([5, 10, 20, 50]); // 预设大小,便于吸附
|
||||
const memorizedSizes = ref([]);
|
||||
const canRemoveSize = computed(() => {
|
||||
return memorizedSizes.value.includes(brushSize.value);
|
||||
});
|
||||
const isSizeSliding = ref(false); // 是否正在滑动大小滑块
|
||||
|
||||
// 笔刷颜色相关
|
||||
const brushColor = ref(BrushStore.state.color);
|
||||
const customColor = ref(BrushStore.state.color);
|
||||
|
||||
// 笔刷透明度相关
|
||||
const brushOpacity = ref(BrushStore.state.opacity);
|
||||
const showOpacityTooltip = ref(false);
|
||||
const opacityPresets = ref([0.1, 0.2, 0.5, 0.8]); // 预设透明度,便于吸附
|
||||
const memorizedOpacities = ref([]);
|
||||
const canRemoveOpacity = computed(() => {
|
||||
return memorizedOpacities.value.includes(brushOpacity.value);
|
||||
});
|
||||
const isOpacitySliding = ref(false); // 是否正在滑动透明度滑块
|
||||
|
||||
// 添加计时器变量用于控制外部变化引起的提示框自动隐藏
|
||||
const sizeTooltipTimer = ref(null);
|
||||
const opacityTooltipTimer = ref(null);
|
||||
const TOOLTIP_HIDE_DELAY = 1500; // 与VerticalSlider组件保持一致的延迟时间
|
||||
|
||||
// 处理滑块开始和结束事件
|
||||
function handleSizeSlideStart() {
|
||||
isSizeSliding.value = true;
|
||||
}
|
||||
|
||||
function handleSizeSlideEnd(event) {
|
||||
isSizeSliding.value = false;
|
||||
}
|
||||
|
||||
function handleOpacitySlideStart() {
|
||||
isOpacitySliding.value = true;
|
||||
}
|
||||
|
||||
function handleOpacitySlideEnd(event) {
|
||||
isOpacitySliding.value = false;
|
||||
}
|
||||
|
||||
// 设置笔刷大小
|
||||
function setBrushSize(size) {
|
||||
brushSize.value = size;
|
||||
BrushStore.setBrushSize(size);
|
||||
|
||||
// 如果工具管理器存在,立即应用此更改
|
||||
if (toolManager) {
|
||||
toolManager.updateBrushSize(size);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置笔刷颜色
|
||||
function setBrushColor(color) {
|
||||
brushColor.value = color;
|
||||
customColor.value = color;
|
||||
BrushStore.setBrushColor(color);
|
||||
|
||||
// 如果工具管理器存在,立即应用此更改
|
||||
if (toolManager && props.activeTool === OperationType.DRAW) {
|
||||
toolManager.updateBrushColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置笔刷透明度
|
||||
function setBrushOpacity(opacity) {
|
||||
brushOpacity.value = opacity;
|
||||
BrushStore.setBrushOpacity(opacity);
|
||||
|
||||
// 如果工具管理器存在,立即应用此更改
|
||||
if (toolManager) {
|
||||
toolManager.updateBrushOpacity(opacity);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加用于自动隐藏大小提示框的方法
|
||||
function startSizeTooltipHideTimer() {
|
||||
// 清除已有的计时器
|
||||
clearSizeTooltipTimer();
|
||||
|
||||
// 创建新计时器
|
||||
sizeTooltipTimer.value = setTimeout(() => {
|
||||
showSizeTooltip.value = false;
|
||||
sizeTooltipTimer.value = null;
|
||||
}, TOOLTIP_HIDE_DELAY);
|
||||
}
|
||||
|
||||
// 清除大小提示框隐藏计时器
|
||||
function clearSizeTooltipTimer() {
|
||||
if (sizeTooltipTimer.value) {
|
||||
clearTimeout(sizeTooltipTimer.value);
|
||||
sizeTooltipTimer.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加用于自动隐藏透明度提示框的方法
|
||||
function startOpacityTooltipHideTimer() {
|
||||
// 清除已有的计时器
|
||||
clearOpacityTooltipTimer();
|
||||
|
||||
// 创建新计时器
|
||||
opacityTooltipTimer.value = setTimeout(() => {
|
||||
showOpacityTooltip.value = false;
|
||||
opacityTooltipTimer.value = null;
|
||||
}, TOOLTIP_HIDE_DELAY);
|
||||
}
|
||||
|
||||
// 清除透明度提示框隐藏计时器
|
||||
function clearOpacityTooltipTimer() {
|
||||
if (opacityTooltipTimer.value) {
|
||||
clearTimeout(opacityTooltipTimer.value);
|
||||
opacityTooltipTimer.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 主动关闭提示框
|
||||
function closeSizeTooltip() {
|
||||
showSizeTooltip.value = false;
|
||||
clearSizeTooltipTimer(); // 清除任何存在的计时器
|
||||
}
|
||||
|
||||
function closeOpacityTooltip() {
|
||||
showOpacityTooltip.value = false;
|
||||
clearOpacityTooltipTimer(); // 清除任何存在的计时器
|
||||
}
|
||||
|
||||
// 记忆当前笔刷大小
|
||||
function memorizeSize() {
|
||||
if (!memorizedSizes.value.includes(brushSize.value)) {
|
||||
memorizedSizes.value.push(brushSize.value);
|
||||
// 记忆值限制为最多5个
|
||||
if (memorizedSizes.value.length > 5) {
|
||||
memorizedSizes.value.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除当前记忆的笔刷大小
|
||||
function removeMemorizedSize() {
|
||||
if (memorizedSizes.value.includes(brushSize.value)) {
|
||||
const index = memorizedSizes.value.indexOf(brushSize.value);
|
||||
memorizedSizes.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 记忆当前笔刷透明度
|
||||
function memorizeOpacity() {
|
||||
if (!memorizedOpacities.value.includes(brushOpacity.value)) {
|
||||
memorizedOpacities.value.push(brushOpacity.value);
|
||||
// 记忆值限制为最多5个
|
||||
if (memorizedOpacities.value.length > 5) {
|
||||
memorizedOpacities.value.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除当前记忆的笔刷透明度
|
||||
function removeMemorizedOpacity() {
|
||||
if (memorizedOpacities.value.includes(brushOpacity.value)) {
|
||||
const index = memorizedOpacities.value.indexOf(brushOpacity.value);
|
||||
memorizedOpacities.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 监听工具的变化
|
||||
watch(
|
||||
() => props.activeTool,
|
||||
(newTool) => {
|
||||
// 当切换到橡皮擦工具时,可以设置特殊的默认值
|
||||
if (newTool === OperationType.ERASER) {
|
||||
// 橡皮擦模式下不需要调整颜色,但可能会调整大小和不透明度
|
||||
} else if (newTool === OperationType.DRAW) {
|
||||
// 恢复到绘制模式时可能有特殊设置
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 监听brushSize的变化,更新到BrushStore
|
||||
watch(
|
||||
() => brushSize.value,
|
||||
(newSize) => {
|
||||
setBrushSize(newSize);
|
||||
}
|
||||
);
|
||||
|
||||
// 监听brushOpacity的变化,更新到BrushStore
|
||||
watch(
|
||||
() => brushOpacity.value,
|
||||
(newOpacity) => {
|
||||
setBrushOpacity(newOpacity);
|
||||
}
|
||||
);
|
||||
|
||||
// 监听BrushStore中的变化
|
||||
watch(
|
||||
() => BrushStore.state.size,
|
||||
(newSize) => {
|
||||
if (Math.abs(brushSize.value - newSize) > 0.1) {
|
||||
brushSize.value = newSize;
|
||||
// 当外部修改了笔刷大小时,显示提示框
|
||||
showSizeTooltip.value = true;
|
||||
// 启动自动隐藏计时器
|
||||
startSizeTooltipHideTimer();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => BrushStore.state.opacity,
|
||||
(newOpacity) => {
|
||||
if (Math.abs(brushOpacity.value - newOpacity) > 0.01) {
|
||||
brushOpacity.value = newOpacity;
|
||||
// 当外部修改了笔刷透明度时,显示提示框
|
||||
showOpacityTooltip.value = true;
|
||||
// 启动自动隐藏计时器
|
||||
startOpacityTooltipHideTimer();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => BrushStore.state.color,
|
||||
(newColor) => {
|
||||
if (brushColor.value !== newColor) {
|
||||
brushColor.value = newColor;
|
||||
customColor.value = newColor;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化时从BrushStore获取当前值
|
||||
brushSize.value = BrushStore.state.size;
|
||||
brushOpacity.value = BrushStore.state.opacity;
|
||||
brushColor.value = BrushStore.state.color;
|
||||
customColor.value = BrushStore.state.color;
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 组件卸载前清除所有计时器
|
||||
clearSizeTooltipTimer();
|
||||
clearOpacityTooltipTimer();
|
||||
});
|
||||
|
||||
// 监听showSizeTooltip和showOpacityTooltip的变化,防止两个滑块的提示框同时显示
|
||||
watch(
|
||||
() => showSizeTooltip.value,
|
||||
(newValue) => {
|
||||
if (newValue && showOpacityTooltip.value) {
|
||||
// 如果大小提示框显示,则隐藏透明度提示框
|
||||
showOpacityTooltip.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => showOpacityTooltip.value,
|
||||
(newValue) => {
|
||||
if (newValue && showSizeTooltip.value) {
|
||||
// 如果透明度提示框显示,则隐藏大小提示框
|
||||
showSizeTooltip.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.brush-control-panel {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 15px;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
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);
|
||||
z-index: 8;
|
||||
backdrop-filter: blur(2px);
|
||||
color: #333;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
// 笔刷大小预览相关样式
|
||||
.brush-preview-container {
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
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;
|
||||
}
|
||||
|
||||
.brush-size-preview {
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
// 透明度预览相关样式
|
||||
.opacity-preview {
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.opacity-checker {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% /
|
||||
10px 10px;
|
||||
}
|
||||
|
||||
.opacity-color {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// 工具提示内容样式
|
||||
.tooltip-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.tooltip-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
background: #f5f5f5;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tooltip-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&.add {
|
||||
color: #4caf50;
|
||||
|
||||
&:hover {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&.remove {
|
||||
color: #f44336;
|
||||
|
||||
&:hover {
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 颜色选择器样式
|
||||
.color-picker-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.current-color-label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.current-color {
|
||||
width: 32px;
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
.system-color-picker {
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// 工具提示标题和关闭按钮
|
||||
.tooltip-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.tooltip-title {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tooltip-close-btn {
|
||||
position: absolute;
|
||||
right: 3px;
|
||||
top: 3px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 99;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
// 淡入淡出动画
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px) translateY(-50%);
|
||||
}
|
||||
|
||||
// 响应式调整
|
||||
@media (max-height: 600px) {
|
||||
.brush-control-panel {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.brush-control-panel {
|
||||
left: 10px;
|
||||
// padding: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1596
src/component/Canvas/CanvasEditor/components/BrushPanel.vue
Normal file
1596
src/component/Canvas/CanvasEditor/components/BrushPanel.vue
Normal file
File diff suppressed because it is too large
Load Diff
525
src/component/Canvas/CanvasEditor/components/HeaderMenu.vue
Normal file
525
src/component/Canvas/CanvasEditor/components/HeaderMenu.vue
Normal file
@@ -0,0 +1,525 @@
|
||||
<script setup>
|
||||
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";
|
||||
|
||||
// 提供brushStore给子组件
|
||||
provide("brushStore", BrushStore);
|
||||
|
||||
const toolManager = inject("toolManager");
|
||||
const layerManager = inject("layerManager");
|
||||
|
||||
const props = defineProps({
|
||||
activeTool: String,
|
||||
canvasWidth: Number,
|
||||
canvasHeight: Number,
|
||||
canvasColor: String,
|
||||
brushSize: Number,
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
"update:canvasWidth",
|
||||
"update:canvasHeight",
|
||||
"update:canvasColor",
|
||||
"update:brushSize",
|
||||
"canvas-size-change",
|
||||
"canvas-color-change",
|
||||
]);
|
||||
|
||||
// 笔刷面板相关状态
|
||||
const showBrushPanel = ref(false);
|
||||
const brushPanelRef = ref(null);
|
||||
|
||||
// 计算属性
|
||||
const shouldShowBrushSettings = computed(() => {
|
||||
return props.activeTool === OperationType.DRAW;
|
||||
});
|
||||
|
||||
function updateCanvasSize() {
|
||||
if (!layerManager) {
|
||||
console.warn("LayerManager 未初始化,无法调整背景层尺寸");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查画布上是否有除了背景层的其他元素
|
||||
const hasOtherElements = layerManager.layers.value.some((layer) => {
|
||||
if (layer.isBackground) return false;
|
||||
// 检查普通图层是否有对象
|
||||
if (layer.fabricObjects && layer.fabricObjects.length > 0) return true;
|
||||
// 检查固定图层是否有对象
|
||||
if (layer.isFixed && layer.fabricObjects && layer.fabricObjects.length > 0)
|
||||
return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hasOtherElements) {
|
||||
// 有其他元素时使用等比缩放命令
|
||||
layerManager.resizeCanvasWithScale(props.canvasWidth, props.canvasHeight);
|
||||
} else {
|
||||
// 只有背景层时使用普通调整命令
|
||||
layerManager.resizeCanvas(props.canvasWidth, props.canvasHeight);
|
||||
}
|
||||
|
||||
emit("canvas-size-change");
|
||||
}
|
||||
|
||||
function updateCanvasColor() {
|
||||
if (!layerManager) {
|
||||
console.warn("LayerManager 未初始化,无法更改背景色");
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新背景层颜色而不是画布颜色
|
||||
layerManager.updateBackgroundColor(props.canvasColor);
|
||||
|
||||
emit("canvas-color-change");
|
||||
}
|
||||
|
||||
// 切换笔刷面板显示状态
|
||||
function toggleBrushPanel() {
|
||||
showBrushPanel.value = !showBrushPanel.value;
|
||||
}
|
||||
|
||||
// 处理笔刷大小变化
|
||||
function handleBrushSizeChange(event) {
|
||||
const newSize = parseFloat(event.target.value);
|
||||
emit("update:brushSize", newSize);
|
||||
}
|
||||
|
||||
// 处理笔刷设置变化,将BrushStore的数据同步到brushManager
|
||||
function syncBrushStoreToManager() {
|
||||
if (!toolManager?.brushManager) return;
|
||||
|
||||
const brushManager = toolManager.brushManager;
|
||||
|
||||
// 检查画笔是否正在更新中
|
||||
if (brushManager.isUpdatingBrush) {
|
||||
console.warn("画笔正在更新中,请稍候...");
|
||||
// 延迟重试,确保在画笔更新完成后应用最新设置
|
||||
setTimeout(syncBrushStoreToManager, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// 监听BrushStore的变化,更新brushManager
|
||||
const size = BrushStore.state.size;
|
||||
const color = BrushStore.state.color;
|
||||
const type = BrushStore.state.type;
|
||||
const opacity = BrushStore.state.opacity;
|
||||
const textureEnabled = BrushStore.state.textureEnabled;
|
||||
const texturePath = BrushStore.state.texturePath;
|
||||
const textureScale = BrushStore.state.textureScale;
|
||||
|
||||
// 将所有更改一次性应用,减少updateBrush调用次数
|
||||
let needsUpdate = false;
|
||||
|
||||
if (
|
||||
brushManager.brushSize &&
|
||||
typeof brushManager.setBrushSize === "function" &&
|
||||
brushManager.getBrushSize() !== size
|
||||
) {
|
||||
brushManager.brushSize.value = size; // 直接设置值,避免触发updateBrush
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
if (
|
||||
brushManager.brushColor &&
|
||||
typeof brushManager.setBrushColor === "function" &&
|
||||
brushManager.getBrushColor() !== color
|
||||
) {
|
||||
brushManager.brushColor.value = color; // 直接设置值,避免触发updateBrush
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof brushManager.setBrushType === "function" &&
|
||||
brushManager.getCurrentBrushType() !== type
|
||||
) {
|
||||
brushManager.setBrushType(type);
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
if (typeof brushManager.setBrushOpacity === "function") {
|
||||
brushManager.setBrushOpacity(opacity);
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
// 同步材质相关设置
|
||||
if (textureEnabled && texturePath) {
|
||||
if (typeof brushManager.setTexturePath === "function") {
|
||||
brushManager.setTexturePath(texturePath);
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof brushManager.setTextureScale === "function" &&
|
||||
brushManager.getTextureScale() !== textureScale
|
||||
) {
|
||||
brushManager.textureScale.value = textureScale; // 直接设置值,避免触发updateBrush
|
||||
needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 只在有变化时调用一次updateBrush,减少重绘次数
|
||||
if (needsUpdate && typeof brushManager.updateBrush === "function") {
|
||||
brushManager.updateBrush();
|
||||
}
|
||||
}
|
||||
|
||||
// 点击外部时关闭笔刷面板
|
||||
function handleClickOutside(event) {
|
||||
if (
|
||||
showBrushPanel.value &&
|
||||
brushPanelRef.value &&
|
||||
!brushPanelRef.value.contains(event.target) &&
|
||||
!event.target.closest(".brush-selector")
|
||||
) {
|
||||
showBrushPanel.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 获取工具管理器和笔刷管理器
|
||||
const brushManager = toolManager?.brushManager;
|
||||
|
||||
// 设置初始的可用笔刷类型
|
||||
if (brushManager) {
|
||||
const availableBrushes = brushManager.getBrushTypes();
|
||||
BrushStore.setAvailableBrushes(availableBrushes);
|
||||
|
||||
// 初始化BrushStore与brushManager的数据同步
|
||||
BrushStore.setBrushSize(brushManager.brushSize?.value || 5);
|
||||
BrushStore.setBrushColor(brushManager.brushColor?.value || "#000000");
|
||||
BrushStore.setBrushType(brushManager.getCurrentBrushType() || "pencil");
|
||||
}
|
||||
|
||||
// 添加点击外部关闭面板的事件监听
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
|
||||
// 监听BrushStore的变化,同步到brushManager
|
||||
const unwatch = watch(
|
||||
() => [
|
||||
BrushStore.state.size,
|
||||
BrushStore.state.color,
|
||||
BrushStore.state.type,
|
||||
BrushStore.state.opacity,
|
||||
BrushStore.state.textureEnabled,
|
||||
BrushStore.state.texturePath,
|
||||
BrushStore.state.textureScale,
|
||||
],
|
||||
syncBrushStoreToManager,
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 组件卸载时移除事件监听
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
unwatch();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="canvas-header">
|
||||
<span class="canvas-title">Canvas</span>
|
||||
|
||||
<!-- 默认设置 -->
|
||||
<div
|
||||
v-if="
|
||||
!activeTool ||
|
||||
activeTool === OperationType.SELECT ||
|
||||
activeTool === OperationType.PAN
|
||||
"
|
||||
class="canvas-settings"
|
||||
>
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Width</span>
|
||||
<input
|
||||
type="text"
|
||||
:value="canvasWidth"
|
||||
class="setting-input"
|
||||
@input="$emit('update:canvasWidth', Number($event.target.value))"
|
||||
@change="updateCanvasSize"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Height</span>
|
||||
<input
|
||||
type="text"
|
||||
:value="canvasHeight"
|
||||
class="setting-input"
|
||||
@input="$emit('update:canvasHeight', Number($event.target.value))"
|
||||
@change="updateCanvasSize"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Color</span>
|
||||
<div class="color-picker-wrapper">
|
||||
<input
|
||||
type="color"
|
||||
:value="canvasColor"
|
||||
class="color-picker"
|
||||
@input="$emit('update:canvasColor', $event.target.value)"
|
||||
@change="updateCanvasColor"
|
||||
/>
|
||||
<span class="color-dropdown">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 绘图工具设置 -->
|
||||
<div v-if="shouldShowBrushSettings" class="canvas-settings">
|
||||
<!-- 简化的笔刷控制UI -->
|
||||
<!-- <div class="setting-group">
|
||||
<span class="setting-label">大小:</span>
|
||||
<input
|
||||
type="range"
|
||||
:value="BrushStore.state.size"
|
||||
min="0.5"
|
||||
max="100"
|
||||
step="0.5"
|
||||
class="size-slider"
|
||||
@input="handleBrushSizeChange"
|
||||
/>
|
||||
<span class="size-value">{{ BrushStore.state.size }}px</span>
|
||||
</div> -->
|
||||
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">笔刷:</span>
|
||||
<div class="brush-selector" @click="toggleBrushPanel">
|
||||
<div
|
||||
class="brush-preview"
|
||||
:style="{
|
||||
backgroundColor: BrushStore.state.color,
|
||||
height: BrushStore.state.type === 'marker' ? '4px' : '2px',
|
||||
opacity: BrushStore.state.opacity,
|
||||
}"
|
||||
></div>
|
||||
<span class="brush-dropdown">▼</span>
|
||||
</div>
|
||||
|
||||
<!-- 笔刷面板 -->
|
||||
<div
|
||||
v-if="showBrushPanel"
|
||||
class="brush-panel-container"
|
||||
ref="brushPanelRef"
|
||||
>
|
||||
<BrushPanel />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">颜色:</span>
|
||||
<div class="color-picker-wrapper">
|
||||
<input
|
||||
type="color"
|
||||
:value="BrushStore.state.color"
|
||||
class="color-picker"
|
||||
@input="BrushStore.setBrushColor($event.target.value)"
|
||||
/>
|
||||
<span class="color-dropdown">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文本工具设置 -->
|
||||
<div v-if="activeTool === OperationType.TEXT" class="canvas-settings">
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Font:</span>
|
||||
<select class="font-select">
|
||||
<option value="Arial">Arial</option>
|
||||
<option value="Times New Roman">Times New Roman</option>
|
||||
<option value="Courier New">Courier New</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Size:</span>
|
||||
<input
|
||||
type="number"
|
||||
class="setting-input"
|
||||
value="16"
|
||||
min="8"
|
||||
max="72"
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Color:</span>
|
||||
<div class="color-picker-wrapper">
|
||||
<input type="color" class="color-picker" value="#000000" />
|
||||
<span class="color-dropdown">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传工具设置 -->
|
||||
<div v-if="activeTool === OperationType.UPLOAD" class="canvas-settings">
|
||||
<div class="setting-group">
|
||||
<span class="setting-label">Upload Type:</span>
|
||||
<select class="setting-select">
|
||||
<option value="image">Image</option>
|
||||
<option value="vector">Vector Graphics</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导出设置 -->
|
||||
<div class="setting-group export-group">
|
||||
<span class="export-model-select">exportModel.select:</span>
|
||||
<span class="export-model-dropdown">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.canvas-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.canvas-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-right: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.canvas-title::before {
|
||||
content: "⟳";
|
||||
margin-right: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.canvas-settings {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.setting-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.setting-input {
|
||||
width: 60px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.setting-select {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.font-select {
|
||||
width: 150px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.color-picker-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: none;
|
||||
padding: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.color-dropdown {
|
||||
font-size: 10px;
|
||||
margin-left: 5px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.size-slider {
|
||||
width: 100px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.size-value {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
.brush-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
width: 80px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.brush-preview {
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background-color: #000;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.brush-dropdown,
|
||||
.export-model-dropdown {
|
||||
font-size: 10px;
|
||||
margin-left: 5px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.export-model-select {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.export-group {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* 笔刷面板 */
|
||||
.brush-panel-container {
|
||||
position: absolute;
|
||||
top: calc(100% + 5px);
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
width: 600px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,506 @@
|
||||
<script setup>
|
||||
import { ref, inject, onMounted } from "vue";
|
||||
import { Skeleton } from "ant-design-vue";
|
||||
|
||||
const loading = ref(true);
|
||||
const shortcuts = ref([]);
|
||||
const keyboardManager = inject("keyboardManager", null);
|
||||
const platform = ref({});
|
||||
|
||||
// 初始化键盘快捷键信息
|
||||
onMounted(() => {
|
||||
// 添加延迟以显示骨架屏效果
|
||||
setTimeout(() => {
|
||||
if (keyboardManager) {
|
||||
// 使用KeyboardManager的平台检测
|
||||
platform.value = {
|
||||
isMac: keyboardManager.platform === "mac",
|
||||
isIOS: keyboardManager.platform === "ios",
|
||||
isIPad:
|
||||
keyboardManager.platform === "ios" &&
|
||||
/iPad/.test(window.navigator.userAgent),
|
||||
isTouchDevice: keyboardManager.isTouchDevice,
|
||||
isWindows: keyboardManager.platform === "windows",
|
||||
isAndroid: keyboardManager.platform === "android",
|
||||
};
|
||||
|
||||
// 使用KeyboardManager的API获取所有快捷键
|
||||
const managerShortcuts = keyboardManager.getShortcuts();
|
||||
|
||||
// 转换为组件所需的格式
|
||||
shortcuts.value = convertShortcuts(managerShortcuts);
|
||||
} else {
|
||||
// 如果没有注入keyboardManager,使用默认检测和默认快捷键
|
||||
platform.value = detectPlatform();
|
||||
shortcuts.value = generateDefaultShortcuts();
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// 转换KeyboardManager返回的快捷键格式为组件需要的格式
|
||||
function convertShortcuts(managerShortcuts) {
|
||||
// 转换快捷键列表
|
||||
const result = [];
|
||||
|
||||
// 基本的Action到显示名称的映射
|
||||
const actionDisplayMap = {
|
||||
undo: "撤销",
|
||||
redo: "重做",
|
||||
delete: "删除选中元素",
|
||||
selectAll: "全选",
|
||||
copy: "复制",
|
||||
paste: "粘贴",
|
||||
cut: "剪切",
|
||||
save: "保存",
|
||||
selectTool: "选择工具",
|
||||
increaseBrushSize: "增加笔触大小",
|
||||
decreaseBrushSize: "减小笔触大小",
|
||||
toggleTempTool: "临时切换工具",
|
||||
newLayer: "新建图层",
|
||||
groupLayers: "组合图层",
|
||||
ungroupLayers: "取消组合",
|
||||
mergeLayers: "合并图层",
|
||||
};
|
||||
|
||||
// 工具ID到显示名称的映射
|
||||
const toolDisplayMap = {
|
||||
select: "选择模式",
|
||||
draw: "绘画模式",
|
||||
eraser: "橡皮擦模式",
|
||||
eyedropper: "吸色工具",
|
||||
pan: "移动画布",
|
||||
lasso: "套索工具",
|
||||
area_custom: "自由选区工具",
|
||||
wave: "波浪工具",
|
||||
liquify: "液化工具",
|
||||
};
|
||||
|
||||
// 处理每个快捷键
|
||||
for (const shortcut of managerShortcuts) {
|
||||
let actionDisplay = actionDisplayMap[shortcut.action] || shortcut.action;
|
||||
|
||||
// 特殊处理工具选择
|
||||
if (
|
||||
shortcut.action === "selectTool" &&
|
||||
shortcut.param &&
|
||||
toolDisplayMap[shortcut.param]
|
||||
) {
|
||||
actionDisplay = toolDisplayMap[shortcut.param];
|
||||
}
|
||||
|
||||
result.push({
|
||||
action: actionDisplay,
|
||||
windows: shortcut.key.replace(/cmdOrCtrl\+/g, "Ctrl+"),
|
||||
mac: shortcut.key.replace(/cmdOrCtrl\+/g, "⌘+"),
|
||||
touch: shortcut.touch || "触控界面点击对应工具",
|
||||
displayKey: shortcut.displayKey,
|
||||
});
|
||||
}
|
||||
|
||||
// 添加一些组件特定的快捷键
|
||||
result.push({
|
||||
action: "缩放画布",
|
||||
windows: "鼠标滚轮",
|
||||
mac: "鼠标滚轮 或 触控板缩放手势",
|
||||
touch: "双指捏合",
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 检测平台 - 作为备用
|
||||
function detectPlatform() {
|
||||
const userAgent = window.navigator.userAgent;
|
||||
return {
|
||||
isMac: /Mac/.test(userAgent),
|
||||
isIOS: /iPad|iPhone|iPod/.test(userAgent) && !window.MSStream,
|
||||
isIPad: /iPad/.test(userAgent),
|
||||
isTouchDevice: "ontouchstart" in window || navigator.maxTouchPoints > 0,
|
||||
isWindows: /Win/.test(userAgent),
|
||||
isAndroid: /Android/.test(userAgent),
|
||||
};
|
||||
}
|
||||
|
||||
// 生成默认快捷键描述
|
||||
function generateDefaultShortcuts() {
|
||||
return [
|
||||
{
|
||||
action: "撤销",
|
||||
windows: "Ctrl+Z",
|
||||
mac: "⌘+Z",
|
||||
touch: "双指向右轻扫",
|
||||
},
|
||||
{
|
||||
action: "重做",
|
||||
windows: "Ctrl+Y 或 Ctrl+Shift+Z",
|
||||
mac: "⌘+Shift+Z",
|
||||
touch: "双指向左轻扫",
|
||||
},
|
||||
{
|
||||
action: "删除选中元素",
|
||||
windows: "Delete 或 Backspace",
|
||||
mac: "Delete 或 ⌫",
|
||||
touch: "长按选中元素后点击删除",
|
||||
},
|
||||
{
|
||||
action: "全选",
|
||||
windows: "Ctrl+A",
|
||||
mac: "⌘+A",
|
||||
touch: "无",
|
||||
},
|
||||
{
|
||||
action: "复制",
|
||||
windows: "Ctrl+C",
|
||||
mac: "⌘+C",
|
||||
touch: "无",
|
||||
},
|
||||
{
|
||||
action: "粘贴",
|
||||
windows: "Ctrl+V",
|
||||
mac: "⌘+V",
|
||||
touch: "无",
|
||||
},
|
||||
{
|
||||
action: "剪切",
|
||||
windows: "Ctrl+X",
|
||||
mac: "⌘+X",
|
||||
touch: "无",
|
||||
},
|
||||
{
|
||||
action: "缩放画布",
|
||||
windows: "鼠标滚轮",
|
||||
mac: "鼠标滚轮 或 触控板缩放手势",
|
||||
touch: "双指捏合",
|
||||
},
|
||||
{
|
||||
action: "移动画布",
|
||||
windows: "Alt+拖动 或 鼠标中键拖动",
|
||||
mac: "Option+拖动 或 触控板双指拖动",
|
||||
touch: "双指拖动",
|
||||
},
|
||||
{
|
||||
action: "绘画模式",
|
||||
windows: "B",
|
||||
mac: "B",
|
||||
touch: "点击画笔工具",
|
||||
},
|
||||
{
|
||||
action: "选择模式",
|
||||
windows: "M",
|
||||
mac: "M",
|
||||
touch: "点击选择工具",
|
||||
},
|
||||
{
|
||||
action: "橡皮擦模式",
|
||||
windows: "E",
|
||||
mac: "E",
|
||||
touch: "点击橡皮擦工具",
|
||||
},
|
||||
{
|
||||
action: "吸色工具",
|
||||
windows: "I",
|
||||
mac: "I",
|
||||
touch: "点击吸色工具",
|
||||
},
|
||||
{
|
||||
action: "增加笔触大小",
|
||||
windows: "]",
|
||||
mac: "]",
|
||||
touch: "拖动笔刷大小滑块",
|
||||
},
|
||||
{
|
||||
action: "减小笔触大小",
|
||||
windows: "[",
|
||||
mac: "[",
|
||||
touch: "拖动笔刷大小滑块",
|
||||
},
|
||||
{
|
||||
action: "增加材质图片大小",
|
||||
windows: "Shift+]",
|
||||
mac: "⇧+]",
|
||||
touch: "拖动材质大小滑块",
|
||||
},
|
||||
{
|
||||
action: "减小材质图片大小",
|
||||
windows: "Shift+[",
|
||||
mac: "⇧+[",
|
||||
touch: "拖动材质大小滑块",
|
||||
},
|
||||
{
|
||||
action: "上传图片",
|
||||
windows: "Ctrl+O",
|
||||
mac: "⌘+O",
|
||||
touch: "点击上传按钮",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// 获取当前平台的快捷键文本
|
||||
function getShortcutForCurrentPlatform(shortcut) {
|
||||
if (platform.value.isTouchDevice) {
|
||||
return shortcut.touch;
|
||||
} else if (platform.value.isMac) {
|
||||
return shortcut.displayKey || shortcut.mac;
|
||||
} else {
|
||||
return shortcut.displayKey || shortcut.windows;
|
||||
}
|
||||
}
|
||||
|
||||
// 按分类获取快捷键
|
||||
function getShortcutsByCategory(category) {
|
||||
const categoryMap = {
|
||||
basic: [
|
||||
"撤销",
|
||||
"重做",
|
||||
"全选",
|
||||
"复制",
|
||||
"粘贴",
|
||||
"剪切",
|
||||
"删除选中元素",
|
||||
"上传图片",
|
||||
],
|
||||
view: ["缩放画布", "移动画布"],
|
||||
tools: [
|
||||
"绘画模式",
|
||||
"选择模式",
|
||||
"橡皮擦模式",
|
||||
"吸色工具",
|
||||
"套索工具",
|
||||
"自由选区工具",
|
||||
"波浪工具",
|
||||
"液化工具",
|
||||
],
|
||||
brush: [
|
||||
"增加笔触大小",
|
||||
"减小笔触大小",
|
||||
"增加材质图片大小",
|
||||
"减小材质图片大小",
|
||||
],
|
||||
layer: ["新建图层", "组合图层", "取消组合", "合并图层"],
|
||||
};
|
||||
|
||||
return shortcuts.value.filter((s) =>
|
||||
categoryMap[category]?.includes(s.action)
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="keyboard-shortcut-help">
|
||||
<h2>键盘快捷键 & 操作指南</h2>
|
||||
|
||||
<Skeleton active :loading="loading">
|
||||
<div class="platform-info">
|
||||
检测到的平台:
|
||||
<span v-if="platform.isMac">MacOS</span>
|
||||
<span v-else-if="platform.isWindows">Windows</span>
|
||||
<span v-else-if="platform.isIPad">iPad</span>
|
||||
<span v-else-if="platform.isIOS">iOS</span>
|
||||
<span v-else-if="platform.isAndroid">Android</span>
|
||||
<span v-else>其他</span>
|
||||
<span v-if="platform.isTouchDevice"> (触控设备)</span>
|
||||
</div>
|
||||
|
||||
<div class="shortcuts-category">
|
||||
<h3>基本操作</h3>
|
||||
<table class="shortcuts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>操作</th>
|
||||
<th>快捷键/手势</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in getShortcutsByCategory('basic')"
|
||||
:key="item.action"
|
||||
>
|
||||
<td>{{ item.action }}</td>
|
||||
<td>{{ getShortcutForCurrentPlatform(item) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="shortcuts-category">
|
||||
<h3>视图操作</h3>
|
||||
<table class="shortcuts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>操作</th>
|
||||
<th>快捷键/手势</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in getShortcutsByCategory('view')"
|
||||
:key="item.action"
|
||||
>
|
||||
<td>{{ item.action }}</td>
|
||||
<td>{{ getShortcutForCurrentPlatform(item) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="shortcuts-category">
|
||||
<h3>工具切换</h3>
|
||||
<table class="shortcuts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>操作</th>
|
||||
<th>快捷键/手势</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in getShortcutsByCategory('tools')"
|
||||
:key="item.action"
|
||||
>
|
||||
<td>{{ item.action }}</td>
|
||||
<td>{{ getShortcutForCurrentPlatform(item) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="shortcuts-category">
|
||||
<h3>笔刷调整</h3>
|
||||
<table class="shortcuts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>操作</th>
|
||||
<th>快捷键/手势</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in getShortcutsByCategory('brush')"
|
||||
:key="item.action"
|
||||
>
|
||||
<td>{{ item.action }}</td>
|
||||
<td>{{ getShortcutForCurrentPlatform(item) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="shortcuts-category">
|
||||
<h3>图层操作</h3>
|
||||
<table class="shortcuts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>操作</th>
|
||||
<th>快捷键/手势</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in getShortcutsByCategory('layer')"
|
||||
:key="item.action"
|
||||
>
|
||||
<td>{{ item.action }}</td>
|
||||
<td>{{ getShortcutForCurrentPlatform(item) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="touch-tips" v-if="platform.isTouchDevice">
|
||||
<h3>触控设备提示</h3>
|
||||
<ul>
|
||||
<li>长按图层面板可访问更多选项</li>
|
||||
<li>双击元素可快速进入编辑模式</li>
|
||||
<li>双指拖动可平移画布</li>
|
||||
<li>双指捏合可缩放画布</li>
|
||||
<li>双指连按可显示元素变换控制点</li>
|
||||
<li>三指左右滑动可进行撤销/重做操作</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Skeleton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.keyboard-shortcut-help {
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
.platform-info {
|
||||
margin-bottom: 16px;
|
||||
padding: 8px;
|
||||
background-color: #f0f9ff;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.shortcuts-category {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.shortcuts-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.shortcuts-table th,
|
||||
.shortcuts-table td {
|
||||
border: 1px solid #eaeaea;
|
||||
padding: 8px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.shortcuts-table th {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.touch-tips {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
background-color: #fffbeb;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #fbbf24;
|
||||
}
|
||||
|
||||
.touch-tips ul {
|
||||
margin: 10px 0 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.touch-tips li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
.keyboard-shortcut-help {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.shortcuts-table th,
|
||||
.shortcuts-table td {
|
||||
padding: 12px 8px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1062
src/component/Canvas/CanvasEditor/components/LayersPanel.vue
Normal file
1062
src/component/Canvas/CanvasEditor/components/LayersPanel.vue
Normal file
File diff suppressed because it is too large
Load Diff
1388
src/component/Canvas/CanvasEditor/components/LiquifyPanel.vue
Normal file
1388
src/component/Canvas/CanvasEditor/components/LiquifyPanel.vue
Normal file
File diff suppressed because it is too large
Load Diff
146
src/component/Canvas/CanvasEditor/components/MinimapPanel.vue
Normal file
146
src/component/Canvas/CanvasEditor/components/MinimapPanel.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, onBeforeUnmount } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
minimapManager: Object,
|
||||
});
|
||||
|
||||
const minimapContainerRef = ref(null);
|
||||
let refreshTimeout = null;
|
||||
|
||||
// 强制重绘小地图,添加防抖处理
|
||||
const forceRefresh = () => {
|
||||
if (refreshTimeout) {
|
||||
clearTimeout(refreshTimeout);
|
||||
}
|
||||
|
||||
refreshTimeout = setTimeout(() => {
|
||||
if (props.minimapManager) {
|
||||
props.minimapManager.refresh();
|
||||
}
|
||||
}, 50);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (props.minimapManager && minimapContainerRef.value) {
|
||||
// 使用新的mount方法挂载小地图
|
||||
props.minimapManager.mount(minimapContainerRef.value);
|
||||
// 初始加载后延迟刷新一次,确保内容正确加载
|
||||
setTimeout(forceRefresh, 200);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.minimapManager,
|
||||
(newVal) => {
|
||||
if (newVal && minimapContainerRef.value) {
|
||||
newVal.mount(minimapContainerRef.value);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 添加resize observer以适应容器大小变化
|
||||
let resizeObserver = null;
|
||||
onMounted(() => {
|
||||
if (window.ResizeObserver) {
|
||||
// 使用防抖处理resize事件,避免过于频繁的刷新
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
forceRefresh();
|
||||
});
|
||||
if (minimapContainerRef.value) {
|
||||
resizeObserver.observe(minimapContainerRef.value.parentElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (resizeObserver && minimapContainerRef.value) {
|
||||
resizeObserver.unobserve(minimapContainerRef.value.parentElement);
|
||||
resizeObserver.disconnect();
|
||||
}
|
||||
if (refreshTimeout) {
|
||||
clearTimeout(refreshTimeout);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="minimap-container">
|
||||
<div class="minimap-header">
|
||||
<span>画布小地图</span>
|
||||
<button class="minimap-refresh" @click="forceRefresh" title="刷新小地图">
|
||||
⟳
|
||||
</button>
|
||||
</div>
|
||||
<div class="minimap-content" ref="minimapContainerRef">
|
||||
<!-- 不再需要直接提供canvas引用,由MinimapManager内部创建 -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.minimap-container {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
width: 200px;
|
||||
height: 140px;
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
overflow: hidden;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.minimap-header {
|
||||
padding: 5px 8px;
|
||||
font-size: 12px;
|
||||
background-color: #f0f0f0;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.minimap-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.minimap-refresh {
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
padding: 0 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.minimap-refresh:hover {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* 触控设备优化 */
|
||||
@media (pointer: coarse) {
|
||||
.minimap-container {
|
||||
width: 220px;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.minimap-header {
|
||||
padding: 8px 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.minimap-refresh {
|
||||
font-size: 18px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
814
src/component/Canvas/CanvasEditor/components/SelectionPanel.vue
Normal file
814
src/component/Canvas/CanvasEditor/components/SelectionPanel.vue
Normal file
@@ -0,0 +1,814 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div class="selection-toolbar" v-if="visible">
|
||||
<!-- 顶部选区类型工具栏 -->
|
||||
<div class="toolbar-section">
|
||||
<div class="toolbar-header">
|
||||
<div class="header-title">选区工具</div>
|
||||
<!-- 移除关闭按钮,完全通过工具切换控制显示隐藏 -->
|
||||
</div>
|
||||
|
||||
<div class="tool-types">
|
||||
<div
|
||||
: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 },
|
||||
]"
|
||||
@click="setSelectionType(OperationType.LASSO_RECTANGLE)"
|
||||
>
|
||||
<svg-icon name="CRectangle" size="32" />
|
||||
<span>{{ $t("矩形") }}</span>
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'tool-btn',
|
||||
{ active: selectionType === OperationType.LASSO_ELLIPSE },
|
||||
]"
|
||||
@click="setSelectionType(OperationType.LASSO_ELLIPSE)"
|
||||
>
|
||||
<svg-icon name="CEllipse" size="30" />
|
||||
<span>{{ $t("椭圆") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<!-- 底部选区操作工具栏 -->
|
||||
<div class="tool-actions">
|
||||
<div class="action-btn" @click="copySelectionToNewLayer">
|
||||
<svg-icon name="CPaste" />
|
||||
<span class="btn-text">{{ $t("拷贝并粘贴") }}</span>
|
||||
</div>
|
||||
<!-- <button
|
||||
class="action-btn"
|
||||
@click="addSelection"
|
||||
:disabled="!hasSelection"
|
||||
title="添加"
|
||||
>
|
||||
<svg-icon name="plus" />
|
||||
<span class="btn-text">{{ $t("添加") }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="action-btn"
|
||||
@click="removeSelection"
|
||||
:disabled="!hasSelection"
|
||||
title="移除"
|
||||
>
|
||||
<svg-icon name="minus" />
|
||||
<span class="btn-text">{{ $t("移除") }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="action-btn"
|
||||
@click="invertSelection"
|
||||
:disabled="!hasSelection"
|
||||
title="反转"
|
||||
>
|
||||
<svg-icon name="flip-horizontal" />
|
||||
<span class="btn-text">{{ $t("反转") }}</span>
|
||||
</button> -->
|
||||
<!-- <button
|
||||
class="action-btn"
|
||||
@click="copySelectionToNewLayer"
|
||||
:disabled="!hasSelection"
|
||||
title="拷贝并粘贴"
|
||||
>
|
||||
<svg-icon name="copy" />
|
||||
<span class="btn-text">{{ $t("拷贝并粘贴") }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="action-btn"
|
||||
@click="openFeatherDialog"
|
||||
:disabled="!hasSelection"
|
||||
title="羽化"
|
||||
>
|
||||
<svg-icon name="feather" />
|
||||
<span class="btn-text">{{ $t("羽化") }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="action-btn"
|
||||
@click="fillSelection"
|
||||
:disabled="!hasSelection"
|
||||
title="颜色填充"
|
||||
>
|
||||
<svg-icon name="fill-color" />
|
||||
<span class="btn-text">{{ $t("颜色填充") }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="action-btn"
|
||||
@click="clearSelection"
|
||||
:disabled="!hasSelection"
|
||||
title="清除"
|
||||
>
|
||||
<svg-icon name="trash" />
|
||||
<span class="btn-text">{{ $t("清除") }}</span>
|
||||
</button> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 羽化设置弹窗 -->
|
||||
<div v-if="showFeatherDialog" class="dialog-overlay">
|
||||
<div class="dialog-container">
|
||||
<div class="dialog-header">
|
||||
<h3>{{ $t("羽化") }}</h3>
|
||||
<button class="close-dialog-btn" @click="cancelFeather">×</button>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<div class="feather-control">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="50"
|
||||
v-model.number="featherAmount"
|
||||
class="slider-control"
|
||||
/>
|
||||
<div class="feather-value">{{ featherAmount }}px</div>
|
||||
</div>
|
||||
<div class="dialog-buttons">
|
||||
<button class="cancel-btn" @click="cancelFeather">
|
||||
{{ $t("取消") }}
|
||||
</button>
|
||||
<button class="confirm-btn" @click="applyFeather">
|
||||
{{ $t("确认") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 颜色选择器 -->
|
||||
<div v-if="showColorPicker" class="dialog-overlay">
|
||||
<div class="dialog-container">
|
||||
<div class="dialog-header">
|
||||
<h3>{{ $t("选择填充颜色") }}</h3>
|
||||
<button class="close-dialog-btn" @click="cancelColorPicker">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<input type="color" v-model="fillColor" class="color-picker" />
|
||||
<div class="dialog-buttons">
|
||||
<button class="cancel-btn" @click="cancelColorPicker">
|
||||
{{ $t("取消") }}
|
||||
</button>
|
||||
<button class="confirm-btn" @click="confirmColorPicker">
|
||||
{{ $t("确认") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import {
|
||||
CreateSelectionCommand,
|
||||
InvertSelectionCommand,
|
||||
ClearSelectionCommand,
|
||||
FeatherSelectionCommand,
|
||||
FillSelectionCommand,
|
||||
CopySelectionToNewLayerCommand,
|
||||
ClearSelectionContentCommand,
|
||||
LassoCutoutCommand,
|
||||
} from "../commands/SelectionCommands";
|
||||
import { ToolCommand } from "../commands/ToolCommands";
|
||||
import { OperationType } from "../utils/layerHelper";
|
||||
|
||||
const props = defineProps({
|
||||
canvas: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
commandManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
selectionManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
layerManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
toolManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
activeTool: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
// 响应式数据
|
||||
const visible = ref(false);
|
||||
const selectionType = ref("rectangle");
|
||||
const featherAmount = ref(0);
|
||||
const fillColor = ref("#000000");
|
||||
const hasSelection = ref(false);
|
||||
const showFeatherDialog = ref(false);
|
||||
const showColorPicker = ref(false);
|
||||
|
||||
// 国际化函数 (简单实现,可根据需要替换为实际的国际化方案)
|
||||
const $t = (key) => key;
|
||||
|
||||
onMounted(() => {
|
||||
// 为选区管理器添加监听,以便在选区变化时更新状态
|
||||
if (props.selectionManager) {
|
||||
// 在选区管理器中添加选区变化的监听
|
||||
checkSelectionStatus();
|
||||
|
||||
// 设置选区状态变化的回调
|
||||
props.selectionManager.onSelectionChanged = () => {
|
||||
checkSelectionStatus();
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 activeTool 变化
|
||||
watch(
|
||||
() => props.activeTool,
|
||||
(newTool) => {
|
||||
// 当工具为LASSO或AREA类型时显示选区面板
|
||||
const selectionTools = [
|
||||
OperationType.LASSO,
|
||||
OperationType.LASSO_RECTANGLE,
|
||||
OperationType.LASSO_ELLIPSE,
|
||||
];
|
||||
|
||||
if (selectionTools.includes(newTool)) {
|
||||
show();
|
||||
// 根据工具类型设置选区类型
|
||||
selectionType.value = newTool;
|
||||
|
||||
// 更新选区管理器的选区类型
|
||||
if (props.selectionManager) {
|
||||
props.selectionManager.setSelectionType(selectionType.value);
|
||||
props.selectionManager.setupSelectionEvents();
|
||||
}
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 显示面板
|
||||
*/
|
||||
function show() {
|
||||
visible.value = true;
|
||||
checkSelectionStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭面板
|
||||
*/
|
||||
function close() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置选区类型
|
||||
*/
|
||||
function setSelectionType(type) {
|
||||
selectionType.value = type;
|
||||
|
||||
// 通过 ToolManager 切换工具,这会自动通知 SelectionManager
|
||||
if (props.toolManager) {
|
||||
props.toolManager.setToolWithCommand(type);
|
||||
}
|
||||
|
||||
// 备用方案:如果没有 toolManager,直接更新 selectionManager
|
||||
else if (props.selectionManager) {
|
||||
props.selectionManager.setSelectionType(type);
|
||||
props.selectionManager.setupSelectionEvents();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查选区状态
|
||||
*/
|
||||
function checkSelectionStatus() {
|
||||
hasSelection.value =
|
||||
props.selectionManager &&
|
||||
props.selectionManager.getSelectionObject() !== null;
|
||||
|
||||
// 同步羽化值
|
||||
if (hasSelection.value) {
|
||||
featherAmount.value = props.selectionManager.getFeatherAmount();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加选区
|
||||
*/
|
||||
function addSelection() {
|
||||
// TODO: 实现添加选区功能
|
||||
console.log("添加选区功能尚未实现");
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除选区
|
||||
*/
|
||||
function removeSelection() {
|
||||
// TODO: 实现移除选区功能
|
||||
console.log("移除选区功能尚未实现");
|
||||
}
|
||||
|
||||
/**
|
||||
* 反转选区
|
||||
*/
|
||||
function invertSelection() {
|
||||
if (!hasSelection.value) return;
|
||||
|
||||
props.commandManager.execute(
|
||||
new InvertSelectionCommand({
|
||||
canvas: props.canvas,
|
||||
selectionManager: props.selectionManager,
|
||||
})
|
||||
);
|
||||
checkSelectionStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除选区
|
||||
*/
|
||||
function clearSelection() {
|
||||
if (!hasSelection.value) return;
|
||||
|
||||
props.commandManager.execute(
|
||||
new ClearSelectionCommand({
|
||||
selectionManager: props.selectionManager,
|
||||
})
|
||||
);
|
||||
checkSelectionStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用羽化效果
|
||||
*/
|
||||
function applyFeather() {
|
||||
if (!hasSelection.value) return;
|
||||
|
||||
props.commandManager.execute(
|
||||
new FeatherSelectionCommand({
|
||||
selectionManager: props.selectionManager,
|
||||
featherAmount: featherAmount.value,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充选区
|
||||
*/
|
||||
function fillSelection() {
|
||||
if (!hasSelection.value) return;
|
||||
showColorPicker.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 套索抠图到新图层
|
||||
*/
|
||||
function copySelectionToNewLayer() {
|
||||
if (!hasSelection.value) return;
|
||||
|
||||
props.commandManager.execute(
|
||||
new LassoCutoutCommand({
|
||||
canvas: props.canvas,
|
||||
layerManager: props.layerManager,
|
||||
selectionManager: props.selectionManager,
|
||||
toolManager: props.toolManager,
|
||||
})
|
||||
);
|
||||
checkSelectionStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除选区内容
|
||||
*/
|
||||
function clearSelectionContent() {
|
||||
if (!hasSelection.value) return;
|
||||
|
||||
props.commandManager.execute(
|
||||
new ClearSelectionContentCommand({
|
||||
canvas: props.canvas,
|
||||
layerManager: props.layerManager,
|
||||
selectionManager: props.selectionManager,
|
||||
})
|
||||
);
|
||||
checkSelectionStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开羽化设置弹窗
|
||||
*/
|
||||
function openFeatherDialog() {
|
||||
showFeatherDialog.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消羽化设置
|
||||
*/
|
||||
function cancelFeather() {
|
||||
showFeatherDialog.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认羽化设置
|
||||
*/
|
||||
function confirmFeather() {
|
||||
applyFeather();
|
||||
showFeatherDialog.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消颜色选择
|
||||
*/
|
||||
function cancelColorPicker() {
|
||||
showColorPicker.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认颜色选择
|
||||
*/
|
||||
function confirmColorPicker() {
|
||||
if (!hasSelection.value) return;
|
||||
|
||||
props.commandManager.execute(
|
||||
new FillSelectionCommand({
|
||||
canvas: props.canvas,
|
||||
layerManager: props.layerManager,
|
||||
selectionManager: props.selectionManager,
|
||||
color: fillColor.value,
|
||||
})
|
||||
);
|
||||
checkSelectionStatus();
|
||||
showColorPicker.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.selection-toolbar {
|
||||
position: absolute;
|
||||
bottom: 22px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
max-width: min(90vw, 640px);
|
||||
margin: 0 auto;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
color: #333;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 平板和手机适配 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.selection-toolbar {
|
||||
bottom: 15px;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
max-width: calc(100vw - 30px);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.selection-toolbar {
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
max-width: calc(100vw - 20px);
|
||||
}
|
||||
}
|
||||
|
||||
.selection-toolbar.is-active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.toolbar-header {
|
||||
// display: flex;
|
||||
// justify-content: center;
|
||||
// align-items: center;
|
||||
padding: 8px 15px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
text-align: left;
|
||||
}
|
||||
.header-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
padding: 3px 6px;
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.2s ease;
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.header-btn:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.toolbar-section {
|
||||
padding: 0 0 10px;
|
||||
}
|
||||
|
||||
.tool-types {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* 平板适配 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.tool-types {
|
||||
gap: 6px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.toolbar-header {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机适配 */
|
||||
@media screen and (max-width: 480px) {
|
||||
.tool-types {
|
||||
gap: 4px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.toolbar-header {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 10px 5px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tool-btn span {
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.tool-btn svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.tool-btn:hover {
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.tool-btn.active {
|
||||
background-color: #007aff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toolbar-divider {
|
||||
height: 1px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
margin: 0 10px 10px;
|
||||
}
|
||||
|
||||
.tool-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
gap: 5px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
/* 平板适配 - 每行4个按钮 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.tool-actions {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px 6px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机适配 - 每行3个按钮 */
|
||||
@media screen and (max-width: 480px) {
|
||||
.tool-actions {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 6px 4px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.header-btn {
|
||||
font-size: 11px;
|
||||
padding: 2px 4px;
|
||||
min-width: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
padding: 8px 2px;
|
||||
}
|
||||
|
||||
.action-btn svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 对话框样式 */
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.dialog-container {
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
width: 280px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dialog-header h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.close-dialog-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #666;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.feather-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.slider-control {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.slider-control::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #007aff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.feather-value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dialog-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.cancel-btn,
|
||||
.confirm-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background-color: #007aff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
</style>
|
||||
1104
src/component/Canvas/CanvasEditor/components/TextEditorPanel.vue
Normal file
1104
src/component/Canvas/CanvasEditor/components/TextEditorPanel.vue
Normal file
File diff suppressed because it is too large
Load Diff
411
src/component/Canvas/CanvasEditor/components/ToolsSidebar.vue
Normal file
411
src/component/Canvas/CanvasEditor/components/ToolsSidebar.vue
Normal file
@@ -0,0 +1,411 @@
|
||||
<script setup>
|
||||
import { ref, inject, computed, onMounted, onUnmounted } from "vue";
|
||||
import { OperationType } from "../utils/layerHelper";
|
||||
import SvgIcon from "@/component/Canvas/SvgIcon/index.vue";
|
||||
|
||||
const props = defineProps({
|
||||
activeTool: String,
|
||||
minimapEnabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isRedGreenMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const commandManager = inject("commandManager");
|
||||
|
||||
// 撤销/重做按钮状态
|
||||
const canUndo = ref(false);
|
||||
const canRedo = ref(false);
|
||||
|
||||
// 监听命令管理器状态变化
|
||||
commandManager.setChangeCallback((info) => {
|
||||
canUndo.value = info.canUndo;
|
||||
canRedo.value = info.canRedo;
|
||||
});
|
||||
|
||||
// 撤销/重做操作
|
||||
const undoFun = () => commandManager.undo();
|
||||
const redoFun = () => commandManager.redo();
|
||||
|
||||
const emit = defineEmits([
|
||||
"tool-selected",
|
||||
"trigger-image-upload",
|
||||
"add-text",
|
||||
"undo",
|
||||
"redo",
|
||||
"toggle-minimap",
|
||||
"zoom-in",
|
||||
"zoom-out",
|
||||
"toggle-red-green-mode",
|
||||
]);
|
||||
|
||||
// 普通模式工具列表
|
||||
const normalToolsList = ref([
|
||||
{
|
||||
id: "undo",
|
||||
title: "Undo",
|
||||
action: undo,
|
||||
icon: { name: "CUndo", size: "20" },
|
||||
class: "undo-btn",
|
||||
},
|
||||
{
|
||||
id: "redo",
|
||||
title: "Redo",
|
||||
action: redo,
|
||||
icon: { name: "CRedo", size: "20" },
|
||||
class: "redo-btn",
|
||||
},
|
||||
{
|
||||
id: OperationType.DRAW,
|
||||
title: "Drawing",
|
||||
action: () => selectTool(OperationType.DRAW),
|
||||
icon: { name: "CBrush", size: "24" },
|
||||
class: "draw-btn",
|
||||
},
|
||||
{
|
||||
id: OperationType.ERASER,
|
||||
title: "Eraser",
|
||||
action: () => selectTool(OperationType.ERASER),
|
||||
icon: { name: "CEraser", size: "22" },
|
||||
class: "eraser-btn",
|
||||
},
|
||||
{
|
||||
id: OperationType.PAN,
|
||||
title: "Pan",
|
||||
action: () => selectTool(OperationType.PAN),
|
||||
icon: { name: "CHand", size: "28" },
|
||||
class: "hand-btn",
|
||||
},
|
||||
{
|
||||
id: OperationType.SELECT,
|
||||
title: "Select",
|
||||
action: () => selectTool(OperationType.SELECT),
|
||||
icon: { name: "CSelect", size: "28" },
|
||||
class: "select-btn",
|
||||
},
|
||||
{
|
||||
id: OperationType.LIQUIFY,
|
||||
title: "Liquefying",
|
||||
action: () => selectTool(OperationType.LIQUIFY),
|
||||
icon: { name: "CLiquefying", size: "32" },
|
||||
class: "liquify-btn",
|
||||
},
|
||||
{
|
||||
id: OperationType.LASSO,
|
||||
title: "Lasso",
|
||||
action: () => selectTool(OperationType.LASSO),
|
||||
icon: { name: "CLasso", size: "28" },
|
||||
class: "lasso-btn",
|
||||
activeList: [
|
||||
OperationType.LASSO,
|
||||
OperationType.LASSO_RECTANGLE,
|
||||
OperationType.AREA_CUSTOM,
|
||||
OperationType.AREA_RECTANGLE,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "zoomIn",
|
||||
title: "Zoom In",
|
||||
action: zoomIn,
|
||||
icon: { name: "CZoomIn", size: "30" },
|
||||
class: "zoom-in-btn",
|
||||
},
|
||||
{
|
||||
id: "zoomOut",
|
||||
title: "Zoom Out",
|
||||
action: zoomOut,
|
||||
icon: { name: "CZoomOut", size: "26" },
|
||||
class: "zoom-out-btn",
|
||||
},
|
||||
{
|
||||
id: "upload",
|
||||
title: "Upload Image",
|
||||
action: triggerImageUpload,
|
||||
icon: { name: "CUpload", size: "26" },
|
||||
class: "upload-btn",
|
||||
},
|
||||
{
|
||||
id: "addText",
|
||||
title: "Add Text",
|
||||
action: () => addText(),
|
||||
icon: { name: "CFont", size: "20" },
|
||||
class: "text-btn",
|
||||
},
|
||||
]);
|
||||
|
||||
// 红绿图模式工具列表
|
||||
const redGreenToolsList = ref([
|
||||
{
|
||||
id: "undo",
|
||||
title: "Undo",
|
||||
action: undo,
|
||||
icon: { name: "CUndo", size: "20" },
|
||||
class: "undo-btn",
|
||||
},
|
||||
{
|
||||
id: "redo",
|
||||
title: "Redo",
|
||||
action: redo,
|
||||
icon: { name: "CRedo", size: "20" },
|
||||
class: "redo-btn",
|
||||
},
|
||||
{
|
||||
id: OperationType.RED_BRUSH,
|
||||
title: "Red Brush (R)",
|
||||
action: () => selectTool(OperationType.RED_BRUSH),
|
||||
icon: { name: "CBrush", size: "24" },
|
||||
class: "red-brush-btn",
|
||||
style: { color: "#FF0000" },
|
||||
},
|
||||
{
|
||||
id: OperationType.GREEN_BRUSH,
|
||||
title: "Green Brush (G)",
|
||||
action: () => selectTool(OperationType.GREEN_BRUSH),
|
||||
icon: { name: "CBrush", size: "24" },
|
||||
class: "green-brush-btn",
|
||||
style: { color: "#00AA00" },
|
||||
},
|
||||
{
|
||||
id: OperationType.ERASER,
|
||||
title: "Eraser (E)",
|
||||
action: () => selectTool(OperationType.ERASER),
|
||||
icon: { name: "CEraser", size: "22" },
|
||||
class: "eraser-btn",
|
||||
},
|
||||
{
|
||||
id: "zoomIn",
|
||||
title: "Zoom In",
|
||||
action: zoomIn,
|
||||
icon: { name: "CZoomIn", size: "30" },
|
||||
class: "zoom-in-btn",
|
||||
},
|
||||
{
|
||||
id: "zoomOut",
|
||||
title: "Zoom Out",
|
||||
action: zoomOut,
|
||||
icon: { name: "CZoomOut", size: "26" },
|
||||
class: "zoom-out-btn",
|
||||
},
|
||||
]);
|
||||
|
||||
// 根据模式选择工具列表
|
||||
const toolsList = computed(() => {
|
||||
return props.isRedGreenMode ? redGreenToolsList.value : normalToolsList.value;
|
||||
});
|
||||
|
||||
function selectTool(tool) {
|
||||
emit("tool-selected", tool);
|
||||
}
|
||||
|
||||
function triggerImageUpload() {
|
||||
emit("trigger-image-upload");
|
||||
}
|
||||
|
||||
function addText() {
|
||||
emit("add-text");
|
||||
}
|
||||
|
||||
function undo() {
|
||||
if (!canUndo.value) return;
|
||||
undoFun();
|
||||
emit("undo", {
|
||||
canUndo: canUndo.value,
|
||||
canRedo: canRedo.value,
|
||||
commandManager,
|
||||
});
|
||||
}
|
||||
|
||||
function redo() {
|
||||
if (!canRedo.value) return;
|
||||
emit("redo", {
|
||||
canUndo: canUndo.value,
|
||||
canRedo: canRedo.value,
|
||||
commandManager,
|
||||
});
|
||||
redoFun();
|
||||
}
|
||||
|
||||
function toggleMinimap() {
|
||||
emit("toggle-minimap", !props.minimapEnabled);
|
||||
}
|
||||
|
||||
function zoomIn() {
|
||||
emit("zoom-in");
|
||||
}
|
||||
|
||||
function zoomOut() {
|
||||
emit("zoom-out");
|
||||
}
|
||||
|
||||
function toggleRedGreenMode() {
|
||||
emit("toggle-red-green-mode");
|
||||
}
|
||||
|
||||
// 键盘快捷键处理
|
||||
function handleKeyDown(event) {
|
||||
// 在红绿图模式下处理特定快捷键
|
||||
if (props.isRedGreenMode) {
|
||||
const key = event.key.toUpperCase();
|
||||
|
||||
// 当处于输入状态时不触发快捷键
|
||||
if (
|
||||
event.target.tagName === "INPUT" ||
|
||||
event.target.tagName === "TEXTAREA"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case "R":
|
||||
selectTool(OperationType.RED_BRUSH);
|
||||
event.preventDefault();
|
||||
break;
|
||||
case "G":
|
||||
selectTool(OperationType.GREEN_BRUSH);
|
||||
event.preventDefault();
|
||||
break;
|
||||
case "E":
|
||||
selectTool(OperationType.ERASER);
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 添加键盘事件监听
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 移除键盘事件监听
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tools-sidebar">
|
||||
<div
|
||||
v-for="tool in toolsList"
|
||||
:key="tool.id"
|
||||
:class="[
|
||||
'tool-btn',
|
||||
tool.class,
|
||||
{
|
||||
active:
|
||||
tool.id === activeTool ||
|
||||
tool.id === activeTool.toLowerCase() ||
|
||||
tool?.activeList?.includes(activeTool),
|
||||
disabled:
|
||||
(tool.id === 'undo' && !canUndo) ||
|
||||
(tool.id === 'redo' && !canRedo),
|
||||
},
|
||||
]"
|
||||
:style="tool.style"
|
||||
@click="tool.action"
|
||||
>
|
||||
<SvgIcon :name="tool.icon.name" :size="tool.icon.size"></SvgIcon>
|
||||
<div class="tool-tooltip">{{ tool.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tools-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 15px 10px;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
background-color: #ffffff;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tool-btn:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.tool-btn:hover .tool-tooltip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tool-btn.active {
|
||||
background-color: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.tool-btn.disabled {
|
||||
cursor: not-allowed;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.tool-tooltip {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
margin-left: 8px;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tool-tooltip:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 100%;
|
||||
margin-top: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: transparent rgba(0, 0, 0, 0.7) transparent transparent;
|
||||
}
|
||||
|
||||
.red-green-mode {
|
||||
background-color: #fff4f4;
|
||||
}
|
||||
|
||||
.mode-indicator {
|
||||
margin-bottom: 10px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: #ffcccc;
|
||||
color: #a33;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mode-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mode-hint {
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
688
src/component/Canvas/CanvasEditor/components/VerticalSlider.vue
Normal file
688
src/component/Canvas/CanvasEditor/components/VerticalSlider.vue
Normal file
@@ -0,0 +1,688 @@
|
||||
<template>
|
||||
<div class="vertical-slider-container" :class="customClass">
|
||||
<div
|
||||
class="slider-track"
|
||||
ref="sliderTrack"
|
||||
@mousedown="startSliding"
|
||||
@touchstart.prevent="startTouchSliding"
|
||||
@click="handleClick"
|
||||
>
|
||||
<div
|
||||
class="slider-fill"
|
||||
:style="{ height: `${displayPercentage}%` }"
|
||||
></div>
|
||||
<div
|
||||
class="slider-thumb"
|
||||
:style="{
|
||||
bottom: `${displayPercentage}%`,
|
||||
transform: `translateX(0) translateY(8px) scale(${thumbScale})`,
|
||||
}"
|
||||
></div>
|
||||
<div
|
||||
v-for="(preset, index) in presets"
|
||||
:key="`preset-${index}`"
|
||||
class="slider-notch"
|
||||
:class="{ active: isActivePreset(preset) }"
|
||||
:style="{ bottom: `${calculatePresetPosition(preset)}%` }"
|
||||
@click.stop="setValue(preset)"
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-for="(preset, index) in memorizedValues"
|
||||
:key="`preset-${index}`"
|
||||
class="slider-notch"
|
||||
:class="{ active: isActivePreset(preset) }"
|
||||
:style="{ bottom: `${calculatePresetPosition(preset)}%` }"
|
||||
@click.stop="setValue(preset)"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- 提示框 -->
|
||||
<transition name="fade">
|
||||
<div class="slider-tooltip" v-if="tooltipVisible" ref="tooltip">
|
||||
<slot name="tooltip-content" :value="modelValue"></slot>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
presets: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
memorizedValues: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
snapThreshold: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
// 用于判断当前值与预设值是否匹配的阈值
|
||||
activeThreshold: {
|
||||
type: Number,
|
||||
default: 0.05,
|
||||
},
|
||||
// 是否将预设值位置按百分比计算
|
||||
isPercentage: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
// 添加步进属性,控制数值的步长
|
||||
step: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
// 增加外部控制tooltip显示的属性
|
||||
showTooltip: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 是否启用点击外部关闭tooltip
|
||||
closeOnOutsideClick: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
"update:modelValue",
|
||||
"slide-start",
|
||||
"slide-end",
|
||||
"click",
|
||||
"update:showTooltip", // 新增emit事件用于双向绑定tooltip状态
|
||||
]);
|
||||
|
||||
const sliderTrack = ref(null);
|
||||
const tooltip = ref(null);
|
||||
const isSliding = ref(false);
|
||||
const internalShowTooltip = ref(false);
|
||||
const slideMoved = ref(false); // 用于跟踪是否发生了滑动移动
|
||||
const hideTooltipTimer = ref(null); // 用于存储隐藏tooltip的计时器
|
||||
|
||||
// 在script setup顶部添加新的ref
|
||||
const isSnapping = ref(false);
|
||||
const snapLockValue = ref(null);
|
||||
const snapLockTimeout = ref(null);
|
||||
const thumbScale = ref(1); // 新增:用于控制滑块的缩放效果
|
||||
|
||||
// 定义一个统一的延迟时间常量
|
||||
const TOOLTIP_HIDE_DELAY = 1500; // 1.5秒,可根据需要调整
|
||||
|
||||
// 开始新的隐藏tooltip计时器
|
||||
function startHideTooltipTimer() {
|
||||
// 先清除已有的计时器
|
||||
clearHideTooltipTimer();
|
||||
|
||||
// 创建新计时器
|
||||
hideTooltipTimer.value = setTimeout(() => {
|
||||
// 只有在用户不在滑动时才隐藏tooltip
|
||||
if (!isSliding.value) {
|
||||
updateTooltipVisibility(false);
|
||||
}
|
||||
hideTooltipTimer.value = null;
|
||||
}, TOOLTIP_HIDE_DELAY);
|
||||
}
|
||||
|
||||
// 清除隐藏tooltip的计时器
|
||||
function clearHideTooltipTimer() {
|
||||
if (hideTooltipTimer.value) {
|
||||
clearTimeout(hideTooltipTimer.value);
|
||||
hideTooltipTimer.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算tooltip的可见性,优先使用props中的showTooltip,否则使用内部状态
|
||||
const tooltipVisible = computed(() => {
|
||||
return props.showTooltip !== undefined
|
||||
? props.showTooltip
|
||||
: internalShowTooltip.value;
|
||||
});
|
||||
|
||||
// 更新tooltip状态的方法
|
||||
function updateTooltipVisibility(visible) {
|
||||
if (props.showTooltip !== undefined) {
|
||||
// 如果父组件提供了showTooltip属性,通过emit更新
|
||||
emit("update:showTooltip", visible);
|
||||
} else {
|
||||
// 否则更新内部状态
|
||||
internalShowTooltip.value = visible;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算显示百分比
|
||||
const displayPercentage = computed(() => {
|
||||
const range = props.max - props.min;
|
||||
return ((props.modelValue - props.min) / range) * 100;
|
||||
});
|
||||
|
||||
// 判断是否是活动预设值
|
||||
function isActivePreset(preset) {
|
||||
return Math.abs(props.modelValue - preset) < props.activeThreshold;
|
||||
}
|
||||
|
||||
// 计算预设值的位置
|
||||
function calculatePresetPosition(preset) {
|
||||
if (props.isPercentage) {
|
||||
return preset * 100;
|
||||
} else {
|
||||
const range = props.max - props.min;
|
||||
return ((preset - props.min) / range) * 100;
|
||||
}
|
||||
}
|
||||
|
||||
// 将值舍入到最接近的步进值
|
||||
function roundToStep(value) {
|
||||
if (!props.step || props.step <= 0) return value;
|
||||
|
||||
const numSteps = Math.round((value - props.min) / props.step);
|
||||
return props.min + numSteps * props.step;
|
||||
}
|
||||
|
||||
// 鼠标滑动处理
|
||||
function startSliding(event) {
|
||||
isSliding.value = true;
|
||||
slideMoved.value = false; // 重置滑动状态
|
||||
updateTooltipVisibility(true);
|
||||
clearHideTooltipTimer(); // 清除任何现有的隐藏计时器
|
||||
updateValueFromMousePosition(event);
|
||||
|
||||
emit("slide-start");
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
window.addEventListener("mouseup", stopSliding);
|
||||
}
|
||||
|
||||
function handleMouseMove(event) {
|
||||
if (isSliding.value) {
|
||||
slideMoved.value = true; // 标记已经发生了滑动
|
||||
updateValueFromMousePosition(event);
|
||||
}
|
||||
}
|
||||
|
||||
function updateValueFromMousePosition(event) {
|
||||
const rect = sliderTrack.value.getBoundingClientRect();
|
||||
const trackHeight = rect.height;
|
||||
const posY = event.clientY - rect.top;
|
||||
|
||||
// 计算相对位置并转换为min-max范围的值
|
||||
let newValue = props.max - (posY / trackHeight) * (props.max - props.min);
|
||||
|
||||
// 边界检查
|
||||
newValue = Math.max(props.min, Math.min(props.max, newValue));
|
||||
|
||||
// 应用步进
|
||||
if (props.step > 0) {
|
||||
newValue = roundToStep(newValue);
|
||||
}
|
||||
|
||||
// 如果当前处于吸附锁定状态,并且移动不超过锁定阈值,则保持当前值不变
|
||||
if (isSnapping.value && snapLockValue.value !== null) {
|
||||
// 增加吸附力度:扩大保持吸附的阈值范围
|
||||
if (
|
||||
Math.abs(newValue - snapLockValue.value) <
|
||||
(props.snapThreshold / trackHeight) * (props.max - props.min) * 1.2
|
||||
) {
|
||||
newValue = snapLockValue.value;
|
||||
} else {
|
||||
// 移动距离超过阈值,解除吸附锁定
|
||||
clearSnapLock();
|
||||
}
|
||||
} else {
|
||||
// 检查是否可以进入吸附状态
|
||||
const snapPercentage =
|
||||
(props.snapThreshold / trackHeight) * (props.max - props.min);
|
||||
|
||||
// 检查是否接近预设值,增加吸附判定范围
|
||||
for (const preset of props.presets) {
|
||||
if (Math.abs(newValue - preset) < snapPercentage * 0.4) {
|
||||
// 增加判定范围
|
||||
// 进入吸附状态
|
||||
setSnapLock(preset);
|
||||
newValue = preset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否接近记忆值
|
||||
if (!isSnapping.value) {
|
||||
for (const memValue of props.memorizedValues) {
|
||||
if (Math.abs(newValue - memValue) < snapPercentage * 0.4) {
|
||||
// 增加判定范围
|
||||
// 进入吸附状态
|
||||
setSnapLock(memValue);
|
||||
newValue = memValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setValue(newValue);
|
||||
}
|
||||
|
||||
// 设置吸附锁定
|
||||
function setSnapLock(value) {
|
||||
if (!isSnapping.value) {
|
||||
isSnapping.value = true;
|
||||
snapLockValue.value = value;
|
||||
|
||||
// 添加视觉反馈:放大滑块
|
||||
thumbScale.value = 1.3;
|
||||
|
||||
// 触感反馈:在支持的设备上触发振动
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate(15); // 短暂振动15ms
|
||||
}
|
||||
|
||||
// 延长锁定时间,增强吸附感
|
||||
clearTimeout(snapLockTimeout.value);
|
||||
snapLockTimeout.value = setTimeout(() => {
|
||||
// 恢复滑块大小
|
||||
thumbScale.value = 1;
|
||||
clearSnapLock();
|
||||
}, 500); // 500ms的吸附感觉,比原来的300ms更强
|
||||
}
|
||||
}
|
||||
|
||||
// 清除吸附锁定
|
||||
function clearSnapLock() {
|
||||
isSnapping.value = false;
|
||||
snapLockValue.value = null;
|
||||
clearTimeout(snapLockTimeout.value);
|
||||
snapLockTimeout.value = null;
|
||||
thumbScale.value = 1; // 确保滑块恢复正常大小
|
||||
}
|
||||
|
||||
function setValue(value) {
|
||||
// 应用步进,确保即使通过点击预设值也会遵循步进
|
||||
if (props.step > 0) {
|
||||
value = roundToStep(value);
|
||||
}
|
||||
emit("update:modelValue", value);
|
||||
}
|
||||
|
||||
function stopSliding() {
|
||||
// 清除吸附锁定
|
||||
clearSnapLock();
|
||||
|
||||
if (isSliding.value) {
|
||||
// 如果确实进行了滑动,则发送滑动结束事件
|
||||
if (slideMoved.value) {
|
||||
emit("slide-end", { isSlide: true });
|
||||
|
||||
// 只有在滑动操作后才启动隐藏计时器
|
||||
startHideTooltipTimer();
|
||||
} else {
|
||||
// 只是点击,不是滑动,不自动隐藏tooltip
|
||||
emit("slide-end", { isSlide: false });
|
||||
}
|
||||
}
|
||||
|
||||
isSliding.value = false;
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("mouseup", stopSliding);
|
||||
}
|
||||
|
||||
// 处理点击事件
|
||||
function handleClick(event) {
|
||||
// 对于纯点击操作,发出click事件
|
||||
if (!slideMoved.value) {
|
||||
clearHideTooltipTimer(); // 清除任何现有的隐藏计时器
|
||||
updateTooltipVisibility(true); // 确保tooltip在点击时显示
|
||||
emit("click");
|
||||
// 点击不启动隐藏计时器
|
||||
}
|
||||
}
|
||||
|
||||
// 触摸事件处理
|
||||
function startTouchSliding(event) {
|
||||
isSliding.value = true;
|
||||
slideMoved.value = false; // 重置滑动状态
|
||||
updateTooltipVisibility(true);
|
||||
updateValueFromTouchPosition(event);
|
||||
|
||||
// 滑动开始的触感反馈
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate(10); // 更轻微的振动反馈
|
||||
}
|
||||
|
||||
emit("slide-start");
|
||||
|
||||
window.addEventListener("touchmove", handleTouchMove, { passive: false });
|
||||
window.addEventListener("touchend", stopTouchSliding);
|
||||
}
|
||||
|
||||
function handleTouchMove(event) {
|
||||
if (isSliding.value) {
|
||||
slideMoved.value = true; // 标记已经发生了滑动
|
||||
event.preventDefault(); // 阻止页面滚动
|
||||
updateValueFromTouchPosition(event);
|
||||
}
|
||||
}
|
||||
|
||||
function updateValueFromTouchPosition(event) {
|
||||
if (!event.touches || event.touches.length === 0) return;
|
||||
|
||||
const touch = event.touches[0];
|
||||
const rect = sliderTrack.value.getBoundingClientRect();
|
||||
const trackHeight = rect.height;
|
||||
const posY = touch.clientY - rect.top;
|
||||
|
||||
// 计算相对位置并转换为min-max范围的值
|
||||
let newValue = props.max - (posY / trackHeight) * (props.max - props.min);
|
||||
|
||||
// 边界检查
|
||||
newValue = Math.max(props.min, Math.min(props.max, newValue));
|
||||
|
||||
// 应用步进
|
||||
if (props.step > 0) {
|
||||
newValue = roundToStep(newValue);
|
||||
}
|
||||
|
||||
// 如果当前处于吸附锁定状态,并且移动不超过锁定阈值,则保持当前值不变
|
||||
if (isSnapping.value && snapLockValue.value !== null) {
|
||||
// 增加吸附力度:扩大保持吸附的阈值范围
|
||||
if (
|
||||
Math.abs(newValue - snapLockValue.value) <
|
||||
(props.snapThreshold / trackHeight) * (props.max - props.min) * 1.5
|
||||
) {
|
||||
newValue = snapLockValue.value;
|
||||
} else {
|
||||
// 移动距离超过阈值,解除吸附锁定,并提供反馈
|
||||
clearSnapLock();
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate(8); // 解除吸附的轻微振动
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 检查是否可以进入吸附状态
|
||||
const snapPercentage =
|
||||
(props.snapThreshold / trackHeight) * (props.max - props.min);
|
||||
|
||||
// 检查是否接近预设值,增加吸附判定范围
|
||||
for (const preset of props.presets) {
|
||||
if (Math.abs(newValue - preset) < snapPercentage * 0.4) {
|
||||
// 增加判定范围
|
||||
// 进入吸附状态
|
||||
setSnapLock(preset);
|
||||
newValue = preset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否接近记忆值
|
||||
if (!isSnapping.value) {
|
||||
for (const memValue of props.memorizedValues) {
|
||||
if (Math.abs(newValue - memValue) < snapPercentage * 0.4) {
|
||||
// 增加判定范围
|
||||
// 进入吸附状态
|
||||
setSnapLock(memValue);
|
||||
newValue = memValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setValue(newValue);
|
||||
}
|
||||
|
||||
function stopTouchSliding() {
|
||||
// 滑动结束时的反馈
|
||||
if (slideMoved.value && navigator.vibrate) {
|
||||
navigator.vibrate(12); // 滑动结束的振动反馈
|
||||
}
|
||||
|
||||
// 清除吸附锁定
|
||||
clearSnapLock();
|
||||
|
||||
if (isSliding.value) {
|
||||
// 如果确实进行了滑动,则发送滑动结束事件
|
||||
if (slideMoved.value) {
|
||||
emit("slide-end", { isSlide: true });
|
||||
|
||||
// 只有在滑动操作后才启动隐藏计时器
|
||||
startHideTooltipTimer();
|
||||
} else {
|
||||
// 只是点击,不是滑动,不自动隐藏tooltip
|
||||
emit("slide-end", { isSlide: false });
|
||||
}
|
||||
}
|
||||
|
||||
isSliding.value = false;
|
||||
window.removeEventListener("touchmove", handleTouchMove);
|
||||
window.removeEventListener("touchend", stopTouchSliding);
|
||||
}
|
||||
|
||||
// 添加点击外部关闭tooltip的处理函数
|
||||
function handleOutsideClick(event) {
|
||||
if (!props.closeOnOutsideClick || !tooltipVisible.value) return;
|
||||
|
||||
// 如果正在滑动,不处理外部点击事件
|
||||
if (isSliding.value) return;
|
||||
|
||||
// 检查点击是否在slider组件外部
|
||||
const containerEl = sliderTrack.value?.parentElement;
|
||||
const tooltipEl = tooltip.value;
|
||||
|
||||
if (
|
||||
containerEl &&
|
||||
tooltipEl &&
|
||||
!containerEl.contains(event.target) &&
|
||||
!tooltipEl.contains(event.target)
|
||||
) {
|
||||
updateTooltipVisibility(false);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 添加全局点击事件监听
|
||||
if (props.closeOnOutsideClick) {
|
||||
// 使用 setTimeout 确保点击事件在其他处理程序之后执行
|
||||
window.addEventListener("click", (e) =>
|
||||
setTimeout(() => handleOutsideClick(e), 0)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 清理事件监听器
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("mouseup", stopSliding);
|
||||
window.removeEventListener("touchmove", handleTouchMove);
|
||||
window.removeEventListener("touchend", stopTouchSliding);
|
||||
window.removeEventListener("click", handleOutsideClick);
|
||||
|
||||
// 清除任何剩余的计时器
|
||||
clearHideTooltipTimer();
|
||||
clearSnapLock();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.vertical-slider-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
height: 150px;
|
||||
position: relative;
|
||||
// margin-top: 8px;
|
||||
// margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.slider-track {
|
||||
position: relative;
|
||||
width: 32px;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.slider-fill {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
// background: linear-gradient(to top, #2196f3, #64b5f6);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.slider-thumb {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
background: #fff;
|
||||
// border: 1px solid #2196f3;
|
||||
border-radius: 3px;
|
||||
cursor: grab;
|
||||
box-shadow: 0 0px 4px rgba(0, 0, 0, 0.4);
|
||||
transition: transform 0.15s cubic-bezier(0.175, 0.885, 0.32, 1.275); // 更平滑的动画效果
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加iPad和移动设备的专有样式
|
||||
@media (pointer: coarse) {
|
||||
.slider-thumb {
|
||||
height: 20px; // 在触摸设备上增加滑块尺寸,更容易点击
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0px 6px rgba(0, 0, 0, 0.5); // 更明显的阴影
|
||||
}
|
||||
|
||||
.slider-track {
|
||||
width: 40px; // 在触摸设备上增加宽度
|
||||
}
|
||||
|
||||
.slider-notch {
|
||||
width: 60%; // 增加刻度线宽度
|
||||
height: 3px; // 增加刻度线高度
|
||||
}
|
||||
}
|
||||
|
||||
// 当设备支持悬停时的效果 (通常是桌面设备)
|
||||
@media (hover: hover) {
|
||||
.slider-thumb:hover {
|
||||
box-shadow: 0 0 5px rgba(33, 150, 243, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.slider-notch {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 50%;
|
||||
height: 2px;
|
||||
background: #999;
|
||||
border-radius: 2px;
|
||||
transform: translateY(1px) translateX(50%);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.active {
|
||||
background: #2196f3;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.slider-tooltip {
|
||||
position: absolute;
|
||||
left: calc(100% + 15px);
|
||||
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);
|
||||
min-width: 120px;
|
||||
z-index: 10;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-width: 8px 8px 8px 0;
|
||||
border-style: solid;
|
||||
border-color: transparent rgba(255, 255, 255, 0.95) transparent transparent;
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义滑块颜色
|
||||
// .size-slider {
|
||||
// .slider-fill {
|
||||
// // background: linear-gradient(to top, #2196f3, #64b5f6);
|
||||
// }
|
||||
|
||||
// .slider-thumb {
|
||||
// border-color: #2196f3;
|
||||
// }
|
||||
|
||||
// .slider-notch.active {
|
||||
// background: #2196f3;
|
||||
// }
|
||||
// }
|
||||
|
||||
// .opacity-slider {
|
||||
// .slider-fill {
|
||||
// // background: linear-gradient(to top, #ff9800, #ffb74d);
|
||||
// }
|
||||
|
||||
// .slider-thumb {
|
||||
// border-color: #ff9800;
|
||||
// }
|
||||
|
||||
// .slider-notch.active {
|
||||
// background: #ff9800;
|
||||
// }
|
||||
// }
|
||||
|
||||
// 淡入淡出动画
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
|
||||
// 响应式调整
|
||||
@media (max-height: 600px) {
|
||||
.vertical-slider-container {
|
||||
height: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.slider-tooltip {
|
||||
left: calc(100% + 10px);
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user