feat: 新增日记视图

This commit is contained in:
KazooTTT
2025-01-04 13:05:06 +08:00
parent 2f69cadde8
commit 78c13b6068
3 changed files with 5560 additions and 4125 deletions

9101
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,254 @@
---
import { getFormattedDate } from '@/utils'
import type { CollectionEntry } from 'astro:content'
interface Props {
posts: CollectionEntry<'post'>[]
currentDate?: Date
}
const { posts, currentDate = new Date() } = Astro.props
// 将日记按日期分组
const postsByDate = new Map()
posts.forEach((post) => {
const date = getFormattedDate(post.data.date, {
year: 'numeric',
month: '2-digit',
day: '2-digit'
})
if (!postsByDate.has(date)) {
postsByDate.set(date, [])
}
postsByDate.get(date).push(post)
})
// 获取所有有日记的月份
const months = Array.from(postsByDate.keys()).map((date) => {
const [year, month] = date.split('-')
return `${year}-${month}`
})
const uniqueMonths = Array.from(new Set(months)).sort().reverse()
const weekDays = ['一', '二', '三', '四', '五', '六', '日']
---
<div class='mx-auto w-full max-w-4xl'>
<div class='mb-4 flex items-center justify-between'>
<button
id='prevMonth'
class='rounded-lg border border-border px-4 py-2 hover:border-green-400/50'
>
上个月
</button>
<h2 class='text-xl font-bold' id='currentMonthDisplay'>
{currentDate.getFullYear()}年{currentDate.getMonth() + 1}月
</h2>
<button
id='nextMonth'
class='rounded-lg border border-border px-4 py-2 hover:border-green-400/50'
>
下个月
</button>
</div>
<div class='grid grid-cols-7 gap-1' id='calendarGrid'>
{weekDays.map((day) => <div class='py-2 text-center font-medium'>{day}</div>)}
</div>
</div>
<script>
interface Post {
slug: string
data: {
title: string
date: string
}
}
interface DateTimeFormatOptions {
year?: 'numeric' | '2-digit'
month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow'
day?: 'numeric' | '2-digit'
}
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')
const calendarGrid = document.getElementById('calendarGrid')
// 使用传入的 currentDate 作为初始日期
let currentDate = new Date(
document.querySelector('[data-current-date]')?.getAttribute('data-current-date') || new Date()
)
// 获取所有有日记的月份
const months = allPosts.map((post) => {
const date = new Date(post.data.date)
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`
})
const uniqueMonths = Array.from(new Set(months)).sort()
function getFormattedDate(date: Date, options: DateTimeFormatOptions): string {
return new Intl.DateTimeFormat('zh-CN', options).format(date)
}
function getCurrentMonthStr(): string {
return `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}`
}
function canNavigateToMonth(direction: 'prev' | 'next'): boolean {
const currentMonthStr = getCurrentMonthStr()
if (direction === 'prev') {
return uniqueMonths.some((m) => m < currentMonthStr)
} else {
const now = new Date()
const currentMonth = new Date(currentDate.getFullYear(), currentDate.getMonth())
const thisMonth = new Date(now.getFullYear(), now.getMonth())
return currentMonth < thisMonth
}
}
function updateNavigationButtons() {
if (prevMonthBtn && nextMonthBtn) {
const canGoPrev = canNavigateToMonth('prev')
const canGoNext = canNavigateToMonth('next')
prevMonthBtn.classList.toggle('cursor-not-allowed', !canGoPrev)
prevMonthBtn.classList.toggle('opacity-50', !canGoPrev)
;(prevMonthBtn as HTMLButtonElement).disabled = !canGoPrev
nextMonthBtn.classList.toggle('cursor-not-allowed', !canGoNext)
nextMonthBtn.classList.toggle('opacity-50', !canGoNext)
;(nextMonthBtn as HTMLButtonElement).disabled = !canGoNext
}
}
function renderCalendar() {
// 获取当月的第一天和最后一天
const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1)
const lastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0)
// 获取当月第一天是星期几0是周日1是周一以此类推
const firstDayWeekday = firstDayOfMonth.getDay()
const adjustedFirstDayWeekday = firstDayWeekday === 0 ? 7 : firstDayWeekday
// 计算日历表格需要显示的天数
const daysInPrevMonth = adjustedFirstDayWeekday - 1
const daysInCurrentMonth = lastDayOfMonth.getDate()
const totalDays = Math.ceil((daysInPrevMonth + daysInCurrentMonth) / 7) * 7
// 获取上个月的最后几天
const lastDayOfPrevMonth = new Date(
currentDate.getFullYear(),
currentDate.getMonth(),
0
).getDate()
// 更新月份显示
if (currentMonthDisplay) {
currentMonthDisplay.textContent = `${currentDate.getFullYear()}年${currentDate.getMonth() + 1}月`
}
// 更新导航按钮状态
updateNavigationButtons()
// 清除现有的日历内容(保留星期标题)
const existingDays = calendarGrid?.querySelectorAll('.calendar-day')
existingDays?.forEach((day) => day.remove())
// 生成日历内容
for (let i = 0; i < totalDays; i++) {
const dayNumber = i - daysInPrevMonth + 1
const isCurrentMonth = dayNumber > 0 && dayNumber <= daysInCurrentMonth
const displayDay = isCurrentMonth
? dayNumber
: dayNumber <= 0
? lastDayOfPrevMonth + dayNumber
: dayNumber - daysInCurrentMonth
const date = new Date(
currentDate.getFullYear(),
isCurrentMonth
? currentDate.getMonth()
: dayNumber <= 0
? currentDate.getMonth() - 1
: currentDate.getMonth() + 1,
displayDay
)
const formattedDate = getFormattedDate(date, {
year: 'numeric',
month: '2-digit',
day: '2-digit'
})
// 查找当天的文章
const postsForDay = allPosts.filter((post) => {
const postDate = new Date(post.data.date)
return (
getFormattedDate(postDate, {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}) === formattedDate
)
})
const hasPost = postsForDay.length > 0
// 创建日历单元格
const dayElement = document.createElement('div')
dayElement.className = `calendar-day min-h-[100px] rounded-lg border p-2 ${
isCurrentMonth ? 'bg-primary-foreground' : 'bg-muted/50'
} ${hasPost ? 'border-green-400/50' : 'border-border'}`
// 添加日期显示
const dateDisplay = document.createElement('div')
dateDisplay.className = 'mb-1 text-right'
const dateSpan = document.createElement('span')
dateSpan.className = `inline-block h-7 w-7 rounded-full text-center leading-7 ${
!isCurrentMonth ? 'text-muted-foreground' : ''
} ${hasPost ? 'bg-green-400/20' : ''}`
dateSpan.textContent = displayDay.toString()
dateDisplay.appendChild(dateSpan)
dayElement.appendChild(dateDisplay)
// 如果有文章,添加文章链接
if (hasPost) {
const postsContainer = document.createElement('div')
postsContainer.className = 'space-y-1 text-xs'
postsForDay.forEach((post) => {
const link = document.createElement('a')
link.href = `/diary/${post.slug}/`
link.className = 'block line-clamp-2 transition-colors hover:text-green-400'
link.title = post.data.title
link.textContent = post.data.title
postsContainer.appendChild(link)
})
dayElement.appendChild(postsContainer)
}
calendarGrid?.appendChild(dayElement)
}
}
// 初始渲染
renderCalendar()
// 添加事件监听器
prevMonthBtn?.addEventListener('click', () => {
if (canNavigateToMonth('prev')) {
currentDate.setMonth(currentDate.getMonth() - 1)
renderCalendar()
}
})
nextMonthBtn?.addEventListener('click', () => {
if (canNavigateToMonth('next')) {
currentDate.setMonth(currentDate.getMonth() + 1)
renderCalendar()
}
})
</script>

View File

@ -5,7 +5,7 @@ import type { GetStaticPaths, Page } from 'astro'
import type { CollectionEntry } from 'astro:content'
import Button from '@/components/Button.astro'
import Pagination from '@/components/Paginator.astro'
import Calendar from '@/components/blog/Calendar.astro'
import PostPreview from '@/components/blog/PostPreview.astro'
import PageLayout from '@/layouts/BaseLayout.astro'
import { getallDiaries, getUniqueCategories, getUniqueTags, sortMDByDate } from '@/utils'
@ -15,36 +15,41 @@ export const getStaticPaths = (async ({ paginate }) => {
const allPostsByDate = sortMDByDate(allPosts)
const uniqueTags = getUniqueTags(allPosts)
const uniqueCategories = getUniqueCategories(allPosts)
return paginate(allPostsByDate, { pageSize: 50, props: { uniqueTags, uniqueCategories } })
// 找到最新的有日记的月份
const latestPost = allPostsByDate[0]
const latestDate = latestPost ? new Date(latestPost.data.date) : new Date()
return paginate(allPostsByDate, {
pageSize: 50,
props: { uniqueTags, uniqueCategories, allPosts: allPostsByDate, latestDate }
})
}) satisfies GetStaticPaths
interface Props {
page: Page<CollectionEntry<'post'>>
uniqueTags: string[]
uniqueCategories: string[]
allPosts: CollectionEntry<'post'>[]
latestDate: Date
}
const { page, uniqueTags, uniqueCategories } = Astro.props
const { page, allPosts, latestDate } = Astro.props
const currentDate = latestDate
const meta = {
description: 'Posts',
title: 'Diary'
}
const paginationProps = {
...(page.url.prev && {
prevUrl: {
text: `← Previous Posts`,
url: page.url.prev
// 序列化文章数据供客户端使用
const serializedPosts = allPosts.map((post) => ({
slug: post.slug,
data: {
title: post.data.title,
date: post.data.date
}
}),
...(page.url.next && {
nextUrl: {
text: `Next Posts →`,
url: page.url.next
}
})
}
}))
---
<PageLayout meta={meta}>
@ -70,15 +75,24 @@ const paginationProps = {
{
page.data.length > 0 && (
<div class='grid gap-y-16 sm:grid-cols-[3fr_1fr] sm:gap-x-8'>
<section aria-label='Diary posts list'>
<div class='space-y-12'>
<script
id='posts-data'
type='application/json'
set:html={JSON.stringify(serializedPosts)}
/>
<div data-current-date={currentDate.toISOString()}>
<Calendar posts={allPosts} currentDate={currentDate} />
</div>
<div class='mt-8'>
<h2 class='mb-4 text-xl font-semibold'>所有日记</h2>
<ul class='flex flex-col gap-y-4 text-start'>
{page.data.map((p) => (
<PostPreview post={p} prefix='/diary/' withDesc />
))}
</ul>
<Pagination {...paginationProps} />
</section>
</div>
</div>
)
}