All checks were successful
continuous-integration/drone/push Build is passing
529 lines
11 KiB
Vue
529 lines
11 KiB
Vue
<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>
|