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:
@@ -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' });
|
||||
Reference in New Issue
Block a user