mirror of
https://github.com/KazooTTT/kazoottt-blog.git
synced 2025-06-16 23:41:21 +08:00
Refactor Calendar component
This commit is contained in:
@ -72,7 +72,10 @@ const weekDays = ['一', '二', '三', '四', '五', '六', '日']
|
||||
day?: 'numeric' | '2-digit'
|
||||
}
|
||||
|
||||
const allPosts = JSON.parse(document.getElementById('posts-data')?.textContent || '[]') as Post[]
|
||||
function initCalendar() {
|
||||
const allPosts = JSON.parse(
|
||||
document.getElementById('posts-data')?.textContent || '[]'
|
||||
) as Post[]
|
||||
const currentMonthDisplay = document.getElementById('currentMonthDisplay')
|
||||
const prevMonthBtn = document.getElementById('prevMonth')
|
||||
const nextMonthBtn = document.getElementById('nextMonth')
|
||||
@ -251,4 +254,13 @@ const weekDays = ['一', '二', '三', '四', '五', '六', '日']
|
||||
renderCalendar()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 页面加载时初始化
|
||||
initCalendar()
|
||||
|
||||
// 在视图转换后重新初始化
|
||||
document.addEventListener('astro:after-swap', () => {
|
||||
initCalendar()
|
||||
})
|
||||
</script>
|
||||
|
@ -5,23 +5,23 @@ import { Image } from 'astro:assets'
|
||||
|
||||
<header class='fixed left-0 right-0 top-0 z-50 bg-white shadow-sm dark:bg-gray-800'>
|
||||
<nav
|
||||
class='mx-auto flex w-full items-center justify-between px-4 py-3 sm:flex sm:w-3/5 sm:items-center lg:w-4/5'
|
||||
class='mx-auto flex w-full items-center justify-between px-4 py-3 lg:flex lg:w-3/5 lg:w-4/5 lg:items-center'
|
||||
aria-label='global'
|
||||
>
|
||||
<a class='flex items-center' href='/'>
|
||||
<Image
|
||||
src={kazootttAvatar}
|
||||
alt='profile photo'
|
||||
class='mr-2 h-8 w-auto rounded-full sm:hidden'
|
||||
class='mr-2 h-8 w-auto rounded-full lg:hidden'
|
||||
loading='eager'
|
||||
/>
|
||||
<div class='hidden flex-none text-xl font-semibold sm:block' aria-label='Brand'>声控烤箱</div>
|
||||
<div class='hidden flex-none text-xl font-semibold lg:block' aria-label='Brand'>声控烤箱</div>
|
||||
</a>
|
||||
|
||||
<!-- Mobile menu button -->
|
||||
<button
|
||||
id='mobileMenuButton'
|
||||
class='rounded-md p-2 hover:bg-border sm:hidden'
|
||||
class='rounded-md p-2 hover:bg-border lg:hidden'
|
||||
aria-label='Toggle mobile menu'
|
||||
>
|
||||
<svg
|
||||
@ -36,7 +36,7 @@ import { Image } from 'astro:assets'
|
||||
</button>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<div class='hidden flex-row items-center justify-center gap-x-7 sm:flex'>
|
||||
<div class='hidden flex-row items-center justify-center gap-x-7 lg:flex'>
|
||||
<div class='relative'>
|
||||
<a
|
||||
href='/blog'
|
||||
@ -145,7 +145,7 @@ import { Image } from 'astro:assets'
|
||||
<!-- Mobile Navigation -->
|
||||
<div
|
||||
id='mobileMenu'
|
||||
class='fixed inset-x-0 top-[72px] z-50 hidden max-h-[calc(100vh-72px)] overflow-y-auto rounded-b-lg bg-white shadow-lg dark:bg-gray-800 sm:hidden'
|
||||
class='fixed inset-x-0 top-[72px] z-50 hidden max-h-[calc(100vh-72px)] overflow-y-auto rounded-b-lg bg-white shadow-lg dark:bg-gray-800 lg:hidden'
|
||||
>
|
||||
<div class='space-y-2 px-4 py-2'>
|
||||
<a
|
||||
|
@ -25,7 +25,7 @@ const {
|
||||
<body class='flex justify-center bg-background'>
|
||||
<ThemeProvider />
|
||||
<main
|
||||
class='flex min-h-screen w-screen max-w-[60rem] flex-col items-center px-6 pb-10 pt-24 font-satoshi text-[0.92rem] leading-relaxed sm:px-10 lg:px-10'
|
||||
class='flex min-h-screen w-screen max-w-[64rem] flex-col items-center px-6 pb-10 pt-24 font-satoshi text-[0.92rem] leading-relaxed sm:px-10 lg:px-10'
|
||||
>
|
||||
<Header />
|
||||
<slot />
|
||||
|
@ -5,6 +5,7 @@ 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'
|
||||
import GiscusComment from '@/components/GiscusComment'
|
||||
|
||||
interface Props {
|
||||
post: CollectionEntry<'post'>
|
||||
@ -55,10 +56,11 @@ const { headings } = await post.render()
|
||||
</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'
|
||||
class='prose-base prose-zinc mt-12 w-full 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>
|
||||
<GiscusComment client:load />
|
||||
</article>
|
||||
</div>
|
||||
<button
|
||||
@ -80,38 +82,6 @@ const { headings } = await post.render()
|
||||
</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>
|
||||
@ -131,79 +101,6 @@ const { headings } = await post.render()
|
||||
|
||||
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>
|
||||
|
@ -81,7 +81,7 @@ const serializedPosts = allPosts.map((post) => ({
|
||||
type='application/json'
|
||||
set:html={JSON.stringify(serializedPosts)}
|
||||
/>
|
||||
<div data-current-date={currentDate.toISOString()}>
|
||||
<div id='calendar-container' data-current-date={currentDate.toISOString()}>
|
||||
<Calendar posts={allPosts} currentDate={currentDate} />
|
||||
</div>
|
||||
|
||||
@ -98,3 +98,28 @@ const serializedPosts = allPosts.map((post) => ({
|
||||
}
|
||||
</div>
|
||||
</PageLayout>
|
||||
|
||||
<script>
|
||||
function initializeCalendar() {
|
||||
const postsDataScript = document.getElementById('posts-data')
|
||||
if (!postsDataScript) return
|
||||
|
||||
// 触发一个自定义事件,通知日历组件重新初始化
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('calendar-init', {
|
||||
detail: {
|
||||
posts: JSON.parse(postsDataScript.textContent || '[]'),
|
||||
currentDate: document.getElementById('calendar-container')?.dataset.currentDate
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// 页面首次加载时初始化
|
||||
initializeCalendar()
|
||||
|
||||
// 在视图转换后重新初始化
|
||||
document.addEventListener('astro:after-swap', () => {
|
||||
initializeCalendar()
|
||||
})
|
||||
</script>
|
||||
|
Reference in New Issue
Block a user