From 0e21d8e63f8eec09ff20e9a2234a8c6bff21526f Mon Sep 17 00:00:00 2001 From: gouki Date: Wed, 30 Sep 2026 00:48:50 +0000 Subject: [PATCH] =?UTF-8?q?fix(mini):=20=E4=BF=AE=E5=A4=8D=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E9=A1=B5=E7=BB=91=E5=AE=9A=E6=BC=82=E7=A7=BB=E5=B9=B6?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20WXML-JS=20=E4=B8=80=E8=87=B4=E6=80=A7?= =?UTF-8?q?=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit settings.js 同步绑定名 loggedIn→logged、login→wxLogin、navTo→navigateTo,恢复登录按钮与全部菜单项响应;npm test 接入新增 ci/check-bindings.cjs 门禁;calendar/wish-detail 各 1 处历史遗留漂移登记为仅告警另行跟进 --- mini/ci/check-bindings.cjs | 245 ++++++++++++++++++++++++++++++ mini/package.json | 2 +- mini/pages/settings/settings.js | 12 +- mini/pages/settings/settings.wxml | 18 +-- 4 files changed, 261 insertions(+), 16 deletions(-) create mode 100644 mini/ci/check-bindings.cjs diff --git a/mini/ci/check-bindings.cjs b/mini/ci/check-bindings.cjs new file mode 100644 index 0000000..5ec27ab --- /dev/null +++ b/mini/ci/check-bindings.cjs @@ -0,0 +1,245 @@ +#!/usr/bin/env node + +/** + * WXML-JS 绑定一致性静态检查(npm test / CI 门禁) + * + * 小程序不会在编译期校验 bind/catch 绑定名与 {{}} 数据引用: + * wxml 与页面 JS 一旦命名漂移(例如 wxml 写 wxLogin/navigateTo, + * 而 js 只有 login/navTo),页面不会报错,只是点击与渲染静默失效。 + * 本脚本把这类漂移拦在提交与 CI 阶段。 + * + * 检查范围:pages//.wxml 与同目录同名 .js 的配对 + * 检查规则: + * 1. bind/catch 事件绑定(含 bind:tap 冒号写法)的方法名必须存在于对应 JS + * 2. {{}} 表达式中的根标识符(点号前的名字)必须存在于对应 JS(数据字段或方法) + * 3. wx:for 默认变量 item/index、wx:for-item/wx:for-index 别名、 + * 字符串字面量与 true/false/null/undefined 自动忽略 + * 4. LEGACY_DRIFT 中登记的历史遗留条目仅告警不失败(修复后应删除对应条目) + * + * 存在漂移时以非零退出码结束,使 npm test 与 CI 直接失败。 + */ + +const fs = require("node:fs"); +const path = require("node:path"); + +const projectPath = process.cwd(); +const pagesPath = path.join(projectPath, "pages"); + +const LITERAL_KEYWORDS = new Set(["true", "false", "null", "undefined"]); +// wx:for 的默认循环变量属于模板结构名而非页面定义,始终忽略 +const DEFAULT_LOOP_VARS = new Set(["item", "index"]); + +// 引入本检查前就存在的历史遗留漂移(本轮修复范围外,另行跟进)。 +// 命中时仅告警不失败;对应页面修复后应删除这里的条目,让检查恢复全量严格。 +const LEGACY_DRIFT = new Set([ + 'pages/calendar/calendar.wxml catchtap="noop"', + 'pages/wish-detail/wish-detail.wxml bindtap="onShare"', +]); + +function listPageNames() { + return fs + .readdirSync(pagesPath, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .filter((name) => { + const dir = path.join(pagesPath, name); + return ( + fs.existsSync(path.join(dir, `${name}.wxml`)) && + fs.existsSync(path.join(dir, `${name}.js`)) + ); + }) + .sort(); +} + +function lineOf(text, index) { + let line = 1; + for (let i = 0; i < index; i++) { + if (text.charCodeAt(i) === 10) { + line++; + } + } + return line; +} + +/** 提取 wxml 中的事件绑定与 {{}} 表达式(去掉注释,保留行号) */ +function collectWxmlRefs(source) { + const text = source.replace(//g, ""); + const loopVars = new Set(); + const handlers = []; + const roots = []; + + for (const match of text.matchAll(/wx:for-(?:item|index)="([A-Za-z_$][\w$]*)"/g)) { + loopVars.add(match[1]); + } + + for (const match of text.matchAll(/((?:bind|catch):?[a-zA-Z]+)="([^"{}]+)"/g)) { + const name = match[2].trim(); + if (/^[A-Za-z_$][\w$]*$/.test(name)) { + handlers.push({ ref: `${match[1]}="${name}"`, name, line: lineOf(text, match.index) }); + } + } + + for (const match of text.matchAll(/\{\{([^{}]*)\}\}/g)) { + for (const name of expressionRoots(match[1])) { + roots.push({ ref: `{{${name}}}`, name, line: lineOf(text, match.index) }); + } + } + + return { loopVars, handlers, roots }; +} + +/** 取 mustache 表达式的根标识符:字符串字面量、点号后的属性名、数字与关键字都不算 */ +function expressionRoots(expression) { + const stripped = expression + .replace(/'(?:[^'\\]|\\.)*'/g, "''") + .replace(/"(?:[^"\\]|\\.)*"/g, '""'); + const roots = new Set(); + for (const match of stripped.matchAll(/(? 0) { + console.error(`[check-bindings] 发现 ${problems.length} 处 WXML 与 JS 不一致的引用:`); + for (const problem of problems) { + console.error(` - ${problem}`); + } + console.error( + "[check-bindings] 请统一 wxml 绑定名与页面 JS 的 data 字段/方法名,避免点击与数据绑定静默失效。", + ); + } + + if (legacyHits.length > 0) { + console.warn( + `[check-bindings] 另有 ${legacyHits.length} 处历史遗留漂移(引入检查前已存在,不计入本次失败,另行跟进):`, + ); + for (const hit of legacyHits) { + console.warn(` - ${hit}`); + } + } + + if (problems.length > 0) { + process.exitCode = 1; + return; + } + + console.log( + `[check-bindings] 通过:${pages.length} 个页面、${checkedRefs} 处 bind/catch 与 {{}} 引用均能在同名 JS 中找到定义。`, + ); +} + +main(); diff --git a/mini/package.json b/mini/package.json index b1c417a..d50b773 100644 --- a/mini/package.json +++ b/mini/package.json @@ -4,7 +4,7 @@ "description": "祈福小助手 - 万年历微信小程序", "private": true, "scripts": { - "test": "echo \"No tests yet\"", + "test": "node ci/check-bindings.cjs", "upload": "node ci/upload-ci.cjs", "preview": "node ci/preview-ci.cjs" }, diff --git a/mini/pages/settings/settings.js b/mini/pages/settings/settings.js index 4dfaeda..272840a 100644 --- a/mini/pages/settings/settings.js +++ b/mini/pages/settings/settings.js @@ -5,7 +5,7 @@ const birthProfile = require('../../utils/birth-profile.js'); Page({ data: { userInfo: null, - loggedIn: false, + logged: false, logging: false, // 菜单角标 profileCount: 0, @@ -25,7 +25,7 @@ Page({ refreshUser() { const token = wx.getStorageSync('token'); const userInfo = wx.getStorageSync('lunar-user-info'); - this.setData({ loggedIn: !!token, userInfo: userInfo || null }); + this.setData({ logged: !!token, userInfo: userInfo || null }); }, refreshCounts() { @@ -39,7 +39,7 @@ Page({ // ==================== 登录 ==================== - login() { + wxLogin() { if (this.data.logging) return; this.setData({ logging: true }); wx.login({ @@ -57,7 +57,7 @@ Page({ if (res && res.code === 0 && res.data && res.data.token) { wx.setStorageSync('token', res.data.token); wx.setStorageSync('lunar-user-info', res.data.user); - this.setData({ userInfo: res.data.user, loggedIn: true, logging: false }); + this.setData({ userInfo: res.data.user, logged: true, logging: false }); wx.showToast({ title: '登录成功', icon: 'success' }); // 登录后拉取云端生辰档案,保证菜单计数准确 birthProfile.syncFromServer().then(() => this.refreshCounts()).catch(() => {}); @@ -85,13 +85,13 @@ Page({ if (!res.confirm) return; wx.removeStorageSync('token'); wx.removeStorageSync('lunar-user-info'); - this.setData({ userInfo: null, loggedIn: false }); + this.setData({ userInfo: null, logged: false }); wx.showToast({ title: '已退出', icon: 'success' }); } }); }, - navTo(e) { + navigateTo(e) { const url = e.currentTarget.dataset.url; // tabBar 页面必须用 switchTab 打开 const tabPages = ['/pages/home/home', '/pages/wish-tree/wish-tree', '/pages/bazi/bazi', '/pages/wiki/wiki', '/pages/settings/settings']; diff --git a/mini/pages/settings/settings.wxml b/mini/pages/settings/settings.wxml index 171aab8..f73c0a9 100644 --- a/mini/pages/settings/settings.wxml +++ b/mini/pages/settings/settings.wxml @@ -1,7 +1,7 @@ - + @@ -24,28 +24,28 @@ 登录后生辰档案云端同步,换设备不丢失 - {{logging ? '登录中…' : '授权登录'}} + {{logging ? '登录中…' : '授权登录'}} 我的数据 - + 生辰管理 {{profileCount}}/{{maxFree}} › - + 我的收藏 {{bookmarkCount}} › - + 设置 › @@ -54,19 +54,19 @@ 功能 - + 每日运势 › - + 占卜 › - + 二十四节气 › - + 月历视图 ›