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

595 lines
14 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-section">
<view class="interact-grid">
<view class="interact-item" @click="navigateTo('/pages/interact/viewedMe')">
<view class="interact-icon viewed">
<image src="/static/ic_seen.png" mode="aspectFit" class="icon-img" />
</view>
<text class="interact-label">看过我</text>
<view class="interact-badge" v-if="interactCounts.viewedMe > 0">
<text>+{{ interactCounts.viewedMe }}</text>
</view>
</view>
<view class="interact-item" @click="navigateTo('/pages/interact/favoritedMe')">
<view class="interact-icon favorited">
<image src="/static/ic_collection.png" mode="aspectFit" class="icon-img" />
</view>
<text class="interact-label">收藏我</text>
<view class="interact-badge" v-if="interactCounts.favoritedMe > 0">
<text>+{{ interactCounts.favoritedMe }}</text>
</view>
</view>
<view class="interact-item" @click="navigateTo('/pages/interact/unlockedMe')">
<view class="interact-icon unlocked">
<image src="/static/ic_unlock.png" mode="aspectFit" class="icon-img" />
</view>
<text class="interact-label">解锁我</text>
<view class="interact-badge" 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>
</scroll-view>
</view>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
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'
export default {
name: 'MessagePage',
components: {
Loading
},
setup() {
const chatStore = useChatStore()
const userStore = useUserStore()
const configStore = useConfigStore()
// 页面状态
const pageLoading = ref(true)
const listLoading = ref(false)
const noMoreData = 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 = () => {
uni.getSystemInfo({
success: (res) => {
statusBarHeight.value = res.statusBarHeight || 20
// 导航栏高度 = 状态栏高度 + 44px
navbarHeight.value = statusBarHeight.value + 44
// 计算滚动区域高度
// 需要等待固定头部渲染后再计算
setTimeout(() => {
calcScrollHeight(res.windowHeight)
}, 100)
}
})
}
// 计算滚动区域高度
const calcScrollHeight = (windowHeight) => {
const query = uni.createSelectorQuery()
query.select('.fixed-header').boundingClientRect((rect) => {
if (rect) {
// 窗口高度 - 导航栏高度 - 固定头部高度 - tabbar高度
const tabbarHeight = 50
scrollHeight.value = windowHeight - navbarHeight.value - rect.height - tabbarHeight
}
}).exec()
}
// 加载互动统计
const loadInteractCounts = async () => {
// 未登录时不加载
if (!userStore.isLoggedIn) return
try {
const [viewedRes, favoritedRes, unlockedRes] = await Promise.all([
getViewedMe(1, 1),
getFavoritedMe(1, 1),
getUnlockedMe(1, 1)
])
interactCounts.value = {
viewedMe: viewedRes?.data?.total || 0,
favoritedMe: favoritedRes?.data?.total || 0,
unlockedMe: unlockedRes?.data?.total || 0
}
} catch (error) {
console.error('加载互动统计失败:', error)
}
}
// 加载会话列表
const loadSessions = async () => {
// 未登录时不加载
if (!userStore.isLoggedIn) return
listLoading.value = true
try {
const res = await getSessions()
if (res && res.success) {
sessions.value = res.data?.list || res.data || []
chatStore.setSessions(sessions.value)
noMoreData.value = true
}
} catch (error) {
console.error('加载会话列表失败:', error)
} 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) => {
return formatTimestamp(timestamp)
}
// 导航到互动页面
const navigateTo = (url) => {
uni.navigateTo({ url })
}
// 点击会话
const handleSessionClick = (session) => {
chatStore.setCurrentSession(session.sessionId)
uni.navigateTo({
url: `/pages/chat/index?sessionId=${session.sessionId}&targetUserId=${session.targetUserId}`
})
}
onMounted(() => {
getSystemInfo()
initPage()
})
return {
pageLoading,
listLoading,
noMoreData,
isRefreshing,
statusBarHeight,
navbarHeight,
scrollHeight,
sessions,
interactCounts,
defaultAvatar,
formatTime,
navigateTo,
handleSessionClick,
handleRefresh,
initPage,
loadInteractCounts,
loadSessions
}
},
// 页面显示时刷新数据
onShow() {
if (this.loadInteractCounts) {
this.loadInteractCounts()
}
if (this.loadSessions) {
this.loadSessions()
}
}
}
</script>
<style lang="scss" scoped>
.message-page {
height: 100vh;
background-color: #f5f6fa;
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-section {
position: relative;
padding: 24rpx 40rpx 32rpx;
.interact-grid {
display: flex;
justify-content: space-between;
.interact-item {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
.interact-icon {
width: 120rpx;
height: 120rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
.icon-img {
width: 56rpx;
height: 56rpx;
}
&.viewed {
background: linear-gradient(135deg, #e8e0ff 0%, #d4c4ff 100%);
}
&.favorited {
background: linear-gradient(135deg, #ffe0e8 0%, #ffc4d4 100%);
}
&.unlocked {
background: linear-gradient(135deg, #fff3e0 0%, #ffe4c4 100%);
}
}
.interact-label {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.interact-badge {
position: absolute;
top: -8rpx;
right: -8rpx;
min-width: 44rpx;
height: 44rpx;
padding: 0 12rpx;
border-radius: 22rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: 24rpx;
color: #fff;
font-weight: 500;
}
}
&:nth-child(1) .interact-badge {
background: #9b7bff;
}
&:nth-child(2) .interact-badge {
background: #ff6b8a;
}
&:nth-child(3) .interact-badge {
background: #ffb347;
}
}
}
}
// 消息滚动区域
.message-scroll {
flex: 1;
position: fixed;
left: 0;
right: 0;
bottom: 100rpx;
}
// 消息列表区域
.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;
}
}
}
}
}
</style>