xiangyixiangqin/miniapp/pages/message/index.vue
2026-01-22 00:58:45 +08:00

698 lines
15 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="message-page">
<!-- 页面加载状态 -->
<Loading type="page" :loading="pageLoading" />
<!-- 顶部背景图 -->
<view class="top-bg">
<image src="/static/title_bg.png" mode="aspectFill" class="bg-img" />
</view>
<!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<text class="navbar-title">消息</text>
</view>
</view>
<!-- 固定头部区域 -->
<view class="fixed-header" :style="{ top: navbarHeight + 'px' }">
<!-- 互动统计区域 -->
<view class="interact-grid">
<view class="interact-item viewed" @click="navigateTo('/pages/interact/viewedMe')">
<view class="item-card">
<view class="icon-box">
<image src="/static/ic_seen.png" mode="aspectFit" class="icon-img" />
</view>
<text class="item-label">看过我</text>
<view class="item-count" v-if="interactCounts.viewedMe > 0">
<text>+{{ interactCounts.viewedMe }}</text>
</view>
</view>
</view>
<view class="interact-item favorited" @click="navigateTo('/pages/interact/favoritedMe')">
<view class="item-card">
<view class="icon-box">
<image src="/static/ic_collection.png" mode="aspectFit" class="icon-img" />
</view>
<text class="item-label">收藏我</text>
<view class="item-count" v-if="interactCounts.favoritedMe > 0">
<text>+{{ interactCounts.favoritedMe }}</text>
</view>
</view>
</view>
<view class="interact-item unlocked" @click="navigateTo('/pages/interact/unlockedMe')">
<view class="item-card">
<view class="icon-box">
<image src="/static/ic_unlock.png" mode="aspectFit" class="icon-img" />
</view>
<text class="item-label">解锁我</text>
<view class="item-count" v-if="interactCounts.unlockedMe > 0">
<text>+{{ interactCounts.unlockedMe }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 消息列表区域(可滚动) -->
<scroll-view class="message-scroll" scroll-y :style="{ height: scrollHeight + 'px' }" refresher-enabled
:refresher-triggered="isRefreshing" @refresherrefresh="handleRefresh">
<view class="message-section">
<!-- 系统消息入口 -->
<view class="system-message-item" @click="navigateTo('/pages/message/system')">
<view class="system-avatar">
<image :src="defaultAvatar" mode="aspectFill" class="avatar-img" />
</view>
<view class="system-info">
<text class="system-title">系统消息</text>
</view>
<view class="system-arrow">
<text class="arrow-icon">〉</text>
</view>
</view>
<!-- 聊天会话列表 -->
<view class="session-list" v-if="sessions.length > 0">
<view class="session-item" v-for="session in sessions" :key="session.sessionId"
@click="handleSessionClick(session)">
<!-- 头像 -->
<view class="session-avatar">
<image class="avatar-img" :src="session.targetAvatar || defaultAvatar" mode="aspectFill" />
<!-- 未读徽章 -->
<view class="unread-badge" v-if="session.unreadCount > 0">
<text>{{ session.unreadCount > 99 ? '99+' : session.unreadCount }}</text>
</view>
</view>
<!-- 会话信息 -->
<view class="session-info">
<view class="session-header">
<text
class="session-nickname">{{ session.targetNickname }}{{ session.relationship ? `${session.relationship}` : '' }}</text>
<text class="session-time">{{ formatTime(session.lastMessageTime) }}</text>
</view>
<view class="session-content">
<text class="last-message">{{ session.lastMessage || '暂无消息' }}</text>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else-if="!pageLoading && !userStore.isLoggedIn">
<image src="/static/ic_empty.png" mode="aspectFit" class="empty-icon" />
<text class="empty-text">登录后查看消息</text>
<button class="login-btn" @click="handleLogin">立即登录</button>
</view>
<!-- 无聊天记录 -->
<view class="empty-state" v-else-if="!pageLoading && sessions.length === 0">
<image src="/static/ic_empty.png" mode="aspectFit" class="empty-icon" />
<text class="empty-text">暂无聊天记录</text>
<text class="empty-tip">去首页看看心仪的对象吧</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import {
ref,
computed,
onMounted,
nextTick,
watch
} from 'vue'
import {
onShow
} from '@dcloudio/uni-app'
import {
useChatStore
} from '@/store/chat.js'
import {
useUserStore
} from '@/store/user.js'
import {
useConfigStore
} from '@/store/config.js'
import {
getSessions
} from '@/api/chat.js'
import {
getViewedMe,
getFavoritedMe,
getUnlockedMe
} from '@/api/interact.js'
import {
formatTimestamp
} from '@/utils/format.js'
import Loading from '@/components/Loading/index.vue'
const chatStore = useChatStore()
const userStore = useUserStore()
const configStore = useConfigStore()
// 页面状态
const pageLoading = ref(true)
const listLoading = ref(false)
const isRefreshing = ref(false)
// 系统信息
const statusBarHeight = ref(20)
const navbarHeight = ref(64)
const scrollHeight = ref(500)
// 数据
const sessions = ref([])
const interactCounts = ref({
viewedMe: 0,
favoritedMe: 0,
unlockedMe: 0
})
// 从 configStore 获取默认头像
const defaultAvatar = computed(() => configStore.defaultAvatar || '/static/logo.png')
// 获取系统信息
const getSystemInfo = () => {
try {
const res = uni.getSystemInfoSync()
statusBarHeight.value = res.statusBarHeight || 20
navbarHeight.value = statusBarHeight.value + 44
nextTick(() => {
calcScrollHeight(res.windowHeight)
})
} catch (error) {
console.error('获取系统信息失败:', error)
}
}
// 计算滚动区域高度
const calcScrollHeight = (windowHeight) => {
const query = uni.createSelectorQuery()
query.select('.fixed-header').boundingClientRect((rect) => {
if (rect) {
const tabbarHeight = 50
scrollHeight.value = windowHeight - navbarHeight.value - rect.height - tabbarHeight
}
}).exec()
}
// 加载互动统计
const loadInteractCounts = async () => {
if (!userStore.isLoggedIn) return
try {
// 获取本地存储的最后查看时间
const lastViewedTime = uni.getStorageSync('lastViewedMeTime') || 0
const lastFavoritedTime = uni.getStorageSync('lastFavoritedMeTime') || 0
const lastUnlockedTime = uni.getStorageSync('lastUnlockedMeTime') || 0
const [viewedRes, favoritedRes, unlockedRes] = await Promise.all([
getViewedMe(1, 100).catch(() => null),
getFavoritedMe(1, 100).catch(() => null),
getUnlockedMe(1, 100).catch(() => null)
])
// 计算新增数量(在最后查看时间之后的记录)
let viewedCount = 0
let favoritedCount = 0
let unlockedCount = 0
if (viewedRes?.data?.items) {
viewedCount = viewedRes.data.items.filter(item => {
const itemTime = new Date(item.viewTime || item.createTime).getTime()
return itemTime > lastViewedTime
}).length
}
if (favoritedRes?.data?.items) {
favoritedCount = favoritedRes.data.items.filter(item => {
const itemTime = new Date(item.favoriteTime || item.createTime).getTime()
return itemTime > lastFavoritedTime
}).length
}
if (unlockedRes?.data?.items) {
unlockedCount = unlockedRes.data.items.filter(item => {
const itemTime = new Date(item.unlockTime || item.createTime).getTime()
return itemTime > lastUnlockedTime
}).length
}
interactCounts.value = {
viewedMe: viewedCount,
favoritedMe: favoritedCount,
unlockedMe: unlockedCount
}
} catch (error) {
console.error('加载互动统计失败:', error)
}
}
// 加载会话列表
const loadSessions = async () => {
if (!userStore.isLoggedIn) return
listLoading.value = true
try {
const res = await getSessions()
if (res?.success) {
sessions.value = res.data || []
chatStore.setSessions(sessions.value)
}
} catch (error) {
console.error('加载会话列表失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
listLoading.value = false
}
}
// 初始化页面
const initPage = async () => {
pageLoading.value = true
try {
await Promise.all([
loadInteractCounts(),
loadSessions()
])
} catch (error) {
console.error('初始化页面失败:', error)
} finally {
pageLoading.value = false
}
}
// 下拉刷新
const handleRefresh = async () => {
isRefreshing.value = true
try {
await Promise.all([
loadInteractCounts(),
loadSessions()
])
} finally {
isRefreshing.value = false
}
}
// 格式化时间
const formatTime = (timestamp) => formatTimestamp(timestamp)
// 导航到互动页面
const navigateTo = (url) => {
// 记录最后查看时间
const now = Date.now()
if (url.includes('viewedMe')) {
uni.setStorageSync('lastViewedMeTime', now)
interactCounts.value.viewedMe = 0
} else if (url.includes('favoritedMe')) {
uni.setStorageSync('lastFavoritedMeTime', now)
interactCounts.value.favoritedMe = 0
} else if (url.includes('unlockedMe')) {
uni.setStorageSync('lastUnlockedMeTime', now)
interactCounts.value.unlockedMe = 0
}
uni.navigateTo({
url
})
}
// 点击会话
const handleSessionClick = (session) => {
chatStore.setCurrentSession(session.sessionId)
uni.navigateTo({
url: `/pages/chat/index?sessionId=${session.sessionId}&targetUserId=${session.targetUserId}`
})
}
// 跳转登录
const handleLogin = () => {
uni.navigateTo({
url: '/pages/login/index'
})
}
// 监听 store 中会话变化WebSocket 推送时自动更新)
watch(() => chatStore.sessions, (newSessions) => {
if (newSessions?.length) {
sessions.value = newSessions
}
}, {
deep: true
})
// 页面显示时刷新数据
onShow(() => {
if (userStore.isLoggedIn) {
loadInteractCounts()
loadSessions()
}
})
onMounted(() => {
getSystemInfo()
initPage()
})
</script>
<style lang="scss" scoped>
.message-page {
height: 100vh;
background-color: #F3F3F3;
display: flex;
flex-direction: column;
overflow: hidden;
}
// 顶部背景图
.top-bg {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 400rpx;
z-index: 1;
.bg-img {
width: 100%;
height: 100%;
}
}
// 自定义导航栏
.custom-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
.navbar-content {
height: 44px;
display: flex;
align-items: center;
justify-content: center;
.navbar-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
}
}
// 固定头部区域
.fixed-header {
position: fixed;
left: 0;
right: 0;
z-index: 10;
}
// 互动统计区域
.interact-grid {
margin-top: 56rpx;
display: flex;
flex-direction: row;
justify-content: space-around;
.interact-item {
.item-card {
display: flex;
flex-direction: column;
align-items: center;
width: 150rpx;
height: 132rpx;
// padding: 24rpx 16rpx 20rpx;
border-radius: 24rpx;
.icon-box {
width: 64rpx;
height: 64rpx;
border-radius: 20rpx;
margin-top: -18rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 6rpx;
.icon-img {
width: 64rpx;
height: 64rpx;
}
}
.item-label {
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-bottom: 6rpx;
}
.item-count {
min-width: 56rpx;
height: 40rpx;
padding: 5rpx 0rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: 20rpx;
color: #fff;
font-weight: 600;
}
}
}
// 看过我 - 紫色
&.viewed .item-card {
background: linear-gradient(0deg, rgba(237, 213, 255, 0.1) 0%, #E0D8FF 100%);
.item-count {
background: #8b5cf6;
}
}
// 收藏我 - 粉色
&.favorited .item-card {
background: linear-gradient(0deg, rgba(255, 213, 240, 0.1) 0%, #FFC4D7 100%);
.item-count {
background: #fb7185;
}
}
// 解锁我 - 黄色
&.unlocked .item-card {
background: linear-gradient(0deg, rgba(255, 239, 213, 0.1)0%, #FFF5D8 100%);
.item-count {
background: #f59e0b;
}
}
}
}
// 消息滚动区域
.message-scroll {
flex: 1;
position: fixed;
left: 0;
right: 0;
margin-top: 84rpx;
bottom: 0;
}
// 消息列表区域
.message-section {
background-color: #F3F3F3;
border-radius: 24rpx 24rpx 0 0;
min-height: 100%;
}
// 系统消息入口
.system-message-item {
display: flex;
align-items: center;
padding: 32rpx;
border-bottom: 1rpx solid #f5f5f5;
// background: linear-gradient(90deg, #f8fbff 0%, #fff 100%);
border-radius: 24rpx 24rpx 0 0;
.system-avatar {
width: 96rpx;
height: 96rpx;
margin-right: 24rpx;
.avatar-img {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
.system-info {
flex: 1;
.system-title {
font-size: 32rpx;
font-weight: 600;
color: #52c41a;
}
}
.system-arrow {
.arrow-icon {
font-size: 32rpx;
color: #ccc;
}
}
}
// 聊天会话列表
.session-list {
.session-item {
display: flex;
align-items: center;
padding: 28rpx 32rpx;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
&:active {
background-color: #f8f8f8;
}
.session-avatar {
position: relative;
width: 96rpx;
height: 96rpx;
margin-right: 24rpx;
flex-shrink: 0;
.avatar-img {
width: 100%;
height: 100%;
border-radius: 50%;
}
.unread-badge {
position: absolute;
top: -8rpx;
right: -8rpx;
min-width: 36rpx;
height: 36rpx;
padding: 0 8rpx;
background-color: #ff4d4f;
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: 22rpx;
color: #fff;
line-height: 1;
}
}
}
.session-info {
flex: 1;
overflow: hidden;
.session-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
.session-nickname {
font-size: 30rpx;
font-weight: 500;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 400rpx;
}
.session-time {
font-size: 24rpx;
color: #999;
flex-shrink: 0;
}
}
.session-content {
.last-message {
font-size: 26rpx;
color: #999;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
}
}
}
}
}
// 空状态
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 40rpx;
.empty-icon {
width: 200rpx;
height: 200rpx;
margin-bottom: 32rpx;
opacity: 0.6;
}
.empty-text {
font-size: 32rpx;
color: #666;
margin-bottom: 16rpx;
}
.empty-tip {
font-size: 26rpx;
color: #999;
}
.login-btn {
margin-top: 32rpx;
width: 240rpx;
height: 80rpx;
line-height: 80rpx;
background: linear-gradient(135deg, #FFBDC2 0%, #FF8A93 100%);
border-radius: 40rpx;
font-size: 30rpx;
color: #fff;
border: none;
&::after {
border: none;
}
}
}
</style>