消息模块-功能联调

main
ysn 4 days ago
parent 1ea1480e1c
commit 864215e901
  1. 14
      src/views/message/components/GroupSetting.vue
  2. 208
      src/views/message/components/MessageDisplay.vue
  3. 878
      src/views/message/components/MessageEditor.vue
  4. 25
      src/views/message/components/MessageList.vue
  5. 53
      src/views/message/components/SearchRecord.vue
  6. 15
      src/views/message/index.vue

@ -142,13 +142,13 @@ export default {
props: {
chat: Object,
groupInfo: Object,
},
data() {
return {
visible: false,
isSearchVisible: false,
groupInfo: {},
isAdmin: false,
showDeleteBtn: null,
currentUserId: this.$store.state.user.userInfo?.id,
@ -162,18 +162,6 @@ export default {
methods: {
async loadGroupInfo() {
this.visible = true;
if (this.chat) {
if (!this.chat?.id) return;
try {
const res = await getMultiChatInfo({ multi_chat_id: this.chat.id });
this.groupInfo = {
...res.data,
user_list: res.data.user_list.sort((a, b) => a.id - b.id),
};
} catch (e) {
console.error("获取群信息失败", e);
}
}
},
editGroupName() {

@ -4,8 +4,8 @@
<div class="chat-header">
<div class="chat-title">
<span class="name" v-if="currentChat">{{ currentChat.name }}</span>
<span v-if="currentChat && currentChat.scene === 4">
({{ currentChat.user_num }})
<span v-if="currentChat && currentChat.scene === ContactsScene.GROUP">
({{ currentChat.user_num - 1 }})
</span>
</div>
<div class="chat-actions">
@ -13,12 +13,13 @@
type="text"
icon="el-icon-chat-line-round"
@click="handleSearchRecord"
v-if="currentChat && currentChat.scene != ContactsScene.NOTIFY"
/>
<el-button
type="text"
icon="el-icon-s-tools"
@click="handleGroupSetting"
v-if="currentChat && currentChat.scene === 4"
v-if="currentChat && currentChat.scene === ContactsScene.GROUP"
/>
</div>
</div>
@ -41,37 +42,67 @@
<div
v-for="(msg, index) in currentMessages"
:key="msg.message_id || msg.id || index"
:class="['message-item', msg.is_self ? 'self' : 'other']"
:class="[
'message-item',
msg.is_self ? 'self' : 'other',
{
'system-message':
msg.type === MessageType.MULTI_CHAT_INIT_JOIN ||
msg.type === MessageType.MULTI_CHAT_QUIT ||
msg.type === MessageType.MULTI_CHAT_EDIT ||
msg.type === MessageType.MULTI_CHAT_JOIN,
},
]"
>
<!-- 时间分割线 -->
<div class="time-divider">
{{ formatMessageTime(msg.create_time) }}
</div>
<!-- 系统消息群成员变更通知 -->
<div
v-if="msg.type === MessageType.MULTI_CHAT_INIT_JOIN"
class="time-divider"
v-if="
msg.type === MessageType.MULTI_CHAT_INIT_JOIN ||
msg.type === MessageType.MULTI_CHAT_JOIN
"
>
<div class="time-divider">
{{ formatMessageTime(msg.update_time) }}
</div>
<div class="time-divider">
{{
currentChat.user_num > 0
? `${currentChat.source_name || "成员"}邀请了${
currentChat.user_num
(msg.source_name || "成员") +
(JSON.parse(msg.content).invited_user_ids.length > 0
? `邀请了${
JSON.parse(msg.content).invited_user_ids.length || 0
}人加入群聊`
: `${currentChat.source_name || "成员"}加入了群聊`
: "加入了群聊")
}}
</div>
<div
v-if="msg.type === MessageType.MULTI_CHAT_QUIT"
class="time-divider"
>
</div>
<template v-else-if="msg.type === MessageType.MULTI_CHAT_QUIT">
<div class="time-divider">
{{ formatMessageTime(msg.update_time) }}
</div>
<div class="time-divider">
{{ msg.source_name || "成员" }}退出了群聊
</div>
<div
class="message-content-wrap"
v-if="
msg.type !== MessageType.MULTI_CHAT_INIT_JOIN &&
msg.type !== MessageType.MULTI_CHAT_QUIT
"
>
</template>
<div v-else-if="msg.type === MessageType.MULTI_CHAT_EDIT">
<div class="time-divider">
{{ formatMessageTime(msg.update_time) }}
</div>
<div class="time-divider">
{{
(msg.sender_name || msg.source_name || "未知用户") +
`修改了群名为"${JSON.parse(msg.content).name || ""}"`
}}
</div>
</div>
<!-- 普通消息 -->
<div v-else>
<div class="time-divider">
{{ formatMessageTime(msg.update_time) }}
</div>
<div class="message-content-wrap">
<!-- 头像 -->
<el-avatar
:size="36"
@ -79,10 +110,12 @@
msg.is_self
? $store.state.user.netConfig.MINIO_ENDPOINT_HTTPS +
$store.state.user.userInfo.avatar
: currentChat.scene === ContactsScene.GROUP
? $store.state.user.netConfig.MINIO_ENDPOINT_HTTPS +
msg.avatar
: $store.state.user.netConfig.MINIO_ENDPOINT_HTTPS +
currentChat.avatar
"
:icon="msg.is_self ? 'el-icon-user-solid' : 'el-icon-user'"
class="message-avatar"
/>
@ -212,7 +245,9 @@
</el-link>
</div>
<div
v-else-if="msg.type === MessageType.CONSULTATION_MESSAGE_INVITE"
v-else-if="
msg.type === MessageType.CONSULTATION_MESSAGE_INVITE
"
class="text-message"
>
视讯邀请:{{ JSON.parse(msg.content).name }}邀请您参加会诊
@ -256,19 +291,26 @@
<span v-else-if="msg.sendFailed" class="status-failed"
><i class="el-icon-warning" /> 失败</span
>
<span
v-else-if="msg.read_status === 1 || msg.read"
v-if="
currentChat.scene === ContactsScene.GROUP &&
currentChat.user_num - msg.read_user_ids.length != 0
"
class="status-read"
>
已读
{{ currentChat.user_num - msg.read_user_ids.length }}人未
</span>
<span v-else class="status-unread">{{
currentChat.scene === 4
? msg.unread_count
? msg.unread_count + "人未读"
: "未读"
: "对方未读"
}}</span>
<span
v-else-if="
currentChat.scene !== ContactsScene.GROUP &&
msg.reader === 0
"
class="status-unread"
>
未读
</span>
</div>
</div>
</div>
</div>
@ -323,12 +365,15 @@ export default {
data() {
return {
MessageType,
ContactsScene,
loadingHistory: false,
hasMoreHistory: true,
loadingNewer: false,
hasMoreNewer: true,
scrollDebounceHistory: false,
scrollDebounceNewer: false,
baseMessageId: 0,
pageSize: 4,
pageSize: 10,
previewVisible: false,
previewImageUrl: "",
isNearBottom: true,
@ -380,12 +425,47 @@ export default {
this.setMessages({ key, messages: [] });
// base_message_id 0
await this.loadMessages({ up: 1, baseMessageId: 0 });
//
await this.$nextTick();
await this.preloadHistoryMessages();
this.$nextTick(() => {
this.scrollToBottom();
});
this.markAsRead();
},
async preloadHistoryMessages() {
const container = this.$refs.messageContainer;
if (!container) return;
const containerHeight = container.clientHeight;
let contentHeight = container.scrollHeight;
let prevHeight = 0;
let loadCount = 0;
const maxLoadCount = 10; // 10
//
while (
contentHeight < containerHeight &&
this.hasMoreHistory &&
!this.loadingHistory &&
loadCount < maxLoadCount
) {
prevHeight = contentHeight;
await this.loadMessages({ up: 1 });
await this.$nextTick();
contentHeight = container.scrollHeight;
loadCount++;
//
if (contentHeight === prevHeight) {
break;
}
}
},
//
// up: 0 1
async loadMessages({ up, baseMessageId }) {
@ -424,7 +504,7 @@ export default {
base_message_id: currentBaseMessageId,
page: 1,
size: this.pageSize,
up: up,
up: 1,
target_id: this.currentChat.id,
};
@ -453,12 +533,17 @@ export default {
const processed = list.map((m) => this.processMessage(m));
let merged;
// ID
const existingIds = new Set(existing.map(m => m.message_id || m.id));
if (up === 0) {
//
merged = [...existing, ...processed];
const newMessages = processed.filter(m => !existingIds.has(m.message_id || m.id));
merged = [...existing, ...newMessages];
} else {
//
merged = [...processed, ...existing];
const newMessages = processed.filter(m => !existingIds.has(m.message_id || m.id));
merged = [...newMessages, ...existing];
}
this.setMessages({ key, messages: merged });
@ -486,24 +571,29 @@ export default {
handleScroll(e) {
const el = e.target;
const scrollTop = el.scrollTop;
const clientHeight = el.clientHeight;
const scrollHeight = el.scrollHeight;
const isNearBottom = scrollTop + clientHeight >= scrollHeight - 50;
const isNearTop = scrollTop <= 30;
//
if (isNearBottom && this.hasMoreNewer && !this.loadingNewer) {
this.loadMessages({ up: 1 });
}
//
if (isNearTop && this.hasMoreHistory && !this.loadingHistory) {
if (
isNearTop &&
this.hasMoreHistory &&
!this.loadingHistory &&
!this.scrollDebounceHistory
) {
this.scrollDebounceHistory = true;
const oldHeight = scrollHeight;
this.loadMessages({ up: 1 }).then(() => {
this.loadMessages({ up: 1 })
.then(() => {
this.$nextTick(() => {
const newHeight = el.scrollHeight;
el.scrollTop = newHeight - oldHeight;
});
})
.finally(() => {
setTimeout(() => {
this.scrollDebounceHistory = false;
}, 1000);
});
}
},
@ -645,7 +735,11 @@ export default {
//
handleDownload(item) {
if (item.file_path) {
window.open(this.$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS + item.file_path, "_blank");
window.open(
this.$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS +
item.file_path,
"_blank"
);
} else {
this.$message.warning("文件链接不可用");
}
@ -656,7 +750,9 @@ export default {
this.$modal.msg("浏览器安全限制:请下载文件后在下载列表中打开文件夹");
//
const a = document.createElement("a");
a.href = this.$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS + item.file_path;
a.href =
this.$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS +
item.file_path;
a.download = item.file_name;
a.target = "_blank";
document.body.appendChild(a);
@ -680,10 +776,11 @@ export default {
},
handleOpenKnowledge(msg) {
const knowledgeId = msg.payload.knowledge_id;
if (knowledgeId) {
this.$router.push(`/knowledge-base?id=${knowledgeId}`);
}
window.open(
this.$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS +
JSON.parse(msg.content).file_path,
"_blank"
);
},
handleOpenConsultationInvite(msg) {
this.$router.push({
@ -754,6 +851,7 @@ export default {
});
}
this.$emit("clear-unread", this.currentChat.id, scene);
this.$emit("refresh-list");
} catch (e) {
console.error("标记已读失败", e);
}

File diff suppressed because it is too large Load Diff

@ -286,14 +286,15 @@ export default {
});
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
(c) =>
c.id === currentChatBackup.id &&
c.scene === currentChatBackup.scene
);
if (found) {
this.handleSelectContact(found);
@ -454,6 +455,26 @@ export default {
return `${contact.source_name || "成员"}加入了群聊`;
}
},
[MessageType.MULTI_CHAT_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_EDIT]: () =>
(contact.source_name || "未知用户") +
`修改了群名为"${JSON.parse(lastMsg.content).name || ""}"`,
[MessageType.MULTI_CHAT_JOIN]: () =>
`${contact.source_name || "成员"}加入了群聊`,
[MessageType.MULTI_CHAT_QUIT]: () =>

@ -13,7 +13,15 @@
<div class="category-tabs">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="图片" name="image" />
<el-tab-pane label="聊天记录" name="chat" />
<el-tab-pane label="聊天记录" name="chat">
<el-input
v-model="searchKeyword"
placeholder="搜索"
class="search-input"
@input="handleSearch"
prefix-icon="el-icon-search"
/>
</el-tab-pane>
</el-tabs>
</div>
@ -33,18 +41,13 @@
<!-- 聊天记录布局 -->
<div v-else>
<div class="search-header">
<el-input
v-model="searchKeyword"
placeholder="搜索"
class="search-input"
@input="handleSearch"
prefix-icon="el-icon-search"
/>
</div>
<div class="chat-list">
<!-- 根据 identity 类型匹配用户信息 -->
<div v-for="(item, index) in list" :key="item.id || index" class="result-item">
<div
v-for="(item, index) in list"
:key="item.id || index"
class="result-item"
>
<el-avatar
:size="40"
:src="getImgUrl(item.sender_avatar)"
@ -53,12 +56,18 @@
{{ getFirstChar(item.sender_name) }}
</el-avatar>
<div class="result-content">
<div class="result-name">{{ item.sender_name || '未知用户' }}</div>
<div v-if="item.file_type === 'image'" class="result-text">[图片]</div>
<div v-else class="result-text">{{ item.content || '' }}</div>
<div class="result-name">
{{ item.sender_name || "未知用户" }}
</div>
<div v-if="item.file_type === 'image'" class="result-text">
[图片]
</div>
<div v-else class="result-text">{{ item.content || "" }}</div>
</div>
<div class="result-right">
<div class="result-time">{{ item.create_time || item.update_time || '' }}</div>
<div class="result-time">
{{ item.create_time || item.update_time || "" }}
</div>
</div>
</div>
</div>
@ -96,7 +105,7 @@ export default {
query: {},
list: [],
page: 1,
size: 60,
size: 10,
loadingMore: false,
isNoMore: false,
//
@ -179,7 +188,8 @@ export default {
const data = res.data.list || [];
//
const processedData = await this.processMessagesWithUserInfo(data);
this.list = this.page === 1 ? processedData : [...this.list, ...processedData];
this.list =
this.page === 1 ? processedData : [...this.list, ...processedData];
if (data.length < this.size) {
this.isNoMore = true;
}
@ -296,15 +306,6 @@ export default {
flex-direction: column;
}
.search-header {
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.search-input {
width: 100%;
}
.category-tabs {
padding: 10px 0;
border-bottom: 1px solid #eee;

@ -7,10 +7,12 @@
@clear-unread="handleClearUnread"
@send-files="handleSendFiles"
@group-setting="handleGroupSetting"
@refresh-list="handleRefreshList"
/>
<MessageEditor
ref="messageEditor"
:group-info="groupInfo"
@send-message="handleSendMessage"
@update-message-status="handleUpdateMessageStatus"
/>
@ -20,6 +22,7 @@
<GroupSetting
ref="groupSettingRef"
:chat="currentChat"
:group-info="groupInfo"
@quit-group="handleQuitGroup"
@dismiss-group="handleDismissGroup"
@refresh-list="handleRefreshList"
@ -33,7 +36,7 @@ import MessageList from "./components/MessageList.vue";
import MessageDisplay from "./components/MessageDisplay.vue";
import GroupSetting from "./components/GroupSetting.vue";
import MessageEditor from "./components/MessageEditor.vue";
import { getMultiChatInfo, editMultiChatName } from '@/api/multichat'
import { getMultiChatInfo, editMultiChatName } from "@/api/multichat";
import { MessageType, ContactsScene, MqttTopics } from "@/utils/constants";
export default {
@ -48,6 +51,7 @@ export default {
data() {
return {
groupInfo: null,
};
},
@ -300,8 +304,15 @@ export default {
handleSelectContact(contact) {
// MQTT
if (contact.scene === ContactsScene.GROUP) {
if (contact.scene == ContactsScene.GROUP) {
this.subscribe(MqttTopics.MULTI_CHAT + contact.id);
//
getMultiChatInfo({ multi_chat_id: contact.id }).then((res) => {
this.groupInfo = res.data || null;
});
//
} else {
this.groupInfo = null;
}
},

Loading…
Cancel
Save