mirror of
https://github.com/KazooTTT/kazoottt-blog.git
synced 2025-06-23 02:31:33 +08:00
229 lines
6.2 KiB
Plaintext
229 lines
6.2 KiB
Plaintext
---
|
||
import type { CollectionEntry } from 'astro:content'
|
||
|
||
import BlogHero from '@/components/blog/Hero.astro'
|
||
import TOC from '@/components/blog/TOC.astro'
|
||
import Button from '@/components/Button.astro'
|
||
import PageLayout from './BaseLayout.astro'
|
||
|
||
interface Props {
|
||
post: CollectionEntry<'post'>
|
||
simple?: boolean
|
||
backHref: string
|
||
}
|
||
|
||
const { post, simple = false, backHref = '/blog' } = Astro.props
|
||
const {
|
||
data: { description, ogImage, title, date },
|
||
slug
|
||
} = post
|
||
|
||
const socialImage = ogImage ?? `/og-image/${slug}.png`
|
||
const articleDate = date?.toISOString()
|
||
const { headings } = await post.render()
|
||
---
|
||
|
||
<PageLayout meta={{ articleDate, description: description ?? '', ogImage: socialImage, title }}>
|
||
<div class='w-full'>
|
||
<Button title='Back' href={backHref} style='button'>
|
||
<svg
|
||
xmlns='http://www.w3.org/2000/svg'
|
||
width='14'
|
||
height='14'
|
||
viewBox='0 0 24 24'
|
||
slot='icon-before'
|
||
>
|
||
<path
|
||
fill='currentColor'
|
||
d='m6.921 12.5l5.792 5.792L12 19l-7-7l7-7l.713.708L6.921 11.5H19v1z'
|
||
>
|
||
</path>
|
||
</svg>
|
||
</Button>
|
||
<div class='mt-8 gap-x-10 lg:flex lg:items-start'>
|
||
{!!headings.length && <TOC headings={headings} />}
|
||
<article class='flex-1 flex-grow break-words' data-pagefind-body>
|
||
<div id='blog-hero'>
|
||
<BlogHero content={post} simple={simple} />
|
||
</div>
|
||
<div
|
||
id='blog-gallery'
|
||
class='prose prose-base prose-zinc mt-12 text-muted-foreground dark:prose-invert prose-headings:font-medium prose-headings:text-foreground prose-headings:before:absolute prose-headings:before:-ms-4 prose-th:before:content-none prose-img:shadow'
|
||
>
|
||
<slot />
|
||
</div>
|
||
</article>
|
||
</div>
|
||
<button
|
||
aria-label='Back to Top'
|
||
class='z-90 fixed bottom-8 end-4 flex h-8 w-8 translate-y-28 items-center justify-center rounded-full border-2 border-border/50 bg-primary-foreground text-3xl opacity-0 transition-all duration-300 hover:border-border/75 data-[show=true]:translate-y-0 data-[show=true]:opacity-100 sm:end-8 sm:h-12 sm:w-12'
|
||
data-show='false'
|
||
id='to-top-btn'
|
||
><svg
|
||
aria-hidden='true'
|
||
class='h-4 w-4'
|
||
fill='none'
|
||
focusable='false'
|
||
stroke='currentColor'
|
||
stroke-width='2'
|
||
viewBox='0 0 24 24'
|
||
xmlns='http://www.w3.org/2000/svg'
|
||
>
|
||
<path d='M4.5 15.75l7.5-7.5 7.5 7.5' stroke-linecap='round' stroke-linejoin='round'></path>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Image Preview Modal -->
|
||
<div
|
||
id='image-modal'
|
||
class='pointer-events-none fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 opacity-0 backdrop-blur-md transition-opacity duration-300 ease-in-out'
|
||
>
|
||
<div class='relative flex h-full w-full items-center justify-center p-4' id='modal-container'>
|
||
<img
|
||
id='modal-image'
|
||
alt='modal-image'
|
||
class='h-auto max-h-full w-auto max-w-full object-contain transition-transform duration-300 ease-in-out'
|
||
/>
|
||
<button
|
||
id='close-modal'
|
||
class='absolute right-4 top-4 text-white transition-colors duration-300 hover:text-gray-300'
|
||
>
|
||
<svg
|
||
xmlns='http://www.w3.org/2000/svg'
|
||
class='h-6 w-6'
|
||
fill='none'
|
||
viewBox='0 0 24 24'
|
||
stroke='currentColor'
|
||
>
|
||
<path
|
||
stroke-linecap='round'
|
||
stroke-linejoin='round'
|
||
stroke-width='2'
|
||
d='M6 18L18 6M6 6l12 12'></path>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</PageLayout>
|
||
|
||
<script>
|
||
const scrollBtn = document.getElementById('to-top-btn') as HTMLButtonElement
|
||
const targetHeader = document.getElementById('blog-hero') as HTMLDivElement
|
||
|
||
function callback(entries: IntersectionObserverEntry[]) {
|
||
entries.forEach((entry) => {
|
||
// only show the scroll to top button when the heading is out of view
|
||
scrollBtn.dataset.show = (!entry.isIntersecting).toString()
|
||
})
|
||
}
|
||
|
||
scrollBtn.addEventListener('click', () => {
|
||
document.documentElement.scrollTo({ behavior: 'smooth', top: 0 })
|
||
})
|
||
|
||
const observer = new IntersectionObserver(callback)
|
||
observer.observe(targetHeader)
|
||
|
||
// Image preview functionality
|
||
const imageModal = document.getElementById('image-modal')
|
||
const modalImage = document.getElementById('modal-image') as HTMLImageElement
|
||
const modalContainer = document.getElementById('modal-container')
|
||
|
||
function openModal() {
|
||
if (imageModal) {
|
||
imageModal.classList.remove('opacity-0', 'pointer-events-none')
|
||
modalImage.style.transform = 'scale(1)'
|
||
isZoomed = false
|
||
document.body.style.overflow = 'hidden'
|
||
}
|
||
}
|
||
|
||
function closeModal() {
|
||
if (imageModal) {
|
||
imageModal.classList.add('opacity-0', 'pointer-events-none')
|
||
modalImage.src = ''
|
||
modalImage.alt = ''
|
||
modalImage.style.transform = 'scale(1)'
|
||
isZoomed = false
|
||
document.body.style.overflow = ''
|
||
}
|
||
}
|
||
|
||
document.addEventListener('click', (e) => {
|
||
const target = e.target as HTMLElement
|
||
if (target.tagName === 'IMG' && target.closest('.prose')) {
|
||
const img = target as HTMLImageElement
|
||
modalImage.src = img.src
|
||
modalImage.alt = img.alt
|
||
openModal()
|
||
}
|
||
})
|
||
|
||
// 处理点击事件
|
||
if (imageModal) {
|
||
imageModal.addEventListener('click', (e) => {
|
||
const clickedElement = e.target as HTMLElement
|
||
// 如果点击的是 modal 背景或 modal-container(不是图片和关闭按钮)
|
||
if (clickedElement === imageModal || clickedElement === modalContainer) {
|
||
closeModal()
|
||
}
|
||
})
|
||
}
|
||
|
||
// 关闭按钮事件
|
||
const closeButton = document.getElementById('close-modal')
|
||
if (closeButton) {
|
||
closeButton.addEventListener('click', closeModal)
|
||
}
|
||
|
||
// ESC 键关闭
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape' && imageModal && !imageModal.classList.contains('opacity-0')) {
|
||
closeModal()
|
||
}
|
||
})
|
||
|
||
// 图片缩放功能
|
||
let isZoomed = false
|
||
modalImage.addEventListener('click', (e) => {
|
||
e.stopPropagation() // 阻止事件冒泡到 modal
|
||
if (isZoomed) {
|
||
modalImage.style.transform = 'scale(1)'
|
||
modalImage.classList.remove('zoomed')
|
||
} else {
|
||
modalImage.style.transform = 'scale(1.5)'
|
||
modalImage.classList.add('zoomed')
|
||
}
|
||
isZoomed = !isZoomed
|
||
})
|
||
</script>
|
||
|
||
<style>
|
||
#image-modal {
|
||
transition: opacity 300ms ease-in-out;
|
||
}
|
||
|
||
#modal-image {
|
||
transition: transform 300ms ease-in-out;
|
||
cursor: zoom-in;
|
||
}
|
||
|
||
#modal-image.zoomed {
|
||
cursor: zoom-out;
|
||
}
|
||
|
||
#close-modal {
|
||
opacity: 0.7;
|
||
transition: opacity 300ms ease-in-out;
|
||
}
|
||
|
||
#close-modal:hover {
|
||
opacity: 1;
|
||
}
|
||
|
||
body.modal-open {
|
||
overflow: hidden;
|
||
}
|
||
</style>
|