我的动态页
06 - 个人动态页开发笔记
创建日期:2026-04-28
用途:/my_activity独立页面的维护参考,包含 ACG 动态(Bangumi)、Steam 动态和 Github 动态三个模块的实现细节
页面基本信息
| 项目 | 内容 |
|---|---|
| 页面模板文件 | usr/themes/classic-22/my-activity.php |
| 共享函数文件 | usr/themes/classic-22/inc/activity-functions.php |
| 异步 API 端点 | activity-api.php(项目根目录) |
| Typecho 模板名 | 个人动态 |
| 页面 slug | my_activity |
| 访问地址 | https://kaiwen.work/my_activity |
| 缓存目录 | usr/cache/(已在 .gitignore,服务器自动创建) |
模块一:ACG 动态(Bangumi API)
API 基本信息
| 项目 | 内容 |
|---|---|
| Base URL | 由 config.inc.php 中的 BANGUMI_API_BASE_URLS 配置,默认 fallback 为 https://api.bgm.tv |
| 认证 | Authorization: Bearer BANGUMI_API_TOKEN(BANGUMI_TOKEN 亦可,二者等价) |
| Token 存放 | config.inc.php → define('BANGUMI_API_TOKEN', '...') 和 define('BANGUMI_TOKEN', BANGUMI_API_TOKEN) |
| Web 跳转 | 由 BANGUMI_WEB_BASE_URL 配置,默认 https://bgm.tv |
| 图片域名 | 由 BANGUMI_IMAGE_HOST_MAP 配置,将 lain.bgm.tv / fast.bgm.tv 等原图域名改写为镜像域名 |
| 缓存文件 | usr/cache/bangumi_activity.json |
| 缓存有效期 | 6 小时 |
调用接口
# 1. 获取最近 10 条收藏
GET /v0/users/xmicrox/collections?limit=10&type=0
# 2. 获取每个收藏的详细信息(含 infobox)
GET /v0/subjects/{subject_id}关键字段说明
| 字段 | 来源 | 说明 |
|---|---|---|
name_cn / name | collections → subject | 中文名优先,fallback 日文名 |
images.common | collections → subject | 封面图 URL |
ep_status | collections | 我的话数进度 |
vol_status | collections | 我的卷数进度 |
rate | collections | 我的评分(0 = 未评) |
rating.score | subjects detail(非 collections) | 网站评分,collections 接口此字段可能为空 |
infobox | subjects detail | 作者/出版社等元数据,值可能为字符串或嵌套数组 |
subject_type 枚举
| 值 | 类型 |
|---|---|
| 1 | 书籍 |
| 2 | 动画 |
| 3 | 音乐 |
| 4 | 游戏 |
| 6 | 三次元 |
infobox 提取逻辑
// 作者/导演:按优先级匹配 key
$authorKeys = ['作者', '原作', '导演', '监督', '原案'];
// 出版/发行:按优先级匹配 key
$publisherKeys = ['出版社', '发行', '动画制作', '制作公司', '发行方', '开发'];
// 注意:value 可能是字符串,也可能是 [{v: '...', k: '...'}] 形式的数组镜像 / 反代配置(2026-06-01)
Bangumi HTTP 通信复用 usr/themes/classic-22/inc/bangumi-client.php:
bangumiRequest()按BANGUMI_API_BASE_URLS顺序请求,某个 API base 网络失败、非 2xx 或 JSON 非法时自动尝试下一个bangumiWebSubjectUrl()用BANGUMI_WEB_BASE_URL生成条目页跳转bangumiRewriteImageUrl()用BANGUMI_IMAGE_HOST_MAP改写封面图 host,保留 path/query- 第三方 API 反代会接收 Bangumi token;仅在信任反代提供方时配置到优先级靠前的位置
config.inc.php 示例(不要在维护文档写入真实 token):
define('BANGUMI_API_BASE_URLS', [
'https://api.bangumi.one',
'https://bgmapi.anibt.net',
'https://api.bgm.tv',
]);
define('BANGUMI_WEB_BASE_URL', 'https://bangumi.one');
define('BANGUMI_IMAGE_HOST_MAP', [
'lain.bgm.tv' => 'lain.bangumi.one',
'fast.bgm.tv' => 'fast.bangumi.one',
]);切换到 anibt 作为主镜像时,只需调整配置顺序和值:
define('BANGUMI_API_BASE_URLS', [
'https://bgmapi.anibt.net',
'https://api.bangumi.one',
'https://api.bgm.tv',
]);
define('BANGUMI_WEB_BASE_URL', 'https://bgmmi.anibt.net');
define('BANGUMI_IMAGE_HOST_MAP', [
'lain.bgm.tv' => 'bgmimg.anibt.net',
'fast.bgm.tv' => 'bgmimg.anibt.net',
]);已知问题 / 注意事项
rating.score必须从/v0/subjects/{id}获取,collections 接口的 subject 对象中该字段有时为空ep_status优先于vol_status展示进度(有 ep_status 则显示话数,否则显示卷数)- 每次刷新数据需删除缓存:
rm {TYPECHO_ROOT}/usr/cache/bangumi_activity.json
模块二:Steam 动态
API 基本信息
| 项目 | 内容 |
|---|---|
| Base URL | https://api.steampowered.com |
| 认证 | Query param ?key=STEAM_API_KEY |
| Key 存放 | config.inc.php → define('STEAM_API_KEY', '...') |
| SteamID | config.inc.php → define('STEAM_ID', '76561198141733567') |
| 缓存文件 | usr/cache/steam_activity.json(+ steam_activity_fallback.json 永久兜底) |
| 缓存有效期 | 6 小时 |
| 缓存写入判据 | 仅当 fetchSteamPlayerSummary() 成功($player !== null)才写入短缓存和 fallback。渲染端硬依赖 player 字段,若以"任一子接口成功"为判据会用 player=null 的半成品污染缓存,导致页面空白持续整个 TTL 周期。详见 00-dev-errors.md E15。 |
调用接口
# 1. 玩家基本资料(头像、昵称、在线状态)
GET /ISteamUser/GetPlayerSummaries/v2/?key={key}&steamids={steamid}
# 2. 近期游戏(最多6条,近2周)
GET /IPlayerService/GetRecentlyPlayedGames/v1/?key={key}&steamid={steamid}&count=6
# 3. 徽章(player_level、badge_count)
GET /IPlayerService/GetBadges/v1/?key={key}&steamid={steamid}personastate 枚举
| 值 | 文字 | 颜色 |
|---|---|---|
| 0 | 离线 | #888888 |
| 1 | 在线 | #57cbde |
| 2 | 忙碌 | #f8c84e |
| 3 | 离开 | #f8c84e |
| 4 | 打盹 | #f8c84e |
| 5 | 想交易 | #57cbde |
| 6 | 想游戏 | #57cbde |
游戏封面 URL 拼装
⚠️ 旧格式对 2024 年后上架的新游戏返回 404,当前使用 IStoreBrowseService/GetItems 获取真实 URL。当前实现(2026-04-29 更新):
调用 fetchSteamGameCovers()(activity-functions.php),批量向 api.steampowered.com/IStoreBrowseService/GetItems/v1/ 请求,从 assets.asset_url_format + assets.header 构造完整封面 URL:
CDN = https://shared.akamai.steamstatic.com/store_item_assets/
URL = CDN + asset_url_format.replace("${FILENAME}", assets.header)
示例(新格式):
https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/4126220/8583acfa.../header.jpg?t=1776328466
示例(老格式,无 hash 段):
https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/730/header.jpg?t=1749053861兜底机制(封面 URL 404 时):onerror 将 <img> 替换为带 Steam 品牌渐变(#1b2838 → #2a475e)的 <div class="steam-cover-missing">。
游戏卡片 UI
游戏卡片使用全幅封面图 + 底部半透明渐变遮罩设计:
- 卡片比例固定
aspect-ratio: 460 / 215(Steam header 图标准比例) - 封面图绝对定位铺满卡片(
object-fit: cover) - 游戏名、时长信息通过
position: absolute; bottom: 0+ 渐变背景(rgba(0,0,0,0.75) → transparent)浮于底部 - 无封面时显示深蓝渐变占位(
.steam-cover-missing)
隐私前提
用户 Steam 个人资料及游戏详情须设为公开,否则 GetRecentlyPlayedGames 返回空数组。
验证字段:communityvisibilitystate = 3
模块三:Github 动态
API 基本信息
| 项目 | 内容 |
|---|---|
| Base URL (REST) | https://api.github.com |
| Base URL (GraphQL) | https://api.github.com/graphql |
| 认证 | Authorization: Bearer GITHUB_PUBLIC_TOKEN |
| Token 类型 | Fine-grained PAT,All repositories,Metadata read-only |
| Token 存放 | config.inc.php → define('GITHUB_PUBLIC_TOKEN', '...') |
| GitHub 用户名 | kxue4 |
| 缓存文件 | usr/cache/github_activity.json |
| 缓存有效期 | 6 小时 |
调用接口
# 1. 贡献热力图(GraphQL,最近90天)
POST https://api.github.com/graphql
{
user(login: "kxue4") {
contributionsCollection(from: "YYYY-MM-DDT00:00:00Z", to: "YYYY-MM-DDT23:59:59Z") {
contributionCalendar {
totalContributions
weeks { contributionDays { contributionCount date } }
}
}
}
}
# 2. 活跃仓库(最近更新的4个,含私有)
GET /user/repos?sort=updated&per_page=4&affiliation=owner
# 3. 事件流(最近15条公开事件)
GET /users/kxue4/events?per_page=15重要限制
| 限制 | 说明 |
|---|---|
| 事件流 vs 私有仓库 | /users/{username}/events 不返回私有仓库事件,即使有 token 认证 |
| 事件保留时间 | GitHub 只保留最近 90 天的事件 |
| REST 速率限制 | 无认证 60次/小时;有认证 5000次/小时 |
| GraphQL 速率限制 | 有认证 5000 points/小时 |
热力图色阶
| CSS 类 | 贡献次数 | 浅色模式颜色 | 深色模式颜色 |
|---|---|---|---|
lv0 | 0 | var(--pico-muted-border-color) | 同左 |
lv1 | 1–2 | #9be9a8 | #0e4429 |
lv2 | 3–5 | #40c463 | #006d32 |
lv3 | 6–9 | #30a14e | #26a641 |
lv4 | 10+ | #216e39 | #39d353 |
支持的事件类型
| type | Emoji | 中文描述 |
|---|---|---|
| PushEvent | ⬆ | 推送了 N 个 commit 到 {branch}(无数量时显示「推送了代码到 {branch}」) |
| CreateEvent | ✨ | 创建了 {ref_type} {ref} |
| DeleteEvent | 🗑 | 删除了 {ref_type} {ref} |
| WatchEvent | ⭐ | Star 了此仓库 |
| ForkEvent | 🍴 | Fork 到 {forkee} |
| PullRequestEvent | 🔀 | {action} PR:{title} |
| IssuesEvent | 📋 | {action} Issue:{title} |
| ReleaseEvent | 🚀 | 发布了 {tag_name} |
| IssueCommentEvent | 💬 | 评论了 Issue:{title} |
页面布局结构
[折叠标题] ▶ ACG 动态 上次更新 2026-04-29 18:28
<div id="acg-content"> ← 骨架屏占位,JS 异步注入
┌──────────┬──────────┐
│ 卡片1 │ 卡片2 │ ← .activity-grid(2列 Grid)
├──────────┼──────────┤
│ ... │ ... │ ← 10 条收藏
└──────────┴──────────┘
[折叠标题] ▶ Steam 动态 上次更新 2026-04-29 19:10
<div id="steam-content"> ← 骨架屏占位,JS 异步注入
┌─────────────────────────────┐
│ [头像] 昵称 在线状态 Lv.XX│ ← .steam-profile(用户状态卡)
├─────────────────────────────┤
│ 近 2 周游戏 │
│ ┌───────────┬───────────┐ │
│ │ [封面全幅] │ [封面全幅] │ │ ← .steam-games-grid(2列)
│ │ ▓▓▓ 遮罩 │ ▓▓▓ 遮罩 │ │ ← 渐变遮罩覆盖底部
│ │ 游戏名 │ 游戏名 │ │
│ │ 时长 │ 时长 │ │
│ └───────────┴───────────┘ │
└─────────────────────────────┘
[折叠标题] ▶ Github 动态 上次更新 2026-04-29 18:28
<div id="github-content"> ← 骨架屏占位,JS 异步注入
┌─────────────────────────────┐
│ 贡献热力图(全宽,90天) │ ← .github-heatmap-wrap
├─────────────────────────────┤
│ 活跃仓库(全宽,4个,2列) │ ← .github-repos-wrap
├─────────────────────────────┤
│ 事件流(时间线) │ ← .github-timeline-wrap
└─────────────────────────────┘响应式断点: 900px(仓库 2列 → 单列,ACG 卡片 2列 → 单列)
异步加载架构
页面采用前端 JS 异步加载,消除 API 调用阻塞首屏渲染。
数据流:
浏览器请求 /my_activity
↓(极快,仅输出骨架屏 HTML)
my-activity.php(无 API 调用)
↓ JS fetch(并行)
├── GET /activity-api.php?module=bangumi → {"html": "..."}
├── GET /activity-api.php?module=steam → {"html": "..."}
└── GET /activity-api.php?module=github → {"html": "..."}
↓ JS 注入 innerHTML
├── #acg-content ← ACG 动态卡片
├── #steam-content ← 用户状态卡 + 游戏网格
└── #github-content ← 热力图 + 仓库 + 事件流关键文件:
| 文件 | 职责 |
|---|---|
my-activity.php | 渲染骨架屏页面骨架 + CSS + JS 加载逻辑 |
activity-api.php | JSON API 端点,接收 ?module=bangumi/steam/github,返回 {"html":"..."} |
usr/themes/classic-22/inc/activity-functions.php | 所有数据函数 + render_bangumi_content() + render_steam_content() + render_github_content() |
activity-api.php 响应格式:
// 成功
{ "html": "<div class=\"activity-grid\">...</div>", "updated_at": "2026-04-29 18:28" }
// 失败(非法 module)
HTTP 400 { "error": "invalid module" }updated_at 来源规则:
| 数据来源 | updated_at 取值 |
|---|---|
| 实时拉取成功 | 当前时间 |
| 6 小时短缓存命中 | 短缓存文件的 filemtime |
| Fallback 命中 | Fallback 文件的 filemtime |
注意: 服务器 PHP 时区需设为Asia/Shanghai(在/etc/php/x.x/fpm/php.ini中设置date.timezone = Asia/Shanghai),否则date()返回 UTC 时间。
配置文件(config.inc.php)
// 以下 token 存放在 config.inc.php(已在 .gitignore,不提交 git)
define('GITHUB_TOKEN', '...'); // 用于访问 kaiwen.work 私有仓库(原有)
define('GITHUB_PUBLIC_TOKEN', '...'); // 用于 Github 动态模块 API 调用
define('BANGUMI_API_TOKEN', '...'); // 用于 ACG 动态模块 API 调用
define('BANGUMI_TOKEN', BANGUMI_API_TOKEN); // 等价别名,供 rating API 体系使用
define('STEAM_API_KEY', '...'); // 用于 Steam 动态模块 API 调用
define('STEAM_ID', '...'); // Steam 用户 64位 ID常用维护操作
# 清除 Bangumi 缓存(强制下次访问刷新数据)
rm {TYPECHO_ROOT}/usr/cache/bangumi_activity.json
# 清除 Github 缓存
rm {TYPECHO_ROOT}/usr/cache/github_activity.json
# 清除 Steam 缓存
rm {TYPECHO_ROOT}/usr/cache/steam_activity.json
# 清除全部缓存
rm {TYPECHO_ROOT}/usr/cache/*.json
# 查看缓存状态
ls -la {TYPECHO_ROOT}/usr/cache/
# 检查 PHP 错误日志(如果页面异常)
tail -50 /var/log/nginx/error.log后续扩展计划
- [x] Github 动态 → 贡献热力图支持自定义天数(当前固定90天)
- [x] 页面新增第三个模块(Steam 动态,已实现:用户状态卡 + 近期游戏网格)
- [ ] Github 动态 → 事件流增加分页或"加载更多"