feat: 添加文档阅读进度追踪功能

- 新增阅读进度条,显示页面滚动进度
- 实现侧边栏绿色小勾标记,标识已阅读文档
- 阅读记录保存在浏览器本地存储
- 支持中文路径的 URL 编码/解码
- 添加调试功能方便问题排查

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Donkey_kevin 2026-05-13 14:54:55 +08:00
parent d03d01d20f
commit 6a196f1925
4 changed files with 361 additions and 5 deletions

View File

@ -21,6 +21,8 @@ GitLink确实开源是CCF官方指定的开源创新服务平台旨在
# 帮助文档
帮助文档有助于您全面了解GitLink平台让我们一起为开源创新贡献力量
<ProgressPanel />
<div class="row">
<div class="col col--12">
<section class="row list">
@ -32,7 +34,7 @@ GitLink确实开源是CCF官方指定的开源创新服务平台旨在
<article class="col col--6 margin-bottom--lg">
<a class="card padding--lg cardContainer" href="/代码库管理/仓库创建">
<h2 class="text--truncate cardTitle" title="代码库管理">代码库管理</h2>
<p>代码库使用及设置[9个文档]</p>
<p>代码库使用及设置[8个文档]</p>
</a></article>
<article class="col col--6 margin-bottom--lg">
<a class="card padding--lg cardContainer" href="/组织管理/组织简介">
@ -60,9 +62,9 @@ GitLink确实开源是CCF官方指定的开源创新服务平台旨在
<p>维基(Wiki)使用及设置[2个文档]</p>
</a></article>
<article class="col col--6 margin-bottom--lg">
<a class="card padding--lg cardContainer" href="/bot市场/bot安装">
<h2 class="text--truncate cardTitle" title="bot市场">bot市场</h2>
<p>bot市场使用及设置[3个文档]</p>
<a class="card padding--lg cardContainer" href="/Bot市场/bot安装">
<h2 class="text--truncate cardTitle" title="Bot市场">Bot市场</h2>
<p>Bot市场使用及设置[4个文档]</p>
</a></article>
<article class="col col--6 margin-bottom--lg">
<a class="card padding--lg cardContainer" href="/第三方服务/跨平台代码同步">

View File

