
简介一款面向教师的微信小程序页面源码围绕网格排版场景设计帮助教师把版面划分为若干等份快速产出课件、教案或演示内容使图文排列简洁整齐。源码包共128个文件压缩包约1.08MB其中png/jpg图片负责界面视觉素材js文件承担交互逻辑与功能实现json用于页面配置和数据绑定wxml构建页面结构wxss控制样式表现各类型相互配合能让使用者较完整地看到工具类小程序从界面搭建到交互响应的实现链路。目前已有59人学习下载。开发者可依据教学需求自由修改源码如调整网格密度、颜色及间距增加作业模板库、课程表生成器等模块也可提取页面框架用于其他小程序项目对于想入门微信小程序开发的读者而言这是一份结构清晰、可运行调试的实战参考有助于理解组件布局、事件处理和基础API调用等核心知识点。1. 网格排版教师工具箱的微信小程序页面源码值得拆开看的是什么一份教师工具箱小程序老师真正每天点开的不是首页那张大卡片而是「田字格字帖」「座位表」「答题卡」「课表」这几个页面。它们表面上功能完全不同底层却是同一件事把一批数据塞进一个行数、列数都不固定的二维网格里还要保证边框对齐、间距均匀、换台手机不跑版。「网格排版教师工具箱」这套页面源码的价值就在这儿。它难的不是某个功能而是把网格抽象成一组可配置参数列数、间距、单元格边长、边框样式。参数定好加一个「米字格」是改配置不是重画一套页面。往下拆先看 WXSS 里的网格骨架怎么搭再看几个典型页面怎么落地最后解决拿到.zip之后怎么在自己的微信小程序开发者工具里跑起来。2. 微信小程序页面源码里的网格骨架rpx、grid 与 flex 怎么选2.1 用 750rpx 换算单元格边长别在 WXSS 里写死像素小程序的rpx以 750 为基准屏幕多宽750rpx 就是多宽。设计稿按 750 宽出量出来多少就是多少 rpx这是最省心的路径。但网格的列数是数据决定的列数一变单元格边长就得跟着变所以不能把宽度写死在类名里。常见做法是页面加载时算一次边长把结果塞进data渲染时用内联 style 下发。下面这个函数把「屏宽、列数、左右留白、格间距」四个输入换算成 rpx同时给出一份 px后面 canvas 绘制和拖拽命中判定都要用 px。// utils/grid.js function calcCellSize({ screenWidth 375, cols 8, padding 16, gap 4 }) { // 750rpx 恒等于屏幕宽度反推出 1rpx 等于多少 px const rpx2px screenWidth / 750; // 先全部按 rpx 计算最后再换算避免中间步骤反复转换丢精度 const usable 750 - padding * 2 - gap * (cols - 1); const cellRpx usable / cols; return { cellRpx: Math.floor(cellRpx), // WXSS 内联样式用取整防止亚像素缝 cellPx: cellRpx * rpx2px, // canvas、触摸命中判定用 stepPx: (cellRpx gap) * rpx2px // 拖拽排序的步长gap 必须算进去 }; } module.exports { calcCellSize };参数怎么改padding是网格整体左右留白改成 0 就是满宽网格适合打印预览gap是格与格之间的空隙字帖建议 0座位表建议 4~8答题卡建议 12 以上给手指留余量。取整这一步不能省750 / 7会得到 107.14直接写进样式在部分 Android 机型上会出现相邻单元格边框一粗一细。注意stepPx一定要把gap加进去后面做长按拖拽换位时只按cellPx算落点会越往右偏得越多。2.2 CSS Grid 和 Flex 在小程序里的取舍小程序的 WXSS 最终跑在 WebView 上iOS 端对display: grid支持完整Android 端要看内核版本低版本内核对grid-template-columns的repeat()和gap支持不完整会出现格子挤成一列。所以选型不是「哪个先进用哪个」而是看这个网格是几维的。布局方案适用网格列数可变低端 Android 表现典型页面CSS Grid严格行列二维支持走内联 style旧内核需降级座位表、答题卡、课表Flex wrap一行内均分即可用百分比宽度最稳字帖、图片墙绝对定位单元格坐标任意完全自由稳但代码量大棋盘类、拼图Canvas 绘制需要导出成图片完全自由稳字帖导出、板书截图判断标准很简单行和列必须同时对齐选 Grid只管一行里排几个选 Flex。教师工具箱里最容易踩的是字帖页面用 Grid 排 10×10 的格子低端机上直接崩成单列改成 Flex 加固定宽度百分比就没事了。2.3 列数走内联 style不要拼接类名WXSS 不支持在repeat()里插值grid-template-columns: repeat({{cols}}, 1fr)写在样式文件里不会被编译。列数这类运行时变量只能通过style属性下发。!-- pages/seat/seat.wxml -- view classgrid stylegrid-template-columns: repeat({{cols}}, 1fr); gap: {{gap}}rpx; view classcell wx:for{{cells}} wx:keyid >/* pages/seat/seat.wxss */ .grid { display: grid; padding: 0 16rpx; /* 兜底万一 grid 不生效单元格仍然有最小高度不至于塌成一条线 */ min-height: 200rpx; } .cell { min-height: 96rpx; display: flex; align-items: center; justify-content: center; background: #f6f8fb; border-radius: 8rpx; font-size: 26rpx; color: #2b3445; }逻辑说明gap用 rpx 下发单位跟项目一致.cell内部用 Flex 做文字居中这是「Grid 管二维定位、Flex 管单元格内部」的常规组合。min-height是给低端机的兜底Grid 失效时至少不会整块消失。参数说明行高由.cell的min-height和内容撑开如果需要固定正方形格子别指望aspect-ratio直接在内联 style 上补height: {{cellRpx}}rpx或者用padding-bottom: 100%这个老办法。3. 教师工具箱里几个典型页面的网格落地3.1 田字格字帖页用伪元素画十字虚线田字格的结构是「外框实线 中间十字虚线」不需要额外节点.cell上的两个伪元素就够。难点在 1rpx 虚线在部分机型上会被渲染成实线这是因为屏幕物理像素取整后虚线间隔小于一个像素。/* components/tian-cell/index.wxss */ .tian { position: relative; box-sizing: border-box; border: 2rpx solid #d94a4a; } /* 横向虚线用 border-top 而不是 height避免亚像素抖动 */ .tian::before { content: ; position: absolute; left: 0; right: 0; top: 50%; border-top: 1rpx dashed #d94a4a; } /* 纵向虚线 */ .tian::after { content: ; position: absolute; top: 0; bottom: 0; left: 50%; border-left: 1rpx dashed #d94a4a; } /* 米字格的两条对角线需要额外节点承载 */ .tian__diag { position: absolute; left: 0; top: 0; width: 100%; height: 100%; background: linear-gradient(45deg, transparent 49.6%, #d94a4a 49.6%, #d94a4a 50.4%, transparent 50.4%), linear-gradient(-45deg, transparent 49.6%, #d94a4a 49.6%, #d94a4a 50.4%, transparent 50.4%); }逻辑说明伪元素只用来画十字两条对角线用背景渐变实现靠百分比控制线宽不依赖transform: rotate在打印预览和导出图片时不会因为旋转产生锯齿。参数说明如果真机上虚线仍显示为实线把1rpx改成1px外框颜色和线色建议拆成两个变量方便一套代码同时出「红色田字格」和「灰色米字格」。字帖页面通常还要标注笔顺笔顺数据放在cells[i].strokes数组里渲染时按需展示不用为每种格型单独建页面。3.2 座位表与课表不规则单元格用 span 处理真实教室不是规整矩阵讲台占一整行、过道要留空、后排可能少两个位置。Grid 的grid-column: span N能处理合并但合并宽度必须和列数联动也就是 span 的数值要参与前面的边长计算否则合并格的边框会和相邻格错位。!-- pages/seat/seat.wxml -- view classgrid stylegrid-template-columns: repeat({{cols}}, 1fr); gap: {{gap}}rpx; view wx:for{{seats}} wx:keykey classcell {{item.empty ? cell--empty : }} style{{item.span 1 ? grid-column: span item.span ; : }} >!-- pages/answer/answer.wxml -- radio-group classsheet bindchangeonAnswerChange view classrow wx:for{{questions}} wx:keyqid wx:for-indexqi text classrow__no{{qi 1}}/text label classopt wx:for{{item.options}} wx:for-itemopt wx:keyvalue radio value{{opt.value}} checked{{opt.checked}} color#2f6fed hidden / view classopt__dot {{opt.checked ? opt__dot--on : }}{{opt.label}}/view /label /view /radio-group.sheet .row { display: flex; align-items: center; gap: 12rpx; margin-bottom: 16rpx; } .row__no { width: 56rpx; color: #8b94a6; font-size: 24rpx; } .opt { flex: 1; } .opt__dot { height: 72rpx; border-radius: 12rpx; background: #f2f4f8; display: flex; align-items: center; justify-content: center; font-size: 26rpx; } .opt__dot--on { background: #2f6fed; color: #fff; }逻辑说明把radio用hidden藏起来但保留在label内点击依然会触发radio-group的change选中态完全由opt.checked控制跨端表现一致。答题卡行数多时.row用 Flex 而不是 Grid是因为行内只有固定的几个选项不需要二维对齐Flex 在长列表里的渲染开销更低。参数说明opt.checked在onAnswerChange里根据e.detail.value重算整个questions数组别直接改this.data里的嵌套字段小程序不会检测到深层变化。题量超过 100 时要考虑分包或分页整页setData大数组会明显卡顿。4. 把这份微信小程序页面源码 .zip 跑起来目录体检与常见报错4.1 解压后先做一遍目录体检拿到的.zip解压出来可能缺文件、可能带node_modules、也可能app.json里登记了实际不存在的页面。先按下面这张清单过一遍再往微信小程序开发者工具里拖。检查项期望状态不通过的表现app.json的pages数组与pages/目录一一对应启动报「Page 未找到」每个页面的四件套.js/.json/.wxml/.wxss同名页面白屏、样式丢失project.config.json有appid、compileType: miniprogram工具不识别为小程序项目静态资源路径全小写、无中文iOS 真机图片不显示usingComponents路径以/开头或用相对路径Component is not found静态资源大小写这条最容易被忽略Windows 上Images/和images/是同一个目录iOS 真机区分大小写模拟器正常、真机白图排查半天找不到原因。统一改成小写成本最低。4.2 app.json 与 project.config.json 的必改项appid换成自己的测试号或已注册的小程序 appidsetting里几个开关决定源码能不能直接跑。{ appid: 你自己的appid, compileType: miniprogram, libVersion: 3.4.0, setting: { es6: true, enhance: true, postcss: true, minified: false, urlCheck: false, ignoreDevUnusedFiles: false } }逻辑说明es6和enhance打开源码里用的async/await、可选链才不会在低版本基础库上直接报语法错minified关掉便于断点调试ignoreDevUnusedFiles关掉避免工具把「暂时没被引用」的组件页剔除导致真机跑不通。参数说明libVersion不要盲目调高源码里如果用了wx.getSystemInfoSync这类已被标记废弃的接口高版本基础库会打警告甚至返回空值改用wx.getWindowInfo、wx.getDeviceInfo分接口获取。用 HBuilderX 开发微信小程序的话注意它的编译产物目录和开发者工具打开的目录要指向同一层直接开源码根目录会出现「找不到 app.json」。4.3 编译报错与真机差异导航栏高度、图片旋转自定义导航栏是教师工具箱常见需求因为它要在顶部放班级切换。计算高度不能写死得按状态栏和胶囊按钮位置推。// utils/navbar.js function getNavBarInfo() { const { statusBarHeight, windowWidth } wx.getWindowInfo(); // 胶囊按钮位置用于推算导航栏内容区高度 const rect wx.getMenuButtonBoundingClientRect(); // 上下留白对称胶囊顶部到状态栏的距离等于导航栏底部到胶囊底部的距离 const navBarHeight (rect.top - statusBarHeight) * 2 rect.height; return { statusBarHeight, navBarHeight, totalHeight: statusBarHeight navBarHeight, // 胶囊左边界到屏幕右边距右侧自定义内容不能超过这条线 safeRight: windowWidth - rect.left }; } module.exports { getNavBarInfo };逻辑说明rect.top - statusBarHeight是胶囊距内容区顶部的留白乘 2 加到胶囊高度上得到的就是内容区高度。safeRight用来约束右上角按钮的位置避免压到系统胶囊。参数说明这套计算只在页面onLoad执行一次即可横竖屏切换的场景要重新取。图片旋转的坑在另一头iOS 相机拍摄的 JPEG 带 EXIF 方向信息image标签会按 EXIF 自动转正放进 canvas 绘制时却不会导致导出图片里的字帖是横躺的。常见做法是绘制前根据 EXIF 方向做一次ctx.rotate或者干脆让用户选图时用wx.chooseMedia并在拿到临时文件后统一重绘一遍。5. 网格参数化配置、长按拖拽排序与批量导出参数化的收尾动作是把散落在各页面的cols、gap、格型统一收进一个配置对象页面只读配置。这样座位表从 6 列切到 8 列只改一行数据。// config/grid-presets.js module.exports { tian: { cols: 10, gap: 0, type: tian, padding: 20, lineColor: #d94a4a }, mi: { cols: 10, gap: 0, type: mi, padding: 20, lineColor: #d94a4a }, seat: { cols: 6, gap: 10, type: seat, padding: 16, lineColor: #e3e8f0 }, answer:{ cols: 4, gap: 12, type: answer,padding: 24, lineColor: transparent } };长按拖拽换座位是这类工具箱的高频诉求。movable-view能拖但不好判断落点自己算坐标更可控touchstart记下起始下标和触点touchmove用stepPx反推当前所在行列同时catchtouchmove阻止页面跟着滚。onSeatLongPress(e) { const { index } e.currentTarget.dataset; this.setData({ dragging: index, dragStart: e.touches[0] }); }, onSeatTouchMove(e) { const { dragging, dragStart } this.data; if (dragging null) return; const { cellPx, stepPx } this.data.metrics; const dx e.touches[0].clientX - dragStart.clientX; const dy e.touches[0].clientY - dragStart.clientY; // 用步长取整得到跨越了几个格子而不是按像素硬算 const colDelta Math.round(dx / stepPx); const rowDelta Math.round(dy / stepPx); if (!colDelta !rowDelta) return; const target this.indexToSeat(dragging, rowDelta, colDelta); this.swapSeats(dragging, target); // 交换后把 dragging 更新为 target }导出这块图片走canvas合成加wx.saveImageToPhotosAlbum表格走二进制写入再打开。写文件时临时目录要用wx.env.USER_DATA_PATH它是小程序可读写目录别往代码包目录写。const fs wx.getFileSystemManager(); const filePath ${wx.env.USER_DATA_PATH}/seat_${Date.now()}.xlsx; // arrayBuffer 由表格库生成encoding 用 binary 避免 UTF-8 二次编码 fs.writeFileSync(filePath, arrayBuffer, binary); wx.openDocument({ filePath, fileType: xlsx, showMenu: true });USER_DATA_PATH有容量配额座位表、成绩单这种一次性文件用完就unlink否则攒到配额上限后writeFileSync会静默失败。最后一步取整别忘拖拽命中用stepPx而不是cellPx少加一个gap拖到第四列之后整块座位表会集体偏一格。本文还有配套的精品资源点击获取