(第三篇)HMTL+CSS+JS-新手小白循序渐进案例入门

发布于:2025-07-03 ⋅ 阅读:(13) ⋅ 点赞:(0)

前言:

继续完成上一个功能的优化,本次目标:实现「可折叠的历史面板」,点击按钮展开/收起历史记录的面板-----------学习 CSS 过渡动画 + JavaScript 状态控制。

首先还是先学习基础知识概念,再实战~


第一部分:CSS 过渡动画基础知识:

一、什么是 CSS 过渡?

让元素样式变化时产生平滑的动画效果(比如宽度从0到300px的渐变)

二、核心属性

transition: [属性名] [持续时间] [速度曲线] [延迟时间];

属性 作用 常用值
transition-property 要过渡的属性 all/width/opacity
transition-duration 动画时长 .3s/500ms
transition-timing-function 速度曲线 ease(默认)/linear/ease-in-out
transition-delay 延迟时间 0s/.2s

三、举例写法:

.history-panel {
  transition: all 0.3s ease; /* 所有属性变化都有0.3秒的缓动动画 */
}

 为什么用 all
因为我们要同时动画宽度、flex属性、内边距等多个样式


 第二部分:JavaScript 状态控制基础知识

一、什么是状态控制?

通过 JS 动态修改元素的 class 来改变其样式,这是前端开发的核心思想!

二、核心方法

方法 作用 示例
classList.add() 添加类名 el.classList.add('active')
classList.remove() 移除类名 el.classList.remove('active')
classList.toggle() 切换类名(有则删,无则加) el.classList.toggle('active')
classList.contains() 检查是否包含类名 if(el.classList.contains('active'))

三、实现

思考:

首先我们先思考,我们需要一个按钮来控制历史面板的展开和收起,即样式需要改变!是不是就运用到了JS的状态控制呢?前面也说了JS可以通过修改class去改变样式,所以我们可以自定义一个CSS类名expanded,用于标记"展开状态",具体如下:

  • 没有 expanded 类 → 收起状态
  • 有 expanded 类 → 展开状态
/* 默认状态(收起) */
.history-panel {
  width: 0;
}

/* 展开状态 */
.history-panel.expanded {  /* 同时有.history-panel和.expanded类时生效 */
  width: 300px;
}

捋一下实现的流程(写代码的步骤): 

 

文字理解:默认收起。当用户点击历史记录的按钮后,触发click事件,我们新增一个expanded类来表示历史面板的展开状态。这时JS就会去检查是否有expanded这个类?没有就添加,然后切换expanded类(展开),有就移除这个类,回到默认的收起。------>发现没,这里就很适用方法classList.toggle()!

那么切换到expanded类的这一步代码就是historyPanel.classList.toggle('expanded')

下一步的检查当前状态:const isExpanded = historyPanel.classList.contains('expanded')

整个流程: 

用户点击按钮 → JS切换类名 → CSS样式生效 → 页面重新渲染 → 用户看到动画效果

 代码实现:

这里就是按照上面的那张时序图去写

                // 切换历史面板展开/收起
                // 在DOM加载完成后执行
                document.addEventListener('DOMContentLoaded', function() {
                    // 1. 获取DOM元素(按钮+历史面板)
                    const toggleBtn = document.getElementById('toggle-history');
                    const historyPanel = document.querySelector('.history-panel');
                    // 2. 给按钮添加点击事件监听
                    toggleBtn.addEventListener('click', function() {
                        // 3. 切换历史面板的'expanded'类
                        historyPanel.classList.toggle('expanded');
                        // 4. 检查当前是否展开
                        const isExpanded = historyPanel.classList.contains('expanded');
                        // 5. 根据状态更新按钮文字
                        toggleBtn.innerHTML = isExpanded ? '◀️ 收起' : '▶️ 历史';
                    });
                });

全部代码:

html:

