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
@@ -8,12 +8,45 @@
* - 单向意向保密:本页不接入互选意向提交(仅在 activity-matches 页由用户主动操作,AGENTS 第 6 条)
* - 下拉刷新 + onReachBottom 加载更多
*/
const { activityStore, appStore, authStore } = require('../../stores');
const { to } = require('../../constants');
import { activityStore, appStore, authStore } from '../../stores';
import { to } from '../../constants';
interface ActivityCategory {
code: string;
label: string;
}
/**
* 列表项。沿用共享 Activity 类型,并允许服务端附加分类字段用于本地筛选。
* 分类字段在共享类型中尚未收敛,此处显式声明,避免用 any 绕过类型检查。
*/
interface ActivityListItem {
activityId?: string;
id?: string;
activityType?: string;
category?: string;
[key: string]: unknown;
}
interface ActivityPageData {
activities: ActivityListItem[];
loading: boolean;
loadingMore: boolean;
hasMore: boolean;
firstLoaded: boolean;
activeCategory: string;
categories: ActivityCategory[];
emptyConfig: {
title: string;
description: string;
illustration: string;
actions: Array<{ text: string; event: string; variant: string }>;
};
}
Page({
data: {
activities: [],
activities: [] as ActivityListItem[],
loading: false,
loadingMore: false,
hasMore: true,
@@ -34,11 +67,11 @@ Page({
{ text: '我的活动', event: 'my', variant: 'secondary' },
],
},
},
} as ActivityPageData,
onLoad() {
if (!authStore.state.isLoggedIn) return;
this._unsub = activityStore.bindToPage(this, {
(this as any)._unsub = activityStore.bindToPage(this, {
activities: 'activities',
activitiesHasMore: 'activitiesHasMore',
});
@@ -54,11 +87,11 @@ Page({
},
onUnload() {
if (this._unsub) this._unsub();
if ((this as any)._unsub) (this as any)._unsub();
},
/** 首次或下拉刷新拉取活动列表 */
async fetchActivities(first = false) {
async fetchActivities(_first = false) {
if (this.data.loading) return;
this.setData({ loading: true });
try {
@@ -102,13 +135,13 @@ Page({
this.loadMore();
},
onCategoryTap(e) {
this.setData({ activeCategory: e.currentTarget.dataset.code });
onCategoryTap(e: WechatMiniprogram.TouchEvent) {
this.setData({ activeCategory: e.currentTarget.dataset.code as string });
this.applyActivityFilter();
},
applyActivityFilter() {
const source = activityStore.state.activities || [];
const source = (activityStore.state.activities || []) as ActivityListItem[];
const { activeCategory } = this.data;
this.setData({
activities: activeCategory === 'all'
@@ -118,14 +151,14 @@ Page({
},
/** 点击活动卡片 → 跳转详情 */
onActivityTap(e) {
onActivityTap(e: any) {
const activityId = e.detail.activityId;
if (!activityId) return;
wx.navigateTo({ url: to.activityDetail(activityId) });
},
/** 空状态:去我的活动 */
onEmptyAction(e) {
onEmptyAction(e: any) {
const { event } = e.detail;
if (event === 'my') {
wx.navigateTo({ url: '/pages/my-activities/my-activities' });