JavaScript 网页交互进阶:5 个经典案例实现(二)—— 覆盖 UI 组件开发与工具函数封装

        在前端开发中,高频复用的交互组件和工具函数是提升开发效率的关键。本文整理了 10 个经典 JavaScript 案例,涵盖 UI 组件、数据管理、性能优化等场景,每个案例均包含核心功能技术点说明及可直接运行的完整代码,适合新手学习参考,也可直接集成到项目中使用。

本文分享了5个实用的JavaScript前端开发案例,包括:

颜色选择器:实现RGB/HEX双向转换与实时预览二维码生成器:支持文本转换与图片下载功能五星级评分组件:实现半星评分与本地存储持久化手风琴折叠面板:支持单开/多开两种交互模式滚动进度条:实时显示页面滚动百分比

案例 1:颜色选择器(RGB/HEX 转换 + 预览)

核心功能

支持通过滑块调整 RGB 三色值(0-255 范围)实时显示对应的 HEX 颜色值并支持手动输入实时预览所选颜色效果RGB 与 HEX 格式双向自动转换

技术点

RGB 与 HEX 颜色格式数学转换算法表单输入事件监听与数据同步实时值边界处理与格式验证DOM 实时更新与视图渲染优化

效果图

实现代码



<!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>
        .color-picker {
            max-width: 500px;
            margin: 2rem auto;
            padding: 2rem;
            background: #fff;
            border-radius: 10px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.1);
            font-family: 'Segoe UI', sans-serif;
        }
        
        .color-preview {
            width: 100%;
            height: 150px;
            border-radius: 8px;
            margin-bottom: 1.5rem;
            transition: background-color 0.3s ease;
            border: 1px solid #eee;
        }
        
        .control-group {
            margin-bottom: 1.5rem;
        }
        
        .control-group label {
            display: block;
            margin-bottom: 0.5rem;
            font-weight: 500;
            color: #333;
        }
        
        .slider-container {
            display: flex;
            align-items: center;
            gap: 1rem;
        }
        
        input[type="range"] {
            flex: 1;
            height: 6px;
            border-radius: 3px;
            -webkit-appearance: none;
            appearance: none;
            outline: none;
        }
        
        input[type="range"]::-webkit-slider-thumb {
            -webkit-appearance: none;
            appearance: none;
            width: 20px;
            height: 20px;
            border-radius: 50%;
            cursor: pointer;
        }
        
        /* 滑块基础样式 */
        #redSlider {
            background: linear-gradient(to right, #000, rgb(255, 0, 0));
        }
        #redSlider::-webkit-slider-thumb {
            background: rgb(255, 0, 0);
        }
        
        #greenSlider {
            background: linear-gradient(to right, #000, rgb(0, 255, 0));
        }
        #greenSlider::-webkit-slider-thumb {
            background: rgb(0, 255, 0);
        }
        
        #blueSlider {
            background: linear-gradient(to right, #000, rgb(0, 0, 255));
        }
        #blueSlider::-webkit-slider-thumb {
            background: rgb(0, 0, 255);
        }
        
        .value-display {
            width: 60px;
            padding: 6px 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            text-align: center;
        }
        
        .hex-input {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 16px;
            margin-top: 0.5rem;
        }
        
        .hex-input:focus {
            outline: none;
            border-color: #66afe9;
            box-shadow: 0 0 0 2px rgba(102, 175, 233, 0.25);
        }
        
        .info-text {
            color: #666;
            font-size: 0.9rem;
            margin-top: 0.5rem;
        }
    </style>