<!DOCTYPE html>                      <!--文档类型声明,告诉浏览器这是一个 HTML5 文档-->
    <html lang="en">                 <!--根标签, 设置文档语言为英语-->
        <head>                       <!--包含文档的元数据,如标题、字符编码、引入的外部资源等-->
            <title>简单表单</title>
            <address>
                Written by island.</br>
                Time:2025-06-10
            </address>
 
            <link rel="stylesheet" href="keyandhistory.css"> <!--引入外部 CSS 文件, 用于样式-->
        </head>
 
        <body>                        <!--文档的主体内容, 包含所有可见的页面内容,如文本、按钮、图片等-->
            <div class="app-container">   <!--使用类名来应用 CSS 样式-->
                <!-- 历史面板移到左边 -->
                <div class="history-panel">
                    <div class="calculate-header">
                        <!-- 新增展开/收起按钮 -->
                        <button id="toggle-history" class="'toggle-btn">▶️历史</button>
                    </div>
                    <div id="history-list"></div>
                </div>

                <!-- 原计算器包裹在新容器中 -->
                <div class="calculator-container">
                    <div class="calculator">
                        <h3>Island的计算器</h3>
                        <input type="text" id="display" class="display" readonly>     <!--readonly 属性使输入框只读,用户无法编辑内容-->
                        <div class="buttons">
                            <button onclick="appendToDisplay('7')">7</button>          <!--将字符串 '7' 作为参数传入函数appendToDisplay-->
                            <button onclick="appendToDisplay('8')">8</button>
                            <button onclick="appendToDisplay('9')">9</button>
                            <button class="operator" onclick="appendToDisplay('+')">+</button>
        
                            <button onclick="appendToDisplay('4')">4</button>
                            <button onclick="appendToDisplay('5')">5</button>
                            <button onclick="appendToDisplay('6')">6</button>
                            <button class="operator" onclick="appendToDisplay('-')">-</button>
        
                            <button onclick="appendToDisplay('1')">1</button>
                            <button onclick="appendToDisplay('2')">2</button>
                            <button onclick="appendToDisplay('3')">3</button>
                            <button class="operator" onclick="appendToDisplay('*')">*</button>
        
                            <button onclick="appendToDisplay('0')">0</button>
                            <button onclick="clearDisplay()">C</button>                <!--点击 C 按钮时调用 clearDisplay 函数-->
                            <button onclick="appendToDisplay('.')">.</button>
                            <button class="operator" onclick="appendToDisplay('/')">/</button>
        
                            <button class="calculate" onclick="calculate()">计算</button>         <!--点击等于按钮时调用 calculate 函数-->
                        </div>
                    </div>
                </div>
                    
                <!--id: 为元素指定唯一标识符,方便后续通过 JavaScript 访问 ;  placeholder: 提供提示文本,当输入框为空时显示-->
                <!--按钮点击时触发 JavaScript 函数 calculate(),计算输入的表达式-->
                <p id="result"></p>
            </div>

            <script src="keyandhistory.js"></script> <!--引入外部 JavaScript 文件, 用于交互逻辑-->
 
        </body>
        </html>

CSS:

 

body {                              /* 选择器,选中 HTML 中的 body 元素 */
    font-family:Arial, sans-serif;  /* 设置页面的字体, 多个字体是备用方案*/
    margin: 0;
    padding: 20px;
    background-color: #f4f4f4;      /* 设置页面背景颜色, 使用十六进制颜色码*/
}
.app-container {
    display: flex;         /* 启用Flex布局 */
    min-height: 90vh;       /* 至少占满90%视口高度 */
    gap: 20px;            /* 子元素间距 */
}
.container {                       /* 选中所有 class="container" 的元素来应用 CSS 样式 */
    max-width: 300px;          /* 设置容器的最大宽度 */
    margin: 0 auto;           /* 水平居中容器 */
    padding: 20px;            /* 设置容器内边距 */
    background-color: #fff;   /* 设置容器背景颜色 */
    border-radius: 5px;       /* 设置容器圆角 */
    box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); /* 设置容器阴影 */
}
.display {
    width: 100%;               /* 输入框和按钮宽度占满容器 */
    padding: 10px;            /* 设置输入框和按钮内边距 */
    margin-bottom: 15px;
    box-sizing: border-box;
    text-align: right;
    font-size: 1.2em;
    border: 1px solid #ddd;
    border-radius: 3px;             /* 设置圆角 */ 
}
.buttons {                     
    display: grid;            /* 使用网格布局 */
    grid-template-columns: repeat(4, 1fr); /* 设置四列等宽 */
    gap: 10px;                /* 设置按钮之间的间距 */

}
button {
    padding: 10px;
    background-color: #f1f1f1;
    border: 1px solid #ddd;
    border-radius: 3px;
    cursor: pointer;           /* 鼠标悬停时显示手型光标 */
    font-size: 1em;        /* 设置按钮文字大小 */
}
button:hover {                      /* :hover 是伪类,当鼠标悬停在元素上时生效 */
    background-color: #e1e1e1; /* 鼠标悬停时改变按钮背景颜色 */
}
.operator {                     /* 选中 class="operator" 的元素来应用 CSS 样式 */
    background-color: #4CAF50; /* 设置运算符按钮的背景颜色 */
    color: white;              /* 设置运算符按钮文字颜色为白色 */
}
.operator:hover {
    background-color: #45a049;
}
.calculate {
    background-color: #2196F3;
    color: white;
    grid-column: span 4;
}
.calculate:hover {
    background-color: #0b7dda;
}  

