(第三篇)HMTL+CSS+JS-新手小白循序渐进案例入门
前言:
继续完成上一个功能的优化,本次目标:实现「可折叠的历史面板」,点击按钮展开/收起历史记录的面板-----------学习 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 ? '◀️ 收起' : '▶️ 历史';});});
结果图:
收起:
展开: