295 lines
7.7 KiB
Svelte
295 lines
7.7 KiB
Svelte
<script lang="ts">
|
|
import { page } from '$app/stores';
|
|
import { resolve } from '$app/paths';
|
|
import type { PageData } from './$types';
|
|
import { onMount, tick } from 'svelte';
|
|
import tocbot from 'tocbot';
|
|
import MarkdownRenderer from '$lib/components/MarkdownRenderer.svelte';
|
|
|
|
let sidebarShow = $state(true);
|
|
let hasToc = $state(false);
|
|
|
|
let { data }: { data: PageData } = $props();
|
|
|
|
let isSeriesOpen = $state(true);
|
|
let seriesContentEl: HTMLElement | null = $state(null);
|
|
|
|
function toggleSeries(e: MouseEvent) {
|
|
const target = e.target as HTMLElement;
|
|
if (target.closest('a')) return;
|
|
|
|
const contentEl = seriesContentEl;
|
|
if (!contentEl) {
|
|
isSeriesOpen = !isSeriesOpen;
|
|
return;
|
|
}
|
|
|
|
if (isSeriesOpen) {
|
|
if (contentEl.style.maxHeight === 'none' || contentEl.style.maxHeight === '') {
|
|
contentEl.style.maxHeight = contentEl.scrollHeight + 'px';
|
|
void contentEl.offsetHeight;
|
|
}
|
|
contentEl.style.maxHeight = '0px';
|
|
isSeriesOpen = false;
|
|
} else {
|
|
isSeriesOpen = true;
|
|
contentEl.style.maxHeight = contentEl.scrollHeight + 'px';
|
|
|
|
const transitionEndHandler = (ev: Event) => {
|
|
if (ev.target === contentEl && isSeriesOpen) {
|
|
contentEl.style.maxHeight = 'none';
|
|
}
|
|
contentEl.removeEventListener('transitionend', transitionEndHandler);
|
|
};
|
|
contentEl.addEventListener('transitionend', transitionEndHandler);
|
|
}
|
|
}
|
|
|
|
onMount(() => {
|
|
const headers = document.querySelectorAll(
|
|
'.js-toc-content h1, .js-toc-content h2, .js-toc-content h3, .js-toc-content h4, .js-toc-content h5, .js-toc-content h6'
|
|
);
|
|
if (headers.length > 0) {
|
|
hasToc = true;
|
|
tick().then(() => {
|
|
tocbot.init({
|
|
tocSelector: '.js-toc',
|
|
contentSelector: '.js-toc-content',
|
|
headingSelector: 'h1, h2, h3, h4, h5',
|
|
hasInnerContainers: false,
|
|
linkClass: 'toc-link',
|
|
activeLinkClass: 'is-active-link',
|
|
listClass: 'toc-list',
|
|
isCollapsedClass: 'is-collapsed',
|
|
collapsibleClass: 'is-collapsible',
|
|
listItemClass: 'toc-list-item',
|
|
collapseDepth: 0,
|
|
scrollSmooth: true,
|
|
headingsOffset: 20
|
|
});
|
|
|
|
if (window.innerWidth < 1200) {
|
|
sidebarShow = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
$effect(() => {
|
|
if (hasToc && sidebarShow) {
|
|
document.body.classList.add('sidebar-show');
|
|
} else {
|
|
document.body.classList.remove('sidebar-show');
|
|
}
|
|
|
|
return () => {
|
|
document.body.classList.remove('sidebar-show');
|
|
};
|
|
});
|
|
|
|
if (seriesContentEl && isSeriesOpen) {
|
|
seriesContentEl.style.maxHeight = 'none';
|
|
}
|
|
|
|
return () => {
|
|
if (hasToc) tocbot.destroy();
|
|
};
|
|
});
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>{data.post.title} - {$page.data.site?.title || 'BlogFlare'}</title>
|
|
</svelte:head>
|
|
|
|
<main id="pjax-container">
|
|
<div class="wrapper container narrow contents-wrap">
|
|
<article class="article-detail yue">
|
|
<div class="banner-title">
|
|
<h1 class="post-title">{data.post.title}</h1>
|
|
<p class="post-meta">
|
|
<span><a href={resolve('/')}>{data.post.author}</a></span> •
|
|
<time>{data.post.date}</time>
|
|
</p>
|
|
</div>
|
|
{#if data.post.cover}
|
|
<div class="post-cover detail-cover">
|
|
<img src={data.post.cover} alt={data.post.title} />
|
|
</div>
|
|
{/if}
|
|
|
|
{#if data.series && data.seriesPosts.length > 0}
|
|
<div class="details series-nav" class:open={isSeriesOpen}>
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div
|
|
class="details-summary series-title"
|
|
id="series-navigation-title"
|
|
onclick={toggleSeries}
|
|
>
|
|
<span
|
|
>{$page.data.aliases?.series || '系列'} -
|
|
<a href={resolve('/series/[slug]', { slug: data.series.slug })}>{data.series.name}</a
|
|
></span
|
|
>
|
|
</div>
|
|
<div bind:this={seriesContentEl} class="details-content series-content">
|
|
<nav>
|
|
<ul>
|
|
{#each data.seriesPosts as seriesPost (seriesPost.id)}
|
|
<li class:current={seriesPost.id === data.post.id}>
|
|
{#if seriesPost.id === data.post.id}
|
|
<span class="active" aria-current="page">{seriesPost.title}</span>
|
|
{:else}
|
|
<a href={resolve('/posts/[slug]', { slug: seriesPost.slug })}
|
|
>{seriesPost.title}</a
|
|
>
|
|
{/if}
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
<MarkdownRenderer content={data.post.content} />
|
|
|
|
{#if data.tags && data.tags.length > 0}
|
|
<section class="tags">
|
|
{#each data.tags as tag (tag.id)}
|
|
<a href={resolve(`/tags/${encodeURIComponent(tag.name)}`)} rel="tag" class="tag-item"
|
|
>{tag.name}</a
|
|
>
|
|
{/each}
|
|
</section>
|
|
{/if}
|
|
</article>
|
|
</div>
|
|
|
|
{#if hasToc}
|
|
<!-- TOC Overlay and Sidebar -->
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div class="toc-mask" onclick={() => (sidebarShow = false)}></div>
|
|
<div class="TOC" aria-label="{$page.data.aliases?.post || '文章'}目录">
|
|
<div class="js-toc"></div>
|
|
</div>
|
|
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div id="toggle-toc" class="ctrler-item" onclick={() => (sidebarShow = !sidebarShow)}>
|
|
<svg
|
|
aria-hidden="true"
|
|
focusable="false"
|
|
role="img"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 448 512"
|
|
width="16"
|
|
height="16"
|
|
><path
|
|
fill="currentColor"
|
|
d="M257.5 445.1l-22.2 22.2c-9.4 9.4-24.6 9.4-33.9 0L27 273c-9.4-9.4-9.4-24.6 0-33.9L201.4 44.7c9.4-9.4 24.6-9.4 33.9 0l22.2 22.2c9.5 9.5 9.3 25-.4 34.3L136.6 216H424c13.3 0 24 10.7 24 24v32c0 13.3-10.7 24-24 24H136.6l120.5 120.8c9.8 9.3 10 24.8.4 34.3z"
|
|
></path></svg
|
|
>
|
|
</div>
|
|
{/if}
|
|
</main>
|
|
|
|
<style>
|
|
/* ===== 系列导航 - 完全对齐 DoIt _series.scss + _details.scss ===== */
|
|
.series-nav {
|
|
margin: 0.8rem 0;
|
|
}
|
|
|
|
/* .series-title — 高度、字号及箭头与 admonition-title 完全一致 */
|
|
.series-nav .series-title {
|
|
position: relative;
|
|
font-size: 1rem;
|
|
font-weight: bold;
|
|
display: flex;
|
|
align-items: center;
|
|
line-height: 1.6;
|
|
padding: 0.25rem 2rem 0.25rem 0.75rem;
|
|
background: #e6e6e6;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
/* 展开收缩箭头 — 与 admonition 完全一致 */
|
|
.series-nav .series-title::after {
|
|
content: '';
|
|
position: absolute;
|
|
right: 0.75rem;
|
|
top: 0.85rem;
|
|
width: 0.4rem;
|
|
height: 0.4rem;
|
|
border-right: 2px solid currentColor;
|
|
border-bottom: 2px solid currentColor;
|
|
transform: rotate(-45deg);
|
|
transition: transform 0.2s ease-in-out;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.series-nav.open .series-title::after {
|
|
transform: rotate(45deg);
|
|
top: 0.7rem;
|
|
}
|
|
|
|
/* .series-content — 展开收缩动画,与 admonition 完全一致 */
|
|
.series-nav .details-content {
|
|
font-size: 0.875rem;
|
|
background-color: #f5f5f5;
|
|
max-height: 0;
|
|
overflow: hidden;
|
|
transition: max-height 0.3s ease-out;
|
|
}
|
|
|
|
.series-nav .details-content > nav > ul {
|
|
margin: 0;
|
|
padding: 0.4rem 1rem 0.4rem 1.8rem;
|
|
}
|
|
|
|
.series-nav .details-content ul {
|
|
text-indent: -0.85rem;
|
|
padding-left: 0.8rem;
|
|
list-style: none;
|
|
}
|
|
|
|
/* "|" 竖线 — 对应 DoIt ul a:first-child::before */
|
|
.series-nav li a:first-child::before {
|
|
content: '|';
|
|
font-weight: bold;
|
|
margin-right: 0.5rem;
|
|
color: #2d809a;
|
|
}
|
|
|
|
/* 当前文章高亮 — 对应 DoIt span.active */
|
|
.series-nav li span.active {
|
|
font-weight: bold;
|
|
color: #2d809a;
|
|
}
|
|
|
|
.series-nav li span.active::before {
|
|
content: '|';
|
|
margin-right: 0.5rem;
|
|
color: #ef3982;
|
|
}
|
|
|
|
/* 链接 hover */
|
|
.series-nav .series-title a {
|
|
color: var(--text-color);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.series-nav .series-title a:hover {
|
|
color: var(--accent-color, #419b38);
|
|
}
|
|
|
|
.series-nav li a {
|
|
color: var(--text-color);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.series-nav li a:hover {
|
|
color: var(--accent-color, #419b38);
|
|
}
|
|
</style>
|