feat: 暗色模式星空背景、主题切换顺序优化

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 00:06:02 +08:00
parent 19b770c948
commit 45fda56fb8
5 changed files with 170 additions and 18 deletions
+2 -2
View File
@@ -12,11 +12,11 @@ const psBeian = config.public.psBeian
&copy; {{ new Date().getFullYear() }} {{ siteConfig.siteName }} &copy; {{ new Date().getFullYear() }} {{ siteConfig.siteName }}
<template v-if="icp"> <template v-if="icp">
&nbsp;·&nbsp; &nbsp;·&nbsp;
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer" class="hover:opacity-70" style="border-bottom: 1px solid var(--color-text-secondary); padding-bottom: 1px;">{{ icp }}</a> <a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer" class="hover:opacity-70 whitespace-nowrap" style="border-bottom: 1px solid var(--color-text-secondary); padding-bottom: 1px;">{{ icp }}</a>
</template> </template>
<template v-if="psBeian"> <template v-if="psBeian">
&nbsp;·&nbsp; &nbsp;·&nbsp;
<a :href="`https://beian.mps.gov.cn/#/query/webSearch?code=${psBeian}`" rel="noreferrer" target="_blank" class="hover:opacity-70" style="border-bottom: 1px solid var(--color-text-secondary); padding-bottom: 1px;"> <a :href="`https://beian.mps.gov.cn/#/query/webSearch?code=${psBeian}`" rel="noreferrer" target="_blank" class="hover:opacity-70 whitespace-nowrap" style="border-bottom: 1px solid var(--color-text-secondary); padding-bottom: 1px;">
<img src="@/assets/备案图标.png" alt="" class="w-3 h-3 inline" />川公网安备{{ psBeian }} <img src="@/assets/备案图标.png" alt="" class="w-3 h-3 inline" />川公网安备{{ psBeian }}
</a> </a>
</template> </template>
+151
View File
@@ -0,0 +1,151 @@
<script setup lang="ts">
const canvasRef = ref<HTMLCanvasElement>()
let ctx: CanvasRenderingContext2D | null = null
let animId = 0
let stars: Star[] = []
let shootingStars: ShootingStar[] = []
let nextShootTime = 0
interface Star {
x: number
y: number
size: number
baseAlpha: number
twinkleSpeed: number
twinklePhase: number
}
interface ShootingStar {
x: number
y: number
speed: number
length: number
alpha: number
angle: number
}
function init() {
const canvas = canvasRef.value
if (!canvas || ctx) return
canvas.width = window.innerWidth
canvas.height = window.innerHeight
ctx = canvas.getContext('2d')
const count = Math.floor((canvas.width * canvas.height) / 2000)
stars = Array.from({ length: count }, () => ({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
size: 0.3 + Math.random() * 2.0,
baseAlpha: 0.3 + Math.random() * 0.7,
twinkleSpeed: 0.2 + Math.random() * 0.8,
twinklePhase: Math.random() * Math.PI * 2,
}))
}
function scheduleShootingStar() {
nextShootTime = Date.now() + 2000 + Math.random() * 8000
}
function spawnShootingStar(w: number, h: number) {
const angle = Math.PI / 4 + (Math.random() - 0.5) * 0.3
shootingStars.push({
x: 0,
y: Math.random() * h * 0.4,
speed: 6 + Math.random() * 8,
length: 60 + Math.random() * 100,
alpha: 1,
angle,
})
}
function isDark(): boolean {
return document.documentElement.classList.contains('dark')
}
function animate() {
const canvas = canvasRef.value
if (!canvas || !ctx) return
if (!isDark()) {
ctx.clearRect(0, 0, canvas.width, canvas.height)
animId = requestAnimationFrame(animate)
return
}
const { width, height } = canvas
ctx.clearRect(0, 0, width, height)
const now = Date.now()
// Stars
for (const s of stars) {
const alpha = s.baseAlpha * (0.5 + 0.5 * Math.sin(now * s.twinkleSpeed * 0.001 + s.twinklePhase))
ctx.beginPath()
ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2)
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`
ctx.fill()
}
// Shooting stars
if (now > nextShootTime && shootingStars.length < 4) {
spawnShootingStar(width, height)
scheduleShootingStar()
}
for (let i = shootingStars.length - 1; i >= 0; i--) {
const ss = shootingStars[i]
ss.x += Math.cos(ss.angle) * ss.speed
ss.y += Math.sin(ss.angle) * ss.speed
ss.alpha -= 0.005
if (ss.alpha <= 0 || ss.x > width + ss.length || ss.y > height) {
shootingStars.splice(i, 1)
continue
}
const tailX = ss.x - Math.cos(ss.angle) * ss.length
const tailY = ss.y - Math.sin(ss.angle) * ss.length
const gradient = ctx.createLinearGradient(tailX, tailY, ss.x, ss.y)
gradient.addColorStop(0, `rgba(255, 255, 255, 0)`)
gradient.addColorStop(1, `rgba(255, 255, 255, ${ss.alpha})`)
ctx.beginPath()
ctx.moveTo(tailX, tailY)
ctx.lineTo(ss.x, ss.y)
ctx.strokeStyle = gradient
ctx.lineWidth = 1.5
ctx.stroke()
}
animId = requestAnimationFrame(animate)
}
onMounted(async () => {
if (typeof window === 'undefined' || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
await nextTick()
init()
animate()
const onResize = () => {
const canvas = canvasRef.value
if (!canvas) return
canvas.width = window.innerWidth
canvas.height = window.innerHeight
const count = Math.floor((canvas.width * canvas.height) / 2000)
stars = Array.from({ length: count }, () => ({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
size: 0.3 + Math.random() * 2.0,
baseAlpha: 0.3 + Math.random() * 0.7,
twinkleSpeed: 0.2 + Math.random() * 0.8,
twinklePhase: Math.random() * Math.PI * 2,
}))
}
window.addEventListener('resize', onResize)
})
const style = `position: fixed; inset: 0; z-index: 0; pointer-events: none;`
</script>
<template>
<canvas ref="canvasRef" :style="style" />
</template>
+13 -9
View File
@@ -2,29 +2,33 @@
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue'
const colorMode = useColorMode() const colorMode = useColorMode()
type Mode = 'system' | 'dark' | 'light'
const modes = ['light', 'dark', 'system'] as const
type Mode = typeof modes[number]
const iconMap: Record<Mode, string> = { const iconMap: Record<Mode, string> = {
light: 'mdi:weather-sunny',
dark: 'mdi:weather-night',
system: 'mdi:monitor', system: 'mdi:monitor',
dark: 'mdi:weather-night',
light: 'mdi:weather-sunny',
} }
const labelMap: Record<Mode, string> = { const labelMap: Record<Mode, string> = {
light: '浅色模式',
dark: '深色模式',
system: '跟随系统', system: '跟随系统',
dark: '深色模式',
light: '浅色模式',
} }
const cycleOrder = computed<Mode[]>(() => {
const isDark = colorMode.value === 'dark'
return isDark ? ['system', 'light', 'dark'] : ['system', 'dark', 'light']
})
const currentMode = computed(() => colorMode.preference as Mode) const currentMode = computed(() => colorMode.preference as Mode)
const currentIcon = computed(() => iconMap[currentMode.value] || iconMap.system) const currentIcon = computed(() => iconMap[currentMode.value] || iconMap.system)
const currentLabel = computed(() => labelMap[currentMode.value] || labelMap.system) const currentLabel = computed(() => labelMap[currentMode.value] || labelMap.system)
function cycle() { function cycle() {
const idx = modes.indexOf(currentMode.value) const order = cycleOrder.value
colorMode.preference = modes[(idx + 1) % modes.length] const idx = order.indexOf(currentMode.value)
colorMode.preference = order[(idx + 1) % order.length]
} }
</script> </script>
+1 -5
View File
@@ -48,8 +48,4 @@ export const siteConfig = {
repoId: '', repoId: '',
category: 'Announcements', category: 'Announcements',
categoryId: '', categoryId: '',
}, },}
// ICP 备案号(留空则不显示)
icp: '',
}
+3 -2
View File
@@ -24,9 +24,10 @@ useSeoMeta({
</script> </script>
<template> <template>
<div class="min-h-screen flex flex-col"> <div class="min-h-screen flex flex-col relative z-10">
<LayoutStarField />
<LayoutNavBar /> <LayoutNavBar />
<main class="flex-1"> <main class="flex-1 relative z-10">
<slot /> <slot />
</main> </main>
<LayoutFooter /> <LayoutFooter />