const request = require('../../utils/request') const GENDER_MAP = { 1: 'male', 2: 'female' } const MATCH_STORAGE_KEY = 'my_matches_records' Page({ data: { source: 'manual', activityId: null, activityMatchText: '', 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) { this.setData({ source: options.source || 'manual', activityId: options.activity_id ? Number(options.activity_id) : null }) }, async onShow() { this.syncOperateState() this.syncCurrentUserGender() await this.refreshCurrentUserGender() 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 loadCandidates() { this.setData({ loading: true }) try { const suffix = this.data.activityId ? `&activity_id=${this.data.activityId}` : '' const candidates = await request({ url: `/matches/candidates?source=${this.data.source}${suffix}` }) 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, activityMatchText: this.data.source === 'activity' && this.data.activityId ? `活动ID ${this.data.activityId} · 当前候选仅来自本活动` : '' }) this.syncDeckCandidates() } catch (err) { console.error('Load candidates failed:', err) wx.showToast({ title: '候选加载失败', icon: 'none' }) this.setData({ candidates: [], currentCandidate: null, nextCandidate: null, activeCandidate: null, activeCandidatePersonalityText: '', activityMatchText: '' }) } finally { this.setData({ loading: false }) } }, async triggerAi() { if (!this.ensureCanOperate()) { return } if (this.data.aiMatching) { 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, activityMatchText: '' }) 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, nicknameText: item.nickname || '匿名用户', birthYearRangeText: item.birth_year_range || '年龄未知', 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 } }) }, getMatchRecordsFromStorage() { return wx.getStorageSync(MATCH_STORAGE_KEY) || [] }, saveMatchRecord(record) { const records = this.getMatchRecordsFromStorage() const existingIndex = records.findIndex((item) => item.match_id === record.match_id) const nextRecords = existingIndex >= 0 ? records.map((item, index) => (index === existingIndex ? record : item)) : [record, ...records] wx.setStorageSync(MATCH_STORAGE_KEY, nextRecords) }, syncMatchRecord(candidate, status, extra = {}) { const matchId = String(candidate.user_id) const baseRecord = { match_id: matchId, matched_at: new Date().toLocaleString().replaceAll('/', '-').slice(0, 16), match_status: status, match_status_text: status === 'success' ? '匹配成功' : '匹配失败', fail_reason: extra.fail_reason || '', other_user: { nickname: candidate.nickname, city: candidate.city, education: candidate.education, hobbies: candidate.personality_tags || [], job_industry: candidate.job_industry, job_company: candidate.job_company, income_range: candidate.income_range, self_intro: candidate.self_intro, avatar_url: candidate.avatar_url || '' } } this.saveMatchRecord(baseRecord) }, 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 { const 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.syncMatchRecord(currentCandidate, 'success') this.setData({ mutualMatchVisible: true, mutualMatchName: result.match_info.nickname || '对方' }) } else { this.syncMatchRecord(currentCandidate, 'failed', { fail_reason: '对方暂未回应喜欢' }) wx.showToast({ title: '已发送喜欢', icon: 'success' }) } this.consumeCurrentCandidate(userId) } catch (err) { console.error('Like user failed:', err) if (err && err.message && err.message.includes('当前仅支持异性匹配')) { wx.showToast({ title: '该候选不符合匹配条件,已自动跳过', icon: 'none' }) this.consumeCurrentCandidate(userId) return } 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.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() wx.navigateTo({ url: '/pages/my-matches/my-matches' }) } })