- 首页:subtitle(markdown 短文本)|latestPhotos 0-6(0 隐藏)|
perTrackLimit 4-8|sections{tracks,photos,heatmap,projects} 开关,
访问器 homeConfig() 夹取越界值,字段级回落默认(零配置=原形态)
- 开源页项目可见性:mode all|only|exclude + include[]/exclude[] +
sort pinned-first|stars|updated + perSource both|github|gitea;
projects.ts 加 LoadProjectsOptions + arrange()(来源→可见性→排序)
- 开源页开篇支持 GitHub README:intro_from off|github-readme,构建期
取 wait4xx/wait4xx README(token 复用宿主 0600 文件)→ 缓存
content/.github-readme.md 入仓 → renderMarkdown;失败逐级回落
(缓存 → yaml blurb)。实测取到 3117 字符,现状仍 off 待用户开启
- 修既有 bug:orderPinned 内硬 slice(0,6) 把开源页 limit=99 也截到 6 条
→ 移除,开源页恢复全量 16 条
- 文档:写作指南加首页/项目墙两张字段表(含 YAML 1.1 `off` 需加引号坑、
README 行为与外部徽章风险说明)
车道:op-gitea(3/3);收编复验:构建 48 页 + E2E 37/37 + 开源页 16 条 +
首页默认形态(2 轨/热力图/项目墙)无回归
验证: node v26.13.0 构建 48 页 exit 0;E2E 37/37 PASS;playwright 实测
Spec: none (首页与开源页后台扩展,车道级交付)
Co-authored-by: GLM-5.3
162 lines
4.9 KiB
Plaintext
162 lines
4.9 KiB
Plaintext
---
|
||
import { Image } from 'astro:assets';
|
||
import { getCollection } from 'astro:content';
|
||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||
import IdCard from '../components/IdCard.astro';
|
||
import PostList from '../components/PostList.astro';
|
||
import ProjectWall from '../components/geek/ProjectWall.astro';
|
||
import Heatmap from '../components/geek/Heatmap.astro';
|
||
import { fetchHeartbeat, agoZh } from '../lib/heartbeat';
|
||
import { t } from '../i18n/ui';
|
||
import { homeConfig, homeTagline, renderMarkdown } from '../lib/site-content';
|
||
import SocialLinks from '../components/SocialLinks.astro';
|
||
|
||
// 首页可配项(后台主页设置):副标题/影像张数/区块开关/每轨条数,字段级回落
|
||
const cfg = await homeConfig('zh');
|
||
const subtitleHtml = cfg.subtitle ? await renderMarkdown(cfg.subtitle) : '';
|
||
const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden'))).sort(
|
||
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
|
||
);
|
||
// 最新影像取集合前 4 张;照片时间序由照片管线(exif 车道)治理,此处只裁切
|
||
const photos = (await getCollection('photos')).slice(0, Math.max(1, cfg.latestPhotos));
|
||
const heartbeat = await fetchHeartbeat();
|
||
// 身份语改由 content/site/home.yaml 供给(后台可改),缺值回落默认文案
|
||
const tagline = await homeTagline('zh');
|
||
---
|
||
|
||
<BaseLayout title="wait4x">
|
||
<IdCard>
|
||
<p class="tagline">{tagline}</p>
|
||
{subtitleHtml && <div class="subtitle" set:html={subtitleHtml} />}
|
||
<SocialLinks />
|
||
</IdCard>
|
||
|
||
{cfg.sections.tracks && <PostList {posts} perTrack={cfg.perTrackLimit} />}
|
||
|
||
{cfg.sections.photos && cfg.latestPhotos > 0 && (
|
||
<section class="latest" aria-label="最新影像">
|
||
<div class="latest-head">
|
||
<span class="lab">最新影像</span>
|
||
<a class="tap more" href="/gallery/">进入图库 →</a>
|
||
</div>
|
||
<div class="thumbs">
|
||
{photos.map((photo, i) => (
|
||
<a class="thumb" href="/gallery/" aria-label={`查看图库:${photo.data.title}`}>
|
||
{/* 首两图 eager(Astro Image 默认 lazy 会把 LCP 压到 6s+),首图再拔 fetchpriority */}
|
||
<Image
|
||
src={photo.data.image}
|
||
alt={photo.data.alt ?? photo.data.title}
|
||
widths={[180, 360]}
|
||
sizes="(min-width: 820px) 20vw, 42vw"
|
||
loading={i < 2 ? 'eager' : 'lazy'}
|
||
fetchpriority={i === 0 ? 'high' : undefined}
|
||
/>
|
||
</a>
|
||
))}
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
{cfg.sections.heatmap && <Heatmap
|
||
labels={{
|
||
title: t('heatmap_title', 'zh'),
|
||
summary: t('heatmap_summary', 'zh'),
|
||
less: t('heatmap_less', 'zh'),
|
||
more: t('heatmap_more', 'zh'),
|
||
note: t('heatmap_note', 'zh'),
|
||
}}
|
||
/>}
|
||
{cfg.sections.projects && <ProjectWall />}
|
||
{heartbeat && (
|
||
<p class="heartbeat" aria-label="最近心跳">
|
||
<span class="beat-dot" aria-hidden="true"></span>
|
||
最近心跳:{agoZh(heartbeat.when)} · {heartbeat.msg}
|
||
</p>
|
||
)}
|
||
</BaseLayout>
|
||
|
||
<style>
|
||
.subtitle {
|
||
font-size: 13.5px;
|
||
color: var(--ink-4);
|
||
margin-top: 2px;
|
||
letter-spacing: 0.01em;
|
||
}
|
||
.subtitle :global(p) { margin: 0; }
|
||
.subtitle :global(a) { color: var(--accent); }
|
||
.tagline {
|
||
font-size: 14px;
|
||
color: var(--ink-3);
|
||
margin-top: 4px;
|
||
letter-spacing: 0.01em;
|
||
}
|
||
.heartbeat {
|
||
font-family: var(--font-mono);
|
||
font-size: 12.5px;
|
||
color: var(--ink-3);
|
||
margin-top: var(--space-2);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.beat-dot {
|
||
width: 7px; height: 7px; border-radius: 50%;
|
||
background: var(--ink-2);
|
||
animation: beat 2.4s ease-in-out infinite;
|
||
}
|
||
@keyframes beat { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
|
||
@media (prefers-reduced-motion: reduce) { .beat-dot { animation: none; } }
|
||
.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>
|