合并merchant文件

This commit is contained in:
2026-03-20 15:43:33 +08:00
parent 29f588a2b2
commit 620ae742df
12 changed files with 3477 additions and 0 deletions

View File

@@ -1,11 +1,17 @@
<!-- 商家端 - 聊天页面 -->
<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>
@@ -32,25 +38,141 @@
</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
@@ -59,6 +181,10 @@
is_read: boolean
is_from_user: boolean
created_at: string
<<<<<<< HEAD
=======
time: string
>>>>>>> local-backup-root-cyj
}
export default {
@@ -70,11 +196,35 @@
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
}
@@ -88,13 +238,33 @@
},
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) {}
},
@@ -141,18 +311,195 @@
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) {}
@@ -163,6 +510,22 @@
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 = {
@@ -180,6 +543,7 @@
.insert([newMessage])
.execute()
<<<<<<< HEAD
if (!response.error) {
this.loadChatMessages()
}
@@ -198,12 +562,73 @@
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; }
@@ -229,4 +654,51 @@
.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>