Browse Source

!125 发布3.2版本

Merge pull request !125 from blue/v_3.0.0
master
blue 1 year ago
committed by Gitee
parent
commit
010d9995d7
No known key found for this signature in database GPG Key ID: 173E9B9CA92EEF8F
  1. 21
      README.md
  2. 4
      im-common/src/main/java/com/bx/imcommon/enums/IMCmdType.java
  3. 31
      im-platform/src/main/java/com/bx/implatform/annotation/RepeatSubmit.java
  4. 100
      im-platform/src/main/java/com/bx/implatform/aspect/RepeatSubmitAspect.java
  5. 5
      im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java
  6. 2
      im-platform/src/main/java/com/bx/implatform/controller/FriendController.java
  7. 2
      im-platform/src/main/java/com/bx/implatform/controller/GroupController.java
  8. 5
      im-platform/src/main/java/com/bx/implatform/controller/LoginController.java
  9. 2
      im-platform/src/main/java/com/bx/implatform/interceptor/AuthInterceptor.java
  10. 2
      im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java
  11. 6
      im-platform/src/main/java/com/bx/implatform/vo/UserVO.java
  12. 2
      im-server/src/main/java/com/bx/imserver/netty/processor/LoginProcessor.java
  13. 20
      im-uniapp/App.vue
  14. 19
      im-uniapp/common/emotion.js
  15. 1
      im-uniapp/common/recorder-app.js
  16. 41
      im-uniapp/common/recorder-h5.js
  17. 2
      im-uniapp/components/chat-item/chat-item.vue
  18. 6
      im-uniapp/components/chat-message-item/chat-message-item.vue
  19. 6
      im-uniapp/components/chat-record/chat-record.vue
  20. 6
      im-uniapp/components/file-upload/file-upload.vue
  21. 51
      im-uniapp/im.scss
  22. 16
      im-uniapp/manifest.json
  23. 1
      im-uniapp/package.json
  24. 20
      im-uniapp/pages.json
  25. 240
      im-uniapp/pages/chat/chat-box.vue
  26. 39
      im-uniapp/pages/friend/friend-add.vue
  27. 100
      im-uniapp/pages/group/group-edit.vue
  28. 93
      im-uniapp/pages/group/group-info.vue
  29. 6
      im-uniapp/pages/login/login.vue
  30. 115
      im-uniapp/pages/mine/mine-edit.vue
  31. 7
      im-uniapp/pages/mine/mine.vue
  32. 10
      im-uniapp/pages/register/register.vue
  33. 2
      im-web/src/view/Home.vue
  34. 6
      im-web/src/view/Login.vue
  35. BIN
      截图/app/1.jpg
  36. BIN
      截图/app/1.png
  37. BIN
      截图/app/2.jpg
  38. BIN
      截图/app/2.png
  39. BIN
      截图/web/单人通话.jpg
  40. BIN
      截图/web/多人通话.jpg
  41. BIN
      截图/web/好友.jpg
  42. BIN
      截图/web/好友列表.png
  43. BIN
      截图/web/私聊.jpg
  44. BIN
      截图/web/私聊.png
  45. BIN
      截图/web/群列表.jpg
  46. BIN
      截图/web/群列表.png
  47. BIN
      截图/web/群聊.jpg
  48. BIN
      截图/web/群聊.png
  49. BIN
      截图/web/群视频.png

21
README.md

@ -10,7 +10,7 @@
1. 支持单人、多人音视频通话(基于原生webrtc实现,需要ssl证书) 1. 支持单人、多人音视频通话(基于原生webrtc实现,需要ssl证书)
1. uniapp端兼容app、h5、微信小程序,可与web端同时在线,并保持消息同步 1. uniapp端兼容app、h5、微信小程序,可与web端同时在线,并保持消息同步
1. 后端采用springboot+netty实现,网页端使用vue,移动端使用uniapp 1. 后端采用springboot+netty实现,网页端使用vue,移动端使用uniapp
1. 服务器支持集群化部署,每个im-server仅处理自身连接用户的消息 1. 服务器支持集群化部署,具有良好的横向扩展能力
详细文档:https://www.yuque.com/u1475064/mufu2a 详细文档:https://www.yuque.com/u1475064/mufu2a
@ -21,7 +21,7 @@
- 后台管理端上线,后台管理代码仓库地址:https://gitee.com/bluexsx/box-im-admin - 后台管理端上线,后台管理代码仓库地址:https://gitee.com/bluexsx/box-im-admin
- 框架和组件版本全面升级: jdk17、springboot3.3、node18等 - 框架和组件版本全面升级: jdk17、springboot3.3、node18等
- 部分界面,功能、性能优化 - 部分界面,功能、性能优化1
#### 在线体验 #### 在线体验
@ -60,9 +60,9 @@ https://www.yuque.com/u1475064/imk5n2/qtezcg32q1d0dr29#SbvXq
| im-uniapp | uniapp页面,可打包成app、h5、微信小程序 | | im-uniapp | uniapp页面,可打包成app、h5、微信小程序 |
#### 消息推送方案 #### 消息推送方案
当消息的发送者和接收者连的不是同一个server时,消息是无法直接推送的,所以我们设计出了能够支持跨节点推送的方案:
![输入图片说明](%E6%88%AA%E5%9B%BE/%E6%B6%88%E6%81%AF%E6%8E%A8%E9%80%81%E9%9B%86%E7%BE%A4%E5%8C%96.jpg) ![输入图片说明](%E6%88%AA%E5%9B%BE/%E6%B6%88%E6%81%AF%E6%8E%A8%E9%80%81%E9%9B%86%E7%BE%A4%E5%8C%96.jpg)
- 当消息的发送者和接收者连的不是同一个server时,消息是无法直接推送的,所以我们需要设计出能够支持跨节点推送的方案
- 利用了redis的list数据实现消息推送,其中key为im:unread:${serverid},每个key的数据可以看做一个queue,每个im-server根据自身的id只消费属于自己的queue - 利用了redis的list数据实现消息推送,其中key为im:unread:${serverid},每个key的数据可以看做一个queue,每个im-server根据自身的id只消费属于自己的queue
- redis记录了每个用户的websocket连接的是哪个im-server,当用户发送消息时,im-platform将根据所连接的im-server的id,决定将消息推向哪个queue - redis记录了每个用户的websocket连接的是哪个im-server,当用户发送消息时,im-platform将根据所连接的im-server的id,决定将消息推向哪个queue
@ -102,24 +102,25 @@ https://www.yuque.com/u1475064/mufu2a/vn5u10ephxh9sau8
#### 界面截图 #### 界面截图
私聊: 私聊:
![输入图片说明](%E6%88%AA%E5%9B%BE/web/%E7%A7%81%E8%81%8A.png) ![输入图片说明](%E6%88%AA%E5%9B%BE/web/%E7%A7%81%E8%81%8A.jpg)
群聊: 群聊:
![输入图片说明](%E6%88%AA%E5%9B%BE/web/%E7%BE%A4%E8%81%8A.png) ![输入图片说明](%E6%88%AA%E5%9B%BE/web/%E7%BE%A4%E8%81%8A.jpg)
群通话: 群通话:
![输入图片说明](%E6%88%AA%E5%9B%BE/web/%E7%BE%A4%E8%A7%86%E9%A2%91.png) ![输入图片说明](%E6%88%AA%E5%9B%BE/web/%E5%A4%9A%E4%BA%BA%E9%80%9A%E8%AF%9D.jpg)
好友列表: 好友列表:
![输入图片说明](%E6%88%AA%E5%9B%BE/web/%E5%A5%BD%E5%8F%8B%E5%88%97%E8%A1%A8.png) ![输入图片说明](%E6%88%AA%E5%9B%BE/web/%E5%A5%BD%E5%8F%8B.jpg)
群列表: 群列表:
![输入图片说明](%E6%88%AA%E5%9B%BE/web/%E7%BE%A4%E5%88%97%E8%A1%A8.png) ![输入图片说明](%E6%88%AA%E5%9B%BE/web/%E7%BE%A4%E5%88%97%E8%A1%A8.jpg)
移动端APP: 移动端APP:
![输入图片说明](%E6%88%AA%E5%9B%BE/app/1.jpg) ![输入图片说明](%E6%88%AA%E5%9B%BE/app/1.png)
![输入图片说明](%E6%88%AA%E5%9B%BE/app/2.jpg)
![输入图片说明](%E6%88%AA%E5%9B%BE/app/2.png)
#### 加入交流群 #### 加入交流群
群1: 741174521(已满) 群1: 741174521(已满)

4
im-common/src/main/java/com/bx/imcommon/enums/IMCmdType.java

@ -6,9 +6,9 @@ import lombok.AllArgsConstructor;
public enum IMCmdType { public enum IMCmdType {
/** /**
* *
*/ */
LOGIN(0, "登"), LOGIN(0, "登"),
/** /**
* 心跳 * 心跳
*/ */

31
im-platform/src/main/java/com/bx/implatform/annotation/RepeatSubmit.java

@ -0,0 +1,31 @@
package com.bx.implatform.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 防止表单重复提交注解
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/**
* 间隔时间小于此时间视为重复提交
*/
int interval() default 5000;
/**
* 间隔时间单位
*/
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
/**
* 提示消息
*/
String message() default "请勿提交重复请求";
}

100
im-platform/src/main/java/com/bx/implatform/aspect/RepeatSubmitAspect.java

@ -0,0 +1,100 @@
package com.bx.implatform.aspect;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.alibaba.fastjson.JSON;
import com.bx.implatform.annotation.RepeatSubmit;
import com.bx.implatform.contant.RedisKey;
import com.bx.implatform.exception.GlobalException;
import com.bx.implatform.session.SessionContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import java.util.Collection;
import java.util.Map;
import java.util.StringJoiner;
/**
* @author: blue
* @date: 2024-12-08
* @version: 1.0
*/
@Aspect
@Component
@AllArgsConstructor
public class RepeatSubmitAspect {
private final RedisTemplate<String, Object> redisTemplate;
@Before("@annotation(repeatSubmit)")
public void doBefore(JoinPoint point, RepeatSubmit repeatSubmit) throws Throwable {
// 如果注解不为0 则使用注解数值
long interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());
HttpServletRequest request =
((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
String url = request.getRequestURL().toString();
Long userId = SessionContext.getSession().getUserId();
String reqParams = argsArrayToString(point.getArgs());
String md5 = SecureUtil.md5(StrUtil.join(":", userId, url, reqParams));
// 唯一标识
String key = String.join(":",RedisKey.IM_REPEAT_SUBMIT,md5) ;
if(redisTemplate.hasKey(key)){
throw new GlobalException(repeatSubmit.message());
}
redisTemplate.opsForValue().set(key,1,repeatSubmit.interval(),repeatSubmit.timeUnit());
}
/**
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray) {
StringJoiner params = new StringJoiner(" ");
if (ArrayUtil.isEmpty(paramsArray)) {
return params.toString();
}
for (Object o : paramsArray) {
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
params.add(JSON.toJSONString(o));
}
}
return params.toString();
}
/**
* 判断是否需要过滤的对象
*
* @param o 对象信息
* @return 如果是需要过滤的对象则返回true否则返回false
*/
@SuppressWarnings("rawtypes")
public boolean isFilterObject(final Object o) {
Class<?> clazz = o.getClass();
if (clazz.isArray()) {
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection)o;
for (Object value : collection) {
return value instanceof MultipartFile;
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map)o;
for (Object value : map.values()) {
return value instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse;
}
}

5
im-platform/src/main/java/com/bx/implatform/contant/RedisKey.java

@ -47,9 +47,10 @@ public final class RedisKey {
* 缓存群聊成员id * 缓存群聊成员id
*/ */
public static final String IM_CACHE_GROUP_MEMBER_ID = "im:cache:group_member_ids"; public static final String IM_CACHE_GROUP_MEMBER_ID = "im:cache:group_member_ids";
/** /**
* 分布式锁前缀 * 重复提交
*/ */
public static final String IM_LOCK_RTC_GROUP = "im:lock:rtc:group"; public static final String IM_REPEAT_SUBMIT = "im:repeat:submit";
} }

2
im-platform/src/main/java/com/bx/implatform/controller/FriendController.java

@ -1,5 +1,6 @@
package com.bx.implatform.controller; package com.bx.implatform.controller;
import com.bx.implatform.annotation.RepeatSubmit;
import com.bx.implatform.entity.Friend; import com.bx.implatform.entity.Friend;
import com.bx.implatform.result.Result; import com.bx.implatform.result.Result;
import com.bx.implatform.result.ResultUtils; import com.bx.implatform.result.ResultUtils;
@ -39,6 +40,7 @@ public class FriendController {
} }
@RepeatSubmit
@PostMapping("/add") @PostMapping("/add")
@Operation(summary = "添加好友", description = "双方建立好友关系") @Operation(summary = "添加好友", description = "双方建立好友关系")
public Result addFriend(@NotNull(message = "好友id不可为空") @RequestParam Long friendId) { public Result addFriend(@NotNull(message = "好友id不可为空") @RequestParam Long friendId) {

2
im-platform/src/main/java/com/bx/implatform/controller/GroupController.java

@ -1,5 +1,6 @@
package com.bx.implatform.controller; package com.bx.implatform.controller;
import com.bx.implatform.annotation.RepeatSubmit;
import com.bx.implatform.result.Result; import com.bx.implatform.result.Result;
import com.bx.implatform.result.ResultUtils; import com.bx.implatform.result.ResultUtils;
import com.bx.implatform.service.GroupService; import com.bx.implatform.service.GroupService;
@ -23,6 +24,7 @@ public class GroupController {
private final GroupService groupService; private final GroupService groupService;
@RepeatSubmit
@Operation(summary = "创建群聊", description = "创建群聊") @Operation(summary = "创建群聊", description = "创建群聊")
@PostMapping("/create") @PostMapping("/create")
public Result<GroupVO> createGroup(@Valid @RequestBody GroupVO vo) { public Result<GroupVO> createGroup(@Valid @RequestBody GroupVO vo) {

5
im-platform/src/main/java/com/bx/implatform/controller/LoginController.java

@ -1,5 +1,6 @@
package com.bx.implatform.controller; package com.bx.implatform.controller;
import com.bx.implatform.annotation.RepeatSubmit;
import com.bx.implatform.dto.LoginDTO; import com.bx.implatform.dto.LoginDTO;
import com.bx.implatform.dto.ModifyPwdDTO; import com.bx.implatform.dto.ModifyPwdDTO;
import com.bx.implatform.dto.RegisterDTO; import com.bx.implatform.dto.RegisterDTO;
@ -13,7 +14,7 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@Tag(name = "注册登") @Tag(name = "注册登")
@RestController @RestController
@RequiredArgsConstructor @RequiredArgsConstructor
public class LoginController { public class LoginController {
@ -21,7 +22,7 @@ public class LoginController {
private final UserService userService; private final UserService userService;
@PostMapping("/login") @PostMapping("/login")
@Operation(summary = "用户登", description = "用户登") @Operation(summary = "用户登", description = "用户登")
public Result<LoginVO> login(@Valid @RequestBody LoginDTO dto) { public Result<LoginVO> login(@Valid @RequestBody LoginDTO dto) {
LoginVO vo = userService.login(dto); LoginVO vo = userService.login(dto);
return ResultUtils.success(vo); return ResultUtils.success(vo);

2
im-platform/src/main/java/com/bx/implatform/interceptor/AuthInterceptor.java

@ -32,7 +32,7 @@ public class AuthInterceptor implements HandlerInterceptor {
//从 http 请求头中取出 token //从 http 请求头中取出 token
String token = request.getHeader("accessToken"); String token = request.getHeader("accessToken");
if (StrUtil.isEmpty(token)) { if (StrUtil.isEmpty(token)) {
log.error("未登,url:{}", request.getRequestURI()); log.error("未登,url:{}", request.getRequestURI());
throw new GlobalException(ResultCode.NO_LOGIN); throw new GlobalException(ResultCode.NO_LOGIN);
} }
String strJson = JwtUtil.getInfo(token); String strJson = JwtUtil.getInfo(token);

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

@ -81,7 +81,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
public LoginVO refreshToken(String refreshToken) { public LoginVO refreshToken(String refreshToken) {
//验证 token //验证 token
if (!JwtUtil.checkSign(refreshToken, jwtProperties.getRefreshTokenSecret())) { if (!JwtUtil.checkSign(refreshToken, jwtProperties.getRefreshTokenSecret())) {
throw new GlobalException("您的登陆信息已过期,请重新登陆"); throw new GlobalException("您的登录信息已过期,请重新登录");
} }
String strJson = JwtUtil.getInfo(refreshToken); String strJson = JwtUtil.getInfo(refreshToken);
Long userId = JwtUtil.getUserId(refreshToken); Long userId = JwtUtil.getUserId(refreshToken);

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

@ -15,12 +15,12 @@ public class UserVO {
private Long id; private Long id;
@NotEmpty(message = "用户名不能为空") @NotEmpty(message = "用户名不能为空")
@Length(max = 64, message = "用户名不能大于64字符") @Length(max = 20, message = "用户名不能大于20字符")
@Schema(description = "用户名") @Schema(description = "用户名")
private String userName; private String userName;
@NotEmpty(message = "用户昵称不能为空") @NotEmpty(message = "用户昵称不能为空")
@Length(max = 64, message = "昵称不能大于64字符") @Length(max = 20, message = "昵称不能大于20字符")
@Schema(description = "用户昵称") @Schema(description = "用户昵称")
private String nickName; private String nickName;
@ -30,7 +30,7 @@ public class UserVO {
@Schema(description = "用户类型 1:普通用户 2:审核账户") @Schema(description = "用户类型 1:普通用户 2:审核账户")
private Integer type; private Integer type;
@Length(max = 1024, message = "个性签名不能大于1024个字符") @Length(max = 128, message = "个性签名不能大于128个字符")
@Schema(description = "个性签名") @Schema(description = "个性签名")
private String signature; private String signature;

2
im-server/src/main/java/com/bx/imserver/netty/processor/LoginProcessor.java

@ -49,7 +49,7 @@ public class LoginProcessor extends AbstractMessageProcessor<IMLoginInfo> {
// 不允许多地登录,强制下线 // 不允许多地登录,强制下线
IMSendInfo<Object> sendInfo = new IMSendInfo<>(); IMSendInfo<Object> sendInfo = new IMSendInfo<>();
sendInfo.setCmd(IMCmdType.FORCE_LOGUT.code()); sendInfo.setCmd(IMCmdType.FORCE_LOGUT.code());
sendInfo.setData("您已在其他地方登,将被强制下线"); sendInfo.setData("您已在其他地方登,将被强制下线");
context.channel().writeAndFlush(sendInfo); context.channel().writeAndFlush(sendInfo);
log.info("异地登录,强制下线,userId:{}", userId); log.info("异地登录,强制下线,userId:{}", userId);
} }

20
im-uniapp/App.vue

@ -47,7 +47,7 @@ export default {
if (cmd == 2) { if (cmd == 2) {
// 线 // 线
uni.showModal({ uni.showModal({
content: '您已在其他地方登,将被强制下线', content: '您已在其他地方登,将被强制下线',
showCancel: false, showCancel: false,
}) })
this.exit(); this.exit();
@ -356,26 +356,32 @@ export default {
url: '/user/self', url: '/user/self',
method: 'GET' method: 'GET'
}) })
},
closeSplashscreen(delay) {
// #ifdef APP-PLUS
//
setTimeout(() => {
console.log("plus.navigator.closeSplashscreen()")
plus.navigator.closeSplashscreen()
}, delay)
// #endif
} }
}, },
onLaunch() { onLaunch() {
this.$mountStore(); this.$mountStore();
// 1s
this.closeSplashscreen(1000);
// //
let loginInfo = uni.getStorageSync("loginInfo") let loginInfo = uni.getStorageSync("loginInfo")
this.refreshToken(loginInfo).then(() => { this.refreshToken(loginInfo).then(() => {
// //
this.init(); this.init();
// this.closeSplashscreen(0);
uni.switchTab({
url: "/pages/chat/chat"
})
}).catch(() => { }).catch(() => {
// //
// #ifdef H5
uni.navigateTo({ uni.navigateTo({
url: "/pages/login/login" url: "/pages/login/login"
}) })
// #endif
}) })
} }
} }

19
im-uniapp/common/emotion.js

@ -7,30 +7,22 @@ const emoTextList = ['憨笑', '媚眼', '开心', '坏笑', '可怜', '爱心',
]; ];
let transform = (content, extClass) => {
let transform = (content) => { return content.replace(/\#[\u4E00-\u9FA5]{1,3}\;/gi, (emoText)=>{
return content.replace(/\#[\u4E00-\u9FA5]{1,3}\;/gi, textToImg);
}
// 将匹配结果替换表情图片 // 将匹配结果替换表情图片
let textToImg = (emoText) => {
let word = emoText.replace(/\#|\;/gi, ''); let word = emoText.replace(/\#|\;/gi, '');
let idx = emoTextList.indexOf(word); let idx = emoTextList.indexOf(word);
if (idx == -1) { if (idx == -1) {
return emoText; return emoText;
} }
let path = textToPath(emoText); let path = textToPath(emoText);
// #ifdef MP let img = `<img src="${path}" class="${extClass}"/>`;
// 微信小程序不能有前面的'/'
path = path.slice(1);
// #endif
let img = `<img src="${path}" style="with:30px;height:30px;
margin: 0 -2px;vertical-align:bottom;"/>`;
return img; return img;
});
} }
let textToPath = (emoText) => { let textToPath = (emoText) => {
let word = emoText.replace(/\#|\;/gi, ''); let word = emoText.replace(/\#|\;/gi, '');
let idx = emoTextList.indexOf(word); let idx = emoTextList.indexOf(word);
@ -42,6 +34,5 @@ let textToPath = (emoText) => {
export default { export default {
emoTextList, emoTextList,
transform, transform,
textToImg,
textToPath textToPath
} }

1
im-uniapp/common/recorder-app.js

@ -62,7 +62,6 @@ let upload = () => {
export { export {
start, start,
pause,
close, close,
upload upload
} }

41
im-uniapp/common/recorder-h5.js

@ -1,30 +1,43 @@
import Recorder from 'js-audio-recorder';
import UNI_APP from '@/.env.js'; import UNI_APP from '@/.env.js';
let rc = null; let rc = null;
let duration = 0;
let chunks = [];
let stream = null;
let start = () => { let start = () => {
if (rc != null) { return navigator.mediaDevices.getUserMedia({ audio: true }).then(audioStream => {
close(); const startTime = new Date().getTime();
chunks = [];
stream = audioStream;
rc = new MediaRecorder(stream)
rc.ondataavailable = (e) => {
console.log("ondataavailable")
chunks.push(e.data)
} }
rc = new Recorder(); rc.onstop = () => {
return rc.start(); duration = (new Date().getTime() - startTime) / 1000;
console.log("时长:", duration)
} }
rc.start()
})
let pause = () => {
rc.pause();
} }
let close = () => { let close = () => {
rc.destroy(); stream.getTracks().forEach((track) => {
rc = null; track.stop()
})
rc.stop()
} }
let upload = () => { let upload = () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const wavBlob = rc.getWAVBlob(); setTimeout(() => {
const newbolb = new Blob([wavBlob], { type: 'audio/wav' }) const newbolb = new Blob(chunks, { 'type': 'audio/mpeg' });
const name = new Date().getDate() + '.wav'; const name = new Date().getDate() + '.mp3';
const file = new File([newbolb], name) const file = new File([newbolb], name)
console.log("upload")
uni.uploadFile({ uni.uploadFile({
url: UNI_APP.BASE_URL + '/file/upload', url: UNI_APP.BASE_URL + '/file/upload',
header: { header: {
@ -39,7 +52,7 @@ let upload = () => {
reject(r.message); reject(r.message);
} else { } else {
const data = { const data = {
duration: parseInt(rc.duration), duration: parseInt(duration),
url: r.data url: r.data
} }
resolve(data); resolve(data);
@ -49,12 +62,12 @@ let upload = () => {
reject(e); reject(e);
} }
}) })
}, 100)
}) })
} }
export { export {
start, start,
pause,
close, close,
upload upload
} }

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

@ -16,7 +16,7 @@
<view class="chat-content"> <view class="chat-content">
<view class="chat-at-text">{{ atText }}</view> <view class="chat-at-text">{{ atText }}</view>
<view class="chat-send-name" v-if="isShowSendName">{{ chat.sendNickName + ':&nbsp;' }}</view> <view class="chat-send-name" v-if="isShowSendName">{{ chat.sendNickName + ':&nbsp;' }}</view>
<rich-text class="chat-content-text" :nodes="$emo.transform(chat.lastContent)"></rich-text> <rich-text class="chat-content-text" :nodes="$emo.transform(chat.lastContent,'emoji-small')"></rich-text>
<uni-badge v-if="chat.unreadCount > 0" :max-num="99" :text="chat.unreadCount" /> <uni-badge v-if="chat.unreadCount > 0" :max-num="99" :text="chat.unreadCount" />
</view> </view>
</view> </view>

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

@ -17,13 +17,13 @@
<view class="chat-msg-bottom"> <view class="chat-msg-bottom">
<view v-if="msgInfo.type == $enums.MESSAGE_TYPE.TEXT"> <view v-if="msgInfo.type == $enums.MESSAGE_TYPE.TEXT">
<long-press-menu :items="menuItems" @select="onSelectMenu"> <long-press-menu :items="menuItems" @select="onSelectMenu">
<rich-text class="chat-msg-text" :nodes="$emo.transform(msgInfo.content)"></rich-text> <rich-text class="chat-msg-text" :nodes="$emo.transform(msgInfo.content, 'emoji-normal')"></rich-text>
</long-press-menu> </long-press-menu>
</view> </view>
<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">
<long-press-menu :items="menuItems" @select="onSelectMenu"> <long-press-menu :items="menuItems" @select="onSelectMenu">
<view class="img-load-box"> <view class="img-load-box">
<image class="send-image" mode="widthFix" :src="JSON.parse(msgInfo.content).thumbUrl" <image class="send-image" mode="heightFix" :src="JSON.parse(msgInfo.content).thumbUrl"
lazy-load="true" @click.stop="onShowFullImage()"> lazy-load="true" @click.stop="onShowFullImage()">
</image> </image>
<loading v-if="loading"></loading> <loading v-if="loading"></loading>
@ -256,6 +256,7 @@ export default {
color: $im-text-color-lighter; color: $im-text-color-lighter;
font-size: $im-font-size-smaller; font-size: $im-font-size-smaller;
line-height: $im-font-size-smaller; line-height: $im-font-size-smaller;
height: $im-font-size-smaller;
} }
.chat-msg-bottom { .chat-msg-bottom {
@ -305,6 +306,7 @@ export default {
.send-image { .send-image {
min-width: 200rpx; min-width: 200rpx;
max-width: 420rpx; max-width: 420rpx;
height: 350rpx;
cursor: pointer; cursor: pointer;
border-radius: 4px; border-radius: 4px;
} }

6
im-uniapp/components/chat-record/chat-record.vue

@ -71,13 +71,12 @@ export default {
}, },
onEndRecord() { onEndRecord() {
this.recording = false; this.recording = false;
//
this.$rc.pause();
// //
this.StopTimer(); this.StopTimer();
//
this.$rc.close();
// //
if (this.moveToCancel) { if (this.moveToCancel) {
this.$rc.close();
console.log("录音取消") console.log("录音取消")
return; return;
} }
@ -87,7 +86,6 @@ export default {
title: "说话时间太短", title: "说话时间太短",
icon: 'none' icon: 'none'
}) })
this.$rc.close();
return; return;
} }
this.$rc.upload().then((data) => { this.$rc.upload().then((data) => {

6
im-uniapp/components/file-upload/file-upload.vue

@ -43,6 +43,12 @@ export default {
} }
}, },
methods: { methods: {
show() {
this.$refs.lsjUpload.show();
},
hide() {
this.$refs.lsjUpload.hide();
},
onUploadEnd(item) { onUploadEnd(item) {
let file = this.fileMap.get(item.path); let file = this.fileMap.get(item.path);
if (item.type == 'fail') { if (item.type == 'fail') {

51
im-uniapp/im.scss

@ -28,6 +28,39 @@ uni-button[size='mini'] {
font-size: $im-font-size-smaller !important; font-size: $im-font-size-smaller !important;
} }
// #ifdef MP-WEIXIN
// wx小程序只有button,没有uni-botton
button {
font-size: $im-font-size !important;
}
button[type='primary'] {
color: #fff !important;
background-color: $im-color-primary !important;
}
button[type='primary'][plain] {
color: $im-color-primary !important;
border: 1px solid $im-color-primary;
background-color: transparent;
}
button[type='warn'] {
color: #fff !important;
background-color: $im-color-danger !important;
}
button[type='warn'][plain] {
color: $im-color-danger !important;
border: 1px solid $im-color-danger !important;
background-color: transparent !important;
}
button[size='mini'] {
font-size: $im-font-size-smaller !important;
}
// #endif
.button-hover[type='primary'] { .button-hover[type='primary'] {
color: #fff !important; color: #fff !important;
background-color: $im-color-primary-dark-1 !important; background-color: $im-color-primary-dark-1 !important;
@ -139,3 +172,21 @@ uni-button[size='mini'] {
margin-top: 20rpx; margin-top: 20rpx;
} }
} }
.emoji-large {
width: 64rpx;
height: 64rpx;
vertical-align: bottom;
}
.emoji-normal {
width: 54rpx;
height: 54rpx;
vertical-align: bottom;
}
.emoji-small {
width: 36rpx;
height: 36rpx;
vertical-align: bottom;
}

16
im-uniapp/manifest.json

@ -11,9 +11,9 @@
"nvueStyleCompiler" : "uni-app", "nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3, "compilerVersion" : 3,
"splashscreen" : { "splashscreen" : {
"alwaysShowBeforeRender": true, "alwaysShowBeforeRender" : false,
"waiting": true, "waiting" : false,
"autoclose": true, "autoclose" : false,
"delay" : 0 "delay" : 0
}, },
/* */ /* */
@ -22,6 +22,9 @@
"Record" : {}, "Record" : {},
"Bluetooth" : {} "Bluetooth" : {}
}, },
"softinput" : {
"mode" : "adjustResize"
},
/* */ /* */
"distribute" : { "distribute" : {
/* android */ /* android */
@ -45,11 +48,7 @@
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\" />", "<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\" />",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\" />" "<uses-permission android:name=\"android.permission.RECORD_AUDIO\" />"
], ],
"abiFilters": [ "abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ],
"armeabi-v7a",
"arm64-v8a",
"x86"
],
"minSdkVersion" : 21 "minSdkVersion" : 21
}, },
/* ios */ /* ios */
@ -132,3 +131,4 @@
} }
} }
/* ios *//* SDK */ /* ios *//* SDK */

1
im-uniapp/package.json

@ -4,7 +4,6 @@
"scripts": {} "scripts": {}
}, },
"dependencies": { "dependencies": {
"js-audio-recorder": "^1.0.7",
"pinyin-pro": "^3.23.1", "pinyin-pro": "^3.23.1",
"vconsole": "^3.15.1" "vconsole": "^3.15.1"
} }

20
im-uniapp/pages.json

@ -7,16 +7,14 @@
"^u-([^-].*)": "@/uni_modules/uview-plus/components/u-$1/u-$1.vue" "^u-([^-].*)": "@/uni_modules/uview-plus/components/u-$1/u-$1.vue"
} }
}, },
"pages": [ "pages": [{
{ "path": "pages/chat/chat"
}, {
"path": "pages/login/login" "path": "pages/login/login"
}, },
{ {
"path": "pages/register/register" "path": "pages/register/register"
}, },
{
"path": "pages/chat/chat"
},
{ {
"path": "pages/friend/friend" "path": "pages/friend/friend"
}, },
@ -30,7 +28,14 @@
"path": "pages/common/user-info" "path": "pages/common/user-info"
}, },
{ {
"path": "pages/chat/chat-box" "path": "pages/chat/chat-box",
"style": {
"navigationStyle": "custom",
"app-plus": {
// adjustPanadjustResize=webview+
"softinputMode": "adjustResize"
}
}
}, },
{ {
"path": "pages/chat/chat-private-video" "path": "pages/chat/chat-private-video"
@ -71,8 +76,7 @@
"selectedColor": "#587ff0", "selectedColor": "#587ff0",
"borderStyle": "black", "borderStyle": "black",
"backgroundColor": "#ffffff", "backgroundColor": "#ffffff",
"list": [ "list": [{
{
"pagePath": "pages/chat/chat", "pagePath": "pages/chat/chat",
"iconPath": "static/tarbar/chat.png", "iconPath": "static/tarbar/chat.png",
"selectedIconPath": "static/tarbar/chat_active.png", "selectedIconPath": "static/tarbar/chat_active.png",

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

@ -1,14 +1,16 @@
<template> <template>
<view class="page chat-box"> <view class="page chat-box">
<nav-bar back more @more="onShowMore">{{ title }}</nav-bar> <nav-bar back more @more="onShowMore">{{ title }}</nav-bar>
<view class="chat-msg" @click="switchChatTabBox('none', true)"> <view class="chat-main-box" :style="{height: chatMainHeight+'px'}">
<view class="chat-msg" @click="switchChatTabBox('none')">
<scroll-view class="scroll-box" scroll-y="true" upper-threshold="200" @scrolltoupper="onScrollToTop" <scroll-view class="scroll-box" scroll-y="true" upper-threshold="200" @scrolltoupper="onScrollToTop"
:scroll-into-view="'chat-item-' + scrollMsgIdx"> :scroll-into-view="'chat-item-' + scrollMsgIdx">
<view v-if="chat" v-for="(msgInfo, idx) in chat.messages" :key="idx"> <view v-if="chat" v-for="(msgInfo, idx) in chat.messages" :key="idx">
<chat-message-item v-if="idx >= showMinIdx" :headImage="headImage(msgInfo)" @call="onRtCall(msgInfo)" <chat-message-item v-if="idx >= showMinIdx" :headImage="headImage(msgInfo)"
:showName="showName(msgInfo)" @recall="onRecallMessage" @copy="onCopyMessage" @call="onRtCall(msgInfo)" :showName="showName(msgInfo)" @recall="onRecallMessage"
@delete="onDeleteMessage" @longPressHead="onLongPressHead(msgInfo)" @download="onDownloadFile" @copy="onCopyMessage" @delete="onDeleteMessage" @longPressHead="onLongPressHead(msgInfo)"
:id="'chat-item-' + idx" :msgInfo="msgInfo" :groupMembers="groupMembers"> @download="onDownloadFile" :id="'chat-item-' + idx" :msgInfo="msgInfo"
:groupMembers="groupMembers">
</chat-message-item> </chat-message-item>
</view> </view>
</scroll-view> </scroll-view>
@ -28,22 +30,25 @@
<view v-else class="iconfont icon-keyboard" @click="onKeyboardInput()"></view> <view v-else class="iconfont icon-keyboard" @click="onKeyboardInput()"></view>
<chat-record v-if="showRecord" class="chat-record" @send="onSendRecord"></chat-record> <chat-record v-if="showRecord" class="chat-record" @send="onSendRecord"></chat-record>
<view v-else class="send-text"> <view v-else class="send-text">
<textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false" <editor id="editor" class="send-text-area" :placeholder="isReceipt ? '[回执消息]' : ''"
:read-only="isReadOnly" @focus="onEditorFocus" @blur="onEditorBlur" @ready="onEditorReady"
@input="onTextInput">
</editor>
<!-- <textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false"
:placeholder="isReceipt ? '[回执消息]' : ''" :adjust-position="false" @confirm="sendTextMessage()" :placeholder="isReceipt ? '[回执消息]' : ''" :adjust-position="false" @confirm="sendTextMessage()"
@keyboardheightchange="onKeyboardheightchange" @input="onTextInput" confirm-type="send" confirm-hold @keyboardheightchange="onKeyboardheightchange" @input="onTextInput" confirm-type="send"
:hold-keyboard="true"></textarea> confirm-hold :hold-keyboard="true"></textarea> -->
</view> </view>
<view v-if="chat && chat.type == 'GROUP'" class="iconfont icon-at" @click="openAtBox()"></view> <view v-if="chat && chat.type == 'GROUP'" class="iconfont icon-at" @click="openAtBox()"></view>
<view class="iconfont icon-icon_emoji" @click="onShowEmoChatTab()"></view> <view class="iconfont icon-icon_emoji" @click="onShowEmoChatTab()"></view>
<view v-if="sendText == ''" class="iconfont icon-add" @click="onShowToolsChatTab()"> <view v-if="isEmpty" class="iconfont icon-add" @click="onShowToolsChatTab()">
</view> </view>
<button v-if="sendText != '' || atUserIds.length > 0" class="btn-send" type="primary" <button v-if="!isEmpty || atUserIds.length > 0" class="btn-send" type="primary"
@touchend.prevent="sendTextMessage()" size="mini">发送</button> @touchend.prevent="sendTextMessage()" size="mini">发送</button>
</view> </view>
</view>
<view class="chat-tab-bar" v-show="chatTabBox != 'none' || (showKeyBoard && !isH5)" <view class="chat-tab-bar">
:style="{ height: `${keyboardHeight}px` }"> <view v-if="chatTabBox == 'tools'" class="chat-tools" :style="{height: keyboardHeight+'px'}">
<view v-if="chatTabBox == 'tools'" class="chat-tools">
<view class="chat-tools-item"> <view class="chat-tools-item">
<image-upload :maxCount="9" sourceType="album" :onBefore="onUploadImageBefore" <image-upload :maxCount="9" sourceType="album" :onBefore="onUploadImageBefore"
:onSuccess="onUploadImageSuccess" :onError="onUploadImageFail"> :onSuccess="onUploadImageSuccess" :onError="onUploadImageFail">
@ -60,7 +65,7 @@
</view> </view>
<view class="chat-tools-item"> <view class="chat-tools-item">
<file-upload :onBefore="onUploadFileBefore" :onSuccess="onUploadFileSuccess" <file-upload ref="fileUpload" :onBefore="onUploadFileBefore" :onSuccess="onUploadFileSuccess"
:onError="onUploadFileFail"> :onError="onUploadFileFail">
<view class="tool-icon iconfont icon-folder"></view> <view class="tool-icon iconfont icon-folder"></view>
</file-upload> </file-upload>
@ -91,9 +96,10 @@
</view> </view>
<!-- #endif --> <!-- #endif -->
</view> </view>
<scroll-view v-if="chatTabBox === 'emo'" class="chat-emotion" scroll-y="true"> <scroll-view v-if="chatTabBox === 'emo'" class="chat-emotion" scroll-y="true"
:style="{height: keyboardHeight+'px'}">
<view class="emotion-item-list"> <view class="emotion-item-list">
<image class="emotion-item" :title="emoText" :src="$emo.textToPath(emoText)" <image class="emotion-item emoji-large" :title="emoText" :src="$emo.textToPath(emoText)"
v-for="(emoText, i) in $emo.emoTextList" :key="i" @click="selectEmoji(emoText)" mode="aspectFit" v-for="(emoText, i) in $emo.emoTextList" :key="i" @click="selectEmoji(emoText)" mode="aspectFit"
lazy-load="true"></image> lazy-load="true"></image>
</view> </view>
@ -122,29 +128,31 @@ export default {
friend: {}, friend: {},
group: {}, group: {},
groupMembers: [], groupMembers: [],
sendText: "",
isReceipt: false, // isReceipt: false, //
scrollMsgIdx: 0, // scrollMsgIdx: 0, //
chatTabBox: 'none', chatTabBox: 'none',
showKeyBoard: false,
showRecord: false, showRecord: false,
keyboardHeight: 322, keyboardHeight: 300,
atUserIds: [], atUserIds: [],
needScrollToBottom: false, // needScrollToBottom: false, //
showMinIdx: 0, // showMinIdx showMinIdx: 0, // showMinIdx
reqQueue: [], // reqQueue: [], //
isSending: false, // isSending: false, //
isH5: false // h5 isShowKeyBoard: false, //
editorCtx: null, //
isEmpty: true, //
isFocus: false, //
isReadOnly: false //
} }
}, },
methods: { methods: {
onRecorderInput() { onRecorderInput() {
this.showRecord = true; this.showRecord = true;
this.switchChatTabBox('none', true); this.switchChatTabBox('none');
}, },
onKeyboardInput() { onKeyboardInput() {
this.showRecord = false; this.showRecord = false;
this.switchChatTabBox('none', false); this.switchChatTabBox('none');
}, },
onSendRecord(data) { onSendRecord(data) {
let msgInfo = { let msgInfo = {
@ -250,7 +258,19 @@ export default {
} }
}, },
sendTextMessage() { sendTextMessage() {
if (!this.sendText.trim() && this.atUserIds.length == 0) { this.editorCtx.getContents({
success: (e) => {
let sendText = this.isReceipt ? "【回执消息】" : "";
e.delta.ops.forEach((op) => {
if (op.insert.image) {
// emo
sendText += `#${op.attributes.alt};`
} else(
//
sendText += op.insert
)
})
if (!sendText.trim() && this.atUserIds.length == 0) {
return uni.showToast({ return uni.showToast({
title: "不能发送空白信息", title: "不能发送空白信息",
icon: "none" icon: "none"
@ -259,26 +279,28 @@ export default {
let receiptText = this.isReceipt ? "【回执消息】" : ""; let receiptText = this.isReceipt ? "【回执消息】" : "";
let atText = this.createAtText(); let atText = this.createAtText();
let msgInfo = { let msgInfo = {
content: receiptText + this.sendText + atText, content: receiptText + sendText + atText,
atUserIds: this.atUserIds, atUserIds: this.atUserIds,
receipt: this.isReceipt, receipt: this.isReceipt,
type: 0 type: 0
} }
this.sendText = "";
// id // id
this.fillTargetId(msgInfo, this.chat.targetId); this.fillTargetId(msgInfo, this.chat.targetId);
this.sendMessageRequest(msgInfo).then((m) => { this.sendMessageRequest(msgInfo).then((m) => {
m.selfSend = true; m.selfSend = true;
this.chatStore.insertMessage(m); this.chatStore.insertMessage(m, this.chat);
// //
this.moveChatToTop(); this.moveChatToTop();
}).finally(() => { }).finally(() => {
// //
this.scrollToBottom(); this.scrollToBottom();
// @ //
this.atUserIds = []; this.atUserIds = [];
this.isReceipt = false; this.isReceipt = false;
this.editorCtx.clear();
}); });
}
})
}, },
createAtText() { createAtText() {
let atText = ""; let atText = "";
@ -325,31 +347,35 @@ export default {
}, },
onShowEmoChatTab() { onShowEmoChatTab() {
this.showRecord = false; this.showRecord = false;
this.switchChatTabBox('emo', true) this.switchChatTabBox('emo')
}, },
onShowToolsChatTab() { onShowToolsChatTab() {
this.showRecord = false; this.showRecord = false;
this.switchChatTabBox('tools', true) this.switchChatTabBox('tools')
}, },
switchChatTabBox(chatTabBox, hideKeyBoard) { switchChatTabBox(chatTabBox) {
this.chatTabBox = chatTabBox; this.chatTabBox = chatTabBox;
if (hideKeyBoard) { if (chatTabBox != 'tools' && this.$refs.fileUpload) {
uni.hideKeyboard(); this.$refs.fileUpload.hide()
this.showKeyBoard = false;
} }
}, },
selectEmoji(emoText) { selectEmoji(emoText) {
this.sendText += `#${emoText};`; let path = this.$emo.textToPath(emoText)
}, //
onKeyboardheightchange(e) { this.isReadOnly = true;
if (e.detail.height > 0) { this.isEmpty = false;
this.showKeyBoard = true; this.$nextTick(() => {
this.switchChatTabBox('none', false) this.editorCtx.insertImage({
this.keyboardHeight = this.rpxTopx(e.detail.height); src: path,
this.scrollToBottom(); alt: emoText,
} else { extClass: 'emoji-small',
this.showKeyBoard = false; nowrap: true,
complete: () => {
this.isReadOnly = false;
this.editorCtx.blur();
} }
});
})
}, },
onUploadImageBefore(file) { onUploadImageBefore(file) {
let data = { let data = {
@ -540,14 +566,22 @@ export default {
} }
}, },
onTextInput(e) { onTextInput(e) {
let idx = e.detail.cursor - 1; this.isEmpty = e.detail.html == '<p><br></p>'
if (this.chat.type == 'GROUP' && e.detail.value[idx] == '@') { },
this.openAtBox(); onEditorReady() {
let sendText = e.detail.value.replace("@", ''); const query = uni.createSelectorQuery().in(this);
this.$nextTick(() => { query.select('#editor').context((res) => {
this.sendText = sendText; this.editorCtx = res.context
}) }).exec()
} },
onEditorFocus(e) {
this.isFocus = true;
this.scrollToBottom()
this.switchChatTabBox('none')
},
onEditorBlur(e) {
this.isFocus = false;
}, },
loadReaded(fid) { loadReaded(fid) {
this.$http({ this.$http({
@ -638,21 +672,27 @@ export default {
}) })
} }
}, },
listenKeyBoardForH5() { listenKeyBoard() {
// #ifdef H5
// H5TextArea@keyboardheightchange // H5TextArea@keyboardheightchange
// //
let initHeight = window.innerHeight; let initHeight = window.innerHeight;
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
let keyboardHeight = initHeight - window.innerHeight; let keyboardHeight = initHeight - window.innerHeight;
if (keyboardHeight > 0) { this.isShowKeyBoard = keyboardHeight > 0;
this.keyboardHeight = keyboardHeight - 20; if (this.isShowKeyBoard) {
this.showKeyBoard = true; this.keyboardHeight = keyboardHeight;
this.switchChatTabBox('none', false)
this.scrollToBottom();
} else {
this.showKeyBoard = false;
} }
}); });
// #endif
// #ifndef H5
uni.onKeyboardHeightChange((res) => {
this.isShowKeyBoard = res.height > 0;
if (this.isShowKeyBoard) {
this.keyboardHeight = res.height; //
}
});
// #endif
}, },
generateId() { generateId() {
// id // id
@ -705,6 +745,26 @@ export default {
} }
}) })
return atUsers; return atUsers;
},
chatMainHeight() {
const sysInfo = uni.getSystemInfoSync();
let h = sysInfo.windowHeight;
//
h -= 50;
// #ifdef H5
// h5sysInfo.windowHeight
if (this.chatTabBox != 'none') {
h -= this.keyboardHeight;
}
// #endif
// #ifndef H5
//
h -= sysInfo.statusBarHeight;
if (this.isShowKeyBoard || this.chatTabBox != 'none') {
h -= this.keyboardHeight;
}
// #endif
return h;
} }
}, },
watch: { watch: {
@ -730,10 +790,6 @@ export default {
} }
}, },
onLoad(options) { onLoad(options) {
// #ifdef H5
this.isH5 = true;
this.listenKeyBoardForH5();
// #endif
// //
this.chat = this.chatStore.chats[options.chatIdx]; this.chat = this.chatStore.chats[options.chatIdx];
// 20 // 20
@ -752,6 +808,8 @@ export default {
this.chatStore.activeChat(options.chatIdx); this.chatStore.activeChat(options.chatIdx);
// //
this.isReceipt = false; this.isReceipt = false;
//
this.listenKeyBoard();
}, },
onShow() { onShow() {
if (this.needScrollToBottom) { if (this.needScrollToBottom) {
@ -765,9 +823,11 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.chat-box { .chat-box {
$icon-color: rgba(0, 0, 0, 0.88);
position: relative; position: relative;
display: flex; background-color: #fafafa;
flex-direction: column;
.header { .header {
display: flex; display: flex;
@ -775,7 +835,7 @@ export default {
align-items: center; align-items: center;
height: 60rpx; height: 60rpx;
padding: 5px; padding: 5px;
background-color: #f9f9f9; background-color: #fafafa;
line-height: 50px; line-height: 50px;
font-size: $im-font-size-large; font-size: $im-font-size-large;
box-shadow: $im-box-shadow-lighter; box-shadow: $im-box-shadow-lighter;
@ -792,6 +852,19 @@ export default {
} }
} }
.chat-main-box {
// #ifdef H5
top: $im-nav-bar-height;
// #endif
// #ifndef H5
top: calc($im-nav-bar-height + var(--status-bar-height));
// #endif
position: fixed;
width: 100%;
display: flex;
flex-direction: column;
z-index: 9;
.chat-msg { .chat-msg {
flex: 1; flex: 1;
padding: 0; padding: 0;
@ -832,18 +905,16 @@ export default {
} }
$icon-color: rgba(0, 0, 0, 0.88);
.send-bar { .send-bar {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 10rpx; padding: 10rpx;
//margin-bottom: 10rpx;
border-top: $im-border solid 1px; border-top: $im-border solid 1px;
background-color: $im-bg; background-color: $im-bg;
height: 80rpx; min-height: 80rpx;
//box-shadow: $im-box-shadow-lighter; margin-bottom: 14rpx;
z-index: 1;
.iconfont { .iconfont {
font-size: 60rpx; font-size: 60rpx;
@ -864,9 +935,14 @@ export default {
font-size: $im-font-size; font-size: $im-font-size;
box-sizing: border-box; box-sizing: border-box;
margin: 0 10rpx; margin: 0 10rpx;
position: relative;
.send-text-area { .send-text-area {
width: 100%; width: 100%;
height: 100%;
min-height: 40rpx;
max-height: 200rpx;
font-size: 30rpx;
} }
} }
@ -874,17 +950,20 @@ export default {
margin: 5rpx; margin: 5rpx;
} }
} }
}
.chat-tab-bar { .chat-tab-bar {
height: 500rpx; position: fixed;
padding: 20rpx; bottom: 0;
background-color: $im-bg; background-color: $im-bg;
.chat-tools { .chat-tools {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
padding-top: 20rpx; align-items: top;
height: 310px;
padding: 40rpx;
box-sizing: border-box;
.chat-tools-item { .chat-tools-item {
width: 25%; width: 25%;
@ -915,19 +994,20 @@ export default {
} }
.chat-emotion { .chat-emotion {
height: 100%; height: 310px;
padding: 20rpx;
box-sizing: border-box;
.emotion-item-list { .emotion-item-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
align-content: center;
.emotion-item { .emotion-item {
width: 34px;
height: 34px;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
padding: 6px; padding: 5px;
} }
} }
} }

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

@ -13,7 +13,13 @@
<view class="user-item"> <view class="user-item">
<head-image :id="user.id" :name="user.nickName" :online="user.online" <head-image :id="user.id" :name="user.nickName" :online="user.online"
:url="user.headImage"></head-image> :url="user.headImage"></head-image>
<view class="user-name">{{ user.nickName }}</view> <view class="user-info">
<view class="user-name">
<view>{{ user.userName }}</view>
<uni-tag v-if="user.status == 1" circle type="error" text="已注销" size="small"></uni-tag>
</view>
<view class="nick-name">{{ `昵称:${user.nickName}`}}</view>
</view>
<view class="user-btns"> <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>
@ -90,22 +96,45 @@ export default {
overflow: hidden; overflow: hidden;
.user-item { .user-item {
height: 120rpx; height: 100rpx;
display: flex; display: flex;
margin-bottom: 1rpx; margin-bottom: 1rpx;
position: relative; position: relative;
padding: 0 30rpx; padding: 18rpx 20rpx;
align-items: center; align-items: center;
background-color: white; background-color: white;
white-space: nowrap; white-space: nowrap;
.user-name { .user-info {
flex: 1; flex: 1;
display: flex;
flex-direction: column;
padding-left: 20rpx; padding-left: 20rpx;
font-size: $im-font-size; font-size: $im-font-size;
line-height: 60rpx;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
.user-name {
display: flex;
flex: 1;
font-size: $im-font-size-large;
white-space: nowrap;
overflow: hidden;
align-items: center;
.uni-tag {
text-align: center;
margin-left: 5rpx;
padding: 1px 5px;
}
}
.nick-name {
display: flex;
font-size: $im-font-size-smaller;
color: $im-text-color-lighter;
padding-top: 8rpx;
}
} }
} }

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

@ -1,31 +1,33 @@
<template> <template>
<view v-if="userStore.userInfo.type == 1" class="page group-edit"> <view class="page group-edit">
<nav-bar back>修改群资料</nav-bar> <nav-bar back>修改群资料</nav-bar>
<uni-card :is-shadow="false" is-full :border="false"> <view class="form">
<uni-forms ref="form" :modelValue="group" :rules="rules" validate-trigger="bind" label-position="top" <view class="form-item">
label-width="100%"> <view class="label">群聊头像</view>
<uni-forms-item name="headImage" class="avatar"> <view class="value"></view>
<image-upload v-if="isOwner" :onSuccess="onUnloadImageSuccess"> <image-upload v-if="isOwner" :onSuccess="onUnloadImageSuccess">
<image :src="group.headImageThumb" class="group-image"></image> <image :src="group.headImageThumb" class="group-image"></image>
</image-upload> </image-upload>
<head-image v-if="!isOwner" :name="group.showGroupName" :url="group.headImageThumb" <head-image v-else class="group-image" :name="group.showGroupName" :url="group.headImageThumb"
:size="200"></head-image> :size="120"></head-image>
</uni-forms-item> </view>
<uni-forms-item label="群聊名称" name="name" :required="true"> <view class="form-item">
<uni-easyinput type="text" v-model="group.name" :disabled="!isOwner" placeholder="请输入群聊名称" /> <view class="label">群聊名称</view>
</uni-forms-item> <input class="input" :class="isOwner?'':'disable'" maxlength="20" v-model="group.name" :disabled="!isOwner" placeholder="请输入群聊名称"/>
<uni-forms-item label="群聊备注" name="remarkGroupName"> </view>
<uni-easyinput v-model="group.remarkGroupName" type="text" :placeholder="group.name" /> <view class="form-item">
</uni-forms-item> <view class="label">群聊备注</view>
<uni-forms-item label="我在本群的昵称" name="remarkNickName"> <input class="input" maxlength="20" v-model="group.remarkGroupName" :placeholder="group.name"/>
<uni-easyinput v-model="group.remarkNickName" type="text" </view>
:placeholder="userStore.userInfo.nickName" /> <view class="form-item">
</uni-forms-item> <view class="label">我在本群的昵称</view>
<uni-forms-item label="群公告" name="notice"> <input class="input" maxlength="20" v-model="group.remarkNickName" :placeholder="userStore.userInfo.nickName"/>
<uni-easyinput type="textarea" v-model="group.notice" :disabled="!isOwner" placeholder="请输入群公告" /> </view>
</uni-forms-item> <view class="form-item">
</uni-forms> <view class="label">群公告</view>
</uni-card> <textarea class="notice" :class="isOwner?'':'disable'" maxlength="512" :disabled="!isOwner" v-model="group.notice" :placeholder="isOwner?'请输入群公告':''"></textarea>
</view>
</view>
<button class="bottom-btn" type="primary" @click="submit()">提交</button> <button class="bottom-btn" type="primary" @click="submit()">提交</button>
</view> </view>
</template> </template>
@ -46,6 +48,7 @@ export default {
} }
} }
}, },
methods: { methods: {
submit() { submit() {
if (this.group.id) { if (this.group.id) {
@ -141,17 +144,54 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.group-edit { .group-edit {
//padding: 20rpx;
.form {
margin-top: 20rpx;
.form-item {
padding: 0 40rpx;
display: flex;
background: white;
align-items: center;
margin-bottom: 2rpx;
.label {
width: 220rpx;
line-height: 100rpx;
font-size: $im-font-size;
white-space: nowrap;
}
.value{
flex: 1;
}
.input {
flex: 1;
text-align: right;
line-height: 100rpx;
font-size: $im-font-size-small;
}
.disable {
color: $im-text-color-lighter;
}
.notice {
flex: 1;
font-size: $im-font-size-small;
max-height: 200rpx;
padding: 14rpx 0;
}
.group-image { .group-image {
width: 200rpx; width: 120rpx;
height: 200rpx; height: 120rpx;
border-radius: 50%;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 5%;
} }
} }
.avatar {
margin-top: -30px;
} }
}
</style> </style>

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

@ -1,5 +1,5 @@
<template> <template>
<view v-if="userStore.userInfo.type == 1" class="page group-info"> <view class="page group-info">
<nav-bar back>群聊信息</nav-bar> <nav-bar back>群聊信息</nav-bar>
<view v-if="!group.quit" class="group-members"> <view v-if="!group.quit" class="group-members">
<view class="member-items"> <view class="member-items">
@ -18,37 +18,37 @@
</view> </view>
<view class="member-more" @click="onShowMoreMmeber()">{{ `查看全部群成员${groupMembers.length}` }}></view> <view class="member-more" @click="onShowMoreMmeber()">{{ `查看全部群成员${groupMembers.length}` }}></view>
</view> </view>
<view class="group-detail"> <view class="form">
<view class="form-item">
<uni-section title="群聊名称"> <view class="label">群聊名称</view>
<template v-slot:right> <view class="value">{{group.name}}</view>
<text class="detail-text">{{ group.name }}</text> </view>
</template> <view class="form-item">
</uni-section> <view class="label">群主</view>
<uni-section title="群主"> <view class="value">{{ownerName}}</view>
<template v-slot:right> </view>
<text class="detail-text">{{ ownerName }}</text> <view class="form-item">
</template> <view class="label">群名备注</view>
</uni-section> <view class="value">{{group.remarkGroupName}}</view>
<uni-section title="群名备注"> </view>
<template v-slot:right> <view class="form-item">
<text class="detail-text"> {{ group.remarkGroupName }}</text> <view class="label">我在本群的昵称</view>
</template> <view class="value">{{group.showNickName}}</view>
</uni-section> </view>
<uni-section title="我在本群的昵称"> <view v-if="group.notice" class="form-item" >
<template v-slot:right> <view class="label">群公告</view>
<text class="detail-text"> {{ group.showNickName }}</text> </view>
</template> <view v-if="group.notice" class="form-item" >
</uni-section>
<uni-section v-if="group.notice" title="群公告">
<uni-notice-bar :text="group.notice" /> <uni-notice-bar :text="group.notice" />
</uni-section> </view>
<view v-if="!group.quit" class="group-edit" @click="onEditGroup()">修改群聊资料 > </view> <view v-if="!group.quit" class="group-edit" @click="onEditGroup()">修改群聊资料 > </view>
</view> </view>
<bar-group v-if="!group.quit"> <bar-group v-if="!group.quit">
<btn-bar type="primary" title="发送消息" @click="onSendMessage()"></btn-bar> <btn-bar type="primary" title="发送消息" @tap="onSendMessage()"></btn-bar>
<btn-bar v-if="!isOwner" type="danger" title="退出群聊" @click="onQuitGroup()"></btn-bar> <btn-bar v-if="!isOwner" type="danger" title="退出群聊" @tap="onQuitGroup()"></btn-bar>
<btn-bar v-if="isOwner" type="danger" title="解散群聊" @click="onDissolveGroup()"></btn-bar> <btn-bar v-if="isOwner" type="danger" title="解散群聊" @tap="onDissolveGroup()"></btn-bar>
</bar-group> </bar-group>
</view> </view>
</template> </template>
@ -113,7 +113,8 @@ export default {
url: "/pages/group/group" url: "/pages/group/group"
}); });
this.groupStore.removeGroup(this.groupId); this.groupStore.removeGroup(this.groupId);
this.chatStore.removeGroupChat(this.groupId); this.chatStore.removeGroupChat(this
.groupId);
}, 100) }, 100)
} }
}) })
@ -142,7 +143,8 @@ export default {
url: "/pages/group/group" url: "/pages/group/group"
}); });
this.groupStore.removeGroup(this.groupId); this.groupStore.removeGroup(this.groupId);
this.chatStore.removeGroupChat(this.groupId); this.chatStore.removeGroupChat(this
.groupId);
}, 100) }, 100)
} }
}) })
@ -194,7 +196,7 @@ export default {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss">
.group-info { .group-info {
.group-members { .group-members {
padding: 30rpx; padding: 30rpx;
@ -245,19 +247,38 @@ export default {
} }
} }
.form {
margin-top: 20rpx;
.group-detail { .form-item {
margin-top: 30rpx; padding: 0 40rpx;
padding: 20rpx 20rpx; display: flex;
background: white; background: white;
align-items: center;
margin-top: 2rpx;
.detail-text { .label {
width: 220rpx;
line-height: 100rpx;
font-size: $im-font-size; font-size: $im-font-size;
white-space: nowrap;
}
.value {
flex: 1;
text-align: right;
line-height: 100rpx;
color: $im-text-color-lighter;
font-size: $im-font-size-small;
white-space: nowrap;
overflow: hidden;
}
} }
.group-edit { .group-edit {
padding-top: 30rpx; padding: 10rpx 40rpx 30rpx 40rpx ;
text-align: center; text-align: center;
background: white;
font-size: $im-font-size-small; font-size: $im-font-size-small;
color: $im-text-color-lighter; color: $im-text-color-lighter;
} }

6
im-uniapp/pages/login/login.vue

@ -1,7 +1,7 @@
<template> <template>
<view class="login"> <view class="login">
<view class="title">欢迎登录</view> <view class="title">欢迎登录</view>
<uni-forms class="form" :modelValue="loginForm" :rules="rules" validate-trigger="bind"> <uni-forms :modelValue="loginForm" :rules="rules" validate-trigger="bind">
<uni-forms-item name="userName"> <uni-forms-item name="userName">
<uni-easyinput type="text" v-model="loginForm.userName" prefix-icon="person" placeholder="用户名" /> <uni-easyinput type="text" v-model="loginForm.userName" prefix-icon="person" placeholder="用户名" />
</uni-forms-item> </uni-forms-item>
@ -69,7 +69,7 @@ export default {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss">
.login { .login {
.title { .title {
padding-top: 150rpx; padding-top: 150rpx;
@ -80,7 +80,7 @@ export default {
font-weight: bold; font-weight: bold;
} }
.form { .uni-forms {
padding: 50rpx; padding: 50rpx;
.btn-submit { .btn-submit {

115
im-uniapp/pages/mine/mine-edit.vue

@ -1,28 +1,35 @@
<template> <template>
<view class="page mine-edit"> <view class="page mine-edit">
<nav-bar back>修改我的信息</nav-bar> <nav-bar back>修改我的信息</nav-bar>
<uni-card :is-shadow="false" is-full :border="false"> <view class="form">
<uni-forms ref="form" :modelValue="userInfo" label-position="top" label-width="100%"> <view class="form-item">
<uni-forms-item name="headImage" class="avatar"> <view class="label">头像</view>
<image-upload :onSuccess="onUnloadImageSuccess"> <image-upload class="value" :onSuccess="onUnloadImageSuccess">
<image :src="userInfo.headImageThumb" class="head-image"></image> <image :src="userInfo.headImageThumb" class="head-image"></image>
</image-upload> </image-upload>
</uni-forms-item> </view>
<uni-forms-item label="用户名" name="userName"> <view class="form-item">
<uni-easyinput type="text" v-model="userInfo.userName" :disabled="true" /> <view class="label">账号</view>
</uni-forms-item> <view class="value">{{userInfo.userName}}</view>
<uni-forms-item label="昵称" name="nickName"> </view>
<uni-easyinput v-model="userInfo.nickName" type="text" :placeholder="userInfo.userName" /> <view class="form-item">
</uni-forms-item> <view class="label">昵称</view>
<uni-forms-item label="性别" name="sex"> <input class="input" maxlength="20" v-model="userInfo.nickName" placeholder="请输入您的昵称" />
<uni-data-checkbox v-model="userInfo.sex" </view>
:localdata="[{ text: '男', value: 0 }, { text: '女', value: 1 }]"></uni-data-checkbox> <view class="form-item">
</uni-forms-item> <view class="label">性别</view>
<uni-forms-item label="签名" name="signature"> <radio-group class="radio-group" @change="onSexChange">
<uni-easyinput type="textarea" v-model="userInfo.signature" placeholder="编辑个性标签,展示我的独特态度" /> <radio class="radio" :value="0" :checked="userInfo.sex==0"></radio>
</uni-forms-item> <radio class="radio" :value="1" :checked="userInfo.sex==1"></radio>
</uni-forms> </radio-group>
</uni-card> </view>
<view class="form-item">
<view class="label">个性签名</view>
<textarea class="signature" maxlength="128" auto-height v-model="userInfo.signature"
:style="{'text-align': signTextAlign}" @linechange="onLineChange"
placeholder="编辑个性签名,展示我的独特态度"></textarea>
</view>
</view>
<button type="primary" class="bottom-btn" @click="onSubmit()">提交</button> <button type="primary" class="bottom-btn" @click="onSubmit()">提交</button>
</view> </view>
</template> </template>
@ -31,11 +38,12 @@
export default { export default {
data() { data() {
return { return {
signTextAlign: 'right',
userInfo: {} userInfo: {}
} }
}, },
methods: { methods: {
onSexchange(e) { onSexChange(e) {
this.userInfo.sex = e.detail.value; this.userInfo.sex = e.detail.value;
}, },
onUnloadImageSuccess(file, res) { onUnloadImageSuccess(file, res) {
@ -53,10 +61,10 @@ export default {
title: "修改成功", title: "修改成功",
icon: 'none' icon: 'none'
}); });
setTimeout(() => {
uni.navigateBack();
}, 1000);
}) })
},
onLineChange(e) {
this.signTextAlign = e.detail.lineCount > 1 ? "left" : "right";
} }
}, },
onLoad() { onLoad() {
@ -69,13 +77,64 @@ export default {
<style scoped lang="scss"> <style scoped lang="scss">
.mine-edit { .mine-edit {
.head-image {
.form {
margin-top: 20rpx;
.form-item {
padding: 0 40rpx;
display: flex;
background: white;
align-items: center;
margin-bottom: 2rpx;
.label {
width: 200rpx; width: 200rpx;
height: 200rpx; line-height: 100rpx;
font-size: $im-font-size;
}
.value {
flex: 1;
text-align: right;
line-height: 100rpx;
color: $im-text-color-lighter;
font-size: $im-font-size-small;
} }
.radio-group {
flex: 1;
text-align: right;
.radio {
margin-left: 50rpx;
}
}
.input {
flex: 1;
text-align: right;
line-height: 100rpx;
font-size: $im-font-size-small;
} }
.avatar { .signature {
margin-top: -30px; flex: 1;
font-size: $im-font-size-small;
max-height: 160rpx;
padding: 14rpx 0;
text-align: right;
}
.head-image {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
border: 1px solid #ccc;
}
}
}
} }
</style> </style>

7
im-uniapp/pages/mine/mine.vue

@ -31,16 +31,13 @@
</view> </view>
</view> </view>
</view> </view>
<view class="info-arrow">
</view>
</view> </view>
</uni-card> </uni-card>
<bar-group> <bar-group>
<arrow-bar title="修改密码" @click="onModifyPassword()"></arrow-bar> <arrow-bar title="修改密码" @tap="onModifyPassword()"></arrow-bar>
</bar-group> </bar-group>
<bar-group> <bar-group>
<btn-bar title="退出登" type="danger" @click="onQuit()"></btn-bar> <btn-bar title="退出登" type="danger" @tap="onQuit()"></btn-bar>
</bar-group> </bar-group>
</view> </view>
</template> </template>

10
im-uniapp/pages/register/register.vue

@ -1,7 +1,7 @@
<template> <template>
<view class="register"> <view class="register">
<view class="title">欢迎注册</view> <view class="title">欢迎注册</view>
<uni-forms class="form" ref="form" :modelValue="dataForm" :rules="rules" validate-trigger="bind" label-width="80px"> <uni-forms ref="form" :modelValue="dataForm" :rules="rules" validate-trigger="bind" label-width="80px">
<uni-forms-item name="userName" label="用户名"> <uni-forms-item name="userName" label="用户名">
<uni-easyinput type="text" v-model="dataForm.userName" placeholder="用户名" /> <uni-easyinput type="text" v-model="dataForm.userName" placeholder="用户名" />
</uni-forms-item> </uni-forms-item>
@ -14,10 +14,10 @@
<uni-forms-item name="corfirmPassword" label="确认密码"> <uni-forms-item name="corfirmPassword" label="确认密码">
<uni-easyinput type="password" v-model="dataForm.corfirmPassword" placeholder="确认密码" /> <uni-easyinput type="password" v-model="dataForm.corfirmPassword" placeholder="确认密码" />
</uni-forms-item> </uni-forms-item>
<button class="btn-submit" @click="submit" type="primary">注册并登</button> <button class="btn-submit" @click="submit" type="primary">注册并登</button>
</uni-forms> </uni-forms>
<navigator class="nav-login" url="/pages/login/login"> <navigator class="nav-login" url="/pages/login/login">
返回登页面 返回登页面
</navigator> </navigator>
</view> </view>
</template> </template>
@ -111,7 +111,7 @@ export default {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss">
.register { .register {
.title { .title {
padding-top: 150rpx; padding-top: 150rpx;
@ -122,7 +122,7 @@ export default {
font-weight: 600; font-weight: 600;
} }
.form { .uni-forms {
padding: 50rpx; padding: 50rpx;
.btn-submit { .btn-submit {

2
im-web/src/view/Home.vue

@ -107,7 +107,7 @@ export default {
// ws // ws
this.$wsApi.close(3000) this.$wsApi.close(3000)
// 线 // 线
this.$alert("您已在其他地方登,将被强制下线", "强制下线通知", { this.$alert("您已在其他地方登,将被强制下线", "强制下线通知", {
confirmButtonText: '确定', confirmButtonText: '确定',
callback: action => { callback: action => {
location.href = "/"; location.href = "/";

6
im-web/src/view/Login.vue

@ -5,7 +5,7 @@
@keyup.enter.native="submitForm('loginForm')"> @keyup.enter.native="submitForm('loginForm')">
<div class="login-brand"> <div class="login-brand">
<img class="logo" src="../../public/logo.png" /> <img class="logo" src="../../public/logo.png" />
<div>盒子IM</div> <div>盒子IM</div>
</div> </div>
<el-form-item label="终端" prop="userName" v-show="false"> <el-form-item label="终端" prop="userName" v-show="false">
<el-input type="terminal" v-model="loginForm.terminal" autocomplete="off"></el-input> <el-input type="terminal" v-model="loginForm.terminal" autocomplete="off"></el-input>
@ -19,7 +19,7 @@
placeholder="密码"></el-input> placeholder="密码"></el-input>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="submitForm('loginForm')"></el-button> <el-button type="primary" @click="submitForm('loginForm')"></el-button>
<el-button @click="resetForm('loginForm')">清空</el-button> <el-button @click="resetForm('loginForm')">清空</el-button>
</el-form-item> </el-form-item>
<div class="register"> <div class="register">
@ -86,7 +86,7 @@ export default {
// token // token
sessionStorage.setItem("accessToken", data.accessToken); sessionStorage.setItem("accessToken", data.accessToken);
sessionStorage.setItem("refreshToken", data.refreshToken); sessionStorage.setItem("refreshToken", data.refreshToken);
this.$message.success("登成功"); this.$message.success("登成功");
this.$router.push("/home/chat"); this.$router.push("/home/chat");
}) })

BIN
截图/app/1.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 813 KiB

BIN
截图/app/1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
截图/app/2.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 767 KiB

BIN
截图/app/2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
截图/web/单人通话.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 KiB

BIN
截图/web/多人通话.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
截图/web/好友.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

BIN
截图/web/好友列表.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 367 KiB

BIN
截图/web/私聊.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

BIN
截图/web/私聊.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 629 KiB

BIN
截图/web/群列表.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

BIN
截图/web/群列表.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 795 KiB

BIN
截图/web/群聊.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

BIN
截图/web/群聊.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 612 KiB

BIN
截图/web/群视频.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

Loading…
Cancel
Save