重构登录后博客页: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:
parent
6037a8186c
commit
c61b8de776
@ -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
|
||||
|
||||
40
Client/src/assets/sides/README.md
Normal file
40
Client/src/assets/sides/README.md
Normal 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` —— 右栏
|
||||
BIN
Client/src/assets/sides/side-01.jpg
Normal file
BIN
Client/src/assets/sides/side-01.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 249 KiB |
BIN
Client/src/assets/sides/side-02.jpg
Normal file
BIN
Client/src/assets/sides/side-02.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 296 KiB |
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -164,15 +164,17 @@ body {
|
||||
animation: rainbow-pan 6s linear infinite;
|
||||
}
|
||||
|
||||
/* ── 整页彩虹极光背景(玻璃栏透出的就是它)──────────────────────
|
||||
固定铺满视口、置于所有内容之下。多个彩色径向光斑缓慢漂移,
|
||||
再大幅模糊成柔和极光。底色为暗色,避免内容区透出时发灰。 */
|
||||
/* ── 整页渐变极光背景(玻璃栏透出的就是它)──────────────────────
|
||||
固定铺满视口、置于所有内容之下。底色为 Lunada 渐变
|
||||
(uigradients「Lunada」:#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 降级 ────────────────────────────────────── */
|
||||
|
||||
Loading…
Reference in New Issue
Block a user