让开发日志能按项目与月份快速回看
顺手统一 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:
parent
3aa966f3d5
commit
28358164c5
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: "首页设计系统落稿"
|
||||
date: 2026-05-03
|
||||
repo: "sepcomet/personal-homepage"
|
||||
repo: "basil/personal-homepage"
|
||||
tags: ["design", "ui", "editorial"]
|
||||
summary: "确定工程编辑部风格:浅色编辑布局为主,局部保留终端式技术感。"
|
||||
commit: "draft"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: "首页首版落地"
|
||||
date: 2026-05-03
|
||||
repo: "sepcomet/personal-homepage"
|
||||
repo: "basil/personal-homepage"
|
||||
tags: ["implementation", "homepage", "astro"]
|
||||
summary: "从零重建 Astro 首页骨架,补齐导航、内容卡片、活动面板和基础详情路由。"
|
||||
commit: "draft"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: "个人主页需求梳理"
|
||||
date: 2026-05-03
|
||||
repo: "sepcomet/personal-homepage"
|
||||
repo: "basil/personal-homepage"
|
||||
tags: ["planning", "astro", "content-model"]
|
||||
summary: "整理首页、日志、项目和分享页范围,确定内容由 Markdown 与 JSON 驱动。"
|
||||
commit: "draft"
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
{
|
||||
"name": "几何塔防-基础",
|
||||
"description": "几何塔防的基础层,使用纯 C# 开发",
|
||||
"gitea_repo": "basil/geometry-tower-defense-Bbase",
|
||||
"gitea_repo": "basil/geometry-tower-defense-base",
|
||||
"cover_image": "/images/projects/geometry-tower-defense.png",
|
||||
"demo_video": "",
|
||||
"download_link": "",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,29 @@ import { site } from '../../config';
|
|||
const logs = (await getCollection('logs')).sort(
|
||||
(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
|
||||
|
|
@ -15,16 +38,108 @@ const logs = (await getCollection('logs')).sort(
|
|||
currentPath="/logs"
|
||||
>
|
||||
<section class="list-page">
|
||||
<div class="container">
|
||||
<div class="container" data-logs-filter-root>
|
||||
<div class="page-intro">
|
||||
<span class="eyebrow">Logs</span>
|
||||
<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 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>
|
||||
|
||||
<p class="logs-empty-state card" data-logs-empty-state hidden>当前筛选条件下还没有日志。</p>
|
||||
</div>
|
||||
</section>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -456,6 +456,53 @@ main {
|
|||
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 {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
|
|
@ -833,6 +880,10 @@ main {
|
|||
align-items: stretch;
|
||||
}
|
||||
|
||||
.logs-filters__field {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.log-card__meta-right {
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
|
|
|
|||
Loading…
Reference in New Issue