让开发日志能按项目与月份快速回看

顺手统一 personal-homepage 相关日志与项目种子数据里的 repo 标识,避免筛选项和项目映射出现分裂。

Constraint: 保持 Astro 静态站点结构与最小改动范围

Rejected: URL query 持久化筛选 | 超出本次最小可用范围

Confidence: high

Scope-risk: narrow

Directive: 后续若补 URL 状态同步,应复用现有 data-repo/data-month 过滤语义

Tested: npm run build; ./scripts/deploy-homepage.sh

Not-tested: 筛选状态刷新保留与分享链接能力
This commit is contained in:
SepComet 2026-05-09 15:21:15 +08:00
parent 3aa966f3d5
commit 28358164c5
6 changed files with 173 additions and 7 deletions

View File

@ -1,7 +1,7 @@
--- ---
title: "首页设计系统落稿" title: "首页设计系统落稿"
date: 2026-05-03 date: 2026-05-03
repo: "sepcomet/personal-homepage" repo: "basil/personal-homepage"
tags: ["design", "ui", "editorial"] tags: ["design", "ui", "editorial"]
summary: "确定工程编辑部风格:浅色编辑布局为主,局部保留终端式技术感。" summary: "确定工程编辑部风格:浅色编辑布局为主,局部保留终端式技术感。"
commit: "draft" commit: "draft"

View File

@ -1,7 +1,7 @@
--- ---
title: "首页首版落地" title: "首页首版落地"
date: 2026-05-03 date: 2026-05-03
repo: "sepcomet/personal-homepage" repo: "basil/personal-homepage"
tags: ["implementation", "homepage", "astro"] tags: ["implementation", "homepage", "astro"]
summary: "从零重建 Astro 首页骨架,补齐导航、内容卡片、活动面板和基础详情路由。" summary: "从零重建 Astro 首页骨架,补齐导航、内容卡片、活动面板和基础详情路由。"
commit: "draft" commit: "draft"

View File

@ -1,7 +1,7 @@
--- ---
title: "个人主页需求梳理" title: "个人主页需求梳理"
date: 2026-05-03 date: 2026-05-03
repo: "sepcomet/personal-homepage" repo: "basil/personal-homepage"
tags: ["planning", "astro", "content-model"] tags: ["planning", "astro", "content-model"]
summary: "整理首页、日志、项目和分享页范围,确定内容由 Markdown 与 JSON 驱动。" summary: "整理首页、日志、项目和分享页范围,确定内容由 Markdown 与 JSON 驱动。"
commit: "draft" commit: "draft"

View File

