418 lines
12 KiB
JavaScript
418 lines
12 KiB
JavaScript
const request = require('../../utils/request')
|
|
|
|
const GENDER_MAP = {
|
|
1: 'male',
|
|
2: 'female'
|
|
}
|
|
|
|
const OPPOSITE_GENDER_MAP = {
|
|
1: 2,
|
|
2: 1
|
|
}
|
|
|
|
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
|
|
})
|
|
},
|
|
|
|
onShow() {
|
|
this.syncOperateState()
|
|
this.syncCurrentUserGender()
|
|
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 })
|
|
},
|
|
|
|
getPreferredCandidateGender() {
|
|
return OPPOSITE_GENDER_MAP[this.data.currentUserGender] || null
|
|
},
|
|
|
|
filterCandidatesByGender(candidates) {
|
|
const preferredGender = this.getPreferredCandidateGender()
|
|
if (!preferredGender) {
|
|
return candidates || []
|
|
}
|
|
|
|
return (candidates || []).filter((candidate) => {
|
|
if (candidate.gender == null) {
|
|
return true
|
|
}
|
|
return Number(candidate.gender) === preferredGender
|
|
})
|
|
},
|
|
|
|
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)
|
|
const filteredCandidates = this.filterCandidatesByGender(normalizedCandidates)
|
|
this.setData({
|
|
candidates: filteredCandidates,
|
|
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()
|
|
if (normalizedCandidates.length > 0 && filteredCandidates.length === 0 && this.data.currentUserGender != null) {
|
|
wx.showToast({ title: '暂无符合性别条件的候选', icon: 'none' })
|
|
}
|
|
} 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 filteredCandidates = this.filterCandidatesByGender(normalizedCandidates)
|
|
const nowText = new Date().toLocaleTimeString().slice(0, 5)
|
|
this.setData({
|
|
candidates: filteredCandidates,
|
|
currentCandidate: null,
|
|
nextCandidate: null,
|
|
activeCandidate: null,
|
|
activeCandidatePersonalityText: '',
|
|
currentIndex: 0,
|
|
dragOffsetX: 0,
|
|
dragRotation: 0,
|
|
swipeHint: '',
|
|
swipeActive: false,
|
|
cardExitClass: '',
|
|
aiSuggestionTimeText: nowText,
|
|
aiSuggestionCount: filteredCandidates.length,
|
|
activityMatchText: ''
|
|
})
|
|
this.syncDeckCandidates()
|
|
if (normalizedCandidates.length > 0 && filteredCandidates.length === 0 && this.data.currentUserGender != null) {
|
|
wx.showToast({ title: '暂无符合性别条件的推荐', icon: 'none' })
|
|
}
|
|
wx.showToast({ title: filteredCandidates.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
|
|
}
|
|
})
|
|
},
|
|
|
|
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
|
|
}
|
|
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.setData({
|
|
mutualMatchVisible: true,
|
|
mutualMatchName: result.match_info.nickname || '对方'
|
|
})
|
|
} else {
|
|
wx.showToast({ title: '已发送喜欢', icon: 'success' })
|
|
}
|
|
|
|
this.consumeCurrentCandidate(userId)
|
|
} catch (err) {
|
|
console.error('Like user failed:', err)
|
|
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' })
|
|
}
|
|
})
|