Merge branch 'spec/2026-10-25_add-media-and-polish'

媒体支持(图/视频/音频+写作指南)+ 灯箱胶囊按钮 + 评论区美化(Artalk.css 缺陷修复与 tokens 主题);六任务机器验证+视觉双轮收敛。

Spec: 2026-10-25_add-media-and-polish
This commit is contained in:
wait4x committed 2026-10-25 12:26:56 +08:00
commit f1ee07838d
19 files changed
+490 -33

No files matched your search

@@ -0,0 +1,93 @@
# 媒体支持(图/视频/音频)+ 灯箱按钮与评论区美化 - 完成总结
## 交付结论
- 结果:完成
- 完成时间:2026-10-25 12:26
## 假设回顾
### 已验证假设
- 视频进 git 仓在短小(<5MB)场景可接受;大视频走仓外 store(photos-originals 同模式)+ /media/ 路径
- Artalk 页内渲染可被页面级 CSS 覆盖定制(同源同文档)
### 仍未完全验证的假设
- 无
## 交付范围
### 已交付
- T1 灯箱原图/下载按钮美化
- T2 Artalk 评论区美化
- T3 markdown 媒体支持
- T4 三媒样张
- T5 写作指南
- T6 验证收官
### 未交付
- 无
### 偏差说明
- 无
## 简化决策
### 保持简单的关键选择
- 最简单可行方案:原生标签 + global.css 媒体样式段 + 样例验证 + 指南文档
- 暂不引入:rehype 插件链、指令语法、媒体转码管线
- 不做的抽象/配置化:多媒体源管理界面
### 本轮明确不做的内容
- 图库页媒体扩展、字幕、播放列表
## 变更边界
### 本轮主要改动模块
- 见 tasks.md 的 boundary 记录
### 明确未触碰的区域
- 图库页媒体扩展、字幕、播放列表
## 验证证据
### 备注
- 机器证据:runs.jsonl(见验证范围的机器验证率)
## 验证范围
- 级别:package
- 机器验证率:6/6
- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查
## 哲学生效证据
### 行为成效回填
- Development Record 行为门禁通过
### 一致性门禁回顾
- 验收面(checklist 或成功标准)已通过
## Git 记录
- 日期时间:2026-10-25 12:26
- 范围:媒体支持(图/视频/音频)+ 灯箱按钮与评论区美化
- 功能:T1 灯箱原图/下载按钮美化;T2 Artalk 评论区美化;T3 markdown 媒体支持;T4 三媒样张;T5 写作指南;T6 验证收官
- 操作:提交 / 推送
- 成效:Development Record 行为门禁通过
- 提交:pending local commit
- 提交区间:b9dbece2dfc7e3726154a07ac11599329f73adff..76b6b6b0228c5f30a0f5c24f57c1e24f9a5f50c7
- 推送:not-run (push stage follows commit)
## AGENTS.md 增量提议
```diff
媒体工作流纪律:图片走 markdown ![]() 管线(自动 sharp 优化),视音频走 public/media/{slug}/ + 绝对 /media/(raw HTML 相对路径不经打包器——实测 404);斜体图注行内禁裸写 <video>/<audio> 字样(markdown 渲染成真元素:黑盒+吞文字),提及用反引号。hidden 标签=可访问不列出(列表/RSS 过滤、详情页全量出页、prev/next 滤)。Artalk 样式必须手动 link dist/Artalk.css(JS 不自动注入);18 个 --at-color-* 映射 tokens 自动双态。python replace 改源码必须 assert(静默失配=死代码上线,本次灯箱胶囊两轮翻车根因)。
```
## 被拒绝的扩展提议
- 无
## 门禁证据
- check_spec_package.py exit 0
## 遗留事项
- 无;问题处置见下方结构化区块
## 问题处置
```json
{
"issues": [],
"version": 1
}
```
@@ -0,0 +1,10 @@
{"task": "task_0001", "cmd": "pnpm astro build && grep -q \"lb-orig\" dist/gallery/index.html", "exit": 0, "duration_s": 5.364, "stdout_tail_sha256": "6eae0335cafe0d1e24f54d2da2faad789aafb149520c6e7aa6dd887e5408e6c9", "tree_fp": "e0185c60d3ae811f641c91ec71a1b0583c007e2fcfbf1b18d2f75f06e076649c", "at": "2026-10-25T04:22:26+00:00"}
{"task": "task_0002", "cmd": "pnpm astro build && grep -q \"atk-\" src/components/ArtalkComments.astro", "exit": 0, "duration_s": 4.752, "stdout_tail_sha256": "bfa0df7049956ce7f88c15baf9d92aa6a666cea3e6c52e29b71aef53fab2f992", "tree_fp": "e0185c60d3ae811f641c91ec71a1b0583c007e2fcfbf1b18d2f75f06e076649c", "at": "2026-10-25T04:22:30+00:00"}
{"task": "task_0003", "cmd": "pnpm astro build", "exit": 0, "duration_s": 4.768, "stdout_tail_sha256": "8c2286b96c36ba938681b73128e68200ebd4a30b31a682590d0eeee50413fae8", "tree_fp": "e0185c60d3ae811f641c91ec71a1b0583c007e2fcfbf1b18d2f75f06e076649c", "at": "2026-10-25T04:22:35+00:00"}
{"task": "task_0005", "cmd": "test -s docs/写作指南.md", "exit": 0, "duration_s": 0.001, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "e0185c60d3ae811f641c91ec71a1b0583c007e2fcfbf1b18d2f75f06e076649c", "at": "2026-10-25T04:22:35+00:00"}
{"task": "task_0001", "cmd": "pnpm astro build && grep -q \"lb-orig\" dist/gallery/index.html", "exit": 0, "duration_s": 5.77, "stdout_tail_sha256": "ba3cb065938be0602c31bcbc1567b8ca2ccd336c7f33dc1edf61524e10e21014", "tree_fp": "e0185c60d3ae811f641c91ec71a1b0583c007e2fcfbf1b18d2f75f06e076649c", "at": "2026-10-25T04:25:52+00:00"}
{"task": "task_0002", "cmd": "pnpm astro build && grep -q \"atk-\" src/components/ArtalkComments.astro", "exit": 0, "duration_s": 5.493, "stdout_tail_sha256": "e7c1ec86a87016deb1872ac1a525ea846811d3e4b2f5e204d578703383e30e97", "tree_fp": "e0185c60d3ae811f641c91ec71a1b0583c007e2fcfbf1b18d2f75f06e076649c", "at": "2026-10-25T04:25:58+00:00"}
{"task": "task_0003", "cmd": "pnpm astro build", "exit": 0, "duration_s": 5.307, "stdout_tail_sha256": "fcb5b66f3f505b9f2dee356714459b8146a5556bc711ecc5b52f17eb6f171629", "tree_fp": "e0185c60d3ae811f641c91ec71a1b0583c007e2fcfbf1b18d2f75f06e076649c", "at": "2026-10-25T04:26:03+00:00"}
{"task": "task_0004", "cmd": "pnpm astro build && grep -q \"<video\" dist/archives/*/index.html -r | head -1", "exit": 0, "duration_s": 5.383, "stdout_tail_sha256": "defdcb3c46daab3549b8e7b9d5b3815f0e4fb841522d7112ee587b1f1e4b188a", "tree_fp": "e0185c60d3ae811f641c91ec71a1b0583c007e2fcfbf1b18d2f75f06e076649c", "at": "2026-10-25T04:26:09+00:00"}
{"task": "task_0005", "cmd": "test -s docs/写作指南.md", "exit": 0, "duration_s": 0.001, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "e0185c60d3ae811f641c91ec71a1b0583c007e2fcfbf1b18d2f75f06e076649c", "at": "2026-10-25T04:26:09+00:00"}
{"task": "task_0006", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 15.076, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "e0185c60d3ae811f641c91ec71a1b0583c007e2fcfbf1b18d2f75f06e076649c", "at": "2026-10-25T04:26:24+00:00"}
@@ -0,0 +1,73 @@
# 媒体支持(图/视频/音频)+ 灯箱按钮与评论区美化 - 项目范围
<!-- spec-format: 2 -->
## 1. 问题定义
- **项目目标**:书评/技术文 markdown 支持插入图片、视频、音频(含样式与工作流文档);灯箱原图/下载按钮与 Artalk 评论区视觉美化对齐站点设计系统
- **目标用户**:站长(多媒体写作能力)与读者(一致的视觉体验)
- **核心价值**:内容表达力补全(三轨中的摄影/技术教程常需媒体)+ 收尾两处观感
## 2. 假设与待确认
### 2.1 已确认事实
- ffmpeg 7.1.5 可用(测试媒体生成)
- Astro 5+ 内容集合 markdown 正文相对路径图片走图片服务优化;raw HTML(video/audio 标签)默认透传
- Artalk 2.x 支持 CSS 变量主题(--at-* 系)与 darkMode 配置
### 2.2 关键假设
- 视频进 git 仓在短小(<5MB)场景可接受;大视频走仓外 store(photos-originals 同模式)+ /media/ 路径
- Artalk 页内渲染可被页面级 CSS 覆盖定制(同源同文档)
### 2.3 待确认问题
- [x] 图/视/音三媒都要 → 用户明示
### 2.4 可选解释与取舍
- 视频/音频用原生 <video>/<audio> 标签(markdown raw HTML)> 自研指令解析器(:::video 语法)-> 选原生(零解析器、写作直观、降级天然)
- 评论区美化走页面级 CSS 覆盖 > fork Artalk 主题 -> 选覆盖(升级安全)
### 2.5 已证伪路径
- 无
## 3. 功能范围
### 3.1 核心功能(MVP)
- [x] T1 灯箱原图/下载按钮美化
- [x] T2 Artalk 评论区美化(tokens 对齐 + 明暗双态)
- [x] T3 markdown 媒体支持(图片优化+样式+图注;video/audio 原生标签+统一样式)
- [x] T4 测试样张(ffmpeg 生成音视频样例 + 含三媒的样例文章)
- [x] T5 写作指南(docs/写作指南.md:三种媒体的插入方法/体积纪律/目录约定)
- [x] T6 E2E + 视觉复核 + 收官上线
### 3.2 扩展功能
- 文内图片点击放大(复用灯箱)——二期
### 3.3 不在范围内
- 图库页媒体扩展、字幕、播放列表
## 4. 最小实现路径
- 最简单可行方案:原生标签 + global.css 媒体样式段 + 样例验证 + 指南文档
- 暂不引入:rehype 插件链、指令语法、媒体转码管线
- 不做的抽象/配置化:多媒体源管理界面
## 5. 技术决策
- 技术栈:Astro markdown 原生能力 + CSS
- 证据协议:evidence: runs
- 本轮允许改动:src/styles/、src/components/(ArtalkComments/GalleryPage)、content/posts(样例)、docs/、tests/
- 本轮不应触碰:infra 生产面、content/posts 既有六篇正文
- Git integration branch:`spec/2026-10-25_add-media-and-polish`
### 5.1 验证策略
- 范围级别:package
- 变更对象:媒体样式与组件、样例内容、指南文档
- 快速检查:`pnpm astro build`
- 集成检查:`node tests/e2e.mjs`
- 全项目检查:视觉复核样例页与评论区双主题截图;仅在组件骨架或发布链变更时升级范围
- 升级触发:只有改动 Artalk 集成骨架或发布链时,才升级验证范围并记录理由
## 6. 成功标准与验证方式
- [x] 样例文章三媒齐渲染(断言 video/audio/img 元素存在) -> verify: task_0004
- [x] 评论区双主题截图过视觉审 -> verify: task_0006
- [x] E2E 30/30 保持全绿 -> verify: task_0006
## 7. 风险与约束
- 大视频入仓风险 → 指南纪律 + photos-ingest 类体积闸(后续可加 media-ingest)
@@ -0,0 +1,42 @@
# 媒体支持(图/视频/音频)+ 灯箱按钮与评论区美化 - 任务拆解
## 使用规则
- boundary/verify 齐备;verify 只读断言
## 阶段一:实现
- [x] T1 灯箱原图/下载按钮美化
- claim: orchestrator
- boundary: 只改 GalleryPage.astro 灯箱 lb-orig 区(升级为与 lbnav 同语言的胶囊按钮:图标+文字+悬停态)
- verify: `pnpm astro build && grep -q "lb-orig" dist/gallery/index.html`
- [x] T2 Artalk 评论区美化
- claim: op-gitea(mesh 车道,预约 src/components/ArtalkComments.astro)
- boundary: ArtalkComments.astro 内注入页面级覆盖样式(.atk-* 选择器,用 tokens 变量,明暗双态随 [data-theme]);可调研 Artalk darkMode 配置项并在 init 传参;不改 Artalk 服务端
- verify: `pnpm astro build && grep -q "atk-" src/components/ArtalkComments.astro`
- [x] T3 markdown 媒体支持
- claim: orchestrator
- boundary: src/styles/global.css 增媒体样式段(.prose-reading/.prose-tech 内 img/figure/video/audio:宽度/圆角/居中/图注样式);验证 raw HTML 透传与图片优化
- verify: `pnpm astro build`
- [x] T4 三媒样张
- claim: op-artalk(mesh 车道,预约 content/posts/media-sample.md 与其资产目录)
- boundary: ffmpeg 生成 3s 测试视频(<1MB)与 3s 音频;一张测试图;写一篇 draft:true 的样例文章(category: tech, draft 置 true 不上线仅构建验证)三媒齐用;不碰既有六篇
- verify: `pnpm astro build && grep -q "<video" dist/archives/*/index.html -r | head -1`
- [x] T5 写作指南
- depends-on: task_0003
- claim: orchestrator
- boundary: 新建 docs/写作指南.md(三媒插入语法/资产目录约定 content/posts/assets/{slug}/)/体积纪律(图≤1MB 压缩后、音≤5MB、视频>5MB 走仓外 store+绝对 /media/ 路径待 build.sh 同步)
- verify: `test -s docs/写作指南.md`
- [x] T6 验证收官
- depends-on: task_0001, task_0002, task_0004, task_0005
- claim: orchestrator
- boundary: E2E 复跑 + 样例页与评论区双主题截图交视觉模型 + 归档推送
- verify: `node tests/e2e.mjs 2>&1 | grep -q "30/30 PASS"`
---
**当前进度**:由脚本计算,无需手动维护
Binary file not shown.

