Merge branch '20260519-3.9.2版本-葛昊天分支'
This commit is contained in:
@@ -495,13 +495,23 @@ jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestStd/components/MesXslRubber
|
|||||||
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestStd/components/MesXslRubberQuickTestMethodSelectModal.vue
|
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestStd/components/MesXslRubberQuickTestMethodSelectModal.vue
|
||||||
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestStd/components/MesXslRubberQuickTestStdMixerPsSelectModal.vue
|
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestStd/components/MesXslRubberQuickTestStdMixerPsSelectModal.vue
|
||||||
|
|
||||||
-- author:jiangxh---date:20260525--for: 【MES】原材料检验标准密炼PS批准时关联胶料快检实验标准置已批准(反审核不回退,触发由审核改为批准)---
|
|
||||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/common/XslMesBizConstants.java
|
|
||||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRubberQuickTestStdService.java
|
|
||||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRubberQuickTestStdServiceImpl.java
|
|
||||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerPsCompileServiceImpl.java
|
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerPsCompileServiceImpl.java
|
||||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRubberQuickTestStdController.java
|
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRubberQuickTestStdController.java
|
||||||
|
|
||||||
|
-- author:xsl---date:20260528--for: 【IM聊天-OA】IM群聊:创建群聊、群列表、群消息收发与未读 ---
|
||||||
|
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_110__sys_im_group_chat.sql
|
||||||
|
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/dto/SysImCreateGroupDTO.java
|
||||||
|
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/entity/SysImConversation.java
|
||||||
|
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/vo/SysImConversationVO.java
|
||||||
|
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/mapper/SysImConversationMapper.java
|
||||||
|
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/mapper/xml/SysImConversationMapper.xml
|
||||||
|
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/ISysImChatService.java
|
||||||
|
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/impl/SysImChatServiceImpl.java
|
||||||
|
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/controller/SysImChatController.java
|
||||||
|
jeecgboot-vue3/src/views/system/im/im.api.ts
|
||||||
|
jeecgboot-vue3/src/views/system/im/ImCreateGroupModal.vue
|
||||||
|
jeecgboot-vue3/src/views/system/im/ImChat.vue
|
||||||
|
|
||||||
-- author:jiangxh---date:20260525--for: 【MES】胶料快检记录主子表、质量管理菜单、胶料信息批量检验生成 ---
|
-- author:jiangxh---date:20260525--for: 【MES】胶料快检记录主子表、质量管理菜单、胶料信息批量检验生成 ---
|
||||||
jeecg-boot/db/mes-xsl-rubber-quick-test-record-menu-permission.sql
|
jeecg-boot/db/mes-xsl-rubber-quick-test-record-menu-permission.sql
|
||||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_108__mes_xsl_rubber_quick_test_record.sql
|
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_108__mes_xsl_rubber_quick_test_record.sql
|
||||||
|
|||||||
@@ -11,10 +11,13 @@ import org.jeecg.common.api.vo.Result;
|
|||||||
import org.jeecg.common.system.vo.LoginUser;
|
import org.jeecg.common.system.vo.LoginUser;
|
||||||
import org.jeecg.common.util.TokenUtils;
|
import org.jeecg.common.util.TokenUtils;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.modules.im.dto.SysImCreateGroupDTO;
|
||||||
|
import org.jeecg.modules.im.dto.SysImGroupMembersDTO;
|
||||||
import org.jeecg.modules.im.dto.SysImSendMessageDTO;
|
import org.jeecg.modules.im.dto.SysImSendMessageDTO;
|
||||||
import org.jeecg.modules.im.service.ISysImChatService;
|
import org.jeecg.modules.im.service.ISysImChatService;
|
||||||
import org.jeecg.modules.im.vo.SysImContactVO;
|
import org.jeecg.modules.im.vo.SysImContactVO;
|
||||||
import org.jeecg.modules.im.vo.SysImConversationVO;
|
import org.jeecg.modules.im.vo.SysImConversationVO;
|
||||||
|
import org.jeecg.modules.im.vo.SysImGroupDetailVO;
|
||||||
import org.jeecg.modules.im.vo.SysImMessageVO;
|
import org.jeecg.modules.im.vo.SysImMessageVO;
|
||||||
import org.jeecg.modules.system.entity.SysUser;
|
import org.jeecg.modules.system.entity.SysUser;
|
||||||
import org.jeecg.modules.system.service.ISysUserService;
|
import org.jeecg.modules.system.service.ISysUserService;
|
||||||
@@ -136,4 +139,87 @@ public class SysImChatController {
|
|||||||
return Result.OK(imChatService.listDeptMembers(user.getId(), resolveTenantId(user, request), user.getOrgCode(), keyword));
|
return Result.OK(imChatService.listDeptMembers(user.getId(), resolveTenantId(user, request), user.getOrgCode(), keyword));
|
||||||
}
|
}
|
||||||
//update-end---author:cursor ---date:20260528 for:【IM聊天-OA】联系人接口(兼容保留,同本部门)-----------
|
//update-end---author:cursor ---date:20260528 for:【IM聊天-OA】联系人接口(兼容保留,同本部门)-----------
|
||||||
|
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天-OA】群聊接口-----------
|
||||||
|
@Operation(summary = "IM聊天-群聊列表")
|
||||||
|
@RequiresPermissions("sys:im:chat:list")
|
||||||
|
@GetMapping("/groups")
|
||||||
|
public Result<List<SysImConversationVO>> groups(HttpServletRequest request) {
|
||||||
|
LoginUser user = currentUser();
|
||||||
|
return Result.OK(imChatService.listGroupConversations(user.getId(), resolveTenantId(user, request)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "IM聊天-创建群聊")
|
||||||
|
@RequiresPermissions("sys:im:chat:group")
|
||||||
|
@PostMapping("/group/create")
|
||||||
|
public Result<SysImConversationVO> createGroup(@RequestBody SysImCreateGroupDTO dto, HttpServletRequest request) {
|
||||||
|
LoginUser user = currentUser();
|
||||||
|
return Result.OK(imChatService.createGroupConversation(user.getId(), resolveTenantId(user, request), user.getOrgCode(), dto));
|
||||||
|
}
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】群聊接口-----------
|
||||||
|
|
||||||
|
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群管理接口-----------
|
||||||
|
@Operation(summary = "IM聊天-群聊详情")
|
||||||
|
@RequiresPermissions("sys:im:chat:list")
|
||||||
|
@GetMapping("/group/detail")
|
||||||
|
public Result<SysImGroupDetailVO> groupDetail(@RequestParam(name = "conversationId") String conversationId) {
|
||||||
|
LoginUser user = currentUser();
|
||||||
|
return Result.OK(imChatService.getGroupDetail(user.getId(), conversationId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "IM聊天-添加群成员")
|
||||||
|
@RequiresPermissions("sys:im:chat:list")
|
||||||
|
@PostMapping("/group/addMembers")
|
||||||
|
public Result<SysImConversationVO> addGroupMembers(@RequestBody SysImGroupMembersDTO dto, HttpServletRequest request) {
|
||||||
|
LoginUser user = currentUser();
|
||||||
|
return Result.OK(imChatService.addGroupMembers(user.getId(), resolveTenantId(user, request), user.getOrgCode(), dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "IM聊天-移除群成员")
|
||||||
|
@RequiresPermissions("sys:im:chat:list")
|
||||||
|
@PostMapping("/group/removeMember")
|
||||||
|
public Result<String> removeGroupMember(@RequestParam(name = "conversationId") String conversationId,
|
||||||
|
@RequestParam(name = "memberUserId") String memberUserId) {
|
||||||
|
LoginUser user = currentUser();
|
||||||
|
imChatService.removeGroupMember(user.getId(), conversationId, memberUserId);
|
||||||
|
return Result.OK("移除成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "IM聊天-修改群名称")
|
||||||
|
@RequiresPermissions("sys:im:chat:list")
|
||||||
|
@PostMapping("/group/rename")
|
||||||
|
public Result<SysImConversationVO> renameGroup(@RequestParam(name = "conversationId") String conversationId,
|
||||||
|
@RequestParam(name = "groupName") String groupName) {
|
||||||
|
LoginUser user = currentUser();
|
||||||
|
return Result.OK(imChatService.renameGroup(user.getId(), conversationId, groupName));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "IM聊天-转让群主")
|
||||||
|
@RequiresPermissions("sys:im:chat:list")
|
||||||
|
@PostMapping("/group/transfer")
|
||||||
|
public Result<String> transferGroupOwner(@RequestParam(name = "conversationId") String conversationId,
|
||||||
|
@RequestParam(name = "newOwnerId") String newOwnerId) {
|
||||||
|
LoginUser user = currentUser();
|
||||||
|
imChatService.transferGroupOwner(user.getId(), conversationId, newOwnerId);
|
||||||
|
return Result.OK("转让成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "IM聊天-退出群聊")
|
||||||
|
@RequiresPermissions("sys:im:chat:list")
|
||||||
|
@PostMapping("/group/quit")
|
||||||
|
public Result<String> quitGroup(@RequestParam(name = "conversationId") String conversationId) {
|
||||||
|
LoginUser user = currentUser();
|
||||||
|
imChatService.quitGroup(user.getId(), conversationId);
|
||||||
|
return Result.OK("已退出群聊");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "IM聊天-解散群聊")
|
||||||
|
@RequiresPermissions("sys:im:chat:list")
|
||||||
|
@PostMapping("/group/dismiss")
|
||||||
|
public Result<String> dismissGroup(@RequestParam(name = "conversationId") String conversationId) {
|
||||||
|
LoginUser user = currentUser();
|
||||||
|
imChatService.dismissGroup(user.getId(), conversationId);
|
||||||
|
return Result.OK("群聊已解散");
|
||||||
|
}
|
||||||
|
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群管理接口-----------
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package org.jeecg.modules.im.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 IM 群聊
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "创建IM群聊")
|
||||||
|
public class SysImCreateGroupDTO {
|
||||||
|
|
||||||
|
@Schema(description = "群名称")
|
||||||
|
private String groupName;
|
||||||
|
|
||||||
|
@Schema(description = "群成员用户ID(不含本人时可自动加入创建人)")
|
||||||
|
private List<String> memberUserIds;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package org.jeecg.modules.im.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IM 群聊添加成员
|
||||||
|
*/
|
||||||
|
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-添加群成员-----------
|
||||||
|
@Data
|
||||||
|
@Schema(description = "IM群聊添加成员")
|
||||||
|
public class SysImGroupMembersDTO {
|
||||||
|
|
||||||
|
@Schema(description = "会话ID")
|
||||||
|
private String conversationId;
|
||||||
|
|
||||||
|
@Schema(description = "新增成员用户ID")
|
||||||
|
private List<String> memberUserIds;
|
||||||
|
}
|
||||||
|
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-添加群成员-----------
|
||||||
@@ -20,10 +20,14 @@ public class SysImConversation implements Serializable {
|
|||||||
|
|
||||||
@TableId(type = IdType.ASSIGN_ID)
|
@TableId(type = IdType.ASSIGN_ID)
|
||||||
private String id;
|
private String id;
|
||||||
/** 会话类型 single单聊 */
|
/** 会话类型 single单聊 group群聊 */
|
||||||
private String convType;
|
private String convType;
|
||||||
/** 单聊唯一键 */
|
/** 单聊唯一键 */
|
||||||
private String userPairKey;
|
private String userPairKey;
|
||||||
|
/** 群名称 */
|
||||||
|
private String groupName;
|
||||||
|
/** 群主用户ID */
|
||||||
|
private String ownerId;
|
||||||
/** 租户ID */
|
/** 租户ID */
|
||||||
private Integer tenantId;
|
private Integer tenantId;
|
||||||
/** 最后一条消息摘要 */
|
/** 最后一条消息摘要 */
|
||||||
|
|||||||
@@ -16,4 +16,9 @@ public interface SysImConversationMapper extends BaseMapper<SysImConversation> {
|
|||||||
* 查询当前用户的会话列表
|
* 查询当前用户的会话列表
|
||||||
*/
|
*/
|
||||||
List<SysImConversationVO> listMyConversations(@Param("userId") String userId, @Param("tenantId") Integer tenantId);
|
List<SysImConversationVO> listMyConversations(@Param("userId") String userId, @Param("tenantId") Integer tenantId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户的群聊列表
|
||||||
|
*/
|
||||||
|
List<SysImConversationVO> listMyGroupConversations(@Param("userId") String userId, @Param("tenantId") Integer tenantId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,4 +23,26 @@
|
|||||||
ORDER BY c.last_time DESC
|
ORDER BY c.last_time DESC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="listMyGroupConversations" resultType="org.jeecg.modules.im.vo.SysImConversationVO">
|
||||||
|
SELECT
|
||||||
|
c.id AS conversationId,
|
||||||
|
c.conv_type AS convType,
|
||||||
|
c.group_name AS groupName,
|
||||||
|
c.owner_id AS ownerId,
|
||||||
|
c.last_content AS lastContent,
|
||||||
|
c.last_time AS lastTime,
|
||||||
|
m.unread_count AS unreadCount,
|
||||||
|
(
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM sys_im_conversation_member gm
|
||||||
|
WHERE gm.conversation_id = c.id
|
||||||
|
) AS memberCount
|
||||||
|
FROM sys_im_conversation_member m
|
||||||
|
INNER JOIN sys_im_conversation c ON c.id = m.conversation_id
|
||||||
|
WHERE m.user_id = #{userId}
|
||||||
|
AND c.tenant_id = #{tenantId}
|
||||||
|
AND c.conv_type = 'group'
|
||||||
|
ORDER BY c.last_time DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ package org.jeecg.modules.im.service;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
|
import org.jeecg.modules.im.dto.SysImCreateGroupDTO;
|
||||||
|
import org.jeecg.modules.im.dto.SysImGroupMembersDTO;
|
||||||
import org.jeecg.modules.im.dto.SysImSendMessageDTO;
|
import org.jeecg.modules.im.dto.SysImSendMessageDTO;
|
||||||
|
|
||||||
import org.jeecg.modules.im.vo.SysImContactVO;
|
import org.jeecg.modules.im.vo.SysImContactVO;
|
||||||
|
|
||||||
import org.jeecg.modules.im.vo.SysImConversationVO;
|
import org.jeecg.modules.im.vo.SysImConversationVO;
|
||||||
|
|
||||||
|
import org.jeecg.modules.im.vo.SysImGroupDetailVO;
|
||||||
|
|
||||||
import org.jeecg.modules.im.vo.SysImMessageVO;
|
import org.jeecg.modules.im.vo.SysImMessageVO;
|
||||||
|
|
||||||
|
|
||||||
@@ -34,7 +38,9 @@ public interface ISysImChatService {
|
|||||||
|
|
||||||
SysImConversationVO openSingleConversation(String userId, Integer tenantId, String orgCode, String targetUserId);
|
SysImConversationVO openSingleConversation(String userId, Integer tenantId, String orgCode, String targetUserId);
|
||||||
|
|
||||||
|
List<SysImConversationVO> listGroupConversations(String userId, Integer tenantId);
|
||||||
|
|
||||||
|
SysImConversationVO createGroupConversation(String userId, Integer tenantId, String orgCode, SysImCreateGroupDTO dto);
|
||||||
|
|
||||||
IPage<SysImMessageVO> listMessages(String userId, String conversationId, Integer pageNo, Integer pageSize, String startTime, String beforeTime);
|
IPage<SysImMessageVO> listMessages(String userId, String conversationId, Integer pageNo, Integer pageSize, String startTime, String beforeTime);
|
||||||
|
|
||||||
@@ -50,5 +56,28 @@ public interface ISysImChatService {
|
|||||||
|
|
||||||
List<SysImContactVO> listDeptMembers(String userId, Integer tenantId, String orgCode, String keyword);
|
List<SysImContactVO> listDeptMembers(String userId, Integer tenantId, String orgCode, String keyword);
|
||||||
|
|
||||||
|
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群管理接口-----------
|
||||||
|
/** 群聊详情(含成员列表,区分群主) */
|
||||||
|
SysImGroupDetailVO getGroupDetail(String userId, String conversationId);
|
||||||
|
|
||||||
|
/** 添加群成员(所有群成员可操作) */
|
||||||
|
SysImConversationVO addGroupMembers(String userId, Integer tenantId, String orgCode, SysImGroupMembersDTO dto);
|
||||||
|
|
||||||
|
/** 移除群成员(仅群主) */
|
||||||
|
void removeGroupMember(String userId, String conversationId, String memberUserId);
|
||||||
|
|
||||||
|
/** 修改群名称(仅群主) */
|
||||||
|
SysImConversationVO renameGroup(String userId, String conversationId, String groupName);
|
||||||
|
|
||||||
|
/** 转让群主(仅群主) */
|
||||||
|
void transferGroupOwner(String userId, String conversationId, String newOwnerId);
|
||||||
|
|
||||||
|
/** 退出群聊(非群主成员) */
|
||||||
|
void quitGroup(String userId, String conversationId);
|
||||||
|
|
||||||
|
/** 解散群聊(仅群主) */
|
||||||
|
void dismissGroup(String userId, String conversationId);
|
||||||
|
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群管理接口-----------
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import org.jeecg.common.constant.WebsocketConst;
|
|||||||
import org.jeecg.common.exception.JeecgBootException;
|
import org.jeecg.common.exception.JeecgBootException;
|
||||||
import org.jeecg.common.util.DateUtils;
|
import org.jeecg.common.util.DateUtils;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.modules.im.dto.SysImCreateGroupDTO;
|
||||||
|
import org.jeecg.modules.im.dto.SysImGroupMembersDTO;
|
||||||
import org.jeecg.modules.im.dto.SysImSendMessageDTO;
|
import org.jeecg.modules.im.dto.SysImSendMessageDTO;
|
||||||
import org.jeecg.modules.im.entity.SysImConversation;
|
import org.jeecg.modules.im.entity.SysImConversation;
|
||||||
import org.jeecg.modules.im.entity.SysImConversationMember;
|
import org.jeecg.modules.im.entity.SysImConversationMember;
|
||||||
@@ -19,6 +21,8 @@ import org.jeecg.modules.im.mapper.SysImMessageMapper;
|
|||||||
import org.jeecg.modules.im.service.ISysImChatService;
|
import org.jeecg.modules.im.service.ISysImChatService;
|
||||||
import org.jeecg.modules.im.vo.SysImContactVO;
|
import org.jeecg.modules.im.vo.SysImContactVO;
|
||||||
import org.jeecg.modules.im.vo.SysImConversationVO;
|
import org.jeecg.modules.im.vo.SysImConversationVO;
|
||||||
|
import org.jeecg.modules.im.vo.SysImGroupDetailVO;
|
||||||
|
import org.jeecg.modules.im.vo.SysImGroupMemberVO;
|
||||||
import org.jeecg.modules.im.vo.SysImMessageVO;
|
import org.jeecg.modules.im.vo.SysImMessageVO;
|
||||||
import org.jeecg.modules.message.websocket.WebSocket;
|
import org.jeecg.modules.message.websocket.WebSocket;
|
||||||
import org.jeecg.modules.system.entity.SysDepart;
|
import org.jeecg.modules.system.entity.SysDepart;
|
||||||
@@ -41,6 +45,8 @@ import java.util.Date;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,6 +57,7 @@ import java.util.stream.Collectors;
|
|||||||
public class SysImChatServiceImpl implements ISysImChatService {
|
public class SysImChatServiceImpl implements ISysImChatService {
|
||||||
|
|
||||||
private static final String CONV_TYPE_SINGLE = "single";
|
private static final String CONV_TYPE_SINGLE = "single";
|
||||||
|
private static final String CONV_TYPE_GROUP = "group";
|
||||||
private static final String MSG_TYPE_TEXT = "text";
|
private static final String MSG_TYPE_TEXT = "text";
|
||||||
private static final String MSG_TYPE_IMAGE = "image";
|
private static final String MSG_TYPE_IMAGE = "image";
|
||||||
private static final String MSG_TYPE_BIZ_RECORD = "biz_record";
|
private static final String MSG_TYPE_BIZ_RECORD = "biz_record";
|
||||||
@@ -116,6 +123,271 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
|||||||
}
|
}
|
||||||
//update-end---author:cursor ---date:20260528 for:【IM聊天-OA】打开单聊会话-----------
|
//update-end---author:cursor ---date:20260528 for:【IM聊天-OA】打开单聊会话-----------
|
||||||
|
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天-OA】群聊会话列表与创建-----------
|
||||||
|
@Override
|
||||||
|
public List<SysImConversationVO> listGroupConversations(String userId, Integer tenantId) {
|
||||||
|
if (oConvertUtils.isEmpty(userId) || tenantId == null || tenantId <= 0) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return conversationMapper.listMyGroupConversations(userId, tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public SysImConversationVO createGroupConversation(String userId, Integer tenantId, String orgCode, SysImCreateGroupDTO dto) {
|
||||||
|
if (dto == null || oConvertUtils.isEmpty(dto.getGroupName())) {
|
||||||
|
throw new JeecgBootException("群名称不能为空");
|
||||||
|
}
|
||||||
|
String groupName = dto.getGroupName().trim();
|
||||||
|
if (groupName.length() > 30) {
|
||||||
|
throw new JeecgBootException("群名称不能超过30字");
|
||||||
|
}
|
||||||
|
List<String> memberIds = normalizeGroupMemberIds(userId, dto.getMemberUserIds());
|
||||||
|
if (memberIds.size() < 2) {
|
||||||
|
throw new JeecgBootException("群聊至少需要2名成员");
|
||||||
|
}
|
||||||
|
if (memberIds.size() > 50) {
|
||||||
|
throw new JeecgBootException("群成员不能超过50人");
|
||||||
|
}
|
||||||
|
for (String memberId : memberIds) {
|
||||||
|
if (userId.equals(memberId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
validateTenantChat(userId, tenantId, orgCode, memberId);
|
||||||
|
}
|
||||||
|
Date now = new Date();
|
||||||
|
SysImConversation conversation = new SysImConversation();
|
||||||
|
conversation.setConvType(CONV_TYPE_GROUP);
|
||||||
|
conversation.setGroupName(groupName);
|
||||||
|
conversation.setOwnerId(userId);
|
||||||
|
conversation.setTenantId(tenantId);
|
||||||
|
conversation.setCreateBy(userId);
|
||||||
|
conversation.setCreateTime(now);
|
||||||
|
conversation.setUpdateTime(now);
|
||||||
|
conversationMapper.insert(conversation);
|
||||||
|
for (String memberId : memberIds) {
|
||||||
|
createMember(conversation.getId(), memberId, now);
|
||||||
|
}
|
||||||
|
return buildGroupConversationVo(conversation, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> normalizeGroupMemberIds(String creatorId, List<String> memberUserIds) {
|
||||||
|
Set<String> memberIds = new LinkedHashSet<>();
|
||||||
|
memberIds.add(creatorId);
|
||||||
|
if (memberUserIds != null) {
|
||||||
|
for (String memberId : memberUserIds) {
|
||||||
|
if (oConvertUtils.isNotEmpty(memberId)) {
|
||||||
|
memberIds.add(memberId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ArrayList<>(memberIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysImConversationVO buildGroupConversationVo(SysImConversation conversation, String userId) {
|
||||||
|
SysImConversationMember member = getMember(userId, conversation.getId());
|
||||||
|
SysImConversationVO vo = new SysImConversationVO();
|
||||||
|
vo.setConversationId(conversation.getId());
|
||||||
|
vo.setConvType(CONV_TYPE_GROUP);
|
||||||
|
vo.setGroupName(conversation.getGroupName());
|
||||||
|
vo.setOwnerId(conversation.getOwnerId());
|
||||||
|
vo.setLastContent(conversation.getLastContent());
|
||||||
|
vo.setLastTime(conversation.getLastTime());
|
||||||
|
vo.setUnreadCount(member == null ? 0 : member.getUnreadCount());
|
||||||
|
Long memberCount = memberMapper.selectCount(new LambdaQueryWrapper<SysImConversationMember>()
|
||||||
|
.eq(SysImConversationMember::getConversationId, conversation.getId()));
|
||||||
|
vo.setMemberCount(memberCount == null ? 0 : memberCount.intValue());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】群聊会话列表与创建-----------
|
||||||
|
|
||||||
|
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群管理-----------
|
||||||
|
private static final int GROUP_MEMBER_MAX = 50;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SysImGroupDetailVO getGroupDetail(String userId, String conversationId) {
|
||||||
|
SysImConversation conversation = assertGroupConversation(userId, conversationId);
|
||||||
|
List<SysImConversationMember> members = memberMapper.selectList(new LambdaQueryWrapper<SysImConversationMember>()
|
||||||
|
.eq(SysImConversationMember::getConversationId, conversationId)
|
||||||
|
.orderByAsc(SysImConversationMember::getCreateTime));
|
||||||
|
List<String> memberUserIds = members.stream()
|
||||||
|
.map(SysImConversationMember::getUserId)
|
||||||
|
.filter(oConvertUtils::isNotEmpty)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
Map<String, SysUser> userMap = new HashMap<>(memberUserIds.size());
|
||||||
|
if (!memberUserIds.isEmpty()) {
|
||||||
|
List<SysUser> users = userMapper.selectBatchIds(memberUserIds);
|
||||||
|
if (users != null) {
|
||||||
|
for (SysUser user : users) {
|
||||||
|
userMap.put(user.getId(), user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String ownerId = conversation.getOwnerId();
|
||||||
|
List<SysImGroupMemberVO> memberVoList = new ArrayList<>(members.size());
|
||||||
|
for (SysImConversationMember member : members) {
|
||||||
|
SysUser user = userMap.get(member.getUserId());
|
||||||
|
SysImGroupMemberVO memberVo = new SysImGroupMemberVO();
|
||||||
|
memberVo.setUserId(member.getUserId());
|
||||||
|
memberVo.setOwner(member.getUserId() != null && member.getUserId().equals(ownerId));
|
||||||
|
if (user != null) {
|
||||||
|
memberVo.setRealname(user.getRealname());
|
||||||
|
memberVo.setUsername(user.getUsername());
|
||||||
|
memberVo.setAvatar(user.getAvatar());
|
||||||
|
}
|
||||||
|
memberVoList.add(memberVo);
|
||||||
|
}
|
||||||
|
// 群主排在最前
|
||||||
|
memberVoList.sort(Comparator.comparingInt(item -> Boolean.TRUE.equals(item.getOwner()) ? 0 : 1));
|
||||||
|
SysImGroupDetailVO detail = new SysImGroupDetailVO();
|
||||||
|
detail.setConversationId(conversation.getId());
|
||||||
|
detail.setGroupName(conversation.getGroupName());
|
||||||
|
detail.setOwnerId(ownerId);
|
||||||
|
detail.setMemberCount(memberVoList.size());
|
||||||
|
detail.setOwner(userId.equals(ownerId));
|
||||||
|
detail.setMembers(memberVoList);
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public SysImConversationVO addGroupMembers(String userId, Integer tenantId, String orgCode, SysImGroupMembersDTO dto) {
|
||||||
|
if (dto == null || oConvertUtils.isEmpty(dto.getConversationId())) {
|
||||||
|
throw new JeecgBootException("会话不存在");
|
||||||
|
}
|
||||||
|
SysImConversation conversation = assertGroupConversation(userId, dto.getConversationId());
|
||||||
|
if (dto.getMemberUserIds() == null || dto.getMemberUserIds().isEmpty()) {
|
||||||
|
throw new JeecgBootException("请选择要添加的成员");
|
||||||
|
}
|
||||||
|
// 已在群成员
|
||||||
|
Set<String> existIds = memberMapper.selectList(new LambdaQueryWrapper<SysImConversationMember>()
|
||||||
|
.eq(SysImConversationMember::getConversationId, conversation.getId()))
|
||||||
|
.stream().map(SysImConversationMember::getUserId).collect(Collectors.toSet());
|
||||||
|
// 去重并过滤已存在成员
|
||||||
|
List<String> toAdd = new ArrayList<>();
|
||||||
|
Set<String> seen = new LinkedHashSet<>();
|
||||||
|
for (String memberId : dto.getMemberUserIds()) {
|
||||||
|
if (oConvertUtils.isEmpty(memberId) || existIds.contains(memberId) || !seen.add(memberId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
toAdd.add(memberId);
|
||||||
|
}
|
||||||
|
if (toAdd.isEmpty()) {
|
||||||
|
throw new JeecgBootException("所选成员已在群内");
|
||||||
|
}
|
||||||
|
if (existIds.size() + toAdd.size() > GROUP_MEMBER_MAX) {
|
||||||
|
throw new JeecgBootException("群成员不能超过" + GROUP_MEMBER_MAX + "人");
|
||||||
|
}
|
||||||
|
// 校验同租户、同部门
|
||||||
|
for (String memberId : toAdd) {
|
||||||
|
validateTenantChat(userId, tenantId, orgCode, memberId);
|
||||||
|
}
|
||||||
|
Date now = new Date();
|
||||||
|
for (String memberId : toAdd) {
|
||||||
|
createMember(conversation.getId(), memberId, now);
|
||||||
|
}
|
||||||
|
return buildGroupConversationVo(conversation, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void removeGroupMember(String userId, String conversationId, String memberUserId) {
|
||||||
|
SysImConversation conversation = assertGroupConversation(userId, conversationId);
|
||||||
|
assertGroupOwner(userId, conversation);
|
||||||
|
if (oConvertUtils.isEmpty(memberUserId)) {
|
||||||
|
throw new JeecgBootException("请选择要移除的成员");
|
||||||
|
}
|
||||||
|
if (memberUserId.equals(conversation.getOwnerId())) {
|
||||||
|
throw new JeecgBootException("群主不能被移除");
|
||||||
|
}
|
||||||
|
memberMapper.delete(new LambdaQueryWrapper<SysImConversationMember>()
|
||||||
|
.eq(SysImConversationMember::getConversationId, conversationId)
|
||||||
|
.eq(SysImConversationMember::getUserId, memberUserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public SysImConversationVO renameGroup(String userId, String conversationId, String groupName) {
|
||||||
|
SysImConversation conversation = assertGroupConversation(userId, conversationId);
|
||||||
|
assertGroupOwner(userId, conversation);
|
||||||
|
if (oConvertUtils.isEmpty(groupName)) {
|
||||||
|
throw new JeecgBootException("群名称不能为空");
|
||||||
|
}
|
||||||
|
String name = groupName.trim();
|
||||||
|
if (name.length() > 30) {
|
||||||
|
throw new JeecgBootException("群名称不能超过30字");
|
||||||
|
}
|
||||||
|
conversation.setGroupName(name);
|
||||||
|
conversation.setUpdateBy(userId);
|
||||||
|
conversation.setUpdateTime(new Date());
|
||||||
|
conversationMapper.updateById(conversation);
|
||||||
|
return buildGroupConversationVo(conversation, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void transferGroupOwner(String userId, String conversationId, String newOwnerId) {
|
||||||
|
SysImConversation conversation = assertGroupConversation(userId, conversationId);
|
||||||
|
assertGroupOwner(userId, conversation);
|
||||||
|
if (oConvertUtils.isEmpty(newOwnerId)) {
|
||||||
|
throw new JeecgBootException("请选择新群主");
|
||||||
|
}
|
||||||
|
if (newOwnerId.equals(userId)) {
|
||||||
|
throw new JeecgBootException("不能转让给自己");
|
||||||
|
}
|
||||||
|
if (getMember(newOwnerId, conversationId) == null) {
|
||||||
|
throw new JeecgBootException("新群主必须是群成员");
|
||||||
|
}
|
||||||
|
conversation.setOwnerId(newOwnerId);
|
||||||
|
conversation.setUpdateBy(userId);
|
||||||
|
conversation.setUpdateTime(new Date());
|
||||||
|
conversationMapper.updateById(conversation);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void quitGroup(String userId, String conversationId) {
|
||||||
|
SysImConversation conversation = assertGroupConversation(userId, conversationId);
|
||||||
|
if (userId.equals(conversation.getOwnerId())) {
|
||||||
|
throw new JeecgBootException("群主不能退出群聊,请先转让群主或解散群聊");
|
||||||
|
}
|
||||||
|
memberMapper.delete(new LambdaQueryWrapper<SysImConversationMember>()
|
||||||
|
.eq(SysImConversationMember::getConversationId, conversationId)
|
||||||
|
.eq(SysImConversationMember::getUserId, userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void dismissGroup(String userId, String conversationId) {
|
||||||
|
SysImConversation conversation = assertGroupConversation(userId, conversationId);
|
||||||
|
assertGroupOwner(userId, conversation);
|
||||||
|
memberMapper.delete(new LambdaQueryWrapper<SysImConversationMember>()
|
||||||
|
.eq(SysImConversationMember::getConversationId, conversationId));
|
||||||
|
conversationMapper.deleteById(conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验会话存在、为群聊且当前用户是群成员 */
|
||||||
|
private SysImConversation assertGroupConversation(String userId, String conversationId) {
|
||||||
|
if (oConvertUtils.isEmpty(conversationId)) {
|
||||||
|
throw new JeecgBootException("会话不存在");
|
||||||
|
}
|
||||||
|
SysImConversation conversation = conversationMapper.selectById(conversationId);
|
||||||
|
if (conversation == null || !CONV_TYPE_GROUP.equals(conversation.getConvType())) {
|
||||||
|
throw new JeecgBootException("群聊不存在");
|
||||||
|
}
|
||||||
|
assertMember(userId, conversationId);
|
||||||
|
return conversation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验当前用户是群主 */
|
||||||
|
private void assertGroupOwner(String userId, SysImConversation conversation) {
|
||||||
|
if (!userId.equals(conversation.getOwnerId())) {
|
||||||
|
throw new JeecgBootException("仅群主可执行该操作");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群管理-----------
|
||||||
|
|
||||||
//update-begin---author:cursor ---date:20260528 for:【IM聊天-OA】消息分页-----------
|
//update-begin---author:cursor ---date:20260528 for:【IM聊天-OA】消息分页-----------
|
||||||
@Override
|
@Override
|
||||||
public IPage<SysImMessageVO> listMessages(String userId, String conversationId, Integer pageNo, Integer pageSize, String startTime, String beforeTime) {
|
public IPage<SysImMessageVO> listMessages(String userId, String conversationId, Integer pageNo, Integer pageSize, String startTime, String beforeTime) {
|
||||||
@@ -179,7 +451,15 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
|||||||
|
|
||||||
SysImConversation conversation = conversationMapper.selectById(dto.getConversationId());
|
SysImConversation conversation = conversationMapper.selectById(dto.getConversationId());
|
||||||
//update-begin---author:cursor ---date:20260528 for:【IM聊天-OA】图片消息会话摘要-----------
|
//update-begin---author:cursor ---date:20260528 for:【IM聊天-OA】图片消息会话摘要-----------
|
||||||
conversation.setLastContent(truncate(resolveLastContent(message.getMsgType(), message.getContent()), 200));
|
String lastContent = resolveLastContent(message.getMsgType(), message.getContent());
|
||||||
|
if (CONV_TYPE_GROUP.equals(conversation.getConvType())) {
|
||||||
|
SysUser sender = userMapper.selectById(userId);
|
||||||
|
String senderName = sender != null ? oConvertUtils.getString(sender.getRealname(), sender.getUsername()) : "";
|
||||||
|
if (oConvertUtils.isNotEmpty(senderName)) {
|
||||||
|
lastContent = senderName + ": " + lastContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conversation.setLastContent(truncate(lastContent, 200));
|
||||||
//update-end---author:cursor ---date:20260528 for:【IM聊天-OA】图片消息会话摘要-----------
|
//update-end---author:cursor ---date:20260528 for:【IM聊天-OA】图片消息会话摘要-----------
|
||||||
conversation.setLastTime(now);
|
conversation.setLastTime(now);
|
||||||
conversation.setUpdateTime(now);
|
conversation.setUpdateTime(now);
|
||||||
@@ -188,7 +468,7 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
|||||||
memberMapper.incrementUnreadExceptSender(dto.getConversationId(), userId);
|
memberMapper.incrementUnreadExceptSender(dto.getConversationId(), userId);
|
||||||
SysImMessageVO messageVo = toMessageVo(message, userId);
|
SysImMessageVO messageVo = toMessageVo(message, userId);
|
||||||
fillBizRecordReceiverPermission(messageVo, message, userId, new HashMap<>(4));
|
fillBizRecordReceiverPermission(messageVo, message, userId, new HashMap<>(4));
|
||||||
pushChatMessage(dto.getConversationId(), userId, messageVo);
|
pushChatMessage(dto.getConversationId(), userId, messageVo, conversation.getConvType());
|
||||||
return messageVo;
|
return messageVo;
|
||||||
}
|
}
|
||||||
//update-end---author:cursor ---date:20260528 for:【IM聊天-OA】发送消息-----------
|
//update-end---author:cursor ---date:20260528 for:【IM聊天-OA】发送消息-----------
|
||||||
@@ -422,6 +702,10 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
|||||||
if (!Boolean.TRUE.equals(vo.getMine()) || !MSG_TYPE_BIZ_RECORD.equals(vo.getMsgType())) {
|
if (!Boolean.TRUE.equals(vo.getMine()) || !MSG_TYPE_BIZ_RECORD.equals(vo.getMsgType())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
SysImConversation conversation = conversationMapper.selectById(message.getConversationId());
|
||||||
|
if (conversation == null || !CONV_TYPE_SINGLE.equals(conversation.getConvType())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
String pagePath = extractBizRecordPagePath(vo.getContent());
|
String pagePath = extractBizRecordPagePath(vo.getContent());
|
||||||
if (oConvertUtils.isEmpty(pagePath)) {
|
if (oConvertUtils.isEmpty(pagePath)) {
|
||||||
return;
|
return;
|
||||||
@@ -509,7 +793,7 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
|||||||
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】发送方提示接收方无功能权限-----------
|
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】发送方提示接收方无功能权限-----------
|
||||||
//update-end---author:cursor ---date:20260528 for:【IM聊天-OA】消息列表批量填充发送人-----------
|
//update-end---author:cursor ---date:20260528 for:【IM聊天-OA】消息列表批量填充发送人-----------
|
||||||
|
|
||||||
private void pushChatMessage(String conversationId, String senderId, SysImMessageVO messageVo) {
|
private void pushChatMessage(String conversationId, String senderId, SysImMessageVO messageVo, String convType) {
|
||||||
List<SysImConversationMember> members = memberMapper.selectList(new LambdaQueryWrapper<SysImConversationMember>()
|
List<SysImConversationMember> members = memberMapper.selectList(new LambdaQueryWrapper<SysImConversationMember>()
|
||||||
.eq(SysImConversationMember::getConversationId, conversationId));
|
.eq(SysImConversationMember::getConversationId, conversationId));
|
||||||
for (SysImConversationMember member : members) {
|
for (SysImConversationMember member : members) {
|
||||||
@@ -520,6 +804,9 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
|||||||
obj.put(WebsocketConst.MSG_CMD, WebsocketConst.MSG_CHAT);
|
obj.put(WebsocketConst.MSG_CMD, WebsocketConst.MSG_CHAT);
|
||||||
obj.put(WebsocketConst.MSG_USER_ID, member.getUserId());
|
obj.put(WebsocketConst.MSG_USER_ID, member.getUserId());
|
||||||
obj.put("conversationId", conversationId);
|
obj.put("conversationId", conversationId);
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天-OA】WebSocket推送会话类型区分群聊-----------
|
||||||
|
obj.put("convType", convType);
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】WebSocket推送会话类型区分群聊-----------
|
||||||
obj.put("messageId", messageVo.getId());
|
obj.put("messageId", messageVo.getId());
|
||||||
obj.put("senderId", messageVo.getSenderId());
|
obj.put("senderId", messageVo.getSenderId());
|
||||||
//update-begin---author:cursor ---date:20260528 for:【IM聊天-OA】WebSocket推送补全头像字段-----------
|
//update-begin---author:cursor ---date:20260528 for:【IM聊天-OA】WebSocket推送补全头像字段-----------
|
||||||
|
|||||||
@@ -34,4 +34,10 @@ public class SysImConversationVO {
|
|||||||
private String targetUsername;
|
private String targetUsername;
|
||||||
@Schema(description = "对方头像")
|
@Schema(description = "对方头像")
|
||||||
private String targetAvatar;
|
private String targetAvatar;
|
||||||
|
@Schema(description = "群名称")
|
||||||
|
private String groupName;
|
||||||
|
@Schema(description = "群成员数")
|
||||||
|
private Integer memberCount;
|
||||||
|
@Schema(description = "群主用户ID")
|
||||||
|
private String ownerId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package org.jeecg.modules.im.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IM 群聊详情(群设置)
|
||||||
|
*/
|
||||||
|
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群详情-----------
|
||||||
|
@Data
|
||||||
|
@Schema(description = "IM群聊详情")
|
||||||
|
public class SysImGroupDetailVO {
|
||||||
|
|
||||||
|
@Schema(description = "会话ID")
|
||||||
|
private String conversationId;
|
||||||
|
@Schema(description = "群名称")
|
||||||
|
private String groupName;
|
||||||
|
@Schema(description = "群主用户ID")
|
||||||
|
private String ownerId;
|
||||||
|
@Schema(description = "群成员数")
|
||||||
|
private Integer memberCount;
|
||||||
|
@Schema(description = "当前用户是否群主")
|
||||||
|
private Boolean owner;
|
||||||
|
@Schema(description = "群成员列表")
|
||||||
|
private List<SysImGroupMemberVO> members;
|
||||||
|
}
|
||||||
|
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群详情-----------
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package org.jeecg.modules.im.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IM 群成员
|
||||||
|
*/
|
||||||
|
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群成员展示-----------
|
||||||
|
@Data
|
||||||
|
@Schema(description = "IM群成员")
|
||||||
|
public class SysImGroupMemberVO {
|
||||||
|
|
||||||
|
@Schema(description = "用户ID")
|
||||||
|
private String userId;
|
||||||
|
@Schema(description = "姓名")
|
||||||
|
private String realname;
|
||||||
|
@Schema(description = "账号")
|
||||||
|
private String username;
|
||||||
|
@Schema(description = "头像")
|
||||||
|
private String avatar;
|
||||||
|
@Schema(description = "是否群主")
|
||||||
|
private Boolean owner;
|
||||||
|
}
|
||||||
|
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-群成员展示-----------
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
-- IM 群聊:扩展会话表字段
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
ALTER TABLE `sys_im_conversation`
|
||||||
|
ADD COLUMN `group_name` varchar(100) DEFAULT NULL COMMENT '群名称' AFTER `user_pair_key`,
|
||||||
|
ADD COLUMN `owner_id` varchar(32) DEFAULT NULL COMMENT '群主用户ID' AFTER `group_name`;
|
||||||
|
|
||||||
|
ALTER TABLE `sys_im_conversation`
|
||||||
|
MODIFY COLUMN `conv_type` varchar(10) NOT NULL DEFAULT 'single' COMMENT '会话类型 single单聊 group群聊';
|
||||||
|
|
||||||
|
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
|
||||||
|
VALUES ('1995000000000000113', '1995000000000000110', '创建群聊', 2, 'sys:im:chat:group', '1', 3.00, 0, 1, 0, '1', 0, 'admin', NOW());
|
||||||
|
|
||||||
|
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
|
||||||
|
SELECT REPLACE(UUID(), '-', ''), r.id, p.id, NULL, NOW(), '127.0.0.1'
|
||||||
|
FROM `sys_role` r
|
||||||
|
CROSS JOIN `sys_permission` p
|
||||||
|
WHERE r.`role_code` = 'admin'
|
||||||
|
AND p.`id` = '1995000000000000113'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
|
||||||
|
);
|
||||||
@@ -10,6 +10,15 @@ import { useUserStore } from '/@/store/modules/user';
|
|||||||
let result: WebSocketResult<any>;
|
let result: WebSocketResult<any>;
|
||||||
const listeners = new Map();
|
const listeners = new Map();
|
||||||
let connectedUrl = '';
|
let connectedUrl = '';
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】WS 重连后通知监听方重新拉取消息-----------
|
||||||
|
let wsConnectionCount = 0;
|
||||||
|
const reconnectListeners = new Set<() => void>();
|
||||||
|
|
||||||
|
export function onWebSocketReconnect(callback: () => void) {
|
||||||
|
reconnectListeners.add(callback);
|
||||||
|
return () => reconnectListeners.delete(callback);
|
||||||
|
}
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】WS 重连后通知监听方重新拉取消息-----------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建系统 WebSocket 地址(含 context-path,如 /jeecg-boot)
|
* 构建系统 WebSocket 地址(含 context-path,如 /jeecg-boot)
|
||||||
@@ -67,7 +76,15 @@ export function connectWebSocket(url: string) {
|
|||||||
protocols: [token],
|
protocols: [token],
|
||||||
// 代码逻辑说明: [issues/6662] 演示系统socket总断,换一个写法
|
// 代码逻辑说明: [issues/6662] 演示系统socket总断,换一个写法
|
||||||
onConnected: function (ws) {
|
onConnected: function (ws) {
|
||||||
|
wsConnectionCount++;
|
||||||
console.log('[WebSocket] 连接成功', ws);
|
console.log('[WebSocket] 连接成功', ws);
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】WS 重连后通知监听方重新拉取消息-----------
|
||||||
|
if (wsConnectionCount > 1) {
|
||||||
|
reconnectListeners.forEach((cb) => {
|
||||||
|
try { cb(); } catch (err) { console.error(err); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】WS 重连后通知监听方重新拉取消息-----------
|
||||||
},
|
},
|
||||||
onDisconnected: function (ws, event) {
|
onDisconnected: function (ws, event) {
|
||||||
console.log('[WebSocket] 连接断开:', ws, event);
|
console.log('[WebSocket] 连接断开:', ws, event);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@
|
|||||||
import { ref, nextTick, onMounted, onUnmounted } from 'vue';
|
import { ref, nextTick, onMounted, onUnmounted } from 'vue';
|
||||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||||
import ImChat from './ImChat.vue';
|
import ImChat from './ImChat.vue';
|
||||||
import { setImChatWindowOpen } from './imCache';
|
import { setImChatWindowOpen, setImActiveTargetUserId } from './imCache';
|
||||||
import { canOpenImChatModal, onImChatModalCloseRequest, openImChat, setImPageContext } from './imSession';
|
import { canOpenImChatModal, onImChatModalCloseRequest, openImChat, setImPageContext } from './imSession';
|
||||||
import { refreshImUnread } from './useImUnread';
|
import { refreshImUnread } from './useImUnread';
|
||||||
|
|
||||||
@@ -28,29 +28,53 @@
|
|||||||
|
|
||||||
const imChatRef = ref<InstanceType<typeof ImChat>>();
|
const imChatRef = ref<InstanceType<typeof ImChat>>();
|
||||||
const pendingTargetUserId = ref('');
|
const pendingTargetUserId = ref('');
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】支持从消息提醒直接打开群聊-----------
|
||||||
|
const pendingConversationId = ref('');
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】支持从消息提醒直接打开群聊-----------
|
||||||
|
|
||||||
function restoreChatSession() {
|
function restoreChatSession() {
|
||||||
nextTick(async () => {
|
nextTick(async () => {
|
||||||
const targetUserId = pendingTargetUserId.value;
|
const targetUserId = pendingTargetUserId.value;
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】支持从消息提醒直接打开群聊-----------
|
||||||
|
const conversationId = pendingConversationId.value;
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】支持从消息提醒直接打开群聊-----------
|
||||||
if (targetUserId) {
|
if (targetUserId) {
|
||||||
pendingTargetUserId.value = '';
|
pendingTargetUserId.value = '';
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】预设本地活跃会话,防止 WS 消息在异步初始化期间丢失-----------
|
||||||
|
imChatRef.value?.presetActivePeer?.(targetUserId);
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】预设本地活跃会话,防止 WS 消息在异步初始化期间丢失-----------
|
||||||
await imChatRef.value?.openTargetChat?.(targetUserId);
|
await imChatRef.value?.openTargetChat?.(targetUserId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】支持从消息提醒直接打开群聊-----------
|
||||||
|
if (conversationId) {
|
||||||
|
pendingConversationId.value = '';
|
||||||
|
await imChatRef.value?.openGroupConversation?.(conversationId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】支持从消息提醒直接打开群聊-----------
|
||||||
imChatRef.value?.restoreSessionIfNeeded?.();
|
imChatRef.value?.restoreSessionIfNeeded?.();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const [registerModal, { closeModal }] = useModalInner((data?: { targetUserId?: string }) => {
|
const [registerModal, { closeModal }] = useModalInner((data?: { targetUserId?: string; conversationId?: string }) => {
|
||||||
//update-begin---author:xsl ---date:20260528 for:【IM聊天-OA】全页 IM 已打开时拒绝弹窗,改由全页承接-----------
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天-OA】全页 IM 已打开时拒绝弹窗,改由全页承接-----------
|
||||||
if (!canOpenImChatModal()) {
|
if (!canOpenImChatModal()) {
|
||||||
closeModal();
|
closeModal();
|
||||||
openImChat({ targetUserId: data?.targetUserId });
|
openImChat({ targetUserId: data?.targetUserId, conversationId: data?.conversationId });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】全页 IM 已打开时拒绝弹窗,改由全页承接-----------
|
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】全页 IM 已打开时拒绝弹窗,改由全页承接-----------
|
||||||
setImChatWindowOpen(true);
|
setImChatWindowOpen(true);
|
||||||
pendingTargetUserId.value = data?.targetUserId || '';
|
pendingTargetUserId.value = data?.targetUserId || '';
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】支持从消息提醒直接打开群聊-----------
|
||||||
|
pendingConversationId.value = data?.conversationId || '';
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】支持从消息提醒直接打开群聊-----------
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】弹窗打开立即预热全局活跃 userId,避免 WS 消息在 nextTick 前丢失-----------
|
||||||
|
if (data?.targetUserId) {
|
||||||
|
setImActiveTargetUserId(data.targetUserId);
|
||||||
|
}
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】弹窗打开立即预热全局活跃 userId,避免 WS 消息在 nextTick 前丢失-----------
|
||||||
restoreChatSession();
|
restoreChatSession();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
161
jeecgboot-vue3/src/views/system/im/ImCreateGroupModal.vue
Normal file
161
jeecgboot-vue3/src/views/system/im/ImCreateGroupModal.vue
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<template>
|
||||||
|
<BasicModal v-bind="$attrs" title="发起群聊" :width="520" @register="registerModal" @ok="handleSubmit">
|
||||||
|
<a-form layout="vertical">
|
||||||
|
<a-form-item label="群名称" required>
|
||||||
|
<a-input v-model:value="groupName" placeholder="请输入群名称" :maxlength="30" show-count />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="选择成员" required>
|
||||||
|
<a-input
|
||||||
|
v-model:value="memberKeyword"
|
||||||
|
placeholder="搜索同事"
|
||||||
|
allow-clear
|
||||||
|
size="small"
|
||||||
|
class="member-search"
|
||||||
|
@pressEnter="loadMembers"
|
||||||
|
/>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div class="member-list">
|
||||||
|
<a-checkbox-group v-model:value="checkedMemberIds" class="member-checkbox-group">
|
||||||
|
<div v-for="item in filteredMembers" :key="item.id" class="member-row">
|
||||||
|
<a-checkbox :value="item.id">
|
||||||
|
<div class="member-info">
|
||||||
|
<a-avatar :size="28" :src="getAvatarUrl(item.avatar)">
|
||||||
|
{{ (item.realname || item.username || '?').slice(0, 1) }}
|
||||||
|
</a-avatar>
|
||||||
|
<span class="member-name">{{ item.realname || item.username }}</span>
|
||||||
|
</div>
|
||||||
|
</a-checkbox>
|
||||||
|
</div>
|
||||||
|
</a-checkbox-group>
|
||||||
|
<a-empty v-if="!filteredMembers.length" description="暂无可选同事" />
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</BasicModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||||
|
import { useMessage } from '/@/hooks/web/useMessage';
|
||||||
|
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
||||||
|
import { useUserStore } from '/@/store/modules/user';
|
||||||
|
import { createGroupConversation, fetchDeptMembers } from './im.api';
|
||||||
|
import { getCachedMembers } from './imCache';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ImCreateGroupModal' });
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
success: [group: Recordable];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { createMessage } = useMessage();
|
||||||
|
const groupName = ref('');
|
||||||
|
const memberKeyword = ref('');
|
||||||
|
const checkedMemberIds = ref<string[]>([]);
|
||||||
|
const members = ref<Recordable[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const filteredMembers = computed(() => {
|
||||||
|
const keyword = memberKeyword.value.trim().toLowerCase();
|
||||||
|
if (!keyword) {
|
||||||
|
return members.value;
|
||||||
|
}
|
||||||
|
return members.value.filter((item) => {
|
||||||
|
const name = `${item.realname || ''}${item.username || ''}`.toLowerCase();
|
||||||
|
return name.includes(keyword);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function resolveSelectableMembers(source?: Recordable[]) {
|
||||||
|
const currentUserId = userStore.getUserInfo?.id || '';
|
||||||
|
const list = source?.length ? source : getCachedMembers() || [];
|
||||||
|
return list.filter((item) => item.id && item.id !== currentUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data?: { members?: Recordable[] }) => {
|
||||||
|
groupName.value = '';
|
||||||
|
memberKeyword.value = '';
|
||||||
|
checkedMemberIds.value = [];
|
||||||
|
setModalProps({ confirmLoading: false });
|
||||||
|
members.value = resolveSelectableMembers(data?.members);
|
||||||
|
await loadMembers();
|
||||||
|
});
|
||||||
|
|
||||||
|
function getAvatarUrl(avatar?: string) {
|
||||||
|
return avatar ? getFileAccessHttpUrl(avatar) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMembers() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const list = ((await fetchDeptMembers(memberKeyword.value || undefined)) || []) as Recordable[];
|
||||||
|
members.value = resolveSelectableMembers(list);
|
||||||
|
} catch {
|
||||||
|
createMessage.error('加载同事列表失败,请稍后重试');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
const name = groupName.value.trim();
|
||||||
|
if (!name) {
|
||||||
|
createMessage.warning('请输入群名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!checkedMemberIds.value.length) {
|
||||||
|
createMessage.warning('请至少选择1名同事');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setModalProps({ confirmLoading: true });
|
||||||
|
try {
|
||||||
|
const group = await createGroupConversation({
|
||||||
|
groupName: name,
|
||||||
|
memberUserIds: checkedMemberIds.value,
|
||||||
|
});
|
||||||
|
emit('success', group);
|
||||||
|
closeModal();
|
||||||
|
} finally {
|
||||||
|
setModalProps({ confirmLoading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.member-search {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-list {
|
||||||
|
max-height: 320px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-row {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-info {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-name {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
164
jeecgboot-vue3/src/views/system/im/ImGroupAddMemberModal.vue
Normal file
164
jeecgboot-vue3/src/views/system/im/ImGroupAddMemberModal.vue
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
<!--update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-添加群成员弹窗----------->
|
||||||
|
<template>
|
||||||
|
<BasicModal v-bind="$attrs" title="添加群成员" :width="520" @register="registerModal" @ok="handleSubmit">
|
||||||
|
<a-form layout="vertical">
|
||||||
|
<a-form-item label="选择成员" required>
|
||||||
|
<a-input
|
||||||
|
v-model:value="memberKeyword"
|
||||||
|
placeholder="搜索同事"
|
||||||
|
allow-clear
|
||||||
|
size="small"
|
||||||
|
class="member-search"
|
||||||
|
@pressEnter="loadMembers"
|
||||||
|
/>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div class="member-list">
|
||||||
|
<a-checkbox-group v-model:value="checkedMemberIds" class="member-checkbox-group">
|
||||||
|
<div v-for="item in filteredMembers" :key="item.id" class="member-row">
|
||||||
|
<a-checkbox :value="item.id">
|
||||||
|
<div class="member-info">
|
||||||
|
<a-avatar :size="28" :src="getAvatarUrl(item.avatar)">
|
||||||
|
{{ (item.realname || item.username || '?').slice(0, 1) }}
|
||||||
|
</a-avatar>
|
||||||
|
<span class="member-name">{{ item.realname || item.username }}</span>
|
||||||
|
</div>
|
||||||
|
</a-checkbox>
|
||||||
|
</div>
|
||||||
|
</a-checkbox-group>
|
||||||
|
<a-empty v-if="!filteredMembers.length" description="暂无可添加的同事" />
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</BasicModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||||
|
import { useMessage } from '/@/hooks/web/useMessage';
|
||||||
|
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
||||||
|
import { useUserStore } from '/@/store/modules/user';
|
||||||
|
import { addGroupMembers, fetchDeptMembers } from './im.api';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ImGroupAddMemberModal' });
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
success: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { createMessage } = useMessage();
|
||||||
|
const memberKeyword = ref('');
|
||||||
|
const checkedMemberIds = ref<string[]>([]);
|
||||||
|
const members = ref<Recordable[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const conversationId = ref('');
|
||||||
|
// 已在群成员(用于过滤候选)
|
||||||
|
const existMemberIds = ref<string[]>([]);
|
||||||
|
|
||||||
|
const filteredMembers = computed(() => {
|
||||||
|
const keyword = memberKeyword.value.trim().toLowerCase();
|
||||||
|
if (!keyword) {
|
||||||
|
return members.value;
|
||||||
|
}
|
||||||
|
return members.value.filter((item) => {
|
||||||
|
const name = `${item.realname || ''}${item.username || ''}`.toLowerCase();
|
||||||
|
return name.includes(keyword);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 过滤掉当前用户与已在群成员 */
|
||||||
|
function resolveSelectableMembers(source: Recordable[]) {
|
||||||
|
const currentUserId = userStore.getUserInfo?.id || '';
|
||||||
|
const excludeSet = new Set([currentUserId, ...existMemberIds.value]);
|
||||||
|
return source.filter((item) => item.id && !excludeSet.has(item.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
const [registerModal, { setModalProps, closeModal }] = useModalInner(
|
||||||
|
async (data?: { conversationId?: string; existMemberIds?: string[] }) => {
|
||||||
|
memberKeyword.value = '';
|
||||||
|
checkedMemberIds.value = [];
|
||||||
|
conversationId.value = data?.conversationId || '';
|
||||||
|
existMemberIds.value = data?.existMemberIds || [];
|
||||||
|
setModalProps({ confirmLoading: false });
|
||||||
|
members.value = [];
|
||||||
|
await loadMembers();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function getAvatarUrl(avatar?: string) {
|
||||||
|
return avatar ? getFileAccessHttpUrl(avatar) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMembers() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const list = ((await fetchDeptMembers(memberKeyword.value || undefined)) || []) as Recordable[];
|
||||||
|
members.value = resolveSelectableMembers(list);
|
||||||
|
} catch {
|
||||||
|
createMessage.error('加载同事列表失败,请稍后重试');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!conversationId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!checkedMemberIds.value.length) {
|
||||||
|
createMessage.warning('请至少选择1名同事');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setModalProps({ confirmLoading: true });
|
||||||
|
try {
|
||||||
|
await addGroupMembers({
|
||||||
|
conversationId: conversationId.value,
|
||||||
|
memberUserIds: checkedMemberIds.value,
|
||||||
|
});
|
||||||
|
createMessage.success('添加成功');
|
||||||
|
emit('success');
|
||||||
|
closeModal();
|
||||||
|
} finally {
|
||||||
|
setModalProps({ confirmLoading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.member-search {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-list {
|
||||||
|
max-height: 320px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-row {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-info {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-name {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!--update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置-添加群成员弹窗----------->
|
||||||
473
jeecgboot-vue3/src/views/system/im/ImGroupSettingDrawer.vue
Normal file
473
jeecgboot-vue3/src/views/system/im/ImGroupSettingDrawer.vue
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
<!--update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置抽屉----------->
|
||||||
|
<template>
|
||||||
|
<BasicDrawer v-bind="$attrs" title="群设置" :width="360" @register="registerDrawer">
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<div class="group-setting">
|
||||||
|
<!-- 群成员宫格 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">
|
||||||
|
群成员
|
||||||
|
<span class="member-count">({{ detail.memberCount || 0 }})</span>
|
||||||
|
</div>
|
||||||
|
<div class="member-grid">
|
||||||
|
<div v-for="m in detail.members" :key="m.userId" class="member-cell">
|
||||||
|
<div class="member-avatar-wrap">
|
||||||
|
<a-avatar :size="44" :src="getAvatarUrl(m.avatar)">
|
||||||
|
{{ (m.realname || m.username || '?').slice(0, 1) }}
|
||||||
|
</a-avatar>
|
||||||
|
<span v-if="m.owner" class="owner-tag">群主</span>
|
||||||
|
<span
|
||||||
|
v-if="removeMode && !m.owner"
|
||||||
|
class="remove-badge"
|
||||||
|
@click="handleRemoveMember(m)"
|
||||||
|
>
|
||||||
|
<Icon icon="ant-design:minus-outlined" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="member-name">{{ m.realname || m.username }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加成员(所有成员可见) -->
|
||||||
|
<div class="member-cell">
|
||||||
|
<div class="member-action-btn" @click="handleOpenAddMember">
|
||||||
|
<Icon icon="ant-design:plus-outlined" />
|
||||||
|
</div>
|
||||||
|
<span class="member-name">添加</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 移除成员(仅群主可见) -->
|
||||||
|
<div v-if="detail.owner" class="member-cell">
|
||||||
|
<div :class="['member-action-btn', { active: removeMode }]" @click="toggleRemoveMode">
|
||||||
|
<Icon icon="ant-design:minus-outlined" />
|
||||||
|
</div>
|
||||||
|
<span class="member-name">{{ removeMode ? '完成' : '移除' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 群名称 -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="setting-row" :class="{ 'is-clickable': detail.owner }" @click="handleEditName">
|
||||||
|
<span class="row-label">群聊名称</span>
|
||||||
|
<span class="row-value">
|
||||||
|
{{ detail.groupName || '未命名群聊' }}
|
||||||
|
<Icon v-if="detail.owner" icon="ant-design:right-outlined" class="row-arrow" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 群主管理 -->
|
||||||
|
<div v-if="detail.owner" class="section">
|
||||||
|
<div class="setting-row is-clickable" @click="handleOpenTransfer">
|
||||||
|
<span class="row-label">转让群主</span>
|
||||||
|
<Icon icon="ant-design:right-outlined" class="row-arrow" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部操作 -->
|
||||||
|
<div class="footer-actions">
|
||||||
|
<a-button v-if="detail.owner" danger block @click="handleDismiss">解散群聊</a-button>
|
||||||
|
<a-button v-else danger block @click="handleQuit">退出群聊</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
|
||||||
|
<!-- 添加成员弹窗 -->
|
||||||
|
<ImGroupAddMemberModal @register="registerAddMemberModal" @success="onMembersChanged" />
|
||||||
|
|
||||||
|
<!-- 修改群名称弹窗 -->
|
||||||
|
<a-modal v-model:open="nameModalVisible" title="修改群名称" :confirm-loading="nameSaving" @ok="submitRename">
|
||||||
|
<a-input v-model:value="editingName" placeholder="请输入群名称" :maxlength="30" show-count />
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 转让群主弹窗 -->
|
||||||
|
<a-modal v-model:open="transferModalVisible" title="转让群主" :confirm-loading="transferSaving" @ok="submitTransfer">
|
||||||
|
<div class="transfer-tip">选择一名群成员作为新群主,转让后你将不再是群主。</div>
|
||||||
|
<a-radio-group v-model:value="transferTargetId" class="transfer-list">
|
||||||
|
<a-radio v-for="m in transferCandidates" :key="m.userId" :value="m.userId" class="transfer-item">
|
||||||
|
<a-avatar :size="28" :src="getAvatarUrl(m.avatar)">
|
||||||
|
{{ (m.realname || m.username || '?').slice(0, 1) }}
|
||||||
|
</a-avatar>
|
||||||
|
<span class="transfer-name">{{ m.realname || m.username }}</span>
|
||||||
|
</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
<a-empty v-if="!transferCandidates.length" description="暂无其他群成员" />
|
||||||
|
</a-modal>
|
||||||
|
</BasicDrawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, reactive, ref } from 'vue';
|
||||||
|
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
|
||||||
|
import { useModal } from '/@/components/Modal';
|
||||||
|
import { Icon } from '/@/components/Icon';
|
||||||
|
import { useMessage } from '/@/hooks/web/useMessage';
|
||||||
|
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
||||||
|
import {
|
||||||
|
fetchGroupDetail,
|
||||||
|
removeGroupMember,
|
||||||
|
renameGroup,
|
||||||
|
transferGroupOwner,
|
||||||
|
quitGroup,
|
||||||
|
dismissGroup,
|
||||||
|
type ImGroupDetail,
|
||||||
|
type ImGroupMember,
|
||||||
|
} from './im.api';
|
||||||
|
import ImGroupAddMemberModal from './ImGroupAddMemberModal.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ImGroupSettingDrawer' });
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
/** 群信息变更(改名/加人/踢人/转让),父组件据此刷新会话 */
|
||||||
|
(e: 'changed', payload: { conversationId: string; groupName?: string; memberCount?: number }): void;
|
||||||
|
/** 退出或解散群聊,父组件据此移除会话并清理当前会话 */
|
||||||
|
(e: 'exited', conversationId: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { createMessage, createConfirm } = useMessage();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const conversationId = ref('');
|
||||||
|
const detail = reactive<ImGroupDetail>({
|
||||||
|
conversationId: '',
|
||||||
|
groupName: '',
|
||||||
|
ownerId: '',
|
||||||
|
memberCount: 0,
|
||||||
|
owner: false,
|
||||||
|
members: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeMode = ref(false);
|
||||||
|
|
||||||
|
// 改名
|
||||||
|
const nameModalVisible = ref(false);
|
||||||
|
const editingName = ref('');
|
||||||
|
const nameSaving = ref(false);
|
||||||
|
|
||||||
|
// 转让群主
|
||||||
|
const transferModalVisible = ref(false);
|
||||||
|
const transferTargetId = ref('');
|
||||||
|
const transferSaving = ref(false);
|
||||||
|
const transferCandidates = computed(() => detail.members.filter((m) => !m.owner));
|
||||||
|
|
||||||
|
const [registerAddMemberModal, { openModal: openAddMemberModal }] = useModal();
|
||||||
|
|
||||||
|
const [registerDrawer, { setDrawerProps }] = useDrawerInner(async (data?: { conversationId?: string }) => {
|
||||||
|
removeMode.value = false;
|
||||||
|
conversationId.value = data?.conversationId || '';
|
||||||
|
if (!conversationId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setDrawerProps({ loading: false });
|
||||||
|
await loadDetail();
|
||||||
|
});
|
||||||
|
|
||||||
|
function getAvatarUrl(avatar?: string) {
|
||||||
|
return avatar ? getFileAccessHttpUrl(avatar) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDetail(data: ImGroupDetail) {
|
||||||
|
detail.conversationId = data.conversationId;
|
||||||
|
detail.groupName = data.groupName;
|
||||||
|
detail.ownerId = data.ownerId;
|
||||||
|
detail.memberCount = data.memberCount;
|
||||||
|
detail.owner = data.owner;
|
||||||
|
detail.members = data.members || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDetail() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await fetchGroupDetail(conversationId.value);
|
||||||
|
applyDetail(data);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyChanged() {
|
||||||
|
emit('changed', {
|
||||||
|
conversationId: conversationId.value,
|
||||||
|
groupName: detail.groupName,
|
||||||
|
memberCount: detail.memberCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpenAddMember() {
|
||||||
|
openAddMemberModal(true, {
|
||||||
|
conversationId: conversationId.value,
|
||||||
|
existMemberIds: detail.members.map((m) => m.userId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onMembersChanged() {
|
||||||
|
await loadDetail();
|
||||||
|
notifyChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRemoveMode() {
|
||||||
|
removeMode.value = !removeMode.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRemoveMember(member: ImGroupMember) {
|
||||||
|
createConfirm({
|
||||||
|
iconType: 'warning',
|
||||||
|
title: '移除成员',
|
||||||
|
content: `确定将「${member.realname || member.username}」移出群聊吗?`,
|
||||||
|
onOk: async () => {
|
||||||
|
await removeGroupMember(conversationId.value, member.userId);
|
||||||
|
createMessage.success('已移除');
|
||||||
|
await loadDetail();
|
||||||
|
notifyChanged();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditName() {
|
||||||
|
if (!detail.owner) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editingName.value = detail.groupName || '';
|
||||||
|
nameModalVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitRename() {
|
||||||
|
const name = editingName.value.trim();
|
||||||
|
if (!name) {
|
||||||
|
createMessage.warning('请输入群名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nameSaving.value = true;
|
||||||
|
try {
|
||||||
|
await renameGroup(conversationId.value, name);
|
||||||
|
detail.groupName = name;
|
||||||
|
nameModalVisible.value = false;
|
||||||
|
createMessage.success('修改成功');
|
||||||
|
notifyChanged();
|
||||||
|
} finally {
|
||||||
|
nameSaving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpenTransfer() {
|
||||||
|
transferTargetId.value = '';
|
||||||
|
transferModalVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitTransfer() {
|
||||||
|
if (!transferTargetId.value) {
|
||||||
|
createMessage.warning('请选择新群主');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
transferSaving.value = true;
|
||||||
|
try {
|
||||||
|
await transferGroupOwner(conversationId.value, transferTargetId.value);
|
||||||
|
transferModalVisible.value = false;
|
||||||
|
createMessage.success('转让成功');
|
||||||
|
await loadDetail();
|
||||||
|
notifyChanged();
|
||||||
|
} finally {
|
||||||
|
transferSaving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleQuit() {
|
||||||
|
createConfirm({
|
||||||
|
iconType: 'warning',
|
||||||
|
title: '退出群聊',
|
||||||
|
content: '退出后将不再接收该群消息,确定退出吗?',
|
||||||
|
onOk: async () => {
|
||||||
|
await quitGroup(conversationId.value);
|
||||||
|
createMessage.success('已退出群聊');
|
||||||
|
emit('exited', conversationId.value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDismiss() {
|
||||||
|
createConfirm({
|
||||||
|
iconType: 'warning',
|
||||||
|
title: '解散群聊',
|
||||||
|
content: '解散后群聊将被删除且无法恢复,确定解散吗?',
|
||||||
|
onOk: async () => {
|
||||||
|
await dismissGroup(conversationId.value);
|
||||||
|
createMessage.success('群聊已解散');
|
||||||
|
emit('exited', conversationId.value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.group-setting {
|
||||||
|
padding: 4px 4px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.member-count {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-cell {
|
||||||
|
width: 56px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-avatar-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.owner-tag {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: -4px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 14px;
|
||||||
|
padding: 0 4px;
|
||||||
|
color: #fff;
|
||||||
|
background: #fa8c16;
|
||||||
|
border-radius: 7px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
right: -4px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #ff4d4f;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-action-btn {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px dashed #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #1677ff;
|
||||||
|
color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: #ff4d4f;
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-name {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #595959;
|
||||||
|
max-width: 56px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
min-height: 28px;
|
||||||
|
|
||||||
|
&.is-clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #262626;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-value {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #888;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-arrow {
|
||||||
|
color: #c0c0c0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-actions {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-tip {
|
||||||
|
color: #888;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 320px;
|
||||||
|
overflow-y: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
:deep(.ant-radio + span) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.transfer-name {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!--update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置抽屉----------->
|
||||||
@@ -6,6 +6,28 @@ enum Api {
|
|||||||
messages = '/sys/im/chat/messages',
|
messages = '/sys/im/chat/messages',
|
||||||
send = '/sys/im/chat/send',
|
send = '/sys/im/chat/send',
|
||||||
read = '/sys/im/chat/read',
|
read = '/sys/im/chat/read',
|
||||||
|
groups = '/sys/im/chat/groups',
|
||||||
|
groupCreate = '/sys/im/chat/group/create',
|
||||||
|
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置接口-----------
|
||||||
|
groupDetail = '/sys/im/chat/group/detail',
|
||||||
|
groupAddMembers = '/sys/im/chat/group/addMembers',
|
||||||
|
groupRemoveMember = '/sys/im/chat/group/removeMember',
|
||||||
|
groupRename = '/sys/im/chat/group/rename',
|
||||||
|
groupTransfer = '/sys/im/chat/group/transfer',
|
||||||
|
groupQuit = '/sys/im/chat/group/quit',
|
||||||
|
groupDismiss = '/sys/im/chat/group/dismiss',
|
||||||
|
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置接口-----------
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImGroupConversation {
|
||||||
|
conversationId: string;
|
||||||
|
convType?: string;
|
||||||
|
groupName?: string;
|
||||||
|
memberCount?: number;
|
||||||
|
ownerId?: string;
|
||||||
|
lastContent?: string;
|
||||||
|
lastTime?: string;
|
||||||
|
unreadCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchDeptMembers = (keyword?: string) => defHttp.get({ url: Api.deptMembers, params: { keyword } });
|
export const fetchDeptMembers = (keyword?: string) => defHttp.get({ url: Api.deptMembers, params: { keyword } });
|
||||||
@@ -39,3 +61,56 @@ export const sendMessage = (data: { conversationId: string; content: string; msg
|
|||||||
|
|
||||||
export const markRead = (conversationId: string) =>
|
export const markRead = (conversationId: string) =>
|
||||||
defHttp.put({ url: Api.read, params: { conversationId } }, { joinParamsToUrl: true, successMessageMode: 'none' });
|
defHttp.put({ url: Api.read, params: { conversationId } }, { joinParamsToUrl: true, successMessageMode: 'none' });
|
||||||
|
|
||||||
|
export const fetchGroups = () => defHttp.get<ImGroupConversation[]>({ url: Api.groups });
|
||||||
|
|
||||||
|
export const createGroupConversation = (data: { groupName: string; memberUserIds: string[] }) =>
|
||||||
|
defHttp.post<ImGroupConversation>({ url: Api.groupCreate, data });
|
||||||
|
|
||||||
|
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置接口-----------
|
||||||
|
export interface ImGroupMember {
|
||||||
|
userId: string;
|
||||||
|
realname?: string;
|
||||||
|
username?: string;
|
||||||
|
avatar?: string;
|
||||||
|
owner?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImGroupDetail {
|
||||||
|
conversationId: string;
|
||||||
|
groupName?: string;
|
||||||
|
ownerId?: string;
|
||||||
|
memberCount?: number;
|
||||||
|
/** 当前登录用户是否群主 */
|
||||||
|
owner?: boolean;
|
||||||
|
members: ImGroupMember[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 群聊详情(含成员列表) */
|
||||||
|
export const fetchGroupDetail = (conversationId: string) =>
|
||||||
|
defHttp.get<ImGroupDetail>({ url: Api.groupDetail, params: { conversationId } });
|
||||||
|
|
||||||
|
/** 添加群成员 */
|
||||||
|
export const addGroupMembers = (data: { conversationId: string; memberUserIds: string[] }) =>
|
||||||
|
defHttp.post<ImGroupConversation>({ url: Api.groupAddMembers, data });
|
||||||
|
|
||||||
|
/** 移除群成员(仅群主) */
|
||||||
|
export const removeGroupMember = (conversationId: string, memberUserId: string) =>
|
||||||
|
defHttp.post({ url: Api.groupRemoveMember, params: { conversationId, memberUserId } }, { joinParamsToUrl: true });
|
||||||
|
|
||||||
|
/** 修改群名称(仅群主) */
|
||||||
|
export const renameGroup = (conversationId: string, groupName: string) =>
|
||||||
|
defHttp.post<ImGroupConversation>({ url: Api.groupRename, params: { conversationId, groupName } }, { joinParamsToUrl: true });
|
||||||
|
|
||||||
|
/** 转让群主(仅群主) */
|
||||||
|
export const transferGroupOwner = (conversationId: string, newOwnerId: string) =>
|
||||||
|
defHttp.post({ url: Api.groupTransfer, params: { conversationId, newOwnerId } }, { joinParamsToUrl: true });
|
||||||
|
|
||||||
|
/** 退出群聊(非群主成员) */
|
||||||
|
export const quitGroup = (conversationId: string) =>
|
||||||
|
defHttp.post({ url: Api.groupQuit, params: { conversationId } }, { joinParamsToUrl: true });
|
||||||
|
|
||||||
|
/** 解散群聊(仅群主) */
|
||||||
|
export const dismissGroup = (conversationId: string) =>
|
||||||
|
defHttp.post({ url: Api.groupDismiss, params: { conversationId } }, { joinParamsToUrl: true });
|
||||||
|
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置接口-----------
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { ref as vueRef } from 'vue';
|
||||||
import { createSessionStorage } from '/@/utils/cache';
|
import { createSessionStorage } from '/@/utils/cache';
|
||||||
import { useUserStoreWithOut } from '/@/store/modules/user';
|
import { useUserStoreWithOut } from '/@/store/modules/user';
|
||||||
import { fetchDeptMembers, fetchMessages } from './im.api';
|
import { fetchDeptMembers, fetchMessages, fetchGroups } from './im.api';
|
||||||
import { getImDefaultStartTime } from './imSettings';
|
import { getImDefaultStartTime } from './imSettings';
|
||||||
import { formatImMessagePreview } from './imMessageUtil';
|
import { formatImMessagePreview } from './imMessageUtil';
|
||||||
import { syncImUnreadFromMembers } from './useImUnread';
|
import { syncImUnreadFromMembers } from './useImUnread';
|
||||||
@@ -67,6 +68,25 @@ export function onImMessagesUpdated(listener: ImMessagesUpdatedListener) {
|
|||||||
return () => messageUpdatedListeners.delete(listener);
|
return () => messageUpdatedListeners.delete(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ImChatSocketUiListener = (data: Record<string, any>) => void;
|
||||||
|
const chatSocketUiListeners = new Set<ImChatSocketUiListener>();
|
||||||
|
|
||||||
|
/** IM 聊天 UI 层 WS 分发(避免多 ImChat 实例重复绑定 onWebSocket) */
|
||||||
|
export function onImChatSocketUi(listener: ImChatSocketUiListener) {
|
||||||
|
chatSocketUiListeners.add(listener);
|
||||||
|
return () => chatSocketUiListeners.delete(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchImChatSocketUi(data: Record<string, any>) {
|
||||||
|
chatSocketUiListeners.forEach((listener) => {
|
||||||
|
try {
|
||||||
|
listener(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
type ImChatWindowOpenListener = (open: boolean) => void;
|
type ImChatWindowOpenListener = (open: boolean) => void;
|
||||||
const chatWindowOpenListeners = new Set<ImChatWindowOpenListener>();
|
const chatWindowOpenListeners = new Set<ImChatWindowOpenListener>();
|
||||||
|
|
||||||
@@ -187,11 +207,83 @@ export function clearImCache() {
|
|||||||
activeConversationId = '';
|
activeConversationId = '';
|
||||||
activeTargetUserId = '';
|
activeTargetUserId = '';
|
||||||
imChatWindowOpen = false;
|
imChatWindowOpen = false;
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】清理群聊未读缓存-----------
|
||||||
|
groupUnreadCache.clear();
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】清理群聊未读缓存-----------
|
||||||
if (scope) {
|
if (scope) {
|
||||||
sessionCache.remove(`${CACHE_KEY}:${scope}`);
|
sessionCache.remove(`${CACHE_KEY}:${scope}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】群聊未读缓存,独立于成员缓存,支持 WS 实时累加和角标统计-----------
|
||||||
|
interface ImGroupUnreadEntry {
|
||||||
|
unreadCount: number;
|
||||||
|
lastContent?: string;
|
||||||
|
lastTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** key = conversationId */
|
||||||
|
const groupUnreadCache = new Map<string, ImGroupUnreadEntry>();
|
||||||
|
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】响应式群聊未读总数,供 ImChat.vue tab 角标实时更新-----------
|
||||||
|
/** 响应式群聊未读总数(跨组件共享,无需加载群列表即可显示 tab 角标) */
|
||||||
|
export const reactiveGroupUnreadCount = vueRef(0);
|
||||||
|
|
||||||
|
function recalcGroupUnreadCount() {
|
||||||
|
let total = 0;
|
||||||
|
for (const entry of groupUnreadCache.values()) {
|
||||||
|
total += entry.unreadCount;
|
||||||
|
}
|
||||||
|
reactiveGroupUnreadCount.value = total;
|
||||||
|
}
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】响应式群聊未读总数,供 ImChat.vue tab 角标实时更新-----------
|
||||||
|
|
||||||
|
/** 从接口列表初始化群聊未读缓存(登录预取或 loadGroups 后调用) */
|
||||||
|
export function initGroupUnreadFromList(
|
||||||
|
groups: Array<{ conversationId: string; unreadCount?: number; lastContent?: string; lastTime?: string }>,
|
||||||
|
) {
|
||||||
|
for (const group of groups || []) {
|
||||||
|
if (group.conversationId) {
|
||||||
|
groupUnreadCache.set(group.conversationId, {
|
||||||
|
unreadCount: group.unreadCount || 0,
|
||||||
|
lastContent: group.lastContent,
|
||||||
|
lastTime: group.lastTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recalcGroupUnreadCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** WS 收到群消息时累加未读 */
|
||||||
|
export function incrementGroupUnread(conversationId: string, lastContent: string, lastTime: string) {
|
||||||
|
const current = groupUnreadCache.get(conversationId) || { unreadCount: 0 };
|
||||||
|
groupUnreadCache.set(conversationId, {
|
||||||
|
unreadCount: current.unreadCount + 1,
|
||||||
|
lastContent,
|
||||||
|
lastTime,
|
||||||
|
});
|
||||||
|
recalcGroupUnreadCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开群聊会话时清零(已读) */
|
||||||
|
export function resetGroupUnread(conversationId: string) {
|
||||||
|
const current = groupUnreadCache.get(conversationId);
|
||||||
|
if (current) {
|
||||||
|
groupUnreadCache.set(conversationId, { ...current, unreadCount: 0 });
|
||||||
|
recalcGroupUnreadCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 供 syncImUnreadFromMembers 和 refreshImUnread 合并到总未读统计 */
|
||||||
|
export function getCachedGroupUnreadItems(): Array<{ id: string; conversationId: string; unreadCount: number }> {
|
||||||
|
return Array.from(groupUnreadCache.entries()).map(([id, data]) => ({
|
||||||
|
id,
|
||||||
|
conversationId: id,
|
||||||
|
unreadCount: data.unreadCount,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】群聊未读缓存,独立于成员缓存,支持 WS 实时累加和角标统计-----------
|
||||||
|
|
||||||
export function getCachedMembers(): ImMemberItem[] | null {
|
export function getCachedMembers(): ImMemberItem[] | null {
|
||||||
const snap = ensureMemory();
|
const snap = ensureMemory();
|
||||||
return snap.members.length ? snap.members : null;
|
return snap.members.length ? snap.members : null;
|
||||||
@@ -323,6 +415,15 @@ export async function prefetchImChatData(force = false): Promise<void> {
|
|||||||
setCachedMembers(members);
|
setCachedMembers(members);
|
||||||
syncImUnreadFromMembers(members);
|
syncImUnreadFromMembers(members);
|
||||||
|
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】预取群聊未读数,使顶部角标包含群聊未读-----------
|
||||||
|
fetchGroups()
|
||||||
|
.then((groups: any[]) => {
|
||||||
|
initGroupUnreadFromList(groups || []);
|
||||||
|
syncImUnreadFromMembers([...members, ...getCachedGroupUnreadItems()]);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】预取群聊未读数,使顶部角标包含群聊未读-----------
|
||||||
|
|
||||||
const candidates = members
|
const candidates = members
|
||||||
.filter((item) => item.conversationId && (item.unreadCount || item.lastTime))
|
.filter((item) => item.conversationId && (item.unreadCount || item.lastTime))
|
||||||
.sort((a, b) => dayjs(b.lastTime || 0).valueOf() - dayjs(a.lastTime || 0).valueOf())
|
.sort((a, b) => dayjs(b.lastTime || 0).valueOf() - dayjs(a.lastTime || 0).valueOf())
|
||||||
@@ -342,14 +443,37 @@ export async function prefetchImChatData(force = false): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 顶部角标:收到新消息时更新缓存未读数 */
|
/** 顶部角标:收到新消息时更新缓存未读数 */
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】按 messageId 去重,防止多个 WS 监听者重复调用导致角标翻倍-----------
|
||||||
|
const _processedMsgIds = new Set<string>();
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】按 messageId 去重,防止多个 WS 监听者重复调用导致角标翻倍-----------
|
||||||
export function handleImChatSocket(data: Record<string, any>) {
|
export function handleImChatSocket(data: Record<string, any>) {
|
||||||
if (data.cmd !== 'chat') {
|
if (data.cmd !== 'chat') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】同一消息去重处理,避免多监听器重复累加未读-----------
|
||||||
|
const msgId = data.messageId as string;
|
||||||
|
if (msgId) {
|
||||||
|
if (_processedMsgIds.has(msgId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_processedMsgIds.add(msgId);
|
||||||
|
// 5 秒后清理,防止 Set 无限增长
|
||||||
|
setTimeout(() => _processedMsgIds.delete(msgId), 5000);
|
||||||
|
}
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】同一消息去重处理,避免多监听器重复累加未读-----------
|
||||||
const conversationId = data.conversationId as string;
|
const conversationId = data.conversationId as string;
|
||||||
const senderId = data.senderId as string;
|
const senderId = data.senderId as string;
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天-OA】单聊 WS 即时同步缓存并区分群聊-----------
|
||||||
|
const isGroupMessage = data.convType === 'group';
|
||||||
|
const isViewingSinglePeer = !!activeTargetUserId && activeTargetUserId === senderId;
|
||||||
const isActiveConversation =
|
const isActiveConversation =
|
||||||
isImChatUiOpen() && !!conversationId && conversationId === activeConversationId;
|
isImChatUiOpen() &&
|
||||||
|
!!conversationId &&
|
||||||
|
(conversationId === activeConversationId || (!isGroupMessage && isViewingSinglePeer));
|
||||||
|
|
||||||
|
if (isActiveConversation && conversationId !== activeConversationId) {
|
||||||
|
activeConversationId = conversationId;
|
||||||
|
}
|
||||||
|
|
||||||
if (isActiveConversation) {
|
if (isActiveConversation) {
|
||||||
const userStore = useUserStoreWithOut();
|
const userStore = useUserStoreWithOut();
|
||||||
@@ -365,21 +489,32 @@ export function handleImChatSocket(data: Record<string, any>) {
|
|||||||
mine: senderId === currentUserId,
|
mine: senderId === currentUserId,
|
||||||
createTime: data.createTime,
|
createTime: data.createTime,
|
||||||
});
|
});
|
||||||
const members = getCachedMembers() || [];
|
if (isGroupMessage) {
|
||||||
const peerMember = members.find((item) => item.conversationId === conversationId);
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】活跃群聊消息到达时,合并群聊未读数到角标-----------
|
||||||
const peerUserId = peerMember?.id || (senderId !== currentUserId ? senderId : activeTargetUserId);
|
syncImUnreadFromMembers([...(getCachedMembers() || []), ...getCachedGroupUnreadItems()]);
|
||||||
if (peerUserId) {
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】活跃群聊消息到达时,合并群聊未读数到角标-----------
|
||||||
patchCachedMember(
|
} else {
|
||||||
peerUserId,
|
const members = getCachedMembers() || [];
|
||||||
{
|
const peerMember = members.find((item) => item.conversationId === conversationId);
|
||||||
conversationId,
|
const peerUserId = peerMember?.id || (senderId !== currentUserId ? senderId : activeTargetUserId);
|
||||||
lastContent: formatImMessagePreview(data.content, data.msgType),
|
if (peerUserId) {
|
||||||
lastTime: data.createTime,
|
patchCachedMember(
|
||||||
unreadCount: 0,
|
peerUserId,
|
||||||
},
|
{
|
||||||
{ moveToTop: true },
|
conversationId,
|
||||||
);
|
lastContent: formatImMessagePreview(data.content, data.msgType),
|
||||||
|
lastTime: data.createTime,
|
||||||
|
unreadCount: 0,
|
||||||
|
},
|
||||||
|
{ moveToTop: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else if (isGroupMessage) {
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】非活跃群聊收到 WS 消息:累加群聊未读并更新顶部角标-----------
|
||||||
|
incrementGroupUnread(conversationId, formatImMessagePreview(data.content, data.msgType), data.createTime as string);
|
||||||
|
syncImUnreadFromMembers([...(getCachedMembers() || []), ...getCachedGroupUnreadItems()]);
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】非活跃群聊收到 WS 消息:累加群聊未读并更新顶部角标-----------
|
||||||
} else {
|
} else {
|
||||||
ensureCachedMember(senderId, {
|
ensureCachedMember(senderId, {
|
||||||
username: data.senderName as string,
|
username: data.senderName as string,
|
||||||
@@ -398,6 +533,10 @@ export function handleImChatSocket(data: Record<string, any>) {
|
|||||||
},
|
},
|
||||||
{ unreadIncrement: 1 },
|
{ unreadIncrement: 1 },
|
||||||
);
|
);
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】单聊未读角标也合并群聊未读,保持角标一致性-----------
|
||||||
|
syncImUnreadFromMembers([...(getCachedMembers() || []), ...getCachedGroupUnreadItems()]);
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】单聊未读角标也合并群聊未读,保持角标一致性-----------
|
||||||
}
|
}
|
||||||
syncImUnreadFromMembers(getCachedMembers() || []);
|
dispatchImChatSocketUi(data);
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】单聊 WS 即时同步缓存并区分群聊-----------
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,22 @@ export function useImPageContext() {
|
|||||||
type ImOpenTargetListener = (targetUserId: string) => void | Promise<void>;
|
type ImOpenTargetListener = (targetUserId: string) => void | Promise<void>;
|
||||||
const openTargetListeners = new Set<ImOpenTargetListener>();
|
const openTargetListeners = new Set<ImOpenTargetListener>();
|
||||||
|
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】从消息提醒直接打开群聊会话-----------
|
||||||
|
type ImOpenGroupListener = (conversationId: string) => void | Promise<void>;
|
||||||
|
const openGroupListeners = new Set<ImOpenGroupListener>();
|
||||||
|
|
||||||
|
export function onImOpenGroupRequest(listener: ImOpenGroupListener) {
|
||||||
|
openGroupListeners.add(listener);
|
||||||
|
return () => openGroupListeners.delete(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function notifyOpenGroup(conversationId: string) {
|
||||||
|
for (const listener of openGroupListeners) {
|
||||||
|
await listener(conversationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】从消息提醒直接打开群聊会话-----------
|
||||||
|
|
||||||
export function setImChatPageActive(active: boolean) {
|
export function setImChatPageActive(active: boolean) {
|
||||||
imChatPageActive.value = active;
|
imChatPageActive.value = active;
|
||||||
}
|
}
|
||||||
@@ -78,6 +94,10 @@ export function requestCloseImChatModal() {
|
|||||||
*/
|
*/
|
||||||
export async function openImChat(options?: {
|
export async function openImChat(options?: {
|
||||||
targetUserId?: string;
|
targetUserId?: string;
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】支持从消息提醒直接打开群聊-----------
|
||||||
|
/** 群聊会话 ID(与 targetUserId 二选一) */
|
||||||
|
conversationId?: string;
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】支持从消息提醒直接打开群聊-----------
|
||||||
pageContext?: ImPageContext | null;
|
pageContext?: ImPageContext | null;
|
||||||
}): Promise<'page' | 'modal'> {
|
}): Promise<'page' | 'modal'> {
|
||||||
//update-begin---author:xsl ---date:20260528 for:【IM聊天-OA】弹窗打开时快照背后功能页名称-----------
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天-OA】弹窗打开时快照背后功能页名称-----------
|
||||||
@@ -88,6 +108,10 @@ export async function openImChat(options?: {
|
|||||||
if (isImChatPageActive()) {
|
if (isImChatPageActive()) {
|
||||||
if (options?.targetUserId) {
|
if (options?.targetUserId) {
|
||||||
await notifyOpenTarget(options.targetUserId);
|
await notifyOpenTarget(options.targetUserId);
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】全页 IM 时直接导航到指定群聊-----------
|
||||||
|
} else if (options?.conversationId) {
|
||||||
|
await notifyOpenGroup(options.conversationId);
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】全页 IM 时直接导航到指定群聊-----------
|
||||||
}
|
}
|
||||||
return 'page';
|
return 'page';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ref } from 'vue';
|
|||||||
import { fetchDeptMembers } from './im.api';
|
import { fetchDeptMembers } from './im.api';
|
||||||
import {
|
import {
|
||||||
getCachedMembers,
|
getCachedMembers,
|
||||||
|
getCachedGroupUnreadItems,
|
||||||
getImActiveConversationId,
|
getImActiveConversationId,
|
||||||
getImActiveTargetUserId,
|
getImActiveTargetUserId,
|
||||||
isImChatUiOpen,
|
isImChatUiOpen,
|
||||||
@@ -62,7 +63,9 @@ export async function refreshImUnread(force = false) {
|
|||||||
if (!force) {
|
if (!force) {
|
||||||
const cached = getCachedMembers();
|
const cached = getCachedMembers();
|
||||||
if (cached && !isMembersCacheStale()) {
|
if (cached && !isMembersCacheStale()) {
|
||||||
syncImUnreadFromMembers(cached);
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】从缓存刷新时也合并群聊未读-----------
|
||||||
|
syncImUnreadFromMembers([...cached, ...getCachedGroupUnreadItems()]);
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】从缓存刷新时也合并群聊未读-----------
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,7 +73,9 @@ export async function refreshImUnread(force = false) {
|
|||||||
try {
|
try {
|
||||||
const members = ((await fetchDeptMembers()) || []) as Array<{ unreadCount?: number; conversationId?: string; id?: string }>;
|
const members = ((await fetchDeptMembers()) || []) as Array<{ unreadCount?: number; conversationId?: string; id?: string }>;
|
||||||
setCachedMembers(members);
|
setCachedMembers(members);
|
||||||
syncImUnreadFromMembers(members);
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】接口刷新时也合并群聊未读-----------
|
||||||
|
syncImUnreadFromMembers([...members, ...getCachedGroupUnreadItems()]);
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】接口刷新时也合并群聊未读-----------
|
||||||
} finally {
|
} finally {
|
||||||
refreshing = false;
|
refreshing = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<a-list-item-meta>
|
<a-list-item-meta>
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="im-chat-msg-title">
|
<div class="im-chat-msg-title">
|
||||||
<span class="im-chat-msg-name">{{ item.realname || item.username }}</span>
|
<span class="im-chat-msg-name">{{ item.displayName }}</span>
|
||||||
<a-badge :count="item.unreadCount" :overflow-count="99" />
|
<a-badge :count="item.unreadCount" :overflow-count="99" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -18,9 +18,14 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #avatar>
|
<template #avatar>
|
||||||
<a-badge dot :offset="[-2, 2]">
|
<a-badge dot :offset="[-2, 2]">
|
||||||
<a-avatar :src="getAvatarUrl(item.avatar)">
|
<!--update-begin---author:xsl ---date:20260528 for:【IM聊天】群聊显示群图标,单聊显示头像------------->
|
||||||
{{ (item.realname || item.username || '?').slice(0, 1) }}
|
<a-avatar v-if="item.type === 'group'" :style="{ backgroundColor: '#1677ff' }">
|
||||||
|
<Icon icon="ant-design:team-outlined" />
|
||||||
</a-avatar>
|
</a-avatar>
|
||||||
|
<a-avatar v-else :src="getAvatarUrl(item.avatar)">
|
||||||
|
{{ (item.displayName || '?').slice(0, 1) }}
|
||||||
|
</a-avatar>
|
||||||
|
<!--update-end---author:xsl ---date:20260528 for:【IM聊天】群聊显示群图标,单聊显示头像----------->
|
||||||
</a-badge>
|
</a-badge>
|
||||||
</template>
|
</template>
|
||||||
</a-list-item-meta>
|
</a-list-item-meta>
|
||||||
@@ -37,11 +42,13 @@
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useModal } from '/@/components/Modal';
|
import { useModal } from '/@/components/Modal';
|
||||||
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
||||||
import { fetchDeptMembers } from '/@/views/system/im/im.api';
|
import { fetchDeptMembers, fetchGroups } from '/@/views/system/im/im.api';
|
||||||
import { formatImMessagePreview } from '/@/views/system/im/imMessageUtil';
|
import { formatImMessagePreview } from '/@/views/system/im/imMessageUtil';
|
||||||
import { syncImUnreadFromMembers } from '/@/views/system/im/useImUnread';
|
import { syncImUnreadFromMembers } from '/@/views/system/im/useImUnread';
|
||||||
|
import { getCachedGroupUnreadItems, initGroupUnreadFromList } from '/@/views/system/im/imCache';
|
||||||
import ImChatModal from '/@/views/system/im/ImChatModal.vue';
|
import ImChatModal from '/@/views/system/im/ImChatModal.vue';
|
||||||
import { openImChat } from '/@/views/system/im/imSession';
|
import { openImChat } from '/@/views/system/im/imSession';
|
||||||
|
import { Icon } from '/@/components/Icon';
|
||||||
|
|
||||||
defineOptions({ name: 'SysImChatMessageList' });
|
defineOptions({ name: 'SysImChatMessageList' });
|
||||||
|
|
||||||
@@ -49,26 +56,59 @@
|
|||||||
(e: 'closeModal'): void;
|
(e: 'closeModal'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
interface ChatMemberItem {
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】统一单聊与群聊展示类型-----------
|
||||||
|
type ItemType = 'single' | 'group';
|
||||||
|
|
||||||
|
interface UnreadItem {
|
||||||
|
type: ItemType;
|
||||||
|
/** 单聊:userId;群聊:conversationId */
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
displayName: string;
|
||||||
realname?: string;
|
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
conversationId?: string;
|
conversationId?: string;
|
||||||
lastContent?: string;
|
lastContent?: string;
|
||||||
lastTime?: string;
|
lastTime?: string;
|
||||||
unreadCount?: number;
|
unreadCount?: number;
|
||||||
}
|
}
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】统一单聊与群聊展示类型-----------
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const members = ref<ChatMemberItem[]>([]);
|
const members = ref<any[]>([]);
|
||||||
|
const groups = ref<any[]>([]);
|
||||||
const [registerImChatModal, { openModal: openImChatModal }] = useModal();
|
const [registerImChatModal, { openModal: openImChatModal }] = useModal();
|
||||||
|
|
||||||
const unreadList = computed(() =>
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】合并单聊和群聊未读列表-----------
|
||||||
members.value
|
const unreadList = computed<UnreadItem[]>(() => {
|
||||||
|
const singleItems: UnreadItem[] = members.value
|
||||||
.filter((item) => (item.unreadCount || 0) > 0)
|
.filter((item) => (item.unreadCount || 0) > 0)
|
||||||
.sort((a, b) => dayjs(b.lastTime || 0).valueOf() - dayjs(a.lastTime || 0).valueOf()),
|
.map((item) => ({
|
||||||
);
|
type: 'single',
|
||||||
|
id: item.id,
|
||||||
|
displayName: item.realname || item.username || '',
|
||||||
|
avatar: item.avatar,
|
||||||
|
conversationId: item.conversationId,
|
||||||
|
lastContent: item.lastContent,
|
||||||
|
lastTime: item.lastTime,
|
||||||
|
unreadCount: item.unreadCount,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const groupItems: UnreadItem[] = groups.value
|
||||||
|
.filter((item) => (item.unreadCount || 0) > 0)
|
||||||
|
.map((item) => ({
|
||||||
|
type: 'group',
|
||||||
|
id: item.conversationId,
|
||||||
|
displayName: item.groupName || '群聊',
|
||||||
|
conversationId: item.conversationId,
|
||||||
|
lastContent: item.lastContent,
|
||||||
|
lastTime: item.lastTime,
|
||||||
|
unreadCount: item.unreadCount,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [...singleItems, ...groupItems].sort(
|
||||||
|
(a, b) => dayjs(b.lastTime || 0).valueOf() - dayjs(a.lastTime || 0).valueOf(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】合并单聊和群聊未读列表-----------
|
||||||
|
|
||||||
const locale = computed(() => ({
|
const locale = computed(() => ({
|
||||||
emptyText: loading.value ? ' ' : '暂无未读聊天消息',
|
emptyText: loading.value ? ' ' : '暂无未读聊天消息',
|
||||||
@@ -96,36 +136,52 @@
|
|||||||
return d.format('MM-DD HH:mm');
|
return d.format('MM-DD HH:mm');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】同时拉取单聊成员和群聊列表,修复 syncImUnreadFromMembers 丢失群聊未读-----------
|
||||||
async function reload(silent = false) {
|
async function reload(silent = false) {
|
||||||
//update-begin---author:cursor ---date:20250528 for:【IM聊天-OA】聊天消息列表静默刷新,避免 spin 遮罩闪烁-----------
|
const showLoading = !silent && members.value.length === 0 && groups.value.length === 0;
|
||||||
const showLoading = !silent && members.value.length === 0;
|
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
members.value = ((await fetchDeptMembers()) || []) as ChatMemberItem[];
|
const [fetchedMembers, fetchedGroups] = await Promise.all([
|
||||||
syncImUnreadFromMembers(members.value);
|
fetchDeptMembers().catch(() => []),
|
||||||
|
fetchGroups().catch(() => []),
|
||||||
|
]);
|
||||||
|
members.value = (fetchedMembers || []) as any[];
|
||||||
|
groups.value = (fetchedGroups || []) as any[];
|
||||||
|
// 同步群聊未读到全局缓存
|
||||||
|
initGroupUnreadFromList(groups.value);
|
||||||
|
// 合并单聊 + 群聊未读数,防止 syncImUnreadFromMembers 只传成员数据导致群聊角标清零
|
||||||
|
syncImUnreadFromMembers([...members.value, ...getCachedGroupUnreadItems()]);
|
||||||
} catch {
|
} catch {
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
members.value = [];
|
members.value = [];
|
||||||
|
groups.value = [];
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//update-end---author:cursor ---date:20250528 for:【IM聊天-OA】聊天消息列表静默刷新,避免 spin 遮罩闪烁-----------
|
|
||||||
}
|
}
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】同时拉取单聊成员和群聊列表,修复 syncImUnreadFromMembers 丢失群聊未读-----------
|
||||||
|
|
||||||
async function handleOpenChat(item: ChatMemberItem) {
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】点击群聊条目直接打开群聊会话-----------
|
||||||
//update-begin---author:xsl ---date:20260528 for:【IM聊天-OA】统一 IM 打开入口,全页优先-----------
|
async function handleOpenChat(item: UnreadItem) {
|
||||||
const mode = await openImChat({ targetUserId: item.id, pageContext: null });
|
if (item.type === 'group') {
|
||||||
if (mode === 'modal') {
|
const mode = await openImChat({ conversationId: item.conversationId });
|
||||||
openImChatModal(true, { targetUserId: item.id, pageContext: null });
|
if (mode === 'modal') {
|
||||||
|
openImChatModal(true, { conversationId: item.conversationId });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const mode = await openImChat({ targetUserId: item.id, pageContext: null });
|
||||||
|
if (mode === 'modal') {
|
||||||
|
openImChatModal(true, { targetUserId: item.id, pageContext: null });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】统一 IM 打开入口,全页优先-----------
|
|
||||||
emit('closeModal');
|
emit('closeModal');
|
||||||
}
|
}
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】点击群聊条目直接打开群聊会话-----------
|
||||||
|
|
||||||
defineExpose({ reload });
|
defineExpose({ reload });
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -223,10 +223,19 @@
|
|||||||
async function showMessageDetail(record){
|
async function showMessageDetail(record){
|
||||||
if (isImChatNotice(record)) {
|
if (isImChatNotice(record)) {
|
||||||
//update-begin---author:xsl ---date:20260528 for:【IM聊天-OA】统一 IM 打开入口,全页优先-----------
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天-OA】统一 IM 打开入口,全页优先-----------
|
||||||
const mode = await openImChat({ targetUserId: record.imTargetUserId, pageContext: null });
|
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息支持打开群聊会话-----------
|
||||||
if (mode === 'modal') {
|
if (record.imConvType === 'group') {
|
||||||
openImChatModal(true, { targetUserId: record.imTargetUserId, pageContext: null });
|
const mode = await openImChat({ conversationId: record.imConversationId });
|
||||||
|
if (mode === 'modal') {
|
||||||
|
openImChatModal(true, { conversationId: record.imConversationId });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const mode = await openImChat({ targetUserId: record.imTargetUserId, pageContext: null });
|
||||||
|
if (mode === 'modal') {
|
||||||
|
openImChatModal(true, { targetUserId: record.imTargetUserId, pageContext: null });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息支持打开群聊会话-----------
|
||||||
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】统一 IM 打开入口,全页优先-----------
|
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】统一 IM 打开入口,全页优先-----------
|
||||||
emit('close-modal');
|
emit('close-modal');
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -154,7 +154,9 @@
|
|||||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||||
import { useImUnread } from '/@/views/system/im/useImUnread';
|
import { useImUnread } from '/@/views/system/im/useImUnread';
|
||||||
import { onWebSocket, offWebSocket, ensureWebSocketConnected } from '/@/hooks/web/useWebSocket';
|
import { onWebSocket, offWebSocket, ensureWebSocketConnected } from '/@/hooks/web/useWebSocket';
|
||||||
import { handleImChatSocket, isImChatUiOpen } from '/@/views/system/im/imCache';
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】只保留 isImChatUiOpen,handleImChatSocket 已移除(由 notify 统一调用)-----------
|
||||||
|
import { isImChatUiOpen } from '/@/views/system/im/imCache';
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】只保留 isImChatUiOpen,handleImChatSocket 已移除(由 notify 统一调用)-----------
|
||||||
import calendar from '/@/assets/icons/calendarNotice.png';
|
import calendar from '/@/assets/icons/calendarNotice.png';
|
||||||
import folder from '/@/assets/icons/folderNotice.png';
|
import folder from '/@/assets/icons/folderNotice.png';
|
||||||
import system from '/@/assets/icons/systemNotice.png';
|
import system from '/@/assets/icons/systemNotice.png';
|
||||||
@@ -439,8 +441,12 @@
|
|||||||
|
|
||||||
function onModalWebSocket(data) {
|
function onModalWebSocket(data) {
|
||||||
if (data.cmd === 'chat') {
|
if (data.cmd === 'chat') {
|
||||||
handleImChatSocket(data);
|
//update-begin---author:xsl ---date:20260528 for:【IM聊天】SysMessageModal 与 notify/index.vue 同时监听 WS,handleImChatSocket 已由 notify 处理,此处只做 UI 刷新-----------
|
||||||
|
// handleImChatSocket 已在 notify/index.vue 的 onWebSocketMessage 中调用
|
||||||
|
// 如果在此重复调用,incrementGroupUnread 和 dispatchImChatSocketUi 会触发两次
|
||||||
|
// 导致 patchGroup(unreadIncrement:1) 执行两次,群聊 tab 角标变为 2(实际只有 1 条消息)
|
||||||
refreshImUnread(false);
|
refreshImUnread(false);
|
||||||
|
//update-end---author:xsl ---date:20260528 for:【IM聊天】SysMessageModal 与 notify/index.vue 同时监听 WS,handleImChatSocket 已由 notify 处理,此处只做 UI 刷新-----------
|
||||||
if (isImChatUiOpen()) {
|
if (isImChatUiOpen()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
import { fetchDeptMembers } from '/@/views/system/im/im.api';
|
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息补充群聊提醒-----------
|
||||||
|
import { fetchDeptMembers, fetchGroups } from '/@/views/system/im/im.api';
|
||||||
|
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息补充群聊提醒-----------
|
||||||
import { formatImMessagePreview } from '/@/views/system/im/imMessageUtil';
|
import { formatImMessagePreview } from '/@/views/system/im/imMessageUtil';
|
||||||
|
|
||||||
export const IM_CHAT_BUS_TYPE = 'im_chat';
|
export const IM_CHAT_BUS_TYPE = 'im_chat';
|
||||||
@@ -27,6 +29,12 @@ export interface ImChatNoticeRecord {
|
|||||||
imTargetUsername?: string;
|
imTargetUsername?: string;
|
||||||
imAvatar?: string;
|
imAvatar?: string;
|
||||||
imUnreadCount?: number;
|
imUnreadCount?: number;
|
||||||
|
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息补充群聊提醒-----------
|
||||||
|
/** 会话类型:single 单聊 / group 群聊 */
|
||||||
|
imConvType?: 'single' | 'group';
|
||||||
|
/** 群聊会话 ID(群聊提醒打开会话时使用) */
|
||||||
|
imConversationId?: string;
|
||||||
|
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息补充群聊提醒-----------
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImContactLike {
|
interface ImContactLike {
|
||||||
@@ -40,6 +48,16 @@ interface ImContactLike {
|
|||||||
unreadCount?: number;
|
unreadCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息补充群聊提醒-----------
|
||||||
|
interface ImGroupLike {
|
||||||
|
conversationId: string;
|
||||||
|
groupName?: string;
|
||||||
|
lastContent?: string;
|
||||||
|
lastTime?: string;
|
||||||
|
unreadCount?: number;
|
||||||
|
}
|
||||||
|
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息补充群聊提醒-----------
|
||||||
|
|
||||||
/** 标星列表不包含 IM;全部消息或未指定类型、聊天类型时合并 IM */
|
/** 标星列表不包含 IM;全部消息或未指定类型、聊天类型时合并 IM */
|
||||||
export function shouldIncludeImChatInList(params: ImChatSearchParams) {
|
export function shouldIncludeImChatInList(params: ImChatSearchParams) {
|
||||||
if (params.starFlag === '1') {
|
if (params.starFlag === '1') {
|
||||||
@@ -142,14 +160,49 @@ export function mapImContactToNotice(contact: ImContactLike): ImChatNoticeRecord
|
|||||||
imTargetUsername: contact.username,
|
imTargetUsername: contact.username,
|
||||||
imAvatar: contact.avatar,
|
imAvatar: contact.avatar,
|
||||||
imUnreadCount: contact.unreadCount || 0,
|
imUnreadCount: contact.unreadCount || 0,
|
||||||
|
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息补充群聊提醒-----------
|
||||||
|
imConvType: 'single',
|
||||||
|
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息补充群聊提醒-----------
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterImChatNotices(contacts: ImContactLike[], params: ImChatSearchParams) {
|
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息补充群聊提醒-----------
|
||||||
let list = (contacts || [])
|
/** 将群聊会话映射为聊天提醒记录 */
|
||||||
|
export function mapImGroupToNotice(group: ImGroupLike): ImChatNoticeRecord | null {
|
||||||
|
if (!group?.conversationId || !group?.lastTime) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const name = group.groupName || '群聊';
|
||||||
|
const preview = formatImMessagePreview(group.lastContent) || '发来一条新消息';
|
||||||
|
const timeText = dayjs(group.lastTime).isValid() ? dayjs(group.lastTime).format('YYYY-MM-DD HH:mm:ss') : String(group.lastTime);
|
||||||
|
return {
|
||||||
|
id: `im_chat_group_${group.conversationId}`,
|
||||||
|
busType: IM_CHAT_BUS_TYPE,
|
||||||
|
titile: `${name}:${preview}`,
|
||||||
|
msgContent: preview,
|
||||||
|
sendTime: timeText,
|
||||||
|
createTime: timeText,
|
||||||
|
readFlag: (group.unreadCount || 0) > 0 ? '0' : '1',
|
||||||
|
noticeType: IM_CHAT_NOTICE_TYPE,
|
||||||
|
starFlag: '0',
|
||||||
|
imTargetUserId: '',
|
||||||
|
imUnreadCount: group.unreadCount || 0,
|
||||||
|
imConvType: 'group',
|
||||||
|
imConversationId: group.conversationId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterImChatNotices(contacts: ImContactLike[], groups: ImGroupLike[], params: ImChatSearchParams) {
|
||||||
|
const singleList = (contacts || [])
|
||||||
.map(mapImContactToNotice)
|
.map(mapImContactToNotice)
|
||||||
.filter((item): item is ImChatNoticeRecord => !!item)
|
.filter((item): item is ImChatNoticeRecord => !!item);
|
||||||
.filter((item) => isInRange(item.sendTime, params.rangeDateKey, params.rangeDate));
|
|
||||||
|
// 指定发件人筛选时,仅匹配单聊(群聊不属于单个发件人)
|
||||||
|
const groupList = params.fromUser
|
||||||
|
? []
|
||||||
|
: (groups || []).map(mapImGroupToNotice).filter((item): item is ImChatNoticeRecord => !!item);
|
||||||
|
|
||||||
|
let list = [...singleList, ...groupList].filter((item) => isInRange(item.sendTime, params.rangeDateKey, params.rangeDate));
|
||||||
|
|
||||||
if (params.fromUser) {
|
if (params.fromUser) {
|
||||||
list = list.filter((item) => item.imTargetUsername === params.fromUser || item.imTargetUserId === params.fromUser);
|
list = list.filter((item) => item.imTargetUsername === params.fromUser || item.imTargetUserId === params.fromUser);
|
||||||
@@ -159,9 +212,13 @@ export function filterImChatNotices(contacts: ImContactLike[], params: ImChatSea
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchImChatNoticeList(params: ImChatSearchParams) {
|
export async function fetchImChatNoticeList(params: ImChatSearchParams) {
|
||||||
const contacts = ((await fetchDeptMembers()) || []) as ImContactLike[];
|
const [contacts, groups] = (await Promise.all([
|
||||||
return filterImChatNotices(contacts, params);
|
Promise.resolve(fetchDeptMembers()).catch(() => []),
|
||||||
|
Promise.resolve(fetchGroups()).catch(() => []),
|
||||||
|
])) as [ImContactLike[], ImGroupLike[]];
|
||||||
|
return filterImChatNotices(contacts || [], groups || [], params);
|
||||||
}
|
}
|
||||||
|
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】全部消息补充群聊提醒-----------
|
||||||
|
|
||||||
export function mergeMessageList(systemList: any[] = [], imList: ImChatNoticeRecord[] = []) {
|
export function mergeMessageList(systemList: any[] = [], imList: ImChatNoticeRecord[] = []) {
|
||||||
if (!imList.length) {
|
if (!imList.length) {
|
||||||
|
|||||||
Reference in New Issue
Block a user