cansnow 56442ab47d 修正服务器地址,修正聊天内容跟服务器的联动,
服务器有个设置 不会通过ws发送当前发送的信息,目前是关掉的,找不到设置在上面地方了
2025-06-08 21:19:07 +08:00

973 lines
32 KiB
Vue

<template>
<view class="container" :style="pageHeight">
<uni-nav-bar @clickLeft="goto(1, 1)" left-icon="back" :title="channel.name || $t('Chat')" :border="false"
backgroundColor="#F5F5F5">
<template v-slot:right>
<view class="nav-right">
<uni-icons v-if="channel.type === 'O'" type="more-filled" size="24"
@click="showGroupMenu"></uni-icons>
</view>
</template>
</uni-nav-bar>
<view class="box-1">
<scroll-view scroll-y refresher-background="transparent" style="height: 100%;"
@refresherrefresh="refresherrefresh" :refresher-enabled="hasMoreMessages" :scroll-with-animation="false"
:refresher-triggered="scrollView.refresherTriggered" :scroll-into-view="scrollView.intoView"
@scrolltoupper="loadMoreMessages">
<view class="talk-list">
<!-- 加载更多提示 -->
<view v-if="isLoading" class="loading-more">
<uni-icons type="spinner-cycle" size="20" color="#999"></uni-icons>
<text>加载中...</text>
</view>
<!-- 消息列表 -->
<view v-for="item in talkList" :key="item.id" :id="'msg-' + item.id" class="message-item">
<view class="item flex-col"
:class="[item.user_id == currentUserId ? 'push' : 'pull', item.status === 'failed' ? 'failed' : '']">
<image :src="getAvatarUrl(item)" mode="aspectFill" class="pic"
@tap="showUserInfo(item.from_user)">
</image>
<view class="body">
<view class="nickname">{{ item.from_user.nickname || item.from_user.username ||
'Unknown'}}
</view>
<view class="content" :class="{ 'image-content': item.type === 'image' }">
<template v-if="item.type === 'image'">
<image :src="getImageUrl(item.content)" mode="widthFix"
style="max-width: 400rpx;" @tap="previewImage(item.content)"
@load="onImageLoad(item.id)"></image>
</template>
<template v-else>
<text>{{ item.message }}</text>
</template>
<!-- 消息状态 -->
<view v-if="item.user_id == currentUserId" class="message-status">
<uni-icons v-if="item.status === 'sending'" type="spinner-cycle" size="14"
color="#999"></uni-icons>
<uni-icons v-else-if="item.status === 'failed'" type="closeempty" size="14"
color="#ff4d4f" @tap="resendMessage(item)"></uni-icons>
<uni-icons v-else-if="item.status === 'sent'" type="checkmarkempty" size="14"
color="#999"></uni-icons>
</view>
</view>
<view class="time">{{ formatTime(item.create_time) }}</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<view class="box-2">
<view class="input-area flex-col">
<view class="flex-grow">
<uni-easyinput type="text" class="input" v-model="content" :placeholder="$t('Type a message')"
placeholder-style="color:#999;" :cursor-spacing="6" trim="both" confirmType="send"
:inputBorder="false" @confirm="sendMessage" @focus="onInputFocus"
@blur="onInputBlur"></uni-easyinput>
</view>
<view class="action-buttons">
<button @tap="showemoji">
<uni-icons type="checkbox-filled" size="24" color="#666"></uni-icons>
</button>
<button @tap="showaddons">
<uni-icons type="plus" size="24" color="#666"></uni-icons>
</button>
</view>
</view>
<view class="addons" v-show="addons_show">
<button @tap="sendphoto">
<uni-icons type="image" size="24" color="#666"></uni-icons>
{{$t('照片')}}
</button>
<button @tap="sendcamera">
<uni-icons type="camera-filled" size="24" color="#666"></uni-icons>
{{$t('拍摄')}}
</button>
</view>
<view class="emoji" v-show="emoji_show">
<view v-for="emoji in emojiList" :key="emoji" @click="addEmoji(emoji)">{{ emoji }}</view>
</view>
</view>
<!-- 群组菜单弹窗 -->
<uni-popup ref="groupMenu" type="bottom">
<view class="group-menu">
<view class="menu-item" @tap="showGroupMembers">
<uni-icons type="person" size="20"></uni-icons>
<text>{{ $t('Group Members') }}</text>
</view>
<view class="menu-item" @tap="showGroupInfo">
<uni-icons type="info" size="20"></uni-icons>
<text>{{ $t('Group Info') }}</text>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import { mapState, mapMutations } from 'vuex';
import mattermost from '@/static/im/mattermost.js';
import emojiList from '@/static/im/emoji.js';
// 日期格式化函数
function formatTime(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
// 今天的消息只显示时间
if (diff < 24 * 60 * 60 * 1000 && date.getDate() === now.getDate()) {
return date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
hour12: false
});
}
// 昨天的消息显示"昨天"和时间
if (diff < 48 * 60 * 60 * 1000 && date.getDate() === now.getDate() - 1) {
return `昨天 ${date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
hour12: false
})}`;
}
// 一周内的消息显示星期几和时间
if (diff < 7 * 24 * 60 * 60 * 1000) {
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
return `星期${weekdays[date.getDay()]} ${date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
hour12: false
})}`;
}
// 其他消息显示完整日期和时间
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
}
let users = {};
export default {
data() {
return {
init: {
cdnurl: "http://www.dxmt.io"
},
channel:{},
// 滚动容器
scrollView: {
refresherTriggered: false,
intoView: '',
safeAreaHeight: 0
},
// 聊天列表数据
talkList: [],
// 请求参数
ajax: {
limit: 20,
last_page: 1,
page: 0,
flag: false,
loading: false
},
// 发送内容
content: '',
// 图片上传状态
uploading: false,
// 消息状态
messageStatus: new Map(),
// 图片加载状态
imageLoading: new Map(),
// 当前用户ID
currentUserId: '',
emojiList:emojiList,
emoji_show:false,
addons_show:false,
}
},
computed: {
...mapState(['userInfo']),
// 页面高度
pageHeight() {
const safeAreaHeight = this.scrollView.safeAreaHeight;
return safeAreaHeight > 0 ? `height: calc(${safeAreaHeight}px - var(--window-top));` : '';
},
// 是否有更多消息
hasMoreMessages() {
return this.ajax.last_page > this.ajax.page;
},
// 是否正在加载
isLoading() {
return this.ajax.loading;
}
},
async onLoad(options) {
try {
if(this.userInfo.im_token){
this.currentUserId = this.userInfo.im_user.id;
mattermost.init();
// 初始化目标信息
await this.initChannel(options);
// 获取系统信息
this.initSystemInfo();
// 获取历史消息
await this.loadMessages();
// 注册消息处理器
this.registerMessageHandlers();
}
// 标记频道为已读
//await this.markChannelAsRead();
} catch (error) {
console.error('页面加载失败:', error);
uni.showToast({
title: this.$t('Load failed'),
icon: 'none'
});
}
},
onUnload() {
// 移除消息处理器
this.unregisterMessageHandlers();
},
methods: {
...mapMutations(['setUnreadCount']),
// 初始化目标信息
async initChannel(options) {
let channel = await mattermost.getChannelById(options.target_id);
if(channel.type=='O'){
channel.name = channel.display_name || channel.name
}
if(channel.type=='D'){
var target_user_id = channel.name.replace(mattermost.getCurrentUserId(),'').replace('__','')
var target_user={};
if(mattermost.kown_users[target_user_id]){
target_user = mattermost.kown_users[target_user_id];
}else{
let channelMembers = await mattermost.getUsersByIds([target_user_id]);
target_user = channelMembers.data[0];
}
channel.name = target_user.nickname || target_user.username;
}
this.channel = channel;
},
// 初始化系统信息
initSystemInfo() {
// #ifdef H5
this.scrollView.safeAreaHeight = uni.getSystemInfoSync().safeArea.height;
// #endif
},
// 注册消息处理器
registerMessageHandlers() {
console.log('注册消息处理器');
mattermost.on('posted', this.handleNewMessage);
mattermost.on('post_edited', this.handleMessageEdited);
mattermost.on('post_deleted', this.handleMessageDeleted);
mattermost.on('status_change', this.handleStatusChange);
},
// 移除消息处理器
unregisterMessageHandlers() {
console.log('移除消息处理器');
mattermost.off('posted', this.handleNewMessage);
mattermost.off('post_edited', this.handleMessageEdited);
mattermost.off('post_deleted', this.handleMessageDeleted);
mattermost.off('status_change', this.handleStatusChange);
},
// 处理消息数据
processMessageData(post, senderName = '') {
// 构建用户信息
const fromUser = mattermost.kown_users[post.user_id];
// 处理消息类型
let messageType = 'text';
let messageContent = post.message;
let imageUrl = '';
if (post.props && post.props.attachments) {
const attachment = post.props.attachments[0];
if (attachment.type === 'image') {
messageType = 'image';
imageUrl = attachment.image_url;
}
}
return {
...post,
status: 'success',
from_user: fromUser
};
},
// 加载消息
async loadMessages() {
if (this.ajax.flag || this.ajax.loading) return;
this.ajax.flag = true;
this.ajax.loading = true;
try {
// 获取消息列表
const posts = await mattermost.getChannelPosts(this.channel.id, {
page: this.ajax.page,
per_page: this.ajax.limit
});
console.log('获取到的消息列表:', posts);
if (!posts || posts.length === 0) {
console.log('没有更多消息');
this.ajax.last_page = this.ajax.page;
return;
}
var newuser_ids = [];
for (var k in posts) {
if (!mattermost.kown_users[posts[k].user_id] && newuser_ids.indexOf(posts[k].user_id) == -1) {
newuser_ids.push(posts[k].user_id);
};
}
if (newuser_ids.length > 0) {
var usersinfo = await mattermost.getUsersByIds(newuser_ids);
console.log('获取到的用户信息:', usersinfo);
usersinfo.data.forEach(user => {
mattermost.kown_users[user.id] = user;
});
}
for (var k in posts) {
posts[k]['from_user'] = mattermost.kown_users[posts[k]['user_id']];//users[posts[k]['user_id']];
}
// 更新消息列表
if (this.ajax.page === 0) {
// 首次加载,直接显示消息
this.talkList = posts;
// 首次加载后滚动到底部
this.$nextTick(() => {
this.scrollToBottom();
});
} else {
// 加载更多历史消息,添加到列表前面
// 过滤掉已存在的消息
const newMessages = posts.filter(msg =>
!this.talkList.some(existingMsg => existingMsg.id === msg.id)
);
for (var k in newMessages) {
this.talkList[k] = newMessages[k];
}
}
console.log('更新后的消息列表:', this.talkList);
// 更新分页信息
this.ajax.page++;
this.ajax.last_page = Math.ceil(posts.length / this.ajax.limit);
} catch (error) {
console.error('加载消息失败:', error);
uni.showToast({
title: '加载失败',
icon: 'none'
});
} finally {
this.ajax.flag = false;
this.ajax.loading = false;
this.scrollView.refresherTriggered = false;
}
},
// 发送消息
async sendMessage() {
if (!this.content.trim()) return;
//console.log(this.talkList);
try {
//console.log('发送消息:', this.content);
// 发送消息
const result = await mattermost.sendTextMessage(this.channel.id, this.content);
if (result && result.id) {
this.content = '';
result['from_user'] = await mattermost.getUserById(result.user_id);
this.talkList.push(result);
console.log('发送成功:', result,this.talkList);
// 更新消息状态
//result.from_user = this.im_user;
//this.talkList[result.id] = result;
} else {
throw new Error('发送失败');
}
} catch (error) {
console.error('发送消息失败:', error);
uni.showToast({
title: '发送失败',
icon: 'none'
});
}
},
// 发送图片消息
async sendImageMessage(tempFilePath) {
try {
// 上传图片
const result = await mattermost.sendImageMessage(this.channel.id, tempFilePath);
if (!result || !result.id) {
throw new Error('发送失败');
}
result['from_user'] = await mattermost.getUserById(result.user_id);
this.talkList.push(result);
//this.content = '';
//console.log('发送图片消息成功:', result);
} catch (error) {
console.error('发送图片消息失败:', error);
uni.showToast({
title: '发送失败',
icon: 'none'
});
}
},
// 处理新消息
handleNewMessage(message) {
console.log('收到新消息:', message);
// 检查消息格式
if (!message || !message.post) {
console.error('无效的消息格式:', message);
return;
}
// 解析 post 数据
let post;
try {
post = typeof message.post === 'string' ? JSON.parse(message.post) : message.post;
} catch (error) {
console.error('解析消息数据失败:', error);
return;
}
// 检查 post 数据
if (!post || !post.channel_id) {
console.error('无效的 post 数据:', post);
return;
}
if (post.channel_id !== this.channel.id) {
console.log('不是当前频道的消息,忽略');
return;
}
// 检查消息是否已存在
if (this.talkList.some(msg => msg.id === post.id)) {
console.log('消息已存在,忽略:', post.id);
return;
}
const newMessage = this.processMessageData(post, message.sender_name);
console.log('添加新消息到列表:', newMessage);
this.talkList.push(newMessage);
// 使用 nextTick 确保 DOM 更新后再滚动
this.$nextTick(() => {
this.scrollToBottom();
});
},
// 处理消息编辑
handleMessageEdited(message) {
// 检查消息格式
if (!message || !message.post) {
console.error('无效的消息格式:', message);
return;
}
// 解析 post 数据
let post;
try {
post = typeof message.post === 'string' ? JSON.parse(message.post) : message.post;
} catch (error) {
console.error('解析消息数据失败:', error);
return;
}
if (!post || !post.channel_id) {
console.error('无效的 post 数据:', post);
return;
}
if (post.channel_id !== this.channel.id) return;
const index = this.talkList.findIndex(msg => msg.id === post.id);
if (index !== -1) {
this.talkList[index] = {
...this.talkList[index],
content: post.message,
edited: true
};
}
},
// 处理消息删除
handleMessageDeleted(message) {
// 检查消息格式
if (!message || !message.post) {
console.error('无效的消息格式:', message);
return;
}
// 解析 post 数据
let post;
try {
post = typeof message.post === 'string' ? JSON.parse(message.post) : message.post;
} catch (error) {
console.error('解析消息数据失败:', error);
return;
}
if (!post || !post.channel_id) {
console.error('无效的 post 数据:', post);
return;
}
if (post.channel_id !== this.channel.id) return;
const index = this.talkList.findIndex(msg => msg.id === post.id);
if (index !== -1) {
this.talkList[index].content = this.$t('This message was deleted');
this.talkList[index].deleted = true;
}
},
// 处理状态变化
handleStatusChange(status) {
// 更新用户在线状态
this.talkList = this.talkList.map(msg => {
if (msg.from_user.id === status.user_id) {
return {
...msg,
from_user: {
...msg.from_user,
status: status.status
}
};
}
return msg;
});
},
// 重新发送消息
async resendMessage(message) {
const index = this.talkList.findIndex(msg => msg.id === message.id);
if (index === -1) return;
this.talkList[index].status = 'sending';
try {
const response = await this.sendMessage(message.content, message.type);
} catch (error) {
this.talkList[index].status = 'failed';
uni.showToast({
title: this.$t('Resend failed'),
icon: 'none'
});
}
},
// 标记频道为已读
async markChannelAsRead() {
try {
await mattermost.markChannelAsViewed(this.channel.id);
this.setUnreadCount({ channelId: this.channel.id, count: 0 });
} catch (error) {
console.error('标记已读失败:', error);
}
},
// 滚动到底部
scrollToBottom() {
if (this.talkList.length > 0) {
const lastMessage = this.talkList[this.talkList.length - 1];
this.scrollToMessage(lastMessage.id);
}
},
// 滚动到指定消息
scrollToMessage(messageId) {
this.scrollView.intoView = `msg-${messageId}`;
},
// 加载更多消息
async loadMoreMessages() {
if (this.hasMoreMessages && !this.isLoading) {
// 记录当前滚动位置
const oldScrollHeight = this.scrollView.scrollHeight;
await this.loadMessages();
// 保持滚动位置
this.$nextTick(() => {
const newScrollHeight = this.scrollView.scrollHeight;
const scrollTop = newScrollHeight - oldScrollHeight;
this.scrollView.scrollTop = scrollTop;
});
}
},
// 下拉刷新
async refresherrefresh() {
this.scrollView.refresherTriggered = true;
this.ajax.page = 0;
await this.loadMessages();
},
// 预览图片
previewImage(url) {
const images = this.talkList
.filter(msg => msg.type === 'image')
.map(msg => this.getImageUrl(msg.content));
uni.previewImage({
urls: images,
current: this.getImageUrl(url)
});
},
// 获取头像URL
getAvatarUrl(item, avatar) {
//console.log('getAvatarUrl',item);
avatar = item.from_user?.avatar;
if (!avatar) return '/static/img/avatar.png';
return avatar.startsWith('http') ? avatar : `${this.init.cdnurl}${avatar}`;
},
// 获取图片URL
getImageUrl(url) {
if (!url) return '';
return url.startsWith('http') ? url : `${this.init.cdnurl}${url}`;
},
// 格式化时间
formatTime(timestamp) {
return formatTime(timestamp);
},
// 显示用户信息
showUserInfo(user) {
// TODO: 实现用户信息展示
},
// 显示群组菜单
showGroupMenu() {
this.$refs.groupMenu.open();
},
// 显示群组成员
showGroupMembers() {
// TODO: 实现群组成员展示
},
// 显示群组信息
showGroupInfo() {
// TODO: 实现群组信息展示
},
// 输入框获得焦点
onInputFocus() {
this.scrollToBottom();
},
// 输入框失去焦点
onInputBlur() {
// TODO: 处理输入框失焦事件
},
// 图片加载完成
onImageLoad(messageId) {
this.imageLoading.set(messageId, true);
this.$nextTick(() => {
this.scrollToBottom();
});
},
goto(url, type) {
if (type == 2) {
return uni.switchTab({ url: url })
}
if (type == 1) {
return uni.navigateBack({ delta: url });
}
uni.navigateTo({
url: url
})
},
sendphoto(){
uni.chooseImage({
count: 9,
sizeType: ['original', 'compressed'],
sourceType: ['album'],
success: (res) => {
const tempFilePaths = res.tempFilePaths;
const file = tempFilePaths[0];
this.sendImageMessage(file);
}
})
},
sendcamera(){
uni.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['camera'],
success: (res) => {
const tempFilePaths = res.tempFilePaths;
const file = tempFilePaths[0];
this.sendImageMessage(file);
}
});
},
showemoji(){
this.emoji_show=!this.emoji_show;
if(this.emoji_show){
this.addons_show=!this.emoji_show;
}
},
addEmoji(emoji){
this.content += emoji;
},
showaddons(){
this.addons_show=!this.addons_show;
if(this.addons_show){
this.emoji_show=!this.addons_show;
}
}
}
}
</script>
<style lang="scss" scoped>
.container {
height: 100vh;
overflow: hidden;
background-color: #eee;
display: flex;
flex-direction: column;
font-family: Poppins, Poppins;
.box-1 {
flex: 1;
overflow: hidden;
.talk-list {
padding: 20rpx;
.loading-more {
text-align: center;
padding: 20rpx;
color: #999;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
.uni-icons {
margin-right: 10rpx;
}
}
.message-item {
margin-bottom: 30rpx;
.item {
display: flex;
&.push {
flex-direction: row-reverse;
.body {
margin-right: 20rpx;
margin-left: 0;
align-items: flex-end;
.content {
background-color: #BBD8FF;
&::before {
right: -16rpx;
left: auto;
border-color: transparent transparent transparent #BBD8FF;
}
}
}
}
&.failed {
.content {
background-color: #ff4d4f;
color: #fff;
}
}
.pic {
width: 84rpx;
height: 84rpx;
border-radius: 50%;
flex-shrink: 0;
}
.body {
margin-left: 20rpx;
max-width: 80%;
display: flex;
flex-direction: column;
.nickname {
font-size: 24rpx;
color: #999;
margin-bottom: 10rpx;
}
.content {
background-color: #fff;
color: #333;
padding: 24rpx;
border-radius: 10rpx;
position: relative;
word-break: break-all;
&::before {
content: '';
position: absolute;
left: -16rpx;
top: 20rpx;
border: 8rpx solid transparent;
border-right-color: #fff;
}
&.image-content {
padding: 10rpx;
background-color: transparent;
&::before {
display: none;
}
image {
border-radius: 10rpx;
}
}
.message-status {
position: absolute;
right: -30rpx;
bottom: 0;
}
}
.time {
font-size: 24rpx;
color: #999;
margin-top: 10rpx;
}
}
}
}
}
}
.box-2 {
background: #F6F6F6;
padding: 24rpx;
.input-area {
display: flex;
align-items: center;
gap:24rpx;
.flex-grow{flex:1;}
.uni-easyinput__content{border-radius: 8rpx;}
.action-buttons{
display: flex;
align-items: center;
uni-button{
height: 56rpx;
line-height: 56rpx;
background: transparent;
}
}
}
.addons{
display: flex;
align-items: center;
gap:48rpx;
padding: 48rpx 0;
height: 224rpx;
uni-button{
width: 25%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-weight: 400;
font-size: 24rpx;
background: transparent;
color: #999999;
line-height: 2;
.uni-icons{
background: #fff;
height: 128rpx;
width: 128rpx;
}
}
}
.emoji{
display: flex;
flex-wrap: wrap;
max-height: 30vh;
overflow-y: scroll;
padding: 24rpx 0;
gap:10rpx;
view{
}
}
}
}
.group-menu {
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
padding: 30rpx;
.menu-item {
display: flex;
align-items: center;
padding: 20rpx 0;
.uni-icons {
margin-right: 20rpx;
color: #666;
}
text {
font-size: 28rpx;
color: #333;
}
}
}
</style>