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. 19
      im-uniapp/common/wssocket.js
  14. 182
      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. 4
      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. 38
      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 {
TEXT(0,"文字"),
FILE(1,"文件"),
IMAGE(2,"图片"),
IMAGE(1,"图片"),
FILE(2,"文件"),
AUDIO(3,"音频"),
VIDEO(4,"视频"),
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 '群头像缩略图',
`notice` varchar(1024) default '' comment '群公告',
`remark` varchar(255) default '' comment '群备注',
`deleted` tinyint(1) DEFAULT 0 comment '是否已删除',
`created_time` datetime DEFAULT CURRENT_TIMESTAMP comment '创建时间'
`deleted` tinyint(1) default 0 comment '是否已删除',
`created_time` datetime default CURRENT_TIMESTAMP comment '创建时间'
)ENGINE=InnoDB CHARSET=utf8mb3 comment '';
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 http-equiv="X-UA-Compatible" content="IE=edge">
<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>
</head>
<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 = {
TEXT: 0,
FILE:1,
IMAGE:2,
IMAGE:1,
FILE:2,
AUDIO:3,
VIDEO:4,
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 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>
<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)">
<img class="send-image" :src="JSON.parse(msgInfo.content).thumbUrl" @click="showFullImageBox()" />
</div>
<span title="发送失败" v-show="loadFail" @click="handleSendFail" class="send-fail el-icon-warning"></span>
</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-info">
<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 {
state: {
@ -85,11 +86,11 @@ export default {
}
}
// 插入新的数据
if(msgInfo.type == 1){
if(msgInfo.type == MESSAGE_TYPE.IMAGE ){
chat.lastContent = "[图片]";
}else if(msgInfo.type == 2){
}else if(msgInfo.type == MESSAGE_TYPE.FILE){
chat.lastContent = "[文件]";
}else if(msgInfo.type == 3){
}else if(msgInfo.type == MESSAGE_TYPE.AUDIO){
chat.lastContent = "[语音]";
}else{
chat.lastContent = msgInfo.content;

15
im-uniapp/App.vue

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

4
im-uniapp/common/enums.js

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

28
im-uniapp/common/request.js

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

19
im-uniapp/common/wssocket.js

@ -4,7 +4,7 @@ let messageCallBack = null;
let openCallBack = null;
let isConnect = false; //连接标识 避免重复连接
let hasLogin = false;
let hasInit = false;
let createWebSocket = (url, token) => {
wsurl = url;
accessToken = token;
@ -27,7 +27,12 @@ let initWebSocket = () => {
reConnect(); //如果无法连接上webSocket 那么重新连接!可能会因为服务器重新部署,或者短暂断网等导致无法创建连接
}
});
// 不能绑定多次事件,不然多触发,即便之前已经调了uni.closeSocket
if(hasInit){
return;
}
hasInit = true;
uni.onSocketOpen((res) => {
console.log("WebSocket连接已打开");
isConnect = true;
@ -77,7 +82,7 @@ let initWebSocket = () => {
showCancel: false,
})
})
};
//定义重连函数
@ -97,15 +102,19 @@ let closeWebSocket = () => {
resolve();
return;
}
console.log("关闭websocket连接");
uni.closeSocket({
code: 1000,
code: 3000,
complete: (res) => {
console.log("关闭websocket连接");
hasLogin = false;
isConnect = false;
resolve();
},
fail:(e)=>{
console.log("关闭websocket连接失败",e);
}
})
})

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

