856 lines
18 KiB
Vue
856 lines
18 KiB
Vue
|
|
<template>
|
|||
|
|
<transition name="fade">
|
|||
|
|
<div class="part-selector-toolbar" v-if="visible" :class="{active:!closePanel}">
|
|||
|
|
<div class="btn" @click="setClosePanel"><i class="fi fi-br-angle-left"></i></div>
|
|||
|
|
<!-- 顶部选区类型工具栏 -->
|
|||
|
|
<div class="toolbar-section">
|
|||
|
|
<div class="toolbar-header">
|
|||
|
|
<div class="header-title">{{ t("Canvas.GarmentPartSelector") }}</div>
|
|||
|
|
<!-- 移除关闭按钮,完全通过工具切换控制显示隐藏 -->
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="tool-types">
|
|||
|
|
<div
|
|||
|
|
:class="[
|
|||
|
|
'tool-btn',
|
|||
|
|
{ active: selectionType === OperationType.LASSO },
|
|||
|
|
]"
|
|||
|
|
@click="setSelectionType(OperationType.LASSO)"
|
|||
|
|
>
|
|||
|
|
<svg-icon name="CFree" size="20" />
|
|||
|
|
<span>{{ $t("Canvas.freehandSketching") }}</span>
|
|||
|
|
</div>
|
|||
|
|
<div
|
|||
|
|
:class="[
|
|||
|
|
'tool-btn',
|
|||
|
|
{ active: selectionType === OperationType.LASSO_RECTANGLE },
|
|||
|
|
]"
|
|||
|
|
@click="setSelectionType(OperationType.LASSO_RECTANGLE)"
|
|||
|
|
>
|
|||
|
|
<svg-icon name="CRectangle" size="26" />
|
|||
|
|
<span>{{ $t("Canvas.rectangle") }}</span>
|
|||
|
|
</div>
|
|||
|
|
<div
|
|||
|
|
:class="[
|
|||
|
|
'tool-btn',
|
|||
|
|
{ active: selectionType === OperationType.LASSO_ELLIPSE },
|
|||
|
|
]"
|
|||
|
|
@click="setSelectionType(OperationType.LASSO_ELLIPSE)"
|
|||
|
|
>
|
|||
|
|
<svg-icon name="CEllipse" size="24" />
|
|||
|
|
<span>{{ $t("Canvas.ellipse") }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 分割线 -->
|
|||
|
|
<div class="toolbar-divider"></div>
|
|||
|
|
|
|||
|
|
<!-- 底部选区操作工具栏 -->
|
|||
|
|
<div class="tool-actions">
|
|||
|
|
<div class="action-btn" @click="copySelectionToNewLayer">
|
|||
|
|
<svg-icon name="CPaste" size="16" />
|
|||
|
|
<span class="btn-text">{{ $t("Canvas.creation") }}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="action-btn" @click="cutSelectionToNewLayer">
|
|||
|
|
<svg-icon name="CCut" size="26" />
|
|||
|
|
<span class="btn-text">{{ $t("Canvas.CreateAndCopy") }}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="action-btn" @click="clearSelectionContent">
|
|||
|
|
<svg-icon name="CClear" size="18" />
|
|||
|
|
<span class="btn-text">{{ $t("Canvas.TheClearlySelectedContent") }}</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("Canvas.close") }}
|
|||
|
|
</button>
|
|||
|
|
<button class="confirm-btn" @click="applyFeather">
|
|||
|
|
{{ $t("Canvas.confirmEdit") }}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 颜色选择器 -->
|
|||
|
|
<div v-if="showColorPicker" class="dialog-overlay">
|
|||
|
|
<div class="dialog-container">
|
|||
|
|
<div class="dialog-header">
|
|||
|
|
<h3>{{ $t("Canvas.SelectFillColor") }}</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("Canvas.close") }}
|
|||
|
|
</button>
|
|||
|
|
<button class="confirm-btn" @click="confirmColorPicker">
|
|||
|
|
{{ $t("Canvas.confirmEdit") }}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</transition>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { ref, onMounted, watch } from "vue";
|
|||
|
|
import { useI18n } from "vue-i18n";
|
|||
|
|
import {
|
|||
|
|
CreateSelectionCommand,
|
|||
|
|
InvertSelectionCommand,
|
|||
|
|
FeatherSelectionCommand,
|
|||
|
|
FillSelectionCommand,
|
|||
|
|
// CopySelectionToNewLayerCommand,
|
|||
|
|
// ClearSelectionContentCommand,
|
|||
|
|
} from "../commands/SelectionCommands";
|
|||
|
|
import { ToolCommand } from "../commands/ToolCommands";
|
|||
|
|
import {
|
|||
|
|
LassoCutoutCommand,
|
|||
|
|
ClearSelectionCommand,
|
|||
|
|
// CutSelectionToNewLayerCommand,
|
|||
|
|
} from "../commands/LassoCutoutCommand";
|
|||
|
|
|
|||
|
|
import { OperationType } from "../utils/layerHelper";
|
|||
|
|
import { ClearSelectionContentCommand } from "../commands/ClearSelectionContentCommand";
|
|||
|
|
import { CutSelectionToNewLayerCommand } from "../commands/CutSelectionToNewLayerCommand";
|
|||
|
|
|
|||
|
|
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 closePanel = ref(false)
|
|||
|
|
const setClosePanel = ()=>{
|
|||
|
|
closePanel.value = !closePanel.value
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 国际化
|
|||
|
|
const { t } = useI18n();
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
// 为选区管理器添加监听,以便在选区变化时更新状态
|
|||
|
|
if (props.selectionManager) {
|
|||
|
|
// 在选区管理器中添加选区变化的监听
|
|||
|
|
checkSelectionStatus();
|
|||
|
|
|
|||
|
|
// 设置选区状态变化的回调
|
|||
|
|
// eslint-disable-next-line vue/no-mutating-props
|
|||
|
|
props.selectionManager.onSelectionChanged = () => {
|
|||
|
|
checkSelectionStatus();
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 监听 activeTool 变化
|
|||
|
|
watch(
|
|||
|
|
() => props.activeTool,
|
|||
|
|
(newTool) => {
|
|||
|
|
// 当工具为LASSO或AREA类型时显示选区面板
|
|||
|
|
const selectionTools = [
|
|||
|
|
OperationType.PART,
|
|||
|
|
OperationType.PART_RECTANGLE,
|
|||
|
|
OperationType.PART_BRUSH,
|
|||
|
|
OperationType.PART_ERASER
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
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;
|
|||
|
|
closePanel.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 cutSelectionToNewLayer() {
|
|||
|
|
if (!hasSelection.value) return;
|
|||
|
|
props.commandManager.execute(
|
|||
|
|
new CutSelectionToNewLayerCommand({
|
|||
|
|
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">
|
|||
|
|
.part-selector-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);
|
|||
|
|
user-select: none;
|
|||
|
|
&.active{
|
|||
|
|
transform: translateY(100%);
|
|||
|
|
> .btn{
|
|||
|
|
> i{
|
|||
|
|
transform: rotate(90deg);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
> .btn{
|
|||
|
|
width: 100%;
|
|||
|
|
text-align: center;
|
|||
|
|
cursor: pointer;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
height: 22px;
|
|||
|
|
|
|||
|
|
> i{
|
|||
|
|
font-size: 1.4rem;
|
|||
|
|
transform: rotate(270deg);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 平板和手机适配 */
|
|||
|
|
@media screen and (max-width: 768px) {
|
|||
|
|
.part-selector-toolbar {
|
|||
|
|
bottom: 15px;
|
|||
|
|
left: 15px;
|
|||
|
|
right: 15px;
|
|||
|
|
max-width: calc(100vw - 30px);
|
|||
|
|
border-radius: 6px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media screen and (max-width: 480px) {
|
|||
|
|
.part-selector-toolbar {
|
|||
|
|
bottom: 10px;
|
|||
|
|
left: 10px;
|
|||
|
|
right: 10px;
|
|||
|
|
max-width: calc(100vw - 20px);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.part-selector-toolbar.is-active {
|
|||
|
|
transform: translateY(0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.toolbar-header {
|
|||
|
|
// display: flex;
|
|||
|
|
// justify-content: center;
|
|||
|
|
// align-items: center;
|
|||
|
|
padding: 8px 0;
|
|||
|
|
// 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 0;
|
|||
|
|
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 3rem 1.2rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tool-types {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(3, 1fr);
|
|||
|
|
gap: 8px;
|
|||
|
|
padding: 10px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.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: 6px;
|
|||
|
|
color: #333;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.2s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tool-btn span {
|
|||
|
|
margin-top: 0;
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.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-bottom: 5px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tool-actions {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(3, 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;
|
|||
|
|
flex-direction: row;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
background: none;
|
|||
|
|
border: none;
|
|||
|
|
color: #333;
|
|||
|
|
cursor: pointer;
|
|||
|
|
padding: 0;
|
|||
|
|
gap: 4px;
|
|||
|
|
.c-svg {
|
|||
|
|
width: auto;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.action-btn svg {
|
|||
|
|
width: 22px;
|
|||
|
|
height: 22px;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-text {
|
|||
|
|
display: block;
|
|||
|
|
font-size: 12px;
|
|||
|
|
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 0;
|
|||
|
|
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>
|