合并画布代码

This commit is contained in:
X1627315083
2025-06-18 11:05:23 +08:00
parent 903c0ebdf5
commit 9c7fae36eb
118 changed files with 23633 additions and 8201 deletions

View File

@@ -0,0 +1,369 @@
<script setup>
import { ref, watch, nextTick, onMounted, onUnmounted } from "vue";
import SvgIcon from "../../../SvgIcon/index.vue";
const props = defineProps({
visible: Boolean,
position: {
type: Object,
default: () => ({ x: 0, y: 0 }),
},
items: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["close", "select"]);
const menuRef = ref(null);
const adjustedPosition = ref({ x: 0, y: 0 });
const hoveredItem = ref(null);
const submenuPositions = ref(new Map());
const hideTimer = ref(null); // 添加隐藏定时器
// 计算菜单位置,处理边界问题
const calculatePosition = () => {
if (!menuRef.value || !props.visible) return;
const menu = menuRef.value;
const menuRect = menu.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
let x = props.position.x;
let y = props.position.y;
// 右边界检测
if (x + menuRect.width > windowWidth - 10) {
x = x - menuRect.width;
}
// 底边界检测
if (y + menuRect.height > windowHeight - 10) {
y = windowHeight - menuRect.height - 10;
}
// 左边界检测
if (x < 10) {
x = 10;
}
// 顶边界检测
if (y < 10) {
y = 10;
}
adjustedPosition.value = { x, y };
};
// 计算子菜单位置
const calculateSubmenuPosition = (itemElement, itemIndex) => {
if (!itemElement || !menuRef.value) return { x: 0, y: 0, direction: "right" };
const itemRect = itemElement.getBoundingClientRect();
const menuRect = menuRef.value.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
// 预估子菜单宽度(可以根据实际情况调整)
const submenuWidth = 200;
const submenuHeight = 300; // 预估高度
let x = itemRect.right + 4;
// 直接使用菜单项相对于主菜单容器的偏移量
let y = itemElement.offsetTop;
let direction = "right";
// 右边界检测,如果右侧空间不足,显示在左侧
if (x + submenuWidth > windowWidth - 10) {
x = itemRect.left - submenuWidth - 4;
direction = "left";
}
// 底边界检测 - 基于子菜单的绝对位置检查
const absoluteSubmenuBottom = itemRect.top + submenuHeight;
if (absoluteSubmenuBottom > windowHeight - 10) {
// 计算可用的最大Y位置相对于主菜单
const maxAbsoluteY = windowHeight - submenuHeight - 10;
const maxRelativeY = maxAbsoluteY - menuRect.top;
y = Math.max(0, maxRelativeY);
}
// 左边界检测
if (x < 10) {
x = 10;
direction = "right";
}
// 确保 y 不为负数
if (y < 0) {
y = 0;
}
y = 0;
const position = { x, y, direction };
submenuPositions.value.set(itemIndex, position);
return position;
};
// 清除隐藏定时器
const clearHideTimer = () => {
if (hideTimer.value) {
clearTimeout(hideTimer.value);
hideTimer.value = null;
}
};
// 显示子菜单
const showSubmenu = (item, index, element) => {
if (item.children && item.children.length > 0) {
clearHideTimer();
hoveredItem.value = index;
nextTick(() => {
calculateSubmenuPosition(element, index);
});
}
};
// 隐藏子菜单(延迟)
const hideSubmenu = (index) => {
clearHideTimer();
hideTimer.value = setTimeout(() => {
if (hoveredItem.value === index) {
hoveredItem.value = null;
}
}, 150);
};
// 处理鼠标进入菜单项
const handleItemMouseEnter = (item, index, event) => {
const element = event.target.closest(".context-menu-item");
showSubmenu(item, index, element);
};
// 处理鼠标在菜单项内移动
const handleItemMouseMove = (item, index, event) => {
// 如果当前菜单项有子菜单但子菜单未显示,则显示子菜单
if (
item.children &&
item.children.length > 0 &&
hoveredItem.value !== index
) {
const element = event.target.closest(".context-menu-item");
showSubmenu(item, index, element);
}
};
// 处理鼠标离开菜单项
const handleItemMouseLeave = (item, index) => {
// 只有当有子菜单时才延迟隐藏
if (item.children && item.children.length > 0) {
hideSubmenu(index);
} else {
hoveredItem.value = null;
}
};
// 处理鼠标进入子菜单
const handleSubmenuMouseEnter = (index) => {
clearHideTimer();
hoveredItem.value = index;
};
// 处理鼠标离开子菜单
const handleSubmenuMouseLeave = (index) => {
hideSubmenu(index);
};
// 监听可见性和位置变化
watch([() => props.visible, () => props.position], () => {
if (props.visible) {
nextTick(() => {
calculatePosition();
});
} else {
hoveredItem.value = null;
submenuPositions.value.clear();
}
});
// 处理菜单项点击
const handleItemClick = (item, index) => {
if (item.disabled || item.type === "divider") return;
// 如果有子菜单,不关闭菜单
if (item.children && item.children.length > 0) {
return;
}
emit("select", item, index);
if (item.action) {
item.action();
}
emit("close");
};
// 处理子菜单项点击
const handleSubItemClick = (subItem, parentIndex, subIndex) => {
if (subItem.disabled || subItem.type === "divider") return;
emit("select", subItem, `${parentIndex}-${subIndex}`);
if (subItem.action) {
subItem.action();
}
emit("close");
};
// 处理外部点击关闭
const handleOutsideClick = (event) => {
if (menuRef.value && !menuRef.value.contains(event.target)) {
emit("close");
}
};
// 处理 ESC 键关闭
const handleEscKey = (event) => {
if (event.key === "Escape") {
emit("close");
}
};
onMounted(() => {
document.addEventListener("click", handleOutsideClick, true);
document.addEventListener("contextmenu", handleOutsideClick, true);
document.addEventListener("keydown", handleEscKey);
});
onUnmounted(() => {
document.removeEventListener("click", handleOutsideClick, true);
document.removeEventListener("contextmenu", handleOutsideClick, true);
document.removeEventListener("keydown", handleEscKey);
});
</script>
<template>
<teleport to="body">
<transition name="context-menu">
<div
v-if="visible"
ref="menuRef"
class="context-menu"
:style="{
top: `${adjustedPosition.y}px`,
left: `${adjustedPosition.x}px`,
}"
@contextmenu.prevent
>
<template v-for="(item, index) in items" :key="index">
<!-- 分隔线 -->
<div
v-if="item.type === 'divider'"
class="context-menu-divider"
></div>
<!-- 菜单项 -->
<div
v-else
class="context-menu-item"
:class="{
disabled: item.disabled,
danger: item.danger,
'has-children': item.children && item.children.length > 0,
hovered: hoveredItem === index,
}"
@click="handleItemClick(item, index)"
@mouseenter="handleItemMouseEnter(item, index, $event)"
@mousemove="handleItemMouseMove(item, index, $event)"
@mouseleave="handleItemMouseLeave(item, index)"
>
<span class="context-menu-icon" v-if="item.icon">
<SvgIcon
:name="item.icon"
size="14"
:style="{
transform: item.inverIcon ? `rotate(90deg)` : 'none',
}"
/>
</span>
<span class="context-menu-label">{{ item.label }}</span>
<span class="context-menu-shortcut" v-if="item.shortcut">
{{ item.shortcut }}
</span>
<span
class="context-menu-arrow"
v-if="item.children && item.children.length > 0"
>
<SvgIcon name="CRight" size="12" />
</span>
<!-- 子菜单 -->
<transition name="context-submenu">
<div
v-if="
item.children &&
item.children.length > 0 &&
hoveredItem === index
"
class="context-submenu"
:class="{
'submenu-left':
submenuPositions.get(index)?.direction === 'left',
}"
@mouseenter="handleSubmenuMouseEnter(index)"
@mouseleave="handleSubmenuMouseLeave"
>
<template
v-for="(subItem, subIndex) in item.children"
:key="subIndex"
>
<!-- 子菜单分隔线 -->
<div
v-if="subItem.type === 'divider'"
class="context-menu-divider"
></div>
<!-- 子菜单项 -->
<div
v-else
class="context-menu-item"
:class="{
disabled: subItem.disabled,
danger: subItem.danger,
}"
@click="handleSubItemClick(subItem, index, subIndex)"
>
<span class="context-menu-icon" v-if="subItem.icon">
<SvgIcon
:name="subItem.icon"
size="14"
:style="{
transform: subItem.inverIcon
? `rotate(90deg)`
: 'none',
}"
/>
</span>
<span class="context-menu-label">{{ subItem.label }}</span>
<span class="context-menu-shortcut" v-if="subItem.shortcut">
{{ subItem.shortcut }}
</span>
</div>
</template>
</div>
</transition>
</div>
</template>
</div>
</transition>
</teleport>
</template>
<style scoped lang="less">
@import "./contextMenu.less";
</style>

