2025-06-09 10:25:54 +08:00
|
|
|
|
<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">
|
2025-06-26 00:37:07 +08:00
|
|
|
|
<svg-icon name="CPaste" size="20" />
|
2025-06-09 10:25:54 +08:00
|
|
|
|
<span class="btn-text">{{ $t("拷贝并粘贴") }}</span>
|
|
|
|
|
|
</div>
|
2025-06-26 00:37:07 +08:00
|
|
|
|
<div class="action-btn" @click="cutSelectionToNewLayer">
|
|
|
|
|
|
<svg-icon name="CCut" size="30" />
|
|
|
|
|
|
<span class="btn-text">{{ $t("剪切并粘贴") }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="action-btn" @click="clearSelectionContent">
|
|
|
|
|
|
<svg-icon name="CClear" size="22" />
|
|
|
|
|
|
<span class="btn-text">{{ $t("清除选择内容") }}</span>
|
|
|
|
|
|
</div>
|
2025-06-09 10:25:54 +08:00
|
|
|
|
<!-- <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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-26 00:37:07 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 剪切选区到新图层
|
|
|
|
|
|
*/
|
|
|
|
|
|
function cutSelectionToNewLayer() {
|
|
|
|
|
|
if (!hasSelection.value) return;
|
|
|
|
|
|
props.commandManager.execute(
|
|
|
|
|
|
new CopySelectionToNewLayerCommand({
|
|
|
|
|
|
canvas: props.canvas,
|
|
|
|
|
|
layerManager: props.layerManager,
|
|
|
|
|
|
selectionManager: props.selectionManager,
|
|
|
|
|
|
toolManager: props.toolManager,
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
checkSelectionStatus();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-09 10:25:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 清除选区内容
|
|
|
|
|
|
*/
|
|
|
|
|
|
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);
|
2025-06-26 00:37:07 +08:00
|
|
|
|
user-select: none;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 平板和手机适配 */
|
|
|
|
|
|
@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;
|
2025-06-26 00:37:07 +08:00
|
|
|
|
font-size: 14px;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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 {
|
2025-06-26 00:37:07 +08:00
|
|
|
|
font-size: 14px;
|
2025-06-09 10:25:54 +08:00
|
|
|
|
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>
|