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. uniapp端兼容app、h5、微信小程序,可与web端同时在线,并保持消息同步
1. 后端采用springboot+netty实现,网页端使用vue,移动端使用uniapp
1. 服务器支持集群化部署,每个im-server仅处理自身连接用户的消息
1. 服务器支持集群化部署,具有良好的横向扩展能力
详细文档:https://www.yuque.com/u1475064/mufu2a
@ -21,7 +21,7 @@
- 后台管理端上线,后台管理代码仓库地址:https://gitee.com/bluexsx/box-im-admin
- 框架和组件版本全面升级: jdk17、springboot3.3、node18等
- 部分界面,功能、性能优化
- 部分界面,功能、性能优化1
#### 在线体验
@ -60,9 +60,9 @@ https://www.yuque.com/u1475064/imk5n2/qtezcg32q1d0dr29#SbvXq
| 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)
- 当消息的发送者和接收者连的不是同一个server时,消息是无法直接推送的,所以我们需要设计出能够支持跨节点推送的方案
- 利用了redis的list数据实现消息推送,其中key为im:unread:${serverid},每个key的数据可以看做一个queue,每个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:
![输入图片说明](%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(已满)

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

@ -6,9 +6,9 @@ import lombok.AllArgsConstructor;
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
*/
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;
import com.bx.implatform.annotation.RepeatSubmit;
import com.bx.implatform.entity.Friend;
import com.bx.implatform.result.Result;
import com.bx.implatform.result.ResultUtils;
@ -39,6 +40,7 @@ public class FriendController {
}
@RepeatSubmit
@PostMapping("/add")
@Operation(summary = "添加好友", description = "双方建立好友关系")
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;
import com.bx.implatform.annotation.RepeatSubmit;
import com.bx.implatform.result.Result;
import com.bx.implatform.result.ResultUtils;
import com.bx.implatform.service.GroupService;
@ -23,6 +24,7 @@ public class GroupController {
private final GroupService groupService;
@RepeatSubmit
@Operation(summary = "创建群聊", description = "创建群聊")
@PostMapping("/create")
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;
import com.bx.implatform.annotation.RepeatSubmit;
import com.bx.implatform.dto.LoginDTO;
import com.bx.implatform.dto.ModifyPwdDTO;
import com.bx.implatform.dto.RegisterDTO;
@ -13,7 +14,7 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@Tag(name = "注册登")
@Tag(name = "注册登")
@RestController
@RequiredArgsConstructor
public class LoginController {
@ -21,7 +22,7 @@ public class LoginController {
private final UserService userService;
@PostMapping("/login")
@Operation(summary = "用户登", description = "用户登")
@Operation(summary = "用户登", description = "用户登")
public Result<LoginVO> login(@Valid @RequestBody LoginDTO dto) {
LoginVO vo = userService.login(dto);
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
String token = request.getHeader("accessToken");
if (StrUtil.isEmpty(token)) {
log.error("未登,url:{}", request.getRequestURI());
log.error("未登,url:{}", request.getRequestURI());
throw new GlobalException(ResultCode.NO_LOGIN);
}
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) {
//验证 token
if (!JwtUtil.checkSign(refreshToken, jwtProperties.getRefreshTokenSecret())) {
throw new GlobalException("您的登陆信息已过期,请重新登陆");
throw new GlobalException("您的登录信息已过期,请重新登录");
}
String strJson = JwtUtil.getInfo(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;
@NotEmpty(message = "用户名不能为空")
@Length(max = 64, message = "用户名不能大于64字符")
@Length(max = 20, message = "用户名不能大于20字符")
@Schema(description = "用户名")
private String userName;
@NotEmpty(message = "用户昵称不能为空")
@Length(max = 64, message = "昵称不能大于64字符")
@Length(max = 20, message = "昵称不能大于20字符")
@Schema(description = "用户昵称")
private String nickName;
@ -30,7 +30,7 @@ public class UserVO {
@Schema(description = "用户类型 1:普通用户 2:审核账户")
private Integer type;
@Length(max = 1024, message = "个性签名不能大于1024个字符")
@Length(max = 128, message = "个性签名不能大于128个字符")
@Schema(description = "个性签名")
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<>();
sendInfo.setCmd(IMCmdType.FORCE_LOGUT.code());
sendInfo.setData("您已在其他地方登,将被强制下线");
sendInfo.setData("您已在其他地方登,将被强制下线");
context.channel().writeAndFlush(sendInfo);
log.info("异地登录,强制下线,userId:{}", userId);
}

20
im-uniapp/App.vue

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

19
im-uniapp/common/emotion.js

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

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

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

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

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

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

@ -16,7 +16,7 @@
<view class="chat-content">
<view class="chat-at-text">{{ atText }}</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" />
</view>
</view>

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

@ -17,13 +17,13 @@
<view class="chat-msg-bottom">
<view v-if="msgInfo.type == $enums.MESSAGE_TYPE.TEXT">
<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>
</view>
<view class="chat-msg-image" v-if="msgInfo.type == $enums.MESSAGE_TYPE.IMAGE">
<long-press-menu :items="menuItems" @select="onSelectMenu">
<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()">
</image>
<loading v-if="loading"></loading>
@ -256,6 +256,7 @@ export default {
color: $im-text-color-lighter;
font-size: $im-font-size-smaller;
line-height: $im-font-size-smaller;
height: $im-font-size-smaller;
}
.chat-msg-bottom {
@ -305,6 +306,7 @@ export default {
.send-image {
min-width: 200rpx;
max-width: 420rpx;
height: 350rpx;
cursor: pointer;
border-radius: 4px;
}

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

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

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

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

51
im-uniapp/im.scss

@ -28,6 +28,39 @@ uni-button[size='mini'] {
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'] {
color: #fff !important;
background-color: $im-color-primary-dark-1 !important;
@ -139,3 +172,21 @@ uni-button[size='mini'] {
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",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"alwaysShowBeforeRender" : false,
"waiting" : false,
"autoclose" : false,
"delay" : 0
},
/* */
@ -22,6 +22,9 @@
"Record" : {},
"Bluetooth" : {}
},
"softinput" : {
"mode" : "adjustResize"
},
/* */
"distribute" : {
/* android */
@ -45,11 +48,7 @@
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\" />",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\" />"
],
"abiFilters": [
"armeabi-v7a",
"arm64-v8a",
"x86"
],
"abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ],
"minSdkVersion" : 21
},
/* ios */
@ -132,3 +131,4 @@
}
}
/* ios *//* SDK */

1
im-uniapp/package.json

@ -4,7 +4,6 @@
"scripts": {}
},
"dependencies": {
"js-audio-recorder": "^1.0.7",
"pinyin-pro": "^3.23.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"
}
},
"pages": [
{
"pages": [{
"path": "pages/chat/chat"
}, {
"path": "pages/login/login"
},
{
"path": "pages/register/register"
},
{
"path": "pages/chat/chat"
},
{
"path": "pages/friend/friend"
},
@ -30,7 +28,14 @@
"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"
@ -71,8 +76,7 @@
"selectedColor": "#587ff0",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"list": [{
"pagePath": "pages/chat/chat",
"iconPath": "static/tarbar/chat.png",
"selectedIconPath": "static/tarbar/chat_active.png",

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

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

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

@ -13,7 +13,13 @@
<view class="user-item">
<head-image :id="user.id" :name="user.nickName" :online="user.online"
: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">
<button type="primary" v-show="!isFriend(user.id)" size="mini"
@click.stop="onAddFriend(user)">加为好友</button>
@ -90,22 +96,45 @@ export default {
overflow: hidden;
.user-item {
height: 120rpx;
height: 100rpx;
display: flex;
margin-bottom: 1rpx;
position: relative;
padding: 0 30rpx;
padding: 18rpx 20rpx;
align-items: center;
background-color: white;
white-space: nowrap;
.user-name {
.user-info {
flex: 1;
display: flex;
flex-direction: column;
padding-left: 20rpx;
font-size: $im-font-size;
line-height: 60rpx;
white-space: nowrap;
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>
<view v-if="userStore.userInfo.type == 1" class="page group-edit">
<view class="page group-edit">
<nav-bar back>修改群资料</nav-bar>
<uni-card :is-shadow="false" is-full :border="false">
<uni-forms ref="form" :modelValue="group" :rules="rules" validate-trigger="bind" label-position="top"
label-width="100%">
<uni-forms-item name="headImage" class="avatar">
<view class="form">
<view class="form-item">
<view class="label">群聊头像</view>
<view class="value"></view>
<image-upload v-if="isOwner" :onSuccess="onUnloadImageSuccess">
<image :src="group.headImageThumb" class="group-image"></image>
</image-upload>
<head-image v-if="!isOwner" :name="group.showGroupName" :url="group.headImageThumb"
:size="200"></head-image>
</uni-forms-item>
<uni-forms-item label="群聊名称" name="name" :required="true">
<uni-easyinput type="text" v-model="group.name" :disabled="!isOwner" placeholder="请输入群聊名称" />
</uni-forms-item>
<uni-forms-item label="群聊备注" name="remarkGroupName">
<uni-easyinput v-model="group.remarkGroupName" type="text" :placeholder="group.name" />
</uni-forms-item>
<uni-forms-item label="我在本群的昵称" name="remarkNickName">
<uni-easyinput v-model="group.remarkNickName" type="text"
:placeholder="userStore.userInfo.nickName" />
</uni-forms-item>
<uni-forms-item label="群公告" name="notice">
<uni-easyinput type="textarea" v-model="group.notice" :disabled="!isOwner" placeholder="请输入群公告" />
</uni-forms-item>
</uni-forms>
</uni-card>
<head-image v-else class="group-image" :name="group.showGroupName" :url="group.headImageThumb"
:size="120"></head-image>
</view>
<view class="form-item">
<view class="label">群聊名称</view>
<input class="input" :class="isOwner?'':'disable'" maxlength="20" v-model="group.name" :disabled="!isOwner" placeholder="请输入群聊名称"/>
</view>
<view class="form-item">
<view class="label">群聊备注</view>
<input class="input" maxlength="20" v-model="group.remarkGroupName" :placeholder="group.name"/>
</view>
<view class="form-item">
<view class="label">我在本群的昵称</view>
<input class="input" maxlength="20" v-model="group.remarkNickName" :placeholder="userStore.userInfo.nickName"/>
</view>
<view class="form-item">
<view class="label">群公告</view>
<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>
</view>
</template>
@ -46,6 +48,7 @@ export default {
}
}
},
methods: {
submit() {
if (this.group.id) {
@ -141,17 +144,54 @@ export default {
<style lang="scss" scoped>
.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 {
width: 200rpx;
height: 200rpx;
width: 120rpx;
height: 120rpx;
border-radius: 50%;
border: 1px solid #ccc;
border-radius: 5%;
}
}
.avatar {
margin-top: -30px;
}
}
</style>

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

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

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

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

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

@ -1,28 +1,35 @@
<template>
<view class="page mine-edit">
<nav-bar back>修改我的信息</nav-bar>
<uni-card :is-shadow="false" is-full :border="false">
<uni-forms ref="form" :modelValue="userInfo" label-position="top" label-width="100%">
<uni-forms-item name="headImage" class="avatar">
<image-upload :onSuccess="onUnloadImageSuccess">
<view class="form">
<view class="form-item">
<view class="label">头像</view>
<image-upload class="value" :onSuccess="onUnloadImageSuccess">
<image :src="userInfo.headImageThumb" class="head-image"></image>
</image-upload>
</uni-forms-item>
<uni-forms-item label="用户名" name="userName">
<uni-easyinput type="text" v-model="userInfo.userName" :disabled="true" />
</uni-forms-item>
<uni-forms-item label="昵称" name="nickName">
<uni-easyinput v-model="userInfo.nickName" type="text" :placeholder="userInfo.userName" />
</uni-forms-item>
<uni-forms-item label="性别" name="sex">
<uni-data-checkbox v-model="userInfo.sex"
:localdata="[{ text: '男', value: 0 }, { text: '女', value: 1 }]"></uni-data-checkbox>
</uni-forms-item>
<uni-forms-item label="签名" name="signature">
<uni-easyinput type="textarea" v-model="userInfo.signature" placeholder="编辑个性标签,展示我的独特态度" />
</uni-forms-item>
</uni-forms>
</uni-card>
</view>
<view class="form-item">
<view class="label">账号</view>
<view class="value">{{userInfo.userName}}</view>
</view>
<view class="form-item">
<view class="label">昵称</view>
<input class="input" maxlength="20" v-model="userInfo.nickName" placeholder="请输入您的昵称" />
</view>
<view class="form-item">
<view class="label">性别</view>
<radio-group class="radio-group" @change="onSexChange">
<radio class="radio" :value="0" :checked="userInfo.sex==0"></radio>
<radio class="radio" :value="1" :checked="userInfo.sex==1"></radio>
</radio-group>
</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>
</view>
</template>
@ -31,11 +38,12 @@
export default {
data() {
return {
signTextAlign: 'right',
userInfo: {}
}
},
methods: {
onSexchange(e) {
onSexChange(e) {
this.userInfo.sex = e.detail.value;
},
onUnloadImageSuccess(file, res) {
@ -53,10 +61,10 @@ export default {
title: "修改成功",
icon: 'none'
});
setTimeout(() => {
uni.navigateBack();
}, 1000);
})
},
onLineChange(e) {
this.signTextAlign = e.detail.lineCount > 1 ? "left" : "right";
}
},
onLoad() {
@ -69,13 +77,64 @@ export default {
<style scoped lang="scss">
.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;
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 {
margin-top: -30px;
.signature {
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>

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

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

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

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

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

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

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

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