feat(editor): 面包屑超出父容器 80% 时折叠中间项并对单项打点

- 路径过长时仅保留首项 + ... + 末两项,避免横向溢出工作区
- 单项设置 max-width 并通过内部 span 显示省略号,悬浮 tooltip 展示完整名称
- 通过 ResizeObserver 监听父容器宽度变化实时重测

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
roymondchen 2026-05-07 19:20:46 +08:00
parent 0724c76689
commit 7b870e5908
2 changed files with 111 additions and 6 deletions

View File

@ -1,17 +1,22 @@
<template>
<div v-if="nodes.length === 1" class="m-editor-breadcrumb">
<template v-for="(item, index) in path" :key="item.id">
<TMagicButton link :disabled="item.id === node?.id" @click="select(item)">{{ item.name }}</TMagicButton
><span v-if="index < path.length - 1">/</span>
<div v-if="nodes.length === 1" ref="containerRef" class="m-editor-breadcrumb">
<template v-for="(item, index) in displayPath" :key="item.isEllipsis ? `ellipsis-${index}` : item.id">
<span v-if="item.isEllipsis" class="m-editor-breadcrumb-ellipsis">...</span>
<TMagicTooltip v-else :content="item.name" placement="top" :show-after="500">
<TMagicButton class="m-editor-breadcrumb-item" link :disabled="item.id === node?.id" @click="select(item)">{{
item.name
}}</TMagicButton>
</TMagicTooltip>
<span v-if="index < displayPath.length - 1" class="m-editor-breadcrumb-separator">/</span>
</template>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import type { MNode } from '@tmagic/core';
import { TMagicButton } from '@tmagic/design';
import { TMagicButton, TMagicTooltip } from '@tmagic/design';
import { getNodePath } from '@tmagic/utils';
import { useServices } from '@editor/hooks/use-services';
@ -20,6 +25,8 @@ defineOptions({
name: 'MEditorBreadcrumb',
});
type DisplayItem = (MNode & { isEllipsis?: false }) | { isEllipsis: true; id: string; name: string };
const { editorService } = useServices();
const node = computed(() => editorService.get('node'));
@ -27,6 +34,80 @@ const nodes = computed(() => editorService.get('nodes'));
const root = computed(() => editorService.get('root'));
const path = computed(() => getNodePath(node.value?.id || '', root.value?.items || []));
const containerRef = ref<HTMLElement | null>(null);
//
const COLLAPSE_RATIO = 0.8;
const collapsed = ref(false);
const displayPath = computed<DisplayItem[]>(() => {
const list = path.value;
// first + ... + last2 = 4 > 3
if (!collapsed.value || list.length <= 3) {
return list as DisplayItem[];
}
return [
list[0],
{ isEllipsis: true, id: '__ellipsis__', name: '...' },
list[list.length - 2],
list[list.length - 1],
] as DisplayItem[];
});
const measureOverflow = async () => {
//
if (collapsed.value) {
collapsed.value = false;
await nextTick();
}
const el = containerRef.value;
const parent = el?.parentElement;
if (!el || !parent) return;
// scrollWidth max-width
const contentWidth = el.scrollWidth;
const parentWidth = parent.clientWidth;
if (parentWidth <= 0) return;
collapsed.value = contentWidth > parentWidth * COLLAPSE_RATIO;
};
let resizeObserver: ResizeObserver | null = null;
const observe = () => {
resizeObserver?.disconnect();
const el = containerRef.value;
if (!el || typeof ResizeObserver === 'undefined') return;
resizeObserver = new ResizeObserver(() => {
measureOverflow();
});
resizeObserver.observe(el);
if (el.parentElement) {
resizeObserver.observe(el.parentElement);
}
};
onMounted(() => {
observe();
measureOverflow();
});
onBeforeUnmount(() => {
resizeObserver?.disconnect();
resizeObserver = null;
});
watch(
() => nodes.value.length,
async () => {
await nextTick();
observe();
measureOverflow();
},
);
watch(path, async () => {
await nextTick();
measureOverflow();
});
const select = async (node: MNode) => {
await editorService.select(node);
editorService.get('stage')?.select(node.id);

View File

@ -3,4 +3,28 @@
left: 5px;
top: 5px;
z-index: 10;
display: flex;
align-items: center;
white-space: nowrap;
.m-editor-breadcrumb-item {
max-width: 100px;
min-width: 0;
overflow: hidden;
// Element Plus button 内部文本节点
> span {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.m-editor-breadcrumb-separator,
.m-editor-breadcrumb-ellipsis {
margin: 0 4px;
flex-shrink: 0;
}
}