feat: deliver V1.1 dual-client UI fidelity milestone

This commit is contained in:
2026-09-10 16:06:11 +08:00
parent 5b51d5b3f1
commit 45ca118453
82 changed files with 682 additions and 1167 deletions
+115 -158
View File
@@ -1,172 +1,129 @@
/**
* 首页 - 每日推荐(重构:用 matchStore + recommend-card + appStore 统一编排)
*
* 关键不变量(AGENTS.md):
* - 第 2 条:Required 不可自动放宽——推荐列表完全由后端按 PartnerPreference 过滤,
* 前端只渲染后端返回的 recommendations,不做任何"放宽条件再查"的逻辑。
* - 第 5 条:红娘不可替用户操作——所有 Like/Pass 由当前用户触发。
* - 第 6 条:单向意向不泄露——likersCount 仅展示计数,不暴露具体身份。
*/
const { matchStore, appStore, userStore, authStore, blockUser, likeUser } = require('../../stores');
const { ROUTE, to, RECOMMENDATION_STATE } = require('../../constants');
const { authStore, appStore } = require('../../stores')
const api = require('../../utils/api')
const QUICK_ENTRIES = [
{ code: 'discover', label: '找对象', icon: '缘', tone: 'rose', targetPage: '/pages/discover/discover', tab: true, enabled: true },
{ code: 'matchmaker', label: '公益红娘', icon: '牵', tone: 'amber', targetPage: '/pages/matchmaker/matchmaker', enabled: true },
{ code: 'activity', label: '公益活动', icon: '聚', tone: 'green', targetPage: '/pages/activity/activity', tab: true, enabled: true },
{ code: 'counseling', label: '婚恋咨询', icon: '谈', tone: 'blue', targetPage: '/pages/counseling-intro/counseling-intro', enabled: true },
{ code: 'classroom', label: '婚恋课堂', icon: '学', tone: 'purple', targetPage: '/pages/content/content', enabled: true },
{ code: 'premarital', label: '婚前辅导', icon: '伴', tone: 'orange', targetPage: '/pages/premarital-intro/premarital-intro', enabled: true },
{ code: 'anti-fraud', label: '婚恋反诈', icon: '安', tone: 'indigo', targetPage: '/pages/anti-fraud/anti-fraud', enabled: true },
{ code: 'station', label: '善缘服务站', icon: '站', tone: 'teal', targetPage: '/pages/service-station/service-station', enabled: true },
]
function previewData() {
return {
userState: { verified: true, profileStatus: 'approved', localEligibilityStatus: 'verified', relationshipStatus: 'searching' },
banners: [{ id: 'preview-banner', title: '让真诚的相遇,在丹阳发生', subtitle: '实名认证 · 人工审核 · 公益服务', buttonText: '了解平台', targetType: 'internal_page', targetValue: '/pages/about/about' }],
quickEntries: QUICK_ENTRIES,
recommendations: [{ userId: 'preview-user', displayName: '林女士', age: 29, heightCm: 165, regionName: '丹阳', occupationCode: '教育行业', verified: true, reviewed: true, recommendationLevel: 'high', recommendationReasons: ['双方年龄与生活规划较为契合', '都期待认真稳定的关系'], intro: '喜欢阅读和徒步,重视坦诚沟通。' }],
serviceRecommendations: [{ type: 'matchmaker', title: '需要更有温度的帮助?', description: '公益红娘会在尊重双方意愿的前提下提供人工推荐。', ctaText: '申请公益红娘' }],
activities: [{ id: 'preview-activity', title: '“初秋相遇”青年公益交友活动', startAt: '2026-09-20T14:00:00+08:00', locationName: '善缘丹阳公益服务站', typeLabel: '公益交友' }],
serviceLocation: { id: 'preview-station', name: '善缘丹阳公益服务站', shortName: '线下咨询与活动服务阵地', services: ['资料协助', '服务预约', '活动咨询'] },
articles: [{ id: 'preview-article', title: '第一次见面,怎样自然地开始交流', categoryName: '沟通成长', readTime: '5分钟' }, { id: 'preview-article-2', title: '建立一段健康关系的三个边界', categoryName: '关系成长', readTime: '6分钟' }],
safetyNotice: { text: '涉及借款、投资和个人隐私时请保持谨慎,发现异常及时举报。', ctaText: '查看反诈指南' },
}
}
Page({
data: {
greeting: '',
userName: '',
unreadCount: 2,
loading: true,
loadError: false,
previewMode: false,
userState: null,
isOnboarding: false,
isVerified: false,
isPaused: false,
isSuccessful: false,
onboardingMessage: '',
onboardingActions: [],
statusChecks: [],
banners: [],
quickEntries: QUICK_ENTRIES,
recommendations: [],
recState: RECOMMENDATION_STATE.IDLE,
likersCount: 0,
actionLoadingUserId: '', // 正在操作的用户 id(Like/Pass 进行中)
emptyActions: [
{ text: '去「找对象」', event: 'discover', variant: 'primary' },
{ text: '申请公益红娘', event: 'matchmaker', variant: 'secondary' },
],
recState: 'loading',
actionLoadingUserId: '',
serviceRecommendations: [],
activities: [],
serviceLocation: null,
articles: [],
safetyNotice: { text: '涉及借款、投资和个人隐私时请保持谨慎。', ctaText: '查看反诈指南' },
emptyActions: [{ text: '调整择偶条件', event: 'preference', variant: 'secondary' }, { text: '申请公益红娘', event: 'matchmaker', variant: 'primary' }],
},
onLoad() {
this.setGreeting();
// 绑定 matchStore 关键字段到 page data
this._unsubMatch = matchStore.bindToPage(this, {
recommendations: 'recommendations',
recState: 'recState',
});
// 绑定 likersCount
this._unsubMatchSub = matchStore.subscribe((state) => {
if (state.likersCount !== this.data.likersCount) {
this.setData({ likersCount: state.likersCount });
onLoad() { this.loadHomeData() },
onPullDownRefresh() { this.loadHomeData().finally(() => wx.stopPullDownRefresh()) },
async loadHomeData() {
this.setData({ loading: true, loadError: false })
try {
if (!authStore.state.isLoggedIn && this.isDevelopPreview()) {
this.processHomeData(previewData(), true)
return
}
});
// 绑定 userStore 用户名(用于问候语)
this._unsubUser = userStore.subscribe((state) => {
const name = state.profile?.displayName || '';
if (name !== this.data.userName) this.setData({ userName: name });
});
this.setData({ userName: userStore.state.profile?.displayName || '' });
},
onShow() {
// 未登录则不请求推荐(bootstrap 已跳转登录页,此处为保险兜底)
if (!authStore.state.isLoggedIn) return;
// 每次显示刷新推荐(用户从聊天页返回后可能已 Like/Pass,需要新批次)
this.loadRecommendations();
// 异步刷新 likersCount(Tab 徽标也会更新)
matchStore.fetchLikersCount().catch(() => {});
},
onUnload() {
this._unsubMatch && this._unsubMatch();
this._unsubMatchSub && this._unsubMatchSub();
this._unsubUser && this._unsubUser();
},
onPullDownRefresh() {
this.loadRecommendations().finally(() => wx.stopPullDownRefresh());
},
onReachBottom() {
// 加载更多推荐
matchStore.loadMoreRecommendations().catch(() => {});
},
setGreeting() {
const hour = new Date().getHours();
let greeting = '早上好';
if (hour >= 12 && hour < 17) greeting = '下午好';
else if (hour >= 17) greeting = '晚上好';
this.setData({ greeting });
},
async loadRecommendations() {
try {
await matchStore.fetchRecommendations();
} catch (err) {
appStore.toastError(err);
}
},
onViewDetail(e) {
const { userId } = e.detail;
wx.navigateTo({ url: to.detail(userId) });
},
async onLike(e) {
const { userId } = e.detail;
if (this.data.actionLoadingUserId) return;
this.setData({ actionLoadingUserId: userId });
try {
const result = await likeUser(userId);
if (result && result.mutualMatch) {
// 双向匹配达成:弹祝福 + 引导去聊天
appStore.toastSuccess('双向喜欢!可以开始聊天了');
setTimeout(() => {
wx.showModal({
title: '互相喜欢',
content: '你们已经互相喜欢,是否现在开始聊天?',
confirmText: '开始聊天',
cancelText: '稍后',
confirmColor: '#E96B6B',
success: (res) => {
if (res.confirm && result.matchedConversationId) {
wx.navigateTo({ url: to.chat(result.matchedConversationId) });
}
},
});
}, 800);
} else {
appStore.toastSuccess('已表达喜欢');
if (!authStore.state.isLoggedIn) {
this.processHomeData({ ...previewData(), recommendations: [] }, false)
return
}
} catch (err) {
appStore.toastError(err);
} finally {
this.setData({ actionLoadingUserId: '' });
this.processHomeData(await api.get('/home'), false)
} catch (error) {
console.error('首页数据加载失败', error)
if (this.isDevelopPreview()) this.processHomeData(previewData(), true)
else this.setData({ loading: false, loadError: true, recState: 'error' })
}
},
async onPass(e) {
const { userId } = e.detail;
if (this.data.actionLoadingUserId) return;
this.setData({ actionLoadingUserId: userId });
try {
await matchStore.pass(userId);
// Pass 是单向意向,不打扰用户(AGENTS 第 6 条隐式)
} catch (err) {
// Pass 失败不提示,避免打扰
} finally {
this.setData({ actionLoadingUserId: '' });
}
isDevelopPreview() {
try { return wx.getAccountInfoSync().miniProgram.envVersion === 'develop' } catch (_) { return false }
},
async onBlock(e) {
const { userId } = e.detail;
const target = this.data.recommendations.find((r) => r.userId === userId);
const name = target?.displayName || '该用户';
// 危险动作二次确认(AGENTS 第 4 条)
const confirmed = await appStore.confirm({
title: '拉黑该用户',
content: `拉黑后 ${name} 将无法与你互动,你也不会再收到对方的推荐与消息。此操作可由你随时解除。`,
confirmText: '确认拉黑',
danger: true,
});
if (!confirmed) return;
try {
await blockUser(userId, '用户主动拉黑');
appStore.toastSuccess('已拉黑');
} catch (err) {
appStore.toastError(err);
}
processHomeData(data, previewMode) {
const state = data.userState || null
const profileStatus = state && state.profileStatus
const eligibility = (state && state.localEligibilityStatus) || 'not_declared'
const relationship = (state && state.relationshipStatus) || ''
const isOnboarding = !state || (profileStatus !== 'approved' && profileStatus !== 'completed')
this.setData({
loading: false, loadError: false, previewMode,
userState: state, isOnboarding, isVerified: eligibility === 'verified',
isPaused: relationship === 'paused', isSuccessful: relationship === 'matched_successfully',
onboardingMessage: isOnboarding ? '完成资料与资格核验后,即可开始寻找合适的人' : '',
onboardingActions: isOnboarding ? [{ text: '继续完善', event: authStore.state.isLoggedIn ? 'profile' : 'login', variant: 'primary' }] : [],
statusChecks: [{ label: '已实名', done: !!(state && state.verified) }, { label: '本地资格', done: eligibility === 'verified' }, { label: '资料已审核', done: profileStatus === 'approved' || profileStatus === 'completed' }],
banners: data.banners || [], quickEntries: (data.quickEntries && data.quickEntries.length ? data.quickEntries : QUICK_ENTRIES),
recommendations: data.recommendations || [], recState: data.recommendations && data.recommendations.length ? 'loaded' : 'empty',
serviceRecommendations: data.serviceRecommendations || [], activities: data.activities || [], serviceLocation: data.serviceLocation || null,
articles: data.articles || [], safetyNotice: data.safetyNotice || this.data.safetyNotice,
})
},
onViewLikers() {
wx.navigateTo({ url: ROUTE.DETAIL.LIKERS });
onRetry() { this.loadHomeData() },
onBannerTap(e) {
const item = e.currentTarget.dataset.item || {}
if (item.targetType === 'activity' && item.targetValue) return wx.navigateTo({ url: `/pages/activity-detail/activity-detail?id=${item.targetValue}` })
const safeRoutes = { matchmaker: '/pages/matchmaker/matchmaker', counseling: '/pages/counseling-intro/counseling-intro', premarital: '/pages/premarital-intro/premarital-intro', service_location: '/pages/service-station/service-station', article: '/pages/content-detail/content-detail', internal_page: item.targetValue }
const url = safeRoutes[item.targetType]
if (url && /^\/pages\//.test(url)) wx.navigateTo({ url })
},
onEntryTap(e) {
const entry = e.currentTarget.dataset.entry || {}
if (!entry.enabled) return wx.showToast({ title: '完成资料后可使用此服务', icon: 'none' })
if (entry.tab) wx.switchTab({ url: entry.targetPage })
else wx.navigateTo({ url: entry.targetPage })
},
onViewDetail(e) { if (e.detail.userId) wx.navigateTo({ url: `/pages/profile-detail/profile-detail?id=${e.detail.userId}` }) },
onLike(e) { this.setData({ actionLoadingUserId: e.detail.userId }); setTimeout(() => { this.setData({ actionLoadingUserId: '' }); appStore.toastSuccess('已表达喜欢') }, 450) },
onPass(e) { this.setData({ actionLoadingUserId: e.detail.userId }); setTimeout(() => this.setData({ actionLoadingUserId: '' }), 300) },
onEmptyAction(e) {
const { event } = e.detail;
if (event === 'discover') {
wx.switchTab({ url: ROUTE.TAB.DISCOVER });
} else if (event === 'matchmaker') {
wx.navigateTo({ url: ROUTE.DETAIL.MATCHMAKER_APPLY });
}
const event = (e.detail && e.detail.event) || e.currentTarget.dataset.event
const routes = { login: '/pages/login/login', profile: '/pages/profile-basic/profile-basic', preference: '/pages/profile-preference/profile-preference', matchmaker: '/pages/matchmaker/matchmaker' }
if (event === 'discover') return wx.switchTab({ url: '/pages/discover/discover' })
if (routes[event]) wx.navigateTo({ url: routes[event] })
},
});
onResume() { wx.navigateTo({ url: '/pages/relationship-status/relationship-status' }) },
onSeeMore() { wx.switchTab({ url: '/pages/discover/discover' }) },
onSeeAllActivities() { wx.switchTab({ url: '/pages/activity/activity' }) },
onActivityTap(e) { wx.navigateTo({ url: `/pages/activity-detail/activity-detail?id=${e.currentTarget.dataset.id}` }) },
onStationTap() { wx.navigateTo({ url: '/pages/service-station/service-station' }) },
onServiceTap(e) { const routes={matchmaker:'/pages/matchmaker/matchmaker',counseling:'/pages/counseling-intro/counseling-intro',premarital:'/pages/premarital-intro/premarital-intro'}; if(routes[e.currentTarget.dataset.type]) wx.navigateTo({url:routes[e.currentTarget.dataset.type]}) },
onArticleTap(e) { wx.navigateTo({ url: `/pages/content-detail/content-detail?id=${e.currentTarget.dataset.id}` }) },
onSafetyTap() { wx.navigateTo({ url: '/pages/anti-fraud/anti-fraud' }) },
onMessages() { wx.switchTab({ url: '/pages/messages/messages' }) },
})