|
|
|
|
@ -33,7 +33,12 @@ |
|
|
|
|
class="search-item" |
|
|
|
|
@click="handleSelectSearchResult(user, 1)" |
|
|
|
|
> |
|
|
|
|
<el-avatar :size="36" :src="user.avatar" /> |
|
|
|
|
<el-avatar |
|
|
|
|
:size="36" |
|
|
|
|
:src=" |
|
|
|
|
$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS + user.avatar |
|
|
|
|
" |
|
|
|
|
/> |
|
|
|
|
<div class="search-item-info"> |
|
|
|
|
<div class="search-item-name">{{ user.name }}</div> |
|
|
|
|
<div class="search-item-dept">{{ user.department }}</div> |
|
|
|
|
@ -48,10 +53,16 @@ |
|
|
|
|
class="search-item" |
|
|
|
|
@click="handleSelectSearchResult(group, 4)" |
|
|
|
|
> |
|
|
|
|
<el-avatar :size="36" :src="group.avatar" icon="el-icon-s-custom" /> |
|
|
|
|
<el-avatar |
|
|
|
|
:size="36" |
|
|
|
|
:src=" |
|
|
|
|
$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS + group.avatar |
|
|
|
|
" |
|
|
|
|
icon="el-icon-s-custom" |
|
|
|
|
/> |
|
|
|
|
<div class="search-item-info"> |
|
|
|
|
<div class="search-item-name">{{ group.name }}</div> |
|
|
|
|
<div class="search-item-dept">{{ group.member_count }}人</div> |
|
|
|
|
<div class="search-item-dept">{{ group.user_num }}人</div> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
@ -68,60 +79,76 @@ |
|
|
|
|
<div class="contact-avatar-wrap"> |
|
|
|
|
<el-avatar |
|
|
|
|
:size="44" |
|
|
|
|
:src="contact.avatar" |
|
|
|
|
:src=" |
|
|
|
|
$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS + contact.avatar |
|
|
|
|
" |
|
|
|
|
:icon=" |
|
|
|
|
contact.scene === 4 ? 'el-icon-s-custom' : 'el-icon-user-solid' |
|
|
|
|
" |
|
|
|
|
/> |
|
|
|
|
<el-badge |
|
|
|
|
v-if="contact.unread_count > 0" |
|
|
|
|
:value="contact.unread_count" |
|
|
|
|
v-if="contact.un_read_count > 0" |
|
|
|
|
:value="contact.un_read_count" |
|
|
|
|
class="unread-badge" |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
<div class="contact-info"> |
|
|
|
|
<div class="contact-top"> |
|
|
|
|
<span class="contact-name">{{ contact.name }}</span> |
|
|
|
|
<span class="contact-time">{{ |
|
|
|
|
formatTime(contact.last_message_time) |
|
|
|
|
}}</span> |
|
|
|
|
<span class="contact-time"> |
|
|
|
|
{{ formatTime(contact.update_time) }} |
|
|
|
|
</span> |
|
|
|
|
</div> |
|
|
|
|
<div class="contact-bottom"> |
|
|
|
|
<span class="contact-last-msg">{{ |
|
|
|
|
formatLastMessage(contact) |
|
|
|
|
}}</span> |
|
|
|
|
<i v-if="contact.is_pinned" class="el-icon-top pin-icon" /> |
|
|
|
|
<span class="contact-last-msg"> |
|
|
|
|
{{ contact.last_message_read === 0 ? "[对方未读]" : "" }} |
|
|
|
|
{{ formatLastMessage(contact) }} |
|
|
|
|
</span> |
|
|
|
|
<!-- 置顶 1-是 0-否 --> |
|
|
|
|
<i v-if="contact.top === 1" class="el-icon-top pin-icon" /> |
|
|
|
|
<!-- 免打扰 1-是 0-否 --> |
|
|
|
|
<i |
|
|
|
|
v-if="contact.quiet === 1" |
|
|
|
|
class="el-icon-close-notification pin-icon" |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<div v-if="!sortedContacts.length" class="empty-contacts"> |
|
|
|
|
<div v-if="!sortedContacts.length && !loading" class="empty-contacts"> |
|
|
|
|
<i class="el-icon-chat-line-square" /> |
|
|
|
|
<p>暂无消息</p> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<!-- 加载更多提示 --> |
|
|
|
|
<div v-if="loading" class="loading-tip"> |
|
|
|
|
<i class="el-icon-loading" /> |
|
|
|
|
<span>加载中...</span> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<!-- 没有更多数据提示 --> |
|
|
|
|
<div v-if="!hasMore && sortedContacts.length > 0" class="no-more-tip"> |
|
|
|
|
<span>没有更多数据了</span> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<!-- 引入发起群聊弹窗组件 --> |
|
|
|
|
<!-- 创建群聊弹窗 --> |
|
|
|
|
<CreateGroupDialog |
|
|
|
|
:visible.sync="createGroupVisible" |
|
|
|
|
:recentContacts="recentContacts" |
|
|
|
|
:recentGroups="recentGroups" |
|
|
|
|
@create-success="handleCreateGroupSuccess" |
|
|
|
|
ref="createGroupDialogRef" |
|
|
|
|
title="创建群聊" |
|
|
|
|
:min-select-count="1" |
|
|
|
|
@confirm="confirmCreateGroup" |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
</template> |
|
|
|
|
|
|
|
|
|
<script> |
|
|
|
|
import { mapGetters, mapActions } from "vuex"; |
|
|
|
|
import { |
|
|
|
|
getLatestContacts, |
|
|
|
|
searchUsers, |
|
|
|
|
searchGroups, |
|
|
|
|
} from "@/api/message/index"; |
|
|
|
|
import { MessageType, ContactsScene } from "@/utils/constants"; |
|
|
|
|
import { getLatestContacts, searchUsers, searchGroups } from "@/api/message"; |
|
|
|
|
import { createMultiChat } from "@/api/multichat"; |
|
|
|
|
import { MessageType, ContactsScene, MqttTopics } from "@/utils/constants"; |
|
|
|
|
// 引入子组件 |
|
|
|
|
import CreateGroupDialog from "./CreateGroupDialog.vue"; |
|
|
|
|
|
|
|
|
|
export default { |
|
|
|
|
name: "MessageList", |
|
|
|
|
components: { CreateGroupDialog }, |
|
|
|
|
@ -132,7 +159,11 @@ export default { |
|
|
|
|
searchUsers: [], |
|
|
|
|
searchGroups: [], |
|
|
|
|
searchTimer: null, |
|
|
|
|
createGroupVisible: false, |
|
|
|
|
// 分页相关 |
|
|
|
|
currentPage: 1, |
|
|
|
|
pageSize: 20, |
|
|
|
|
hasMore: true, |
|
|
|
|
loading: false, |
|
|
|
|
}; |
|
|
|
|
}, |
|
|
|
|
computed: { |
|
|
|
|
@ -145,7 +176,9 @@ export default { |
|
|
|
|
sortedContacts() { |
|
|
|
|
const list = [...this.latestContacts]; |
|
|
|
|
return list.sort((a, b) => { |
|
|
|
|
if (a.is_pinned !== b.is_pinned) return b.is_pinned - a.is_pinned; |
|
|
|
|
if (a.is_pinned !== b.is_pinned) { |
|
|
|
|
return b.is_pinned - a.is_pinned; |
|
|
|
|
} |
|
|
|
|
const timeA = a.last_message_time |
|
|
|
|
? new Date(a.last_message_time).getTime() |
|
|
|
|
: 0; |
|
|
|
|
@ -155,16 +188,6 @@ export default { |
|
|
|
|
return timeB - timeA; |
|
|
|
|
}); |
|
|
|
|
}, |
|
|
|
|
// 最近联系人 |
|
|
|
|
recentContacts() { |
|
|
|
|
return this.latestContacts.filter( |
|
|
|
|
(c) => c.scene === ContactsScene.PRIVATE |
|
|
|
|
); |
|
|
|
|
}, |
|
|
|
|
// 最近群组 |
|
|
|
|
recentGroups() { |
|
|
|
|
return this.latestContacts.filter((c) => c.scene === ContactsScene.GROUP); |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
mounted() { |
|
|
|
|
this.loadContacts(); |
|
|
|
|
@ -173,72 +196,116 @@ export default { |
|
|
|
|
...mapActions("message", ["setLatestContacts", "setCurrentChat"]), |
|
|
|
|
...mapActions("mqtt", ["subscribe"]), |
|
|
|
|
|
|
|
|
|
async loadContacts() { |
|
|
|
|
async loadContacts(scene = ContactsScene.ALL, isLoadMore = false) { |
|
|
|
|
if (this.loading) return; |
|
|
|
|
|
|
|
|
|
// 首次加载时重置分页状态 |
|
|
|
|
if (!isLoadMore) { |
|
|
|
|
this.currentPage = 1; |
|
|
|
|
this.hasMore = true; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
this.loading = true; |
|
|
|
|
try { |
|
|
|
|
this.setLatestContacts([ |
|
|
|
|
{ |
|
|
|
|
id: "user1", |
|
|
|
|
name: "王医生", |
|
|
|
|
avatar: "", |
|
|
|
|
scene: 1, |
|
|
|
|
unread_count: 0, |
|
|
|
|
last_message: "建议您注意饮食调理", |
|
|
|
|
last_message_time: new Date(Date.now() - 6500000).toISOString(), |
|
|
|
|
is_online: true, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
id: "user2", |
|
|
|
|
name: "李护士", |
|
|
|
|
avatar: "", |
|
|
|
|
scene: 1, |
|
|
|
|
unread_count: 1, |
|
|
|
|
last_message: "这是今天的护理记录", |
|
|
|
|
last_message_time: new Date(Date.now() - 14100000).toISOString(), |
|
|
|
|
is_online: true, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
id: "user3", |
|
|
|
|
name: "张主任", |
|
|
|
|
avatar: "", |
|
|
|
|
scene: 1, |
|
|
|
|
unread_count: 0, |
|
|
|
|
last_message: "这是上次会诊的视频", |
|
|
|
|
last_message_time: new Date(Date.now() - 21300000).toISOString(), |
|
|
|
|
is_online: false, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
id: "group1", |
|
|
|
|
name: "医疗会诊群", |
|
|
|
|
avatar: "", |
|
|
|
|
scene: 4, |
|
|
|
|
unread_count: 3, |
|
|
|
|
last_message: "本次会议已被设置为重要", |
|
|
|
|
last_message_time: new Date().toISOString(), |
|
|
|
|
expanded: false, |
|
|
|
|
allSelected: false, |
|
|
|
|
members: [ |
|
|
|
|
{ id: "user1", name: "王医生" }, |
|
|
|
|
{ id: "user2", name: "李护士" }, |
|
|
|
|
], |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
id: "group2", |
|
|
|
|
name: "基层医生交流群", |
|
|
|
|
avatar: "", |
|
|
|
|
scene: 4, |
|
|
|
|
unread_count: 0, |
|
|
|
|
last_message: "欢迎赵医生加入", |
|
|
|
|
last_message_time: new Date(Date.now() - 42600000).toISOString(), |
|
|
|
|
expanded: false, |
|
|
|
|
allSelected: false, |
|
|
|
|
members: [ |
|
|
|
|
{ id: "user1", name: "王医生" }, |
|
|
|
|
{ id: "user2", name: "李护士" }, |
|
|
|
|
], |
|
|
|
|
}, |
|
|
|
|
]); |
|
|
|
|
const res = await getLatestContacts({ |
|
|
|
|
scene, |
|
|
|
|
page: this.currentPage, |
|
|
|
|
size: this.pageSize, |
|
|
|
|
}); |
|
|
|
|
const contacts = res.data?.list || res.data || []; |
|
|
|
|
|
|
|
|
|
if (isLoadMore) { |
|
|
|
|
// 加载更多:追加数据 |
|
|
|
|
const existingContacts = [...this.latestContacts]; |
|
|
|
|
this.setLatestContacts([...existingContacts, ...contacts]); |
|
|
|
|
} else { |
|
|
|
|
// 首次加载:重置数据 |
|
|
|
|
this.setLatestContacts(contacts); |
|
|
|
|
} |
|
|
|
|
// 默认选中第一个联系人 |
|
|
|
|
if (contacts.length > 0 && !this.currentChat) { |
|
|
|
|
this.handleSelectContact(contacts[0]); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 判断是否还有更多数据 |
|
|
|
|
if (contacts.length < this.pageSize) { |
|
|
|
|
this.hasMore = false; |
|
|
|
|
} |
|
|
|
|
} catch (e) { |
|
|
|
|
console.error("获取联系人列表失败", e); |
|
|
|
|
} finally { |
|
|
|
|
this.loading = false; |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
// 滚动加载更多 |
|
|
|
|
handleScroll(e) { |
|
|
|
|
const el = e.target; |
|
|
|
|
const scrollTop = el.scrollTop; |
|
|
|
|
const clientHeight = el.clientHeight; |
|
|
|
|
const scrollHeight = el.scrollHeight; |
|
|
|
|
|
|
|
|
|
console.log("滚动事件触发:", { |
|
|
|
|
scrollTop, |
|
|
|
|
clientHeight, |
|
|
|
|
scrollHeight, |
|
|
|
|
hasMore: this.hasMore, |
|
|
|
|
loading: this.loading, |
|
|
|
|
currentPage: this.currentPage, |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// 滚动到底部时加载更多 |
|
|
|
|
if (scrollTop + clientHeight >= scrollHeight - 50) { |
|
|
|
|
if (this.hasMore && !this.loading) { |
|
|
|
|
console.log( |
|
|
|
|
"触发加载下一页, 当前页:", |
|
|
|
|
this.currentPage, |
|
|
|
|
"下一页:", |
|
|
|
|
this.currentPage + 1 |
|
|
|
|
); |
|
|
|
|
this.currentPage++; |
|
|
|
|
this.loadContacts(ContactsScene.ALL, true); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
// 公开方法:刷新列表 |
|
|
|
|
// options: { selectFirst: true打开第一个, keepCurrent: true保持当前选中 } |
|
|
|
|
async refreshList(options = {}) { |
|
|
|
|
const { selectFirst = false, keepCurrent = false } = options; |
|
|
|
|
// 保存当前选中的聊天 |
|
|
|
|
const currentChatBackup = { ...this.currentChat }; |
|
|
|
|
// 重置分页 |
|
|
|
|
this.currentPage = 1; |
|
|
|
|
this.hasMore = true; |
|
|
|
|
// 重新加载列表 |
|
|
|
|
this.loading = true; |
|
|
|
|
try { |
|
|
|
|
const res = await getLatestContacts({ |
|
|
|
|
scene: ContactsScene.ALL, |
|
|
|
|
page: 1, |
|
|
|
|
size: this.pageSize, |
|
|
|
|
}); |
|
|
|
|
const contacts = res.data?.list || res.data || []; |
|
|
|
|
this.setLatestContacts(contacts); |
|
|
|
|
|
|
|
|
|
if (selectFirst && contacts.length > 0) { |
|
|
|
|
// 新建群聊场景:打开第一个 |
|
|
|
|
this.handleSelectContact(contacts[0]); |
|
|
|
|
} else if (keepCurrent && currentChatBackup && contacts.length > 0) { |
|
|
|
|
// 群设置修改场景:尝试保持当前选中 |
|
|
|
|
const found = contacts.find( |
|
|
|
|
(c) => c.id === currentChatBackup.id && c.scene === currentChatBackup.scene |
|
|
|
|
); |
|
|
|
|
if (found) { |
|
|
|
|
this.handleSelectContact(found); |
|
|
|
|
} else if (contacts.length > 0) { |
|
|
|
|
// 当前聊天不在列表中,选择第一个 |
|
|
|
|
this.handleSelectContact(contacts[0]); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} catch (e) { |
|
|
|
|
console.error("获取联系人失败", e); |
|
|
|
|
console.error("刷新联系人列表失败", e); |
|
|
|
|
} finally { |
|
|
|
|
this.loading = false; |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
handleSearchInput() { |
|
|
|
|
@ -248,23 +315,44 @@ export default { |
|
|
|
|
this.searchGroups = []; |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
this.searchTimer = setTimeout(() => this.doSearch(), 300); |
|
|
|
|
this.searchTimer = setTimeout(() => { |
|
|
|
|
this.doSearch(); |
|
|
|
|
}, 300); |
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
async doSearch() { |
|
|
|
|
this.searchUsers = [ |
|
|
|
|
{ id: "user1", name: "基层医生", department: "在线" }, |
|
|
|
|
{ id: "user2", name: "郭君" }, |
|
|
|
|
]; |
|
|
|
|
this.searchGroups = [{ id: "group1", name: "基层医生、会诊专家、郭君" }]; |
|
|
|
|
const key = this.searchKey.trim(); |
|
|
|
|
if (!key) return; |
|
|
|
|
try { |
|
|
|
|
const [userRes, groupRes] = await Promise.all([ |
|
|
|
|
searchUsers({ search_key: key, page: 1, size: 10 }), |
|
|
|
|
searchGroups({ search_key: key, page: 1, size: 10 }), |
|
|
|
|
]); |
|
|
|
|
this.searchUsers = userRes.data?.list || userRes.data || []; |
|
|
|
|
this.searchGroups = groupRes.data?.list || groupRes.data || []; |
|
|
|
|
} catch (e) { |
|
|
|
|
console.error("搜索失败", e); |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
handleSearchBlur() { |
|
|
|
|
setTimeout(() => { |
|
|
|
|
this.isSearching = false; |
|
|
|
|
this.searchKey = ""; |
|
|
|
|
this.searchUsers = []; |
|
|
|
|
this.searchGroups = []; |
|
|
|
|
}, 200); |
|
|
|
|
}, |
|
|
|
|
handleSelectSearchResult(item, scene) { |
|
|
|
|
this.handleSelectContact({ ...item, scene }); |
|
|
|
|
const contact = { |
|
|
|
|
id: item.id, |
|
|
|
|
name: item.name, |
|
|
|
|
avatar: item.avatar, |
|
|
|
|
scene: scene, |
|
|
|
|
unread_count: 0, |
|
|
|
|
last_message: "", |
|
|
|
|
last_message_time: new Date().toISOString(), |
|
|
|
|
}; |
|
|
|
|
this.handleSelectContact(contact); |
|
|
|
|
this.isSearching = false; |
|
|
|
|
this.searchKey = ""; |
|
|
|
|
}, |
|
|
|
|
@ -279,73 +367,213 @@ export default { |
|
|
|
|
this.currentChat.scene === contact.scene |
|
|
|
|
); |
|
|
|
|
}, |
|
|
|
|
// 格式化日期 - 统一显示为 yyyy-MM-dd 格式 |
|
|
|
|
formatTime(timeStr) { |
|
|
|
|
if (!timeStr) return ""; |
|
|
|
|
const date = new Date(timeStr); |
|
|
|
|
const now = new Date(); |
|
|
|
|
if (date.toDateString() === now.toDateString()) |
|
|
|
|
return date.toLocaleTimeString("zh-CN", { |
|
|
|
|
hour: "2-digit", |
|
|
|
|
minute: "2-digit", |
|
|
|
|
}); |
|
|
|
|
const yesterday = new Date(now); |
|
|
|
|
yesterday.setDate(yesterday.getDate() - 1); |
|
|
|
|
if (date.toDateString() === yesterday.toDateString()) return "昨天"; |
|
|
|
|
return date.toLocaleDateString("zh-CN", { |
|
|
|
|
month: "2-digit", |
|
|
|
|
day: "2-digit", |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// 重置到当天0点计算天数差 |
|
|
|
|
const dateDay = new Date( |
|
|
|
|
date.getFullYear(), |
|
|
|
|
date.getMonth(), |
|
|
|
|
date.getDate() |
|
|
|
|
); |
|
|
|
|
const nowDay = new Date(now.getFullYear(), now.getMonth(), now.getDate()); |
|
|
|
|
const diffDays = Math.floor((nowDay - dateDay) / (24 * 60 * 60 * 1000)); |
|
|
|
|
|
|
|
|
|
// 今天:显示 HH:mm |
|
|
|
|
if (diffDays === 0) { |
|
|
|
|
const hour = date.getHours().toString().padStart(2, "0"); |
|
|
|
|
const minute = date.getMinutes().toString().padStart(2, "0"); |
|
|
|
|
return `${hour}:${minute}`; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 今年的消息:显示 MM-dd HH:mm |
|
|
|
|
if (date.getFullYear() === now.getFullYear()) { |
|
|
|
|
const month = (date.getMonth() + 1).toString().padStart(2, "0"); |
|
|
|
|
const day = date.getDate().toString().padStart(2, "0"); |
|
|
|
|
const hour = date.getHours().toString().padStart(2, "0"); |
|
|
|
|
const minute = date.getMinutes().toString().padStart(2, "0"); |
|
|
|
|
return `${month}-${day} ${hour}:${minute}`; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 非今年的消息:显示 yyyy-MM-dd HH:mm |
|
|
|
|
const year = date.getFullYear(); |
|
|
|
|
const month = (date.getMonth() + 1).toString().padStart(2, "0"); |
|
|
|
|
const day = date.getDate().toString().padStart(2, "0"); |
|
|
|
|
const hour = date.getHours().toString().padStart(2, "0"); |
|
|
|
|
const minute = date.getMinutes().toString().padStart(2, "0"); |
|
|
|
|
return `${year}-${month}-${day} ${hour}:${minute}`; |
|
|
|
|
}, |
|
|
|
|
// 格式化最后一条消息 |
|
|
|
|
formatLastMessage(contact) { |
|
|
|
|
const msg = contact.last_message || ""; |
|
|
|
|
const msg = contact.last_message; |
|
|
|
|
if (!msg) return ""; |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
const parsed = JSON.parse(msg); |
|
|
|
|
const lastMsg = typeof msg === "string" ? JSON.parse(msg) : msg; |
|
|
|
|
const msgType = contact.last_message_type; |
|
|
|
|
const typeMap = { |
|
|
|
|
[MessageType.TEXT]: parsed.payload, |
|
|
|
|
[MessageType.IMAGE]: "[图片]", |
|
|
|
|
[MessageType.VIDEO]: "[视频]", |
|
|
|
|
[MessageType.TEXT]: () => { |
|
|
|
|
const content = lastMsg.content || lastMsg.payload || ""; |
|
|
|
|
return content.length > 30 |
|
|
|
|
? content.slice(0, 30) + "..." |
|
|
|
|
: content || "[文本消息]"; |
|
|
|
|
}, |
|
|
|
|
[MessageType.IMAGE]: () => "[图片]", |
|
|
|
|
[MessageType.VIDEO]: () => { |
|
|
|
|
const fileName = lastMsg.file_name || "[视频文件]"; |
|
|
|
|
return `[视频] ${ |
|
|
|
|
fileName.length > 20 ? fileName.slice(0, 20) + "..." : fileName |
|
|
|
|
}`; |
|
|
|
|
}, |
|
|
|
|
[MessageType.AUDIO]: () => "[语音]", |
|
|
|
|
[MessageType.FILE]: () => { |
|
|
|
|
const fileName = lastMsg.file_name || "[文件]"; |
|
|
|
|
return `[文件] ${ |
|
|
|
|
fileName.length > 20 ? fileName.slice(0, 20) + "..." : fileName |
|
|
|
|
}`; |
|
|
|
|
}, |
|
|
|
|
[MessageType.KNOWLEDGE_SHARE]: () => "[知识库分享]", |
|
|
|
|
[MessageType.REPORT_SHARE]: () => "[病例分享]", |
|
|
|
|
[MessageType.MULTI_CHAT_INIT_JOIN]: () => { |
|
|
|
|
try { |
|
|
|
|
const payload = |
|
|
|
|
typeof lastMsg.content === "string" |
|
|
|
|
? JSON.parse(lastMsg.content) |
|
|
|
|
: lastMsg.content; |
|
|
|
|
const userIds = payload?.invited_user_ids || []; |
|
|
|
|
if (userIds.length > 0) { |
|
|
|
|
return `${contact.source_name || "成员"}邀请了${ |
|
|
|
|
userIds.length |
|
|
|
|
}人加入群聊`; |
|
|
|
|
} |
|
|
|
|
return `${contact.source_name || "成员"}加入了群聊`; |
|
|
|
|
} catch (e) { |
|
|
|
|
return `${contact.source_name || "成员"}加入了群聊`; |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
[MessageType.MULTI_CHAT_JOIN]: () => |
|
|
|
|
`${contact.source_name || "成员"}加入了群聊`, |
|
|
|
|
[MessageType.MULTI_CHAT_QUIT]: () => |
|
|
|
|
`${contact.source_name || "成员"}退出了群聊`, |
|
|
|
|
[MessageType.MULTI_CHAT_NOTICE]: () => "[群公告]", |
|
|
|
|
[MessageType.CONSULTATION_MESSAGE_INVITE]: () => "[会诊邀请]", |
|
|
|
|
[MessageType.NOTIFY_CONTENT]: () => { |
|
|
|
|
try { |
|
|
|
|
const payload = |
|
|
|
|
typeof lastMsg.content === "string" |
|
|
|
|
? JSON.parse(lastMsg.content) |
|
|
|
|
: lastMsg.content; |
|
|
|
|
return payload.title || "[系统通知]"; |
|
|
|
|
} catch (e) { |
|
|
|
|
return "[系统通知]"; |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
}; |
|
|
|
|
return typeMap[parsed.type] || msg; |
|
|
|
|
|
|
|
|
|
if (typeMap[msgType]) { |
|
|
|
|
return typeMap[msgType](); |
|
|
|
|
} |
|
|
|
|
// 默认处理:显示content或文件名称 |
|
|
|
|
if ( |
|
|
|
|
lastMsg.content && |
|
|
|
|
typeof lastMsg.content === "string" && |
|
|
|
|
lastMsg.content.length > 0 |
|
|
|
|
) { |
|
|
|
|
return lastMsg.content.length > 30 |
|
|
|
|
? lastMsg.content.slice(0, 30) + "..." |
|
|
|
|
: lastMsg.content; |
|
|
|
|
} |
|
|
|
|
if (lastMsg.file_name) { |
|
|
|
|
return `[文件] ${ |
|
|
|
|
lastMsg.file_name.length > 20 |
|
|
|
|
? lastMsg.file_name.slice(0, 20) + "..." |
|
|
|
|
: lastMsg.file_name |
|
|
|
|
}`; |
|
|
|
|
} |
|
|
|
|
return "[消息]"; |
|
|
|
|
} catch (e) { |
|
|
|
|
return msg; |
|
|
|
|
// 如果解析失败,直接显示原始内容 |
|
|
|
|
const strMsg = typeof msg === "string" ? msg : JSON.stringify(msg); |
|
|
|
|
return strMsg.length > 30 ? strMsg.slice(0, 30) + "..." : strMsg; |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
handleScroll() {}, |
|
|
|
|
|
|
|
|
|
// 打开创建群聊 |
|
|
|
|
// 发起群聊 |
|
|
|
|
handleCreateGroup() { |
|
|
|
|
this.createGroupVisible = true; |
|
|
|
|
this.$refs.createGroupDialogRef.show(); |
|
|
|
|
}, |
|
|
|
|
// 发起群聊确认 |
|
|
|
|
async confirmCreateGroup(selectedMembers) { |
|
|
|
|
try { |
|
|
|
|
const res = await createMultiChat({ |
|
|
|
|
name: |
|
|
|
|
this.userInfo.name + |
|
|
|
|
"、" + |
|
|
|
|
selectedMembers.map((member) => member.name).join("、"), |
|
|
|
|
avatar: "", |
|
|
|
|
init_users: selectedMembers.map((member) => member.id), |
|
|
|
|
}); |
|
|
|
|
// 刷新列表并打开第一个 |
|
|
|
|
await this.refreshList({ selectFirst: true }); |
|
|
|
|
await this.subscribe(MqttTopics.MULTI_CHAT + res.data.multi_chat_id); |
|
|
|
|
this.$message.success("创建成功"); |
|
|
|
|
} catch (e) { |
|
|
|
|
this.$message.error("创建失败: " + (e.message || "未知错误")); |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
// 外部调用:更新联系人最后消息 |
|
|
|
|
updateContactLastMessage(contactId, scene, message, time) { |
|
|
|
|
const contacts = [...this.latestContacts]; |
|
|
|
|
const idx = contacts.findIndex( |
|
|
|
|
(c) => c.id === contactId && c.scene === scene |
|
|
|
|
); |
|
|
|
|
if (idx >= 0) { |
|
|
|
|
contacts[idx].last_message = |
|
|
|
|
typeof message === "string" ? message : JSON.stringify(message); |
|
|
|
|
contacts[idx].last_message_time = time || new Date().toISOString(); |
|
|
|
|
if (!this.isActiveContact(contacts[idx])) { |
|
|
|
|
contacts[idx].unread_count = (contacts[idx].unread_count || 0) + 1; |
|
|
|
|
} |
|
|
|
|
this.setLatestContacts(contacts); |
|
|
|
|
} else { |
|
|
|
|
// 新联系人,需要刷新列表 |
|
|
|
|
this.loadContacts(); |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
// 群聊创建成功 |
|
|
|
|
handleCreateGroupSuccess(selectedMembers) { |
|
|
|
|
const memberNames = selectedMembers.slice(0, 3).map((m) => m.name); |
|
|
|
|
const groupName = |
|
|
|
|
memberNames.join("、") + |
|
|
|
|
(selectedMembers.length > 3 ? `、等${selectedMembers.length}人` : ""); |
|
|
|
|
const contact = { |
|
|
|
|
id: "group_" + Date.now(), |
|
|
|
|
name: groupName, |
|
|
|
|
avatar: "", |
|
|
|
|
scene: ContactsScene.GROUP, |
|
|
|
|
unread_count: 0, |
|
|
|
|
last_message: "", |
|
|
|
|
last_message_time: new Date().toISOString(), |
|
|
|
|
}; |
|
|
|
|
this.$message.success("群聊创建成功"); |
|
|
|
|
this.loadContacts(); |
|
|
|
|
this.handleSelectContact(contact); |
|
|
|
|
// 外部调用:清除未读 |
|
|
|
|
clearUnread(contactId, scene) { |
|
|
|
|
const contacts = [...this.latestContacts]; |
|
|
|
|
const idx = contacts.findIndex( |
|
|
|
|
(c) => c.id === contactId && c.scene === scene |
|
|
|
|
); |
|
|
|
|
if (idx >= 0) { |
|
|
|
|
contacts[idx].unread_count = 0; |
|
|
|
|
this.setLatestContacts(contacts); |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
updateContactLastMessage() {}, |
|
|
|
|
clearUnread() {}, |
|
|
|
|
removeContact() {}, |
|
|
|
|
// 外部调用:删除联系人 |
|
|
|
|
removeContact(contactId, scene) { |
|
|
|
|
const contacts = this.latestContacts.filter( |
|
|
|
|
(c) => !(c.id === contactId && c.scene === scene) |
|
|
|
|
); |
|
|
|
|
this.setLatestContacts(contacts); |
|
|
|
|
if ( |
|
|
|
|
this.currentChat && |
|
|
|
|
this.currentChat.id === contactId && |
|
|
|
|
this.currentChat.scene === scene |
|
|
|
|
) { |
|
|
|
|
this.setCurrentChat(null); |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
}; |
|
|
|
|
</script> |
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped> |
|
|
|
|
/* 样式完全保留不变 */ |
|
|
|
|
.message-list { |
|
|
|
|
width: 280px; |
|
|
|
|
height: 100%; |
|
|
|
|
@ -354,44 +582,54 @@ export default { |
|
|
|
|
border-right: 1px solid #ebeef5; |
|
|
|
|
background: #fff; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.list-header { |
|
|
|
|
padding: 12px; |
|
|
|
|
display: flex; |
|
|
|
|
align-items: center; |
|
|
|
|
gap: 8px; |
|
|
|
|
border-bottom: 1px solid #ebeef5; |
|
|
|
|
|
|
|
|
|
.el-input { |
|
|
|
|
flex: 1; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.create-group-btn { |
|
|
|
|
flex-shrink: 0; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.search-dropdown { |
|
|
|
|
flex: 1; |
|
|
|
|
overflow-y: auto; |
|
|
|
|
background: #fff; |
|
|
|
|
|
|
|
|
|
.search-section { |
|
|
|
|
padding: 8px 0; |
|
|
|
|
|
|
|
|
|
.search-title { |
|
|
|
|
padding: 4px 12px; |
|
|
|
|
font-size: 12px; |
|
|
|
|
color: #909399; |
|
|
|
|
font-weight: 500; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.search-item { |
|
|
|
|
display: flex; |
|
|
|
|
align-items: center; |
|
|
|
|
padding: 8px 12px; |
|
|
|
|
cursor: pointer; |
|
|
|
|
transition: background 0.2s; |
|
|
|
|
|
|
|
|
|
&:hover { |
|
|
|
|
background: #f5f7fa; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.search-item-info { |
|
|
|
|
margin-left: 10px; |
|
|
|
|
flex: 1; |
|
|
|
|
min-width: 0; |
|
|
|
|
|
|
|
|
|
.search-item-name { |
|
|
|
|
font-size: 14px; |
|
|
|
|
color: #303133; |
|
|
|
|
@ -399,6 +637,7 @@ export default { |
|
|
|
|
overflow: hidden; |
|
|
|
|
text-overflow: ellipsis; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.search-item-dept { |
|
|
|
|
font-size: 12px; |
|
|
|
|
color: #909399; |
|
|
|
|
@ -408,9 +647,11 @@ export default { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.contact-list { |
|
|
|
|
flex: 1; |
|
|
|
|
overflow-y: auto; |
|
|
|
|
|
|
|
|
|
.contact-item { |
|
|
|
|
display: flex; |
|
|
|
|
align-items: center; |
|
|
|
|
@ -418,29 +659,36 @@ export default { |
|
|
|
|
cursor: pointer; |
|
|
|
|
transition: background 0.2s; |
|
|
|
|
position: relative; |
|
|
|
|
|
|
|
|
|
&:hover { |
|
|
|
|
background: #f5f7fa; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
&.active { |
|
|
|
|
background: #ecf5ff; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.contact-avatar-wrap { |
|
|
|
|
position: relative; |
|
|
|
|
flex-shrink: 0; |
|
|
|
|
|
|
|
|
|
.unread-badge { |
|
|
|
|
position: absolute; |
|
|
|
|
top: -4px; |
|
|
|
|
right: -4px; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.contact-info { |
|
|
|
|
flex: 1; |
|
|
|
|
margin-left: 10px; |
|
|
|
|
min-width: 0; |
|
|
|
|
|
|
|
|
|
.contact-top { |
|
|
|
|
display: flex; |
|
|
|
|
justify-content: space-between; |
|
|
|
|
align-items: center; |
|
|
|
|
|
|
|
|
|
.contact-name { |
|
|
|
|
font-size: 14px; |
|
|
|
|
color: #303133; |
|
|
|
|
@ -450,17 +698,20 @@ export default { |
|
|
|
|
text-overflow: ellipsis; |
|
|
|
|
max-width: 140px; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.contact-time { |
|
|
|
|
font-size: 12px; |
|
|
|
|
color: #c0c4cc; |
|
|
|
|
flex-shrink: 0; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.contact-bottom { |
|
|
|
|
display: flex; |
|
|
|
|
justify-content: space-between; |
|
|
|
|
align-items: center; |
|
|
|
|
margin-top: 4px; |
|
|
|
|
|
|
|
|
|
.contact-last-msg { |
|
|
|
|
font-size: 12px; |
|
|
|
|
color: #909399; |
|
|
|
|
@ -469,6 +720,7 @@ export default { |
|
|
|
|
text-overflow: ellipsis; |
|
|
|
|
max-width: 170px; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.pin-icon { |
|
|
|
|
color: #409eff; |
|
|
|
|
font-size: 12px; |
|
|
|
|
@ -476,17 +728,44 @@ export default { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.empty-contacts { |
|
|
|
|
text-align: center; |
|
|
|
|
padding: 60px 20px; |
|
|
|
|
color: #c0c4cc; |
|
|
|
|
|
|
|
|
|
i { |
|
|
|
|
font-size: 48px; |
|
|
|
|
margin-bottom: 12px; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
p { |
|
|
|
|
font-size: 14px; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.loading-tip { |
|
|
|
|
text-align: center; |
|
|
|
|
padding: 10px; |
|
|
|
|
color: #909399; |
|
|
|
|
font-size: 12px; |
|
|
|
|
|
|
|
|
|
i { |
|
|
|
|
margin-right: 4px; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.no-more-tip { |
|
|
|
|
text-align: center; |
|
|
|
|
padding: 10px; |
|
|
|
|
color: #c0c4cc; |
|
|
|
|
font-size: 12px; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.select-user-option { |
|
|
|
|
display: flex; |
|
|
|
|
align-items: center; |
|
|
|
|
gap: 8px; |
|
|
|
|
} |
|
|
|
|
</style> |