View File

@@ -0,0 +1,514 @@
<script setup>
import { ref, nextTick, computed, inject } from "vue";
import { Checkbox } from "ant-design-vue";
import { VueDraggable } from "vue-draggable-plus";
import SvgIcon from "../../../SvgIcon/index.vue";
import { isGroupLayer } from "../../utils/layerHelper";
// 设置组件名称,用于递归渲染
defineOptions({
name: "LayerItem",
});
const props = defineProps({
layer: {
type: Object,
required: true,
},
isChild: {
type: Boolean,
default: false,
},
isActive: {
type: Boolean,
default: false,
},
isSelected: {
type: Boolean,
default: false,
},
isMultiSelectMode: {
type: Boolean,
default: false,
},
isEditing: {
type: Boolean,
default: false,
},
editingName: {
type: String,
default: "",
},
canDelete: {
type: Boolean,
default: true,
},
thumbnailUrl: {
type: String,
default: null,
},
isHidenDragHandle: {
type: Boolean,
default: false,
},
expandedGroupIds: {
type: Set,
default: () => new Set(),
},
});
const emit = defineEmits([
"click",
"double-click",
"context-menu",
"checkbox-change",
"toggle-visibility",
"toggle-lock",
"delete",
"edit-confirm",
"edit-cancel",
"edit-keydown",
"touch-start",
"touch-move",
"touch-end",
"child-layers-sort",
"update-child-layers",
"toggle-group-expanded",
// 新增子图层专用事件
"toggle-child-visibility",
"toggle-child-lock",
"delete-child",
"rename-child",
// v-model相关事件
"update:editingName",
]);
const layerManager = inject("layerManager", null);
// 计算属性
const isGroupLayerType = computed(() => {
return isGroupLayer(props.layer);
});
// 计算属性:检查组图层是否展开
const isGroupExpanded = computed(() => {
return props.expandedGroupIds.has(props.layer.id);
});
// 获取子图层
const childLayers = computed(() => {
if (!isGroupLayerType.value) return [];
// 优先使用 layer.children 属性
if (props.layer.children && Array.isArray(props.layer.children)) {
return props.layer.children;
}
return [];
});
// 切换组图层展开/收起状态
const toggleGroupExpanded = () => {
emit("toggle-group-expanded", props.layer.id);
};
// 获取图层类型图标
function getLayerTypeIcon(layer) {
if (!layer) return "🖼️";
if (isGroupLayer(layer)) {
return "📁";
}
if (layer.fabricObject) {
switch (layer.fabricObject.type) {
case "image":
return "🖼️";
case "text":
return "📝";
case "rect":
return "▢";
case "circle":
return "⬤";
case "path":
return "✎";
default:
return "⬤";
}
}
return "🖼️";
}
function getLayerTypeText(layerType) {
const typeMap = {
EMPTY: "空图层",
TEXT: "文本",
IMAGE: "图片",
SHAPE: "形状",
GROUP: "组合",
BACKGROUND: "背景",
FIXED: "固定",
};
return typeMap[layerType] || "未知";
}
// 事件处理
function handleClick(event) {
emit("click", props.layer, event);
}
function handleDoubleClick(event) {
emit("double-click", props.layer, event);
}
function handleContextMenu(event) {
emit("context-menu", event, props.layer);
}
function handleCheckboxChange(event) {
emit("checkbox-change", props.layer.id, event);
}
function handleToggleVisibility() {
if (props.isChild) {
// 子图层需要传递父图层ID - 从父级组件获取
const parentId = props.layer.parentId || findParentLayerId();
emit("toggle-child-visibility", props.layer.id, parentId);
} else {
// 一级图层
emit("toggle-visibility", props.layer.id);
}
}
function handleToggleLock() {
if (props.isChild) {
// 子图层需要传递父图层ID - 从父级组件获取
const parentId = props.layer.parentId || findParentLayerId();
emit("toggle-child-lock", props.layer.id, parentId);
} else {
// 一级图层
emit("toggle-lock", props.layer);
}
}
function handleDelete() {
if (!props.canDelete) {
console.warn("当前图层无法删除:", props.layer.id);
return;
}
if (props.isChild) {
// 子图层删除需要传递父图层ID
const parentId = props.layer.parentId || findParentLayerId();
if (parentId) {
emit("delete-child", props.layer.id, parentId);
} else {
console.warn("无法找到子图层的父图层ID:", props.layer.id);
}
} else {
// 一级图层删除
emit("delete", props.layer.id);
}
}
function handleEditConfirm() {
if (props.isChild) {
// 子图层重命名需要传递父图层ID
const parentId = props.layer.parentId || findParentLayerId();
if (props.editingName && props.editingName.trim() && parentId) {
emit("rename-child", props.layer.id, parentId, props.editingName.trim());
} else if (!parentId) {
console.warn("无法找到子图层的父图层ID:", props.layer.id);
}
// 发送编辑取消事件,清理编辑状态
emit("edit-cancel");
} else {
// 一级图层重命名
emit("edit-confirm");
}
}
function handleEditCancel() {
emit("edit-cancel");
}
function handleEditKeydown(event) {
emit("edit-keydown", event); // 修复事件名称,从 "edit-keyboard" 改为 "edit-keydown"
}
function handleTouchStart(event) {
emit("touch-start", event, props.layer);
}
function handleTouchMove(event) {
emit("touch-move", event);
}
function handleTouchEnd(event) {
emit("touch-end", event);
}
function handleUpdateChildLayers(newChildren) {
// 更新当前组图层的children数组
console.log(
"更新子图层顺序:",
"父图层ID:",
props.layer.id,
"新顺序:",
newChildren
);
emit("update-child-layers", props.layer.id, newChildren);
}
// 子图层递归事件处理
function handleChildClick(childLayer, event) {
emit("click", childLayer, event);
}
function handleChildDoubleClick(childLayer, event) {
emit("double-click", childLayer, event);
}
function handleChildContextMenu(event, childLayer) {
emit("context-menu", event, childLayer);
}
function handleChildToggleVisibility(childLayerId) {
emit("toggle-visibility", childLayerId);
}
function handleChildToggleLock(childLayer) {
emit("toggle-lock", childLayer);
}
// 动画钩子函数
function onEnter(el) {
// 设置初始状态
el.style.height = "0";
el.style.opacity = "0";
el.style.paddingTop = "0";
el.style.paddingBottom = "0";
el.style.marginTop = "0";
el.style.marginBottom = "0";
el.style.overflow = "hidden";
// 强制重排
el.offsetHeight;
// 获取最终高度
el.style.height = "auto";
const finalHeight = el.scrollHeight;
el.style.height = "0";
// 执行动画
requestAnimationFrame(() => {
el.style.transition = "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)";
el.style.height = finalHeight + "px";
el.style.opacity = "1";
el.style.paddingTop = "";
el.style.paddingBottom = "";
el.style.marginTop = "";
el.style.marginBottom = "";
});
}
function onLeave(el) {
// 设置当前高度
el.style.height = el.scrollHeight + "px";
el.style.overflow = "hidden";
// 强制重排
el.offsetHeight;
// 执行收起动画
el.style.transition = "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)";
el.style.height = "0";
el.style.opacity = "0";
el.style.paddingTop = "0";
el.style.paddingBottom = "0";
el.style.marginTop = "0";
el.style.marginBottom = "0";
}
// 查找父图层ID的辅助方法 - 增强版本
function findParentLayerId() {
// 首先检查 layer 对象是否已经有 parentId 属性
if (props.layer.parentId) {
return props.layer.parentId;
}
// 如果没有,尝试从 layerManager 中查找
if (layerManager && layerManager.layers) {
for (const layer of layerManager.layers.value) {
if (
layer.children &&
Array.isArray(layer.children) &&
layer.children.some((child) => child.id === props.layer.id)
) {
return layer.id;
}
}
}
console.warn("无法找到图层的父图层:", props.layer.id);
return null;
}
</script>
<template>
<div>
<!-- 主图层项 -->
<div
:class="[
'layer-item',
{
'child-layer': isChild,
active: isActive,
selected: isSelected,
'group-layer': isGroupLayerType,
editing: isEditing,
'multi-select-mode': isMultiSelectMode,
invisible: !layer.visible,
locked: layer.locked,
'fixed-layer': layer.isBackground || layer.isFixed,
},
]"
@click="handleClick"
@dblclick="handleDoubleClick"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
@contextmenu.prevent="handleContextMenu"
>
<!-- 拖拽手柄 -->
<div class="layer-drag-handle" :title="$t('拖拽排序')">
<SvgIcon
v-if="!isHidenDragHandle"
:name="isChild ? 'CSort' : 'CSort'"
:size="16"
></SvgIcon>
</div>
<!-- 图层头部 -->
<div class="layer-header">
<!-- 多选复选框 -->
<div
v-if="isMultiSelectMode && !isChild"
class="layer-checkbox"
@click.stop
>
<Checkbox :checked="isSelected" @change="handleCheckboxChange" />
</div>
<!-- 图层预览图标 -->
<div class="layer-review">
<img
v-if="thumbnailUrl"
:src="thumbnailUrl"
class="layer-thumbnail"
:alt="$t('图层预览')"
/>
<span v-else class="layer-type-icon">{{
getLayerTypeIcon(layer)
}}</span>
</div>
<!-- 图层名称 -->
<div class="layer-name-container" :title="layer.name">
<div class="layer-name-wrapper">
<span v-if="!isEditing" class="layer-name text-ellipsis">
{{ layer.name }}
</span>
<input
v-else
:value="editingName"
:data-layer-id="layer.id"
:data-child-layer-id="isChild ? layer.id : undefined"
class="layer-name-input"
@blur="handleEditConfirm"
@keydown="handleEditKeydown"
@click.stop
@input="$emit('update:editingName', $event.target.value)"
/>
</div>
</div>
<!-- 图层操作按钮 -->
<div class="layer-actions" v-if="!(isGroupLayerType && !isChild)">
<!-- 可见性切换 -->
<div
class="visibility-btn"
@click.stop="handleToggleVisibility"
:title="$t('显示/隐藏图层')"
>
<SvgIcon v-if="layer.visible" name="CEye" :size="16"></SvgIcon>
<SvgIcon v-else name="CUnEye" :size="16"></SvgIcon>
</div>
<!-- 锁定状态 -->
<span
v-if="layer.locked"
class="status-icon locked"
:class="{ disabled: layer.isBackground || layer.isFixed }"
:title="$t('锁定')"
@click.stop="handleToggleLock"
>
<SvgIcon name="CLock" :size="18"></SvgIcon>
</span>
<span
v-else
class="status-icon"
:title="$t('未锁定')"
@click.stop="handleToggleLock"
>
<SvgIcon name="CUnLock" :size="18"></SvgIcon>
</span>
<!-- 删除按钮 -->
<div
class="delete-btn"
:class="{ disabled: !canDelete }"
:title="$t('删除图层')"
@click.stop="handleDelete"
>
<SvgIcon name="CDelete" size="14"></SvgIcon>
</div>
</div>
<!-- 组图层展开/收起图标 -->
<div
v-if="isGroupLayerType && !isChild"
class="group-expand-icon"
@click.stop="toggleGroupExpanded"
@dblclick.stop=""
:title="isGroupExpanded ? $t('收起组') : $t('展开组')"
>
<SvgIcon
name="CRight"
:size="12"
:style="{
transform: isGroupExpanded ? 'rotate(45deg)' : 'rotate(0deg)',
transition: 'transform 0.2s',
}"
/>
</div>
<!-- 图层状态指示器 -->
<!-- <div v-if="!isChild" class="layer-status">
<span
v-if="isGroupLayerType"
class="status-icon group"
:title="$t('组图层')"
>
📁
</span>
</div> -->
</div>
</div>
</div>
</template>
<style scoped lang="less">
@import "./layersPanel.less";
</style>

