const request = require('../../utils/request') const { resolveImageUrl } = require('../../utils/media') const GENDER_MAP = { 1: 'male', 2: 'female' } Page({ data: { source: 'manual', activityId: null, activityTitle: '', activityMatchText: '', stageText: '', selectionSummaryText: '', candidates: [], currentCandidate: null, nextCandidate: null, loading: false, activeCandidate: null, activeCandidatePersonalityText: '', currentIndex: 0, dragOffsetX: 0, dragRotation: 0, swipeHint: '', swipeActive: false, dragging: false, cardExitClass: '', mutualMatchVisible: false, mutualMatchName: '', canOperate: false, aiMatching: false, aiSuggestionTimeText: '', aiSuggestionCount: 0, currentUserGender: null }, onLoad(options) { const storedEntry = wx.getStorageSync('match_entry_context') || {} const source = options.source || storedEntry.source || 'manual' const activityId = options.activity_id ? Number(options.activity_id) : storedEntry.activity_id ? Number(storedEntry.activity_id) : null this.setData({ source, activityId }) if (storedEntry.source || storedEntry.activity_id) { wx.removeStorageSync('match_entry_context') } }, onShareAppMessage() { if (this.data.source === 'activity' && this.data.activityId) { return { title: this.data.activityTitle || '一起看看这场活动', path: `/pages/activity-detail/activity-detail?id=${this.data.activityId}` } } return { title: '来小程序看看活动和匹配吧', path: '/pages/home/home' } }, async onShow() { this.syncOperateState() this.syncCurrentUserGender() await this.refreshCurrentUserGender() if (this.data.source === 'activity' && this.data.activityId) { await this.loadActivityMatchState() } await this.loadCandidates() }, syncOperateState() { this.setData({ canOperate: !!wx.getStorageSync('token') }) }, syncCurrentUserGender() { const app = getApp() const userInfo = app && app.globalData ? app.globalData.userInfo : null const currentUserGender = userInfo && userInfo.gender != null ? Number(userInfo.gender) : null this.setData({ currentUserGender }) }, async refreshCurrentUserGender() { try { const userInfo = await request({ url: '/users/me' }) const app = getApp() if (app && app.globalData) { app.globalData.userInfo = userInfo } const currentUserGender = userInfo && userInfo.gender != null ? Number(userInfo.gender) : null this.setData({ currentUserGender }) return currentUserGender } catch (err) { console.warn('Refresh current user gender failed:', err) return this.data.currentUserGender } }, ensureCanOperate() { const canOperate = !!wx.getStorageSync('token') this.setData({ canOperate }) if (!canOperate) { wx.showToast({ title: '请先登录后再操作', icon: 'none' }) return false } return true }, async loadActivityMatchState() { try { const state = await request({ url: `/activities/${this.data.activityId}/match-state` }) const stageTextMap = { before_start: '活动未开始,暂不可查看嘉宾信息', matching_open: '右滑加入我的选择,最多 3 位嘉宾', matching_closed: '匹配已截止,仅可查看结果', ended: '活动已结束' } this.setData({ activityTitle: state.activity_title || '', stageText: stageTextMap[state.stage] || '', selectionSummaryText: `已选 ${state.selected_count || 0}/${state.selection_limit || 3},剩余 ${state.remaining_count || 0}`, activityMatchText: state.match_deadline ? `截止时间:${state.match_deadline}` : '', canOperate: !!state.can_choose }) } catch (err) { console.warn('Load activity match state failed:', err) this.setData({ stageText: '', selectionSummaryText: '', activityMatchText: '' }) } }, async loadCandidates() { this.setData({ loading: true }) try { let candidates = [] if (this.data.activityId) { this.setData({ source: 'activity' }) candidates = await request({ url: `/activities/${this.data.activityId}/candidates` }) } else { candidates = await request({ url: `/matches/candidates?source=${this.data.source}` }) } const normalizedCandidates = this.normalizeCandidates(candidates) this.setData({ candidates: normalizedCandidates, currentCandidate: null, nextCandidate: null, activeCandidate: null, activeCandidatePersonalityText: '', currentIndex: 0, dragOffsetX: 0, dragRotation: 0, swipeHint: '', swipeActive: false, cardExitClass: '', aiSuggestionTimeText: '', aiSuggestionCount: 0 }) this.syncDeckCandidates() if (this.data.source === 'activity') { wx.showToast({ title: normalizedCandidates.length ? '已进入活动匹配' : '暂无可匹配对象', icon: 'none' }) } } catch (err) { console.error('Load candidates failed:', err) wx.showToast({ title: err.message || '候选加载失败', icon: 'none' }) this.setData({ candidates: [], currentCandidate: null, nextCandidate: null, activeCandidate: null, activeCandidatePersonalityText: '' }) } finally { this.setData({ loading: false }) } }, async triggerAi() { if (!this.ensureCanOperate()) { return } if (this.data.aiMatching || this.data.activityId || this.data.source === 'activity') { return } this.setData({ loading: true, source: 'ai', aiMatching: true }) wx.showLoading({ title: 'AI 推荐中...' }) try { const candidates = await request({ url: '/matches/ai-suggest', method: 'POST' }) const normalizedCandidates = this.normalizeCandidates(candidates) const nowText = new Date().toLocaleTimeString().slice(0, 5) this.setData({ candidates: normalizedCandidates, currentCandidate: null, nextCandidate: null, activeCandidate: null, activeCandidatePersonalityText: '', currentIndex: 0, dragOffsetX: 0, dragRotation: 0, swipeHint: '', swipeActive: false, cardExitClass: '', aiSuggestionTimeText: nowText, aiSuggestionCount: normalizedCandidates.length }) this.syncDeckCandidates() wx.showToast({ title: normalizedCandidates.length ? 'AI 推荐已更新' : '暂时没有新的推荐', icon: 'none' }) } catch (err) { console.error('Trigger AI match failed:', err) wx.showToast({ title: 'AI 匹配失败,请重试', icon: 'none' }) this.setData({ candidates: [], currentCandidate: null, nextCandidate: null, activeCandidate: null, activeCandidatePersonalityText: '' }) } finally { wx.hideLoading() this.setData({ loading: false, aiMatching: false }) } }, getCurrentCandidate() { return this.data.candidates[this.data.currentIndex] || null }, getNextCandidate() { return this.data.candidates[this.data.currentIndex + 1] || null }, syncDeckCandidates() { this.setData({ currentCandidate: this.getCurrentCandidate(), nextCandidate: this.getNextCandidate() }) }, normalizeCandidates(candidates) { return (candidates || []).map((item) => { const personalityTags = Array.isArray(item.personality_tags) ? item.personality_tags : [] const matchReasons = Array.isArray(item.match_reasons) ? item.match_reasons : [] const matchScore = Number(item.match_score) const hasMatchScore = Number.isFinite(matchScore) const safeMatchScore = hasMatchScore ? Math.max(0, Math.min(matchScore, 100)) : null const matchScoreLevel = hasMatchScore ? (safeMatchScore >= 80 ? 'high' : safeMatchScore >= 60 ? 'mid' : 'low') : 'none' return { ...item, avatar_blur_url: resolveImageUrl(item.avatar_blur_url || ''), nicknameText: item.nickname || '匿名用户', birthYearText: item.birth_year != null ? String(item.birth_year) : '出生年份未知', cityText: item.city || '城市未知', personality_tags: personalityTags, matchScoreText: hasMatchScore ? safeMatchScore.toFixed(1) : '', matchScorePercent: hasMatchScore ? safeMatchScore.toFixed(1) : '0', hasMatchScore, matchScoreLevel, matchReasonsText: matchReasons.join(' / '), hasMatchReasons: matchReasons.length > 0, selected: !!item.selected } }) }, openCard(e) { const candidate = this.getCurrentCandidate() || this.data.candidates.find((item) => item.user_id === e.currentTarget.dataset.id) this.setData({ activeCandidate: candidate || null, activeCandidatePersonalityText: candidate && candidate.personality_tags ? candidate.personality_tags.join('、') : '' }) }, closeCard() { this.setData({ activeCandidate: null, activeCandidatePersonalityText: '' }) }, noop() {}, handleTouchStart(e) { if (!this.ensureCanOperate()) { return } if (!this.getCurrentCandidate()) { return } const touch = e.touches && e.touches[0] if (!touch) { return } this.touchStartX = touch.clientX this.touchStartY = touch.clientY this.setData({ dragging: true, swipeActive: false, cardExitClass: '' }) }, handleTouchMove(e) { if (!this.data.canOperate) { return } if (!this.data.dragging || this.touchStartX === undefined) { return } const touch = e.touches && e.touches[0] if (!touch) { return } const deltaX = touch.clientX - this.touchStartX const deltaY = (touch.clientY || 0) - (this.touchStartY || 0) if (Math.abs(deltaY) > 18 && Math.abs(deltaY) > Math.abs(deltaX)) { return } const offsetX = Math.max(Math.min(deltaX, 320), -320) const rotation = Math.max(Math.min(offsetX / 18, 14), -14) const swipeHint = offsetX > 36 ? 'like' : offsetX < -36 ? 'skip' : '' this.setData({ dragOffsetX: offsetX, dragRotation: rotation, swipeHint, swipeActive: !!swipeHint }) }, handleTouchEnd() { if (!this.data.canOperate) { this.resetSwipeState() return } if (!this.data.dragging) { return } const currentCandidate = this.getCurrentCandidate() const offsetX = this.data.dragOffsetX this.setData({ dragging: false }) this.touchStartX = undefined this.touchStartY = undefined if (!currentCandidate) { this.resetSwipeState() return } const commitThreshold = 90 if (offsetX >= commitThreshold) { this.likeCurrentCandidate(currentCandidate.user_id) return } if (offsetX <= -commitThreshold) { this.skipCurrentCandidate(currentCandidate.user_id) return } this.resetSwipeState() }, handleTouchCancel() { this.touchStartX = undefined this.touchStartY = undefined this.resetSwipeState() }, resetSwipeState() { this.setData({ dragOffsetX: 0, dragRotation: 0, swipeHint: '', swipeActive: false, dragging: false, cardExitClass: '' }) this.touchStartX = undefined this.touchStartY = undefined this.syncDeckCandidates() }, playExitAnimation(direction, callback) { const className = direction === 'like' ? 'card-exit-like' : 'card-exit-skip' this.setData({ cardExitClass: className, swipeHint: direction, swipeActive: true }) setTimeout(() => { callback() }, 220) }, consumeCurrentCandidate(userId) { const remaining = this.data.candidates.filter((item) => item.user_id !== userId) this.setData({ candidates: remaining, currentIndex: 0, currentCandidate: null, nextCandidate: null, activeCandidate: this.data.activeCandidate && this.data.activeCandidate.user_id === userId ? null : this.data.activeCandidate, activeCandidatePersonalityText: this.data.activeCandidate && this.data.activeCandidate.user_id === userId ? '' : this.data.activeCandidatePersonalityText }) this.resetSwipeState() this.syncDeckCandidates() }, async likeCurrentCandidate(userId) { if (!this.ensureCanOperate()) { return } const currentCandidate = this.getCurrentCandidate() if (!currentCandidate || currentCandidate.user_id !== userId) { wx.showToast({ title: '候选不存在或已失效', icon: 'none' }) this.resetSwipeState() return } this.playExitAnimation('like', async () => { try { let result if (this.data.source === 'activity' && this.data.activityId) { result = await request({ url: `/activities/${this.data.activityId}/choices`, method: 'POST', data: { to_user_id: userId } }) this.setData({ selectionSummaryText: `已选 ${result.selected_count || 0}/3,剩余 ${result.remaining_count || 0}` }) } else { result = await request({ url: '/matches/like', method: 'POST', data: { to_user_id: userId, source: this.data.source, activity_id: this.data.activityId } }) } if (result.is_mutual) { this.setData({ mutualMatchVisible: true, mutualMatchName: currentCandidate.nicknameText || '对方' }) } else { wx.showToast({ title: this.data.source === 'activity' ? '已加入我的选择' : '已发送喜欢', icon: 'success' }) } this.consumeCurrentCandidate(userId) } catch (err) { console.error('Like user failed:', err) wx.showToast({ title: err.message || '操作失败', icon: 'none' }) this.resetSwipeState() } }) }, skipCurrentCandidate(userId) { if (!this.ensureCanOperate()) { return } this.playExitAnimation('skip', () => { this.consumeCurrentCandidate(userId) }) }, async likeUser(e) { const userId = e.currentTarget.dataset.id this.likeCurrentCandidate(userId) }, getAiStatusText() { if (this.data.activityId || this.data.source === 'activity') { return this.data.stageText || '仅展示当前活动内可匹配嘉宾' } if (this.data.aiMatching) { return 'AI 正在推荐中' } if (this.data.aiSuggestionTimeText) { return `上次更新 ${this.data.aiSuggestionTimeText}` } return '点击获取新的推荐' }, skipUser(e) { const userId = e.currentTarget.dataset.id this.skipCurrentCandidate(userId) }, closeMutualMatch() { this.setData({ mutualMatchVisible: false, mutualMatchName: '' }) }, goMyMatches() { this.closeMutualMatch() if (this.data.source === 'activity' && this.data.activityId) { wx.navigateTo({ url: `/pages/my-matches/my-matches?activity_id=${this.data.activityId}` }) return } wx.navigateTo({ url: '/pages/my-matches/my-matches' }) } })