CI/CD Pipeline / Test Shell Guard (Phase 23 Gate) (push) Canceled after 0s
CI/CD Pipeline / Backend Build & Test (push) Canceled after 0s
CI/CD Pipeline / Admin Web Build (push) Canceled after 0s
CI/CD Pipeline / Mini Program Compile Check (push) Canceled after 0s
CI/CD Pipeline / Migration Validation (push) Canceled after 0s
CI/CD Pipeline / UI Contracts (source-structure gates) (push) Canceled after 0s
CI/CD Pipeline / E2E (live stack) (push) Canceled after 0s
335 lines
14 KiB
TypeScript
335 lines
14 KiB
TypeScript
/**
|
||
* 首页 - V1.1 新版(U18)
|
||
* 结构:品牌区 → 用户状态条 → Banner → 8入口 → 推荐 → 服务建议 → 活动 → 服务站 → 安全提醒
|
||
*/
|
||
const { authStore, appStore, matchStore } = require('../../stores')
|
||
const api = require('../../utils/api')
|
||
// 顶部安全区:实测右上角胶囊按钮矩形,避免品牌栏入口被胶囊压住(见 utils/nav-metrics.js)
|
||
const { navSafeData } = require('../../utils/nav-metrics')
|
||
|
||
// 4×2 宫格的视觉映射(tone → index.wxss 的 .tone-* 渐变砖;subtitle → 主标题下的说明行)。
|
||
// 图标不再走图片资源:改成「CSS 渐变砖 + 白色矢量图形」(见 index.wxss .tone-* 的 data-uri 背景层),
|
||
// 所以这里没有 iconAsset。tone 名与色相语义一一对应(app.wxss --gradient-entry-* 注释里有实算对比度):
|
||
// 3.01:1 是「白图形 vs 渐变较亮端」的实算值,7 个色相均 ≥3:1(WCAG 1.4.11 非文本图形)。
|
||
const ENTRY_VISUALS = {
|
||
find_object: { tone: 'rose', subtitle: '真实邂逅' },
|
||
matchmaker: { tone: 'orange', subtitle: '官方牵线' },
|
||
activities: { tone: 'violet', subtitle: '线下联谊' },
|
||
counseling: { tone: 'blue', subtitle: '专业指导' },
|
||
classroom: { tone: 'purple', subtitle: '提升自我' },
|
||
premarital: { tone: 'pink', subtitle: '幸福起航' },
|
||
anti_fraud: { tone: 'green', subtitle: '守护真爱' },
|
||
service_station: { tone: 'amber', subtitle: '就在身边' },
|
||
}
|
||
|
||
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: {
|
||
navVars: '',
|
||
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() {
|
||
// 顶部安全区先算:onLoad 早于首次渲染,避免出现「先压住胶囊再跳开」的闪动
|
||
this.setData(navSafeData())
|
||
// 订阅登录态:会话失效时回到登录引导;刷新成功且首页停在错误态时自动重试。
|
||
this._unsubAuth = authStore.subscribe((state) => {
|
||
if (state.authStatus === 'expired') {
|
||
this.setData({ loading: false, loadError: false, recState: 'idle' })
|
||
this.checkAuthAndLoad()
|
||
return
|
||
}
|
||
if (state.isLoggedIn && this.data.loadError && !this.data.loading) {
|
||
this.loadHomeData()
|
||
}
|
||
})
|
||
this.checkAuthAndLoad()
|
||
},
|
||
|
||
onShow() {
|
||
if (authStore.state.isLoggedIn && !this.data.loading) {
|
||
this.loadHomeData()
|
||
}
|
||
},
|
||
|
||
onUnload() {
|
||
if (typeof this._unsubAuth === 'function') {
|
||
this._unsubAuth()
|
||
this._unsubAuth = null
|
||
}
|
||
},
|
||
|
||
/** 错误态「重试」按钮:重新鉴权判断并加载首页 */
|
||
onRetry() {
|
||
this.checkAuthAndLoad()
|
||
},
|
||
|
||
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('双向意向匹配成功,可查看公益服务')
|
||
} 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.navigateTo({ url: '/pages/notifications/notifications' }) },
|
||
|
||
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')}`
|
||
},
|
||
})
|