PersonalWebApplication/Client/src/components/BlogPage.tsx
Ryan c61b8de776 重构登录后博客页:Lunada 渐变背景 + 两侧图片液态玻璃;登录报错改英文
- 登录页凭据/网络/HTTP 错误提示改为英文
- 博客页改为:通栏顶部彩虹栏 + 通栏 Hero(彩虹光标动效)+ 图片·内容·图片三栏
- 两侧图片用 import.meta.glob 自动索引 src/assets/sides/,按高度自适应、居中,叠加液态玻璃覆层
- 背景改为 Lunada 渐变极光
- 性能优化:极光改静止、中间纸/顶栏去掉 backdrop-filter,降低多层毛玻璃每帧重算开销

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:57:00 +08:00

233 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { motion } from 'framer-motion'
import { useSystemStats } from '../hooks/useSystemStats'
import { fadeUp, stagger } from './motion'
import { AnnouncementBar } from './AnnouncementBar'
import { RainbowSpotlight } from './RainbowSpotlight'
interface BlogPageProps {
/** 当前登录用户名 */
username: string
/** 退出登录 */
onSignOut: () => void
}
interface Post {
title: string
date: string
readingTime: string
excerpt: string
tag: string
}
// 示例文章数据(占位)。将来可换成从后端 /api/web/posts 拉取。
const POSTS: Post[] = [
{
title: 'An Interactive Guide to CSS Gradients',
date: 'Jun 2026',
readingTime: '12 min',
excerpt:
'从线性到锥形渐变,拆解每个参数如何影响最终画面,并顺手做一道流动的彩虹。',
tag: 'CSS',
},
{
title: '用 Rust + axum 写一个会呼吸的监控后端',
date: 'May 2026',
readingTime: '9 min',
excerpt:
'采样与请求解耦、RwLock 快照、CORS 只拦浏览器——把顶部栏那行系统状态讲透。',
tag: 'Rust',
},
{
title: 'framer-motion 里那些恰到好处的微交互',
date: 'Apr 2026',
readingTime: '7 min',
excerpt: '光标跟随、入场编排、spring 手感。动效不是炫技,是把注意力放对地方。',
tag: 'Motion',
},
{
title: '为什么我给只有 20 个用户的系统拒绝了数据库',
date: 'Mar 2026',
readingTime: '6 min',
excerpt: '一个进程内 HashMap 就够了。聊聊在小规模下,简单是怎样跑赢“正确架构”的。',
tag: 'Backend',
},
]
// 两侧图片区的图片:用 Vite 的 import.meta.glob 自动索引 src/assets/sides/ 下的所有图。
// 走 bundler import —— 自带内容哈希、构建期校验、可优化;丢新图进该文件夹即生效,无需改代码。
// 按文件名升序取用:第 1 张 → 左栏,第 2 张 → 右栏。详见 src/assets/sides/README.md。
const sideImages = Object.entries(
import.meta.glob<string>('../assets/sides/*.{jpg,jpeg,png,webp}', {
eager: true,
query: '?url',
import: 'default',
}),
)
.sort(([pathA], [pathB]) => pathA.localeCompare(pathB))
.map(([, url]) => url)
const SIDE_IMAGE_LEFT = sideImages[0]
const SIDE_IMAGE_RIGHT = sideImages[1] ?? sideImages[0]
export function BlogPage({ username, onSignOut }: BlogPageProps) {
// 顶部栏沿用系统状态彩蛋,保持与登录页同一风格。
const stats = useSystemStats()
return (
<div className="relative flex min-h-svh flex-col">
{/* 整页 Lunada 渐变极光背景,固定铺满视口、置于所有内容之下 */}
<div className="aurora" aria-hidden />
<AnnouncementBar text={stats.text} online={stats.online} />
{/* 顶部彩虹栏:通栏,横跨左右图片区之上;底部一条流动彩虹线 */}
<header className="glass-sheet relative z-20 w-full">
<div className="mx-auto flex w-full max-w-6xl items-center justify-between px-6 py-5">
<span className="rainbow-text font-display text-lg font-semibold tracking-tight">
cc.dev
</span>
<div className="flex items-center gap-4 text-sm text-muted">
<span className="hidden sm:inline">
Signed in as <span className="text-text">{username}</span>
</span>
<button
onClick={onSignOut}
className="rounded-full border border-line px-4 py-1.5 text-xs font-medium uppercase tracking-[0.16em] text-muted transition-colors hover:border-accent hover:text-accent"
>
Sign out
</button>
</div>
</div>
<div className="rainbow-gradient h-1 w-full" aria-hidden />
</header>
{/* Hero 通栏:彩虹光标动效 + 大字标题 + 灵感来源,占满一整行(在三栏之上) */}
<section className="glass-sheet relative z-10 w-full overflow-hidden py-20 sm:py-28">
<RainbowSpotlight />
<motion.div
variants={stagger(0.15, 0.12)}
initial="hidden"
animate="show"
className="relative z-10 mx-auto flex w-full max-w-3xl flex-col gap-6 px-6"
>
<motion.span
variants={fadeUp}
className="text-xs font-medium uppercase tracking-[0.3em] text-accent"
>
Personal blog
</motion.span>
<motion.h1
variants={fadeUp}
className="font-display text-5xl font-medium leading-[1.05] text-text sm:text-6xl lg:text-7xl"
>
Hello, I'm <span className="rainbow-text italic">cc</span>
<span className="cursor-bar ml-1 inline-block not-italic text-accent">_</span>
</motion.h1>
<motion.p
variants={fadeUp}
className="max-w-xl text-base leading-relaxed text-muted"
>
我在这里写关于前端动效、Rust 后端和把事情做简单的笔记。把鼠标移到这片区域,
会有一道彩虹跟着你——灵感来自{' '}
<a
href="https://www.joshwcomeau.com/"
target="_blank"
rel="noreferrer"
className="text-accent underline-offset-4 hover:underline"
>
Josh W. Comeau
</a>
</motion.p>
</motion.div>
</section>
{/* 三栏:左侧图片 · 中间内容(文章列表) · 右侧图片。
中间内容保持原比例max-w-3xl两侧 flex-1 图片区吃掉剩余留白(窄屏隐藏)。 */}
<div className="relative flex flex-1 justify-center">
{/* 左侧图片区:高度适配展示框、宽度按比例(不拉伸变形)、居中(水平溢出裁切)
上覆液态玻璃。缺图时透出 Lunada 背景。 */}
<aside
className="relative hidden flex-1 overflow-hidden bg-center bg-no-repeat lg:block"
style={{ backgroundImage: `url(${SIDE_IMAGE_LEFT})`, backgroundSize: 'auto 100%' }}
aria-hidden
>
{/* 液态玻璃覆层 */}
<div className="glass-image absolute inset-0" />
</aside>
{/* 中间内容纸(玻璃纸张) */}
<div className="glass-sheet relative flex w-full max-w-3xl flex-col">
<main className="flex-1">
{/* 文章列表 */}
<section className="w-full px-6 py-14">
<h2 className="mb-10 text-xs font-medium uppercase tracking-[0.3em] text-muted">
Latest writing
</h2>
<motion.ul
variants={stagger(0.1, 0.08)}
initial="hidden"
animate="show"
className="flex flex-col"
>
{POSTS.map((post) => (
<motion.li key={post.title} variants={fadeUp}>
<a
href="#"
className="group flex flex-col gap-3 border-b border-line/50 py-7 transition-colors sm:flex-row sm:items-baseline sm:gap-8"
>
{/* 左侧彩虹竖条hover 时出现 */}
<span
aria-hidden
className="rainbow-gradient hidden w-1 self-stretch rounded-full opacity-0 transition-opacity duration-300 group-hover:opacity-100 sm:block"
/>
<div className="w-28 shrink-0 font-mono text-xs uppercase tracking-wider text-muted">
<div>{post.date}</div>
<div className="mt-1 text-accent/70">{post.tag}</div>
</div>
<div className="flex-1">
<h3 className="rainbow-clip font-display text-xl font-medium text-text transition-colors duration-200 group-hover:text-transparent">
{post.title}
</h3>
<p className="mt-2 text-sm leading-relaxed text-muted">
{post.excerpt}
</p>
<span className="mt-2 inline-block text-xs text-muted/70">
{post.readingTime} read
</span>
</div>
</a>
</motion.li>
))}
</motion.ul>
</section>
</main>
{/* 页脚:一条流动彩虹装饰线 */}
<footer className="mt-auto">
<div className="rainbow-gradient h-1 w-full" />
<div className="flex w-full items-center justify-between px-6 py-8 text-xs text-muted">
<span>© {new Date().getFullYear()} cc</span>
<span className="font-mono tracking-wider">built with React · Rust · 🌈</span>
</div>
</footer>
</div>
{/* 右侧图片区(与左侧同样的高度适配 + 居中 + 液态玻璃覆层) */}
<aside
className="relative hidden flex-1 overflow-hidden bg-center bg-no-repeat lg:block"
style={{ backgroundImage: `url(${SIDE_IMAGE_RIGHT})`, backgroundSize: 'auto 100%' }}
aria-hidden
>
{/* 液态玻璃覆层 */}
<div className="glass-image absolute inset-0" />
</aside>
</div>
</div>
)
}