Browse Source

Merge remote-tracking branch 'origin/master'

master
La123123 4 weeks ago
parent
commit
8a35b7e05a
  1. 79
      im-platform/src/main/java/com/bx/implatform/controller/UserController.java
  2. 6
      im-platform/src/main/java/com/bx/implatform/entity/User.java
  3. 4
      im-platform/src/main/java/com/bx/implatform/vo/UserVO.java
  4. 32
      im-uniapp/App.vue
  5. 40
      im-uniapp/pages/chat/chat-box.vue
  6. 6
      im-uniapp/static/i18n/ara.json
  7. 6
      im-uniapp/static/i18n/de.json
  8. 7
      im-uniapp/static/i18n/en.json
  9. 6
      im-uniapp/static/i18n/fra.json
  10. 34
      im-uniapp/static/i18n/jp.json
  11. 34
      im-uniapp/static/i18n/kor.json
  12. 6
      im-uniapp/static/i18n/pt.json
  13. 22
      im-uniapp/static/i18n/ru.json
  14. 14
      im-uniapp/static/i18n/vie.json
  15. 9
      im-uniapp/static/i18n/zh.json
  16. 105
      im-uniapp/store/chatStore.js
  17. 128
      im-web/src/components/account/AccountSwitchMenu.vue
  18. 21
      im-web/src/components/chat/ChatBox.vue

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

