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:
1 parent
ebafa415b2
commit
86e8009f34
2 files changed
+144
-66
No files matched your search
@@ -2,13 +2,22 @@
|
|||||||
// 年份分组 + 轨道分节的文章列表(首页与 /archives/ 共用)
|
// 年份分组 + 轨道分节的文章列表(首页与 /archives/ 共用)
|
||||||
// 轨道标签单一事实源:content.config 的 POST_TRACKS,此处不另立词表
|
// 轨道标签单一事实源:content.config 的 POST_TRACKS,此处不另立词表
|
||||||
import type { CollectionEntry } from 'astro:content';
|
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 {
|
interface Props {
|
||||||
/** 调用方已完成 draft 过滤与 pubDate 倒序 */
|
/** 调用方已完成 draft 过滤与 pubDate 倒序 */
|
||||||
posts: CollectionEntry<'posts'>[];
|
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'>[]>();
|
const byYear = new Map<number, CollectionEntry<'posts'>[]>();
|
||||||
for (const post of posts) {
|
for (const post of posts) {
|
||||||
@@ -30,11 +39,11 @@ const mmdd = (d: Date) =>
|
|||||||
if (rows.length === 0) return null;
|
if (rows.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<div class="track-block">
|
<div class="track-block">
|
||||||
<h3 class="track">{trackOf(cat)}</h3>
|
<h3 class="track">{TRACK_LABEL[cat]}</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{rows.map((post) => (
|
{rows.map((post) => (
|
||||||
<li>
|
<li>
|
||||||
<a class="row tap" href={`/archives/${post.id}/`}>
|
<a class="row tap" href={hrefOf(post.id)}>
|
||||||
{cat === 'reading' && (
|
{cat === 'reading' && (
|
||||||
<i
|
<i
|
||||||
class="emo"
|
class="emo"
|
||||||
|
|||||||
+131
-62
@@ -1,66 +1,91 @@
|
|||||||
---
|
---
|
||||||
// /en/ 首页:英文 UI + 双语内容(postsEn 优先,缺则中文回退)
|
// /en/ 首页:与 zh 版结构逐区对齐(名片区/文章列表/最新影像/热力图/项目墙)
|
||||||
|
import { Image } from 'astro:assets';
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
import { t } from '../../i18n/ui';
|
|
||||||
import IdCard from '../../components/IdCard.astro';
|
import IdCard from '../../components/IdCard.astro';
|
||||||
import { Image } from 'astro:assets';
|
import PostList from '../../components/PostList.astro';
|
||||||
const photos = (await getCollection('photos')).slice(0, 4);
|
import ProjectWall from '../../components/geek/ProjectWall.astro';
|
||||||
import Heatmap from '../../components/geek/Heatmap.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 posts = (await getCollection('posts', ({ data }) => !data.draft)).sort(
|
||||||
const zh = await getCollection('posts');
|
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
||||||
const bySlug = new Map(en.map((e) => [e.id, e]));
|
);
|
||||||
const posts = zh
|
const photos = (await getCollection('photos')).slice(0, 4);
|
||||||
.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);
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title="wait4x" description="wait4x's blog: reading · code · photography" track="tech">
|
<BaseLayout title="wait4x" description="wait4x's blog: reading · code · photography">
|
||||||
<h1 class="visually-hidden">wait4x</h1>
|
|
||||||
<IdCard>
|
<IdCard>
|
||||||
<a href="https://github.com/wait4xx" rel="noopener noreferrer" aria-label="GitHub">↗</a>
|
<div class="socials">
|
||||||
<a href="/en/rss.xml" aria-label="RSS">RSS</a>
|
<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>
|
</IdCard>
|
||||||
{years.map((year) => (
|
|
||||||
<section class="latest" aria-label={t('latest_photos', 'en')}>
|
<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>
|
<span class="lab">{t('latest_photos', 'en')}</span>
|
||||||
<a href="/en/gallery/">{t('enter_gallery', 'en')}</a>
|
<a class="tap more" href="/en/gallery/">{t('enter_gallery', 'en')}</a>
|
||||||
<div class="strip">
|
</div>
|
||||||
{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>))}
|
<div class="thumbs">
|
||||||
</div>
|
{photos.map((photo) => (
|
||||||
</section>
|
<a class="thumb" href="/en/gallery/" aria-label={`Gallery: ${photo.data.title}`}>
|
||||||
<section class="year">
|
<Image
|
||||||
<h2 class="y-label">{year}</h2>
|
src={photo.data.image}
|
||||||
{(['reading', 'tech', 'photo'] as const).map((cat) => {
|
alt={photo.data.alt ?? photo.data.title}
|
||||||
const list = posts.filter((p) => p.zh.data.category === cat && p.zh.data.pubDate.getFullYear() === year);
|
widths={[180, 360]}
|
||||||
return list.length ? (
|
sizes="(min-width: 820px) 20vw, 42vw"
|
||||||
<>
|
/>
|
||||||
<h3 class="track">{t(cat === 'reading' ? 'category_reading' : cat === 'tech' ? 'category_tech' : 'category_photo', 'en')}</h3>
|
</a>
|
||||||
<ul class="plist">
|
))}
|
||||||
{list.map(({ zh: p, en: e }) => (
|
</div>
|
||||||
<li>
|
</section>
|
||||||
<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
|
<Heatmap
|
||||||
labels={{
|
labels={{
|
||||||
title: t('heatmap_title', 'en'),
|
title: t('heatmap_title', 'en'),
|
||||||
@@ -70,17 +95,61 @@ const t9 = (cat: keyof typeof POST_TRACKS) => trackOf(cat);
|
|||||||
note: t('heatmap_note', 'en'),
|
note: t('heatmap_note', 'en'),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<ProjectWall />
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.year { margin-top: var(--space-5); }
|
.socials {
|
||||||
.y-label { font-family: var(--font-mono); font-size: 20px; color: var(--ink-1); }
|
display: flex;
|
||||||
.track { font-size: 13px; color: var(--ink-3); margin: var(--space-3) 0 var(--space-2); letter-spacing: 0.06em; }
|
align-items: center;
|
||||||
.plist { list-style: none; padding: 0; display: grid; gap: var(--space-1); }
|
gap: var(--space-2);
|
||||||
.plink { display: flex; align-items: baseline; gap: var(--space-2); padding-block: var(--space-1); }
|
margin-top: var(--space-2);
|
||||||
.plink:hover .ptitle { color: var(--accent); }
|
}
|
||||||
.ptitle { color: var(--ink-1); transition: color var(--dur) var(--ease); }
|
.social {
|
||||||
.mdot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--mood, var(--ink-4)); margin-right: 6px; }
|
color: var(--ink-3);
|
||||||
.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; }
|
border-radius: 8px;
|
||||||
.pdate { margin-left: auto; font-family: var(--font-mono); font-size: 12px; color: var(--ink-3); }
|
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>
|
</style>
|
||||||
Reference in new issue
Block a user