Browse Source

ui美化

master
xsx 2 years ago
parent
commit
ee570cdbd5
  1. 19
      im-client/src/main/java/com/bx/imclient/IMClient.java
  2. 39
      im-client/src/main/java/com/bx/imclient/sender/IMSender.java
  3. 14
      im-platform/src/main/java/com/bx/implatform/controller/UserController.java
  4. 6
      im-platform/src/main/java/com/bx/implatform/service/IUserService.java
  5. 14
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java
  6. 1
      im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java
  7. 48
      im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java
  8. 3
      im-platform/src/main/java/com/bx/implatform/vo/GroupMemberVO.java
  9. 24
      im-platform/src/main/java/com/bx/implatform/vo/OnlineTerminalVO.java
  10. 4
      im-server/src/main/java/com/bx/imserver/netty/IMChannelHandler.java
  11. 4
      im-server/src/main/java/com/bx/imserver/netty/processor/HeartbeatProcessor.java
  12. 8
      im-ui/src/api/enums.js
  13. BIN
      im-ui/src/assets/default_head.png
  14. 2
      im-ui/src/components/chat/ChatItem.vue
  15. 2
      im-ui/src/components/chat/ChatMessageItem.vue
  16. 67
      im-ui/src/components/common/HeadImage.vue
  17. 22
      im-ui/src/components/common/UserInfo.vue
  18. 5
      im-ui/src/components/friend/AddFriend.vue
  19. 19
      im-ui/src/components/friend/FriendItem.vue
  20. 2
      im-ui/src/components/group/GroupItem.vue
  21. 4
      im-ui/src/components/group/GroupMember.vue
  22. 26
      im-ui/src/store/friendStore.js
  23. 4
      im-ui/src/view/Friend.vue
  24. 11
      im-ui/src/view/Group.vue
  25. 10
      im-ui/src/view/Home.vue
  26. 2
      im-ui/src/view/Login.vue
  27. 8
      im-uniapp/common/enums.js
  28. 11
      im-uniapp/components/chat-item/chat-item.vue
  29. 65
      im-uniapp/components/chat-message-item/chat-message-item.vue
  30. 45
      im-uniapp/components/friend-item/friend-item.vue
  31. 20
      im-uniapp/components/group-item/group-item.vue
  32. 104
      im-uniapp/components/head-image/head-image.vue
  33. 28
      im-uniapp/pages/common/user-info.vue
  34. 32
      im-uniapp/pages/friend/friend-add.vue
  35. 4
      im-uniapp/pages/group/group-edit.vue
  36. 30
      im-uniapp/pages/group/group-info.vue
  37. 37
      im-uniapp/pages/group/group-invite.vue
  38. 45
      im-uniapp/pages/group/group-member.vue
  39. 36
      im-uniapp/store/friendStore.js

19
im-client/src/main/java/com/bx/imclient/IMClient.java

@ -1,14 +1,14 @@
package com.bx.imclient; package com.bx.imclient;
import com.bx.imclient.sender.IMSender; import com.bx.imclient.sender.IMSender;
import com.bx.imcommon.enums.IMTerminalType;
import com.bx.imcommon.model.IMGroupMessage; import com.bx.imcommon.model.IMGroupMessage;
import com.bx.imcommon.model.IMPrivateMessage; import com.bx.imcommon.model.IMPrivateMessage;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
@Configuration @Configuration
public class IMClient { public class IMClient {
@ -31,8 +31,19 @@ public class IMClient {
* @param userIds 用户id列表 * @param userIds 用户id列表
* @return 在线的用户列表 * @return 在线的用户列表
*/ */
public List<Long> isOnline(List<Long> userIds){ public List<Long> getOnlineUser(List<Long> userIds){
return imSender.isOnline(userIds); return imSender.getOnlineUser(userIds);
}
/**
* 判断多个用户是否在线
*
* @param userIds 用户id列表
* @return 在线的用户终端
*/
public Map<Long,List<IMTerminalType>> getOnlineTerminal(List<Long> userIds){
return imSender.getOnlineTerminal(userIds);
} }
/** /**

39
im-client/src/main/java/com/bx/imclient/sender/IMSender.java

@ -14,6 +14,7 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Service @Service
@ -90,12 +91,12 @@ public class IMSender {
List<Object> serverIds = redisTemplate.opsForValue().multiGet(sendMap.keySet()); List<Object> serverIds = redisTemplate.opsForValue().multiGet(sendMap.keySet());
// 格式:map<服务器id,list<接收方>> // 格式:map<服务器id,list<接收方>>
Map<Integer, List<IMUserInfo>> serverMap = new HashMap<>(); Map<Integer, List<IMUserInfo>> serverMap = new HashMap<>();
List<IMUserInfo> offLineUsers = Collections.synchronizedList(new LinkedList<>()); List<IMUserInfo> offLineUsers = new LinkedList<>();
int idx = 0; int idx = 0;
for (Map.Entry<String,IMUserInfo> entry : sendMap.entrySet()) { for (Map.Entry<String,IMUserInfo> entry : sendMap.entrySet()) {
Integer serverId = (Integer)serverIds.get(idx++); Integer serverId = (Integer)serverIds.get(idx++);
if (serverId != null) { if (serverId != null) {
List<IMUserInfo> list = serverMap.computeIfAbsent(serverId, o -> Collections.synchronizedList(new LinkedList<>())); List<IMUserInfo> list = serverMap.computeIfAbsent(serverId, o -> new LinkedList<>());
list.add(entry.getValue()); list.add(entry.getValue());
} else { } else {
// 加入离线列表 // 加入离线列表
@ -150,34 +151,40 @@ public class IMSender {
} }
} }
public Boolean isOnline(Long userId) { public Map<Long,List<IMTerminalType>> getOnlineTerminal(List<Long> userIds){
String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, userId.toString(), "*");
return !redisTemplate.keys(key).isEmpty();
}
public List<Long> isOnline(List<Long> userIds){
if(CollectionUtil.isEmpty(userIds)){ if(CollectionUtil.isEmpty(userIds)){
return Collections.emptyList(); return Collections.EMPTY_MAP;
} }
// 把所有用户的key都存起来 // 把所有用户的key都存起来
Map<String,Long> keyMap = new HashMap<>(); Map<String,IMUserInfo> userMap = new HashMap<>();
for(Long id:userIds){ for(Long id:userIds){
for (Integer terminal : IMTerminalType.codes()) { for (Integer terminal : IMTerminalType.codes()) {
String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, id.toString(), terminal.toString()); String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, id.toString(), terminal.toString());
keyMap.put(key,id); userMap.put(key,new IMUserInfo(id,terminal));
} }
} }
// 批量拉取 // 批量拉取
List<Object> serverIds = redisTemplate.opsForValue().multiGet(keyMap.keySet()); List<Object> serverIds = redisTemplate.opsForValue().multiGet(userMap.keySet());
int idx = 0; int idx = 0;
List<Long> onlineIds = new LinkedList<>(); Map<Long,List<IMTerminalType>> onlineMap = new HashMap<>();
for (Map.Entry<String,Long> entry : keyMap.entrySet()) { for (Map.Entry<String,IMUserInfo> entry : userMap.entrySet()) {
// serverid有值表示用户在线 // serverid有值表示用户在线
if(serverIds.get(idx++) != null){ if(serverIds.get(idx++) != null){
onlineIds.add(entry.getValue()); IMUserInfo userInfo = entry.getValue();
List<IMTerminalType> terminals = onlineMap.computeIfAbsent(userInfo.getId(), o -> new LinkedList<>());
terminals.add(IMTerminalType.fromCode(userInfo.getTerminal()));
} }
} }
// 去重并返回 // 去重并返回
return onlineIds.stream().distinct().collect(Collectors.toList()); return onlineMap;
}
public Boolean isOnline(Long userId) {
String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID, userId.toString(), "*");
return !redisTemplate.keys(key).isEmpty();
}
public List<Long> getOnlineUser(List<Long> userIds){
return new LinkedList<>(getOnlineTerminal(userIds).keySet());
} }
} }

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

@ -7,6 +7,7 @@ import com.bx.implatform.service.IUserService;
import com.bx.implatform.session.SessionContext; import com.bx.implatform.session.SessionContext;
import com.bx.implatform.session.UserSession; import com.bx.implatform.session.UserSession;
import com.bx.implatform.util.BeanUtils; import com.bx.implatform.util.BeanUtils;
import com.bx.implatform.vo.OnlineTerminalVO;
import com.bx.implatform.vo.UserVO; import com.bx.implatform.vo.UserVO;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
@ -34,6 +35,13 @@ public class UserController {
return ResultUtils.success(onlineIds); return ResultUtils.success(onlineIds);
} }
@GetMapping("/terminal/online")
@ApiOperation(value = "判断用户哪个终端在线",notes="返回在线的用户id的终端集合")
public Result<List<OnlineTerminalVO>> getOnlineTerminal(@NotEmpty @RequestParam("userIds") String userIds){
return ResultUtils.success(userService.getOnlineTerminals(userIds));
}
@GetMapping("/self") @GetMapping("/self")
@ApiOperation(value = "获取当前用户信息",notes="获取当前用户信息") @ApiOperation(value = "获取当前用户信息",notes="获取当前用户信息")
public Result<UserVO> findSelfInfo(){ public Result<UserVO> findSelfInfo(){
@ -46,10 +54,8 @@ public class UserController {
@GetMapping("/find/{id}") @GetMapping("/find/{id}")
@ApiOperation(value = "查找用户",notes="根据id查找用户") @ApiOperation(value = "查找用户",notes="根据id查找用户")
public Result findByIde(@NotEmpty @PathVariable("id") long id){ public Result findById(@NotEmpty @PathVariable("id") Long id){
User user = userService.getById(id); return ResultUtils.success(userService.findUserById(id));
UserVO userVO = BeanUtils.copyProperties(user,UserVO.class);
return ResultUtils.success(userVO);
} }
@PutMapping("/update") @PutMapping("/update")

6
im-platform/src/main/java/com/bx/implatform/service/IUserService.java

@ -6,6 +6,7 @@ import com.bx.implatform.entity.User;
import com.bx.implatform.dto.LoginDTO; import com.bx.implatform.dto.LoginDTO;
import com.bx.implatform.dto.RegisterDTO; import com.bx.implatform.dto.RegisterDTO;
import com.bx.implatform.vo.LoginVO; import com.bx.implatform.vo.LoginVO;
import com.bx.implatform.vo.OnlineTerminalVO;
import com.bx.implatform.vo.UserVO; import com.bx.implatform.vo.UserVO;
import java.util.List; import java.util.List;
@ -25,8 +26,13 @@ public interface IUserService extends IService<User> {
void update(UserVO vo); void update(UserVO vo);
UserVO findUserById(Long id);
List<UserVO> findUserByName(String name); List<UserVO> findUserByName(String name);
List<Long> checkOnline(String userIds); List<Long> checkOnline(String userIds);
List<OnlineTerminalVO> getOnlineTerminals(String userIds);
} }

14
im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java

@ -3,6 +3,7 @@ package com.bx.implatform.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bx.imclient.IMClient;
import com.bx.implatform.contant.Constant; import com.bx.implatform.contant.Constant;
import com.bx.implatform.contant.RedisKey; import com.bx.implatform.contant.RedisKey;
import com.bx.implatform.entity.Friend; import com.bx.implatform.entity.Friend;
@ -52,6 +53,9 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
@Autowired @Autowired
private IFriendService friendsService; private IFriendService friendsService;
@Autowired
private IMClient imClient;
/** /**
* 创建新群聊 * 创建新群聊
* *
@ -292,7 +296,15 @@ public class GroupServiceImpl extends ServiceImpl<GroupMapper, Group> implements
@Override @Override
public List<GroupMemberVO> findGroupMembers(Long groupId) { public List<GroupMemberVO> findGroupMembers(Long groupId) {
List<GroupMember> members = groupMemberService.findByGroupId(groupId); List<GroupMember> members = groupMemberService.findByGroupId(groupId);
return members.stream().map(m->BeanUtils.copyProperties(m,GroupMemberVO.class)).collect(Collectors.toList()); List<Long> userIds = members.stream().map(GroupMember::getUserId).collect(Collectors.toList());
List<Long> onlineUserIds = imClient.getOnlineUser(userIds);
return members.stream().map(m->{
GroupMemberVO vo = BeanUtils.copyProperties(m,GroupMemberVO.class);
vo.setOnline(onlineUserIds.contains(m.getUserId()));
return vo;
}).sorted((m1,m2)->{
return m2.getOnline().compareTo(m1.getOnline());
}).collect(Collectors.toList());
} }
} }

1
im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java

@ -6,7 +6,6 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bx.imclient.IMClient; import com.bx.imclient.IMClient;
import com.bx.imcommon.contant.IMConstant; import com.bx.imcommon.contant.IMConstant;
import com.bx.imcommon.enums.IMTerminalType;
import com.bx.imcommon.model.IMPrivateMessage; import com.bx.imcommon.model.IMPrivateMessage;
import com.bx.imcommon.model.IMUserInfo; import com.bx.imcommon.model.IMUserInfo;
import com.bx.implatform.entity.Friend; import com.bx.implatform.entity.Friend;

48
im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java

@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bx.imclient.IMClient; import com.bx.imclient.IMClient;
import com.bx.imcommon.enums.IMTerminalType;
import com.bx.imcommon.util.JwtUtil; import com.bx.imcommon.util.JwtUtil;
import com.bx.implatform.config.JwtProperties; import com.bx.implatform.config.JwtProperties;
import com.bx.implatform.dto.LoginDTO; import com.bx.implatform.dto.LoginDTO;
@ -24,6 +25,7 @@ import com.bx.implatform.session.SessionContext;
import com.bx.implatform.session.UserSession; import com.bx.implatform.session.UserSession;
import com.bx.implatform.util.BeanUtils; import com.bx.implatform.util.BeanUtils;
import com.bx.implatform.vo.LoginVO; import com.bx.implatform.vo.LoginVO;
import com.bx.implatform.vo.OnlineTerminalVO;
import com.bx.implatform.vo.UserVO; import com.bx.implatform.vo.UserVO;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -33,7 +35,9 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays; import java.util.Arrays;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -95,7 +99,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IU
/** /**
* 用refreshToken换取新 token * 用refreshToken换取新 token
* *
* @param refreshToken * @param refreshToken 刷新token
* @return 登录token * @return 登录token
*/ */
@Override @Override
@ -150,7 +154,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IU
* 根据用户名查询用户 * 根据用户名查询用户
* *
* @param username 用户名 * @param username 用户名
* @return * @return 用户信息
*/ */
@Override @Override
public User findUserByUserName(String username) { public User findUserByUserName(String username) {
@ -205,12 +209,25 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IU
log.info("用户信息更新,用户:{}}", user); log.info("用户信息更新,用户:{}}", user);
} }
/**
* 根据用户昵id查询用户以及在线状态
*
* @param id 用户id
* @return 用户信息
*/
@Override
public UserVO findUserById(Long id) {
User user = this.getById(id);
UserVO vo = BeanUtils.copyProperties(user,UserVO.class);
vo.setOnline(imClient.isOnline(id));
return vo;
}
/** /**
* 根据用户昵称查询用户最多返回20条数据 * 根据用户昵称查询用户最多返回20条数据
* *
* @param name 用户名或昵称 * @param name 用户名或昵称
* @return * @return 用户列表
*/ */
@Override @Override
public List<UserVO> findUserByName(String name) { public List<UserVO> findUserByName(String name) {
@ -221,7 +238,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IU
.last("limit 20"); .last("limit 20");
List<User> users = this.list(queryWrapper); List<User> users = this.list(queryWrapper);
List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList()); List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
List<Long> onlineUserIds = imClient.isOnline(userIds); List<Long> onlineUserIds = imClient.getOnlineUser(userIds);
return users.stream().map(u-> { return users.stream().map(u-> {
UserVO vo = BeanUtils.copyProperties(u,UserVO.class); UserVO vo = BeanUtils.copyProperties(u,UserVO.class);
vo.setOnline(onlineUserIds.contains(u.getId())); vo.setOnline(onlineUserIds.contains(u.getId()));
@ -239,7 +256,28 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IU
public List<Long> checkOnline(String userIds) { public List<Long> checkOnline(String userIds) {
List<Long> userIdList = Arrays.stream(userIds.split(",")) List<Long> userIdList = Arrays.stream(userIds.split(","))
.map(Long::parseLong).collect(Collectors.toList()); .map(Long::parseLong).collect(Collectors.toList());
return imClient.isOnline(userIdList); return imClient.getOnlineUser(userIdList);
} }
/**
* 获取用户在线的终端类型
*
* @param userIds 用户id多个用,分割
* @return 在线用户终端
*/
@Override
public List<OnlineTerminalVO> getOnlineTerminals(String userIds) {
List<Long> userIdList = Arrays.stream(userIds.split(","))
.map(Long::parseLong).collect(Collectors.toList());
// 查询在线的终端
Map<Long,List<IMTerminalType>> terminalMap = imClient.getOnlineTerminal(userIdList);
// 组装vo
List<OnlineTerminalVO> vos = new LinkedList<>();
terminalMap.forEach((userId,types)->{
List<Integer> terminals = types.stream().map(IMTerminalType::code).collect(Collectors.toList());
vos.add(new OnlineTerminalVO(userId,terminals));
});
return vos;
}
} }

3
im-platform/src/main/java/com/bx/implatform/vo/GroupMemberVO.java

@ -21,6 +21,9 @@ public class GroupMemberVO {
@ApiModelProperty("是否已退出") @ApiModelProperty("是否已退出")
private Boolean quit; private Boolean quit;
@ApiModelProperty(value = "是否在线")
private Boolean online;
@ApiModelProperty("备注") @ApiModelProperty("备注")
private String remark; private String remark;

24
im-platform/src/main/java/com/bx/implatform/vo/OnlineTerminalVO.java

@ -0,0 +1,24 @@
package com.bx.implatform.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.List;
/**
* @author: 谢绍许
* @date: 2023-10-28 21:17:59
* @version: 1.0
*/
@Data
@AllArgsConstructor
public class OnlineTerminalVO {
@ApiModelProperty(value = "用户id")
private Long userId;
@ApiModelProperty(value = "在线终端类型")
private List<Integer> terminals;
}

4
im-server/src/main/java/com/bx/imserver/netty/IMChannelHandler.java

@ -91,8 +91,8 @@ public class IMChannelHandler extends SimpleChannelInboundHandler<IMSendInfo> {
AttributeKey<Long> attr = AttributeKey.valueOf("USER_ID"); AttributeKey<Long> attr = AttributeKey.valueOf("USER_ID");
Long userId = ctx.channel().attr(attr).get(); Long userId = ctx.channel().attr(attr).get();
AttributeKey<Integer> terminalAttr = AttributeKey.valueOf(ChannelAttrKey.TERMINAL_TYPE); AttributeKey<Integer> terminalAttr = AttributeKey.valueOf(ChannelAttrKey.TERMINAL_TYPE);
Integer ternimal = ctx.channel().attr(terminalAttr).get(); Integer terminal = ctx.channel().attr(terminalAttr).get();
log.info("心跳超时,即将断开连接,用户id:{},终端类型:{} ",userId,ternimal); log.info("心跳超时,即将断开连接,用户id:{},终端类型:{} ",userId,terminal);
ctx.channel().close(); ctx.channel().close();
} }
} else { } else {

4
im-server/src/main/java/com/bx/imserver/netty/processor/HeartbeatProcessor.java

@ -45,8 +45,8 @@ public class HeartbeatProcessor extends AbstractMessageProcessor<IMHeartbeatInfo
AttributeKey<Long> userIdAttr = AttributeKey.valueOf(ChannelAttrKey.USER_ID); AttributeKey<Long> userIdAttr = AttributeKey.valueOf(ChannelAttrKey.USER_ID);
Long userId = ctx.channel().attr(userIdAttr).get(); Long userId = ctx.channel().attr(userIdAttr).get();
AttributeKey<Integer> terminalAttr = AttributeKey.valueOf(ChannelAttrKey.TERMINAL_TYPE); AttributeKey<Integer> terminalAttr = AttributeKey.valueOf(ChannelAttrKey.TERMINAL_TYPE);
Integer ternimal = ctx.channel().attr(terminalAttr).get(); Integer terminal = ctx.channel().attr(terminalAttr).get();
String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID,userId.toString(),ternimal.toString()); String key = String.join(":", IMRedisKey.IM_USER_SERVER_ID,userId.toString(),terminal.toString());
redisTemplate.expire(key, IMConstant.ONLINE_TIMEOUT_SECOND, TimeUnit.SECONDS); redisTemplate.expire(key, IMConstant.ONLINE_TIMEOUT_SECOND, TimeUnit.SECONDS);
} }
} }

8
im-ui/src/api/enums.js

@ -22,7 +22,13 @@ const USER_STATE = {
BUSY: 2 BUSY: 2
} }
const TERMINAL_TYPE = {
WEB: 0,
APP: 1
}
export { export {
MESSAGE_TYPE, MESSAGE_TYPE,
USER_STATE USER_STATE,
TERMINAL_TYPE
} }

BIN
im-ui/src/assets/default_head.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

2
im-ui/src/components/chat/ChatItem.vue

@ -1,7 +1,7 @@
<template> <template>
<div class="chat-item" :class="active ? 'active' : ''" @contextmenu.prevent="showRightMenu($event)"> <div class="chat-item" :class="active ? 'active' : ''" @contextmenu.prevent="showRightMenu($event)">
<div class="chat-left"> <div class="chat-left">
<head-image :url="chat.headImage" :size="50" :id="chat.type=='PRIVATE'?chat.targetId:0"></head-image> <head-image :url="chat.headImage" :name="chat.showName" :size="50" :id="chat.type=='PRIVATE'?chat.targetId:0"></head-image>
<div v-show="chat.unreadCount>0" class="unread-text">{{chat.unreadCount}}</div> <div v-show="chat.unreadCount>0" class="unread-text">{{chat.unreadCount}}</div>
</div> </div>
<div class="chat-right"> <div class="chat-right">

2
im-ui/src/components/chat/ChatMessageItem.vue

@ -5,7 +5,7 @@
<div class="chat-msg-normal" v-show="msgInfo.type>=0 && msgInfo.type<10" :class="{'chat-msg-mine':mine}"> <div class="chat-msg-normal" v-show="msgInfo.type>=0 && msgInfo.type<10" :class="{'chat-msg-mine':mine}">
<div class="head-image"> <div class="head-image">
<head-image :size="40" :url="headImage" :id="msgInfo.sendId"></head-image> <head-image :name="showName" :size="40" :url="headImage" :id="msgInfo.sendId"></head-image>
</div> </div>
<div class="chat-msg-content"> <div class="chat-msg-content">
<div v-show="mode==1 && msgInfo.groupId && !msgInfo.selfSend" class="chat-msg-top"> <div v-show="mode==1 && msgInfo.groupId && !msgInfo.selfSend" class="chat-msg-top">

67
im-ui/src/components/common/HeadImage.vue

@ -1,6 +1,9 @@
<template> <template>
<div class="head-image" @click="showUserInfo($event)"> <div class="head-image" @click="showUserInfo($event)">
<img :src="url" :style="{width: size+'px',height: size+'px',cursor: 'pointer'}" /> <img class="avatar-image" v-show="url" :src="url" :style="avatarImageStyle" />
<div class="avatar-text" v-show="!url" :style="avatarTextStyle">
{{name.substring(0,1).toUpperCase()}}</div>
<div v-show="online" class="online" title="用户当前在线"></div>
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
@ -10,7 +13,11 @@
export default { export default {
name: "headImage", name: "headImage",
data() { data() {
return {} return {
colors:["#7dd24b","#c7515a","#db68ef","#15d29b","#85029b",
"#c9b455","#fb2609","#bda818","#af0831","#326eb6"]
}
}, },
props: { props: {
id:{ id:{
@ -22,6 +29,14 @@
}, },
url: { url: {
type: String type: String
},
name:{
type: String,
default: "X"
},
online:{
type: Boolean,
default:false
} }
}, },
methods:{ methods:{
@ -36,26 +51,54 @@
}) })
} }
} }
} },
computed:{
avatarImageStyle(){
return `width:${this.size}px; height:${this.size}px;`
},
avatarTextStyle(){
return `width: ${this.size}px;height:${this.size}px;
color:${this.textColor};font-size:${this.size*0.6}px;`
},
textColor(){
let hash = 0;
for (var i = 0; i< this.name.length; i++) {
hash += this.name.charCodeAt(i);
}
return this.colors[hash%this.colors.length];
}
}
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.head-image { .head-image {
position: relative; position: relative;
img { cursor: pointer;
.avatar-image {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
border-radius: 10%; border-radius: 10%;
} }
img:before { .avatar-text{
content: ''; background-color: #f2f2f2; /* 默认背景色 */
display: block; border-radius: 50%; /* 圆角效果 */
width: 100%; display: flex;
height: 100%; align-items: center;
background: url('../../assets/default_head.png') no-repeat 0 0; justify-content: center;
background-size: 100%; border: 1px solid #ccc;
}
.online{
position: absolute;
right: -5px;
bottom: 0;
width: 12px;
height: 12px;
background: limegreen;
border-radius: 50%;
border: 3px solid white;
} }
} }
</style> </style>

22
im-ui/src/components/common/UserInfo.vue

@ -3,7 +3,8 @@
<div class="user-info" :style="{left: pos.x+'px',top: pos.y+'px'}" @click.stop> <div class="user-info" :style="{left: pos.x+'px',top: pos.y+'px'}" @click.stop>
<div class="user-info-box"> <div class="user-info-box">
<div class="avatar"> <div class="avatar">
<head-image :url="user.headImageThumb" :size="60" @click.native="showFullImage()"> </head-image> <head-image :name="user.nickName" :url="user.headImageThumb" :size="60" :online="user.online"
@click.native="showFullImage()"> </head-image>
</div> </div>
<div> <div>
<el-descriptions :column="1" :title="user.userName" class="user-info-items"> <el-descriptions :column="1" :title="user.userName" class="user-info-items">
@ -13,7 +14,7 @@
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
</div> </div>
</div> </div>
<el-divider content-position="center"></el-divider> <el-divider content-position="center"></el-divider>
<div class="user-btn-group"> <div class="user-btn-group">
@ -34,7 +35,7 @@
}, },
data() { data() {
return { return {
} }
}, },
props: { props: {
@ -79,9 +80,9 @@
this.$store.commit("addFriend", friend); this.$store.commit("addFriend", friend);
}) })
}, },
showFullImage(){ showFullImage() {
if(this.user.headImage){ if (this.user.headImage) {
this.$store.commit("showFullImageBox",this.user.headImage); this.$store.commit("showFullImageBox", this.user.headImage);
} }
} }
}, },
@ -118,14 +119,15 @@
margin-left: 10px; margin-left: 10px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
.el-descriptions__header {
.el-descriptions__header {
margin-bottom: 5px; margin-bottom: 5px;
} }
.el-descriptions__title { .el-descriptions__title {
font-size: 20px; font-size: 20px;
} }
.el-descriptions-item__cell { .el-descriptions-item__cell {
padding-bottom: 1px; padding-bottom: 1px;
} }
@ -136,4 +138,4 @@
text-align: center; text-align: center;
} }
} }
</style> </style>

5
im-ui/src/components/friend/AddFriend.vue

@ -7,7 +7,10 @@
<div v-for="(user) in users" :key="user.id" v-show="user.id != $store.state.userStore.userInfo.id"> <div v-for="(user) in users" :key="user.id" v-show="user.id != $store.state.userStore.userInfo.id">
<div class="item"> <div class="item">
<div class="avatar"> <div class="avatar">
<head-image :url="user.headImage"></head-image> <head-image :name="user.nickName"
:url="user.headImage"
:online="user.online"
></head-image>
</div> </div>
<div class="add-friend-text"> <div class="add-friend-text">
<div class="text-user-name"> <div class="text-user-name">

19
im-ui/src/components/friend/FriendItem.vue

@ -1,11 +1,17 @@
<template> <template>
<div class="friend-item" :class="active ? 'active' : ''" @contextmenu.prevent="showRightMenu($event)"> <div class="friend-item" :class="active ? 'active' : ''" @contextmenu.prevent="showRightMenu($event)">
<div class="friend-avatar"> <div class="friend-avatar">
<head-image :url="friend.headImage"> </head-image> <head-image :name="friend.nickName"
:url="friend.headImage"
:online="friend.online">
</head-image>
</div> </div>
<div class="friend-info"> <div class="friend-info">
<div class="friend-name">{{ friend.nickName}}</div> <div class="friend-name">{{ friend.nickName}}</div>
<div class="friend-online" :class="friend.online ? 'online':''">{{ friend.online?"[在线]":"[离线]"}}</div> <div class="friend-online online">
<span v-show="friend.onlineWeb" class="el-icon-s-platform" title="电脑端在线"></span>
<span v-show="friend.onlineApp" class="el-icon-mobile-phone" title="移动端在线"></span>
</div>
</div> </div>
<right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="rightMenu.items" <right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="rightMenu.items"
@close="rightMenu.show=false" @select="handleSelectMenu"></right-menu> @close="rightMenu.show=false" @select="handleSelectMenu"></right-menu>
@ -121,11 +127,10 @@
} }
.friend-online { .friend-online {
font-size: 12px; padding-right: 15px;
font-size: 16px;
&.online { font-weight: 600;
color: #5fb878; color: #2f6dce;
}
} }
} }
} }

2
im-ui/src/components/group/GroupItem.vue

@ -1,7 +1,7 @@
<template> <template>
<div class="group-item" :class="active ? 'active' : ''"> <div class="group-item" :class="active ? 'active' : ''">
<div class="group-avatar"> <div class="group-avatar">
<head-image :url="group.headImage"> </head-image> <head-image :name="group.remark" :url="group.headImage"> </head-image>
</div> </div>
<div class="group-name"> <div class="group-name">
<div>{{group.remark}}</div> <div>{{group.remark}}</div>

4
im-ui/src/components/group/GroupMember.vue

@ -1,6 +1,8 @@
<template> <template>
<div class="group-member"> <div class="group-member">
<head-image :url="member.headImage" :size="50" :id="member.userId"> <head-image :id="member.userId" :name="member.aliasName"
:url="member.headImage" :size="50"
:online="member.online" >
<div v-if="showDel" @click.stop="handleDelete()" class="btn-kick el-icon-error"></div> <div v-if="showDel" @click.stop="handleDelete()" class="btn-kick el-icon-error"></div>
</head-image> </head-image>
<div class="member-name">{{member.aliasName}}</div> <div class="member-name">{{member.aliasName}}</div>

26
im-ui/src/store/friendStore.js

@ -1,4 +1,5 @@
import httpRequest from '../api/httpRequest.js' import httpRequest from '../api/httpRequest.js'
import {TERMINAL_TYPE} from "../api/enums.js"
export default { export default {
@ -51,11 +52,11 @@ export default {
} }
state.friends.forEach((f)=>{userIds.push(f.id)}); state.friends.forEach((f)=>{userIds.push(f.id)});
httpRequest({ httpRequest({
url: '/user/online', url: '/user/terminal/online',
method: 'get', method: 'get',
params: {userIds: userIds.join(',')} params: {userIds: userIds.join(',')}
}).then((onlineIds) => { }).then((onlineTerminals) => {
this.commit("setOnlineStatus",onlineIds); this.commit("setOnlineStatus",onlineTerminals);
}) })
// 30s后重新拉取 // 30s后重新拉取
@ -64,13 +65,22 @@ export default {
this.commit("refreshOnlineStatus"); this.commit("refreshOnlineStatus");
},30000) },30000)
}, },
setOnlineStatus(state,onlineIds){ setOnlineStatus(state,onlineTerminals){
console.log("setOnlineStatus")
state.friends.forEach((f)=>{ state.friends.forEach((f)=>{
let onlineFriend = onlineIds.find((id)=> f.id==id); let userTerminal = onlineTerminals.find((o)=> f.id==o.userId);
f.online = onlineFriend != undefined; if(userTerminal){
console.log(userTerminal)
f.online = true;
f.onlineTerminals = userTerminal.terminals;
f.onlineWeb = userTerminal.terminals.indexOf(TERMINAL_TYPE.WEB)>=0
f.onlineApp = userTerminal.terminals.indexOf(TERMINAL_TYPE.APP)>=0
}else{
f.online = false;
f.onlineTerminals = [];
f.onlineWeb = false;
f.onlineApp = false;
}
}); });
let activeFriend = state.friends[state.activeIndex]; let activeFriend = state.friends[state.activeIndex];
state.friends.sort((f1,f2)=>{ state.friends.sort((f1,f2)=>{
if(f1.online&&!f2.online){ if(f1.online&&!f2.online){

4
im-ui/src/view/Friend.vue

@ -27,7 +27,9 @@
</div> </div>
<div v-show="userInfo.id"> <div v-show="userInfo.id">
<div class="user-detail"> <div class="user-detail">
<head-image class="detail-head-image" :size="200" :url="userInfo.headImage" <head-image class="detail-head-image" :size="200"
:name="userInfo.nickName"
:url="userInfo.headImage"
@click.native="showFullImage()"></head-image> @click.native="showFullImage()"></head-image>
<div class="info-item"> <div class="info-item">
<el-descriptions title="好友信息" class="description" :column="1"> <el-descriptions title="好友信息" class="description" :column="1">

11
im-ui/src/view/Group.vue

@ -26,12 +26,16 @@
<div v-show="activeGroup.id"> <div v-show="activeGroup.id">
<div class="r-group-info"> <div class="r-group-info">
<div> <div>
<file-upload class="avatar-uploader" :action="imageAction" :disabled="!isOwner" <file-upload v-show="isOwner" class="avatar-uploader" :action="imageAction"
:showLoading="true" :maxSize="maxSize" @success="handleUploadSuccess" :showLoading="true" :maxSize="maxSize" @success="handleUploadSuccess"
:fileTypes="['image/jpeg', 'image/png', 'image/jpg','image/webp']"> :fileTypes="['image/jpeg', 'image/png', 'image/jpg','image/webp']">
<img v-if="activeGroup.headImage" :src="activeGroup.headImage" class="avatar"> <img v-if="activeGroup.headImage" :src="activeGroup.headImage" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i> <i v-else class="el-icon-plus avatar-uploader-icon"></i>
</file-upload> </file-upload>
<head-image v-show="!isOwner" class="avatar" :size="200"
:url="activeGroup.headImage"
:name="activeGroup.remark">
</head-image>
<el-button class="send-btn" @click="handleSendMessage()">发送消息</el-button> <el-button class="send-btn" @click="handleSendMessage()">发送消息</el-button>
</div> </div>
<el-form class="r-group-form" label-width="130px" :model="activeGroup" :rules="rules" <el-form class="r-group-form" label-width="130px" :model="activeGroup" :rules="rules"
@ -91,14 +95,15 @@
import FileUpload from '../components/common/FileUpload'; import FileUpload from '../components/common/FileUpload';
import GroupMember from '../components/group/GroupMember.vue'; import GroupMember from '../components/group/GroupMember.vue';
import AddGroupMember from '../components/group/AddGroupMember.vue'; import AddGroupMember from '../components/group/AddGroupMember.vue';
import HeadImage from '../components/common/HeadImage.vue';
export default { export default {
name: "group", name: "group",
components: { components: {
GroupItem, GroupItem,
GroupMember, GroupMember,
FileUpload, FileUpload,
AddGroupMember AddGroupMember,
HeadImage
}, },
data() { data() {
return { return {

10
im-ui/src/view/Home.vue

@ -2,7 +2,9 @@
<el-container> <el-container>
<el-aside width="80px" class="navi-bar"> <el-aside width="80px" class="navi-bar">
<div class="user-head-image"> <div class="user-head-image">
<head-image :url="$store.state.userStore.userInfo.headImageThumb" :size="60" @click.native="showSettingDialog=true"> <head-image :name="$store.state.userStore.userInfo.nickName"
:url="$store.state.userStore.userInfo.headImageThumb"
:size="60" @click.native="showSettingDialog=true">
</head-image> </head-image>
</div> </div>
@ -269,12 +271,6 @@
padding: 10px; padding: 10px;
padding-top: 50px; padding-top: 50px;
.user-head-image {
position: relative;
width: 50px;
height: 50px;
}
.el-menu { .el-menu {
border: none; border: none;
flex: 1; flex: 1;

2
im-ui/src/view/Login.vue

@ -44,7 +44,7 @@
}; };
return { return {
loginForm: { loginForm: {
terminal: 0, terminal: this.$enums.TERMINAL_TYPE.WEB,
userName: '', userName: '',
password: '' password: ''
}, },

8
im-uniapp/common/enums.js

@ -22,7 +22,13 @@ const USER_STATE = {
BUSY: 2 BUSY: 2
} }
const TERMINAL_TYPE = {
WEB: 0,
APP: 1
}
export { export {
MESSAGE_TYPE, MESSAGE_TYPE,
USER_STATE USER_STATE,
TERMINAL_TYPE
} }

11
im-uniapp/components/chat-item/chat-item.vue

@ -1,8 +1,8 @@
<template> <template>
<view class="chat-item" @click="showChatBox()"> <view class="chat-item" @click="showChatBox()">
<view class="left"> <view class="left">
<image class="head-image" :src="chat.headImage" mode="aspectFill" lazy-load="true"></image> <head-image :url="chat.headImage" :name="chat.showName" :size="100"></head-image>
<view v-show="chat.unreadCount>0" class="unread-text">{{chat.unreadCount}}</view> <view v-if="chat.unreadCount>0" class="unread-text">{{chat.unreadCount}}</view>
</view> </view>
<view class="chat-right"> <view class="chat-right">
<view class="chat-name"> <view class="chat-name">
@ -64,13 +64,6 @@
width: 100rpx; width: 100rpx;
height: 100rpx; height: 100rpx;
.head-image {
width: 100%;
height: 100%;
border-radius: 10%;
border: #eeeeee solid 1px;
}
.unread-text { .unread-text {
position: absolute; position: absolute;
background-color: red; background-color: red;

65
im-uniapp/components/chat-message-item/chat-message-item.vue

@ -1,17 +1,19 @@
<template> <template>
<view class="chat-msg-item"> <view class="chat-msg-item">
<view class="chat-msg-tip" v-show="msgInfo.type==$enums.MESSAGE_TYPE.RECALL">{{msgInfo.content}}</view> <view class="chat-msg-tip" v-if="msgInfo.type==$enums.MESSAGE_TYPE.RECALL">{{msgInfo.content}}</view>
<view class="chat-msg-tip" v-show="msgInfo.type==$enums.MESSAGE_TYPE.TIP_TIME"> <view class="chat-msg-tip" v-if="msgInfo.type==$enums.MESSAGE_TYPE.TIP_TIME">
{{$date.toTimeText(msgInfo.sendTime)}}</view> {{$date.toTimeText(msgInfo.sendTime)}}</view>
<view class="chat-msg-normal" v-show="msgInfo.type>=0 && msgInfo.type<10" <view class="chat-msg-normal" v-if="msgInfo.type>=0 && msgInfo.type<10"
:class="{'chat-msg-mine':msgInfo.selfSend}"> :class="{'chat-msg-mine':msgInfo.selfSend}">
<view class="avatar" @click="onShowUserInfo(msgInfo.sendId)">
<image class="head-image" :src="headImage" lazy-load="true"></image> <head-image class="avatar" :id="msgInfo.sendId" :url="headImage"
</view> :name="showName" :size="80"></head-image>
<view class="chat-msg-content" @longpress="onShowMenu($event)"> <view class="chat-msg-content" @longpress="onShowMenu($event)">
<view v-show="msgInfo.groupId && !msgInfo.selfSend" class="chat-msg-top"> <view v-if="msgInfo.groupId && !msgInfo.selfSend" class="chat-msg-top">
<text>{{showName}}</text> <text>{{showName}}</text>
</view> </view>
@ -20,12 +22,12 @@
:nodes="$emo.transform(msgInfo.content)"></rich-text> :nodes="$emo.transform(msgInfo.content)"></rich-text>
<view class="chat-msg-image" v-if="msgInfo.type==$enums.MESSAGE_TYPE.IMAGE"> <view class="chat-msg-image" v-if="msgInfo.type==$enums.MESSAGE_TYPE.IMAGE">
<view class="img-load-box"> <view class="img-load-box">
<image class="send-image" :src="JSON.parse(msgInfo.content).thumbUrl" lazy-load="true" <image class="send-image" mode="widthFix" :src="JSON.parse(msgInfo.content).thumbUrl" lazy-load="true"
@click.stop="onShowFullImage()"> @click.stop="onShowFullImage()">
</image> </image>
<loading v-show="loading"></loading> <loading v-if="loading"></loading>
</view> </view>
<text title="发送失败" v-show="loadFail" @click="onSendFail" <text title="发送失败" v-if="loadFail" @click="onSendFail"
class="send-fail iconfont icon-warning-circle-fill"></text> class="send-fail iconfont icon-warning-circle-fill"></text>
</view> </view>
@ -37,9 +39,9 @@
<view class="chat-file-size">{{fileSize}}</view> <view class="chat-file-size">{{fileSize}}</view>
</view> </view>
<view class="chat-file-icon iconfont icon-file"></view> <view class="chat-file-icon iconfont icon-file"></view>
<loading v-show="loading"></loading> <loading v-if="loading"></loading>
</view> </view>
<text title="发送失败" v-show="loadFail" @click="onSendFail" <text title="发送失败" v-if="loadFail" @click="onSendFail"
class="send-fail iconfont icon-warning-circle-fill"></text> class="send-fail iconfont icon-warning-circle-fill"></text>
</view> </view>
<!-- <!--
@ -52,7 +54,7 @@
</view> </view>
</view> </view>
<pop-menu v-show="menu.show" :menu-style="menu.style" :items="menuItems" @close="menu.show=false" <pop-menu v-if="menu.show" :menu-style="menu.style" :items="menuItems" @close="menu.show=false"
@select="onSelectMenu"></pop-menu> @select="onSelectMenu"></pop-menu>
</view> </view>
</template> </template>
@ -132,13 +134,7 @@
uni.previewImage({ uni.previewImage({
urls: [imageUrl] urls: [imageUrl]
}) })
},
onShowUserInfo(userId){
uni.navigateTo({
url: "/pages/common/user-info?id=" + userId
})
} }
}, },
computed: { computed: {
loading() { loading() {
@ -201,24 +197,13 @@
position: relative; position: relative;
font-size: 0; font-size: 0;
margin-bottom: 15rpx; margin-bottom: 15rpx;
padding-left: 100rpx; padding-left: 110rpx;
min-height: 80rpx; min-height: 80rpx;
.avatar { .avatar {
position: absolute; position: absolute;
display: flex;
justify-content: center;
align-items: center;
width: 70rpx;
height: 70rpx;
top: 0; top: 0;
left: 0; left: 0;
.head-image {
width: 100%;
height: 100%;
border-radius: 10%;
}
} }
@ -243,7 +228,7 @@
line-height: 60rpx; line-height: 60rpx;
margin-top: 10rpx; margin-top: 10rpx;
padding: 10rpx; padding: 10rpx;
background-color: rgb(235,235,245); background-color: #ebebf5;
border-radius: 10rpx; border-radius: 10rpx;
color: #333; color: #333;
font-size: 30rpx; font-size: 30rpx;
@ -260,7 +245,7 @@
width: 0; width: 0;
height: 0; height: 0;
border-style: solid dashed dashed; border-style: solid dashed dashed;
border-color: rgb(235,235,245) transparent transparent; border-color: #ebebf5 transparent transparent;
overflow: hidden; overflow: hidden;
border-width: 20rpx; border-width: 20rpx;
} }
@ -279,10 +264,8 @@
.send-image { .send-image {
min-width: 200rpx; min-width: 200rpx;
min-height: 150rpx; min-height: 150rpx;
max-width: 400rpx; max-width: 500rpx;
max-height: 300rpx; border: 8rpx solid #ebebf5;
border: #dddddd solid 1px;
box-shadow: 2px 2px 2px #c0c0c0;
cursor: pointer; cursor: pointer;
} }
} }
@ -360,7 +343,7 @@
&.chat-msg-mine { &.chat-msg-mine {
text-align: right; text-align: right;
padding-left: 0; padding-left: 0;
padding-right: 100rpx; padding-right: 110rpx;
.avatar { .avatar {
left: auto; left: auto;
@ -375,13 +358,13 @@
padding-right: 0; padding-right: 0;
.chat-msg-text { .chat-msg-text {
margin-left: 10px; margin-left: 10px;
background-color: rgb(88, 127, 240); background-color: #587ff0;
color: #fff; color: #fff;
box-shadow: 2px 2px 1px #ccc; box-shadow: 1px 1px 1px #ccc;
&:after { &:after {
left: auto; left: auto;
right: -10px; right: -10px;
border-top-color: rgb(88, 127, 240); border-top-color: #587ff0;
} }
} }

45
im-uniapp/components/friend-item/friend-item.vue

@ -1,13 +1,12 @@
<template> <template>
<view class="friend-item" @click="showFriendInfo()"> <view class="friend-item" @click="showFriendInfo()">
<view class="friend-avatar"> <head-image :name="friend.nickName" :online="friend.online" :url="friend.headImage"
<image class="head-image" :src="friend.headImage" lazy-load="true" mode="aspectFill"></image> :size="100"></head-image>
</view>
<view class="friend-info"> <view class="friend-info">
<view class="friend-name">{{ friend.nickName}}</view> <view class="friend-name">{{ friend.nickName}}</view>
<view class="friend-online" <view class="friend-online">
:class="friend.online ? 'online':''"> <text v-show="friend.onlineWeb" title="网页端在线">网页端</text>
{{ friend.online?"[在线]":"[离线]"}} <text v-show="friend.onlineApp" title="移动端在线">移动端</text>
</view> </view>
</view> </view>
</view> </view>
@ -19,10 +18,10 @@
data() { data() {
return {} return {}
}, },
methods:{ methods: {
showFriendInfo(){ showFriendInfo() {
uni.navigateTo({ uni.navigateTo({
url: "/pages/common/user-info?id="+this.friend.id url: "/pages/common/user-info?id=" + this.friend.id
}) })
}, },
}, },
@ -45,32 +44,18 @@
padding-right: 10rpx; padding-right: 10rpx;
background-color: white; background-color: white;
white-space: nowrap; white-space: nowrap;
&:hover { &:hover {
background-color: #eeeeee; background-color: #eeeeee;
} }
.friend-avatar {
display: flex;
justify-content: center;
align-items: center;
width: 100rpx;
height: 100rpx;
.head-image{
width: 100%;
height: 100%;
border-radius: 10%;
border: #eeeeee solid 1px;
}
}
.friend-info { .friend-info {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-left: 20rpx; padding-left: 20rpx;
text-align: left; text-align: left;
.friend-name { .friend-name {
font-size: 30rpx; font-size: 30rpx;
font-weight: 600; font-weight: 600;
@ -78,14 +63,6 @@
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
} }
.friend-online {
font-size: 28rpx;
&.online {
color: #5fb878;
}
}
} }
} }
</style> </style>

20
im-uniapp/components/group-item/group-item.vue

@ -1,8 +1,7 @@
<template> <template>
<view class="group-item" @click="showGroupInfo()"> <view class="group-item" @click="showGroupInfo()">
<view class="group-avatar"> <head-image :name="group.remark"
<image class="head-image" :src="group.headImage" lazy-load="true" mode="aspectFill"></image> :url="group.headImage" :size="100"></head-image>
</view>
<view class="group-name"> <view class="group-name">
<view>{{ group.remark}}</view> <view>{{ group.remark}}</view>
</view> </view>
@ -45,21 +44,6 @@
background-color: #eeeeee; background-color: #eeeeee;
} }
.group-avatar {
display: flex;
justify-content: center;
align-items: center;
width: 100rpx;
height: 100rpx;
.head-image{
width: 100%;
height: 100%;
border-radius: 10%;
border: #eeeeee solid 1px;
}
}
.group-name { .group-name {
font-size: 32rpx; font-size: 32rpx;
padding-left: 20rpx; padding-left: 20rpx;

104
im-uniapp/components/head-image/head-image.vue

@ -0,0 +1,104 @@
<template>
<view class="head-image" @click="showUserInfo($event)">
<image class="avatar-image" v-if="url" :src="url"
:style="avatarImageStyle" lazy-load="true" mode="aspectFill"/>
<view class="avatar-text" v-if="!url" :style="avatarTextStyle">
{{name.substring(0,1).toUpperCase()}}
</view>
<view v-if="online" class="online" title="用户当前在线">
</view>
</view>
</template>
<script>
export default {
name: "head-image",
data() {
return {
colors: ["#7dd24b", "#c7515a", "#db68ef", "#15d29b", "#85029b",
"#c9b455", "#fb2609", "#bda818", "#af0831", "#326eb6"
]
}
},
props: {
id: {
type: Number
},
size: {
type: Number,
default: 50
},
url: {
type: String
},
name: {
type: String,
default: "?"
},
online: {
type: Boolean,
default: false
}
},
methods: {
showUserInfo(e) {
if (this.id && this.id > 0) {
uni.navigateTo({
url: "/pages/common/user-info?id="+this.id
})
}
}
},
computed: {
avatarImageStyle() {
return `width:${this.size}rpx; height:${this.size}rpx;`
},
avatarTextStyle() {
return `width: ${this.size}rpx;height:${this.size}rpx;
color:${this.textColor};font-size:${this.size*0.6}rpx;`
},
textColor() {
let hash = 0;
for (var i = 0; i < this.name.length; i++) {
hash += this.name.charCodeAt(i);
}
return this.colors[hash % this.colors.length];
}
}
}
</script>
<style scoped lang="scss">
.head-image {
position: relative;
cursor: pointer;
.avatar-image {
position: relative;
overflow: hidden;
border-radius: 10%;
}
.avatar-text {
background-color: #f2f2f2;
/* 默认背景色 */
border-radius: 50%;
/* 圆角效果 */
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #ccc;
}
.online {
position: absolute;
right: -10%;
bottom: 0;
width: 24rpx;
height: 24rpx;
background: limegreen;
border-radius: 50%;
border: 6rpx solid white;
}
}
</style>

28
im-uniapp/pages/common/user-info.vue

@ -1,9 +1,9 @@
<template> <template>
<view class="page user-info"> <view class="page user-info">
<view class="content"> <view class="content">
<view class="avatar" @click="onShowFullImage()"> <head-image :name="userInfo.nickName" :url="userInfo.headImage"
<image class="head-image" :src="userInfo.headImage" lazy-load="true" mode="aspectFill"></image> :size="160" @click="onShowFullImage()"></head-image>
</view>
<view class="info-item"> <view class="info-item">
<view class="info-primary"> <view class="info-primary">
<text class="info-username"> <text class="info-username">
@ -41,9 +41,11 @@
methods: { methods: {
onShowFullImage(){ onShowFullImage(){
let imageUrl = this.userInfo.headImage; let imageUrl = this.userInfo.headImage;
uni.previewImage({ if(imageUrl){
urls: [imageUrl] uni.previewImage({
}) urls: [imageUrl]
})
}
}, },
onSendMessage() { onSendMessage() {
let chat = { let chat = {
@ -152,20 +154,6 @@
justify-content: space-between; justify-content: space-between;
padding: 20rpx; padding: 20rpx;
.avatar {
display: flex;
justify-content: center;
align-items: center;
width: 160rpx;
height: 160rpx;
.head-image {
width: 100%;
height: 100%;
border-radius: 10%;
}
}
.info-item { .info-item {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;

32
im-uniapp/pages/friend/friend-add.vue

@ -7,14 +7,17 @@
<view class="user-items"> <view class="user-items">
<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true"> <scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true">
<view v-for="(user) in users" :key="user.id" v-show="user.id != $store.state.userStore.userInfo.id"> <view v-for="(user) in users" :key="user.id" v-show="user.id != $store.state.userStore.userInfo.id">
<uni-list-chat :avatar="user.headImage" :title="user.userName" :note="'昵称:'+user.nickName" <view class="user-item" @click="onSwitchChecked(friend)">
:clickable="true" @click="onShowUserInfo(user)"> <head-image :id="user.id" :name="user.nickName"
<view class="chat-custom-right"> :online="user.online" :url="user.headImage"
:size="100"></head-image>
<view class="user-name">{{ user.nickName}}</view>
<view class="user-btns">
<button type="primary" v-show="!isFriend(user.id)" size="mini" <button type="primary" v-show="!isFriend(user.id)" size="mini"
@click.stop="onAddFriend(user)">加为好友</button> @click.stop="onAddFriend(user)">加为好友</button>
<button type="default" v-show="isFriend(user.id)" size="mini" disabled>已添加</button> <button type="default" v-show="isFriend(user.id)" size="mini" disabled>已添加</button>
</view> </view>
</uni-list-chat> </view>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
@ -87,6 +90,27 @@
position: relative; position: relative;
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
.user-item {
height: 120rpx;
display: flex;
margin-bottom: 1rpx;
position: relative;
padding: 0 30rpx ;
align-items: center;
background-color: white;
white-space: nowrap;
.user-name {
flex:1;
padding-left: 20rpx;
font-size: 30rpx;
font-weight: 600;
line-height: 60rpx;
white-space: nowrap;
overflow: hidden;
}
}
.scroll-bar { .scroll-bar {
height: 100%; height: 100%;
} }

4
im-uniapp/pages/group/group-edit.vue

@ -3,9 +3,11 @@
<uni-forms ref="form" :modelValue="group" :rules="rules" validate-trigger="bind" label-position="top" <uni-forms ref="form" :modelValue="group" :rules="rules" validate-trigger="bind" label-position="top"
label-width="100%"> label-width="100%">
<uni-forms-item label="群聊头像:" name="headImage"> <uni-forms-item label="群聊头像:" name="headImage">
<image-upload :disabled="!isOwner" :onSuccess="onUnloadImageSuccess"> <image-upload v-show="isOwner" :onSuccess="onUnloadImageSuccess">
<image :src="group.headImage" class="head-image"></image> <image :src="group.headImage" class="head-image"></image>
</image-upload> </image-upload>
<head-image v-show="!isOwner" :name="group.remark"
:url="group.headImage" :size="200"></head-image>
</uni-forms-item> </uni-forms-item>
<uni-forms-item label="群聊名称:" name="name" :required="true"> <uni-forms-item label="群聊名称:" name="name" :required="true">
<uni-easyinput type="text" v-model="group.name" :disabled="!isOwner" placeholder="请输入群聊名称" /> <uni-easyinput type="text" v-model="group.name" :disabled="!isOwner" placeholder="请输入群聊名称" />

30
im-uniapp/pages/group/group-info.vue

@ -3,10 +3,9 @@
<view class="group-members"> <view class="group-members">
<view class="member-items"> <view class="member-items">
<view v-for="(member,idx) in groupMembers" :key="idx"> <view v-for="(member,idx) in groupMembers" :key="idx">
<view class="member-item" v-if="idx<9" @click="onShowUserInfo(member.userId)"> <view class="member-item" v-if="idx<9">
<view class="member-avatar"> <head-image :id="member.userId" :name="member.aliasName" :url="member.headImage"
<image class="head-image" :src="member.headImage" mode="aspectFill"> </image> :size="100" :online="member.online" ></head-image>
</view>
<view class="member-name"> <view class="member-name">
<text>{{member.aliasName}}</text> <text>{{member.aliasName}}</text>
</view> </view>
@ -69,11 +68,6 @@
url: `/pages/group/group-invite?id=${this.groupId}` url: `/pages/group/group-invite?id=${this.groupId}`
}) })
}, },
onShowUserInfo(userId) {
uni.navigateTo({
url: "/pages/common/user-info?id=" + userId
})
},
onShowMoreMmeber() { onShowMoreMmeber() {
uni.navigateTo({ uni.navigateTo({
url: `/pages/group/group-member?id=${this.groupId}` url: `/pages/group/group-member?id=${this.groupId}`
@ -215,27 +209,11 @@
flex-direction: column; flex-direction: column;
margin: 8rpx 2rpx; margin: 8rpx 2rpx;
position: relative; position: relative;
align-items: center; align-items: center;
padding-right: 5px; padding-right: 5px;
background-color: #fafafa; background-color: #fafafa;
white-space: nowrap; white-space: nowrap;
.member-avatar {
display: flex;
justify-content: center;
align-items: center;
width: 100rpx;
height: 100rpx;
.head-image {
width: 100%;
height: 100%;
border-radius: 10%;
border: #d8d8d8 solid 1px;
}
}
.member-name { .member-name {
width: 100%; width: 100%;
flex: 1; flex: 1;

37
im-uniapp/pages/group/group-invite.vue

@ -7,12 +7,19 @@
<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true"> <scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true">
<view v-for="friend in friendItems" v-show="!searchText || friend.nickName.startsWith(searchText)" <view v-for="friend in friendItems" v-show="!searchText || friend.nickName.startsWith(searchText)"
:key="friend.id"> :key="friend.id">
<uni-list-chat :avatar="friend.headImage" :title="friend.nickName" :clickable="true" <view class="friend-item" @click="onSwitchChecked(friend)">
@click="onSwitchChecked(friend)"> <head-image :name="friend.nickName"
<view class="chat-custom-right"> :online="friend.online" :url="friend.headImage"
:size="100"></head-image>
<view class="friend-name">{{ friend.nickName}}</view>
<view class="friend-checked">
<radio :checked="friend.checked" :disabled="friend.disabled" @click.stop="onSwitchChecked(friend)"/> <radio :checked="friend.checked" :disabled="friend.disabled" @click.stop="onSwitchChecked(friend)"/>
</view> </view>
</uni-list-chat> </view>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
@ -83,6 +90,7 @@
id: f.id, id: f.id,
headImage: f.headImage, headImage: f.headImage,
nickName: f.nickName, nickName: f.nickName,
online: f.online
} }
item.disabled = this.isGroupMember(f.id); item.disabled = this.isGroupMember(f.id);
item.checked = item.disabled; item.checked = item.disabled;
@ -127,6 +135,27 @@
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
.friend-item {
height: 120rpx;
display: flex;
margin-bottom: 1rpx;
position: relative;
padding: 0 30rpx ;
align-items: center;
background-color: white;
white-space: nowrap;
.friend-name {
flex:1;
padding-left: 20rpx;
font-size: 30rpx;
font-weight: 600;
line-height: 60rpx;
white-space: nowrap;
overflow: hidden;
}
}
.scroll-bar { .scroll-bar {
height: 100%; height: 100%;
} }

45
im-uniapp/pages/group/group-member.vue

@ -7,13 +7,18 @@
<scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true"> <scroll-view class="scroll-bar" scroll-with-animation="true" scroll-y="true">
<view v-for="(member,idx) in groupMembers" <view v-for="(member,idx) in groupMembers"
v-show="!searchText || member.aliasName.startsWith(searchText)" :key="idx"> v-show="!searchText || member.aliasName.startsWith(searchText)" :key="idx">
<uni-list-chat :avatar="member.headImage" :title="member.aliasName" :clickable="true" <view class="member-item" @click="onShowUserInfo(member.userId)">
@click="onShowUserInfo(member.userId)"> <head-image :name="member.aliasName"
<view class="chat-custom-right"> :online="member.online" :url="member.headImage"
:size="100"></head-image>
<view class="member-name">{{ member.aliasName}}</view>
<view class="member-kick">
<button type="warn" v-show="isOwner && !isSelf(member.userId)" size="mini" <button type="warn" v-show="isOwner && !isSelf(member.userId)" size="mini"
@click.stop="onKickOut(member,idx)">移出群聊</button> @click.stop="onKickOut(member,idx)">移出群聊</button>
</view> </view>
</uni-list-chat> </view>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
@ -41,7 +46,7 @@
title: '确认移出?', title: '确认移出?',
content: `确定将成员'${member.aliasName}'移出群聊吗?`, content: `确定将成员'${member.aliasName}'移出群聊吗?`,
success: (res) => { success: (res) => {
if(res.cancel) if (res.cancel)
return; return;
this.$http({ this.$http({
url: `/group/kick/${this.group.id}?userId=${member.userId}`, url: `/group/kick/${this.group.id}?userId=${member.userId}`,
@ -87,7 +92,7 @@
this.loadGroupMembers(options.id); this.loadGroupMembers(options.id);
}, },
onUnload() { onUnload() {
if(this.isModify){ if (this.isModify) {
// //
let pages = getCurrentPages(); let pages = getCurrentPages();
let prevPage = pages[pages.length - 2]; let prevPage = pages[pages.length - 2];
@ -98,16 +103,38 @@
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.group-invite{ .group-member {
position: relative; position: relative;
border: #dddddd solid 1px; border: #dddddd solid 1px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.member-items{ .member-items {
position: relative; position: relative;
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
.member-item {
height: 120rpx;
display: flex;
margin-bottom: 1rpx;
position: relative;
padding: 0 30rpx ;
align-items: center;
background-color: white;
white-space: nowrap;
.member-name {
flex:1;
padding-left: 20rpx;
font-size: 30rpx;
font-weight: 600;
line-height: 60rpx;
white-space: nowrap;
overflow: hidden;
}
}
.scroll-bar { .scroll-bar {
height: 100%; height: 100%;
} }

36
im-uniapp/store/friendStore.js

@ -1,4 +1,5 @@
import http from '../common/request'; import http from '../common/request'
import {TERMINAL_TYPE} from '../common/enums.js'
export default { export default {
@ -21,8 +22,8 @@ export default {
}) })
}, },
removeFriend(state, id) { removeFriend(state, id) {
state.friends.forEach((f,idx) => { state.friends.forEach((f, idx) => {
if(f.id == id){ if (f.id == id) {
state.friends.splice(idx, 1) state.friends.splice(idx, 1)
} }
}); });
@ -30,11 +31,22 @@ export default {
addFriend(state, friend) { addFriend(state, friend) {
state.friends.push(friend); state.friends.push(friend);
}, },
setOnlineStatus(state, onlineIds) { setOnlineStatus(state, onlineTerminals) {
state.friends.forEach((f) => { state.friends.forEach((f) => {
let onlineFriend = onlineIds.find((id) => f.id == id); let userTerminal = onlineTerminals.find((o) => f.id == o.userId);
f.online = onlineFriend != undefined; if (userTerminal) {
console.log(userTerminal)
f.online = true;
f.onlineTerminals = userTerminal.terminals;
f.onlineWeb = userTerminal.terminals.indexOf(TERMINAL_TYPE.WEB) >= 0
f.onlineApp = userTerminal.terminals.indexOf(TERMINAL_TYPE.APP) >= 0
} else {
f.online = false;
f.onlineTerminals = [];
f.onlineWeb = false;
f.onlineApp = false;
}
}); });
state.friends.sort((f1, f2) => { state.friends.sort((f1, f2) => {
@ -48,16 +60,16 @@ export default {
}); });
}, },
refreshOnlineStatus(state) { refreshOnlineStatus(state) {
if (state.friends.length > 0 ) { if (state.friends.length > 0) {
let userIds = []; let userIds = [];
state.friends.forEach((f) => { state.friends.forEach((f) => {
userIds.push(f.id) userIds.push(f.id)
}); });
http({ http({
url: '/user/online?userIds='+ userIds.join(','), url: '/user/terminal/online?userIds=' + userIds.join(','),
method: 'GET' method: 'GET'
}).then((onlineIds) => { }).then((onlineTerminals) => {
this.commit("setOnlineStatus", onlineIds); this.commit("setOnlineStatus", onlineTerminals);
}) })
} }
// 30s后重新拉取 // 30s后重新拉取
@ -66,7 +78,7 @@ export default {
this.commit("refreshOnlineStatus"); this.commit("refreshOnlineStatus");
}, 30000) }, 30000)
}, },
clear(state){ clear(state) {
clearTimeout(state.timer); clearTimeout(state.timer);
state.friends = []; state.friends = [];
state.timer = null; state.timer = null;

Loading…
Cancel
Save