Files
web-fusion/src/views/pk-mini/Message.vue
2026-02-08 16:35:01 +08:00

452 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<!-- 消息页面 -->
<div class="message-page">
<el-splitter class="message-splitter">
<!-- 会话列表 -->
<el-splitter-panel :size="25" :min="20" :max="35">
<div class="conversation-list">
<div
v-for="(item, index) in chatList"
:key="index"
class="conversation-item"
:class="{ active: selectedChat === item }"
@click="selectChat(item)"
>
<el-badge :value="item.unread > 0 ? item.unread : ''" :max="99">
<div class="conv-avatar">
<img :src="item.data?.avatar || defaultAvatar" alt="" />
</div>
</el-badge>
<div class="conv-info">
<div class="conv-header">
<span class="conv-name">{{ item.data?.nickname || '用户' }}</span>
<span class="conv-time">{{ formatTime(item.lastMessage?.timestamp) }}</span>
</div>
<div class="conv-preview">{{ item.lastMessage?.payload?.text || '' }}</div>
</div>
</div>
<div v-if="chatList.length === 0" class="empty-tip">暂无会话</div>
</div>
</el-splitter-panel>
<!-- 消息列表 -->
<el-splitter-panel>
<div v-if="selectedChat" class="chat-container">
<div class="chat-messages" ref="chatMessagesRef">
<div
v-for="(msg, index) in messagesList"
:key="index"
class="message-item"
:class="{ mine: msg.senderId == currentUser.id }"
>
<div class="message-avatar">
<img :src="msg.senderId == currentUser.id ? currentUser.headerIcon : selectedChat.data?.avatar" alt="" />
</div>
<div class="message-bubble">
<div v-if="msg.type === 'text'" class="text-message">{{ msg.payload.text }}</div>
<PictureMessage v-else-if="msg.type === 'image'" :item="msg" />
<PKMessage v-else-if="msg.type === 'pk'" :item="msg" />
<VoiceMessage v-else-if="msg.type === 'audio'" :item="msg.payload.url" :size="msg.payload.duration" />
</div>
</div>
</div>
<div class="chat-input-area">
<div class="input-toolbar">
<div class="toolbar-btn" @click="handleSendImage">
<span class="material-icons-round">image</span>
</div>
</div>
<div class="input-box">
<textarea
v-model="inputText"
placeholder="输入消息..."
@keydown.enter.prevent="sendMessage"
></textarea>
<el-button type="primary" @click="sendMessage">发送</el-button>
</div>
</div>
</div>
<div v-else class="chat-placeholder">
<span class="material-icons-round placeholder-icon">chat_bubble_outline</span>
<p>选择左侧会话开始聊天</p>
</div>
</el-splitter-panel>
</el-splitter>
<!-- 隐藏的文件输入 -->
<input
ref="fileInputRef"
type="file"
accept="image/*"
style="display: none"
@change="handleFileSelect"
/>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import { getMainUserData } from '@/utils/pk-mini/storage'
import { TimestamptolocalTime } from '@/utils/pk-mini/timeConversion'
import { isGoEasyEnabled } from '@/config/pk-mini'
import {
goEasyGetConversations,
goEasyGetMessages,
goEasySendMessage,
goEasySendImageMessage,
goEasyMessageRead,
getPkGoEasy,
GoEasy
} from '@/utils/pk-mini/goeasy'
import PictureMessage from '@/components/pk-mini/chat/PictureMessage.vue'
import PKMessage from '@/components/pk-mini/chat/PKMessage.vue'
import VoiceMessage from '@/components/pk-mini/chat/VoiceMessage.vue'
import { ElMessage } from 'element-plus'
const defaultAvatar = 'https://vv-1317974657.cos.ap-shanghai.myqcloud.com/util/default-avatar.png'
const chatList = ref([])
const selectedChat = ref(null)
const messagesList = ref([])
const inputText = ref('')
const currentUser = ref({})
const chatMessagesRef = ref(null)
const fileInputRef = ref(null)
const formatTime = TimestamptolocalTime
async function loadConversations() {
if (!isGoEasyEnabled()) {
ElMessage.warning('消息功能暂时不可用GoEasy 订阅未续费)')
return
}
try {
const result = await goEasyGetConversations()
chatList.value = result?.content || []
} catch (e) {
console.error('加载会话列表失败', e)
}
}
async function selectChat(item) {
if (!isGoEasyEnabled()) return
selectedChat.value = item
try {
const messages = await goEasyGetMessages({ id: item.userId, timestamp: null })
messagesList.value = messages || []
await nextTick()
scrollToBottom()
// 标记消息已读
goEasyMessageRead({ id: item.userId }).catch(() => {})
item.unread = 0
} catch (e) {
console.error('加载消息失败', e)
}
}
function scrollToBottom() {
if (chatMessagesRef.value) {
chatMessagesRef.value.scrollTop = chatMessagesRef.value.scrollHeight
}
}
async function sendMessage() {
if (!isGoEasyEnabled()) {
ElMessage.warning('消息功能暂时不可用GoEasy 订阅未续费)')
return
}
if (!inputText.value.trim()) return
if (!selectedChat.value) return
try {
const msg = await goEasySendMessage({
text: inputText.value,
id: selectedChat.value.userId,
avatar: currentUser.value.headerIcon,
nickname: currentUser.value.nickName
})
messagesList.value.push(msg)
inputText.value = ''
await nextTick()
scrollToBottom()
} catch (e) {
console.error('发送消息失败', e)
ElMessage.error('发送失败')
}
}
function handleSendImage() {
if (!isGoEasyEnabled()) {
ElMessage.warning('消息功能暂时不可用GoEasy 订阅未续费)')
return
}
if (!selectedChat.value) {
ElMessage.warning('请先选择一个会话')
return
}
fileInputRef.value?.click()
}
async function handleFileSelect(event) {
const file = event.target.files?.[0]
event.target.value = ''
if (!file || !isGoEasyEnabled()) return
if (!selectedChat.value) return
try {
const msg = await goEasySendImageMessage({
imagefile: file,
id: selectedChat.value.userId,
avatar: currentUser.value.headerIcon,
nickname: currentUser.value.nickName
})
messagesList.value.push(msg)
await nextTick()
scrollToBottom()
} catch (e) {
console.error('发送图片失败', e)
ElMessage.error('发送图片失败')
}
}
onMounted(() => {
currentUser.value = getMainUserData() || {}
if (isGoEasyEnabled()) {
loadConversations()
// 监听新消息
const goeasy = getPkGoEasy()
if (goeasy) {
goeasy.im.on(GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, onMessageReceived)
}
}
})
function onMessageReceived(message) {
if (!isGoEasyEnabled()) return
// 更新会话列表中的未读数
const conv = chatList.value.find(c => c.userId === message.senderId)
if (conv) {
conv.unread = (conv.unread || 0) + 1
conv.lastMessage = message
}
// 如果当前正在查看该会话,添加消息到列表
if (selectedChat.value && selectedChat.value.userId === message.senderId) {
messagesList.value.push(message)
nextTick(() => scrollToBottom())
goEasyMessageRead({ id: message.senderId }).catch(() => {})
}
}
onUnmounted(() => {
if (isGoEasyEnabled()) {
const goeasy = getPkGoEasy()
if (goeasy) {
try {
goeasy.im.off(GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, onMessageReceived)
} catch (e) {
console.warn('清理 GoEasy 监听器失败', e)
}
}
}
})
</script>
<style scoped lang="less">
.message-page {
width: 100%;
height: 100%;
background: white;
border-radius: 16px;
overflow: hidden;
}
.message-splitter {
height: 100%;
}
.conversation-list {
height: 100%;
overflow: auto;
background: #fafafa;
}
.conversation-item {
display: flex;
padding: 15px;
cursor: pointer;
transition: background 0.2s;
border-bottom: 1px solid #eee;
}
.conversation-item:hover, .conversation-item.active {
background: #f0f0f0;
}
.conv-avatar {
width: 50px;
height: 50px;
border-radius: 10px;
overflow: hidden;
margin-right: 12px;
}
.conv-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.conv-info {
flex: 1;
min-width: 0;
}
.conv-header {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
}
.conv-name {
font-weight: bold;
color: #333;
}
.conv-time {
font-size: 12px;
color: #999;
}
.conv-preview {
font-size: 13px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-container {
height: 100%;
display: flex;
flex-direction: column;
}
.chat-messages {
flex: 1;
overflow: auto;
padding: 20px;
}
.message-item {
display: flex;
margin-bottom: 20px;
}
.message-item.mine {
flex-direction: row-reverse;
}
.message-avatar {
width: 45px;
height: 45px;
border-radius: 10px;
overflow: hidden;
margin: 0 12px;
flex-shrink: 0;
}
.message-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.message-bubble {
max-width: 60%;
}
.text-message {
padding: 12px 16px;
background: #f5f5f5;
border-radius: 12px;
font-size: 15px;
line-height: 1.5;
}
.message-item.mine .text-message {
background: #7bbd0093;
}
.chat-input-area {
border-top: 1px solid #eee;
}
.input-toolbar {
display: flex;
padding: 10px 15px;
background: #e4f9f9;
}
.toolbar-btn {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 8px;
transition: background 0.2s;
}
.toolbar-btn:hover {
background: rgba(255, 255, 255, 0.8);
}
.toolbar-btn .material-icons-round {
color: #03aba8;
}
.input-box {
display: flex;
padding: 10px 15px;
gap: 10px;
}
.input-box textarea {
flex: 1;
border: none;
outline: none;
resize: none;
height: 50px;
font-size: 14px;
padding: 10px;
}
.chat-placeholder {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #03aba8;
}
.placeholder-icon {
font-size: 80px;
opacity: 0.3;
margin-bottom: 20px;
}
.empty-tip {
text-align: center;
padding: 50px;
color: #999;
}
</style>