xiangyixiangqin/miniapp/pages/message/index.vue
2026-01-28 18:20:46 +08:00

788 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="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-wrapper"
v-for="session in sessions"
:key="session.sessionId"
>
<view
class="session-item"
:style="{ transform: `translateX(${session.offsetX || 0}px)` }"
@touchstart="handleTouchStart($event, session)"
@touchmove="handleTouchMove($event, session)"
@touchend="handleTouchEnd($event, session)"
@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 class="delete-btn" @click.stop="handleDeleteSession(session)">
<text>删除</text>
</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,
deleteSession
} from '@/api/chat.js'
import {
getInteractCounts,
markInteractAsRead
} 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 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
})
// 滑动删除相关
const touchStartX = ref(0)
const deleteWidth = 80 // 删除按钮宽度
// 从 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 res = await getInteractCounts()
if (res?.code === 0 && res.data) {
interactCounts.value = {
viewedMe: res.data.viewedMeCount || 0,
favoritedMe: res.data.favoritedMeCount || 0,
unlockedMe: res.data.unlockedMeCount || 0
}
}
} catch (error) {
console.error('加载互动统计失败:', error)
}
}
// 加载会话列表
const loadSessions = async () => {
if (!userStore.isLoggedIn) return
try {
const res = await getSessions()
console.log('[Message] 加载会话列表:', res)
if (res?.success || res?.code === 0) {
sessions.value = res.data || []
console.log('[Message] 会话列表数据:', sessions.value.map(s => ({
sessionId: s.sessionId,
targetNickname: s.targetNickname,
unreadCount: s.unreadCount
})))
chatStore.setSessions(sessions.value)
}
} catch (error) {
console.error('加载会话列表失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
}
}
// 初始化页面
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 = async (url) => {
// 根据 URL 确定互动类型并标记已读
let interactType = ''
if (url.includes('viewedMe')) {
interactType = 'viewedMe'
interactCounts.value.viewedMe = 0
} else if (url.includes('favoritedMe')) {
interactType = 'favoritedMe'
interactCounts.value.favoritedMe = 0
} else if (url.includes('unlockedMe')) {
interactType = 'unlockedMe'
interactCounts.value.unlockedMe = 0
}
// 调用后端标记已读(异步,不阻塞跳转)
if (interactType) {
markInteractAsRead(interactType).catch(err => {
console.error('标记已读失败:', err)
})
}
uni.navigateTo({ url })
}
// 点击会话
const handleSessionClick = (session) => {
// 如果正在滑动状态,先复位
if (session.offsetX && session.offsetX < 0) {
session.offsetX = 0
return
}
chatStore.setCurrentSession(session.sessionId)
uni.navigateTo({
url: `/pages/chat/index?sessionId=${session.sessionId}&targetUserId=${session.targetUserId}`
})
}
// 滑动开始
const handleTouchStart = (e, session) => {
touchStartX.value = e.touches[0].clientX
// 关闭其他已打开的滑动项
sessions.value.forEach(s => {
if (s.sessionId !== session.sessionId && s.offsetX) {
s.offsetX = 0
}
})
}
// 滑动中
const handleTouchMove = (e, session) => {
const currentX = e.touches[0].clientX
const diff = currentX - touchStartX.value
// 只允许左滑
if (diff < 0) {
session.offsetX = Math.max(diff, -deleteWidth)
} else if (session.offsetX < 0) {
session.offsetX = Math.min(0, session.offsetX + diff)
}
}
// 滑动结束
const handleTouchEnd = (e, session) => {
// 如果滑动超过一半,展开删除按钮,否则复位
if (session.offsetX < -deleteWidth / 2) {
session.offsetX = -deleteWidth
} else {
session.offsetX = 0
}
}
// 删除会话
const handleDeleteSession = async (session) => {
uni.showModal({
title: '提示',
content: '确定删除该聊天记录吗?',
success: async (res) => {
if (res.confirm) {
try {
const result = await deleteSession(session.sessionId)
if (result?.success || result?.code === 0) {
// 从列表中移除
const index = sessions.value.findIndex(s => s.sessionId === session.sessionId)
if (index > -1) {
sessions.value.splice(index, 1)
}
// 同步到store
chatStore.setSessions(sessions.value)
uni.showToast({ title: '删除成功', icon: 'success' })
} else {
uni.showToast({ title: result?.message || '删除失败', icon: 'none' })
}
} catch (error) {
console.error('删除会话失败:', error)
uni.showToast({ title: '删除失败', icon: 'none' })
}
}
}
})
}
// 跳转登录
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-wrapper {
position: relative;
overflow: hidden;
.delete-btn {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 160rpx;
background-color: #ff4d4f;
display: flex;
align-items: center;
justify-content: center;
text {
color: #fff;
font-size: 28rpx;
}
}
}
.session-item {
display: flex;
align-items: center;
padding: 28rpx 32rpx;
border-bottom: 1rpx solid #f5f5f5;
background-color: #F3F3F3;
transition: transform 0.2s ease;
position: relative;
z-index: 1;
&: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>