拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Vue 3实战:从零构建课程表日历组件

1. 拿到“第三次作业”那天晚上事情发生在上周四的《Web前端开发》课上。老师在快下课时丢出一句话“把第三次作业做一下下次课之前交。”底下瞬间安静了两秒然后就是一阵翻书、开电脑、交头接耳的声音。我旁边室友扭头问我“第三次作业是啥来着”我打开教学平台一看公告里挂着几行字——第三次作业课程表日历组件。说实话看到“第三次作业”这个标题时我第一反应是这题目怎么这么随意连个具体的功能说明都没有但仔细翻完作业要求后我才意识到这恰恰是老师故意的。他要的不是你照着截图做一个一模一样的页面而是让你在需求不清、边界模糊的情况下自己定义问题、拆分功能、设计交互最后实现一个能用的日历课程表。这种自由度过高的题目反而比那种详细到像素级的作业难得多因为没人为你兜底所有选择和取舍都要自己拍板。这篇文章我就把这次“第三次作业”从接到题目到交作业的全过程拆开讲讲包括我怎么理解需求、怎么选技术方案、怎么设计数据结构、怎么一步步把日程渲染出来、踩了哪些坑。如果你也在做类似的日历组件、排课表、日程管理类的前端项目这份经验应该能帮你省下不少试错时间。先说下最终成果我用 Vue 3 Vite Tailwind CSS 做了一个课程表日历组件支持周视图和月视图切换课程卡片按颜色区分点击日期可以查看当天日程底部还有本周课时统计。整个项目从零到交作业用了大概三天晚上的时间代码量在 900 行左右。这个规模对于一次课程作业来说刚好既有一定复杂度又不至于把自己耗死。2. 真正动手前的思考读懂“第三次作业”背后的意图2.1 作业需求里的三个关键点翻来覆去看了好几遍作业要求我发现真正有效的信息其实就三点需要做一个日历视图能展示每天对应的课程信息包括课程名、节次、教室。支持按周查看和按月查看两种模式切换时数据要对应刷新。界面需要合理美观不能只是数据结构演示必须接近真实可用的产品。这三点分别对应三个能力维度数据建模、交互设计、UI 呈现。老师没有说要你用什么框架没有限定要不要后端也没有规定课程数据从哪来。这意味着我可以在“纯前端写死数据”和“简单后端模拟接口”之间自由选择。最终我选了纯前端方案原因后面会细说。另外作业还强调了一句话要有响应式布局支持手机和电脑访问。这一点容易忽略但恰恰是最能拉开作业档次的地方。很多同学用桌面端布局糊弄过去到手机上一看全乱。我这次把响应式这件事放在了前面设计而不是最后才想起来去适配。2.2 为什么选 Vue 3 而不是 React 或原生 JS技术选型这件事说难不难说简单也不简单。我最终选了 Vue 3主要有几个考量。第一Vue 的单文件组件结构对这类“日历 课程表”项目非常友好。日历组件天然需要拆分出日期格子、课程卡片、视图切换按钮等子组件用组件树来组织思路特别顺。第二Vue 的响应式系统可以省掉大量手动操作 DOM 的代码。比如切换月份后日期格子和课程数据的联动更新在 Vue 里只需要修改一个响应式变量即可剩下的交给框架处理。第三也是比较现实的一点Vue 3 的中文资料和生态更成熟。遇到问题搜一下基本都能找到答案这对赶作业的学生来说太重要了。2.3 数据从哪里来先定义假数据再考虑接口作业没有要求接后端所以我采用了“前端本地定义课程数据”的方案。这个方案的好处是开发速度快不受网络环境影响演示时也不会因为服务器挂了而翻车。但这不意味着数据结构可以随便写。我特意把课程数据设计成后端接口返回的格式这样以后想接真实数据时不需要大规模重构。每门课程包含这些字段{ id: 1, name: Web前端开发, teacher: 李老师, location: 教学楼A-301, dayOfWeek: 1, // 1代表周一7代表周日 startSection: 3, // 第几节开始 endSection: 4, // 第几节结束 weeks: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], // 上课周次 color: #4F46E5 // 卡片颜色标识 }这里有几个设计上的小心思。用 dayOfWeek 而非具体的日期字符串是为了配合日历的周视图和月视图切换。用 startSection 和 endSection 表示节次区间而不是一个简单的“上午/下午”标签是为了在渲染课程卡片时能精确计算高度和位置。weeks 数组用于判断某一天是否真的有课搭配当前显示的周次可以做过滤。我把这些数据放在一个独立的data/courses.js文件里和组件代码分离。这样修改课程信息时不需要碰 UI 代码符合基本的关注点分离原则。2.4 不用后端但把接口结构提前想清楚虽然没有后端我仍然在项目中预留了一个api.js里面写了一个模拟接口函数export async function fetchCourses() { // 模拟网络请求延迟 return new Promise((resolve) { setTimeout(() { resolve(courseData); }, 500); }); }组件里通过onMounted调用这个接口来加载数据。这个设计看起来多此一举但实际上帮了我大忙——写报告时能说明“项目已预留接口层后续可无缝对接真实后端”也保证组件生命周期内的数据流是规范的单向数据流。3. 日历组件的核心设计视图切换与数据联动3.1 周视图 vs 月视图数据结构怎么配合日历课程表的核心难点不在渲染而在视图切换时数据的精确对应。周视图需要展示“这一周里每天有哪些课”月视图需要展示“这个月里每天有哪些课”。两者本质上都是“日期 → 课程数组”的映射只是聚合的粒度不同。我的做法是在主组件里维护一个currentDate响应式对象表示当前选中的日期。周视图基于它计算出一周七天的日期数组月视图基于它计算出整月的日期网格包括补位的前后月份日期。无论哪种视图最后都统一到一个核心方法function getCoursesForDate(date) { const dayOfWeek date.getDay() 0 ? 7 : date.getDay(); const weekOfTerm getWeekOfTerm(date); return allCourses.filter(course course.dayOfWeek dayOfWeek course.weeks.includes(weekOfTerm) ); }getCoursesForDate 接收一个 Date 对象返回当天要显示的课程数组。通过这个函数的过滤周视图和月视图共用同一份课程数据只是渲染容器不同这就避免了“两套视图两套数据逻辑”的维护噩梦。3.2 周次计算这个学期到底有多少周这里有个隐蔽的坑课程表里的“第几周”怎么算不同学校的校历不同有的从9月1日开始有的从开学第一天开始。我需要一个函数能根据当前日期推算出它属于这个学期的第几周。为了简化我在项目里定义了一个termStartDate并硬编码为“2025年秋季学期第一周的周一”。计算逻辑如下function getWeekOfTerm(date) { const start new Date(termStartDate); const diff date.getTime() - start.getTime(); const days Math.floor(diff / (1000 * 60 * 60 * 24)); return Math.floor(days / 7) 1; }这段代码不复杂但精度问题值得注意如果把时间精确到毫秒跨时区或夏令时可能会出现日期偏移。我用setHours(0,0,0,0)把两个日期都归零后再做差值避免边界情况。实际用下来这种方法在本地环境下是靠谱的。3.3 月视图的日期网格与补位逻辑月视图的经典问题是某个月的1号不一定是周一那前面空出的格子怎么处理我的方案是往前补上上个月的月末日期往后补上下个月的月初日期让网格始终保持 6 行 × 7 列共 42 个格子。这样布局整齐CSS Grid 可以轻松搞定。function buildMonthGrid(year, month) { const firstDay new Date(year, month, 1); const firstDayOfWeek firstDay.getDay() 0 ? 7 : firstDay.getDay(); const daysInMonth new Date(year, month 1, 0).getDate(); const prevMonthDays new Date(year, month, 0).getDate(); const grid []; // 填充上月补位 for (let i firstDayOfWeek - 1; i 0; i--) { grid.push({ date: new Date(year, month - 1, prevMonthDays - i 1), isCurrentMonth: false }); } // 当月日期 for (let i 1; i daysInMonth; i) { grid.push({ date: new Date(year, month, i), isCurrentMonth: true }); } // 填充下月补位 const remaining 42 - grid.length; for (let i 1; i remaining; i) { grid.push({ date: new Date(year, month 1, i), isCurrentMonth: false }); } return grid; }这个函数每次返回 42 个日期对象带有是否属于当前月份的标记。渲染时非本月的日期用浅灰色文字表示点击可以切换到对应的月份。这里用到的取月份天数技巧——new Date(year, month 1, 0).getDate()——是 JavaScript 日期处理里非常经典的一招建议记住。3.4 周视图的“今天高亮”与滑动定位周视图我设计成横向排布的七个日期列第一列是时间轴第1节到第12节后面七列对应周一到周日。每个课程卡片根据startSection和endSection计算它的纵向起始位置和高度。这里的关键是纵坐标换算。假设一个课程从第 3 节上到第 4 节每节课的高度是 60px那么卡片应该从 2 × 60 120px 开始高度为 2 × 60 120px还要减去上下间距。用 CSS 的绝对定位来实现div classcourse-card :style{ top: (course.startSection - 1) * 60 px, height: (course.endSection - course.startSection 1) * 60 - 8 px }视觉上我做了一个小设计将今天的日期列背景设为浅蓝色并设置了一个“回到今天”的按钮。点击后自动把 currentDate 设回当前日期同时用scrollIntoView把今天的列滚动到可视区域中间。这个交互在手机上特别有用不然切换日期后容易迷路。4. 实操过程中踩到的真实坑4.1 Vue 响应式丢失问题这是我在开发中最先遇到的问题。刚开始我把课程列表放在一个普通 JavaScript 数组里然后在onMounted中通过接口赋值。结果界面上始终不更新。后来排查发现Vue 3 的响应式依赖ref或reactive包裹。我直接给一个普通变量赋值脱离了响应式系统视图自然不动。解决办法很简单——用ref包裹课程数组赋初值为空数组等接口返回后再赋值。还有一个小细节ref在模板中会自动解包但在 JavaScript 逻辑中需要访问.value这个初学容易搞混。const courses ref([]); async function loadCourses() { const data await fetchCourses(); courses.value data; }4.2 月份切换后“今天”标记消失用户查看其他月份时底部状态栏还是会显示“今天是9月25日”之类的内容。问题出在 getWeekOfTerm 函数接收了当前选中日期而非真实系统日期。我增加了一个isToday(date)辅助函数和isCurrentMonth(date)一起在模板中判断特殊样式function isToday(date) { const today new Date(); return date.toDateString() today.toDateString(); }toDateString这个方法对比日期很方便它返回类似 “Thu Sep 25 2025” 的字符串既比较了年月日又忽略了时分秒。这个函数后来还被用到了周视图的头部分类和课程卡片的边框高亮上。4.3 课程卡片互相遮挡的层级问题当同一天有多门课程时间段重叠时卡片会叠加在一起。这在实际排课中很常见比如导修课和讲座可能同时在晚间开始。我的初始方案是用 z-index 调整但治标不治本课程多起来视觉效果依旧混乱。后来我采用了简单的“分段错开”策略在渲染当天课程列表前按照 startSection 排序如果出现重叠的课程就把后出现的课程向右侧偏移 10px并设置半透明背景让下层内容隐约可见。这样虽然没有实现复杂的重叠算法但已经能保证课程信息基本可读。这个优化虽然只是针对个别情况但写进报告里体现的是“你考虑到了异常场景”的能力给分时印象分会好不少。4.4 响应式布局中触控和点击事件的冲突在手机上测试时我发现点击课程卡片偶尔会触发视图切换。排查后确认是课程卡片上的click.stop没有生效。原因是我在父容器的click事件中直接调用了changeView事件冒泡机制下子元素虽然加了 stop但父容器的监听器注册在了冒泡阶段实际还是会先执行子元素逻辑。最后的解决办法是子元素用click.stop阻止冒泡同时在父容器事件中增加一个if判断只有当点击目标确实是空白区域时才触发视图切换。这个策略在处理类似事件冲突时很值得借鉴。4.5 样式工具类组合暴长Tailwind 的边界用 Tailwind CSS 开发时课程卡片上的类名很快变得很长classabsolute left-1 right-1 p-2 rounded-lg shadow-md text-xs border-l-4 flex flex-col justify-between overflow-hidden cursor-pointer transition-all duration-150 hover:opacity-90 hover:shadow-lg这本身没问题但遇到需要根据状态动态切换颜色时类名拼接容易出错。我后来把课程卡片的样式分成了“基础样式”和“动态样式”两层。基础样式写成一个固定的常量字符串动态样式用计算属性返回再用模板字符串拼接。这个方式清晰了不少后续维护也方便。5. 界面优化与组件拆分让作业像“产品”而不像“作业”5.1 组件拆分为什么不要把所有东西写在 App.vue第一次写完时几乎全部逻辑都堆在 App.vue 里大约 500 行代码。功能没问题但可读性很差排查问题时一个函数要翻半天。利用晚上时间做了组件化重构。最终拆成这几个组件CalendarHeader.vue— 顶部月份和周次信息以及视图切换按钮WeekView.vue— 周视图渲染容器MonthView.vue— 月视图渲染容器CourseCard.vue— 课程卡片颜色、时间、交互反馈StatsBar.vue— 底部统计栏显示每天课程数和每周总节数这里要特别提一下 CourseCard 组件化的时机。我在重构前先确认了这个组件在周视图和月视图中都会被用到只是尺寸和样式稍有差异。于是让它接受size属性判断显示模式。这样逻辑复用、视觉统一代码量反而不升反降。5.2 响应式自适应的三个断点作业要求支持手机和电脑我设了三个断点小于 640px周视图只显示 7 列中的 5 列通过左右滑动查看完整一周课程卡片字号缩小到 10px。640px 到 1024px完整显示 7 列时间轴宽度缩窄卡片文字只显示课程名和教室。大于 1024px完整显示所有信息包括教师、周次、教室卡片间距也变大。实现响应式主要靠 Tailwind 的响应式前缀md:、lg:以及少量自定义媒体查询。我发现最耗时的是“手机上课程文字怎么排”这个问题。最终方案是只显示课程名和上课节次其余信息通过点击弹窗查看详情。弹窗很小但信息完整还带了一个“加入提醒”按钮纯 UI 展示观感立刻上一个台阶。5.3 颜色的心理学为什么视觉均匀很重要课程的 8 种主题色我不是随便选的。我选了 HSL 色相环上间距均匀的颜色保证相邻课程的卡片颜色有明显差异避免同学误看。const COLORS [ #4F46E5, // 靛蓝 #EA580C, // 橙 #16A34A, // 绿 #DC2626, // 红 #7C3AED, // 紫 #0891B2, // 青 #DB2777, // 粉 #CA8A04 // 金 ];不同课程分配颜色时我尽量让同一课程的每周颜色一致避免在周视图中出现颜色跳跃。方法是在数据源中显式指定每门课的 color 字段而不是用哈希随机生成——后者在月视图中同一门课的颜色可能会变非常影响辨识度。6. 常见问题与排查技巧实录6.1 表格形式作业实现中的高频问题速查问题现象可能原因解决方式修改课程数据后页面不更新数据没有用 ref/reactive 包裹把数据放入 ref 并通过 .value 访问月份切换后课程全消失周次计算和当前月份不一致检查 termStartDate 是否正确设置周视图课程高度错位节次计算少减了 1顶部的 top 值必须从 (startSection-1) 开始算手机端卡片重叠z-index 和事件冒泡冲突用 click.stop 父容器判断点击目标月视图标题日期错一天JavaScript 的月份从 0 开始获取月份时记得 1课程颜色不稳定颜色用随机算法生成数据源预设颜色字段保持恒定6.2 一个调试日期的封装技巧日期调试占据了这次开发的大头。我封装了一个formatDate工具函数用来统一输出 “2025-09-25 周四” 这种格式在 console.log 中快速定位问题。没有这个函数时我每次都要手写拼接不仅慢还容易错。export function formatDate(date) { const y date.getFullYear(); const m String(date.getMonth() 1).padStart(2, 0); const d String(date.getDate()).padStart(2, 0); const weekdays [日, 一, 二, 三, 四, 五, 六]; return ${y}-${m}-${d} 周${weekdays[date.getDay()]}; }把常用的 Date 操作封装成独立工具函数是我做过的最值当的决定。后续实现“上一周/下一周”切换按钮时这个函数直接复用省了很多事。6.3 本地开发与演示时最容易忽视的事最后分享一个很实际的提醒交作业前一定要自己拿着手机访问一下本地服务。不是用浏览器开发者模式的模拟器而是真实的手机。我在开发时用开发者模式模拟 iPhone 看着一切都好等真的用手机打开时发现课程卡片的字体太小、点击区域不够大还有横向滚动条。另一件事是把项目跑在npm run dev -- --host模式下手机和电脑连同一个 WiFi 才能访问。有些同学以为只能用 localhost 演示其实 Vite 默认只监听 localhost加上--host才能让局域网内的设备访问。这个细节如果提前知道演示时就不会手忙脚乱了。7. 从“第三次作业”里带走的个人体会把这次作业做完再回头看刚拿到题目时那种“这要怎么做”的迷茫其实已经很难想象了。我最大的感悟是越是模糊的题目越能逼你把主动权握在自己手里。没人告诉你要切月视图还是周视图你就得自己比较没人告诉你课程数据放哪你就得自己论证前后端方案没人告诉你需要响应式你就得站在使用者的角度想一遍。我个人在收尾时发现最花时间的往往不是写代码本身而是“定义什么才算完成”的判断。一个日历课程表可以写得很简单一张表格罗列所有课程完事。但也可以做得很深入按周按月切换、今天高亮、响应式布局、点击查看详情、课时统计。中间相差的其实就是你愿意为“把事情做好”多投入多少思考。做作业期间我养成了一个习惯每完成一个功能就打开手机浏览器看一眼效果同时在心里过一遍“如果老师点开这个界面第一眼会关注什么”。这个视角转换很有效它帮我把课程卡片的信息层级理顺了也避免了过度设计。如果你也在做类似的日历项目希望你从这篇文章里带走的不仅是几个函数的实现更是“先定义清楚数据再决定怎么渲染”“把通用逻辑抽成纯函数”“为真实使用场景留好扩展接口”这些思路。遇到问题不要慌一步一步去追根因大多数时候答案就在你看似不起眼的 console.log 里。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门