xiangqinxiaochengxu/miniprogram/pages/match/match.js

498 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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