Правки в компонент поста
This commit is contained in:
parent
d0f41672d1
commit
9c5b9a1dbe
11 changed files with 405 additions and 316 deletions
161
frontend/src/components/blog/ArticleTableOfContents.astro
Normal file
161
frontend/src/components/blog/ArticleTableOfContents.astro
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
---
|
||||
interface TOCItem {
|
||||
id: string;
|
||||
text: string;
|
||||
level: number;
|
||||
}
|
||||
interface Props {
|
||||
items: TOCItem[];
|
||||
}
|
||||
const { items } = Astro.props;
|
||||
---
|
||||
|
||||
{items.length > 0 && (
|
||||
<aside class="toc-container" id="toc-sidebar">
|
||||
<nav class="toc-nav">
|
||||
<h3 class="toc-title">Оглавление</h3>
|
||||
<ul class="toc-list">
|
||||
{items.map((item) => (
|
||||
<li class={`toc-item level-${item.level}`} data-toc-target={item.id}>
|
||||
<a href={`#${item.id}`} class="toc-link">
|
||||
<span class="toc-text">{item.text}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
)}
|
||||
|
||||
<style>
|
||||
.toc-container { width: 100%; }
|
||||
.toc-nav {
|
||||
background: #ffffff;
|
||||
border-radius: 1.25rem;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.03);
|
||||
}
|
||||
.toc-title {
|
||||
color: #1e3050;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 800;
|
||||
margin: 0 0 1.25rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 2px solid #eac26e;
|
||||
}
|
||||
|
||||
.toc-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
counter-reset: toc-h2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
/* ЧЕРНАЯ НУМЕРАЦИЯ */
|
||||
.toc-item.level-2 { counter-reset: toc-h3; counter-increment: toc-h2; }
|
||||
.toc-item.level-3 { counter-increment: toc-h3; padding-left: 1.25rem; }
|
||||
|
||||
.toc-link {
|
||||
display: flex;
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: #64748b;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* Номера в оглавлении */
|
||||
.toc-link::before {
|
||||
color: #1e3050;
|
||||
font-weight: 700;
|
||||
margin-right: 0.6rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.level-2 .toc-link::before { content: counter(toc-h2) "."; }
|
||||
.level-3 .toc-link::before { content: counter(toc-h2) "." counter(toc-h3) "."; font-size: 0.85em; opacity: 0.7; }
|
||||
|
||||
.toc-link:hover { color: #1e3050; background: #f1f5f9; }
|
||||
|
||||
/* АКТИВНЫЙ ПУНКТ (При скролле) */
|
||||
.toc-item.active .toc-link {
|
||||
color: #eac26e;
|
||||
font-weight: 700;
|
||||
background: rgba(234, 194, 110, 0.1);
|
||||
}
|
||||
.toc-item.active .toc-link::before { color: #eac26e; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function setupTOC() {
|
||||
const toc = document.getElementById('toc-sidebar');
|
||||
if (!toc) return;
|
||||
|
||||
const items = toc.querySelectorAll('.toc-item');
|
||||
const links = toc.querySelectorAll<HTMLAnchorElement>('.toc-link');
|
||||
const HEADER_OFFSET = 120; // Высота хедера + запас
|
||||
|
||||
// --- 1. ПЛАВНЫЙ СКРОЛЛ ПРИ КЛИКЕ ---
|
||||
links.forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const id = link.getAttribute('href')?.substring(1);
|
||||
const target = document.getElementById(id || '');
|
||||
|
||||
if (target) {
|
||||
const elementPosition = target.getBoundingClientRect().top;
|
||||
const offsetPosition = elementPosition + window.pageYOffset - HEADER_OFFSET;
|
||||
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior: "smooth"
|
||||
});
|
||||
|
||||
// Обновляем URL
|
||||
history.pushState(null, '', `#${id}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// --- 2. ПОДСВЕТКА ПРИ СКРОЛЛЕ (SCROLL SPY) ---
|
||||
const headingElements = Array.from(items)
|
||||
.map(item => {
|
||||
const id = item.getAttribute('data-toc-target');
|
||||
return id ? document.getElementById(id) : null;
|
||||
})
|
||||
.filter((el): el is HTMLElement => el !== null);
|
||||
|
||||
const observerOptions = {
|
||||
root: null,
|
||||
rootMargin: '-120px 0px -70% 0px', // Зона срабатывания: верхняя часть экрана
|
||||
threshold: 0
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const activeId = entry.target.id;
|
||||
|
||||
items.forEach(item => {
|
||||
if (item.getAttribute('data-toc-target') === activeId) {
|
||||
item.classList.add('active');
|
||||
} else {
|
||||
item.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
headingElements.forEach(heading => observer.observe(heading));
|
||||
}
|
||||
|
||||
// Запуск для обычной загрузки и для Astro ViewTransitions
|
||||
document.addEventListener('DOMContentLoaded', setupTOC);
|
||||
document.addEventListener('astro:page-load', setupTOC);
|
||||
</script>
|
||||
|
|
@ -20,16 +20,6 @@ const {
|
|||
imageUrl = '/images/blog/default.avif',
|
||||
slug = '#'
|
||||
} = Astro.props;
|
||||
|
||||
// Форматируем дату
|
||||
const formatDate = (dateStr: string) => {
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
---
|
||||
|
||||
<article class="blog-card" data-animation="fade-up">
|
||||
|
|
@ -53,7 +43,7 @@ const formatDate = (dateStr: string) => {
|
|||
<line x1="8" x2="8" y1="2" y2="6"></line>
|
||||
<line x1="3" x2="21" y1="10" y2="10"></line>
|
||||
</svg>
|
||||
{formatDate(date)}
|
||||
{date}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="meta-icon">
|
||||
|
|
@ -178,7 +168,7 @@ const formatDate = (dateStr: string) => {
|
|||
.card-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.25rem;
|
||||
justify-content: space-between;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
margin-top: auto;
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ const { initialLikes = 0, initialDislikes = 0, postId } = Astro.props;
|
|||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 0.875rem;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
background: rgba(15, 23, 42, 0.75);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 2rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ const encodedUrl = encodeURIComponent(url);
|
|||
}
|
||||
|
||||
.share-label {
|
||||
color: #64748b;
|
||||
color: #ffffff;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,16 @@ const filteredPosts = currentSlug
|
|||
border-top: 2px solid #f1f5f9;
|
||||
}
|
||||
|
||||
/* Сбрасываем нумерацию заголовков от ArticleLayout */
|
||||
.related-posts .blog-card .card-title {
|
||||
counter-increment: none !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.related-posts .blog-card .card-title::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #1e293b;
|
||||
font-size: clamp(1.5rem, 3vw, 2rem);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue