三连决胜:任务11/12/13 | 课型:实习课 | 建议课时:4节
支持+-*/%^()与负号的递归下降解析计算器,拒绝eval。
新窗口运行 下载源码<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Task 11 - JS算术(安全版)</title>
<style>
body { font-family: Arial, "Microsoft YaHei", sans-serif; background: #29303b;
display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
.panel { background: #fff; padding: 30px; border-radius: 12px; width: 420px; }
input { width: 100%; padding: 12px; font-size: 18px; border: 2px solid #123a52; border-radius: 8px; box-sizing: border-box; }
button { margin-top: 14px; width: 100%; padding: 12px; font-size: 17px; border: none;
border-radius: 8px; background: #0f8c7f; color: #fff; cursor: pointer; }
.result { margin-top: 14px; font-size: 20px; color: #123a52; }
.hint { font-size: 13px; color: #6b8a99; margin-top: 10px; }
</style>
</head>
<body>
<div class="panel">
<input id="txt" value="2+3*4^2/(1+1)" placeholder="输入表达式,如 2+3*4">
<button id="btn">计 算</button>
<div class="result">计算结果为:<span></span></div>
<div class="hint">支持 + - * / % ^ 括号 负数 小数 | 无eval,递归下降解析</div>
</div>
<script>
function calculate(input) {
const tokens = input.match(/\d+\.?\d*|[-+*/%^()]/g);
if (!tokens) throw new Error('表达式为空');
let pos = 0;
const peek = () => tokens[pos];
const next = () => tokens[pos++];
function parseAdd() {
let val = parseMul();
while (peek() === '+' || peek() === '-') {
const op = next(); const rhs = parseMul();
val = op === '+' ? val + rhs : val - rhs;
}
return val;
}
function parseMul() {
let val = parsePow();
while (peek() === '*' || peek() === '/' || peek() === '%') {
const op = next(); const rhs = parsePow();
if (op === '*') val *= rhs; else if (op === '/') val /= rhs; else val %= rhs;
}
return val;
}
function parsePow() {
const base = parseAtom();
if (peek() === '^') { next(); return base ** parsePow(); }
return base;
}
function parseAtom() {
if (peek() === '-') { next(); return -parseAtom(); }
if (peek() === '(') {
next(); const val = parseAdd();
if (next() !== ')') throw new Error('缺少右括号');
return val;
}
return parseFloat(next());
}
const result = parseAdd();
if (pos !== tokens.length) throw new Error('表达式不完整');
return result;
}
document.getElementById('btn').addEventListener('click', () => {
const expr = document.getElementById('txt').value;
const out = document.querySelector('span');
try { out.textContent = calculate(expr); }
catch (err) { out.textContent = '错误:' + err.message; }
});
</script>
</body>
</html>Pointer Events实现流畅拖拽方块,兼容鼠标与触屏。
新窗口运行 下载源码<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Task 12 - JS Draggable</title>
<style>
body { margin: 0; height: 100vh; background: #29303b; font-family: Arial, sans-serif; overflow: hidden; }
#box { width: 120px; height: 120px; background: linear-gradient(135deg, #38d6c4, #0f8c7f);
border-radius: 12px; position: absolute; top: 100px; left: 100px; cursor: grab;
display: flex; align-items: center; justify-content: center; color: #fff;
font-weight: bold; user-select: none; touch-action: none; }
#box:active { cursor: grabbing; }
</style>
</head>
<body>
<div id="box">拖我</div>
<script>
const box = document.getElementById('box');
let dragging = false, offsetX = 0, offsetY = 0;
box.addEventListener('pointerdown', e => {
dragging = true;
offsetX = e.clientX - box.offsetLeft;
offsetY = e.clientY - box.offsetTop;
box.setPointerCapture(e.pointerId); // 捕获指针:移出元素也能跟踪
});
box.addEventListener('pointermove', e => {
if (!dragging) return;
box.style.left = (e.clientX - offsetX) + 'px';
box.style.top = (e.clientY - offsetY) + 'px';
});
box.addEventListener('pointerup', () => dragging = false);
</script>
</body>
</html>5×3切片网格依次飞入的过渡动画——背景定位+animationend(CSS渐变模拟图片)。
新窗口运行 下载源码<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Task 13 - 切片过渡</title>
<style>
body { margin: 0; min-height: 100vh; background: #123a52; display: flex;
flex-direction: column; align-items: center; justify-content: center; font-family: Arial, sans-serif; }
table { border-collapse: collapse; }
td { width: 100px; height: 100px; padding: 0; }
.slice { width: 100%; height: 100%;
background: linear-gradient(135deg, #38d6c4, #1b5a7a, #c77728);
background-size: 500px 300px; opacity: 0; transform: translateY(-40px);
transition: all .5s ease; }
.slice.on { opacity: 1; transform: translateY(0); }
button { margin-top: 24px; padding: 10px 28px; border: none; border-radius: 8px;
background: #38d6c4; color: #123a52; font-weight: bold; cursor: pointer; }
</style>
</head>
<body>
<table id="grid"></table>
<button id="play">播放过渡动画</button>
<script>
const COLS = 5, ROWS = 3;
const grid = document.getElementById('grid');
const slices = [];
for (let y = 0; y < ROWS; y++) {
const tr = document.createElement('tr');
for (let x = 0; x < COLS; x++) {
const td = document.createElement('td');
const d = document.createElement('div');
d.className = 'slice';
d.style.backgroundPosition = (-x * 100) + 'px ' + (-y * 100) + 'px';
td.appendChild(d); tr.appendChild(td); slices.push(d);
}
grid.appendChild(tr);
}
document.getElementById('play').addEventListener('click', () => {
slices.forEach(s => s.classList.remove('on'));
slices.forEach((s, i) => setTimeout(() => s.classList.add('on'), i * 80));
// 最后一片动画结束时收尾
slices[slices.length - 1].addEventListener('transitionend', () => {
console.log('过渡完成!');
}, { once: true });
});
</script>
</body>
</html>任务目标:攻克竞赛任务11/12/13三个难题
| 任务内容与评分 |
|---|
| 任务1:写出eval的三条罪状(10分) |
| 任务2:完成任务11计算器(含括号与错误处理)(30分) |
| 任务3:完成任务12拖拽(25分) |
| 任务4:完成任务13切片动画(25分) |
| 任务5:用事件委托重写一个列表点击(10分) |