推送项目重构代码
This commit is contained in:
270
ruoyi-ui/src/components/HomeModules/modules/ImChatPanel.vue
Normal file
270
ruoyi-ui/src/components/HomeModules/modules/ImChatPanel.vue
Normal file
@@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<div class="im-chat-panel" v-loading="loading">
|
||||
<div v-if="errorMsg" class="im-error">
|
||||
<i class="el-icon-warning-outline"></i>
|
||||
{{ errorMsg }}
|
||||
<el-button v-if="canRetry" type="text" @click="init">重试</el-button>
|
||||
</div>
|
||||
|
||||
<div v-else class="im-layout">
|
||||
<!-- 左侧会话列表 -->
|
||||
<div class="conv-list">
|
||||
<div v-if="!conversations.length && !loading" class="empty">暂无会话</div>
|
||||
<div v-for="conv in conversations" :key="conv.conversationID"
|
||||
class="conv-item" :class="{ active: current && current.conversationID === conv.conversationID }"
|
||||
@click="openConv(conv)">
|
||||
<div class="conv-avatar" :style="avatarStyle(conv)">{{ avatarText(conv) }}</div>
|
||||
<div class="conv-body">
|
||||
<div class="conv-title">
|
||||
<span class="conv-name">{{ conv.showName || '未命名' }}</span>
|
||||
<span class="conv-time">{{ formatTime(conv.latestMsgSendTime) }}</span>
|
||||
</div>
|
||||
<div class="conv-snippet">
|
||||
<span class="snippet">{{ snippet(conv) }}</span>
|
||||
<span v-if="conv.unreadCount > 0" class="badge">{{ conv.unreadCount > 99 ? '99+' : conv.unreadCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧消息 -->
|
||||
<div class="msg-pane">
|
||||
<template v-if="current">
|
||||
<div class="msg-header">{{ current.showName || '未命名' }}</div>
|
||||
<div class="msg-list" ref="msgListRef">
|
||||
<div v-for="m in messages" :key="m.clientMsgID"
|
||||
class="msg-row" :class="{ mine: m.sendID === myUserId }">
|
||||
<div class="msg-bubble">
|
||||
<div class="msg-meta">{{ m.senderNickname || m.sendID }} · {{ formatTime(m.sendTime) }}</div>
|
||||
<div class="msg-text">{{ renderText(m) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!messages.length" class="empty">无消息</div>
|
||||
</div>
|
||||
<div class="msg-input">
|
||||
<el-input v-model="draft" size="mini" placeholder="输入消息,回车发送"
|
||||
@keyup.enter.native="send" />
|
||||
<el-button type="primary" size="mini" :disabled="!draft.trim()" @click="send">发送</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="empty pick-conv">从左侧选择一个会话</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getImCredentials } from '@/api/system/im'
|
||||
import { im, imBus, MessageType } from '@/utils/imClient'
|
||||
|
||||
export default {
|
||||
name: 'ImChatPanel',
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
errorMsg: '',
|
||||
canRetry: false,
|
||||
conversations: [],
|
||||
current: null,
|
||||
messages: [],
|
||||
draft: '',
|
||||
myUserId: ''
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.init()
|
||||
imBus.$on('new-message', this.onNewMessage)
|
||||
imBus.$on('conv-changed', this.refreshConversations)
|
||||
imBus.$on('disconnected', () => { this.errorMsg = 'IM 断线,尝试重连…'; this.canRetry = true })
|
||||
},
|
||||
beforeDestroy () {
|
||||
imBus.$off('new-message', this.onNewMessage)
|
||||
imBus.$off('conv-changed', this.refreshConversations)
|
||||
},
|
||||
methods: {
|
||||
async init () {
|
||||
this.loading = true
|
||||
this.errorMsg = ''
|
||||
try {
|
||||
const res = await getImCredentials()
|
||||
if (!res || res.code !== 200 || !res.data) {
|
||||
this.errorMsg = (res && res.msg) || '获取 IM 凭据失败'
|
||||
this.canRetry = true
|
||||
return
|
||||
}
|
||||
const cred = res.data
|
||||
const ok = await im.login(cred)
|
||||
if (!ok) {
|
||||
const e = im.lastError
|
||||
const detail = e && (e.errMsg || e.message || JSON.stringify(e))
|
||||
this.errorMsg = '登录 IM 失败:' + (detail || '未知原因(查看 Console)')
|
||||
this.canRetry = true
|
||||
return
|
||||
}
|
||||
this.myUserId = cred.imUserId
|
||||
await this.refreshConversations()
|
||||
} catch (e) {
|
||||
this.errorMsg = e.message || '初始化失败'
|
||||
this.canRetry = true
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async refreshConversations () {
|
||||
this.conversations = await im.getConversations(0, 50)
|
||||
},
|
||||
async openConv (conv) {
|
||||
this.current = conv
|
||||
this.messages = await im.getMessages(conv.conversationID, 30)
|
||||
this.$nextTick(() => this.scrollBottom())
|
||||
im.markRead(conv.conversationID)
|
||||
},
|
||||
async send () {
|
||||
if (!this.current || !this.draft.trim()) return
|
||||
const text = this.draft
|
||||
this.draft = ''
|
||||
try {
|
||||
await im.sendText(this.current, text)
|
||||
const newMessages = await im.getMessages(this.current.conversationID, 30)
|
||||
this.messages = newMessages
|
||||
this.$nextTick(() => this.scrollBottom())
|
||||
} catch (e) {
|
||||
this.$message.error('发送失败:' + (e.message || e))
|
||||
}
|
||||
},
|
||||
onNewMessage (msg) {
|
||||
// 当前会话的消息追加
|
||||
if (this.current && msg && this.current.conversationID === msg.conversationID) {
|
||||
this.messages.push(msg)
|
||||
this.$nextTick(() => this.scrollBottom())
|
||||
im.markRead(this.current.conversationID)
|
||||
}
|
||||
// 异步刷新会话列表(未读、最新消息)
|
||||
this.refreshConversations()
|
||||
},
|
||||
scrollBottom () {
|
||||
const el = this.$refs.msgListRef
|
||||
if (el) el.scrollTop = el.scrollHeight
|
||||
},
|
||||
snippet (conv) {
|
||||
const t = conv.latestMsg
|
||||
if (!t) return ''
|
||||
try {
|
||||
const m = JSON.parse(t)
|
||||
if (m.textElem) return m.textElem.content
|
||||
if (m.pictureElem) return '[图片]'
|
||||
if (m.voiceElem) return '[语音]'
|
||||
if (m.fileElem) return '[文件]'
|
||||
if (m.contentType === 110) return '[系统通知] ' + (m.customElem && JSON.parse(m.customElem.data || '{}').title || '')
|
||||
return ''
|
||||
} catch (e) { return '' }
|
||||
},
|
||||
renderText (m) {
|
||||
if (!m) return ''
|
||||
if (m.contentType === MessageType.TextMessage) return m.textElem && m.textElem.content
|
||||
if (m.contentType === MessageType.PictureMessage) return '[图片]'
|
||||
if (m.contentType === MessageType.VoiceMessage) return '[语音]'
|
||||
if (m.contentType === MessageType.FileMessage) return '[文件] ' + (m.fileElem && m.fileElem.fileName || '')
|
||||
if (m.contentType === 110) {
|
||||
try {
|
||||
const data = JSON.parse(m.customElem && m.customElem.data || '{}')
|
||||
return '[系统通知] ' + (data.title || '') + ' · ' + (data.description || '')
|
||||
} catch (e) { return '[系统通知]' }
|
||||
}
|
||||
return '[暂不支持的消息]'
|
||||
},
|
||||
avatarText (conv) {
|
||||
const s = (conv.showName || '?').trim()
|
||||
return s.charAt(s.length - 1) || '?'
|
||||
},
|
||||
avatarStyle (conv) {
|
||||
const COLORS = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399', '#a06bf3']
|
||||
const hash = (conv.conversationID || '').split('').reduce((a, c) => a + c.charCodeAt(0), 0)
|
||||
return { background: COLORS[hash % COLORS.length] }
|
||||
},
|
||||
formatTime (t) {
|
||||
if (!t) return ''
|
||||
const d = new Date(typeof t === 'number' && t < 1e12 ? t * 1000 : t)
|
||||
const now = new Date()
|
||||
if (d.toDateString() === now.toDateString()) {
|
||||
return d.getHours().toString().padStart(2, '0') + ':' + d.getMinutes().toString().padStart(2, '0')
|
||||
}
|
||||
return (d.getMonth() + 1) + '/' + d.getDate()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.im-chat-panel { height: 100%; min-height: 240px; font-size: 12px; }
|
||||
.im-error {
|
||||
padding: 24px; text-align: center; color: #f56c6c;
|
||||
i { font-size: 24px; display: block; margin-bottom: 6px; }
|
||||
}
|
||||
.im-layout { display: flex; height: 100%; gap: 8px; }
|
||||
.conv-list {
|
||||
flex: 0 0 38%;
|
||||
border-right: 1px solid #ebeef5;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
.empty { color: #c0c4cc; text-align: center; padding: 24px 0; }
|
||||
.pick-conv { padding-top: 80px; }
|
||||
.conv-item {
|
||||
display: flex; gap: 6px; padding: 6px 4px; cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background .15s;
|
||||
&:hover { background: #f5f7fa; }
|
||||
&.active { background: #ecf5ff; }
|
||||
}
|
||||
.conv-avatar {
|
||||
flex: 0 0 32px; width: 32px; height: 32px; border-radius: 50%;
|
||||
color: #fff; font-size: 13px; font-weight: 600;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.conv-body { flex: 1; min-width: 0; }
|
||||
.conv-title {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
.conv-name { font-weight: 600; color: #303133; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.conv-time { color: #909399; font-size: 10px; flex-shrink: 0; margin-left: 4px; }
|
||||
}
|
||||
.conv-snippet {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
.snippet { color: #909399; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.badge {
|
||||
background: #f56c6c; color: #fff;
|
||||
border-radius: 9px; padding: 0 5px;
|
||||
font-size: 10px; line-height: 14px; height: 14px; min-width: 14px;
|
||||
text-align: center; flex-shrink: 0; margin-left: 4px;
|
||||
}
|
||||
}
|
||||
.msg-pane {
|
||||
flex: 1; display: flex; flex-direction: column; min-width: 0;
|
||||
}
|
||||
.msg-header {
|
||||
padding: 6px 8px; border-bottom: 1px solid #ebeef5;
|
||||
font-weight: 600; color: #303133;
|
||||
}
|
||||
.msg-list {
|
||||
flex: 1; overflow-y: auto; padding: 8px 4px;
|
||||
}
|
||||
.msg-row {
|
||||
margin-bottom: 6px;
|
||||
display: flex;
|
||||
}
|
||||
.msg-row.mine { justify-content: flex-end; }
|
||||
.msg-bubble {
|
||||
max-width: 80%;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
.msg-row.mine .msg-bubble { background: #ecf5ff; }
|
||||
.msg-meta { color: #909399; font-size: 10px; margin-bottom: 2px; }
|
||||
.msg-text { color: #303133; word-break: break-word; }
|
||||
.msg-input {
|
||||
display: flex; gap: 4px; padding: 6px 4px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
.msg-input .el-input { flex: 1; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user