重构登录后博客页:Lunada 渐变背景 + 两侧图片液态玻璃;登录报错改英文

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ryan 2026-06-18 10:57:00 +08:00
parent 6037a8186c
commit c61b8de776
7 changed files with 202 additions and 77 deletions

View File

@ -24,14 +24,14 @@ export async function login(username: string, password: string): Promise<LoginRe
body: JSON.stringify({ username, password }),
})
} catch {
throw new Error('无法连接服务器,请确认后端已启动')
throw new Error('Cannot reach the server. Please make sure the backend is running.')
}
if (res.status === 401) {
throw new Error('用户名或密码错误')
throw new Error('Incorrect username or password')
}
if (!res.ok) {
throw new Error(`登录失败HTTP ${res.status}`)
throw new Error(`Sign-in failed (HTTP ${res.status})`)
}
return (await res.json()) as LoginResult

View File

@ -0,0 +1,40 @@
# 登录后页面 · 两侧图片区
这里的图片走 **bundler import**:`BlogPage.tsx` 用 Vite 的 `import.meta.glob`
**自动索引本文件夹下的所有图片**,因此:
- **丢图进来即生效,无需改代码**;删图同理。
- 打包时自动加内容哈希(缓存失效)、构建期校验(图缺了会编译报错)、可接入优化。
- 引用路径不再写死字符串,所以**不要**再用 `public/` 里的 `/images/...` 方式。
## 取图规则
`BlogPage` 把本目录的图**按文件名升序**排列后取用:
| 顺序 | 位置 |
| ---- | ---- |
| 第 1 张 | 左栏 |
| 第 2 张 | 右栏 |
所以建议用 `side-01.jpg`、`side-02.jpg` 这种带序号的名字控制顺序;
想交换左右,改文件名顺序即可。多于两张时只会用到前两张(可按需扩展代码)。
支持的扩展名:`jpg` / `jpeg` / `png` / `webp`(在 `BlogPage.tsx` 的 glob 模式里调整)。
## 推荐尺寸 / 比例
两侧栏是**窄而高的竖条**(中间内容固定 `max-w-3xl`,两侧吃掉剩余宽度),
图片用 `background cover` 填满、居中裁切:
- **方向**:竖版(portrait)
- **比例**:约 `2:3` ~ `9:16`(页面越长越偏 `9:16`)
- **分辨率**:建议 ≥ `1000 × 1600`,高分屏清晰可用 `1200 × 1800`+
- **主体**:重要内容放画面中央(上下/左右边缘可能被裁)
- **格式**:`webp`(体积最小)> `jpg` > `png`;插画/需透明用 `png`
缺图时该区域会自动透出页面的 Lunada 渐变背景,不会显示裂图占位符。
## 当前文件
- `side-01.jpg` —— 左栏
- `side-02.jpg` —— 右栏

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

View File

@ -53,17 +53,36 @@ const POSTS: Post[] = [
},
]
// 两侧图片区的图片:用 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="flex min-h-svh flex-col bg-bg">
<div className="relative flex min-h-svh flex-col">
{/* 整页 Lunada 渐变极光背景,固定铺满视口、置于所有内容之下 */}
<div className="aurora" aria-hidden />
<AnnouncementBar text={stats.text} online={stats.online} />
{/* 顶部导航 */}
<header className="relative z-20 border-b border-line/60">
<div className="mx-auto flex w-full max-w-3xl items-center justify-between px-6 py-5">
{/* 顶部彩虹栏:通栏,横跨左右图片区之上;底部一条流动彩虹线 */}
<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>
@ -79,55 +98,72 @@ export function BlogPage({ username, onSignOut }: BlogPageProps) {
</button>
</div>
</div>
<div className="rainbow-gradient h-1 w-full" aria-hidden />
</header>
<main className="flex-1">
{/* Hero整段全宽彩虹光斑横跨整个页面文字内容仍居中限宽 */}
<section className="relative w-full overflow-hidden py-20 sm:py-28">
<RainbowSpotlight />
{/* 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.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"
>
<motion.span
variants={fadeUp}
className="text-xs font-medium uppercase tracking-[0.3em] text-accent"
>
Personal blog
</motion.span>
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.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"
<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"
>
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>
Josh W. Comeau
</a>
</motion.p>
</motion.div>
</section>
{/* 文章列表 */}
<section className="mx-auto w-full max-w-3xl border-t border-line/60 px-6 py-14">
{/* · () ·
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>
@ -171,14 +207,26 @@ export function BlogPage({ username, onSignOut }: BlogPageProps) {
</section>
</main>
{/* 页脚:一条流动彩虹装饰线 */}
<footer className="mt-auto">
<div className="rainbow-gradient h-1 w-full" />
<div className="mx-auto flex w-full max-w-3xl 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>
{/* 页脚:一条流动彩虹装饰线 */}
<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>
</footer>
{/* 右侧图片区(与左侧同样的高度适配 + 居中 + 液态玻璃覆层) */}
<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>
)
}

View File

@ -27,7 +27,7 @@ export function LoginForm({ onSignIn }: LoginFormProps) {
await onSignIn(username.trim(), password, remember)
// 成功后由上层切换到博客页,本组件随之卸载,无需额外处理。
} catch (err) {
setError(err instanceof Error ? err.message : '登录失败')
setError(err instanceof Error ? err.message : 'Sign-in failed')
setSubmitting(false)
}
}

View File