</head>
<body>
    <div class="color-picker">
        <div class="color-preview" id="colorPreview"></div>
        
        <div class="control-group">
            <label for="redSlider">红色 (R)</label>
            <div class="slider-container">
                <input type="range" id="redSlider" min="0" max="255" value="255">
                <input type="number" class="value-display" id="redValue" min="0" max="255" value="255">
            </div>
        </div>
        
        <div class="control-group">
            <label for="greenSlider">绿色 (G)</label>
            <div class="slider-container">
                <input type="range" id="greenSlider" min="0" max="255" value="255">
                <input type="number" class="value-display" id="greenValue" min="0" max="255" value="255">
            </div>
        </div>
        
        <div class="control-group">
            <label for="blueSlider">蓝色 (B)</label>
            <div class="slider-container">
                <input type="range" id="blueSlider" min="0" max="255" value="255">
                <input type="number" class="value-display" id="blueValue" min="0" max="255" value="255">
            </div>
        </div>
        
        <div class="control-group">
            <label for="hexInput">HEX 颜色值</label>
            <input type="text" id="hexInput" class="hex-input" value="#FFFFFF" placeholder="#RRGGBB">
            <p class="info-text">支持直接输入HEX值(如#FF0088)进行颜色选择</p>
        </div>
    </div>
 
    <script>
        // 获取DOM元素
        const colorPreview = document.getElementById('colorPreview');
        const redSlider = document.getElementById('redSlider');
        const greenSlider = document.getElementById('greenSlider');
        const blueSlider = document.getElementById('blueSlider');
        const redValue = document.getElementById('redValue');
        const greenValue = document.getElementById('greenValue');
        const blueValue = document.getElementById('blueValue');
        const hexInput = document.getElementById('hexInput');
        
        // RGB转HEX函数
        function rgbToHex(r, g, b) {
            // 将十进制转为十六进制并补零
            const toHex = (num) => {
                const hex = num.toString(16);
                return hex.length === 1 ? '0' + hex : hex;
            };
            
            const hex = `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
            return hex;
        }
        
        // HEX转RGB函数
        function hexToRgb(hex) {
            // 移除#号
            hex = hex.replace(/^#/, '');
            
            // 处理3位HEX值(如#FFF)
            if (hex.length === 3) {
                hex = hex.split('').map(char => char + char).join('');
            }
            
            // 解析RGB值
            const r = parseInt(hex.substring(0, 2), 16);
            const g = parseInt(hex.substring(2, 4), 16);
            const b = parseInt(hex.substring(4, 6), 16);
            
            // 验证有效性
            if (isNaN(r) || isNaN(g) || isNaN(b)) {
                return null;
            }
            
            return { r, g, b };
        }
        
        // 更新颜色预览
        function updateColorPreview() {
            const r = parseInt(redSlider.value);
            const g = parseInt(greenSlider.value);
            const b = parseInt(blueSlider.value);
            
            // 设置预览颜色
            colorPreview.style.backgroundColor = `rgb(${r}, ${g}, ${b})`;
            
            // 更新HEX输入框
            const hex = rgbToHex(r, g, b);
            hexInput.value = hex;
        }
        
        // 同步RGB输入值
        function syncRGBValues() {
            redValue.value = redSlider.value;
            greenValue.value = greenSlider.value;
            blueValue.value = blueSlider.value;
            updateColorPreview();
        }
        
        // 从HEX值更新RGB
        function updateFromHex() {
            const hex = hexInput.value.trim();
            const rgb = hexToRgb(hex);
            
            if (rgb) {
                redSlider.value = rgb.r;
                greenSlider.value = rgb.g;
                blueSlider.value = rgb.b;
                syncRGBValues();
            }
        }
        
        // 事件监听
        redSlider.addEventListener('input', syncRGBValues);
        greenSlider.addEventListener('input', syncRGBValues);
        blueSlider.addEventListener('input', syncRGBValues);
        
        redValue.addEventListener('change', () => {
            // 确保值在0-255范围内
            const value = Math.min(255, Math.max(0, parseInt(redValue.value) || 0));
            redValue.value = value;
            redSlider.value = value;
            updateColorPreview();
        });
        
        greenValue.addEventListener('change', () => {
            const value = Math.min(255, Math.max(0, parseInt(greenValue.value) || 0));
            greenValue.value = value;
            greenSlider.value = value;
            updateColorPreview();
        });
        
        blueValue.addEventListener('change', () => {
            const value = Math.min(255, Math.max(0, parseInt(blueValue.value) || 0));
            blueValue.value = value;
            blueSlider.value = value;
            updateColorPreview();
        });
        
        hexInput.addEventListener('input', (e) => {
            // 自动添加#号
            if (e.target.value && !e.target.value.startsWith('#') && e.target.value.length > 0) {
                e.target.value = '#' + e.target.value;
            }
            updateFromHex();
        });
        
        hexInput.addEventListener('blur', () => {
            // 失去焦点时格式化HEX值
            const hex = hexInput.value.trim();
            const rgb = hexToRgb(hex);
            if (rgb) {
                hexInput.value = rgbToHex(rgb.r, rgb.g, rgb.b);
            }
        });
        
        // 初始化
        syncRGBValues();
    </script>
</body>
</html>

案例 2:二维码生成器(文本转二维码 + 下载功能)

核心功能

实时将输入的文本(支持网址、文字、数字等)转换为二维码支持调整二维码尺寸大小提供二维码图片下载功能(PNG 格式)输入内容为空时给出友好提示

技术点

第三方二维码生成库(qrcode.js)的集成与使用Canvas API 操作与图片转换触发浏览器下载文件的 Blob 与 URL 对象使用输入事件防抖处理与用户体验优化

效果图

实现代码



<!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', system-ui, sans-serif;
        }
        
        body {
            background-color: #f5f7fa;
            padding: 2rem 0;
        }
        
        .container {
            max-width: 600px;
            margin: 0 auto;
            background: #fff;
            padding: 2rem;
            border-radius: 12px;
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
        }
        
        h1 {
            text-align: center;
            color: #333;
            margin-bottom: 1.5rem;
            font-weight: 600;
        }
        
        .input-group {
            margin-bottom: 1.5rem;
        }
        
        label {
            display: block;
            margin-bottom: 0.8rem;
            color: #555;
            font-weight: 500;
        }
        
        #textInput {
            width: 100%;
            padding: 12px 15px;
            border: 1px solid #ddd;
            border-radius: 8px;
            font-size: 16px;
            transition: border-color 0.3s;
        }
        
        #textInput:focus {
            outline: none;
            border-color: #4285f4;
            box-shadow: 0 0 0 3px rgba(66, 133, 244, 0.1);
        }
        
        .size-control {
            display: flex;
            align-items: center;
            gap: 1rem;
            margin-bottom: 1.5rem;
        }
        
        #sizeSlider {
            flex: 1;
            height: 6px;
            border-radius: 3px;
            -webkit-appearance: none;
            background: #eee;
        }
        
        #sizeSlider::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 20px;
            height: 20px;
            border-radius: 50%;
            background: #4285f4;
            cursor: pointer;
        }
        
        #sizeValue {
            width: 60px;
            text-align: center;
            padding: 6px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        
        .qr-container {
            text-align: center;
            padding: 2rem;
            background-color: #f9f9f9;
            border-radius: 8px;
            margin-bottom: 1.5rem;
            min-height: 200px;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        
        #qrCode {
            max-width: 100%;
            max-height: 300px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            display: none; /* 初始隐藏 */
        }
        
        .placeholder {
            color: #999;
            font-size: 14px;
        }
        
        .btn-group {
            display: flex;
            gap: 1rem;
        }
        
        button {
            padding: 12px 20px;
            border: none;
            border-radius: 8px;
            font-size: 16px;
            font-weight: 500;
            cursor: pointer;
            transition: all 0.3s;
        }
        
        #generateBtn {
            flex: 1;
            background-color: #4285f4;
            color: white;
        }
        
        #generateBtn:hover {
            background-color: #3367d6;
        }
        
        #downloadBtn {
            flex: 1;
            background-color: #34a853;
            color: white;
        }
        
        #downloadBtn:hover {
            background-color: #2d8643;
        }
        
        #downloadBtn:disabled {
            background-color: #cccccc;
            cursor: not-allowed;
        }
        
        .error-message {
            color: #ea4335;
            font-size: 14px;
            margin-top: 0.5rem;
            display: none;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>二维码生成器</h1>
        
        <div class="input-group">
            <label for="textInput">输入网址/文本</label>
            <input type="text" id="textInput" placeholder="请输入要转换为二维码的内容...">
            <div class="error-message" id="errorMsg">请输入内容后再生成二维码</div>
        </div>
        
        <div class="size-control">
            <label for="sizeSlider">二维码尺寸</label>
            <input type="range" id="sizeSlider" min="128" max="512" step="32" value="256">
            <span id="sizeValue">256px</span>
        </div>
        
        <div class="qr-container">
            <div class="placeholder" id="placeholderText">生成的二维码将显示在这里</div>
            <canvas id="qrCode"></canvas>
        </div>
        
        <div class="btn-group">
            <button id="generateBtn">生成二维码</button>
            <button id="downloadBtn" disabled>下载二维码</button>
        </div>
    </div>
 
    <!-- 引入qrcode.js库 -->
    <script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.1/build/qrcode.min.js"></script>
    
    <script>
        // 获取DOM元素
        const textInput = document.getElementById('textInput');
        const sizeSlider = document.getElementById('sizeSlider');
        const sizeValue = document.getElementById('sizeValue');
        const qrCode = document.getElementById('qrCode');
        const generateBtn = document.getElementById('generateBtn');
        const downloadBtn = document.getElementById('downloadBtn');
        const placeholderText = document.getElementById('placeholderText');
        const errorMsg = document.getElementById('errorMsg');
        
        // 当前二维码是否已生成
        let qrGenerated = false;
        
        // 更新尺寸显示
        sizeSlider.addEventListener('input', () => {
            sizeValue.textContent = `${sizeSlider.value}px`;
        });
        
        // 防抖函数 - 优化性能
        function debounce(func, delay = 300) {
            let timer;
            return (...args) => {
                clearTimeout(timer);
                timer = setTimeout(() => {
                    func.apply(this, args);
                }, delay);
            };
        }
        
        // 生成二维码
        const generateQRCode = debounce(() => {
            const text = textInput.value.trim();
            const size = parseInt(sizeSlider.value);
            
            // 验证输入
            if (!text) {
                errorMsg.style.display = 'block';
                placeholderText.style.display = 'block';
                qrCode.style.display = 'none'; // 隐藏二维码
                qrGenerated = false;
                downloadBtn.disabled = true;
                return;
            }
            
            errorMsg.style.display = 'none';
            placeholderText.style.display = 'none';
            qrCode.style.display = 'block'; // 显示二维码容器
            
            // 生成二维码
            QRCode.toCanvas(qrCode, text, {
                width: size,
                margin: 1,
                color: {
                    dark: '#000000',
                    light: '#ffffff'
                }
            }, (error) => {
                if (error) {
                    console.error('生成二维码失败:', error);
                    alert('生成二维码时出错,请重试');
                    qrCode.style.display = 'none';
                    placeholderText.style.display = 'block';
                    placeholderText.textContent = '生成二维码失败,请重试';
                } else {
                    qrGenerated = true;
                    downloadBtn.disabled = false;
                }
            });
        });
        
        // 下载二维码
        function downloadQRCode() {
            if (!qrGenerated) return;
            
            // 从canvas获取图片数据
            qrCode.toBlob((blob) => {
                // 创建下载链接
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                // 使用当前时间作为文件名,避免重复
                const fileName = `qrcode-${new Date().getTime()}.png`;
                a.download = fileName;
                
                // 触发下载
                document.body.appendChild(a);
                a.click();
                
                // 清理
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
            });
        }
        
        // 事件监听
        generateBtn.addEventListener('click', generateQRCode);
        downloadBtn.addEventListener('click', downloadQRCode);
        textInput.addEventListener('input', generateQRCode);
        sizeSlider.addEventListener('input', () => {
            if (qrGenerated) {
                generateQRCode(); // 尺寸改变时重新生成二维码
            }
        });
    </script>
</body>
</html>

案例3:五星级评分组件(半星支持 + 持久化)

核心功能

支持精确到半星的评分(0-5 星,间隔 0.5 星)鼠标悬浮时实时预览评分效果点击选择评分并自动保存到本地存储页面刷新后自动恢复上次评分结果显示当前评分的具体数值

技术点

CSS 伪元素实现半星视觉效果事件委托处理鼠标交互事件本地存储(localStorage)实现数据持久化鼠标位置计算实现半星精准评分评分状态的视觉反馈与过渡动画

效果图

实现代码



<!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', system-ui, sans-serif;
        }
        
        .rating-container {
            max-width: 600px;
            margin: 4rem auto;
            padding: 2rem;
            background: #fff;
            border-radius: 12px;
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
            text-align: center;
        }
        
        h1 {
            color: #333;
            margin-bottom: 2rem;
            font-weight: 600;
        }
        
        .rating-wrapper {
            position: relative;
            display: inline-block;
            font-size: 48px; /* 星星大小 */
            color: #ddd; /* 未选中星星颜色 */
            cursor: pointer;
            margin-bottom: 1.5rem;
            user-select: none; /* 防止文本选中 */
        }
        
        /* 星星容器 */
        .stars {
            position: relative;
            display: flex;
        }
        
        /* 单个星星容器(用于半星效果) */
        .star {
            position: relative;
            width: 48px;
            height: 48px;
            overflow: hidden;
            transition: transform 0.1s ease;
        }
        
        /* 星星悬停效果 */
        .star:hover {
            transform: scale(1.05);
        }
        
        /* 星星符号 */
        .star::before {
            content: "★";
            position: absolute;
            left: 0;
            color: inherit;
            transition: color 0.2s ease;
        }
        
        /* 半星效果覆盖层 - 关键修复:调整定位确保不溢出 */
        .star-cover {
            position: absolute;
            top: 0;
            left: 0;
            width: 50%;
            height: 100%;
            overflow: hidden;
            transform: translateX(0); /* 确保初始位置正确 */
        }
        
        .star-cover::before {
            content: "★";
            position: absolute;
            left: 0; /* 调整为0,确保半星在容器内 */
            color: #ffce31; /* 选中星星颜色 */
        }
        
        /* 评分结果显示 */
        .rating-result {
            font-size: 18px;
            color: #555;
            margin-bottom: 1rem;
        }
        
        .rating-result span {
            color: #ff9500;
            font-weight: 500;
        }
        
        .rating-hint {
            color: #999;
            font-size: 14px;
            line-height: 1.6;
        }
    </style>
</head>
<body>
    <div class="rating-container">
        <h1>星级评分组件</h1>
        
        <div class="rating-wrapper" id="ratingWrapper">
            <div class="stars" id="stars">
                <!-- 5颗星,每颗星包含一个半星覆盖层 -->
                <div class="star">
                    <div class="star-cover"></div>
                </div>
                <div class="star">
                    <div class="star-cover"></div>
                </div>
                <div class="star">
                    <div class="star-cover"></div>
                </div>
                <div class="star">
                    <div class="star-cover"></div>
                </div>
                <div class="star">
                    <div class="star-cover"></div>
                </div>
            </div>
        </div>
        
        <div class="rating-result">
            当前评分: <span id="ratingValue">0</span> 星
        </div>
        
        <p class="rating-hint">
            提示:鼠标悬停或滑动可以预览评分,点击或释放鼠标可以确认评分(支持半星评分),刷新页面后仍会保留您的评分
        </p>
    </div>
 
    <script>
        // 获取DOM元素
        const ratingWrapper = document.getElementById('ratingWrapper');
        const stars = document.getElementById('stars');
        const starElements = document.querySelectorAll('.star');
        const ratingValue = document.getElementById('ratingValue');
        
        // 评分相关变量
        const MAX_RATING = 5; // 最高5星
        let currentRating = 0; // 当前评分
        let isDragging = false; // 是否正在拖动
        let wrapperRect = null; // 评分容器的位置信息
        
        // 计算评分容器的位置信息(用于精准计算)
        function updateWrapperRect() {
            wrapperRect = ratingWrapper.getBoundingClientRect();
        }
        
        // 初始化时计算一次位置
        updateWrapperRect();
        // 窗口大小改变时重新计算位置
        window.addEventListener('resize', updateWrapperRect);
        
        // 从本地存储加载评分
        function loadRating() {
            const savedRating = localStorage.getItem('userRating');
            if (savedRating !== null) {
                currentRating = parseFloat(savedRating);
                updateRatingDisplay(currentRating);
            }
        }
        
        // 保存评分到本地存储
        function saveRating(rating) {
            localStorage.setItem('userRating', rating.toString());
        }
        
        // 更新评分显示 - 确保半星渲染准确
        function updateRatingDisplay(rating) {
            // 清除所有星星的选中状态
            starElements.forEach(star => {
                star.style.color = '';
                star.querySelector('.star-cover').style.display = 'none';
            });
            
            // 计算完整星星和半星数量
            const fullStars = Math.floor(rating);
            const hasHalfStar = rating % 1 >= 0.5;
            
            // 设置完整星星
            for (let i = 0; i < fullStars; i++) {
                if (starElements[i]) {
                    starElements[i].style.color = '#ffce31';
                }
            }
            
            // 设置半星 - 确保仅半星显示且不溢出
            if (hasHalfStar && fullStars < MAX_RATING) {
                starElements[fullStars].style.color = '#ffce31';
                starElements[fullStars].querySelector('.star-cover').style.display = 'block';
            }
            
            // 更新评分文本
            ratingValue.textContent = rating.toFixed(1);
        }
        
        // 根据鼠标位置计算评分(核心算法,确保位置精准)
        function calculateRatingFromPosition(event) {
            // 如果位置信息未获取,重新获取
            if (!wrapperRect) updateWrapperRect();
            
            // 计算鼠标在评分组件内的相对X坐标
            const x = event.clientX - wrapperRect.left;
            
            // 确保坐标在有效范围内
            if (x < 0) return 0;
            if (x > wrapperRect.width) return MAX_RATING;
            
            // 每颗星的宽度
            const starWidth = wrapperRect.width / MAX_RATING;
            
            // 计算基础评分
            let rating = x / starWidth;
            
            // 四舍五入到最近的0.5星
            rating = Math.round(rating * 2) / 2;
            
            // 限制评分范围
            return Math.min(Math.max(rating, 0), MAX_RATING);
        }
        
        // 处理鼠标移动(滑动评分预览)
        function handleMouseMove(event) {
            // 如果正在拖动或鼠标在评分区域上按下,更新预览
            if (isDragging) {
                const rating = calculateRatingFromPosition(event);
                updateRatingDisplay(rating);
            }
        }
        
        // 处理鼠标按下(开始拖动评分)
        function handleMouseDown(event) {
            isDragging = true;
            // 按下时立即更新评分预览
            const rating = calculateRatingFromPosition(event);
            updateRatingDisplay(rating);
            // 阻止默认行为防止拖动时选中文本
            event.preventDefault();
        }
        
        // 处理鼠标释放(确认评分)
        function handleMouseUp(event) {
            if (isDragging) {
                isDragging = false;
                currentRating = calculateRatingFromPosition(event);
                updateRatingDisplay(currentRating);
                saveRating(currentRating); // 保存评分
            }
        }
        
        // 处理鼠标离开
        function handleMouseLeave() {
            if (isDragging) {
                isDragging = false;
                updateRatingDisplay(currentRating);
            }
        }
        
        // 处理点击评分
        function handleClick(event) {
            const rating = calculateRatingFromPosition(event);
            currentRating = rating;
            updateRatingDisplay(rating);
            saveRating(rating);
        }
        
        // 绑定事件监听
        stars.addEventListener('mousemove', handleMouseMove);
        stars.addEventListener('mousedown', handleMouseDown);
        document.addEventListener('mouseup', handleMouseUp);
        stars.addEventListener('mouseleave', handleMouseLeave);
        stars.addEventListener('click', handleClick);
        
        // 支持触摸设备
        stars.addEventListener('touchstart', (e) => {
            isDragging = true;
            const touch = e.touches[0];
            const rating = calculateRatingFromPosition(touch);
            updateRatingDisplay(rating);
            e.preventDefault();
        });
        
        stars.addEventListener('touchmove', (e) => {
            if (isDragging) {
                const touch = e.touches[0];
                const rating = calculateRatingFromPosition(touch);
                updateRatingDisplay(rating);
                e.preventDefault();
            }
        });
        
        stars.addEventListener('touchend', (e) => {
            if (isDragging) {
                isDragging = false;
                const touch = e.changedTouches[0];
                currentRating = calculateRatingFromPosition(touch);
                updateRatingDisplay(currentRating);
                saveRating(currentRating);
            }
        });
        
        // 初始化 - 加载保存的评分
        loadRating();
    </script>
</body>
</html>

案例4:手风琴折叠面板:单开 / 多开模式完整实现方案

核心功能

点击面板标题可展开 / 折叠对应的内容区域。支持两种模式切换:单开模式(同一时间仅一个面板展开)、多开模式(多个面板可同时展开)。展开 / 折叠过程带平滑过渡动画,提升用户体验。

关键技术点

DOM 操作:通过 
querySelector
 选取元素,动态修改 
class
 控制面板状态。事件委托:将点击事件绑定到父容器,减少事件监听数量,提升性能。CSS 过渡:用 
transition
 实现内容区域高度变化的平滑动画,避免生硬切换。状态管理:通过变量标记当前模式,控制点击时是否关闭其他已展开面板。

效果图

实现代码



<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>JS 手风琴折叠面板</title>
    <!-- 引入Font Awesome图标库 -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css">
    <style>
        /* 容器样式:居中布局 */
        .accordion-container {
            max-width: 800px;
            margin: 50px auto;
            padding: 0 20px;
        }
 
        /* 模式切换按钮:基础样式 */
        .mode-toggle {
            padding: 10px 20px;
            margin-bottom: 20px;
            background: #409eff;
            color: #fff;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
            transition: background 0.3s;
        }
        .mode-toggle:hover {
            background: #66b1ff;
        }
 
        /* 手风琴面板组:清除默认间距 */
        .accordion {
            width: 100%;
            border: 1px solid #e6e6e6;
            border-radius: 4px;
            overflow: hidden;
        }
 
        /* 单个面板:底部边框分隔 */
        .accordion-item {
            border-bottom: 1px solid #e6e6e6;
        }
        .accordion-item:last-child {
            border-bottom: none;
        }
 
        /* 面板标题栏:点击区域,Flex布局 */
        .accordion-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 16px 20px;
            background: #f5f5f5;
            cursor: pointer;
            font-size: 16px;
            transition: background 0.3s;
        }
        .accordion-header:hover {
            background: #eee;
        }
 
        /* 箭头图标:过渡动画,展开时旋转 */
        .accordion-header i {
            transition: transform 0.3s;
        }
        .accordion-item.active .accordion-header i {
            transform: rotate(180deg);
        }
 
        /* 面板内容区:默认折叠(高度0,隐藏溢出) */
        .accordion-content {
            height: 0;
            padding: 0 20px;
            overflow: hidden;
            background: #fff;
            transition: height 0.3s, padding 0.3s; /* 高度和内边距过渡 */
        }
 
        /* 展开状态:动态计算高度,显示内边距 */
        .accordion-item.active .accordion-content {
            padding: 20px;
            border-top: 1px solid #e6e6e6;
        }
    </style>
</head>
<body>
    <div class="accordion-container">
        <!-- 模式切换按钮 -->
        <button class="mode-toggle" id="modeToggle">
            当前模式:<span id="modeText">单开</span>
        </button>
 
        <!-- 手风琴面板组 -->
        <div class="accordion" id="accordion">
            <!-- 面板1 -->
            <div class="accordion-item">
                <div class="accordion-header">
                    <span>面板 1:前端基础</span>
                    <i class="fa fa-chevron-down"></i>
                </div>
                <div class="accordion-content">
                    包含 HTML、CSS、JavaScript 三大核心技术,是构建网页的基础。HTML 负责结构,CSS 负责样式,JavaScript 负责交互逻辑。
                </div>
            </div>
 
            <!-- 面板2 -->
            <div class="accordion-item">
                <div class="accordion-header">
                    <span>面板 2:框架选型</span>
                    <i class="fa fa-chevron-down"></i>
                </div>
                <div class="accordion-content">
                    常用前端框架有 Vue、React、Angular。Vue 上手简单,生态完善;React 组件化思想成熟,适合复杂应用;Angular 适合大型企业级项目。
                </div>
            </div>
 
            <!-- 面板3 -->
            <div class="accordion-item">
                <div class="accordion-header">
                    <span>面板 3:性能优化</span>
                    <i class="fa fa-chevron-down"></i>
                </div>
                <div class="accordion-content">
                    优化方向包括:减少 HTTP 请求、压缩资源(JS/CSS/图片)、使用懒加载、合理利用缓存、避免 DOM 频繁操作等。
                </div>
            </div>
        </div>
    </div>
 
    <script>
        // 确保DOM加载完成后执行
        document.addEventListener('DOMContentLoaded', function() {
            // 获取DOM元素
            const accordion = document.getElementById('accordion');
            const modeToggle = document.getElementById('modeToggle');
            const modeText = document.getElementById('modeText');
            
            // 初始化状态
            let singleMode = true;
            
            // 计算内容高度的函数(修复动画问题)
            function updateContentHeight() {
                const activeItems = accordion.querySelectorAll('.accordion-item.active');
                activeItems.forEach(item => {
                    const content = item.querySelector('.accordion-content');
                    content.style.height = content.scrollHeight + 'px';
                });
            }
            
            // 关闭所有面板
            function closeAllItems() {
                const activeItems = accordion.querySelectorAll('.accordion-item.active');
                activeItems.forEach(item => {
                    item.classList.remove('active');
                    const content = item.querySelector('.accordion-content');
                    content.style.height = '0';
                });
            }
            
            // 面板点击事件
            accordion.addEventListener('click', (e) => {
                const header = e.target.closest('.accordion-header');
                if (!header) return;
                
                const currentItem = header.parentElement;
                const isActive = currentItem.classList.contains('active');
                const content = currentItem.querySelector('.accordion-content');
                
                if (singleMode) {
                    closeAllItems();
                    if (!isActive) {
                        currentItem.classList.add('active');
                        content.style.height = content.scrollHeight + 'px';
                    }
                } else {
                    if (isActive) {
                        currentItem.classList.remove('active');
                        content.style.height = '0';
                    } else {
                        currentItem.classList.add('active');
                        content.style.height = content.scrollHeight + 'px';
                    }
                }
            });
            
            // 模式切换事件
            modeToggle.addEventListener('click', () => {
                singleMode = !singleMode;
                modeText.textContent = singleMode ? '单开' : '多开';
                
                if (singleMode) {
                    closeAllItems();
                }
            });
            
            // 窗口大小变化时重新计算高度
            window.addEventListener('resize', updateContentHeight);
        });
    </script>
</body>
</html>

案例5:滚动进度条实现:实时显示页面滚动百分比

核心功能

页面滚动时,进度条同步显示滚动进度(0%~100%)。顶部固定显示进度条,不遮挡页面内容。实时计算并展示滚动百分比数值,视觉反馈更清晰。

关键技术点

滚动事件监听:通过 
scroll
 事件捕获页面滚动状态。DOM 尺寸计算:利用 
document.documentElement
 的 
scrollHeight

clientHeight

scrollTop
 计算滚动进度。样式动态更新:通过修改元素 
width
 实现进度条动画,
textContent
 更新百分比数值。防抖优化:可选添加防抖函数,减少滚动事件触发频率,提升性能

效果图

实现代码



<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>JS 滚动进度条(带百分比显示)</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
 
        /* 进度条容器:固定在顶部,全屏宽度 */
        .progress-container {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 4px;
            background-color: #f1f1f1;
            z-index: 9999; /* 确保不被其他元素遮挡 */
        }
 
        /* 进度条本体:初始宽度 0,背景渐变 */
        .progress-bar {
            height: 100%;
            width: 0%;
            background: linear-gradient(90deg, #409eff, #66b1ff);
            transition: width 0.2s ease; /* 平滑过渡动画 */
        }
 
        /* 百分比显示:固定在右上角,悬浮样式 */
        .progress-percent {
            position: fixed;
            top: 10px;
            right: 20px;
            padding: 4px 12px;
            background-color: rgba(64, 158, 255, 0.9);
            color: #fff;
            border-radius: 20px;
            font-size: 14px;
            font-family: "Microsoft YaHei", sans-serif;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
            z-index: 9999;
        }
 
        /* 页面内容样式:模拟长页面 */
        .content {
            max-width: 1000px;
            margin: 50px auto;
            padding: 0 20px;
            font-size: 18px;
            line-height: 1.8;
            color: #333;
        }
 
        .content h2 {
            margin: 40px 0 20px;
            color: #2c3e50;
        }
 
        .content p {
            margin-bottom: 25px;
        }
    </style>
</head>
<body>
    <!-- 进度条容器 -->
    <div class="progress-container">
        <div class="progress-bar" id="progressBar"></div>
    </div>
 
    <!-- 百分比显示 -->
    <div class="progress-percent" id="progressPercent">0%</div>
 
    <!-- 页面内容(模拟长页面) -->
    <div class="content">
        <h2>前端开发基础</h2>
        <p>前端开发是创建 Web 页面或 App 等前端界面的过程,主要关注用户可见的部分。核心技术包括 HTML、CSS 和 JavaScript,三者各司其职:HTML 负责构建页面结构,CSS 负责美化页面样式,JavaScript 负责实现页面交互逻辑。</p>
        <p>随着技术发展,前端生态不断丰富,出现了各类框架和工具,如 Vue、React、Angular 等框架,Webpack、Vite 等构建工具,以及 Less、Sass 等 CSS 预处理器,极大提升了开发效率和项目可维护性。</p>
 
        <h2>滚动事件与 DOM 尺寸</h2>
        <p>在 JavaScript 中,`window` 对象的 `scroll` 事件可监听页面滚动状态。要计算滚动进度,需获取三个关键尺寸:`scrollHeight`(文档总高度,包括不可见部分)、`clientHeight`(视口高度,可见区域高度)、`scrollTop`(已滚动距离,文档顶部到视口顶部的距离)。</p>
        <p>滚动进度的计算公式为:(scrollTop / (scrollHeight - clientHeight)) * 100%。其中 `scrollHeight - clientHeight` 是页面可滚动的总距离,避免因视口高度导致计算偏差。</p>
 
        <h2>性能优化技巧</h2>
        <p>滚动事件触发频率极高(每秒可达数十次),直接在事件回调中执行 DOM 操作可能导致页面卡顿。可通过防抖函数限制回调执行频率,例如设置 100ms 内只执行一次,平衡实时性和性能。</p>
        <p>此外,使用 `requestAnimationFrame` 替代直接修改样式,能让进度条动画更流畅,与浏览器重绘节奏保持一致,提升视觉体验。</p>
 
        <h2>进度条应用场景</h2>
        <p>滚动进度条常见于博客、文档、长表单等场景。例如,技术文档页面添加进度条,用户可快速了解阅读进度;长表单页面通过进度条提示填写进度,减少用户焦虑感。</p>
        <p>除了顶部横向进度条,还可扩展为侧边纵向进度条、圆形进度条等样式,适配不同页面设计风格。通过修改 CSS 样式(如高度、颜色、位置),可轻松自定义进度条外观。</p>
 
        <h2>扩展功能建议</h2>
        <p>1. 进度条颜色渐变:根据滚动进度切换颜色,例如 0%~30% 蓝色、30%~70% 绿色、70%~100% 橙色。</p>
        <p>2. 进度记忆功能:通过 `localStorage` 存储滚动位置,刷新页面后恢复上次进度,适合长文档阅读。</p>
        <p>3. 自定义进度条样式:支持用户选择进度条位置(顶部/侧边)、高度、颜色,提升交互灵活性。</p>
        <p>4. 完成提示:滚动到页面底部时,进度条添加完成动画(如闪烁、变色),并显示“已阅读完毕”提示。</p>
    </div>
 
    <script>
        // 确保 DOM 加载完成后执行
        document.addEventListener('DOMContentLoaded', function() {
            // 获取 DOM 元素
            const progressBar = document.getElementById('progressBar');
            const progressPercent = document.getElementById('progressPercent');
 
            // 防抖函数:限制函数执行频率
            function debounce(fn, delay = 100) {
                let timer = null;
                return function() {
                    clearTimeout(timer);
                    timer = setTimeout(() => {
                        fn.apply(this, arguments);
                    }, delay);
                };
            }
 
            // 计算并更新滚动进度
            function updateProgress() {
                // 获取关键尺寸
                const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
                const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
                const clientHeight = document.documentElement.clientHeight || window.innerHeight;
 
                // 计算滚动进度(避免除以 0)
                const progress = scrollHeight > clientHeight ? (scrollTop / (scrollHeight - clientHeight)) * 100 : 100;
                const progressNum = Math.round(progress); // 四舍五入为整数
 
                // 更新进度条和百分比
                progressBar.style.width = `${progressNum}%`;
                progressPercent.textContent = `${progressNum}%`;
            }
 
            // 监听滚动事件(添加防抖优化)
            window.addEventListener('scroll', debounce(updateProgress));
 
            // 页面加载完成后初始化进度(避免初始状态异常)
            updateProgress();
        });
    </script>
</body>
</html>

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容