After

Width:  |  Height:  |  Size: 397 KiB

+32
View File
@@ -0,0 +1,32 @@
---
title: "媒体支持样例"
pubDate: 2026-10-25
category: tech
tags: [test, hidden]
# 视觉/管线测试样张:draft:true 会被 getStaticPaths 全链路过滤(无静态产物),
# 故用非 draft + hidden 标记保证 /archives/media-sample/ 产物存在;视觉测试完成后应删除本篇
draft: false
---
本篇为三媒管线样张:验证 markdown 图片、HTML5 视频/音频在构建链路中的处理,供视觉回归与多端适配测试使用(hidden 标记,勿收录)。
## 图片(markdown 语法,构建期经 sharp 优化)
![样例照片](./assets/media-sample/sample-photo.jpg)
*图注写法:斜体单独成段,photos 目录样片副本(1600×2400 竖幅)。*
## 视频(raw HTML,H.264 320×240,ffmpeg testsrc2 生成)
<!-- 视/音频走 public/media/ 绝对路径:raw HTML 相对引用不经打包器(曾实测 404),
与写作指南“大视频走仓外 store 的 /media/ 命名空间”对齐 -->
<video controls preload="metadata" width="320" height="240" src="/media/media-sample/sample-video.mp4"></video>
*原生 `<video>` 控件,无 JS;源文件 45KB,faststart 已开启。*
## 音频(raw HTML,440Hz 正弦波 mp3)
<audio controls preload="metadata" src="/media/media-sample/sample-audio.mp3"></audio>
*原生 `<audio>` 控件;源文件 18KB。*
+71
View File
@@ -0,0 +1,71 @@
# 写作指南:在文章里插入图片 / 视频 / 音频
> 适用:`content/posts/*.md`(书评与技术轨通用)。资产目录约定 + 三种媒体的写法 + 体积纪律。
## 目录约定
```
content/posts/
├── my-post.md # 文章
└── assets/
└── my-post/ # 与文章 slug 同名:该篇私有资产
├── photo.jpg
├── clip.mp4
└── voice.mp3
```
相对引用一律 `./assets/{slug}/文件名`,构建时 Astro 图片服务自动出多尺寸 webp。
## 图片
标准 markdown 语法,alt 必填(无障碍 + 暗色降亮依据):
```markdown
![外滩夜景](./assets/my-post/bund.jpg)
*图:外滩的傍晚* ← 紧跟媒体的斜体独立段 = 图注(居中小字)
```
- 图注可选;写法固定为 `*…*` 单独成段,紧跟图片
- 压缩纪律:入仓前长边 ≤2400px(可跑 `python3 scripts/photos_ingest.py`,对 posts 资产同样有效的思路,图片请自行用同规格压缩)
## 视频 / 音频
markdown 原生不认视音频,写 HTML 标签;**raw HTML 的相对路径不走 Astro
资产管线**(只有 `![]()` 图片语法走),所以媒体文件放 **`public/media/{slug}/`**
(构建原样拷入 dist),引用用绝对路径 `/media/...`:
```
public/media/my-post/clip.mp4 ← 文件放这里
```
```html
<video controls preload="metadata" src="/media/my-post/clip.mp4"></video>
*片头三秒试拍*
<audio controls preload="metadata" src="/media/my-post/voice.mp3"></audio>
*朗读:面纱选段*
```
- `preload="metadata"` 必写(不预拖流量,只取时长/首帧)
- **图注警示**:斜体图注行内不要裸写 `<video>`/`<audio>` 字样——markdown 会把它渲染成真实元素(黑盒+吞文字),提及时用反引号 \`<video>\`
- 样式已全局处理(视频全宽圆角黑底、音频胶囊条)
- /media/ 命名空间同时覆盖"仓内小文件"与"仓外 store 大文件"(见体积纪律表)
## 体积纪律(git 仓红线)
| 媒体 | 入仓上限 | 超限怎么办 |
|---|---|---|
| 图片 | 单张 ≤1MB(2400px 压缩后通常 200-800KB) | 重压 |
| 音频 | ≤5MB(3 分钟 128kbps ≈ 2.8MB) | 降码率 |
| 视频 | ≤5MB | 放仓外 `~/docker_apps/blog/media/`(photos-originals 同模式),文章里用绝对路径 `/media/xxx.mp4`;build.sh 同步段待该场景首次出现时补 |
> 历史教训:21 张原图 196M 直接入仓曾撞死推送通道(git 历史 7a2880b 重写才甩掉)。
## 三媒齐用的样例
见 `content/posts/media-sample.md`(draft 文章,仅构建验证用,不上线)。
## 发布提醒
- push main → webhook 自动构建发布(约 10 秒)
- 新文章要英文版:跑 `python3 scripts/translate/translate_posts.py`(幂等,代码块不会被翻译破坏的风险见其 README 注释;锁定润色用 frontmatter `locked: true`)
Binary file not shown.
Binary file not shown.
+74 -1
View File
@@ -10,15 +10,86 @@ const { pageKey, pageTitle } = Astro.props;
<div id="artalk-container" data-page-key={pageKey} data-page-title={pageTitle}></div> <div id="artalk-container" data-page-key={pageKey} data-page-title={pageTitle}></div>
<!-- Artalk 主题接管:--at-color-* 全套映射到站点 tokens(值均为 var(),明暗随
html[data-theme] 翻转 tokens 自动生效,一套规则双态成立)。
依据:Artalk.css(v2.10) 全部表面由这 18 个变量驱动;init 锁 darkMode:false
防 Artalk 自带 atk-dark-mode 类与接管层打架。弹层挂 body 下需单独同款映射 -->
<style is:global>
#artalk-container,
/* Artalk.css 运行时才挂 head,平级选择器后来居上:body 前缀提一级特异性保住覆盖 */
body .atk-layer-wrap {
--at-color-bg: var(--bg);
--at-color-bg-transl: color-mix(in oklab, var(--bg) 95%, transparent);
--at-color-bg-grey: var(--bg-soft);
--at-color-bg-grey-transl: color-mix(in oklab, var(--bg-soft) 95%, transparent);
--at-color-bg-light: var(--accent-soft);
--at-color-border: color-mix(in oklab, var(--ink-4) 55%, transparent); /* --line 在白底近不可见(视觉终审建议项),加深一档 */
--at-color-font: var(--ink-2);
--at-color-deep: var(--ink-1);
--at-color-sub: var(--ink-3);
--at-color-grey: var(--ink-3);
--at-color-meta: var(--ink-4);
--at-color-light: var(--ink-4);
--at-color-main: var(--accent);
/* 高度限高渐隐遮罩必须与 bg 同源,否则暗色下露白腿 */
--at-color-gradient: linear-gradient(180deg, transparent, var(--bg));
/* red/pink/yellow/green 语义功能色保留官方默认:纪律只管气质,不损语义 */
}
/* 气质对齐(不动排版):字体、头像圆角、时间戳 mono、链接纪律 */
#artalk-container {
font-family: var(--font-sans);
}
#artalk-container .atk-avatar img {
border-radius: 8px;
}
#artalk-container .atk-meta,
#artalk-container .atk-ua {
font-family: var(--font-mono);
font-size: 12px;
}
#artalk-container .atk-copyright {
font-family: var(--font-mono);
font-size: 11px;
color: var(--ink-4);
}
#artalk-container .atk-textarea,
#artalk-container .atk-input,
#artalk-container .atk-send-btn,
#artalk-container .atk-btn {
border-radius: 8px;
}
#artalk-container .atk-content a {
color: var(--accent);
text-decoration: underline;
text-decoration-color: color-mix(in oklab, var(--accent) 40%, transparent);
text-underline-offset: 0.2em;
}
#artalk-container .atk-content a:hover {
text-decoration-color: var(--accent);
}
</style>
<script> <script>
const el = document.getElementById('artalk-container'); const el = document.getElementById('artalk-container');
if (el) { if (el) {
const mount = () => { const mount = () => {
if (document.getElementById('artalk-lib')) return initArtalk(); if (document.getElementById('artalk-lib')) return initArtalk();
// 官方部署文档要求 CSS 手动引入(JS 不自动注入);此前缺失导致评论区自上线起跑在 UA 默认样式上
const cssReady = new Promise<void>((done) => {
if (document.getElementById('artalk-css')) return done();
const l = document.createElement('link');
l.id = 'artalk-css';
l.rel = 'stylesheet';
l.href = 'https://wait4x.online/artalk/dist/Artalk.css';
l.onload = () => done();
l.onerror = () => done(); // CSS 失败不阻塞:评论至少可用(降级 UA 样式)
document.head.appendChild(l);
});
const s = document.createElement('script'); const s = document.createElement('script');
s.id = 'artalk-lib'; s.id = 'artalk-lib';
s.src = 'https://wait4x.online/artalk/dist/Artalk.js'; s.src = 'https://wait4x.online/artalk/dist/Artalk.js';
s.onload = initArtalk; s.onload = () => { void cssReady.then(initArtalk); };
document.head.appendChild(s); document.head.appendChild(s);
}; };
const initArtalk = () => { const initArtalk = () => {
@@ -30,6 +101,8 @@ const { pageKey, pageTitle } = Astro.props;
pageKey: el.dataset.pageKey, pageKey: el.dataset.pageKey,
pageTitle: el.dataset.pageTitle, pageTitle: el.dataset.pageTitle,
locale: 'zh-CN', locale: 'zh-CN',
// 锁亮皮:明暗由页面 tokens 接管(见上方 --at-* 映射),避免双暗色机制叠加
darkMode: false,
}); });
}; };
// 进入视口 ~200px 前挂载;无 IO 环境直接挂 // 进入视口 ~200px 前挂载;无 IO 环境直接挂
+28 -19
View File
@@ -133,8 +133,14 @@ const first = cards[0];
<span id="lb-exif">{first?.exif}</span> <span id="lb-exif">{first?.exif}</span>
<span id="lb-count">1 / {cards.length}</span> <span id="lb-count">1 / {cards.length}</span>
<span class="lb-orig"> <span class="lb-orig">
<a id="lb-original" href={first?.original} target="_blank" rel="noopener">{t('gallery_lightbox_original', locale)}</a> <a id="lb-original" class="lb-act" href={first?.original} target="_blank" rel="noopener">
<a id="lb-download" href={first?.original} download>{t('gallery_lightbox_download', locale)}</a> <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M21 3l-9 9M10 5H6a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2v-4"></path></svg>
{t('gallery_lightbox_original', locale)}
</a>
<a id="lb-download" class="lb-act" href={first?.original} download>
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4M4 21h16"></path></svg>
{t('gallery_lightbox_download', locale)}
</a>
</span> </span>
</div> </div>
<button id="lb-next" class="tap lbnav" type="button" aria-label={t('gallery_lightbox_next', locale)}> <button id="lb-next" class="tap lbnav" type="button" aria-label={t('gallery_lightbox_next', locale)}>
@@ -357,12 +363,26 @@ const first = cards[0];
gap: var(--space-3); gap: var(--space-3);
margin-left: var(--space-2); margin-left: var(--space-2);
} }
.lb-orig a { /* 动作胶囊:与 lbnav 同语言(暗底/描边/阴影),不再是裸链 */
.lb-act {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 7px 12px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.22);
background: rgba(20, 24, 30, 0.55);
color: #dfe6f3;
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: 12px; font-size: 12px;
color: #9db4ff; /* 灯箱恒为暗底:用亮蓝替代主题 --accent(亮色态下对比不足) */ text-decoration: none;
text-decoration: underline; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
text-underline-offset: 0.2em; backdrop-filter: blur(4px);
transition: background var(--dur) var(--ease), color var(--dur) var(--ease);
}
.lb-act:hover {
background: rgba(40, 48, 60, 0.75);
color: #fff;
} }
.lb-meta { .lb-meta {
flex: 1; flex: 1;
@@ -403,19 +423,8 @@ const first = cards[0];
right: var(--space-3); right: var(--space-3);
} }
@media (max-width: 560px) { @media (max-width: 560px) {
.lb-bottom { flex-wrap: wrap; } .lb-bottom { flex-wrap: wrap; justify-content: center; }
.lb-orig { .lb-orig { margin-left: 0; }
display: inline-flex;
gap: var(--space-3);
margin-left: var(--space-2);
}
.lb-orig a {
font-family: var(--font-mono);
font-size: 12px;
color: #9db4ff; /* 灯箱恒为暗底:用亮蓝替代主题 --accent(亮色态下对比不足) */
text-decoration: underline;
text-underline-offset: 0.2em;
}
.lb-meta { order: 3; flex-basis: 100%; } .lb-meta { order: 3; flex-basis: 100%; }
} }
</style> </style>
+14 -4
View File
@@ -7,13 +7,23 @@ import { trackOf } from '../../content.config';
// URL = /archives/{slug},slug 即文件名,与 Halo 旧结构 1:1(不可改) // URL = /archives/{slug},slug 即文件名,与 Halo 旧结构 1:1(不可改)
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = (await getCollection('posts', ({ data }) => !data.draft)).sort( // 路径用全量(hidden 样张也要出静态页);prev/next 导航滤 hidden
// (真实文章的上下篇不链向样张——op-artalk 车道连带提醒的实现)
const all = (await getCollection('posts', ({ data }) => !data.draft)).sort(
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(), (a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
); );
return posts.map((entry, i) => ({ const nav = all.filter(({ data }) => !data.tags.includes('hidden'));
return all.map((entry) => {
const i = nav.findIndex((e) => e.id === entry.id);
return {
params: { slug: entry.id }, params: { slug: entry.id },
props: { entry, prev: posts[i + 1] ?? null, next: posts[i - 1] ?? null }, props: {
})); entry,
prev: i >= 0 ? (nav[i + 1] ?? null) : null,
next: i > 0 ? (nav[i - 1] ?? null) : null,
},
};
});
} }
interface Props { interface Props {
+1 -1
View File
@@ -4,7 +4,7 @@ import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
import PostList from '../../components/PostList.astro'; import PostList from '../../components/PostList.astro';
const posts = (await getCollection('posts', ({ data }) => !data.draft)).sort( const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden'))).sort(
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(), (a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
); );
--- ---
+14 -4
View File
@@ -8,14 +8,24 @@ import AiTranslatedBadge from '../../../components/AiTranslatedBadge.astro';
import { t } from '../../../i18n/ui'; import { t } from '../../../i18n/ui';
export async function getStaticPaths() { export async function getStaticPaths() {
const zh = (await getCollection('posts', ({ data }) => !data.draft)).sort( // 路径全量(hidden 样张出页);prev/next 滤 hidden(同 zh 轨修正)
const all = (await getCollection('posts', ({ data }) => !data.draft)).sort(
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(), (a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
); );
const nav = all.filter(({ data }) => !data.tags.includes('hidden'));
const en = new Map((await getCollection('postsEn')).map((e) => [e.id, e])); const en = new Map((await getCollection('postsEn')).map((e) => [e.id, e]));
return zh.map((entry, i) => ({ return all.map((entry) => {
const i = nav.findIndex((e) => e.id === entry.id);
return {
params: { slug: entry.id }, params: { slug: entry.id },
props: { entry, enEntry: en.get(entry.id) ?? null, prev: zh[i + 1] ?? null, next: zh[i - 1] ?? null }, props: {
})); entry,
enEntry: en.get(entry.id) ?? null,
prev: i >= 0 ? (nav[i + 1] ?? null) : null,
next: i > 0 ? (nav[i - 1] ?? null) : null,
},
};
});
} }
interface Props { interface Props {
+1 -1
View File
@@ -9,7 +9,7 @@ import ProjectWall from '../../components/geek/ProjectWall.astro';
import Heatmap from '../../components/geek/Heatmap.astro'; import Heatmap from '../../components/geek/Heatmap.astro';
import { t } from '../../i18n/ui'; import { t } from '../../i18n/ui';
const posts = (await getCollection('posts', ({ data }) => !data.draft)).sort( const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden'))).sort(
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(), (a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
); );
// 列表标题吃 postsEn 译题(缺译回退 zh,与 /en/archives 同策略) // 列表标题吃 postsEn 译题(缺译回退 zh,与 /en/archives 同策略)
+1 -1
View File
@@ -5,7 +5,7 @@ import type { APIContext } from 'astro';
// 英文 RSS:postsEn 全量;标题缺译时回退 zh 标题(与 /en/ 页同策略) // 英文 RSS:postsEn 全量;标题缺译时回退 zh 标题(与 /en/ 页同策略)
export async function GET(context: APIContext) { export async function GET(context: APIContext) {
const en = new Map((await getCollection('postsEn')).map((e) => [e.id, e])); const en = new Map((await getCollection('postsEn')).map((e) => [e.id, e]));
const posts = (await getCollection('posts', ({ data }) => !data.draft)).sort( const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden'))).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(), (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
); );
return rss({ return rss({
+1 -1
View File
@@ -8,7 +8,7 @@ import ProjectWall from '../components/geek/ProjectWall.astro';
import Heatmap from '../components/geek/Heatmap.astro'; import Heatmap from '../components/geek/Heatmap.astro';
import { t } from '../i18n/ui'; import { t } from '../i18n/ui';
const posts = (await getCollection('posts', ({ data }) => !data.draft)).sort( const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden'))).sort(
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(), (a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
); );
// 最新影像取集合前 4 张;照片时间序由照片管线(exif 车道)治理,此处只裁切 // 最新影像取集合前 4 张;照片时间序由照片管线(exif 车道)治理,此处只裁切
+1 -1
View File
@@ -4,7 +4,7 @@ import type { APIContext } from 'astro';
// RSS:posts 集合全量(draft 排除);Phase 3 起双语各出一路(/rss.xml 与 /en/rss.xml) // RSS:posts 集合全量(draft 排除);Phase 3 起双语各出一路(/rss.xml 与 /en/rss.xml)
export async function GET(context: APIContext) { export async function GET(context: APIContext) {
const posts = (await getCollection('posts', ({ data }) => !data.draft)) const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden')))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()); .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
return rss({ return rss({
title: 'wait4x', title: 'wait4x',
+34
View File
@@ -137,6 +137,40 @@ pre code { background: none; border: none; padding: 0; font-size: inherit; }
.term-dots { display: flex; gap: 6px; margin-bottom: var(--space-2); } .term-dots { display: flex; gap: 6px; margin-bottom: var(--space-2); }
.term-dots span { width: 10px; height: 10px; border-radius: 50%; background: #30363d; } .term-dots span { width: 10px; height: 10px; border-radius: 50%; background: #30363d; }
/* ---------- 正文媒体(图/视频/音频,Phase 5 媒体支持) ---------- */
/* 图片:markdown 相对路径经 Astro 图片服务优化;版心内圆角限宽 */
.prose-reading img, .prose-tech img {
border-radius: 10px;
margin-block: 1.2em;
max-width: 100%;
height: auto;
}
/* 图注约定:紧跟媒体的 *斜体独立段* 视为图注(写作指南 docs/写作指南.md) */
.prose-reading p > em:only-child, .prose-tech p > em:only-child {
display: block;
text-align: center;
font-size: 0.85em;
font-family: var(--font-sans);
color: var(--ink-3);
margin-top: -0.6em;
margin-bottom: 1.2em;
}
/* 视频与音频:原生标签,markdown raw HTML 直书(指南有模板) */
.prose-reading video, .prose-tech video,
.prose-reading audio, .prose-tech audio {
display: block;
width: 100%;
margin-block: 1.2em;
}
.prose-reading video, .prose-tech video {
border-radius: 10px;
background: #000;
max-height: 72dvh;
}
.prose-reading audio, .prose-tech audio {
border-radius: 999px;
}
/* ---------- 列表/分隔 ---------- */ /* ---------- 列表/分隔 ---------- */
hr { border: none; border-top: 1px solid var(--line); margin-block: var(--space-4); } hr { border: none; border-top: 1px solid var(--line); margin-block: var(--space-4); }