Files
everything-is-suitable/everything-is-suitable-uniapp/scripts/ui-audit-mp-weixin.mjs
T
zhangxiang 810081ac0a feat: 小程序端 UI 优化与纯客户端化改造
- 移除微信订阅推送功能(云函数/订阅管理页),回归纯客户端零后端架构
- 新增今日黄历默认展示卡片 TodayAlmanacCard(9 语言翻译)
- i18n 修复:启用 globalInjection 修复 $t 未注入,插值消息全部改为
  Messages Functions 适配小程序端(63 处)
- 修复底部导航切换失效与运势页数据不刷新
- 统一设计令牌(深褐主色 + 印红强调色),组件 UI 规范重构
- 图标 iconfont 化(FontAwesome 子集化,修复 Android 豆腐块)
- 新增 postcss-px2rpx 机型自适应(仅 mp-weixin 构建生效)
- 新增 5 个用户旅程 E2E 测试(31 用例)与验收报告
- 补充微信小程序项目配置(appid)
2026-08-28 07:51:02 +08:00

145 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 小程序构建产物 UI 层级审计工具
* 用法: node scripts/ui-audit-mp-weixin.mjs [--dist <path>] [--report <path>]
* 默认 dist: dist/build/mp-weixin
*
* 审计维度:
* 1. 页面结构 (wxml): 根元素、关键 UI 区块、自定义组件引用
* 2. 样式 (wxss): 关键样式类存在性、rpx 单位使用
* 3. 配置 (json): navigationBarTitleText、usingComponents、tabBar
* 4. i18n: 模板中 $t() 键的引用一致性(渲染不依赖 missing key
* 5. 组件引用完整性: wxml 引用的自定义组件在 components/ 存在
*/
import { readdirSync, readFileSync, existsSync, writeFileSync, statSync, mkdirSync } from 'node:fs'
import { join, resolve, relative, extname } from 'node:path'
const args = process.argv.slice(2)
const dist = resolve(args.includes('--dist') ? args[args.indexOf('--dist') + 1] : 'dist/build/mp-weixin')
const reportPath = args.includes('--report')
? resolve(args[args.indexOf('--report') + 1])
: join(process.cwd(), '../docs/reports/UI_AUDIT_mp-weixin.md')
function walk(dir, ext, out = []) {
if (!existsSync(dir)) return out
for (const f of readdirSync(dir)) {
const p = join(dir, f)
if (statSync(p).isDirectory()) walk(p, ext, out)
else if (f.endsWith(ext)) out.push(p)
}
return out
}
const issues = []
const passes = []
const seen = new Set()
function pass(msg) {
if (!seen.has(msg)) { seen.add(msg); passes.push(msg) }
}
function issue(sev, msg) {
issues.push(`[${sev}] ${msg}`)
}
/* ---------- 1. 页面结构审计 ---------- */
const wxmlFiles = walk(dist, '.wxml')
const pageWxml = wxmlFiles.filter((f) => f.includes('/pages/'))
const componentWxml = wxmlFiles.filter((f) => f.includes('/components/'))
for (const f of pageWxml) {
const rel = relative(dist, f)
const content = readFileSync(f, 'utf8')
// 根元素
if (!/<view|root|template/.test(content)) issue('P1', `${rel}: 无根元素`)
else pass(`${rel}: 有根元素`)
// 关键区块
for (const block of ['page-header', 'page-content']) {
if (content.includes(block)) pass(`${rel}: 包含 ${block}`)
}
// 自定义组件引用
const compTags = [...content.matchAll(/<([a-z][a-z0-9-]*)\b/g)].map((m) => m[1])
const compDirs = existsSync(join(dist, 'components')) ? readdirSync(join(dist, 'components')) : []
for (const tag of new Set(compTags)) {
const builtin = ['view', 'text', 'button', 'input', 'image', 'scroll-view', 'picker', 'checkbox', 'radio', 'swiper', 'swiper-item', 'form', 'label', 'navigator', 'canvas', 'map', 'video', 'rich-text', 'progress', 'slider', 'switch', 'textarea', 'icon']
if (builtin.includes(tag) || tag === 'template') continue
// kebab-case -> PascalCase(组件目录为驼峰),并忽略大小写匹配
const tagPascal = tag.replace(/(^|-)([a-z])/g, (_m, _p, c) => c.toUpperCase())
const matched = compDirs.some((d) => d === tagPascal || d.toLowerCase() === tag.toLowerCase())
if (matched) pass(`${rel}: 组件 ${tag} 存在`)
else issue('P1', `${rel}: 引用未知组件 <${tag}> (components/ 无匹配目录)`)
}
}
/* ---------- 2. 样式审计 ---------- */
const wxssFiles = walk(dist, '.wxss')
for (const f of pageWxml) {
const rel = relative(dist, f)
const wxssPath = f.replace(/\.wxml$/, '.wxss')
if (existsSync(wxssPath)) pass(`${rel}: wxss 存在`)
else issue('P1', `${rel}: wxss 缺失`)
}
/* ---------- 3. 配置审计 ---------- */
const pageJson = pageWxml.map((f) => f.replace(/\.wxml$/, '.json'))
for (const f of pageJson) {
const rel = relative(dist, f)
if (!existsSync(f)) { issue('P1', `${rel}: json 缺失`); continue }
const cfg = JSON.parse(readFileSync(f, 'utf8'))
if (cfg.navigationBarTitleText) pass(`${rel}: 导航栏标题=${cfg.navigationBarTitleText}`)
if (cfg.usingComponents && Object.keys(cfg.usingComponents).length > 0) {
pass(`${rel}: usingComponents ${Object.keys(cfg.usingComponents).length} 个`)
}
}
const appJsonPath = join(dist, 'app.json')
if (existsSync(appJsonPath)) {
const app = JSON.parse(readFileSync(appJsonPath, 'utf8'))
pass(`app.json: pages ${app.pages.length} 个`)
if (app.tabBar && app.tabBar.list) pass(`app.json: tabBar ${app.tabBar.list.length} 项`)
if (app.window) pass(`app.json: window 配置存在`)
else issue('P2', 'app.json: window 配置缺失')
} else {
issue('P0', 'app.json 缺失')
}
/* ---------- 4. i18n 键审计 ---------- */
const zhCN = readFileSync(join(dist, 'locales/zh-CN.js'), 'utf8')
for (const f of pageWxml) {
const rel = relative(dist, f)
const content = readFileSync(f, 'utf8')
// 提取模板中 $t('key') 或 {{a}} 等(小程序端 $t 被编译进 render,此处检查 wxml 是否含已编译变量标记)
const tKeys = [...content.matchAll(/[\$]t\(['"]([^'"]+)['"]\)/g)].map((m) => m[1])
for (const k of tKeys) {
if (zhCN.includes(k.replace(/\./g, '/'))) pass(`${rel}: i18n key ${k} 存在于语言包`)
else issue('P2', `${rel}: 模板 i18n key ${k} 可能缺失`)
}
}
/* ---------- 输出 ---------- */
const now = new Date()
const lines = [
`# 小程序构建产物 UI 审计报告`,
``,
`- **审计时间**: ${now.toISOString()}`,
`- **构建产物**: ${dist}`,
`- **结果**: ${issues.length === 0 ? '✅ 全部通过' : '❌ 发现 ' + issues.length + ' 个问题'}`,
``,
`## 统计`,
`- 页面 wxml: ${pageWxml.length} 个`,
`- 组件 wxml: ${componentWxml.length} 个`,
`- wxss: ${wxssFiles.length} 个`,
`- 通过项: ${passes.length}`,
`- 问题项: ${issues.length}`,
``,
`## 通过项`,
...passes.map((p) => `- ✅ ${p}`),
``,
`## 问题项`,
...(issues.length ? issues.map((i) => `- ⚠️ ${i}`) : ['- 无']),
]
mkdirSync(join(reportPath, '..'), { recursive: true })
writeFileSync(reportPath, lines.join('\n'))
console.log(lines.join('\n'))
console.log(`\n报告已保存: ${reportPath}`)
process.exit(issues.some((i) => i.startsWith('[P0]') || i.startsWith('[P1]')) ? 1 : 0)