@ -46,7 +46,7 @@
{ {
"name": "几何塔防-基础", "name": "几何塔防-基础",
"description": "几何塔防的基础层,使用纯 C# 开发", "description": "几何塔防的基础层,使用纯 C# 开发",
"gitea_repo": "basil/geometry-tower-defense-Bbase", "gitea_repo": "basil/geometry-tower-defense-base",
"cover_image": "/images/projects/geometry-tower-defense.png", "cover_image": "/images/projects/geometry-tower-defense.png",
"demo_video": "", "demo_video": "",
"download_link": "", "download_link": "",

View File

@ -7,6 +7,29 @@ import { site } from '../../config';
const logs = (await getCollection('logs')).sort( const logs = (await getCollection('logs')).sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime(), (a, b) => b.data.date.getTime() - a.data.date.getTime(),
); );
const monthFormatter = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: 'long',
});
function getMonthKey(date: Date) {
const parts = new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
}).formatToParts(date);
const year = parts.find((part) => part.type === 'year')?.value ?? '';
const month = parts.find((part) => part.type === 'month')?.value ?? '';
return `${year}-${month}`;
}
const repoOptions = [...new Set(logs.map((log) => log.data.repo))].sort((a, b) => a.localeCompare(b));
const monthOptions = [...new Set(logs.map((log) => getMonthKey(log.data.date)))].map((value) => ({
value,
label: monthFormatter.format(new Date(`${value}-01T00:00:00+08:00`)),
}));
--- ---
<Layout <Layout
@ -15,16 +38,108 @@ const logs = (await getCollection('logs')).sort(
currentPath="/logs" currentPath="/logs"
> >
<section class="list-page"> <section class="list-page">
<div class="container"> <div class="container" data-logs-filter-root>
<div class="page-intro"> <div class="page-intro">
<span class="eyebrow">Logs</span> <span class="eyebrow">Logs</span>
<h1>开发日志</h1> <h1>开发日志</h1>
<p>这里承载由 Markdown 驱动的开发日志。当前先放入 3 条示例内容,后续可由外部系统继续写入。</p> <p>这里承载由 Markdown 驱动的开发日志。现在可以按项目和月份快速筛选,方便回看不同阶段的开发记录。</p>
</div>
<div class="logs-filters card">
<div class="logs-filters__controls">
<label class="logs-filters__field">
<span>项目</span>
<select data-logs-filter-repo>
<option value="">全部项目</option>
{repoOptions.map((repo) => (
<option value={repo}>{repo}</option>
))}
</select>
</label>
<label class="logs-filters__field">
<span>时间</span>
<select data-logs-filter-month>
<option value="">全部时间</option>
{monthOptions.map((month) => (
<option value={month.value}>{month.label}</option>
))}
</select>
</label>
</div>
<p class="logs-filters__status" data-logs-filter-status>共 {logs.length} 条日志</p>
</div> </div>
<div class="logs-list"> <div class="logs-list">
{logs.map((log) => <LogCard slug={log.id} {...log.data} />)} {
logs.map((log) => (
<div
data-log-item
data-repo={log.data.repo}
data-month={getMonthKey(log.data.date)}
>
<LogCard slug={log.id} {...log.data} />
</div>
))
}
</div> </div>
<p class="logs-empty-state card" data-logs-empty-state hidden>当前筛选条件下还没有日志。</p>
</div> </div>
</section> </section>
</Layout> </Layout>
<script>
const filterRoots = document.querySelectorAll('[data-logs-filter-root]');
filterRoots.forEach((root) => {
const repoSelect = root.querySelector('[data-logs-filter-repo]');
const monthSelect = root.querySelector('[data-logs-filter-month]');
const status = root.querySelector('[data-logs-filter-status]');
const emptyState = root.querySelector('[data-logs-empty-state]');
const items = Array.from(root.querySelectorAll('[data-log-item]'));
if (!(repoSelect instanceof HTMLSelectElement) || !(monthSelect instanceof HTMLSelectElement)) {
return;
}
const renderStatus = (visibleCount) => {
if (!status) return;
status.textContent =
visibleCount === items.length
? `共 ${items.length} 条日志`
: `筛选结果:${visibleCount} / ${items.length} 条日志`;
};
const applyFilters = () => {
const repoValue = repoSelect.value;
const monthValue = monthSelect.value;
let visibleCount = 0;
items.forEach((item) => {
if (!(item instanceof HTMLElement)) return;
const matchesRepo = !repoValue || item.dataset.repo === repoValue;
const matchesMonth = !monthValue || item.dataset.month === monthValue;
const shouldShow = matchesRepo && matchesMonth;
item.hidden = !shouldShow;
if (shouldShow) {
visibleCount += 1;
}
});
if (emptyState instanceof HTMLElement) {
emptyState.hidden = visibleCount > 0;
}
renderStatus(visibleCount);
};
repoSelect.addEventListener('change', applyFilters);
monthSelect.addEventListener('change', applyFilters);
applyFilters();
});
</script>

View File

@ -456,6 +456,53 @@ main {
gap: 1rem; gap: 1rem;
} }
.logs-filters {
display: grid;
gap: 1rem;
margin-bottom: 1rem;
}
.logs-filters__controls {
display: flex;
flex-wrap: wrap;
gap: 0.9rem 1rem;
}
.logs-filters__field {
display: grid;
gap: 0.45rem;
min-width: min(280px, 100%);
}
.logs-filters__field span {
font-size: 0.88rem;
font-weight: 600;
color: var(--muted-strong);
}
.logs-filters__field select {
width: 100%;
min-height: 2.9rem;
padding: 0.7rem 0.9rem;
border: 1px solid var(--border);
border-radius: 0.95rem;
background: var(--surface-strong);
color: var(--text);
font: inherit;
}
.logs-filters__status {
margin: 0;
color: var(--muted);
font-size: 0.92rem;
}
.logs-empty-state {
margin-top: 1rem;
color: var(--muted);
text-align: center;
}
.log-card { .log-card {
display: grid; display: grid;
gap: 1rem; gap: 1rem;
@ -833,6 +880,10 @@ main {
align-items: stretch; align-items: stretch;
} }
.logs-filters__field {
min-width: 100%;
}
.log-card__meta-right { .log-card__meta-right {
justify-content: flex-start; justify-content: flex-start;
text-align: left; text-align: left;