# 项目四：平陆运河旅游——综合教学文稿

## 项目概述
以旅游主题练习 HTML、CSS 与原生 JavaScript，完成可独立打开的四个递进版本。所有 SVG 是原创教学示意，不冒充景区实景；学习线路不代表实际开放。天气来自真实公共 API，登记仅在当前浏览器。

## 任务编写说明
每任务建议 2—4 课时，按八个环节组织。先照步骤操作，再解释概念，最后独立改动与验收。成品位置 public/index.html；独立阶段位于 public/stages/；学生文件位于 学生起始素材/。直接打开 index.html 可看静态页面；浏览器对 file:// 存储的实现不同，推荐在素材根目录运行 python3 -m http.server 8080，再访问 http://localhost:8080/public/。无需安装框架。教学短代码用于单点练习，完整的防错处理见对应成品 app.js。

# 任务1：搭建 HTML 网页结构

## 一、任务描述
本任务完成“搭建 HTML 网页结构”。成品参照 public/stages/task-1-static/index.html；学生起点为 学生起始素材/task-1/index.html。本任务不引入 CSS 与 JavaScript。

## 二、问题引导
把内容组织清楚，为什么比先选颜色更重要？图片无法加载时读者还能了解什么？

## 三、知识准备
header、nav、main、section、footer；相对路径、alt、列表、表格。先找到对应元素或文件，再在浏览器中观察效果。

## 四、任务实施
1. 在学生起始素材/task-1 中打开 index.html。补齐页面标题与 lang="zh-CN"，保留 UTF-8 和 viewport。
2. 在 header 中输入网站名称；为 nav 添加 href="#about" 的链接，为 section 添加 id="about"。逐一点击验证锚点。
3. 用 h1 表示页面主题，用 h2 表示栏目，避免为字号而跳级。写出运河印象和教学用途声明。
4. 用 img 引入 assets/canal.svg，添加“原创教学示意图，非实景”的 alt。路径相对于当前 HTML，而不是电脑磁盘根目录。
5. 把主题看点写成列表或 article；把学习线路写成带 caption、thead、tbody 的表格。th 用 scope 关联行或列。
6. 保存并刷新。与 stages/task-1-static/index.html 比较，只检查内容和结构，不要求 CSS 外观。

### 最小代码练习
```html
<nav><a href="#about">运河印象</a></nav>
<main><section id="about"><h2>运河印象</h2>
<img src="assets/canal.svg" alt="运河原创教学示意图，非实景">
<p>本页面仅用于课堂学习。</p></section></main>
```

> 小提示：保存文件后刷新；先查看控制台第一条错误，再检查路径和拼写。

**试一试：** 把图片文件改名，观察失效，再同时修正 src；说明为什么不是网络故障。

**想一想：** 把内容组织清楚，为什么比先选颜色更重要？图片无法加载时读者还能了解什么？

## 五、任务验收
浏览器标题与正文中文正常；导航到达目标；关闭图片后 alt 有意义；表格存在标题和表头。

同伴互查：逐项记录“通过/未通过/修改办法”，不能只写“页面能打开”。

## 六、任务小结
语义标签帮助机器和人理解结构。相对路径以当前文件为起点。

## 七、任务拓展
增加“资料来源”栏目，只引用可核实来源，不补造开放时间。

## 八、知识巩固
完成《知识巩固题》任务1组；先独立答，再对照教师答案解释原因。

# 任务2：用 CSS 实现响应式页面

## 一、任务描述
本任务完成“用 CSS 实现响应式页面”。成品参照 public/stages/task-2-responsive/index.html；学生起点为 学生起始素材/task-2/index.html。起点继承上一任务已完成的成果，不能跳过上一步基础。

## 二、问题引导
手机屏幕变窄时三张卡片应该如何排列？固定宽度为什么容易造成溢出？

## 三、知识准备
选择器、盒模型、CSS 变量、Flex、Grid、媒体查询、焦点状态。先找到对应元素或文件，再在浏览器中观察效果。

