数据结构优化
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<header class="fixed top-0 w-full z-50 glass-nav transition-all duration-300" id="main-header">
|
||||
<div class="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
|
||||
<div class="cursor-pointer group" @click="goTo('/')">
|
||||
<span class="font-serif text-2xl italic tracking-wider text-white group-hover:text-art-accent transition-colors">年糕崽崽.Dev</span>
|
||||
<span class="font-serif text-2xl italic tracking-wider text-white group-hover:text-art-accent transition-colors">{{ siteTitle || '年糕崽崽.Dev' }}</span>
|
||||
</div>
|
||||
<nav class="hidden md:flex items-center gap-10">
|
||||
<button
|
||||
@@ -73,10 +73,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUpdated, watch, nextTick } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { getPublicSettings } from '../services/api'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const activeNav = ref('home')
|
||||
const siteTitle = ref<string>('')
|
||||
|
||||
const toggleMobileMenu = () => {
|
||||
const menu = document.getElementById('mobile-menu')
|
||||
@@ -114,9 +116,23 @@ const refreshIcons = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取网站配置
|
||||
const loadSiteSettings = async () => {
|
||||
try {
|
||||
const settings = await getPublicSettings()
|
||||
if (settings.site_title) {
|
||||
siteTitle.value = settings.site_title
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load site settings:', error)
|
||||
// 使用默认值,不抛出错误
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateActiveNav()
|
||||
refreshIcons()
|
||||
loadSiteSettings()
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
|
||||
283
client/src/components/StarBackground.vue
Normal file
283
client/src/components/StarBackground.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<template>
|
||||
<canvas ref="canvasRef" class="fixed inset-0 w-full h-full z-0 pointer-events-none"></canvas>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
|
||||
// 配置参数
|
||||
const config = {
|
||||
starCount: 150, // 星星数量
|
||||
meteorChance: 0.005, // 流星出现概率 (每帧)
|
||||
clickWaveColor: 'rgba(100, 200, 255, 0.4)', // 点击波纹颜色
|
||||
}
|
||||
|
||||
// 类型定义
|
||||
interface Star {
|
||||
x: number
|
||||
y: number
|
||||
radius: number
|
||||
alpha: number
|
||||
fadingOut: boolean
|
||||
speed: number
|
||||
}
|
||||
|
||||
interface Meteor {
|
||||
x: number
|
||||
y: number
|
||||
length: number
|
||||
speed: number
|
||||
angle: number
|
||||
}
|
||||
|
||||
interface Wave {
|
||||
x: number
|
||||
y: number
|
||||
radius: number
|
||||
alpha: number
|
||||
}
|
||||
|
||||
interface Particle {
|
||||
x: number
|
||||
y: number
|
||||
vx: number
|
||||
vy: number
|
||||
life: number
|
||||
color: string
|
||||
}
|
||||
|
||||
let ctx: CanvasRenderingContext2D | null = null
|
||||
let animationFrameId: number
|
||||
let width = 0
|
||||
let height = 0
|
||||
|
||||
// 状态存储
|
||||
const stars: Star[] = []
|
||||
const meteors: Meteor[] = []
|
||||
const waves: Wave[] = []
|
||||
const particles: Particle[] = []
|
||||
|
||||
// 初始化星星
|
||||
const initStars = () => {
|
||||
stars.length = 0
|
||||
for (let i = 0; i < config.starCount; i++) {
|
||||
stars.push({
|
||||
x: Math.random() * width,
|
||||
y: Math.random() * height,
|
||||
radius: Math.random() * 1.5,
|
||||
alpha: Math.random(),
|
||||
fadingOut: Math.random() > 0.5,
|
||||
speed: Math.random() * 0.02 + 0.005
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制并更新星星
|
||||
const drawStars = () => {
|
||||
if (!ctx) return
|
||||
|
||||
ctx.fillStyle = '#FFF'
|
||||
stars.forEach(star => {
|
||||
// 闪烁逻辑
|
||||
if (star.fadingOut) {
|
||||
star.alpha -= star.speed
|
||||
if (star.alpha < 0) {
|
||||
star.alpha = 0
|
||||
star.fadingOut = false
|
||||
}
|
||||
} else {
|
||||
star.alpha += star.speed
|
||||
if (star.alpha > 1) {
|
||||
star.alpha = 1
|
||||
star.fadingOut = true
|
||||
}
|
||||
}
|
||||
|
||||
ctx.globalAlpha = star.alpha
|
||||
ctx.beginPath()
|
||||
ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
})
|
||||
ctx.globalAlpha = 1
|
||||
}
|
||||
|
||||
// 生成流星
|
||||
const spawnMeteor = () => {
|
||||
if (Math.random() < config.meteorChance) {
|
||||
const startX = Math.random() * width
|
||||
// 稍微倾斜的角度
|
||||
meteors.push({
|
||||
x: startX,
|
||||
y: 0,
|
||||
length: Math.random() * 80 + 20,
|
||||
speed: Math.random() * 10 + 5,
|
||||
angle: Math.PI / 4 // 45度角
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制并更新流星
|
||||
const drawMeteors = () => {
|
||||
if (!ctx) return
|
||||
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)'
|
||||
ctx.lineWidth = 1.5
|
||||
|
||||
for (let i = meteors.length - 1; i >= 0; i--) {
|
||||
const m = meteors[i]
|
||||
|
||||
// 计算尾巴终点
|
||||
const endX = m.x - m.length * Math.cos(m.angle)
|
||||
const endY = m.y - m.length * Math.sin(m.angle)
|
||||
|
||||
// 绘制流星
|
||||
const gradient = ctx.createLinearGradient(m.x, m.y, endX, endY)
|
||||
gradient.addColorStop(0, 'rgba(255,255,255,1)')
|
||||
gradient.addColorStop(1, 'rgba(255,255,255,0)')
|
||||
ctx.strokeStyle = gradient
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(m.x, m.y)
|
||||
ctx.lineTo(endX, endY)
|
||||
ctx.stroke()
|
||||
|
||||
// 移动
|
||||
m.x += m.speed * Math.cos(m.angle)
|
||||
m.y += m.speed * Math.sin(m.angle)
|
||||
|
||||
// 移除出屏幕的流星
|
||||
if (m.x > width || m.y > height) {
|
||||
meteors.splice(i, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制并更新波纹(点击效果)
|
||||
const drawWaves = () => {
|
||||
if (!ctx) return
|
||||
|
||||
for (let i = waves.length - 1; i >= 0; i--) {
|
||||
const w = waves[i]
|
||||
ctx.beginPath()
|
||||
ctx.arc(w.x, w.y, w.radius, 0, Math.PI * 2)
|
||||
ctx.strokeStyle = config.clickWaveColor
|
||||
ctx.globalAlpha = w.alpha
|
||||
ctx.lineWidth = 2
|
||||
ctx.stroke()
|
||||
ctx.globalAlpha = 1
|
||||
|
||||
w.radius += 2
|
||||
w.alpha -= 0.02
|
||||
|
||||
if (w.alpha <= 0) {
|
||||
waves.splice(i, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制并更新粒子(点击爆炸效果)
|
||||
const drawParticles = () => {
|
||||
if (!ctx) return
|
||||
|
||||
for (let i = particles.length - 1; i >= 0; i--) {
|
||||
const p = particles[i]
|
||||
ctx.fillStyle = p.color
|
||||
ctx.beginPath()
|
||||
ctx.arc(p.x, p.y, Math.random() * 2, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
|
||||
p.x += p.vx
|
||||
p.y += p.vy
|
||||
p.life -= 0.02
|
||||
|
||||
if (p.life <= 0) {
|
||||
particles.splice(i, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 动画主循环
|
||||
const animate = () => {
|
||||
if (!ctx || !canvasRef.value) return
|
||||
|
||||
// 清空画布 (使用带有透明度的黑色可以制造拖尾效果,这里我们直接清空保持清晰)
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
// 绘制层级
|
||||
drawStars()
|
||||
spawnMeteor()
|
||||
drawMeteors()
|
||||
drawWaves()
|
||||
drawParticles()
|
||||
|
||||
animationFrameId = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
// 点击事件处理
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
// 添加扩散波纹
|
||||
waves.push({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
radius: 0,
|
||||
alpha: 1
|
||||
})
|
||||
|
||||
// 添加爆炸粒子
|
||||
const particleCount = 8
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
const angle = (Math.PI * 2 / particleCount) * i
|
||||
const speed = Math.random() * 3 + 1
|
||||
particles.push({
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
vx: Math.cos(angle) * speed,
|
||||
vy: Math.sin(angle) * speed,
|
||||
life: 1.0,
|
||||
color: `rgba(200, 230, 255, ${Math.random()})`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 窗口大小调整
|
||||
const handleResize = () => {
|
||||
if (canvasRef.value) {
|
||||
width = window.innerWidth
|
||||
height = window.innerHeight
|
||||
canvasRef.value.width = width
|
||||
canvasRef.value.height = height
|
||||
// 重新分布星星,或者只在必要时调整
|
||||
initStars()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (canvasRef.value) {
|
||||
ctx = canvasRef.value.getContext('2d')
|
||||
handleResize()
|
||||
|
||||
// 初始化
|
||||
initStars()
|
||||
animate()
|
||||
|
||||
// 监听全局点击和resize
|
||||
window.addEventListener('resize', handleResize)
|
||||
window.addEventListener('click', handleClick)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
window.removeEventListener('resize', handleResize)
|
||||
window.removeEventListener('click', handleClick)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 确保画布在最底层但又不影响其他背景色(如果需要混合模式可以调整) */
|
||||
canvas {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user