consumer模块完成度95%,安卓端大部分页面能正常获取数据,页面样式显示基本正常,逐渐完善;消费者端的积分、余额、评价、优惠券等小模块正在完善
This commit is contained in:
@@ -17,10 +17,13 @@
|
||||
|
||||
<!-- 聊天内容 -->
|
||||
<scroll-view
|
||||
scroll-y
|
||||
scroll-y="true"
|
||||
class="chat-content"
|
||||
:scroll-into-view="scrollToView"
|
||||
scroll-with-animation
|
||||
:scroll-with-animation="false"
|
||||
:show-scrollbar="false"
|
||||
upper-threshold="100"
|
||||
@scrolltoupper="onScrollToUpper"
|
||||
>
|
||||
<!-- 聊天消息列表 -->
|
||||
<view class="chat-messages">
|
||||
@@ -39,13 +42,13 @@
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
:class="['message-item', message.type]"
|
||||
:id="'msg-' + message.id"
|
||||
:id="message.viewId"
|
||||
>
|
||||
<!-- 对方消息 -->
|
||||
<view v-if="message.type === 'received'" class="message-wrapper">
|
||||
<image
|
||||
class="avatar"
|
||||
src="/static/icons/shop-default.png"
|
||||
:src="merchantAvatar"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="message-content-wrapper">
|
||||
@@ -67,7 +70,7 @@
|
||||
</view>
|
||||
<image
|
||||
class="avatar me"
|
||||
src="/static/avatar-default.png"
|
||||
src="/static/default-avatar.png"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
@@ -127,6 +130,7 @@ import { getCurrentUser } from '@/utils/store.uts'
|
||||
|
||||
type UiChatMessage = {
|
||||
id: string
|
||||
viewId: string
|
||||
type: string
|
||||
content: string
|
||||
time: string
|
||||
@@ -141,19 +145,39 @@ const scrollToView = ref<string>('')
|
||||
const currentUserId = ref<string>('')
|
||||
const merchantId = ref<string>('') // 商家ID
|
||||
const headerTitle = ref<string>('在线客服')
|
||||
const merchantAvatar = ref<string>('/static/default-shop.png') // 商家头像
|
||||
const navPaddingTop = ref<string>('30px') // 默认值,包含状态栏高度+原有内边距
|
||||
const isInitialLoading = ref<boolean>(true)
|
||||
let realtimeChannel: AkSupaRealtimeChannel | null = null
|
||||
|
||||
// 模拟表情列表
|
||||
const emojiList = ['😊', '😂', '🤣', '😍', '😘', '🥰', '😭', '😡', '👍', '👏', '🙏', '🎉', '❤️', '🔥', '⭐']
|
||||
|
||||
function scrollToBottom(): void {
|
||||
nextTick(() => {
|
||||
if (messages.value.length > 0) {
|
||||
const lastMsgId = messages.value[messages.value.length - 1].id
|
||||
scrollToView.value = 'msg-' + lastMsgId
|
||||
}
|
||||
})
|
||||
function scrollToBottom() : void {
|
||||
if (messages.value.length === 0) return
|
||||
|
||||
// 获取最后一条消息的 ID
|
||||
const lastMsg = messages.value[messages.value.length - 1]
|
||||
const targetId = 'msg-' + lastMsg.id
|
||||
|
||||
// 关键点:在 UVue 安卓端,直接连续赋值可能被合并。
|
||||
// 我们先清除 ID,然后在下一帧赋值,确保 scroll-view 监听到变化。
|
||||
scrollToView.value = ''
|
||||
|
||||
// 增加多次尝试,确保在 DOM 彻底完成渲染(包含由于高度计算引起的多次排版)后定位。
|
||||
setTimeout(() => {
|
||||
scrollToView.value = targetId
|
||||
console.log('[scrollToBottom] 发起第一次滚动定位:', targetId)
|
||||
|
||||
// 二次校准:针对长消息或图片导致的高度变化
|
||||
setTimeout(() => {
|
||||
scrollToView.value = ''
|
||||
setTimeout(() => {
|
||||
scrollToView.value = targetId
|
||||
console.log('[scrollToBottom] 二次校准完成:', targetId)
|
||||
}, 50)
|
||||
}, 100)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function getCurrentTime(): string {
|
||||
@@ -167,14 +191,12 @@ function setupRealtimeSubscription(): void {
|
||||
console.log('开始建立聊天实时订阅...')
|
||||
console.log('当前用户ID:', currentUserId.value, '商家ID:', merchantId.value)
|
||||
|
||||
const filter = ({
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
table: 'ml_chat_messages'
|
||||
} as UTSJSONObject)
|
||||
|
||||
realtimeChannel = supa.channel('public:ml_chat_messages')
|
||||
.on('postgres_changes', filter, (payload: any) => {
|
||||
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')
|
||||
@@ -228,8 +250,12 @@ function setupRealtimeSubscription(): void {
|
||||
const date = new Date(createdAt)
|
||||
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().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
|
||||
@@ -253,40 +279,96 @@ function setupRealtimeSubscription(): void {
|
||||
}
|
||||
|
||||
async function loadChatHistory(): Promise<void> {
|
||||
let rawMsgs: ChatMessage[] = []
|
||||
let rawMsgs : ChatMessage[] = []
|
||||
|
||||
if (merchantId.value != '') {
|
||||
rawMsgs = await supabaseService.getChatMessages(merchantId.value)
|
||||
} else {
|
||||
console.warn("No merchant ID provided for chat")
|
||||
return
|
||||
}
|
||||
if (merchantId.value != '') {
|
||||
rawMsgs = await supabaseService.getChatMessages(merchantId.value)
|
||||
} else {
|
||||
console.warn("No merchant ID provided for chat")
|
||||
return
|
||||
}
|
||||
|
||||
// 使用 for 循环替代 map
|
||||
const uiMessages: UiChatMessage[] = []
|
||||
for (let i = rawMsgs.length - 1; i >= 0; i--) {
|
||||
const m = rawMsgs[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')}`
|
||||
// 确保时间顺序是升序(旧的在前,新的在后)
|
||||
// Supabase 返回的消息如果是降序,我们需要 reverse 过来显示
|
||||
const sortedRawMsgs = rawMsgs.sort((a, b) => {
|
||||
const timeA = new Date(a.created_at ?? '').getTime()
|
||||
const timeB = new Date(b.created_at ?? '').getTime()
|
||||
return timeA - timeB
|
||||
})
|
||||
|
||||
const sender = m.sender_id ?? ''
|
||||
const msgType = (currentUserId.value != '' && sender == currentUserId.value) ? 'sent' : 'received'
|
||||
const rawId = (m.id ?? '').toString()
|
||||
const msgId = rawId != '' ? rawId : Date.now().toString() + i.toString()
|
||||
const uiMessages : UiChatMessage[] = []
|
||||
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 sender = m.sender_id ?? ''
|
||||
const msgType = (currentUserId.value != '' && sender == currentUserId.value) ? 'sent' : 'received'
|
||||
const rawId = (m.id ?? '').toString()
|
||||
const msgId = rawId != '' ? rawId : Date.now().toString() + i.toString()
|
||||
const safeViewId = 'msg_' + msgId.replace(/[^a-zA-Z0-9]/g, '_')
|
||||
|
||||
const uiMsg : UiChatMessage = {
|
||||
id: msgId,
|
||||
viewId: safeViewId,
|
||||
type: msgType,
|
||||
content: m.content ?? '',
|
||||
time: timeStr
|
||||
}
|
||||
uiMessages.push(uiMsg)
|
||||
}
|
||||
messages.value = uiMessages
|
||||
|
||||
if (isInitialLoading.value) {
|
||||
// 增加一点初始化延迟,等待 scroll-view 渲染就绪
|
||||
setTimeout(() => {
|
||||
scrollToBottom()
|
||||
isInitialLoading.value = false
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
function onScrollToUpper(e: any): void {
|
||||
console.log('[onScrollToUpper] 触发加载历史记录')
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
const uiMsg: UiChatMessage = {
|
||||
id: msgId,
|
||||
type: msgType,
|
||||
content: m.content ?? '',
|
||||
time: timeStr
|
||||
if (response.error != null) {
|
||||
console.error('[loadMerchantInfo] 获取商家信息失败:', response.error)
|
||||
return
|
||||
}
|
||||
uiMessages.push(uiMsg)
|
||||
|
||||
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)
|
||||
}
|
||||
messages.value = uiMessages
|
||||
|
||||
setTimeout(() => {
|
||||
scrollToBottom()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
@@ -320,6 +402,7 @@ onMounted(() => {
|
||||
})
|
||||
}
|
||||
|
||||
loadMerchantInfo()
|
||||
loadChatHistory()
|
||||
setupRealtimeSubscription()
|
||||
})
|
||||
@@ -340,15 +423,16 @@ const sendMessage = async () => {
|
||||
|
||||
// 发送到 Supabase
|
||||
if (merchantId.value != '') {
|
||||
// 不使用乐观更新,等待实时订阅推送
|
||||
// 这样可以确保多端同步
|
||||
console.log('[sendMessage] 开始发送消息到:', merchantId.value)
|
||||
const success = await supabaseService.sendMessage(merchantId.value, content)
|
||||
console.log('[sendMessage] 发送结果:', success)
|
||||
if (!success) {
|
||||
uni.showToast({
|
||||
title: '发送失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
// 不需要手动添加消息,等待实时订阅推送
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,22 +522,23 @@ const goBack = () => {
|
||||
<style>
|
||||
.chat-page {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 聊天头部 */
|
||||
.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;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-back {
|
||||
@@ -492,8 +577,16 @@ const goBack = () => {
|
||||
/* 聊天内容区 */
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
padding-bottom: 70px; /* 为输入区留出空间 */
|
||||
height: 0;
|
||||
padding: 10px;
|
||||
padding-bottom: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
/* 系统消息 */
|
||||
@@ -551,31 +644,37 @@ const goBack = () => {
|
||||
}
|
||||
|
||||
.message-content-wrapper {
|
||||
/* max-width removed */
|
||||
}
|
||||
|
||||
.sender-name {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 5px;
|
||||
max-width: 70%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
background-color: white;
|
||||
padding: 10px 15px;
|
||||
border-radius: 18px;
|
||||
border-radius: 12px;
|
||||
position: relative;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
/* max-width wrap removed */
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
align-self: flex-start; /* 关键:根据内容宽度自适应,不撑满 */
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.message-bubble.me {
|
||||
background-color: #95ec69;
|
||||
border-bottom-right-radius: 4px;
|
||||
align-self: flex-end; /* 关键:靠右对齐且宽度自适应 */
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
|
||||
.message-bubble-not-me .message-content {
|
||||
border-bottom-left-radius: 4px;
|
||||
.message-bubble:not(.me) {
|
||||
border-top-left-radius: 2px;
|
||||
}
|
||||
|
||||
.sender-name {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
margin-bottom: 2px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
@@ -583,6 +682,9 @@ const goBack = () => {
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 5px;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
@@ -596,11 +698,12 @@ const goBack = () => {
|
||||
background-color: white;
|
||||
border-top: 1px solid #eee;
|
||||
padding: 10px 15px;
|
||||
padding-bottom: 20px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.input-tools {
|
||||
@@ -653,9 +756,8 @@ const goBack = () => {
|
||||
border-top: 1px solid #eee;
|
||||
padding: 10px;
|
||||
height: 200px;
|
||||
/* overflow-y removed */
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
bottom: 80px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 99;
|
||||
|
||||
Reference in New Issue
Block a user