consumer模块完成95%,在和商家端对接聊天购物闭环

This commit is contained in:
2026-02-06 17:10:31 +08:00
parent 06b7369494
commit e2f1dfb097
1454 changed files with 5425 additions and 210555 deletions

View File

@@ -2,12 +2,12 @@
<template>
<view class="chat-page">
<!-- 聊天头部 -->
<view class="chat-header">
<view class="chat-header" :style="{ paddingTop: navPaddingTop }">
<view class="header-back" @click="goBack">
<text class="back-icon"></text>
</view>
<view class="header-info">
<text class="chat-title">在线客服</text>
<text class="chat-title">{{ headerTitle }}</text>
<text class="chat-status">在线</text>
</view>
<view class="header-actions">
@@ -119,8 +119,10 @@
</template>
<script setup lang="uts">
import { ref, reactive, onMounted, nextTick } from 'vue'
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
import { supabaseService, type ChatMessage } from '@/utils/supabaseService.uts'
import supa from '@/components/supadb/aksupainstance.uts'
import { getCurrentUser } from '@/utils/store.uts'
// 响应式数据
const messages = ref<any[]>([])
@@ -128,64 +130,113 @@ const inputMessage = ref<string>('')
const inputFocus = ref<boolean>(false)
const showEmoji = ref<boolean>(false)
const scrollToView = ref<string>('')
const currentUserId = ref<string>('')
const merchantId = ref<string>('') // 商家ID
const headerTitle = ref<string>('在线客服')
const navPaddingTop = ref<string>('30px') // 默认值,包含状态栏高度+原有内边距
let realtimeChannel: any = null
// 模拟表情列表
const emojiList = ['😊', '😂', '🤣', '😍', '😘', '🥰', '😭', '😡', '👍', '👏', '🙏', '🎉', '❤️', '🔥', '⭐']
// Mock 聊天记录
const mockMessages = [
{
id: 1,
type: 'received',
content: '您好,欢迎咨询!有什么可以帮助您的吗?',
time: '14:30'
},
{
id: 2,
type: 'sent',
content: '你好,我昨天下的订单一直没有发货',
time: '14:31'
},
{
id: 3,
type: 'received',
content: '请问您的订单号是多少?我帮您查询一下',
time: '14:32'
},
{
id: 4,
type: 'sent',
content: '订单号是202311230001',
time: '14:33'
},
{
id: 5,
type: 'received',
content: '正在为您查询订单状态...',
time: '14:34'
}
]
// 生命周期
onMounted(() => {
loadChatHistory()
onLoad((options: any) => {
// 动态获取状态栏高度
const sysInfo = uni.getSystemInfoSync()
const statusBarH = sysInfo.statusBarHeight
// 状态栏高度 + 10px 原有顶部内边距
navPaddingTop.value = (statusBarH + 10) + 'px'
if (options.merchantId) {
merchantId.value = options.merchantId
}
if (options.merchantName) {
headerTitle.value = options.merchantName
}
})
onMounted(async () => {
const user = await getCurrentUser()
if (user) {
currentUserId.value = user.id
}
loadChatHistory()
setupRealtimeSubscription()
})
onUnmounted(() => {
if (realtimeChannel) {
supa.removeChannel(realtimeChannel)
}
})
// 建立实时订阅
const setupRealtimeSubscription = () => {
console.log('开始建立聊天实时订阅...')
realtimeChannel = supa.channel('public:ml_chat_messages')
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'ml_chat_messages' }, (payload: any) => {
const newMsg = payload.new
console.log('收到新消息:', newMsg)
// 如果是我发的消息因为已经乐观更新了所以忽略或者根据ID更新状态
if (newMsg.sender_id === currentUserId.value) {
return
}
// 如果是发给我的消息
if (newMsg.receiver_id === currentUserId.value) {
// 如果指定了商家,只接收该商家的消息
if (merchantId.value && newMsg.sender_id !== merchantId.value) {
return
}
// 转换为UI消息格式
const date = new Date(newMsg.created_at || new Date().toISOString())
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
const incomingMsg = {
id: newMsg.id || Date.now(), // 优先使用DB ID
type: 'received',
content: newMsg.content,
time: timeStr
}
messages.value.push(incomingMsg)
scrollToBottom()
}
})
.subscribe((status: string) => {
console.log('订阅状态:', status)
})
}
// 加载聊天记录
const loadChatHistory = async () => {
const rawMsgs = await supabaseService.getUserChatMessages()
let rawMsgs: ChatMessage[] = []
if (merchantId.value) {
rawMsgs = await supabaseService.getChatMessages(merchantId.value)
} else {
rawMsgs = await supabaseService.getUserChatMessages()
}
messages.value = rawMsgs.reverse().map((m: ChatMessage) => {
const date = new Date(m.created_at || new Date().toISOString())
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
// Use explicit 'as' casting to avoid type errors if needed, though map handles it
const msg : any = {
id: m.id,
type: m.is_from_user ? 'sent' : 'received',
type: m.is_from_user ? 'sent' : 'received', // 假设is_from_user标志是准确的或者比较 sender_id
content: m.content,
time: timeStr
}
// 双重确认类型
if (currentUserId.value && m.sender_id === currentUserId.value) {
msg.type = 'sent'
} else if (currentUserId.value && m.sender_id !== currentUserId.value) {
msg.type = 'received'
}
return msg
})
@@ -200,7 +251,7 @@ const sendMessage = async () => {
const content = inputMessage.value.trim()
if (!content) return
// 添加发送的消息
// 添加发送的消息 (乐观更新)
const newMessage = {
id: Date.now(),
type: 'sent',
@@ -214,44 +265,34 @@ const sendMessage = async () => {
// 滚动到底部
scrollToBottom()
// Backend Save
await supabaseService.sendChatMessage(content)
// 发送到 Supabase
// 如果有 merchantId发送给指定商家否则可能是发给系统或默认客服
const success = await supabaseService.sendChatMessage(content, merchantId.value || null)
// 模拟客服回复2秒后
if (!success) {
uni.showToast({
title: '发送失败',
icon: 'none'
})
// 实际项目中可能需要标记消息为发送失败状态
}
// 移除模拟回复,依赖 Realtime 接收真实回复
/*
setTimeout(() => {
simulateCustomerReply()
}, 2000)
*/
}
// 模拟客服回复
// 模拟客服回复 (已禁用,改用 Realtime)
/*
const simulateCustomerReply = async () => {
const replies = [
'好的,已为您记录',
'这个问题需要进一步核实',
'我明白了,马上为您处理',
'请稍等,正在为您查询',
'感谢您的反馈'
]
const randomReply = replies[Math.floor(Math.random() * replies.length)]
await supabaseService.simulateServiceReply(randomReply)
addReceivedMessage(randomReply)
// ...
}
*/
// 添加接收的消息
const addReceivedMessage = (content: string) => {
const newMessage = {
id: Date.now(),
type: 'received',
content: content,
time: getCurrentTime()
}
messages.value.push(newMessage)
scrollToBottom()
}
/* 移除不再使用的 simulateCustomerReply 和 addReceivedMessage */
// 滚动到底部
const scrollToBottom = () => {
@@ -358,7 +399,9 @@ const goBack = () => {
.chat-header {
background-color: white;
padding: 10px 15px;
/* padding-top 由内联样式控制 */
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #eee;
@@ -437,6 +480,7 @@ const goBack = () => {
/* 消息项 */
.message-wrapper {
display: flex;
flex-direction: row;
margin-bottom: 15px;
}
@@ -517,6 +561,7 @@ const goBack = () => {
.input-tools {
display: flex;
flex-direction: row;
margin-bottom: 10px;
}
@@ -528,6 +573,7 @@ const goBack = () => {
.input-wrapper {
display: flex;
flex-direction: row;
align-items: center;
}
@@ -573,6 +619,7 @@ const goBack = () => {
.emoji-category {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}