feat: 调整页面顶部区域&重构项目页热力图&优化markdown内容页
This commit is contained in:
41
src/generator/cache/projects.js
vendored
41
src/generator/cache/projects.js
vendored
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
71
src/generator/utils/githubContributions.js
Normal file
71
src/generator/utils/githubContributions.js
Normal 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,
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user