@ -1,49 +1,58 @@
<template>
<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-normal" v-show="msgInfo.type!=$enums.MESSAGE_TYPE.RECALL" :class="{'chat-msg-mine':msgInfo.selfSend}">
<view class="avatar">
<view class="chat-msg-normal" v-show="msgInfo.type!=$enums.MESSAGE_TYPE.RECALL"
:class="{'chat-msg-mine':msgInfo.selfSend}">
<view class="avatar" @click="onShowUserInfo(msgInfo.sendId)">
<image class="head-image" :src="headImage"></image>
</view>
<view class="chat-msg-content">
<view class="chat-msg-content" @longpress="onShowMenu($event)">
<view class="chat-msg-top">
<text>{{showName}}</text>
<chat-time :time="msgInfo.sendTime"></chat-time>
</view>
<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>
<view class="chat-msg-image" v-if="msgInfo.type==$enums.MESSAGE_TYPE.FILE">
<view class="img-load-box" >
<image class="send-image" :src="JSON.parse(msgInfo.content).thumbUrl" lazy-load="true" ></image>
<rich-text class="chat-msg-text" v-if="msgInfo.type==$enums.MESSAGE_TYPE.TEXT"
:nodes="$emo.transform(msgInfo.content)"></rich-text>
<view class="chat-msg-image" v-if="msgInfo.type==$enums.MESSAGE_TYPE.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>
</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 class="chat-msg-file" v-if="msgInfo.type==$enums.MESSAGE_TYPE.IMAGE">
<view class="chat-file-box" v-loading="loading">
<view class="chat-msg-file" v-if="msgInfo.type==$enums.MESSAGE_TYPE.FILE">
<view class="chat-file-box">
<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>
<view class="chat-file-icon">
<text type="primary" class="el-icon-document"></text>
</view>
<view class="chat-file-icon iconfont icon-file"></view>
<loading v-show="loading"></loading>
</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 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>
</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>
</template>
@ -62,36 +71,52 @@
msgInfo: {
type: Object,
required: true
},
menu:{
type: Boolean,
default: true
}
},
data() {
return {
audioPlayState: 'STOP',
rightMenu: {
menu: {
show: false,
pos: {
x: 0,
y: 0
}
style: ""
}
}
},
methods: {
handleSendFail() {
this.$message.error("该文件已发送失败,目前不支持自动重新发送,建议手动重新发送")
onShowMenu(e) {
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() {
let imageUrl = JSON.parse(this.msgInfo.content).originUrl;
if (imageUrl) {
this.$store.commit('showFullImageBox', imageUrl);
}
onSendFail() {
uni.showToast({
title: "该文件已发送失败,目前不支持自动重新发送,建议手动重新发送",
icon: "none"
})
},
handlePlayVoice() {
onPlayVoice() {
if (!this.audio) {
this.audio = new Audio();
}
@ -99,27 +124,33 @@
this.audio.play();
this.handlePlayVoice = 'RUNNING';
},
showRightMenu(e) {
this.rightMenu.pos = {
x: e.x,
y: e.y
};
this.rightMenu.show = "true";
},
handleSelectMenu(item) {
onSelectMenu(item) {
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: {
loading() {
return !this.isTimeout && this.msgInfo.loadStatus && this.msgInfo.loadStatus === "loading";
},
loadFail() {
return this.msgInfo.loadStatus && (this.isTimeout || this.msgInfo.loadStatus === "fail");
return this.msgInfo.loadStatus && (this.isTimeout || this.msgInfo.loadStatus === "fail");
},
isTimeout(){
return (new Date().getTime() - new Date(this.msgInfo.sendTime).getTime()) > 30*1000;
isTimeout() {
return (new Date().getTime() - new Date(this.msgInfo.sendTime).getTime()) > 30 * 1000;
},
data() {
return JSON.parse(this.msgInfo.content)
@ -139,29 +170,36 @@
items.push({
key: 'DELETE',
name: '删除',
icon: 'el-icon-delete'
icon: 'trash'
});
if (this.msgInfo.selfSend && this.msgInfo.id > 0) {
items.push({
key: 'RECALL',
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;
}
},
mounted() {
//console.log(this.msgInfo);
}
}
</script>
<style lang="scss">
<style scoped lang="scss">
.chat-msg-item {
padding: 20rpx;
.chat-msg-tip {
line-height: 50px;
line-height: 60rpx;
text-align: center;
}
.chat-msg-normal {
@ -180,15 +218,15 @@
height: 100rpx;
top: 0;
left: 0;
.head-image{
.head-image {
width: 100%;
height: 100%;
border-radius: 5%;
}
}
.chat-msg-content {
display: flex;
@ -221,7 +259,7 @@
font-size: 14px;
word-break: break-all;
white-space: pre-line;
&:after {
content: "";
position: absolute;
@ -236,16 +274,16 @@
}
}
.chat-msg-image {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
align-items: center;
.img-load-box{
.img-load-box {
position: relative;
.send-image {
min-width: 200rpx;
min-height: 150rpx;
@ -255,7 +293,7 @@
cursor: pointer;
}
}
.send-fail {
color: #e60c0c;
@ -273,10 +311,11 @@
cursor: pointer;
.chat-file-box {
position: relative;
display: flex;
flex-wrap: nowrap;
align-items: center;
width: 20%;
width: 65%;
min-height: 80px;
border: #dddddd solid 1px;
border-radius: 3px;
@ -293,20 +332,21 @@
font-size: 16px;
font-weight: 600;
margin-bottom: 15px;
word-break: break-all;
}
}
.chat-file-icon {
font-size: 50px;
font-size: 80rpx;
color: #d42e07;
}
}
.send-fail {
color: #e60c0c;
font-size: 30px;
font-size: 50rpx;
cursor: pointer;
margin: 0 20px;
margin: 0 20rpx;
}
}
@ -376,4 +416,4 @@
}
}
</style>
</style>

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: {
maxSize: {
type: Number,
default: null
default: 5*1024*1024
},
sourceType:{
type: String,
default: 'album'
},
onBefore: {
type: Function,
@ -34,14 +38,12 @@
},
methods: {
selectAndUpload() {
console.log("selectAndUpload");
uni.chooseImage({
count: 9, //9
sourceType: ['album'], //album camera 使使
sourceType: [this.sourceType], //album camera 使使
sizeType: ['original'], //original compressed
success: (res) => {
res.tempFiles.forEach((file) => {
console.log("选择文件");
//
if (this.maxSize && file.size > this.maxSize) {
this.$message.error(`文件大小不能超过 ${this.fileSizeStr}!`);
@ -58,7 +60,6 @@
})
},
uploadImage(file) {
console.log("上传文件")
uni.uploadFile({
url: process.env.BASE_URL + '/image/upload',
header: {
@ -71,13 +72,11 @@
if(data.code != 200){
this.onError && this.onError(file, data);
}else{
console.log("上传成功")
this.onSuccess && this.onSuccess(file, data);
}
},
fail: (err) => {
console.log("上传失败")
console.log(this.onError)
console.log(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" : {
"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="header">
<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 class="chat-msg" @click="switchChatTabBox('none',true)">
<scroll-view class="scroll-box" scroll-y="true" :scroll-into-view="'chat-item-'+scrollMsgIdx">
<view v-for="(msgInfo,idx) in chat.messages" :key="idx">
<chat-message-item :headImage="headImage(msgInfo)" :showName="showName(msgInfo)"
@recall="onRecallMessage" @delete="onDeleteMessage" @download="onDownloadFile"
:id="'chat-item-'+idx" :msgInfo="msgInfo">
</chat-message-item>
</view>
@ -17,13 +18,11 @@
<view class="iconfont icon-voice-circle"></view>
<view class="send-text">
<textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false"
:adjust-position="false" @confirm="sendTextMessage()"
@keyboardheightchange="onKeyboardheightchange" confirm-type="send" @blur="onSendTextBlur"
confirm-hold :hold-keyboard="true"></textarea>
:adjust-position="false" @confirm="sendTextMessage()" @keyboardheightchange="onKeyboardheightchange"
confirm-type="send" confirm-hold :hold-keyboard="true"></textarea>
</view>
<view class="iconfont icon-icon_emoji" @touchend.prevent="switchChatTabBox('emo',true)"></view>
<view v-show="sendText==''" class="iconfont icon-add-circle"
@touchend.prevent="switchChatTabBox('tools',true)">
<view class="iconfont icon-icon_emoji" @click="switchChatTabBox('emo',true)"></view>
<view v-show="sendText==''" class="iconfont icon-add" @click="switchChatTabBox('tools',true)">
</view>
<button v-show="sendText!=''" class="btn-send" type="primary" @touchend.prevent="sendTextMessage()"
size="mini">发送</button>
@ -32,16 +31,36 @@
<view class="chat-tab-bar" v-show="chatTabBox!='none' ||showKeyBoard " :style="{height:`${keyboardHeight}px`}">
<view v-if="chatTabBox == 'tools'" class="chat-tools">
<view class="chat-tools-item">
<image-upload :onBefore="onUploadImageBefore" :onSuccess="onUploadImageSuccess"
<image-upload sourceType="album" :onBefore="onUploadImageBefore" :onSuccess="onUploadImageSuccess"
:onError="onUploadImageFail">
<view class="tool-icon iconfont icon-picture"></view>
</image-upload>
<view class="tool-name">相册</view>
</view>
<view class="chat-tools-item" v-for="(tool, idx) in tools" @click.stop="onClickTool(tool)">
<view class="tool-icon iconfont" :class="tool.icon"></view>
<view class="tool-name">{{ tool.name }}</view>
<view class="chat-tools-item">
<image-upload sourceType="camera" :onBefore="onUploadImageBefore" :onSuccess="onUploadImageSuccess"
: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 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>
<scroll-view v-if="chatTabBox==='emo'" class="chat-emotion" scroll-y="true">
@ -69,24 +88,7 @@
scrollMsgIdx: 0, //
chatTabBox: 'none',
showKeyBoard: false,
keyboardHeight: 322,
tools: [{
name: "拍摄",
icon: "icon-camera"
},
{
name: "语音输入",
icon: "icon-microphone"
},
{
name: "文件",
icon: "icon-folder"
},
{
name: "呼叫",
icon: "icon-call"
}
]
keyboardHeight: 322
}
},
methods: {
@ -151,7 +153,7 @@
}
},
scrollToMsgIdx(idx) {
// scrollMsgIdx
// scrollMsgIdx
if (idx == this.scrollMsgIdx && idx > 0) {
this.$nextTick(() => {
//
@ -168,7 +170,6 @@
},
switchChatTabBox(chatTabBox, hideKeyBoard) {
this.chatTabBox = chatTabBox;
this.scrollToBottom();
if (hideKeyBoard) {
uni.hideKeyboard();
}
@ -176,7 +177,8 @@
selectEmoji(emoText) {
this.sendText += `#${emoText};`;
},
onKeyboardheightchange(e) {;
onKeyboardheightchange(e) {
;
if (e.detail.height > 0) {
this.showKeyBoard = true;
this.switchChatTabBox('none', false)
@ -185,9 +187,6 @@
this.showKeyBoard = false;
}
},
onSendTextBlur() {
//this.switchChatTabBox("none")
},
onUploadImageBefore(file) {
let data = {
originUrl: file.path,
@ -200,7 +199,7 @@
content: JSON.stringify(data),
sendTime: new Date().getTime(),
selfSend: true,
type: 1,
type: this.$enums.MESSAGE_TYPE.IMAGE,
loadStatus: "loading"
}
// id
@ -231,13 +230,121 @@
msgInfo.loadStatus = 'fail';
this.$store.commit("insertMessage", msgInfo);
},
onClickTool(tool) {
switch (tool.name) {
case "相册":
break;
onUploadFileBefore(file) {
let data = {
name: file.name,
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) {
this.$http({

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

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

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

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

38
im-uniapp/store/chatStore.js

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

4
im-uniapp/store/groupStore.js

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

18
im-uniapp/store/index.js

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

5
im-uniapp/store/userStore.js

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

Loading…
Cancel
Save