1092 lines
26 KiB
Vue
1092 lines
26 KiB
Vue
<template>
|
||
<view class="chat-container">
|
||
<!-- 顶部导航栏 -->
|
||
<!-- 消息列表 -->
|
||
<scroll-view
|
||
class="message-list"
|
||
scroll-y
|
||
:scroll-top="scrollTop"
|
||
scroll-with-animation
|
||
@scroll="onScroll"
|
||
>
|
||
<view class="message-container">
|
||
<view v-for="(msg, index) in messages" :key="index" class="message-wrapper" :class="msg.flow === 'out' ? 'message-wrapper-out' : 'message-wrapper-in'">
|
||
<!-- 头像 -->
|
||
<!-- 头像 -->
|
||
<view class="avatar" :class="msg.flow === 'out' ? 'avatar-out' : 'avatar-in'">
|
||
<image
|
||
v-if="msg.avatar"
|
||
:src="msg.avatar"
|
||
class="avatar-image"
|
||
mode="aspectFill"
|
||
/>
|
||
<text v-else class="avatar-text">{{ msg.flow === 'out' ? '我' : '医' }}</text>
|
||
</view>
|
||
|
||
<!-- 消息内容 -->
|
||
<view class="message-bubble" :class="msg.flow === 'out' ? 'bubble-out' : 'bubble-in'">
|
||
<!-- 文本消息 -->
|
||
<text v-if="msg.type === 'text'" class="message-text">{{ msg.text }}</text>
|
||
|
||
<!-- 图片消息 -->
|
||
<image v-if="msg.type === 'image'" class="message-image" :src="msg.imageUrl" mode="widthFix" @click="previewImage(msg.imageUrl)" />
|
||
|
||
<!-- 文件消息 -->
|
||
<view v-if="msg.type === 'file'" class="message-file" @click="downloadFile(msg)">
|
||
<view class="file-icon-wrapper">
|
||
<text class="file-icon">*</text>
|
||
</view>
|
||
<view class="file-details">
|
||
<text class="file-name">{{ msg.fileName }}</text>
|
||
<text class="file-size">{{ formatFileSize(msg.fileSize) }}</text>
|
||
</view>
|
||
<text class="download-icon">⬇</text>
|
||
</view>
|
||
|
||
<!-- 表情消息 -->
|
||
<text v-if="msg.type === 'face'" class="message-emoji">{{ msg.emoji }}</text>
|
||
|
||
<!-- 消息时间 -->
|
||
<view class="message-meta">
|
||
<text class="message-time">{{ formatTime(msg.time) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 空状态 -->
|
||
<view v-if="messages.length === 0" class="empty-state">
|
||
<text class="empty-icon">💬</text>
|
||
<text class="empty-text">暂无消息</text>
|
||
<text class="empty-hint">开始聊天吧</text>
|
||
</view>
|
||
|
||
<!-- 底部占位,确保最后一条消息不被遮挡 -->
|
||
<view class="bottom-placeholder"></view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 输入区域 -->
|
||
<view class="input-container" :style="{ paddingBottom: keyboardHeight + 'px' }">
|
||
<!-- 工具栏 -->
|
||
<view class="toolbar">
|
||
<!-- <view class="tool-item" @click="showEmojiPicker">
|
||
<uni-icons type="tool " size="30" color="#999"></uni-icons>
|
||
<text class="tool-label">表情</text>
|
||
</view> -->
|
||
<view class="tool-item" @click="chooseImage">
|
||
<uni-icons type="image " size="30" color="#999"></uni-icons>
|
||
<text class="tool-label">图片</text>
|
||
</view>
|
||
<view class="tool-item" @click="chooseFile">
|
||
<uni-icons type="paperclip " size="30" color="#999"></uni-icons>
|
||
<text class="tool-label">文件</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 输入框 -->
|
||
<view class="input-wrapper">
|
||
<input
|
||
class="message-input"
|
||
v-model="inputText"
|
||
placeholder="输入消息..."
|
||
:adjust-position="false"
|
||
:hold-keyboard="true"
|
||
@focus="onInputFocus"
|
||
@blur="onInputBlur"
|
||
@confirm="sendTextMessage"
|
||
/>
|
||
<view class="send-button" :class="{ 'send-button-active': inputText.trim() }" @click="sendTextMessage">
|
||
<text class="send-text">发送</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 表情选择器 -->
|
||
<view v-if="showEmoji" class="emoji-panel" @click.self="showEmoji = false">
|
||
<view class="emoji-content">
|
||
<view class="emoji-header">
|
||
<text class="emoji-title">选择表情</text>
|
||
<view class="emoji-close" @click="showEmoji = false">
|
||
<text class="close-icon">✕</text>
|
||
</view>
|
||
</view>
|
||
<scroll-view class="emoji-scroll" scroll-y>
|
||
<view class="emoji-grid">
|
||
<view v-for="(emoji, index) in emojiList" :key="index" class="emoji-cell" @click="sendEmoji(emoji)">
|
||
<text class="emoji-icon">{{ emoji }}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, nextTick } from 'vue';
|
||
import { onLoad, onUnload } from '@dcloudio/uni-app';
|
||
import TencentCloudChat from '@tencentcloud/lite-chat/basic';
|
||
import conversationPlugin from '@tencentcloud/lite-chat/plugins/conversation';
|
||
import messageEnhancerPlugin from '@tencentcloud/lite-chat/plugins/message-enhancer';
|
||
import richMediaMessagePlugin from '@tencentcloud/lite-chat/plugins/rich-media-message';
|
||
import groupPlugin from '@tencentcloud/lite-chat/plugins/group';
|
||
|
||
const messages = ref([]);
|
||
const inputText = ref('');
|
||
const scrollTop = ref(0);
|
||
const showEmoji = ref(false);
|
||
const keyboardHeight = ref(0);
|
||
const userAvatar = ref(''); // 当前用户头像
|
||
let chat = null;
|
||
let conversationID = '';
|
||
let targetUserID = '';
|
||
let currentUserID = ''; // 当前登录用户ID
|
||
let scrollTimer = null; // 滚动定时器
|
||
|
||
const emojiList = [
|
||
'😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂',
|
||
'🙂', '🙃', '😉', '😊', '😇', '🥰', '😍', '🤩',
|
||
'😘', '😗', '😚', '😙', '😋', '😛', '😜', '🤪',
|
||
'😝', '🤑', '🤗', '🤭', '🤫', '🤔', '🤐', '🤨',
|
||
'😐', '😑', '😶', '😏', '😒', '🙄', '😬', '🤥',
|
||
'😌', '😔', '😪', '🤤', '😴', '😷', '🤒', '🤕',
|
||
'🤢', '🤮', '🤧', '🥵', '🥶', '😵', '🤯', '🤠',
|
||
'🥳', '😎', '🤓', '🧐', '😕', '😟', '🙁', '☹️',
|
||
'😮', '😯', '😲', '😳', '🥺', '😦', '😧', '😨',
|
||
'😰', '😥', '😢', '😭', '😱', '😖', '😣', '😞',
|
||
'👍', '👎', '👌', '✌️', '🤞', '🤟', '🤘', '🤙',
|
||
'👏', '🙌', '👐', '🤲', '🤝', '🙏', '💪', '❤️'
|
||
];
|
||
|
||
onLoad(async (options) => {
|
||
try {
|
||
const app = getApp();
|
||
const userID = app.globalData.userID || options.userID;
|
||
const userSig = app.globalData.userSig || options.userSig;
|
||
const SDKAppID = app.globalData.SDKAppID || options.SDKAppID;
|
||
targetUserID = options.targetUserID;
|
||
currentUserID = userID; // 保存当前用户ID
|
||
|
||
// 获取用户头像
|
||
const userData = uni.getStorageSync('userData');
|
||
if (userData && userData.avatar) {
|
||
userAvatar.value = userData.avatar;
|
||
}
|
||
|
||
if (!userID || !userSig || !SDKAppID) {
|
||
uni.showToast({ title: '登录信息缺失', icon: 'none' });
|
||
setTimeout(() => uni.navigateBack(), 1500);
|
||
return;
|
||
}
|
||
|
||
conversationID = `C2C${targetUserID}`;
|
||
console.log('当前用户:', currentUserID, '对方用户:', targetUserID);
|
||
|
||
if (!app.globalData.imChat) {
|
||
chat = TencentCloudChat.create({
|
||
SDKAppID: Number(SDKAppID),
|
||
scene: 'uniapp-wx'
|
||
});
|
||
|
||
chat.use(conversationPlugin);
|
||
chat.use(messageEnhancerPlugin);
|
||
chat.use(richMediaMessagePlugin);
|
||
chat.use(groupPlugin);
|
||
|
||
await chat.login({ userID, userSig });
|
||
let promise = chat.updateMyProfile({
|
||
nick: userData.nickname,
|
||
avatar: userData.avatar,
|
||
gender: TencentCloudChat.TYPES.GENDER_MALE,
|
||
})
|
||
promise.then(function(imResponse) {
|
||
console.log('sssssssssssssssssssss',imResponse.data,userData.avatar); // 更新资料成功
|
||
}).catch(function(imError) {
|
||
console.warn('updateMyProfile error:', imError); // 更新资料失败的相关信息
|
||
});
|
||
app.globalData.imChat = chat;
|
||
chat.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, onMessageReceived);
|
||
} else {
|
||
chat = app.globalData.imChat;
|
||
chat.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, onMessageReceived);
|
||
}
|
||
|
||
await loadHistoryMessages();
|
||
uni.showToast({ title: '聊天已就绪', icon: 'success' });
|
||
|
||
// 监听键盘高度变化
|
||
uni.onKeyboardHeightChange((res) => {
|
||
keyboardHeight.value = res.height;
|
||
if (res.height > 0) {
|
||
// 键盘弹起时滚动到底部
|
||
setTimeout(() => scrollToBottom(), 100);
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('初始化失败:', error);
|
||
uni.showModal({
|
||
title: '初始化失败',
|
||
content: error.message || '聊天功能初始化失败',
|
||
showCancel: false,
|
||
success: () => uni.navigateBack()
|
||
});
|
||
}
|
||
});
|
||
|
||
onUnload(() => {
|
||
if (chat) chat.off(TencentCloudChat.EVENT.MESSAGE_RECEIVED, onMessageReceived);
|
||
if (scrollTimer) clearTimeout(scrollTimer);
|
||
});
|
||
|
||
async function loadHistoryMessages() {
|
||
try {
|
||
const res = await chat.getMessageList({ conversationID, count: 15 });
|
||
if (res.data && res.data.messageList) {
|
||
// SDK返回的消息是从新到旧,reverse后变成从旧到新,最新消息在底部
|
||
const parsedMessages = res.data.messageList
|
||
.map(parseMessage)
|
||
.filter(msg => msg !== null) // 过滤掉null消息
|
||
|
||
messages.value = parsedMessages;
|
||
console.log('加载历史消息:', parsedMessages.length, '条');
|
||
scrollToBottom();
|
||
}
|
||
} catch (error) {
|
||
console.error('加载历史消息失败:', error);
|
||
}
|
||
}
|
||
|
||
function parseMessage(msg) {
|
||
// 根据消息的 from 字段判断是发出还是接收
|
||
const actualFlow = msg.from === currentUserID ? 'out' : 'in';
|
||
|
||
// 获取头像:发出的消息使用当前用户头像,接收的消息从消息对象中获取
|
||
let avatar = '';
|
||
if (actualFlow === 'out') {
|
||
avatar = userAvatar.value;
|
||
} else {
|
||
// 尝试从消息对象中获取对方的头像
|
||
// 消息对象可能包含 fromUserProfile 或其他头像字段
|
||
avatar = msg.fromUserProfile?.avatar ||
|
||
msg.fromProfile?.avatar ||
|
||
msg.senderProfile?.avatar ||
|
||
msg.avatar ||
|
||
'';
|
||
}
|
||
|
||
const baseMsg = {
|
||
time: msg.time * 1000,
|
||
flow: actualFlow,
|
||
avatar: avatar
|
||
};
|
||
|
||
switch (msg.type) {
|
||
case TencentCloudChat.TYPES.MSG_TEXT:
|
||
return { ...baseMsg, type: 'text', text: msg.payload.text };
|
||
|
||
case TencentCloudChat.TYPES.MSG_IMAGE:
|
||
// 优先使用原图,如果没有则使用缩略图
|
||
const imageUrl = msg.payload.imageInfoArray?.[0]?.url ||
|
||
msg.payload.imageInfoArray?.[1]?.url ||
|
||
msg.payload.imageInfoArray?.[2]?.url;
|
||
return { ...baseMsg, type: 'image', imageUrl };
|
||
|
||
case TencentCloudChat.TYPES.MSG_FILE:
|
||
return {
|
||
...baseMsg,
|
||
type: 'file',
|
||
fileName: msg.payload.fileName,
|
||
fileSize: msg.payload.fileSize,
|
||
fileUrl: msg.payload.fileUrl
|
||
};
|
||
|
||
case TencentCloudChat.TYPES.MSG_FACE:
|
||
// 表情消息,data字段包含表情符号
|
||
return { ...baseMsg, type: 'face', emoji: msg.payload.data };
|
||
|
||
case TencentCloudChat.TYPES.MSG_CUSTOM:
|
||
// 自定义消息(如通话消息)
|
||
try {
|
||
const customData = JSON.parse(msg.payload.data);
|
||
console.log('自定义消息:', customData);
|
||
|
||
// 过滤掉输入状态消息
|
||
if (customData.businessID === 'user_typing_status') {
|
||
console.log('过滤输入状态消息');
|
||
return null; // 返回null表示不显示此消息
|
||
}
|
||
|
||
// 通话消息
|
||
if (customData.businessID === 1 || customData.businessID === 'av_call') {
|
||
const callType = customData.call_type === 1 ? '语音通话' : '视频通话';
|
||
return { ...baseMsg, type: 'text', text: `[${callType}]` };
|
||
}
|
||
|
||
// 其他自定义消息,如果有text字段则显示
|
||
if (customData.text) {
|
||
return { ...baseMsg, type: 'text', text: customData.text };
|
||
}
|
||
|
||
// 没有可显示内容的自定义消息,不显示
|
||
console.log('过滤无内容的自定义消息');
|
||
return null;
|
||
} catch (e) {
|
||
console.error('解析自定义消息失败:', e);
|
||
return null; // 解析失败的消息不显示
|
||
}
|
||
|
||
case TencentCloudChat.TYPES.MSG_AUDIO:
|
||
return { ...baseMsg, type: 'text', text: '[语音消息]' };
|
||
|
||
case TencentCloudChat.TYPES.MSG_VIDEO:
|
||
return { ...baseMsg, type: 'text', text: '[视频消息]' };
|
||
|
||
case TencentCloudChat.TYPES.MSG_LOCATION:
|
||
return { ...baseMsg, type: 'text', text: '[位置消息]' };
|
||
|
||
default:
|
||
console.log('未知消息类型:', msg.type);
|
||
return null; // 未知类型不显示
|
||
}
|
||
}
|
||
|
||
function onMessageReceived(event) {
|
||
event.data.forEach(msg => {
|
||
if (msg.conversationID === conversationID) {
|
||
const parsedMsg = parseMessage(msg);
|
||
// 只添加非null的消息
|
||
if (parsedMsg !== null) {
|
||
messages.value.push(parsedMsg);
|
||
scrollToBottom();
|
||
setTimeout(() => scrollToBottom(), 150);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
async function sendTextMessage() {
|
||
if (!inputText.value.trim()) return;
|
||
try {
|
||
const message = chat.createTextMessage({
|
||
to: targetUserID,
|
||
conversationType: TencentCloudChat.TYPES.CONV_C2C,
|
||
payload: { text: inputText.value }
|
||
});
|
||
await chat.sendMessage(message);
|
||
messages.value.push({ type: 'text', text: inputText.value, time: Date.now(), flow: 'out', avatar: userAvatar.value });
|
||
inputText.value = '';
|
||
// 立即滚动,然后再延迟滚动一次确保成功
|
||
scrollToBottom();
|
||
setTimeout(() => scrollToBottom(), 150);
|
||
} catch (error) {
|
||
console.error('发送消息失败:', error);
|
||
uni.showToast({ title: '发送失败', icon: 'none' });
|
||
}
|
||
}
|
||
|
||
function chooseImage() {
|
||
// #ifdef MP-WEIXIN
|
||
uni.chooseMedia({
|
||
count: 1,
|
||
mediaType: ['image'],
|
||
sizeType: ['original', 'compressed'],
|
||
sourceType: ['album', 'camera'],
|
||
success: (res) => {
|
||
console.log('选择图片成功:', res);
|
||
sendImageMessage(res);
|
||
},
|
||
fail: (err) => {
|
||
console.error('选择图片失败:', err);
|
||
uni.showToast({ title: '选择图片失败', icon: 'none' });
|
||
}
|
||
});
|
||
// #endif
|
||
// #ifndef MP-WEIXIN
|
||
uni.chooseImage({
|
||
count: 1,
|
||
sizeType: ['original', 'compressed'],
|
||
sourceType: ['album', 'camera'],
|
||
success: (res) => {
|
||
console.log('选择图片成功:', res);
|
||
sendImageMessage(res);
|
||
},
|
||
fail: (err) => {
|
||
console.error('选择图片失败:', err);
|
||
uni.showToast({ title: '选择图片失败', icon: 'none' });
|
||
}
|
||
});
|
||
// #endif
|
||
}
|
||
|
||
async function sendImageMessage(res) {
|
||
try {
|
||
console.log('开始发送图片,res对象:', res);
|
||
uni.showLoading({ title: '发送中...' });
|
||
|
||
// 根据文档,直接传入 uni.chooseMedia 或 uni.chooseImage 的 success 回调参数
|
||
const message = chat.createImageMessage({
|
||
to: targetUserID,
|
||
conversationType: TencentCloudChat.TYPES.CONV_C2C,
|
||
payload: {
|
||
file: res // 直接传入整个res对象
|
||
},
|
||
onProgress: (event) => {
|
||
console.log('图片上传进度:', event);
|
||
}
|
||
});
|
||
|
||
console.log('图片消息创建成功,开始发送');
|
||
const sendResult = await chat.sendMessage(message);
|
||
console.log('图片发送成功:', sendResult);
|
||
|
||
// 从发送结果中获取图片URL
|
||
const imageUrl = sendResult.data.message.payload.imageInfoArray?.[0]?.url || res.tempFilePaths?.[0] || res.tempFiles?.[0]?.tempFilePath;
|
||
messages.value.push({ type: 'image', imageUrl, time: Date.now(), flow: 'out', avatar: userAvatar.value });
|
||
uni.hideLoading();
|
||
uni.showToast({ title: '发送成功', icon: 'success' });
|
||
scrollToBottom();
|
||
setTimeout(() => scrollToBottom(), 150);
|
||
} catch (error) {
|
||
console.error('发送图片失败:', error);
|
||
uni.hideLoading();
|
||
uni.showToast({ title: '发送失败: ' + (error.message || '未知错误'), icon: 'none', duration: 3000 });
|
||
}
|
||
}
|
||
|
||
function chooseFile() {
|
||
// #ifdef MP-WEIXIN
|
||
wx.chooseMessageFile({
|
||
count: 1,
|
||
type: 'all',
|
||
success: (res) => {
|
||
console.log('选择文件成功:', res);
|
||
sendFileMessage(res);
|
||
},
|
||
fail: (err) => {
|
||
console.error('选择文件失败:', err);
|
||
uni.showToast({ title: '选择文件失败', icon: 'none' });
|
||
}
|
||
});
|
||
// #endif
|
||
// #ifndef MP-WEIXIN
|
||
uni.showToast({ title: '当前平台不支持', icon: 'none' });
|
||
// #endif
|
||
}
|
||
|
||
async function sendFileMessage(res) {
|
||
try {
|
||
console.log('开始发送文件,res对象:', res);
|
||
uni.showLoading({ title: '发送中...' });
|
||
|
||
// 根据文档,直接传入 wx.chooseMessageFile 的 success 回调参数
|
||
const message = chat.createFileMessage({
|
||
to: targetUserID,
|
||
conversationType: TencentCloudChat.TYPES.CONV_C2C,
|
||
payload: {
|
||
file: res // 直接传入整个res对象
|
||
},
|
||
onProgress: (event) => {
|
||
console.log('文件上传进度:', event);
|
||
}
|
||
});
|
||
|
||
console.log('文件消息创建成功,开始发送');
|
||
const sendResult = await chat.sendMessage(message);
|
||
console.log('文件发送成功:', sendResult);
|
||
|
||
const file = res.tempFiles?.[0] || {};
|
||
messages.value.push({
|
||
type: 'file',
|
||
fileName: file.name || '文件',
|
||
fileSize: file.size || 0,
|
||
time: Date.now(),
|
||
flow: 'out',
|
||
avatar: userAvatar.value
|
||
});
|
||
uni.hideLoading();
|
||
uni.showToast({ title: '发送成功', icon: 'success' });
|
||
scrollToBottom();
|
||
setTimeout(() => scrollToBottom(), 150);
|
||
} catch (error) {
|
||
console.error('发送文件失败:', error);
|
||
uni.hideLoading();
|
||
uni.showToast({ title: '发送失败: ' + (error.message || '未知错误'), icon: 'none', duration: 3000 });
|
||
}
|
||
}
|
||
|
||
function showEmojiPicker() {
|
||
showEmoji.value = !showEmoji.value;
|
||
}
|
||
|
||
async function sendEmoji(emoji) {
|
||
try {
|
||
const message = chat.createFaceMessage({
|
||
to: targetUserID,
|
||
conversationType: TencentCloudChat.TYPES.CONV_C2C,
|
||
payload: { index: 0, data: emoji }
|
||
});
|
||
await chat.sendMessage(message);
|
||
messages.value.push({ type: 'face', emoji, time: Date.now(), flow: 'out', avatar: userAvatar.value });
|
||
showEmoji.value = false;
|
||
scrollToBottom();
|
||
setTimeout(() => scrollToBottom(), 150);
|
||
} catch (error) {
|
||
console.error('发送表情失败:', error);
|
||
uni.showToast({ title: '发送失败', icon: 'none' });
|
||
}
|
||
}
|
||
|
||
function previewImage(url) {
|
||
uni.previewImage({ urls: [url], current: url });
|
||
}
|
||
|
||
function downloadFile(msg) {
|
||
uni.showModal({
|
||
title: '下载文件',
|
||
content: `是否下载文件:${msg.fileName}?`,
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
uni.downloadFile({
|
||
url: msg.fileUrl,
|
||
success: (res) => {
|
||
if (res.statusCode === 200) uni.showToast({ title: '下载成功', icon: 'success' });
|
||
}
|
||
});
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
async function makeVideoCall() {
|
||
try {
|
||
if (!uni.CallManager) {
|
||
uni.showToast({ title: '通话功能未初始化', icon: 'none' });
|
||
return;
|
||
}
|
||
await uni.CallManager.call({ userID: targetUserID, type: 2 });
|
||
} catch (error) {
|
||
console.error('发起通话失败:', error);
|
||
uni.showToast({ title: '发起通话失败', icon: 'none' });
|
||
}
|
||
}
|
||
|
||
function onInputFocus() {
|
||
console.log('输入框获得焦点');
|
||
showEmoji.value = false;
|
||
}
|
||
|
||
function onInputBlur() {
|
||
console.log('输入框失去焦点');
|
||
}
|
||
|
||
function onScroll(e) {
|
||
// 监听滚动事件,可用于判断是否在底部
|
||
}
|
||
|
||
function scrollToBottom() {
|
||
if (scrollTimer) {
|
||
clearTimeout(scrollTimer);
|
||
}
|
||
|
||
nextTick(() => {
|
||
// 使用查询节点的方式获取准确高度
|
||
const query = uni.createSelectorQuery();
|
||
query.select('.message-container').boundingClientRect();
|
||
query.exec((res) => {
|
||
if (res && res[0]) {
|
||
const height = res[0].height;
|
||
console.log('消息容器高度:', height);
|
||
// 设置一个足够大的值确保滚动到底部
|
||
scrollTop.value = height + 1000;
|
||
|
||
// 再次确认滚动
|
||
scrollTimer = setTimeout(() => {
|
||
scrollTop.value = height + 1000 + 1;
|
||
}, 100);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function goBack() {
|
||
uni.navigateBack();
|
||
}
|
||
|
||
function formatTime(timestamp) {
|
||
const date = new Date(timestamp);
|
||
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
||
}
|
||
|
||
function formatFileSize(bytes) {
|
||
if (bytes < 1024) return bytes + ' B';
|
||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
|
||
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 容器 */
|
||
.chat-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
background: #f7f8fa;
|
||
}
|
||
|
||
/* 顶部导航栏 */
|
||
.chat-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
height: 88rpx;
|
||
padding: 0 20rpx;
|
||
background: #fff;
|
||
border-bottom: 1rpx solid #e7e7e7;
|
||
}
|
||
|
||
.header-left {
|
||
width: 88rpx;
|
||
height: 88rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.back-icon {
|
||
font-size: 48rpx;
|
||
color: #576b95;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.header-center {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.header-title {
|
||
font-size: 34rpx;
|
||
font-weight: 500;
|
||
color: #000;
|
||
}
|
||
|
||
.header-status {
|
||
font-size: 22rpx;
|
||
color: #888;
|
||
margin-top: 2rpx;
|
||
}
|
||
|
||
.header-right {
|
||
width: 88rpx;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.video-call-btn {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.video-icon {
|
||
font-size: 44rpx;
|
||
color: #576b95;
|
||
}
|
||
|
||
/* 消息列表 */
|
||
.message-list {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.message-container {
|
||
padding: 20rpx 24rpx;
|
||
padding-bottom: 280rpx;
|
||
min-height: 100%;
|
||
}
|
||
|
||
.message-wrapper {
|
||
display: flex;
|
||
margin-bottom: 24rpx;
|
||
animation: fadeIn 0.25s ease;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; transform: translateY(10rpx); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.message-wrapper-in {
|
||
flex-direction: row;
|
||
}
|
||
|
||
.message-wrapper-out {
|
||
flex-direction: row-reverse;
|
||
}
|
||
|
||
/* 头像 */
|
||
.avatar {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
border-radius: 8rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
font-weight: 500;
|
||
background: #e8e8e8;
|
||
}
|
||
|
||
.avatar-in {
|
||
background: #e8e8e8;
|
||
margin-right: 16rpx;
|
||
}
|
||
|
||
.avatar-out {
|
||
background: #95ec69;
|
||
margin-left: 16rpx;
|
||
}
|
||
|
||
.avatar-text {
|
||
font-size: 28rpx;
|
||
color: #fff;
|
||
}
|
||
|
||
.avatar-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
/* 消息气泡 */
|
||
.message-bubble {
|
||
max-width: 500rpx;
|
||
padding: 20rpx 24rpx;
|
||
border-radius: 8rpx;
|
||
position: relative;
|
||
}
|
||
|
||
.bubble-in {
|
||
background: #fff;
|
||
}
|
||
|
||
.bubble-out {
|
||
background: #95ec69;
|
||
}
|
||
|
||
/* 文本消息 */
|
||
.message-text {
|
||
font-size: 30rpx;
|
||
line-height: 1.5;
|
||
color: #000;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.bubble-out .message-text {
|
||
color: #000;
|
||
}
|
||
|
||
/* 图片消息 */
|
||
.message-image {
|
||
max-width: 100%;
|
||
border-radius: 8rpx;
|
||
display: block;
|
||
}
|
||
|
||
/* 文件消息 */
|
||
.message-file {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
padding: 12rpx;
|
||
background: rgba(0, 0, 0, 0.04);
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.bubble-out .message-file {
|
||
background: rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
.file-icon-wrapper {
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
background: #576b95;
|
||
border-radius: 8rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.bubble-out .file-icon-wrapper {
|
||
background: rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.file-icon {
|
||
font-size: 40rpx;
|
||
}
|
||
|
||
.file-details {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6rpx;
|
||
}
|
||
|
||
.file-name {
|
||
font-size: 28rpx;
|
||
color: #000;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.bubble-out .file-name {
|
||
color: #000;
|
||
}
|
||
|
||
.file-size {
|
||
font-size: 22rpx;
|
||
color: #888;
|
||
}
|
||
|
||
.bubble-out .file-size {
|
||
color: #666;
|
||
}
|
||
|
||
.download-icon {
|
||
font-size: 28rpx;
|
||
color: #576b95;
|
||
}
|
||
|
||
.bubble-out .download-icon {
|
||
color: #000;
|
||
}
|
||
|
||
/* 表情消息 */
|
||
.message-emoji {
|
||
font-size: 96rpx;
|
||
line-height: 1;
|
||
}
|
||
|
||
/* 消息元信息 */
|
||
.message-meta {
|
||
margin-top: 8rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.message-time {
|
||
font-size: 20rpx;
|
||
color: #b2b2b2;
|
||
}
|
||
|
||
.bubble-out .message-time {
|
||
color: #888;
|
||
}
|
||
|
||
/* 空状态 */
|
||
.empty-state {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 600rpx;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 100rpx;
|
||
opacity: 0.25;
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 28rpx;
|
||
color: #b2b2b2;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.empty-hint {
|
||
font-size: 24rpx;
|
||
color: #d0d0d0;
|
||
}
|
||
|
||
/* 底部占位 */
|
||
.bottom-placeholder {
|
||
height: 40rpx;
|
||
}
|
||
|
||
/* 输入区域 */
|
||
.input-container {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
background: #f7f8fa;
|
||
border-top: 1rpx solid #e7e7e7;
|
||
padding: 16rpx 20rpx;
|
||
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
|
||
z-index: 100;
|
||
transition: padding-bottom 0.3s ease;
|
||
}
|
||
|
||
/* 工具栏 */
|
||
.toolbar {
|
||
display: flex;
|
||
gap: 32rpx;
|
||
margin-bottom: 16rpx;
|
||
padding: 0 8rpx;
|
||
}
|
||
|
||
.tool-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 6rpx;
|
||
}
|
||
|
||
.tool-icon {
|
||
font-size: 48rpx;
|
||
}
|
||
|
||
.tool-label {
|
||
font-size: 20rpx;
|
||
color: #888;
|
||
}
|
||
|
||
/* 输入框 */
|
||
.input-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
margin-bottom: 45rpx;
|
||
}
|
||
|
||
.message-input {
|
||
flex: 1;
|
||
height: 72rpx;
|
||
padding: 0 24rpx;
|
||
background: #fff;
|
||
border-radius: 8rpx;
|
||
border: 1rpx solid #e7e7e7;
|
||
font-size: 28rpx;
|
||
color: #000;
|
||
}
|
||
|
||
.send-button {
|
||
width: 112rpx;
|
||
height: 72rpx;
|
||
background: #ededed;
|
||
border-radius: 8rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.send-button-active {
|
||
background: #07c160;
|
||
}
|
||
|
||
.send-text {
|
||
font-size: 28rpx;
|
||
color: #888;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.send-button-active .send-text {
|
||
color: #fff;
|
||
}
|
||
|
||
/* 表情选择器 */
|
||
.emoji-panel {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.4);
|
||
display: flex;
|
||
align-items: flex-end;
|
||
z-index: 1000;
|
||
animation: fadeIn 0.25s ease;
|
||
}
|
||
|
||
.emoji-content {
|
||
width: 100%;
|
||
max-height: 65vh;
|
||
background: #f7f8fa;
|
||
border-radius: 24rpx 24rpx 0 0;
|
||
animation: slideUp 0.25s ease;
|
||
}
|
||
|
||
@keyframes slideUp {
|
||
from { transform: translateY(100%); }
|
||
to { transform: translateY(0); }
|
||
}
|
||
|
||
.emoji-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 28rpx 28rpx 20rpx;
|
||
border-bottom: 1rpx solid #e7e7e7;
|
||
background: #fff;
|
||
}
|
||
|
||
.emoji-title {
|
||
font-size: 30rpx;
|
||
font-weight: 500;
|
||
color: #000;
|
||
}
|
||
|
||
.emoji-close {
|
||
width: 52rpx;
|
||
height: 52rpx;
|
||
background: #f0f0f0;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.close-icon {
|
||
font-size: 28rpx;
|
||
color: #888;
|
||
}
|
||
|
||
.emoji-scroll {
|
||
max-height: 55vh;
|
||
padding: 20rpx;
|
||
background: #f7f8fa;
|
||
}
|
||
|
||
.emoji-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.emoji-cell {
|
||
width: 96rpx;
|
||
height: 96rpx;
|
||
background: #fff;
|
||
border-radius: 12rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.emoji-cell:active {
|
||
transform: scale(0.92);
|
||
background: #f0f0f0;
|
||
}
|
||
|
||
.emoji-icon {
|
||
font-size: 52rpx;
|
||
}
|
||
</style>
|