/* 历史记录面板---------默认状态(收起) */
.history-panel {

    display: flex;
    align-items: flex-start;
    flex: 0 0 80px;                            /* 收起时只显示图标 */
    width: 0;
    overflow: hidden;                          /* 隐藏溢出内容 */
    padding: 15px 5px;
    transition: all 0.3s ease;                 /* 平滑过渡效果 */
    position: relative;                        /* 相对定位以便后续绝对定位 */
    min-width: 0;                              /* 覆盖flex默认最小内容宽度 */
    white-space: nowrap;                       /* 防止文本换行影响 */
}

/* 历史记录面板---------展开状态 */
.history-panel.expanded {                      /* 同时有.history-panel和.expanded类时生效 */
    flex: 0 0  300px;                          /* 展开时显示完整宽度 */
    width: 300px;                              /* 展开时显示完整宽度 */
    min-width: initial;                        /* 恢复默认 */
}
.history-panel h4 {
    opacity: 0; /* 初始隐藏标题 */
    transition: opacity 0.2s; /* 平滑过渡效果 */
}
#history-list {
    padding: 15px;
    min-height: 100%;  /* 确保高度撑满 */
    padding-top: 50px;
}

/* 按钮样式 */
.toggle-btn {   
    background: none; /* 按钮背景透明 */
    border: none; /* 去除边框 */
    cursor: pointer; /* 鼠标悬停时显示手型光标 */
    font-size: 14px;
    padding: 5px 10px;
    transition: transform 0.3s ease; /* 平滑过渡效果 */
}
.toggle-btn:hover {
    background: #f0f0f0; /* 鼠标悬停时改变背景颜色 */
    transform: scale(1.1); /* 鼠标悬停时放大按钮 */
}
.calculate-header {
    display: flex;
    justify-content: space-between; /* 水平分布标题和按钮 */
    align-items: center;
    margin-bottom: 15px; /* 标题和按钮之间的间距 */
    width: 85px;
}
.history-item {
    flex: 1;                 /* 占据剩余所有空间 */
    display: flex;
    justify-content: center; /* 水平居中 */
    border-bottom: 1px dashed #eee; /* 使用虚线分隔每个历史记录项 */
    cursor: pointer; /* 鼠标悬停时显示手型光标 */
    white-space: wrap; /* 允许文本换行 */
}
.history-item:hover {
    background-color: #f9f9f9; /* 鼠标悬停时改变背景颜色 */
}
/* 计算器本体 */
.calculator {
width: 300px;            /* 固定宽度 */
background: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 5px rgba(0,0,0,0.1);
}

JS:

