简介这份资源是面向微信小程序初学者与前端练手者的原生课程表项目源码基于微信小程序原生框架开发帮助读者快速理解并实现一个可查看、管理每周课程安排的小工具适合学生、教师及想通过完整小项目巩固基础的前端学习者。压缩包共12个文件约8KB包含5个json配置、4个js逻辑、2个wxss样式和1个wxml结构文件分别承担项目配置、数据交互、页面样式与视图布局等职责目录结构清晰便于按模块阅读。资源围绕页面布局、数据绑定、事件交互、本地存储与生命周期等核心知识点展开读者可从中掌握条件与循环渲染、Flex布局、模态框调用及课程数据持久化等实现思路并借鉴响应式适配与用户体验优化的做法。目前已有811人学习下载适合作为小程序入门到进阶的实践参考。1. 原生小程序做课程表为什么“够用”比“炫技”更难每年开学季总有一批同学想给自己或班级做一个课程表小程序。打开开发者工具新建一个原生项目写几个view和text数据往data里一塞页面就出来了——看起来半小时能收工。但真正上线用一周问题全冒出来课程格子对不齐、单双周显示错乱、换手机后数据没了、教务系统导出的课表格式五花八门。原生小程序课程表这个方向难点从来不在“画格子”而在“数据模型怎么设计、周次怎么算、本地存储怎么兜底”。这篇文章面向的是想用微信小程序原生语法WXML WXSS JS不引入第三方 UI 框架落地一个能长期用的课程表工具的开发者。我会把课程表拆成数据层、渲染层、交互层三块讲清楚每一块的最小可用实现、参数怎么定、以及我踩过的那些坑。读完你应该能自己从零搭出一个支持多学期、单双周、本地持久化的课程表并且知道哪些地方偷懒会翻车。2. 课程表的数据模型先把“周次”这件事想明白课程表看起来是二维表格本质是“时间轴 课程实体”的映射。很多新手一上来就画 7 列 × 12 行的网格把课程名直接写进格子结果遇到单双周、连堂课、调休就全乱套。正确的做法是先定义清楚三个概念学期起始日期、教学周总数、每节课的时间段。这三个定下来后面所有渲染都是计算出来的。2.1 用“周次 星期 节次”三元组定位一节课一节课的唯一定位不是“第几行第几列”而是(周次, 星期几, 第几节)。周次是相对学期第一周的偏移量星期几是 1 到 7节次是 1 到 N。这样设计的好处是调休、放假、单双周都只是在周次维度上做过滤不影响课程本身的存储。我一般会把课程数据存成一个数组每个元素是一节课的实例// 课程数据模型一节具体的课 const courseItem { id: c_001, // 唯一标识用于编辑和删除 name: 高等数学, // 课程名 teacher: 某老师, // 教师虚构代称 location: 教三-201, // 上课地点 dayOfWeek: 1, // 星期几1周一7周日 startSection: 1, // 开始节次 endSection: 2, // 结束节次连堂课用这个表示 startWeek: 1, // 起始周 endWeek: 16, // 结束周 weekType: all, // all每周, odd单周, even双周 color: #4A90D9 // 课程颜色用于区分 };这段代码的关键在于startWeek/endWeek/weekType三个字段的组合。weekType为all时课程在startWeek到endWeek之间每周都出现为odd时只在奇数周出现为even时只在偶数周出现。连堂课用startSection和endSection的差值表示跨几节渲染时合并单元格。参数说明dayOfWeek用 1 到 7 而不是 0 到 6是为了和大多数人的直觉对齐避免在计算时反复 ±1。color建议存十六进制字符串渲染时直接绑定到 style不要存颜色名否则换主题时很麻烦。2.2 学期配置起始日期和总周数怎么定学期配置是整个课程表的“基准坐标系”。没有它周次就是空谈。我一般会做一个独立的配置对象存在本地存储里允许用户手动修改// 学期配置 const semesterConfig { name: 2024-2025学年第一学期, startDate: 2024-09-02, // 第一周的周一日期 totalWeeks: 18, // 教学周总数 sectionsPerDay: 12 // 每天最多几节课 };startDate必须是第一周的周一这样计算“今天是第几周”时直接用日期差除以 7 再取整。如果用户填的是其他星期几需要先归一到周一。totalWeeks决定周次选择器的上限sectionsPerDay决定网格的行数。计算当前周次的逻辑// 计算某个日期属于第几教学周 function getWeekNumber(dateStr, startDateStr) { const date new Date(dateStr); const start new Date(startDateStr); // 归一到当天零点避免时分秒干扰 date.setHours(0, 0, 0, 0); start.setHours(0, 0, 0, 0); const diffDays Math.floor((date - start) / (24 * 60 * 60 * 1000)); // 负数说明还没开学返回 0 表示未开始 if (diffDays 0) return 0; return Math.floor(diffDays / 7) 1; }这里有个容易翻车的点new Date(2024-09-02)在不同 JavaScript 引擎里可能被解析成 UTC 时间导致日期差算出来差一天。稳妥做法是用new Date(year, month - 1, day)手动构造或者统一用Date.parse加时区修正。我在真机上遇到过 iOS 和 Android 解析结果不一致的情况后来全部改成手动拆分字符串构造日期。2.3 单双周和调休过滤逻辑要放在渲染前有了课程数据和学期配置渲染某一周的课表时先过滤出该周有效的课程// 获取指定周次的课程列表 function getCoursesByWeek(allCourses, weekNum) { return allCourses.filter(course { // 周次范围过滤 if (weekNum course.startWeek || weekNum course.endWeek) return false; // 单双周过滤 if (course.weekType odd weekNum % 2 0) return false; if (course.weekType even weekNum % 2 1) return false; return true; }); }这个过滤函数是纯函数不依赖任何页面状态方便单独测试。调休的处理我一般不在数据层做而是在渲染层加一个“调休覆盖表”如果某一周被调休就把那周的课程按调休规则映射到另一天。这个表可以手动维护也可以从配置里读。提示单双周的判断基准是“教学周次”而不是“自然周次”。如果学期中间有放假导致教学周不连续需要额外维护一个“有效教学周列表”不能简单用weekNum % 2。3. 原生渲染用 grid 布局把课程格子对齐数据模型定好之后渲染就是水到渠成的事。原生小程序没有 CSS Grid 的完整支持但display: grid在大部分机型上已经可用。如果追求极致兼容可以用flex嵌套或者绝对定位。我一般用 grid代码量最少对齐也最稳。3.1 WXML 结构表头 网格 课程块页面结构分三部分顶部的周次切换栏、左侧的节次列、中间的课程网格。课程块用绝对定位叠在网格上这样连堂课的合并渲染更灵活。!-- 课程表主体 -- view classschedule-wrapper !-- 周次切换 -- view classweek-bar view classweek-btn bindtapprevWeek上一周/view view classweek-label第 {{currentWeek}} 周/view view classweek-btn bindtapnextWeek下一周/view /view !-- 网格区域 -- view classgrid-container !-- 表头星期 -- view classgrid-header view classcorner-cell/view view classday-cell wx:for{{days}} wx:keyindex{{item}}/view /view !-- 节次列 课程网格 -- view classgrid-body view classsection-col view classsection-cell wx:for{{sections}} wx:keyindex{{item}}/view /view view classcourse-area !-- 背景格子 -- view classbg-cell wx:for{{bgCells}} wx:keyindex/view !-- 课程块 -- view classcourse-block wx:for{{weekCourses}} wx:keyid styletop:{{item.top}}px;left:{{item.left}}px;height:{{item.height}}px;width:{{item.width}}px;background:{{item.color}} bindtaponCourseTap >/* 课程表样式 */ .schedule-wrapper { padding: 20rpx; background: #f5f6fa; } .grid-container { background: #fff; border-radius: 12rpx; overflow: hidden; } .grid-header { display: flex; height: 80rpx; line-height: 80rpx; background: #f0f2f5; } .corner-cell { width: 80rpx; flex-shrink: 0; } .day-cell { flex: 1; text-align: center; font-size: 26rpx; color: #333; } .grid-body { display: flex; position: relative; } .section-col { width: 80rpx; flex-shrink: 0; } .section-cell { height: 100rpx; line-height: 100rpx; text-align: center; font-size: 24rpx; color: #999; border-bottom: 1rpx solid #eee; } .course-area { flex: 1; position: relative; } .bg-cell { height: 100rpx; border-bottom: 1rpx solid #eee; border-right: 1rpx solid #eee; box-sizing: border-box; } .course-block { position: absolute; border-radius: 8rpx; padding: 8rpx; box-sizing: border-box; overflow: hidden; color: #fff; font-size: 22rpx; }这里section-cell和bg-cell的高度都是100rpx课程块的top和height就按这个基数计算。course-area的宽度是屏幕宽度减去80rpx的节次列再除以 7 就是每列的宽度。这些计算在 JS 里用wx.getSystemInfoSync()拿到屏幕宽度后完成。3.3 坐标计算把课程数据转成像素位置渲染前需要把课程数据转成绝对定位的像素值。这个函数是整个渲染层的核心// 将课程数据转换为渲染所需的坐标 function calcCoursePosition(course, cellWidth, cellHeight) { // 列索引星期几减 1 const colIndex course.dayOfWeek - 1; // 行索引开始节次减 1 const rowIndex course.startSection - 1; // 跨行数 const rowSpan course.endSection - course.startSection 1; return { left: colIndex * cellWidth, top: rowIndex * cellHeight, width: cellWidth - 4, // 留 4px 间隙避免课程块贴边 height: rowSpan * cellHeight - 4 }; }cellWidth和cellHeight从页面初始化时计算一次存到data里。width和height减去 4 是为了让课程块之间有空隙视觉上更清晰。这个 4 是经验值可以根据实际效果调整。注意wx.getSystemInfoSync()返回的windowWidth单位是 px而 WXSS 里用的是 rpx。计算时要么全部用 px要么把 rpx 转成 px。我一般统一用 px在 WXSS 里也用 px 写死避免换算混乱。4. 本地存储与导入导出数据不能只活在内存里课程表小程序如果每次打开都要重新录入用户用两次就卸载了。本地存储是底线导入导出是加分项。原生小程序的wx.setStorageSync同步接口足够用但要注意存储上限和序列化问题。4.1 用 wx.setStorageSync 做持久化存储结构我一般分两个 key一个存学期配置一个存课程列表。分开存的好处是修改学期配置时不用重写整个课程列表。// 存储 key 常量 const STORAGE_KEYS { SEMESTER: semester_config, COURSES: course_list }; // 保存学期配置 function saveSemesterConfig(config) { try { wx.setStorageSync(STORAGE_KEYS.SEMESTER, config); } catch (e) { console.error(保存学期配置失败, e); wx.showToast({ title: 保存失败, icon: none }); } } // 读取课程列表 function loadCourses() { try { const courses wx.getStorageSync(STORAGE_KEYS.COURSES); return Array.isArray(courses) ? courses : []; } catch (e) { console.error(读取课程失败, e); return []; } }wx.setStorageSync单个 key 的上限是 1MB课程列表通常远小于这个值。但要注意如果课程数据里包含函数或循环引用序列化会失败。所以课程对象必须是纯数据不能存方法。4.2 导入导出用 JSON 字符串做交换格式导入导出我一般用 JSON 字符串通过剪贴板或者文件分享。导出时把学期配置和课程列表打包成一个对象// 导出全部数据 function exportData() { const data { version: 1, semester: wx.getStorageSync(STORAGE_KEYS.SEMESTER), courses: wx.getStorageSync(STORAGE_KEYS.COURSES) }; return JSON.stringify(data); } // 导入数据 function importData(jsonStr) { try { const data JSON.parse(jsonStr); // 版本校验方便后续升级 if (!data.version || !data.courses) { throw new Error(数据格式不正确); } wx.setStorageSync(STORAGE_KEYS.SEMESTER, data.semester); wx.setStorageSync(STORAGE_KEYS.COURSES, data.courses); return true; } catch (e) { console.error(导入失败, e); return false; } }version字段是为了后续数据格式升级时做兼容。导入时先校验version和courses是否存在避免把错误数据写进存储。导出后可以调用wx.setClipboardData复制到剪贴板用户粘贴到备忘录或者发给同学。4.3 从教务系统导入解析 HTML 表格的常见做法很多学校教务系统导出的课表是 HTML 表格复制出来是一堆table标签。我一般让用户把 HTML 粘贴到一个输入框然后用正则提取课程信息。这个方案不完美但比手动录入快得多。// 从 HTML 表格文本中提取课程简化版 function parseHtmlSchedule(html) { const courses []; // 匹配 td 中的课程文本实际规则因教务系统而异 const tdRegex /td[^]*([\s\S]*?)\/td/g; let match; let cellIndex 0; while ((match tdRegex.exec(html)) ! null) { const text match[1].replace(/[^]/g, ).trim(); if (text text ! nbsp;) { // 根据 cellIndex 推算星期和节次这里需要按实际表格结构调整 const dayOfWeek (cellIndex % 7) 1; const section Math.floor(cellIndex / 7) 1; courses.push({ id: imported_ cellIndex, name: text, dayOfWeek: dayOfWeek, startSection: section, endSection: section, startWeek: 1, endWeek: 16, weekType: all, location: , teacher: , color: #4A90D9 }); } cellIndex; } return courses; }这个解析函数是简化版实际教务系统的表格结构千差万别需要根据具体 HTML 调整cellIndex到星期和节次的映射规则。我一般会先让用户粘贴一小段 HTML在控制台里打印出td的数量和顺序再写映射逻辑。导入后一定要让用户手动核对一遍不能直接信任解析结果。提示解析 HTML 时不要用innerHTML直接渲染避免 XSS 风险。所有提取的文本都要经过转义再显示。5. 避坑与排查课程表上线后最容易翻车的 5 个地方课程表看着简单但真机上的表现和开发者工具里差别很大。下面这 5 个问题我几乎每次做课程表都会遇到按“现象 → 原因 → 解决”整理出来方便你对照排查。5.1 课程块错位iOS 和 Android 的日期解析不一致现象在开发者工具里课程块位置正确真机上 iOS 正常、Android 偏移一天或者反过来。原因new Date(2024-09-02)这种字符串构造在不同引擎里时区处理不同。iOS 倾向于按 UTC 解析Android 按本地时区解析导致日期差计算出现 ±1 天误差。解决统一用手动构造日期避免字符串解析。把new Date(dateStr)改成拆分字符串后用new Date(year, month - 1, day)构造。所有涉及日期计算的地方都走同一个工具函数不要散落在各处。5.2 单双周显示反了周次基准没对齐现象用户设置的是单周课程结果在双周也显示出来了或者第一周该显示的课没显示。原因weekType的判断用了自然周次而不是教学周次或者学期起始日期不是周一导致第一周的周次计算偏移。解决确保startDate是周一getWeekNumber返回的是教学周次。在设置页面加一个“当前是第几周”的预览让用户自己核对。如果学期中间有放假维护一个“有效教学周”数组单双周判断基于这个数组的索引而不是周次数字本身。5.3 存储写入失败课程数据里混入了非序列化对象现象保存课程时wx.setStorageSync报错或者保存成功但读取出来是空。原因课程对象里包含了Date对象、函数、undefined或者循环引用。wx.setStorageSync内部用 JSON 序列化这些类型都会导致失败或丢失。解决保存前用JSON.parse(JSON.stringify(courses))做一次深拷贝过滤掉非数据字段。所有课程对象只保留字符串、数字、布尔值。如果确实需要存日期存时间戳数字读取时再转换。5.4 课程块点击事件穿透绝对定位层级问题现象点击课程块时触发的是背景格子的点击事件或者点击没有反应。原因课程块和背景格子都在同一个course-area里课程块虽然视觉上在上层但如果没有设置z-index事件可能被背景格子捕获。解决给.course-block加z-index: 10给.bg-cell加z-index: 1。同时确保课程块的bindtap绑在课程块本身而不是父容器上。如果课程块内部有多个子元素用catchtap阻止冒泡。5.5 周次切换卡顿每次切换都重新计算全部课程现象课程数量多了之后切换周次明显卡顿尤其在低端安卓机上。原因每次切换周次都遍历全部课程做过滤和坐标计算没有缓存。解决把课程按周次预计算好存成一个以周次为 key 的映射表。切换周次时直接取对应周的数据不需要重新过滤。预计算在课程数据变更时做一次之后切换都是 O(1) 读取。// 预计算每周的课程 function buildWeekMap(courses, totalWeeks) { const weekMap {}; for (let w 1; w totalWeeks; w) { weekMap[w] getCoursesByWeek(courses, w); } return weekMap; }这个映射表在课程增删改时重建一次切换周次时直接weekMap[currentWeek]取数据。代价是内存占用增加但课程表数据量小完全可以接受。6. 进阶技巧让课程表从“能用”变成“好用”基础功能跑通之后有几个小技巧能明显提升使用体验。这些不是必须做的但做了之后用户留存会好很多。第一个是“今日课程”提醒。在首页顶部加一个卡片显示今天剩余的课程按时间排序。实现上就是取当前周次、当前星期几的课程过滤掉已经结束的节次。这个卡片不需要用户操作打开小程序就能看到比翻周次方便得多。第二个是课程颜色自动分配。手动给每门课选颜色很麻烦我一般用课程名的哈希值映射到一个预设色板。这样同一门课每次导入颜色一致不同课程颜色区分度也够。// 根据课程名生成稳定颜色 const COLOR_PALETTE [#4A90D9, #E67E22, #27AE60, #8E44AD, #E74C3C, #16A085]; function getCourseColor(courseName) { let hash 0; for (let i 0; i courseName.length; i) { hash courseName.charCodeAt(i) ((hash 5) - hash); } const index Math.abs(hash) % COLOR_PALETTE.length; return COLOR_PALETTE[index]; }第三个是“周次快捷跳转”。在周次标签上长按弹出一个选择器直接跳到指定周。这个交互比反复点“下一周”快得多尤其适合需要查看期末周课程安排的场景。第四个是数据备份提醒。本地存储有被清理的风险我一般在小程序里加一个“导出备份”的入口并在检测到课程数量超过 10 条时每隔一段时间提醒用户导出一次。提醒频率不要太高一个月一次足够。最后一个技巧是关于性能的课程块的渲染用wx:for时wx:key一定要用课程的唯一id不要用index。用index在课程增删时会导致错误的复用出现课程块闪烁或者内容错位。这个坑我在早期项目里踩过排查了半天才发现是wx:key的问题。做课程表这几年我最大的教训是不要一开始就追求功能大而全。先把数据模型和本地存储做扎实保证“录入的课不会丢、显示的课不会错”再去加导入导出、提醒、主题这些锦上添花的东西。很多课程表小程序死掉不是因为功能少而是因为某次更新把用户数据搞丢了。数据安全这条底线守住了用户才愿意长期用。希望帮到你。本文还有配套的精品资源点击获取