@ -164,15 +164,17 @@ body {
animation: rainbow-pan 6s linear infinite;
}
/* 整页彩虹极光背景玻璃栏透出的就是它
固定铺满视口置于所有内容之下多个彩色径向光斑缓慢漂移
再大幅模糊成柔和极光底色为暗色避免内容区透出时发灰 */
/* 整页渐变极光背景玻璃栏透出的就是它
固定铺满视口置于所有内容之下底色为 Lunada 渐变
uigradientsLunada#A7BFE8 #6190E8上面叠几团同色系
蓝光斑缓慢漂移并大幅模糊让平面渐变产生液态流动感 */
.aurora {
position: fixed;
inset: 0;
z-index: -10;
overflow: hidden;
background-color: var(--color-bg);
/* Lunada 渐变打底 */
background: linear-gradient(135deg, #6190e8 0%, #89a8e6 48%, #a7bfe8 100%);
}
.aurora::before {
@ -180,30 +182,65 @@ body {
position: absolute;
inset: -25%;
background:
radial-gradient(28% 30% at 18% 28%, rgba(255, 95, 109, 0.55), transparent 70%),
radial-gradient(26% 30% at 82% 26%, rgba(56, 189, 248, 0.50), transparent 70%),
radial-gradient(30% 34% at 72% 76%, rgba(167, 139, 250, 0.50), transparent 70%),
radial-gradient(28% 32% at 26% 80%, rgba(74, 222, 128, 0.45), transparent 70%),
radial-gradient(24% 28% at 50% 50%, rgba(255, 177, 61, 0.40), transparent 70%);
filter: blur(70px);
animation: drift 28s ease-in-out infinite;
will-change: transform;
radial-gradient(30% 34% at 18% 28%, rgba(97, 144, 232, 0.65), transparent 70%),
radial-gradient(28% 32% at 82% 24%, rgba(167, 191, 232, 0.70), transparent 70%),
radial-gradient(34% 38% at 72% 78%, rgba(120, 162, 236, 0.60), transparent 70%),
radial-gradient(26% 30% at 26% 82%, rgba(192, 211, 245, 0.55), transparent 70%),
radial-gradient(24% 28% at 50% 50%, rgba(140, 175, 240, 0.45), transparent 70%);
filter: blur(60px);
/* 静止:持续漂移会让上层多个 backdrop-filter 每帧重算模糊,是卡顿主因 */
}
/* 两侧液态玻璃竖栏
backdrop-filter 把背后的极光磨成柔光内侧一道高光边模拟玻璃厚度 */
backdrop-filter 把背后的极光磨成柔光上下渐变的白色叠层 + 内侧两道
高光边模拟玻璃厚度做出液态玻璃的体积感 */
.glass-rail {
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(16px) saturate(140%);
-webkit-backdrop-filter: blur(16px) saturate(140%);
box-shadow: inset 0 0 60px rgba(255, 255, 255, 0.03);
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.12),
rgba(255, 255, 255, 0.03) 55%,
rgba(255, 255, 255, 0.08)
);
backdrop-filter: blur(18px) saturate(150%);
-webkit-backdrop-filter: blur(18px) saturate(150%);
box-shadow:
inset 0 0 60px rgba(255, 255, 255, 0.06),
inset 1px 0 0 rgba(255, 255, 255, 0.20),
inset -1px 0 0 rgba(255, 255, 255, 0.20);
}
/* 中间内容纸张:也带轻微玻璃感(压暗 + 模糊背后极光保证可读) */
/* 中间内容纸张压暗的暖色玻璃模糊背后极光保证可读
两侧各一道高光描边与左右玻璃竖栏拼出连续的玻璃接缝 */
.glass-sheet {
background: rgba(23, 19, 15, 0.82);
backdrop-filter: blur(22px) saturate(120%);
-webkit-backdrop-filter: blur(22px) saturate(120%);
/* 底色 84% 不透明背后极光本就几乎透不过来省掉昂贵的 backdrop-filter
观感基本无差却避免这条又高又大的区域每帧重算模糊 */
background: rgba(23, 19, 15, 0.84);
box-shadow:
inset 1px 0 0 rgba(255, 255, 255, 0.12),
inset -1px 0 0 rgba(255, 255, 255, 0.12),
0 30px 80px rgba(20, 30, 60, 0.30);
}
/* 两侧图片上的液态玻璃覆层backdrop-filter 把图片磨成柔光
叠一道斜向高光 + 内侧四边亮边做出通透的玻璃质感
盖在 <aside> 的背景图之上inset-0 铺满 */
.glass-image {
backdrop-filter: blur(6px) saturate(150%);
-webkit-backdrop-filter: blur(6px) saturate(150%);
background: linear-gradient(
125deg,
rgba(255, 255, 255, 0.24) 0%,
rgba(255, 255, 255, 0.06) 28%,
rgba(255, 255, 255, 0) 52%,
rgba(255, 255, 255, 0.05) 72%,
rgba(255, 255, 255, 0.14) 100%
);
box-shadow:
inset 0 0 90px rgba(255, 255, 255, 0.10),
inset 1px 0 0 rgba(255, 255, 255, 0.22),
inset -1px 0 0 rgba(255, 255, 255, 0.22),
inset 0 1px 0 rgba(255, 255, 255, 0.18),
inset 0 -1px 0 rgba(255, 255, 255, 0.12);
}
/* ── reduced-motion 降级 ────────────────────────────────────── */