
简介一份面向教师群体的微信小程序页面源码聚焦网格排版工具场景帮助教师快速制作教学资料、演示文稿与电子教案。源码包共128个文件包含js逻辑、json配置、wxss样式、wxml页面结构等核心代码以及36张png图片素材整体仅1.08MB结构紧凑便于阅读。已有59人学习下载适合想了解微信小程序开发或需要定制教学工具的教师与初级开发者。通过源码可清晰看到页面布局与交互逻辑能直接修改配色、网格参数与功能模块也可将其作为模板扩展其他排版需求。整体代码量适中兼具实用性与学习价值。1. 教师工具箱里的“网格排版”到底在解决什么问题一位小学数学老师要打印田字格练字纸、作文格、英语四线三格或者排一份期末座位表他不会打开 CAD也不会用 Excel 反复调列宽。他要的是输入行数、列数、线的粗细一秒钟生成一张能打印或发到家长群的网格图。这类需求大量出现在教案排版、书法练习、手抄报底板、五线谱纸生成等场景里。微信小程序天然适合承载这种工具无需安装、打开即用、能直接存图分享。而这个标题称为“页面源码”意味着它是一份完整的小程序前端工程重点不在后端而在页面结构、样式算法和交互逻辑。对开发者来说这类源码的价值不只是“能跑”。网格排版是典型的“参数 → 数据 → 视图”单向流程小程序的数据绑定、WXSS 适配、Canvas 导出都能在一个小项目里完整走一遍。你想理解微信小程序页面源码怎么组织、数据怎么驱动视图、网格类组件怎么写才不卡拆这个 zip 比看电商模板更合适。接下来的内容不会逐文件朗读代码而是沿着从解压到跑通、从静态布局到参数化生成、从预览到导出的路径把一份网格排版小程序页面需要的关键实现讲清楚。2. 处理微信小程序页面源码.zip从解压到目录拆解拿到 zip 之后最常见的问题是“导入后编译报错”。先不急着双击 app.json正确的做法是把整个 zip 解压到一个没有中文和空格的路径比如D:\projects\grid-tools。然后打开微信开发者工具选择“导入项目”目录指向D:\projects\grid-toolsAppID 选择“测试号”即可。之所以建议测试号是因为你大概率不会用这份源码去发布正式版本测试号不需要注册小程序账号也不需要配置服务器域名。2.1 导入开发者工具时高频出现的两个报错第一个报错是“未找到 app.json 或查找不到 app.json”。这通常是因为你把路径选到了二次解压产生的嵌套目录比如D:\projects\grid-tools\grid-tools-master。解决方法是把内层所有文件剪切到外层根目录确保app.json直接位于你导入的那个文件夹下。第二个报错是“当前页面路径不存在”如果你修改过pages目录里的文件名要同步更新app.json的pages数组小程序的页面注册表是手写的不会自动扫描目录。编译通过后第一件事不是点开每个页面而是打开调试器的 Console 面板看有没有 WXML 警告比如Some selectors are not allowed in component wxss。网格排版这类页面大量使用标签选择器而小程序组件样式默认隔离如果你把page或view写进了组件的 wxss编译不报错但样式不生效。处理方式是全部改成 class 选择器或者给组件加上styleIsolation: apply-shared。2.2 教师工具箱源码的典型目录结构与职责划分目录/文件职责关键点app.json全局页面注册、窗口外观改页面标题和导航栏颜色在这里app.wxss全局公共样式适合放页面底色和通用按钮类pages/index工具箱首页通常是网格入口多个工具按钮pages/grid核心网格生成页本源码最值得读的页面components/可复用组件网格预览画布、参数滑块等utils/grid.js网格纯算法逻辑行/列计算、坐标换算、导出尺寸static/图片图标资源按钮图标和默认纸张纹理需要说明的是这里列出的结构是这类源码最常见的样子拿到手未必一字不差。但你只需要记住一个原则utils里放的是纯函数pages里放的是页面胶水代码。网格排版这种功能算法和 UI 分离得越彻底后面扩展纸张类型作文纸、方格纸、拼音纸就越容易。2.3 页面数据流只有一条线data 是网格的唯一事实来源网格排版界面无论做得多花哨最核心的数据结构无非是行数、列数、格子宽高、线色、线宽、纸型。在pages/grid/index.js的data里它们通常长这样data: { rows: 8, cols: 8, cellWidth: 60, lineColor: #666666, lineWidth: 1, gridType: square, // square | tianzi | mizige | fourline showBorder: true }这不是一个普通的变量列表它定义了整个页面的状态边界。所有 slider 滑动、picker 选择、开关切换最终都通过setData修改这组数据WXML 中wx:for依赖rows和cols重新渲染Canvas 导出依赖同样的数据重新绘制。为什么要强调这条线因为很多初学者会直接在 WXML 里写死8个view一旦用户把列数从 8 改成 10页面就崩了。网格排版的核心在“排版”二字排版的本质是参数化数据不动页面只是静态图。setData的代价也需要评估rows和cols变化时你不能一次性把整个二维数组塞进去那样会触发大量节点 diff。更稳妥的做法是在setData里只更新行列数标量网格节点交给 WXML 的wx:for自动展开每个格子的样式用内联 style 绑定cellWidth。这样数据量小渲染也流畅。3. 网格排版页面的核心rpx 适配、CSS Grid 与格子变体网格排版这一切能成立的基础是把“纸”抽象成“行、列、间距、线型”四个可计算的变量。页面 UI 可以切换纸张类型而底层代码只做一件事根据参数生成一个格子数组然后交给 WXML 渲染。这种结构下“新增一种网格”不再是复制粘贴一个页面而是往纯函数里加一个 branch。3.1 用 JS 生成网格数据而不是在 WXML 里写死循环先看数据生成逻辑。这是utils/grid.js里最常见的一段代码function generateGrid(rows, cols) { const cells []; for (let r 0; r rows; r) { for (let c 0; c cols; c) { cells.push({ row: r, col: c, style: left: ${c * 100 / cols}%; top: ${r * 100 / rows}%; width: ${100 / cols}%; height: ${100 / rows}%; }); } } return cells; } module.exports { generateGrid };这段代码把行数和列数换算成了百分比定位而不是 px 或 rpx。这样做的理由很直接网格页面的预览区宽度不同手机屏幕宽度也不同百分比能让网格在任何容器里自适应。每个 cell 对象包含row和col索引后面 Canvas 导出时需要根据索引反算坐标。页面 JS 里只需要const cells generateGrid(this.data.rows, this.data.cols)然后setData({ cells })即可。有两点值得注意。一是style里的百分比拼接不能出现多余空格left: ${c * 100 / cols}%;和top: ${r * 100 / rows}%;必须保持严格格式否则 WXML 内联样式解析失败时网格会全部堆在左上角。二是rows和cols的数值在 slider 里要限制最大值通常rows不超过 20cols不超过 15否则 cell 数量超过 300 个后低端 Android 机上的滚动和重绘会出现肉眼可见的掉帧。3.2 CSS Grid 还是 flex网格排版小程序的选型理由布局层有两个候选方案CSS Grid 和 flex。微信小程序基础库从 2.10.0 开始对 CSS Grid 支持已经稳定iOS 和 Android 的 WebView 渲染差异不大。网格排版场景建议优先使用 CSS Grid因为这里的语义就是严格的二维网格grid-template-columns: repeat(8, 1fr)直接表达“8 等分”代码可读性远超嵌套 flex。.grid-container { display: grid; grid-template-columns: repeat(v-bind(cols), 1fr); grid-template-rows: repeat(v-bind(rows), 1fr); width: 100%; height: 100%; border-left: 1rpx solid #ccc; border-top: 1rpx solid #ccc; } .grid-cell { border-right: 1rpx solid #ccc; border-bottom: 1rpx solid #ccc; box-sizing: border-box; }上面这段是 WXSS 的示意写法实际小程序里repeat(v-bind(cols), 1fr)不能直接这样写你需要把gridTemplateColumns作为内联 style 绑定到容器节点上stylegrid-template-columns: repeat({{cols}}, 1fr); grid-template-rows: repeat({{rows}}, 1fr);。这是小程序动态样式的一个限制但正好符合我们的数据驱动原则。border-left和border-top加在容器上border-right和border-bottom加在格子上能避免相邻格子边框叠加导致线条变粗。如果项目需要兼容非常老的基础库或者页面里网格需要嵌套在scroll-view中横向滚动flex 方案更稳妥。每个 cell 用flex: 0 0 calc(100% / cols)控制宽度外层flex-wrap: wrap。两种方案的 cell 内部样式完全一致影响的只是排布容器所以不必担心重构成本。3.3 田字格、米字格和四线三格的实现细节教师工具箱和普通网格组件的分水岭在于“格内辅助线”。田字格的虚十字、米字格的对角线、四线三格的横线本质上都是在基础方格上叠加 CSS 伪元素或额外 view。以田字格为例最常见的实现是在.grid-cell内部增加两个绝对定位的 view一个横向、一个纵向view classgrid-cell stylewidth: {{cellWidth}}rpx; height: {{cellWidth}}rpx; view classcell-line-h wx:if{{gridType tianzi}}/view view classcell-line-v wx:if{{gridType tianzi}}/view /view.cell-line-h { position: absolute; left: 0; right: 0; top: 50%; height: 1rpx; background: #999; transform: scaleY(0.5); } .cell-line-v { position: absolute; top: 0; bottom: 0; left: 50%; width: 1rpx; background: #999; transform: scaleX(0.5); }transform: scaleY(0.5)是为了解决 1rpx 在 Retina 屏上显示过粗的问题。米字格则是在田字格基础上增加两条对角线可以在.grid-cell上加background: linear-gradient(to top right, transparent calc(50% - 0.5rpx), #999, transparent calc(50% 0.5rpx)), linear-gradient(to top left, ...)。但 linear-gradient 的对角线方向在小程序里兼容性不如伪元素稳定稳妥一点还是用两个旋转 45 度的 view。四线三格则要覆盖整行不能按 cell 切分更适合在网格容器上方单独画三条横线。这些辅助线的共同原则是线的样式必须由gridType这个参数驱动wx:if控制显隐而不是写三个不同的页面。预览时的显示效果要和最终导出的 Canvas 绘制一致否则老师点导出后得到一张和预览完全不同的图这个工具的口碑就毁了。4. 参数面板与 Canvas 导出让页面源码变成可用的“工具箱”网格页面能在模拟器里跑通只是第一步。真正决定这个源码价值的是交互层老师能调什么参数、调整后能不能实时看到效果、最终能不能把结果导出成高清图片发到家长群。这一章解决的就是这几个问题。4.1 参数表单slider、picker 与实时预览的联动节奏参数面板的常见布局是页面左侧或上部是网格预览右侧或底部是控制区。控制区包含行数 slider、列数 slider、线宽 slider、纸型 picker、颜色选择器以及“显示边框”开关。一个容易忽略的交互细节是 slider 的live属性——微信小程序的slider组件有live和active等属性当live为false时滑动过程只在松手时触发bindchange此时预览的更新频率低拖动手感会更跟手。反之如果追求数值实时跳动需要监听changing事件并在回调里做防抖。onColsChanging(e) { const cols e.detail.value; if (this._timer) clearTimeout(this._timer); this._timer setTimeout(() { this.setData({ cols }); }, 50); }这里的_timer是页面实例上的自定义属性不用放在data里。50ms 的防抖能避免 slider 还在移动时网格疯狂重排。原本用bindchange只有松手才触发一次不需要防抖但如果你发现松手后网格才变化观感上“慢半拍”那就改用changing加防抖。纸型 picker 的数据源应该与utils/grid.js里的算法分支保持一致gridType名称适用场景square方格纸数学草稿、抄写tianzi田字格低年级生字书写mizige米字格书法练习fourline四线三格英语字母书写picker 的range直接绑定这个数组的name字段bindchange里只 set 一个gridType字符串。不要在 picker 的 change 事件里跑生成网格的逻辑生成逻辑应该统一收敛到renderGrid()函数里被所有控件调用。4.2 导出高清图片用 Canvas 重绘而不是对预览截屏微信小程序有wx.canvasToTempFilePath可以把 canvas 内容保存为图片但新手最容易掉进的坑是直接给预览区域的 view 加type2d的 canvas 盖层然后把 view 的 DOM 截图下来。这个方案在开发者工具里能出图真机上导出的是白屏或模糊的图。原因是小程序没有 Web 标准的html2canvas你截的不是渲染结果而是 canvas 节点本身。正确做法是新建一个屏幕外的隐藏 canvas用同一套网格参数把它完整绘制一遍再导出。绘制代码放在utils/grid.js里接收 canvas 对象和参数对象function drawGridToCanvas(canvas, ctx, params, pixelRatio) { const { rows, cols, cellWidth, lineColor, lineWidth, gridType } params; const width cols * cellWidth; const height rows * cellWidth; canvas.width width * pixelRatio; canvas.height height * pixelRatio; ctx.scale(pixelRatio, pixelRatio); ctx.clearRect(0, 0, width, height); ctx.strokeStyle lineColor; ctx.lineWidth lineWidth; for (let r 0; r rows; r) { ctx.beginPath(); ctx.moveTo(0, r * cellWidth); ctx.lineTo(width, r * cellWidth); ctx.stroke(); } for (let c 0; c cols; c) { ctx.beginPath(); ctx.moveTo(c * cellWidth, 0); ctx.lineTo(c * cellWidth, height); ctx.stroke(); } if (gridType tianzi) { // 在每个格子内画十字虚线实现略 } }调用方在页面里用wx.createSelectorQuery()获取 canvas 节点再通过wx.canvasToTempFilePath导出。关键参数是pixelRatio它应该取wx.getSystemInfoSync().pixelRatio通常是 2 或 3。如果你直接用 CSS 像素尺寸设置 canvas导出图片在电脑上放大后会发虚线变成灰色块。还有两点细节值得写进注释里。第一保存到相册前要处理授权调用wx.authorize({ scope: scope.writePhotosAlbum })如果用户之前拒绝过需要引导到设置页。第二隐藏 canvas 不能设置display: none否则createSelectorQuery取不到节点用position: fixed; left: -9999rpx; top: 0;实现屏幕外渲染。4.3 页面级分享把参数写进路径让老师分享“同一张纸”工具箱类小程序和内容类小程序最大的区别是分享应该携带状态。老师调好一套米字格参数后发给同事同事点开应该看到同一套参数而不是打开默认的方格纸。实现方式是重写onShareAppMessageonShareAppMessage() { const params this.data; const query rows${params.rows}cols${params.cols}gridType${params.gridType}; return { title: 米字格生成器 - 小学数学练字, path: /pages/grid/index?${query} }; }在onLoad里解析options并 setData就能完成状态恢复。注意onShareAppMessage的path必须以/开头否则分享卡片打开后找不到页面。这里把cellWidth排除在 query 外因为不同屏幕的 rpx 基准不同强行传递像素值反而会让网格比例失真。5. 改页面时不崩、导出不走样的四个高频问题与两个实用技巧源码跑通、功能能点之后真正的工程化工作才开始。这段内容集中处理真机和不同基础库版本下的兼容问题以及如何让网格生成算法脱离页面独立复用。5.1 真机与开发者工具表现不一致的 4 个高频卡点第一个是 rpx 和 px 混用导致的对不齐。slider 的数值单位是 px而你在 WXML 里用 rpx 渲染格子两者不能在一条公式里直接换算。常见做法是全部用 px 渲染预览再用wx.getSystemInfoSync().windowWidth / 750计算出 rpx 换算系数。不要在utils/grid.js里写任何 rpx算法层只认 px页面展示层负责单位转换。第二个是 iOS 上 Canvas 绘制的时间戳问题。如果用户快速滑动行数 slider 后立即点导出iOS WebView 的 canvas 还没完成上一帧重绘导出结果可能是上一次的图。导出按钮要做节流记录上次导出的时间戳间隔小于 800ms 时给出 Toast 提示而不是默默执行。第三个是顶部导航栏高度对布局的影响。网格预览如果做成全屏沉浸式需要把导航栏高度算进可用高度里。用wx.getMenuButtonBoundingClientRect()获取胶囊按钮位置结合wx.getSystemInfoSync().statusBarHeight计算出自定义导航栏高度再把这个高度传给预览容器的内联样式避免页面底部被遮挡。第四个是滚动穿透。参数面板在scroll-view里滚动时手指滑到网格预览区域页面背景会跟着滚。解决方法是给scroll-view设置catchtouchmove为true或者用disable-scroll属性控制。注意catchtouchmove会拦截所有触摸事件如果预览区需要监听点击事件改用scroll-view的enhanced属性加show-scrollbar。5.2 核心网格算法写给组件复用return 坐标集合而不是 DOM在页面里写generateGrid没问题但如果你想在首页加一个“最近使用模板”的缩略图每个缩略图都要重新跑一遍行列循环页面就臃肿了。把网格生成从二维循环抽象为“线段的坐标集合”getLines(rows, cols, width, height)返回所有横线和竖线的起点终点坐标。组件拿到坐标集合后既可以用 WXML 的wx:for渲染 view 线条也可以用 Canvas 直接绘制。两者共用同一个算法源呈现效果天然一致。5.3 参数模板存储把老师的常用设置做成“一键恢复”实现这个只需要wx.setStorageSync和wx.getStorageSync两个 API。在导出按钮旁边放一个“保存为模板”的按钮把data里除cells外的全部可序列化参数存入本地。页面onLoad时先读取模板有则覆盖默认值。模板数量控制在 5 个以内超出时按时间覆盖最旧的。这个功能可以把“每次打开都要重调行列数”的挫败感降到最低也是让网格排版小程序从“演示代码”变成“日常工具”的关键一步。最后要提的验证手段是导出图片后把图片放大到 200% 再检查线的灰度如果边缘出现锯齿或双线回查pixelRatio的传递链路十有八九是 canvas 的真实宽高和导出参数不一致。本文还有配套的精品资源点击获取