Files
danyang-match/miniapp/pages/index/index.ts
T
WorkBuddy 08b9cb2468 fix(p21-2e): 红娘服务拦截接线(blocker 首次生效 + 竞态兜底);2d 确认 Onboarding 本地资格已实现;文档回写
P16 遗留 2e(红娘服务拦截):
- checkCanWithdraw 硬编码 0 → 接 MatchmakerServiceMapper.countActiveByUserId(非终态口径
  submitted/assigned/in_service/waiting_user,与 findActiveByOperatorAndUser 一致),blocker 首次真正生效
- notifyMatchmaker TODO 空转 → terminateActiveMatchmakerServices 竞态兜底(残留非终态置 cancelled);
  不发通知:Phase 18 D2 的 notifications.user_id→users(id) FK 教训
- 守卫 MatchmakerWithdrawBlockerGuardTest 4 用例全绿;首跑暴露测试基建坑:@Transactional Service
  是 CGLIB 代理,ReflectionTestUtils 直调私有方法拿到的是代理实例的 null 字段 → 须先
  AopTestUtils.getTargetObject 解包(已在测试内注释说明)
- 另发现历史遗留:countInProgressByOperator 使用白名单外的 'processing'(已登记)

P04-T10:复核确认 Onboarding 本地资格步骤早已实现(onboarding 消息/动作 + 路由 + /me/status 字段),
JIRA ⏳ → ✅;清除首页语义错位的死状态 isVerified(miniapp tsc EXIT=0)

文档:JIRA 收尾行/计划文档 Task 3.1/3.3 勾选
2026-09-19 19:30:38 +08:00

