Files
medical-mall/pages/mall/merchant/chat.uvue
2026-03-20 15:43:33 +08:00

705 lines
24 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="chat-page">
<<<<<<< HEAD
<view class="chat-header">
=======
<!-- 聊天头部 -->
<view class="chat-header" :style="{ paddingTop: navPaddingTop }">
>>>>>>> local-backup-root-cyj
<view class="header-back" @click="goBack">
<text class="back-icon"></text>
</view>
<view class="header-info">
<<<<<<< HEAD
<text class="chat-title">{{ chatTitle }}</text>
<text class="chat-status">在线</text>
</view>
<view class="header-actions"></view>
</view>
<scroll-view scroll-y class="chat-content" :scroll-into-view="scrollToView" scroll-with-animation>
<view class="chat-messages">
<view v-for="msg in chatMessages" :key="msg.id" :class="['message-item', msg.is_from_user ? 'me' : 'received']" :id="'msg-' + msg.id">
<view v-if="!msg.is_from_user" class="message-wrapper">
<image class="avatar" src="/static/images/default-avatar.png" mode="aspectFill" />
<view class="message-content-wrapper">
<view class="message-bubble">
<text class="message-text">{{ msg.content }}</text>
<text class="message-time">{{ formatTime(msg.created_at) }}</text>
</view>
</view>
</view>
<view v-else class="message-wrapper me">
<view class="message-content-wrapper">
<view class="message-bubble me">
<text class="message-text">{{ msg.content }}</text>
<text class="message-time">{{ formatTime(msg.created_at) }}</text>
</view>
</view>
<image class="avatar me" src="/static/images/default-shop.png" mode="aspectFill" />
=======
<view class="header-info-text-wrapper">
<text class="chat-title">{{ chatTitle }}</text>
<text class="chat-status">在线</text>
</view>
</view>
<view class="header-actions">
<view class="action-icon" @click="showMoreActions">
<text class="action-icon-text">⋯</text>
</view>
</view>
</view>
<!-- 聊天内容 -->
<scroll-view
scroll-y="true"
class="chat-content"
:scroll-into-view="scrollToView"
:scroll-with-animation="true"
:show-scrollbar="false"
>
<view class="chat-messages">
<!-- 系统消息 -->
<view class="message-item system">
<text class="system-text">已接入客户对话</text>
</view>
<!-- 消息列表 -->
<view
v-for="message in chatMessages"
:key="message.id"
:class="['message-item', message.is_from_user ? 'received' : 'sent']"
:id="message.viewId"
>
<!-- 客户消息 -->
<view v-if="message.is_from_user" class="message-wrapper">
<image
class="avatar"
:src="userAvatar"
mode="aspectFill"
/>
<view class="message-content-wrapper">
<text class="sender-name">{{ chatTitle }}</text>
<view class="message-bubble received-bubble" :class="{ 'image-bubble': message.msg_type === 'image' || message.content.includes('/chat_images/') }">
<text v-if="!(message.msg_type === 'image' || message.content.includes('/chat_images/'))" class="message-text">{{ message.content }}</text>
<image v-else mode="widthFix" class="message-image" :src="message.content" @click="previewImage(message.content)" />
<text class="message-time">{{ message.time }}</text>
</view>
</view>
</view>
<!-- 我的消息 -->
<view v-else class="message-wrapper me">
<view class="message-content-wrapper">
<view class="message-bubble me" :class="{ 'image-bubble': message.msg_type === 'image' || message.content.includes('/chat_images/') }">
<text v-if="!(message.msg_type === 'image' || message.content.includes('/chat_images/'))" class="message-text">{{ message.content }}</text>
<image v-else mode="widthFix" class="message-image" :src="message.content" @click="previewImage(message.content)" />
<text class="message-time">{{ message.time }}</text>
</view>
</view>
<image
class="avatar me"
:src="shopAvatar"
mode="aspectFill"
/>
>>>>>>> local-backup-root-cyj
</view>
</view>
</view>
</scroll-view>
<<<<<<< HEAD
<view class="chat-input">
<input v-model="inputText" class="input-field" placeholder="请输入消息..." confirm-type="send" @confirm="sendMessage" />
<view class="send-btn" @click="sendMessage">
<text class="send-icon">➤</text>
</view>
</view>
=======
<!-- 聊天输入区 -->
<view class="chat-input">
<view class="input-tools">
<text class="tool-icon" @click="showEmojiPicker">😊</text>
<text class="tool-icon" @click="showImagePicker">📷</text>
</view>
<view class="input-wrapper">
<input
class="message-input"
v-model="inputText"
placeholder="请输入消息..."
:focus="inputFocus"
@confirm="sendMessage"
confirm-type="send"
/>
<button
class="send-button"
:class="{ active: inputText.trim() != '' }"
@click="sendMessage"
>
发送
</button>
</view>
</view>
<!-- 表情选择器 -->
<scroll-view v-if="showEmoji" class="emoji-picker" direction="vertical">
<view class="emoji-category">
<text
v-for="emoji in emojiList"
:key="emoji"
class="emoji-item"
@click="insertEmoji(emoji)"
>
{{ emoji }}
</text>
</view>
</scroll-view>
>>>>>>> local-backup-root-cyj
</view>
</template>
<script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts'
<<<<<<< HEAD
type ChatMessageType = {
id: string
=======
import type { AkSupaRealtimeChannel } from '@/components/supadb/aksupa.uts'
type ChatMessageType = {
id: string
viewId: string
>>>>>>> local-backup-root-cyj
session_id: string
sender_id: string
receiver_id: string
content: string
msg_type: string
is_read: boolean
is_from_user: boolean
created_at: string
<<<<<<< HEAD
=======
time: string
>>>>>>> local-backup-root-cyj
}
export default {
data() {
return {
sessionId: '',
chatUserId: '',
chatTitle: '客户',
inputText: '',
chatMessages: [] as ChatMessageType[],
scrollToView: '',
<<<<<<< HEAD
merchantId: ''
=======
merchantId: '',
userAvatar: '/static/images/default-avatar.png',
shopAvatar: '/static/images/default-shop.png',
navPaddingTop: '30px',
showEmoji: false,
inputFocus: false,
realtimeChannel: null as AkSupaRealtimeChannel | null
}
},
computed: {
emojiList(): string[] {
return ['😊', '😂', '🤣', '😍', '😘', '🥰', '😭', '😡', '👍', '👏', '🙏', '🎉', '❤️', '🔥', '⭐']
>>>>>>> local-backup-root-cyj
}
},
onLoad(options: any) {
<<<<<<< HEAD
=======
const sysInfo = uni.getSystemInfoSync()
const statusBarH = sysInfo.statusBarHeight
this.navPaddingTop = (statusBarH + 10) + 'px'
console.log('chat page onLoad options:', options)
>>>>>>> local-backup-root-cyj
if (options.session_id) {
this.sessionId = options.session_id
}
if (options.user_id) {
this.chatUserId = options.user_id
}
if (options.title) {
this.chatTitle = decodeURIComponent(options.title)
}
this.initMerchantId()
},
onShow() {
<<<<<<< HEAD
this.loadChatMessages()
=======
console.log('chat page onShow, chatUserId:', this.chatUserId, 'merchantId:', this.merchantId)
if (this.merchantId) {
this.loadChatMessages()
this.setupRealtimeSubscription()
} else {
setTimeout(() => {
this.loadChatMessages()
this.setupRealtimeSubscription()
}, 300)
}
},
onUnload() {
if (this.realtimeChannel != null) {
supa.removeChannel(this.realtimeChannel!)
}
>>>>>>> local-backup-root-cyj
},
methods: {
async initMerchantId() {
try {
const session = supa.getSession()
<<<<<<< HEAD
this.merchantId = session?.user?.getString('id') || uni.getStorageSync('user_id') || ''
} catch (e) {}
},
async loadChatMessages() {
try {
let query
if (this.sessionId) {
query = supa
.from('ml_chat_messages')
.select('*')
.eq('session_id', this.sessionId)
.order('created_at', { ascending: true })
} else if (this.chatUserId && this.merchantId) {
query = supa
.from('ml_chat_messages')
.select('*')
.or(`and(sender_id.eq.${this.chatUserId},receiver_id.eq.${this.merchantId}),and(sender_id.eq.${this.merchantId},receiver_id.eq.${this.chatUserId})`)
.order('created_at', { ascending: true })
}
if (query) {
const response = await query.execute()
if (response.data && (response.data as any[]).length > 0) {
const rawData = response.data as any[]
const messages: ChatMessageType[] = []
for (let i = 0; i < rawData.length; i++) {
const item = rawData[i] as UTSJSONObject
const senderId = item.getString('sender_id')
messages.push({
id: item.getString('id') || '',
session_id: item.getString('session_id') || '',
sender_id: senderId || '',
receiver_id: item.getString('receiver_id') || '',
content: item.getString('content') || '',
msg_type: item.getString('msg_type') || 'text',
is_read: item.getBoolean('is_read') || false,
is_from_user: senderId === this.merchantId,
created_at: item.getString('created_at') || ''
} as ChatMessageType)
}
this.chatMessages = messages
this.scrollToView = messages.length > 0 ? 'msg-' + messages[messages.length - 1].id : ''
this.markAsRead()
}
=======
if (session != null && session.user != null) {
this.merchantId = session.user.getString('id') || ''
}
if (!this.merchantId) {
this.merchantId = uni.getStorageSync('user_id') || ''
}
// 加载店铺头像
this.loadShopAvatar()
} catch (e) {
console.error('获取商户ID失败:', e)
}
},
async loadShopAvatar() {
try {
const response = await supa
.from('ml_shops')
.select('shop_logo')
.eq('merchant_id', this.merchantId)
.limit(1)
.execute()
if (response.data && (response.data as any[]).length > 0) {
const shopData = (response.data as any[])[0] as UTSJSONObject
const logo = shopData.getString('shop_logo')
if (logo && logo != '') {
this.shopAvatar = logo
}
}
} catch (e) {
console.error('加载店铺头像失败:', e)
}
},
async loadChatMessages() {
if (!this.chatUserId) {
console.error('chatUserId 为空')
return
}
try {
const response = await supa
.from('ml_chat_messages')
.select('*')
.or(`sender_id.eq.${this.chatUserId},receiver_id.eq.${this.chatUserId}`)
.order('created_at', { ascending: true })
.limit(200)
.execute()
console.log('聊天记录查询结果:', response.data, 'error:', response.error)
if (response.error != null) {
console.error('查询聊天记录失败:', response.error)
return
}
if (response.data && (response.data as any[]).length > 0) {
const rawData = response.data as any[]
const messages: ChatMessageType[] = []
for (let i = 0; i < rawData.length; i++) {
const item = rawData[i] as UTSJSONObject
const senderId = item.getString('sender_id') || ''
const isFromUser = senderId === this.chatUserId
const createdAt = item.getString('created_at') || ''
const msgId = item.getString('id') || ''
const safeViewId = 'msg_' + msgId.replace(/[^a-zA-Z0-9]/g, '_')
const date = new Date(createdAt)
const timeStr = date.getHours().toString().padStart(2, '0') + ':' + date.getMinutes().toString().padStart(2, '0')
messages.push({
id: msgId,
viewId: safeViewId,
session_id: item.getString('session_id') || '',
sender_id: senderId,
receiver_id: item.getString('receiver_id') || '',
content: item.getString('content') || '',
msg_type: item.getString('msg_type') || 'text',
is_read: item.getBoolean('is_read') || false,
is_from_user: isFromUser,
created_at: createdAt,
time: timeStr
} as ChatMessageType)
}
this.chatMessages = messages
this.scrollToBottom()
this.markAsRead()
} else {
console.log('没有找到聊天记录')
this.chatMessages = []
>>>>>>> local-backup-root-cyj
}
} catch (e) {
console.error('加载聊天记录失败:', e)
}
},
<<<<<<< HEAD
=======
setupRealtimeSubscription(): void {
console.log('开始建立聊天实时订阅...')
this.realtimeChannel = supa.channel('merchant-chat-' + Date.now().toString())
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'ml_chat_messages'
}, (payload: any) => {
console.log('收到实时消息:', payload)
const payloadObj = (payload instanceof UTSJSONObject) ? (payload as UTSJSONObject) : (JSON.parse(JSON.stringify(payload ?? {})) as UTSJSONObject)
const newMsgAny = payloadObj.get('new')
if (newMsgAny == null) return
const newMsg = (newMsgAny instanceof UTSJSONObject) ? (newMsgAny as UTSJSONObject) : (JSON.parse(JSON.stringify(newMsgAny)) as UTSJSONObject)
const senderId = newMsg.getString('sender_id') ?? ''
const receiverId = newMsg.getString('receiver_id') ?? ''
const msgId = newMsg.getString('id') ?? ''
const content = newMsg.getString('content') ?? ''
// 检查是否与当前聊天相关
if (senderId != this.chatUserId && receiverId != this.chatUserId) {
return
}
// 检查消息是否已存在
for (let i = 0; i < this.chatMessages.length; i++) {
if (this.chatMessages[i].id == msgId) return
}
const isFromUser = senderId === this.chatUserId
const createdAt = newMsg.getString('created_at') ?? new Date().toISOString()
const date = new Date(createdAt)
const timeStr = date.getHours().toString().padStart(2, '0') + ':' + date.getMinutes().toString().padStart(2, '0')
const safeViewId = 'msg_' + msgId.replace(/[^a-zA-Z0-9]/g, '_')
this.chatMessages.push({
id: msgId,
viewId: safeViewId,
session_id: newMsg.getString('session_id') || '',
sender_id: senderId,
receiver_id: receiverId,
content: content,
msg_type: newMsg.getString('msg_type') || 'text',
is_read: false,
is_from_user: isFromUser,
created_at: createdAt,
time: timeStr
} as ChatMessageType)
this.scrollToBottom()
if (isFromUser) {
this.markAsRead()
}
})
.subscribe((status: string, err: any | null) => {
console.log('订阅状态:', status)
if (err != null) {
console.log('订阅错误:', err)
}
})
},
scrollToBottom(): void {
if (this.chatMessages.length === 0) return
const lastMsg = this.chatMessages[this.chatMessages.length - 1]
const targetId = lastMsg.viewId
this.scrollToView = ''
setTimeout(() => {
this.scrollToView = targetId
}, 100)
},
>>>>>>> local-backup-root-cyj
async markAsRead() {
try {
await supa
.from('ml_chat_messages')
.update({ is_read: true })
.eq('receiver_id', this.merchantId)
<<<<<<< HEAD
.eq('is_read', false)
.execute()
} catch (e) {}
},
async sendMessage() {
if (!this.inputText.trim()) return
const content = this.inputText.trim()
this.inputText = ''
=======
.eq('sender_id', this.chatUserId)
.eq('is_read', false)
.execute()
} catch (e) {
console.error('标记已读失败:', e)
}
},
async sendMessage() {
const content = this.inputText.trim()
if (content == '') return
this.inputText = ''
this.showEmoji = false
>>>>>>> local-backup-root-cyj
try {
const newMessage = {
session_id: this.sessionId || null,
sender_id: this.merchantId,
receiver_id: this.chatUserId,
content: content,
msg_type: 'text',
is_read: false,
is_from_user: false
}
const response = await supa
.from('ml_chat_messages')
.insert([newMessage])
.execute()
<<<<<<< HEAD
if (!response.error) {
this.loadChatMessages()
}
} catch (e) {
console.error('发送消息失败:', e)
}
},
goBack() {
uni.navigateBack()
},
formatTime(timeStr: string): string {
if (!timeStr) return ''
const date = new Date(timeStr)
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
return `${hours}:${minutes}`
=======
if (response.error != null) {
console.error('发送消息失败:', response.error)
uni.showToast({ title: '发送失败', icon: 'none' })
}
} catch (e) {
console.error('发送消息异常:', e)
uni.showToast({ title: '发送失败', icon: 'none' })
}
},
showEmojiPicker(): void {
this.showEmoji = !this.showEmoji
if (this.showEmoji) {
uni.hideKeyboard()
}
},
insertEmoji(emoji: string): void {
this.inputText += emoji
this.showEmoji = false
this.inputFocus = true
},
showImagePicker(): void {
uni.chooseImage({
count: 1,
success: (res) => {
console.log('选择图片:', res.tempFilePaths)
}
})
},
showMoreActions(): void {
uni.showActionSheet({
itemList: ['结束对话', '清除记录'],
success: (res) => {
switch (res.tapIndex) {
case 0:
uni.navigateBack()
break
case 1:
this.chatMessages = []
break
}
}
})
},
previewImage(url : string) {
if (url == '') return
uni.previewImage({
urls: [url],
current: 0
})
},
goBack() {
uni.navigateBack()
>>>>>>> local-backup-root-cyj
}
}
}
</script>
<style>
<<<<<<< HEAD
.chat-page { display: flex; flex-direction: column; height: 100vh; background-color: #f5f5f5; }
.chat-header { display: flex; align-items: center; padding: 20rpx 30rpx; background-color: #fff; border-bottom: 1rpx solid #eee; }
.header-back { padding: 10rpx 20rpx 10rpx 0; }
.back-icon { font-size: 48rpx; color: #333; font-weight: bold; }
.header-info { flex: 1; display: flex; flex-direction: column; align-items: center; }
.chat-title { font-size: 32rpx; color: #333; font-weight: 500; }
.chat-status { font-size: 22rpx; color: #4CAF50; }
.header-actions { padding: 10rpx; }
.chat-content { flex: 1; padding: 20rpx; }
.chat-messages { display: flex; flex-direction: column; }
.message-item { margin-bottom: 30rpx; }
.message-wrapper { display: flex; align-items: flex-start; }
.message-wrapper.me { flex-direction: row-reverse; }
.avatar { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin: 0 20rpx; }
.message-content-wrapper { max-width: 70%; }
.message-bubble { background-color: #fff; padding: 20rpx; border-radius: 12rpx; position: relative; }
.message-bubble.me { background-color: #007AFF; }
.me .message-text { color: #fff; }
.me .message-time { color: rgba(255,255,255,0.7); }
.message-text { font-size: 28rpx; color: #333; line-height: 1.4; }
.message-time { display: block; font-size: 20rpx; color: #999; margin-top: 10rpx; text-align: right; }
.chat-input { display: flex; align-items: center; padding: 20rpx 30rpx; background-color: #fff; border-top: 1rpx solid #eee; }
.input-field { flex: 1; height: 72rpx; background-color: #f5f5f5; border-radius: 36rpx; padding: 0 30rpx; font-size: 28rpx; }
.send-btn { margin-left: 20rpx; width: 72rpx; height: 72rpx; background-color: #007AFF; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
.send-icon { font-size: 32rpx; color: #fff; }
=======
.chat-page { width: 100%; flex: 1; background-color: #f5f5f5; display: flex; flex-direction: column; overflow: hidden; }
.chat-header { background-color: white; padding-left: 15px; padding-right: 15px; padding-bottom: 10px; display: flex; flex-direction: row; align-items: center; justify-content: space-between; border-bottom-width: 1px; border-bottom-style: solid; border-bottom-color: #eee; flex-shrink: 0; }
.header-back { width: 40px; }
.back-icon { font-size: 24px; color: #333; }
.header-info { flex: 1; }
.header-info-text-wrapper { display: flex; flex-direction: column; align-items: center; }
.chat-title { font-size: 16px; font-weight: bold; color: #333; margin-bottom: 2px; }
.chat-status { font-size: 12px; color: #34c759; }
.header-actions .action-icon { font-size: 20px; color: #333; width: 40px; }
.action-icon-text { text-align: right; width: 100%; }
.chat-content { flex: 1; height: 0; padding: 10px; padding-bottom: 20px; }
.chat-messages { display: flex; flex-direction: column; padding-bottom: 80px; }
.message-item.system { display: flex; flex-direction: row; justify-content: center; margin-bottom: 20px; }
.system-text { font-size: 12px; color: #999; background-color: #f0f0f0; padding-top: 5px; padding-bottom: 5px; padding-left: 15px; padding-right: 15px; border-radius: 15px; text-align: center; }
.message-wrapper { display: flex; flex-direction: row; margin-bottom: 15px; }
.message-wrapper.me { justify-content: flex-end; }
.avatar { width: 40px; height: 40px; border-radius: 20px; margin-right: 10px; flex-shrink: 0; background-color: #e0e0e0; }
.avatar.me { margin-right: 0; margin-left: 10px; }
.message-content-wrapper { width: 260px; display: flex; flex-direction: column; }
.message-bubble { background-color: white; padding-top: 10px; padding-bottom: 10px; padding-left: 15px; padding-right: 15px; border-radius: 12px; }
.received-bubble { align-self: flex-start; border-top-left-radius: 2px; }
.message-bubble.me { background-color: #95ec69; align-self: flex-end; border-top-right-radius: 2px; }
.sender-name { font-size: 11px; color: #999; margin-bottom: 2px; align-self: flex-start; }
.message-text { font-size: 15px; color: #333; line-height: 1.4; margin-bottom: 5px; }
.message-time { font-size: 11px; color: #999; text-align: right; }
.chat-input { background-color: white; border-top-width: 1px; border-top-style: solid; border-top-color: #eee; padding-top: 10px; padding-bottom: 20px; padding-left: 15px; padding-right: 15px; position: fixed; bottom: 0; left: 0; right: 0; flex-shrink: 0; }
.input-tools { display: flex; flex-direction: row; margin-bottom: 10px; }
.tool-icon { font-size: 20px; margin-right: 15px; color: #666; }
.input-wrapper { display: flex; flex-direction: row; align-items: center; }
.message-input { flex: 1; background-color: #f5f5f5; border-radius: 20px; padding-top: 10px; padding-bottom: 10px; padding-left: 15px; padding-right: 15px; font-size: 15px; margin-right: 10px; min-height: 40px; max-height: 100px; }
.send-button { background-color: #ccc; color: white; border-radius: 20px; padding-top: 8px; padding-bottom: 8px; padding-left: 20px; padding-right: 20px; font-size: 14px; min-width: 60px; }
.send-button.active { background-color: #ff5000; }
.emoji-picker { background-color: white; border-top-width: 1px; border-top-style: solid; border-top-color: #eee; padding: 10px; height: 200px; position: fixed; bottom: 80px; left: 0; right: 0; }
.emoji-category { display: flex; flex-direction: row; flex-wrap: wrap; }
.emoji-item { font-size: 24px; padding: 8px; width: 45px; height: 45px; display: flex; align-items: center; justify-content: center; }
.image-bubble { padding: 0 !important; background-color: transparent !important; }
.image-bubble .message-time { margin-top: 5px; text-align: right; }
.message-image { width: 150px; border-radius: 8px; }
>>>>>>> local-backup-root-cyj
</style>