2026-06-18 00:00:38 +08:00
|
|
|
|
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',
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-06-18 10:57:00 +08:00
|
|
|
|
// 两侧图片区的图片:用 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]
|
|
|
|
|
|
|
2026-06-18 00:00:38 +08:00
|
|
|
|
export function BlogPage({ username, onSignOut }: BlogPageProps) {
|
|
|
|
|
|
// 顶部栏沿用系统状态彩蛋,保持与登录页同一风格。
|
|
|
|
|
|
const stats = useSystemStats()
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-06-18 10:57:00 +08:00
|
|
|
|
<div className="relative flex min-h-svh flex-col">
|
|
|
|
|
|
{/* 整页 Lunada 渐变极光背景,固定铺满视口、置于所有内容之下 */}
|
|
|
|
|
|
<div className="aurora" aria-hidden />
|
|
|
|
|
|
|
2026-06-18 00:00:38 +08:00
|
|
|
|
<AnnouncementBar text={stats.text} online={stats.online} />
|
|
|
|
|
|
|
2026-06-18 10:57:00 +08:00
|
|
|
|
{/* 顶部彩虹栏:通栏,横跨左右图片区之上;底部一条流动彩虹线 */}
|
|
|
|
|
|
<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">
|
2026-06-18 00:00:38 +08:00
|
|
|
|
<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>
|
2026-06-18 10:57:00 +08:00
|
|
|
|
<div className="rainbow-gradient h-1 w-full" aria-hidden />
|
2026-06-18 00:00:38 +08:00
|
|
|
|
</header>
|
|
|
|
|
|
|
2026-06-18 10:57:00 +08:00
|
|
|
|
{/* Hero 通栏:彩虹光标动效 + 大字标题 + 灵感来源,占满一整行(在三栏之上) */}
|
|
|
|
|
|
<section className="glass-sheet relative z-10 w-full overflow-hidden py-20 sm:py-28">
|
|
|
|
|
|
<RainbowSpotlight />
|
2026-06-18 00:00:38 +08:00
|
|
|
|
|
2026-06-18 10:57:00 +08:00
|
|
|
|
<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"
|
2026-06-18 00:00:38 +08:00
|
|
|
|
>
|
2026-06-18 10:57:00 +08:00
|
|
|
|
Personal blog
|
|
|
|
|
|
</motion.span>
|
2026-06-18 00:00:38 +08:00
|
|
|
|
|
2026-06-18 10:57:00 +08:00
|
|
|
|
<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>
|
2026-06-18 00:00:38 +08:00
|
|
|
|
|
2026-06-18 10:57:00 +08:00
|
|
|
|
<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"
|
2026-06-18 00:00:38 +08:00
|
|
|
|
>
|
2026-06-18 10:57:00 +08:00
|
|
|
|
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>
|
2026-06-18 00:00:38 +08:00
|
|
|
|
|
2026-06-18 10:57:00 +08:00
|
|
|
|
{/* 中间内容纸(玻璃纸张) */}
|
|
|
|
|
|
<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">
|
2026-06-18 00:00:38 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-06-18 10:57:00 +08:00
|
|
|
|
{/* 页脚:一条流动彩虹装饰线 */}
|
|
|
|
|
|
<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>
|
2026-06-18 00:00:38 +08:00
|
|
|
|
</div>
|
2026-06-18 10:57:00 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 右侧图片区(与左侧同样的高度适配 + 居中 + 液态玻璃覆层) */}
|
|
|
|
|
|
<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>
|
2026-06-18 00:00:38 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|