--- // 年份分组 + 轨道分节的文章列表(首页与归档页共用) // 轨道标签单一事实源:content.config 的 POST_TRACKS,此处不另立词表 // 双形态(2026-11-02 用户拍板折叠需求): // 默认形态 = 首页/列表平铺(年 → 轨道分节),可 limit 截断 + moreHref 出口 // fold 形态 = 归档页:按年
(最新年默认开)+ 年内按月分组,零 JS 原生折叠 import type { CollectionEntry } from 'astro:content'; import { POST_TRACKS, type PostCategory } from '../content.config'; import { t } from '../i18n/ui'; interface Props { /** 调用方已完成 draft 过滤与 pubDate 倒序 */ posts: CollectionEntry<'posts'>[]; locale?: 'zh' | 'en'; /** slug → 译题(en 列表显示英文标题,视觉复核范围外观察) */ titleOverrides?: Record; /** 截断条数(首页 6 篇);缺省全量 */ limit?: number; /** 截断后的「全部文章 →」出口链接;缺省不渲染 */ moreHref?: string; /** 分轨模式:阅读轨与技朮轨各取 N 条(首页两段式,各段自带「更多」出口) */ perTrack?: number; /** true = 归档折叠形态:按年 details(最新年开)+年内按月分组 */ fold?: boolean; } const { posts, locale = 'zh', titleOverrides, limit, moreHref, perTrack, fold = false } = Astro.props; const visible = typeof limit === 'number' ? posts.slice(0, limit) : posts; // 轨道标签走字典(en 首页复用本组件时的双语位);zh 保持原词表语义 const TRACK_LABEL: Record = { 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}/`); // 置顶浮出分组流(2026-11-05 用户实测修复):置顶文章若留在分组流里, // 它所在月会因「顺次建桶」被提前建到顶部,整月文章跟着上浮—— // 所以置顶单独一节渲染在年/月分组之前,分组只对非置顶文章按自然时序建桶。 const pinned = visible.filter((p) => p.data.pinned); const flow = visible.filter((p) => !p.data.pinned); const byYear = new Map[]>(); for (const post of flow) { const year = post.data.pubDate.getUTCFullYear(); const bucket = byYear.get(year); if (bucket) bucket.push(post); else byYear.set(year, [post]); } const units = t('unit_posts', locale); const mmdd = (d: Date) => `${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`; // 月分组标签:zh「2026 年 11 月」/ en「November 2026」(UTC,与年份口径一致) const monthLabel = (d: Date) => locale === 'zh' ? `${d.getUTCFullYear()} 年 ${d.getUTCMonth() + 1} 月` : `${d.toLocaleString('en-US', { month: 'long', timeZone: 'UTC' })} ${d.getUTCFullYear()}`; const byMonth = (list: CollectionEntry<'posts'>[]) => { const m = new Map[]>(); for (const post of list) { const key = `${post.data.pubDate.getUTCFullYear()}-${post.data.pubDate.getUTCMonth()}`; const bucket = m.get(key); if (bucket) bucket.push(post); else m.set(key, [post]); } return [...m.entries()]; }; --- {perTrack ? (
{( [ { key: 'reading' as PostCategory, more: locale === 'en' ? '/en/books/' : '/books/' }, { key: 'tech' as PostCategory, more: locale === 'en' ? '/en/archives/' : '/archives/' }, ] as const ).map(({ key, more }) => { const rows = posts.filter((post) => post.data.category === key).slice(0, perTrack); if (rows.length === 0) return null; return (

{TRACK_LABEL[key]} {t('home_more_posts', locale)}