This commit is contained in:
xx 2025-06-10 03:14:16 +08:00
commit 265dcb2bf1
5 changed files with 603 additions and 72 deletions

View File

@ -546,6 +546,13 @@
{ {
"navigationBarTitleText": "select" "navigationBarTitleText": "select"
} }
},
{
"path" : "pages/im/member",
"style" :
{
"navigationBarTitleText" : ""
}
},{ },{
"path" : "pages/about/helpdetail", "path" : "pages/about/helpdetail",
"style" : "style" :

View File

@ -10,7 +10,7 @@
</template> </template>
</uni-nav-bar> </uni-nav-bar>
<view class="box-1"> <view class="box-1" @click="emoji_show=addons_show=false">
<scroll-view scroll-y refresher-background="transparent" style="height: 100%;" <scroll-view scroll-y refresher-background="transparent" style="height: 100%;"
@refresherrefresh="refresherrefresh" :refresher-enabled="hasMoreMessages" :scroll-with-animation="false" @refresherrefresh="refresherrefresh" :refresher-enabled="hasMoreMessages" :scroll-with-animation="false"
:refresher-triggered="scrollView.refresherTriggered" :scroll-into-view="scrollView.intoView" :refresher-triggered="scrollView.refresherTriggered" :scroll-into-view="scrollView.intoView"
@ -30,13 +30,11 @@
@tap="showUserInfo(item.from_user)"> @tap="showUserInfo(item.from_user)">
</image> </image>
<view class="body"> <view class="body">
<view class="nickname">{{ item.from_user.nickname || item.from_user.username || <view class="nickname">{{ item.from_user.nickname || item.from_user.username || 'Unknown'}}</view>
'Unknown'}}
</view>
<view class="content" :class="{ 'image-content': item.type === 'image' }"> <view class="content" :class="{ 'image-content': item.type === 'image' }">
<template v-if="item.type === 'image'"> <template v-if="item.type === 'image'">
<image :src="getImageUrl(item.content)" mode="widthFix" <image :src="item.message.thumbnail" mode="widthFix"
style="max-width: 400rpx;" @tap="previewImage(item.content)" style="max-width: 400rpx;" @tap="previewImage(item)"
@load="onImageLoad(item.id)"></image> @load="onImageLoad(item.id)"></image>
</template> </template>
<template v-else> <template v-else>
@ -69,10 +67,10 @@
@blur="onInputBlur"></uni-easyinput> @blur="onInputBlur"></uni-easyinput>
</view> </view>
<view class="action-buttons"> <view class="action-buttons">
<button @tap="showemoji"> <button @tap="showemoji" style="display: flex;align-items: center;">
<uni-icons type="checkbox-filled" size="24" color="#666"></uni-icons> <img src="@/static/im/emoji.png" style="height:48rpx" mode="widthFix" />
</button> </button>
<button @tap="showaddons"> <button @tap="showaddons" style="display: flex;align-items: center;">
<uni-icons type="plus" size="24" color="#666"></uni-icons> <uni-icons type="plus" size="24" color="#666"></uni-icons>
</button> </button>
</view> </view>
@ -165,7 +163,7 @@ export default {
data() { data() {
return { return {
init: { init: {
cdnurl: 'http://q.sjqqzc.top' cdnurl: 'http://api.dxmt.io'
}, },
channel:{}, channel:{},
// //
@ -197,6 +195,11 @@ export default {
emojiList:emojiList, emojiList:emojiList,
emoji_show:false, emoji_show:false,
addons_show:false, addons_show:false,
//
imageLoadQueue: [],
isProcessingQueue: false,
//
placeholderImage: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
} }
}, },
computed: { computed: {
@ -254,7 +257,21 @@ export default {
async initChannel(options) { async initChannel(options) {
let channel = await mattermost.getChannelById(options.target_id); let channel = await mattermost.getChannelById(options.target_id);
if(channel.type=='O'){ if(channel.type=='O'){
channel.name = channel.display_name || channel.name channel.name = channel.display_name || channel.name;
//
// const members = await mattermost.getChannelMembers(channel.id);
// console.log(':', members);
// const users_ids = [];
// for (const member of members) {
// if (member.user_id && member.user_id !== mattermost.getCurrentUserId()) {
// users_ids.push(member.user_id);
// }
// }
// const users = await mattermost.getUsersByIds(users_ids);
// console.log(':', users);
// for (const user of users.data) {
// mattermost.kown_users[user.id] = user;
// }
} }
if(channel.type=='D'){ if(channel.type=='D'){
var target_user_id = channel.name.replace(mattermost.getCurrentUserId(),'').replace('__','') var target_user_id = channel.name.replace(mattermost.getCurrentUserId(),'').replace('__','')
@ -297,30 +314,95 @@ export default {
// //
processMessageData(post, senderName = '') { async processMessageData(post, senderName = '') {
console.log('处理消息数据:', post);
// //
const fromUser = mattermost.kown_users[post.user_id]; const fromUser = await mattermost.getUserById(post.user_id);
// //
let messageType = 'text'; let messageType = 'text';
let messageContent = post.message; let messageContent = post.message;
let imageUrl = ''; let imageUrl = '';
if (post.props && post.props.attachments) { let imageInfo = null;
const attachment = post.props.attachments[0]; //
if (attachment.type === 'image') { if (post.file_ids && post.file_ids.length > 0) {
messageType = 'image'; messageType = 'image';
imageUrl = attachment.image_url; // metadata.files
if (post.metadata && post.metadata.files && post.metadata.files.length > 0) {
const file = post.metadata.files[0];
if (file.mime_type && file.mime_type.startsWith('image/')) {
//
imageInfo = {
id: file.id,
width: file.width,
height: file.height,
mimeType: file.mime_type
};
console.log(file)
messageContent = {
id: file.id,
//thumbnail: this.placeholderImage, // 使
thumbnail: `${mattermost.adminBaseUrl}/api/v4/files/${file.id}/thumbnail`,
preview: null,
original: `${mattermost.adminBaseUrl}/api/v4/files/${file.id}/preview`
};
//
//this.addToImageLoadQueue(post.id, file.id);
}
} }
} }
return { return {
...post, ...post,
id: post.id,
user_id: post.user_id,
message: messageContent,
type: messageType,
create_time: post.create_at,
status: 'success', status: 'success',
from_user: fromUser from_user: fromUser,
imageInfo: imageInfo
}; };
}, },
//
addToImageLoadQueue(messageId, fileId) {
this.imageLoadQueue.push({ messageId, fileId });
if (!this.isProcessingQueue) {
this.processImageLoadQueue();
}
},
//
async processImageLoadQueue() {
if (this.isProcessingQueue || this.imageLoadQueue.length === 0) return;
this.isProcessingQueue = true;
const { messageId, fileId } = this.imageLoadQueue.shift();
try {
//
const thumbnailData = await mattermost.getThumbnail(fileId);
//
const messageIndex = this.talkList.findIndex(msg => msg.id === messageId);
if (messageIndex !== -1) {
this.$set(this.talkList[messageIndex].message, 'thumbnail', thumbnailData);
}
} catch (error) {
console.error('加载缩略图失败:', error);
} finally {
this.isProcessingQueue = false;
//
if (this.imageLoadQueue.length > 0) {
setTimeout(() => this.processImageLoadQueue(), 100);
}
}
},
// //
async loadMessages() { async loadMessages() {
if (this.ajax.flag || this.ajax.loading) return; if (this.ajax.flag || this.ajax.loading) return;
@ -355,7 +437,8 @@ export default {
}); });
} }
for (var k in posts) { for (var k in posts) {
posts[k]['from_user'] = mattermost.kown_users[posts[k]['user_id']];//users[posts[k]['user_id']]; posts[k] = await this.processMessageData(posts[k]);
//posts[k]['from_user'] = await mattermost.getUserById(posts[k]['user_id']);//users[posts[k]['user_id']];
} }
// //
if (this.ajax.page === 0) { if (this.ajax.page === 0) {
@ -405,15 +488,14 @@ export default {
//console.log(':', this.content); //console.log(':', this.content);
// //
const result = await mattermost.sendTextMessage(this.channel.id, this.content); const result = await mattermost.sendTextMessage(this.channel.id, this.content);
//console.log(':', this.content);
if (result && result.id) { if (result && result.id) {
this.content = ''; this.content = '';
result['from_user'] = await mattermost.getUserById(result.user_id); //result['from_user'] = await mattermost.getUserById(result.user_id);
this.talkList.push(result); if(this.channel.type !='O'){
console.log('发送成功:', result,this.talkList); this.handleNewMessage({post:result});
// }
//result.from_user = this.im_user; //console.log(':', result,this.talkList);
//this.talkList[result.id] = result;
} else { } else {
throw new Error('发送失败'); throw new Error('发送失败');
} }
@ -435,8 +517,9 @@ export default {
if (!result || !result.id) { if (!result || !result.id) {
throw new Error('发送失败'); throw new Error('发送失败');
} }
result['from_user'] = await mattermost.getUserById(result.user_id); if(this.channel.type !='O'){
this.talkList.push(result); this.handleNewMessage({post:result});
}
//this.content = ''; //this.content = '';
//console.log(':', result); //console.log(':', result);
@ -448,10 +531,8 @@ export default {
}); });
} }
}, },
// //
handleNewMessage(message) { async handleNewMessage(message) {
console.log('收到新消息:', message); console.log('收到新消息:', message);
// //
@ -485,8 +566,7 @@ export default {
console.log('消息已存在,忽略:', post.id); console.log('消息已存在,忽略:', post.id);
return; return;
} }
const newMessage = await this.processMessageData(post, message.sender_name);
const newMessage = this.processMessageData(post, message.sender_name);
console.log('添加新消息到列表:', newMessage); console.log('添加新消息到列表:', newMessage);
this.talkList.push(newMessage); this.talkList.push(newMessage);
@ -642,13 +722,27 @@ export default {
}, },
// //
previewImage(url) { previewImage(message) {
console
.warn('previewImage', message);
if (!message.message || !message.message.id) return;
const file_id = message.imageInfo.id;
// URL
const images = this.talkList const images = this.talkList
.filter(msg => msg.type === 'image') .filter(msg => msg.type === 'image' && msg.imageInfo && msg.imageInfo.id)
.map(msg => this.getImageUrl(msg.content)); .map(msg => {
return `${mattermost.adminBaseUrl}/api/v4/files/${msg.imageInfo.id}`;
});
uni.previewImage({ uni.previewImage({
urls: images, urls: images,
current: this.getImageUrl(url) current: `${mattermost.adminBaseUrl}/api/v4/files/${message.imageInfo.id}`,
success: () => {
console.log('预览图片成功');
},
fail: (err) => {
console.error('预览图片失败:', err);
}
}); });
}, },
@ -762,7 +856,6 @@ export default {
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.container { .container {
height: 100vh; height: 100vh;

390
pages/im/member.vue Normal file
View File

@ -0,0 +1,390 @@
<template>
<view class="container">
<uni-nav-bar
:title="$t('MT Team')"
left-icon="left"
@clickLeft="goto('/pages/im/index',2)"
:border="false"
backgroundColor="F5f5f5">
</uni-nav-bar>
<view class="box-1">
<scroll-view
scroll-y
refresher-background="transparent"
style="height: 100%;"
@refresherrefresh="refresherrefresh"
:refresher-enabled="ajax.last_page > ajax.page"
:scroll-with-animation="false"
:refresher-triggered="scrollView.refresherTriggered"
>
<uni-list>
<template v-for="item in friend_list" >
<uni-list-chat
v-if="item.id"
:key="`${item.id}`"
:avatar-circle="true"
:title="item.display_name || item.name || item.nickname || item.email"
:avatar="getAvatarUrl(item)"
:note="item.last_msg ? (item.last_msg_type === 'image' ? '[图片]' : item.last_msg) : '暂无消息'"
:time="item.last_post_at ? date(item.last_post_at) : ''"
:badge-text="item.unread_count > 0 ? (item.unread_count > 99 ? '99+' : item.unread_count) : ''"
clickable
@click="handleContactClick(item)"
></uni-list-chat>
</template>
<!--这里显示最近联系人-->
</uni-list>
</scroll-view>
</view>
</view>
</template>
<script>
import {
mapState,
mapMutations
} from 'vuex';
import mattermost from '@/static/im/mattermost.js';
import base from '@/config/baseUrl'; // base
export default {
data() {
return {
//
config: {
cdnurl: '',
//
},
friend_list: {},
//
ajax: {
limit: 20, //
last_page: 1, //
page: 0, //
flag: false, //
loading: false //
},
scrollView: {
refresherTriggered: false //
},
}
},
computed: {
...mapState(['userInfo']),
// 访 ajax
hasMoreData() {
return this.ajax && this.ajax.last_page > this.ajax.page;
}
},
onShow() {
this.init();
},
onLoad() {
this.init();
},
onUnload() {
//
mattermost.off('posted', this.handleNewMessage);
mattermost.off('post_deleted', this.handleMessageDeleted);
mattermost.off('post_edited', this.handleMessageEdited);
},
onPullDownRefresh() {
this.loadContactList().finally(() => {
uni.stopPullDownRefresh();
});
},
methods: {
...mapMutations(['setUserInfo']),
async init(){
try {
if(!this.userInfo.token || !this.userInfo.im_token){
return ;
}
//
//const userinfo = await this.$http.get('/api/user/profile');
//this.setUserInfo(userinfo.data);
//console.log(':', this.userInfo);
//
const initRes = await this.$http.get('/api/common/init?lang=' + this.$i18n.locale);
if (initRes.code === 0) {
this.config = initRes.data;
this.config.cdnurl = initRes.data.cdnurl || "http://www.dxmt.io";
}
// Mattermost
await mattermost.init();
//
this.loadContactList();
//
mattermost.on('posted', this.handleNewMessage);
mattermost.on('post_deleted', this.handleMessageDeleted);
mattermost.on('post_edited', this.handleMessageEdited);
} catch (error) {
console.error('页面加载失败:', error);
uni.showToast({
title: '加载失败',
icon: 'none'
});
}
},
//
async loadContactList() {
if (this.ajax.flag || this.ajax.loading) return;
this.ajax.flag = true;
this.ajax.loading = true;
try {
//
const fixedContactsRes = await this.$http.get('/api/chat/get_team_member_list');
let fixedContacts = [];
if (fixedContactsRes.code === 0 && Array.isArray(fixedContactsRes.data)) {
fixedContacts = fixedContactsRes.data;
}
//
const recent_contacts = await mattermost.getRecentContacts();
const sortedContacts = {};
for (const key in fixedContacts) {
if (Object.prototype.hasOwnProperty.call(fixedContacts, key)) {
sortedContacts[fixedContacts[key].id] = fixedContacts[key];
}
}
for (const key in sortedContacts) {
if (Object.prototype.hasOwnProperty.call(sortedContacts, key)) {
sortedContacts[key].unread_count = sortedContacts[key].unread_count || 0;
this.friend_list[key] = sortedContacts[key];
}
}
console.log('联系人列表:',this.friend_list);
//
if (this.ajax.page === 0) {
//this.friend_list = sortedContacts;
} else {
//this.friend_list = [...this.friend_list, ...sortedContacts];
}
//
this.ajax.page++;
this.ajax.last_page = Math.ceil(sortedContacts.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 handleContactClick(item) {
try {
// ID
var url = await mattermost.talk_to_user(item); //
uni.navigateTo({
url: url
});
} catch (error) {
console.error('处理联系人点击失败:', error);
uni.showToast({
title: '操作失败',
icon: 'none'
});
}
},
//
async handleNewMessage(message) {
//
const updateContact = async(list) => {
let is_exist = false;
let post = JSON.parse(message.post);
for(var k in list){
const contact = list[k];
if(message.channel_type == 'D'){
//
if(contact.id === post.user_id) {
is_exist = true;
list[k]= contact;
list[k]['last_msg']=post.message;
list[k]['last_msg_type']= post.type || 'text';
list[k]['last_msg_time']= post.create_at;
list[k]['unread_count']= contact.unread_count+=1;
}
}
if(message.channel_type == 'O'){
if(contact.id === post.channel_id) {
is_exist = true;
list[k]= contact;
list[k]['last_msg']=post.message;
list[k]['last_msg_type']= post.type || 'text';
list[k]['last_msg_time']= post.create_at;
list[k]['unread_count']= contact.unread_count+=1;
}
//
}
}
if(message.channel_type == 'D' && !is_exist){
console.log('新消息:', message,'不存在');
let send_user = await mattermost.getUserInfo(post.user_id);
//console.log('send_user',send_user);
let contact = send_user.data;
contact['last_msg']=post.message;
contact['last_msg_type']= post.type || 'text';
contact['last_msg_time']= post.create_at;
contact['unread_count']= 1;
list[k]= contact;
}
let _keys = Object.keys(list).sort((a, b) => {
const timeA = a.last_msg_time ? new Date(a.last_msg_time).getTime() : 0;
const timeB = b.last_msg_time ? new Date(b.last_msg_time).getTime() : 0;
return timeB - timeA;
});
let _list = {};
for (let index = 0; index < _keys.length; index++) {
_list[_keys[index]] = list[_keys[index]];
}
return _list;
};
this.friend_list = await updateContact(this.friend_list);
},
//
handleMessageDeleted(message) {
const updateContact = (list) => {
return list.map(contact => {
if(contact.id === message.channel_id) {
return {
...contact,
last_msg: '此消息已删除',
last_msg_type: 'text',
last_msg_time: message.create_at
};
}
return contact;
});
};
this.friend_list = updateContact(this.friend_list);
},
//
handleMessageEdited(message) {
const updateContact = (list) => {
return list.map(contact => {
if(contact.id === message.channel_id) {
return {
...contact,
last_msg: message.message,
last_msg_time: message.update_at
};
}
return contact;
});
};
this.friend_list = updateContact(this.friend_list);
},
refresherrefresh() {
this.scrollView.refresherTriggered = true;
this.ajax.page = 0;
this.loadContactList();
},
goto(url,type){
if(type==2){
return uni.switchTab({url:url})
}
if(type==1){
return uni.navigateBack({delta:url});
}
uni.navigateTo({
url:url
})
},
/**
* JavaScript 仿 PHP date() 函数
* @param {string} format - 格式字符串
* @param {number|Date} [timestamp] - 可选的时间戳或Date对象默认为当前时间
* @return {string} 格式化后的日期时间字符串
*/
date(format, timestamp) {
//
if(!timestamp){
return '';
}
let date = timestamp;
if(!(timestamp instanceof Date)){
date = Date(timestamp * 1000);
if(timestamp > 1749000000){
date = Date(timestamp);
}
}
//
const replacements = {
//
'd': () => String(date.getDate()).padStart(2, '0'),
'j': () => date.getDate(),
//
'm': () => String(date.getMonth() + 1).padStart(2, '0'),
'n': () => date.getMonth() + 1,
//
'Y': () => date.getFullYear(),
'y': () => String(date.getFullYear()).slice(-2),
//
'H': () => String(date.getHours()).padStart(2, '0'),
'i': () => String(date.getMinutes()).padStart(2, '0'),
's': () => String(date.getSeconds()).padStart(2, '0'),
//
'w': () => date.getDay(), // (0-6)
'A': () => date.getHours() >= 12 ? 'PM' : 'AM',
'a': () => date.getHours() >= 12 ? 'pm' : 'am',
};
//
let result = '';
for (let i = 0; i < format.length; i++) {
const char = format[i];
if (char in replacements) {
result += replacements[char]();
} else {
result += char;
}
}
return result;
},
getAvatarUrl(item) {
if(!item.avatar){
return this.config.cdnurl+'/static/img/avatar.png';
}
return (item.avatar.startsWith('http') ? '' : this.config.cdnurl) + item.avatar;
}
}
}
</script>
<style lang="scss" scoped>
.container {
height: 100vh;
background-color: #F5F5F5;
font-family: "poppins";
.box-1 {
flex: 1;
height: calc(100vh - var(--window-top) - 44px);
}
}
</style>

BIN
static/im/emoji.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -73,7 +73,7 @@ class MattermostClient {
const protocol = process.env.NODE_ENV === 'development' ? 'ws' : 'wss'; const protocol = process.env.NODE_ENV === 'development' ? 'ws' : 'wss';
// 构建 WebSocket URL使用用户 API 域名 // 构建 WebSocket URL使用用户 API 域名
const connectionId = Date.now().toString(36) + Math.random().toString(36).substr(2); const connectionId = Date.now().toString(36) + Math.random().toString(36).substr(2);
this.wsUrl = this.userBaseUrl.replace('https',protocol) + `/api/v4/websocket?connection_id=${connectionId}&sequence_number=0`; this.wsUrl = this.userBaseUrl.replace('https',protocol)+`/api/v4/websocket?connection_id=${connectionId}&sequence_number=0`;
// 重置状态 // 重置状态
this.sequence = 1; this.sequence = 1;
this.reconnectAttempts = 0; this.reconnectAttempts = 0;
@ -591,7 +591,11 @@ class MattermostClient {
// 文件上传封装 // 文件上传封装
async uploadFile(options) { async uploadFile(options) {
const { url, filePath, name = 'files', headers = {} } = options; const { channelId, filePath, name = 'files', headers = {} } = options;
if (!channelId) {
throw new Error('channelId 是必需的');
}
// 添加认证头 // 添加认证头
const requestHeaders = { const requestHeaders = {
@ -599,24 +603,28 @@ class MattermostClient {
...headers ...headers
}; };
const [error, response] = await uni.uploadFile({ // 直接上传文件
url, const [uploadError, uploadResponse] = await uni.uploadFile({
url: `${this.userBaseUrl}/api/v4/files`,
filePath, filePath,
name, name,
header: requestHeaders header: requestHeaders,
formData: {
channel_id: channelId
}
}).catch(err => [err, null]); }).catch(err => [err, null]);
if (error) { if (uploadError) {
console.error(`文件上传失败 [${url}]:`, error); console.error('文件上传失败:', uploadError);
throw error; throw uploadError;
} }
if (response.statusCode >= 200 && response.statusCode < 300) { if (uploadResponse.statusCode >= 200 && uploadResponse.statusCode < 300) {
return response.data; return JSON.parse(uploadResponse.data);
} }
console.error(`文件上传失败 [${url}]:`, response); console.error('文件上传失败:', uploadResponse);
throw new Error(`文件上传失败: ${response.statusCode}`); throw new Error(`文件上传失败: ${uploadResponse.statusCode}`);
} }
// 获取频道消息 // 获取频道消息
@ -664,16 +672,15 @@ class MattermostClient {
try { try {
// 1. 上传文件 // 1. 上传文件
const fileData = await this.uploadFile({ const fileData = await this.uploadFile({
url: `${this.userBaseUrl}/api/v4/files`, channelId,
filePath filePath
}); });
const fileInfo = JSON.parse(fileData); const fileId = fileData.file_infos[0].id;
const fileId = fileInfo.file_infos[0].id;
// 2. 发送带图片的消息 // 2. 发送带图片的消息
return this.request({ return this.request({
url: `${this.userBaseUrl}/api/v4/posts`, url: `/api/v4/posts`,
method: 'POST', method: 'POST',
data: { data: {
channel_id: channelId, channel_id: channelId,
@ -687,6 +694,40 @@ class MattermostClient {
throw error; throw error;
} }
} }
async getThumbnail(file_id, width = 200, height = 200) {
return this.adminBaseUrl+'/api/v4/files/${file_id}/thumbnail?width=${width}&height=${height}';
try {
const response = await this.request({
url: `/api/v4/files/${file_id}/thumbnail`,
method: 'GET',
data: {
width,
height
}
});
return response;
} catch (error) {
console.error('获取缩略图失败:', error);
throw error;
}
}
async getPreview(file_id, width = 800, height = 800) {
try {
const response = await this.request({
url: `/api/v4/files/${file_id}/preview`,
method: 'GET',
data: {
width,
height
}
});
return response;
} catch (error) {
console.error('获取预览图失败:', error);
throw error;
}
}
// 标记消息为已读 // 标记消息为已读
async markChannelAsViewed(channelId) { async markChannelAsViewed(channelId) {