Files
zyt/TUICallKit-Vue3/TUIKit/pages/chat/chat.vue
T
2026-03-11 09:49:47 +08:00

1092 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>