306 lines
12 KiB
TypeScript
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.
/**
* 首页 - V1.1 新版(U18)
* 结构:品牌区 → 用户状态条 → Banner → 8入口 → 推荐 → 服务建议 → 活动 → 服务站 → 安全提醒
*/
const { authStore, appStore, matchStore } = require('../../stores')
const api = require('../../utils/api')
const ENTRY_VISUALS = {
find_object: { tone: 'rose', iconAsset: '/assets/icons/home/find-object.png' },
matchmaker: { tone: 'purple', iconAsset: '/assets/icons/home/matchmaker.png' },
activities: { tone: 'blue', iconAsset: '/assets/icons/home/activity.png' },
counseling: { tone: 'pink', iconAsset: '/assets/icons/home/counseling.png' },
classroom: { tone: 'amber', iconAsset: '/assets/icons/home/classroom.png' },
premarital: { tone: 'indigo', iconAsset: '/assets/icons/home/premarital.png' },
anti_fraud: { tone: 'red', iconAsset: '/assets/icons/home/anti-fraud.png' },
service_station: { tone: 'green', iconAsset: '/assets/icons/home/service-station.png' },
}
const QUICK_ENTRIES = [
{ code: 'find_object', label: '找对象', targetPage: '/pages/discover/discover', enabled: true },
{ code: 'matchmaker', label: '公益红娘', targetPage: '/pages/matchmaker/matchmaker', enabled: true },
{ code: 'activities', label: '公益活动', targetPage: '/pages/activity/activity', enabled: true },
{ code: 'counseling', label: '婚恋咨询', targetPage: '/pages/counseling-intro/counseling-intro', enabled: true },
{ code: 'classroom', label: '婚恋课堂', targetPage: '/pages/content/content', enabled: true },
{ code: 'premarital', label: '婚前辅导', targetPage: '/pages/premarital-intro/premarital-intro', enabled: true },
{ code: 'anti_fraud', label: '婚恋反诈', targetPage: '/pages/anti-fraud/anti-fraud', enabled: true },
{ code: 'service_station', label: '善缘服务站', targetPage: '/pages/service-station/service-station', enabled: true },
].map((entry) => ({ ...entry, ...ENTRY_VISUALS[entry.code] }))
Page({
data: {
unreadCount: 0,
loading: true,
loadError: false,
userState: null,
isOnboarding: false,
isPaused: false,
isSuccessful: false,
onboardingMessage: '',
onboardingActions: [],
statusChecks: [],
banners: [],
currentBannerIndex: 0,
quickEntries: QUICK_ENTRIES,
recommendations: [],
recState: 'idle',
actionLoadingUserId: '',
serviceRecommendations: [],
activities: [],
serviceLocation: null,
articles: [],
safetyNotice: {
text: '实名认证和人工审核可以提高信息真实性,交往过程中仍请谨慎涉及借款、投资和个人隐私。',
ctaText: '查看婚恋反诈指南',
ctaTarget: ''
},
emptyActions: [
{ text: '查看择偶条件', event: 'preference', variant: 'secondary' },
{ text: '申请公益红娘', event: 'matchmaker', variant: 'primary' },
],
},
onLoad() {
this.checkAuthAndLoad()
},
onShow() {
if (authStore.state.isLoggedIn && !this.data.loading) {
this.loadHomeData()
}
},
onPullDownRefresh() {
this.loadHomeData().finally(() => wx.stopPullDownRefresh())
},
// ==================== 核心加载 ====================
async checkAuthAndLoad() {
if (!authStore.state.isLoggedIn) {
this.setData({
loading: false,
isOnboarding: true,
onboardingMessage: '登录后即可开始寻找合适的人',
onboardingActions: [{ text: '去登录', event: 'login', variant: 'primary' }],
})
return
}
await this.loadHomeData()
},
async loadHomeData() {
this.setData({ loading: true, loadError: false })
try {
const res = await api.get('/home')
this.processHomeData(res)
} catch (err) {
console.error('首页数据加载失败', err)
this.setData({ loading: false, loadError: true, recState: 'error' })
}
},
processHomeData(data) {
const { userState, banners, quickEntries, recommendations,
serviceRecommendations, activities, serviceLocation, safetyNotice } = data
const relStatus = userState?.relationshipStatus || ''
const profileStatus = userState?.profileStatus || ''
const eligStatus = userState?.localEligibilityStatus || 'not_declared'
const isOnboarding = !userState || !userState.verified || (profileStatus !== 'approved' && profileStatus !== 'completed')
const isPaused = relStatus === 'paused'
const isSuccessful = relStatus === 'matched_successfully'
// 注意:本地资格核验通过与否由 checks 里的「本地资格」项表达(eligStatus === 'verified')。
// 此前曾有一个 isVerified = eligStatus === 'verified' 的死状态 —— 名字像「实名」实为本地资格,
// WXML/WXSS 零引用且语义错位(实名判定是 userState.verified),已移除以防未来误用。
const checks = [
{ label: '已实名', done: userState?.verified || false },
{ label: '本地资格', done: eligStatus === 'verified' },
{ label: '资料已审核', done: profileStatus === 'approved' || profileStatus === 'completed' },
]
this.setData({
loading: false, loadError: false,
userState,
isOnboarding, isPaused, isSuccessful,
onboardingMessage: this.buildOnboardingMessage(userState, profileStatus, eligStatus),
onboardingActions: this.buildOnboardingActions(userState, profileStatus, eligStatus),
statusChecks: checks,
banners: banners || [],
quickEntries: quickEntries?.length
? QUICK_ENTRIES.map((defaultEntry) => {
const serverEntry = quickEntries.find((entry) => entry.code === defaultEntry.code)
return serverEntry
? { ...defaultEntry, ...serverEntry, ...ENTRY_VISUALS[defaultEntry.code] }
: { ...defaultEntry, enabled: false, disabledReason: '服务暂未开放' }
})
: QUICK_ENTRIES,
recommendations: recommendations || [],
recState: (recommendations && recommendations.length > 0) ? 'loaded' : 'empty',
serviceRecommendations: serviceRecommendations || [], activities: activities || [], serviceLocation,
articles: data.articles || [],
safetyNotice: safetyNotice || this.data.safetyNotice,
})
},
buildOnboardingMessage(state, profileStatus, eligStatus) {
if (!state) return '登录后即可开始寻找合适的人'
if (!state.verified) return '完成实名认证后,即可继续完善资料并使用公益婚恋服务'
if (profileStatus === 'pending_review') return '资料正在人工审核,请耐心等待审核结果'
if (eligStatus === 'not_declared') return '完成本地资格声明后即可开始寻找合适的人'
if (eligStatus === 'needs_evidence') return '补充本地资格佐证后即可开始寻找合适的人'
if (profileStatus !== 'approved' && profileStatus !== 'completed') return '资料审核通过后即可开始寻找合适的人'
return '完成资料后开始寻找合适的人'
},
buildOnboardingActions(state, profileStatus, eligStatus) {
if (!state) return [{ text: '去登录', event: 'login', variant: 'primary' }]
const actions = []
if (eligStatus === 'not_declared' || eligStatus === 'needs_evidence') {
actions.push({ text: '声明本地资格', event: 'local-eligibility', variant: 'primary' })
}
if (profileStatus !== 'approved' && profileStatus !== 'completed') {
actions.push({ text: '完善资料', event: 'profile', variant: 'secondary' })
}
if (actions.length === 0) {
actions.push({ text: '开始寻找', event: 'discover', variant: 'primary' })
}
return actions
},
// ==================== 事件处理 ====================
onBannerChange(e) {
this.setData({ currentBannerIndex: e.detail.current })
},
onBannerTap(e) {
const { targetType, targetValue } = e.currentTarget.dataset.item
if (targetType === 'activity' && targetValue) {
wx.navigateTo({ url: `/pages/activity-detail/activity-detail?id=${targetValue}` })
} else if (targetType === 'matchmaker') {
wx.navigateTo({ url: '/pages/matchmaker/matchmaker' })
} else {
const routes = { 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: targetValue }
const url = routes[targetType]
if (url && /^\/pages\//.test(url)) wx.navigateTo({ url })
}
},
onEntryTap(e) {
const { code, enabled, targetPage } = e.currentTarget.dataset.entry
if (!enabled) {
wx.showToast({ title: e.currentTarget.dataset.entry.disabledReason || '暂不可使用此功能', icon: 'none' })
return
}
if (targetPage === '/pages/discover/discover' || targetPage === '/pages/activity/activity') wx.switchTab({ url: targetPage })
else if (targetPage?.startsWith('/pages/')) wx.navigateTo({ url: targetPage })
},
onViewDetail(e) {
const userId = e.detail?.userId || e.currentTarget.dataset?.userId
if (userId) wx.navigateTo({ url: `/pages/profile-detail/profile-detail?id=${userId}` })
},
async onLike(e) {
const userId = e.detail?.userId
if (!userId || this.data.actionLoadingUserId) return
this.setData({ actionLoadingUserId: userId })
try {
const result = await matchStore.like(userId)
this.setData({ recommendations: this.data.recommendations.filter((item) => item.userId !== userId) })
if (result?.mutualMatch) {
appStore.toastSuccess('双向喜欢!可以开始聊天了')
if (result.matchedConversationId) {
wx.navigateTo({ url: `/pages/chat/chat?id=${result.matchedConversationId}` })
}
} else {
appStore.toastSuccess('已表达喜欢')
}
} catch (err) {
appStore.toastError(err instanceof Error ? err.message : '操作失败')
} finally {
this.setData({ actionLoadingUserId: '' })
}
},
async onPass(e) {
const userId = e.detail?.userId
if (!userId || this.data.actionLoadingUserId) return
this.setData({ actionLoadingUserId: userId })
try {
await matchStore.pass(userId)
this.setData({ recommendations: this.data.recommendations.filter((item) => item.userId !== userId) })
appStore.toastSuccess('已略过')
} catch (err) {
appStore.toastError(err instanceof Error ? err.message : '操作失败')
} finally {
this.setData({ actionLoadingUserId: '' })
}
},
onEmptyAction(e) {
const event = e.currentTarget.dataset.event
switch (event) {
case 'login':
wx.reLaunch({ url: '/pages/login/login' })
break
case 'discover':
wx.switchTab({ url: '/pages/discover/discover' })
break
case 'matchmaker':
wx.navigateTo({ url: '/pages/matchmaker/matchmaker' })
break
case 'preference':
wx.navigateTo({ url: '/pages/profile-preference/profile-preference' })
break
case 'local-eligibility':
wx.navigateTo({ url: '/pages/local-eligibility/local-eligibility' })
break
case 'profile':
wx.navigateTo({ url: '/pages/profile-basic/profile-basic' })
break
}
},
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) {
const id = e.currentTarget.dataset.id
if (id) wx.navigateTo({ url: `/pages/activity-detail/activity-detail?id=${id}` })
},
onStationTap() {
wx.navigateTo({ url: '/pages/service-station/service-station' })
},
onServiceTap(e) {
const type = e.currentTarget.dataset.type
switch (type) {
case 'matchmaker': wx.navigateTo({ url: '/pages/matchmaker/matchmaker' })
break
case 'counseling':
wx.navigateTo({ url: '/pages/counseling-intro/counseling-intro' })
break
case 'premarital':
wx.navigateTo({ url: '/pages/premarital-intro/premarital-intro' })
break
}
},
onSafetyTap() {
wx.navigateTo({ url: '/pages/anti-fraud/anti-fraud' })
},
onArticleTap(e) { wx.navigateTo({ url: `/pages/content-detail/content-detail?id=${e.currentTarget.dataset.id}` }) },
onMessages() { wx.switchTab({ url: '/pages/messages/messages' }) },
formatDate(timestamp) {
if (!timestamp) return ''
const d = new Date(timestamp)
return `${d.getMonth() + 1}月${d.getDate()}日 ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
},
})