## 四、任务实施
1. 复制已完成的任务一，创建 styles.css，并在 head 用 link rel="stylesheet" 引入。
2. 在 :root 定义 --ink、--cream、--teal；为 body 设置字色、背景、字体和行高，为所有元素设置 box-sizing:border-box。
3. 给页头使用 Flex；给三张看点卡片设置 display:grid 和三列。给图片设置 width:100% 与 object-fit:cover。
4. 为 .section 设置 max-width 和 margin:auto，使宽屏内容不过度拉长。使用 padding 留出阅读空间。
5. 在 @media(max-width:700px) 中将卡片改为一列，让两栏正文变为单栏。表格放入 overflow-x:auto 的容器。
6. 在开发者工具检查 375、768、1280 像素；用 Tab 浏览导航与按钮，保留 :focus-visible。对照第二阶段成品。

### 最小代码练习
```css
.cards { display:grid; grid-template-columns:repeat(3,1fr); gap:20px; }
img { max-width:100%; }
@media (max-width:700px) {
  .cards { grid-template-columns:1fr; }
}
a:focus-visible { outline:3px solid #e88d55; }
```

> 小提示：保存文件后刷新；先查看控制台第一条错误，再检查路径和拼写。

**试一试：** 把卡片改成两列后测试 700px 和 701px，比较媒体查询的边界。

**想一想：** 手机屏幕变窄时三张卡片应该如何排列？固定宽度为什么容易造成溢出？

## 五、任务验收
375px 页面无整体横向滚动；1280px 卡片三列；图片无拉伸；键盘焦点可见。

同伴互查：逐项记录“通过/未通过/修改办法”，不能只写“页面能打开”。

## 六、任务小结
响应式不是缩小全部内容，而是根据可用空间改变排版。

## 七、任务拓展
为 prefers-reduced-motion 添加动画偏好适配，不依靠颜色区分所有状态。

## 八、知识巩固
完成《知识巩固题》任务2组；先独立答，再对照教师答案解释原因。

# 任务3：添加轮播、北京时间与天气

## 一、任务描述
本任务完成“添加轮播、北京时间与天气”。成品参照 public/stages/task-3-api/index.html；学生起点为 学生起始素材/task-3/index.html。起点继承上一任务已完成的成果，不能跳过上一步基础。

## 二、问题引导
每秒更新时钟是否意味着每秒请求天气？网络失败时能否显示编造的晴天温度？

## 三、知识准备
DOM、事件、setInterval、Intl、fetch、async/await、JSON、异常、缓存。先找到对应元素或文件，再在浏览器中观察效果。

## 四、任务实施
1. 复制第二阶段；添加 defer 脚本。用 querySelector 取得图片、按钮、时钟和天气结果节点。
2. 创建图片数组和当前下标。上一张、下一张通过取模切换。每 6 秒自动切换；暂停按钮和减少动态效果偏好应被尊重。
3. 创建 updateClock，使用 Intl.DateTimeFormat 指定 Asia/Shanghai，并每秒调用。注明它读取设备时间，不是网络校时。
4. 在下拉框放入钦州与南宁教学坐标。构造 Open-Meteo URL，请求 current=temperature_2m,weather_code 与 timezone=Asia%2FShanghai。
5. 先显示加载中；检查 response.ok，再 await response.json()。验证 current 的温度、代码和时间，再输出数据时间及中文天气。
6. 把真实响应和保存时间存入 localStorage，10 分钟内复用并标记缓存。用 AbortController 设置超时。失败时有缓存显示“离线历史缓存”，无缓存明确说无数据。
7. 快速切换城市时使用请求序号，只让最后一次请求更新界面。通过开发者工具阻断 api.open-meteo.com，测试失败、缓存和重试。

### 最小代码练习
```javascript
async function readWeather() {
  try {
    const response = await fetch("https://api.open-meteo.com/v1/forecast?latitude=21.98&longitude=108.65&current=temperature_2m,weather_code&timezone=Asia%2FShanghai");
    if (!response.ok) throw new Error("请求失败");
    const data = await response.json();
    document.querySelector("#weather-result").textContent = data.current.temperature_2m + " °C";
  } catch {
    document.querySelector("#weather-result").textContent = "天气获取失败，请稍后重试";
  }
}
readWeather();
```

