feat: 调整页面顶部区域&重构项目页热力图&优化markdown内容页

This commit is contained in:
rbetree
2026-01-20 19:20:34 +08:00
parent 280d376bac
commit 1625f7342c
14 changed files with 807 additions and 92 deletions

View File

@@ -5,6 +5,46 @@ const { createLogger } = require('../utils/logger');
const log = createLogger('cache:projects');
function tryLoadProjectsHeatmapCache(pageId, config) {
if (!pageId) return null;
const cacheDirFromEnv = process.env.PROJECTS_CACHE_DIR
? String(process.env.PROJECTS_CACHE_DIR)
: '';
const cacheDirFromConfig =
config && config.site && config.site.github && config.site.github.cacheDir
? String(config.site.github.cacheDir)
: '';
const cacheDir = cacheDirFromEnv || cacheDirFromConfig || 'dev';
const cacheBaseDir = path.isAbsolute(cacheDir) ? cacheDir : path.join(process.cwd(), cacheDir);
const cachePath = path.join(cacheBaseDir, `${pageId}.heatmap-cache.json`);
if (!fs.existsSync(cachePath)) return null;
try {
const raw = fs.readFileSync(cachePath, 'utf8');
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') return null;
const username = parsed.username ? String(parsed.username).trim() : '';
const html = parsed.html ? String(parsed.html) : '';
if (!username || !html) return null;
return {
username,
html,
meta: {
pageId: parsed.pageId || pageId,
generatedAt: parsed.generatedAt || '',
sourceUrl: parsed.sourceUrl || '',
},
};
} catch (e) {
log.warn('heatmap 缓存读取失败,将降级为运行时加载', { path: cachePath });
return null;
}
}
function tryLoadProjectsRepoCache(pageId, config) {
if (!pageId) return null;
@@ -135,6 +175,7 @@ function buildProjectsMeta(config) {
module.exports = {
tryLoadProjectsRepoCache,
tryLoadProjectsHeatmapCache,
applyRepoMetaToCategories,
buildProjectsMeta,
};

View File

@@ -7,6 +7,7 @@ const {
} = require('../cache/articles');
const {
tryLoadProjectsRepoCache,
tryLoadProjectsHeatmapCache,
applyRepoMetaToCategories,
buildProjectsMeta,
} = require('../cache/projects');
@@ -61,6 +62,14 @@ function resolveTemplateName(pageId, data) {
function applyProjectsData(data, pageId, config) {
data.siteCardStyle = 'repo';
data.projectsMeta = buildProjectsMeta(config);
const heatmapCache = tryLoadProjectsHeatmapCache(pageId, config);
if (data.projectsMeta && data.projectsMeta.heatmap && heatmapCache) {
data.projectsMeta.heatmap.html = heatmapCache.html;
data.projectsMeta.heatmap.generatedAt = heatmapCache.meta.generatedAt;
data.projectsMeta.heatmap.sourceUrl = heatmapCache.meta.sourceUrl;
}
if (Array.isArray(data.categories)) {
const repoCache = tryLoadProjectsRepoCache(pageId, config);
if (repoCache && repoCache.map) {

View File

@@ -0,0 +1,71 @@
/**
* 从 GitHub contributions 页面 HTML 中提取 `.js-yearly-contributions` 的 innerHTML。
*
* 说明:
* - 该页面是 HTML非稳定 API结构可能变化这里做 best-effort 解析。
* - 为避免引入额外依赖,使用轻量的 div 匹配算法。
*
* @param {string} html 完整 HTML 文本
* @returns {string|null} `.js-yearly-contributions` 的 innerHTML
*/
function extractYearlyContributionsInnerHtml(html) {
const source = String(html || '');
if (!source) return null;
const marker = 'js-yearly-contributions';
const markerIndex = source.indexOf(marker);
if (markerIndex < 0) return null;
// 找到包含该 class 的 <div ...> 起始位置
const openTagStart = source.lastIndexOf('<div', markerIndex);
if (openTagStart < 0) return null;
// 通过匹配 <div ...> 与 </div> 的嵌套层级,定位该 div 的结束位置
let depth = 0;
let cursor = openTagStart;
let endIndex = -1;
while (cursor < source.length) {
const nextOpen = source.indexOf('<div', cursor);
const nextClose = source.indexOf('</div', cursor);
if (nextOpen === -1 && nextClose === -1) break;
const isOpen = nextOpen !== -1 && (nextClose === -1 || nextOpen < nextClose);
const tagIndex = isOpen ? nextOpen : nextClose;
const tagEnd = source.indexOf('>', tagIndex);
if (tagEnd === -1) break;
if (isOpen) {
depth += 1;
} else {
depth -= 1;
if (depth === 0) {
endIndex = tagEnd + 1;
break;
}
}
cursor = tagEnd + 1;
}
if (endIndex === -1) return null;
const outerHtml = source.slice(openTagStart, endIndex);
if (!outerHtml.includes(marker)) return null;
const openEnd = outerHtml.indexOf('>');
const closeStart = outerHtml.lastIndexOf('</div');
if (openEnd === -1 || closeStart === -1 || closeStart <= openEnd) return null;
let inner = outerHtml.slice(openEnd + 1, closeStart);
// 防御性处理:移除潜在的 script 标签(理论上 GitHub 片段不包含)
inner = inner.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
return inner.trim() ? inner : null;
}
module.exports = {
extractYearlyContributionsInnerHtml,
};

View File

@@ -22,6 +22,20 @@ module.exports = function initRouting(state, dom, api) {
}
});
// 通知:页面已切换(供按需组件初始化,如 github-calendar
// 注意:必须在 active class 切换之后触发,否则监听方可能认为页面仍不可见。
try {
document.dispatchEvent(
new CustomEvent('menav:pageChanged', {
detail: {
pageId,
},
})
);
} catch (error) {
// ignore
}
// 初始加载完成后设置标志
if (state.isInitialLoad) {
state.isInitialLoad = false;