View File

@@ -0,0 +1,328 @@
<script setup>
import { computed, inject } from "vue";
import { VueDraggable } from "vue-draggable-plus";
import LayerItem from "./LayerItem.vue";
defineOptions({
name: "LayersList",
});
const props = defineProps({
layers: Array,
activeLayerId: String,
sortableRootLayers: Array,
selectedLayerIds: Array,
isMultiSelectMode: Boolean,
editingLayerId: String,
editingLayerName: String,
thumbnailManager: Object,
groupName: String,
expandedGroupIds: Set, // 新增:展开状态集合
isChild: Boolean,
parentLayerId: String, // 新增父图层ID
});
const emit = defineEmits([
"layer-click",
"layer-double-click",
"context-menu",
"checkbox-change",
"toggle-visibility",
"toggle-lock",
"delete",
"edit-confirm",
"edit-cancel",
"edit-keydown",
"touch-start",
"touch-move",
"touch-end",
"update:editing-name",
"root-layers-sort",
"child-layers-sort",
"select-child-layer",
"start-child-layer-edit",
"child-context-menu",
"finish-child-layer-edit",
"cancel-child-layer-edit",
"child-layer-edit-keydown",
"toggle-group-expanded",
// 新增子图层专用事件
"toggle-child-visibility",
"toggle-child-lock",
"delete-child",
"rename-child",
]);
// 检查图层是否被选中
const isLayerSelected = (layerId) => {
return props.selectedLayerIds.includes(layerId);
};
// 获取图层缩略图URL
function getLayerThumbnail(layerId) {
if (props.thumbnailManager) {
return props.thumbnailManager.getLayerThumbnail(layerId);
}
return null;
}
// 事件转发方法
const forwardEvent = (eventName, ...args) => {
emit(eventName, ...args);
};
// 处理根级图层拖拽排序
const handleRootLayersSort = (event) => {
if (props.isChild) {
// 子图层事件处理
// 确保排序只影响当前组图层的children而不是全局layers
emit(
"child-layers-sort",
event,
props.sortableRootLayers,
props.parentLayerId
);
} else {
emit("root-layers-sort", event);
}
};
const canDeleteComputed = computed(() => {
// 如果是子图层,检查父图层是否可以删除
if (props.isChild) {
const parentLayer = props.layers.find(
(layer) => layer.id === props.parentLayerId
);
return parentLayer?.children?.length > 1;
}
// 否则直接返回根图层的可删除状态
return props.layers.length > 3;
});
</script>
<template>
<div class="layers-list">
<!-- 可排序的根级图层 -->
<VueDraggable
:model-value="sortableRootLayers"
@end="handleRootLayersSort"
class="sortable-layers"
:animation="200"
:disabled="false"
handle=".layer-drag-handle"
ghost-class="ghost"
chosen-class="chosen"
drag-class="drag"
:group="groupName"
>
<!-- 遍历可排序的根级图层 -->
<template v-for="(layer, index) in sortableRootLayers" :key="layer.id">
<div class="layer-group">
<!-- 使用 LayerItem 子组件 -->
<LayerItem
:layer="layer"
:is-child="isChild"
:is-active="layer.id === activeLayerId"
:is-selected="isLayerSelected(layer.id)"
:is-multi-select-mode="isMultiSelectMode"
:is-editing="editingLayerId === layer.id"
:editing-name="editingLayerName"
:can-delete="
canDeleteComputed &&
!layer.isBackground &&
!layer.isFixed &&
!layer.locked
"
:thumbnail-url="getLayerThumbnail(layer.id)"
:expanded-group-ids="expandedGroupIds"
@click="(...args) => forwardEvent('layer-click', ...args)"
@double-click="
(...args) => forwardEvent('layer-double-click', ...args)
"
@context-menu="(...args) => forwardEvent('context-menu', ...args)"
@checkbox-change="
(...args) => forwardEvent('checkbox-change', ...args)
"
@toggle-visibility="
(...args) => forwardEvent('toggle-visibility', ...args)
"
@toggle-lock="(...args) => forwardEvent('toggle-lock', ...args)"
@delete="(...args) => forwardEvent('delete', ...args)"
@edit-confirm="(...args) => forwardEvent('edit-confirm', ...args)"
@edit-cancel="(...args) => forwardEvent('edit-cancel', ...args)"
@edit-keydown="(...args) => forwardEvent('edit-keydown', ...args)"
@touch-start="(...args) => forwardEvent('touch-start', ...args)"
@touch-move="(...args) => forwardEvent('touch-move', ...args)"
@touch-end="(...args) => forwardEvent('touch-end', ...args)"
@update:editing-name="
(...args) => forwardEvent('update:editing-name', ...args)
"
@toggle-group-expanded="
(...args) => forwardEvent('toggle-group-expanded', ...args)
"
@toggle-child-visibility="
(...args) => forwardEvent('toggle-child-visibility', ...args)
"
@toggle-child-lock="
(...args) => forwardEvent('toggle-child-lock', ...args)
"
@delete-child="(...args) => forwardEvent('delete-child', ...args)"
@rename-child="(...args) => forwardEvent('rename-child', ...args)"
/>
<!-- 子图层列表 (递归渲染) -->
<div
v-if="
layer?.children?.length > 0 &&
!layer.isBackground &&
!layer.isFixed &&
expandedGroupIds?.has(layer.id)
"
class="child-layers"
>
<LayersList
:layers="layers"
:sortableRootLayers="layer.children"
:active-layer-id="activeLayerId"
:selected-layer-ids="selectedLayerIds"
:is-multi-select-mode="isMultiSelectMode"
:editing-layer-id="editingLayerId"
:editing-layer-name="editingLayerName"
:thumbnail-manager="thumbnailManager"
:expanded-group-ids="expandedGroupIds"
:isChild="true"
:parentLayerId="layer.id"
group-name="layers-child"
@layer-click="(...args) => forwardEvent('layer-click', ...args)"
@layer-double-click="
(...args) => forwardEvent('layer-double-click', ...args)
"
@context-menu="(...args) => forwardEvent('context-menu', ...args)"
@checkbox-change="
(...args) => forwardEvent('checkbox-change', ...args)
"
@toggle-visibility="
(...args) => forwardEvent('toggle-visibility', ...args)
"
@toggle-lock="(...args) => forwardEvent('toggle-lock', ...args)"
@delete="(...args) => forwardEvent('delete', ...args)"
@edit-confirm="(...args) => forwardEvent('edit-confirm', ...args)"
@edit-cancel="(...args) => forwardEvent('edit-cancel', ...args)"
@edit-keydown="(...args) => forwardEvent('edit-keydown', ...args)"
@touch-start="(...args) => forwardEvent('touch-start', ...args)"
@touch-move="(...args) => forwardEvent('touch-move', ...args)"
@touch-end="(...args) => forwardEvent('touch-end', ...args)"
@update:editing-name="
(...args) => forwardEvent('update:editing-name', ...args)
"
@root-layers-sort="
(...args) => forwardEvent('root-layers-sort', ...args)
"
@child-layers-sort="
(...args) => forwardEvent('child-layers-sort', ...args)
"
@select-child-layer="
(...args) => forwardEvent('select-child-layer', ...args)
"
@start-child-layer-edit="
(...args) => forwardEvent('start-child-layer-edit', ...args)
"
@child-context-menu="
(...args) => forwardEvent('child-context-menu', ...args)
"
@toggle-child-visibility="
(...args) => forwardEvent('toggle-child-visibility', ...args)
"
@toggle-child-lock="
(...args) => forwardEvent('toggle-child-lock', ...args)
"
@finish-child-layer-edit="
(...args) => forwardEvent('finish-child-layer-edit', ...args)
"
@cancel-child-layer-edit="
(...args) => forwardEvent('cancel-child-layer-edit', ...args)
"
@child-layer-edit-keydown="
(...args) => forwardEvent('child-layer-edit-keydown', ...args)
"
@toggle-group-expanded="
(...args) => forwardEvent('toggle-group-expanded', ...args)
"
@delete-child="(...args) => forwardEvent('delete-child', ...args)"
@rename-child="(...args) => forwardEvent('rename-child', ...args)"
/>
</div>
</div>
</template>
</VueDraggable>
</div>
</template>
<style scoped lang="less">
// 从父组件的样式文件中继承相关样式
.layers-list {
flex: 1;
overflow-y: auto;
.sortable-layers {
min-height: 20px;
}
// .layer-group {
// // margin-bottom: 1px;
// }
.child-layers {
position: relative;
padding-left: 20px;
&::after {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 20px;
background-color: #e0e0e0;
z-index: 9;
}
.layer-item {
margin-left: 8px;
}
}
.fixed-layers {
border-top: 1px solid #e0e0e0;
}
// 拖拽状态样式
.ghost {
opacity: 0.5;
background: #f0f0f0;
}
.chosen {
opacity: 0.8;
}
.drag {
opacity: 0.6;
}
}
// 响应式设计
@media (max-width: 768px) {
.layers-list {
.child-layers {
padding-left: 25px;
&::after {
width: 25px;
}
.layer-item {
margin-left: 8px;
}
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,232 @@
// 右键菜单样式
.context-menu {
position: fixed;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 6px;
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
z-index: 1000;
max-width: 280px;
padding: 4px 0;
font-size: 14px;
// overflow: hidden;
top: 60px; // 默认位置,可根据实际需要调整
// 动画相关
&.context-menu-enter-active,
&.context-menu-leave-active {
transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1),opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
&.context-menu-enter-from {
opacity: 0;
transform: scale(0.9);
}
&.context-menu-leave-to {
opacity: 0;
transform: scale(0.95);
}
}
.context-menu-item {
display: flex;
align-items: center;
padding: 5px 12px;
cursor: pointer;
transition: all 0.2s;
color: rgba(0, 0, 0, 0.85);
white-space: nowrap;
min-height: 32px;
position: relative;
&:hover:not(.disabled) {
background-color: #f5f5f5;
}
&:active:not(.disabled) {
background-color: #e6e6e6;
}
&.disabled {
color: rgba(0, 0, 0, 0.25);
cursor: not-allowed;
.context-menu-icon {
color: rgba(0, 0, 0, 0.25);
}
}
&.danger {
color: #ff4d4f;
&:hover:not(.disabled) {
background-color: #fff2f0;
color: #ff7875;
}
.context-menu-icon {
color: #ff4d4f;
}
}
&.has-children {
position: relative;
}
&.hovered {
background-color: #f5f5f5;
}
}
.context-menu-icon {
display: flex;
align-items: center;
justify-content: center;
margin-right: 8px;
color: rgba(0, 0, 0, 0.65);
flex-shrink: 0;
}
.context-menu-label {
flex: 1;
line-height: 1.5;
}
.context-menu-shortcut {
margin-left: 16px;
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
.context-menu-arrow {
margin-left: 8px;
color: rgba(0, 0, 0, 0.45);
transition: transform 0.2s;
}
.context-menu-divider {
height: 1px;
margin: 4px 0;
background-color: rgba(0, 0, 0, 0.06);
}
// 子菜单样式
.context-submenu {
position: absolute;
left: 100%;
top: 0;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 6px;
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
z-index: 1001;
min-width: 160px;
max-width: 280px;
padding: 4px 0;
font-size: 14px;
// overflow: hidden;
&.submenu-left {
left: auto;
right: 100%;
margin-left: 0;
margin-right: 0;
}
}
// 子菜单动画
.context-submenu-enter-active,
.context-submenu-leave-active {
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
.context-submenu-enter-from {
opacity: 0;
transform: scale(0.9) translateY(-8px);
}
.context-submenu-leave-to {
opacity: 0;
transform: scale(0.95) translateY(-4px);
}
// 响应式优化
@media (max-width: 768px) {
.context-menu {
min-width: 140px;
font-size: 13px;
}
.context-menu-item {
min-height: 36px;
padding: 6px 10px;
}
.context-menu-shortcut {
display: none;
}
.context-submenu {
min-width: 140px;
font-size: 13px;
}
}
// 暗色主题支持
@media (prefers-color-scheme: dark) {
.context-menu {
background-color: #1f1f1f;
border-color: rgba(255, 255, 255, 0.08);
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.48),
0 3px 6px -4px rgba(0, 0, 0, 0.32),
0 9px 28px 8px rgba(0, 0, 0, 0.35);
}
.context-menu-item {
color: rgba(255, 255, 255, 0.85);
&:hover:not(.disabled) {
background-color: rgba(255, 255, 255, 0.08);
}
&:active:not(.disabled) {
background-color: rgba(255, 255, 255, 0.12);
}
&.disabled {
color: rgba(255, 255, 255, 0.3);
}
&.hovered {
background-color: rgba(255, 255, 255, 0.08);
}
}
.context-menu-icon {
color: rgba(255, 255, 255, 0.65);
}
.context-menu-shortcut {
color: rgba(255, 255, 255, 0.45);
}
.context-menu-arrow {
color: rgba(255, 255, 255, 0.45);
}
.context-menu-divider {
background-color: rgba(255, 255, 255, 0.12);
}
.context-submenu {
background-color: #1f1f1f;
border-color: rgba(255, 255, 255, 0.08);
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.48),
0 3px 6px -4px rgba(0, 0, 0, 0.32),
0 9px 28px 8px rgba(0, 0, 0, 0.35);
}
}

View File

@@ -0,0 +1,844 @@
// 文本省略样式
.text-ellipsis {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
// 主容器样式
.layers-panel-inner {
display: flex;
flex-direction: column;
user-select: none;
z-index: 6;
overflow-y: auto;
height: 100%;
max-height: 85vh;
-webkit-overflow-scrolling: touch;
}
// 头部样式
.layers-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-bottom: 1px solid #e0e0e0;
h3 {
margin: 0;
font-size: 16px;
font-weight: 500;
color: #333;
}
}
.layer-actions-group {
display: flex;
gap: 8px;
.normal-actions,
.multi-select-actions {
display: flex;
gap: 8px;
}
}
// 操作按钮样式
.action-btn {
width: 32px;
height: 32px;
background: none;
border: 1px solid #d9d9d9;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: #666;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background-color: #f0f0f0;
border-color: #40a9ff;
color: #40a9ff;
}
&:active {
background-color: #e6f7ff;
border-color: #1890ff;
color: #1890ff;
}
&[disabled] {
opacity: 0.5;
cursor: not-allowed;
background-color: #f5f5f5;
color: #bfbfbf;
border-color: #e6e6e6;
&:hover {
background-color: #f5f5f5;
color: #bfbfbf;
border-color: #e6e6e6;
}
}
}
// 特殊按钮样式
.group-btn {
background-color: #e6f7ff;
border-color: #91d5ff;
color: #1890ff;
&:hover {
background-color: #bae7ff;
border-color: #69c0ff;
}
&.disabled{
background-color: #f0f5ff;
border-color: #d9ecff;
color: #bfbfbf;
}
}
.ungroup-btn {
background-color: #fff2e8;
border-color: #ffbb96;
color: #fa8c16;
&:hover {
background-color: #ffd8bf;
border-color: #ff9c6e;
color: #fa8c16;
}
&.disabled{
background-color: #f0f5ff;
border-color: #d9ecff;
color: #bfbfbf;
}
}
.delete-selected-btn {
background-color: #fff2f0;
border-color: #ffccc7;
color: #ff4d4f;
&:hover {
background-color: #ffebe6;
border-color: #ff7875;
color: #ff4d4f;
}
}
.clear-selection-btn {
background-color: #f6f6f6;
border-color: #d9d9d9;
color: #595959;
&:hover {
background-color: #e6e6e6;
border-color: #bfbfbf;
color: #595959;
}
}
// 多选信息提示
.multi-select-info {
padding: 10px 6px;
// background-color: #e6f7ff;
background-color: rgba(238, 238, 238,0.4);
border-bottom: 1px solid #91d5ff;
color: #333;
font-size: 13px;
line-height: 1.4;
small {
display: block;
margin-top: 4px;
color: #666;
font-size: 11px;
}
}
// 图层列表
.layers-list {
position: relative;
flex: 1;
overflow-y: auto;
}
// 图层项样式
.layer-item {
position: relative;
width: 100%;
display: flex;
flex-direction: column;
padding: 10px 0;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
border-left: 0;
border-right: 0;
border: none;
border-bottom: 1px solid #f5f2f2;
padding-left: 30px;
padding-right: 10px;
&.group-layer {
background-color: rgba(240, 248, 255, 0.3);
border-color: #e6f7ff;
}
&:hover {
background-color: #f0f0f0;
border-color: #e0e0e0;
}
&.active {
background-color: #e6f7ff;
border-color: #91d5ff;
}
&.selected {
background-color: #bae7ff;
border-color: #91d5ff;
// box-shadow: 0 0 0 1px #1890ff;
}
&.editing {
background-color: #fff7e6;
border-color: #ffd666;
}
// &.multi-select-mode {
// // padding-left: 30px;
// }
&.disabled {
opacity: 0.6;
cursor: not-allowed;
&:hover {
background-color: transparent;
}
}
}
// 图层头部
.layer-header {
display: flex;
align-items: center;
width: 100%;
gap: 6px;
}
// 图层预览
.layer-review {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
flex: none;
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% /
10px 10px;
border: 1px solid #e0e0e0;
}
.layer-thumbnail {
width: 100%;
height: 100%;
object-fit: contain;
}
.layer-type-icon {
font-size: 14px;
}
// 可见性按钮
.visibility-btn {
width: 22px;
height: 22px;
cursor: pointer;
flex: none;
color: #333;
display: flex;
align-items: center;
justify-content: center;
&.hidden {
color: #999;
}
}
// 图层名称
.layer-name-container {
flex: 1;
margin: 0 6px;
overflow: hidden;
// max-width: 204px;
.layer-name-wrapper{
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
}
.layer-name {
color: #333;
font-size: 14px;
text-align: left;
display: block;
width: 100%;
}
.layer-name-input {
width: 100%;
padding: 2px 4px;
border: 1px solid #1890ff;
border-radius: 3px;
font-size: 14px;
color: #333;
background-color: #fff;
outline: none;
box-sizing: border-box;
&:focus {
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
// 图层状态
.layer-status {
display: flex;
align-items: center;
gap: 4px;
}
.status-icon {
font-size: 12px;
&.locked {
color: #1890ff;
}
&.group {
color: #fa8c16;
}
&.disabled {
color: #ccc;
cursor: not-allowed;
}
}
// 图层操作
.layer-actions {
display: flex;
gap: 6px;
}
.delete-btn {
font-size: 16px;
cursor: pointer;
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
color: #ff4d4f;
}
&.disabled{
color: #ccc;
cursor: not-allowed;
// pointer-events: none;
}
}
// 拖拽手柄
.layer-drag-handle {
position: absolute;
top: 0;
left: 0;
width: 20px;
height: 100%;
cursor: move;
flex: none;
display: flex;
align-items: center;
justify-content: center;
color: #999;
margin-right: 4px;
background: #eee;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
&:hover {
color: #333;
}
}
// 复选框
.layer-checkbox {
width: 18px;
height: 18px;
margin-right: 0;
cursor: pointer;
flex: none;
display: flex;
align-items: center;
// input[type="checkbox"] {
// width: 16px;
// height: 16px;
// cursor: pointer;
// accent-color: #1890ff;
// }
}
// 子图层样式
.child-layers {
}
.child-layer {
padding: 8px 20px 8px 32px;
background-color: rgba(240, 240, 240, 0.3);
border-left: 2px solid #e0e0e0;
&:hover {
background-color: rgba(224, 224, 224, 0.5);
}
&.active {
background-color: rgba(230, 247, 255, 0.5);
border-left: 2px solid #1890ff;
}
&.editing {
background-color: rgba(255, 247, 230, 0.5);
border-left: 2px solid #ffd666;
}
.layer-actions {
position: static;
display: flex;
gap: 4px;
button {
width: 20px;
height: 20px;
border: none;
background: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #999;
border-radius: 2px;
transition: all 0.2s;
&:hover {
background-color: #f0f0f0;
color: #333;
}
}
}
}
.layer-indent {
width: 20px;
flex: none;
}
.layer-info {
flex: 1;
margin: 0 8px;
.layer-name {
font-size: 13px;
color: #333;
margin-bottom: 2px;
}
.layer-status {
display: flex;
align-items: center;
gap: 4px;
}
.layer-type {
font-size: 11px;
color: #999;
}
}
.child-drag-handle {
width: 16px;
height: 16px;
cursor: move;
flex: none;
display: flex;
align-items: center;
justify-content: center;
color: #999;
margin-right: 4px;
&:hover {
color: #333;
}
}
// 固定图层样式
.fixed-layers {
border-top: 1px solid #e0e0e0;
// background-color: #fafafa;
background-color: rgba(238, 238, 238,0.4);
.layer-drag-handle{
cursor: default;
}
}
.fixed-layer {
background-color: #fafafa;
// border-left: 3px solid #1890ff;
// &:hover {
// background-color: #e6f7ff;
// }
}
.fixed-layer-indicator {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
color: #1890ff;
margin-right: 4px;
}
.background-indicator,
.fixed-indicator {
display: flex;
align-items: center;
justify-content: center;
margin-left: 4px;
}
.background-icon,
.fixed-icon {
font-size: 14px;
color: #999;
}
// 拖拽样式
.sortable-layers {
width: 100%;
}
.ghost {
opacity: 0.5;
background-color: #f0f0f0;
border: 2px dashed #1890ff;
}
.chosen {
background-color: #e6f7ff;
border: 1px solid #1890ff;
}
.drag {
opacity: 0.8;
transform: rotate(5deg);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
// 子图层拖拽样式
.child-layers {
.ghost {
opacity: 0.4;
background-color: #fff7e6;
border: 2px dashed #faad14;
}
.chosen {
background-color: #fff7e6;
border: 1px solid #faad14;
}
.drag {
opacity: 0.7;
transform: rotate(3deg);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
}
}
// 响应式设计
@media (max-width: 768px) {
.layers-panel-inner {
max-height: 70vh;
}
.layer-item {
padding: 12px;
padding-left: 35px;
}
.layer-header {
min-height: 40px;
}
.layer-drag-handle,
.visibility-btn {
width: 24px;
}
.layer-review {
width: 36px;
height: 36px;
}
.action-btn {
width: 36px;
height: 36px;
}
.multi-select-info {
// padding: 12px;
small {
font-size: 11px;
}
}
// .layer-name-container {
// // max-width: 182px;
// }
}
// iPad 优化
@media (min-width: 768px) and (max-width: 1024px) {
.layer-item {
padding: 10px;
padding-left: 30px;
}
.layer-drag-handle:hover,
.visibility-btn:hover {
background-color: rgba(0, 0, 0, 0.04);
border-radius: 4px;
}
}
// 触摸设备优化
@media (hover: none) and (pointer: coarse) {
.layer-item {
padding-left: 30px;
&:hover {
background-color: transparent;
}
&:active {
background-color: #f0f0f0;
}
}
.action-btn {
&:hover {
background-color: transparent;
border-color: #d9d9d9;
color: #666;
}
&:active {
background-color: #e6f7ff;
border-color: #1890ff;
color: #1890ff;
}
}
}
// 组图层展开/收起图标样式
.group-expand-icon {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
cursor: pointer;
border-radius: 2px;
transition: all 0.2s ease;
margin-right: 4px;
// &:hover {
// background-color: rgba(0, 0, 0, 0.1);
// }
// 展开/收起图标的过渡动画
.svg-icon {
transition: transform 0.2s ease;
}
}
// 组图层样式
.group-layer {
.layer-type-icon {
font-size: 14px;
}
}
// 子图层缩进和连接线
.child-layers {
position: relative;
&::before {
content: '';
position: absolute;
left: 8px;
top: 0;
bottom: 0;
width: 1px;
background-color: #e0e0e0;
}
}
.layer-indent {
width: 16px;
position: relative;
&::after {
content: '';
position: absolute;
left: 8px;
top: 50%;
width: 8px;
height: 1px;
background-color: #e0e0e0;
}
}
// 子图层展开/收起动画样式
.child-layers-expand-enter-active,
.child-layers-expand-leave-active {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
.child-layers-expand-enter-from {
height: 0;
opacity: 0;
padding-top: 0;
padding-bottom: 0;
margin-top: 0;
margin-bottom: 0;
}
.child-layers-expand-leave-to {
height: 0;
opacity: 0;
padding-top: 0;
padding-bottom: 0;
margin-top: 0;
margin-bottom: 0;
}
// 展开图标旋转动画优化
.group-expand-icon {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
cursor: pointer;
border-radius: 2px;
transition: all 0.2s ease;
margin-right: 4px;
&:hover {
background-color: rgba(0, 0, 0, 0.1);
transform: scale(1.1);
}
&:active {
transform: scale(0.95);
}
// 展开/收起图标的过渡动画
.svg-icon {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
}
// 子图层展开时的额外样式
.child-layers {
position: relative;
background: linear-gradient(135deg, rgba(240, 248, 255, 0.1) 0%, rgba(240, 248, 255, 0.05) 100%);
// border-radius: 4px;
// margin-top: 2px;
&::before {
content: '';
position: absolute;
left: 8px;
top: 0;
bottom: 0;
width: 1px;
background: linear-gradient(to bottom, #e0e0e0 0%, rgba(224, 224, 224, 0.3) 100%);
}
// 子图层项动画
.layer-item {
animation: slideInRight 0.2s ease-out;
animation-fill-mode: both;
&:nth-child(1) { animation-delay: 0.05s; }
&:nth-child(2) { animation-delay: 0.1s; }
&:nth-child(3) { animation-delay: 0.15s; }
&:nth-child(4) { animation-delay: 0.2s; }
&:nth-child(5) { animation-delay: 0.25s; }
}
}
// 子图层项进入动画
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
// 移动端动画优化
@media (hover: none) and (pointer: coarse) {
.child-layers-expand-enter-active,
.child-layers-expand-leave-active {
transition: all 0.25s ease;
}
.group-expand-icon {
&:hover {
transform: none;
background-color: transparent;
}
&:active {
transform: scale(0.9);
background-color: rgba(0, 0, 0, 0.1);
}
}
.child-layers .layer-item {
animation-duration: 0.15s;
}
}