前端javascript如何实现阅读位置记忆【可运行源码】

阅读位置记忆功能demo

功能说明

1. 自动保存:滚动页面时,系统会自动保存当前阅读位置(防抖处理,每秒保存一次)
2. 自动恢复:重新打开页面时,会自动跳转到上次阅读的位置
3. 手动控制:
  1. 手动保存当前位置

  2. 手动跳转到上次保存的位置

  3. 清除保存的记录

4. 视图反馈
  1. 状态指示器显示保存状态

  2. 恢复时高亮显示当前章节

  3. 显示滚动进度条

  4. 侧边位置标记显示当前章节和进度

使用方法

  1. 直接复制上面的代码到HTML文件中

  2. 用浏览器打开该文件

  3. 滚动页面阅读内容

  4. 刷新页面或关闭后重新打开,页面会自动跳转到上次阅读的位置

xml 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>阅读位置记忆功能</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
        }
        
        body {
            background-color: #f5f7fa;
            color: #333;
            line-height: 1.6;
        }
        
        .container {
            max-width: 1000px;
            margin: 0 auto;
            padding: 20px;
        }
        
        header {
            text-align: center;
            padding: 30px 0;
            background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
            color: white;
            border-radius: 10px;
            margin-bottom: 30px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
        }
        
        h1 {
            font-size: 2.5rem;
            margin-bottom: 10px;
        }
        
        .subtitle {
            font-size: 1.1rem;
            opacity: 0.9;
            max-width: 600px;
            margin: 0 auto;
        }
        
        .control-panel {
            background-color: white;
            padding: 20px;
            border-radius: 10px;
            margin-bottom: 30px;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
            display: flex;
            flex-wrap: wrap;
            gap: 15px;
            justify-content: center;
        }
        
        .btn {
            padding: 12px 24px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-weight: 600;
            transition: all 0.3s ease;
            display: flex;
            align-items: center;
            gap: 8px;
        }
        
        .btn-primary {
            background-color: #4a6ee0;
            color: white;
        }
        
        .btn-primary:hover {
            background-color: #3a5ed0;
            transform: translateY(-2px);
        }
        
        .btn-success {
            background-color: #10b981;
            color: white;
        }
        
        .btn-success:hover {
            background-color: #0da271;
            transform: translateY(-2px);
        }
        
        .btn-warning {
            background-color: #f59e0b;
            color: white;
        }
        
        .btn-warning:hover {
            background-color: #e5900a;
            transform: translateY(-2px);
        }
        
        .status-indicator {
            display: flex;
            align-items: center;
            padding: 12px 20px;
            background-color: #f8fafc;
            border-radius: 5px;
            font-weight: 500;
        }
        
        .indicator-dot {
            width: 10px;
            height: 10px;
            border-radius: 50%;
            margin-right: 10px;
            background-color: #6b7280;
        }
        
        .indicator-dot.active {
            background-color: #10b981;
            animation: pulse 2s infinite;
        }
        
        @keyframes pulse {
            0% { opacity: 1; }
            50% { opacity: 0.5; }
            100% { opacity: 1; }
        }
        
        .content {
            background-color: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
            margin-bottom: 30px;
        }
        
        .chapter {
            margin-bottom: 40px;
            padding-bottom: 30px;
            border-bottom: 1px solid #e5e7eb;
        }
        
        .chapter:last-child {
            border-bottom: none;
            margin-bottom: 0;
            padding-bottom: 0;
        }
        
        .chapter-title {
            font-size: 1.8rem;
            color: #1f2937;
            margin-bottom: 20px;
            padding-left: 15px;
            border-left: 5px solid #4a6ee0;
        }
        
        .chapter-content {
            font-size: 1.05rem;
        }
        
        .chapter-content p {
            margin-bottom: 15px;
            text-align: justify;
        }
        
        .highlight {
            background-color: rgba(255, 255, 0, 0.3);
            transition: background-color 0.5s ease;
        }
        
        footer {
            text-align: center;
            padding: 20px;
            color: #6b7280;
            font-size: 0.9rem;
        }
        
        .position-marker {
            position: fixed;
            right: 20px;
            top: 50%;
            transform: translateY(-50%);
            background-color: rgba(74, 110, 224, 0.9);
            color: white;
            padding: 15px;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
            z-index: 100;
            width: 180px;
            text-align: center;
            display: none;
        }
        
        .position-marker h3 {
            font-size: 1rem;
            margin-bottom: 8px;
        }
        
        .position-info {
            font-size: 1.2rem;
            font-weight: bold;
        }
        
        .scroll-progress {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 4px;
            background-color: #e5e7eb;
            z-index: 1000;
        }
        
        .scroll-progress-bar {
            height: 100%;
            background: linear-gradient(90deg, #6a11cb 0%, #2575fc 100%);
            width: 0%;
            transition: width 0.2s ease;
        }
        
        @media (max-width: 768px) {
            .container {
                padding: 15px;
            }
            
            h1 {
                font-size: 2rem;
            }
            
            .control-panel {
                flex-direction: column;
                align-items: stretch;
            }
            
            .btn {
                justify-content: center;
            }
            
            .position-marker {
                display: none !important;
            }
        }
    </style>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
    <!-- 滚动进度条 -->
    <div class="scroll-progress">
        <div class="scroll-progress-bar"></div>
    </div>
    
    <!-- 位置标记 -->
    <div class="position-marker">
        <h3>上次阅读位置</h3>
        <div class="position-info">第 <span id="chapter-num">0</span> 章</div>
        <div class="position-info" id="position-percent">0%</div>
    </div>
    
    <div class="container">
        <header>
            <h1><i class="fas fa-book-bookmark"></i> 阅读位置记忆功能</h1>
            <p class="subtitle">离开页面后,系统会自动保存您的阅读位置。重新打开时,会自动跳转到上次阅读的位置。</p>
        </header>
        
        <div class="control-panel">
            <div class="status-indicator">
                <span class="indicator-dot" id="status-dot"></span>
                <span id="status-text">状态:未检测到历史记录</span>
            </div>
            
            <button class="btn btn-primary" id="save-btn">
                <i class="fas fa-save"></i> 手动保存当前位置
            </button>
            
            <button class="btn btn-success" id="jump-btn">
                <i class="fas fa-arrow-right"></i> 跳转到上次位置
            </button>
            
            <button class="btn btn-warning" id="clear-btn">
                <i class="fas fa-trash-alt"></i> 清除记录
            </button>
        </div>
        
        <div class="content" id="content">
            <!-- 内容将通过JavaScript生成 -->
        </div>
        
        <footer>
            <p>© 2025 阅读位置记忆演示 | 使用 localStorage 实现位置记忆功能</p>
            <p>尝试滚动页面,然后刷新或关闭页面,重新打开时会自动跳转到上次阅读的位置。</p>
        </footer>
    </div>

    <script>
        // 生成示例内容
        const chapters = [
            {
                title: "第一章:初识前端开发",
                content: `前端开发是创建Web页面或App等前端界面呈现给用户的过程。通过HTML、CSS及JavaScript以及衍生出来的各种技术、框架、解决方案,来实现互联网产品的用户界面交互。
                
                随着互联网技术的发展,HTML5、CSS3、ES6等现代前端技术的应用,使得前端开发能够实现更丰富的交互和更好的用户体验。前端工程师需要与设计师、后端工程师协作,完成产品的前端开发工作。
                
                前端开发领域技术更新迅速,开发者需要不断学习新技术、新框架,以适应快速发展的行业需求。React、Vue、Angular等框架的出现,大大提高了前端开发的效率。`
            },
            {
                title: "第二章:JavaScript的核心概念",
                content: `JavaScript是一种具有函数优先的轻量级、解释型或即时编译型的编程语言。虽然它是作为开发Web页面的脚本语言而出名,但是它也被用到了很多非浏览器环境中。
                
                变量作用域、闭包、原型链、异步编程等是JavaScript的核心概念。理解这些概念对于编写高质量JavaScript代码至关重要。
                
                ES6引入了许多新特性,如let和const声明、箭头函数、模板字符串、解构赋值、Promise等,这些特性使得JavaScript更加强大和易用。现代前端开发几乎都基于ES6及以上版本。`
            },
            {
                title: "第三章:DOM操作与事件处理",
                content: `文档对象模型(DOM)是HTML和XML文档的编程接口。它提供了对文档的结构化表述,并定义了一种方式可以使程序对该结构进行访问,从而改变文档的结构、样式和内容。
                
                DOM将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合。简言之,它会将web页面和脚本或程序语言连接起来。
                
                事件处理是前端交互的核心。JavaScript通过事件监听器来响应用户的操作,如点击、悬停、滚动等。事件委托是一种常用的优化技术,它利用事件冒泡机制,将事件监听器添加到父元素上,而不是每个子元素上。`
            },
            {
                title: "第四章:现代前端框架",
                content: `React、Vue和Angular是目前最流行的三大前端框架。它们都采用了组件化的开发模式,将UI拆分为独立可复用的代码片段,并对每个片段进行独立构思。
                
                React由Facebook开发,以其虚拟DOM和单向数据流而闻名。Vue由尤雨溪创建,以其渐进式框架和易用性受到开发者喜爱。Angular由Google维护,是一个完整的企业级框架。
                
                这些框架都提供了状态管理、路由、构建工具等完整的前端开发解决方案。选择哪个框架取决于项目需求、团队技能和个人偏好。`
            },
            {
                title: "第五章:响应式设计与移动优先",
                content: `响应式Web设计是一种网页设计方法,使网站能在各种设备(从桌面电脑到移动电话)上很好地工作。其核心是使用弹性网格布局、弹性图片和媒体查询。
                
                移动优先是一种设计策略,首先为移动设备设计网站,然后逐步增强为平板电脑和桌面电脑的设计。这种策略确保网站在小屏幕上有良好的体验。
                
                随着移动设备使用量的增加,响应式设计和移动优先策略变得越来越重要。CSS框架如Bootstrap、Tailwind CSS等提供了实现响应式设计的工具。`
            },
            {
                title: "第六章:前端性能优化",
                content: `前端性能优化是提高网站加载速度和响应速度的过程。性能优化的目标包括减少页面加载时间、减少资源大小、优化渲染路径等。
                
                常见的前端性能优化技术包括:代码压缩、图片优化、懒加载、代码分割、缓存策略、减少重绘和回流等。使用Webpack、Rollup等构建工具可以自动化许多优化任务。
                
                性能直接影响用户体验和SEO排名。Google的Core Web Vitals指标已成为衡量网站用户体验的重要标准,包括LCP(最大内容绘制)、FID(首次输入延迟)和CLS(累积布局偏移)。`
            },
            {
                title: "第七章:前端工程化",
                content: `前端工程化是指将软件工程的方法和原则应用到前端开发中,以提高开发效率、代码质量和团队协作。它包括构建工具、代码规范、测试、部署等流程。
                
                现代前端工程化通常包括以下工具:包管理器(npm、yarn)、模块打包器(Webpack、Rollup)、编译器(Babel)、代码检查工具(ESLint)、样式预处理(Sass、Less)等。
                
                持续集成/持续部署(CI/CD)也是前端工程化的重要组成部分,它可以自动化测试和部署流程,确保代码质量。`
            },
            {
                title: "第八章:前端未来发展",
                content: `前端领域正在快速发展,新技术不断涌现。WebAssembly允许在浏览器中运行高性能代码;Progressive Web Apps(PWA)提供类似原生应用的体验;Web Components实现真正的组件复用。
                
                随着物联网、人工智能和5G技术的发展,前端开发将面临新的机遇和挑战。前端工程师可能需要掌握更多的跨平台开发技能,如React Native、Flutter等。
                
                前端开发的未来将是多元化、全栈化的。前端工程师不仅需要掌握前端技术,还需要了解后端、DevOps、设计等相关知识,以应对日益复杂的产品需求。`
            }
        ];
        
        // 全局变量
        let scrollTimeout;
        let lastSavedPosition = 0;
        let isRestoring = false;
        const STORAGE_KEY = 'reading_position';
        
        // DOM元素
        const contentEl = document.getElementById('content');
        const statusDot = document.getElementById('status-dot');
        const statusText = document.getElementById('status-text');
        const saveBtn = document.getElementById('save-btn');
        const jumpBtn = document.getElementById('jump-btn');
        const clearBtn = document.getElementById('clear-btn');
        const positionMarker = document.querySelector('.position-marker');
        const chapterNumEl = document.getElementById('chapter-num');
        const positionPercentEl = document.getElementById('position-percent');
        const scrollProgressBar = document.querySelector('.scroll-progress-bar');
        
        // 初始化:生成内容
        function initContent() {
            let contentHTML = '';
            chapters.forEach((chapter, index) => {
                contentHTML += `
                    <div class="chapter" id="chapter-${index + 1}">
                        <h2 class="chapter-title">${chapter.title}</h2>
                        <div class="chapter-content">
                            ${chapter.content.split('\n').map(p => `<p>${p}</p>`).join('')}
                        </div>
                    </div>
                `;
            });
            contentEl.innerHTML = contentHTML;
        }
        
        // 保存阅读位置
        function saveReadingPosition() {
            const scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
            const totalHeight = document.documentElement.scrollHeight - window.innerHeight;
            const scrollPercent = totalHeight > 0 ? Math.round((scrollPosition / totalHeight) * 100) : 0;
            
            // 计算当前章节
            let currentChapter = 1;
            const chaptersElements = document.querySelectorAll('.chapter');
            for (let i = 0; i < chaptersElements.length; i++) {
                const rect = chaptersElements[i].getBoundingClientRect();
                if (rect.top <= window.innerHeight / 2) {
                    currentChapter = i + 1;
                }
            }
            
            const positionData = {
                scrollTop: scrollPosition,
                timestamp: new Date().getTime(),
                chapter: currentChapter,
                percent: scrollPercent
            };
            
            localStorage.setItem(STORAGE_KEY, JSON.stringify(positionData));
            lastSavedPosition = scrollPosition;
            
            // 更新状态
            updateStatus(true);
            
            // 显示保存提示
            showNotification('位置已保存!');
            
            console.log('位置已保存:', positionData);
        }
        
        // 恢复阅读位置
        function restoreReadingPosition() {
            const savedData = localStorage.getItem(STORAGE_KEY);
            
            if (!savedData) {
                updateStatus(false);
                return false;
            }
            
            try {
                const positionData = JSON.parse(savedData);
                isRestoring = true;
                
                // 滚动到保存的位置
                window.scrollTo({
                    top: positionData.scrollTop,
                    behavior: 'smooth'
                });
                
                // 高亮当前章节
                highlightCurrentChapter(positionData.chapter);
                
                // 更新状态
                updateStatus(true, positionData);
                
                // 显示恢复提示
                showNotification(`已恢复到上次阅读位置:第${positionData.chapter}章`);
                
                console.log('位置已恢复:', positionData);
                
                // 重置标志
                setTimeout(() => {
                    isRestoring = false;
                }, 1000);
                
                return true;
            } catch (error) {
                console.error('恢复位置时出错:', error);
                updateStatus(false);
                return false;
            }
        }
        
        // 清除保存的位置
        function clearSavedPosition() {
            localStorage.removeItem(STORAGE_KEY);
            updateStatus(false);
            positionMarker.style.display = 'none';
            showNotification('位置记录已清除');
        }
        
        // 更新状态指示器
        function updateStatus(hasData, positionData = null) {
            if (hasData) {
                statusDot.classList.add('active');
                
                if (positionData) {
                    const timeAgo = Math.round((new Date().getTime() - positionData.timestamp) / (1000 * 60));
                    statusText.textContent = `状态:已保存 (${timeAgo}分钟前,第${positionData.chapter}章)`;
                    
                    // 显示位置标记
                    positionMarker.style.display = 'block';
                    chapterNumEl.textContent = positionData.chapter;
                    positionPercentEl.textContent = `${positionData.percent}%`;
                } else {
                    statusText.textContent = '状态:已启用自动保存';
                }
            } else {
                statusDot.classList.remove('active');
                statusText.textContent = '状态:未检测到历史记录';
            }
        }
        
        // 高亮当前章节
        function highlightCurrentChapter(chapterIndex) {
            // 移除所有高亮
            document.querySelectorAll('.chapter').forEach(chapter => {
                chapter.classList.remove('highlight');
            });
            
            // 高亮当前章节
            const currentChapter = document.getElementById(`chapter-${chapterIndex}`);
            if (currentChapter) {
                currentChapter.classList.add('highlight');
                
                // 5秒后移除高亮
                setTimeout(() => {
                    currentChapter.classList.remove('highlight');
                }, 5000);
            }
        }
        
        // 显示通知
        function showNotification(message) {
            // 创建通知元素
            const notification = document.createElement('div');
            notification.style.cssText = `
                position: fixed;
                top: 20px;
                right: 20px;
                background-color: #10b981;
                color: white;
                padding: 15px 20px;
                border-radius: 5px;
                box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
                z-index: 10000;
                font-weight: 500;
                transform: translateX(120%);
                transition: transform 0.3s ease;
            `;
            notification.textContent = message;
            document.body.appendChild(notification);
            
            // 显示通知
            setTimeout(() => {
                notification.style.transform = 'translateX(0)';
            }, 10);
            
            // 3秒后隐藏并移除
            setTimeout(() => {
                notification.style.transform = 'translateX(120%)';
                setTimeout(() => {
                    document.body.removeChild(notification);
                }, 300);
            }, 3000);
        }
        
        // 更新滚动进度条
        function updateScrollProgress() {
            const winScroll = document.body.scrollTop || document.documentElement.scrollTop;
            const height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
            const scrolled = (winScroll / height) * 100;
            scrollProgressBar.style.width = scrolled + "%";
        }
        
        // 初始化
        function init() {
            // 生成内容
            initContent();
            
            // 检查是否有保存的位置并尝试恢复
            const hasRestored = restoreReadingPosition();
            
            // 如果没有恢复位置,更新状态
            if (!hasRestored) {
                updateStatus(false);
            }
            
            // 事件监听
            saveBtn.addEventListener('click', saveReadingPosition);
            jumpBtn.addEventListener('click', restoreReadingPosition);
            clearBtn.addEventListener('click', clearSavedPosition);
            
            // 监听滚动事件(防抖处理)
            window.addEventListener('scroll', () => {
                // 更新滚动进度条
                updateScrollProgress();
                
                // 如果不是正在恢复位置,则保存位置
                if (!isRestoring) {
                    clearTimeout(scrollTimeout);
                    scrollTimeout = setTimeout(saveReadingPosition, 1000);
                }
                
                // 检测当前章节
                const chaptersElements = document.querySelectorAll('.chapter');
                let currentChapter = 1;
                for (let i = 0; i < chaptersElements.length; i++) {
                    const rect = chaptersElements[i].getBoundingClientRect();
                    if (rect.top <= window.innerHeight / 2) {
                        currentChapter = i + 1;
                    }
                }
                
                // 更新位置标记
                const scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
                const totalHeight = document.documentElement.scrollHeight - window.innerHeight;
                const scrollPercent = totalHeight > 0 ? Math.round((scrollPosition / totalHeight) * 100) : 0;
                
                chapterNumEl.textContent = currentChapter;
                positionPercentEl.textContent = `${scrollPercent}%`;
            });
            
            // 页面卸载前保存位置
            window.addEventListener('beforeunload', () => {
                saveReadingPosition();
            });
            
            // 初始化滚动进度条
            updateScrollProgress();
        }
        
        // 页面加载完成后初始化
        document.addEventListener('DOMContentLoaded', init);
    </script>
</body>
</html>
相关推荐
苏打水com6 小时前
第十七篇:Day49-51 前端工程化进阶——从“手动”到“自动化”(对标职场“提效降本”需求)
前端·javascript·css·vue.js·html
文心快码BaiduComate6 小时前
Comate强力赋能:「趣绘像素岛」从体验泥潭到高性能可用的蜕变之路
前端·后端·程序员
『 时光荏苒 』6 小时前
使用Vue播放M3U8视频流的方法
前端·javascript·vue.js
Apifox6 小时前
Apifox + AI:接口自动化测试的智能化实践
前端·后端·测试
Tjohn96 小时前
前后端分离项目(Vue-SpringBoot)迁移记录
前端·vue.js·spring boot
CaoLv6 小时前
无需后端!用 React + WebLLM 把大模型装进浏览器,手撸一个“有脾气”的 AI 机器人 🤖
前端
消防大队VUE支队6 小时前
🗓️ 2262年将有两个春节!作为前端的你,日历控件真的写对了吗?
前端·javascript
鸭蛋超人不会飞7 小时前
axios简易封装,适配H5开发
前端·javascript·vue.js
风止何安啊7 小时前
从 “翻页书” 到 “魔术盒”:React 路由凭啥如此丝滑?
前端·react.js·面试