Browse Source

uniapp 发送文件、消息删除、撤回

master
xsx 3 years ago
parent
commit
0cde490c87
  1. 4
      im-platform/src/main/java/com/bx/implatform/enums/MessageType.java
  2. 4
      im-platform/src/main/resources/db/db.sql
  3. BIN
      im-ui/public/favicon.ico
  4. 2
      im-ui/public/index.html
  5. BIN
      im-ui/public/logo.png
  6. 4
      im-ui/src/api/enums.js
  7. BIN
      im-ui/src/assets/logo.png
  8. 4
      im-ui/src/components/chat/ChatMessageItem.vue
  9. 7
      im-ui/src/store/chatStore.js
  10. 15
      im-uniapp/App.vue
  11. 4
      im-uniapp/common/enums.js
  12. 28
      im-uniapp/common/request.js
  13. 15
      im-uniapp/common/wssocket.js
  14. 156
      im-uniapp/components/chat-message-item/chat-message-item.vue
  15. 94
      im-uniapp/components/file-upload/file-upload.vue
  16. 15
      im-uniapp/components/image-upload/image-upload.vue
  17. 67
      im-uniapp/components/pop-menu/pop-menu.vue
  18. 5
      im-uniapp/manifest.json
  19. 191
      im-uniapp/pages/chat/chat-box.vue
  20. 2
      im-uniapp/pages/login/login.vue
  21. 14
      im-uniapp/static/icon/iconfont.css
  22. BIN
      im-uniapp/static/icon/iconfont.ttf
  23. BIN
      im-uniapp/static/logo.png
  24. 34
      im-uniapp/store/chatStore.js
  25. 4
      im-uniapp/store/groupStore.js
  26. 18
      im-uniapp/store/index.js
  27. 5
      im-uniapp/store/userStore.js

4
im-platform/src/main/java/com/bx/implatform/enums/MessageType.java

