- 登录页凭据/网络/HTTP 错误提示改为英文 - 博客页改为:通栏顶部彩虹栏 + 通栏 Hero(彩虹光标动效)+ 图片·内容·图片三栏 - 两侧图片用 import.meta.glob 自动索引 src/assets/sides/,按高度自适应、居中,叠加液态玻璃覆层 - 背景改为 Lunada 渐变极光 - 性能优化:极光改静止、中间纸/顶栏去掉 backdrop-filter,降低多层毛玻璃每帧重算开销 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
233 lines
9.1 KiB
TypeScript
233 lines
9.1 KiB
TypeScript
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>
|
||
)
|
||
}
|