Files
medical-mall/pages/mall/merchant/messages.uvue
2026-03-20 17:30:30 +08:00

239 lines
8.4 KiB
Plaintext
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="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>