CI/CD Pipeline / Backend Build & Test (push) Canceled after 0s
CI/CD Pipeline / Admin Web Build (push) Canceled after 0s
CI/CD Pipeline / Mini Program Compile Check (push) Canceled after 0s
CI/CD Pipeline / Migration Validation (push) Canceled after 0s
CI/CD Pipeline / Test Shell Guard (Phase 23 Gate) (push) Canceled after 0s
CI/CD Pipeline / UI Contracts (source-structure gates) (push) Canceled after 0s
CI/CD Pipeline / E2E (live stack) (push) Canceled after 0s
310 lines
9.7 KiB
Vue
310 lines
9.7 KiB
Vue
<template>
|
||
<PageLayout title="活动互选">
|
||
<div class="mr">
|
||
<UiErrorState v-if="error" @retry="load" />
|
||
<UiLoading v-else-if="loading" />
|
||
|
||
<!-- 单个互选对象:确认 / 等待 / 已确认 -->
|
||
<template v-else-if="hasPartner">
|
||
<div class="mr__partner">
|
||
<UiAvatar :src="partnerAvatar" :name="partnerName" :size="60" />
|
||
<div class="mr__partner-name">{{ partnerName }}</div>
|
||
<div class="mr__partner-hint">活动互选一致,双方确认后才可开启沟通</div>
|
||
</div>
|
||
|
||
<UiEmpty v-if="!status.exists && !statusLoaded" description="未找到该活动的互选记录" />
|
||
|
||
<div v-if="bothConfirmed" class="mr__status mr__status--success">
|
||
<div class="mr__status-icon mr__status-icon--success">✓</div>
|
||
<div class="mr__status-title">双方已确认</div>
|
||
<div class="mr__status-hint">
|
||
{{ hasActiveMatch ? '你们已有沟通入口,可直接进入缘分中心查看' : '沟通入口已开启,可前往缘分中心查看' }}
|
||
</div>
|
||
<van-button type="primary" round @click="goFate">
|
||
{{ hasActiveMatch ? '查看已有沟通' : '查看沟通' }}
|
||
</van-button>
|
||
</div>
|
||
|
||
<div v-else-if="partnerDeclined" class="mr__status">
|
||
<div class="mr__status-icon">💤</div>
|
||
<div class="mr__status-title">对方暂不联系</div>
|
||
<div class="mr__status-hint">对方已选择暂不开通沟通,您可以稍后再看看其他缘分。</div>
|
||
<van-button round plain @click="router.back()">返回</van-button>
|
||
</div>
|
||
|
||
<div v-else-if="myAccepted" class="mr__status">
|
||
<div class="mr__status-icon">⏳</div>
|
||
<div class="mr__status-title">已发送邀请</div>
|
||
<div class="mr__status-hint">对方确认后将收到平台通知</div>
|
||
<div class="mr__status-actions">
|
||
<van-button type="primary" round @click="goWaiting">查看等待状态</van-button>
|
||
<van-button round plain type="danger" :loading="acting === 'decline'" @click="onDecline">
|
||
暂不联系
|
||
</van-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="mr__status">
|
||
<div class="mr__status-icon">💬</div>
|
||
<div class="mr__status-title">向对方表达兴趣</div>
|
||
<div class="mr__status-hint">双方确认后才会开启沟通并收到平台通知</div>
|
||
<div class="mr__status-actions">
|
||
<van-button type="primary" round :loading="acting === 'confirm'" @click="onConfirm">
|
||
确认开启沟通
|
||
</van-button>
|
||
<van-button round plain @click="onDecline" :loading="acting === 'decline'">
|
||
暂不联系
|
||
</van-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 互选结果列表 -->
|
||
<template v-else>
|
||
<div v-if="results.length">
|
||
<div
|
||
v-for="r in results"
|
||
:key="r.partnerId"
|
||
class="mr__card"
|
||
@click="openPartner(r)"
|
||
>
|
||
<UiAvatar :src="avatarOf(r)" :name="r.partnerName" :size="48" />
|
||
<div class="mr__card-main">
|
||
<div class="mr__card-name">{{ r.partnerName || '新朋友' }}</div>
|
||
<UiTag :type="r.bothConfirmed ? 'success' : 'warning'">
|
||
{{ r.bothConfirmed ? '双方已确认' : '等待确认' }}
|
||
</UiTag>
|
||
</div>
|
||
<van-icon name="arrow" color="#9aa0b5" />
|
||
</div>
|
||
</div>
|
||
<UiEmpty v-else description="暂无互选结果">
|
||
<span class="mr__empty-hint">参加活动并向感兴趣的人表达意向后,双方一致即可在此查看</span>
|
||
</UiEmpty>
|
||
</template>
|
||
</div>
|
||
</PageLayout>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { activityMutualService } from '@/api/services'
|
||
import { useAppStore } from '@/stores/app'
|
||
import PageLayout from '@/layouts/PageLayout.vue'
|
||
import UiAvatar from '@/components/UiAvatar.vue'
|
||
import UiTag from '@/components/UiTag.vue'
|
||
import UiLoading from '@/components/UiLoading.vue'
|
||
import UiEmpty from '@/components/UiEmpty.vue'
|
||
import UiErrorState from '@/components/UiErrorState.vue'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const appStore = useAppStore()
|
||
|
||
const activityId = computed(() => String(route.query.id || ''))
|
||
const partnerId = computed(() => String(route.query.partnerId || ''))
|
||
const hasPartner = computed(() => !!partnerId.value)
|
||
|
||
const loading = ref(true)
|
||
const error = ref(false)
|
||
const results = ref<any[]>([])
|
||
const status = ref<any>({ exists: false })
|
||
const statusLoaded = ref(false)
|
||
const partnerName = ref('新朋友')
|
||
const partnerAvatar = ref('')
|
||
const hasActiveMatch = ref(false)
|
||
const acting = ref('')
|
||
|
||
const myAccepted = computed(() => status.value?.myStatus === 'accepted')
|
||
const bothConfirmed = computed(() => !!status.value?.bothConfirmed)
|
||
const partnerDeclined = computed(() => status.value?.partnerStatus === 'declined')
|
||
|
||
function avatarOf(r: any): string {
|
||
const raw = r.partnerAvatar || r.partnerAvatarFileId
|
||
return typeof raw === 'string' && raw.includes('://') ? raw : ''
|
||
}
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
error.value = false
|
||
try {
|
||
if (!activityId.value) {
|
||
results.value = []
|
||
return
|
||
}
|
||
if (hasPartner.value) {
|
||
await loadPartner()
|
||
} else {
|
||
const result: any = await activityMutualService.getResults(activityId.value)
|
||
results.value = result?.items || result?.records || (Array.isArray(result) ? result : [])
|
||
}
|
||
} catch (err: any) {
|
||
error.value = true
|
||
appStore.toastError(err?.message || '加载互选结果失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadPartner() {
|
||
const [statusRes, listRes]: any[] = await Promise.all([
|
||
activityMutualService.getMyConsentStatus(activityId.value, partnerId.value).catch(() => null),
|
||
activityMutualService.getResults(activityId.value).catch(() => null),
|
||
])
|
||
status.value = statusRes || { exists: false }
|
||
statusLoaded.value = true
|
||
const list: any[] = listRes?.items || listRes?.records || (Array.isArray(listRes) ? listRes : [])
|
||
const me = list.find((x) => (x.partnerId || x.targetUserId) === partnerId.value)
|
||
if (me) {
|
||
partnerName.value = me.partnerName || me.name || '新朋友'
|
||
partnerAvatar.value = avatarOf({ partnerAvatar: me.partnerAvatar, partnerAvatarFileId: me.partnerAvatarFileId })
|
||
hasActiveMatch.value = !!me.hasActiveMatch
|
||
}
|
||
}
|
||
|
||
function openPartner(r: any) {
|
||
const pid = r.partnerId || r.targetUserId
|
||
if (!pid) return
|
||
router.push({ path: '/activity/mutual/result', query: { id: activityId.value, partnerId: pid } })
|
||
}
|
||
function goWaiting() {
|
||
router.push({ path: '/activity/mutual/waiting', query: { id: activityId.value, partnerId: partnerId.value } })
|
||
}
|
||
function goFate() {
|
||
router.push('/fate-center')
|
||
}
|
||
|
||
async function onConfirm() {
|
||
if (acting.value) return
|
||
acting.value = 'confirm'
|
||
try {
|
||
// 二阶段同意(INV-014/015):先确保同意记录存在,再提交「我方确认」。
|
||
if (!status.value?.exists) {
|
||
await activityMutualService.createConsent(activityId.value, partnerId.value)
|
||
}
|
||
const result: any = await activityMutualService.confirmConnection(activityId.value, partnerId.value)
|
||
status.value = { ...(status.value || {}), exists: true, ...result }
|
||
if (result?.bothConfirmed) {
|
||
hasActiveMatch.value = !!result?.hasActiveMatch
|
||
appStore.toastSuccess('双方已确认,可查看沟通')
|
||
} else {
|
||
appStore.toastSuccess('已发送邀请,等待对方确认')
|
||
goWaiting()
|
||
}
|
||
} catch (err: any) {
|
||
appStore.toastError(err?.message || '操作失败')
|
||
} finally {
|
||
acting.value = ''
|
||
}
|
||
}
|
||
|
||
async function onDecline() {
|
||
if (acting.value) return
|
||
acting.value = 'decline'
|
||
try {
|
||
await activityMutualService.declineConnection(activityId.value, partnerId.value)
|
||
appStore.toastSuccess('已选择暂不联系')
|
||
router.back()
|
||
} catch (err: any) {
|
||
appStore.toastError(err?.message || '操作失败')
|
||
} finally {
|
||
acting.value = ''
|
||
}
|
||
}
|
||
|
||
onMounted(load)
|
||
</script>
|
||
|
||
<style scoped>
|
||
.mr {
|
||
padding: 12px 16px 16px;
|
||
}
|
||
.mr__partner {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 24px 0 8px;
|
||
}
|
||
.mr__partner-name {
|
||
font-size: 17px;
|
||
font-weight: 600;
|
||
color: var(--mini-text-strong);
|
||
}
|
||
.mr__partner-hint {
|
||
font-size: 12px;
|
||
color: var(--mini-text-secondary);
|
||
}
|
||
.mr__status {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 10px;
|
||
background: var(--mini-surface);
|
||
border-radius: var(--mini-radius);
|
||
padding: 24px 16px;
|
||
margin-top: 12px;
|
||
box-shadow: var(--mini-shadow);
|
||
text-align: center;
|
||
}
|
||
.mr__status--success {
|
||
background: rgba(47, 179, 109, 0.08);
|
||
}
|
||
.mr__status-icon {
|
||
font-size: 40px;
|
||
}
|
||
.mr__status-icon--success {
|
||
width: 52px;
|
||
height: 52px;
|
||
border-radius: 50%;
|
||
background: var(--mini-success);
|
||
color: #fff;
|
||
font-size: 26px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.mr__status-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: var(--mini-text-strong);
|
||
}
|
||
.mr__status-hint {
|
||
font-size: 12px;
|
||
color: var(--mini-text-secondary);
|
||
line-height: 1.6;
|
||
}
|
||
.mr__status-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-top: 6px;
|
||
}
|
||
.mr__card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
background: var(--mini-surface);
|
||
border-radius: var(--mini-radius);
|
||
padding: 12px;
|
||
margin-bottom: 10px;
|
||
box-shadow: var(--mini-shadow);
|
||
}
|
||
.mr__card-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.mr__card-name {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: var(--mini-text-strong);
|
||
}
|
||
.mr__empty-hint {
|
||
font-size: 12px;
|
||
color: var(--mini-text-muted);
|
||
}
|
||
</style>
|