xiangqinxiaochengxu/miniprogram/pages/match/match.js
2026-04-17 21:28:56 +08:00

453 lines
14 KiB
JavaScript

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' })
}
})