fix(polish): en 首页按 zh 全结构重写——PostList locale 化与版面平权

- PostList 组件加 locale prop(轨道标签走字典、en 链接 /en/ 前缀)
- en/index 镜像 zh 结构:名片区三图标(GitHub/RSS/邮箱)+ 文章列表
  + 最新影像(latest-head space-between + 4 列瓷砖网格,修复上轮
  粘连与整宽竖排两处硬伤)+ 热力图 + 项目墙,区块顺序与 zh 一致
- 闭环截图:zh 滚动至热力图锚点(修复验证缺口)

验证: pnpm astro build 21 页 exit 0;node tests/e2e.mjs 30/30 PASS

Spec: 2026-10-25_fix-site-polish

Co-authored-by: GLM-5.3
This commit is contained in:
wait4xandGLM-5.3 committed 2026-10-25 11:23:30 +08:00
1 parent ebafa415b2
commit 86e8009f34
2 files changed
+141 -63

No files matched your search

+13 -4
View File
@@ -2,13 +2,22 @@
// 年份分组 + 轨道分节的文章列表(首页与 /archives/ 共用)
// 轨道标签单一事实源:content.config 的 POST_TRACKS,此处不另立词表
import type { CollectionEntry } from 'astro:content';
import { POST_TRACKS, trackOf, type PostCategory } from '../content.config';
import { POST_TRACKS, type PostCategory } from '../content.config';
import { t } from '../i18n/ui';
interface Props {
/** 调用方已完成 draft 过滤与 pubDate 倒序 */
posts: CollectionEntry<'posts'>[];
locale?: 'zh' | 'en';
}
const { posts } = Astro.props;
const { posts, locale = 'zh' } = Astro.props;
// 轨道标签走字典(en 首页复用本组件时的双语位);zh 保持原词表语义
const TRACK_LABEL: Record<PostCategory, string> = {
reading: t('category_reading', locale),
tech: t('category_tech', locale),
photo: t('category_photo', locale),
};
const hrefOf = (id: string) => (locale === 'en' ? `/en/archives/${id}/` : `/archives/${id}/`);
const byYear = new Map<number, CollectionEntry<'posts'>[]>();
for (const post of posts) {
@@ -30,11 +39,11 @@ const mmdd = (d: Date) =>
if (rows.length === 0) return null;
return (
<div class="track-block">
<h3 class="track">{trackOf(cat)}</h3>
<h3 class="track">{TRACK_LABEL[cat]}</h3>
<ul>
{rows.map((post) => (
<li>
<a class="row tap" href={`/archives/${post.id}/`}>
<a class="row tap" href={hrefOf(post.id)}>
{cat === 'reading' && (
<i
class="emo"
+128 -59
View File
@@ -1,66 +1,91 @@
---
// /en/ 首页:英文 UI + 双语内容(postsEn 优先,缺则中文回退)
// /en/ 首页:与 zh 版结构逐区对齐(名片区/文章列表/最新影像/热力图/项目墙)
import { Image } from 'astro:assets';
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { t } from '../../i18n/ui';
import IdCard from '../../components/IdCard.astro';
import { Image } from 'astro:assets';
const photos = (await getCollection('photos')).slice(0, 4);
import PostList from '../../components/PostList.astro';
import ProjectWall from '../../components/geek/ProjectWall.astro';
import Heatmap from '../../components/geek/Heatmap.astro';
import { POST_TRACKS, trackOf } from '../../content.config';
import { t } from '../../i18n/ui';
const en = await getCollection('postsEn');
const zh = await getCollection('posts');
const bySlug = new Map(en.map((e) => [e.id, e]));
const posts = zh
.filter(({ data }) => !data.draft)
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())
.map((p) => ({ zh: p, en: bySlug.get(p.id) }));
const years = [...new Set(posts.map((p) => p.zh.data.pubDate.getFullYear()))].sort((a, b) => b - a);
const t9 = (cat: keyof typeof POST_TRACKS) => trackOf(cat);
const posts = (await getCollection('posts', ({ data }) => !data.draft)).sort(
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
);
const photos = (await getCollection('photos')).slice(0, 4);
---
<BaseLayout title="wait4x" description="wait4x's blog: reading · code · photography" track="tech">
<h1 class="visually-hidden">wait4x</h1>
<BaseLayout title="wait4x" description="wait4x's blog: reading · code · photography">
<IdCard>
<a href="https://github.com/wait4xx" rel="noopener noreferrer" aria-label="GitHub">↗</a>
<a href="/en/rss.xml" aria-label="RSS">RSS</a>
<div class="socials">
<a
class="tap social"
href="https://github.com/wait4xx"
rel="noopener noreferrer"
aria-label="GitHub"
title="GitHub"
>
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true">
<path
d="M12 2C6.48 2 2 6.58 2 12.25c0 4.53 2.87 8.37 6.84 9.73.5.1.68-.22.68-.49 0-.24-.01-.88-.01-1.73-2.78.62-3.37-1.37-3.37-1.37-.45-1.18-1.11-1.5-1.11-1.5-.91-.63.07-.62.07-.62 1 .07 1.53 1.06 1.53 1.06.89 1.57 2.34 1.12 2.91.85.09-.66.35-1.11.63-1.37-2.22-.26-4.56-1.14-4.56-5.07 0-1.12.39-2.03 1.03-2.75-.1-.26-.45-1.3.1-2.7 0 0 .84-.28 2.75 1.05a9.3 9.3 0 0 1 5 0c1.91-1.33 2.75-1.05 2.75-1.05.55 1.4.2 2.44.1 2.7.64.72 1.03 1.63 1.03 2.75 0 3.94-2.34 4.8-4.57 5.06.36.32.68.94.68 1.9 0 1.37-.01 2.47-.01 2.81 0 .27.18.6.69.49A10.26 10.26 0 0 0 22 12.25C22 6.58 17.52 2 12 2Z"
></path>
</svg>
</a>
<a class="tap social" href="/en/rss.xml" aria-label="RSS feed" title="RSS feed">
<svg
viewBox="0 0 24 24"
width="16"
height="16"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
aria-hidden="true"
>
<path d="M4 11.5a8.5 8.5 0 0 1 8.5 8.5M4 4.5a15.5 15.5 0 0 1 15.5 15.5"></path>
<circle cx="5.4" cy="18.6" r="1.5" fill="currentColor" stroke="none"></circle>
</svg>
</a>
<a class="tap social" href="mailto:admin@wait4x.online" aria-label="Email" title="Email">
<svg
viewBox="0 0 24 24"
width="16"
height="16"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<rect x="3" y="5.5" width="18" height="13" rx="2.5"></rect>
<path d="m4 7.5 8 5.5 8-5.5"></path>
</svg>
</a>
</div>
</IdCard>
{years.map((year) => (
<PostList {posts} locale="en" />
<section class="latest" aria-label={t('latest_photos', 'en')}>
<div class="latest-head">
<span class="lab">{t('latest_photos', 'en')}</span>
<a href="/en/gallery/">{t('enter_gallery', 'en')}</a>
<div class="strip">
{photos.map((p) => (<a href="/en/gallery/"><Image src={p.data.image} alt={p.data.alt ?? p.data.title} widths={[160, 320]} sizes="20vw" loading="lazy" /></a>))}
<a class="tap more" href="/en/gallery/">{t('enter_gallery', 'en')}</a>
</div>
<div class="thumbs">
{photos.map((photo) => (
<a class="thumb" href="/en/gallery/" aria-label={`Gallery: ${photo.data.title}`}>
<Image
src={photo.data.image}
alt={photo.data.alt ?? photo.data.title}
widths={[180, 360]}
sizes="(min-width: 820px) 20vw, 42vw"
/>
</a>
))}
</div>
</section>
<section class="year">
<h2 class="y-label">{year}</h2>
{(['reading', 'tech', 'photo'] as const).map((cat) => {
const list = posts.filter((p) => p.zh.data.category === cat && p.zh.data.pubDate.getFullYear() === year);
return list.length ? (
<>
<h3 class="track">{t(cat === 'reading' ? 'category_reading' : cat === 'tech' ? 'category_tech' : 'category_photo', 'en')}</h3>
<ul class="plist">
{list.map(({ zh: p, en: e }) => (
<li>
<a href={`/en/archives/${p.id}/`} class="plink">
<span class="ptitle" style={p.data.mood ? `--mood:${p.data.mood}` : undefined}>
{p.data.mood && <i class="mdot" aria-hidden="true" />}
{e?.data.title ?? p.data.title}
</span>
{!e && <i class="fall">zh</i>}
<time class="pdate">{p.data.pubDate.toISOString().slice(5, 10)}</time>
</a>
</li>
))}
</ul>
</>
) : null;
})}
</section>
))}
<Heatmap
labels={{
title: t('heatmap_title', 'en'),
@@ -70,17 +95,61 @@ const t9 = (cat: keyof typeof POST_TRACKS) => trackOf(cat);
note: t('heatmap_note', 'en'),
}}
/>
<ProjectWall />
</BaseLayout>
<style>
.year { margin-top: var(--space-5); }
.y-label { font-family: var(--font-mono); font-size: 20px; color: var(--ink-1); }
.track { font-size: 13px; color: var(--ink-3); margin: var(--space-3) 0 var(--space-2); letter-spacing: 0.06em; }
.plist { list-style: none; padding: 0; display: grid; gap: var(--space-1); }
.plink { display: flex; align-items: baseline; gap: var(--space-2); padding-block: var(--space-1); }
.plink:hover .ptitle { color: var(--accent); }
.ptitle { color: var(--ink-1); transition: color var(--dur) var(--ease); }
.mdot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--mood, var(--ink-4)); margin-right: 6px; }
.fall { font-family: var(--font-mono); font-size: 10px; color: var(--ink-4); border: 1px solid var(--line); border-radius: 3px; padding: 0 3px; font-style: normal; }
.pdate { margin-left: auto; font-family: var(--font-mono); font-size: 12px; color: var(--ink-3); }
.socials {
display: flex;
align-items: center;
gap: var(--space-2);
margin-top: var(--space-2);
}
.social {
color: var(--ink-3);
border-radius: 8px;
transition: color var(--dur) var(--ease);
}
.social:hover { color: var(--accent); }
.latest { margin-top: var(--space-6); }
.latest-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-3);
}
.lab {
font-size: 12.5px;
color: var(--ink-4);
letter-spacing: 0.08em;
}
.more {
font-size: 13px;
color: var(--accent);
padding-inline: var(--space-2);
transition: color var(--dur) var(--ease);
}
.more:hover { color: var(--ink-1); }
.thumbs {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-2);
margin-top: var(--space-3);
}
.thumb {
aspect-ratio: 1;
border-radius: 8px;
overflow: hidden;
}
.thumb :global(img) {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform var(--dur) var(--ease);
}
.thumb:hover :global(img) { transform: scale(1.03); }
@media (min-width: 820px) {
.thumbs { grid-template-columns: repeat(4, 1fr); }
}
</style>