@ -4,8 +4,8 @@ package com.bx.implatform.enums;
public enum MessageType { public enum MessageType {
TEXT(0,"文字"), TEXT(0,"文字"),
FILE(1,"文件"), IMAGE(1,"图片"),
IMAGE(2,"图片"), FILE(2,"文件"),
AUDIO(3,"音频"), AUDIO(3,"音频"),
VIDEO(4,"视频"), VIDEO(4,"视频"),
RECALL(10,"撤回"), RECALL(10,"撤回"),

4
im-platform/src/main/resources/db/db.sql

@ -45,8 +45,8 @@ create table `im_group`(
`head_image_thumb` varchar(255) default '' comment '群头像缩略图', `head_image_thumb` varchar(255) default '' comment '群头像缩略图',
`notice` varchar(1024) default '' comment '群公告', `notice` varchar(1024) default '' comment '群公告',
`remark` varchar(255) default '' comment '群备注', `remark` varchar(255) default '' comment '群备注',
`deleted` tinyint(1) DEFAULT 0 comment '是否已删除', `deleted` tinyint(1) default 0 comment '是否已删除',
`created_time` datetime DEFAULT CURRENT_TIMESTAMP comment '创建时间' `created_time` datetime default CURRENT_TIMESTAMP comment '创建时间'
)ENGINE=InnoDB CHARSET=utf8mb3 comment ''; )ENGINE=InnoDB CHARSET=utf8mb3 comment '';
create table `im_group_member`( create table `im_group_member`(

BIN
im-ui/public/favicon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

2
im-ui/public/index.html

@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="icon" href="<%= BASE_URL %>logo.png">
<title>盒子IM</title> <title>盒子IM</title>
</head> </head>
<body> <body>

BIN
im-ui/public/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

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

@ -1,8 +1,8 @@
const MESSAGE_TYPE = { const MESSAGE_TYPE = {
TEXT: 0, TEXT: 0,
FILE:1, IMAGE:1,
IMAGE:2, FILE:2,
AUDIO:3, AUDIO:3,
VIDEO:4, VIDEO:4,
RECALL:10, RECALL:10,

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

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

@ -12,13 +12,13 @@
</div> </div>
<div class="chat-msg-bottom" @contextmenu.prevent="showRightMenu($event)"> <div class="chat-msg-bottom" @contextmenu.prevent="showRightMenu($event)">
<span class="chat-msg-text" v-if="msgInfo.type==$enums.MESSAGE_TYPE.TEXT" v-html="$emo.transform(msgInfo.content)"></span> <span class="chat-msg-text" v-if="msgInfo.type==$enums.MESSAGE_TYPE.TEXT" v-html="$emo.transform(msgInfo.content)"></span>
<div class="chat-msg-image" v-if="msgInfo.type==$enums.MESSAGE_TYPE.FILE"> <div class="chat-msg-image" v-if="msgInfo.type==$enums.MESSAGE_TYPE.IMAGE">
<div class="img-load-box" v-loading="loading" element-loading-text="上传中.." element-loading-background="rgba(0, 0, 0, 0.4)"> <div class="img-load-box" v-loading="loading" element-loading-text="上传中.." element-loading-background="rgba(0, 0, 0, 0.4)">
<img class="send-image" :src="JSON.parse(msgInfo.content).thumbUrl" @click="showFullImageBox()" /> <img class="send-image" :src="JSON.parse(msgInfo.content).thumbUrl" @click="showFullImageBox()" />
</div> </div>
<span title="发送失败" v-show="loadFail" @click="handleSendFail" class="send-fail el-icon-warning"></span> <span title="发送失败" v-show="loadFail" @click="handleSendFail" class="send-fail el-icon-warning"></span>
</div> </div>
<div class="chat-msg-file" v-if="msgInfo.type==$enums.MESSAGE_TYPE.IMAGE"> <div class="chat-msg-file" v-if="msgInfo.type==$enums.MESSAGE_TYPE.FILE">
<div class="chat-file-box" v-loading="loading"> <div class="chat-file-box" v-loading="loading">
<div class="chat-file-info"> <div class="chat-file-info">
<el-link class="chat-file-name" :underline="true" target="_blank" type="primary" :href="data.url">{{data.name}}</el-link> <el-link class="chat-file-name" :underline="true" target="_blank" type="primary" :href="data.url">{{data.name}}</el-link>

7
im-ui/src/store/chatStore.js

@ -1,3 +1,4 @@
import {MESSAGE_TYPE} from "../api/enums.js"
export default { export default {
state: { state: {
@ -85,11 +86,11 @@ export default {
} }
} }
// 插入新的数据 // 插入新的数据
if(msgInfo.type == 1){ if(msgInfo.type == MESSAGE_TYPE.IMAGE ){
chat.lastContent = "[图片]"; chat.lastContent = "[图片]";
}else if(msgInfo.type == 2){ }else if(msgInfo.type == MESSAGE_TYPE.FILE){
chat.lastContent = "[文件]"; chat.lastContent = "[文件]";
}else if(msgInfo.type == 3){ }else if(msgInfo.type == MESSAGE_TYPE.AUDIO){
chat.lastContent = "[语音]"; chat.lastContent = "[语音]";
}else{ }else{
chat.lastContent = msgInfo.content; chat.lastContent = msgInfo.content;

15
im-uniapp/App.vue

@ -11,16 +11,18 @@
} }
}, },
methods: { methods: {
init(loginInfo) { init() {
// //
store.dispatch("load").then(() => { store.dispatch("load").then(() => {
// websocket // websocket
this.initWebSocket(loginInfo); this.initWebSocket();
}).catch((e) => { }).catch((e) => {
console.log(e);
this.exit(); this.exit();
}) })
}, },
initWebSocket(loginInfo) { initWebSocket() {
let loginInfo = uni.getStorageSync("loginInfo")
let userId = store.state.userStore.userInfo.id; let userId = store.state.userStore.userInfo.id;
wsApi.createWebSocket(process.env.WS_URL, loginInfo.accessToken); wsApi.createWebSocket(process.env.WS_URL, loginInfo.accessToken);
wsApi.onopen(() => { wsApi.onopen(() => {
@ -129,7 +131,7 @@
console.log("exit"); console.log("exit");
wsApi.closeWebSocket(); wsApi.closeWebSocket();
uni.removeStorageSync("loginInfo"); uni.removeStorageSync("loginInfo");
uni.navigateTo({ uni.reLaunch({
url: "/pages/login/login" url: "/pages/login/login"
}) })
}, },
@ -142,10 +144,9 @@
}, },
onLaunch() { onLaunch() {
// //
let loginInfo = uni.getStorageSync("loginInfo"); if (uni.getStorageSync("loginInfo")) {
if (loginInfo) {
// //
this.init(loginInfo) this.init()
} else { } else {
// //
uni.navigateTo({ uni.navigateTo({

4
im-uniapp/common/enums.js

@ -1,8 +1,8 @@
const MESSAGE_TYPE = { const MESSAGE_TYPE = {
TEXT: 0, TEXT: 0,
FILE:1, IMAGE: 1,
IMAGE:2, FILE:2,
AUDIO:3, AUDIO:3,
VIDEO:4, VIDEO:4,
RECALL:10, RECALL:10,

28
im-uniapp/common/request.js

@ -24,28 +24,30 @@ const request = (options) => {
console.log("token失效,尝试重新获取") console.log("token失效,尝试重新获取")
if (isRefreshToken) { if (isRefreshToken) {
// 正在刷新token,把其他请求存起来 // 正在刷新token,把其他请求存起来
return new Promise(resolve => { requestList.push(() => {
requestList.push(() => { resolve(request(options))
resolve(request(options))
})
}) })
return;
} }
isRefreshToken = true; isRefreshToken = true;
// 发送请求, 进行刷新token操作, 获取新的token // 发送请求, 进行刷新token操作, 获取新的token
const res = await reqRefreshToken(loginInfo).catch((res) => { const res = await reqRefreshToken(loginInfo);
return navToLogin(); if (!res || res.data.code != 200) {
}).finally(()=>{
requestList.forEach(cb => cb());
requestList = []; requestList = [];
isRefreshToken = false; isRefreshToken = false;
}) console.log("刷新token失败")
if (res.data.code != 200) { navToLogin();
return navToLogin(); return;
} }
// 保存token
uni.setStorageSync("loginInfo", res.data.data); uni.setStorageSync("loginInfo", res.data.data);
requestList.forEach(cb => cb());
requestList = [];
isRefreshToken = false;
// 保存token
console.log(res.data.data.accessToken)
// 重新发送刚才的请求 // 重新发送刚才的请求
return request(options) return resolve(request(options))
} else { } else {
uni.showToast({ uni.showToast({

15
im-uniapp/common/wssocket.js

@ -4,7 +4,7 @@ let messageCallBack = null;
let openCallBack = null; let openCallBack = null;
let isConnect = false; //连接标识 避免重复连接 let isConnect = false; //连接标识 避免重复连接
let hasLogin = false; let hasLogin = false;
let hasInit = false;
let createWebSocket = (url, token) => { let createWebSocket = (url, token) => {
wsurl = url; wsurl = url;
accessToken = token; accessToken = token;
@ -28,6 +28,11 @@ let initWebSocket = () => {
} }
}); });
// 不能绑定多次事件,不然多触发,即便之前已经调了uni.closeSocket
if(hasInit){
return;
}
hasInit = true;
uni.onSocketOpen((res) => { uni.onSocketOpen((res) => {
console.log("WebSocket连接已打开"); console.log("WebSocket连接已打开");
isConnect = true; isConnect = true;
@ -97,15 +102,19 @@ let closeWebSocket = () => {
resolve(); resolve();
return; return;
} }
console.log("关闭websocket连接");
uni.closeSocket({ uni.closeSocket({
code: 1000, code: 3000,
complete: (res) => { complete: (res) => {
console.log("关闭websocket连接");
hasLogin = false; hasLogin = false;
isConnect = false; isConnect = false;
resolve(); resolve();
},
fail:(e)=>{
console.log("关闭websocket连接失败",e);
} }
}) })
}) })

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

@ -2,40 +2,47 @@
<view class="chat-msg-item"> <view class="chat-msg-item">
<view class="chat-msg-tip" v-show="msgInfo.type==$enums.MESSAGE_TYPE.RECALL">{{msgInfo.content}}</view> <view class="chat-msg-tip" v-show="msgInfo.type==$enums.MESSAGE_TYPE.RECALL">{{msgInfo.content}}</view>
<view class="chat-msg-normal" v-show="msgInfo.type!=$enums.MESSAGE_TYPE.RECALL" :class="{'chat-msg-mine':msgInfo.selfSend}"> <view class="chat-msg-normal" v-show="msgInfo.type!=$enums.MESSAGE_TYPE.RECALL"
<view class="avatar"> :class="{'chat-msg-mine':msgInfo.selfSend}">
<view class="avatar" @click="onShowUserInfo(msgInfo.sendId)">
<image class="head-image" :src="headImage"></image> <image class="head-image" :src="headImage"></image>
</view> </view>
<view class="chat-msg-content"> <view class="chat-msg-content" @longpress="onShowMenu($event)">
<view class="chat-msg-top"> <view class="chat-msg-top">
<text>{{showName}}</text> <text>{{showName}}</text>
<chat-time :time="msgInfo.sendTime"></chat-time> <chat-time :time="msgInfo.sendTime"></chat-time>
</view> </view>
<view class="chat-msg-bottom"> <view class="chat-msg-bottom">
<rich-text class="chat-msg-text" v-if="msgInfo.type==$enums.MESSAGE_TYPE.TEXT" :nodes="$emo.transform(msgInfo.content)"></rich-text> <rich-text class="chat-msg-text" v-if="msgInfo.type==$enums.MESSAGE_TYPE.TEXT"
<view class="chat-msg-image" v-if="msgInfo.type==$enums.MESSAGE_TYPE.FILE"> :nodes="$emo.transform(msgInfo.content)"></rich-text>
<view class="img-load-box" > <view class="chat-msg-image" v-if="msgInfo.type==$enums.MESSAGE_TYPE.IMAGE">
<image class="send-image" :src="JSON.parse(msgInfo.content).thumbUrl" lazy-load="true" ></image> <view class="img-load-box">
<image class="send-image" :src="JSON.parse(msgInfo.content).thumbUrl" lazy-load="true"
@click.stop="onShowFullImage()">
</image>
<loading v-show="loading"></loading> <loading v-show="loading"></loading>
</view> </view>
<text title="发送失败" v-show="loadFail" class="send-fail iconfont icon-warning-circle-fill"></text> <text title="发送失败" v-show="loadFail" @click="onSendFail"
class="send-fail iconfont icon-warning-circle-fill"></text>
</view> </view>
<!--
<view class="chat-msg-file" v-if="msgInfo.type==$enums.MESSAGE_TYPE.IMAGE"> <view class="chat-msg-file" v-if="msgInfo.type==$enums.MESSAGE_TYPE.FILE">
<view class="chat-file-box" v-loading="loading"> <view class="chat-file-box">
<view class="chat-file-info"> <view class="chat-file-info">
<el-link class="chat-file-name" :underline="true" target="_blank" type="primary" :href="data.url">{{data.name}}</el-link> <uni-link class="chat-file-name" :text="data.name" showUnderLine="true" color="#007BFF"
:href="data.url"></uni-link>
<view class="chat-file-size">{{fileSize}}</view> <view class="chat-file-size">{{fileSize}}</view>
</view> </view>
<view class="chat-file-icon"> <view class="chat-file-icon iconfont icon-file"></view>
<text type="primary" class="el-icon-document"></text> <loading v-show="loading"></loading>
</view>
</view> </view>
<text title="发送失败" v-show="loadFail" @click="handleSendFail" class="send-fail el-icon-warning"></text> <text title="发送失败" v-show="loadFail" @click="onSendFail"
class="send-fail iconfont icon-warning-circle-fill"></text>
</view> </view>
<view class="chat-msg-voice" v-if="msgInfo.type==$enums.MESSAGE_TYPE.AUDIO" @click="handlePlayVoice()"> <!--
<view class="chat-msg-voice" v-if="msgInfo.type==$enums.MESSAGE_TYPE.AUDIO" @click="onPlayVoice()">
<audio controls :src="JSON.parse(msgInfo.content).url"></audio> <audio controls :src="JSON.parse(msgInfo.content).url"></audio>
</view> </view>
--> -->
@ -44,6 +51,8 @@
</view> </view>
</view> </view>
<pop-menu v-show="menu.show" :menu-style="menu.style" :items="menuItems" @close="menu.show=false"
@select="onSelectMenu"></pop-menu>
</view> </view>
</template> </template>
@ -62,36 +71,52 @@
msgInfo: { msgInfo: {
type: Object, type: Object,
required: true required: true
},
menu:{
type: Boolean,
default: true
} }
}, },
data() { data() {
return { return {
audioPlayState: 'STOP', audioPlayState: 'STOP',
rightMenu: { menu: {
show: false, show: false,
pos: { style: ""
x: 0,
y: 0
}
} }
} }
}, },
methods: { methods: {
handleSendFail() { onShowMenu(e) {
this.$message.error("该文件已发送失败,目前不支持自动重新发送,建议手动重新发送") uni.getSystemInfo({
success: (res) => {
let touches = e.touches[0];
let style = "";
/* 因 非H5端不兼容 style 属性绑定 Object ,所以拼接字符 */
if (touches.clientY > (res.windowHeight / 2)) {
style = `bottom:${res.windowHeight-touches.clientY}px;`;
} else {
style = `top:${touches.clientY}px;`;
}
if (touches.clientX > (res.windowWidth / 2)) {
style += `right:${res.windowWidth-touches.clientX}px;`;
} else {
style += `left:${touches.clientX}px;`;
}
this.menu.style = style;
//
this.$nextTick(() => {
this.menu.show = true;
});
}
})
}, },
showFullImageBox() { onSendFail() {
let imageUrl = JSON.parse(this.msgInfo.content).originUrl; uni.showToast({
if (imageUrl) { title: "该文件已发送失败,目前不支持自动重新发送,建议手动重新发送",
this.$store.commit('showFullImageBox', imageUrl); icon: "none"
} })
}, },
handlePlayVoice() { onPlayVoice() {
if (!this.audio) { if (!this.audio) {
this.audio = new Audio(); this.audio = new Audio();
} }
@ -99,26 +124,32 @@
this.audio.play(); this.audio.play();
this.handlePlayVoice = 'RUNNING'; this.handlePlayVoice = 'RUNNING';
}, },
showRightMenu(e) { onSelectMenu(item) {
this.rightMenu.pos = {
x: e.x,
y: e.y
};
this.rightMenu.show = "true";
},
handleSelectMenu(item) {
this.$emit(item.key.toLowerCase(), this.msgInfo); this.$emit(item.key.toLowerCase(), this.msgInfo);
this.menu.show = false;
},
onShowFullImage() {
let imageUrl = JSON.parse(this.msgInfo.content).originUrl;
uni.previewImage({
urls: [imageUrl]
})
},
onShowUserInfo(userId){
uni.navigateTo({
url: "/pages/common/user-info?id=" + userId
})
} }
}, },
computed: { computed: {
loading() { loading() {
return !this.isTimeout && this.msgInfo.loadStatus && this.msgInfo.loadStatus === "loading"; return !this.isTimeout && this.msgInfo.loadStatus && this.msgInfo.loadStatus === "loading";
}, },
loadFail() { loadFail() {
return this.msgInfo.loadStatus && (this.isTimeout || this.msgInfo.loadStatus === "fail"); return this.msgInfo.loadStatus && (this.isTimeout || this.msgInfo.loadStatus === "fail");
}, },
isTimeout(){ isTimeout() {
return (new Date().getTime() - new Date(this.msgInfo.sendTime).getTime()) > 30*1000; return (new Date().getTime() - new Date(this.msgInfo.sendTime).getTime()) > 30 * 1000;
}, },
data() { data() {
@ -139,29 +170,36 @@
items.push({ items.push({
key: 'DELETE', key: 'DELETE',
name: '删除', name: '删除',
icon: 'el-icon-delete' icon: 'trash'
}); });
if (this.msgInfo.selfSend && this.msgInfo.id > 0) { if (this.msgInfo.selfSend && this.msgInfo.id > 0) {
items.push({ items.push({
key: 'RECALL', key: 'RECALL',
name: '撤回', name: '撤回',
icon: 'el-icon-refresh-left' icon: 'refreshempty'
});
}
if (this.msgInfo.type == this.$enums.MESSAGE_TYPE.FILE) {
items.push({
key: 'DOWNLOAD',
name: '下载并打开',
icon: 'download'
}); });
} }
return items; return items;
} }
},
mounted() {
//console.log(this.msgInfo);
} }
} }
</script> </script>
<style lang="scss"> <style scoped lang="scss">
.chat-msg-item { .chat-msg-item {
padding: 20rpx; padding: 20rpx;
.chat-msg-tip { .chat-msg-tip {
line-height: 50px; line-height: 60rpx;
text-align: center;
} }
.chat-msg-normal { .chat-msg-normal {
@ -181,7 +219,7 @@
top: 0; top: 0;
left: 0; left: 0;
.head-image{ .head-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 5%; border-radius: 5%;
@ -243,7 +281,7 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
.img-load-box{ .img-load-box {
position: relative; position: relative;
.send-image { .send-image {
@ -273,10 +311,11 @@
cursor: pointer; cursor: pointer;
.chat-file-box { .chat-file-box {
position: relative;
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
align-items: center; align-items: center;
width: 20%; width: 65%;
min-height: 80px; min-height: 80px;
border: #dddddd solid 1px; border: #dddddd solid 1px;
border-radius: 3px; border-radius: 3px;
@ -293,20 +332,21 @@
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
margin-bottom: 15px; margin-bottom: 15px;
word-break: break-all;
} }
} }
.chat-file-icon { .chat-file-icon {
font-size: 50px; font-size: 80rpx;
color: #d42e07; color: #d42e07;
} }
} }
.send-fail { .send-fail {
color: #e60c0c; color: #e60c0c;
font-size: 30px; font-size: 50rpx;
cursor: pointer; cursor: pointer;
margin: 0 20px; margin: 0 20rpx;
} }
} }

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

@ -0,0 +1,94 @@
<template>
<view @click="selectAndUpload()">
<slot></slot>
</view>
</template>
<script>
export default {
name: "file-upload",
data() {
return {
uploadHeaders: {
"accessToken": uni.getStorageSync('loginInfo').accessToken
}
}
},
props: {
maxSize: {
type: Number,
default: 10*1024*1024
},
onBefore: {
type: Function,
default: null
},
onSuccess: {
type: Function,
default: null
},
onError: {
type: Function,
default: null
}
},
methods: {
selectAndUpload() {
let chooseFile = uni.chooseFile || uni.chooseMessageFile;
console.log(chooseFile)
chooseFile({
success: (res) => {
res.tempFiles.forEach((file) => {
//
if (this.maxSize && file.size > this.maxSize) {
this.$message.error(`文件大小不能超过 ${this.fileSizeStr}!`);
this.$emit("fail", file);
return;
}
if (!this.onBefore || this.onBefore(file)) {
//
this.uploadFile(file);
}
})
}
})
},
uploadFile(file) {
uni.uploadFile({
url: process.env.BASE_URL + '/file/upload',
header: {
accessToken: uni.getStorageSync("loginInfo").accessToken
},
filePath: file.path, //
name: 'file',
success: (res) => {
let data = JSON.parse(res.data);
if(data.code != 200){
this.onError && this.onError(file, data);
}else{
this.onSuccess && this.onSuccess(file, data);
}
},
fail: (err) => {
this.onError && this.onError(file, err);
}
})
}
},
computed: {
fileSizeStr() {
if (this.maxSize > 1024 * 1024) {
return Math.round(this.maxSize / 1024 / 1024) + "M";
}
if (this.maxSize > 1024) {
return Math.round(this.maxSize / 1024) + "KB";
}
return this.maxSize + "B";
}
}
}
</script>
<style>
</style>

15
im-uniapp/components/image-upload/image-upload.vue

@ -17,7 +17,11 @@
props: { props: {
maxSize: { maxSize: {
type: Number, type: Number,
default: null default: 5*1024*1024
},
sourceType:{
type: String,
default: 'album'
}, },
onBefore: { onBefore: {
type: Function, type: Function,
@ -34,14 +38,12 @@
}, },
methods: { methods: {
selectAndUpload() { selectAndUpload() {
console.log("selectAndUpload");
uni.chooseImage({ uni.chooseImage({
count: 9, //9 count: 9, //9
sourceType: ['album'], //album camera 使使 sourceType: [this.sourceType], //album camera 使使
sizeType: ['original'], //original compressed sizeType: ['original'], //original compressed
success: (res) => { success: (res) => {
res.tempFiles.forEach((file) => { res.tempFiles.forEach((file) => {
console.log("选择文件");
// //
if (this.maxSize && file.size > this.maxSize) { if (this.maxSize && file.size > this.maxSize) {
this.$message.error(`文件大小不能超过 ${this.fileSizeStr}!`); this.$message.error(`文件大小不能超过 ${this.fileSizeStr}!`);
@ -58,7 +60,6 @@
}) })
}, },
uploadImage(file) { uploadImage(file) {
console.log("上传文件")
uni.uploadFile({ uni.uploadFile({
url: process.env.BASE_URL + '/image/upload', url: process.env.BASE_URL + '/image/upload',
header: { header: {
@ -71,13 +72,11 @@
if(data.code != 200){ if(data.code != 200){
this.onError && this.onError(file, data); this.onError && this.onError(file, data);
}else{ }else{
console.log("上传成功")
this.onSuccess && this.onSuccess(file, data); this.onSuccess && this.onSuccess(file, data);
} }
}, },
fail: (err) => { fail: (err) => {
console.log("上传失败") console.log(err);
console.log(this.onError)
this.onError && this.onError(file, err); this.onError && this.onError(file, err);
} }
}) })

67
im-uniapp/components/pop-menu/pop-menu.vue

@ -0,0 +1,67 @@
<template>
<view class="pop-menu" @tap="onClose()" @contextmenu.prevent="">
<view class="menu" :style="menuStyle">
<view class="menu-item" v-for="(item) in items" :key="item.key" @click.prevent="onSelectMenu(item)">
<uni-icons :type="item.icon" size="22"></uni-icons>
<text> {{item.name}}</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: "pop-menu",
data() {
return {}
},
props: {
menuStyle: {
type: String
},
items: {
type: Array
}
},
methods: {
onSelectMenu(item) {
this.$emit("select", item);
},
onClose() {
this.$emit("close");
}
}
}
</script>
<style lang="scss" scoped>
.pop-menu {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 9999;
}
.menu {
width: 250rpx;
position: fixed;
border: 1px solid #d0d0d0;
box-shadow: 0 0 20rpx gray;
background-color: #eeeeee;
.menu-item {
height: 30px;
line-height: 30px;
font-size: 20px;
display: flex;
padding: 10px;
align-items: center;
border-bottom: 1px solid #d0d0d0;
}
}
</style>

5
im-uniapp/manifest.json

@ -68,5 +68,8 @@
"uniStatistics" : { "uniStatistics" : {
"enable" : false "enable" : false
}, },
"vueVersion" : "3" "vueVersion" : "3",
"h5" : {
"title" : "盒子IM"
}
} }

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

@ -2,12 +2,13 @@
<view class=" page chat-box"> <view class=" page chat-box">
<view class="header"> <view class="header">
<text class="title">{{title}}</text> <text class="title">{{title}}</text>
<uni-icons class="btn-side" type="more-filled" size="30"></uni-icons> <uni-icons class="btn-side" type="more-filled" size="30" @click="onShowMore()"></uni-icons>
</view> </view>
<view class="chat-msg" @click="switchChatTabBox('none',true)"> <view class="chat-msg" @click="switchChatTabBox('none',true)">
<scroll-view class="scroll-box" scroll-y="true" :scroll-into-view="'chat-item-'+scrollMsgIdx"> <scroll-view class="scroll-box" scroll-y="true" :scroll-into-view="'chat-item-'+scrollMsgIdx">
<view v-for="(msgInfo,idx) in chat.messages" :key="idx"> <view v-for="(msgInfo,idx) in chat.messages" :key="idx">
<chat-message-item :headImage="headImage(msgInfo)" :showName="showName(msgInfo)" <chat-message-item :headImage="headImage(msgInfo)" :showName="showName(msgInfo)"
@recall="onRecallMessage" @delete="onDeleteMessage" @download="onDownloadFile"
:id="'chat-item-'+idx" :msgInfo="msgInfo"> :id="'chat-item-'+idx" :msgInfo="msgInfo">
</chat-message-item> </chat-message-item>
</view> </view>
@ -17,13 +18,11 @@
<view class="iconfont icon-voice-circle"></view> <view class="iconfont icon-voice-circle"></view>
<view class="send-text"> <view class="send-text">
<textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false" <textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false"
:adjust-position="false" @confirm="sendTextMessage()" :adjust-position="false" @confirm="sendTextMessage()" @keyboardheightchange="onKeyboardheightchange"
@keyboardheightchange="onKeyboardheightchange" confirm-type="send" @blur="onSendTextBlur" confirm-type="send" confirm-hold :hold-keyboard="true"></textarea>
confirm-hold :hold-keyboard="true"></textarea>
</view> </view>
<view class="iconfont icon-icon_emoji" @touchend.prevent="switchChatTabBox('emo',true)"></view> <view class="iconfont icon-icon_emoji" @click="switchChatTabBox('emo',true)"></view>
<view v-show="sendText==''" class="iconfont icon-add-circle" <view v-show="sendText==''" class="iconfont icon-add" @click="switchChatTabBox('tools',true)">
@touchend.prevent="switchChatTabBox('tools',true)">
</view> </view>
<button v-show="sendText!=''" class="btn-send" type="primary" @touchend.prevent="sendTextMessage()" <button v-show="sendText!=''" class="btn-send" type="primary" @touchend.prevent="sendTextMessage()"
size="mini">发送</button> size="mini">发送</button>
@ -32,16 +31,36 @@
<view class="chat-tab-bar" v-show="chatTabBox!='none' ||showKeyBoard " :style="{height:`${keyboardHeight}px`}"> <view class="chat-tab-bar" v-show="chatTabBox!='none' ||showKeyBoard " :style="{height:`${keyboardHeight}px`}">
<view v-if="chatTabBox == 'tools'" class="chat-tools"> <view v-if="chatTabBox == 'tools'" class="chat-tools">
<view class="chat-tools-item"> <view class="chat-tools-item">
<image-upload :onBefore="onUploadImageBefore" :onSuccess="onUploadImageSuccess" <image-upload sourceType="album" :onBefore="onUploadImageBefore" :onSuccess="onUploadImageSuccess"
:onError="onUploadImageFail"> :onError="onUploadImageFail">
<view class="tool-icon iconfont icon-picture"></view> <view class="tool-icon iconfont icon-picture"></view>
</image-upload> </image-upload>
<view class="tool-name">相册</view> <view class="tool-name">相册</view>
</view> </view>
<view class="chat-tools-item" v-for="(tool, idx) in tools" @click.stop="onClickTool(tool)"> <view class="chat-tools-item">
<view class="tool-icon iconfont" :class="tool.icon"></view> <image-upload sourceType="camera" :onBefore="onUploadImageBefore" :onSuccess="onUploadImageSuccess"
<view class="tool-name">{{ tool.name }}</view> :onError="onUploadImageFail">
<view class="tool-icon iconfont icon-camera"></view>
</image-upload>
<view class="tool-name">拍摄</view>
</view>
<view class="chat-tools-item">
<file-upload :onBefore="onUploadFileBefore" :onSuccess="onUploadFileSuccess"
:onError="onUploadFileFail">
<view class="tool-icon iconfont icon-folder"></view>
</file-upload>
<view class="tool-name">文件</view>
</view> </view>
<view class="chat-tools-item">
<view class="tool-icon iconfont icon-microphone"></view>
<view class="tool-name">语音输入</view>
</view>
<view class="chat-tools-item">
<view class="tool-icon iconfont icon-call"></view>
<view class="tool-name">呼叫</view>
</view>
</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">
@ -69,24 +88,7 @@
scrollMsgIdx: 0, // scrollMsgIdx: 0, //
chatTabBox: 'none', chatTabBox: 'none',
showKeyBoard: false, showKeyBoard: false,
keyboardHeight: 322, keyboardHeight: 322
tools: [{
name: "拍摄",
icon: "icon-camera"
},
{
name: "语音输入",
icon: "icon-microphone"
},
{
name: "文件",
icon: "icon-folder"
},
{
name: "呼叫",
icon: "icon-call"
}
]
} }
}, },
methods: { methods: {
@ -151,7 +153,7 @@
} }
}, },
scrollToMsgIdx(idx) { scrollToMsgIdx(idx) {
// scrollMsgIdx // scrollMsgIdx
if (idx == this.scrollMsgIdx && idx > 0) { if (idx == this.scrollMsgIdx && idx > 0) {
this.$nextTick(() => { this.$nextTick(() => {
// //
@ -168,7 +170,6 @@
}, },
switchChatTabBox(chatTabBox, hideKeyBoard) { switchChatTabBox(chatTabBox, hideKeyBoard) {
this.chatTabBox = chatTabBox; this.chatTabBox = chatTabBox;
this.scrollToBottom();
if (hideKeyBoard) { if (hideKeyBoard) {
uni.hideKeyboard(); uni.hideKeyboard();
} }
@ -176,7 +177,8 @@
selectEmoji(emoText) { selectEmoji(emoText) {
this.sendText += `#${emoText};`; this.sendText += `#${emoText};`;
}, },
onKeyboardheightchange(e) {; onKeyboardheightchange(e) {
;
if (e.detail.height > 0) { if (e.detail.height > 0) {
this.showKeyBoard = true; this.showKeyBoard = true;
this.switchChatTabBox('none', false) this.switchChatTabBox('none', false)
@ -185,9 +187,6 @@
this.showKeyBoard = false; this.showKeyBoard = false;
} }
}, },
onSendTextBlur() {
//this.switchChatTabBox("none")
},
onUploadImageBefore(file) { onUploadImageBefore(file) {
let data = { let data = {
originUrl: file.path, originUrl: file.path,
@ -200,7 +199,7 @@
content: JSON.stringify(data), content: JSON.stringify(data),
sendTime: new Date().getTime(), sendTime: new Date().getTime(),
selfSend: true, selfSend: true,
type: 1, type: this.$enums.MESSAGE_TYPE.IMAGE,
loadStatus: "loading" loadStatus: "loading"
} }
// id // id
@ -231,13 +230,121 @@
msgInfo.loadStatus = 'fail'; msgInfo.loadStatus = 'fail';
this.$store.commit("insertMessage", msgInfo); this.$store.commit("insertMessage", msgInfo);
}, },
onClickTool(tool) { onUploadFileBefore(file) {
switch (tool.name) { let data = {
case "相册": name: file.name,
break; size: file.size,
url: file.path
}
let msgInfo = {
id: 0,
sendId: this.mine.id,
content: JSON.stringify(data),
sendTime: new Date().getTime(),
selfSend: true,
type: this.$enums.MESSAGE_TYPE.FILE,
loadStatus: "loading"
}
// id
this.fillTargetId(msgInfo, this.chat.targetId);
//
this.$store.commit("insertMessage", msgInfo);
// file
file.msgInfo = msgInfo;
//
this.scrollToBottom();
return true;
},
onUploadFileSuccess(file, res) {
let data = {
name: file.name,
size: file.size,
url: res.data
}
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.content = JSON.stringify(data);
this.$http({
url: this.messageAction,
method: 'POST',
data: msgInfo
}).then((id) => {
msgInfo.loadStatus = 'ok';
msgInfo.id = id;
this.$store.commit("insertMessage", msgInfo);
})
},
onUploadFileFail(file, res) {
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.loadStatus = 'fail';
this.$store.commit("insertMessage", msgInfo);
},
onDeleteMessage(msgInfo) {
uni.showModal({
title: '删除消息',
content: '确认删除消息?',
success: (res) => {
if (!res.cancel) {
this.$store.commit("deleteMessage", msgInfo);
uni.showToast({
title: "删除成功",
icon: "none"
})
}
}
})
},
onRecallMessage(msgInfo) {
uni.showModal({
title: '撤回消息',
content: '确认撤回消息?',
success: (res) => {
if (!res.cancel) {
let url = `/message/${this.chat.type.toLowerCase()}/recall/${msgInfo.id}`
this.$http({
url: url,
method: 'DELETE'
}).then(() => {
msgInfo = JSON.parse(JSON.stringify(msgInfo));
msgInfo.type = this.$enums.MESSAGE_TYPE.RECALL;
msgInfo.content = '你撤回了一条消息';
this.$store.commit("insertMessage", msgInfo);
})
}
}
})
},
onDownloadFile(msgInfo) {
let url = JSON.parse(msgInfo.content).url;
uni.downloadFile({
url: url,
success(res) {
if (res.statusCode === 200) {
var filePath = encodeURI(res.tempFilePath);
uni.openDocument({
filePath: filePath,
showMenu: true
});
}
},
fail(e){
console.log(e);
uni.showToast({
title: "文件下载失败",
icon: "none"
})
}
});
},
onShowMore(){
if (this.chat.type == "GROUP") {
uni.navigateTo({
url: "/pages/group/group-info?id="+this.group.id
})
}else{
uni.navigateTo({
url: "/pages/common/user-info?id="+this.friend.id
})
} }
}, },
loadGroup(groupId) { loadGroup(groupId) {
this.$http({ this.$http({

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

@ -49,7 +49,7 @@
console.log("登录成功,自动跳转到聊天页面...") console.log("登录成功,自动跳转到聊天页面...")
uni.setStorageSync("loginInfo", data); uni.setStorageSync("loginInfo", data);
// App.vue // App.vue
getApp().init(data) getApp().init()
// //
uni.switchTab({ uni.switchTab({
url: "/pages/chat/chat" url: "/pages/chat/chat"

14
im-uniapp/static/icon/iconfont.css

@ -1,6 +1,6 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 4272106 */ font-family: "iconfont"; /* Project id 4272106 */
src: url('iconfont.ttf?t=1696173135884') format('truetype'); src: url('iconfont.ttf?t=1697301725830') format('truetype');
} }
.iconfont { .iconfont {
@ -11,6 +11,14 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-file:before {
content: "\e671";
}
.icon-add:before {
content: "\e66c";
}
.icon-warning-circle-fill:before { .icon-warning-circle-fill:before {
content: "\e848"; content: "\e848";
} }
@ -19,10 +27,6 @@
content: "\e93d"; content: "\e93d";
} }
.icon-add-circle:before {
content: "\e664";
}
.icon-camera:before { .icon-camera:before {
content: "\e600"; content: "\e600";
} }

BIN
im-uniapp/static/icon/iconfont.ttf

Binary file not shown.

BIN
im-uniapp/static/logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

34
im-uniapp/store/chatStore.js

@ -1,3 +1,6 @@
import {MESSAGE_TYPE} from '@/common/enums.js';
import userStore from './userStore';
export default { export default {
state: { state: {
@ -34,7 +37,7 @@ export default {
}; };
state.chats.unshift(chat); state.chats.unshift(chat);
} }
uni.setStorageSync("chats",state.chats); this.commit("saveToStorage");
}, },
activeChat(state, idx) { activeChat(state, idx) {
state.activeIndex = idx; state.activeIndex = idx;
@ -44,7 +47,7 @@ export default {
}, },
removeChat(state, idx) { removeChat(state, idx) {
state.chats.splice(idx, 1); state.chats.splice(idx, 1);
uni.setStorageSync("chats",state.chats); this.commit("saveToStorage");
}, },
removeGroupChat(state, groupId) { removeGroupChat(state, groupId) {
for (let idx in state.chats) { for (let idx in state.chats) {
@ -77,11 +80,11 @@ export default {
} }
} }
// 插入新的数据 // 插入新的数据
if(msgInfo.type == 1){ if(msgInfo.type == MESSAGE_TYPE.IMAGE){
chat.lastContent = "[图片]"; chat.lastContent = "[图片]";
}else if(msgInfo.type == 2){ }else if(msgInfo.type == MESSAGE_TYPE.FILE){
chat.lastContent = "[文件]"; chat.lastContent = "[文件]";
}else if(msgInfo.type == 3){ }else if(msgInfo.type == MESSAGE_TYPE.AUDIO){
chat.lastContent = "[语音]"; chat.lastContent = "[语音]";
}else{ }else{
chat.lastContent = msgInfo.content; chat.lastContent = msgInfo.content;
@ -99,18 +102,21 @@ export default {
for (let idx in chat.messages) { for (let idx in chat.messages) {
if(msgInfo.id && chat.messages[idx].id == msgInfo.id){ if(msgInfo.id && chat.messages[idx].id == msgInfo.id){
Object.assign(chat.messages[idx], msgInfo); Object.assign(chat.messages[idx], msgInfo);
this.commit("saveToStorage");
return; return;
} }
// 正在发送中的消息可能没有id,通过发送时间判断 // 正在发送中的消息可能没有id,通过发送时间判断
if(msgInfo.selfSend && chat.messages[idx].selfSend if(msgInfo.selfSend && chat.messages[idx].selfSend
&& chat.messages[idx].sendTime == msgInfo.sendTime){ && chat.messages[idx].sendTime == msgInfo.sendTime){
Object.assign(chat.messages[idx], msgInfo); Object.assign(chat.messages[idx], msgInfo);
this.commit("saveToStorage");
return; return;
} }
} }
// 新的消息 // 新的消息
chat.messages.push(msgInfo); chat.messages.push(msgInfo);
uni.setStorageSync("chats",state.chats); console.log(chat.unreadCount)
this.commit("saveToStorage");
}, },
deleteMessage(state, msgInfo){ deleteMessage(state, msgInfo){
@ -139,7 +145,7 @@ export default {
break; break;
} }
} }
uni.setStorageSync("chats",state.chats); this.commit("saveToStorage");
}, },
updateChatFromFriend(state, friend) { updateChatFromFriend(state, friend) {
for (let i in state.chats) { for (let i in state.chats) {
@ -150,7 +156,7 @@ export default {
break; break;
} }
} }
uni.setStorageSync("chats",state.chats); this.commit("saveToStorage");
}, },
updateChatFromGroup(state, group) { updateChatFromGroup(state, group) {
for (let i in state.chats) { for (let i in state.chats) {
@ -161,14 +167,22 @@ export default {
break; break;
} }
} }
uni.setStorageSync("chats",state.chats); this.commit("saveToStorage");
},
saveToStorage(state){
let userId = userStore.state.userInfo.id;
uni.setStorage({
key:"chats-"+userId,
data: state.chats
})
} }
}, },
actions:{ actions:{
loadChat(context) { loadChat(context) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let userId = userStore.state.userInfo.id;
uni.getStorage({ uni.getStorage({
key:"chats", key:"chats-"+userId,
success(res) { success(res) {
context.commit("setChats",res.data); context.commit("setChats",res.data);
resolve() resolve()

4
im-uniapp/store/groupStore.js

@ -44,8 +44,8 @@ export default {
}).then((groups) => { }).then((groups) => {
context.commit("setGroups", groups); context.commit("setGroups", groups);
resolve(); resolve();
}).catch(() => { }).catch((res) => {
reject(); reject(res);
}) })
}); });
} }

18
im-uniapp/store/index.js

@ -2,7 +2,9 @@ import chatStore from './chatStore.js';
import friendStore from './friendStore.js'; import friendStore from './friendStore.js';
import userStore from './userStore.js'; import userStore from './userStore.js';
import groupStore from './groupStore.js'; import groupStore from './groupStore.js';
import {createStore} from 'vuex'; import {
createStore
} from 'vuex';
const store = createStore({ const store = createStore({
modules: { modules: {
chatStore, chatStore,
@ -13,12 +15,14 @@ const store = createStore({
state: {}, state: {},
actions: { actions: {
load(context) { load(context) {
const promises = []; return this.dispatch("loadUser").then(() => {
promises.push(this.dispatch("loadUser")); const promises = [];
promises.push(this.dispatch("loadFriend")); promises.push(this.dispatch("loadFriend"));
promises.push(this.dispatch("loadGroup")); promises.push(this.dispatch("loadGroup"));
promises.push(this.dispatch("loadChat")); promises.push(this.dispatch("loadChat"));
return Promise.all(promises); return Promise.all(promises);
})
} }
}, },
strict: true strict: true

5
im-uniapp/store/userStore.js

@ -24,10 +24,11 @@ export default {
url: '/user/self', url: '/user/self',
method: 'GET' method: 'GET'
}).then((userInfo) => { }).then((userInfo) => {
console.log(userInfo)
context.commit("setUserInfo",userInfo); context.commit("setUserInfo",userInfo);
resolve(); resolve();
}).catch(()=>{ }).catch((res)=>{
reject(); reject(res);
}); });
}) })
} }

Loading…
Cancel
Save