Browse Source

修改切换账号功能可以删除账号,新增撤回消息语言

master
[yxf] 4 weeks ago
parent
commit
62fc8c7caf
  1. 35
      im-platform/src/main/java/com/bx/implatform/controller/UserController.java
  2. 32
      im-uniapp/App.vue
  3. 18
      im-uniapp/pages/chat/chat-box.vue
  4. 6
      im-uniapp/static/i18n/ara.json
  5. 6
      im-uniapp/static/i18n/de.json
  6. 7
      im-uniapp/static/i18n/en.json
  7. 6
      im-uniapp/static/i18n/fra.json
  8. 34
      im-uniapp/static/i18n/jp.json
  9. 34
      im-uniapp/static/i18n/kor.json
  10. 6
      im-uniapp/static/i18n/pt.json
  11. 22
      im-uniapp/static/i18n/ru.json
  12. 14
      im-uniapp/static/i18n/vie.json
  13. 9
      im-uniapp/static/i18n/zh.json
  14. 107
      im-uniapp/store/chatStore.js
  15. 128
      im-web/src/components/account/AccountSwitchMenu.vue

35
im-platform/src/main/java/com/bx/implatform/controller/UserController.java

@ -304,5 +304,40 @@ public class UserController {
return ResultUtils.success(vo);
}
@PostMapping("/deleteSwitchAccount")
@Operation(summary = "删除可切换账号", description = "从当前客服的可切换列表中移除指定账号")
public Result<?> deleteSwitchAccount(@RequestBody JSONObject jsonObject) {
Long targetUserId = jsonObject.getLong("targetUserId");
if (ObjectUtil.isNull(targetUserId)) {
return ResultUtils.error(XSS_PARAM_ERROR, "参数错误");
}
UserSession session = SessionContext.getSession();
Long currentUserId = session.getUserId();
User currentUser = userService.getById(currentUserId);
if (currentUser == null) {
return ResultUtils.error(XSS_PARAM_ERROR);
}
// 取出可切换账号并移除目标ID
String switchableIdsStr = currentUser.getSwitchableAccountIds();
if (StrUtil.isBlank(switchableIdsStr)) {
return ResultUtils.success("移除成功");
}
List<Long> idList = Arrays.stream(switchableIdsStr.split(","))
.filter(StrUtil::isNotBlank)
.map(Long::parseLong)
.filter(id -> !id.equals(targetUserId))
.collect(Collectors.toList());
String newIds = idList.stream().map(String::valueOf).collect(Collectors.joining(","));
// 更新到数据库
currentUser.setSwitchableAccountIds(newIds);
userService.updateById(currentUser);
return ResultUtils.success("移除成功");
}
}

32
im-uniapp/App.vue

