xiangyixiangqin/miniapp/pages/index/index.vue
2026-01-07 17:52:35 +08:00

821 lines
19 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" />
<!-- 固定头部区域 -->
<view class="fixed-header">
<!-- 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>
<!-- 会员广告条 - 固定在底部 -->
<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="user-scroll-area" scroll-y :style="{ height: scrollHeight }" refresher-enabled
:refresher-triggered="isRefreshing" @refresherrefresh="handleRefresh" @scrolltolower="handleScrollToLower">
<!-- 用户列表 -->
<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" />
</scroll-view>
</view>
</template>
<script>
import {
ref,
computed,
onMounted
} from 'vue'
import {
useUserStore
} from '@/store/user.js'
import {
useConfigStore
} from '@/store/config.js'
import {
getHomeConfig,
getPopupConfig,
getDefaultAvatarConfig
} from '@/api/config.js'
import {
getRecommend
} from '@/api/user.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 scrollHeight = ref('300px')
const isRefreshing = ref(false)
// 分页参数
const pageIndex = ref(1)
const pageSize = 10
const total = ref(0)
// 数据
const recommendList = ref([])
// 计算属性 - 处理图片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 loadHomeConfig = async () => {
try {
const res = await getHomeConfig()
if (res && res.code === 0) {
configStore.setHomeConfig({
banners: res.data?.banners || [],
kingKongs: res.data?.kingKongs || []
})
}
} catch (error) {
console.error('加载首页配置失败:', error)
}
}
// 加载每日弹窗配置
const loadDailyPopup = async () => {
try {
const res = await getPopupConfig(1)
if (res && res.code === 0 && res.data) {
configStore.setDailyPopup(res.data)
}
} catch (error) {
console.error('加载弹窗配置失败:', error)
}
}
// 加载会员广告配置
const loadMemberAdConfig = async () => {
try {
const res = await getPopupConfig(3) // 3=会员广告
if (res && res.code === 0 && res.data) {
configStore.setMemberAdConfig(res.data)
}
} catch (error) {
console.error('加载会员广告配置失败:', error)
}
}
// 加载默认头像配置
const loadDefaultAvatarConfig = async () => {
try {
const res = await getDefaultAvatarConfig()
if (res && res.code === 0 && res.data?.avatarUrl) {
configStore.setDefaultAvatarConfig(res.data.avatarUrl)
}
} catch (error) {
console.error('加载默认头像配置失败:', error)
}
}
// 加载推荐用户列表
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()
// 先加载不需要登录的配置
await Promise.all([
loadHomeConfig(),
loadDailyPopup(),
loadMemberAdConfig(),
loadDefaultAvatarConfig()
])
// 加载推荐列表(无论是否登录都加载)
await loadRecommendList()
// 检查弹窗显示
checkPopups()
// 计算滚动区域高度
setTimeout(() => {
calcScrollHeight()
}, 100)
} 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
// 判断是否是tabbar页面
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()
// 如果有链接Popup组件会自动处理跳转
}
// 刷新推荐列表
const handleRefreshRecommend = async () => {
await loadRecommendList()
uni.showToast({
title: '已刷新',
icon: 'success'
})
}
// 用户卡片点击
const handleUserClick = (userId) => {
uni.navigateTo({
url: `/pages/profile/detail?userId=${userId}`
})
}
// 联系用户
const handleUserContact = (userId) => {
// 1. 首先检查是否已登录
if (!userStore.isLoggedIn) {
uni.showModal({
title: '提示',
content: '请先登录后再联系对方',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/auth/login'
})
}
}
})
return
}
// 2. 检查是否完善资料
if (!userStore.isProfileCompleted) {
uni.showModal({
title: '提示',
content: '请先完善您的资料,才能联系对方',
confirmText: '去完善',
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/profile/edit'
})
}
}
})
return
}
// 3. 跳转到聊天页面(后续会实现解锁逻辑)
uni.navigateTo({
url: `/pages/chat/index?targetUserId=${userId}`
})
}
// 计算滚动区域高度
const calcScrollHeight = () => {
uni.getSystemInfo({
success: (res) => {
// 获取固定头部高度后计算剩余高度
const query = uni.createSelectorQuery()
query.select('.fixed-header').boundingClientRect((rect) => {
if (rect) {
// 屏幕高度 - 头部高度 - tabbar高度(50px)
const tabbarHeight = 50
const height = res.windowHeight - rect.height - tabbarHeight
scrollHeight.value = `${height}px`
}
}).exec()
}
})
}
// 滚动到底部 - 加载更多
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,
scrollHeight,
isRefreshing,
banners,
kingKongs,
showMemberAd,
memberAdConfig,
memberAdBgStyle,
showGenderPopup,
showDailyPopup,
dailyPopup,
recommendList,
initPage,
loadRecommendList,
handleSearchClick,
handleBannerClick,
handleKingKongClick,
handleMemberAdClick,
handleCloseMemberAd,
handleGenderSelect,
handleCloseDailyPopup,
handleDailyPopupConfirm,
handleRefreshRecommend,
handleUserClick,
handleUserContact,
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;
display: flex;
flex-direction: column;
background-color: #f8f8f8;
overflow: hidden;
}
// 固定头部区域
.fixed-header {
flex-shrink: 0;
}
// Banner区域
.banner-section {
position: relative;
width: 100%;
}
// Banner 轮播图
.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;
.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;
background: transparent;
padding: 0;
border-radius: 0;
font-weight: 600;
font-style: italic;
box-shadow: none;
}
}
.section-subtitle {
font-size: 24rpx;
color: #999;
}
.section-more {
display: flex;
align-items: center;
text {
font-size: 26rpx;
color: #ff6b6b;
}
}
}
// 会员广告条 - 固定在底部导航栏上方
.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;
}
}
}
}
// 可滚动的用户列表区域
.user-scroll-area {
flex: 1;
padding: 0 20rpx;
box-sizing: border-box;
.empty-wrapper {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
</style>