> 小提示：保存文件后刷新；先查看控制台第一条错误，再检查路径和拼写。

**试一试：** 保存一次真实天气后断网并点刷新，观察离线标签；清除天气键再试，比较两种失败状态。

**想一想：** 每秒更新时钟是否意味着每秒请求天气？网络失败时能否显示编造的晴天温度？

## 五、任务验收
轮播可前后切换与暂停；时钟秒数变化；联网展示响应温度和数据时间；缓存有标签；断网不伪造实时天气。

同伴互查：逐项记录“通过/未通过/修改办法”，不能只写“页面能打开”。

## 六、任务小结
异步请求可能失败或乱序；时钟刷新频率与天气请求频率必须分离。

## 七、任务拓展
给天气代码增加图标，但同时保留文字；讨论缓存过期与天气变化风险。

## 八、知识巩固
完成《知识巩固题》任务3组；先独立答，再对照教师答案解释原因。

# 任务4：完成本地登记与编号查询

## 一、任务描述
本任务完成“完成本地登记与编号查询”。成品参照 public/stages/task-4-form/index.html；学生起点为 学生起始素材/task-4/index.html。起点继承上一任务已完成的成果，不能跳过上一步基础。

## 二、问题引导
为什么同一编号在另一台设备上查不到？成功提示应该在保存之前还是之后显示？

## 三、知识准备
label、required、min/max、submit、preventDefault、FormData、JSON、localStorage、find。先找到对应元素或文件，再在浏览器中观察效果。

## 四、任务实施
1. 复制第三阶段，添加 register-form 和 query-form。每个字段都有 label；昵称只用虚构值，不收集真实联系方式。
2. 加入昵称、线路、日期、人数、交通单选、兴趣复选和备注。设置昵称长度、人数 1—30、备注 200 字；日期不早于北京时间今天。
3. 监听 submit 并 preventDefault；先用 reportValidity 检查原生约束，再用 trim 检查空白昵称和日期。
4. 用 FormData 读取数据；复选框用 getAll。生成日期前缀加随机后缀的 PL 编号，不把编号当密码或全球唯一凭据。
5. 从 localStorage 读取数组，JSON.parse 还原；push 新对象，再 JSON.stringify 后 setItem。只有写入成功才显示编号。损坏或禁用存储要提示错误。
6. 在编号查询中用 find 匹配。用 textContent 输出昵称和备注，避免把输入当 HTML 执行。增加清空查询按钮，它不能删除记录。
7. 删除按钮先 confirm，再 removeItem 只删除本项目登记键，不调用 localStorage.clear。刷新页面后核对记录数。分别测试取消和确认删除。

### 最小代码练习
```javascript
const key = "classroom.practice";
const records = JSON.parse(localStorage.getItem(key) || "[]");
records.push({ id:"PL-DEMO", nickname:"课堂小舟" });
localStorage.setItem(key, JSON.stringify(records));
const result = records.find(item => item.id === "PL-DEMO");
console.log(result.nickname); // 课堂小舟
// 演示结束只删除练习键，不清空别的网站数据
localStorage.removeItem(key);
```

> 小提示：保存文件后刷新；先查看控制台第一条错误，再检查路径和拼写。

**试一试：** 备注中输入 <img src=x onerror=alert(1)>，它应作为普通文本显示，不应弹窗。

**想一想：** 为什么同一编号在另一台设备上查不到？成功提示应该在保存之前还是之后显示？

## 五、任务验收
空表单/过去日期/不合法人数被拒绝；成功后有编号；刷新能查；无结果有说明；取消删除保留，确认删除清除。

同伴互查：逐项记录“通过/未通过/修改办法”，不能只写“页面能打开”。

## 六、任务小结
localStorage 是按来源隔离的字符串存储，不是数据库，也不是跨设备预约系统。

## 七、任务拓展
为单条记录增加删除按钮和确认框；若改成真实预约需考虑后端、认证、隐私与服务条款。

## 八、知识巩固
完成《知识巩固题》任务4组；先独立答，再对照教师答案解释原因。
