|
|
|
|
@@ -15,6 +15,8 @@ const { data: siteSettings } = await useFetch('/api/site-settings', {
|
|
|
|
|
logoText: '井',
|
|
|
|
|
logoUrl: '',
|
|
|
|
|
socialLinks: [],
|
|
|
|
|
adSidebarCode: '',
|
|
|
|
|
adPostSidebarCode: '',
|
|
|
|
|
copyrightText: '©2026 sori.studio'
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
@@ -39,7 +41,7 @@ const recommendedSites = computed(() => {
|
|
|
|
|
return list.filter((x) => x?.isVisible !== false)
|
|
|
|
|
})
|
|
|
|
|
const isPostDetailRoute = computed(() => route.path.startsWith('/post/'))
|
|
|
|
|
const sidebarAdCode = computed(() => isPostDetailRoute.value ? '' : siteSettings.value?.adSidebarCode)
|
|
|
|
|
const sidebarAdCode = computed(() => isPostDetailRoute.value ? siteSettings.value?.adPostSidebarCode : siteSettings.value?.adSidebarCode)
|
|
|
|
|
const postTocItems = computed(() => Array.isArray(postToc.value) ? postToc.value : [])
|
|
|
|
|
const followLinks = computed(() => getVisibleSocialLinks(siteSettings.value?.socialLinks || []))
|
|
|
|
|
|
|
|
|
|
@@ -100,20 +102,23 @@ const scrollActiveTocIntoView = (id) => {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nav = tocNavRef.value
|
|
|
|
|
const scrollContainer = nav.closest('.site-sidebar-scroll')
|
|
|
|
|
const link = nav.querySelector(`[data-toc-id="${id}"]`)
|
|
|
|
|
|
|
|
|
|
if (!(link instanceof HTMLElement)) {
|
|
|
|
|
if (!(link instanceof HTMLElement) || !(scrollContainer instanceof HTMLElement)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const navTop = nav.scrollTop
|
|
|
|
|
const navBottom = navTop + nav.clientHeight
|
|
|
|
|
const linkTop = link.offsetTop
|
|
|
|
|
const containerRect = scrollContainer.getBoundingClientRect()
|
|
|
|
|
const linkRect = link.getBoundingClientRect()
|
|
|
|
|
const navTop = scrollContainer.scrollTop
|
|
|
|
|
const navBottom = navTop + scrollContainer.clientHeight
|
|
|
|
|
const linkTop = navTop + linkRect.top - containerRect.top
|
|
|
|
|
const linkBottom = linkTop + link.offsetHeight
|
|
|
|
|
const buffer = 24
|
|
|
|
|
|
|
|
|
|
if (linkTop < navTop + buffer) {
|
|
|
|
|
nav.scrollTo({
|
|
|
|
|
scrollContainer.scrollTo({
|
|
|
|
|
top: Math.max(0, linkTop - buffer),
|
|
|
|
|
behavior: 'smooth'
|
|
|
|
|
})
|
|
|
|
|
@@ -121,8 +126,8 @@ const scrollActiveTocIntoView = (id) => {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (linkBottom > navBottom - buffer) {
|
|
|
|
|
nav.scrollTo({
|
|
|
|
|
top: linkBottom - nav.clientHeight + buffer,
|
|
|
|
|
scrollContainer.scrollTo({
|
|
|
|
|
top: linkBottom - scrollContainer.clientHeight + buffer,
|
|
|
|
|
behavior: 'smooth'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
@@ -240,7 +245,7 @@ watch([postTocItems, () => route.fullPath], async () => {
|
|
|
|
|
<template>
|
|
|
|
|
<aside class="right-sidebar site-sidebar flex w-full flex-col overflow-hidden border-[var(--site-line)] max-lg:border-l-0 max-lg:border-t max-lg:px-4 max-lg:pb-10 lg:sticky lg:top-[57px] lg:z-10 lg:h-[calc(100vh-57px)] lg:max-h-[calc(100vh-57px)] lg:w-[287px] lg:self-start lg:border-l lg:border-t-0 lg:px-0">
|
|
|
|
|
<div class="right-sidebar__scroll site-sidebar-scroll flex min-h-0 flex-1 flex-col">
|
|
|
|
|
<div class="right-sidebar__block site-sidebar-section py-5 pl-5 pr-5 sm:pr-0 max-lg:px-0">
|
|
|
|
|
<div v-if="!isPostDetailRoute" class="right-sidebar__block site-sidebar-section py-5 pl-5 pr-5 sm:pr-0 max-lg:px-0">
|
|
|
|
|
<div class="right-sidebar__profile flex items-center gap-3">
|
|
|
|
|
<div class="right-sidebar__logo grid h-12 w-12 shrink-0 place-items-center overflow-hidden rounded-2xl text-2xl font-bold text-[var(--site-invert-text)]">
|
|
|
|
|
<img
|
|
|
|
|
@@ -262,7 +267,7 @@ watch([postTocItems, () => route.fullPath], async () => {
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="followLinks.length" class="right-sidebar__block site-sidebar-section py-5 sm:pl-5 pr-0">
|
|
|
|
|
<div v-if="!isPostDetailRoute && followLinks.length" class="right-sidebar__block site-sidebar-section py-5 sm:pl-5 pr-0">
|
|
|
|
|
<div class="right-sidebar__row flex items-center justify-between">
|
|
|
|
|
<p class="right-sidebar__eyebrow text-xs font-semibold uppercase site-muted">
|
|
|
|
|
Follow
|
|
|
|
|
@@ -404,24 +409,29 @@ watch([postTocItems, () => route.fullPath], async () => {
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
v-if="isPostDetailRoute"
|
|
|
|
|
class="right-sidebar__block right-sidebar__toc flex min-h-0 flex-1 flex-col py-5 pl-5 pr-0 max-lg:hidden"
|
|
|
|
|
class="right-sidebar__block right-sidebar__toc py-5 pl-5 pr-0 max-lg:hidden"
|
|
|
|
|
>
|
|
|
|
|
<div class="right-sidebar__row flex shrink-0 items-center justify-between">
|
|
|
|
|
<p class="right-sidebar__eyebrow text-xs font-semibold uppercase site-muted">
|
|
|
|
|
TOC
|
|
|
|
|
목차
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<nav ref="tocNavRef" class="right-sidebar__toc-nav mt-4 min-h-0 flex-1 overflow-y-auto pr-2" aria-label="게시글 목차">
|
|
|
|
|
<ul v-if="postTocItems.length" class="right-sidebar__toc-list list-none space-y-2 p-0">
|
|
|
|
|
<li v-for="item in postTocItems" :key="item.id">
|
|
|
|
|
<nav ref="tocNavRef" class="right-sidebar__toc-nav mt-4 pr-2" aria-label="게시글 목차">
|
|
|
|
|
<ul v-if="postTocItems.length" class="right-sidebar__toc-list flex list-none flex-col gap-2 border-l border-[var(--site-line)] p-0">
|
|
|
|
|
<li
|
|
|
|
|
v-for="item in postTocItems"
|
|
|
|
|
:key="item.id"
|
|
|
|
|
class="right-sidebar__toc-item relative flex h-6 items-center transition-colors"
|
|
|
|
|
:class="activeTocId === item.id ? 'right-sidebar__toc-item--active' : ''"
|
|
|
|
|
>
|
|
|
|
|
<a
|
|
|
|
|
class="right-sidebar__toc-link site-interactive block rounded-md py-1.5 pr-3 text-sm leading-snug transition-colors hover:text-[var(--site-accent)]"
|
|
|
|
|
class="right-sidebar__toc-link site-interactive flex h-full items-center rounded-md pr-3 text-sm leading-snug transition-colors hover:text-[var(--site-accent)]"
|
|
|
|
|
:class="{
|
|
|
|
|
'border-[var(--site-accent)] bg-[var(--site-panel)] text-[var(--site-accent)] font-semibold': activeTocId === item.id,
|
|
|
|
|
'border-transparent text-[var(--site-text)]': activeTocId !== item.id,
|
|
|
|
|
'pl-2 font-semibold': item.level === 1,
|
|
|
|
|
'pl-5': item.level === 2,
|
|
|
|
|
'pl-8 text-xs': item.level === 3,
|
|
|
|
|
'bg-[var(--site-panel)] text-[var(--site-accent)] font-semibold': activeTocId === item.id,
|
|
|
|
|
'text-[var(--site-text)]': activeTocId !== item.id,
|
|
|
|
|
'pl-4 font-semibold': item.level === 1,
|
|
|
|
|
'pl-7': item.level === 2,
|
|
|
|
|
'pl-10': item.level === 3,
|
|
|
|
|
'site-muted': item.level === 3 && activeTocId !== item.id
|
|
|
|
|
}"
|
|
|
|
|
:href="`#${item.id}`"
|
|
|
|
|
@@ -491,7 +501,7 @@ watch([postTocItems, () => route.fullPath], async () => {
|
|
|
|
|
<SiteAdSlot
|
|
|
|
|
class="right-sidebar__ad-slot py-5 pl-5 pr-0 max-lg:px-0"
|
|
|
|
|
:code="sidebarAdCode"
|
|
|
|
|
location="sidebar"
|
|
|
|
|
:location="isPostDetailRoute ? 'post-sidebar-right' : 'sidebar'"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@@ -511,4 +521,15 @@ watch([postTocItems, () => route.fullPath], async () => {
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
line-height: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.right-sidebar__toc-item--active::before {
|
|
|
|
|
content: "";
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: -2px;
|
|
|
|
|
top: 50%;
|
|
|
|
|
width: 3px;
|
|
|
|
|
height: 24px;
|
|
|
|
|
background: var(--site-accent);
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|