feat(ui): activity 页迁 TS + 修复过期契约 + 新增 11 页静态预览器

## 背景
2026-09-13 UI 重设计方案的 13 个契约门禁中 3 个为红,逐一查明性质后分类处理。

## 1. activity 页迁移 TypeScript(11/11 页现已全 TS)
- activity.js → activity.ts(139 行),业务逻辑零改动,仅补类型
- 此前 STATUS.md 记「9/11 TS-only、3 页待迁」,但 messages/matchmaker
  已在 4f91be0 迁完,实际只剩 activity 一页,本提交后清零

## 2. miniapp-reference-contracts 契约自身过期(非代码问题)
- 契约引用 messages.js / matchmaker.js / activity.js,但页面已 TS 化,
  readFileSync 抛裸 ENOENT 栈,看不出「契约引用了陈旧文件名」这一真实原因
- 改为统一走 readPageLogic(page):断言 .ts 存在 + .js 不存在(锁单一运行源),
  找不到文件时给出可读断言而非崩溃
- 核心 11 页循环内统一加 TS-only 校验,防止 JS 回流

## 3. phase07-home-contracts 契约断言架构写反(非代码问题)
- 契约要求 HomeApplicationService 字面出现 userIdentityMapper.findByUserId,
  但实际是 capabilityService.isIdentityVerified(userId)(HomeApplicationService:116)
- 这符合 AGENTS.md #26「前端入口可见性由服务端 CapabilityService 返回决定」,
  让首页穿透 Capability 直接持有 mapper 反而是架构重复
- 且契约自身另一条已断言 CapabilityService 必须有 isIdentityVerified,
  两条要求互相矛盾
- 改为:查库断言落在 caps 侧;首页断言必须委托 capabilityService.isIdentityVerified;
  新增 doesNotMatch(home, /userIdentityMapper/) 禁止首页绕过 Capability
- 保留原有 doesNotMatch(home, /setVerified\(true\)/) 防硬编码断言

## 4. 新增 11 页静态预览器(本机无微信开发者工具)
- scripts/preview-miniapp.mjs:把 WXML + WXSS 转成可浏览器打开的静态 HTML
- 能力:wx:if/wx:elif/wx:else 链、wx:for 列表、嵌套三元、比较/取反运算、
  自定义组件按属性展开、slot 替换、rpx→px 换算、组件样式收集
- mock 数据严格对齐 types/index.d.ts 与各页 WXML 实际读取字段
  (如 regionName/heightCm/educationCode、status: registration_open),
  不用想象字段,避免误导验收判断
- 产出 artifacts/ui-redesign/miniapp/preview.html(11 页可切换,284K)
- 明确边界:非真机渲染、不执行页面 JS、不连后端,
  只用于视觉走查,不得作为功能验收证据

## 验证
- 契约门禁 12/13 通过(唯一红的 ui-document-baseline-contracts 涉及
  STATUS.md 验收口径,待用户看真机效果后裁决,本轮不动)
- 11 页渲染体量:116/70/47/56/94/68/39/39/41/37/38 个类名元素,
  零残留 {{}} 插值、零残留原始组件标签
