Files
SuBlog/pages/index.vue
T

335 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { siteConfig } from '~/config/site'
import { formatPostDate, getSeriesSummaries, isVisiblePost, sortPostsByPublished } from '~/utils/posts'
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const dayOfWeek = today.getDay()
const weekendStatus = getWeekendStatus(dayOfWeek)
type WeekendStatus = {
label: string
value: string
}
onMounted(() => {
const el = document.getElementById('busuanzi_value_site_pv')
if (!el) return
el.textContent = '1'
const animate = (target: number) => {
if (target <= 1) return
const duration = 800
const start = performance.now()
const tick = (now: number) => {
const progress = Math.min((now - start) / duration, 1)
const eased = 1 - Math.pow(1 - progress, 3)
el.textContent = Math.floor(1 + (target - 1) * eased).toLocaleString()
if (progress < 1) requestAnimationFrame(tick)
}
requestAnimationFrame(tick)
}
const observer = new MutationObserver(() => {
const val = parseInt(el.textContent || '0', 10)
if (val > 1) {
observer.disconnect()
animate(val)
}
})
observer.observe(el, { childList: true, characterData: true, subtree: true })
setTimeout(() => observer.disconnect(), 10000)
})
// 从 API 获取法定节假日
const { data: holidaysData } = await useAsyncData('holidays', () =>
$fetch('https://timor.tech/api/holiday/year/' + today.getFullYear()).catch(() => null)
)
const nextHoliday = computed(() => {
if (!holidaysData.value) return null
const list = holidaysData.value?.holiday
if (!list) return null
const entries = Object.values(list) as any[]
const upcoming = entries
.filter((h: any) => h.date && new Date(h.date + 'T00:00:00') >= today)
.sort((a: any, b: any) => a.date.localeCompare(b.date))
return upcoming[0] || null
})
const holidayDays = computed(() => {
if (!nextHoliday.value) return null
return daysUntil(new Date(nextHoliday.value.date + 'T00:00:00'))
})
function daysUntil(d: Date): number {
return Math.ceil((d.getTime() - today.getTime()) / 86400000)
}
function getWeekendStatus(dayOfWeek: number): WeekendStatus {
if (dayOfWeek === 5) return { label: '周末', value: '明天就是' }
if (dayOfWeek === 6) return { label: '周末', value: '周末愉快' }
if (dayOfWeek === 0) return { label: '周末', value: '余额 1 天' }
return {
label: '周末',
value: `还有 ${(6 - dayOfWeek + 7) % 7}`,
}
}
useHead({
script: [
{ src: '//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js', async: true, defer: true },
],
})
// 网易云热评
const { data: hitokoto } = await useAsyncData('wyy-comment', () => $fetch<{ user: string; music: string; content: string }>('/api/wyy'))
// 首页内容入口
const { data: homePosts } = await useAsyncData('home-posts', () =>
queryCollection('posts')
.order('pinned', 'DESC')
.order('published', 'DESC')
.all()
)
const visibleHomePosts = computed(() => sortPostsByPublished((homePosts.value || []).filter(isVisiblePost)))
const featuredPosts = computed(() => {
const posts = [
...visibleHomePosts.value.filter(post => post.pinned),
...visibleHomePosts.value.filter(post => !post.pinned),
]
const seen = new Set<string>()
return posts.filter((post) => {
const key = post.path || post.id || post.title || ''
if (!key || seen.has(key)) return false
seen.add(key)
return true
}).slice(0, 5)
})
const homeSeriesSummaries = computed(() => getSeriesSummaries(visibleHomePosts.value))
const homeSeries = computed(() => homeSeriesSummaries.value.slice(0, 3))
const homeSeriesCount = computed(() => homeSeriesSummaries.value.length)
const homeTagCount = computed(() => new Set(visibleHomePosts.value.flatMap(post => post.tags || [])).size)
const contentLinks = [
{ label: '文章', icon: 'mdi:post-outline', href: '/posts' },
{ label: '归档', icon: 'mdi:archive-outline', href: '/archive' },
{ label: '系列', icon: 'mdi:playlist-edit', href: '/series' },
]
const secondaryNavLinks = computed(() =>
siteConfig.navLinks.filter(link => !contentLinks.some(item => item.href === link.href))
)
</script>
<template>
<div class="mx-auto flex w-full max-w-4xl flex-col gap-10 px-4 py-8 sm:py-12">
<!-- 站点身份 -->
<section class="anim-fade-in-up">
<div class="flex flex-col items-center gap-5 text-center sm:flex-row sm:items-center sm:text-left">
<LayoutAvatarOrbit
:src="siteConfig.avatar"
:alt="siteConfig.siteName"
/>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-muted-foreground">{{ siteConfig.subtitle }}</p>
<h1 class="mt-1 text-4xl font-bold sm:text-5xl">{{ siteConfig.siteName }}</h1>
<p class="mt-3 text-base leading-relaxed text-muted-foreground">
{{ siteConfig.description }}{{ siteConfig.bio }}
</p>
<div v-if="hitokoto" class="mt-4 text-sm leading-relaxed text-muted-foreground">
<p class="italic">"{{ hitokoto.content }}"</p>
<p class="mt-1 text-xs opacity-70"> {{ hitokoto.user }} · {{ hitokoto.music }}</p>
</div>
<p class="mt-4 text-sm text-muted-foreground">
您是第 <span id="busuanzi_value_site_pv" class="font-medium text-primary"></span> 个访客
</p>
</div>
</div>
</section>
<!-- 辅助信息 -->
<section class="anim-fade-in-up anim-delay-1">
<div class="grid gap-4 md:grid-cols-2">
<div class="rounded-xl border px-5 py-4" style="border-color: var(--color-border); background-color: var(--color-bg-card);">
<h2 class="mb-3 text-sm font-semibold text-muted-foreground">社交</h2>
<div class="flex flex-wrap gap-2">
<a
v-for="link in siteConfig.socialLinks"
:key="link.name"
:href="link.url"
target="_blank"
rel="noopener noreferrer"
class="btn-pill"
>
<img
:src="`https://api.iconify.design/${link.icon}.svg?color=${encodeURIComponent(link.color || 'currentColor')}`"
:alt="link.name"
class="h-5 w-5"
/>
<span>{{ link.name }}</span>
</a>
</div>
</div>
<div class="rounded-xl border px-5 py-4" style="border-color: var(--color-border); background-color: var(--color-bg-card);">
<h2 class="mb-3 text-sm font-semibold text-muted-foreground">更多页面</h2>
<div class="flex flex-wrap gap-2">
<NuxtLink
v-for="link in secondaryNavLinks"
:key="link.href"
:to="link.href"
class="btn-pill"
>
<Icon :icon="link.icon" class="h-5 w-5" />
<span>{{ link.label }}</span>
</NuxtLink>
</div>
</div>
<div class="rounded-xl border px-5 py-4" style="border-color: var(--color-border); background-color: var(--color-bg-card);">
<h2 class="mb-3 text-sm font-semibold text-muted-foreground">公告</h2>
<div class="announcement-happy">
<p class="announcement-text text-sm leading-relaxed">
<strong>欢迎来到{{ siteConfig.siteName }}~ 用代码创造快乐用热爱点亮生活 (゚ヮ゚)</strong>
</p>
<p class="announcement-text mt-1 text-sm leading-relaxed">
遇到问题的话可以去 <a href="https://github.com/wishesl/SuBlog/issues" target="_blank" rel="noopener noreferrer">Issues</a> 提反馈哦会尽快修的
</p>
</div>
</div>
<div class="rounded-xl border px-5 py-4" style="border-color: var(--color-border); background-color: var(--color-bg-card);">
<h2 class="mb-3 text-sm font-semibold text-muted-foreground">小日历</h2>
<div class="flex flex-wrap items-center gap-x-5 gap-y-3 text-sm">
<div class="flex items-center gap-2">
<Icon icon="mdi:calendar-weekend" class="h-5 w-5 text-muted-foreground" aria-hidden="true" />
<span class="text-muted-foreground">{{ weekendStatus.label }}</span>
<span class="font-semibold text-primary">{{ weekendStatus.value }}</span>
</div>
<div v-if="nextHoliday" class="flex items-center gap-2">
<Icon icon="mdi:pine-tree" class="h-5 w-5 text-muted-foreground" aria-hidden="true" />
<span class="text-muted-foreground">{{ nextHoliday.name }}</span>
<span class="font-semibold text-primary">{{ holidayDays }} </span>
</div>
</div>
</div>
</div>
</section>
<!-- 内容入口 -->
<section class="anim-fade-in-up anim-delay-2">
<div class="mb-5 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div>
<h2 class="text-2xl font-bold">文章更新</h2>
<p class="mt-1 text-sm text-muted-foreground">
{{ visibleHomePosts.length }} 篇文章 · {{ homeSeriesCount }} 个系列 · {{ homeTagCount }} 个标签
</p>
</div>
<div class="flex flex-wrap gap-2">
<NuxtLink
v-for="link in contentLinks"
:key="link.href"
:to="link.href"
class="btn-pill"
>
<Icon :icon="link.icon" class="h-5 w-5" />
<span>{{ link.label }}</span>
</NuxtLink>
</div>
</div>
<div class="overflow-hidden rounded-xl border" style="border-color: var(--color-border); background-color: var(--color-bg-card);">
<NuxtLink
v-for="post in featuredPosts"
:key="post.path || post.id"
:to="post.path || '/posts'"
class="group block border-b px-5 py-4 transition-colors last:border-b-0 hover:bg-muted"
style="border-color: var(--color-border);"
>
<div class="mb-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span
v-if="post.pinned"
class="rounded-full px-2 py-0.5 font-medium"
style="background: var(--color-text); color: var(--color-bg);"
>
置顶
</span>
<time v-if="post.published">{{ formatPostDate(post.published) }}</time>
<span v-if="post.series">· {{ post.series }}</span>
<span v-if="post.difficulty">· {{ post.difficulty }}</span>
</div>
<h3 class="font-semibold transition-opacity group-hover:opacity-70">
{{ post.title }}
</h3>
<p v-if="post.description" class="mt-1 line-clamp-2 text-sm leading-relaxed text-muted-foreground">
{{ post.description }}
</p>
</NuxtLink>
<p v-if="featuredPosts.length === 0" class="px-5 py-6 text-center text-sm text-muted-foreground">
暂无文章
</p>
</div>
<div v-if="homeSeries.length" class="mt-4 flex flex-wrap items-center gap-2">
<span class="text-sm text-muted-foreground">系列入口</span>
<NuxtLink
v-for="series in homeSeries"
:key="series.name"
:to="series.path"
class="tag-pill"
>
{{ series.name }} · {{ series.count }}
</NuxtLink>
</div>
</section>
</div>
</template>
<style scoped>
@keyframes rainbow-flow {
0% { background-position: 0% 50%; }
100% { background-position: 200% 50%; }
}
.announcement-happy {
animation: rainbow-flow 3s linear infinite;
}
.announcement-text,
.announcement-text * {
background: linear-gradient(90deg,
oklch(0.78 0.18 0),
oklch(0.78 0.18 45),
oklch(0.78 0.18 90),
oklch(0.78 0.18 135),
oklch(0.78 0.18 180),
oklch(0.78 0.18 225),
oklch(0.78 0.18 270),
oklch(0.78 0.18 315),
oklch(0.78 0.18 360)
);
background-size: 200% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: rainbow-flow 3s linear infinite;
}
</style>