Files
medical-mall/pages/mall/consumer/chat_new.uvue

624 lines
17 KiB
Plaintext

<template>
<view class="chat-page">
<!-- 鑱婂ぉ澶撮儴 -->
<view class="chat-header" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="header-content">
<view class="header-back" @click="goBack">
<text class="back-icon">鈥?/text>
</view>
<view class="header-info">
<text class="chat-title">{{ merchantName || '鍦ㄧ嚎瀹㈡湇' }}</text>
<text class="chat-status">鍦ㄧ嚎</text>
</view>
<view class="header-actions">
<text class="action-icon" @click="showMoreActions">鈰?/text>
</view>
</view>
</view>
<!-- 鑱婂ぉ鍐呭 -->
<scroll-view
scroll-y
class="chat-content"
:scroll-into-view="scrollToView"
scroll-with-animation
@scrolltoupper="loadMoreHistory"
>
<!-- 鍗犱綅锛岄槻姝㈠唴瀹硅澶撮儴閬尅 -->
<view :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 鑱婂ぉ娑堟伅鍒楄〃 -->
<view class="chat-messages">
<!-- 绯荤粺鎻愮ず -->
<view class="message-item system">
<text class="system-text">宸茶繛鎺ュ埌鍟嗗锛屽紑濮嬭亰澶╁惂</text>
</view>
<!-- 娑堟伅椤?-->
<view
v-for="(message, index) in messages"
:key="message.id"
:class="['message-item', message.type]"
:id="'msg-' + message.id"
>
<!-- 鏃堕棿鏄剧ず閫昏緫锛氭瘡5鍒嗛挓鏄剧ず涓€娆℃椂闂?-->
<view v-if="shouldShowTime(index)" class="time-divider">
<text>{{ formatTime(message.rawTime) }}</text>
</view>
<!-- 瀵规柟娑堟伅 -->
<view v-if="message.type === 'received'" class="message-wrapper">
<image
class="avatar"
:src="merchantLogo || '/static/logo.png'"
mode="aspectFill"
/>
<view class="message-content-wrapper">
<!-- <text class="sender-name">{{ merchantName }}</text> -->
<view class="message-bubble">
<text class="message-text">{{ message.content }}</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">{{ message.content }}</text>
</view>
</view>
<image
class="avatar me"
:src="userAvatar || '/static/images/default-product.png'"
mode="aspectFill"
/>
</view>
</view>
<!-- 搴曢儴濉厖锛岄槻姝㈣杈撳叆妗嗛伄鎸?-->
<view style="height: 20px;"></view>
<view id="bottom-anchor" style="height: 1px;"></view>
</view>
</scroll-view>
<!-- 鑱婂ぉ杈撳叆鍖?-->
<view class="chat-input-area">
<view class="input-tools">
<text class="tool-icon" @click="toggleEmoji">馃槉</text>
<!-- <text class="tool-icon" @click="chooseImage">馃摲</text> -->
</view>
<view class="input-wrapper">
<input
class="message-input"
v-model="inputMessage"
placeholder="璇疯緭鍏ユ秷鎭?.."
:adjust-position="true"
confirm-type="send"
@confirm="sendMessage"
/>
<button
class="send-button"
:class="{ active: inputMessage.trim().length > 0 }"
@click="sendMessage"
>
鍙戦€?
</button>
</view>
<!-- 琛ㄦ儏閫夋嫨鍣?(绠€鍖栫増) -->
<scroll-view scroll-y v-if="showEmoji" class="emoji-picker">
<view class="emoji-grid">
<text
v-for="emoji in emojiList"
:key="emoji"
class="emoji-item"
@click="insertEmoji(emoji)"
>
{{ emoji }}
</text>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, onUnmounted } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import supabaseService from '@/utils/supabaseService.uts'
import type { ChatMessage } from '@/utils/supabaseService.uts'
import supa from '@/components/supadb/aksupainstance.uts'
// import { getCurrentUser } from '@/utils/store.uts'
// 鐣岄潰鐘舵€?
const statusBarHeight = ref(0)
const scrollToView = ref('')
const showEmoji = ref(false)
const inputMessage = ref('')
// 涓氬姟鏁版嵁
const merchantId = ref('')
const merchantName = ref('')
const merchantLogo = ref('')
const userAvatar = ref('')
const currentUserId = ref('')
const messages = ref<any[]>([])
const emojiList = ['馃槉', '馃槀', '馃憤', '馃憣', '鉂わ笍', '馃尮', '馃檹', '馃帀', '馃槨', '馃槶', '馃', '馃憢', '馃', '馃拪', '馃彞']
let realtimeChannel: any | null = null
onLoad((options: any) => {
// 鑾峰彇鐘舵€佹爮楂樺害
const sys = uni.getSystemInfoSync()
statusBarHeight.value = sys.statusBarHeight ?? 0
// 鑾峰彇鍙傛暟
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
merchantName.value = optObj.getString('merchantName') ?? '鍟嗗'
merchantLogo.value = optObj.getString('merchantLogo') ?? ''
console.log('寮€濮嬭亰澶╋紝鍟嗗ID:', merchantId.value)
} else {
merchantName.value = '骞冲彴瀹㈡湇'
}
// 鑾峰彇褰撳墠鐢ㄦ埛
const uid = supabaseService.getCurrentUserId()
if (uid != null) {
currentUserId.value = uid
// 绠€鍗曡幏鍙栦竴涓嬪ご鍍忥紝瀹為檯搴旇浠嶱rofile鑾峰彇
userAvatar.value = 'https://picsum.photos/100'
}
// 鍔犺浇鍘嗗彶娑堟伅
loadHistory()
// 寮€鍚疄鏃惰闃?
startRealtimeSubscription()
})
onUnmounted(() => {
if (realtimeChannel != null) {
supa.removeChannel(realtimeChannel!)
}
})
// 鍔犺浇鍘嗗彶璁板綍
const loadHistory = async () => {
let rawMsgs: ChatMessage[] = []
if (merchantId.value) {
// 鑾峰彇涓庣壒瀹氬晢瀹剁殑鑱婂ぉ
rawMsgs = await supabaseService.getChatMessages(merchantId.value)
} else {
// 鑾峰彇鎵€鏈夛紙姣斿瀹㈡湇锛?
rawMsgs = await supabaseService.getUserChatMessages()
}
const formatted = rawMsgs.reverse().map((m: ChatMessage) => formatMessage(m))
messages.value = formatted
scrollToBottom()
}
const loadMoreHistory = () => {
// TODO: 瀹炵幇涓嬫媺鍔犺浇鏇村鍘嗗彶
}
// 寮€鍚疄鏃惰闃?
const startRealtimeSubscription = () => {
if (currentUserId.value == '') return
console.log('寮€鍚秷鎭洃鍚?..')
const filterObj = ({
event: 'INSERT',
schema: 'public',
table: 'ml_chat_messages'
} as UTSJSONObject)
realtimeChannel = supa.channel(`chat_${currentUserId.value}`)
.on(
'postgres_changes',
filterObj,
(payload: any) => {
console.log('鏀跺埌鏂版秷鎭?', payload)
const payloadObj = (payload instanceof UTSJSONObject) ? (payload as UTSJSONObject) : (JSON.parse(JSON.stringify(payload ?? {})) as UTSJSONObject)
const newAny = payloadObj.get('new')
if (newAny == null) return
const newMsg = (newAny instanceof UTSJSONObject) ? (newAny as UTSJSONObject) : (JSON.parse(JSON.stringify(newAny)) as UTSJSONObject)
// 鍙湁鏉ヨ嚜褰撳墠鑱婂ぉ鐨勫晢瀹剁殑娑堟伅鎵嶆樉绀猴紝鎴栬€呭鏋滄槸鍏ㄥ眬瀹㈡湇妯″紡
const senderId = newMsg.getString('sender_id') ?? ''
if (senderId === merchantId.value || merchantId.value == '') {
const formatted = formatMessage({
id: newMsg.getString('id') ?? '',
content: newMsg.getString('content') ?? '',
msg_type: newMsg.getString('msg_type') ?? '',
sender_id: senderId,
receiver_id: newMsg.getString('receiver_id') ?? '',
is_from_user: false, // 鏀跺埌鐨勪竴瀹氫笉鏄嚜宸卞彂鐨?
created_at: newMsg.getString('created_at') ?? ''
} as ChatMessage)
messages.value.push(formatted)
scrollToBottom()
// 闇囧姩鎻愮ず
uni.vibrateShort({})
}
}
)
.subscribe()
}
// 鏍煎紡鍖栨秷鎭?
const formatMessage = (m: ChatMessage): any => {
// 濡傛灉 sender_id 鏄嚜宸憋紝灏辨槸 'sent'锛屽惁鍒?'received'
// 娉ㄦ剰锛氭暟鎹簱瀛楁 is_from_user 鏈夋椂鍙兘鍙槸鏍囪鏄惁鐢盋绔敤鎴峰彂璧凤紝
// 鏈€鍑嗙‘鐨勬槸瀵规瘮 id
let isMe = false
if (currentUserId.value) {
isMe = m.sender_id === currentUserId.value
} else {
isMe = m.is_from_user === true
}
return {
id: m.id,
type: isMe ? 'sent' : 'received',
content: m.content,
rawTime: m.created_at || new Date().toISOString(),
senderId: m.sender_id
}
}
const sendMessage = async () => {
const text = inputMessage.value.trim()
if (text == '') return
// 涔愯鏇存柊 UI
const tempId = 'temp_' + Date.now()
const tempMsg = {
id: tempId,
type: 'sent',
content: text,
rawTime: new Date().toISOString(),
senderId: currentUserId.value
}
messages.value.push(tempMsg)
inputMessage.value = ''
scrollToBottom()
showEmoji.value = false
// 鍙戦€佽姹?
// 娉ㄦ剰锛氬鏋?merchantId 涓虹┖锛宻endChatMessage 绗簩涓弬鏁颁紶 null锛屼細鍙樻垚鏃犱富娑堟伅
const success = await supabaseService.sendChatMessage(text, merchantId.value ? merchantId.value : null)
if (!success) {
uni.showToast({ title: '鍙戦€佸け璐?, icon: 'none' })
// 杩欓噷鍙互鍔犱竴涓噸璇曟寜閽垨绉婚櫎娑堟伅
}
}
const scrollToBottom = () => {
// 寤舵椂婊氬姩浠ョ‘淇濊鍥炬洿鏂?
setTimeout(() => {
scrollToView.value = 'bottom-anchor'
// Hack: 閲嶇疆鍐嶈缃互寮哄埗瑙﹀彂
setTimeout(() => {
scrollToView.value = 'msg-' + (messages.value.length > 0 ? messages.value[messages.value.length-1].id : '')
}, 50)
}, 100)
}
const goBack = () => {
uni.navigateBack({})
}
const formatTime = (isoString: string): string => {
const date = new Date(isoString)
const now = new Date()
// 濡傛灉鏄粖澶╋紝鏄剧ず HH:mm
if (date.toDateString() === now.toDateString()) {
const h = date.getHours().toString().padStart(2, '0')
const m = date.getMinutes().toString().padStart(2, '0')
return `${h}:${m}`
}
// 鍚﹀垯鏄剧ず MM-DD HH:mm
const mo = (date.getMonth() + 1).toString().padStart(2, '0')
const d = date.getDate().toString().padStart(2, '0')
const h = date.getHours().toString().padStart(2, '0')
const m = date.getMinutes().toString().padStart(2, '0')
return `${mo}-${d} ${h}:${m}`
}
const shouldShowTime = (index: number): boolean => {
if (index === 0) return true
const prev = messages.value[index - 1]
const curr = messages.value[index]
const t1 = new Date(prev.rawTime).getTime()
const t2 = new Date(curr.rawTime).getTime()
// 闂撮殧瓒呰繃5鍒嗛挓(300000ms)鏄剧ず鏃堕棿
return (t2 - t1) > 300000
}
const toggleEmoji = () => {
showEmoji.value = !showEmoji.value
if (showEmoji.value) {
scrollToBottom()
}
}
const insertEmoji = (emoji: string) => {
inputMessage.value += emoji
}
const showMoreActions = () => {
uni.showActionSheet({
itemList: ['娓呯┖璁板綍', '鎶曡瘔鍟嗗'],
success: (res) => {
if (res.tapIndex === 0) {
messages.value = [] // 浠呮湰鍦版竻绌?
}
}
})
}
</script>
<style>
.chat-page {
background-color: #f5f5f5;
height: 100vh;
display: flex;
flex-direction: column;
}
.chat-header {
background-color: #fff;
border-bottom: 1px solid #eee;
z-index: 100;
}
.header-content {
height: 44px;
display: flex;
flex-direction: row;
align-items: center;
padding: 0 16px;
justify-content: space-between;
}
.header-back {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: flex-start;
}
.back-icon {
font-size: 32px;
color: #333;
line-height: 1;
}
.header-info {
display: flex;
flex-direction: column;
align-items: center;
}
.chat-title {
font-size: 16px;
font-weight: bold;
color: #333;
}
.chat-status {
font-size: 10px;
color: #4CAF50;
margin-top: 2px;
}
.header-actions {
width: 40px;
display: flex;
justify-content: flex-end;
}
.action-icon {
font-size: 24px;
color: #333;
}
.chat-content {
flex: 1;
/* height: 0; flex grow handles it */
background-color: #f5f5f5;
padding: 10px 0;
}
.chat-messages {
padding: 16px;
padding-bottom: 20px;
display: flex;
flex-direction: column;
}
.time-divider {
display: flex;
justify-content: center;
margin: 16px 0;
}
.time-divider text {
background-color: rgba(0,0,0,0.1);
color: #999;
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
}
.system {
justify-content: center;
margin-bottom: 16px;
display: flex;
align-items: center;
}
.system-text {
background-color: #e0e0e0;
color: #666;
font-size: 12px;
padding: 4px 12px;
border-radius: 12px;
}
.message-item {
margin-bottom: 16px;
display: flex;
width: 100%;
}
.received {
justify-content: flex-start;
flex-direction: row;
}
.sent {
justify-content: flex-end;
}
.message-wrapper {
display: flex;
flex-direction: row;
max-width: 80%;
align-items: flex-start;
}
.me {
flex-direction: row; /* Keep standard flow but justify-end handles position */
}
.avatar {
width: 40px;
height: 40px;
border-radius: 20px;
background-color: #ddd;
margin-right: 8px;
flex-shrink: 0;
}
.avatar.me {
margin-right: 0;
margin-left: 8px;
}
.message-content-wrapper {
display: flex;
flex-direction: column;
}
.sender-name {
font-size: 12px;
color: #999;
margin-bottom: 4px;
margin-left: 4px;
}
.message-bubble {
background-color: #fff;
padding: 10px 14px;
border-radius: 4px 12px 12px 12px;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.message-bubble.me {
background-color: #95ec69; /* WeChat green */
border-radius: 12px 4px 12px 12px;
}
.message-text {
font-size: 15px;
color: #333;
line-height: 1.4;
word-break: break-all;
}
.chat-input-area {
background-color: #f9f9f9;
padding: 10px;
border-top: 1px solid #e0e0e0;
padding-bottom: env(safe-area-inset-bottom);
}
.input-wrapper {
display: flex;
flex-direction: row;
align-items: center;
background-color: #fff;
border-radius: 24px;
padding: 8px 12px;
}
.message-input {
flex: 1;
font-size: 16px;
height: 36px;
}
.send-button {
margin-left: 8px;
background-color: #e0e0e0;
color: #999;
font-size: 14px;
padding: 4px 12px;
border-radius: 16px;
border: none;
line-height: 24px;
}
.send-button.active {
background-color: #4CAF50;
color: #fff;
}
.input-tools {
display: flex;
flex-direction: row;
padding-bottom: 8px;
}
.tool-icon {
font-size: 24px;
color: #666;
margin-right: 16px;
padding: 4px;
}
.emoji-picker {
height: 150px;
background-color: #f9f9f9;
border-top: 1px solid #eee;
padding: 10px;
}
.emoji-grid {
display: flex;
flex-wrap: wrap;
flex-direction: row;
}
.emoji-item {
font-size: 24px;
padding: 8px;
margin: 4px;
}
</style>