- 小程序 tsc --noEmit 通过
This commit is contained in:
Codex
2026-09-16 12:33:48 +08:00
parent 3244e02e7e
commit ce172ffd4f
5 changed files with 7086 additions and 18 deletions
+26 -5
View File
@@ -4,6 +4,25 @@ import { resolve } from 'node:path'
const root = resolve(import.meta.dirname, '..')
const read = (path) => readFileSync(resolve(root, path), 'utf8')
/**
* 需要读取页面运行源时先断言存在。
* 直接 readFileSync 会抛裸 ENOENT 栈,看不出「契约引用了陈旧文件名」这一真实原因——
* 页面在 2026-09-14 全量 TS 化后,本契约曾因仍引用 .js 而崩溃(而非给出可读断言)。
*/
const readSource = (path) => {
assert.ok(existsSync(resolve(root, path)), `契约引用的运行源不存在: ${path}(页面已迁移?请更新契约路径)`)
return read(path)
}
/** 断言页面运行源为 TypeScript 且不存在同名 .js 双源 */
const readPageLogic = (page) => {
const ts = `miniapp/pages/${page}/${page}.ts`
assert.equal(
existsSync(resolve(root, `miniapp/pages/${page}/${page}.js`)),
false,
`${page} 存在 .js 与 .ts 双源,违反单一运行源约定`,
)
return readSource(ts)
}
const app = JSON.parse(read('miniapp/app.json'))
const corePages = [
'index',
@@ -43,6 +62,8 @@ for (const page of corePages) {
const template = read(base + '.wxml')
assert.match(template, /reference-page/, `${page} 未接入参考图页面骨架`)
assert.doesNotMatch(template, /[\u{1F300}-\u{1FAFF}]/u, `${page} 含 Emoji`)
// 首批 11 页必须为 TypeScript 单一运行源(readPageLogic 会同时断言 .js 不存在)
readPageLogic(page)
}
const homeLogic = read('miniapp/pages/index/index.ts')
@@ -60,7 +81,7 @@ assert.match(discoverTemplate, /candidate-list/, '找对象页缺少参考图候
assert.match(discoverTemplate, /\/assets\/icons\/ui\/search\.png/, '找对象页必须使用独立搜索图标')
const messagesTemplate = read('miniapp/pages/messages/messages.wxml')
const messagesLogic = read('miniapp/pages/messages/messages.js')
const messagesLogic = readPageLogic('messages')
assert.match(messagesTemplate, /message-shortcuts/, '消息页缺少四项快捷入口')
assert.match(messagesTemplate, /conversation-list/, '消息页缺少会话列表容器')
assert.match(messagesTemplate, /ui-conv__badge|unreadTotal/, '消息页缺少未读提示')
@@ -69,7 +90,7 @@ assert.match(messagesLogic, /canSendMessage/, '消息页进入聊天前必须校
const activityTemplate = read('miniapp/pages/activity/activity.wxml')
const activityCard = read('miniapp/components/activity-card/index.wxml')
const activityLogic = read('miniapp/pages/activity/activity.js')
const activityLogic = readPageLogic('activity')
assert.match(activityTemplate, /activity-categories/, '活动页缺少活动分类')
assert.match(activityTemplate, /activity-calendar/, '活动页缺少日历入口')
assert.match(activityCard, /ui-act__capacity/, '活动卡缺少人数和容量')
@@ -85,7 +106,7 @@ for (const label of ['我的红娘', '我的咨询', '我的活动', '我的预
}
const matchmakerTemplate = read('miniapp/pages/matchmaker/matchmaker.wxml')
const matchmakerLogic = read('miniapp/pages/matchmaker/matchmaker.js')
const matchmakerLogic = readPageLogic('matchmaker')
const counselingTemplate = read('miniapp/pages/counseling-intro/counseling-intro.wxml')
const premaritalTemplate = read('miniapp/pages/premarital-intro/premarital-intro.wxml')
for (const template of [matchmakerTemplate, counselingTemplate, premaritalTemplate]) {
@@ -104,7 +125,7 @@ assert.match(premaritalTemplate, /预约辅导/, '婚前辅导页缺少预约按
assert.match(premaritalTemplate, /服务状态/, '婚前辅导页缺少服务状态')
const contentTemplate = read('miniapp/pages/content/content.wxml')
const contentLogic = read('miniapp/pages/content/content.ts')
const contentLogic = readPageLogic('content')
assert.match(contentTemplate, /content-categories/, '婚恋课堂缺少分类导航')
assert.match(contentTemplate, /content-list/, '婚恋课堂缺少内容列表')
assert.match(contentTemplate, /viewCount/, '婚恋课堂缺少真实浏览量')
@@ -120,7 +141,7 @@ assert.match(antiFraudTemplate, /举报可疑行为/, '婚恋反诈缺少举报
assert.match(antiFraudTemplate, /安全求助/, '婚恋反诈缺少求助入口')
const stationTemplate = read('miniapp/pages/service-station/service-station.wxml')
const stationLogic = read('miniapp/pages/service-station/service-station.ts')
const stationLogic = readPageLogic('service-station')
for (const label of ['地址', '联系电话', '开放时间', '服务项目', '地图定位', '预约到站服务']) {
assert.ok(stationTemplate.includes(label), `服务站缺少 ${label}`)
}