# 一、 服务器通讯发表情包图片及发图片功能
# 1. 先更新聊天类完整代码
将聊天类文件 /common/js/chatClass.js 代码更新一下(代码做了微调)
import {requestUrl} from '@/common/mixins/configData.js';
import {registerGuest} from '@/pages/loginCenter/visitor.js'; //导入具体的方法
import store from '@/store'; // 引入vuex store
class chatClass {
// 构造函数
constructor() {
// ws地址
this.url = '';
if (requestUrl.http.startsWith('http')) {
this.url = 'ws' + requestUrl.http.replace('http', '');
} else if (requestUrl.http.startsWith('https')) {
this.url = 'wss' + requestUrl.http.replace('https', '');
}
// 是否上线
this.isOnline = false;
// socketTask对象
this.chatSocket = null;
// 我的信息 vuex 或者从本地获取
let user = uni.getStorageSync('chatuser') ?
JSON.parse(uni.getStorageSync('chatuser')) : '';
this.user = user;
// 连接websocket
if (this.user && this.user.token) {
this.connectSocket();
}
// 心跳
this.heartbeatTimer = null;
//聊天对象信息
this.ToObject = false;
//消息页整个聊天列表数据的未读数
this.xiaoxiNoreadNum = 0;
}
// 连接websocket
connectSocket() {
this.chatSocket = uni.connectSocket({
// http://192.168.2.7:7001
// ws://192.168.2.7:7001/ws
// https://lesson07.51yrc.com
// wss://lesson07.51yrc.com/ws
url: this.url + `/ws?token=${this.user.token}`,
complete: () => {},
timeout: 10000, // 10秒超时
});
// 连接成功
this.chatSocket.onOpen(() => {
console.log('websocket连接成功');
// 调用方法
this.onOpen();
//启动心跳
this.heartbeatTimer = setInterval(() => {
if (this.chatSocket && this.chatSocket.readyState === 1) {
this.chatSocket.send(JSON.stringify({
type: 'ping'
}));
}
}, 25000); // 每隔25秒发送一次心跳
});
// 接收信息
this.chatSocket.onMessage(res => {
// 处理一下不同平台消息格式
try {
const data = typeof res.data === 'string' ?
JSON.parse(res.data) : res.data;
//调用方法
this.onMessage(data);
if (data.type === 'ping') {
// 响应心跳
this.chatSocket.send(JSON.stringify({
type: 'pong'
}));
}
} catch (e) {
console.error('接收信息错误', e);
}
});
// 断开连接
this.chatSocket.onClose(res => {
console.log('断开连接的原因', res);
clearInterval(this.heartbeatTimer); // 清除心跳
this.heartbeatTimer = null;
//调用方法
this.onClose();
// 尝试重新连接
setTimeout(() => {
console.log('断开连接尝试重新连接websocket');
//dispatch('initChatuserAction');
this.connectSocket();
}, 3000);
});
// 错误处理
this.chatSocket.onError(err => {
console.error('websocket 错误:', err);
clearInterval(this.heartbeatTimer); // 清除心跳
this.heartbeatTimer = null;
// 尝试重新连接
setTimeout(() => {
console.log('错误处理尝试重新连接websocket');
//dispatch('initChatuserAction');
this.connectSocket();
}, 5000);
});
}
// 连接成功
onOpen() {
// 用户上线
this.isOnline = true;
// 获取离线消息(不在线的时候别人或者群发的消息)
this.chatGetmessageOffLine();
}
// 断开连接
onClose() {
// 用户下线
this.isOnline = false;
this.chatSocket = null;
}
// 接收信息
onMessage(data) {
console.log('websocket接收信息', data);
// 处理接收到的消息
this.doMessage(data);
}
// 关闭链接
close() {
// 用户退出登录
// 调用socketTask 对象 close方法
this.chatSocket.close();
}
// 创建聊天对象信息
createChatToObject(arg) {
this.ToObject = arg;
console.log('聊天对象信息', this.ToObject);
}
// 销毁聊天对象信息
destroyChatToObject() {
this.ToObject = false;
}
// 页面发消息的格式和服务器要一致
formatSendMessage(args) {
return {
id: 0, // 自动生成 UUID,唯一id, 聊天记录id,方便撤回消息
from_avatar: this.user.avatar, // 发送者头像
from_name: this.user.nickname || this.user.username, // 发送者名称
from_id: this.user.id, // 发送者id
to_id: this.ToObject.id, // 接收者id
to_name: this.ToObject.name, // 接收者名称
to_avatar: this.ToObject.avatar, // 接收者头像
chatType: this.ToObject.chatType, // 聊天类型 单聊
type: args.type, // 消息类型
data: args.data, // 消息内容
options: args.options ? args.options : {}, // 其它参数
create_time: (new Date()).getTime(), // 创建时间
isremove: 0, // 0未撤回 1已撤回
// 发送状态 pending 发送中 success 成功 fail 失败
sendStatus: args.sendStatus ? args.sendStatus : 'pending',
}
}
// 发送消息(单聊)
sendmessage(msg) {
return new Promise((result, reject) => {
// 把发送的聊天信息存在本地
let { k } = this.addChatInfo(msg);
// 消息页的聊天列表更新一下
this.updateXiaoXiList(msg);
// 查看我是否在线websocket是否正常连接
if (!this.meIsOnline()) return reject('我掉线了');
// 发送消息
uni.$u.http.post(requestUrl.http + `/api/chat/socket/sendmessage`, {
sendto_id: this.ToObject.id,
chatType: this.ToObject.chatType, // 单聊 single 群聊 group
type: msg.type,
data: msg.data,
options: encodeURIComponent(JSON.stringify(msg.options)), // 选填
}, {
header: {
token: uni.getStorageSync('chatuser_token'),
}
}).then(res => {
console.log('发送消息到服务器结果res', res);
msg.id = res.data.data.id;
msg.sendStatus = 'success';
console.log('发送消息成功之后的msg', msg);
// 更新本地的历史记录 send不用传因为已经发送完成了
this.updateChatInfo(msg, k);
// 成功返回给页面
result(res);
}).catch(err => {
//console.log('发送消息到服务器失败', err);
msg.sendStatus = 'fail';
// 更新本地的历史记录 send不用传因为已经发送完成了
this.updateChatInfo(msg, k);
// 失败返回给页面
reject(err);
});
});
}
// 把聊天信息存在本地
addChatInfo(msg, isSend = true) {
// 存本地key值设计 chatDetail_我的id_单聊群聊_和谁聊接收人(个人还是群)id
// key:`chatDetail_${this.user.id}_${msg.chatType}_${xx}`
// 重点分析接收人id
// 如果是单聊则是用户id,如果是群聊,则是群id(群聊id放在消息to_id中)
let id = msg.chatType == 'single' ? (isSend ? msg.to_id : msg.from_id) : msg.to_id;
let key = `chatDetail_${this.user.id}_${msg.chatType}_${id}`;
// 先获取历史记录
let list = this.getChatInfo(key);
console.log('获取历史记录', list);
// 做个标识,方便之后拿具体某条历史消息
msg.k = 'k' + list.length;
// 将消息放入历史记录
list.push(msg);
// 重新存历史记录到本地(因为加了新消息)
uni.setStorageSync(key, JSON.stringify(list));
// 返回
return {
data: msg,
k: msg.k,
}
}
// 获取历史记录, 传key值则找指定聊天记录
getChatInfo(key = false) {
if (!key) {
// 没有传key 则找当前会话聊天记录
key = `chatDetail_${this.user.id}_${this.ToObject.chatType}_${this.ToObject.id}`;
}
// console.log('获取历史记录, 传key值则找指定聊天记录的key',key);
let list = uni.getStorageSync(key);
// console.log('获取历史记录得到的数据', list);
if (list) {
if (typeof list == 'string') {
list = JSON.parse(list);
}
} else {
list = [];
}
return list;
}
// 更新指定的历史记录信息(不急着更新可以异步)
async updateChatInfo(msg, k, isSend = true) {
// 获取原来的历史记录
// 存本地key值设计 chatDetail_我的id_单聊群聊_和谁聊接收人(个人还是群)id
// key:`chatDetail_${this.user.id}_${msg.chatType}_${xx}`
// 重点分析接收人id
// isSend = true 代表我是发送人from_id
// 接收人就是to_id
// 如果是单聊则是用户id,如果是群聊,则是群id(群聊id放在消息to_id中)
let id = msg.chatType == 'single' ? (isSend ? msg.to_id : msg.from_id) : msg.to_id; //接收人|群id
let key = `chatDetail_${this.user.id}_${msg.chatType}_${id}`;
console.log('更新指定的历史记录信息key', key);
// 先获取历史记录
let list = this.getChatInfo(key);
console.log('先获取历史记录', list);
// 根据标识k去查找要更新的历史记录
let index = list.findIndex(e => e.k == k);
// 没找到
if (index == -1) return;
// 找到了,修改指定消息
list[index] = msg;
// 改完之后,整个重新存一下
console.log('改完之后,整个重新存一下', key, list);
uni.setStorageSync(key, list);
}
// 删除指定的历史记录信息
deleteChatInfo(to_id, chatType){
return new Promise((resolve,reject) => {
let xiaoxiList = this.getXiaoXiList();
// 找到当前聊天
let index = xiaoxiList.findIndex(v => v.id == to_id && v.chatType == chatType);
if(index != -1){
// 找到了
// 删除这个历史记录
xiaoxiList.splice(index, 1);
// 处理完了之后,存储消息页列表本地历史信息
this.setXiaoXiList(xiaoxiList);
// 更新(获取)消息页,整个消息列表的未读数(不急可以异步执行)
this.updateXiaoXiListNoreadNum();
// 消息页,整个消息列表的数据也存入了vuex中
// 更新一下vuex中的消息列表的数据
uni.$emit('updateXiaoXiList', xiaoxiList);
// 执行后续操作
return resolve();
}
return reject();
});
}
// 清空指定的所有历史记录信息
//(如某个群所有聊天信息,与某个人的所有聊天信息)
clearChatInfo(to_id, chatType){
let key = `chatDetail_${this.user.id}_${chatType}_${to_id}`;
// 删除本地记录
uni.removeStorageSync(key);
console.log('清空与某个群或者某个人的所有聊天信息');
// 消息页数据重新处理
return new Promise((resolve,reject) => {
let xiaoxiList = this.getXiaoXiList();
// 找到当前聊天
let index = xiaoxiList.findIndex(v => v.id == to_id && v.chatType == chatType);
if(index != -1){
// 找到了
// 删除这个历史记录
xiaoxiList.splice(index, 1);
// 处理完了之后,存储消息页列表本地历史信息
this.setXiaoXiList(xiaoxiList);
// 更新(获取)消息页,整个消息列表的未读数(不急可以异步执行)
this.updateXiaoXiListNoreadNum();
// 消息页,整个消息列表的数据也存入了vuex中
// 更新一下vuex中的消息列表的数据
uni.$emit('updateXiaoXiList', xiaoxiList);
// 执行后续操作
return resolve();
}
return reject();
});
}
// 修改某个对话信息(单聊和群聊, 处理设置功能)
// [如:置顶、免打扰、是否展示昵称、是否提醒、是否确认进群等]
updateSomeOneChatItem(someone, updatedata){
return new Promise((resolve,reject) => {
let xiaoxiList = this.getXiaoXiList();
// 找到当前聊天
let index = xiaoxiList.findIndex(v => v.id == someone.id &&
v.chatType == someone.chatType);
if(index != -1){
// 找到了
console.log('传递过来的要更新的数据',updatedata);
// 更新数据
xiaoxiList[index] = {
...xiaoxiList[index],
// 重新赋值
istop: updatedata.istop, //置顶
nowarn: updatedata.nowarn, //免打扰
stongwarn: updatedata.stongwarn, //是否提醒
shownickname: updatedata.shownickname, //是否显示群成员昵称
};
// 处理完了之后,存储消息页列表本地历史信息
this.setXiaoXiList(xiaoxiList);
// 更新(获取)消息页,整个消息列表的未读数(不急可以异步执行)
// this.updateXiaoXiListNoreadNum();
// 消息页,整个消息列表的数据也存入了vuex中
// 更新一下vuex中的消息列表的数据
uni.$emit('updateXiaoXiList', xiaoxiList);
// 执行后续操作
return resolve(xiaoxiList[index]);
}
return reject();
});
}
// 消息页的聊天列表更新一下
updateXiaoXiList(msg, isSend = true) {
console.log('消息页最新的一条消息',msg);
// 获取消息页列表本地历史信息(消息页消息列表)
let list = this.getXiaoXiList();
console.log('获取消息页列表旧历史', list);
// 判断是不是正在和对方聊天,正在聊天页
// 如果正在聊天页,就没有必要更新消息页的列表了
let isCurrentChat = false; // 默认不在聊天页
// 消息页每条数据需要配合服务器的数据,(消息页消息列表)大概有这些字段
/*
{
// 单聊
id: `用户|群id`,
avatar: `用户|群头像`,
name: `用户|群昵称`,
chatType:'单聊|群聊',
update_time: '最新的时间',
data: '最新一条消息',
type:'最新一条消息类型',
noreadnum:'未读数',
istop:'置顶情况',
shownickname:'是否展示昵称',
nowarn:'消息免打扰',
stongwarn: '消息提醒'
// 群聊还有以下字段
user_id: '群管理员id',
remark:'群公告',
invite_confirm:'确认进群'
...
}
*/
// 重点处理上面的这几个字段
let id = 0; //接收人|群 id
let avatar = ''; //接收人|群 头像
let name = ''; // 接收人|群 称呼
// 先判断是单聊还是群聊
if (msg.chatType == 'single') {
//单聊
//先看聊天对象是否存在
/*
if(this.ToObject){
// 存在聊天对象则在聊天页根据isSend判断
// isSend为true 则我是发送者
// 则这条消息msg.to_id 就是接收人的id 与聊天人 this.ToObject.id
// 如果二者相等,则说明正在跟当前接收人聊天
// 那么消息页就不用更新当前聊天人的信息比如提示发了几条消息等
isCurrentChat = isSend ? this.ToObject.id === msg.to_id :
// 否则我不是发送者刚好反过来
this.ToObject.id === msg.from_id;
}else{
// 不存在聊天对象肯定就不在聊天页
isCurrentChat = false;
} */
isCurrentChat = this.ToObject ? isSend ? this.ToObject.id === msg.to_id :
this.ToObject.id === msg.from_id : false;
// 处理 接收人|群 id avatar name
id = isSend ? msg.to_id : msg.from_id;
avatar = isSend ? msg.to_avatar : msg.from_avatar;
name = isSend ? msg.to_name : msg.from_name;
} else if (msg.chatType == 'group') {
//群聊
//先看聊天对象是否存在
isCurrentChat = this.ToObject && this.ToObject.id === msg.to_id;
// 处理 接收人|群 id avatar name
id = msg.to_id ;
avatar = msg.to_avatar ;
name = msg.to_name ;
}
// 接下来看消息页消息列表是否存在跟当前聊天人的对话
let index = list.findIndex(v => {
// 查消息类型和接收人聊天人id
return v.chatType == msg.chatType && v.id == id;
});
// 最后把消息页最新聊天的最后一条消息展示处理一下
let data = typeof msg.data == 'string' ? JSON.parse(msg.data) : msg.data;
// 当发送消息是图片视频等,消息页列表最新聊天的最后一条消息显示[图片][视频]等
let datadesc = this.XiaoXiListAnyOneLastMsgFormat(data, msg, isSend);
// 字段noreadnum 未读消息数量判断
// isSend为true说明现在处于聊天页
// 处于聊天页或者聊天当中,那么消息页,聊天列表就没必要+1
let noreadnum = (isSend || isCurrentChat) ? 0 : 1;
// 看能不能查到跟当前聊天人的对话
if (index == -1) {
// 如果查不到,则新建一个跟当前聊天人的对话信息放到消息页列表最上面
// 新建对话信息
// 单聊
let chatItem = {
id: id,
avatar: avatar,
name: name,
chatType: msg.chatType,
update_time: (new Date()).getTime(),
data: data,
datadesc: datadesc,
type: msg.type,
noreadnum: noreadnum,
istop: false, //是否置顶
shownickname: 0, //是否显示昵称
nowarn: 0, //消息免打扰
stongwarn: 0, //是否提示来消息了
};
// 群聊
if (msg.chatType == 'group') {
console.log('群聊此时的消息处理', msg);
chatItem = {
...chatItem,
user_id: msg.group && msg.group.user_id ? msg.group.user_id : 0, //群主
remark: msg.group && msg.group.remark ? msg.group.remark : '', // 群公告
// 是否需要管理员确认才能进群 默认不需要0
invite_confirm: msg.group && msg.group.invite_confirm ? msg.group.invite_confirm : 0,
// 是否显示群成员昵称
shownickname: true, //群聊默认显示
}
}
// 放在最上面
list = [chatItem, ...list];
} else {
// 查到了,则更新消息页,消息列表中的这条对话信息让它是最新的
let findItem = list[index];
// 则更新以下内容:时间 内容 类型等等
findItem.update_time = (new Date()).getTime();
findItem.data = data;
findItem.datadesc = datadesc;
findItem.type = msg.type;
findItem.avatar = avatar;
findItem.name = name;
// 未读数更新
findItem.noreadnum += noreadnum;
console.log('查到了,则更新消息页最新一条消息', findItem);
// 把这条消息放在消息页,消息列表最上面
list = this.arrToFirst(list, index);
}
// 重新存一下 存储消息页列表本地历史信息
this.setXiaoXiList(list);
// 更新(获取)消息页,整个消息列表的未读数
this.updateXiaoXiListNoreadNum(list);
// 消息页,整个消息列表的数据也存入了vuex中
// 更新一下vuex中的消息列表的数据
uni.$emit('updateXiaoXiList', list);
// 最后返回
console.log('获取或更新消息页列表为最新数据', list);
return list;
}
// 当发送消息是图片视频等,消息页列表最新聊天的最后一条消息显示[图片][视频]等
XiaoXiListAnyOneLastMsgFormat(data, msg, isSend){
console.log('消息页显示[图片][视频]等的data处理数据',data);
// 显示到消息列表的新属性
let datadesc = ``;
switch(data.dataType){
case 'image':
if(data && data.otherData && data.otherData.type){
if(data.otherData.type == 'iconMenus'){
datadesc = `[表情]`;
if(data.otherData.typedata && data.otherData.typedata.name){
datadesc += `[${data.otherData.typedata.name}]`;
}
}else if(data.otherData.type == 'image'){
datadesc = `[图片]`;
}
}
break;
case 'audio':
datadesc = `[语音]`;
break;
case 'video':
datadesc = `[视频]`;
break;
case 'file':
datadesc = `[文件]`;
break;
case 'pdf':
datadesc = `[pdf文件]`;
break;
case 'docx':
datadesc = `[word文档]`;
break;
}
// 是否显示发送者
datadesc = isSend ? datadesc : `${msg.from_name}: ${datadesc}`;
console.log('消息页显示[图片][视频]等的显示数据',datadesc);
return datadesc;
}
// 更新(获取)消息页,整个消息列表的未读数(不急可以异步执行)
async updateXiaoXiListNoreadNum(list = false) {
// 获取消息页列表本地历史信息
list = !list ? this.getXiaoXiList() : list;
// 循环list里面的每一项把属性noreadnum相加一起
let num = 0;
list.forEach(v => {
num += v.noreadnum;
});
// 可在这里执行更新,或者赋值给实例属性在页面调用
// 实例属性:消息页整个聊天列表数据的未读数
this.xiaoxiNoreadNum = num;
console.log('消息页整个聊天列表数据的未读数:', num);
// 消息总未读数变化触发
uni.$emit('totalNoReadNum', num);
// 还可以返回
return num;
}
// 数组元素置顶
arrToFirst(arr, index) {
// 判断:因为等于0本来就在最上面
if (index != 0) {
arr.unshift(arr.splice(index, 1)[0]);
}
return arr;
}
// 获取消息页列表本地历史信息
getXiaoXiList() {
// 定义消息列表key,支持多用户切换
let key = 'chatlist_' + this.user.id;
let list = uni.getStorageSync(key);
return list ? JSON.parse(list) : [];
}
// 存储消息页列表本地历史信息
setXiaoXiList(list) {
// 定义消息列表key,支持多用户切换
let key = 'chatlist_' + this.user.id;
uni.setStorageSync(key, JSON.stringify(list));
}
// 查看我是否在线websocket是否正常连接
meIsOnline() {
if (!this.isOnline) {
// 我不在线可以提示我确认重新连接websocket
this.connectWebsocketcomfirm();
return false;
}
return true;
}
// 提示我确认重新连接websocket
connectWebsocketcomfirm(msdata = null, confirmCallback = false, cancelCallback = false) {
uni.showModal({
title: msdata && msdata.title ? msdata.title : '系统提示',
content: msdata && msdata.content ? msdata.content : '由于服务器或者网络原因,您已经掉线了,是否重新连接',
showCancel: true,
cancelText: msdata && msdata.cancelText ? msdata.cancelText : '取消',
confirmText: msdata && msdata.confirmText ? msdata.confirmText : '重新连接',
success: res => {
if (res.confirm) {
if(confirmCallback && typeof confirmCallback == 'function'){
confirmCallback();
}else{
this.connectSocket();
}
}else{
console.log('点了取消');
if(cancelCallback && typeof cancelCallback == 'function'){
cancelCallback();
}
}
},
});
}
// 处理接收到的消息
async doMessage(msg){
console.log('处理接收到的消息',msg);
if(msg.type == 'system'){
console.log('系统消息单独处理');
}else if(msg.type == 'singleChat'){
let res = msg.data;
// 把聊天信息存在本地
let { data } = this.addChatInfo(res, false);
// 消息页的聊天列表更新一下
this.updateXiaoXiList(data, false);
// 全局通知数据
uni.$emit('onMessage', data);
}
}
// 进入聊天页,将消息页当前聊天用户的未读数清零
async goChatPageUpdateXiaoXiNoReadNum(to_id, chatType){
let xiaoxiList = this.getXiaoXiList();
// 找到当前聊天
let index = xiaoxiList.findIndex(v => v.id == to_id && v.chatType == chatType);
if(index != -1){
// 找到了
xiaoxiList[index].noreadnum = 0;
// 修改完了之后,存储消息页列表本地历史信息
this.setXiaoXiList(xiaoxiList);
// 更新(获取)消息页,整个消息列表的未读数(不急可以异步执行)
this.updateXiaoXiListNoreadNum();
// 消息页,整个消息列表的数据也存入了vuex中
// 更新一下vuex中的消息列表的数据
uni.$emit('updateXiaoXiList', xiaoxiList);
}
}
// 聊天页设置相关信息获取
getChatPageSet(to_id, chatType){
let xiaoxiList = this.getXiaoXiList();
// 找到当前聊天
let index = xiaoxiList.findIndex(v => v.id == to_id && v.chatType == chatType);
if(index != -1){
// 找到了
return xiaoxiList[index];
}
return null;
}
// 获取离线消息(不在线的时候别人或者群发的消息)
chatGetmessageOffLine(){
uni.$u.http.post(requestUrl.http + `/api/chat/chatGetmessageOffLine`, {}, {
header: {
token: this.user.token,
},
}).then(res => {
console.log('服务器返回离线消息', res);
}).catch(err => {
console.log('服务器返回离线消息失败', err);
if(err.data && err.data.data == 'Token 令牌不合法!'){
if(this.user.role == 'visitor'){
console.log('游客如果token令牌错误,重新获取token并连接websocket');
this.registerGuestAndConnect();
}else if(this.user.role == 'user'){
console.log('登录用户token不正确,说明在的别的设备登录了,则清空本地登录信息,在换成游客模式');
store.dispatch('logoutAction', ()=>{
this.doRegisterGuest();
});
}
}
});
}
// 游客如果token令牌错误,重新获取token并连接websocket
async registerGuestAndConnect(){
try{
this.connectWebsocketcomfirm({
title:'来自系统的提示',
content:'您之前在其它的设备打开过现在已掉线,是否在本设备重新连接',
}, ()=>{
this.doRegisterGuest();
});
}catch(error){
console.error('游客获取token失败',error);
}
}
// 游客如果token令牌错误,重新获取token并连接websocket
async doRegisterGuest(){
const userData = await registerGuest(store);
console.log('游客重新获取token等信息', userData);
if(userData && userData.token){
// 更新用户信息
this.user = userData;
// 连接websocket
this.connectSocket();
}
}
}
export default chatClass;
# 2. 设置上传图片等文件是服务器还是阿里云OSS
在文件 /common/mixins/configData.js
...
// 设置上传图片等文件是服务器还是阿里云OSS
export const uploadfileSaveType = {
// 图片
image: 'myserver', // 服务器 myserver 阿里云 AliyunOSS
};
# 3. 上传图片等文件到服务器或者阿里云接口说明
- 具体使用查看接口说明:三十、上传图片等文件到服务器或者阿里云
- 后端文档说明:一、uni-app项目发送文件[图片视频等]到服务器或者阿里云OSS
# 4. 上传功能实现
在文件 /pages/chat/plusIconAction.js
import UniPermission from '@/common/mixins/uni_permission.js';
import {requestUrl, uploadfileSaveType} from '@/common/mixins/configData.js';
// H5端获取视频封面:videoPath 是视频地址
...
export default{
data(){
return {
...,
iconMenus:[
// { name:"微笑", icon:"/static/tabbar/index.png",
// iconType:"image", eventType:"smile" },
...
],
plusMenus:[ // 加号扩展菜单栏目
...
],
chatDataList:[
...
],
}
},
methods:{
//点击加号扩展菜单的某一项
...,
// 拍照片发送
...,
//选择相册照片发送
...,
//选择相册视频发送
...,
// 拍视频发送
...,
// 发照片:相册发照片| 拍照片
sendPhotoAlbumOrCamera(option){
uni.chooseImage({
...
success: (res) => {
console.log('选择照片res',res);
// 发送到服务器或者第三方云存储
// 页面效果渲染效果
this.uploadFile(res).then(urls => {
console.log('所有文件上传成功', urls);
// 这里不需要手动发送消息,因为uploadFile中已经处理
}).catch(error => {
console.error('上传失败', error);
});
},
fail: (err) => {
...
}
});
},
// 发视频 : 相册 | 拍摄
...,
// 发相册图片|发相册视频 权限申请
...,
//预览多张图片
...,
//发送消息
sendMessage(msgType, option = {}){
console.log('发送消息',msgType);
let msg = {
avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-07.png',
nickname: '小二哥',
user_id: 2,
chat_time: (new Date()).getTime(),
data: '',
type:msgType, //image,video
isremove:false,
};
switch (msgType){
case 'text':
msg.data = this.messageValue;
break;
case 'iconMenus':
console.log('iconMenus的数据',option);
msg.data = option.icon;
msg.dataType = option.iconType;
msg.otherData = {
type: 'iconMenus',
typedata: option,
}
break;
case 'image':
console.log('image的数据',option);
msg.data = option.path;
msg.dataType = 'image';
msg.otherData = {
type: 'image',
typedata: option,
}
break;
case 'audio':
...
break;
case 'video':
...
break;
}
// 组织一下消息格式和服务器一致
...
// console.log('发消息格式数据',serverMsg); return;
// 显示到页面上
...
// 发送消息的状态
...
// 拿到要发送消息的索引实际就是当前聊天列表的长度
...
// 在页面上显示
console.log('看一下在页面显示的数据',msg);
...
// 发给服务器消息
...
// 清空发送的内容然后还要滚动到底部
...
},
//格式化视频时长
...,
// 发送图片视频等文件到服务器或者云存储
async uploadFile(res){
console.log('图片上传原始数据',res);
const that = this;
const files = res.tempFiles || res.tempFilePaths.map(path => ({ path }));
// 存储所有上传任务
const uploadTasks = [];
const uploadedUrls = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
// 创建上传任务Promise
const task = new Promise((resolve, reject) => {
// 执行上传图片到服务器或者阿里云oss获取图片地址
if (uploadfileSaveType.image == 'myserver') {
console.log('图片上传到服务器');
// 构建formData
// let formData = new FormData();
// formData.append('file', file);
// 自定义文件路径
const imagepath = `chatImgs`;
// 上传
/*
const uploadTask = uni.uploadFile({
url: requestUrl.http + `/api/chat/uploadStreamSingleToServerDiy/${imagepath}`,
filePath: file.path,
name: 'file',
formData: {
token: that.me.token
},
header: {
'token': that.me.token
},
success: (uploadRes) => {
console.log('上传服务器的结果',uploadRes);
if (uploadRes.statusCode === 200) {
try {
const data = JSON.parse(uploadRes.data);
resolve(data.data.url);
} catch (e) {
reject(new Error('解析响应数据失败'));
}
} else {
reject(new Error('上传失败,状态码: ' + uploadRes.statusCode));
}
},
fail: (err) => {
reject(err);
}
});
// 监听上传进度
uploadTask.onProgressUpdate((res) => {
console.log('监听上传进度', res);
console.log('上传进度' + res.progress);
console.log('已经上传的数据长度' + res.totalBytesSent);
console.log('预期需要上传的数据总长度' + res.totalBytesExpectedToSend);
// 测试条件,取消上传任务。
// if (res.progress > 50) {
// uploadTask.abort();
// }
// 可以在这里更新UI显示上传进度
that.$set(file, 'progress', res.progress);
});
*/
this.uploadFileAjax(that, file, {
url: requestUrl.http + `/api/chat/uploadStreamSingleToServerDiy/${imagepath}`,
filePath: file.path,
name: 'file',
formData: {
token: that.me.token
},
header: {
'token': that.me.token
},
}, (progress)=>{
console.log(`第${i+1}个文件上传进度`, progress);
}).then(res =>{
console.log('图片上传到服务器的结果',res);
resolve(res.data.url);
}).catch(err =>{
console.log('图片上传到服务器的失败结果',err);
reject(err);
});
}else if (uploadfileSaveType.image == 'AliyunOSS') {
console.log('图片上传到阿里云oss');
// 构建formData
// let formData = new FormData();
// formData.append('img', file);
// 上传
this.uploadFileAjax(that, file, {
url: requestUrl.http + '/api/chat/uploadAliyun',
filePath: file.path,
name: 'img',
formData: {
token: that.me.token,
imageClassId: 0 // 添加必要的参数
},
header: {
'token': that.me.token
},
}, (progress)=>{
console.log(`第${i+1}个文件上传进度`, progress);
}).then(ossRes =>{
console.log('图片上传到阿里云的结果',ossRes);
if(ossRes && ossRes.data.length){
resolve(ossRes.data[0].url);
}
}).catch(err =>{
console.log('图片上传到阿里云的失败结果',err);
reject(err);
});
}
});
uploadTasks.push(task);
}
try {
// 等待所有文件上传完成
const urls = await Promise.all(uploadTasks);
//console.log('等待所有文件上传完成',urls);return;
// 所有文件上传成功
for (let i = 0; i < urls.length; i++) {
uploadedUrls.push(urls[i]);
// 发送消息到聊天
that.sendMessage('image', {
path: urls[i],
localPath: files[i].path // 保留本地路径用于显示
});
}
return uploadedUrls;
} catch (error) {
console.error('文件上传失败:', error);
uni.showToast({
title: '上传失败: ' + error.message,
icon: 'none',
duration: 3000
});
throw error;
}
},
// 发送图片视频等文件交互
uploadFileAjax(that, file, option, onProgress = false){
return new Promise((resolve,reject)=>{
// 上传
const uploadTask = uni.uploadFile({
url: option.url,
filePath: option.filePath,
name: option.name,
formData: option.formData,
header: option.header,
success: (uploadRes) => {
console.log('上传服务器的结果',uploadRes);
if (uploadRes.statusCode === 200) {
try {
const data = JSON.parse(uploadRes.data);
resolve(data);
} catch (e) {
reject(new Error('解析响应数据失败'));
}
} else {
reject(new Error('上传失败,状态码: ' + uploadRes.statusCode));
}
},
fail: (err) => {
reject(err);
}
});
// 监听上传进度
uploadTask.onProgressUpdate((res) => {
console.log('监听上传进度', res);
console.log('上传进度' + res.progress);
console.log('已经上传的数据长度' + res.totalBytesSent);
console.log('预期需要上传的数据总长度' + res.totalBytesExpectedToSend);
// 测试条件,取消上传任务。
// if (res.progress > 50) {
// uploadTask.abort();
// }
// 可以在这里更新UI显示上传进度
that.$set(file, 'progress', res.progress);
if(onProgress && typeof onProgress == 'function'){
onProgress(res.progress);
}
});
});
},
},
}
# 5. 页面渲染问题:聊天页处理
在页面 /pages/chat/chat.nvue
<template>
<view>
...
</view>
</template>
<script>
...
export default {
...,
methods: {
...mapMutations(['regSendMessage']),
// 接收的或者历史记录格式化成需要的聊天数据属性渲染页面
formatServerMsg(v){
console.log('接收的或者历史记录渲染到聊天页原始数据',v);
let evedata = typeof v.data == 'string' ?
JSON.parse(v.data) : v.data;
// 渲染到聊天页的数据
let chatdata = {
...v,
chat_time: v.create_time,
data: evedata.data,
dataType: evedata.dataType, // 新增属性
otherData: evedata.otherData, // 新增属性
type: v.type, //image,video
isremove:false,
avatar : v.from_avatar.startsWith('http') ? v.from_avatar :
requestUrl.http + v.from_avatar,
nickname : v.from_name,
user_id : v.from_id,
};
console.log('接收的或者历史记录渲染到聊天页最终数据', chatdata);
return chatdata;
},
...
},
...
}
</script>
# 6. 页面渲染问题:组件(/components/chat-item/chat-item.vue)
在组件 /components/chat-item/chat-item.vue
<template>
<view class="px-3">
...
</view>
</template>
<script>
...
export default{
...,
mounted() {
console.log('chat-item组件获取的item',this.item);
},
...
}
</script>
# 7. 页面渲染问题:组件(/components/chat-item-image/chat-item-image.vue)
在组件 /components/chat-item-image/chat-item-image.vue
<template>
<view>
...
<!-- 优化后的 最终方案兼容多端 -->
<image lazy-load :mode="imageMode"
:src="avatarShow"
:style="computedStyles[index]"
:class="imageClass"
@click="$emit('click')"
@load="good_handleImageLoad($event,item,index)"></image>
</view>
</template>
<script>
...
export default{
...
props:{
...
},
data(){
...
},
mounted() {
console.log('chat-item-image组件获取的item',this.item);
},
computed:{
// 处理通过服务器发送的图片
avatarShow(){
if(this.item && this.item.data){
return this.item.data.startsWith('http') ?
this.item.data : requestUrl.http + this.item.data;
}
return ``;
},
// 图片等比例展示
imageShowStyle(){
return `width:${this.width}px;height: ${this.height}px;`;
},
...
},
...
}
</script>
# 8. 消息页显示问题
在消息页组件 /components/chat-chatlist/chat-chatlist.vue
<template>
<view ...>
<!-- 头像 -->
...
<!-- 右边 -->
<view ...>
<!-- 上面:昵称 + 时间 -->
...
<!-- 下面:聊天内容 -->
<view class="pr-5">
<text class="text-light-muted u-line-1"
style="font-size: 24rpx;">{{showText}}</text>
</view>
</view>
</view>
</template>
<script>
...
export default{
...,
mounted() {
console.log('消息页chat-chatlist组件数据',this.item);
},
...,
computed:{
...,
// 昵称
...,
// 头像
...,
// 昵称下面的小字
showText(){
if(this.item.type == 'text'){
// 纯文本 和 头像文字
if(typeof this.item.data == 'string'){
return this.item.datadesc + this.item.data;
}else if(typeof this.item.data == 'object'){
return this.item.datadesc + this.item.data.data;
}
}else{
return this.item.datadesc;
}
},
}
}
</script>
# 二、 服务器通讯发视频功能(及发图片功能的改进)
说明:
如果有同学在微信小程序端用真机调试,发现图片(包括头像等)不显示,是因为微信真机调试需要配置域名白名单,具体配置方法可以参考微信小程序官方文档(后期我们上线小程序项目也会讲到),那么大家先用微信开发者工具调试。
阿里云OSS提供了视频封面截取功能,方便快速获取视频封面。
# 1. 阿里云OSS视频封面截取方法
https://thinkphp-eggjs.oss-cn-hangzhou.aliyuncs.com/images/20250826/xxxx_xxxx.mp4?x-oss-process=video/snapshot,t_20,m_fast,w_260,f_png# 2. 方法说明
参数 参数解释 单位说明 取值范围 t 截图时间(视频播放到那个时间的帧的画面) 单位:ms(毫秒) (视频时长范围内)[0,视频时长] m 截图模式,不指定则为默认模式,根据时间精确截图,如果指定为 fast则截取该时间点之前的最近的一个关键帧枚举值: fastw 截图宽度,如果指定为0则自动计算 单位:px(像素值) (视频宽度)[0,视频宽度] h 截图高度,如果指定为0则自动计算,如果w和h都为0则输出为原视频宽高 单位:px(像素值) (视频高度)[0,视频高度] f 输出截图图片格式 枚举值: jpg、png
- 服务器通讯发视频功能及多图发送功能比较复杂,需要分几节课进行讲解,大家每节课可以多听几遍,跟着敲代码。
# 1. 定义各种类型文件上传到哪里:是自己的服务器还是阿里云OSS
在文件 /common/mixins/configData.js
// 导出常量请求地址
...
// 游客注册的盐值
...
// 设置上传图片等文件是存到服务器还是阿里云oss
export const uploadfileSaveType = {
// 服务器 myserver 阿里云 AliyunOSS
// 图片
image:'AliyunOSS',
// 视频
video:'AliyunOSS',
};
# 2. 聊天页
在页面 /pages/chat/chat.nvue
<template>
<view>
...
</view>
</template>
<script>
...
export default {
...
methods: {
...
//点击聊天区域
scrollViewClick(){
// #ifdef APP
console.log('点击聊天区域');
this.KeyboardHeight = 0;
uni.hideKeyboard();
this.$refs.tooltipPlus.hide();
this.sendMessageMode = "text";
// #endif
},
...
},
}
</script>
# 3. 聊天页组件
在组件 /components/chat-item/chat-item.vue
<template>
<view class="px-3">
<!-- 时间 -->
...
<!-- 撤回消息 -->
...
<!-- 进群首条消息提示 -->
...
<!-- 聊天内容 -->
...
<!-- 给服务器发消息状态 -->
<view v-if="item.sendStatus && item.sendStatus != 'success' &&
item.sendStatus == 'pending'"
class="flex align-center justify-end pr-5 pb-5">
<text class="font-sm"
:class="[item.sendStatus == 'fail' ?
'text-danger' : 'text-success']">
{{item.sendStatus == 'fail' ? '消息发送失败' : '消息正在发送中...'}}
</text>
</view>
<!-- 弹出菜单 -->
...
</view>
</template>
<script>
...
export default{
...
mounted() {
console.log('chat-item组件获取的信息item', this.item);
},
...
}
</script>
# 4. 聊天页组件: 视频组件
在组件 /components/chat-item-video-poster/chat-item-video-poster.vue
<template>
<view>
<!-- <text class="font-sm">状态:{{item.sendStatus}}--进度:{{item.progress}}--数据:{{item}}--</text> -->
<!-- 视频封面占位 -->
<view v-if="item.otherData.showPoster"
class="position-relative" @click="openVideoShow">
<!-- 视频封面 -->
<chat-item-image :item="item" :index="index"
imageClass="rounded"
:maxWidth="300" :maxHeight="400"></chat-item-image>
<!-- 蒙版 -->
<view class="position-absolute left-0 right-0 top-0 bottom-0 rounded"
style="z-index: 98;"
:style="maskStyle"></view>
<!-- 视频时长 -->
<view
class="position-absolute left-0 right-0 top-0 bottom-0 rounded flex flex-row justify-end align-end mr-1 mb-1" style="z-index: 99;">
<text class="font-sm text-white">{{item.otherData.duration}}</text>
</view>
<!-- 上传进度和播放按钮处理 -->
<view class="position-absolute left-0 right-0 top-0 bottom-0 flex flex-row align-center justify-center"
style="z-index: 100;">
<!-- 上传进度 -->
<view v-if="item.progress"
class="mx-2 flex flex-column align-center justify-center">
<!-- 视频进度 -->
<view v-if="item.progress < 100"
class="flex flex-column align-center justify-center">
<text class="text-white font-sm">视频已完成处理</text>
<text class="text-white font mt-3">{{item.progress + '%'}}</text>
</view>
<!-- 封面处理中 -->
<text v-if="item.progress == 100"
class="text-white font-sm">视频消息处理中</text>
</view>
<!-- 播放按钮 -->
<view v-else
class="flex flex-row flex-1 align-center justify-center">
<text class="iconfont text-white"
:style="videoPlayIconStyle"></text>
</view>
</view>
</view>
<!-- 视频占位 -->
<view v-else
style="width: 200rpx;height: 350rpx;"
class="position-relative">
<video :src="item.otherData.videoData"
style="width: 200rpx;height: 350rpx;"
muted autoplay :controls="false"
object-fit="cover">
<!-- 遮罩 -->
<!-- #ifndef MP -->
<cover-view
class="position-absolute left-0 right-0 top-0 bottom-0 flex flex-column align-center justify-center"
style="z-index: 98;background-color: rgba(0, 0, 0, 0.7);"
@click="openVideoShow">
<!-- 上传进度 -->
<view v-if="item.progress"
class="mx-2 flex flex-column align-center justify-center">
<!-- 视频进度 -->
<view v-if="item.progress < 100"
class="flex flex-column align-center justify-center">
<text class="text-white font-sm">视频已完成处理</text>
<text class="text-white font mt-3">{{item.progress + '%'}}</text>
</view>
<!-- 封面处理中 -->
<text v-if="item.progress == 100"
class="text-white font-sm">视频消息处理中</text>
</view>
<!-- 播放按钮 -->
<view v-else
class="flex flex-row flex-1 align-center justify-center">
<text class="iconfont text-white"
:style="videoPlayIconStyle"></text>
</view>
</cover-view>
<!-- #endif -->
</video>
<!-- #ifdef MP -->
<view
class="position-absolute left-0 right-0 top-0 bottom-0 flex flex-column align-center justify-center"
style="z-index: 98;background-color: rgba(0, 0, 0, 0.7);"
@click="openVideoShow">
<!-- 上传进度 -->
<view v-if="item.progress"
class="mx-2 flex flex-column align-center justify-center">
<!-- 视频进度 -->
<view v-if="item.progress < 100"
class="flex flex-column align-center justify-center">
<text class="text-white font-sm">视频已完成处理</text>
<text class="text-white font mt-3">{{item.progress + '%'}}</text>
</view>
<!-- 封面处理中 -->
<text v-if="item.progress == 100"
class="text-white font-sm">视频消息处理中</text>
</view>
<!-- 播放按钮 -->
<view v-else
class="flex flex-row flex-1 align-center justify-center">
<text class="iconfont text-white"
:style="videoPlayIconStyle"></text>
</view>
</view>
<!-- #endif -->
</view>
</view>
</template>
<script>
export default{
...,
mounted() {
console.log('chat-item-video-poster组件获取的信息item', this.item);
},
computed:{
...,
// 蒙版样式
maskStyle(){
let opacity = this.item.progress ? 0.7 : 0.4;
return `background-color: rgba(0, 0, 0, ${opacity});`;
},
},
...
}
</script>
# 5. 聊天页组件:图片组件
在组件 /components/chat-item-image/chat-item-image.vue
<template>
<view class="position-relative">
<!-- 优化后的 最终方案兼容多端 -->
<image
lazy-load :mode="imageMode"
:src="avatarShow"
:style="computedStyles[index]"
:class="imageClass"
@click="$emit('click')"
@load="good_handleImageLoad($event,item,index)"></image>
<!-- 上传进度 -->
<view v-if="item.progress && item.type == 'image'"
class="position-absolute left-0 right-0 top-0 bottom-0 flex flex-row align-center justify-center"
style="z-index: 100;"
:style="maskStyle">
<!-- 上传进度 -->
<view class="mx-2 flex flex-column align-center justify-center">
<!-- 进度 -->
<view v-if="item.progress < 100"
class="flex flex-column align-center justify-center">
<text class="text-white font-sm">图片已完成处理</text>
<text class="text-white font mt-3">{{item.progress + '%'}}</text>
</view>
<!-- 处理中 -->
<text v-if="item.progress == 100"
class="text-white font-sm">图片消息处理中</text>
</view>
</view>
</view>
</template>
<script>
...
export default{
...,
mounted() {
console.log('chat-item-image组件获取的信息item', this.item);
},
computed:{
// 蒙版样式
maskStyle(){
let opacity = this.item.progress ? 0.7 : 0.1;
return `background-color: rgba(0, 0, 0, ${opacity});`;
},
// 处理通过服务器发送的图片
avatarShow(){
if(this.item && this.item.data){
if(this.item.type == 'image'){
return this.item.data.startsWith('/') ?
requestUrl.http + this.item.data : this.item.data;
}else{
return this.item.data;
}
}
return ``;
},
// 图片等比例展示
...
},
...
}
</script>
# 6. 发视频发多张图片处理
在文件 /pages/chat/plusIconAction.js
import UniPermission from '@/common/mixins/uni_permission.js';
import {requestUrl, uploadfileSaveType} from '@/common/mixins/configData.js';
// H5端获取视频封面:videoPath 是视频地址
...
export default{
data(){
return {
//属性
videoPlaceHolderPoster:'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/video/video_placeholder.jpg',
recorderIcon:'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/audio/audio-icon/recorder.gif',
iconMenus:[...],
// 加号扩展菜单栏目
plusMenus:[...],
chatDataList:[...],
}
},
methods:{
//点击加号扩展菜单的某一项
...,
// 拍照片发送
...,
//选择相册照片发送
...,
//选择相册视频发送
...,
// 拍视频发送
...,
// 发照片:相册发照片| 拍照片
sendPhotoAlbumOrCamera(option){
uni.chooseImage({
count:option.count,
//sizeType:['original','compressed'],
sourceType:[option.sourceType],
success: (res) => {
console.log('选择照片res',res);
// 发送到服务器或者第三方云存储阿里云oss,页面效果渲染效果
// const uploadedUrls = this.uploadFile(res, 'image');
// console.log('图片上传成功', uploadedUrls);
// 网速不好的时候,发照片需要时间,可以提出进度提示
// 那么需要先预览图片,给进度提示,当照片上传完成后显示及发消息
// 本地预览发一次消息【兼容多张照片】
// 拿到当前聊天页数据最后一条数据的索引
let lastIndex = this.chatDataList.length - 1;
// 处理图片文件
let files = res.tempFiles ||
(res.tempFilePaths && res.tempFilePaths.map(path => ({ path }))) || [];
// 兼容多张图片处理
for(let i = 0; i < files.length; i++){
const file = files[i];
const doRes = {
poster: file.path,
durationFormatted: 0,
showPoster: true,
};
this.sendMessage('image',{
poster: doRes.poster,
duration: doRes.durationFormatted,
showPoster: doRes.showPoster,
// 图片路径
videoPath: file.path,
// 是否跟服务器通讯 将这条消息发给其他人
isSendToServer: false,
// 文件进度 初始进度为0
progress: 0,
// 发送消息的状态[准备发文件]
sendStatus: 'pendingFile',
// 加一个path属性
path: file.path,
// 预览数据索引
previewFileIndex: lastIndex + (i + 1),
// 标记预览数据
isPreviewData: true,
}).then(previewFileIndex => {
// 拿到预览文件在聊天页的索引进行后续操作
console.log('拿到预览文件在聊天页的索引进行后续操作',
previewFileIndex);
// 创建一个只包含当前文件的res对象
const singleFileRes = {
tempFiles: [file],
tempFilePaths: [file.path]
};
// 接下来发送给服务器或者第三方云存储(如:阿里云oss)及后续处理
// 这个跟服务器和网络有关,网络越差时间越长:秒级
// 发图片--使用服务器返回的图片URL发送消息
this.uploadFile(singleFileRes, 'image', previewFileIndex)
.then(uploadedUrls =>{
console.log('图片上传成功', uploadedUrls);
// 获取图片及发websocket消息
// 图片路径判断
const ImagePath = uploadedUrls[0].startsWith('/') ?
requestUrl.http + uploadedUrls[0] : uploadedUrls[0];
// 发websockt消息
this.sendMsgToUsers(ImagePath, doRes, ImagePath,'image', previewFileIndex);
}).catch(err => {
console.error('图片上传失败', err);
// 上传失败,可能需要更新预览消息的状态为失败
this.chatDataList[previewFileIndex].sendStatus = 'fail';
});
}).catch(err => {
console.error('发送预览消息失败', err);
});
}
},
fail: (err) => {
console.error('选择图片失败:', err);
let errorMsg = '选择图片失败';
if(err.errMsg.includes('permission')){
errorMsg = '相册访问权限不足';
}else if(err.errMsg.includes('cancel')){
return; // 用户不授权不提示
}
uni.showToast({
title:errorMsg,icon:'none',duration:3000
});
}
});
},
// 发视频 : 相册 | 拍摄
async sendVideoAlbumOrCamera(option){
uni.chooseVideo({
sourceType:[option.sourceType],
// extension:['mp4'],
compressed:true,
maxDuration:60,
camera:'back',
success: async (res) => {
console.log('选择相册视频res',res);
if(res.tempFilePath){
// 初始时候选择视频处理各个平台视频封面时长等
const doRes = await this.VideoRender(res);
console.log('初始时候选择视频处理各个平台视频封面时长等', doRes);
// 发视频需要时间,视频发送给服务器过程中
// 本人的聊天页面显示发送进度封面等信息
// 本地预览发消息--这个很快毫秒级
// 视频封面(H5端封面在本地预览时候就可以获取封面数据)
// (小程序本地预览封面可以,但是发消息对方无法获取封面,因为是临时封面地址)
// (app要用插件)
// 拿到当前聊天页数据最后一条数据的索引
let lastIndex = this.chatDataList.length - 1;
// 本地预览发一次消息
this.sendMessage('video',{
poster: doRes.poster,
duration: doRes.durationFormatted,
showPoster: doRes.showPoster,
// 视频路径是本地视频
videoPath: res.tempFilePath,
// 是否跟服务器通讯 将这条消息发给其他人
isSendToServer: false,
// 文件进度 初始进度为0
progress: 0,
// 发送消息的状态[准备发文件]
sendStatus: 'pendingFile',
// 加一个path属性
path: doRes.poster,
// 预览数据索引
previewFileIndex: lastIndex + 1,
// 标记预览数据
isPreviewData: true,
}).then(previewFileIndex => {
// 拿到预览文件在聊天页的索引进行后续操作
console.log('拿到预览文件在聊天页的索引进行后续操作',
previewFileIndex);
// 接下来发送给服务器或者第三方云存储(如:阿里云oss)及后续处理
// 这个跟服务器和网络有关,网络越差时间越长:秒级
// 发视频--使用服务器返回的视频URL发送消息
this.uploadFile(res, 'video', previewFileIndex)
.then(uploadedUrls =>{
console.log('视频上传成功', uploadedUrls);
// 获取视频封面及发websocket消息
// 视频路径判断
const videoPath = uploadedUrls[0].startsWith('/') ?
requestUrl.http + uploadedUrls[0] : uploadedUrls[0];
// 视频封面主要是微信小程序和app获取较麻烦(前面开发页面时候讲过)
// 为了统一多端,这里在服务端获取视频封面
let videoPoster = ``;
// 如果视频上传到阿里云可以通过阿里云的接口获取视频封面
if(uploadfileSaveType.video == 'AliyunOSS'){
//通过阿里云的接口获取视频封面
videoPoster = videoPath +
`?x-oss-process=video/snapshot,t_20,m_fast,w_260,f_png`;
// 发websockt消息
this.sendMsgToUsers(videoPoster, doRes,
videoPath, 'video', previewFileIndex);
}
// 如果视频上传到服务器则需要服务器返回视频封面
if(uploadfileSaveType.video == 'myserver'){
console.log('视频上传到服务器服务器返回视频封面_视频地址',
videoPath);
// 获取视频封面
this.getVideoScreenshot(videoPath, 20, 260,'png')
.then(VideoScreen=>{
console.log('视频上传到服务器服务器返回视频封面_视频封面地址', VideoScreen);
videoPoster = VideoScreen && VideoScreen.startsWith('/') ?
requestUrl.http + VideoScreen : VideoScreen;
// 发websockt消息
this.sendMsgToUsers(videoPoster, doRes, videoPath,'video', previewFileIndex);
});
}
});
});
}
},
fail: (err) => {
console.error('选择视频失败:', err);
let errorMsg = '选择视频失败';
if(err.errMsg.includes('permission')){
errorMsg = '相册访问权限不足';
}else if(err.errMsg.includes('cancel')){
return; // 用户不授权不提示
}
uni.showToast({
title:errorMsg,icon:'none',duration:3000
});
}
});
},
// 发相册图片|发相册视频 权限申请
async handlePermission(options){
try{
...
if(granted){
console.log(options.permission.grantedText);
// 调用对应方法
this[options.methodsName]();
// 弹出框隐藏 -- 点击聊天区域一样的效果
this.clickPlusHidetooltip();
}else{
...
}
}catch(error){
...
}
},
//预览多张图片
...,
//发送消息
async sendMessage(msgType, option = {}){
return new Promise((resolve,reject)=>{
console.log('发送消息',msgType);
let msg = {
avatar: 'https://docs-51yrc-com.oss-cn-hangzhou.aliyuncs.com/chat/avatar-07.png',
nickname: '小二哥',
user_id: 2,
chat_time: (new Date()).getTime(),
data: '',
type:msgType, //image,video
isremove:false,
// 是否跟服务器通讯
isSendToServer: option.isSendToServer == false ? false : true,
// 上传进度字段
progress: option.progress !== undefined ?
option.progress : 0,
// 发送消息的状态
sendStatus: option.sendStatus !== undefined ?
option.sendStatus : 'pending',
// 预览数据索引
previewFileIndex: option.previewFileIndex !== undefined ?
option.previewFileIndex : undefined,
// 标记预览数据
isPreviewData: option.isPreviewData !== undefined ?
option.isPreviewData : undefined,
};
switch (msgType){
case 'text':
msg.data = this.messageValue;
break;
case 'iconMenus':
console.log('iconMenus的数据',option);
msg.data = option.icon;
msg.dataType = option.iconType;
msg.otherData = {
type: 'iconMenus',
typedata: option,
}
break;
case 'image':
console.log('image的数据',option);
msg.data = option.path;
msg.dataType = 'image';
msg.otherData = {
type: 'image',
typedata: option,
}
break;
case 'audio':
console.log('audio的数据',option);
msg.data = option.tempFilePath;
msg.dataType = 'audio';
msg.otherData = {
duration: Math.round(option.duration / 1000),
}
break;
case 'video':
console.log('video的数据',option);
msg.data = option.poster;
msg.dataType = 'video';
msg.otherData = {
duration:option.duration, // 视频时长
poster:option.poster,
videoData:option.videoPath,
showPoster:option.showPoster, // 是否显示封面
};
break;
}
// 组织一下消息格式和服务器一致
let serverMsg = this.chatClass.formatSendMessage({
type: msgType,
data:{
data: msg.data,
dataType: msg.dataType ? msg.dataType : false,
otherData: msg.otherData ? msg.otherData : null,
},
options: option,
});
// console.log('发消息格式数据',serverMsg); return;
// 显示到页面上
msg.avatar = serverMsg.from_avatar.startsWith('http') ?
serverMsg.from_avatar :
requestUrl.http + serverMsg.from_avatar;
msg.nickname = serverMsg.from_name;
msg.user_id = serverMsg.from_id;
// 发送消息的状态
// msg.sendStatus = 'pending';
// 拿到要发送消息的索引
let sendmsgIndex = -1;
if(msg.previewFileIndex != undefined &&
msg.previewFileIndex != -1){
// 如果是预览数据则替换预览数据
sendmsgIndex = msg.previewFileIndex;
// 替换的数据
let replaceData = {...serverMsg, ...msg};
console.log('-----如果是预览数据则替换之后的数据-------',
replaceData);
this.chatDataList.splice(sendmsgIndex,1,replaceData);
}else{
// 没有则是聊天列表长度
sendmsgIndex = this.chatDataList.length;
// 在页面上显示
this.chatDataList.push(msg);
}
// 发给服务器消息
if(msg.isSendToServer){
console.log('-----发给服务器websocket消息最终数据------',
serverMsg);
this.chatClass.sendmessage(serverMsg)
.then(result => {
console.log('页面接收服务器返回结果',result);
// 拿到刚发送的消息赋值
this.chatDataList[sendmsgIndex].id = result.data.data.id;
this.chatDataList[sendmsgIndex].sendStatus = 'success';
// 执行后续操作
resolve();
}).catch(error => {
console.log('页面接收服务器错误结果',error);
this.chatDataList[sendmsgIndex].sendStatus = 'fail';
// 返回错误
reject(error);
});
}else{
console.log('不发给服务器只在本地预览拿到当前预览数据', msg);
console.log('不发给服务器只在本地预览拿到当前预览文件的索引',
msg.previewFileIndex);
// 执行后续操作 对预览的文件显示进度
// 拿到当前预览文件的索引
resolve(msg.previewFileIndex);
}
// 清空发送的内容然后还要滚动到底部
if(msgType == 'text') this.messageValue = '';
this.chatContentToBottom();
});
},
//格式化视频时长
...,
// 发送图片视频等文件到服务器或者云存储
async uploadFile(res, filetype = 'image', previewFileIndex = null){
console.log('上传原始数据',res);
const that = this;
let files = [];
if(filetype == 'image'){
// 处理图片文件
files = res.tempFiles ||
(res.tempFilePaths &&
res.tempFilePaths.map(path => ({ path }))) || [];
}else if(filetype == 'video'){
// 视频文件:`tempFile`在H5端是File对象,在小程序端是类似File的对象
// 为了统一,使用tempFilePath 兼容多端
if (res.tempFilePath) {
files = [{
path: res.tempFilePath,
size: res.size,
duration: res.duration
}];
} else {
console.error('视频文件路径不存在');
throw new Error('视频文件路径不存在');
}
}
console.log('处理后的文件数组', files);
// 如果是单文件上传,直接处理
if(files.length === 1){
const file = files[0];
// 创建一个上传任务Promise
const task = new Promise((resolve,reject)=>{
// 根据配置选择上传方式
if(uploadfileSaveType.image == 'myserver' ||
uploadfileSaveType.video == 'myserver'){
// 自定义文件夹路径
let imagepath = ``;
if(uploadfileSaveType.image == 'myserver'){
imagepath = `chatImgs`;
}else if(uploadfileSaveType.video == 'myserver'){
imagepath = `chatVideos`;
}
//let imagepath = filetype === 'image' ? `chatImgs` : `chatVideos`;
// 文件上传到服务器自定义文件夹
this.uploadFileAjaxServer(0, imagepath, that,
file, previewFileIndex)
.then(uploadServer =>{
console.log('文件上传到服务器的结果', uploadServer);
resolve(uploadServer);
}).catch(reject);
}else if(uploadfileSaveType.image == 'AliyunOSS' ||
uploadfileSaveType.video == 'AliyunOSS'){
// 文件上传到阿里云OSS
this.uploadFileAjaxAliyunOSS(0, that,
file, previewFileIndex)
.then(uploadAliyunOSS => {
console.log('文件上传到阿里云OSS的结果', uploadAliyunOSS);
resolve(uploadAliyunOSS);
}).catch(reject);
}
});
try{
const url = await task;
return [url];
}catch(error){
console.log('文件上传失败:' , error);
throw error;
}
}else{
// 多文件处理逻辑(如果有需要)
// ...
}
},
async _xxx(){
// 存储所有上传的任务
const uploadTasks = [];
const uploadedUrls = [];
// 循环上传
for(let i=0; i < files.length; i++ ){
const file = files[i];
// 创建一个上传任务Promise
const task = new Promise((resolve,reject)=>{
// 上传文件到服务器
// 自定义文件夹路径
let imagepath = ``;
if(uploadfileSaveType.image == 'myserver'){
imagepath = `chatImgs`;
}else if(uploadfileSaveType.video == 'myserver'){
imagepath = `chatVideos`;
}
if(uploadfileSaveType.image == 'myserver' ||
uploadfileSaveType.video == 'myserver'){
// 文件上传到服务器自定义文件夹
this.uploadFileAjaxServer(i, imagepath, that,
file, previewFileIndex)
.then(uploadServer =>{
console.log('文件上传到服务器的结果', uploadServer);
resolve(uploadServer);
});
}else if(uploadfileSaveType.image == 'AliyunOSS' ||
uploadfileSaveType.video == 'AliyunOSS'){
// 文件上传到阿里云OSS
this.uploadFileAjaxAliyunOSS(i, that, file, previewFileIndex)
.then(uploadAliyunOSS => {
console.log('文件上传到阿里云OSS的结果', uploadAliyunOSS);
resolve(uploadAliyunOSS);
});
}
});
uploadTasks.push(task);
}
try{
// 等待所有文件上次完成
const urls = await Promise.all(uploadTasks);
console.log('等待所有文件上传完成', urls);
// 所有的文件上传成功
for(let i = 0; i < urls.length; i++){
uploadedUrls.push(urls[i]);
// 如果是图片,发送图片消息
// if (filetype === 'image') {
// this.sendMessage('image', {
// path: urls[i],
// localPath: files[i].path, // 保留本地路径用于显示
// });
// }
// 视频消息在sendVideoAlbumOrCamera中发送
}
return uploadedUrls;
}catch(error){
console.log('文件上传失败:' , error);
uni.showToast({
title: '文件上传:' + error.message,
icon:'none',
duration:3000
});
throw error;
}
},
// 文件上传到服务器自定义文件夹
uploadFileAjaxServer(i, imagepath, that, file, previewFileIndex){
return new Promise((resolve,reject)=>{
// 文件上传到服务器
this.uploadFileAjax(that, file, {
url: requestUrl.http +
`/api/chat/uploadStreamSingleToServerDiy/${imagepath}` ,
filePath: file.path,
name: 'file',
formData: {
token : that.me.token
},
header:{
'token': that.me.token
},
}, (progress)=>{
console.log(`第${i+1}个文件上传进度`, progress);
}, previewFileIndex).then(res=>{
console.log('图片(视频等)上传到服务器的结果',res);
resolve(res.data.url);
}).catch(err =>{
console.log('图片(视频等)上传到服务器失败结果',err);
reject(err);
});
});
},
// 文件上传到阿里云OSS
uploadFileAjaxAliyunOSS(i, that, file, previewFileIndex){
return new Promise((resolve, reject)=>{
// 文件上传到阿里云OSS
this.uploadFileAjax(that, file, {
url: requestUrl.http + `/api/chat/uploadAliyun` ,
filePath: file.path,
name: 'img',
formData: {
token : that.me.token,
imageClassId: 0
},
header:{
'token': that.me.token
},
}, (progress)=>{
console.log(`第${i+1}个文件上传进度`, progress);
}, previewFileIndex).then(ossRes =>{
console.log('图片(视频等)上传阿里云的结果',ossRes);
if(ossRes && ossRes.data.length){
resolve(ossRes.data[0].url);
}else {
reject(new Error('上传阿里云返回数据格式错误'));
}
}).catch(err =>{
console.log('图片(视频等)上传阿里云失败结果',err);
reject(err);
});
});
},
// 专门上传文件的方法
uploadFileAjax(that, file, option,
onProgress = false, previewFileIndex = null){
return new Promise((resolve,reject)=>{
// 上传
const uploadTask = uni.uploadFile({
url: option.url ,
filePath: option.filePath,
name: option.name,
formData: option.formData,
header: option.header,
success: (uploadRes) => {
console.log('上传服务器的结果',uploadRes);
if(uploadRes.statusCode == 200){
try{
const data = JSON.parse(uploadRes.data);
resolve(data);
}catch(e){
reject(new Error('解析响应数据失败'));
}
}else{
reject(new Error('上传失败,状态码:' + uploadRes.statusCode));
}
},
fail: (err) => {
reject(err);
}
});
// 监听上传进度
uploadTask.onProgressUpdate((res) => {
console.log('上传进度' + res.progress);
console.log('已经上传的数据长度' + res.totalBytesSent);
console.log('预期需要上传的数据总长度' + res.totalBytesExpectedToSend);
// 测试条件,取消上传任务。
// if (res.progress > 50) {
// uploadTask.abort();
// }
// 上传进度更新到页面
that.$set(file, 'progress', res.progress);
if(onProgress && typeof onProgress == 'function'){
onProgress(res.progress);
}
// 如果有消息索引,更新对应消息的进度
if (previewFileIndex !== null &&
that.chatDataList[previewFileIndex]) {
that.$set(that.chatDataList[previewFileIndex],
'progress', res.progress);
console.log(`索引${previewFileIndex}的对话上传简单`,
res.progress);
}
});
});
},
// 初始时候选择视频处理各个平台视频封面时长等
async VideoRender(res){
// 数据处理
let poster = '';
let durationFormatted = '';
let showPoster = false; // 是否显示封面
// #ifdef H5
try{
// h5封面
poster = await getH5VideoThumbnail(res.tempFilePath);
durationFormatted = this.formatVideoDuration(res.duration);
showPoster = true;
}catch(e){
console.log('H5获取封面失败',e);
poster = this.videoPlaceHolderPoster;
durationFormatted = '0:00';
}
// #endif
// #ifdef MP
try{
// 小程序封面
poster = res.thumbTempFilePath;
durationFormatted = this.formatVideoDuration(res.duration);
showPoster = poster ? true : false;
}catch(e){
console.log('小程序获取封面失败',e);
poster = this.videoPlaceHolderPoster;
durationFormatted = '0:00';
}
// #endif
// #ifdef APP
try{
// APP封面
//引入插件
// if(uni.getSystemInfoSync().platform === "android"){
// // 安卓手机获取封面
// const plug=uni.requireNativePlugin("Html5app-VideoCover");
// plug.setVideoPath({
// 'url':res.tempFilePath,
// 'time':1,
// },ret=>{
// console.log('安卓手机获取封面的结果',ret);
// });
// }
poster = this.videoPlaceHolderPoster;
durationFormatted = this.formatVideoDuration(res.duration);
}catch(e){
console.log('APP封面获取失败',e);
poster = this.videoPlaceHolderPoster;
durationFormatted = '0:00';
}
// #endif
return {
poster,
durationFormatted,
showPoster,
}
},
// 如果视频上传到服务器则需要服务器返回视频封面
async getVideoScreenshot(videoPath, time = 20,
width = 260, format = 'png'){
return new Promise((resolve,reject) =>{
uni.$u.http.post(requestUrl.http +
`/api/chat/getVideoScreenshot`, {
videoUrl: videoPath, //视频地址
time: time || 20, //毫秒
width: width || 260, //视频截图图片宽 px
format: format || 'png', //输出格式默认png|jpg
} , {
header: {
token: this.me.token,
},
}).then(res => {
console.log('服务器返回视频截图结果', res);
// return res.data.data.base64;
resolve(res.data.data.url);
}).catch(err=>{
console.log('服务器返回视频截图错误结果', err);
reject(err);
});
});
},
// 发websockt消息
sendMsgToUsers(poster, doRes, filePath ,
type = 'video', previewFileIndex = -1){
// 根据情况定义otherData的值
let otherData = {
poster: poster || doRes.poster,
duration: type === 'video' ? doRes.durationFormatted : 0,
showPoster: poster ? true : doRes.showPoster,
path: filePath,
};
if(type === 'video'){
otherData = {
...otherData,
videoData: filePath,
};
}else if(type === 'image'){
otherData = {
...otherData,
imageData: filePath,
};
}
// 发websockt消息
console.log('-----发websockt消息------索引',
previewFileIndex);
this.sendMessage(type,{
...otherData,
// 视频路径是服务器返回的URL
videoPath: filePath,
// 是否跟服务器通讯 将这条消息发给其他人
isSendToServer: true,
// 文件进度 上传完成之后重置到0
progress: 0,
// 发送消息的状态[准备发websocket]
sendStatus: 'pending',
// 加一个path属性
path: type === 'video' ? (poster || doRes.poster) : filePath,
// 预览数据替换成发消息的数据传预览数据索引
previewFileIndex: previewFileIndex,
// 对应前端渲染属性
data: type === 'video' ? (poster || doRes.poster) : filePath,
dataType: type,
otherData: otherData,
});
},
// 弹出框隐藏 -- 点击聊天区域一样的效果
clickPlusHidetooltip(){
this.KeyboardHeight = 0;
uni.hideKeyboard();
this.$refs.tooltipPlus.hide();
this.sendMessageMode = "text";
},
},
}
# 7. 视频上传到自己的服务器获取视频封面的接口说明
接口说明文档内容:三十一、视频上传到服务器获取视频封面
# 三、服务器通讯发语音消息
# 1. 在聊天页 /pages/chat/chat.nvue
mounted() {
...
// 全局监听 全局注册一个发送语音的事件,然后全局
this.regSendMessage(res=>{
if(!this.isRecorderCancel){
res.duration = this.recorderDuration * 1000;
// this.sendMessage('audio',res);
this.sendWebsocketAudio('audio',res);
}
});
}
# 2. 发语音功能实现
在文件 /pages/chat/plusIconAction.js
//发送消息
sendMessage(msgType, option = {}){
return new Promise((resolve,reject) =>{
...
switch (msgType){
...
case 'audio':
console.log('audio的数据',option);
msg.data = option.path;
msg.dataType = 'audio';
msg.otherData = {
duration: Math.round(option.duration / 1000),
}
break;
...
}
...
});
},
// 发送图片视频等文件到服务器或者云存储
async uploadFile(res, filetype = 'image', previewFileIndex = null){
...
if(filetype == 'image'){
...
}else if(filetype == 'video'){
...
}else if(filetype == 'audio'){
files = [{
path: res.tempFilePath,
size: res.fileSize,
duration: res.duration,
}];
}
console.log('处理后的文件数据', files);
...
},
// 发送websocket消息
sendMsgToUsers(poster, doRes, filePath, type = 'video', previewFileIndex = -1){
// 根据情况定义otherData
let otherData = {
...
duration: type == 'video' || type == 'audio' ?
doRes.durationFormatted : 0, // 时长
...
}
...
},
// 发语音消息
sendWebsocketAudio(type,res){
console.log('发语音消息',res);
const doRes = {
poster: false,
durationFormatted: res.duration,
showPoster: false,
};
// 拿到当前聊天页数据最后一条数据的索引
let lastIndex = this.chatDataList.length - 1;
// 本地预览发一次消息
this.sendMessage('audio',{
poster: doRes.poster,
duration: doRes.durationFormatted,
showPoster: doRes.showPoster,
// 音频路径是本地音频路径
videoPath: res.tempFilePath,
// 是否跟服务器通讯 将这个消息发给其他人
isSendToServer: false,
// 文件进度 初始进度为0
progress: 0,
// 发送消息的状态【准备发文件】
sendStatus: 'pendingFile',
// 加一个path属性
path: res.tempFilePath,
// 预览数据索引
previewFileIndex: lastIndex + 1,
// 标记预览数据
isPreviewData: true,
}).then(previewFileIndex=>{
// 拿到预览文件在聊天页的索引进行后续操作
console.log('拿到预览文件在聊天页的索引进行后续操作',
previewFileIndex);
// 接下来发送给服务器或者第三方云存储(如:阿里云oss)及后续处理
// 这个跟服务器和网络有关,网络越差时间越长:秒级
// 发音频--使用服务器返回的视频URL发送消息
this.uploadFile(res, 'audio', previewFileIndex).then(uploadedUrls =>{
console.log('音频上传成功', uploadedUrls);
// 获取视频封面然后发websocket消息
// 判断(处理)视频地址
const audioPath = uploadedUrls[0].startsWith('/') ?
requestUrl.http + uploadedUrls[0] : uploadedUrls[0];
// 发送websocket消息
this.sendMsgToUsers(false, doRes, audioPath, 'audio', previewFileIndex);
});
});
},
# 四、聊天页撤回转发消息等功能实现
内容较多,在新页面打开 聊天页撤回转发消息等功能实现