让开发日志能按项目与月份快速回看
顺手统一 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: "首页设计系统落稿"
|
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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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": "",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue