新增IM聊天群管理接口,包括群聊详情、添加成员、移除成员、修改群名称、转让群主、退出群聊及解散群聊功能,提升群聊管理体验。
This commit is contained in:
@@ -12,10 +12,12 @@ 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.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;
|
||||||
@@ -155,4 +157,69 @@ public class SysImChatController {
|
|||||||
return Result.OK(imChatService.createGroupConversation(user.getId(), resolveTenantId(user, request), user.getOrgCode(), dto));
|
return Result.OK(imChatService.createGroupConversation(user.getId(), resolveTenantId(user, request), user.getOrgCode(), dto));
|
||||||
}
|
}
|
||||||
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】群聊接口-----------
|
//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,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】群设置-添加群成员-----------
|
||||||
@@ -5,12 +5,15 @@ 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.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;
|
||||||
|
|
||||||
|
|
||||||
@@ -53,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】群设置-群管理接口-----------
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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.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;
|
||||||
@@ -20,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;
|
||||||
@@ -198,6 +201,193 @@ public class SysImChatServiceImpl implements ISysImChatService {
|
|||||||
}
|
}
|
||||||
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】群聊会话列表与创建-----------
|
//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) {
|
||||||
|
|||||||
@@ -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】群设置-群成员展示-----------
|
||||||
@@ -131,6 +131,9 @@
|
|||||||
</button>
|
</button>
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<a-menu @click="handleSettingsMenuClick">
|
<a-menu @click="handleSettingsMenuClick">
|
||||||
|
<!--update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群聊设置入口------------->
|
||||||
|
<a-menu-item v-if="isGroupChat" key="groupSettings">群设置</a-menu-item>
|
||||||
|
<!--update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群聊设置入口----------->
|
||||||
<a-menu-item key="chatSettings">聊天设置</a-menu-item>
|
<a-menu-item key="chatSettings">聊天设置</a-menu-item>
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</template>
|
</template>
|
||||||
@@ -214,6 +217,10 @@
|
|||||||
<ImChatSettingsModal @register="registerSettingsModal" @saved="onChatSettingsSaved" />
|
<ImChatSettingsModal @register="registerSettingsModal" @saved="onChatSettingsSaved" />
|
||||||
<ImPageListPickModal @register="registerListPickModal" @confirm="handleListRowsSend" />
|
<ImPageListPickModal @register="registerListPickModal" @confirm="handleListRowsSend" />
|
||||||
<ImCreateGroupModal @register="registerCreateGroupModal" @success="handleGroupCreated" />
|
<ImCreateGroupModal @register="registerCreateGroupModal" @success="handleGroupCreated" />
|
||||||
|
<!--update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置抽屉------------->
|
||||||
|
<ImGroupSettingDrawer @register="registerGroupSettingDrawer" @changed="handleGroupSettingChanged" @exited="handleGroupExited" />
|
||||||
|
<!--update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置抽屉----------->
|
||||||
|
|
||||||
<a-modal :open="previewVisible" :footer="null" width="720px" @cancel="closeImagePreview">
|
<a-modal :open="previewVisible" :footer="null" width="720px" @cancel="closeImagePreview">
|
||||||
<img alt="图片预览" style="width: 100%" :src="previewImageUrl" />
|
<img alt="图片预览" style="width: 100%" :src="previewImageUrl" />
|
||||||
</a-modal>
|
</a-modal>
|
||||||
@@ -243,6 +250,10 @@
|
|||||||
import ImPageListPickModal from './ImPageListPickModal.vue';
|
import ImPageListPickModal from './ImPageListPickModal.vue';
|
||||||
import ImBizRecordMessageContent from './ImBizRecordMessageContent.vue';
|
import ImBizRecordMessageContent from './ImBizRecordMessageContent.vue';
|
||||||
import ImCreateGroupModal from './ImCreateGroupModal.vue';
|
import ImCreateGroupModal from './ImCreateGroupModal.vue';
|
||||||
|
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置抽屉-----------
|
||||||
|
import ImGroupSettingDrawer from './ImGroupSettingDrawer.vue';
|
||||||
|
import { useDrawer } from '/@/components/Drawer';
|
||||||
|
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置抽屉-----------
|
||||||
import { useModal } from '/@/components/Modal';
|
import { useModal } from '/@/components/Modal';
|
||||||
import { useMessage } from '/@/hooks/web/useMessage';
|
import { useMessage } from '/@/hooks/web/useMessage';
|
||||||
import { buildImBizRecordPayload, parseImBizRecordPayload, serializeImBizRecordPayload } from './imBizRecordMessage';
|
import { buildImBizRecordPayload, parseImBizRecordPayload, serializeImBizRecordPayload } from './imBizRecordMessage';
|
||||||
@@ -499,6 +510,9 @@
|
|||||||
const [registerListPickModal, { openModal: openListPickModal }] = useModal();
|
const [registerListPickModal, { openModal: openListPickModal }] = useModal();
|
||||||
const [registerCreateGroupModal, { openModal: openCreateGroupModal }] = useModal();
|
const [registerCreateGroupModal, { openModal: openCreateGroupModal }] = useModal();
|
||||||
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】点击功能名气泡选择列表明细发送-----------
|
//update-end---author:xsl ---date:20260528 for:【IM聊天-OA】点击功能名气泡选择列表明细发送-----------
|
||||||
|
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置抽屉-----------
|
||||||
|
const [registerGroupSettingDrawer, { openDrawer: openGroupSettingDrawer, closeDrawer: closeGroupSettingDrawer }] = useDrawer();
|
||||||
|
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置抽屉-----------
|
||||||
const defaultHistoryDays = ref(getImDefaultHistoryDays());
|
const defaultHistoryDays = ref(getImDefaultHistoryDays());
|
||||||
const currentUserId = computed(() => userStore.getUserInfo?.id || '');
|
const currentUserId = computed(() => userStore.getUserInfo?.id || '');
|
||||||
const deptLabel = computed(() => {
|
const deptLabel = computed(() => {
|
||||||
@@ -715,9 +729,60 @@
|
|||||||
function handleSettingsMenuClick({ key }: { key: string }) {
|
function handleSettingsMenuClick({ key }: { key: string }) {
|
||||||
if (key === 'chatSettings') {
|
if (key === 'chatSettings') {
|
||||||
openSettingsModal(true, {});
|
openSettingsModal(true, {});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】打开群设置抽屉-----------
|
||||||
|
if (key === 'groupSettings') {
|
||||||
|
if (!activeGroup.value?.conversationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openGroupSettingDrawer(true, { conversationId: activeGroup.value.conversationId });
|
||||||
|
}
|
||||||
|
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】打开群设置抽屉-----------
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//update-begin---author:cursor ---date:20260529 for:【IM聊天-OA】群设置变更/退出回调-----------
|
||||||
|
/** 群信息变更(改名/加人/踢人/转让):同步标题、成员数,并刷新群列表 */
|
||||||
|
async function handleGroupSettingChanged(payload: { conversationId: string; groupName?: string; memberCount?: number }) {
|
||||||
|
if (!payload?.conversationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
patchGroup(payload.conversationId, {
|
||||||
|
groupName: payload.groupName,
|
||||||
|
memberCount: payload.memberCount,
|
||||||
|
}, { moveToTop: false });
|
||||||
|
if (activeGroup.value?.conversationId === payload.conversationId) {
|
||||||
|
activeGroup.value = {
|
||||||
|
...activeGroup.value,
|
||||||
|
groupName: payload.groupName ?? activeGroup.value.groupName,
|
||||||
|
memberCount: payload.memberCount ?? activeGroup.value.memberCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 后台静默刷新群列表,保证成员数等准确
|
||||||
|
await loadGroups(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 退出或解散群聊:从列表移除,若为当前会话则清空聊天区 */
|
||||||
|
async function handleGroupExited(conversationId: string) {
|
||||||
|
if (!conversationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
groupList.value = groupList.value.filter((item) => item.conversationId !== conversationId);
|
||||||
|
resetGroupUnread(conversationId);
|
||||||
|
syncAllUnread();
|
||||||
|
if (activeGroup.value?.conversationId === conversationId) {
|
||||||
|
activeGroup.value = null;
|
||||||
|
activeChatType.value = '';
|
||||||
|
activeConversationId.value = '';
|
||||||
|
setImActiveConversationId('');
|
||||||
|
messageList.value = [];
|
||||||
|
draft.value = '';
|
||||||
|
clearImActiveSession();
|
||||||
|
}
|
||||||
|
closeGroupSettingDrawer();
|
||||||
|
}
|
||||||
|
//update-end---author:cursor ---date:20260529 for:【IM聊天-OA】群设置变更/退出回调-----------
|
||||||
|
|
||||||
function onChatSettingsSaved() {
|
function onChatSettingsSaved() {
|
||||||
defaultHistoryDays.value = getImDefaultHistoryDays();
|
defaultHistoryDays.value = getImDefaultHistoryDays();
|
||||||
if (activeConversationId.value) {
|
if (activeConversationId.value) {
|
||||||
|
|||||||
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】群设置抽屉----------->
|
||||||
@@ -8,6 +8,15 @@ enum Api {
|
|||||||
read = '/sys/im/chat/read',
|
read = '/sys/im/chat/read',
|
||||||
groups = '/sys/im/chat/groups',
|
groups = '/sys/im/chat/groups',
|
||||||
groupCreate = '/sys/im/chat/group/create',
|
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 {
|
export interface ImGroupConversation {
|
||||||
@@ -57,3 +66,51 @@ export const fetchGroups = () => defHttp.get<ImGroupConversation[]>({ url: Api.g
|
|||||||
|
|
||||||
export const createGroupConversation = (data: { groupName: string; memberUserIds: string[] }) =>
|
export const createGroupConversation = (data: { groupName: string; memberUserIds: string[] }) =>
|
||||||
defHttp.post<ImGroupConversation>({ url: Api.groupCreate, data });
|
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】群设置接口-----------
|
||||||
|
|||||||
Reference in New Issue
Block a user