239 lines
8.4 KiB
Plaintext
239 lines
8.4 KiB
Plaintext
<!-- 商家端 - 消息中心页面 -->
|
||
<template>
|
||
<view class="messages-page">
|
||
<view class="header">
|
||
<text class="header-title">消息</text>
|
||
<text class="header-subtitle">与客户的聊天记录</text>
|
||
</view>
|
||
|
||
<scroll-view class="messages-list" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
|
||
<view v-if="loading && conversations.length === 0" class="loading-container">
|
||
<text class="loading-icon">⏳</text>
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
|
||
<view v-else-if="conversations.length === 0" class="empty-container">
|
||
<text class="empty-icon">💬</text>
|
||
<text class="empty-text">暂无会话</text>
|
||
<text class="empty-hint">有客户消息时会在这里显示</text>
|
||
</view>
|
||
|
||
<view v-else class="conv-list">
|
||
<view v-for="conv in conversations" :key="conv.sessionId" class="conversation-card" @click="goToChat(conv)">
|
||
<view class="conv-avatar-wrap">
|
||
<image class="conv-avatar" :src="conv.avatar || '/static/images/default-avatar.png'" mode="aspectFill" />
|
||
<view v-if="conv.unread > 0" class="online-dot"></view>
|
||
</view>
|
||
<view class="conv-info">
|
||
<view class="conv-row">
|
||
<text class="conv-name">{{ conv.name }}</text>
|
||
<text class="conv-time">{{ conv.lastTime }}</text>
|
||
</view>
|
||
<view class="conv-bottom">
|
||
<text class="conv-preview">{{ conv.lastMessage }}</text>
|
||
<view v-if="conv.unread > 0" class="unread-badge">
|
||
<text class="unread-num">{{ conv.unread > 99 ? '99+' : conv.unread }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<text class="conv-arrow">›</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="safe-bottom"></view>
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script lang="uts">
|
||
import supa from '@/components/supadb/aksupainstance.uts'
|
||
|
||
type MessageType = {
|
||
id: string
|
||
session_id: string
|
||
sender_id: string
|
||
receiver_id: string
|
||
content: string
|
||
msg_type: string
|
||
is_read: boolean
|
||
is_from_user: boolean
|
||
created_at: string
|
||
}
|
||
|
||
type ConversationType = {
|
||
sessionId: string
|
||
name: string
|
||
avatar: string
|
||
lastMessage: string
|
||
lastTime: string
|
||
lastTimeRaw: string
|
||
unread: number
|
||
userId: string
|
||
}
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
conversations: [] as ConversationType[],
|
||
loading: false,
|
||
refreshing: false,
|
||
merchantId: ''
|
||
}
|
||
},
|
||
|
||
onLoad() {
|
||
this.initMerchantId()
|
||
},
|
||
|
||
onShow() {
|
||
this.loadMessages()
|
||
},
|
||
|
||
methods: {
|
||
async initMerchantId() {
|
||
try {
|
||
const session = supa.getSession()
|
||
this.merchantId = session?.user?.getString('id') || uni.getStorageSync('user_id') || ''
|
||
} catch (e) {}
|
||
},
|
||
|
||
async loadMessages() {
|
||
this.loading = true
|
||
|
||
try {
|
||
const query = supa
|
||
.from('ml_chat_messages')
|
||
.select('*')
|
||
.or(`receiver_id.eq.${this.merchantId},sender_id.eq.${this.merchantId}`)
|
||
.order('created_at', { ascending: false })
|
||
.limit(100)
|
||
|
||
const response = await query.execute()
|
||
|
||
if (response.error != null || !response.data) {
|
||
this.conversations = []
|
||
return
|
||
}
|
||
|
||
const rawData = response.data as any[]
|
||
const sessionMap = new Map<string, ConversationType>()
|
||
|
||
for (let i = 0; i < rawData.length; i++) {
|
||
const item = rawData[i] as UTSJSONObject
|
||
const isFromUser = item.getBoolean('is_from_user') || false
|
||
const senderId = item.getString('sender_id') || ''
|
||
const receiverId = item.getString('receiver_id') || ''
|
||
|
||
// is_from_user=true 表示消息来自用户,sender_id是用户ID
|
||
// is_from_user=false 表示消息来自商家,receiver_id是用户ID
|
||
const otherUserId = isFromUser ? senderId : receiverId
|
||
|
||
// 只用 otherUserId 分组,确保每个客户只有一个会话
|
||
if (otherUserId === '' || otherUserId === this.merchantId) continue
|
||
|
||
const isRead = item.getBoolean('is_read') || false
|
||
const content = item.getString('content') || ''
|
||
const createdAt = item.getString('created_at') || ''
|
||
const sessionId = item.getString('session_id') || otherUserId
|
||
|
||
if (!sessionMap.has(otherUserId)) {
|
||
sessionMap.set(otherUserId, {
|
||
sessionId: sessionId,
|
||
name: '客户',
|
||
avatar: '',
|
||
lastMessage: content,
|
||
lastTime: this.formatTime(createdAt),
|
||
lastTimeRaw: createdAt,
|
||
unread: 0,
|
||
userId: otherUserId
|
||
})
|
||
}
|
||
|
||
const conv = sessionMap.get(otherUserId)!
|
||
// 更新最后一条消息(按时间最新的)
|
||
if (createdAt > conv.lastTimeRaw) {
|
||
conv.lastMessage = content
|
||
conv.lastTime = this.formatTime(createdAt)
|
||
conv.lastTimeRaw = createdAt
|
||
}
|
||
// 未读消息:消息来自用户且未读
|
||
if (!isRead && isFromUser) {
|
||
conv.unread++
|
||
}
|
||
}
|
||
|
||
this.conversations = Array.from(sessionMap.values()).sort((a, b) => b.unread - a.unread)
|
||
|
||
} catch (e) {
|
||
console.error('加载消息失败:', e)
|
||
} finally {
|
||
this.loading = false
|
||
this.refreshing = false
|
||
}
|
||
},
|
||
|
||
goToChat(conv: ConversationType) {
|
||
uni.navigateTo({
|
||
url: `/pages/mall/merchant/chat?user_id=${conv.userId}&session_id=${conv.sessionId}&title=${encodeURIComponent(conv.name)}`
|
||
})
|
||
},
|
||
|
||
onRefresh() {
|
||
this.refreshing = true
|
||
this.loadMessages()
|
||
},
|
||
|
||
formatTime(timeStr: string): string {
|
||
if (!timeStr) return ''
|
||
const date = new Date(timeStr)
|
||
const now = new Date()
|
||
const diff = now.getTime() - date.getTime()
|
||
const hours = Math.floor(diff / (1000 * 60 * 60))
|
||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||
|
||
if (hours < 1) return '刚刚'
|
||
if (hours < 24) return `${hours}小时前`
|
||
if (days === 1) return '昨天'
|
||
if (days < 7) return `${days}天前`
|
||
return `${date.getMonth() + 1}-${date.getDate()}`
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.messages-page { background-color: #f5f7fa; min-height: 100vh; display: flex; flex-direction: column; }
|
||
|
||
.header { background-color: #fff; padding-top: 60rpx; padding-bottom: 24rpx; padding-left: 30rpx; padding-right: 30rpx; border-bottom-width: 1rpx; border-bottom-style: solid; border-bottom-color: #eee; }
|
||
.header-title { font-size: 44rpx; font-weight: bold; color: #333; display: block; margin-bottom: 8rpx; }
|
||
.header-subtitle { font-size: 26rpx; color: #999; }
|
||
|
||
.messages-list { flex: 1; padding: 20rpx; }
|
||
|
||
.loading-container { display: flex; flex-direction: column; align-items: center; justify-content: center; padding-top: 100rpx; padding-bottom: 100rpx; }
|
||
.loading-icon { font-size: 60rpx; margin-bottom: 20rpx; }
|
||
.loading-text { font-size: 28rpx; color: #999; }
|
||
|
||
.empty-container { display: flex; flex-direction: column; align-items: center; justify-content: center; padding-top: 100rpx; padding-bottom: 100rpx; }
|
||
.empty-icon { font-size: 100rpx; margin-bottom: 20rpx; }
|
||
.empty-text { font-size: 32rpx; color: #666; margin-bottom: 10rpx; }
|
||
.empty-hint { font-size: 26rpx; color: #999; }
|
||
|
||
.conv-list { display: flex; flex-direction: column; }
|
||
.conversation-card { display: flex; flex-direction: row; align-items: center; background-color: #fff; border-radius: 20rpx; padding: 24rpx; margin-bottom: 16rpx; }
|
||
.conv-avatar-wrap { position: relative; margin-right: 20rpx; }
|
||
.conv-avatar { width: 100rpx; height: 100rpx; border-radius: 50rpx; background-color: #e0e0e0; }
|
||
.online-dot { position: absolute; bottom: 4rpx; right: 4rpx; width: 24rpx; height: 24rpx; background-color: #4CAF50; border-radius: 12rpx; border-width: 3rpx; border-style: solid; border-color: #fff; }
|
||
.conv-info { flex: 1; }
|
||
.conv-row { display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 12rpx; }
|
||
.conv-name { font-size: 32rpx; color: #333; font-weight: bold; }
|
||
.conv-time { font-size: 24rpx; color: #999; }
|
||
.conv-bottom { display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
|
||
.conv-preview { font-size: 28rpx; color: #666; flex: 1; margin-right: 16rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.unread-badge { min-width: 36rpx; height: 36rpx; background-color: #FF3B30; border-radius: 18rpx; display: flex; align-items: center; justify-content: center; padding-left: 10rpx; padding-right: 10rpx; }
|
||
.unread-num { font-size: 22rpx; color: #fff; font-weight: bold; }
|
||
.conv-arrow { font-size: 40rpx; color: #ccc; margin-left: 10rpx; }
|
||
|
||
.safe-bottom { height: 30rpx; }
|
||
</style>
|