19b770c948
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
220 lines
7.5 KiB
Vue
220 lines
7.5 KiB
Vue
<script setup lang="ts">
|
||
import { Icon } from '@iconify/vue'
|
||
import { siteConfig } from '~/config/site'
|
||
|
||
const now = new Date()
|
||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||
|
||
// 下一个周六或周日
|
||
const dayOfWeek = today.getDay()
|
||
const daysToSat = dayOfWeek === 6 ? 0 : (6 - dayOfWeek + 7) % 7
|
||
const nextWeekend = new Date(today)
|
||
nextWeekend.setDate(today.getDate() + daysToSat)
|
||
|
||
const weekendDays = daysUntil(nextWeekend)
|
||
|
||
// 从 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)
|
||
}
|
||
|
||
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'))
|
||
|
||
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)
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="flex flex-col items-center gap-8 px-4 pt-4 pb-12">
|
||
|
||
<!-- 公告卡片 -->
|
||
<div class="w-full max-w-2xl text-center anim-fade-in-up">
|
||
<div class="announcement-happy rounded-xl p-6">
|
||
<p class="announcement-text text-sm md:text-base">
|
||
<strong>欢迎来到《{{ siteConfig.siteName }}》~ 用代码创造快乐,用热爱点亮生活 (☞゚ヮ゚)☞</strong>
|
||
</p>
|
||
<p class="announcement-text text-sm md:text-base mt-1">
|
||
遇到问题的话,可以去 <a href="https://github.com/wishesl/SuBlog/issues" target="_blank" rel="noopener noreferrer">Issues</a> 提反馈哦,会尽快修的!
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 头像 -->
|
||
<div class="anim-fade-in-up anim-delay-1">
|
||
<img
|
||
:src="siteConfig.avatar"
|
||
:alt="siteConfig.siteName"
|
||
class="h-32 w-32 rounded-full shadow-lg"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 标题 + 口号 + 访客 -->
|
||
<div class="text-center anim-fade-in-up anim-delay-2">
|
||
<h1 class="text-4xl font-bold mb-2">{{ siteConfig.siteName }}</h1>
|
||
<p v-if="hitokoto" class="text-sm italic leading-relaxed mb-1" style="color: var(--color-text-secondary)">
|
||
"{{ hitokoto.content }}"
|
||
</p>
|
||
<p v-if="hitokoto" class="text-xs opacity-60 mb-1" style="color: var(--color-text-secondary)">
|
||
—— {{ hitokoto.user }} · {{ hitokoto.music }}
|
||
</p>
|
||
<p class="text-sm mt-6" style="color: var(--color-text-secondary)">
|
||
您是第 <span id="busuanzi_value_site_pv" class="font-medium" style="color: var(--color-primary)"></span> 个访客
|
||
</p>
|
||
</div>
|
||
|
||
<!-- 放假倒计时 -->
|
||
<div class="w-full max-w-sm anim-fade-in-up anim-delay-3">
|
||
<div class="card relative overflow-hidden py-4">
|
||
<div class="flex items-center justify-center gap-6 text-sm">
|
||
<div class="flex items-center gap-2">
|
||
<img src="https://api.iconify.design/mdi:calendar-weekend.svg?color=%239ca3af" alt="" class="w-4 h-4" />
|
||
<span class="text-muted-foreground">周末</span>
|
||
<span class="font-semibold" style="color: var(--color-primary);">{{ weekendDays }} 天</span>
|
||
</div>
|
||
<template v-if="nextHoliday">
|
||
<span class="text-muted-foreground/30">|</span>
|
||
<div class="flex items-center gap-2">
|
||
<img src="https://api.iconify.design/mdi:pine-tree.svg?color=%239ca3af" alt="" class="w-4 h-4" />
|
||
<span class="text-muted-foreground">{{ nextHoliday.name }}</span>
|
||
<span class="font-semibold" style="color: var(--color-primary);">{{ holidayDays }} 天</span>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 社交卡片 -->
|
||
<div class="w-full max-w-2xl mx-auto anim-fade-in-up anim-delay-4">
|
||
<div class="card relative overflow-hidden py-6">
|
||
<div class="absolute inset-0 flex items-center justify-center pointer-events-none select-none z-0">
|
||
<span class="text-5xl font-black tracking-widest" style="color: var(--color-text); opacity: 0.04;">社交</span>
|
||
</div>
|
||
<div class="relative z-10 px-6 flex flex-wrap gap-3 justify-center">
|
||
<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="w-5 h-5"
|
||
/>
|
||
<span>{{ link.name }}</span>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 导航卡片 -->
|
||
<div class="w-full max-w-2xl mx-auto anim-fade-in-up anim-delay-5">
|
||
<div class="card relative overflow-hidden py-6">
|
||
<div class="absolute inset-0 flex items-center justify-center pointer-events-none select-none z-0">
|
||
<span class="text-5xl font-black tracking-widest" style="color: var(--color-text); opacity: 0.04;">导航</span>
|
||
</div>
|
||
<div class="relative z-10 px-6 flex flex-wrap gap-3 justify-center">
|
||
<NuxtLink
|
||
v-for="link in siteConfig.navLinks"
|
||
:key="link.href"
|
||
:to="link.href"
|
||
class="btn-pill"
|
||
>
|
||
<Icon :icon="link.icon" class="w-5 h-5" />
|
||
<span>{{ link.label }}</span>
|
||
</NuxtLink>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</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>
|