数据结构优化

This commit is contained in:
李琦
2026-01-20 15:23:37 +08:00
parent 946855bff2
commit cdbd3dc038
14 changed files with 835 additions and 198 deletions

View File

@@ -7,21 +7,31 @@ 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)', // 点击波纹颜色
starCount: 600, // 大幅增加星星数量
rotationSpeed: 0.0002, // 减慢旋转速度,更具呼吸感
clickMeteorChance: 1.0 // 点击触发流星概率
}
// 类型定义
interface Star {
x: number
y: number
radius: number
alpha: number
fadingOut: boolean
speed: number
// 轨道参数
orbitRadius: number
angle: number
angularSpeed: number
// 属性参数
radius: number // 显示半径
// 闪烁参数
alpha: number // 当前透明度
baseAlpha: number // 基础透明度
twinkleSpeed: number
twinklePhase: number
}
interface Meteor {
@@ -30,6 +40,8 @@ interface Meteor {
length: number
speed: number
angle: number
alpha: number
width: number
}
interface Wave {
@@ -37,6 +49,8 @@ interface Wave {
y: number
radius: number
alpha: number
lineWidth: number
hue: number
}
interface Particle {
@@ -46,213 +60,257 @@ interface Particle {
vy: number
life: number
color: string
size: number
}
// 状态
let ctx: CanvasRenderingContext2D | null = null
let animationFrameId: number
let width = 0
let height = 0
let centerX = 0
let centerY = 0
// 状态存储
// 对象池
const stars: Star[] = []
const meteors: Meteor[] = []
const waves: Wave[] = []
const particles: Particle[] = []
// 初始化星星
// 初始化星星 (星系分布 + 大小差异化)
const initStars = () => {
stars.length = 0
const maxRadius = Math.sqrt(width * width + height * height) * 0.8
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 orbitRadius = Math.random() * maxRadius
const angle = Math.random() * Math.PI * 2
// 绘制并更新星星
const drawStars = () => {
if (!ctx) return
// 模拟不同大小的星星分布
// 80% 是微小的背景星15% 是中等星星5% 是亮眼的大星
const sizeRandom = Math.random()
let radius, baseAlpha
ctx.fillStyle = '#FFF'
stars.forEach(star => {
// 闪烁逻辑
if (star.fadingOut) {
star.alpha -= star.speed
if (star.alpha < 0) {
star.alpha = 0
star.fadingOut = false
}
if (sizeRandom > 0.95) {
// 大星 (Hero Stars)
radius = Math.random() * 1.5 + 1.2
baseAlpha = Math.random() * 0.4 + 0.6 // 比较亮
} else if (sizeRandom > 0.8) {
// 中星
radius = Math.random() * 0.8 + 0.6
baseAlpha = Math.random() * 0.3 + 0.3
} else {
star.alpha += star.speed
if (star.alpha > 1) {
star.alpha = 1
star.fadingOut = true
}
// 微星 (Background Dust)
radius = Math.random() * 0.4 + 0.2
baseAlpha = Math.random() * 0.3 + 0.1
}
if (ctx) {
ctx.globalAlpha = star.alpha
ctx.beginPath()
ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2)
ctx.fill()
}
})
if (ctx) {
ctx.globalAlpha = 1
}
}
// 旋转速度微调:外圈通常比内圈慢(或快,取决于想要的视觉效果),这里给一点随机性
const speedVariation = (Math.random() - 0.5) * 0.0001
const angularSpeed = config.rotationSpeed + speedVariation
// 生成流星
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度角
stars.push({
x: 0, y: 0,
orbitRadius,
angle,
angularSpeed,
radius,
alpha: baseAlpha,
baseAlpha,
twinkleSpeed: Math.random() * 0.02 + 0.005,
twinklePhase: Math.random() * Math.PI * 2
})
}
}
// 绘制并更新流星
const drawMeteors = () => {
if (!ctx) return
// 渲染循环
const render = () => {
if (!ctx || !canvasRef.value) return
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)'
ctx.lineWidth = 1.5
// 1. 清空画布
ctx.clearRect(0, 0, width, height)
// 2. 绘制星星
stars.forEach(star => {
// A. 更新角度 (纯自转,不再跟随鼠标)
star.angle += star.angularSpeed
// B. 计算位置
star.x = centerX + Math.cos(star.angle) * star.orbitRadius
star.y = centerY + Math.sin(star.angle) * star.orbitRadius
// C. 闪烁计算 (呼吸感)
star.twinklePhase += star.twinkleSpeed
const twinkle = Math.sin(star.twinklePhase) * 0.2
star.alpha = star.baseAlpha + twinkle
// 限制 alpha 范围
if (star.alpha < 0) star.alpha = 0
if (star.alpha > 1) star.alpha = 1
// 绘制
ctx!.beginPath()
ctx!.arc(star.x, star.y, star.radius, 0, Math.PI * 2)
// 只给大星星加一点辉光,增强设计感
if (star.radius > 1.2) {
ctx!.shadowBlur = 6
ctx!.shadowColor = `rgba(255, 255, 255, ${star.alpha * 0.5})`
} else {
ctx!.shadowBlur = 0
}
ctx!.fillStyle = `rgba(255, 255, 255, ${star.alpha})`
ctx!.fill()
})
ctx.shadowBlur = 0 // 重置辉光
// 3. 绘制流星
updateAndDrawMeteors()
// 4. 绘制波纹 (Shockwave)
updateAndDrawWaves()
// 5. 绘制粒子 (Sparks)
updateAndDrawParticles()
animationFrameId = requestAnimationFrame(render)
}
// --- 辅助绘制函数 ---
const updateAndDrawMeteors = () => {
if (Math.random() < 0.002) createMeteor() // 稍微降低自然流星频率,减少干扰
for (let i = meteors.length - 1; i >= 0; i--) {
const m = meteors[i]
if (!ctx) break
// 计算尾巴终点
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
gradient.addColorStop(0, `rgba(255, 255, 255, ${m.alpha})`)
gradient.addColorStop(0.2, `rgba(200, 230, 255, ${m.alpha * 0.8})`)
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)')
ctx.beginPath()
ctx.moveTo(m.x, m.y)
ctx.lineTo(endX, endY)
ctx.lineWidth = m.width
ctx.strokeStyle = gradient
ctx.lineCap = 'round'
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)
if (m.x > width + 100 || m.x < -100 || m.y > height + 100 || m.y < -100) {
m.alpha -= 0.02
if (m.alpha <= 0) meteors.splice(i, 1)
}
}
}
// 绘制并更新波纹(点击效果)
const drawWaves = () => {
if (!ctx) return
const updateAndDrawWaves = () => {
for (let i = waves.length - 1; i >= 0; i--) {
const w = waves[i]
if (!ctx) break
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.strokeStyle = `hsla(${w.hue}, 80%, 70%, ${w.alpha})`
ctx.lineWidth = w.lineWidth
ctx.stroke()
ctx.globalAlpha = 1
w.radius += 2
w.alpha -= 0.02
w.radius += 2.5
w.alpha *= 0.95
w.lineWidth *= 0.95
if (w.alpha <= 0) {
waves.splice(i, 1)
}
if (w.alpha < 0.01) waves.splice(i, 1)
}
}
// 绘制并更新粒子(点击爆炸效果)
const drawParticles = () => {
if (!ctx) return
const updateAndDrawParticles = () => {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i]
if (!ctx) break
ctx.fillStyle = p.color
ctx.beginPath()
ctx.arc(p.x, p.y, Math.random() * 2, 0, Math.PI * 2)
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2)
ctx.fill()
p.x += p.vx
p.y += p.vy
p.life -= 0.02
p.vy += 0.08
p.vx *= 0.96
p.vy *= 0.96
if (p.life <= 0) {
particles.splice(i, 1)
}
p.life -= 0.02
p.size *= 0.95
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 createMeteor = (force = false) => {
const startX = Math.random() * width
const angle = Math.PI / 4 + (Math.random() - 0.5) * 0.3
meteors.push({
x: startX,
y: -50,
length: Math.random() * 100 + 80,
speed: Math.random() * 15 + 12,
angle: angle,
alpha: 1,
width: Math.random() * 2 + 0.5
})
}
// 点击事件处理
// 交互处理
const handleClick = (e: MouseEvent) => {
// 添加扩散波纹
const hue = Math.floor(Math.random() * 360)
waves.push({
x: e.clientX,
y: e.clientY,
radius: 0,
alpha: 1
radius: 10,
alpha: 1,
lineWidth: 4,
hue: hue
})
// 添加爆炸粒子
const particleCount = 8
const particleCount = 16
for (let i = 0; i < particleCount; i++) {
const angle = (Math.PI * 2 / particleCount) * i
const speed = Math.random() * 3 + 1
const angle = (Math.PI * 2 / particleCount) * i + Math.random() * 0.5
const speed = Math.random() * 5 + 2
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()})`
color: `hsla(${hue}, ${80 + Math.random() * 20}%, 70%, 1)`,
size: Math.random() * 3 + 1
})
}
createMeteor(true)
}
// 窗口大小调整
const handleResize = () => {
if (canvasRef.value) {
width = window.innerWidth
height = window.innerHeight
canvasRef.value.width = width
canvasRef.value.height = height
// 重新分布星星,或者只在必要时调整
centerX = width / 2
centerY = height / 2
initStars()
}
}
@@ -262,13 +320,11 @@ onMounted(() => {
ctx = canvasRef.value.getContext('2d')
handleResize()
// 初始化
initStars()
animate()
// 监听全局点击和resize
window.addEventListener('resize', handleResize)
// Removed mousemove listener for pure atmospheric effect
window.addEventListener('click', handleClick)
render()
}
})
@@ -280,7 +336,6 @@ onUnmounted(() => {
</script>
<style scoped>
/* 确保画布在最底层但又不影响其他背景色(如果需要混合模式可以调整) */
canvas {
display: block;
}

View File

@@ -1,7 +1,8 @@
<template>
<Teleport to="body">
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm" @click.self="close">
<div class="bg-[#1a1a1a] border border-white/10 rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] overflow-hidden animate-reveal flex flex-col">
<Transition name="modal">
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm" @click.self="close">
<div class="bg-[#1a1a1a] border border-white/10 rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] overflow-hidden flex flex-col">
<div class="p-4 border-b border-white/10 flex justify-between items-center shrink-0">
<h3 class="text-lg font-serif italic text-white">访问记录 - {{ postTitle }}</h3>
<button @click="close" class="text-white/50 hover:text-white">
@@ -28,25 +29,20 @@
<table class="w-full border-collapse">
<thead>
<tr class="border-b border-white/10">
<th class="text-left py-2 px-3 text-sm font-medium text-white/80">用户ID</th>
<th class="text-left py-2 px-3 text-sm font-medium text-white/80">IP地址</th>
<th class="text-left py-2 px-3 text-sm font-medium text-white/80">归属地</th>
<th class="text-left py-2 px-3 text-sm font-medium text-white/80">访问路径</th>
<th class="text-left py-2 px-3 text-sm font-medium text-white/80">状态码</th>
<th class="text-left py-2 px-3 text-sm font-medium text-white/80">响应时间</th>
<th class="text-left py-2 px-3 text-sm font-medium text-white/80">访问时间</th>
</tr>
</thead>
<tbody>
<tr v-for="log in logs.list" :key="log.id" class="border-b border-white/5 hover:bg-white/5 transition-colors">
<td class="py-2 px-3 text-sm text-white/90">
<span v-if="log.userId && log.userId > 0" class="text-art-accent">用户 #{{ log.userId }}</span>
<span v-else class="text-art-muted">游客</span>
</td>
<td class="py-2 px-3 text-sm text-white/90 font-mono">{{ log.ip }}</td>
<td class="py-2 px-3 text-sm text-white/80">{{ log.region || 'Unknown' }}</td>
<td class="py-2 px-3 text-sm text-white/80 font-mono">{{ log.path }}</td>
<td class="py-2 px-3 text-sm">
<span :class="getStatusClass(log.statusCode)">
{{ log.statusCode }}
</span>
</td>
<td class="py-2 px-3 text-sm text-white/80">{{ log.responseTime }}ms</td>
<td class="py-2 px-3 text-sm text-white/80">{{ log.createdAt }}</td>
</tr>
</tbody>
@@ -77,12 +73,13 @@
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { getPostAccessLogs, AccessLog, PaginationResponse } from '../../services/api'
import { getPostAccessLogs, PostAccessLog, PaginationResponse } from '../../services/api'
import { useToast } from '../../composables/useToast'
interface Props {
@@ -101,7 +98,7 @@ const emit = defineEmits<{
const toast = useToast()
const loading = ref(false)
const logs = ref<PaginationResponse<AccessLog>>({
const logs = ref<PaginationResponse<PostAccessLog>>({
list: [],
total: 0,
page: 1,
@@ -138,16 +135,6 @@ const changePage = (page: number) => {
fetchLogs()
}
const getStatusClass = (status: number) => {
if (status >= 200 && status < 300) {
return 'text-green-400'
} else if (status >= 400 && status < 500) {
return 'text-yellow-400'
} else if (status >= 500) {
return 'text-red-400'
}
return 'text-white/60'
}
watch(() => props.isOpen, (newVal) => {
if (newVal && props.postId) {
@@ -165,18 +152,24 @@ watch(() => props.postId, () => {
</script>
<style scoped>
.animate-reveal {
animation: reveal 0.3s ease-out;
/* Modal transition */
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.3s ease;
}
@keyframes reveal {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
.modal-enter-active .bg-\[#1a1a1a\] {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
.modal-enter-from .bg-\[#1a1a1a\],
.modal-leave-to .bg-\[#1a1a1a\] {
transform: translateY(-20px);
opacity: 0;
}
</style>