This commit is contained in:
1 parent
93eac7e146
commit
8dfe2624ab
1 file changed
+94
-3
@@ -1,12 +1,80 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { tick } from 'svelte';
|
||||||
import type { EditorOutline } from '$lib/editor/outline.svelte';
|
import type { EditorOutline } from '$lib/editor/outline.svelte';
|
||||||
|
|
||||||
let { outline, onselect }: { outline: EditorOutline; onselect?: () => void } = $props();
|
let { outline, onselect }: { outline: EditorOutline; onselect?: () => void } = $props();
|
||||||
let baseLevel = $derived(Math.min(...outline.headings.map((heading) => heading.level), 6));
|
let baseLevel = $derived(Math.min(...outline.headings.map((heading) => heading.level), 6));
|
||||||
|
|
||||||
|
let listEl: HTMLUListElement | undefined = $state();
|
||||||
|
let enableAnimation = $state(false);
|
||||||
|
let indicatorStyle = $state({ top: 0, height: 0, visible: false });
|
||||||
|
|
||||||
|
function updateIndicator(withAnimation = true) {
|
||||||
|
if (!listEl || outline.active === undefined || outline.active === null || outline.active < 0) {
|
||||||
|
indicatorStyle = { top: 0, height: 0, visible: false };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttons = listEl.querySelectorAll<HTMLButtonElement>('button');
|
||||||
|
const activeBtn = buttons[outline.active];
|
||||||
|
|
||||||
|
if (!activeBtn) {
|
||||||
|
indicatorStyle = { top: 0, height: 0, visible: false };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!withAnimation) {
|
||||||
|
enableAnimation = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
indicatorStyle = {
|
||||||
|
top: activeBtn.offsetTop,
|
||||||
|
height: activeBtn.offsetHeight,
|
||||||
|
visible: true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!withAnimation) {
|
||||||
|
// 在下一帧恢复动画能力,避免后续切换时丢失平滑过渡
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
enableAnimation = true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
enableAnimation = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
// 监听大纲激活索引与标题列表变化
|
||||||
|
void outline.active;
|
||||||
|
void outline.headings;
|
||||||
|
|
||||||
|
tick().then(() => {
|
||||||
|
// 初次挂载静默定位,后续章节变动启用平滑动效
|
||||||
|
updateIndicator(enableAnimation);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!listEl) return;
|
||||||
|
// 监听容器尺寸变化(如侧边栏宽度改变或窗口缩放),静默重校准指示块
|
||||||
|
const ro = new ResizeObserver(() => {
|
||||||
|
updateIndicator(false);
|
||||||
|
});
|
||||||
|
ro.observe(listEl);
|
||||||
|
return () => ro.disconnect();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav aria-label="正文大纲" class="article-outline">
|
<nav aria-label="正文大纲" class="article-outline">
|
||||||
{#if outline.headings.length}
|
{#if outline.headings.length}
|
||||||
<ul>
|
<ul bind:this={listEl}>
|
||||||
|
<div
|
||||||
|
class="active-indicator"
|
||||||
|
class:visible={indicatorStyle.visible}
|
||||||
|
class:animate={enableAnimation}
|
||||||
|
style:transform="translateY({indicatorStyle.top}px)"
|
||||||
|
style:height="{indicatorStyle.height}px"
|
||||||
|
></div>
|
||||||
{#each outline.headings as heading, index (heading.from)}
|
{#each outline.headings as heading, index (heading.from)}
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
@@ -41,6 +109,7 @@
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
ul {
|
ul {
|
||||||
|
position: relative;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -49,6 +118,8 @@
|
|||||||
margin: 0 0 4px;
|
margin: 0 0 4px;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -60,19 +131,39 @@
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: color 0.2s ease;
|
||||||
}
|
}
|
||||||
button:hover {
|
button:hover {
|
||||||
background: rgba(65, 155, 56, 0.06);
|
background: rgba(65, 155, 56, 0.06);
|
||||||
}
|
}
|
||||||
button.active {
|
button.active {
|
||||||
border-left-color: var(--accent-color, #419b38);
|
|
||||||
color: var(--accent-color, #419b38);
|
color: var(--accent-color, #419b38);
|
||||||
background: rgba(65, 155, 56, 0.1);
|
|
||||||
}
|
}
|
||||||
button:focus-visible {
|
button:focus-visible {
|
||||||
outline: 2px solid var(--accent-color, #419b38);
|
outline: 2px solid var(--accent-color, #419b38);
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
|
.active-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
border-left: 2px solid var(--accent-color, #419b38);
|
||||||
|
background: rgba(65, 155, 56, 0.1);
|
||||||
|
opacity: 0;
|
||||||
|
will-change: transform, height;
|
||||||
|
}
|
||||||
|
.active-indicator.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.active-indicator.animate {
|
||||||
|
transition:
|
||||||
|
transform 240ms cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
|
height 240ms cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
|
opacity 160ms ease;
|
||||||
|
}
|
||||||
span {
|
span {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
|
|||||||
Reference in new issue
Block a user