campus-errand/miniapp/pages/message/message.vue
18631081161 681d2b5fe8
All checks were successful
continuous-integration/drone/push Build is passing
提现
2026-04-02 16:55:18 +08:00

529 lines
11 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">
<!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<text class="navbar-title">消息</text>
</view>
</view>
<view :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 消息内容 -->
<view class="message-content">
<!-- 系统消息入口 -->
<view class="entry-item" @click="goSystemMsg">
<view class="entry-icon system-icon">
<text class="icon-text">📢</text>
</view>
<view class="entry-info">
<text class="entry-title">系统消息</text>
<text class="entry-desc">平台公告和重要通知</text>
</view>
<view class="entry-right">
<view v-if="unreadCount.systemUnread > 0" class="badge">
{{ unreadCount.systemUnread > 99 ? '99+' : unreadCount.systemUnread }}
</view>
<text class="arrow"></text>
</view>
</view>
<!-- 订单通知入口 -->
<view class="entry-item" @click="goOrderNotify">
<view class="entry-icon order-icon">
<text class="icon-text">📋</text>
</view>
<view class="entry-info">
<text class="entry-title">订单通知</text>
<text class="entry-desc">订单状态变更通知</text>
</view>
<view class="entry-right">
<view v-if="unreadCount.orderNotificationUnread > 0" class="badge">
{{ unreadCount.orderNotificationUnread > 99 ? '99+' : unreadCount.orderNotificationUnread }}
</view>
<text class="arrow"></text>
</view>
</view>
<!-- 聊天记录用户列表 -->
<view class="section-title" v-if="chatList.length > 0">聊天记录</view>
<view
class="chat-item-wrap"
v-for="item in chatList"
:key="item.orderId"
>
<view
class="chat-item"
:style="{ transform: `translateX(${item.slideX || 0}px)` }"
@touchstart="onTouchStart($event, item)"
@touchmove="onTouchMove($event, item)"
@touchend="onTouchEnd(item)"
@click="goChat(item)"
>
<view class="avatar-wrap">
<image class="chat-avatar" :src="item.targetAvatar || '/static/logo.png'" mode="aspectFill"></image>
<view v-if="item.imUnread > 0" class="unread-badge">
<text>{{ item.imUnread > 99 ? '99+' : item.imUnread }}</text>
</view>
</view>
<view class="chat-info">
<view class="chat-top">
<text class="chat-name">{{ displayNickname(item) }}</text>
<text class="chat-time">{{ formatTime(item.lastTime) }}</text>
</view>
<text class="chat-msg">{{ getOrderLabel(item) }}</text>
</view>
<view class="chat-tag" :class="'tag-' + item.status">
<text>{{ getStatusLabel(item.status) }}</text>
</view>
</view>
<view class="delete-btn" @click="onDeleteChat(item)">
<text>删除</text>
</view>
</view>
<!-- 空状态 -->
<view v-if="chatList.length === 0" class="empty-chat">
<text class="empty-text">暂无聊天记录</text>
</view>
</view>
</view>
</template>
<script>
import { getUnreadCount, getChatOrderList } from '../../utils/api'
import { getConversationList, initIM } from '../../utils/im'
export default {
data() {
return {
statusBarHeight: 0,
unreadCount: {
systemUnread: 0,
orderNotificationUnread: 0,
totalUnread: 0
},
// 聊天记录列表(腾讯 IM SDK 集成后从 SDK 获取)
chatList: [],
imUnreadTotal: 0
}
},
onShow() {
const sysInfo = uni.getSystemInfoSync()
this.statusBarHeight = sysInfo.statusBarHeight || 0
this.loadUnreadCount()
this.loadChatList()
this.updateTabBarBadge()
// 定时刷新未读数每5秒
this._refreshTimer = setInterval(() => {
this.loadChatList()
}, 5000)
},
onHide() {
if (this._refreshTimer) {
clearInterval(this._refreshTimer)
this._refreshTimer = null
}
},
methods: {
/** 加载未读消息数 */
async loadUnreadCount() {
try {
const res = await getUnreadCount()
this.unreadCount = {
systemUnread: res.systemUnread || 0,
orderNotificationUnread: res.orderNotificationUnread || 0,
totalUnread: res.totalUnread || 0
}
this.updateTabBarBadge()
} catch (e) {
// 静默处理
}
},
/** 加载聊天记录列表含IM未读数 */
async loadChatList() {
try {
const list = await getChatOrderList()
this.chatList = (list || []).map(item => ({ ...item, imUnread: 0 }))
// 从 IM 获取群会话未读数
try {
await initIM()
const convList = await getConversationList()
const unreadMap = {}
convList.forEach(c => { unreadMap[c.groupId] = c.unreadCount || 0 })
let totalImUnread = 0
this.chatList.forEach(item => {
const groupId = item.imGroupId || `order_${item.orderId}`
item.imUnread = unreadMap[groupId] || 0
totalImUnread += item.imUnread
})
// 更新 tabBar badge系统未读 + IM未读
this.imUnreadTotal = totalImUnread
this.updateTabBarBadge()
} catch (e) {
// IM 未登录时静默处理
}
} catch (e) {
console.error('[消息页] 加载聊天列表失败:', e)
}
},
/** 更新底部导航栏未读数 badge */
updateTabBarBadge() {
const total = (this.unreadCount.totalUnread || 0) + (this.imUnreadTotal || 0)
if (total > 0) {
uni.setTabBarBadge({
index: 2,
text: total > 99 ? '99+' : String(total)
})
} else {
uni.removeTabBarBadge({ index: 2 })
}
},
/** 跳转系统消息页 */
goSystemMsg() {
uni.navigateTo({ url: '/pages/message/system-msg' })
},
/** 跳转订单通知页 */
goOrderNotify() {
uni.navigateTo({ url: '/pages/message/order-notify' })
},
/** 跳转聊天页 */
goChat(item) {
uni.navigateTo({
url: `/pages/message/chat?orderId=${item.orderId}`
})
},
/** 显示昵称,过滤掉 IM 用户ID 格式 */
displayNickname(item) {
const nick = item.targetNickname
if (!nick || /^user_\d+$/.test(nick)) {
return item.targetUserId ? `用户${item.targetUserId}` : '用户'
}
return nick
},
/** 获取订单摘要标签 */
getOrderLabel(item) {
const typeMap = { Pickup: '代取', Delivery: '代送', Help: '万能帮', Purchase: '代购', Food: '美食街' }
const typeName = typeMap[item.orderType] || item.orderType
return `${typeName} · ${item.itemName || '订单'} · ¥${item.commission}`
},
/** 获取状态标签 */
getStatusLabel(status) {
const map = { Pending: '待接单', InProgress: '进行中', WaitConfirm: '待确认', Completed: '已完成' }
return map[status] || status
},
/** 格式化时间显示 */
formatTime(dateStr) {
if (!dateStr) return ''
const date = new Date(typeof dateStr === 'string' && !dateStr.endsWith('Z') ? dateStr + 'Z' : dateStr)
const now = new Date()
const isToday = date.toDateString() === now.toDateString()
const pad = (n) => String(n).padStart(2, '0')
if (isToday) {
return `${pad(date.getHours())}:${pad(date.getMinutes())}`
}
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
},
/** 触摸开始 */
onTouchStart(e, item) {
item._startX = e.touches[0].clientX
item._startY = e.touches[0].clientY
item._moving = false
},
/** 触摸移动 */
onTouchMove(e, item) {
const dx = e.touches[0].clientX - item._startX
const dy = e.touches[0].clientY - item._startY
// 水平滑动才处理
if (Math.abs(dx) < Math.abs(dy)) return
item._moving = true
// 限制滑动范围
const x = Math.max(-70, Math.min(0, dx + (item._prevX || 0)))
item.slideX = x
},
/** 触摸结束 */
onTouchEnd(item) {
// 超过一半就展开,否则收回
if (item.slideX < -35) {
item.slideX = -70
item._prevX = -70
} else {
item.slideX = 0
item._prevX = 0
}
},
/** 删除聊天记录 */
onDeleteChat(item) {
this.chatList = this.chatList.filter(c => c.orderId !== item.orderId)
}
}
}
</script>
<style scoped>
.message-page {
min-height: 100vh;
background-color: #f5f5f5;
}
/* 自定义导航栏 */
.custom-navbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 999;
background: linear-gradient(to right, #FFB700, #FFD59B);
}
.navbar-content {
height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.navbar-title {
font-size: 34rpx;
font-weight: bold;
color: #363636;
}
.message-content {
padding: 20rpx 24rpx;
}
.entry-item {
display: flex;
align-items: center;
background-color: #ffffff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
}
.entry-icon {
width: 80rpx;
height: 80rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
flex-shrink: 0;
}
.system-icon {
background-color: #e8f4fd;
}
.order-icon {
background-color: #fff3e0;
}
.icon-text {
font-size: 40rpx;
}
.entry-info {
flex: 1;
}
.entry-title {
font-size: 30rpx;
color: #333333;
font-weight: 500;
display: block;
}
.entry-desc {
font-size: 24rpx;
color: #999999;
margin-top: 6rpx;
display: block;
}
.entry-right {
display: flex;
align-items: center;
flex-shrink: 0;
}
.badge {
background-color: #ff4d4f;
color: #ffffff;
font-size: 22rpx;
min-width: 36rpx;
height: 36rpx;
line-height: 36rpx;
text-align: center;
border-radius: 18rpx;
padding: 0 10rpx;
margin-right: 12rpx;
}
.arrow {
font-size: 36rpx;
color: #cccccc;
}
.section-title {
font-size: 28rpx;
color: #999999;
margin: 30rpx 0 16rpx;
}
.chat-item-wrap {
position: relative;
overflow: hidden;
margin-bottom: 16rpx;
border-radius: 16rpx;
}
.chat-item {
display: flex;
align-items: center;
background-color: #ffffff;
border-radius: 16rpx;
padding: 24rpx 30rpx;
transition: transform 0.2s;
position: relative;
z-index: 1;
}
.delete-btn {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 70px;
background: #ff4d4f;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0 16rpx 16rpx 0;
}
.delete-btn text {
color: #fff;
font-size: 28rpx;
}
.chat-avatar {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
flex-shrink: 0;
}
.avatar-wrap {
position: relative;
margin-right: 24rpx;
flex-shrink: 0;
}
.avatar-wrap .chat-avatar {
margin-right: 0;
}
.unread-badge {
position: absolute;
top: -8rpx;
right: -8rpx;
background: #ff4d4f;
min-width: 36rpx;
height: 36rpx;
line-height: 36rpx;
border-radius: 18rpx;
padding: 0 8rpx;
text-align: center;
}
.unread-badge text {
font-size: 20rpx;
color: #fff;
}
.chat-info {
flex: 1;
overflow: hidden;
}
.chat-top {
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-name {
font-size: 30rpx;
color: #333333;
font-weight: 500;
}
.chat-time {
font-size: 22rpx;
color: #cccccc;
}
.chat-msg {
font-size: 24rpx;
color: #999999;
margin-top: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
}
.chat-tag {
flex-shrink: 0;
margin-left: 12rpx;
}
.chat-tag text {
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 6rpx;
}
.tag-Completed text {
color: #52c41a;
background: #f6ffed;
}
.tag-InProgress text {
color: #e64340;
background: #fff1f0;
}
.tag-WaitConfirm text {
color: #FFB700;
background: #FFF8E6;
}
.tag-Pending text {
color: #999;
background: #f5f5f5;
}
.empty-chat {
text-align: center;
padding: 80rpx 0;
}
.empty-text {
font-size: 28rpx;
color: #cccccc;
}
</style>