release: freeze demo v1.1.0 2026-10-19
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
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
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user