前言:
继续完成上一个功能的优化,本次目标:实现「可折叠的历史面板」,点击按钮展开/收起历史记录的面板-----------学习 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 ? '◀️ 收起' : '▶️ 历史';
});
});
结果图:
收起:
展开: