# 一、撤回消息功能实现
# 1. 页面渲染数据赋值改动
在文件 /pages/chat/plusIconAction.js
...
//发送消息
sendMessage(msgType, option = {}){
return new Promise((resolve,reject) =>{
console.log('发送消息',msgType);
...
// 组织一下消息格式和服务器一致
...
// console.log('发消息格式数据',serverMsg); return;
// 显示到页面上
msg.avatar = serverMsg.from_avatar.startsWith('/') ?
requestUrl.http + serverMsg.from_avatar : serverMsg.from_avatar ;
msg.nickname = serverMsg.from_name;
msg.user_id = serverMsg.from_id;
// 直接渲染到页面的msg信息重新赋值一下
msg = {...serverMsg, ...msg};
console.log('----直接渲染到页面的msg信息重新赋值一下---', msg);
// 发送消息的状态
// msg.sendStatus = 'pending';
// 拿到要发送消息的索引
let sendmsgIndex = -1;
if(msg.previewFileIndex != undefined && msg.previewFileIndex != -1){
// 如果是预览数据则替换预览数据
...
// 替换的数据
...
}else{
...
}
// 发给服务器消息
if(msg.isSendToServer){
console.log('发消息给服务器的数据', serverMsg);
this.chatClass.sendmessage(serverMsg).then(result => {
console.log('页面接收服务器返回结果',result);
// 拿到刚发送的消息赋值
...
// 执行后续操作
...
}).catch(error => {
...
});
}else{
...
}
// 清空发送的内容然后还要滚动到底部
...
});
},
...
# 2. 撤回消息服务器交互
在组件 /components/chat-item/chat-item.vue中
<template>
<view class="px-3">
<!-- 时间 -->
...
<!-- 撤回消息 -->
<view v-if="item.isremove"
class="flex align-center justify-center py-3">
<text class="font-sm text-light-muted">{{ isMe ? '您' : item.nickname}}撤回了一条信息</text>
</view>
<!-- 进群首条消息提示 -->
...
<!-- 聊天内容 -->
...
</view>
</template>
<script>
...
export default{
...,
methods:{
...,
clickType(e) {
console.log('点击菜单',e);
switch (e){
case 'copy':
break;
case 'removeChatItem':
console.log('撤回消息', this.item);
this.chatClass.revokeChatInfo(this.item).then((res)=>{
// 自己的聊天页消息撤回
this.item.isremove = true;
// 消息页显示处理
console.log('看一下撤回消息服务器返回的结果',res);
// 重点注意:消息页的id单聊是聊天用户id,群聊是群id
// 对应到res里面的是to_id
res.id = res.to_id;
// 消息页的聊天列表更新一下
this.chatClass.updateXiaoXiList(res);
});
break;
}
this.$refs.chatTooltip.hide();
},
...
}
}
</script>
# 3. 撤回消息服务器方法
在类文件 /common/js/chatClass.js
...
// 处理接收到的消息
async doMessage(msg){
console.log('处理接收到的消息',msg);
if(msg.type == 'system'){
console.log('系统消息单独处理');
}else if(msg.type == 'singleChat'){
let msgData = msg.data;
if(msgData.actionType && msgData.actionType == 'revoke'){
// 撤回处理
// 获取历史记录, 传key值则找指定聊天记录
// 不传key则构建一个聊天对象因为对方可能不在聊天页
// this.ToObject.chatType = msgData.chatType;
// this.ToObject.id = msgData.chatType == 'single' ? msgData.from_id : msgData.to_id; //接收人|群id
let keyId = msgData.chatType == 'single' ? msgData.from_id : msgData.to_id; //接收人|群id
// 因为this.ToObject初始化默认false,无法赋值,所以我们直接传key
let key = `chatDetail_${this.user.id}_${msgData.chatType}_${keyId}`;
let chatInfoList = this.getChatInfo(key);
console.log('----撤回处理获取所有历史记录----',chatInfoList);
// 根据id找到要撤回的消息
let index = chatInfoList.findIndex(v => v.id == msgData.id);
if(index != -1){
// 找到了
let revokeMsg = chatInfoList[index];
console.log('----找到了要撤回的消息历史记录----', revokeMsg);
revokeMsg.isremove = 1;
// 找到k属性
let k = revokeMsg.k;
// 更新指定的历史记录信息(不急着更新可以异步)
await this.updateChatInfo(revokeMsg, k, false);
// 消息页列表要不要更新:取决于撤回的消息是不是最新的消息
if(chatInfoList.length - 1 == index){
// 是最新的消息
// 消息页修改data 和 type
revokeMsg.data = msgData.data;
revokeMsg.type = msgData.type;
revokeMsg.update_time = (new Date()).getTime();
// 消息页的聊天列表更新一下
this.updateXiaoXiList(revokeMsg, false);
// 全局通知数据--聊天页做更新
}else{
// 不是最新消息
// 全局通知数据--聊天页做更新
}
// 全局通知数据--聊天页做更新
uni.$emit('onMessage', {
actionType: msgData.actionType,
...revokeMsg,
});
}
}else{
// 把聊天信息存在本地
let { data } = this.addChatInfo(msgData, false);
// 消息页的聊天列表更新一下
this.updateXiaoXiList(data, false);
// 全局通知数据
uni.$emit('onMessage', data);
}
}
}
...
// 撤回消息(包括跟个人或者群聊天的某条自己发送的消息)
revokeChatInfo(options){
return new Promise((resolve,reject)=>{
uni.$u.http.post(requestUrl.http + `/api/chat/revokeMessage`, {
to_id: options.to_id,
to_name: options.to_name,
to_avatar: options.to_avatar,
id: options.id,
chatType: options.chatType,
create_time: options.create_time,
}, {
header: {
token: this.user.token,
},
}).then(res => {
console.log('服务器撤回消息', res);
// 删除(与某个群所有或者某条聊天信息,与某个人的所有或者某条聊天信息)
this.clearChatInfo(options.to_id, options.chatType, options.id)
.then(()=>{
console.log('撤回消息已完成我自己页面消息的撤回效果');
resolve(res.data.data);
});
}).catch(err => {
console.log('服务器撤回消息失败', err);
uni.showToast({title: err.data.data, icon:'none'});
reject(err);
});
});
}
# 4. 撤回消息服务器方法接口说明
接口说明具体查看:三十二、撤回消息接口说明
# 5. 通过websocket撤回消息的用户处理
在页面 /pages/chat/chat.nvue
onLoad(e) {
...
try{
...
// 监听处理接收到的消息
uni.$on('onMessage', v => {
console.log('-----监听处理接收到的消息在聊天页------',v);
if((v.from_id == this.arg.id && v.chatType == 'single') ||
(v.chatType == 'group' && this.arg.id == v.to_id)){
if(v.actionType && v.actionType == 'revoke'){
// 撤回处理
let index = this.chatDataList.findIndex(e => e.id == v.id);
if(index != -1){
console.log('找到撤回的消息index', index);
this.chatDataList[index].isremove = 1;
}
}else{
// 正常接收显示
this.chatDataList.push(this.formatServerMsg(v));
}
}
});
}catch{
...
}
},
methods: {
...
// 接收的或者历史记录格式化成需要的聊天数据属性渲染页面
formatServerMsg(v){
...
// 渲染到聊天页的数据
let chatdata = {
...v,
...
type: v.type, //image,video
isremove: v.isremove,
...
};
console.log('接收的或者历史记录渲染到页面的最终数据', chatdata);
return chatdata;
},
# 6. 消息页处理
在组件 /components/chat-chatlist/chat-chatlist.vue
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 if(this.item.type == 'systemNotice'){
let data = ``;
if(typeof this.item.data == 'string'){
data = this.item.data;
}else if(typeof this.item.data == 'object'){
data = this.item.data.data;
}
let mynickname = this.me.nickname || this.me.username;
console.log('计算属性看一下我的昵称',mynickname);
console.log('计算属性看一下data', data);
if(data.indexOf(mynickname) > -1){
data = data.replace(mynickname, '您');
}
return data;
}else{
return this.item.datadesc;
}
},
}
# 二、删除聊天页某条消息功能实现
说明:
- 我们把
撤回消息和删除消息一起讲,主要是为了让大家理解两者的区别,相对来说,删除消息比撤回消息要简单一些,因为撤回消息不仅要撤回我本地的消息,还要通知其他人,撤回他们记录中的对应消息。而删除消息,则只需要删除我本地记录中的消息。
当然删除消息也有跟撤回消息不同的地方,就是如果你删除的消息还有上一条,则消息页应该显示上一条消息,如果聊天页就一条消息被你删除了,则聊天页应该删除这个对话。 - 如果同学们在学习本节课的过程中,特别是最后调试出现的效果跟老师的不一样,那可能是老师对类文件
/common/js/chatClass.js做了微调而没有及时更新文档,同学们可以下载本节课的课件,把课件中的类文件替换到你当前类文件代码,在进行调试。 - 或者你直接赋值下面的类文件代码,贴到你的项目中:
在类文件
/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值',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) => {
try{
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();
}
}catch{
return reject();
}
});
}
// 删除或者(撤回)(与某个群所有或者某条聊天信息,与某个人的所有或者某条聊天信息)
clearChatInfo(to_id, chatType, msgid = false, doaction = 'revoke'){
let key = `chatDetail_${this.user.id}_${chatType}_${to_id}`;
// 根据key获取信息
let chatDetail = uni.getStorageSync(key);
chatDetail = chatDetail && typeof chatDetail == 'string' ?
JSON.parse(chatDetail) : chatDetail;
console.log('本地根据key获取的聊天详细信息', chatDetail);
if(msgid){
// 删除与某人或者某群对话中的某一条聊天记录
if(chatDetail){
// 拿到具体的某条聊天信息
let k = chatDetail.findIndex(v=> v.id === msgid);
if(k > -1){
console.log('删除或者(撤回)与某人或者某群对话中的某一条聊天记录', chatDetail[k]);
if(doaction && doaction == 'delete'){
// 删除这条记录
chatDetail.splice(k, 1);
}else{
// 撤回操作
// 将这条记录的属性isremove改成1或者true 就会自动隐藏
chatDetail[k].isremove = 1;
}
// 重新存储
uni.setStorageSync(key, chatDetail);
}
}
}else{
// 删除与某人或者某群对话中的本地所有记录
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){
// 找到了
if(msgid){
if(chatDetail){
// 执行后续操作 - 如:更新消息页
return resolve(index);
}else{
return reject();
}
}else{
// 消息页删除这个记录
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();
});
}
// 消息页将某条消息更新成你指定的消息
XiaoXiListUpdataZhiDingMsg(XiaoXiListId,chatType,msg){
console.log('---消息页将某条消息更新成你指定的消息id---',XiaoXiListId);
console.log('---消息页将某条消息更新成你指定的消息内容---',msg);
// 获取消息页列表本地历史信息(消息页消息列表)
let list = this.getXiaoXiList();
console.log('获取消息页列表旧历史', list);
// 查消息页列表索引
let index = list.findIndex(v => v.chatType == chatType && v.id == XiaoXiListId);
if(index != -1){
// 主要是更新data部分
let data = msg.data;
try {
if(typeof msg.data == 'string'){
data = JSON.parse(msg.data);
}
}catch {
data = msg.data;
}
// 当发送消息是图片视频等,消息页列表最新聊天的最后一条消息显示[图片][视频]等
let isSend = msg.from_id === this.user.id ? true : false;
let datadesc = this.XiaoXiListAnyOneLastMsgFormat(data, msg, isSend);
// 找到了消息列表的数据,将这个数据换成指定的数据
let findItem = list[index];
// 更新以下内容:时间 内容 类型等等
findItem.update_time = msg.create_time;
findItem.data = data;
findItem.datadesc = datadesc;
findItem.type = msg.type;
// findItem.avatar = avatar; // 消息页的头像不用更新,因为是跟当前聊天对象的记录
// findItem.name = name; // 消息页的name不用更新,因为是跟当前聊天对象的记录
findItem.isremove = msg.isremove;
// 处理完了之后,存储消息页列表本地历史信息
this.setXiaoXiList(list);
// 更新(获取)消息页,整个消息列表的未读数(不急可以异步执行)
this.updateXiaoXiListNoreadNum();
// 消息页,整个消息列表的数据也存入了vuex中
// 更新一下vuex中的消息列表的数据
uni.$emit('updateXiaoXiList', list);
}
}
// 消息页的聊天列表更新一下
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 data = msg.data;
try {
if(typeof msg.data == 'string'){
data = JSON.parse(msg.data);
}
}catch {
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, //是否提示来消息了
isremove: msg.isremove, // 是否撤回
redirect: msg.redirect ? msg.redirect : false, // 跳转字段
};
// 群聊
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.isremove = msg.isremove;
findItem.redirect = msg.redirect ? msg.redirect : false;
// 未读数更新
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 = ``;
let dataType = data.dataType ? data.dataType : msg.dataType;
switch(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 = `[图片]`;
}
}else{
if(msg && msg.otherData && msg.otherData.type){
if(msg.otherData.type == 'iconMenus'){
datadesc = `[表情]`;
if(msg.otherData.typedata && msg.otherData.typedata.name){
datadesc += `[${msg.otherData.typedata.name}]`;
}
}else if(msg.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 msgData = msg.data;
if(msgData.actionType && msgData.actionType =='revoke'){
// 撤回操作
// 获取历史记录, 传key值则找指定聊天记录
// 不传key则构建一个聊天对象因为对方可能不在聊天页
// this.ToObject.chatType = msgData.chatType;
// this.ToObject.id = msgData.chatType == 'single' ? msgData.from_id : msgData.to_id; //接收人|群id
let keyId = msgData.chatType == 'single' ? msgData.from_id : msgData.to_id; //接收人|群id
// 因为this.ToObject初始化默认false,无法赋值,所以我们直接传key
let key = `chatDetail_${this.user.id}_${msgData.chatType}_${keyId}`;
let chatInfoList = this.getChatInfo(key);
console.log('----撤回处理获取所有历史记录----',chatInfoList);
// 根据id找到要撤回的消息
let index = chatInfoList.findIndex(v => v.id == msgData.id);
if(index != -1){
// 找到了
let revokeMsg = chatInfoList[index];
console.log('----找到了要撤回的消息历史记录----', revokeMsg);
revokeMsg.isremove = 1;
// 找到k属性
let k = revokeMsg.k;
// 更新指定的历史记录信息(不急着更新可以异步)
await this.updateChatInfo(revokeMsg, k, false);
// 消息页列表要不要更新:取决于撤回的消息是不是最新的消息
if(chatInfoList.length - 1 == index){
// 是最新的消息
// 消息页修改data 和 type
revokeMsg.data = msgData.data;
revokeMsg.type = msgData.type;
revokeMsg.update_time = (new Date()).getTime();
// 消息页的聊天列表更新一下
this.updateXiaoXiList(revokeMsg, false);
// 全局通知数据--聊天页做更新
}else{
// 不是最新消息
// 全局通知数据--聊天页做更新
}
// 全局通知数据--聊天页做更新
uni.$emit('onMessage', {
actionType: msgData.actionType,
...revokeMsg,
});
}
}else{
// 把聊天信息存在本地
let { data } = this.addChatInfo(msgData, 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();
}
}
// 撤回消息
revokeChatInfo(options){
return new Promise((resolve,reject)=>{
uni.$u.http.post(requestUrl.http + `/api/chat/revokeMessage`,options,{
header:{
token: this.user.token,
}
}).then(res=>{
console.log('服务器撤回消息结果',res);
// 删除或者(撤回)(与某个群所有或者某条聊天信息,与某个人的所有或者某条聊天信息)
this.clearChatInfo(options.to_id, options.chatType, options.id).then(()=>{
resolve(res.data.data);
});
}).catch(err=>{
console.log('服务器撤回消息失败结果',err);
uni.showToast({title: err.data.data, icon:'none'});
reject(err);
});
});
}
}
export default chatClass;
# 1. 删除聊天页某条本地消息(并返回消息页对应聊天对象索引)方法
在类文件 /common/js/chatClass.js
// 删除或者(撤回)(与某个群所有或者某条聊天信息,与某个人的所有或者某条聊天信息)
clearChatInfo(to_id, chatType, msgid = false, doaction = 'revoke'){
let key = `chatDetail_${this.user.id}_${chatType}_${to_id}`;
// 根据key获取信息
let chatDetail = uni.getStorageSync(key);
chatDetail = chatDetail && typeof chatDetail == 'string' ?
JSON.parse(chatDetail) : chatDetail;
console.log('本地根据key获取的聊天详细信息', chatDetail);
if(msgid){
// 删除与某人或者某群对话中的某一条聊天记录
if(chatDetail){
// 拿到具体的某条聊天信息
let k = chatDetail.findIndex(v=> v.id === msgid);
if(k > -1){
console.log('删除或者(撤回)与某人或者某群对话中的某一条聊天记录', chatDetail[k]);
if(doaction && doaction == 'delete'){
// 删除这条记录
chatDetail.splice(k, 1);
}else{
// 撤回操作
// 将这条记录的属性isremove改成1或者true 就会自动隐藏
chatDetail[k].isremove = 1;
}
// 重新存储
uni.setStorageSync(key, chatDetail);
}
}
}else{
// 删除与某人或者某群对话中的本地所有记录
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){
// 找到了
if(msgid){
if(chatDetail){
// 执行后续操作 - 如:更新消息页
return resolve(index);
}else{
return reject();
}
}else{
// 消息页删除这个记录
xiaoxiList.splice(index, 1);
// 处理完了之后,存储消息页列表本地历史信息
this.setXiaoXiList(xiaoxiList);
// 更新(获取)消息页,整个消息列表的未读数(不急可以异步执行)
this.updateXiaoXiListNoreadNum();
// 消息页,整个消息列表的数据也存入了vuex中
// 更新一下vuex中的消息列表的数据
uni.$emit('updateXiaoXiList', xiaoxiList);
// 执行后续操作
return resolve();
}
}
return reject();
});
}
# 2. 删除聊天页本地某条聊天消息
在组件 /components/chat-item/chat-item.vue
data(){
return {
...
menuList: [{
name: "删除",
type: 'deleteChatItem'
},
...
],
...
}
},
...
clickType(e) {
console.log('点击菜单',e);
switch (e){
case 'deleteChatItem':
// 删除自己的某条聊天记录(本地)
// 重点处理to_id
let to_id = this.item.chatType == 'single' ?
this.me.id == this.item.to_id ?
this.item.from_id : this.item.to_id : this.item.to_id;
// 删除本地记录
this.chatClass.clearChatInfo(to_id, this.item.chatType,
this.item.id, 'delete').then((xiaoxiListIndex)=>{
// 通知聊天页删除这条记录
uni.$emit('deleteChatItem',{
item: this.item,
xiaoxiListId: to_id, // 消息页对话的id
});
});
break;
case 'removeChatItem':
...
break;
}
...
},
...
# 3. 删除聊天页页面某条聊天消息
在页面 /pages/chat/chat.nvue
...
onLoad(e) {
...
try{
...
// 监听处理接收到的消息
uni.$on('onMessage', v => {
...
});
// 监听删除了某条聊天记录
uni.$on('deleteChatItem', v => {
console.log('---传过来的v---',v);
console.log('---当前的聊天数据---',this.chatDataList);
console.log('---v.xiaoxiListId---', v.xiaoxiListId);
// 聊天页最后一条消息索引
let lastIndex = this.chatDataList.length - 1;
// 要删除的消息索引
let deleteIndex = this.chatDataList.findIndex(e => e.id === v.item.id);
if(deleteIndex != -1){
// 删除聊天页的指定聊天记录
this.chatDataList.splice(deleteIndex, 1);
}
// 如果删除的消息就是最后一条消息,则还要更新一下消息页的显示
if(deleteIndex == lastIndex){
// 如果有上一条消息,消息页显示上一条消息
if(deleteIndex > 0){
let prevIndex = lastIndex - 1;
let msg = this.chatDataList[prevIndex];
console.log('删除时候有上一条记录', msg);
// 消息页将某条消息更新成你指定的消息
this.chatClass.XiaoXiListUpdataZhiDingMsg(v.xiaoxiListId, v.item.chatType, msg);
}else{
// 没有上一条消息,就一条消息被你删了
// 删除消息页指定的历史记录信息
this.chatClass.deleteChatInfo(v.xiaoxiListId, v.item.chatType);
}
}
});
}catch{
...
}
},
...
# 4. 调整消息页data的数据显示
在页面 /pages/xiaoxi/xiaoxi.nvue
methods: {
// 获取消息列表
getDataList(){
console.log('消息列表', this.xiaoxiList);
this.chatList = this.xiaoxiList.map(v => {
return {
...v,
avatar: v.avatar.startsWith('http') ? v.avatar : requestUrl.http + v.avatar,
nickname: v.name,
chat_time: parseTimeJs.gettime(v.update_time),
data: v.data && v.data.data ? v.data.data : v.data, // 调整data显示
datacount: v.noreadnum,
isZhiding: v.istop,
}
});
console.log('消息列表最终显示到页面的数据', this.chatList);
},
...
}
# 5. 消息页页面显示
在组件 /components/chat-chatlist/chat-chatlist.vue
computed:{
...
// 昵称下面的小字
showText(){
if(this.item.isremove){
return this.item.datadesc + `撤回了一条消息`;
}
if(this.item.type == 'text'){
// 纯文字 和 头像文字
if(typeof this.item.data == 'string' ||
typeof this.item.data == 'number'){
return this.item.datadesc + this.item.data;
}else if(typeof this.item.data == 'object'){
return this.item.datadesc + this.item.data.data;
}
}else if(this.item.type == 'systemNotice'){
...
}
else{
...
}
},
}
# 三、好友申请及同意添加为好友实时通知
由于我们已经使用了websocket,所以好友申请及同意添加为好友,我们可以通过websocket推送过来的消息在消息页处理更直观,这部分内容属于后端处理的内容,感兴趣的可以看一下后端文档:[三、申请添加好友进行实时通知(后端文档)]。
# 1. 好友申请列表修复头像显示错误
在页面 /pages/applyMyfriend/applyMyfriend.vue
...
filters:{
//头像
avatarShow(item){
if(item && item.user && item.user.avatar){
let avatar = item && item.user.avatar;
avatar = avatar && avatar.startsWith('http') ? avatar : `${requestUrl.http}${avatar}`;
return avatar;
}
return ``;
},
...
}
...
# 2. 好友申请实时显示
如果有用户申请加你为好友,则你会收到webscoket推送过来的好友申请通知(这部分内容主要由后端处理),文档可查看:[三、申请添加好友进行实时通知(后端文档)]
# 3. 通知添加好友在消息页点击之后跳转到添加好友页面
在页面 /pages/xiaoxi/xiaoxi.nvue
...
openChat(e) {
console.log('进入聊天详情页', e);
// 更新一下未读消息数量
this.chatClass.goChatPageUpdateXiaoXiNoReadNum(e.item.id,e.item.chatType);
// 判断一下有没有跳转字段
if(e.item.redirect && typeof e.item.redirect == 'object'){
// 根据跳转情况判断
uni[e.item.redirect.type]({
url: e.item.redirect.url,
});
return;
}else{
// 进入聊天页 传一下用户的信息过去在页面展示
const userchat = {
id: e.item.id,
name: e.item.name,
avatar: e.item.avatar,
chatType: e.item.chatType, // 单聊 single 群聊 group
};
uni.navigateTo({
url: `/pages/chat/chat?arg=${encodeURIComponent(JSON.stringify(userchat))}`,
});
}
},
...
# 4. 同意添加为好友实时通知
如果对方同意加你为好友,则可以收到webscoket推送过来的消息(这部分内容主要由后端处理),文档可查看:[四、同意添加为好友实时通知(后端文档)]
# 四、删除好友
# 1. 删除好友接口说明
接口说明可查看:[三十三、删除好友]
# 2. 删除好友功能实现
在页面 /pages/setpageInfo/setpageInfo.vue
<template>
<view>
<!-- 导航栏 -->
...
<!-- 内容 -->
<view>
<!-- 聊天相关 -->
<view v-if="setdata.action == 'chatset'">
...
</view>
<!-- 聊天页设置 -->
<view v-if="setdata.action == 'chatpageset'">
<view v-if="chatpageset.id && chatpageset.chatType">
<!-- 头像部分 -->
...
<!-- 群相关设置 -->
<view v-if="chatpageset.chatType == 'group'">
<!-- 群二维码 -->
...
<!-- 我在群里面的昵称 -->
...
<!-- 群名称 -->
...
<!-- 群公告 -->
...
</view>
<!-- 查找聊天内容 -->
...
<!-- 消息设置 -->
...
<!-- 解散群或退群 -->
<view>
<view class="p-3">
<u-button type="primary" plain :text="deleteOrQuitGroup"
@click="deleteOrQuitGroupfn"></u-button>
</view>
<u-gap height="10" bgColor="#EDEDED"></u-gap>
</view>
</view>
</view>
<!-- 群二维码 -->
...
<!-- 扫码之后的页面 -->
...
</view>
</view>
</template>
<script>
...
export default {
...
methods: {
// 加入群聊
...
// 解散群或者退群或者删除该好友
deleteOrQuitGroupfn(){
if(this.chatpageset.chatType == 'group'){
const arg = {
apiurl: '/api/chat/groupDeleteOrQuit',
data: {
id: this.chatpageset.id,
},
};
this.groupUpdate(arg).then(res => {
uni.showToast({title:'操作成功', icon:'none'});
if(this.chatpageset.user_id){
return this.navigateBack();
}else{
uni.switchTab({
url:'/pages/xiaoxi/xiaoxi'
});
}
});
}else if(this.chatpageset.chatType == 'single'){
console.log('删除好友');
uni.showModal({
content:'确认删除该好友?',
success: (res) => {
if(res.confirm){
// 执行删除
uni.$u.http.post(requestUrl.http +
`/api/chat/deletegoodfriend/${this.chatpageset.id}`,{},{
header:{
token: this.me.token,
}
}).then(res=>{
console.log('删除好友服务器结果', res);
uni.showToast({title: res.data.data,icon:'none'});
// 清除与好友的聊天记录
this.chatClass.clearChatInfo(this.chatpageset.id,
this.chatpageset.chatType).then(()=>{
// 清空完成之后聊天页的数据也要清空
uni.$emit('clearChatInfo');
});
// 跳转到消息页
setTimeout(()=>{
uni.switchTab({
url:'/pages/xiaoxi/xiaoxi'
});
},2000);
}).catch(err=>{
uni.showToast({title: err.data.data,icon:'none'});
});
}
}
});
}
},
...
}
}
</script>
# 五、修改我的头像昵称等信息
# 1. 修改我的头像昵称接口说明
接口说明可查看:[三十四、修改我的信息(修改我的头像昵称等信息)]
# 2. 新增修改账号信息设置入口
在页面 /pages/setpage/setpage.vue
<template>
<view>
<!-- 导航栏 -->
...
<!-- 内容 -->
<view>
<!-- 账号 -->
<u-cell-group>
<u-cell title="账号信息设置"
:customStyle="{'background-color':'#ffffff'}"
isLink titleStyle="font-size:18px;"
@click="setfun('userinfoSet','账号信息设置')"></u-cell>
</u-cell-group>
<!-- 聊天相关 -->
...
<!-- 退出 -->
...
</view>
</view>
</template>
# 3. 修改我的头像昵称功能实现页面部分
在页面 /pages/setpageInfo/setpageInfo.vue
<template>
<view>
<!-- 导航栏 -->
...
<!-- 内容 -->
<view>
<!-- 聊天相关 -->
<view v-if="setdata.action == 'chatset'">
...
</view>
<!-- 聊天页设置 -->
<view v-if="setdata.action == 'chatpageset'">
...
</view>
<!-- 群二维码 -->
<view v-if="setdata.action == 'groupQrcode'">
...
</view>
<!-- 扫码之后的页面 -->
<view v-if="setdata.action == 'autoAddGroup'">
...
</view>
<!-- 账号信息设置 -->
<view v-if="setdata.action == 'userinfoSet'">
<!-- 账号(不可修改) -->
<u-cell-group>
<u-cell>
<view slot="title">
<view class="mb-2">
<text class="font-weight-bold">账号(不可修改)</text>
</view>
<u-input v-model="chatpageset.username"
border="none" :maxlength="20"
:adjustPosition="false" clearable disabled
customStyle="padding:20rpx;"></u-input>
</view>
</u-cell>
</u-cell-group>
<u-gap height="10" bgColor="#EDEDED"></u-gap>
<!-- 昵称 -->
<u-cell-group>
<u-cell>
<view slot="title">
<view class="mb-2">
<text class="font-weight-bold">昵称</text>
</view>
<u-input v-model="chatpageset.nickname"
border="none" :maxlength="20"
:adjustPosition="false" clearable
placeholder="昵称最多20个字"
@confirm="updateUserInfo('nickname')"
customStyle="padding:20rpx;"></u-input>
</view>
</u-cell>
</u-cell-group>
<u-gap height="10" bgColor="#EDEDED"></u-gap>
<!-- 头像 -->
<u-cell-group>
<u-cell>
<view slot="title">
<view class="mb-2">
<text class="font-weight-bold">头像</text>
</view>
<u-avatar :src="chatpageset.avatar" shape="square"
customStyle="border:1px solid #eeeeee;" :size="36"
@click="updateUserInfo('avatar')"></u-avatar>
</view>
</u-cell>
</u-cell-group>
<u-gap height="10" bgColor="#EDEDED"></u-gap>
<!-- ActionSheet 操作菜单 -->
<u-action-sheet :actions="actionSheetList"
title="有以下方式供您更换头像" :show="actionSheetShow"
safeAreaInsetBottom
@select="selectActionSheet"
@close="actionSheetShow = false"></u-action-sheet>
</view>
</view>
</view>
</template>
<script>
...
import updateUserInfoJs from '@/pages/setpageInfo/updateUserInfo.js';
export default {
mixins:[toolJs,updateUserInfoJs],
...,
onLoad(e) {
...
// 动态生成数据
if(e.action){
this.setdata.action = e.action;
if(this.setdata.action == 'chatset'){
...
}else if(this.setdata.action == 'chatpageset'){
...
}else if(this.setdata.action == 'groupQrcode'){
...
}else if(this.setdata.action == 'autoAddGroup'){
...
}else if(this.setdata.action == 'userinfoSet'){
console.log('账号信息设置', this.me);
this.chatpageset = this.me;
}
}
},
...,
methods: {
...,
// 更新群信息
groupUpdate(arg){
return new Promise((resolve,reject) => {
uni.$u.http.post(requestUrl.http + `${arg.apiurl}`, arg.data , {
header: {
token: this.me.token,
},
}).then(res => {
console.log('服务器返回群名称更新结果', res);
// if(res.data.data == 'ok'){
// resolve();
// }
resolve(res);
}).catch(err =>{
...
});
});
},
...
}
}
</script>
# 4. 修改我的头像昵称功能实现js部分
新建js文件 /pages/setpageInfo/updateUserInfo.js
import UniPermission from '@/common/mixins/uni_permission.js';
import { requestUrl, uploadfileSaveType } from '@/common/mixins/configData.js';
export default {
data(){
return {
// 更换头像是拍照还是从相册选择一张
actionSheetList:[
{
name:'相册',
subname:"可从相册选择一张图片来当头像",
color:'#1BA035',
fontSize:'20',
eventType:'photo',
},
{
name:'拍一张',
subname:"可拍摄一张照片当头像",
color:'#1BA035',
fontSize:'20',
eventType:'cameraPhoto',
},
],
actionSheetShow:false,
}
},
methods: {
// 修改我的账号信息
updateUserInfo(fieldname) {
let arg = {};
switch (fieldname) {
case 'nickname':
arg = {
apiurl: '/api/chat/updateUserinfo',
data: {
fieldname: fieldname,
fieldValue: this.chatpageset.nickname,
},
};
// 我的信息更新一下
this.me[fieldname] = this.chatpageset.nickname;
break;
case 'avatar':
// 因为换图片需要从相册选择图片或者拍照,先看是否有权限
return this.getUniPermission();
break;
}
// 服务器处理
this.groupUpdate(arg).then(res => {
// 成功处理
uni.showToast({
title: res.data.data,
icon: 'none'
});
// 更新我在本地的信息chatuser及vuex
this.$store.dispatch('regloginAction', this.me);
});
},
// 因为换图片需要从相册选择图片或者拍照,先看是否有权限
getUniPermission(){
console.log('弹窗选择是拍照还是相册选择');
this.actionSheetShow = true;
},
// 点击了ActionSheet 操作菜单
async selectActionSheet(item){
this.actionSheetShow = false;
console.log('点击了ActionSheet 操作菜单', item);
switch (item.eventType){
case 'photo':
await this.handlePermission({
permission:{
name:'photo',
title:'本功能需要您打开相册',
desc:'需要访问您的相册来选择图片',
grantedText:'用户已授权开启相册,可以选择照片了',
nograntedText:'您没有授权开启相册,无法发图片',
},
methodsName:'chooseImage',
});
break;
case 'cameraPhoto':
await this.handlePermission({
permission:{
name:'camera',
title:'本功能需要您打开摄像头',
desc:'需要访问您的摄像头来拍摄照片',
grantedText:'用户已授权打开摄像头,可以拍摄照片了',
nograntedText:'您没有授权打开摄像头,无法拍摄照片',
},
methodsName:'cameraPhoto',
});
break;
}
},
// 发相册图片|发相册视频 权限申请
async handlePermission(options){
try{
const permission = new UniPermission();
const granted = await permission.requestPermission(options.permission.name,
options.permission.desc,options.permission.title);
if(granted){
console.log(options.permission.grantedText);
// 调用对应的方法
this[options.methodsName]();
}else{
uni.showToast({
title: options.permission.nograntedText,
icon:'none',
duration:3000
});
}
}catch(error){
console.error('权限申请异常:' + error);
uni.showToast({
title:'权限申请失败:' + error.message,
icon:'none',
duration:3000
});
}
},
//选择相册照片发送
chooseImage(){
this.sendPhotoAlbumOrCamera({
count:1,
sourceType:'album',
});
},
// 拍照片发送
cameraPhoto(){
this.sendPhotoAlbumOrCamera({
count:1,
sourceType:'camera',
});
},
// 发照片:相册发照片| 拍照片
sendPhotoAlbumOrCamera(option){
const that = this;
uni.chooseImage({
count:option.count,
//sizeType:['original','compressed'],
sizeType: ['compressed'],
sourceType:[option.sourceType],
success: (chooseRes) => {
console.log('选择照片chooseRes',chooseRes);
const tempFilePath = chooseRes.tempFilePaths[0];
// 显示加载中
uni.showLoading({
title: '上传中...',
mask: true
});
// 根据配置选择上传方式
if (uploadfileSaveType.image === 'myserver') {
that.uploadImageToServer(tempFilePath);
} else if (uploadfileSaveType.image === 'AliyunOSS') {
that.uploadImageToAliyunOSS(tempFilePath);
}
},
});
},
// 上传图片到服务器
uploadImageToServer(filePath) {
const that = this;
// 自定义文件夹路径 - 使用avatars专门存放头像
const imagepath = `avatars`;
uni.uploadFile({
url: requestUrl.http + `/api/chat/uploadStreamSingleToServerDiy/${imagepath}`,
filePath: filePath,
name: 'file',
formData: {
token: this.me.token
},
header: {
'token': this.me.token
},
success: (uploadRes) => {
uni.hideLoading();
console.log('上传到本地服务器图片结果',uploadRes);
if (uploadRes.statusCode === 200) {
try {
const data = JSON.parse(uploadRes.data);
console.log('上传到本地服务器图片数据',data);
if (data && data.data && data.data.url) {
// 更新头像
that.updateAvatar(data.data.url);
} else {
uni.showToast({
title: '上传失败',
icon: 'none'
});
}
} catch (e) {
uni.showToast({
title: '解析响应数据失败',
icon: 'none'
});
}
} else {
uni.showToast({
title: '上传失败',
icon: 'none'
});
}
},
fail: (err) => {
uni.hideLoading();
uni.showToast({
title: '上传失败',
icon: 'none'
});
console.error('上传失败', err);
}
});
},
// 上传图片到阿里云OSS
uploadImageToAliyunOSS(filePath) {
const that = this;
uni.uploadFile({
url: requestUrl.http + `/api/chat/uploadAliyun`,
filePath: filePath,
name: 'img',
formData: {
token: this.me.token,
imageClassId: 0
},
header: {
'token': this.me.token
},
success: (uploadRes) => {
uni.hideLoading();
console.log('上传到阿里云OSS图片结果',uploadRes);
if (uploadRes.statusCode === 200) {
try {
const data = JSON.parse(uploadRes.data);
if (data && data.data && data.data.length && data.data[0].url) {
// 更新头像
that.updateAvatar(data.data[0].url);
} else {
uni.showToast({
title: '上传失败',
icon: 'none'
});
}
} catch (e) {
uni.showToast({
title: '解析响应数据失败',
icon: 'none'
});
}
} else {
uni.showToast({
title: '上传失败',
icon: 'none'
});
}
},
fail: (err) => {
uni.hideLoading();
uni.showToast({
title: '上传失败',
icon: 'none'
});
console.error('上传失败', err);
}
});
},
// 更新头像到服务器
updateAvatar(avatarUrl) {
const that = this;
uni.$u.http.post(requestUrl.http + '/api/chat/updateUserinfo', {
fieldname: 'avatar',
fieldValue: avatarUrl
}, {
header: {
token: this.me.token
}
}).then(res => {
// 更新本地数据
that.chatpageset.avatar = avatarUrl.startsWith('http') ? avatarUrl : requestUrl.http + avatarUrl;
that.me.avatar = avatarUrl.startsWith('http') ? avatarUrl : requestUrl.http + avatarUrl;
// 更新vuex中的用户信息
that.$store.dispatch('regloginAction', that.me);
uni.showToast({
title: '头像更新成功',
icon: 'success'
});
}).catch(err => {
uni.showToast({
title: err.data.data || '更新失败',
icon: 'none'
});
});
},
}
}
# 5. 新增更新头像(头像剪裁)处理
关于头像剪裁的说明:
- 头像剪裁涉及到canvas的使用,关于canvas之前课程没有讲到,后期会在专题课中讲解。
- 头像剪裁功能可以看成是一个公共独立功能,开发一次之后,以后在其它项目就直接拿来用,因此代码相对比较固定,对应不经常变动代码且实用的功能,我们称之为
轮子, 我们应该避免重复造轮子,因此这部分代码给大家说一下流程,就不敲代码了,大家按照老师的流程,把代码补全就可以了。
# ① 页面部分
在页面 /pages/setpageInfo/setpageInfo.vue
<template>
<view>
<!-- 导航栏 -->
<chat-navbar v-if="setdata.action != 'avatarCut'"
...>
</chat-navbar>
<!-- 内容 -->
<view>
<!-- 聊天相关 -->
<view v-if="setdata.action == 'chatset'">
...
</view>
<!-- 聊天页设置 -->
<view v-if="setdata.action == 'chatpageset'">
...
</view>
<!-- 群二维码 -->
<view v-if="setdata.action == 'groupQrcode'">
...
</view>
<!-- 扫码之后的页面 -->
<view v-if="setdata.action == 'autoAddGroup'">
...
</view>
<!-- 账号信息设置 -->
<view v-if="setdata.action == 'userinfoSet'">
...
</view>
<!-- 头像剪裁500rpx * 500rpx -->
<view v-if="setdata.action == 'avatarCut'"
class="flex flex-column"
style="height: 100vh; background-color: rgba(0,0,0,0.9);">
<!-- 导航栏 -->
<view class="position-fixed left-0 right-0 top-0">
<chat-navbar
:title="title" :fixed="true" :showPlus="false"
:showUser="false" :showBack="true"
navbarClass="bg-black text-white" :h5WeiXinNeedNavbar="false">
</chat-navbar>
</view>
<!-- 剪裁区域 -->
<view class="flex-1 flex justify-center align-center"
:style="{paddingTop: navBarHeight + 'px', paddingBottom: '100rpx'}">
<!-- 剪裁窗口,固定大小500rpx * 500rpx -->
<view class="position-relative"
style="width: 500rpx; height: 500rpx;">
<canvas canvas-id="avatarCanvas" id="avatarCanvas"
class="position-absolute"
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px', left: '0', top: '0' }"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"></canvas>
<!-- 剪裁框 -->
<view class="position-absolute"
style="width: 500rpx; height: 500rpx; left: 0; top: 0; box-sizing: border-box; border: 1px solid #fff; pointer-events: none;">
<!-- 辅助线 -->
<view class="position-absolute"
style="width: 1px; height: 100%; left: 33.33%; background-color: rgba(255,255,255,0.4);"></view>
<view class="position-absolute"
style="width: 1px; height: 100%; left: 66.66%; background-color: rgba(255,255,255,0.4);"></view>
<view class="position-absolute"
style="width: 100%; height: 1px; top: 33.33%; background-color: rgba(255,255,255,0.4);"></view>
<view class="position-absolute"
style="width: 100%; height: 1px; top: 66.66%; background-color: rgba(255,255,255,0.4);"></view>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="flex justify-between align-center px-5 position-fixed left-0 right-0 bottom-0 bg-black"
style="height: 100rpx; z-index: 999; bottom: 180rpx;">
<text class="text-white" style="font-size: 32rpx;" @click="cancelCrop">取消</text>
<text class="text-white" style="font-size: 32rpx;" @click="confirmCrop">确定</text>
</view>
</view>
</view>
</view>
</template>
<script>
...
import updateUserInfoJs from '@/pages/setpageInfo/updateUserInfo.js';
import avatarCutJs from '@/pages/setpageInfo/avatarCut.js';
export default {
mixins:[toolJs,updateUserInfoJs,avatarCutJs],
...,
}
<script>
# ② js部分
新建处理图片剪裁功能的js文件 /pages/setpageInfo/avatarCut.js
import { requestUrl, uploadfileSaveType } from '@/common/mixins/configData.js';
export default {
data() {
return {
tempFilePath: '', // 原始图片路径
cropperReady: false, // 剪裁器是否就绪
canvasWidth: 0, // 画布宽度
canvasHeight: 0, // 画布高度
scale: 1, // 缩放比例
minScale: 0.5, // 最小缩放
maxScale: 3, // 最大缩放
cropperContext: null, // 画布上下文
imageWidth: 0, // 图片宽度
imageHeight: 0, // 图片高度
offsetX: 0, // 图片偏移X
offsetY: 0, // 图片偏移Y
startX: 0, // 触摸开始X
startY: 0, // 触摸开始Y
initialOffsetX: 0, // 初始偏移X
initialOffsetY: 0, // 初始偏移Y
initialScale: 1, // 初始缩放比例
initialDistance: 0, // 初始两指距离
croppedWidth: 500, // 剪裁宽度(rpx,需要转换为px)
croppedHeight: 500, // 剪裁高度(rpx,需要转换为px)
windowWidth: 0, // 窗口宽度(px)
windowHeight: 0, // 窗口高度(px)
navBarHeight: 0, // 导航栏高度(px)
isPinching: false, // 是否正在缩放
}
},
methods: {
// 初始化剪裁器
initCropper() {
const that = this;
// 获取系统信息
uni.getSystemInfo({
success: function(res) {
that.windowWidth = res.windowWidth;
that.windowHeight = res.windowHeight;
// 计算导航栏高度(假设自定义导航栏高度为90rpx + 状态栏高度)
const statusBarHeight = res.statusBarHeight;
that.navBarHeight = uni.upx2px(90) + statusBarHeight;
// 将rpx转换为px
const croppedWidthPx = uni.upx2px(that.croppedWidth);
const croppedHeightPx = uni.upx2px(that.croppedHeight);
// 计算可用高度(屏幕高度 - 导航栏高度 - 操作按钮高度)
const availableHeight = that.windowHeight - that.navBarHeight - uni.upx2px(100);
// 画布大小设置为剪裁窗口大小
that.canvasWidth = croppedWidthPx;
that.canvasHeight = croppedHeightPx;
// 获取图片信息
uni.getImageInfo({
src: that.tempFilePath,
success: function(imageInfo) {
that.imageWidth = imageInfo.width;
that.imageHeight = imageInfo.height;
// 初始化画布
that.cropperContext = uni.createCanvasContext('avatarCanvas', that);
// 计算初始缩放比例,使图片适应剪裁框
const widthScale = croppedWidthPx / imageInfo.width;
const heightScale = croppedHeightPx / imageInfo.height;
that.scale = Math.max(widthScale, heightScale) * 1.2; // 稍微放大一点,确保填满剪裁框
that.minScale = that.scale; // 最小缩放比例设为初始比例,确保图片至少填满剪裁框
that.maxScale = that.scale * 3;
// 重置偏移量
that.offsetX = 0;
that.offsetY = 0;
// 绘制图片
that.drawImage();
that.cropperReady = true;
},
fail: function(err) {
console.error('获取图片信息失败', err);
uni.showToast({
title: '图片加载失败',
icon: 'none'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
});
}
});
},
// 绘制图片到画布
drawImage() {
const ctx = this.cropperContext;
// 清空画布
ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
// 绘制图片
const scaledWidth = this.imageWidth * this.scale;
const scaledHeight = this.imageHeight * this.scale;
// 计算图片绘制位置,确保居中显示
const drawX = (this.canvasWidth - scaledWidth) / 2 + this.offsetX;
const drawY = (this.canvasHeight - scaledHeight) / 2 + this.offsetY;
ctx.drawImage(
this.tempFilePath,
drawX, drawY,
scaledWidth, scaledHeight
);
ctx.draw(false); // 使用非同步绘制
},
// 取消剪裁
cancelCrop() {
uni.navigateBack();
},
// 确认剪裁
confirmCrop() {
const that = this;
if (!that.cropperReady) {
uni.showToast({
title: '剪裁器未就绪',
icon: 'none'
});
return;
}
uni.showLoading({
title: '正在剪裁...',
mask: true
});
// 剪裁图片
setTimeout(() => {
uni.canvasToTempFilePath({
x: 0,
y: 0,
width: that.canvasWidth,
height: that.canvasHeight,
destWidth: that.croppedWidth,
destHeight: that.croppedHeight,
canvasId: 'avatarCanvas',
success: function(res) {
uni.hideLoading();
// 上传剪裁后的图片
that.uploadCroppedImage(res.tempFilePath);
},
fail: function(err) {
uni.hideLoading();
uni.showToast({
title: '剪裁失败',
icon: 'none'
});
console.error('剪裁失败', err);
}
}, that);
}, 300);
},
// 上传剪裁后的图片
uploadCroppedImage(filePath) {
// 显示加载中
uni.showLoading({
title: '上传中...',
mask: true
});
const that = this;
// 根据配置选择上传方式
if (uploadfileSaveType.image === 'myserver') {
that.uploadImageToServer(filePath);
} else if (uploadfileSaveType.image === 'AliyunOSS') {
that.uploadImageToAliyunOSS(filePath);
}
},
// 上传图片到服务器
uploadImageToServer(filePath) {
const that = this;
// 自定义文件夹路径 - 使用avatars专门存放头像
const imagepath = `avatars`;
uni.uploadFile({
url: requestUrl.http + `/api/chat/uploadStreamSingleToServerDiy/${imagepath}`,
filePath: filePath,
name: 'file',
formData: {
token: this.me.token
},
header: {
'token': this.me.token
},
success: (uploadRes) => {
uni.hideLoading();
console.log('上传到本地服务器图片结果',uploadRes);
if (uploadRes.statusCode === 200) {
try {
const data = JSON.parse(uploadRes.data);
console.log('上传到本地服务器图片数据',data);
if (data && data.data && data.data.url) {
// 更新头像
that.updateAvatar(data.data.url);
} else {
uni.showToast({
title: '上传失败',
icon: 'none'
});
}
} catch (e) {
uni.showToast({
title: '解析响应数据失败',
icon: 'none'
});
}
} else {
uni.showToast({
title: '上传失败',
icon: 'none'
});
}
},
fail: (err) => {
uni.hideLoading();
uni.showToast({
title: '上传失败',
icon: 'none'
});
console.error('上传失败', err);
}
});
},
// 上传图片到阿里云OSS
uploadImageToAliyunOSS(filePath) {
const that = this;
uni.uploadFile({
url: requestUrl.http + `/api/chat/uploadAliyun`,
filePath: filePath,
name: 'img',
formData: {
token: this.me.token,
imageClassId: 0
},
header: {
'token': this.me.token
},
success: (uploadRes) => {
uni.hideLoading();
console.log('上传到阿里云OSS图片结果',uploadRes);
if (uploadRes.statusCode === 200) {
try {
const data = JSON.parse(uploadRes.data);
if (data && data.data && data.data.length && data.data[0].url) {
// 更新头像
that.updateAvatar(data.data[0].url);
} else {
uni.showToast({
title: '上传失败',
icon: 'none'
});
}
} catch (e) {
uni.showToast({
title: '解析响应数据失败',
icon: 'none'
});
}
} else {
uni.showToast({
title: '上传失败',
icon: 'none'
});
}
},
fail: (err) => {
uni.hideLoading();
uni.showToast({
title: '上传失败',
icon: 'none'
});
console.error('上传失败', err);
}
});
},
// 更新头像到服务器
updateAvatar(avatarUrl) {
const that = this;
uni.$u.http.post(requestUrl.http + '/api/chat/updateUserinfo', {
fieldname: 'avatar',
fieldValue: avatarUrl
}, {
header: {
token: this.me.token
}
}).then(res => {
// 更新本地数据
that.chatpageset.avatar = avatarUrl.startsWith('http') ? avatarUrl : requestUrl.http + avatarUrl;
that.me.avatar = avatarUrl.startsWith('http') ? avatarUrl : requestUrl.http + avatarUrl;
// 更新vuex中的用户信息
that.$store.dispatch('regloginAction', that.me);
uni.showToast({
title: '头像更新成功',
icon: 'success'
});
// 在剪切头像页面返回上一页
setTimeout(()=>{
uni.navigateBack({
delta: 1
});
},1000);
}).catch(err => {
uni.showToast({
title: err.data.data || '更新失败',
icon: 'none'
});
});
},
// 计算两点之间的距离
getDistance(p1, p2) {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
return Math.sqrt(dx * dx + dy * dy);
},
// 计算两点之间的中心点
getMidpoint(p1, p2) {
return {
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2
};
},
// 触摸事件处理(实现拖拽和缩放功能)
touchStart(e) {
if (!this.cropperReady) return;
// 单指触摸 - 拖拽
if (e.touches.length === 1) {
this.startX = e.touches[0].clientX;
this.startY = e.touches[0].clientY;
this.initialOffsetX = this.offsetX;
this.initialOffsetY = this.offsetY;
this.isPinching = false;
}
// 双指触摸 - 缩放
else if (e.touches.length === 2) {
const touch1 = { x: e.touches[0].clientX, y: e.touches[0].clientY };
const touch2 = { x: e.touches[1].clientX, y: e.touches[1].clientY };
this.initialDistance = this.getDistance(touch1, touch2);
this.initialScale = this.scale;
this.isPinching = true;
// 计算缩放中心点
const midpoint = this.getMidpoint(touch1, touch2);
this.scaleCenterX = midpoint.x;
this.scaleCenterY = midpoint.y;
}
},
touchMove(e) {
if (!this.cropperReady) return;
// 单指触摸 - 拖拽
if (e.touches.length === 1 && !this.isPinching) {
if (!this.startX) return;
const moveX = e.touches[0].clientX - this.startX;
const moveY = e.touches[0].clientY - this.startY;
this.offsetX = this.initialOffsetX + moveX;
this.offsetY = this.initialOffsetY + moveY;
// 限制移动范围
const scaledWidth = this.imageWidth * this.scale;
const scaledHeight = this.imageHeight * this.scale;
const maxX = Math.max(0, (scaledWidth - this.canvasWidth) / 2);
const maxY = Math.max(0, (scaledHeight - this.canvasHeight) / 2);
this.offsetX = Math.min(Math.max(this.offsetX, -maxX), maxX);
this.offsetY = Math.min(Math.max(this.offsetY, -maxY), maxY);
this.drawImage();
}
// 双指触摸 - 缩放
else if (e.touches.length === 2) {
const touch1 = { x: e.touches[0].clientX, y: e.touches[0].clientY };
const touch2 = { x: e.touches[1].clientX, y: e.touches[1].clientY };
const currentDistance = this.getDistance(touch1, touch2);
if (this.initialDistance > 0) {
// 计算缩放比例
let newScale = this.initialScale * (currentDistance / this.initialDistance);
// 限制缩放范围
newScale = Math.min(Math.max(newScale, this.minScale), this.maxScale);
// 计算缩放中心点在图片上的位置
const scaledWidth = this.imageWidth * this.scale;
const scaledHeight = this.imageHeight * this.scale;
// 计算缩放中心点在图片上的相对位置
const imgX = (this.scaleCenterX - this.canvasWidth / 2 - this.offsetX) / this.scale;
const imgY = (this.scaleCenterY - this.canvasHeight / 2 - this.offsetY) / this.scale;
// 更新缩放比例
this.scale = newScale;
// 计算新的偏移量,使缩放中心点保持不变
const newScaledWidth = this.imageWidth * this.scale;
const newScaledHeight = this.imageHeight * this.scale;
this.offsetX = this.scaleCenterX - this.canvasWidth / 2 - imgX * this.scale;
this.offsetY = this.scaleCenterY - this.canvasHeight / 2 - imgY * this.scale;
// 限制移动范围
const maxX = Math.max(0, (newScaledWidth - this.canvasWidth) / 2);
const maxY = Math.max(0, (newScaledHeight - this.canvasHeight) / 2);
this.offsetX = Math.min(Math.max(this.offsetX, -maxX), maxX);
this.offsetY = Math.min(Math.max(this.offsetY, -maxY), maxY);
this.drawImage();
}
}
},
touchEnd(e) {
this.startX = 0;
this.startY = 0;
// 如果只剩下一根手指,结束缩放
if (e.touches.length === 0 && this.isPinching) {
this.isPinching = false;
this.initialDistance = 0;
}
}
},
}
# 6. 关于修改我的头像昵称等信息几个文件完整代码
提供完整代码,供大家参考
# ① 页面 /pages/setpageInfo/setpageInfo.vue
<template>
<view>
<!-- 导航栏 -->
<chat-navbar v-if="setdata.action != 'avatarCut'"
:title="title" :fixed="true" :showPlus="false" :showUser="false" :showBack="true"
navbarClass="bg-white" :h5WeiXinNeedNavbar="false">
</chat-navbar>
<!-- 内容 -->
<view>
<!-- 聊天相关 -->
<view v-if="setdata.action == 'chatset'">
<!-- 游客(未登录用户) -->
<u-cell-group title="游客(未登录用户)" :border="false"
:customStyle="{'background-color':'#eeeeee'}">
<u-cell v-if="setdata.visitor.list.length"
:customStyle="{'background-color':'#ffffff'}">
<view slot="title" class="flex align-center border">
<view class="flex justify-center py-2 flex-1"
v-for="(item,index) in setdata.visitor.list" :key="index"
:class="[setdata.visitor.current == index ?
'bg-success text-white' : 'bg-light']"
@click="setdata.visitor.current = index">
<text style="font-size: 24rpx;">{{item.title}}</text>
</view>
</view>
</u-cell>
</u-cell-group>
<!-- 登录用户(非好友) -->
<u-cell-group title="登录用户(非好友)" :border="false"
:customStyle="{'background-color':'#eeeeee'}">
<u-cell v-if="setdata.user.list.length"
:customStyle="{'background-color':'#ffffff'}">
<view slot="title" class="flex align-center border">
<view class="flex justify-center py-2 flex-1"
v-for="(item,index) in setdata.user.list" :key="index"
:class="[setdata.user.current == index ?
'bg-success text-white' : 'bg-light']"
@click="setdata.user.current = index">
<text style="font-size: 24rpx;">{{item.title}}</text>
</view>
</view>
</u-cell>
</u-cell-group>
<!-- 提交设置 -->
<view class="p-3">
<u-button type="primary" text="确 定" @click="submitSet"
:loading="btn.loading" :loadingText="btn.loadingText"
:disabled="btn.disabled"></u-button>
</view>
</view>
<!-- 聊天页设置 -->
<view v-if="setdata.action == 'chatpageset'">
<view v-if="chatpageset.id && chatpageset.chatType">
<!-- 头像部分 -->
<view class="flex flex-wrap p-3">
<!-- 单聊 -->
<view v-if="chatpageset.chatType == 'single'"
class="flex flex-column align-center justify-center mr-2">
<u-avatar :src="chatpageset|avatarShow" shape="square"
customStyle="border:1px solid #eeeeee;" :size="36"></u-avatar>
<text class="font-small mt-1 text-muted">{{chatpageset.name}}</text>
</view>
<!-- 群聊 循环头像昵称 -->
<view v-if="chatpageset.chatType == 'group'"
class="flex flex-row flex-wrap">
<view v-for="(item,index) in chatpageset.users" :key="index"
class="flex flex-column align-center justify-center mb-2 mr-1">
<u-avatar :src="item|avatarShow" shape="square"
customStyle="border:1px solid #eeeeee;" :size="36"></u-avatar>
<text class="font-small mt-1 text-muted">{{item|groupusername}}</text>
</view>
</view>
<!-- 加入群聊 -->
<view class="border flex align-center justify-center"
style="width: 36px;height: 36px;border-style: dashed;"
@click="clickCell('选择好友', 'addUserToGroup')">
<u-icon name="plus" size="24" color="#999999"></u-icon>
</view>
<!-- 删除群聊用户 -->
<view v-if="chatpageset.chatType == 'group'"
class="border flex align-center justify-center ml-1"
style="width: 36px;height: 36px;border-style: dashed;"
@click="clickCell('群组删除某个用户', 'deleteUserFromGroup')">
<u-icon name="minus" size="24" color="#999999"></u-icon>
</view>
</view>
<u-gap height="10" bgColor="#EDEDED"></u-gap>
<!-- 群相关设置 -->
<view v-if="chatpageset.chatType == 'group'">
<!-- 群二维码 -->
<u-cell-group>
<u-cell title="群二维码"
:border="false" clickable isLink
:customStyle="customStyles"
:iconStyle="iconStyles"
:titleStyle="titleStyles"
@click="clickCell('群二维码', 'groupQrcode')"></u-cell>
</u-cell-group>
<u-gap height="10" bgColor="#EDEDED"></u-gap>
<!-- 我在群里面的昵称 -->
<u-cell-group>
<u-cell>
<view slot="title">
<view class="mb-2">
<text class="font-weight-bold">我在群里面的昵称</text>
</view>
<u-input v-model="chatpageset.nickname"
border="none" :maxlength="20"
:adjustPosition="false" clearable
placeholder="群昵称最多20个字"
@confirm="groupnickname"
customStyle="padding:20rpx;"></u-input>
</view>
</u-cell>
</u-cell-group>
<u-gap height="10" bgColor="#EDEDED"></u-gap>
<!-- 群名称 -->
<u-cell-group>
<u-cell>
<view slot="title">
<view class="mb-2">
<text class="font-weight-bold">群名称</text>
</view>
<u-input v-model="chatpageset.name"
border="none" :maxlength="20"
:adjustPosition="false" clearable
placeholder="群名称最多20个字"
@confirm="groupUpdateName"
:disabled="!chatpageset.user_id"
customStyle="padding:20rpx;"></u-input>
</view>
</u-cell>
</u-cell-group>
<u-gap height="10" bgColor="#EDEDED"></u-gap>
<!-- 群公告 -->
<u-cell-group>
<u-cell>
<view slot="title">
<view class="mb-2">
<text class="font-weight-bold">群公告</text>
</view>
<u-textarea v-model="chatpageset.remark"
border="none" :maxlength="500"
:adjustPosition="false"
placeholder="群公告设置"
count autoHeight
:disabled="!chatpageset.user_id"
@confirm="groupremark"></u-textarea>
</view>
</u-cell>
</u-cell-group>
<u-gap height="10" bgColor="#EDEDED"></u-gap>
</view>
<!-- 查找聊天内容 -->
<u-cell-group>
<u-cell title="查找聊天内容"
:border="false" clickable isLink
:customStyle="customStyles"
:iconStyle="iconStyles"
:titleStyle="titleStyles"
@click="clickCell('查找聊天内容', 'searhChatData')"></u-cell>
</u-cell-group>
<u-gap height="10" bgColor="#EDEDED"></u-gap>
<!-- 消息设置 -->
<u-cell-group>
<u-cell v-if="chatpageset.chatType == 'group'"
title="是否显示群成员昵称"
:border="false" clickable
:customStyle="customStyles"
:iconStyle="iconStyles"
:titleStyle="titleStyles">
<view slot="value">
<u-switch v-model="chatpageset.shownickname"
@change="changeSwitch('shownickname')"></u-switch>
</view>
</u-cell>
<u-cell title="消息免打扰"
:border="false" clickable
:customStyle="customStyles"
:iconStyle="iconStyles"
:titleStyle="titleStyles">
<view slot="value">
<u-switch v-model="chatpageset.nowarn" @change="changeSwitch('nowarn')"></u-switch>
</view>
</u-cell>
<u-cell title="置顶聊天"
:border="false" clickable
:customStyle="customStyles"
:iconStyle="iconStyles"
:titleStyle="titleStyles">
<view slot="value">
<u-switch v-model="chatpageset.istop" @change="changeSwitch('istop')"></u-switch>
</view>
</u-cell>
<u-cell title="提醒"
:border="false" clickable
:customStyle="customStyles"
:iconStyle="iconStyles"
:titleStyle="titleStyles">
<view slot="value">
<u-switch v-model="chatpageset.stongwarn" @change="changeSwitch('stongwarn')"></u-switch>
</view>
</u-cell>
</u-cell-group>
<u-gap height="10" bgColor="#EDEDED"></u-gap>
<u-cell-group>
<u-cell title="清空聊天记录"
:border="false" clickable isLink
:customStyle="customStyles"
:iconStyle="iconStyles"
:titleStyle="titleStyles"
@click="clickCell('清空聊天记录', 'deleteChatData')"></u-cell>
</u-cell-group>
<u-gap height="10" bgColor="#EDEDED"></u-gap>
<!-- 解散群或退群或者删除好友 -->
<view>
<view class="p-3">
<u-button type="primary" plain :text="deleteOrQuitGroup"
@click="deleteOrQuitGroupfn"></u-button>
</view>
<u-gap height="10" bgColor="#EDEDED"></u-gap>
</view>
</view>
</view>
<!-- 群二维码 -->
<view v-if="setdata.action == 'groupQrcode'">
<view class="m-3 bg-light p-3">
<!-- 群名称 -->
<view class="mb-3 flex align-center justify-center p-1">
<text class="text-muted">群名称:{{chatpageset.name}}</text>
</view>
<!-- 群二维码 -->
<view class="mb-3 flex align-center justify-center">
<view class="p-1 border bg-white flex align-center justify-center">
<image :src="chatpageset.src"
style="width: 600rpx;height: 600rpx;"></image>
</view>
</view>
<!-- 提示 -->
<view class="mb-3 flex align-center justify-center">
<text class="text-muted">{{chatpageset.sysinfo}}</text>
</view>
</view>
</view>
<!-- 扫码之后的页面 -->
<view v-if="setdata.action == 'autoAddGroup'">
<!-- 群头像及介绍 -->
<view class="m-3 bg-light p-3 flex flex-row align-center">
<!-- 群头像 -->
<view v-if="avatar && avatar.length"
style="width: 150rpx;" class="flex align-center justify-center position-relative">
<!-- 一张 -->
<view v-if="avatar.length == 1">
<u--image :src="avatar[0]" mode="widthFix"
width="90rpx" height="90rpx" radius="8rpx"></u--image>
</view>
<!-- 多张 -->
<view v-else style="width: 90rpx;background-color: #eeeeee;"
class="rounded">
<!-- 2张 3张 4张 -->
<view v-if="avatar.length == 2 || avatar.length == 3 || avatar.length == 4"
class="flex flex-row flex-wrap align-center justify-center"
style="height: 90rpx;align-content: center;">
<u-avatar v-for="(v,k) in avatar" :key="k"
:src="v" shape="square" size="45rpx"></u-avatar>
</view>
<!-- 5张到9张 -->
<view v-if="avatar.length >= 5"
class="flex flex-row flex-wrap align-center justify-center"
style="height: 90rpx;align-content: center;">
<u-avatar v-for="(v,k) in avatar" :key="k"
:src="v" shape="square" size="30rpx"></u-avatar>
</view>
</view>
</view>
<!-- 群名称及介绍 -->
<view class="flex flex-column justify-center ml-2">
<text class="mb-1 u-line-1">{{chatpageset.name}}</text>
<text class="text-muted mb-1 font-sm">{{chatpageset.users.length}}人加入</text>
</view>
</view>
<!-- 加群按钮 -->
<view class="p-3">
<u-button text="加入群聊" type="success" @click="addGroupfn"></u-button>
</view>
<!-- 群公告 -->
<view v-if="chatpageset.remark"
class="p-3 flex flex-column">
<text class="font mb-2">群介绍</text>
<view class="bg-light p-3"
style="text-align: justify;">
<text class="font-sm">{{chatpageset.remark}}</text>
</view>
</view>
<!-- h5端页面在微信打开没有导航栏给它一个底部导航栏 -->
<view v-if="h5weixin">
<u-tabbar
:value="0"
:fixed="true"
:placeholder="true"
:safeAreaInsetBottom="true"
>
<u-tabbar-item text="首页" icon="home"
@click="clickCell('首页','switchTab', '/')"></u-tabbar-item>
<u-tabbar-item text="消息" icon="chat" :badge="1"
@click="clickCell('消息','switchTab', '/pages/xiaoxi/xiaoxi')"></u-tabbar-item>
<u-tabbar-item text="我的" icon="account"
@click="clickCell('我的','navigateTo', '/pages/wode/wode')"></u-tabbar-item>
</u-tabbar>
</view>
</view>
<!-- 账号信息设置 -->
<view v-if="setdata.action == 'userinfoSet'">
<!-- 账号(不可修改) -->
<u-cell-group>
<u-cell>
<view slot="title">
<view class="mb-2">
<text class="font-weight-bold">账号(不可修改)</text>
</view>
<u-input v-model="chatpageset.username"
border="none" :maxlength="20"
:adjustPosition="false" clearable disabled
customStyle="padding:20rpx;"></u-input>
</view>
</u-cell>
</u-cell-group>
<u-gap height="10" bgColor="#EDEDED"></u-gap>
<!-- 昵称 -->
<u-cell-group>
<u-cell>
<view slot="title">
<view class="mb-2">
<text class="font-weight-bold">昵称</text>
</view>
<u-input v-model="chatpageset.nickname"
border="none" :maxlength="20"
:adjustPosition="false" clearable
placeholder="昵称最多20个字"
@confirm="updateUserInfo('nickname')"
customStyle="padding:20rpx;"></u-input>
</view>
</u-cell>
</u-cell-group>
<u-gap height="10" bgColor="#EDEDED"></u-gap>
<!-- 头像 -->
<u-cell-group>
<u-cell>
<view slot="title">
<view class="mb-2">
<text class="font-weight-bold">头像</text>
</view>
<u-avatar :src="chatpageset.avatar" shape="square"
customStyle="border:1px solid #eeeeee;" :size="36"
@click="updateUserInfo('avatar')"></u-avatar>
</view>
</u-cell>
</u-cell-group>
<u-gap height="10" bgColor="#EDEDED"></u-gap>
<!-- ActionSheet 操作菜单 -->
<u-action-sheet :actions="actionSheetList"
title="有以下方式供您更换头像" :show="actionSheetShow"
safeAreaInsetBottom
@select="selectActionSheet"
@close="actionSheetShow = false"></u-action-sheet>
</view>
<!-- 头像剪裁500rpx * 500rpx -->
<view v-if="setdata.action == 'avatarCut'"
class="flex flex-column"
style="height: 100vh; background-color: rgba(0,0,0,0.9);">
<!-- 导航栏 -->
<view class="position-fixed left-0 right-0 top-0">
<chat-navbar
:title="title" :fixed="true" :showPlus="false"
:showUser="false" :showBack="true"
navbarClass="bg-black text-white" :h5WeiXinNeedNavbar="false">
</chat-navbar>
</view>
<!-- 剪裁区域 -->
<view class="flex-1 flex justify-center align-center"
:style="{paddingTop: navBarHeight + 'px', paddingBottom: '100rpx'}">
<!-- 剪裁窗口,固定大小500rpx * 500rpx -->
<view class="position-relative"
style="width: 500rpx; height: 500rpx;">
<canvas canvas-id="avatarCanvas" id="avatarCanvas"
class="position-absolute"
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px', left: '0', top: '0' }"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"></canvas>
<!-- 剪裁框 -->
<view class="position-absolute"
style="width: 500rpx; height: 500rpx; left: 0; top: 0; box-sizing: border-box; border: 1px solid #fff; pointer-events: none;">
<!-- 辅助线 -->
<view class="position-absolute"
style="width: 1px; height: 100%; left: 33.33%; background-color: rgba(255,255,255,0.4);"></view>
<view class="position-absolute"
style="width: 1px; height: 100%; left: 66.66%; background-color: rgba(255,255,255,0.4);"></view>
<view class="position-absolute"
style="width: 100%; height: 1px; top: 33.33%; background-color: rgba(255,255,255,0.4);"></view>
<view class="position-absolute"
style="width: 100%; height: 1px; top: 66.66%; background-color: rgba(255,255,255,0.4);"></view>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="flex justify-between align-center px-5 position-fixed left-0 right-0 bottom-0 bg-black"
style="height: 100rpx; z-index: 999; bottom: 180rpx;">
<text class="text-white" style="font-size: 32rpx;" @click="cancelCrop">取消</text>
<text class="text-white" style="font-size: 32rpx;" @click="confirmCrop">确定</text>
</view>
</view>
</view>
</view>
</template>
<script>
import toolJs from '@/common/mixins/tool.js';
import {requestUrl} from '@/common/mixins/configData.js';
import {mapState,mapGetters,mapMutations,mapActions} from 'vuex';
import updateUserInfoJs from '@/pages/setpageInfo/updateUserInfo.js';
import avatarCutJs from '@/pages/setpageInfo/avatarCut.js';
export default {
mixins:[toolJs,updateUserInfoJs,avatarCutJs],
data() {
return {
title:'',
setdata:{
visitor:{},
user:{},
},
btn:{
loading:false,
loadingText:'正在提交中',
disabled:false,
},
chatpageset:{
users:[],
avatar:'',
},
}
},
onLoad(e) {
console.log(e);
// if(!e.action || !this.me || this.me.role == 'visitor') return this.navigateBack();
if(!e.action || !this.me) return this.navigateBack();
if(e.title){
this.title = decodeURIComponent(e.title);
uni.setNavigationBarTitle({
title:this.title,
});
}
// 动态生成数据
if(e.action){
this.setdata.action = e.action;
if(this.setdata.action == 'chatset'){
// 针对聊天的设置信息
this.setdata.visitor = {
list:[
{ title: '禁止聊天(先登录)', value:0 },
{ title: '可以向我发一条消息', value:1 },
{ title: '可以聊天', value:2 },
],
current:1,
};
this.setdata.user = {
list:[
{ title: '禁止聊天(先加好友)', value:0 },
{ title: '可以向我发一条消息', value:1 },
{ title: '可以聊天', value:2 },
],
current:1,
};
// 获取用户的设置信息
uni.$u.http.get(requestUrl.http + `/api/userinfo/${this.me.uuid}`).then(res => {
console.log('获取用户的设置信息结果', res);
let userset = res.data.data.userset;
if(userset){
userset = JSON.parse(userset);
console.log('userset转对象的结果',userset);
this.setdata.visitor.current = userset.chatset.visitor.sendCount;
this.setdata.user.current = userset.chatset.user.sendCount;
}
})
}else if(this.setdata.action == 'chatpageset'){
this.chatpageset.id = e.id;
this.chatpageset.chatType = e.chatType;
this.chatpageset.avatar = decodeURIComponent(e.avatar);
this.chatpageset.name = decodeURIComponent(e.name);
// 聊天页设置相关信息获取
let getChatPageSet = this.chatClass.getChatPageSet(this.chatpageset.id, this.chatpageset.chatType);
console.log('聊天页设置相关信息获取',getChatPageSet);
// 设置相关
this.chatpageset = {
// 单聊
...this.chatpageset,
istop: getChatPageSet && getChatPageSet.istop ? true : false,
shownickname: getChatPageSet && getChatPageSet.shownickname ? true : false,
nowarn: getChatPageSet && getChatPageSet.nowarn ? true : false,
stongwarn: getChatPageSet && getChatPageSet.stongwarn ? true : false,
// 群聊还有以下字段
user_id: 0,
remark: '',
invite_confirm: 0,
}
console.log('设置相关', this.chatpageset);
// 群聊设置
if(this.chatpageset.chatType == 'group'){
this.getgroupinfo();
}
}else if(this.setdata.action == 'groupQrcode'){
this.chatpageset.id = e.id;
this.chatpageset.chatType = e.chatType;
this.chatpageset.avatar = decodeURIComponent(e.avatar);
this.chatpageset.name = decodeURIComponent(e.name);
console.log('二维码页面信息',this.chatpageset);
// 获取二维码
// #ifdef H5
// 二维码返回的地址举例:http://192.168.2.6:8081/#/pages/setpageInfo/setpageInfo?action=autoAddGroup&title=群介绍&id=33&chatType=group
// 生成二维码
// 得的当前完整的网址
const fullURL = window.location.href;
// 截取#之前的部分
const http = fullURL.split('#')[0];
this.chatpageset.src = requestUrl.http + `/api/chat/groupQrcode/${this.chatpageset.id}?token=${this.me.token}&type=H5&http=${encodeURIComponent(http)}&chatType=${this.chatpageset.chatType}`;
// #endif
// #ifdef APP || MP
this.chatpageset.src = requestUrl.http + `/api/chat/groupQrcode/${this.chatpageset.id}?token=${this.me.token}`;
// #endif
//二维码提示说明
let sysinfo = uni.getSystemInfoSync().appName;
// #ifdef H5
this.chatpageset.sysinfo = `使用微信或者手机浏览器扫码加群`;
// #endif
// #ifdef MP
this.chatpageset.sysinfo = `使用${sysinfo}小程序扫码加群`;
// #endif
// #ifdef APP
this.chatpageset.sysinfo = `使用${sysinfo}APP扫码加群`;
// #endif
}else if(this.setdata.action == 'autoAddGroup'){
console.log('扫码之后打开的地址');
this.chatpageset.id = e.id;
this.chatpageset.chatType = e.chatType;
// 获取群资料
if(this.chatpageset.chatType == 'group'){
this.getgroupinfo();
}
}else if(this.setdata.action == 'userinfoSet'){
console.log('账号信息设置', this.me);
this.chatpageset = this.me;
}else if(this.setdata.action == 'avatarCut'){
this.tempFilePath = decodeURIComponent(e.tempFilePath);
this.title = decodeURIComponent(e.title);
uni.setNavigationBarTitle({
title: this.title,
});
// 初始化剪裁器
this.$nextTick(() => {
setTimeout(() => {
this.initCropper();
}, 300);
});
}
}
},
filters:{
//头像
avatarShow(item){
if(item){
if(item.avatar){
return item.avatar.startsWith('http') ? item.avatar : `${requestUrl.http}${item.avatar}`;
}
return item.user.avatar.startsWith('http') ? item.user.avatar : `${requestUrl.http}${item.user.avatar}`;
}
return ``;
},
// 群用户称呼
groupusername(item){
if(item){
if(item.nickname){
return item.nickname.length > 3 ? item.nickname.substring(0,3) + `...` : item.nickname;
}
if(item.user.nickname){
return item.user.nickname.length > 3 ? item.user.nickname.substring(0,3) + `...` :
item.user.nickname;
}
if(item.user.username){
return item.user.username.length > 3 ? item.user.username.substring(0,3) + `...` :
item.user.username;
}
}
return ``;
}
},
computed:{
...mapState({
me : state => state.Chatuser.regloginUser,
chatClass : state => state.Chatuser.chatClass,
}),
customStyles(){
return `padding-top:20rpx;padding-bottom:20rpx`;
},
iconStyles(){
return `font-size:52rpx;margin-right:30rpx;color:#5696da`;
},
titleStyles(){
return `font-size:32rpx;`;
},
// 解散群或退群
deleteOrQuitGroup(){
if(this.chatpageset.chatType == 'group'){
return this.chatpageset.user_id ? '解散该群' : '退出该群';
}else if(this.chatpageset.chatType == 'single'){
return '删除该好友';
}
return ``;
},
// 群头像
avatar(){
if(this.chatpageset && this.chatpageset.avatar){
let arr = this.chatpageset.avatar.split(',');
arr = arr.map(v => {
if(v.startsWith('http')){
return v;
}else{
return requestUrl.http + v;
}
});
// console.log('头像',arr);
return arr;
}
return [];
},
// h5端在微信打开
h5weixin(){
return this.isWeixinBrowser();
},
},
methods: {
// 加入群聊
addGroupfn(){
console.log('加入群聊,看管理员怎么设置的');
},
// 解散群或者退群或者删除好友
deleteOrQuitGroupfn(){
if(this.chatpageset.chatType == 'group'){
const arg = {
apiurl: '/api/chat/groupDeleteOrQuit',
data: {
id: this.chatpageset.id,
},
};
this.groupUpdate(arg).then(res => {
uni.showToast({title:'操作成功', icon:'none'});
if(this.chatpageset.user_id){
return this.navigateBack();
}else{
uni.switchTab({
url:'/pages/xiaoxi/xiaoxi'
});
}
});
}else if(this.chatpageset.chatType == 'single'){
console.log('删除好友');
uni.showModal({
content:`确定删除这个好友吗?`,
success: (res) => {
if(res.confirm){
// 执行删除
uni.$u.http.post(requestUrl.http +
`/api/chat/deletegoodfriend/${this.chatpageset.id}`,{},{
header:{
token: this.me.token,
}
}).then(res=>{
uni.showToast({title:res.data.data, icon:'none'});
// 消息页清除聊天记录
this.chatClass.clearChatInfo(this.chatpageset.id,
this.chatpageset.chatType).then(()=>{
// 清空完成之后聊天页的数据也要清空
uni.$emit('clearChatInfo');
});
// 调转消息页
setTimeout(()=>{
uni.switchTab({
url:'/pages/xiaoxi/xiaoxi'
});
},2000);
}).catch(err=>{
uni.showToast({title:err.data.data, icon:'none'});
});
}
}
});
}
},
// 获取群资料
getgroupinfo(){
uni.$u.http.get(requestUrl.http + `/api/chat/groupinfo/${this.chatpageset.id}`, {
header: {
token: this.me.token,
},
}).then(res => {
console.log('服务器返回群资料', res);
let info = res.data.data;
this.chatpageset.user_id = info.user_id;
this.chatpageset.remark = info.remark;
this.chatpageset.invite_confirm = info.invite_confirm;
this.chatpageset.users = info.group_users;
//console.log('显示到页面的群资料',this.chatpageset);
this.chatpageset.user_id = this.chatpageset.user_id == this.me.id ? true : false;
// 我在群里面的昵称
let me_index = this.chatpageset.users.findIndex(v => v.user_id == this.me.id);
if(me_index != -1){
this.chatpageset.nickname = this.chatpageset.users[me_index].nickname;
}
// 群头像
this.chatpageset.avatar = info.avatar;
// 群名称
this.chatpageset.name = info.name;
console.log('显示到页面的群资料',this.chatpageset);
}).catch(err =>{
this.navigateBack();
});
},
// 修改我在群里面的昵称
groupnickname(){
const arg = {
apiurl: '/api/chat/groupnickname',
data: {
id: this.chatpageset.id,
nickname: this.chatpageset.nickname,
},
};
this.groupUpdate(arg).then(res => {
uni.showToast({title:'群昵称设置成功', icon:'none'});
this.getgroupinfo();
});
},
// 更新群公告
groupremark(){
const arg = {
apiurl: '/api/chat/groupremark',
data: {
id: this.chatpageset.id,
remark: this.chatpageset.remark,
},
};
this.groupUpdate(arg).then(res => {
uni.showToast({title:'群公告修改成功', icon:'none'});
});
},
// 更新群名称
groupUpdateName(){
const arg = {
apiurl: '/api/chat/groupUpdateName',
data: {
id: this.chatpageset.id,
name: this.chatpageset.name,
},
};
this.groupUpdate(arg).then(res => {
uni.showToast({title:'群名称修改成功', icon:'none'});
// 使用vuex兼容H5端nvue页面标题
this.$store.dispatch('groupUpdateName',{
name: this.chatpageset.name
});
});
},
// 更新群信息
groupUpdate(arg){
return new Promise((resolve,reject) => {
uni.$u.http.post(requestUrl.http + `${arg.apiurl}`, arg.data , {
header: {
token: this.me.token,
},
}).then(res => {
console.log('服务器返回群名称更新结果', res);
// if(res.data.data == 'ok'){
// resolve();
// }
resolve(res);
}).catch(err =>{
uni.showModal({
content:err.data.data,
showCancel:false,
confirmText:'我知道了'
});
});
});
},
// 点击开关
changeSwitch(key){
console.log(`点击开关${key}`,this.chatpageset[key]);
console.log('通过开关设置单聊或者群聊信息',this.chatpageset);
this.chatClass.updateSomeOneChatItem({
id: this.chatpageset.id,
chatType: this.chatpageset.chatType,
}, this.chatpageset).then(res =>{
console.log('设置之后的数据', res);
// 方式一: 全局通知聊天页
// 方式二:直接在聊天页获取消息页的信息进行处理
});
},
clickCell(title, type, url = ''){
console.log(title, type);
if(type == 'addUserToGroup'){
// 单独建群
if(this.chatpageset.chatType == 'single'){
let op = {
action: 'addUserToGroup',
title: encodeURIComponent(title),
};
uni.navigateTo({
url: `/pages/friendsList/friendsList?action=${op.action}&title=${op.title}`,
});
}else if(this.chatpageset.chatType == 'group'){
console.log('往当前的群里面加人');
}
}else if(type == 'groupQrcode'){
console.log('打开群二维码',this.chatpageset);
let op = {
action: 'groupQrcode',
title: encodeURIComponent(title),
id: this.chatpageset.id,
name: encodeURIComponent(this.chatpageset.name),
avatar: encodeURIComponent(this.chatpageset.avatar),
chatType: this.chatpageset.chatType,
};
uni.redirectTo({
url:`/pages/setpageInfo/setpageInfo?action=${op.action}&title=${op.title}&id=${op.id}&name=${op.name}&avatar=${op.avatar}&chatType=${op.chatType}`,
});
}else if(type == 'switchTab' || type == 'navigateTo'){
uni[type]({
url: url,
});
}else if(type == 'deleteChatData'){
uni.showModal({
content:'是否清空聊天记录?',
success: (res) => {
if(res.confirm){
this.chatClass.clearChatInfo(this.chatpageset.id,
this.chatpageset.chatType).then(()=>{
uni.showToast({title:'清空成功',icon:'success'});
// 清空完成之后聊天页的数据也要清空
uni.$emit('clearChatInfo');
});
}
}
})
}
},
submitSet(){
let userset = {};
// 聊天设置
if(this.setdata.action == 'chatset'){
const chatset = {
visitor: {
sendCount:this.setdata.visitor.list[this.setdata.visitor.current].value,
needFollow:false,
},
user:{
sendCount:this.setdata.user.list[this.setdata.user.current].value,
needFollow:false,
}
}
userset.chatset = chatset;
}
console.log('提交给服务器的设置数据',userset);
this.btn.loading = true;
this.btn.disabled = true;
uni.$u.http.post(requestUrl.http + `/api/chat/userset`, {
userset: JSON.stringify(userset),
}, {
header: {
token: this.me.token,
},
}).then(res => {
console.log('服务器返回聊天设置的结果', res);
this.btn.loading = false;
if(res.data.data == 'ok'){
uni.showToast({
title: '设置成功',
icon: 'none',
duration: 1000
});
setTimeout(() => {
return this.navigateBack();
}, 1000);
}
}).catch(err => {
this.btn.loading = false;
if (err.data && err.data.data) {
uni.showToast({
title: err.data.data,
icon: 'none',
duration: 5000
});
}
});
}
}
}
</script>
<style>
</style>
# ② 混入文件 /pages/setpageInfo/updateUserInfo.js
import UniPermission from '@/common/mixins/uni_permission.js';
import { requestUrl, uploadfileSaveType } from '@/common/mixins/configData.js';
export default {
data(){
return {
// 更换头像是拍照还是从相册选择一张
actionSheetList:[
{
name:'相册',
subname:"可从相册选择一张图片来当头像",
color:'#1BA035',
fontSize:'20',
eventType:'photo',
},
{
name:'拍一张',
subname:"可拍摄一张照片当头像",
color:'#1BA035',
fontSize:'20',
eventType:'cameraPhoto',
},
],
actionSheetShow:false,
}
},
methods: {
// 修改我的账号信息
updateUserInfo(fieldname) {
let arg = {};
switch (fieldname) {
case 'nickname':
arg = {
apiurl: '/api/chat/updateUserinfo',
data: {
fieldname: fieldname,
fieldValue: this.chatpageset.nickname,
},
};
// 我的信息更新一下
this.me[fieldname] = this.chatpageset.nickname;
break;
case 'avatar':
// 因为换图片需要从相册选择图片或者拍照,先看是否有权限
return this.getUniPermission();
break;
}
// 服务器处理
this.groupUpdate(arg).then(res => {
// 成功处理
uni.showToast({
title: res.data.data,
icon: 'none'
});
// 更新我在本地的信息chatuser及vuex
this.$store.dispatch('regloginAction', this.me);
});
},
// 因为换图片需要从相册选择图片或者拍照,先看是否有权限
getUniPermission(){
console.log('弹窗选择是拍照还是相册选择');
this.actionSheetShow = true;
},
// 点击了ActionSheet 操作菜单
async selectActionSheet(item){
this.actionSheetShow = false;
console.log('点击了ActionSheet 操作菜单', item);
switch (item.eventType){
case 'photo':
await this.handlePermission({
permission:{
name:'photo',
title:'本功能需要您打开相册',
desc:'需要访问您的相册来选择图片',
grantedText:'用户已授权开启相册,可以选择照片了',
nograntedText:'您没有授权开启相册,无法发图片',
},
methodsName:'chooseImage',
});
break;
case 'cameraPhoto':
await this.handlePermission({
permission:{
name:'camera',
title:'本功能需要您打开摄像头',
desc:'需要访问您的摄像头来拍摄照片',
grantedText:'用户已授权打开摄像头,可以拍摄照片了',
nograntedText:'您没有授权打开摄像头,无法拍摄照片',
},
methodsName:'cameraPhoto',
});
break;
}
},
// 发相册图片|发相册视频 权限申请
async handlePermission(options){
try{
const permission = new UniPermission();
const granted = await permission.requestPermission(options.permission.name,
options.permission.desc,options.permission.title);
if(granted){
console.log(options.permission.grantedText);
// 调用对应的方法
this[options.methodsName]();
}else{
uni.showToast({
title: options.permission.nograntedText,
icon:'none',
duration:3000
});
}
}catch(error){
console.error('权限申请异常:' + error);
uni.showToast({
title:'权限申请失败:' + error.message,
icon:'none',
duration:3000
});
}
},
//选择相册照片发送
chooseImage(){
this.sendPhotoAlbumOrCamera({
count:1,
sourceType:'album',
});
},
// 拍照片发送
cameraPhoto(){
this.sendPhotoAlbumOrCamera({
count:1,
sourceType:'camera',
});
},
// 发照片:相册发照片| 拍照片
sendPhotoAlbumOrCamera(option){
const that = this;
uni.chooseImage({
count:option.count,
//sizeType:['original','compressed'],
sizeType: ['compressed'],
sourceType:[option.sourceType],
success: (chooseRes) => {
console.log('选择照片chooseRes',chooseRes);
const tempFilePath = chooseRes.tempFilePaths[0];
// 跳转到头像剪裁页面
uni.navigateTo({
url: `/pages/setpageInfo/setpageInfo?action=avatarCut&tempFilePath=${encodeURIComponent(tempFilePath)}&title=${encodeURIComponent('头像剪裁')}`
});
// 显示加载中
// uni.showLoading({
// title: '上传中...',
// mask: true
// });
// 根据配置选择上传方式
// if (uploadfileSaveType.image === 'myserver') {
// that.uploadImageToServer(tempFilePath);
// } else if (uploadfileSaveType.image === 'AliyunOSS') {
// that.uploadImageToAliyunOSS(tempFilePath);
// }
},
});
},
// 上传图片到服务器
uploadImageToServer(filePath) {
const that = this;
// 自定义文件夹路径 - 使用avatars专门存放头像
const imagepath = `avatars`;
uni.uploadFile({
url: requestUrl.http + `/api/chat/uploadStreamSingleToServerDiy/${imagepath}`,
filePath: filePath,
name: 'file',
formData: {
token: this.me.token
},
header: {
'token': this.me.token
},
success: (uploadRes) => {
uni.hideLoading();
console.log('上传到本地服务器图片结果',uploadRes);
if (uploadRes.statusCode === 200) {
try {
const data = JSON.parse(uploadRes.data);
console.log('上传到本地服务器图片数据',data);
if (data && data.data && data.data.url) {
// 更新头像
that.updateAvatar(data.data.url);
} else {
uni.showToast({
title: '上传失败',
icon: 'none'
});
}
} catch (e) {
uni.showToast({
title: '解析响应数据失败',
icon: 'none'
});
}
} else {
uni.showToast({
title: '上传失败',
icon: 'none'
});
}
},
fail: (err) => {
uni.hideLoading();
uni.showToast({
title: '上传失败',
icon: 'none'
});
console.error('上传失败', err);
}
});
},
// 上传图片到阿里云OSS
uploadImageToAliyunOSS(filePath) {
const that = this;
uni.uploadFile({
url: requestUrl.http + `/api/chat/uploadAliyun`,
filePath: filePath,
name: 'img',
formData: {
token: this.me.token,
imageClassId: 0
},
header: {
'token': this.me.token
},
success: (uploadRes) => {
uni.hideLoading();
console.log('上传到阿里云OSS图片结果',uploadRes);
if (uploadRes.statusCode === 200) {
try {
const data = JSON.parse(uploadRes.data);
if (data && data.data && data.data.length && data.data[0].url) {
// 更新头像
that.updateAvatar(data.data[0].url);
} else {
uni.showToast({
title: '上传失败',
icon: 'none'
});
}
} catch (e) {
uni.showToast({
title: '解析响应数据失败',
icon: 'none'
});
}
} else {
uni.showToast({
title: '上传失败',
icon: 'none'
});
}
},
fail: (err) => {
uni.hideLoading();
uni.showToast({
title: '上传失败',
icon: 'none'
});
console.error('上传失败', err);
}
});
},
// 更新头像到服务器
updateAvatar(avatarUrl) {
const that = this;
uni.$u.http.post(requestUrl.http + '/api/chat/updateUserinfo', {
fieldname: 'avatar',
fieldValue: avatarUrl
}, {
header: {
token: this.me.token
}
}).then(res => {
// 更新本地数据
that.chatpageset.avatar = avatarUrl.startsWith('http') ? avatarUrl : requestUrl.http + avatarUrl;
that.me.avatar = avatarUrl.startsWith('http') ? avatarUrl : requestUrl.http + avatarUrl;
// 更新vuex中的用户信息
that.$store.dispatch('regloginAction', that.me);
uni.showToast({
title: '头像更新成功',
icon: 'success'
});
// 在剪切头像页面返回上一页
setTimeout(()=>{
uni.navigateBack({
delta: 1
});
},1000);
}).catch(err => {
uni.showToast({
title: err.data.data || '更新失败',
icon: 'none'
});
});
},
}
}
# ③ 混入文件 /pages/setpageInfo/avatarCut.js
import { requestUrl, uploadfileSaveType } from '@/common/mixins/configData.js';
export default {
data() {
return {
tempFilePath: '', // 原始图片路径
cropperReady: false, // 剪裁器是否就绪
canvasWidth: 0, // 画布宽度
canvasHeight: 0, // 画布高度
scale: 1, // 缩放比例
minScale: 0.5, // 最小缩放
maxScale: 3, // 最大缩放
cropperContext: null, // 画布上下文
imageWidth: 0, // 图片宽度
imageHeight: 0, // 图片高度
offsetX: 0, // 图片偏移X
offsetY: 0, // 图片偏移Y
startX: 0, // 触摸开始X
startY: 0, // 触摸开始Y
initialOffsetX: 0, // 初始偏移X
initialOffsetY: 0, // 初始偏移Y
initialScale: 1, // 初始缩放比例
initialDistance: 0, // 初始两指距离
croppedWidth: 500, // 剪裁宽度(rpx,需要转换为px)
croppedHeight: 500, // 剪裁高度(rpx,需要转换为px)
windowWidth: 0, // 窗口宽度(px)
windowHeight: 0, // 窗口高度(px)
navBarHeight: 0, // 导航栏高度(px)
isPinching: false, // 是否正在缩放
}
},
methods: {
// 初始化剪裁器
initCropper() {
const that = this;
// 获取系统信息
uni.getSystemInfo({
success: function(res) {
that.windowWidth = res.windowWidth;
that.windowHeight = res.windowHeight;
// 计算导航栏高度(假设自定义导航栏高度为90rpx + 状态栏高度)
const statusBarHeight = res.statusBarHeight;
that.navBarHeight = uni.upx2px(90) + statusBarHeight;
// 将rpx转换为px
const croppedWidthPx = uni.upx2px(that.croppedWidth);
const croppedHeightPx = uni.upx2px(that.croppedHeight);
// 计算可用高度(屏幕高度 - 导航栏高度 - 操作按钮高度)
const availableHeight = that.windowHeight - that.navBarHeight - uni.upx2px(100);
// 画布大小设置为剪裁窗口大小
that.canvasWidth = croppedWidthPx;
that.canvasHeight = croppedHeightPx;
// 获取图片信息
uni.getImageInfo({
src: that.tempFilePath,
success: function(imageInfo) {
that.imageWidth = imageInfo.width;
that.imageHeight = imageInfo.height;
// 初始化画布
that.cropperContext = uni.createCanvasContext('avatarCanvas', that);
// 计算初始缩放比例,使图片适应剪裁框
const widthScale = croppedWidthPx / imageInfo.width;
const heightScale = croppedHeightPx / imageInfo.height;
that.scale = Math.max(widthScale, heightScale) * 1.2; // 稍微放大一点,确保填满剪裁框
that.minScale = that.scale; // 最小缩放比例设为初始比例,确保图片至少填满剪裁框
that.maxScale = that.scale * 3;
// 重置偏移量
that.offsetX = 0;
that.offsetY = 0;
// 绘制图片
that.drawImage();
that.cropperReady = true;
},
fail: function(err) {
console.error('获取图片信息失败', err);
uni.showToast({
title: '图片加载失败',
icon: 'none'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
});
}
});
},
// 绘制图片到画布
drawImage() {
const ctx = this.cropperContext;
// 清空画布
ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
// 绘制图片
const scaledWidth = this.imageWidth * this.scale;
const scaledHeight = this.imageHeight * this.scale;
// 计算图片绘制位置,确保居中显示
const drawX = (this.canvasWidth - scaledWidth) / 2 + this.offsetX;
const drawY = (this.canvasHeight - scaledHeight) / 2 + this.offsetY;
ctx.drawImage(
this.tempFilePath,
drawX, drawY,
scaledWidth, scaledHeight
);
ctx.draw(false); // 使用非同步绘制
},
// 取消剪裁
cancelCrop() {
uni.navigateBack();
},
// 确认剪裁
confirmCrop() {
const that = this;
if (!that.cropperReady) {
uni.showToast({
title: '剪裁器未就绪',
icon: 'none'
});
return;
}
uni.showLoading({
title: '正在剪裁...',
mask: true
});
// 剪裁图片
setTimeout(() => {
uni.canvasToTempFilePath({
x: 0,
y: 0,
width: that.canvasWidth,
height: that.canvasHeight,
destWidth: that.croppedWidth,
destHeight: that.croppedHeight,
canvasId: 'avatarCanvas',
success: function(res) {
uni.hideLoading();
// 上传剪裁后的图片
that.uploadCroppedImage(res.tempFilePath);
},
fail: function(err) {
uni.hideLoading();
uni.showToast({
title: '剪裁失败',
icon: 'none'
});
console.error('剪裁失败', err);
}
}, that);
}, 300);
},
// 上传剪裁后的图片
uploadCroppedImage(filePath) {
// 显示加载中
uni.showLoading({
title: '上传中...',
mask: true
});
const that = this;
// 根据配置选择上传方式
if (uploadfileSaveType.image === 'myserver') {
that.uploadImageToServer(filePath);
} else if (uploadfileSaveType.image === 'AliyunOSS') {
that.uploadImageToAliyunOSS(filePath);
}
},
// 上传图片到服务器
uploadImageToServer(filePath) {
const that = this;
// 自定义文件夹路径 - 使用avatars专门存放头像
const imagepath = `avatars`;
uni.uploadFile({
url: requestUrl.http + `/api/chat/uploadStreamSingleToServerDiy/${imagepath}`,
filePath: filePath,
name: 'file',
formData: {
token: this.me.token
},
header: {
'token': this.me.token
},
success: (uploadRes) => {
uni.hideLoading();
console.log('上传到本地服务器图片结果',uploadRes);
if (uploadRes.statusCode === 200) {
try {
const data = JSON.parse(uploadRes.data);
console.log('上传到本地服务器图片数据',data);
if (data && data.data && data.data.url) {
// 更新头像
that.updateAvatar(data.data.url);
} else {
uni.showToast({
title: '上传失败',
icon: 'none'
});
}
} catch (e) {
uni.showToast({
title: '解析响应数据失败',
icon: 'none'
});
}
} else {
uni.showToast({
title: '上传失败',
icon: 'none'
});
}
},
fail: (err) => {
uni.hideLoading();
uni.showToast({
title: '上传失败',
icon: 'none'
});
console.error('上传失败', err);
}
});
},
// 上传图片到阿里云OSS
uploadImageToAliyunOSS(filePath) {
const that = this;
uni.uploadFile({
url: requestUrl.http + `/api/chat/uploadAliyun`,
filePath: filePath,
name: 'img',
formData: {
token: this.me.token,
imageClassId: 0
},
header: {
'token': this.me.token
},
success: (uploadRes) => {
uni.hideLoading();
console.log('上传到阿里云OSS图片结果',uploadRes);
if (uploadRes.statusCode === 200) {
try {
const data = JSON.parse(uploadRes.data);
if (data && data.data && data.data.length && data.data[0].url) {
// 更新头像
that.updateAvatar(data.data[0].url);
} else {
uni.showToast({
title: '上传失败',
icon: 'none'
});
}
} catch (e) {
uni.showToast({
title: '解析响应数据失败',
icon: 'none'
});
}
} else {
uni.showToast({
title: '上传失败',
icon: 'none'
});
}
},
fail: (err) => {
uni.hideLoading();
uni.showToast({
title: '上传失败',
icon: 'none'
});
console.error('上传失败', err);
}
});
},
// 更新头像到服务器
updateAvatar(avatarUrl) {
const that = this;
uni.$u.http.post(requestUrl.http + '/api/chat/updateUserinfo', {
fieldname: 'avatar',
fieldValue: avatarUrl
}, {
header: {
token: this.me.token
}
}).then(res => {
// 更新本地数据
that.chatpageset.avatar = avatarUrl.startsWith('http') ? avatarUrl : requestUrl.http + avatarUrl;
that.me.avatar = avatarUrl.startsWith('http') ? avatarUrl : requestUrl.http + avatarUrl;
// 更新vuex中的用户信息
that.$store.dispatch('regloginAction', that.me);
uni.showToast({
title: '头像更新成功',
icon: 'success'
});
// 在剪切头像页面返回上一页
setTimeout(()=>{
this.navigateBack();
},1000);
}).catch(err => {
uni.showToast({
title: err.data.data || '更新失败',
icon: 'none'
});
});
},
// 计算两点之间的距离
getDistance(p1, p2) {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
return Math.sqrt(dx * dx + dy * dy);
},
// 计算两点之间的中心点
getMidpoint(p1, p2) {
return {
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2
};
},
// 触摸事件处理(实现拖拽和缩放功能)
touchStart(e) {
if (!this.cropperReady) return;
// 单指触摸 - 拖拽
if (e.touches.length === 1) {
this.startX = e.touches[0].clientX;
this.startY = e.touches[0].clientY;
this.initialOffsetX = this.offsetX;
this.initialOffsetY = this.offsetY;
this.isPinching = false;
}
// 双指触摸 - 缩放
else if (e.touches.length === 2) {
const touch1 = { x: e.touches[0].clientX, y: e.touches[0].clientY };
const touch2 = { x: e.touches[1].clientX, y: e.touches[1].clientY };
this.initialDistance = this.getDistance(touch1, touch2);
this.initialScale = this.scale;
this.isPinching = true;
// 计算缩放中心点
const midpoint = this.getMidpoint(touch1, touch2);
this.scaleCenterX = midpoint.x;
this.scaleCenterY = midpoint.y;
}
},
touchMove(e) {
if (!this.cropperReady) return;
// 单指触摸 - 拖拽
if (e.touches.length === 1 && !this.isPinching) {
if (!this.startX) return;
const moveX = e.touches[0].clientX - this.startX;
const moveY = e.touches[0].clientY - this.startY;
this.offsetX = this.initialOffsetX + moveX;
this.offsetY = this.initialOffsetY + moveY;
// 限制移动范围
const scaledWidth = this.imageWidth * this.scale;
const scaledHeight = this.imageHeight * this.scale;
const maxX = Math.max(0, (scaledWidth - this.canvasWidth) / 2);
const maxY = Math.max(0, (scaledHeight - this.canvasHeight) / 2);
this.offsetX = Math.min(Math.max(this.offsetX, -maxX), maxX);
this.offsetY = Math.min(Math.max(this.offsetY, -maxY), maxY);
this.drawImage();
}
// 双指触摸 - 缩放
else if (e.touches.length === 2) {
const touch1 = { x: e.touches[0].clientX, y: e.touches[0].clientY };
const touch2 = { x: e.touches[1].clientX, y: e.touches[1].clientY };
const currentDistance = this.getDistance(touch1, touch2);
if (this.initialDistance > 0) {
// 计算缩放比例
let newScale = this.initialScale * (currentDistance / this.initialDistance);
// 限制缩放范围
newScale = Math.min(Math.max(newScale, this.minScale), this.maxScale);
// 计算缩放中心点在图片上的位置
const scaledWidth = this.imageWidth * this.scale;
const scaledHeight = this.imageHeight * this.scale;
// 计算缩放中心点在图片上的相对位置
const imgX = (this.scaleCenterX - this.canvasWidth / 2 - this.offsetX) / this.scale;
const imgY = (this.scaleCenterY - this.canvasHeight / 2 - this.offsetY) / this.scale;
// 更新缩放比例
this.scale = newScale;
// 计算新的偏移量,使缩放中心点保持不变
const newScaledWidth = this.imageWidth * this.scale;
const newScaledHeight = this.imageHeight * this.scale;
this.offsetX = this.scaleCenterX - this.canvasWidth / 2 - imgX * this.scale;
this.offsetY = this.scaleCenterY - this.canvasHeight / 2 - imgY * this.scale;
// 限制移动范围
const maxX = Math.max(0, (newScaledWidth - this.canvasWidth) / 2);
const maxY = Math.max(0, (newScaledHeight - this.canvasHeight) / 2);
this.offsetX = Math.min(Math.max(this.offsetX, -maxX), maxX);
this.offsetY = Math.min(Math.max(this.offsetY, -maxY), maxY);
this.drawImage();
}
}
},
touchEnd(e) {
this.startX = 0;
this.startY = 0;
// 如果只剩下一根手指,结束缩放
if (e.touches.length === 0 && this.isPinching) {
this.isPinching = false;
this.initialDistance = 0;
}
}
},
}