498 lines
15 KiB
JavaScript
498 lines
15 KiB
JavaScript
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' })
|
||
}
|
||
})
|