531 lines
13 KiB
Vue
531 lines
13 KiB
Vue
<script setup>
|
||
import { ref, nextTick, computed, inject } from "vue";
|
||
import { Checkbox } from "ant-design-vue";
|
||
import { VueDraggable } from "vue-draggable-plus";
|
||
import { isGroupLayer, SpecialLayerId } from "../../utils/layerHelper";
|
||
import { fillToCssStyle, palletToFill, fillToPallet } from "../../utils/helper";
|
||
import { SetColorLayerFillCommand } from "../../commands/LayerCommands";
|
||
import { useI18n } from 'vue-i18n'
|
||
const {t} = useI18n()
|
||
// 设置组件名称,用于递归渲染
|
||
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.layer.isDisableUnlock) return;
|
||
|
||
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;
|
||
}
|
||
const canvasManager = inject('canvasManager');
|
||
const layerObject = computed(() => {
|
||
const layer = props.layer;
|
||
const id = layer.fabricObject?.id || layer.fabricObjects?.[0]?.id || layer.id;
|
||
return canvasManager.getLayerObjectById(id);
|
||
});
|
||
const palletPanel = inject("palletPanel");
|
||
const clickColor = () => {
|
||
const fill = layerObject.value.fill;
|
||
if (fill) {
|
||
const obj = fillToPallet(fill);
|
||
palletPanel(obj).then((res) => {
|
||
const cmd = new SetColorLayerFillCommand({
|
||
canvas: canvasManager.canvas,
|
||
layerManager: layerManager,
|
||
object: layerObject.value,
|
||
newFill: palletToFill(res),
|
||
originColor: res,
|
||
});
|
||
layerManager.commandManager.execute(cmd);
|
||
});
|
||
}
|
||
}
|
||
</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,
|
||
},
|
||
]"
|
||
:data-layer-id="layer.id"
|
||
@click="handleClick"
|
||
@dblclick="handleDoubleClick"
|
||
@touchstart="handleTouchStart"
|
||
@touchmove="handleTouchMove"
|
||
@touchend="handleTouchEnd"
|
||
@contextmenu.prevent="handleContextMenu"
|
||
>
|
||
<!-- 拖拽手柄 -->
|
||
<div class="layer-drag-handle" :title="$t('拖拽排序')" v-if="!isHidenDragHandle">
|
||
<SvgIcon :name="isChild ? 'CSort' : 'CSort'" :size="32"></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="layer.thumbnailUrl"
|
||
:src="layer.thumbnailUrl"
|
||
class="layer-thumbnail"
|
||
:alt="$t('Canvas.preview')"
|
||
/>
|
||
</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-color-btn"
|
||
v-if="layer.id === SpecialLayerId.COLOR"
|
||
@click.stop="clickColor"
|
||
:style="{
|
||
background: fillToCssStyle(layerObject.fill),
|
||
}"
|
||
></div>
|
||
|
||
<!-- 图层操作按钮 -->
|
||
<div class="layer-actions" >
|
||
<!-- 可见性切换 -->
|
||
<div
|
||
class="visibility-btn"
|
||
@click.stop="handleToggleVisibility"
|
||
:title="$t('Canvas.showHiddenLayer')"
|
||
>
|
||
<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 || layer.isDisableUnlock || layer.isFixedOther }"
|
||
: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('Canvas.deleteLayer')"
|
||
@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('Canvas.CollapseUp') : $t('Canvas.CollapseDown')"
|
||
>
|
||
<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>
|