ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

3步搞定课程表制作,这份速查手册让开发效率翻倍

3步搞定课程表制作,这份速查手册让开发效率翻倍 3步搞定课程表制作,这份速查手册让开发效率翻倍 官方文档翻到第三页就头晕?别急,这就是我们做课程表制作项目时最头疼的问题。 与其对着冗长的 API 文档死磕,不如直接看这份实战速查手册。 下面这套方案,是从零搭建一个高可用课程表系统的完整路径。 项目目标与场景拆解 很多新手一上来就想做“完美系统”,结果卡在需求分析上。 课程表制作的核心逻辑其实很清晰:网格化布局 + 状态管理 + 数据持久化。 我们不做花里胡哨的拖拽特效,先实现最核心的功能:二维网格展示:行是时间,列是星期,单元格是课程。 课程添加/删除:点击单元格,弹窗填写课程名、老师、教室。 冲突检测:同一时间同一教室不能排两门课。 本地存储:刷新页面数据不丢失。这就是一个最小可行产品(MVP)。 目录结构规划 工欲善其事,必先利其器。混乱的文件结构是后期维护的噩梦。 推荐采用如下扁平化但逻辑清晰的结构: course-scheduler/ ├── index.html # 入口文件 ├── css/ │ └── style.css # 样式表,聚焦网格布局 ├── js/ │ ├── main.js # 入口脚本,初始化逻辑 │ ├── grid.js # 网格渲染核心逻辑 │ ├── storage.js # 本地存储封装 │ └── utils.js # 工具函数,如时间格式化 └── README.md # 项目说明为什么这样分?grid.js 独立出来,因为网格渲染是纯逻辑,不依赖 UI 框架,方便测试。 storage.js 封装 localStorage,如果未来切换到后端 API,只需改这一个文件。 utils.js 存放通用函数,保持主文件整洁。核心代码实现详解 这是本篇的精华,也是速查手册里最该收藏的部分。 1. 构建基础网格 不要手动写 35 个 td,那是反人类的行为。用 JS 动态生成。 // js/grid.js export function renderGrid(containerId, days, periods) {const container = document.getElementById(containerId);container.innerHTML = ''; // 清空旧网格// 创建表格const table = document.createElement('table');table.className = 'schedule-grid';// 表头:星期const headerRow = document.createElement('tr');const thCorner = document.createElement('th');thCorner.innerText = '时间';headerRow.appendChild(thCorner);days.forEach(day = {const th = document.createElement('th');th.innerText = day;headerRow.appendChild(th);});table.appendChild(headerRow);// 内容行:时间段periods.forEach((period, idx) = {const tr = document.createElement('tr');const thTime = document.createElement('th');thTime.innerText = period;tr.appendChild(thTime);days.forEach(day = {const td = document.createElement('td');// 关键:给每个单元格唯一 ID,格式为 day_periodtd.id = `${day}-${idx}`;td.dataset.day = day;td.dataset.period = idx;td.onclick = () = handleCellClick(td);tr.appendChild(td);});table.appendChild(tr);});container.appendChild(table); }逐行解析:container.innerHTML = '':防止重复渲染导致 DOM 节点叠加。 td.dataset.day:利用 HTML5 data-* 属性存储元数据,比解析 id 更优雅,后续取数据直接用 td.dataset.day,无需正则。 handleCellClick:事件委托的雏形,后续可优化为在 table 上监听,提升性能。2. 数据模型与冲突检测 课程数据不能只存字符串,要结构化。 // js/main.js // 数据结构定义 let scheduleData = {Mon: [{ period: 0, course: 高等数学, teacher: 张三, room: A-101 },// period: 1 为空,表示休息],Tue: [] };// 冲突检测核心逻辑 function checkConflict(day, period, roomId) {// 1. 检查同一教室在同一时间是否有课if (roomId) {for (let d in scheduleData) {if (d === day) continue; // 不同天不需要检查教室冲突const periodData = scheduleData[d].find(c = c.period === period);if (periodData periodData.room === roomId) {return { valid: false, message: `教室 ${roomId} 在 ${d} 第 ${period} 节已被占用` };}}}// 2. 检查同一学生在同一时间是否有课(如果需要多学生视图)// 此处简化,仅做教室冲突return { valid: true, message: 可以排课 }; }避坑指南: 很多初学者会在 checkConflict 里直接操作 DOM 去查其他格子的内容。大错特错。数据与视图分离是前端开发的铁律。永远以 scheduleData 为唯一数据源(Single Source of Truth)。 3. 渲染更新与存储同步 修改数据后,必须同步到视图和存储。 // js/main.js function updateCell(day, period, courseInfo) {// 1. 更新内存数据const dayData = scheduleData[day] || [];const index = dayData.findIndex(c = c.period === period);if (index -1) {dayData[index] = courseInfo; // 更新} else {dayData.push(courseInfo); // 新增}scheduleData[day] = dayData;// 2. 持久化存储saveToStorage();// 3. 更新视图renderCellUI(day, period, courseInfo); }function saveToStorage() {// 使用 JSON.stringify 防止对象被转成 [object Object]localStorage.setItem('courseSchedule', JSON.stringify(scheduleData)); }function loadFromStorage() {const stored = localStorage.getItem('courseSchedule');if (stored) {scheduleData = JSON.parse(stored);} }关键点: JSON.stringify 和 JSON.parse 是本地存储的标配。如果直接存对象,刷新后你会得到一堆 undefined。 运行与测试策略 代码写完只是开始,测试才能暴露问题。 1. 手动测试清单基础交互:点击空格子,弹窗出现;填写信息,点击保存,格子变色显示课程名。冲突测试:在周一第1节排“数学”,教室 A。 在周二第1节排“英语”,教室 A。 观察是否弹出提示“教室 A 在周二第1节已被占用”(注:这里逻辑需调整为检查同一天不同节或不同天同一教室,具体取决于业务需求,通常教室冲突是同一天不同节,或者同一时间不同教室。上面代码逻辑需根据实际业务调整,此处假设是检查同一时间不同教室占用同一房间,这在现实中较少见,通常是检查同一时间同一学生冲突。修正:教室冲突通常是同一时间同一教室只能有一门课,即同一天的同一 period 只能有一个 room。如果不同天,教室可以复用。上面的 checkConflict 逻辑中 if (d === day) continue 意味着跳过同一天,这是错误的。应该检查同一天同一 period 是否已有课程,且 room 相同。或者更简单的:同一天同一 period 只能排一门课,无论教室。如果业务允许同一时间不同教室排不同课,那就不需要教室冲突检测,只需要学生冲突检测。为了简化,我们假设同一天同一时间段只能有一门课。)修正后的冲突逻辑建议: function checkConflict(day, period) {// 同一天同一时间段是否已有课程const existingCourse = scheduleData[day]?.find(c = c.period === period);if (existingCourse) {return { valid: false, message: `该时间段已有课程:${existingCourse.course}` };}return { valid: true }; }刷新测试:保存后刷新页面,数据是否还在?删除测试:右键或点击删除按钮,数据是否从内存、DOM、LocalStorage 中彻底清除?2. 边界情况非法输入:课程名留空,应阻止保存。 超长文本:课程名超过 10 个字,CSS 是否设置了 text-overflow: ellipsis? 移动端适配:网格在小屏幕下是否溢出?建议添加 overflow-x: auto 到容器。优化扩展方向 基础版跑通后,如何让它更专业? 1. 性能优化虚拟滚动:如果课程表超过 100 行(比如大学一学期的完整课表),直接渲染所有 DOM 节点会卡顿。引入虚拟滚动库(如 vue-virtual-scroller 或原生实现)只渲染可视区域。 事件委托:将 onclick 绑定在 table 上,而不是每个 td 上。 table.addEventListener('click', (e) = {if (e.target.tagName === 'TD') {handleCellClick(e.target);} });2. 功能扩展导入/导出:提供“导出为图片”功能,使用 html2canvas 库将表格截图保存,方便学生分享。 主题切换:深色模式支持,通过 CSS 变量实现,一键切换。 提醒功能:集成 Notification API,在课前 10 分钟弹窗提醒(需用户授权)。3. 架构升级状态管理:如果逻辑变复杂,引入 Pinia (Vue) 或 Redux (React) 管理全局状态。 后端接入:将 localStorage 替换为 RESTful API。GET /api/schedule?user_id=123 POST /api/schedule DELETE /api/schedule/:id小结与互动 这份速查手册涵盖了从目录结构到核心逻辑的完整闭环。 课程表制作看似简单,实则涉及状态管理、数据持久化、UI 渲染三大核心能力。 很多开发者容易陷入两个极端:过度设计:一开始就搞微服务、消息队列,结果一个简单页面写了三天。 忽视规范:数据混在 DOM 里,改一个地方崩三处。记住: 清晰的目录结构 + 严格的数据/视图分离 + 完善的冲突检测 = 可维护的高质量代码。 官方源码仓库中关于 DOM 操作的规范(如 MDN Web Docs 中的 dataset 属性说明)值得反复研读,那是避免低级错误的基础。 你在项目里踩过这个坑吗? 比如数据刷新丢失、冲突检测漏判、或者移动端适配崩盘?评论区聊聊,咱们一起避坑。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进