@ -68,7 +68,7 @@ public class UserController {
@Operation(summary = "保存用户分组", description = "单个分组,保存到 group_ids 字段") @Operation(summary = "保存用户分组", description = "单个分组,保存到 group_ids 字段")
public Result<?> saveGroup(@RequestBody JSONObject jsonObject) { public Result<?> saveGroup(@RequestBody JSONObject jsonObject) {
Long userId = jsonObject.getLong("userId"); Long userId = jsonObject.getLong("userId");
String groupId = jsonObject.getStr("groupIds"); // 前端传分组ID String groupId = jsonObject.getStr("groupIds");
userService.saveUserGroup(userId, groupId); userService.saveUserGroup(userId, groupId);
return ResultUtils.success(); return ResultUtils.success();
@ -104,6 +104,27 @@ public class UserController {
return ResultUtils.success(); return ResultUtils.success();
} }
@PostMapping("/updateLanguage")
@Operation(summary = "更新用户语言", description = "更新当前用户的语言设置:zh/en/jp/kor等")
public Result<?> updateLanguage(@RequestBody JSONObject jsonObject) {
String language = jsonObject.getStr("language");
if (StrUtil.isBlank(language)) {
return ResultUtils.error(ResultCode.XSS_PARAM_ERROR, "语言不能为空");
}
UserSession session = SessionContext.getSession();
Long userId = session.getUserId();
User user = userService.getById(userId);
if (user == null) {
return ResultUtils.error(ResultCode.XSS_PARAM_ERROR, "用户不存在");
}
user.setLanguage(language);
boolean success = userService.updateById(user);
return success ? ResultUtils.success("语言更新成功") : ResultUtils.error(ResultCode.XSS_PARAM_ERROR, "更新失败");
}
@GetMapping("/findByName") @GetMapping("/findByName")
@Operation(summary = "查找用户", description = "根据用户名或昵称查找用户") @Operation(summary = "查找用户", description = "根据用户名或昵称查找用户")
public Result<List<UserVO>> findByName(@RequestParam String name) { public Result<List<UserVO>> findByName(@RequestParam String name) {
@ -113,18 +134,15 @@ public class UserController {
@PostMapping("/getEnableChangeCustomer") @PostMapping("/getEnableChangeCustomer")
@Operation(summary = "获取可转接的客服", description = "转接客服") @Operation(summary = "获取可转接的客服", description = "转接客服")
public Result<List<Map<String, Object>>> getEnableChangeCustomer() { public Result<List<Map<String, Object>>> getEnableChangeCustomer() {
// 获取当前客服id、转接客服id、转接用户id
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
Long userId = session.getUserId(); Long userId = session.getUserId();
if(ObjectUtil.isNull(userId)){ if(ObjectUtil.isNull(userId)){
return ResultUtils.error(XSS_PARAM_ERROR); return ResultUtils.error(XSS_PARAM_ERROR);
} }
List<User> list = userService.getEnableChangeCustomerList(userId); List<User> list = userService.getEnableChangeCustomerList(userId);
//使用Map返回id、昵称
List<Map<String, Object>> result = list.stream().map(user -> { List<Map<String, Object>> result = list.stream().map(user -> {
Map<String, Object> map = new HashMap<>(); Map<String, Object> map = new HashMap<>();
map.put("id", user.getId()); map.put("id", user.getId());
@ -152,7 +170,6 @@ public class UserController {
return ResultUtils.error(XSS_PARAM_ERROR); return ResultUtils.error(XSS_PARAM_ERROR);
} }
// 获取当前用户信息
User currentUser = userService.getById(userId); User currentUser = userService.getById(userId);
if (currentUser == null) { if (currentUser == null) {
return ResultUtils.error(XSS_PARAM_ERROR); return ResultUtils.error(XSS_PARAM_ERROR);
@ -160,7 +177,6 @@ public class UserController {
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
// 获取可切换的账号ID列表(逗号分隔的字符串,如 "13,14")
String switchableIdsStr = currentUser.getSwitchableAccountIds(); String switchableIdsStr = currentUser.getSwitchableAccountIds();
List<Map<String, Object>> switchableUsers = new ArrayList<>(); List<Map<String, Object>> switchableUsers = new ArrayList<>();
@ -173,7 +189,6 @@ public class UserController {
if (!ids.isEmpty()) { if (!ids.isEmpty()) {
List<User> users = userService.listByIds(ids); List<User> users = userService.listByIds(ids);
// 过滤掉被封禁的账号
users = users.stream() users = users.stream()
.filter(u -> !Boolean.TRUE.equals(u.getIsBanned())) .filter(u -> !Boolean.TRUE.equals(u.getIsBanned()))
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -192,27 +207,21 @@ public class UserController {
result.put("switchableUsers", switchableUsers); result.put("switchableUsers", switchableUsers);
// 获取当前用户的 unique_token
String currentUserUniqueToken = currentUser.getUniqueToken(); String currentUserUniqueToken = currentUser.getUniqueToken();
// 构建查询条件
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<User>() LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<User>()
.eq(User::getIsCustomer, 2) .eq(User::getIsCustomer, 2)
.ne(User::getId, userId) .ne(User::getId, userId)
.eq(User::getIsBanned, 0); .eq(User::getIsBanned, 0);
// 添加 unique_token 条件
if (StrUtil.isNotBlank(currentUserUniqueToken)) { if (StrUtil.isNotBlank(currentUserUniqueToken)) {
// 当前用户有 unique_token,只查询相同 unique_token 的客服
queryWrapper.eq(User::getUniqueToken, currentUserUniqueToken); queryWrapper.eq(User::getUniqueToken, currentUserUniqueToken);
} else { } else {
// 当前用户没有 unique_token,只查询也没有 unique_token 的客服
queryWrapper.isNull(User::getUniqueToken).or().eq(User::getUniqueToken, ""); queryWrapper.isNull(User::getUniqueToken).or().eq(User::getUniqueToken, "");
} }
List<User> availableUsers = userService.list(queryWrapper); List<User> availableUsers = userService.list(queryWrapper);
// 获取已添加的ID集合
Set<Long> existingIds = new HashSet<>(); Set<Long> existingIds = new HashSet<>();
if (StrUtil.isNotBlank(switchableIdsStr)) { if (StrUtil.isNotBlank(switchableIdsStr)) {
Arrays.stream(switchableIdsStr.split(",")) Arrays.stream(switchableIdsStr.split(","))
@ -221,14 +230,12 @@ public class UserController {
.forEach(existingIds::add); .forEach(existingIds::add);
} }
// 标记是否已添加
List<Map<String, Object>> availableUsersList = availableUsers.stream().map(user -> { List<Map<String, Object>> availableUsersList = availableUsers.stream().map(user -> {
Map<String, Object> map = new HashMap<>(); Map<String, Object> map = new HashMap<>();
map.put("id", user.getId()); map.put("id", user.getId());
map.put("userName", user.getUserName()); map.put("userName", user.getUserName());
map.put("nickName", user.getNickName()); map.put("nickName", user.getNickName());
map.put("headImage", user.getHeadImage()); map.put("headImage", user.getHeadImage());
// map.put("headImageThumb", user.getHeadImageThumb());
map.put("isAdded", existingIds.contains(user.getId())); map.put("isAdded", existingIds.contains(user.getId()));
return map; return map;
}).collect(Collectors.toList()); }).collect(Collectors.toList());
@ -244,7 +251,6 @@ public class UserController {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
// 获取当前客服id、转接客服id、转接用户id
Long customerId = session.getUserId(); Long customerId = session.getUserId();
Long targetId = jsonObject.getLong("targetId"); Long targetId = jsonObject.getLong("targetId");
Long userId = jsonObject.getLong("userId"); Long userId = jsonObject.getLong("userId");
@ -267,22 +273,18 @@ public class UserController {
Long targetUserId = jsonObject.getLong("targetUserId"); Long targetUserId = jsonObject.getLong("targetUserId");
Integer terminal = jsonObject.getInt("terminal"); Integer terminal = jsonObject.getInt("terminal");
// 获取当前登录用户
UserSession currentSession = SessionContext.getSession(); UserSession currentSession = SessionContext.getSession();
User currentUser = userService.getById(currentSession.getUserId()); User currentUser = userService.getById(currentSession.getUserId());
// 权限校验:只有客服才能切换账号
if (currentUser.getIsCustomer() != 2) { if (currentUser.getIsCustomer() != 2) {
return ResultUtils.error(XSS_PARAM_ERROR, "无权限切换账号"); return ResultUtils.error(XSS_PARAM_ERROR, "无权限切换账号");
} }
// 获取目标用户信息
User targetUser = userService.getById(targetUserId); User targetUser = userService.getById(targetUserId);
if (ObjectUtil.isNull(targetUser)) { if (ObjectUtil.isNull(targetUser)) {
return ResultUtils.error(ResultCode.XSS_PARAM_ERROR, "目标用户不存在"); return ResultUtils.error(ResultCode.XSS_PARAM_ERROR, "目标用户不存在");
} }
// 生成新的token
UserSession newSession = BeanUtils.copyProperties(targetUser, UserSession.class); UserSession newSession = BeanUtils.copyProperties(targetUser, UserSession.class);
newSession.setUserId(targetUser.getId()); newSession.setUserId(targetUser.getId());
newSession.setTerminal(terminal); newSession.setTerminal(terminal);
@ -299,10 +301,41 @@ public class UserController {
vo.setRefreshTokenExpiresIn(jwtProperties.getRefreshTokenExpireIn()); vo.setRefreshTokenExpiresIn(jwtProperties.getRefreshTokenExpireIn());
vo.setUser(targetUser); vo.setUser(targetUser);
// log.info("账号切换:从用户 {} 切换到用户 {}", currentSession.getUserId(), targetUserId);
return ResultUtils.success(vo); 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);
}
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("移除成功");
}
}

6
im-platform/src/main/java/com/bx/implatform/entity/User.java

@ -134,6 +134,8 @@ public class User {
*/ */
private String switchableAccountIds; private String switchableAccountIds;
/**
* 语言
*/
private String language;
} }

4
im-platform/src/main/java/com/bx/implatform/vo/UserVO.java

@ -78,6 +78,8 @@ public class UserVO {
// 修改为完整的UserGroup列表 // 修改为完整的UserGroup列表
private List<UserLabelVO> labelList; // 完整的分组信息列表 private List<UserLabelVO> labelList; // 完整的分组信息列表
private String platformName; // 新增:平台名称 private String platformName;
private String language;
} }

32
im-uniapp/App.vue

@ -99,26 +99,37 @@ export default {
}); });
}, },
// //
//
handleCustomerChanged(msgInfo) { 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.friendStore.loadFriend().then(() => {
//
this.chatStore.refreshChats(); this.chatStore.refreshChats();
//
const pages = getCurrentPages(); const pages = getCurrentPages();
const currentPage = pages[pages.length - 1]; const currentPage = pages[pages.length - 1];
if (currentPage.route === 'pages/chat/chat-box') { if (currentPage.route === 'pages/chat/chat-box') {
//
const targetId = currentPage.options.targetId; const targetId = currentPage.options.targetId;
this.reloadChatMessages(targetId); this.reloadChatMessages(targetId);
} }
}); });
}, },
// //
reloadChatMessages(targetId) { reloadChatMessages(targetId) {
// //
@ -205,14 +216,14 @@ export default {
msg.selfSend = msg.sendId == this.userStore.userInfo.id; msg.selfSend = msg.sendId == this.userStore.userInfo.id;
// id // id
let friendId = msg.selfSend ? msg.recvId : msg.sendId; let friendId = msg.selfSend ? msg.recvId : msg.sendId;
// //
let existingFriend = this.friendStore.findFriend(friendId); let existingFriend = this.friendStore.findFriend(friendId);
if (!existingFriend && !msg.selfSend) { if (!existingFriend && !msg.selfSend) {
console.log("收到未知用户消息,刷新应用:", friendId); console.log("收到未知用户消息,刷新应用:", friendId);
//
this.loadStore().then(() => { this.loadStore().then(() => {
//
// #ifdef H5 // #ifdef H5
window.location.reload(); window.location.reload();
// #endif // #endif
@ -222,7 +233,6 @@ export default {
// #endif // #endif
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
//
uni.reLaunch({ uni.reLaunch({
url: '/pages/chat/chat' url: '/pages/chat/chat'
}); });
@ -269,7 +279,7 @@ export default {
this.chatStore.setDnd(chatInfo, JSON.parse(msg.content)); this.chatStore.setDnd(chatInfo, JSON.parse(msg.content));
return; return;
} }
// //
let friend = this.loadFriendInfo(friendId); let friend = this.loadFriendInfo(friendId);
this.insertPrivateMessage(friend, msg); this.insertPrivateMessage(friend, msg);
}, },

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

@ -268,8 +268,8 @@ export default {
isFileOpen: false, isFileOpen: false,
isPasting: false, isPasting: false,
hasPasteListener: false, hasPasteListener: false,
currentLang: 'zh',
showLangModal: false, // showLangModal: false, //
currentLang: uni.getStorageSync("app_language") || "zh",
langList: [ langList: [
{ label: "中文", value: "zh" }, { label: "中文", value: "zh" },
{ label: "English", value: "en" }, { label: "English", value: "en" },
@ -285,17 +285,25 @@ export default {
}; };
}, },
methods: { methods: {
// //
selectLang(lang) { selectLang(lang) {
this.currentLang = lang; this.currentLang = lang;
this.$i18n.locale = lang;
uni.setStorageSync("app_language", lang);
this.showLangDrop = false; this.showLangDrop = false;
//
this.$http({
url: "/user/updateLanguage",
method: "POST",
data: { language: lang }
}).then(() => {
console.log("语言保存成功");
}).finally(() => {
this.$i18n.locale = lang;
uni.setStorageSync("app_language", lang);
setTimeout(() => { setTimeout(() => {
window.location.reload(); // H5 window.location.reload();
}, 100); }, 100);
});
}, },
loadCommonQuestions(userId) { loadCommonQuestions(userId) {
this.$http({ this.$http({
@ -1257,13 +1265,18 @@ export default {
this.chatStore.updateChatFromUser(this.userInfo); this.chatStore.updateChatFromUser(this.userInfo);
} }
}, },
loadFriend(friendId) { async loadFriend(friendId) {
return new Promise((resolve) => {
this.$http({ this.$http({
url: `/user/find/${friendId}`, url: `/user/find/${friendId}`,
method: "GET", method: "GET",
}).then((userInfo) => { }).then((userInfo) => {
this.userInfo = userInfo; this.userInfo = userInfo;
this.updateFriendInfo(); this.updateFriendInfo();
resolve(); // then
}).catch(() => {
resolve();
});
}); });
}, },
rpxTopx(rpx) { rpxTopx(rpx) {
@ -1592,6 +1605,7 @@ export default {
}, },
}, },
async onLoad(options) { async onLoad(options) {
try { try {
this.currentLang = uni.getStorageSync("app_language") || "zh"; this.currentLang = uni.getStorageSync("app_language") || "zh";
uni.showLoading({ title: this.$t('common.loading'), mask: true }); uni.showLoading({ title: this.$t('common.loading'), mask: true });
@ -1647,7 +1661,9 @@ export default {
} }
this.readedMessage(); this.readedMessage();
this.loadFriend(targetId); // this.loadFriend(targetId);
await this.loadFriend(targetId);
this.loadReaded(targetId); this.loadReaded(targetId);
this.loadCommonQuestions(targetId); this.loadCommonQuestions(targetId);
@ -1658,6 +1674,7 @@ export default {
await this.getSetting(); await this.getSetting();
this.$nextTick(() => this.scrollToBottom()); this.$nextTick(() => this.scrollToBottom());
} catch (err) { } catch (err) {
console.error("错误:", err); console.error("错误:", err);
} finally { } finally {
@ -1678,11 +1695,10 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
/* ========== 右上角语言按钮 ========== */
.lang-wrap { .lang-wrap {
position: fixed; position: fixed;
top: var(--status-bar-height); top: var(--status-bar-height);
right: 40rpx; /* 按钮靠右距离,美观不贴边 */ right: 40rpx;
z-index: 9999 !important; z-index: 9999 !important;
height: $im-nav-bar-height; height: $im-nav-bar-height;
display: flex; display: flex;
@ -1700,11 +1716,10 @@ export default {
cursor: pointer; cursor: pointer;
} }
/* ========== 自定义下拉面板(向左展开!永远不裁切) ========== */
.lang-drop-panel { .lang-drop-panel {
position: absolute; position: absolute;
top: 100%; top: 100%;
right: 0; /* 面板右边缘和按钮右边缘对齐,**向左展开** */ right: 0;
min-width: 180rpx; min-width: 180rpx;
background: #fff; background: #fff;
border-radius: 12rpx; border-radius: 12rpx;
@ -1714,7 +1729,6 @@ export default {
z-index: 10000; z-index: 10000;
} }
/* 下拉每一个语言选项 */
.lang-drop-item { .lang-drop-item {
padding: 24rpx 30rpx; padding: 24rpx 30rpx;
font-size: 28rpx; font-size: 28rpx;
@ -1733,7 +1747,6 @@ export default {
} }
} }
/* ========== 全局遮罩(点击空白关闭下拉) ========== */
.lang-mask { .lang-mask {
position: fixed; position: fixed;
top: 0; top: 0;
@ -1743,7 +1756,6 @@ export default {
z-index: 9998; z-index: 9998;
} }
/* 原有标题样式不动 */
.nav-title-wrapper { .nav-title-wrapper {
width: 100%; width: 100%;
text-align: center; text-align: center;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -19,21 +19,25 @@
"copyFailed": "Sao chép thất bại", "copyFailed": "Sao chép thất bại",
"downloadFailed": "Tải thất bại", "downloadFailed": "Tải thất bại",
"deleteMessage": "Xóa tin nhắn", "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", "recallMessage": "Thu hồi tin nhắn",
"confirmRecall": "Thu hồi tin này?", "confirmRecall": "Thu hồi tin nhắn này?",
"deleteSuccess": "Xóa thành công", "deleteSuccess": "Đã xóa",
"resendNotSupported": "Không hỗ trợ gửi lại", "resendNotSupported": "Không hỗ trợ gửi lại",
"sendFailed": "Gửi thất bại", "sendFailed": "Gửi thất bại",
"uploadFailed": "Tải lên 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}", "groupBanned": "Bị cấm trong nhóm: {reason}",
"atAll": "Tất cả", "atAll": "Tất cả",
"file": "Tệp", "file": "Tệp",
"album": "Thư viện", "album": "Thư viện",
"camera": "Máy ảnh", "camera": "Máy ảnh",
"noFriends": "Chưa có bạn bè", "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": { "common": {
"confirm": "Xác nhận", "confirm": "Xác nhận",

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

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

105
im-uniapp/store/chatStore.js

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

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

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

21
im-web/src/components/chat/ChatBox.vue

@ -165,6 +165,10 @@
<div class="info-label">来源地址</div> <div class="info-label">来源地址</div>
<div class="info-value">{{ userInfo.platformName }}</div> <div class="info-value">{{ userInfo.platformName }}</div>
</div> </div>
<div class="info-item">
<div class="info-label">用户语言</div>
<div class="info-value">{{ getLanguageText(userInfo.language) }}</div>
</div>
<div class="info-item"> <div class="info-item">
<div class="info-label">标签</div> <div class="info-label">标签</div>
<div class="info-value"> <div class="info-value">
@ -340,6 +344,7 @@ export default {
maxTmpId: 0, maxTmpId: 0,
countryCode: "en", countryCode: "en",
countryCodeList: [ countryCodeList: [
{ label: "中文", value: "zh" },
{ label: "英语", value: "en" }, { label: "英语", value: "en" },
{ label: "日语", value: "jp" }, { label: "日语", value: "jp" },
{ label: "韩语", value: "kor" }, { label: "韩语", value: "kor" },
@ -353,6 +358,22 @@ export default {
}; };
}, },
methods: { methods: {
getLanguageText(lang) {
if (!lang) return '未知';
const langMap = {
'zh': '中文',
'en': '英语',
'jp': '日语',
'kor': '韩语',
'vie': '越南语',
'ru': '俄语',
'de': '德语',
'fra': '法语',
'pt': '葡萄牙语',
'ara': '阿拉伯语',
};
return langMap[lang.toLowerCase()] || lang;
},
moveChatToTop() { moveChatToTop() {
let chatIdx = this.chatStore.findChatIdx(this.chat); let chatIdx = this.chatStore.findChatIdx(this.chat);
this.chatStore.moveTop(chatIdx); this.chatStore.moveTop(chatIdx);

Loading…
Cancel
Save