# Conflicts:
#	src/api/videoCommunication.js
main
SWX\10484 1 week ago
commit 61d152a6e8
  1. 2
      src/api/knowledge.js
  2. 50
      src/api/message/index.js
  3. 32
      src/api/multichat.js
  4. 13
      src/api/system.js
  5. 36
      src/api/videoCommunication.js
  6. 3
      src/utils/constants.js
  7. 104
      src/views/message/components/CreateGroupDialog.vue
  8. 278
      src/views/message/components/GroupSetting.vue
  9. 1257
      src/views/message/components/MessageDisplay.vue
  10. 587
      src/views/message/components/MessageList.vue
  11. 507
      src/views/message/components/SearchRecord.vue
  12. 270
      src/views/message/index.vue
  13. 36
      src/views/videoCommunication/index.vue

@ -44,7 +44,7 @@ export function postKnowledgeEdit(data) {
export function postKnowledgeDelete(data) {
return request({ url: '/knowledge/delete', method: 'post', data })
}
// 知识库文件-分享文件s
// 知识库文件-分享文件
export function postMessagePushToUser(data) {
return request({ url: '/messages/push/to/user', method: 'post', data })
}

@ -7,7 +7,7 @@ export function getLatestContacts(data) {
data
})
}
// 发送消息
export function sendPrivateMessage(data) {
return request({
url: '/messages/push/to/user',
@ -24,13 +24,6 @@ export function sendMultiChatMessage(data) {
})
}
export function pullPrivateMessages(data) {
return request({
url: '/messages/pull',
method: 'post',
data
})
}
export function pullMultiChatMessages(data) {
return request({
@ -63,19 +56,52 @@ export function searchGroups(data) {
data
})
}
export function getMessagesFileList(data) {
// 聊天记录-图片
export function postMessagesFileList(data) {
return request({
url: '/messages/file/list',
method: 'post',
data
})
}
export function searchMessages(data) {
// 聊天记录
export function postMessagesSearch(data) {
return request({
url: '/messages/search',
method: 'post',
data
})
}
// 个人消息-列表
export function postMessagesPull(data) {
return request({
url: '/messages/pull',
method: 'post',
data
})
}
// 系统通知-列表
export function postNotifyPull(data) {
return request({
url: '/notify/pull',
method: 'post',
data
})
}
// 系统通知-发消息
export function postNotifyPush(data) {
return request({
url: '/notify/push',
method: 'post',
data
})
}
// 系统通知-已读
export function postNotifyRead(data) {
return request({
url: '/notify/read',
method: 'post',
data
})
}

@ -3,15 +3,15 @@ import request from '@/utils/request'
export function createMultiChat(data) {
return request({
url: '/api/v1/multichat/create',
url: '/multichat/create',
method: 'post',
data
})
}
// 修改群名
export function editMultiChatName(data) {
return request({
url: '/api/v1/multichat/edit_name',
url: '/multichat/edit_name',
method: 'post',
data
})
@ -19,7 +19,7 @@ export function editMultiChatName(data) {
export function editMultiChatAvatar(data) {
return request({
url: '/api/v1/multichat/edit_avatar',
url: '/multichat/edit_avatar',
method: 'post',
data
})
@ -27,39 +27,39 @@ export function editMultiChatAvatar(data) {
export function updateMultiChatNotice(data) {
return request({
url: '/api/v1/multichat/notice',
url: '/multichat/notice',
method: 'post',
data
})
}
export function getMultiChatInfo(params) {
export function getMultiChatInfo(data) {
return request({
url: '/api/v1/multichat/info',
method: 'get',
params
url: '/multichat/info',
method: 'post',
data
})
}
// 邀请加入群聊
export function inviteToMultiChat(data) {
return request({
url: '/api/v1/multichat/invite',
url: '/multichat/invite',
method: 'post',
data
})
}
// 移除群聊成员-退出群聊
export function quitMultiChat(data) {
return request({
url: '/api/v1/multichat/quit',
url: '/multichat/quit',
method: 'post',
data
})
}
// 解散群聊
export function dismissMultiChat(data) {
return request({
url: '/api/v1/multichat/dismiss',
url: '/multichat/dismiss',
method: 'post',
data
})
@ -67,7 +67,7 @@ export function dismissMultiChat(data) {
export function pullMultiChatMessages(params) {
return request({
url: '/api/v1/multichat/message/pull',
url: '/multichat/message/pull',
method: 'get',
params
})

@ -1,13 +0,0 @@
import request from '@/utils/request'
export function getSystemNotify(data) {
return request({ url: '/api/v1/notify/pull', method: 'post', data })
}
export function pushSystemNotify(data) {
return request({ url: '/api/v1/notify/push', method: 'post', data })
}
export function readSystemNotify(data) {
return request({ url: '/api/v1/notify/read', method: 'post', data })
}

@ -1,4 +1,40 @@
import request from '@/utils/request'
export const meetingModes = () => {
return [
{
title: "实时会诊",
icon: "el-icon-user-solid",
color: "blue",
routeName: "RealTimeConsultation",
routePath: "/videoCommunication/realTimeConsultation",
type: 1,
},
{
title: "带教培训",
icon: "el-icon-video-play",
color: "yellow",
routeName: "Training",
routePath: "/videoCommunication/training",
type: 9,
},
{
title: "在线质控",
icon: "el-icon-lightbulb",
color: "grayblue",
routeName: "QualityControl",
routePath: "/videoCommunication/qualityControl",
type: 6,
},
{
title: "病例研讨",
icon: "el-icon-chat-line-square",
color: "greenblue",
routeName: "CaseStudy",
routePath: "/videoCommunication/caseStudy",
type: 3,
},
]
}
// 视讯-创建房间
export function postConsultationCreate(data) {
return request({

@ -103,7 +103,8 @@ export const MessageFileType = {
export const ContactsScene = {
ALL: 0,
PRIVATE: 1,
GROUP: 4
GROUP: 4,
NOTIFY: 6
}
// MQTT 主题

@ -45,7 +45,10 @@
>
<div class="member-item-label">
<el-avatar
:src="member.avatar"
:src="
$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS +
member.avatar
"
icon="el-icon-user-solid"
/>
<span class="member-name">{{ member.name }}</span>
@ -70,7 +73,13 @@
:class="{ expanded: group.expanded }"
@click="toggleGroupExpand(group)"
>
<el-avatar :size="32" :src="group.avatar" />
<el-avatar
:size="32"
:src="
$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS +
group.avatar
"
/>
<span class="group-name">{{ group.name }}</span>
<i class="el-icon-arrow-right expand-icon" />
</div>
@ -86,7 +95,7 @@
v-model="group.allSelected"
@change="toggleGroupAllSelect(group)"
/>
<span class="member-name">全选</span>
<span class="member-name"> 全选 </span>
</div>
<!-- 成员列表 -->
<div
@ -107,7 +116,13 @@
@change="toggleSelect(member)"
>
<div class="member-item-label">
<el-avatar :size="32" :src="member.avatar" />
<el-avatar
:size="32"
:src="
$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS +
member.avatar
"
/>
<span class="member-name">{{ member.name }}</span>
</div>
</el-checkbox>
@ -206,7 +221,13 @@
:key="member.id"
class="selected-item"
>
<el-avatar :size="40" :src="member.avatar" />
<el-avatar
:size="40"
:src="
$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS +
member.avatar
"
/>
<span class="selected-name">{{ member.name }}</span>
<span class="remove-btn" @click="removeSelected(member)">
<i class="el-icon-close" />
@ -250,6 +271,7 @@ import {
getGroupsListUser,
searchUsers,
} from "@/api/contacts/index.js";
import { getMultiChatInfo } from "@/api/multichat";
export default {
name: "CreateGroupDialog",
@ -264,7 +286,7 @@ export default {
},
maxSelectCount: {
type: Number,
default: 1,
default: 500,
},
confirmCallback: {
type: Function,
@ -329,7 +351,7 @@ export default {
full_group: item.full_group,
avatar: item.avatar,
online: item.online === 1 || item.online === true,
selected: false,
selected: this.isSelected(item.id),
}));
} catch (e) {
console.error("获取最近联系人失败", e);
@ -455,7 +477,7 @@ export default {
full_group: item.full_group,
avatar: item.avatar,
online: item.online === 1 || item.online === true,
selected: false,
selected: this.isSelected(item.id),
}));
} catch (e) {
console.error("搜索联系人失败", e);
@ -490,7 +512,7 @@ export default {
name: item.name,
avatar: item.avatar,
online: item.online === 1 || item.online === true,
selected: false,
selected: this.isSelected(item.id),
}));
} catch (e) {
console.error(`获取群组 ${group.name} 成员失败`, e);
@ -504,21 +526,20 @@ export default {
async toggleGroupExpand(group) {
group.expanded = !group.expanded;
if (group.expanded) {
if (!group.members || group.members.length === 0) {
try {
const res = await getGroupsListUser({ group_id: group.id });
const list = res.data?.list || [];
group.members = list.map((item) => ({
id: item.id,
name: item.name,
avatar: item.avatar,
online: item.online === 1 || item.online === true,
selected: this.isSelected(item.id),
}));
} catch (e) {
console.error(`获取群组 ${group.name} 成员失败`, e);
group.members = [];
}
// getMultiChatInfo
try {
const res = await getMultiChatInfo({ multi_chat_id: group.id });
const data = res.data || {};
// 使 user_id
const userList = data.user_list || [];
// ID
group.members = userList
.filter((item) => item.id !== data.user_id)
.map((item) => {
return { ...item, selected: this.isSelected(item.id) };
});
} catch (e) {
group.members = [];
}
group.allSelected = group.members.every((m) => m.selected);
}
@ -967,9 +988,38 @@ export default {
align-items: center;
gap: 10px;
padding: 8px 16px;
cursor: pointer;
transition: background 0.2s;
position: relative;
}
//
.group-detail-info {
padding: 12px 16px;
margin: 8px 0;
background: #f5f7fa;
border-radius: 6px;
border-left: 3px solid #409eff;
.group-detail-row {
display: flex;
align-items: flex-start;
margin-bottom: 8px;
&:last-child {
margin-bottom: 0;
}
.group-detail-label {
color: #909399;
font-size: 13px;
flex-shrink: 0;
}
.group-detail-value {
color: #606266;
font-size: 13px;
word-break: break-all;
line-height: 1.5;
}
}
}
}
</style>

@ -9,11 +9,16 @@
<div class="group-setting">
<!-- 群头像和名称 -->
<div class="group-info">
<el-avatar :size="60" :src="groupInfo.avatar" class="group-avatar">
<img v-if="groupInfo.avatar" :src="groupInfo.avatar" />
<span v-else class="avatar-text">{{
(groupInfo.name || "群聊").slice(0, 2)
}}</span>
<el-avatar
:size="40"
:src="
$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS +
groupInfo.avatar
"
class="group-avatar"
:icon="'el-icon-s-custom'"
>
<span class="avatar-text"> {{ groupInfo.name }}</span>
</el-avatar>
<div class="group-detail">
<div class="group-name">{{ groupInfo.name }}</div>
@ -26,21 +31,36 @@
/>
</div>
<div class="section">
<div class="section-title">
群成员 {{ (groupInfo.members && groupInfo.members.length) || 0 }}
</div>
<div class="section-title">群成员 {{ groupInfo.user_num }}</div>
<div class="member-list">
<div class="member-item" @click="showAddMember">
<el-icon
class="el-icon-circle-plus-outline"
style="font-size: 40px"
/>
<span class="member-name">添加成员</span>
</div>
<div
v-for="member in groupInfo.members"
v-for="member in groupInfo.user_list"
:key="member.id"
class="member-item"
@mouseenter="showDeleteBtn = member.id"
@mouseleave="showDeleteBtn = null"
>
<div class="avatar-wrapper">
<el-avatar :size="40" :src="member.avatar" />
<el-avatar
:size="40"
:src="
$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS +
member.avatar
"
/>
<span
v-if="showDeleteBtn === member.id && isAdmin"
v-if="
userInfo.id === groupInfo.user_id &&
userInfo.id !== member.id &&
showDeleteBtn === member.id
"
class="delete-btn"
@click.stop="handleRemoveMember(member)"
>
@ -48,145 +68,131 @@
</span>
</div>
<span class="member-name">{{ member.name }}</span>
<span v-if="member.isAdmin" class="admin-tag">管理员</span>
</div>
<div class="member-item" @click="showAddMember">
<el-icon
class="el-icon-circle-plus-outline"
style="font-size: 40px"
/>
<span class="member-name">添加成员</span>
</div>
</div>
</div>
<!-- 群管理员 -->
<div v-if="groupInfo.members.some((m) => m.isAdmin)" class="section">
<div
v-if="groupInfo.user_list.some((m) => m.id === groupInfo.user_id)"
class="section"
>
<div class="section-title">群管理员</div>
<div class="member-list">
<div
v-for="member in groupInfo.members.filter((m) => m.isAdmin)"
v-for="member in groupInfo.user_list.filter(
(m) => m.id === groupInfo.user_id
)"
:key="member.id"
class="member-item"
>
<el-avatar :size="40" :src="member.avatar" />
<el-avatar
:size="40"
:src="
$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS +
member.avatar
"
/>
<span class="member-name">{{ member.name }}</span>
</div>
</div>
</div>
<!-- 查找聊天记录 -->
<div class="section">
<div class="search-record-btn" @click="showSearchRecord">
<i class="el-icon-search" />
<span class="btn-text">查找聊天记录</span>
<span class="btn-desc">图片视频文件等</span>
<i class="el-icon-arrow-right" />
</div>
</div>
<!-- 操作按钮 -->
<div class="actions">
<el-button type="danger" @click="handleQuitGroup" v-if="!isAdmin">
<el-button type="danger" @click="handleQuitGroup">
退出群组
</el-button>
<template v-else>
<el-button type="danger" @click="handleQuitGroup"
>退出群组</el-button
>
<el-button type="danger" plain @click="handleDismissGroup">
解散群组</el-button
>
</template>
<el-button
type="danger"
plain
@click="handleDismissGroup"
v-show="userInfo.id === groupInfo.user_id"
>
解散群组
</el-button>
</div>
</div>
</el-dialog>
<!-- 查找聊天记录弹窗 -->
<SearchRecord
:visible.sync="isSearchVisible"
:chat="chat"
@locate="handleLocateMessage"
<!-- 添加成员弹窗 -->
<CreateGroupDialog
ref="addMemberDialog"
title="添加成员"
:minSelectCount="1"
@confirm="handleAddMembers"
/>
</div>
</template>
<script>
import SearchRecord from "./SearchRecord.vue";
import {
getMultiChatInfo,
inviteToMultiChat,
editMultiChatName,
quitMultiChat,
dismissMultiChat,
} from "@/api/multichat";
import CreateGroupDialog from "./CreateGroupDialog.vue";
export default {
name: "GroupSetting",
components: {
SearchRecord,
CreateGroupDialog,
},
props: {
visible: Boolean,
chat: Object,
},
data() {
return {
visible: false,
isSearchVisible: false,
groupInfo: {
name: "",
avatar: "",
members: [],
},
groupInfo: {},
isAdmin: false,
showDeleteBtn: null,
currentUserId: this.$store.state.user.userInfo?.id,
};
},
watch: {
visible(val) {
if (val && this.chat) {
this.loadGroupInfo();
}
computed: {
userInfo() {
return this.$store.state.user.userInfo;
},
},
methods: {
loadGroupInfo() {
// Mock
const mockGroups = {
group1: {
name: "基层医生、会诊专家、郭君",
avatar: "",
members: [
{ id: "user1", name: "会诊专家", avatar: "", isAdmin: true },
{ id: "user2", name: "基层医生", avatar: "", isAdmin: true },
{ id: "user3", name: "郭君", avatar: "", isAdmin: false },
],
},
group2: {
name: "会诊专家、基层医生",
avatar: "",
members: [
{ id: "user1", name: "会诊专家", avatar: "", isAdmin: true },
{ id: "user2", name: "基层医生", avatar: "", isAdmin: false },
],
},
};
this.groupInfo = mockGroups[this.chat.id] || {
name: this.chat.name,
avatar: this.chat.avatar,
members: [],
};
//
this.isAdmin = this.groupInfo.members[0].isAdmin || false;
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() {
let multi_chat_id = this.chat.id;
this.$prompt("请输入新的群名称", "修改群名称", {
confirmButtonText: "确定",
cancelButtonText: "取消",
inputValue: this.groupInfo.name,
})
.then(({ value }) => {
this.groupInfo.name = value;
this.$message.success("群名称修改成功");
editMultiChatName({
multi_chat_id: multi_chat_id,
name: value,
}).then(() => {
this.groupInfo.name = value;
this.$message.success("群名称修改成功");
//
this.$emit("refresh-list");
});
})
.catch(() => {
this.$message.info("已取消修改");
@ -202,26 +208,45 @@ export default {
},
showAddMember() {
this.$message.info("添加成员功能");
this.$refs.addMemberDialog.show();
},
async handleAddMembers(selectedMembers) {
await inviteToMultiChat({
multi_chat_id: this.chat.id,
invite_users: selectedMembers.map((m) => m.id),
});
//
await this.loadGroupInfo();
//
this.$emit("refresh-list");
this.$message.success("添加成员成功");
},
handleRemoveMember(member) {
this.$confirm(`确定要移除 "${member.name}" 吗?`, "提示", {
this.$confirm(`确定要将该成员踢出群聊吗?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
this.groupInfo.members = this.groupInfo.members.filter(
(m) => m.id !== member.id
);
this.$message.success("移除成功");
quitMultiChat({
//
invite_type_detail: 9,
// ID
multi_chat_id: this.chat.id,
// ID
user_id: member.id,
}).then(() => {
this.$message.success("移除成功");
//
this.loadGroupInfo();
//
this.$emit("refresh-list");
});
})
.catch(() => {
this.$message.info("已取消移除");
});
},
handleQuitGroup() {
this.$confirm("确定要退出该群组吗?", "提示", {
confirmButtonText: "确定",
@ -229,25 +254,40 @@ export default {
type: "warning",
})
.then(() => {
this.$message.success("退出成功");
this.$emit("quit-group", this.chat);
this.$emit("update:visible", false);
quitMultiChat({
// 退
invite_type_detail: 8,
// // ID
multi_chat_id: this.chat.id,
// ID
user_id: this.currentUserId,
}).then(() => {
this.$message.success("退出成功");
//
this.visible = false;
this.$emit("quit-group", this.chat);
});
})
.catch(() => {
this.$message.info("已取消退出");
});
},
handleDismissGroup() {
this.$confirm("确定要解散该群组吗?此操作不可撤销!", "提示", {
this.$confirm("确定要解散群组吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "error",
})
.then(() => {
this.$message.success("解散成功");
this.$emit("dismiss-group", this.chat);
this.$emit("update:visible", false);
dismissMultiChat({
// ID
multi_chat_id: this.chat.id,
}).then(() => {
this.$message.success("解散成功");
//
this.visible = false;
this.$emit("dismiss-group", this.chat);
});
})
.catch(() => {
this.$message.info("已取消解散");
@ -258,16 +298,9 @@ export default {
</script>
<style lang="scss" scoped>
.group-setting {
padding: 20px 0;
}
.group-info {
display: flex;
align-items: center;
padding: 15px;
background: #f5f7fa;
border-radius: 8px;
margin-bottom: 20px;
.group-avatar {
@ -282,13 +315,6 @@ export default {
.group-detail {
flex: 1;
.group-name {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.group-member-count {
font-size: 12px;
color: #909399;
@ -301,8 +327,6 @@ export default {
}
.section {
margin-bottom: 20px;
.section-title {
font-size: 14px;
font-weight: 600;
@ -313,14 +337,13 @@ export default {
.member-list {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.member-item {
display: flex;
flex-direction: column;
align-items: center;
width: calc((100% - 30px) / 3);
width: calc(100% / 5);
.avatar-wrapper {
position: relative;
@ -353,6 +376,7 @@ export default {
.member-name {
margin-top: 6px;
margin-bottom: 6px;
font-size: 12px;
color: #606266;
text-align: center;

File diff suppressed because it is too large Load Diff

@ -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>

@ -1,206 +1,289 @@
<template>
<el-dialog
title="查找聊天记录"
title="聊天记录"
:visible.sync="visible"
width="40%"
append-to-body
:close-on-click-modal="true"
destroy-on-close
:before-close="() => (visible = false)"
>
<div class="search-record">
<!-- 搜索框 -->
<div class="search-header">
<el-input
v-model="searchKeyword"
placeholder="搜索"
class="search-input"
@input="handleSearch"
prefix-icon="el-icon-search"
/>
</div>
<!-- 分类标签 -->
<div class="category-tabs">
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
<el-tab-pane label="综合" name="all" />
<el-tab-pane label="文件" name="file" />
<el-tab-pane label="图片与视频" name="media" />
<el-tab-pane label="链接" name="link" />
<el-tab-pane label="日期" name="date" />
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="图片" name="image" />
<el-tab-pane label="聊天记录" name="chat" />
</el-tabs>
</div>
<!-- 搜索结果列表 -->
<div class="result-list">
<div
v-for="(result, index) in filteredResults"
:key="result.message_id || result.id || index"
class="result-item"
@click="handleLocate(result)"
>
<el-avatar :size="40" :src="result.avatar" class="result-avatar">
<span v-if="!result.avatar">{{ result.sender_name.slice(0, 1) || '?' }}</span>
</el-avatar>
<div class="result-content">
<div class="result-name">{{ result.sender_name }}</div>
<div class="result-text">{{ result.displayText }}</div>
<!-- 列表容器 -->
<div class="result-list" ref="scrollWrap" @scroll="handleScroll">
<!-- 图片布局一行4张 -->
<div v-if="activeTab === 'image'" class="image-grid">
<div v-for="(item, index) in list" :key="index" class="image-item">
<el-image
class="img-box"
:src="getImgUrl(item.file_path)"
:alt="item.file_name"
:preview-src-list="[getImgUrl(item.file_path)]"
/>
</div>
</div>
<!-- 聊天记录布局 -->
<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="result-right">
<div class="result-time">{{ result.displayTime }}</div>
<span v-if="result.type === 'link'" class="result-tag">链接</span>
<span v-if="result.type === 'file'" class="result-tag">文件</span>
<span v-if="result.type === 'media'" class="result-tag">图片</span>
<span v-if="result.type === 'audio'" class="result-tag">语音</span>
<div class="chat-list">
<!-- 根据 identity 类型匹配用户信息 -->
<div v-for="(item, index) in list" :key="item.id || index" class="result-item">
<el-avatar
:size="40"
:src="getImgUrl(item.sender_avatar)"
class="result-avatar"
>
{{ 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>
<div class="result-right">
<div class="result-time">{{ item.create_time || item.update_time || '' }}</div>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="filteredResults.length === 0" class="empty-state">
<div v-if="list.length === 0 && !loadingMore" class="empty-state">
<i class="el-icon-search" />
<span>未找到相关消息</span>
</div>
<!-- 加载更多提示 -->
<div v-if="loadingMore" class="loading-tip">
<i class="el-icon-loading"></i> 加载中...
</div>
<div v-if="isNoMore && list.length > 0" class="no-more-tip">
没有更多了
</div>
</div>
</div>
</el-dialog>
</template>
<script>
import { MessageType } from "@/utils/constants";
import { getMultiChatInfo } from "@/api/multichat";
import { postMessagesFileList, postMessagesSearch } from "@/api/message";
import { mapGetters } from "vuex";
export default {
name: "SearchRecord",
props: {
visible: Boolean,
chat: Object,
messages: Array,
},
data() {
return {
visible: false,
searchKeyword: "",
activeTab: "all",
activeTab: "image",
query: {},
list: [],
page: 1,
size: 60,
loadingMore: false,
isNoMore: false,
//
groupMembers: {}, // key: groupId, value: { userId: memberInfo }
};
},
computed: {
searchResults() {
if (!this.messages || this.messages.length === 0) return [];
return this.messages.map((msg) => {
let displayText = "";
let type = "text";
switch (msg.type) {
case MessageType.TEXT:
displayText = msg.payload;
type = this.isUrl(msg.payload) ? "link" : "text";
break;
case MessageType.IMAGE:
displayText = "[图片]";
type = "media";
break;
case MessageType.VIDEO:
displayText = "[视频]";
type = "media";
break;
case MessageType.AUDIO:
displayText = "[语音消息]";
type = "audio";
break;
case MessageType.FILE:
displayText = `[文件] ${msg.payload.file_name || "附件"}`;
type = "file";
break;
case MessageType.REPORT_SHARE:
displayText = `[报告] ${msg.payload.title || "报告分享"}`;
type = "file";
break;
case MessageType.KNOWLEDGE_SHARE:
displayText = `[知识] ${msg.payload.title || "知识分享"}`;
type = "file";
break;
case MessageType.CONSULTATION_MESSAGE_INVITE:
displayText = `[会诊邀请] ${msg.payload.title || "会诊邀请"}`;
type = "text";
break;
case MessageType.NOTIFY_CONTENT:
displayText = `[系统通知] ${msg.payload.description || msg.payload.title || "系统通知"}`;
type = "text";
break;
default:
displayText = String(msg.payload || "[未知消息]");
type = "text";
}
return {
...msg,
displayText,
type,
displayTime: this.formatTime(msg.timestamp || msg.create_time),
avatar: "",
sender_name: msg.sender_name || (msg.is_self ? "我" : "未知"),
};
...mapGetters(["userInfo", "latestContacts"]),
},
methods: {
//
show(chat) {
this.query = { ...chat };
this.visible = true;
this.resetPage();
this.searchKeyword = "";
this.groupMembers = {};
this.$nextTick(() => {
this.getListData();
});
},
filteredResults() {
let results = this.searchResults;
//
if (this.activeTab !== "all") {
if (this.activeTab === "file") {
results = results.filter((r) => r.type === "file");
} else if (this.activeTab === "media") {
results = results.filter((r) => r.type === "media");
} else if (this.activeTab === "link") {
results = results.filter((r) => r.type === "link");
} else if (this.activeTab === "date") {
//
//
resetPage() {
this.page = 1;
this.isNoMore = false;
this.loadingMore = false;
this.list = [];
if (this.$refs.scrollWrap) {
this.$refs.scrollWrap.scrollTop = 0;
}
},
//
handleTabClick() {
this.resetPage();
this.getListData();
},
//
handleSearch() {
this.resetPage();
this.getListData();
},
//
async getListData() {
if (this.loadingMore || this.isNoMore) return;
this.loadingMore = true;
let request;
if (this.activeTab === "image") {
request = postMessagesFileList({
file_types: ["image"],
page: this.page,
scene: this.query.scene,
size: this.size,
target_id: this.query.id,
type: "image",
});
} else {
request = postMessagesSearch({
datetime_end: "",
datetime_start: "",
file_types: [],
is_at_self: 0,
page: this.page,
scene: this.query.scene,
search_key: this.searchKeyword,
size: this.size,
source_id: 0,
target_id: this.query.id,
type: "text",
});
}
try {
const res = await request;
const data = res.data.list || [];
//
const processedData = await this.processMessagesWithUserInfo(data);
this.list = this.page === 1 ? processedData : [...this.list, ...processedData];
if (data.length < this.size) {
this.isNoMore = true;
}
} catch (e) {
this.$message.error("数据加载失败");
} finally {
this.loadingMore = false;
}
},
//
if (this.searchKeyword) {
const keyword = this.searchKeyword.toLowerCase();
results = results.filter(
(r) =>
(r.sender_name && r.sender_name.toLowerCase().includes(keyword)) ||
(r.displayText && r.displayText.toLowerCase().includes(keyword))
);
//
async processMessagesWithUserInfo(messages) {
for (const msg of messages) {
const userInfo = await this.searchChat(msg);
msg.sender_avatar = userInfo.avatar;
msg.sender_name = userInfo.name;
}
return messages;
},
//
return results.sort((a, b) => {
const timeA = new Date(a.timestamp || a.create_time).getTime();
const timeB = new Date(b.timestamp || b.create_time).getTime();
return timeB - timeA;
});
//
async searchChat(msg) {
const identity = msg.identity || "";
const sourceId = msg.source_id;
const targetId = msg.target_id;
// identity "multi_chat"
if (identity.includes("multi_chat")) {
return await this.getGroupMemberInfo(targetId, sourceId);
}
// identity "chat"
return this.getPrivateChatUserInfo(sourceId);
},
},
methods: {
isUrl(text) {
if (!text) return false;
const urlPattern = /https?:\/\/[^\s]+/;
return urlPattern.test(text);
//
async getGroupMemberInfo(groupId, userId) {
//
if (!this.groupMembers[groupId]) {
try {
const res = await getMultiChatInfo({ multi_chat_id: groupId });
const members = res.data.user_list || [];
const memberMap = {};
members.forEach((member) => {
memberMap[member.id] = {
avatar: member.avatar,
name: member.name,
};
});
this.groupMembers[groupId] = memberMap;
} catch (e) {
console.error("获取群成员列表失败", e);
this.groupMembers[groupId] = {};
}
}
const member = this.groupMembers[groupId][userId];
return member || { avatar: "", name: "未知成员" };
},
formatTime(timestamp) {
if (!timestamp) return "";
const date = new Date(timestamp);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${year}${month}${day}${hours}:${minutes}`;
//
getPrivateChatUserInfo(sourceId) {
const currentUserId = this.userInfo?.id;
//
if (sourceId === currentUserId) {
return {
avatar: this.userInfo?.avatar || "",
name: this.userInfo?.name || "我",
};
}
//
const contact = this.latestContacts.find((c) => c.id === sourceId);
if (contact) {
return {
avatar: contact.avatar || "",
name: contact.name || "未知用户",
};
}
return { avatar: "", name: "未知用户" };
},
handleSearch() {
// computed
//
handleScroll() {
const wrap = this.$refs.scrollWrap;
if (!wrap || this.loadingMore || this.isNoMore) return;
const { scrollTop, scrollHeight, clientHeight } = wrap;
if (scrollTop + clientHeight >= scrollHeight - 20) {
this.page++;
this.getListData();
}
},
handleTabChange() {
// computed
//
getImgUrl(path) {
if (!path) return "";
return this.$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS + path;
},
handleLocate(result) {
this.$emit("locate", result);
this.$emit("update:visible", false);
//
getFirstChar(name) {
return name ? name.slice(0, 1) : "?";
},
},
};
@ -233,61 +316,81 @@ export default {
padding: 10px 0;
}
.result-item {
display: flex;
align-items: center;
padding: 12px 15px;
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background-color: #f5f7fa;
}
}
.result-avatar {
margin-right: 12px;
}
.result-content {
flex: 1;
min-width: 0;
/* 图片网格 一行4张 */
.image-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
padding: 0 10px;
}
.result-name {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 4px;
}
.result-text {
font-size: 13px;
color: #606266;
.image-item {
border-radius: 6px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
background: #f5f5f5;
aspect-ratio: 1 / 1;
}
.result-right {
text-align: right;
margin-left: 15px;
.img-box {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.2s;
}
.result-time {
font-size: 12px;
color: #909399;
margin-bottom: 4px;
.image-item:hover .img-box {
transform: scale(1.05);
}
.result-tag {
font-size: 11px;
padding: 2px 6px;
border-radius: 4px;
background-color: #ecf5ff;
color: #409eff;
/* 聊天列表样式 */
.chat-list {
.result-item {
display: flex;
align-items: center;
padding: 12px 15px;
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background-color: #f5f7fa;
}
}
.result-avatar {
margin-right: 12px;
}
.result-content {
flex: 1;
min-width: 0;
}
.result-name {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 4px;
}
.result-text {
font-size: 13px;
color: #606266;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.result-right {
text-align: right;
margin-left: 15px;
}
.result-time {
font-size: 12px;
color: #909399;
margin-bottom: 4px;
}
.result-tag {
font-size: 11px;
padding: 2px 6px;
border-radius: 4px;
background-color: #ecf5ff;
color: #409eff;
}
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
@ -295,10 +398,18 @@ export default {
justify-content: center;
padding: 40px 0;
color: #909399;
i {
font-size: 48px;
margin-bottom: 16px;
}
}
/* 加载/无更多 */
.loading-tip,
.no-more-tip {
text-align: center;
padding: 10px 0;
font-size: 12px;
color: #999;
}
</style>

@ -1,11 +1,8 @@
<template>
<div class="message-main">
<MessageList
ref="messageList"
@select-contact="handleSelectContact"
/>
<MessageList ref="messageList" @select-contact="handleSelectContact" />
<div class="message-right">
<MessageDisplay
<MessageDisplay
ref="messageDisplay"
@clear-unread="handleClearUnread"
@send-files="handleSendFiles"
@ -21,60 +18,61 @@
<!-- 群设置弹窗 -->
<GroupSetting
:visible.sync="groupSettingVisible"
ref="groupSettingRef"
:chat="currentChat"
@quit-group="handleQuitGroup"
@dismiss-group="handleDismissGroup"
@refresh-list="handleRefreshList"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
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 } from '@/api/multichat'
import { MessageType, ContactsScene, MqttTopics } from '@/utils/constants'
import { mapGetters, mapActions } from "vuex";
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 { MessageType, ContactsScene, MqttTopics } from "@/utils/constants";
export default {
name: 'MessageMain',
name: "MessageMain",
components: {
MessageList,
MessageDisplay,
GroupSetting,
MessageEditor
MessageEditor,
},
data() {
return {
groupSettingVisible: false
}
};
},
computed: {
...mapGetters(['mqttMessages', 'currentChat', 'userInfo', 'mqttConnected'])
...mapGetters(["mqttMessages", "currentChat", "userInfo", "mqttConnected"]),
},
watch: {
mqttMessages: {
deep: true,
handler(newMessages, oldMessages) {
const oldLen = oldMessages ? oldMessages.length : 0
const newLen = newMessages ? newMessages.length : 0
const oldLen = oldMessages ? oldMessages.length : 0;
const newLen = newMessages ? newMessages.length : 0;
if (newLen > oldLen) {
for (let i = oldLen; i < newLen; i++) {
this.handleMqttMessage(newMessages[i])
this.handleMqttMessage(newMessages[i]);
}
}
}
}
},
},
},
mounted() {
this.initMqttSubscriptions()
this.initMqttSubscriptions();
this.handleUrlParams();
},
beforeDestroy() {
@ -82,21 +80,46 @@ export default {
},
methods: {
...mapActions('mqtt', ['subscribe', 'unsubscribe']),
...mapActions("mqtt", ["subscribe", "unsubscribe"]),
...mapActions("message", ["setCurrentChat"]),
// URL
handleUrlParams() {
const { contactId, contactName, scene } = this.$route.query;
if (contactId && scene) {
const chat = {
id: contactId,
name: contactName || "未知联系人",
scene: parseInt(scene),
avatar: "",
unread_count: 0,
last_message: "",
last_message_time: new Date().toISOString(),
};
//
this.setCurrentChat(chat);
// MQTT
if (parseInt(scene) === ContactsScene.GROUP) {
this.subscribe(MqttTopics.MULTI_CHAT + contactId);
}
// URL
this.$router.replace({ query: {} });
}
},
async initMqttSubscriptions() {
//
if (this.userInfo?.id) {
await this.subscribe(MqttTopics.PRIVATE_CHAT + this.userInfo.id)
await this.subscribe(MqttTopics.PRIVATE_CHAT + this.userInfo.id);
}
},
handleMqttMessage(msg) {
const type = msg.type || msg.message_type
const type = msg.type || msg.message_type;
switch (type) {
case MessageType.READ:
this.handleReadMessage(msg)
break
this.handleReadMessage(msg);
break;
case MessageType.TEXT:
case MessageType.IMAGE:
case MessageType.VIDEO:
@ -105,54 +128,62 @@ export default {
case MessageType.REPORT_SHARE:
case MessageType.KNOWLEDGE_SHARE:
case MessageType.CONSULTATION_MESSAGE_INVITE:
this.handleBaseMessage(msg)
break
this.handleBaseMessage(msg);
break;
case MessageType.MULTI_CHAT_INVITE:
this.handleMultiChatInvite(msg)
break
this.handleMultiChatInvite(msg);
break;
case MessageType.MULTI_CHAT_JOIN:
this.handleMultiChatJoin(msg)
break
this.handleMultiChatJoin(msg);
break;
case MessageType.MULTI_CHAT_EDIT:
this.handleMultiChatEdit(msg)
break
this.handleMultiChatEdit(msg);
break;
case MessageType.MULTI_CHAT_QUIT:
this.handleMultiChatQuit(msg)
break
this.handleMultiChatQuit(msg);
break;
case MessageType.MULTI_CHAT_DISMISS:
this.handleMultiChatDismiss(msg)
break
this.handleMultiChatDismiss(msg);
break;
case MessageType.NOTIFY_CONTENT:
this.handleNotifyContent(msg)
break
this.handleNotifyContent(msg);
break;
default:
console.log('未处理的MQTT消息类型:', type, msg)
console.log("未处理的MQTT消息类型:", type, msg);
}
},
handleReadMessage(msg) {
//
if (this.$refs.messageDisplay) {
this.$refs.messageDisplay.updateMessageStatus(msg.message_id, { read: true, read_status: 1 })
this.$refs.messageDisplay.updateMessageStatus(msg.message_id, {
read: true,
read_status: 1,
});
}
},
handleBaseMessage(msg) {
const isSelf = msg.source_id === this.userInfo?.id
const scene = msg.scene || (msg.multi_chat_id ? ContactsScene.GROUP : ContactsScene.PRIVATE)
const targetId = msg.target_id || msg.source_id
const contactId = isSelf ? targetId : msg.source_id
const isSelf = msg.source_id === this.userInfo?.id;
const scene =
msg.scene ||
(msg.multi_chat_id ? ContactsScene.GROUP : ContactsScene.PRIVATE);
const targetId = msg.target_id || msg.source_id;
const contactId = isSelf ? targetId : msg.source_id;
//
if (this.currentChat &&
this.currentChat.id === (scene === ContactsScene.GROUP ? msg.multi_chat_id : contactId) &&
this.currentChat.scene === scene) {
if (
this.currentChat &&
this.currentChat.id ===
(scene === ContactsScene.GROUP ? msg.multi_chat_id : contactId) &&
this.currentChat.scene === scene
) {
if (this.$refs.messageDisplay) {
this.$refs.messageDisplay.addChatMessage(msg)
this.$refs.messageDisplay.addChatMessage(msg);
}
//
if (!isSelf) {
this.handleClearUnread(this.currentChat.id, this.currentChat.scene)
this.handleClearUnread(this.currentChat.id, this.currentChat.scene);
}
}
@ -163,112 +194,126 @@ export default {
scene,
msg,
msg.timestamp || new Date().toISOString()
)
);
}
},
async handleMultiChatInvite(msg) {
const payload = msg.payload || {}
const multiChatId = payload.multi_chat_id || msg.multi_chat_id
if (!multiChatId) return
const payload = msg.payload || {};
const multiChatId = payload.multi_chat_id || msg.multi_chat_id;
if (!multiChatId) return;
//
await this.subscribe(MqttTopics.MULTI_CHAT + multiChatId)
await this.subscribe(MqttTopics.MULTI_CHAT + multiChatId);
//
try {
const res = await getMultiChatInfo({ multi_chat_id: multiChatId })
const group = res.data
const res = await getMultiChatInfo({ multi_chat_id: multiChatId });
const group = res.data;
if (group && this.$refs.messageList) {
this.$refs.messageList.loadContacts()
this.$refs.messageList.loadContacts();
}
} catch (e) {
console.error('获取群详情失败', e)
console.error("获取群详情失败", e);
}
},
async handleMultiChatJoin(msg) {
const multiChatId = msg.multi_chat_id || msg.payload?.multi_chat_id
if (!multiChatId) return
const multiChatId = msg.multi_chat_id || msg.payload?.multi_chat_id;
if (!multiChatId) return;
try {
await getMultiChatInfo({ multi_chat_id: multiChatId })
await getMultiChatInfo({ multi_chat_id: multiChatId });
if (this.$refs.messageList) {
this.$refs.messageList.loadContacts()
this.$refs.messageList.loadContacts();
}
} catch (e) {
console.error('获取群详情失败', e)
console.error("获取群详情失败", e);
}
},
async handleMultiChatEdit(msg) {
const multiChatId = msg.multi_chat_id || msg.payload?.multi_chat_id
if (!multiChatId) return
const multiChatId = msg.multi_chat_id || msg.payload?.multi_chat_id;
if (!multiChatId) return;
try {
await getMultiChatInfo({ multi_chat_id: multiChatId })
await getMultiChatInfo({ multi_chat_id: multiChatId });
if (this.$refs.messageList) {
this.$refs.messageList.loadContacts()
this.$refs.messageList.loadContacts();
}
//
if (this.currentChat && this.currentChat.id === multiChatId && this.currentChat.scene === ContactsScene.GROUP) {
this.$refs.messageList.loadContacts()
if (
this.currentChat &&
this.currentChat.id === multiChatId &&
this.currentChat.scene === ContactsScene.GROUP
) {
this.$refs.messageList.loadContacts();
}
} catch (e) {
console.error('获取群详情失败', e)
console.error("获取群详情失败", e);
}
},
handleMultiChatQuit(msg) {
const payload = msg.payload || {}
const multiChatId = payload.multi_chat_id || msg.multi_chat_id
const quitUserId = payload.user_id || msg.source_id
if (!multiChatId) return
const payload = msg.payload || {};
const multiChatId = payload.multi_chat_id || msg.multi_chat_id;
const quitUserId = payload.user_id || msg.source_id;
if (!multiChatId) return;
if (quitUserId === this.userInfo?.id) {
// 退
this.unsubscribe(MqttTopics.MULTI_CHAT + multiChatId)
this.unsubscribe(MqttTopics.MULTI_CHAT + multiChatId);
if (this.$refs.messageList) {
this.$refs.messageList.removeContact(multiChatId, ContactsScene.GROUP)
this.$refs.messageList.removeContact(
multiChatId,
ContactsScene.GROUP
);
}
} else {
// 退
if (this.$refs.messageList) {
this.$refs.messageList.loadContacts()
this.$refs.messageList.loadContacts();
}
}
},
handleMultiChatDismiss(msg) {
const multiChatId = msg.multi_chat_id || msg.payload?.multi_chat_id
if (!multiChatId) return
this.unsubscribe(MqttTopics.MULTI_CHAT + multiChatId)
const multiChatId = msg.multi_chat_id || msg.payload?.multi_chat_id;
if (!multiChatId) return;
this.unsubscribe(MqttTopics.MULTI_CHAT + multiChatId);
if (this.$refs.messageList) {
this.$refs.messageList.removeContact(multiChatId, ContactsScene.GROUP);
}
},
//
handleRefreshList() {
if (this.$refs.messageList) {
this.$refs.messageList.removeContact(multiChatId, ContactsScene.GROUP)
this.$refs.messageList.refreshList({ keepCurrent: true });
}
},
handleNotifyContent(msg) {
//
this.$notify({
title: '系统通知',
message: msg.payload?.content || msg.content || '新通知',
type: 'info',
duration: 5000
})
title: "系统通知",
message: msg.payload?.content || msg.content || "新通知",
type: "info",
duration: 5000,
});
},
handleSelectContact(contact) {
// MQTT
if (contact.scene === ContactsScene.GROUP) {
this.subscribe(MqttTopics.MULTI_CHAT + contact.id)
this.subscribe(MqttTopics.MULTI_CHAT + contact.id);
}
},
handleClearUnread(contactId, scene) {
if (this.$refs.messageList) {
this.$refs.messageList.clearUnread(contactId, scene)
this.$refs.messageList.clearUnread(contactId, scene);
}
},
handleSendMessage(message) {
if (this.$refs.messageDisplay) {
this.$refs.messageDisplay.addChatMessage(message)
this.$refs.messageDisplay.addChatMessage(message);
}
//
if (this.$refs.messageList) {
@ -277,47 +322,52 @@ export default {
message.scene,
message,
new Date().toISOString()
)
);
}
},
handleUpdateMessageStatus(messageId, updates) {
if (this.$refs.messageDisplay) {
this.$refs.messageDisplay.updateMessageStatus(messageId, updates)
this.$refs.messageDisplay.updateMessageStatus(messageId, updates);
}
},
handleSendFiles(files) {
if (this.$refs.messageEditor) {
files.forEach(file => {
const type = file.type.startsWith('image/') ? 'image'
: file.type.startsWith('video/') ? 'video'
: file.type.startsWith('audio/') ? 'audio'
: 'file'
this.$refs.messageEditor.sendFileMessage(file, type)
})
files.forEach((file) => {
const type = file.type.startsWith("image/")
? "image"
: file.type.startsWith("video/")
? "video"
: file.type.startsWith("audio/")
? "audio"
: "file";
this.$refs.messageEditor.sendFileMessage(file, type);
});
}
},
handleGroupSetting(chat) {
this.groupSettingVisible = true
handleGroupSetting() {
this.$refs.groupSettingRef.loadGroupInfo();
},
handleQuitGroup(chat) {
// 退
if (this.$refs.messageList) {
this.$refs.messageList.removeContact(chat.id, chat.scene)
//
this.$refs.messageList.refreshList({ selectFirst: true });
}
},
handleDismissGroup(chat) {
//
if (this.$refs.messageList) {
this.$refs.messageList.removeContact(chat.id, chat.scene)
//
this.$refs.messageList.refreshList({ selectFirst: true });
}
}
}
}
},
},
};
</script>
<style lang="scss" scoped>

@ -175,6 +175,7 @@
<script>
import {
meetingModes,
postConsultationInfo,
getConsultationList,
getConsultationMeetingInfo,
@ -191,40 +192,7 @@ export default {
size: 10,
total: 0,
},
meetingModes: [
{
title: "实时会诊",
icon: "el-icon-user-solid",
color: "blue",
routeName: "RealTimeConsultation",
routePath: "/videoCommunication/realTimeConsultation",
type: 1,
},
{
title: "带教培训",
icon: "el-icon-video-play",
color: "yellow",
routeName: "Training",
routePath: "/videoCommunication/training",
type: 9,
},
{
title: "在线质控",
icon: "el-icon-lightbulb",
color: "grayblue",
routeName: "QualityControl",
routePath: "/videoCommunication/qualityControl",
type: 6,
},
{
title: "病例研讨",
icon: "el-icon-chat-line-square",
color: "greenblue",
routeName: "CaseStudy",
routePath: "/videoCommunication/caseStudy",
type: 3,
},
],
meetingModes: meetingModes(),
list: [],
meetingDetailVisible: false,
meetingDetail: {},

Loading…
Cancel
Save