@ -99,26 +99,37 @@ export default {
});
},
//
//
handleCustomerChanged(msgInfo) {
console.log('客服已变更,刷新聊天记录:', msgInfo);
console.log('【客服转接】后台下发变更通知', msgInfo);
// msgInfo oldKfId idnewKfId id
const oldKfId = msgInfo.oldKfId;
const newKfId = msgInfo.newKfId;
//
// ========== -> ==========
if (oldKfId && newKfId && oldKfId != newKfId) {
const oldChat = this.chatStore.findChatByFriend(oldKfId);
const newChat = this.chatStore.findChatByFriend(newKfId);
if (oldChat && oldChat.messages.length > 0) {
console.log('【开始强制合并】旧客服', oldKfId, '→ 新客服', newKfId);
//
this.chatStore.mergeOldCustomerToNew(oldKfId, newKfId);
}
}
//
this.friendStore.loadFriend().then(() => {
//
this.chatStore.refreshChats();
//
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
if (currentPage.route === 'pages/chat/chat-box') {
//
const targetId = currentPage.options.targetId;
this.reloadChatMessages(targetId);
}
});
},
//
reloadChatMessages(targetId) {
//
@ -205,14 +216,14 @@ export default {
msg.selfSend = msg.sendId == this.userStore.userInfo.id;
// id
let friendId = msg.selfSend ? msg.recvId : msg.sendId;
//
let existingFriend = this.friendStore.findFriend(friendId);
if (!existingFriend && !msg.selfSend) {
console.log("收到未知用户消息,刷新应用:", friendId);
//
this.loadStore().then(() => {
//
// #ifdef H5
window.location.reload();
// #endif
@ -222,7 +233,6 @@ export default {
// #endif
// #ifdef MP-WEIXIN
//
uni.reLaunch({
url: '/pages/chat/chat'
});
@ -269,7 +279,7 @@ export default {
this.chatStore.setDnd(chatInfo, JSON.parse(msg.content));
return;
}
//
//
let friend = this.loadFriendInfo(friendId);
this.insertPrivateMessage(friend, msg);
},

18
im-uniapp/pages/chat/chat-box.vue

@ -1592,6 +1592,7 @@ export default {
},
},
async onLoad(options) {
try {
this.currentLang = uni.getStorageSync("app_language") || "zh";
uni.showLoading({ title: this.$t('common.loading'), mask: true });
@ -1658,6 +1659,23 @@ export default {
await this.getSetting();
this.$nextTick(() => this.scrollToBottom());
this.$socket.on('customer_transfer', (data) => {
/**
* data后端返回格式
* { oldKfId: 旧客服ID, newKfId: 新客服ID }
*/
const { oldKfId, newKfId } = data;
if (!oldKfId || !newKfId) return;
// chatStore
this.chatStore.mergeOldCustomerToNew(oldKfId, newKfId);
console.log(data);
//
this.currentTargetId = newKfId;
this.$nextTick(() => {
this.scrollToBottom();
});
})
} catch (err) {
console.error("错误:", err);
} finally {

6
im-uniapp/static/i18n/ara.json

@ -33,7 +33,11 @@
"album": "ألبوم",
"camera": "كاميرا",
"noFriends": "لا أصدقاء",
"title": "دردشة"
"title": "دردشة",
"you": "أنت",
"other": "الطرف الآخر",
"recalledMessage": "قام بتراجع رسالة",
"quoteRecalled": "تم تراجع المحتوى المقتبس"
},
"common": {
"confirm": "موافق",

6
im-uniapp/static/i18n/de.json

@ -33,7 +33,11 @@
"album": "Album",
"camera": "Kamera",
"noFriends": "Keine Freunde",
"title": "Chat"
"title": "Chat",
"you": "Du",
"other": "Gegenüber",
"recalledMessage": "hat eine Nachricht zurückgenommen",
"quoteRecalled": "Zitat zurückgenommen"
},
"common": {
"confirm": "OK",

7
im-uniapp/static/i18n/en.json

@ -7,7 +7,6 @@
"read": "Read",
"unread": "Unread",
"readedCount": "{count} read",
"guessWantAsk": "You may want to ask",
"typing": "typing...",
"autoReplyFailed": "Auto reply failed",
@ -34,7 +33,11 @@
"album": "Album",
"camera": "Camera",
"noFriends": "No friends",
"title": "Chat"
"title": "Chat",
"you": "You",
"other": "Other",
"recalledMessage": "recalled a message",
"quoteRecalled": "Quote recalled"
},
"common": {
"confirm": "OK",

6
im-uniapp/static/i18n/fra.json

@ -33,7 +33,11 @@
"album": "Album",
"camera": "Caméra",
"noFriends": "Pas d'amis",
"title": "Discussion"
"title": "Discussion",
"you": "Vous",
"other": "Interlocuteur",
"recalledMessage": "a rappelé un message",
"quoteRecalled": "Citation rappelée"
},
"common": {
"confirm": "OK",

34
im-uniapp/static/i18n/jp.json

@ -9,31 +9,35 @@
"readedCount": "{count}人が既読",
"guessWantAsk": "質問したいこと",
"typing": "入力中...",
"autoReplyFailed": "自動返信失敗",
"autoReplyFailed": "自動返信失敗しました",
"receiptMessage": "【既読確認】",
"inputPlaceholder": "メッセージを入力",
"send": "送信",
"newMessages": "{count}件の新着メッセージ",
"cannotSendBlank": "空メッセージは送信できません",
"copySuccess": "コピー成功",
"copyFailed": "コピー失敗",
"downloadFailed": "ダウンロード失敗",
"deleteMessage": "メッセージ削除",
"cannotSendBlank": "空メッセージは送信できません",
"copySuccess": "コピーしました",
"copyFailed": "コピー失敗",
"downloadFailed": "ダウンロード失敗",
"deleteMessage": "メッセージ削除",
"confirmDelete": "このメッセージを削除しますか?",
"recallMessage": "メッセージ取り消し",
"recallMessage": "メッセージを取り消す",
"confirmRecall": "このメッセージを取り消しますか?",
"deleteSuccess": "削除成功",
"resendNotSupported": "再送信に対応していません",
"sendFailed": "送信失敗",
"uploadFailed": "アップロード失敗",
"userBanned": "発言禁止されています: {reason}",
"groupBanned": "グループで発言禁止: {reason}",
"deleteSuccess": "削除しました",
"resendNotSupported": "このメッセージの再送信に対応していません",
"sendFailed": "送信失敗",
"uploadFailed": "アップロード失敗",
"userBanned": "発言を禁止されています:{reason}",
"groupBanned": "グループで発言を禁止されています:{reason}",
"atAll": "全員",
"file": "ファイル",
"album": "アルバム",
"camera": "カメラ",
"noFriends": "友達なし",
"title": "チャット"
"noFriends": "友達がいません",
"title": "チャット",
"you": "あなた",
"other": "相手",
"recalledMessage": "メッセージを取り消しました",
"quoteRecalled": "引用メッセージは取り消されました"
},
"common": {
"confirm": "確定",

34
im-uniapp/static/i18n/kor.json

@ -1,39 +1,43 @@
{
"chat": {
"copy": "복사",
"recall": "취소",
"recall": "회수",
"delete": "삭제",
"download": "다운로드 열기",
"download": "다운로드 열기",
"read": "읽음",
"unread": "읽음",
"unread": "읽지 않음",
"readedCount": "{count}명 읽음",
"guessWantAsk": "물어보고 싶은 것",
"guessWantAsk": "물어볼 내용",
"typing": "입력 중...",
"autoReplyFailed": "자동 회신 실패",
"receiptMessage": "【읽음 확인】",
"inputPlaceholder": "메시지 입력",
"inputPlaceholder": "메시지 입력하세요",
"send": "전송",
"newMessages": "{count}개의 새 메시지",
"cannotSendBlank": "빈 메시지는 보낼 수 없습니다",
"copySuccess": "복사 성공",
"copySuccess": "복사 완료",
"copyFailed": "복사 실패",
"downloadFailed": "다운로드 실패",
"deleteMessage": "메시지 삭제",
"confirmDelete": "메시지를 삭제할까요?",
"recallMessage": "메시지 취소",
"confirmRecall": "이 메시지를 취소할까요?",
"deleteSuccess": "삭제 성공",
"resendNotSupported": "재전송 지원 안됨",
"confirmDelete": "메시지를 삭제할까요?",
"recallMessage": "메시지 회수",
"confirmRecall": "메시지를 회수할까요?",
"deleteSuccess": "삭제 완료",
"resendNotSupported": "해당 메시지는 재전송할 수 없습니다",
"sendFailed": "전송 실패",
"uploadFailed": "업로드 실패",
"userBanned": "채팅 금지: {reason}",
"groupBanned": "그룹 채팅 금지: {reason}",
"userBanned": "채팅이 제한되었습니다: {reason}",
"groupBanned": "그룹 채팅이 제한되었습니다: {reason}",
"atAll": "모두",
"file": "파일",
"album": "앨범",
"camera": "카메라",
"noFriends": "친구 없음",
"title": "채팅"
"noFriends": "친구가 없습니다",
"title": "채팅",
"you": "당신",
"other": "상대방",
"recalledMessage": "메시지를 회수했습니다",
"quoteRecalled": "인용 내용이 회수되었습니다"
},
"common": {
"confirm": "확인",

6
im-uniapp/static/i18n/pt.json

@ -33,7 +33,11 @@
"album": "Álbum",
"camera": "Câmera",
"noFriends": "Sem amigos",
"title": "Chat"
"title": "Chat",
"you": "Você",
"other": "Outro",
"recalledMessage": "retirou uma mensagem",
"quoteRecalled": "Citação retirada"
},
"common": {
"confirm": "OK",

22
im-uniapp/static/i18n/ru.json

@ -6,41 +6,45 @@
"download": "Скачать и открыть",
"read": "Прочитано",
"unread": "Не прочитано",
"readedCount": "{count} человек прочитало",
"readedCount": "{count} человек прочитали",
"guessWantAsk": "Возможно, вы хотите спросить",
"typing": "Печатает...",
"autoReplyFailed": "Автоответ не удался",
"autoReplyFailed": "Ошибка автоответа",
"receiptMessage": "【Прочитано】",
"inputPlaceholder": "Введите сообщение",
"send": "Отправить",
"newMessages": "{count} новых сообщений",
"cannotSendBlank": "Нельзя отправить пустое",
"cannotSendBlank": "Нельзя отправить пустое сообщение",
"copySuccess": "Скопировано",
"copyFailed": "Ошибка копирования",
"downloadFailed": "Ошибка загрузки",
"deleteMessage": "Удалить сообщение",
"confirmDelete": "Удалить это сообщение?",
"confirmDelete": "Удалить сообщение?",
"recallMessage": "Отозвать сообщение",
"confirmRecall": "Отозвать это сообщение?",
"confirmRecall": "Отозвать сообщение?",
"deleteSuccess": "Удалено",
"resendNotSupported": "Не поддерживается",
"resendNotSupported": "Повторная отправка не поддерживается",
"sendFailed": "Ошибка отправки",
"uploadFailed": "Ошибка загрузки",
"userBanned": "Вы заблокированы: {reason}",
"groupBanned": "В группе блокировка: {reason}",
"groupBanned": "Заблокировано в группе: {reason}",
"atAll": "Все",
"file": "Файл",
"album": "Альбом",
"camera": "Камера",
"noFriends": "Нет друзей",
"title": "Чат"
"title": "Чат",
"you": "Вы",
"other": "Собеседник",
"recalledMessage": "отозвал сообщение",
"quoteRecalled": "Цитата отозвана"
},
"common": {
"confirm": "ОК",
"cancel": "Отмена",
"loading": "Загрузка...",
"justNow": "Только что",
"minutesAgo": "мин назад",
"minutesAgo": "мин. назад",
"yesterday": "Вчера"
}
}

14
im-uniapp/static/i18n/vie.json

@ -19,21 +19,25 @@
"copyFailed": "Sao chép thất bại",
"downloadFailed": "Tải thất bại",
"deleteMessage": "Xóa tin nhắn",
"confirmDelete": "Xóa tin này?",
"confirmDelete": "Xóa tin nhắn này?",
"recallMessage": "Thu hồi tin nhắn",
"confirmRecall": "Thu hồi tin này?",
"deleteSuccess": "Xóa thành công",
"confirmRecall": "Thu hồi tin nhắn này?",
"deleteSuccess": "Đã xóa",
"resendNotSupported": "Không hỗ trợ gửi lại",
"sendFailed": "Gửi thất bại",
"uploadFailed": "Tải lên thất bại",
"userBanned": "Bạn bị cấm nói: {reason}",
"userBanned": "Bạn bị cấm trò chuyện: {reason}",
"groupBanned": "Bị cấm trong nhóm: {reason}",
"atAll": "Tất cả",
"file": "Tệp",
"album": "Thư viện",
"camera": "Máy ảnh",
"noFriends": "Chưa có bạn bè",
"title": "Trò chuyện"
"title": "Trò chuyện",
"you": "Bạn",
"other": "Người kia",
"recalledMessage": "đã thu hồi một tin nhắn",
"quoteRecalled": "Nội dung trích dẫn đã được thu hồi"
},
"common": {
"confirm": "Xác nhận",

9
im-uniapp/static/i18n/zh.json

@ -7,7 +7,6 @@
"read": "已读",
"unread": "未读",
"readedCount": "{count}人已读",
"guessWantAsk": "你可能想问",
"typing": "正在输入...",
"autoReplyFailed": "自动回复失败",
@ -24,7 +23,7 @@
"recallMessage": "撤回消息",
"confirmRecall": "确定撤回此消息?",
"deleteSuccess": "删除成功",
"resendNotSupported": "暂不支持重发该类型消息",
"resendNotSupported": "暂不支持重发该消息",
"sendFailed": "发送失败",
"uploadFailed": "上传失败",
"userBanned": "你已被禁言,原因:{reason}",
@ -34,7 +33,11 @@
"album": "相册",
"camera": "相机",
"noFriends": "暂无好友",
"title": "聊天"
"title": "聊天",
"you": "你",
"other": "对方",
"recalledMessage": "撤回了一条消息",
"quoteRecalled": "引用内容已撤回"
},
"common": {
"confirm": "确定",

107
im-uniapp/store/chatStore.js

@ -3,6 +3,7 @@ import { MESSAGE_TYPE, MESSAGE_STATUS } from '@/common/enums.js';
import useFriendStore from './friendStore.js';
import useGroupStore from './groupStore.js';
import useUserStore from './userStore';
import { i18n } from '@/main.js'
let cacheChats = [];
export default defineStore('chatStore', {
@ -20,9 +21,7 @@ export default defineStore('chatStore', {
this.chats = [];
for (let chat of chatsData.chats) {
chat.stored = false;
// 暂存至缓冲区
cacheChats.push(JSON.parse(JSON.stringify(chat)));
// 加载期间显示只前15个会话做做样子,一切都为了加快初始化时间
if (this.chats.length < 15) {
this.chats.push(chat);
}
@ -31,28 +30,35 @@ export default defineStore('chatStore', {
this.groupMsgMaxId = chatsData.groupMsgMaxId || 0;
},
// 客服转接:合并旧客服会话到新客服(你要的功能)
mergeOldCustomerToNew(oldKfId, newKfId) {
const oldChat = this.findChatByFriend(oldKfId);
const newChat = this.findChatByFriend(newKfId);
if (!oldChat || !newChat) return;
if (!oldChat || !newChat) {
console.warn('合并失败:旧/新会话不存在', oldKfId, newKfId);
return;
}
newChat.messages = [...oldChat.messages, ...newChat.messages];
newChat.unreadCount += oldChat.unreadCount;
newChat.lastContent = oldChat.lastContent || newChat.lastContent;
newChat.lastSendTime = Math.max(oldChat.lastSendTime || 0, newChat.lastSendTime || 0);
newChat.atMe = newChat.atMe || oldChat.atMe;
newChat.atAll = newChat.atAll || oldChat.atAll;
newChat.stored = false;
oldChat.stored = false;
const keepHot = 300;
const totalMsg = newChat.messages.length;
newChat.hotMinIdx = totalMsg <= keepHot ? 0 : Math.max(0, totalMsg - keepHot);
newChat.readedMessageIdx = Math.min(oldChat.readedMessageIdx || 0, newChat.readedMessageIdx || 0);
this.clearCustomerCache(oldKfId);
this.removePrivateChat(oldKfId);
newChat.stored = false;
this.saveToStorage(true);
console.log('【合并完成】旧客服消息全部转入新客服', newChat.messages.length);
},
// 强制清理某个用户的本地缓存(清理旧客服)
clearCustomerCache(userId) {
const userStore = useUserStore();
const currentUserId = userStore.userInfo.id;
@ -68,12 +74,10 @@ export default defineStore('chatStore', {
if (chats[idx].type == chatInfo.type &&
chats[idx].targetId === chatInfo.targetId) {
chat = chats[idx];
// 放置头部
this.moveTop(idx)
break;
}
}
// 创建会话
if (chat == null) {
chat = {
targetId: chatInfo.targetId,
@ -121,7 +125,6 @@ export default defineStore('chatStore', {
for (let idx = chat.readedMessageIdx; idx < chat.messages.length; idx++) {
let m = chat.messages[idx];
if (m.id && m.selfSend && m.status < MESSAGE_STATUS.RECALL) {
// pos.maxId为空表示整个会话已读
if (!pos.maxId || m.id <= pos.maxId) {
m.status = MESSAGE_STATUS.READED
chat.readedMessageIdx = idx;
@ -183,63 +186,54 @@ export default defineStore('chatStore', {
}
},
insertMessage(msgInfo, chatInfo) {
// 获取对方id或群id
const t = i18n.global.t;
let type = chatInfo.type;
// 记录消息的最大id
if (msgInfo.id && type == "PRIVATE" && msgInfo.id > this.privateMsgMaxId) {
this.privateMsgMaxId = msgInfo.id;
}
if (msgInfo.id && type == "GROUP" && msgInfo.id > this.groupMsgMaxId) {
this.groupMsgMaxId = msgInfo.id;
}
// 如果是已存在消息,则覆盖旧的消息数据
let chat = this.findChat(chatInfo);
let message = this.findMessage(chat, msgInfo);
if (message) {
console.log("message:", message)
// ✅ 方式1:使用 Vue.set 或 $set 确保响应式更新
// 先删除旧消息
const msgIndex = chat.messages.findIndex(m =>
(m.id && m.id === msgInfo.id) ||
(m.tmpId && m.tmpId === msgInfo.tmpId)
);
if (msgIndex !== -1) {
// 创建新对象而不是修改原对象
const updatedMessage = { ...message, ...msgInfo };
// 使用 splice 替换,确保响应式
chat.messages.splice(msgIndex, 1, updatedMessage);
}
chat.stored = false;
this.saveToStorage();
return;
}
// 会话列表内容
// ====================== 多语言消息类型 ======================
if (msgInfo.type == MESSAGE_TYPE.IMAGE) {
chat.lastContent = "[图片]";
chat.lastContent = t('chat.image');
} else if (msgInfo.type == MESSAGE_TYPE.FILE) {
chat.lastContent = "[文件]";
chat.lastContent = t('chat.file');
} else if (msgInfo.type == MESSAGE_TYPE.AUDIO) {
chat.lastContent = "[语音]";
chat.lastContent = t('chat.voice');
} else if (msgInfo.type == MESSAGE_TYPE.ACT_RT_VOICE) {
chat.lastContent = "[语音通话]";
chat.lastContent = t('chat.voiceCall');
} else if (msgInfo.type == MESSAGE_TYPE.ACT_RT_VIDEO) {
chat.lastContent = "[视频通话]";
chat.lastContent = t('chat.videoCall');
} else if (msgInfo.type == MESSAGE_TYPE.TEXT ||
msgInfo.type == MESSAGE_TYPE.RECALL ||
msgInfo.type == MESSAGE_TYPE.TIP_TEXT) {
chat.lastContent = msgInfo.content;
}
// ============================================================
chat.lastSendTime = msgInfo.sendTime;
chat.sendNickName = msgInfo.sendNickName;
// 未读加1
if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED &&
msgInfo.status != MESSAGE_STATUS.RECALL && msgInfo.type != MESSAGE_TYPE.TIP_TEXT) {
chat.unreadCount++;
}
// 是否有人@我
if (!msgInfo.selfSend && chat.type == "GROUP" && msgInfo.atUserIds &&
msgInfo.status != MESSAGE_STATUS.READED) {
const userStore = useUserStore();
@ -251,7 +245,6 @@ export default defineStore('chatStore', {
chat.atAll = true;
}
}
// 间隔大于10分钟插入时间显示
if (!chat.lastTimeTip || (chat.lastTimeTip < msgInfo.sendTime - 600 * 1000)) {
chat.messages.push({
sendTime: msgInfo.sendTime,
@ -259,17 +252,14 @@ export default defineStore('chatStore', {
});
chat.lastTimeTip = msgInfo.sendTime;
}
// 插入消息
chat.messages.push(msgInfo);
chat.stored = false;
this.saveToStorage();
},
updateMessage(msgInfo, chatInfo) {
// 获取对方id或群id
let chat = this.findChat(chatInfo);
let message = this.findMessage(chat, msgInfo);
if (message) {
// 属性拷贝
Object.assign(message, msgInfo);
chat.stored = false;
this.saveToStorage();
@ -280,12 +270,10 @@ export default defineStore('chatStore', {
let chat = this.findChat(chatInfo);
let delIdx = -1;
for (let idx in chat.messages) {
// 已经发送成功的,根据id删除
if (chat.messages[idx].id && chat.messages[idx].id == msgInfo.id) {
delIdx = idx;
break;
}
// 正在发送中的消息可能没有id,只有临时id
if (chat.messages[idx].tmpId && chat.messages[idx].tmpId == msgInfo.tmpId) {
delIdx = idx;
break;
@ -304,21 +292,20 @@ export default defineStore('chatStore', {
this.saveToStorage(isColdMessage);
}
},
recallMessage(msgInfo, chatInfo) {
const t = i18n.global.t;
let chat = this.findChat(chatInfo);
if (!chat) return;
let isColdMessage = false;
// 要撤回的消息id
let id = msgInfo.content;
let name = msgInfo.selfSend ? '你' : chat.type == 'PRIVATE' ? '对方' : msgInfo.sendNickName;
let name = msgInfo.selfSend ? t('chat.you') : (chat.type == 'PRIVATE' ? t('chat.other') : msgInfo.sendNickName);
for (let idx in chat.messages) {
let m = chat.messages[idx];
if (m.id && m.id == id) {
// 改造成一条提示消息
m.status = MESSAGE_STATUS.RECALL;
m.content = name + "撤回了一条消息";
m.content = `${name}${t('chat.recalledMessage')}`;
m.type = MESSAGE_TYPE.TIP_TEXT
// 会话列表
chat.lastContent = m.content;
chat.lastSendTime = msgInfo.sendTime;
chat.sendNickName = '';
@ -327,9 +314,8 @@ export default defineStore('chatStore', {
}
isColdMessage = idx < chat.hotMinIdx;
}
// 被引用的消息也要撤回
if (m.quoteMessage && m.quoteMessage.id == msgInfo.id) {
m.quoteMessage.content = "引用内容已撤回";
m.quoteMessage.content = t('chat.quoteRecalled');
m.quoteMessage.status = MESSAGE_STATUS.RECALL;
m.quoteMessage.type = MESSAGE_TYPE.TIP_TEXT
}
@ -341,7 +327,6 @@ export default defineStore('chatStore', {
let chat = this.findChatByFriend(friend.id)
if (chat && (chat.headImage != friend.headImage ||
chat.showName != friend.nickName)) {
// 更新会话中的群名和头像
chat.headImage = friend.headImage;
chat.showName = friend.nickName;
chat.stored = false;
@ -350,7 +335,6 @@ export default defineStore('chatStore', {
},
updateChatFromUser(user) {
let chat = this.findChatByFriend(user.id);
// 更新会话中的昵称和头像
if (chat && (chat.headImage != user.headImageThumb ||
chat.showName != user.nickName)) {
chat.headImage = user.headImageThumb;
@ -363,7 +347,6 @@ export default defineStore('chatStore', {
let chat = this.findChatByGroup(group.id);
if (chat && (chat.headImage != group.headImageThumb ||
chat.showName != group.showGroupName)) {
// 更新会话中的群名称和头像
chat.headImage = group.headImageThumb;
chat.showName = group.showGroupName;
chat.stored = false;
@ -381,7 +364,6 @@ export default defineStore('chatStore', {
},
refreshChats() {
let chats = cacheChats || this.chats;
// 更新会话免打扰状态
const friendStore = useFriendStore();
const groupStore = useGroupStore();
chats.forEach(chat => {
@ -397,36 +379,23 @@ export default defineStore('chatStore', {
}
}
})
// 排序
chats.sort((chat1, chat2) => chat2.lastSendTime - chat1.lastSendTime);
// #ifndef APP-PLUS
// h5和小程序的stroge一般只有5m,大约只能存储1w条消息,所以可能需要清理部分历史消息
const storageInfo = uni.getStorageInfoSync();
console.log(`storage缓存: ${storageInfo.currentSize} KB`)
// 空间不足(大于3mb)时,清理这个设备登录过其他账户的消息
if (storageInfo && storageInfo.currentSize > 3000) {
console.log("storage空间不足,清理其他用户缓存..")
this.cleanOtherUserCache();
}
// 保证消息总数量不超过3000条,每个会话不超过500条
this.fliterMessage(chats, 3000, 500);
// #endif
// 记录热数据索引位置
chats.forEach(chat => {
if (!chat.hotMinIdx || chat.hotMinIdx != chat.messages.length) {
chat.hotMinIdx = chat.messages.length;
chat.stored = false;
}
});
// 将消息一次性装载回来
this.chats = chats;
// 清空缓存,不再使用
cacheChats = null;
// 消息持久化
this.saveToStorage(true);
},
fliterMessage(chats, maxTotalSize, maxPerChatSize) {
// 每个会话只保留maxPerChatSize条消息
let remainTotalSize = 0;
chats.forEach(chat => {
if (chat.messages.length > maxPerChatSize) {
@ -435,12 +404,9 @@ export default defineStore('chatStore', {
}
remainTotalSize += chat.messages.length;
})
// 保证消息总数不超过maxTotalSize条,否则继续清理
if (remainTotalSize > maxTotalSize) {
this.fliterMessage(chats, maxTotalSize, maxPerChatSize / 2);
}
console.log("消息留存总数量:", remainTotalSize)
console.log("单会话消息数量:", maxPerChatSize)
},
cleanOtherUserCache() {
const userStore = useUserStore();
@ -448,15 +414,12 @@ export default defineStore('chatStore', {
const prefix = "chats-app-" + userId;
const res = uni.getStorageInfoSync();
res.keys.forEach(key => {
// 清理其他用户的消息
if (key.startsWith("chats-app") && !key.startsWith(prefix)) {
uni.removeStorageSync(key);
console.log("清理key:", key)
}
})
},
saveToStorage(withColdMessage) {
// 加载中不保存,防止卡顿
if (this.loading) {
return;
}
@ -464,20 +427,17 @@ export default defineStore('chatStore', {
let userId = userStore.userInfo.id;
let key = "chats-app-" + userId;
let chatKeys = [];
// 按会话为单位存储,只存储有改动的会话
this.chats.forEach((chat) => {
let chatKey = `${key}-${chat.type}-${chat.targetId}`
if (!chat.stored) {
if (chat.delete) {
uni.removeStorageSync(chatKey);
} else {
// 存储冷数据
if (withColdMessage) {
let coldChat = Object.assign({}, chat);
coldChat.messages = chat.messages.slice(0, chat.hotMinIdx);
uni.setStorageSync(chatKey, coldChat)
}
// 存储热消息
let hotKey = chatKey + '-hot';
let hotChat = Object.assign({}, chat);
hotChat.messages = chat.messages.slice(chat.hotMinIdx)
@ -489,14 +449,12 @@ export default defineStore('chatStore', {
chatKeys.push(chatKey);
}
})
// 会话核心信息
let chatsData = {
privateMsgMaxId: this.privateMsgMaxId,
groupMsgMaxId: this.groupMsgMaxId,
chatKeys: chatKeys
}
uni.setStorageSync(key, chatsData)
// 清理已删除的会话
this.chats = this.chats.filter(chat => !chat.delete)
},
clear(state) {
@ -521,18 +479,15 @@ export default defineStore('chatStore', {
if (!coldChat && !hotChat) {
return;
}
// 防止消息一直处在发送中状态
hotChat && hotChat.messages.forEach(msg => {
if (msg.status == MESSAGE_STATUS.SENDING) {
msg.status = MESSAGE_STATUS.FAILED
}
})
// 冷热消息合并
let chat = Object.assign({}, coldChat, hotChat);
if (hotChat && coldChat) {
chat.messages = coldChat.messages.concat(hotChat.messages)
}
// 历史版本没有readedMessageIdx字段,做兼容一下
chat.readedMessageIdx = chat.readedMessageIdx || 0;
chatsData.chats.push(chat);
})
@ -577,20 +532,17 @@ export default defineStore('chatStore', {
if (!chat) {
return null;
}
// 通过id判断
if (msgInfo.id) {
for (let idx = chat.messages.length - 1; idx >= 0; idx--) {
let m = chat.messages[idx];
if (m.id && msgInfo.id == m.id) {
return m;
}
// 如果id比要查询的消息小,说明没有这条消息
if (m.id && m.id < msgInfo.id) {
break;
}
}
}
// 正在发送中的临时消息可能没有id,只有tmpId
if (msgInfo.selfSend && msgInfo.tmpId) {
for (let idx = chat.messages.length - 1; idx >= 0; idx--) {
let m = chat.messages[idx];
@ -600,7 +552,6 @@ export default defineStore('chatStore', {
if (msgInfo.tmpId == m.tmpId) {
return m;
}
// 如果id比要查询的消息小,说明没有这条消息
if (m.tmpId && m.tmpId < msgInfo.tmpId) {
break;
}

128
im-web/src/components/account/AccountSwitchMenu.vue

@ -65,11 +65,13 @@
@click.stop="handleSwitch(account)">
切换
</el-button>
<i
class="el-icon-close delete-icon"
@click.stop="handleRemove(account)"
title="移除可切换账号">
</i>
<el-button
type="danger"
size="small"
@click.stop="handleDeleteAccount(account)"
>
删除
</el-button>
</div>
</div>
</div>
@ -97,7 +99,6 @@
</div>
</div>
<!-- 添加账号登录对话框 -->
<el-dialog
title="添加账号"
:visible.sync="addAccountDialogVisible"
@ -138,7 +139,7 @@
</template>
<script>
const ACCOUNTS_CACHE_KEY = 'switchable_accounts_cache'; // key
const ACCOUNTS_CACHE_KEY = 'switchable_accounts_cache';
export default {
name: 'AccountSwitchMenu',
@ -228,7 +229,6 @@ export default {
//
saveCachedAccounts(accounts) {
try {
//
const cacheData = accounts.map(a => ({
id: a.id,
userName: a.userName,
@ -245,7 +245,6 @@ export default {
//
addAccountToCache(account) {
const accounts = this.getCachedAccounts();
//
const exists = accounts.some(a => a.id === account.id);
if (!exists) {
accounts.push({
@ -266,18 +265,44 @@ export default {
this.saveCachedAccounts(accounts);
},
async handleDeleteAccount(account) {
try {
await this.$confirm(
`确定要删除账号【${account.nickName}】吗?\n删除后将不再显示此账号`,
'删除账号',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning'
}
)
try {
await this.$http({
url: '/user/deleteSwitchAccount',
method: 'post',
data: { targetUserId: account.id }
})
} catch (err) {
console.log('后端删除失败,继续清理本地')
}
this.removeAccountFromCache(account.id)
await this.loadAccountList()
this.$message.success('删除账号成功')
} catch (error) {
//
}
},
async loadAccountList() {
this.loading = true;
try {
// 1.
let accounts = this.getCachedAccounts();
// 2.
accounts = accounts.filter(a => a.id !== this.currentUser.id);
console.log('从缓存加载账号列表:', accounts);
// 3.
try {
const res = await this.$http({
url: '/user/getSwitchableAccounts',
@ -287,9 +312,7 @@ export default {
const data = res.data || res || {};
const serverAccounts = data.switchableUsers || [];
//
if (serverAccounts.length > 0) {
//
const mergedAccounts = this.mergeAccounts(accounts, serverAccounts);
this.saveCachedAccounts(mergedAccounts);
accounts = mergedAccounts.filter(a => a.id !== this.currentUser.id);
@ -298,7 +321,6 @@ export default {
console.log('后端接口获取失败,使用缓存数据:', error);
}
// 4.
if (accounts.length > 0) {
accounts = await this.fetchUnreadCounts(accounts);
}
@ -307,7 +329,6 @@ export default {
} catch (error) {
console.error('加载账号列表失败:', error);
//
const cached = this.getCachedAccounts();
this.allAccounts = cached.filter(a => a.id !== this.currentUser.id);
} finally {
@ -315,17 +336,13 @@ export default {
}
},
//
mergeAccounts(cached, server) {
const map = new Map();
//
cached.forEach(a => map.set(a.id, a));
// /
server.forEach(a => map.set(a.id, a));
return Array.from(map.values());
},
//
async refreshUnreadCounts() {
if (this.allAccounts.length === 0) return;
@ -337,7 +354,6 @@ export default {
}
},
//
async fetchUnreadCounts(accounts) {
try {
const userIds = accounts.map(a => a.id);
@ -345,9 +361,7 @@ export default {
const res = await this.$http({
url: '/message/private/unreadCounts',
method: 'post',
data: {
userIds: userIds
}
data: { userIds }
});
const unreadMap = res.data || res || {};
@ -359,10 +373,7 @@ export default {
} catch (error) {
console.error('获取未读消息数失败:', error);
return accounts.map(account => ({
...account,
unreadCount: 0
}));
return accounts.map(account => ({ ...account, unreadCount: 0 }));
}
},
@ -442,7 +453,6 @@ export default {
handleClickOutside(event) {
if (!this.visible) return;
if (this.addAccountDialogVisible) return;
const menu = this.$refs.menu || this.$el;
if (menu && !menu.contains(event.target)) {
@ -454,11 +464,11 @@ export default {
}
},
// ========== ==========
handleAddAccount() {
this.loginForm = {
userName: '',
password: ''
};
this.loginForm = { userName: '', password: '' };
//
this.$emit('close');
this.addAccountDialogVisible = true;
this.$nextTick(() => {
@ -484,10 +494,8 @@ export default {
}
});
//
const accountData = res.data || res || {};
//
if (accountData.user) {
this.addAccountToCache(accountData.user);
} else if (accountData.id) {
@ -502,11 +510,7 @@ export default {
} catch (error) {
console.error('添加失败:', error);
if (error.response && error.response.data) {
this.$message.error(error.response.data.message || '添加失败');
} else {
this.$message.error('添加失败');
}
this.$message.error(error?.response?.data?.message || '添加失败');
} finally {
this.adding = false;
}
@ -514,22 +518,15 @@ export default {
},
handleSwitch(account) {
//
this.addAccountToCache(account);
//
this.addAccountToCache(this.currentUser);
this.$emit('switch', account);
},
getTerminalType() {
const userAgent = navigator.userAgent;
if (/mobile/i.test(userAgent)) {
return 2;
}
if (/tablet/i.test(userAgent)) {
return 3;
}
if (/mobile/i.test(userAgent)) return 2;
if (/tablet/i.test(userAgent)) return 3;
return 1;
},
@ -541,17 +538,13 @@ export default {
type: 'warning'
});
//
this.removeAccountFromCache(account.id);
//
try {
await this.$http({
url: '/user/removeSwitchableAccount',
method: 'post',
data: {
targetUserId: account.id
}
data: { targetUserId: account.id }
});
} catch (error) {
console.log('后端移除失败,已从本地缓存移除');
@ -572,9 +565,7 @@ export default {
};
</script>
<style lang="scss">
.add-account-dialog {
border-radius: 10px !important;
}
@ -696,10 +687,6 @@ export default {
&:hover {
background: #f5f7fa;
.delete-icon {
opacity: 1;
}
}
.chat-left {
@ -770,21 +757,6 @@ export default {
align-items: center;
gap: 8px;
flex-shrink: 0;
.delete-icon {
opacity: 0;
color: #999;
font-size: 16px;
padding: 6px;
cursor: pointer;
transition: all 0.2s;
border-radius: 50%;
&:hover {
color: #f56c6c;
background: rgba(245, 108, 108, 0.1);
}
}
}
}
}

Loading…
Cancel
Save