Files
danyang-match/h5-demo/src/pages/activity/ActivityMutualResultPage.vue
T
WorkBuddy 69ebf33e18
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
release: freeze demo v1.1.0 2026-10-19
2026-10-19 20:36:22 +08:00

310 lines
9.7 KiB
Vue
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.
<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>