consumer模块完成度95%,准备部署消费者端测试

This commit is contained in:
cyh666666
2026-03-05 08:45:00 +08:00
parent cceb556c62
commit 7f7f723d93
1043 changed files with 53958 additions and 3445 deletions

View File

@@ -1,17 +1,17 @@
<!-- pages/mall/consumer/chat.uvue -->
<!-- pages/mall/consumer/chat.uvue -->
<template>
<view class="chat-page">
<!-- 聊天头部 -->
<view class="chat-header" :style="{ paddingTop: navPaddingTop }">
<view class="header-back" @click="goBack">
<text class="back-icon"></text>
<text class="back-icon">🔙</text>
</view>
<view class="header-info">
<text class="chat-title">{{ headerTitle }}</text>
<text class="chat-status">在线</text>
</view>
<view class="header-actions">
<text class="action-icon" @click="showMoreActions"></text>
<text class="action-icon" @click="showMoreActions"></text>
</view>
</view>
@@ -78,7 +78,7 @@
</view>
</scroll-view>
<!-- 聊天输入 -->
<!-- 聊天输入 -->
<view class="chat-input">
<view class="input-tools">
<text class="tool-icon" @click="showEmojiPicker">😊</text>
@@ -129,11 +129,11 @@ import type { AkSupaRealtimeChannel } from '@/components/supadb/aksupa.uts'
import { getCurrentUser } from '@/utils/store.uts'
type UiChatMessage = {
id: string
viewId: string
type: string
content: string
time: string
id: string
viewId: string
type: string
content: string
time: string
}
// 响应式数据
@@ -151,7 +151,7 @@ const isInitialLoading = ref<boolean>(true)
let realtimeChannel: AkSupaRealtimeChannel | null = null
// 模拟表情列表
const emojiList = ['😊', '😂', '🤣', '😍', '😘', '🥰', '😭', '😡', '👍', '👏', '🙏', '🎉', '❤️', '🔥', '']
const emojiList = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
function scrollToBottom() : void {
if (messages.value.length === 0) return
@@ -161,13 +161,13 @@ function scrollToBottom() : void {
const targetId = 'msg-' + lastMsg.id
// 关键点:在 UVue 安卓端,直接连续赋值可能被合并。
// 我们先清 ID然后在下一帧赋值确保 scroll-view 监听到变化。
// 我们先清 ID然后在下一帧赋值确保 scroll-view 监听到变化。
scrollToView.value = ''
// 增加多次尝试,确保在 DOM 彻底完成渲染(包含由于高度计算引起的多次排版)后定位。
setTimeout(() => {
scrollToView.value = targetId
console.log('[scrollToBottom] 发起第一次滚动定位:', targetId)
console.log('[scrollToBottom] 发起第一次滚动定位', targetId)
// 二次校准:针对长消息或图片导致的高度变化
setTimeout(() => {
@@ -184,98 +184,98 @@ function getCurrentTime(): string {
const now = new Date()
const hours = now.getHours().toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
return `${hours}:${minutes}`
return ${hours}:
}
function setupRealtimeSubscription(): void {
console.log('开始建立聊天实时订阅...')
console.log('当前用户ID:', currentUserId.value, '商家ID:', merchantId.value)
realtimeChannel = supa.channel('chat-messages-' + Date.now().toString())
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'ml_chat_messages'
}, (payload: any) => {
console.log('=== 收到实时订阅回调 ===')
const payloadObj = (payload instanceof UTSJSONObject) ? (payload as UTSJSONObject) : (JSON.parse(JSON.stringify(payload ?? {})) as UTSJSONObject)
const newMsgAny = payloadObj.get('new')
if (newMsgAny == null) {
console.log('newMsgAny 为空,跳过')
return
}
const newMsg = (newMsgAny instanceof UTSJSONObject) ? (newMsgAny as UTSJSONObject) : (JSON.parse(JSON.stringify(newMsgAny)) as UTSJSONObject)
console.log('收到新消息:', newMsg)
console.log('开始建立聊天实时订阅...')
console.log('当前用户ID:', currentUserId.value, '商家ID:', merchantId.value)
const senderId = newMsg.getString('sender_id') ?? ''
const receiverId = newMsg.getString('receiver_id') ?? ''
const msgId = newMsg.getString('id') ?? ''
const content = newMsg.getString('content') ?? ''
console.log('=== 消息详情 ===')
console.log('消息ID:', msgId)
console.log('发送者ID:', senderId)
console.log('接收者ID:', receiverId)
console.log('当前用户ID:', currentUserId.value)
console.log('商家ID:', merchantId.value)
console.log('消息内容:', content)
realtimeChannel = supa.channel('chat-messages-' + Date.now().toString())
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'ml_chat_messages'
}, (payload: any) => {
console.log('=== 收到实时订阅回调 ===')
const payloadObj = (payload instanceof UTSJSONObject) ? (payload as UTSJSONObject) : (JSON.parse(JSON.stringify(payload ?? {})) as UTSJSONObject)
const newMsgAny = payloadObj.get('new')
if (newMsgAny == null) {
console.log('newMsgAny 为空,跳过')
return
}
const newMsg = (newMsgAny instanceof UTSJSONObject) ? (newMsgAny as UTSJSONObject) : (JSON.parse(JSON.stringify(newMsgAny)) as UTSJSONObject)
console.log('收到新消息', newMsg)
// 检查消息是否已经在列表中(避免重复)
for (let i = 0; i < messages.value.length; i++) {
if (messages.value[i].id == msgId) {
console.log('消息已存在,跳过')
return
}
}
const senderId = newMsg.getString('sender_id') ?? ''
const receiverId = newMsg.getString('receiver_id') ?? ''
const msgId = newMsg.getString('id') ?? ''
const content = newMsg.getString('content') ?? ''
// 判断消息类型
const isMyMessage = (senderId == currentUserId.value)
const isForMe = (receiverId == currentUserId.value)
const isRelatedToCurrentChat = (senderId == merchantId.value || receiverId == merchantId.value)
console.log('=== 条件判断 ===')
console.log('isMyMessage:', isMyMessage)
console.log('isForMe:', isForMe)
console.log('isRelatedToCurrentChat:', isRelatedToCurrentChat)
console.log('=== 消息详情 ===')
console.log('消息ID:', msgId)
console.log('发送者ID:', senderId)
console.log('接收者ID:', receiverId)
console.log('当前用户ID:', currentUserId.value)
console.log('商家ID:', merchantId.value)
console.log('消息内容:', content)
// 如果消息与当前聊天无关,跳过
if (!isRelatedToCurrentChat) {
console.log('消息与当前聊天无关,跳过')
return
}
// 检查消息是否已经在列表中(避免重复)
for (let i = 0; i < messages.value.length; i++) {
if (messages.value[i].id == msgId) {
console.log('消息已存在,跳过')
return
}
}
// 如果是自己发送的消息,或者是发给自己的消息,都显示
if (isMyMessage || isForMe) {
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 isMyMessage = (senderId == currentUserId.value)
const isForMe = (receiverId == currentUserId.value)
const isRelatedToCurrentChat = (senderId == merchantId.value || receiverId == merchantId.value)
// 生成安全的 viewId
const safeViewId = 'msg_' + msgId.replace(/[^a-zA-Z0-9]/g, '_')
console.log('=== 条件判断 ===')
console.log('isMyMessage:', isMyMessage)
console.log('isForMe:', isForMe)
console.log('isRelatedToCurrentChat:', isRelatedToCurrentChat)
const incomingMsg: UiChatMessage = {
id: msgId,
viewId: safeViewId,
type: isMyMessage ? 'sent' : 'received',
content: content,
time: timeStr
}
// 如果消息与当前聊天无关,跳过
if (!isRelatedToCurrentChat) {
console.log('消息与当前聊天无关,跳过')
return
}
console.log('=== 添加新消息到列表 ===')
console.log('消息类型:', incomingMsg.type)
console.log('消息内容:', incomingMsg.content)
messages.value.push(incomingMsg)
scrollToBottom()
} else {
console.log('条件不满足,不添加消息')
}
})
.subscribe((status: string, err: any | null) => {
console.log('订阅状态:', status)
if (err != null) {
console.log('订阅错误:', err)
}
})
// 如果是自己发送的消息,或者是发给自己的消息,都显示
if (isMyMessage || isForMe) {
const createdAt = newMsg.getString('created_at') ?? new Date().toISOString()
const date = new Date(createdAt)
const timeStr = ${date.getHours().toString().padStart(2, '0')}:
// 生成安全从 viewId
const safeViewId = 'msg_' + msgId.replace(/[^a-zA-Z0-9]/g, '_')
const incomingMsg: UiChatMessage = {
id: msgId,
viewId: safeViewId,
type: isMyMessage ? 'sent' : 'received',
content: content,
time: timeStr
}
console.log('=== 添加新消息到列表 ===')
console.log('消息类型:', incomingMsg.type)
console.log('消息内容:', incomingMsg.content)
messages.value.push(incomingMsg)
scrollToBottom()
} else {
console.log('条件不满足,不添加消息')
}
})
.subscribe((status: string, err: any | null) => {
console.log('订阅状态', status)
if (err != null) {
console.log('订阅错误:', err)
}
})
}
async function loadChatHistory(): Promise<void> {
@@ -300,7 +300,7 @@ async function loadChatHistory(): Promise<void> {
for (let i = 0; i < sortedRawMsgs.length; i++) {
const m = sortedRawMsgs[i]
const date = new Date(m.created_at ?? new Date().toISOString())
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
const timeStr = ${date.getHours().toString().padStart(2, '0')}:
const sender = m.sender_id ?? ''
const msgType = (currentUserId.value != '' && sender == currentUserId.value) ? 'sent' : 'received'
@@ -333,42 +333,42 @@ function onScrollToUpper(e: any): void {
}
async function loadMerchantInfo(): Promise<void> {
if (merchantId.value == '') return
try {
const response = await supa
.from('ml_shops')
.select('shop_logo, shop_name')
.eq('merchant_id', merchantId.value)
.limit(1)
.execute()
if (response.error != null) {
console.error('[loadMerchantInfo] 获取商家信息失败:', response.error)
return
}
const rawData = response.data
if (rawData == null) return
const rawList = rawData as any[]
if (rawList.length == 0) return
const shopData = rawList[0]
const shopObj = JSON.parse(JSON.stringify(shopData)) as UTSJSONObject
const logo = shopObj.getString('shop_logo')
if (logo != null && logo != '') {
merchantAvatar.value = logo
}
const name = shopObj.getString('shop_name')
if (name != null && name != '' && headerTitle.value == '在线客服') {
headerTitle.value = name
}
} catch (e) {
console.error('[loadMerchantInfo] 获取商家信息异常:', e)
}
if (merchantId.value == '') return
try {
const response = await supa
.from('ml_shops')
.select('shop_logo, shop_name')
.eq('merchant_id', merchantId.value)
.limit(1)
.execute()
if (response.error != null) {
console.error('[loadMerchantInfo] 获取商家信息失败:', response.error)
return
}
const rawData = response.data
if (rawData == null) return
const rawList = rawData as any[]
if (rawList.length == 0) return
const shopData = rawList[0]
const shopObj = JSON.parse(JSON.stringify(shopData)) as UTSJSONObject
const logo = shopObj.getString('shop_logo')
if (logo != null && logo != '') {
merchantAvatar.value = logo
}
const name = shopObj.getString('shop_name')
if (name != null && name != '' && headerTitle.value == '在线客服') {
headerTitle.value = name
}
} catch (e) {
console.error('[loadMerchantInfo] 获取商家信息异常:', e)
}
}
// 生命周期
@@ -379,33 +379,33 @@ onLoad((options: any) => {
// 状态栏高度 + 10px 原有顶部内边距
navPaddingTop.value = (statusBarH + 10) + 'px'
const optObj = (options instanceof UTSJSONObject) ? (options as UTSJSONObject) : (JSON.parse(JSON.stringify(options ?? {})) as UTSJSONObject)
const mid = optObj.getString('merchantId') ?? ''
if (mid !== '') {
merchantId.value = mid
}
const mname = optObj.getString('merchantName') ?? ''
if (mname !== '') {
headerTitle.value = mname
}
const optObj = (options instanceof UTSJSONObject) ? (options as UTSJSONObject) : (JSON.parse(JSON.stringify(options ?? {})) as UTSJSONObject)
const mid = optObj.getString('merchantId') ?? ''
if (mid !== '') {
merchantId.value = mid
}
const mname = optObj.getString('merchantName') ?? ''
if (mname !== '') {
headerTitle.value = mname
}
})
onMounted(() => {
supabaseService.ensureSession().then((uid) => {
if (uid != null) {
currentUserId.value = uid
} else {
getCurrentUser().then((user) => {
if (user != null) {
currentUserId.value = user.id ?? ''
}
})
}
supabaseService.ensureSession().then((uid) => {
if (uid != null) {
currentUserId.value = uid
} else {
getCurrentUser().then((user) => {
if (user != null) {
currentUserId.value = user.id ?? ''
}
})
}
loadMerchantInfo()
loadChatHistory()
setupRealtimeSubscription()
})
loadMerchantInfo()
loadChatHistory()
setupRealtimeSubscription()
})
})
onUnmounted(() => {
@@ -425,7 +425,7 @@ const sendMessage = async () => {
if (merchantId.value != '') {
console.log('[sendMessage] 开始发送消息到:', merchantId.value)
const success = await supabaseService.sendMessage(merchantId.value, content)
console.log('[sendMessage] 发送结果:', success)
console.log('[sendMessage] 发送结果', success)
if (!success) {
uni.showToast({
title: '发送失败',
@@ -687,13 +687,23 @@ const goBack = () => {
white-space: pre-wrap;
}
.message-text {
font-size: 15px;
color: #333;
line-height: 1.4;
margin-bottom: 5px;
word-wrap: break-word;
word-break: break-all;
white-space: pre-wrap;
}
.message-time {
font-size: 11px;
color: #999;
text-align: right;
}
/* 聊天输入 */
/* 聊天输入 */
.chat-input {
background-color: white;
border-top: 1px solid #eee;
@@ -781,3 +791,4 @@ const goBack = () => {
/* 响应式适配 removed for strict uv-app-x compliance */
</style>