let calculateHistory = []; // 用于存储计算历史记录

                // 按键处理函数:监听键盘事件,获取按下的键,根据不同按键执行对应操作.
                function handleKeyPress(e) {
                    // e.key是用户按下的键
                    const key = e.key;

                    // 数字和运算符直接输入
                    if(/[0-9/./+\-/*//]/.test(key)) {
                        appendToDisplay(key);
                    }

                    // 回车键计算
                    else if (key === 'Enter') {
                        calculate();
                    }

                    // 退格键删除
                    else if (key === 'Backspace') {
                        const display = document.getElementById('display');
                        display.value = display.value.slice(0, -1); // 字符串截取方法,即删除最后一个字符
                    }

                    // ESC键清空
                    else if (key === 'Escape') {
                        clearDispaly();
                    }
                }


                // appendToDisplay 函数用于将按钮点击的数字或运算符添加到显示区域
                function appendToDisplay(value) {
                    // document.getElementById('display') 获取 id 为 display 的元素
                    const display = document.getElementById('display');
                    // 将传入的 value 添加到显示区域的当前值后面
                    display.value += value;
                }
 
                function clearDisplay() {
                    // 清空显示区域
                    const display = document.getElementById('display');
                    const result = document.getElementById('result');
                    
                    display.value = '';       // 清空输入框
                    result.innerHTML = '';    // 清空结果展示
                }
 
                function calculate(){
                    try {
                        // document是 JavaScript 中表示整个 HTML 文档的对象,getElementById是它的一个方法,用于查找具有指定 id 的元素
                        // innerHTML表示元素内部的 HTML 内容,可以用来动态更新页面显示
                        const expression = document.getElementById('display').value; // 获取显示区域的值
                        const result = eval(expression); // 使用 eval 函数计算表达式的值
                        document.getElementById('result').innerHTML = '计算结果: ' + result; // 显示计算结果

                        // 将计算结果添加到历史记录
                        addToHistory(expression, result);
                    } catch (error) {
                        document.getElementById('result').innerHTML = '错误: ' + error.message; // 如果计算出错,显示错误信息
                    }
                }

                function addToHistory(expression, result) {
                    // 1.创建历史记录对象
                    const historyItem = {
                        expr: expression,
                        result: result,
                        timestamp: new Date().toLocaleTimeString() // 获取当前时间
                    };
                    // 2.将历史记录添加到数组
                    calculateHistory.unshift(historyItem); // 使用 unshift 方法将新记录添加到数组开头,即显示在最上面

                    // 3.只保留最近的 10 条记录
                    if (calculateHistory.length > 10) {
                        calculateHistory.pop(); // 移除最老的记录: 使用 pop 方法删除数组最后一个元素
                    }
                    // 4.更新历史记录显示
                    renderHistory();
                }

                function renderHistory() {
                    // 1. 获取DOM容器
                    const  historyList = document.getElementById('history-list');
                    // 2.清空历史记录列表(重要!避免重复渲染)
                    historyList.innerHTML = ''; 
                    // 3.forEach 遍历数组
                    calculateHistory.forEach(item => {   
                        // 4. 创建单个历史记录DOM元素
                        const historyElement = document.createElement('div'); // 创建一个新的 div 元素
                        historyElement.className = 'history-item'; // 设置类名
                        // 5. 填充HTML内容(使用模板字符串)
                        historyElement.innerHTML = `
                            <span>${item.expr} = ${item.result}</span>    <!-- 显示表达式和结果 -->
                            <small style="color: #999; float: right;">${item.timestamp}</small>
                        `;
                        // 6. 添加点击事件监听器: 当用户点击历史记录时,将表达式填入显示区域
                        historyElement.addEventListener('click', () => {
                            document.getElementById('display').value = item.expr; // 点击历史记录时,将表达式填入显示区域
                        });
                        // 7. 将新创建的 div 添加到历史记录列表中
                        historyList.appendChild(historyElement); 
                    })
                }

                // 在script末尾添加事件监听(使用DOMContentLoaded确保 DOM 加载完成后再绑定事件)
                document.addEventListener('DOMContentLoaded', function() {
                    // 监听整个文档的键盘按下事件(keydown事件),调用handleKeyPress处理
                    document.addEventListener('keydown', handleKeyPress);
                })

                // 切换历史面板展开/收起
                // 在DOM加载完成后执行
                document.addEventListener('DOMContentLoaded', function() {
                    // 1. 获取DOM元素
                    const toggleBtn = document.getElementById('toggle-history');
                    const historyPanel = document.querySelector('.history-panel');
                    // 2. 给按钮添加点击事件监听
                    toggleBtn.addEventListener('click', function() {
                        // 3. 切换历史面板的'expanded'类
                        historyPanel.classList.toggle('expanded');
                        // 4. 检查当前是否展开
                        const isExpanded = historyPanel.classList.contains('expanded');
                        // 5. 根据状态更新按钮文字
                        toggleBtn.innerHTML = isExpanded ? '◀️ 收起' : '▶️ 历史';
                    });
                });

结果图:

收起:

展开: 

 


网站公告

今日签到

点亮在社区的每一天
去签到