mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-05-08 09:38:53 +00:00
feat(editor): 面包屑超出父容器 80% 时折叠中间项并对单项打点
- 路径过长时仅保留首项 + ... + 末两项,避免横向溢出工作区 - 单项设置 max-width 并通过内部 span 显示省略号,悬浮 tooltip 展示完整名称 - 通过 ResizeObserver 监听父容器宽度变化实时重测 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
0724c76689
commit
7b870e5908
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user