xiangyixiangqin/miniapp/pages/index/index.vue
2026-01-21 19:36:36 +08:00

794 lines
18 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>
<view class="home-page">
<!-- 页面加载状态 -->
<Loading type="page" :loading="pageLoading" />
<!-- 性别选择弹窗最高优先级 -->
<Popup :visible="showGenderPopup" type="gender" :showClose="false" :closeOnMask="false"
@genderSelect="handleGenderSelect" />
<!-- 每日首次弹窗 -->
<Popup :visible="showDailyPopup" type="daily" :imageUrl="dailyPopup?.imageUrl" :title="dailyPopup?.title"
:buttonText="dailyPopup?.buttonText" :linkUrl="dailyPopup?.linkUrl" @close="handleCloseDailyPopup"
@confirm="handleDailyPopupConfirm" />
<!-- 解锁确认弹窗 -->
<Popup
:visible="showUnlockPopup"
type="unlock"
:remainingQuota="remainingUnlockQuota"
@close="handleCloseUnlockPopup"
@unlock="handleConfirmUnlock"
@goMember="handleGoMember"
/>
<!-- 会员广告条 - 固定在底部 -->
<view class="member-ad-section" v-if="showMemberAd">
<view class="member-ad-bar" :style="memberAdBgStyle">
<view class="ad-content" @click="handleMemberAdClick">
<text class="ad-icon">👑</text>
<text class="ad-text">{{ memberAdConfig?.title || '开通会员,解锁更多优质用户' }}</text>
</view>
<text class="ad-btn" @click="handleMemberAdClick">{{ memberAdConfig?.buttonText || '购买' }}</text>
<view class="ad-close" @click.stop="handleCloseMemberAd">
<text>×</text>
</view>
</view>
</view>
<!-- 整页滚动区域 -->
<scroll-view
class="page-scroll"
scroll-y
refresher-enabled
:refresher-triggered="isRefreshing"
@refresherrefresh="handleRefresh"
@scrolltolower="handleScrollToLower"
>
<!-- Banner区域导航栏和搜索框浮在Banner上 -->
<view class="banner-section">
<!-- Banner 轮播图 -->
<swiper v-if="banners.length > 0" class="banner-swiper" :indicator-dots="banners.length > 1"
:autoplay="true" :interval="3000" :duration="500" indicator-color="rgba(255,255,255,0.5)"
indicator-active-color="#FF6A6A" circular>
<swiper-item v-for="banner in banners" :key="banner.id" @click="handleBannerClick(banner)">
<image class="banner-image" :src="banner.imageUrl" mode="aspectFill" />
</swiper-item>
</swiper>
<!-- 无banner时的默认背景 -->
<view v-else class="banner-placeholder"></view>
<!-- 浮层:导航栏 + 搜索框 -->
<view class="banner-overlay">
<!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<view class="header-title">相宜相亲</view>
</view>
</view>
<!-- 搜索框 -->
<view class="search-section">
<view class="search-bar" @click="handleSearchClick">
<text class="search-icon">🔍</text>
<text class="search-placeholder">搜索你心目中的TA</text>
</view>
</view>
</view>
</view>
<!-- 金刚位导航 -->
<view class="kingkong-section" v-if="kingKongs.length > 0">
<view class="kingkong-grid">
<view class="kingkong-item" v-for="item in kingKongs" :key="item.id"
@click="handleKingKongClick(item)">
<image class="kingkong-icon" :src="item.iconUrl" mode="aspectFit" />
<text class="kingkong-title">{{ item.title }}</text>
</view>
</view>
</view>
<!-- 推荐标题栏 -->
<view class="section-header">
<view class="section-title-wrapper">
<text class="section-title-main">今日优质推荐</text>
<text class="section-title-highlight">已更新</text>
</view>
<text class="section-subtitle">每天早上五点准时更新</text>
</view>
<!-- 用户列表 -->
<view class="user-list" v-if="recommendList.length > 0">
<UserCard v-for="user in recommendList" :key="user.userId" :userId="user.userId"
:nickname="user.nickname" :avatar="user.avatar" :gender="user.gender" :age="user.age"
:birthYear="user.birthYear" :workCity="user.workCity" :hometown="user.hometown"
:height="user.height" :weight="user.weight" :education="user.education"
:educationName="user.educationName" :occupation="user.occupation"
:monthlyIncome="user.monthlyIncome" :intro="user.intro" :isMember="user.isMember"
:isRealName="user.isRealName" :isPhotoPublic="user.isPhotoPublic" :firstPhoto="user.firstPhoto"
:viewedToday="user.viewedToday" @click="handleUserClick" @contact="handleUserContact" />
</view>
<!-- 空状态 -->
<view class="empty-wrapper" v-else-if="!listLoading">
<Empty text="暂无推荐用户" :showButton="false" />
</view>
<!-- 加载更多 -->
<Loading type="more" :loading="listLoading" :noMore="noMoreData" />
<!-- 底部占位tabbar高度 -->
<view class="bottom-placeholder"></view>
</scroll-view>
</view>
</template>
<script>
import {
ref,
computed,
onMounted
} from 'vue'
import {
useUserStore
} from '@/store/user.js'
import {
useConfigStore
} from '@/store/config.js'
import {
getRecommend
} from '@/api/user.js'
import {
checkUnlock,
unlock
} from '@/api/interact.js'
import {
getFullImageUrl
} from '@/utils/image.js'
import Popup from '@/components/Popup/index.vue'
import UserCard from '@/components/UserCard/index.vue'
import Loading from '@/components/Loading/index.vue'
import Empty from '@/components/Empty/index.vue'
export default {
name: 'HomePage',
components: {
Popup,
UserCard,
Loading,
Empty
},
setup() {
const userStore = useUserStore()
const configStore = useConfigStore()
// 状态栏高度
const statusBarHeight = ref(20)
// 页面状态
const pageLoading = ref(true)
const listLoading = ref(false)
const noMoreData = ref(false)
const isRefreshing = ref(false)
// 分页参数
const pageIndex = ref(1)
const pageSize = 10
const total = ref(0)
// 数据
const recommendList = ref([])
// 解锁弹窗状态
const showUnlockPopup = ref(false)
const unlockTargetUserId = ref(0)
const remainingUnlockQuota = ref(0)
// 计算属性 - 处理图片URL
const banners = computed(() => {
return configStore.banners.map(banner => ({
...banner,
imageUrl: getFullImageUrl(banner.imageUrl)
}))
})
const kingKongs = computed(() => {
return configStore.kingKongs.map(item => ({
...item,
iconUrl: getFullImageUrl(item.iconUrl)
}))
})
const showMemberAd = computed(() => configStore.showMemberAd)
const memberAdConfig = computed(() => configStore.memberAdConfig)
// 会员广告条背景样式
const memberAdBgStyle = computed(() => {
const config = configStore.memberAdConfig
if (config && config.imageUrl) {
const fullUrl = getFullImageUrl(config.imageUrl)
return {
backgroundImage: `url(${fullUrl})`,
backgroundSize: 'cover',
backgroundPosition: 'center'
}
}
return {
background: 'linear-gradient(135deg, #ff9a9e 0%, #fecfef 50%, #fecfef 100%)'
}
})
const showGenderPopup = computed(() => configStore.showGenderPopup)
const showDailyPopup = computed(() => configStore.showDailyPopup)
const dailyPopup = computed(() => {
const popup = configStore.dailyPopup
if (!popup) return null
return {
...popup,
imageUrl: getFullImageUrl(popup.imageUrl)
}
})
// 加载推荐用户列表
const loadRecommendList = async (isLoadMore = false) => {
if (listLoading.value) return
if (isLoadMore && noMoreData.value) return
listLoading.value = true
try {
if (!isLoadMore) {
pageIndex.value = 1
recommendList.value = []
noMoreData.value = false
}
const res = await getRecommend(pageIndex.value, pageSize)
if (res && res.code === 0 && res.data) {
const items = res.data.items || []
total.value = res.data.total || 0
if (isLoadMore) {
recommendList.value = [...recommendList.value, ...items]
} else {
recommendList.value = items
}
noMoreData.value = recommendList.value.length >= total.value
pageIndex.value++
}
} catch (error) {
console.error('加载推荐列表失败:', error)
} finally {
listLoading.value = false
}
}
// 检查弹窗显示
const checkPopups = () => {
configStore.checkPopupDisplay({
genderPreference: userStore.genderPreference,
isProfileCompleted: userStore.isProfileCompleted,
isMember: userStore.isMember
})
}
// 初始化页面
const initPage = async () => {
pageLoading.value = true
try {
// 获取状态栏高度
uni.getSystemInfo({
success: (res) => {
statusBarHeight.value = res.statusBarHeight || 20
}
})
// 恢复用户状态
userStore.restoreFromStorage()
// 确保配置已加载App.vue中已调用这里等待完成
if (!configStore.isLoaded) {
await configStore.loadAppConfig()
}
// 加载推荐列表
await loadRecommendList()
// 检查弹窗显示
checkPopups()
} catch (error) {
console.error('初始化页面失败:', error)
} finally {
pageLoading.value = false
}
}
// 搜索点击
const handleSearchClick = () => {
uni.navigateTo({
url: '/pages/search/index'
})
}
// Banner点击
const handleBannerClick = (banner) => {
if (!banner.linkUrl) return
if (banner.linkType === 1) {
uni.navigateTo({
url: banner.linkUrl
})
} else if (banner.linkType === 2) {
uni.setClipboardData({
data: banner.linkUrl,
success: () => {
uni.showToast({
title: '链接已复制',
icon: 'success'
})
}
})
}
}
// 金刚位点击
const handleKingKongClick = (item) => {
if (!item.linkUrl) return
const tabbarPages = ['/pages/index/index', '/pages/message/index', '/pages/mine/index']
if (tabbarPages.includes(item.linkUrl)) {
uni.switchTab({
url: item.linkUrl
})
} else {
uni.navigateTo({
url: item.linkUrl
})
}
}
// 会员广告点击
const handleMemberAdClick = () => {
const linkUrl = configStore.memberAdConfig?.linkUrl
if (linkUrl) {
uni.navigateTo({
url: linkUrl
})
} else {
uni.navigateTo({
url: '/pages/member/index'
})
}
}
// 关闭会员广告
const handleCloseMemberAd = () => {
configStore.closeMemberAd()
}
// 性别选择处理
const handleGenderSelect = (gender) => {
userStore.setGenderPref(gender)
configStore.closeGenderPopup()
loadRecommendList()
checkPopups()
}
// 关闭每日弹窗
const handleCloseDailyPopup = () => {
configStore.closeDailyPopup()
checkPopups()
}
// 每日弹窗确认按钮
const handleDailyPopupConfirm = () => {
configStore.closeDailyPopup()
}
// 用户卡片点击
const handleUserClick = (userId) => {
uni.navigateTo({
url: `/pages/profile/detail?userId=${userId}`
})
}
// 联系用户
const handleUserContact = async (userId) => {
if (!userStore.isLoggedIn) {
uni.showModal({
title: '提示',
content: '请先登录后再联系对方',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/login/index'
})
}
}
})
return
}
if (!userStore.isProfileCompleted) {
uni.showModal({
title: '提示',
content: '请先完善您的资料,才能联系对方',
confirmText: '去完善',
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/profile/edit'
})
}
}
})
return
}
// 检查是否已解锁
try {
uni.showLoading({ title: '加载中...' })
const res = await checkUnlock(userId)
uni.hideLoading()
if (res && res.code === 0 && res.data) {
if (res.data.isUnlocked) {
// 已解锁,直接跳转聊天
uni.navigateTo({
url: `/pages/chat/index?targetUserId=${userId}`
})
} else {
// 未解锁,显示解锁弹窗
unlockTargetUserId.value = userId
remainingUnlockQuota.value = res.data.remainingUnlockQuota || 0
showUnlockPopup.value = true
}
} else {
uni.showToast({ title: '检查解锁状态失败', icon: 'none' })
}
} catch (error) {
uni.hideLoading()
console.error('检查解锁状态失败:', error)
uni.showToast({ title: '网络错误', icon: 'none' })
}
}
// 关闭解锁弹窗
const handleCloseUnlockPopup = () => {
showUnlockPopup.value = false
}
// 确认解锁
const handleConfirmUnlock = async () => {
if (remainingUnlockQuota.value <= 0) {
showUnlockPopup.value = false
uni.navigateTo({ url: '/pages/member/index' })
return
}
try {
uni.showLoading({ title: '解锁中...' })
const unlockRes = await unlock(unlockTargetUserId.value)
uni.hideLoading()
if (unlockRes && unlockRes.code === 0) {
showUnlockPopup.value = false
uni.showToast({ title: '解锁成功', icon: 'success' })
setTimeout(() => {
uni.navigateTo({
url: `/pages/chat/index?targetUserId=${unlockTargetUserId.value}`
})
}, 1000)
} else {
uni.showToast({
title: unlockRes?.message || '解锁失败',
icon: 'none'
})
}
} catch (error) {
uni.hideLoading()
console.error('解锁失败:', error)
uni.showToast({ title: '解锁失败', icon: 'none' })
}
}
// 跳转会员页面
const handleGoMember = () => {
showUnlockPopup.value = false
uni.navigateTo({ url: '/pages/member/index' })
}
// 滚动到底部 - 加载更多
const handleScrollToLower = () => {
if (!noMoreData.value && !listLoading.value) {
loadRecommendList(true)
}
}
// 下拉刷新
const handleRefresh = async () => {
isRefreshing.value = true
try {
await loadRecommendList(false)
} finally {
isRefreshing.value = false
}
}
onMounted(() => {
initPage()
})
return {
statusBarHeight,
pageLoading,
listLoading,
noMoreData,
isRefreshing,
banners,
kingKongs,
showMemberAd,
memberAdConfig,
memberAdBgStyle,
showGenderPopup,
showDailyPopup,
dailyPopup,
recommendList,
showUnlockPopup,
remainingUnlockQuota,
initPage,
loadRecommendList,
handleSearchClick,
handleBannerClick,
handleKingKongClick,
handleMemberAdClick,
handleCloseMemberAd,
handleGenderSelect,
handleCloseDailyPopup,
handleDailyPopupConfirm,
handleUserClick,
handleUserContact,
handleCloseUnlockPopup,
handleConfirmUnlock,
handleGoMember,
handleScrollToLower,
handleRefresh
}
},
onShow() {
const configStore = useConfigStore()
const userStore = useUserStore()
configStore.checkPopupDisplay({
genderPreference: userStore.genderPreference,
isProfileCompleted: userStore.isProfileCompleted,
isMember: userStore.isMember
})
}
}
</script>
<style lang="scss" scoped>
.home-page {
height: 100vh;
background-color: #f8f8f8;
}
// 整页滚动区域
.page-scroll {
height: 100vh;
}
// Banner区域
.banner-section {
position: relative;
width: 100%;
}
.banner-swiper {
width: 100%;
height: 420rpx;
.banner-image {
width: 100%;
height: 100%;
}
}
.banner-placeholder {
width: 100%;
height: 420rpx;
background: linear-gradient(135deg, #FFB6C1 0%, #FFC0CB 100%);
}
// 浮层(导航栏+搜索框)
.banner-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 10;
}
// 自定义导航栏
.custom-navbar {
.navbar-content {
height: 44px;
display: flex;
align-items: center;
justify-content: center;
.header-title {
font-size: 36rpx;
font-weight: 600;
color: #fff;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
}
}
}
// 搜索框
.search-section {
padding: 16rpx 24rpx;
.search-bar {
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.95);
border-radius: 40rpx;
padding: 20rpx 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
.search-icon {
font-size: 28rpx;
margin-right: 12rpx;
color: #999;
}
.search-placeholder {
font-size: 28rpx;
color: #999;
}
}
}
// 金刚位导航
.kingkong-section {
padding: 20rpx;
background-color: #fff;
.kingkong-grid {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
.kingkong-item {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
padding: 16rpx 0;
.kingkong-icon {
width: 80rpx;
height: 80rpx;
margin-bottom: 12rpx;
}
.kingkong-title {
font-size: 24rpx;
color: #333;
}
}
}
}
// 推荐标题栏
.section-header {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 5rpx 20rpx 24rpx;
background-color: #f8f8f8;
.section-title-wrapper {
display: flex;
align-items: center;
margin-bottom: 8rpx;
.section-title-main {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.section-title-highlight {
font-size: 42rpx;
color: #FF6A6A;
font-weight: 600;
font-style: italic;
}
}
.section-subtitle {
font-size: 24rpx;
color: #999;
}
}
// 用户列表
.user-list {
padding: 0 20rpx;
}
// 空状态
.empty-wrapper {
padding: 100rpx 0;
display: flex;
align-items: center;
justify-content: center;
}
// 底部占位
.bottom-placeholder {
height: 120rpx;
}
// 会员广告条
.member-ad-section {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 99;
.member-ad-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx;
.ad-content {
display: flex;
align-items: center;
flex: 1;
.ad-icon {
font-size: 32rpx;
margin-right: 12rpx;
}
.ad-text {
font-size: 26rpx;
color: #333;
font-weight: 500;
}
}
.ad-btn {
background: #fff;
color: #ff6b6b;
font-size: 24rpx;
padding: 12rpx 24rpx;
border-radius: 30rpx;
margin-right: 16rpx;
font-weight: 500;
}
.ad-close {
width: 44rpx;
height: 44rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: 36rpx;
color: #666;
line-height: 1;
}
}
}
}
</style>