数据结构优化
This commit is contained in:
@@ -1,43 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-art-bg text-art-text">
|
<div class="min-h-screen bg-art-bg text-art-text relative overflow-hidden">
|
||||||
|
<!-- Star Background (Only for frontend) -->
|
||||||
|
<!-- Removed the gradient background div to keep original theme color -->
|
||||||
|
<StarBackground v-if="!isAdminOrLogin" class="z-0" />
|
||||||
|
|
||||||
<!-- Global Noise (Only for frontend) -->
|
<!-- Global Noise (Only for frontend) -->
|
||||||
|
<!-- z-index 提高到 60 保持在最上层 -->
|
||||||
<div v-if="!isAdminOrLogin" class="fixed inset-0 pointer-events-none z-[60] bg-noise opacity-30 mix-blend-overlay"></div>
|
<div v-if="!isAdminOrLogin" class="fixed inset-0 pointer-events-none z-[60] bg-noise opacity-30 mix-blend-overlay"></div>
|
||||||
|
|
||||||
<!-- Toast Container -->
|
<!-- Toast Container -->
|
||||||
<div id="toast-container" class="toast-container"></div>
|
<div id="toast-container" class="toast-container relative z-[70]"></div>
|
||||||
|
|
||||||
<!-- Header (Hide on Admin & Login) -->
|
<!-- Header (Hide on Admin & Login) -->
|
||||||
<Header v-if="!isAdminOrLogin" />
|
<Header v-if="!isAdminOrLogin" class="relative z-50" />
|
||||||
|
|
||||||
<!-- Mobile Menu (Hide on Admin & Login) -->
|
<!-- Mobile Menu (Hide on Admin & Login) -->
|
||||||
<MobileMenu v-if="!isAdminOrLogin" />
|
<MobileMenu v-if="!isAdminOrLogin" class="relative z-50" />
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<!-- Conditional classes: Apply max-w-7xl only for frontend pages -->
|
<!-- Conditional classes: Apply max-w-7xl only for frontend pages -->
|
||||||
|
<!-- 添加 relative z-10 确保内容浮在星星之上 -->
|
||||||
<main :class="[
|
<main :class="[
|
||||||
isAdminOrLogin ? 'w-full h-full' : 'pt-32 pb-20 px-6 max-w-7xl mx-auto relative z-10'
|
isAdminOrLogin ? 'w-full h-full' : 'pt-32 pb-20 px-6 max-w-7xl mx-auto relative z-10'
|
||||||
]">
|
]">
|
||||||
<router-view />
|
<router-view />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Footer (Hide on Admin & Login) -->
|
<!-- Footer (Hide on Admin & Login) -->
|
||||||
<Footer v-if="!isAdminOrLogin" />
|
<Footer v-if="!isAdminOrLogin" class="relative z-10" />
|
||||||
|
|
||||||
<!-- Snippet Modal (Hide on Admin & Login) -->
|
<!-- Snippet Modal (Hide on Admin & Login) -->
|
||||||
<!-- SnippetModal is managed by Snippets.vue page, not here -->
|
<!-- SnippetModal is managed by Snippets.vue page, not here -->
|
||||||
|
|
||||||
<!-- Inquiry Modal (Hide on Admin & Login) -->
|
<!-- Inquiry Modal (Hide on Admin & Login) -->
|
||||||
<InquiryModal v-if="!isAdminOrLogin" />
|
<InquiryModal v-if="!isAdminOrLogin" class="relative z-[80]" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import Header from './components/Header.vue'
|
import Header from './components/Header.vue'
|
||||||
import MobileMenu from './components/MobileMenu.vue'
|
import MobileMenu from './components/MobileMenu.vue'
|
||||||
import Footer from './components/Footer.vue'
|
import Footer from './components/Footer.vue'
|
||||||
import InquiryModal from './components/InquiryModal.vue'
|
import InquiryModal from './components/InquiryModal.vue'
|
||||||
|
import StarBackground from './components/StarBackground.vue' // Import the new component
|
||||||
|
import { getPublicSettings } from './services/api'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
@@ -46,4 +54,49 @@ const isAdminOrLogin = computed(() => {
|
|||||||
const path = route.path
|
const path = route.path
|
||||||
return path.startsWith('/admin') || path === '/login'
|
return path.startsWith('/admin') || path === '/login'
|
||||||
})
|
})
|
||||||
</script>
|
|
||||||
|
// 应用网站配置到meta标签(页面标题由路由守卫处理)
|
||||||
|
const applySiteSettings = async () => {
|
||||||
|
try {
|
||||||
|
const settings = await getPublicSettings()
|
||||||
|
|
||||||
|
// 更新meta标签
|
||||||
|
const updateMetaTag = (name: string, content: string) => {
|
||||||
|
if (!content) return
|
||||||
|
|
||||||
|
let meta = document.querySelector(`meta[name="${name}"]`)
|
||||||
|
if (!meta) {
|
||||||
|
meta = document.createElement('meta')
|
||||||
|
meta.setAttribute('name', name)
|
||||||
|
document.head.appendChild(meta)
|
||||||
|
}
|
||||||
|
meta.setAttribute('content', content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新description
|
||||||
|
if (settings.site_description) {
|
||||||
|
updateMetaTag('description', settings.site_description)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新keywords
|
||||||
|
if (settings.site_keywords) {
|
||||||
|
updateMetaTag('keywords', settings.site_keywords)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新author
|
||||||
|
if (settings.site_author) {
|
||||||
|
updateMetaTag('author', settings.site_author)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load site settings:', error)
|
||||||
|
// 使用默认标题,不抛出错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 只在非管理页面应用设置
|
||||||
|
if (!isAdminOrLogin.value) {
|
||||||
|
applySiteSettings()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<header class="fixed top-0 w-full z-50 glass-nav transition-all duration-300" id="main-header">
|
<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="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
|
||||||
<div class="cursor-pointer group" @click="goTo('/')">
|
<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>
|
</div>
|
||||||
<nav class="hidden md:flex items-center gap-10">
|
<nav class="hidden md:flex items-center gap-10">
|
||||||
<button
|
<button
|
||||||
@@ -73,10 +73,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUpdated, watch, nextTick } from 'vue'
|
import { ref, onMounted, onUpdated, watch, nextTick } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { getPublicSettings } from '../services/api'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const activeNav = ref('home')
|
const activeNav = ref('home')
|
||||||
|
const siteTitle = ref<string>('')
|
||||||
|
|
||||||
const toggleMobileMenu = () => {
|
const toggleMobileMenu = () => {
|
||||||
const menu = document.getElementById('mobile-menu')
|
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(() => {
|
onMounted(() => {
|
||||||
updateActiveNav()
|
updateActiveNav()
|
||||||
refreshIcons()
|
refreshIcons()
|
||||||
|
loadSiteSettings()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUpdated(() => {
|
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>
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed, nextTick, onBeforeUnmount } from 'vue'
|
import { ref, onMounted, computed, nextTick, onBeforeUnmount } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { fetchPost, Post } from '../services/api'
|
import { fetchPost, Post, getPublicSettings } from '../services/api'
|
||||||
import { useScrollAnimation } from '../composables/useScrollAnimation'
|
import { useScrollAnimation } from '../composables/useScrollAnimation'
|
||||||
|
|
||||||
// 引入 Markdown 解析依赖
|
// 引入 Markdown 解析依赖
|
||||||
@@ -345,6 +345,19 @@ const addCopyListeners = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新页面标题
|
||||||
|
const updatePageTitle = async (articleTitle: string) => {
|
||||||
|
try {
|
||||||
|
const settings = await getPublicSettings()
|
||||||
|
const siteTitle = settings.site_title || '年糕崽崽.Dev'
|
||||||
|
document.title = `${siteTitle} | ${articleTitle}`
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update page title:', error)
|
||||||
|
// 即使获取配置失败,也更新标题
|
||||||
|
document.title = `年糕崽崽.Dev | ${articleTitle}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fetchBlogDetail = async () => {
|
const fetchBlogDetail = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = ''
|
error.value = ''
|
||||||
@@ -352,6 +365,12 @@ const fetchBlogDetail = async () => {
|
|||||||
const postData = await fetchPost(postId)
|
const postData = await fetchPost(postId)
|
||||||
if (postData) {
|
if (postData) {
|
||||||
post.value = postData
|
post.value = postData
|
||||||
|
|
||||||
|
// 更新页面标题
|
||||||
|
if (postData.title) {
|
||||||
|
await updatePageTitle(postData.title)
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化动画、事件监听和目录 - 在数据加载完成后
|
// 初始化动画、事件监听和目录 - 在数据加载完成后
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -1,16 +1,65 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import { getPublicSettings } from './services/api'
|
||||||
|
|
||||||
|
// 缓存网站配置,避免重复获取
|
||||||
|
let cachedSiteTitle: string | null = null
|
||||||
|
let settingsLoaded = false
|
||||||
|
|
||||||
|
// 加载网站配置
|
||||||
|
const loadSiteTitle = async (): Promise<string> => {
|
||||||
|
if (cachedSiteTitle !== null) {
|
||||||
|
return cachedSiteTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settingsLoaded) {
|
||||||
|
settingsLoaded = true
|
||||||
|
try {
|
||||||
|
const settings = await getPublicSettings()
|
||||||
|
cachedSiteTitle = settings.site_title || '年糕崽崽.Dev'
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load site settings:', error)
|
||||||
|
cachedSiteTitle = '年糕崽崽.Dev'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedSiteTitle || '年糕崽崽.Dev'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置页面标题
|
||||||
|
const setPageTitle = async (to: any) => {
|
||||||
|
const siteTitle = await loadSiteTitle()
|
||||||
|
const routeMeta = to.matched.find((record: any) => record.meta?.title !== undefined)
|
||||||
|
|
||||||
|
// 如果是动态标题页面(如文章详情),先设置默认标题
|
||||||
|
if (routeMeta?.meta?.dynamic) {
|
||||||
|
// 动态标题会在组件中更新,这里先设置基础标题
|
||||||
|
document.title = siteTitle
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取页面标题
|
||||||
|
const pageTitle = routeMeta?.meta?.title || ''
|
||||||
|
|
||||||
|
// 设置完整标题
|
||||||
|
if (pageTitle) {
|
||||||
|
document.title = `${siteTitle} | ${pageTitle}`
|
||||||
|
} else {
|
||||||
|
// 首页只显示网站标题
|
||||||
|
document.title = siteTitle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: '/', name: 'home', component: () => import('./pages/Home.vue') },
|
{ path: '/', name: 'home', component: () => import('./pages/Home.vue'), meta: { title: '' } },
|
||||||
{ path: '/blog', name: 'blog', component: () => import('./pages/Blog.vue') },
|
{ path: '/blog', name: 'blog', component: () => import('./pages/Blog.vue'), meta: { title: '思考' } },
|
||||||
{ path: '/blog/:id', name: 'blog-detail', component: () => import('./pages/BlogDetail.vue') },
|
{ path: '/blog/:id', name: 'blog-detail', component: () => import('./pages/BlogDetail.vue'), meta: { title: '', dynamic: true } },
|
||||||
{ path: '/columns', name: 'columns', component: () => import('./pages/Columns.vue') },
|
{ path: '/columns', name: 'columns', component: () => import('./pages/Columns.vue'), meta: { title: '专栏' } },
|
||||||
{ path: '/columns/:id', name: 'column-detail', component: () => import('./pages/ColumnDetail.vue') },
|
{ path: '/columns/:id', name: 'column-detail', component: () => import('./pages/ColumnDetail.vue'), meta: { title: '', dynamic: true } },
|
||||||
{ path: '/works', name: 'works', component: () => import('./pages/Works.vue') },
|
{ path: '/works', name: 'works', component: () => import('./pages/Works.vue'), meta: { title: '作品' } },
|
||||||
{ path: '/works/:id', name: 'work-detail', component: () => import('./pages/WorkDetail.vue') },
|
{ path: '/works/:id', name: 'work-detail', component: () => import('./pages/WorkDetail.vue'), meta: { title: '', dynamic: true } },
|
||||||
{ path: '/snippets', name: 'snippets', component: () => import('./pages/Snippets.vue') },
|
{ path: '/snippets', name: 'snippets', component: () => import('./pages/Snippets.vue'), meta: { title: '代码' } },
|
||||||
{ path: '/services', name: 'services', component: () => import('./pages/Services.vue') },
|
{ path: '/services', name: 'services', component: () => import('./pages/Services.vue'), meta: { title: '合作' } },
|
||||||
{ path: '/about', name: 'about', component: () => import('./pages/About.vue') },
|
{ path: '/about', name: 'about', component: () => import('./pages/About.vue'), meta: { title: '关于' } },
|
||||||
// 登录路由
|
// 登录路由
|
||||||
{ path: '/login', name: 'login', component: () => import('./pages/Login.vue') },
|
{ path: '/login', name: 'login', component: () => import('./pages/Login.vue') },
|
||||||
|
|
||||||
@@ -113,8 +162,8 @@ const router = createRouter({
|
|||||||
routes
|
routes
|
||||||
})
|
})
|
||||||
|
|
||||||
// 路由守卫,用于保护需要认证的路由
|
// 路由守卫,用于保护需要认证的路由和设置页面标题
|
||||||
router.beforeEach((to, _from, next) => {
|
router.beforeEach(async (to, _from, next) => {
|
||||||
// 检查路由是否需要认证
|
// 检查路由是否需要认证
|
||||||
if (to.matched.some(record => record.meta.requiresAuth)) {
|
if (to.matched.some(record => record.meta.requiresAuth)) {
|
||||||
// 检查本地存储中是否有token
|
// 检查本地存储中是否有token
|
||||||
@@ -122,14 +171,17 @@ router.beforeEach((to, _from, next) => {
|
|||||||
if (!token) {
|
if (!token) {
|
||||||
// 没有token,重定向到登录页
|
// 没有token,重定向到登录页
|
||||||
next({ name: 'login' })
|
next({ name: 'login' })
|
||||||
} else {
|
return
|
||||||
// 有token,继续访问
|
|
||||||
next()
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// 不需要认证的路由,直接访问
|
|
||||||
next()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置页面标题(非管理页面)
|
||||||
|
if (!to.path.startsWith('/admin') && to.path !== '/login') {
|
||||||
|
await setPageTitle(to)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 继续路由导航
|
||||||
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
@@ -1083,6 +1083,30 @@ export const deleteSetting = async (keyName: string): Promise<void> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 公开设置API(前端使用,无需认证)
|
||||||
|
export interface PublicSettings {
|
||||||
|
site_title?: string
|
||||||
|
site_description?: string
|
||||||
|
site_author?: string
|
||||||
|
site_keywords?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPublicSettings = async (): Promise<PublicSettings> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/settings`)
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json()
|
||||||
|
throw new Error(errorData.message || '获取网站配置失败')
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
return data.result || {}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get public settings error:', error)
|
||||||
|
// 返回空对象,前端使用默认值
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 操作日志API
|
// 操作日志API
|
||||||
export const getOperationLogs = async (page: number = 1, pageSize: number = 10): Promise<PaginationResponse<OperationLog>> => {
|
export const getOperationLogs = async (page: number = 1, pageSize: number = 10): Promise<PaginationResponse<OperationLog>> => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -9,11 +9,26 @@ import (
|
|||||||
"github.com/niangaodev/art-code/utils"
|
"github.com/niangaodev/art-code/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetSettings 获取设置
|
// GetSettings 获取公开的网站设置(前端使用)
|
||||||
func GetSettings(c *gin.Context) {
|
func GetSettings(c *gin.Context) {
|
||||||
// 目前没有公开的设置接口,保留作为扩展
|
// 获取所有设置
|
||||||
// 如果需要公开设置,可以类似处理
|
settingsMap, err := repositories.GetAllSettings()
|
||||||
utils.Success(c, gin.H{})
|
if err != nil {
|
||||||
|
utils.ServerError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只返回前端需要的公开配置项
|
||||||
|
publicSettings := make(map[string]string)
|
||||||
|
publicKeys := []string{"site_title", "site_description", "site_author", "site_keywords"}
|
||||||
|
|
||||||
|
for _, key := range publicKeys {
|
||||||
|
if value, exists := settingsMap[key]; exists {
|
||||||
|
publicSettings[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.Success(c, publicSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminGetSettings 获取所有设置
|
// AdminGetSettings 获取所有设置
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ func main() {
|
|||||||
api.GET("/tags/:id", handlers.GetTag)
|
api.GET("/tags/:id", handlers.GetTag)
|
||||||
api.GET("/tags/:id/posts", handlers.GetPostsByTagID)
|
api.GET("/tags/:id/posts", handlers.GetPostsByTagID)
|
||||||
|
|
||||||
|
// 公开设置路由
|
||||||
|
api.GET("/settings", handlers.GetSettings)
|
||||||
|
|
||||||
// 代码执行路由
|
// 代码执行路由
|
||||||
api.POST("/run", handlers.RunCode)
|
api.POST("/run", handlers.RunCode)
|
||||||
api.GET("/proxy", handlers.ProxyRequest)
|
api.GET("/proxy", handlers.ProxyRequest)
|
||||||
|
|||||||
@@ -174,7 +174,16 @@ func ExtractProvinceFromRegion(region string) string {
|
|||||||
return "未知"
|
return "未知"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 所有省份列表(34个)
|
||||||
|
var allProvinces = []string{
|
||||||
|
"北京", "天津", "河北", "山西", "内蒙古", "辽宁", "吉林", "黑龙江",
|
||||||
|
"上海", "江苏", "浙江", "安徽", "福建", "江西", "山东", "河南",
|
||||||
|
"湖北", "湖南", "广东", "广西", "海南", "重庆", "四川", "贵州",
|
||||||
|
"云南", "西藏", "陕西", "甘肃", "青海", "宁夏", "新疆", "台湾", "香港", "澳门",
|
||||||
|
}
|
||||||
|
|
||||||
// GetUserRegions 获取用户地域分布(使用 access_logs 表的 region 字段)
|
// GetUserRegions 获取用户地域分布(使用 access_logs 表的 region 字段)
|
||||||
|
// 返回所有34个省份的数据,没有数据的省份 Count 设为 0
|
||||||
func GetUserRegions(startDate, endDate string) ([]struct {
|
func GetUserRegions(startDate, endDate string) ([]struct {
|
||||||
Region string
|
Region string
|
||||||
Count int
|
Count int
|
||||||
@@ -210,15 +219,18 @@ func GetUserRegions(startDate, endDate string) ([]struct {
|
|||||||
provinceMap := make(map[string]int)
|
provinceMap := make(map[string]int)
|
||||||
for _, r := range results {
|
for _, r := range results {
|
||||||
province := ExtractProvinceFromRegion(r.Region)
|
province := ExtractProvinceFromRegion(r.Region)
|
||||||
provinceMap[province] += r.Count
|
if province != "未知" {
|
||||||
|
provinceMap[province] += r.Count
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换为结果格式
|
// 为所有省份生成数据,没有数据的设为0
|
||||||
var finalResults []struct {
|
var finalResults []struct {
|
||||||
Region string
|
Region string
|
||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
for province, count := range provinceMap {
|
for _, province := range allProvinces {
|
||||||
|
count := provinceMap[province]
|
||||||
finalResults = append(finalResults, struct {
|
finalResults = append(finalResults, struct {
|
||||||
Region string
|
Region string
|
||||||
Count int
|
Count int
|
||||||
@@ -228,14 +240,5 @@ func GetUserRegions(startDate, endDate string) ([]struct {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按数量排序
|
|
||||||
for i := 0; i < len(finalResults)-1; i++ {
|
|
||||||
for j := i + 1; j < len(finalResults); j++ {
|
|
||||||
if finalResults[i].Count < finalResults[j].Count {
|
|
||||||
finalResults[i], finalResults[j] = finalResults[j], finalResults[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalResults, nil
|
return finalResults, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user