应使用语义化 div 结构配合 css grid 实现日程表,列数设为 7(对应周一至周日),行数按时间轴动态生成(如 8:00–20:00 每小时一行共 12 行),通过 grid-column/grid-row 控制跨格,统一用时间戳计算偏移并标准化时区,拖拽时用 datatransfer.setdata 传序列化数据,移动端加 touch-action: manipulation 消除点击延迟。

用原生 HTML + CSS 实现日程表,别碰 table 布局
纯靠 table 排日程表,后期改样式、加交互、适配移动端几乎必然翻车。现代日程表本质是「时间轴 × 日历格」的二维映射,应该用语义化结构(如 div + ARIA)配合 CSS Grid 或 Flex 控制行列对齐。
实操建议:
- 外层用
div设为display: grid,列数 = 7(周一到周日),行数按需动态生成(比如 8:00–20:00 每小时一行,共 12 行) - 每个单元格用
div,通过grid-column和grid-row控制跨列/跨行(例如一个 90 分钟会议从 10:00 开始,需占 1.5 行,实际要拆成两行或用height+position: absolute微调) - 避免用
rowspan/colspan—— 它们在 Grid 中不生效,且和 JS 动态插入冲突极大
getHours() 和 getMinutes() 算时间偏移时容易漏掉时区
直接用 new Date().getHours() 拿到的是本地时区时间,但用户可能跨时区预约、后端返回的是 UTC 时间戳。一旦混用,日程会整体偏移几小时,且只在特定地区复现(比如北京时间用户看美国会议总显示错 12 小时)。
实操建议:
- 所有时间计算统一转成毫秒级时间戳处理:
date.getTime(),再用Math.floor((timestamp - startOfDay) / (60 * 60 * 1000))算第几行 - 渲染前强制标准化时区:后端给的是 ISO 字符串(如
"2024-06-15T08:30:00Z"),前端用new Date(dateString)解析即可自动转成本地,无需手动加减 - 调试时打印
date.toString()和date.toISOString()对比,确认是否已按预期转换
拖拽日程项时 dragstart 事件拿不到原始数据
很多开发者把日程数据存在自定义属性里(比如 data-event-id="123"),但在 dragstart 回调里直接读 event.target.dataset.eventId 有时是 undefined —— 因为拖拽触发时 DOM 可能已被 Vue/React 重绘,或事件冒泡到了父容器。
实操建议:
- 在
dragstart触发瞬间,立刻从event.dataTransfer写入序列化数据:event.dataTransfer.setData('text/plain', JSON.stringify(eventData)) - 不要依赖
event.target查 DOM 属性,尤其在框架中;改用event.currentTarget或绑定时缓存数据引用 - Chrome 和 Safari 对
dataTransfer.setData的 MIME 类型校验严格,只传'text/plain'或'application/json',别用自定义类型如'event/custom'
移动端点击日程区域响应迟钝,click 事件 300ms 延迟没处理
在 iOS Safari 和部分安卓浏览器上,click 事件默认有约 300ms 延迟,用于判断是否为双击缩放。日程表里点一下就该弹窗编辑,延迟会让用户觉得“卡”或误触两次。
实操建议:
- 给容器加
touch-action: manipulation,这是最轻量的解法,兼容性好(iOS 9.3+、Android 5+) - 如果需要更精细控制(比如允许横向滚动但禁用双击),用
touch-action: pan-y pinch-zoom - 别盲目引入
fastclick库 —— 现代浏览器基本已原生支持touch-action,加库反而增加包体积和兼容风险
日程表真正的复杂点不在画格子,而在时间精度(分钟级对齐)、跨天事件(凌晨会议)、多时区协同、以及拖拽过程中与滚动容器的冲突处理。这些地方不写死逻辑、不提前抽象出时间块(TimeSlot)类,后面加「重复日程」「资源视图」时基本得重写渲染层。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