@ -16,7 +16,13 @@ module.exports = {
favicon: 'img/icon.ico',
organizationName: 'luffyZh', // Usually your GitHub org/user name.
projectName: 'docusaurus-luffyzh-website', // Usually your repo name.
scripts: [],
scripts: [
{
src: '/js/reading-progress.js',
async: true,
defer: true,
},
],
// stylesheets: ['styles/dark-mode.css'],
themeConfig: {
docs:{
@ -176,6 +182,18 @@ module.exports = {
// sidebarPath: require.resolve('./sidebars.js'),
editUrl:'https://www.gitlink.org.cn/Gitlink/gitlink_help_center/tree/master/',
routeBasePath: "/",
remarkPlugins: [
async () => {
const { visit } = await import('unist-util-visit');
return (tree) => {
visit(tree, 'mdxJsxFlowElement', (node) => {
if (node.name === 'ProgressPanel') {
node.attributes = node.attributes || [];
}
});
};
},
],
},
theme: {
customCss: require.resolve('./src/css/custom.css'),

View File

@ -160,7 +160,72 @@ html[data-theme='dark'] .docusaurus-highlight-code-line {
background-color: #cce0ff !important;
box-shadow: 0 4px 12px rgba(70, 106, 255, 0.15);
}
/* 阅读进度条 */
.reading-progress {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 4px;
background-color: rgba(70, 106, 255, 0.1);
z-index: 9999;
}
.reading-progress__bar {
height: 100%;
background: linear-gradient(90deg, #466aff, #6b8cff);
transition: width 0.15s ease-out;
}
/* 侧边栏完成标记 */
.sidebar-completed-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
background-color: #22c55e;
color: white;
border-radius: 50%;
font-size: 10px;
font-weight: bold;
flex-shrink: 0;
}
/* 进度面板 */
.progress-panel {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 16px;
border-radius: 12px;
color: white;
margin-bottom: 16px;
}
.progress-panel__title {
font-size: 14px;
font-weight: 600;
margin-bottom: 8px;
}
.progress-panel__bar {
height: 8px;
background-color: rgba(255, 255, 255, 0.3);
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
.progress-panel__fill {
height: 100%;
background-color: #22c55e;
border-radius: 4px;
transition: width 0.3s ease;
}
.progress-panel__text {
font-size: 12px;
opacity: 0.9;
}
/* ===== 文档投票组件样式 ===== */
.vote-container {
display: flex;

View File

@ -0,0 +1,271 @@
(function() {
'use strict';
const STORAGE_KEY = 'gitlink_reading_progress';
const BADGE_CLASS = 'reading-progress-badge';
function getReadDocs() {
try {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : [];
} catch (e) {
console.error('读取进度失败:', e);
return [];
}
}
function saveReadDocs(docs) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(docs));
} catch (e) {
console.error('保存进度失败:', e);
}
}
function normalizePath(path) {
if (!path) return '/';
// URL 解码(处理中文路径)
try {
path = decodeURIComponent(path);
} catch (e) {
// 如果解码失败,保持原样
}
path = path.replace(/^\/+/, '').replace(/\/+$/, '');
return '/' + path;
}
function markAsRead() {
const currentPath = normalizePath(window.location.pathname);
const readDocs = getReadDocs();
if (!readDocs.includes(currentPath)) {
readDocs.push(currentPath);
saveReadDocs(readDocs);
console.log('✅ 已标记为已阅读:', currentPath);
updateBadges();
} else {
console.log(' 已经标记过了:', currentPath);
}
}
function isDocRead(path) {
const readDocs = getReadDocs();
const normalizedPath = normalizePath(path);
return readDocs.some(docPath => normalizePath(docPath) === normalizedPath);
}
function createBadge() {
const badge = document.createElement('span');
badge.className = BADGE_CLASS;
badge.textContent = '✓';
badge.style.cssText = `
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: 18px !important;
height: 18px !important;
background-color: #22c55e !important;
color: white !important;
border-radius: 50% !important;
font-size: 11px !important;
font-weight: bold !important;
flex-shrink: 0 !important;
margin-left: 8px !important;
visibility: visible !important;
opacity: 1 !important;
position: relative !important;
`;
return badge;
}
function updateBadges() {
const readDocs = getReadDocs();
console.log('📚 已阅读的文档数:', readDocs.length, readDocs);
if (readDocs.length === 0) {
console.log('⚠️ 还没有已阅读的文档');
return;
}
// 清除所有旧标记
document.querySelectorAll('.' + BADGE_CLASS).forEach(badge => badge.remove());
// 获取所有可能的链接元素
const selectors = [
'a.menu__link',
'a.menu-link',
'.menu a[href^="/"]',
'.sidebar a[href^="/"]',
'nav a[href^="/"]',
'[class*="sidebar"] a[href^="/"]',
'[class*="menu"] a[href^="/"]'
];
let totalLinks = 0;
let matchedLinks = 0;
selectors.forEach(selector => {
const links = document.querySelectorAll(selector);
links.forEach(link => {
totalLinks++;
const href = link.getAttribute('href');
if (href && !href.startsWith('http') && !href.startsWith('#') && href !== '/') {
const linkPath = normalizePath(href);
if (isDocRead(linkPath)) {
matchedLinks++;
if (!link.querySelector('.' + BADGE_CLASS)) {
const badge = createBadge();
link.appendChild(badge);
console.log('✓ 添加标记:', linkPath, '->', link.textContent.trim().substring(0, 30));
}
}
}
});
});
console.log(`🔍 检查了 ${totalLinks} 个链接,匹配了 ${matchedLinks} 个已阅读文档`);
}
function initProgressBar() {
if (document.querySelector('[data-reading-progress-bar]')) {
return;
}
const progressBar = document.createElement('div');
progressBar.setAttribute('data-reading-progress-bar', 'true');
progressBar.style.cssText = `
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 4px !important;
background-color: rgba(70, 106, 255, 0.1) !important;
z-index: 9999 !important;
pointer-events: none !important;
`;
const bar = document.createElement('div');
bar.style.cssText = `
height: 100% !important;
background: linear-gradient(90deg, #466aff, #6b8cff) !important;
width: 0% !important;
transition: width 0.15s ease-out !important;
`;
progressBar.appendChild(bar);
document.body.appendChild(progressBar);
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight - windowHeight;
const scrolled = window.scrollY;
const progress = (scrolled / documentHeight) * 100;
bar.style.width = Math.min(progress, 100) + '%';
ticking = false;
});
ticking = true;
}
});
}
function initScrollDetection() {
let hasMarked = false;
function checkScroll() {
const scrollPosition = window.innerHeight + window.scrollY;
const pageHeight = document.documentElement.scrollHeight;
const threshold = 200;
const progress = Math.min((scrollPosition / pageHeight) * 100, 100);
if (!hasMarked && (progress >= 90 || (pageHeight - scrollPosition < threshold))) {
hasMarked = true;
console.log('📖 滚动进度: ' + Math.round(progress) + '%,标记为已阅读');
markAsRead();
setTimeout(() => { hasMarked = false; }, 3000);
}
}
window.addEventListener('scroll', () => checkScroll());
setTimeout(checkScroll, 1000);
}
function init() {
console.log('🚀 初始化阅读进度追踪...');
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => setTimeout(initAll, 500));
} else {
setTimeout(initAll, 500);
}
}
function initAll() {
console.log('⚙️ 启动功能...');
initProgressBar();
updateBadges();
initScrollDetection();
// 定期更新
setInterval(updateBadges, 3000);
}
// 启动
init();
// 监听路由变化
let lastUrl = location.href;
new MutationObserver(() => {
const url = location.href;
if (url !== lastUrl) {
lastUrl = url;
console.log('🔄 页面变化,更新标记');
setTimeout(updateBadges, 500);
}
}).observe(document, { subtree: true, childList: true });
// 暴露调试接口
window.readingProgressDebug = {
getReadDocs,
markAsRead,
updateBadges,
clearAll: () => {
localStorage.removeItem(STORAGE_KEY);
document.querySelectorAll('.' + BADGE_CLASS).forEach(b => b.remove());
console.log('🗑️ 已清除所有阅读记录');
updateBadges();
},
test: () => {
console.log('🧪 开始测试...');
const currentPath = normalizePath(window.location.pathname);
console.log('当前路径:', currentPath);
console.log('已阅读文档:', getReadDocs());
const links = document.querySelectorAll('a[href^="/"]');
console.log('找到链接数:', links.length);
links.forEach(link => {
const href = link.getAttribute('href');
if (href && !href.startsWith('http') && href !== '/') {
console.log('链接:', normalizePath(href), '文本:', link.textContent.trim());
}
});
updateBadges();
}
};
console.log('✅ 阅读进度追踪已加载');
console.log('🔧 调试命令:');
console.log(' readingProgressDebug.markAsRead() - 手动标记当前页面');
console.log(' readingProgressDebug.updateBadges() - 更新侧边栏标记');
console.log(' readingProgressDebug.test() - 运行测试');
console.log(' readingProgressDebug.clearAll() - 清除所有记录');
})();