资讯动态

微信小程序原生课程表开发:周视图与本地存储实战

发布时间:2026/10/14 0:32:40 来源:尧图企业网站定制
简介这份资源是面向微信小程序初学者与前端练手者的原生课程表项目源码基于微信小程序原生框架实现可帮助读者快速理解并搭建一个查看、管理每周课程安排的小工具适合学生、教师及想熟悉小程序开发流程的开发者参考。压缩包为rar格式共12个文件约8KB包含5个json配置文件、4个js逻辑脚本、2个wxss样式表与1个wxml页面结构文件分别承担项目配置、数据交互、界面样式与视图布局等职责。目前已有811人学习下载。项目覆盖开发环境搭建、WXML条件与循环渲染、WXSS的Flex布局、data数据绑定与bindtap事件交互、wx.showModal弹窗及本地存储API、页面生命周期方法、响应式适配与调试发布等知识点读者可据此掌握课程表从页面结构到数据持久化的完整实现思路并借鉴其状态管理与用户体验优化做法。1. 微信小程序原生课程表从一张空课表到能用的周视图每学期开学第一周总有人在群里问「今天第几周、上什么课」。市面上的课程表 App 不少但要么广告多要么要登录要么导入流程绕。用微信小程序原生自己写一个课程表最大的好处是不用装 App微信里点开就用数据存本地没有账号体系也没有后端成本。它适合两类人一类是想练手原生小程序、又不想做玩具 Demo 的前端另一类是真的需要一个干净课表、愿意花一个周末把它做出来的学生或老师。这篇不讲「小程序是什么」直接按能跑通的顺序走先把周视图和数据结构定下来再写渲染和手势然后处理本地存储、导入导出、周次切换最后把几个必踩的坑摊开讲。全程只用原生 WXML/WXSS/JS不引第三方 UI 库保证你复制过去就能改。2. 先把数据模型和页面骨架定死别急着写样式2.1 课程表的数据结构为什么不能拍脑袋定很多人一上来就写lessons: [{name, room, teacher}]写到第三周发现要支持单双周、要支持连堂、要支持调课结构就崩了。课程表的核心不是「一节课」而是「一个时间格子上的一段占用」。我一般会拆成三层节次配置、课程条目、周次规则。节次配置决定一天有几节课、每节几点开始这是渲染左侧时间轴的依据。课程条目是用户录入的最小单位包含课程名、地点、老师、颜色。周次规则决定这条课在第几周到第几周、单周还是双周出现。把这三层分开后面加「临时调课」只需要在渲染层做覆盖不动原始数据。// config/schedule.js // 节次配置一天 12 节每节开始时间用于左侧时间轴 const SECTIONS [ { index: 1, start: 08:00, end: 08:45 }, { index: 2, start: 08:55, end: 09:40 }, { index: 3, start: 10:00, end: 10:45 }, { index: 4, start: 10:55, end: 11:40 }, { index: 5, start: 14:00, end: 14:45 }, { index: 6, start: 14:55, end: 15:40 }, { index: 7, start: 16:00, end: 16:45 }, { index: 8, start: 16:55, end: 17:40 }, { index: 9, start: 19:00, end: 19:45 }, { index: 10, start: 19:55, end: 20:40 }, { index: 11, start: 20:50, end: 21:35 }, { index: 12, start: 21:45, end: 22:30 } ]; // 课程条目一条课 一个格子上的占用 // weeks 用字符串描述如 1-16 表示 1 到 16 周1-16|单 表示单周 const SAMPLE_LESSON { id: l_001, name: 高等数学, room: 教三 201, teacher: 某老师, day: 1, // 1周一 ... 7周日 startSection: 1, // 从第几节开始 endSection: 2, // 到第几节结束连堂 weeks: 1-16, color: #5B8FF9 }; module.exports { SECTIONS, SAMPLE_LESSON };这段代码里day用 1 到 7 而不是 0 到 6是为了和「周一」这种人类说法对齐少一次心智转换。startSection和endSection分开是为了支持连堂——两节课占一个格子渲染时高度翻倍。weeks用字符串而不是数组是因为导入导出时字符串更好读解析成本也低。提示weeks的格式一定要在项目一开始就定死后面所有解析、判断、导入导出都依赖它。我见过中途从数组改成字符串、结果导入功能全废的情况。2.2 页面骨架周视图的 DOM 怎么摆原生小程序没有虚拟 DOM 的复杂概念但setData的性能开销是实打实的。周视图的结构建议是外层一个横向滚动容器里面左侧固定时间轴、右侧七天网格。网格用绝对定位放课程块而不是用 flex 一行行排——因为课程块要跨节次flex 很难表达「从第 1 节到第 2 节」这种纵向跨度。!-- pages/schedule/schedule.wxml -- view classschedule !-- 顶部周次切换 -- view classweek-bar view classweek-btn bindtapprevWeek上一周/view view classweek-title第 {{currentWeek}} 周/view view classweek-btn bindtapnextWeek下一周/view /view !-- 主体左侧时间轴 右侧七天网格 -- scroll-view classgrid-wrap scroll-x view classgrid stylewidth: {{gridWidth}}px; !-- 时间轴 -- view classtime-axis view wx:for{{sections}} wx:keyindex classtime-cell text{{item.index}}/text text classtime-text{{item.start}}/text /view /view !-- 七天列 -- view classdays view wx:for{{days}} wx:keyday classday-col view wx:for{{item.lessons}} wx:keyid classlesson-block styletop: {{item.top}}px; height: {{item.height}}px; background: {{item.color}}; bindtaponLessonTap >// utils/layout.js const SECTION_HEIGHT 60; // 每节课块高度 const SECTION_GAP 4; // 节间距 // 把一条课程转成带 top/height 的渲染对象 function toRenderLesson(lesson) { const top (lesson.startSection - 1) * (SECTION_HEIGHT SECTION_GAP); const span lesson.endSection - lesson.startSection 1; const height span * SECTION_HEIGHT (span - 1) * SECTION_GAP; return { ...lesson, top, height }; } // 判断某条课在第 week 周是否出现 function isLessonInWeek(lesson, week) { const [range, parity] lesson.weeks.split(|); const [start, end] range.split(-).map(Number); if (week start || week end) return false; if (parity 单 week % 2 0) return false; if (parity 双 week % 2 1) return false; return true; } module.exports { toRenderLesson, isLessonInWeek, SECTION_HEIGHT, SECTION_GAP };isLessonInWeek是后面周次切换的核心。注意weeks里可能没有|所以split(|)后parity是undefined判断时用 单这种严格比较避免undefined被当成真值。这个函数每次切周都会调用数据量大时可以先按周缓存但一般课表几十条数据直接算就够。3. 手势、周次与本地存储让课表真正能用起来3.1 左右滑动切周用 touch 事件而不是 scroll-view顶部按钮切周能用但体验一般。课程表用户习惯左右滑。原生小程序里可以用bindtouchstart和bindtouchend算横向位移超过阈值就切周。不要用scroll-view的scroll事件去判断因为横向滚动容器本身要用来放七天网格两者会打架。// pages/schedule/schedule.js Page({ data: { currentWeek: 1, days: [], // 七天每天一个 lessons 数组 sections: [], gridWidth: 0 }, onLoad() { this.initGrid(); this.renderWeek(1); }, // 初始化网格宽度时间轴 60 七天 * 100 initGrid() { const { SECTIONS } require(../../config/schedule); this.setData({ sections: SECTIONS, gridWidth: 60 7 * 100 }); }, // 渲染某一周过滤 布局 renderWeek(week) { const { toRenderLesson, isLessonInWeek } require(../../utils/layout); const all wx.getStorageSync(lessons) || []; const days []; for (let d 1; d 7; d) { const lessons all .filter(l l.day d isLessonInWeek(l, week)) .map(toRenderLesson); days.push({ day: d, lessons }); } this.setData({ currentWeek: week, days }); }, prevWeek() { if (this.data.currentWeek 1) { this.renderWeek(this.data.currentWeek - 1); } }, nextWeek() { if (this.data.currentWeek 20) { this.renderWeek(this.data.currentWeek 1); } }, // 手势切周 onTouchStart(e) { this.startX e.touches[0].clientX; }, onTouchEnd(e) { const dx e.changedTouches[0].clientX - this.startX; if (dx 60) this.prevWeek(); if (dx -60) this.nextWeek(); } });renderWeek每次都从 storage 读全量数据再过滤看起来笨但课表数据量小这样写最不容易出 bug。如果以后要支持几百条课可以把全量数据缓存在this.allLessonsstorage 只在增删改时写。阈值 60px 是试出来的太小容易误触太大滑不动。这个值跟屏幕宽度有关严谨点可以用screenWidth * 0.15。3.2 本地存储为什么用 Storage 而不是云开发课程表是纯个人数据没有社交、没有多端同步刚需用wx.setStorageSync就够了。云开发要开通环境、要处理权限、要防并发写对一个小课表来说是过度设计。Storage 的坑在于单个 key 有大小限制且是同步 API写大数组会卡。所以存的时候只存原始课程数组不存渲染后的对象。// utils/storage.js const KEY lessons; function getLessons() { return wx.getStorageSync(KEY) || []; } function saveLessons(lessons) { // 只存原始字段去掉 top/height 这些渲染态 const raw lessons.map(l ({ id: l.id, name: l.name, room: l.room, teacher: l.teacher, day: l.day, startSection: l.startSection, endSection: l.endSection, weeks: l.weeks, color: l.color })); wx.setStorageSync(KEY, raw); } function addLesson(lesson) { const list getLessons(); list.push(lesson); saveLessons(list); } module.exports { getLessons, saveLessons, addLesson };saveLessons里做了一次字段裁剪这是血泪经验如果直接把渲染对象存进去下次读出来会带着旧的top/height布局一变就错位。存储层只认原始数据渲染层每次重新算职责清晰。3.3 导入导出用 JSON 文本走剪贴板课程表最烦的是每学期重新录。做一个导出成 JSON 文本、复制到剪贴板、再导入的功能换手机或重装小程序时能救命。原生小程序用wx.setClipboardData和wx.getClipboardData不需要文件系统权限。// pages/schedule/schedule.js 里追加 exportData() { const { getLessons } require(../../utils/storage); const text JSON.stringify(getLessons()); wx.setClipboardData({ data: text, success: () wx.showToast({ title: 已复制到剪贴板 }) }); }, importData() { const { saveLessons } require(../../utils/storage); wx.getClipboardData({ success: (res) { try { const list JSON.parse(res.data); if (!Array.isArray(list)) throw new Error(不是数组); // 简单校验必填字段 const valid list.every(l l.name l.day l.startSection); if (!valid) throw new Error(字段缺失); saveLessons(list); this.renderWeek(this.data.currentWeek); wx.showToast({ title: 导入成功 }); } catch (err) { wx.showToast({ title: 格式不对, icon: none }); } } }); }导入一定要做校验否则用户粘一段乱七八糟的文本进来JSON.parse抛错还是小事存进去脏数据导致整个课表渲染不出来才是大问题。校验至少看三样是不是数组、每条有没有name、day和startSection是不是数字。宁可提示「格式不对」也不要静默失败。4. 避坑与排查课程表最容易翻车的五个地方4.1 课程块重叠连堂和单双周同时出现时现象同一时间段出现两个课程块叠在一起看不清。原因通常是数据里存在两条day和节次都相同、但周次规则不同的课比如一条单周一条双周渲染时没做互斥判断。解决在renderWeek里对同一天同一节次的课做一次冲突检测如果两条课在当前周都出现按id排序后只保留一条或者给第二条加偏移量。更稳妥的做法是在录入时就校验冲突不让用户存进去。4.2 setData 数据量过大导致卡顿现象切周时页面明显卡一下低端机更明显。原因days里塞了完整课程对象包含teacher、color等渲染用不到的字段setData序列化开销大。解决toRenderLesson里只保留 WXML 用到的字段name、room、top、height、color、id其余在渲染前丢掉。另外days结构不要嵌套太深七天各一个数组已经够用。4.3 周次解析把「1-16|单」当成「1-16」现象单周课在双周也显示。原因split(|)后没判断parity是否存在或者用了if (parity)这种宽松判断空字符串被当成假值但undefined也走同一分支。解决严格用parity 单和parity 双其余情况一律视为全周。这个坑我在两个项目里都见过属于典型玄学 bug查半天。4.4 真机上 scroll-view 横向滚动不跟手现象开发者工具里滑动正常真机上七天网格滑不动。原因scroll-view要横向滚动必须同时设置scroll-x和子元素宽度大于容器宽度且子元素不能是display: inline。解决grid用display: flex或inline-block并显式设widthscroll-view加white-space: nowrap。另外真机上scroll-view的手势和页面纵向滚动会冲突给scroll-view加enable-flex属性有时能缓解。4.5 存储写入时机不对导致数据丢失现象用户添加课程后直接杀进程再打开发现没存上。原因用了异步wx.setStorage但没等回调或者把保存逻辑放在onUnload里而onUnload在杀进程时不一定触发。解决增删改后立即同步写wx.setStorageSync不要攒着。同步写虽然会阻塞几十毫秒但课表数据量小完全可接受。宁可卡一下不要丢数据。5. 进阶技巧用「当前周」自动定位和颜色去重课表做到能用之后最值得加的一个功能是「打开自动定位到当前周」。很多课表 App 都要手动翻其实只要在onLoad里算一下今天距离开学第几周就行。这里的关键是让用户设置一个「开学日期」存到 storage然后每次打开算差值。// utils/week.js // 计算当前是第几周开学日期所在周为第 1 周 function getCurrentWeek(startDateStr) { const start new Date(startDateStr); const now new Date(); // 把两个日期都归到周一避免周中开学导致偏差 const startMonday getMonday(start); const nowMonday getMonday(now); const diffDays Math.floor((nowMonday - startMonday) / 86400000); return Math.floor(diffDays / 7) 1; } function getMonday(date) { const d new Date(date); const day d.getDay() || 7; // 周日算 7 d.setDate(d.getDate() - day 1); d.setHours(0, 0, 0, 0); return d; } module.exports { getCurrentWeek };getMonday里d.getDay() || 7是个小技巧getDay()周日返回 00 || 7得到 7这样周一永远是 1。归到周一再算差值能避免「周三开学、周五打开算成第 1 周还是第 0 周」这种边界问题。算出来的周数如果小于 1 或大于 20就回退到第 1 周防止开学日期设错导致白屏。另一个值得做的是颜色去重。用户手动给每门课选颜色很烦可以按课程名做哈希自动分配一个色板里的颜色。色板不要用纯红纯绿对比度太低选 8 到 10 个中等饱和度的颜色保证白字能看清。参数建议值说明节次高度60px低于 50px 课程名会换行节间距4px太大浪费纵向空间切周阈值60px约屏宽 15%色板数量8 到 10太少容易重复太多记不住周次上限20一般学期 16 到 18 周最后说个我自己的习惯每次改完渲染逻辑一定在真机上用「第 1 周、第 8 周、第 16 周」三个点各切一遍因为单双周和连堂的 bug 往往只在特定周次暴露。开发者工具太干净真机上的滚动和触摸才是照妖镜。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