<template>
|
<view class="chat-container">
|
<!-- 顶部导航栏 -->
|
<view class="chat-header">
|
<view class="header-left">
|
<view class="back-btn" @click="goBack">
|
<text class="iconfont icon-back"></text>
|
</view>
|
</view>
|
|
<view class="header-center">
|
<view class="user-info">
|
<!-- <image class="user-avatar" :src="targetUser.avatarUrl || '/static/default.png'" mode="aspectFill">
|
</image> -->
|
<view class="user-details" v-if="!card">
|
<text class="user-name">{{ targetUser.nickName || '用户' }}</text>
|
<text class="user-status" :class="{'online': isOnline}">
|
{{ isOnline ? '在线' : '离线' }}
|
</text>
|
</view>
|
<view class="user-details" v-else>
|
<text class="user-name">{{ card.name}}</text>
|
<text class="user-status">
|
{{ card.unit[0]+' . '+card.position[0]}}
|
</text>
|
</view>
|
</view>
|
</view>
|
|
<view class="header-right">
|
<view class="more-btn" @click="showMoreActions">
|
<text class="iconfont icon-more"></text>
|
</view>
|
</view>
|
</view>
|
|
<!-- 聊天记录区域 -->
|
<scroll-view class="chat-messages" scroll-y="true" :scroll-top="scrollTop" @scrolltoupper="loadMoreMessages"
|
ref="messageScroll" @scroll="onScroll">
|
<view class="messages-container">
|
<!-- 加载更多提示 -->
|
<view class="load-more" v-if="hasMore" @click="loadMoreMessages">
|
<text class="load-text">加载更多消息</text>
|
</view>
|
|
<!-- 消息列表 -->
|
<view class="message-item" v-for="(message, index) in messages" :key="getMessageKey(message, index)"
|
:class="{'mine': message.sender_id === currentUser.user_id, 'other': message.sender_id !== currentUser.user_id}">
|
|
<!-- 对方消息 -->
|
<view class="message-other" v-if="message.sender_id !== currentUser.user_id">
|
<view class="message-content">
|
<view class="d-s-e">
|
<view class="d-f">
|
<image class="avatar" :src="targetUser.avatarUrl || '/static/default.png'"
|
mode="aspectFill">
|
</image>
|
<view class="message-bubble">
|
<text class="message-text">{{ message.content }}</text>
|
</view>
|
</view>
|
<!-- <view class="message-status" :class="message.is_read ? 'read-read' : 'read-sent'">
|
<text class="icon iconfont icon-eye "></text>
|
</view> -->
|
</view>
|
<view class="message-info">
|
<text class="message-time">{{ formatTime(message.send_time) }}</text>
|
</view>
|
</view>
|
</view>
|
|
<!-- 我的消息 -->
|
<view class="message-mine" v-else>
|
<view class="message-content">
|
<view class="d-s-e">
|
<view class="message-status" :class="message.is_read ? 'read-read' : 'read-sent'">
|
</view>
|
<view class="d-f">
|
<view class="message-bubble">
|
<text class="message-text">{{ message.content }}</text>
|
</view>
|
<image class="avatar" :src="currentUser.avatarUrl || '/static/default.png'"
|
mode="aspectFill">
|
</image>
|
</view>
|
|
</view>
|
<view class="message-info">
|
<text class="message-time">{{ formatTime(message.send_time) }}</text>
|
</view>
|
|
</view>
|
</view>
|
</view>
|
|
<!-- 输入状态提示 -->
|
<view class="typing-indicator" v-if="isTyping">
|
<image class="avatar" :src="targetUser.avatarUrl || '/static/default.png'" mode="aspectFill">
|
</image>
|
<view class="typing-bubble">
|
<view class="typing-dots">
|
<view class="dot"></view>
|
<view class="dot"></view>
|
<view class="dot"></view>
|
</view>
|
<text class="typing-text">对方正在输入...</text>
|
</view>
|
</view>
|
</view>
|
</scroll-view>
|
|
<!-- 底部输入区域 -->
|
<view class="chat-input-area">
|
<view class="input-container">
|
<!-- <view class="input-left">
|
<view class="emoji-btn" @click="toggleEmoji">
|
<text class="iconfont icon-emoji"></text>
|
</view>
|
<view class="more-btn" @click="toggleMore">
|
<text class="iconfont icon-plus"></text>
|
</view>
|
</view> -->
|
|
<view class="input-main">
|
<textarea class="message-input" v-model="inputMessage" :placeholder="placeholderText"
|
@focus="onInputFocus" @blur="onInputBlur" @input="onInputChange" maxlength="500"
|
:auto-height="true" :show-confirm-bar="false">
|
</textarea>
|
</view>
|
|
<view class="input-right">
|
<button class="send-btn" :class="{'active': inputMessage.trim()}" @click="sendMessage"
|
:disabled="!inputMessage.trim()">
|
<text class="send-text">发送</text>
|
</button>
|
</view>
|
</view>
|
|
<!-- 更多功能面板 -->
|
<view class="more-panel" v-if="showMorePanel">
|
<view class="panel-grid">
|
<view class="panel-item" @click="sendImage">
|
<view class="item-icon">
|
<text class="iconfont icon-image"></text>
|
</view>
|
<text class="item-text">图片</text>
|
</view>
|
<view class="panel-item" @click="sendFile">
|
<view class="item-icon">
|
<text class="iconfont icon-file"></text>
|
</view>
|
<text class="item-text">文件</text>
|
</view>
|
<view class="panel-item" @click="sendContact">
|
<view class="item-icon">
|
<text class="iconfont icon-contact"></text>
|
</view>
|
<text class="item-text">名片</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<!-- 操作菜单 -->
|
<Popup :show="actionPopup" type="bottom">
|
<view class="action-menu">
|
<view class="menu-item" @click="viewUserProfile">查看资料</view>
|
<view class="menu-item" @click="clearChatHistory">清空聊天记录</view>
|
<view class="menu-item cancel" @click="closeActionMenu">取消</view>
|
</view>
|
</Popup>
|
</view>
|
</template>
|
|
<script>
|
import Popup from '@/components/uni-popup.vue';
|
export default {
|
components: {
|
Popup,
|
},
|
data() {
|
return {
|
// 当前会话信息
|
conversationId: 0,
|
businessCardId: 0,
|
targetUserId: 0,
|
actionPopup: false,
|
|
// 用户信息
|
currentUser: {},
|
targetUser: {
|
nickName: '',
|
avatarUrl: ''
|
},
|
card: {},
|
wsUrl:"",
|
|
// 聊天数据
|
messages: [],
|
page: 1,
|
limit: 20,
|
loadMore: false,
|
hasMore: true,
|
|
// 输入状态
|
inputMessage: '',
|
showEmojiPanel: false,
|
showMorePanel: false,
|
isTyping: false,
|
isOnline: false,
|
|
// 滚动控制
|
scrollTop: 0,
|
oldScrollTop: 0,
|
|
// 定时器
|
typingTimer: null,
|
readTimer: null,
|
reconnectTimer: null,
|
reconnectAttempts: 0,
|
maxReconnectAttempts: 5,
|
heartbeatTimer: null,
|
is_exit: false //是否退出
|
};
|
},
|
onLoad(options) {
|
console.log('聊天页面参数:', options);
|
|
this.targetUserId = parseInt(options.user_id) || 0;
|
this.businessCardId = parseInt(options.business_card_id) || 0;
|
this.conversationId = parseInt(options.conversation_id) || 0;
|
this.targetUser.nickName = options.nickName || '用户';
|
// 获取当前用户信息
|
this.getCurrentUser();
|
if (!this.conversationId) {
|
// 初始化会话
|
this.initConversation();
|
}
|
// 加载消息
|
this.loadMessages();
|
},
|
onUnload() {
|
this.is_exit = true;
|
// 清理定时器
|
if (this.typingTimer) {
|
clearTimeout(this.typingTimer);
|
}
|
if (this.readTimer) {
|
clearTimeout(this.readTimer);
|
}
|
if (this.reconnectTimer) {
|
clearTimeout(this.reconnectTimer);
|
}
|
// 关闭WebSocket
|
this.closeWebSocket();
|
},
|
methods: {
|
// 生成消息的key值
|
getMessageKey(message, index) {
|
return message.chat_id || index;
|
},
|
|
// 获取当前用户信息
|
getCurrentUser() {
|
let self = this;
|
self._get('user.user/detail', {}, res => {
|
self.currentUser = res.data.userInfo;
|
})
|
},
|
|
// 初始化会话
|
async initConversation() {
|
let self = this;
|
this._post('plus.business.chat.chat/getOrCreateConversation', {
|
business_card_id: this.businessCardId
|
}, res => {
|
this.conversationId = res.data.conversation.conversation_id;
|
this.wsUrl = res.data.wsUrl;
|
// 初始化WebSocket连接
|
this.initWebSocket();
|
});
|
},
|
|
// 加载消息列表
|
async loadMessages() {
|
let self = this;
|
if (self.loadMore) return;
|
|
self.loadMore = true;
|
const params = {
|
page: self.page,
|
limit: self.limit
|
};
|
|
if (self.conversationId) {
|
params.conversation_id = self.conversationId;
|
} else if (self.businessCardId) {
|
params.business_card_id = self.businessCardId;
|
}
|
|
self._get('plus.business.chat.chat/getMessages', params, res => {
|
if (res.code === 1) {
|
const newMessages = res.data.messages.data || [];
|
|
if (self.page === 1) {
|
// First page - sort messages by create_time ascending, newest at bottom
|
self.messages = newMessages.sort((a, b) => {
|
return a.send_time - b.send_time;
|
});
|
} else {
|
// For pagination, prepend older messages
|
const sortedMessages = newMessages.sort((a, b) => {
|
return a.send_time - b.send_time;
|
});
|
self.messages = [...sortedMessages, ...self.messages];
|
}
|
|
self.hasMore = self.messages.length < res.data.messages.total;
|
self.loadMore = false;
|
self.page++;
|
if (self.page == 2) {
|
// 获取目标用户信息
|
this.targetUser = res.data.heUser;
|
this.card = res.data.card;
|
// 标记整个会话为已读
|
if (self.conversationId) {
|
self._post('plus.business.chat.chat/markAsRead', {
|
conversation_id: self.conversationId
|
}, res => {
|
if (res.code === 1) {
|
console.log('会话消息已全部标记为已读');
|
// 更新本地消息状态
|
self.messages.forEach(msg => {
|
if (msg.sender_id !== self.currentUser.user_id) {
|
msg.is_read = true;
|
}
|
});
|
|
// 滚动到底部
|
self.$nextTick(() => {
|
self.scrollToBottom();
|
});
|
}
|
});
|
}
|
}
|
}
|
});
|
|
|
},
|
|
// 发送消息
|
async sendMessage() {
|
let self = this;
|
if (!this.inputMessage.trim()) return;
|
const content = self.inputMessage.trim();
|
self.sendWebSocketMessage(content);
|
},
|
|
// WebSocket相关方法
|
initWebSocket() {
|
// 检查是否已连接
|
if (this.socketTask) {
|
console.log('WebSocket已连接');
|
return;
|
}
|
// 获取当前用户ID
|
const currentUserId = this.currentUser.user_id;
|
|
// 构建WebSocket连接URL - 使用名片聊天专用端口2349
|
const wsUrl = `${this.wsUrl}:2349?user_id=${currentUserId}`;
|
try {
|
// 创建WebSocket连接
|
this.socketTask = uni.connectSocket({
|
url: wsUrl,
|
success: () => {},
|
fail: (err) => {
|
// 尝试重连
|
this.reconnectWebSocket();
|
}
|
});
|
|
// 监听WebSocket打开事件
|
this.socketTask.onOpen(() => {
|
this.isOnline = true;
|
this.reconnectAttempts = 0; // 重置重连计数
|
// 发送心跳包
|
this.startHeartbeat();
|
// 绑定用户ID
|
this.bindUserId();
|
});
|
|
// 监听WebSocket消息事件
|
this.socketTask.onMessage((res) => {
|
console.log('收到WebSocket消息:', res.data);
|
try {
|
const data = JSON.parse(res.data);
|
if (data.Online == 'on') {
|
this.isOnline = false;
|
} else {
|
this.isOnline = true;
|
}
|
this.handleWebSocketMessage(data);
|
} catch (error) {
|
console.error('解析WebSocket消息失败:', error);
|
}
|
});
|
|
// 监听WebSocket错误事件
|
this.socketTask.onError((err) => {
|
console.error('WebSocket错误:', err);
|
this.isOnline = false;
|
// 尝试重连
|
this.reconnectWebSocket();
|
});
|
|
// 监听WebSocket关闭事件
|
this.socketTask.onClose(() => {
|
console.log('WebSocket连接已关闭');
|
this.isOnline = false;
|
this.socketTask = null;
|
|
// 停止心跳
|
if (this.heartbeatTimer) {
|
clearInterval(this.heartbeatTimer);
|
this.heartbeatTimer = null;
|
}
|
|
// 尝试重连
|
this.reconnectWebSocket();
|
});
|
|
} catch (error) {
|
console.error('初始化WebSocket失败:', error);
|
// 尝试重连
|
this.reconnectWebSocket();
|
}
|
},
|
|
// 绑定用户ID
|
bindUserId() {
|
if (this.socketTask && this.currentUser.user_id) {
|
const bindMsg = {
|
type: 'bind',
|
user_id: this.currentUser.user_id
|
};
|
try {
|
this.socketTask.send({
|
data: JSON.stringify(bindMsg)
|
});
|
console.log('用户ID绑定消息已发送');
|
} catch (error) {
|
console.log('发送用户ID绑定消息失败:', error);
|
}
|
}
|
},
|
|
// WebSocket重连机制
|
reconnectWebSocket() {
|
if (this.is_exit) {
|
console.log(this.is_exit);
|
return false;
|
}
|
this.reconnectAttempts++;
|
console.log(`尝试第${this.reconnectAttempts}次重连WebSocket`);
|
|
// 清除之前的重连定时器
|
if (this.reconnectTimer) {
|
clearTimeout(this.reconnectTimer);
|
}
|
|
// 设置重连延迟,指数退避策略
|
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 10000);
|
this.reconnectTimer = setTimeout(() => {
|
this.initWebSocket();
|
}, delay);
|
},
|
|
// 处理WebSocket消息
|
handleWebSocketMessage(data) {
|
switch (data.type) {
|
case 'message':
|
// 收到新消息
|
this.handleNewMessage(data);
|
break;
|
case 'read':
|
// 收到已读确认
|
this.handleReadMessage(data);
|
break;
|
case 'ping':
|
// 心跳响应
|
console.log('收到心跳响应');
|
break;
|
case 'init':
|
// 连接初始化
|
console.log('WebSocket连接初始化完成');
|
break;
|
default:
|
console.log('未知消息类型:', data.type);
|
}
|
},
|
|
// 处理新消息
|
handleNewMessage(data) {
|
// 检查消息是否属于当前会话
|
if ((data.business_card_id && data.business_card_id === this.businessCardId) ||
|
(data.conversation_id && data.conversation_id === this.conversationId)) {
|
|
// 添加消息到列表
|
const newMessage = {
|
chat_id: data.chat_id || Date.now(),
|
conversation_id: data.conversation_id,
|
sender_id: data.user_id,
|
content: data.content,
|
send_time: data.send_time,
|
create_time: data.create_time || Math.floor(Date.now() / 1000),
|
is_read: false
|
};
|
|
this.messages.push(newMessage);
|
|
// 滚动到底部
|
this.$nextTick(() => {
|
this.scrollToBottom();
|
});
|
|
// 标记消息为已读
|
setTimeout(() => {
|
// 通过WebSocket发送已读标记
|
this.markMessageAsRead(newMessage.chat_id);
|
// 调用后端API标记消息为已读
|
this._post('plus.business.chat.chat/markAsRead', {
|
chat_id: newMessage.chat_id,
|
conversation_id: newMessage.conversation_id
|
}, res => {
|
if (res.code === 1) {
|
console.log('消息已读状态更新成功');
|
// 更新本地消息状态
|
const msgIndex = this.messages.findIndex(msg => msg.chat_id === newMessage
|
.chat_id);
|
if (msgIndex !== -1) {
|
this.messages[msgIndex].is_read = true;
|
}
|
}
|
});
|
}, 1000);
|
}
|
},
|
|
// 处理已读消息
|
handleReadMessage(data) {
|
// 筛选出当前用户发送的、尚未标记为已读的消息
|
const unreadMessages = this.messages.filter(msg => {
|
return msg.sender_id === this.currentUser.user_id && !msg.is_read;
|
});
|
if (unreadMessages.length === 0) {
|
console.log('没有需要标记已读的消息');
|
return;
|
}
|
console.log(`找到 ${unreadMessages.length} 条未读消息,开始批量标记`);
|
// 对每条未读消息调用markMessageAsRead方法
|
unreadMessages.forEach(msg => {
|
this.markMessageAsRead(msg.chat_id);
|
// 同时更新本地消息状态
|
msg.is_read = true;
|
});
|
console.log('所有未读消息已标记为已读');
|
},
|
|
// 发送WebSocket消息
|
sendWebSocketMessage(message) {
|
let self = this;
|
if (!this.socketTask) {
|
self.initWebSocket();
|
// 等待连接建立后再发送消息
|
setTimeout(() => {
|
self.sendWebSocketMessage(message);
|
}, 1000);
|
return;
|
}
|
|
const wsMessage = {
|
type: 'message',
|
user_id: this.currentUser.user_id,
|
to_user_id: this.targetUserId,
|
content: message,
|
conversation_id: this.conversationId,
|
business_card_id: this.businessCardId,
|
app_id: this.currentUser.grade.app_id
|
};
|
|
// 检查连接状态
|
if (!this.isOnline) {
|
self.initWebSocket();
|
// 等待连接建立后再发送消息
|
setTimeout(() => {
|
self.sendWebSocketMessage(message);
|
}, 1000);
|
return;
|
}
|
|
try {
|
self.socketTask.send({
|
data: JSON.stringify(wsMessage)
|
});
|
// 添加消息到本地列表
|
const localMessage = {
|
chat_id: Date.now(),
|
conversation_id: this.conversationId,
|
sender_id: this.currentUser.user_id,
|
content: message,
|
create_time: Math.floor(Date.now() / 1000),
|
send_time: (Date.now() / 1000),
|
is_read: false
|
};
|
self.messages.push(localMessage);
|
self.inputMessage = '';
|
|
// 滚动到底部 - 使用更强的保证方式
|
this.$nextTick(() => {
|
// 延迟一小段时间确保DOM更新完成
|
setTimeout(() => {
|
this.scrollToBottom();
|
}, 50);
|
});
|
} catch (error) {
|
console.log('发送WebSocket消息失败:', error);
|
// 尝试重连
|
this.reconnectWebSocket();
|
}
|
},
|
|
// 开始心跳检测
|
startHeartbeat() {
|
// 清除已存在的心跳定时器
|
if (this.heartbeatTimer) {
|
clearInterval(this.heartbeatTimer);
|
}
|
|
// 每30秒发送一次心跳
|
this.heartbeatTimer = setInterval(() => {
|
if (this.socketTask && this.isOnline) {
|
const heartbeatMsg = {
|
type: 'ping',
|
user_id: this.currentUser.user_id,
|
to_user_id: this.targetUserId
|
};
|
try {
|
this.socketTask.send({
|
data: JSON.stringify(heartbeatMsg)
|
});
|
} catch (error) {
|
console.log('发送心跳失败:', error);
|
}
|
}
|
}, 30000); // 30秒
|
},
|
|
// 标记消息为已读
|
markMessageAsRead(chatId) {
|
if (!this.socketTask || !this.isOnline) {
|
return;
|
}
|
|
const readMsg = {
|
type: 'read',
|
user_id: this.currentUser.user_id,
|
to_user_id: this.targetUserId,
|
chat_id: chatId,
|
conversation_id: this.conversationId
|
};
|
console.log(readMsg);
|
try {
|
this.socketTask.send({
|
data: JSON.stringify(readMsg)
|
});
|
} catch (error) {
|
console.log('发送已读标记失败:', error);
|
}
|
},
|
|
closeWebSocket() {
|
if (this.socketTask) {
|
// 发送关闭消息
|
const closeMsg = {
|
type: 'close',
|
user_id: this.currentUser.user_id,
|
to_user_id: this.targetUserId
|
};
|
|
try {
|
this.socketTask.send({
|
data: JSON.stringify(closeMsg)
|
});
|
} catch (error) {
|
console.log('发送关闭消息失败:', error);
|
}
|
|
// 关闭WebSocket连接
|
this.socketTask.close();
|
this.socketTask = null;
|
}
|
|
// 停止心跳
|
if (this.heartbeatTimer) {
|
clearInterval(this.heartbeatTimer);
|
this.heartbeatTimer = null;
|
}
|
|
this.isOnline = false;
|
},
|
|
// 滚动相关方法
|
scrollToBottom() {
|
console.log(this.scrollTop);
|
// 增加滚动距离确保到达底部
|
this.scrollTop = this.oldScrollTop + 10000;
|
},
|
|
onScroll(e) {
|
let oldScrollTop = this.oldScrollTop;
|
this.oldScrollTop = e.detail.scrollHeight;
|
if (oldScrollTop == 0) {
|
this.scrollToBottom()
|
}
|
},
|
|
loadMoreMessages() {
|
if (this.hasMore && !this.loadMore) {
|
this.loadMessages();
|
}
|
},
|
|
// 输入相关方法
|
onInputFocus() {
|
this.showEmojiPanel = false;
|
this.showMorePanel = false;
|
},
|
|
onInputBlur() {
|
// 延迟隐藏输入面板,避免闪烁
|
setTimeout(() => {
|
this.showEmojiPanel = false;
|
this.showMorePanel = false;
|
}, 100);
|
},
|
|
onInputChange() {
|
// 输入状态处理
|
this.handleTyping();
|
},
|
|
handleTyping() {
|
// 发送输入状态
|
if (this.typingTimer) {
|
clearTimeout(this.typingTimer);
|
}
|
|
this.typingTimer = setTimeout(() => {
|
this.isTyping = false;
|
}, 3000);
|
},
|
|
toggleEmoji() {
|
this.showEmojiPanel = !this.showEmojiPanel;
|
this.showMorePanel = false;
|
|
if (this.showEmojiPanel) {
|
this.$nextTick(() => {
|
this.scrollToBottom();
|
});
|
}
|
},
|
|
toggleMore() {
|
this.showMorePanel = !this.showMorePanel;
|
this.showEmojiPanel = false;
|
|
if (this.showMorePanel) {
|
this.$nextTick(() => {
|
this.scrollToBottom();
|
});
|
}
|
},
|
|
insertEmoji(emoji) {
|
this.inputMessage += emoji;
|
},
|
|
// 更多功能
|
sendImage() {
|
this.showMorePanel = false;
|
// 图片选择逻辑
|
uni.chooseImage({
|
count: 1,
|
sizeType: ['compressed'],
|
sourceType: ['album', 'camera'],
|
success: (res) => {
|
console.log('选择图片:', res);
|
// 图片上传和发送逻辑
|
}
|
});
|
},
|
|
sendFile() {
|
this.showMorePanel = false;
|
// 文件选择逻辑
|
this.showError('文件功能暂未开放');
|
},
|
|
sendContact() {
|
this.showMorePanel = false;
|
// 发送名片逻辑
|
this.showError('名片功能暂未开放');
|
},
|
|
// 操作菜单
|
showMoreActions() {
|
this.actionPopup = true;
|
},
|
|
closeActionMenu() {
|
this.actionPopup = false;
|
},
|
|
viewUserProfile() {
|
this.closeActionMenu();
|
// 查看用户资料
|
uni.navigateTo({
|
url: `/pages/plus/business/detail?business_card_id=${this.businessCardId}`
|
});
|
},
|
|
clearChatHistory() {
|
this.closeActionMenu();
|
uni.showModal({
|
title: '提示',
|
content: '确定要清空聊天记录吗?此操作不可恢复',
|
success: (res) => {
|
if (res.confirm) {
|
// 清空聊天记录逻辑
|
this.messages = [];
|
this.showSuccess('聊天记录已清空');
|
}
|
}
|
});
|
},
|
|
// 工具方法
|
goBack() {
|
uni.navigateBack();
|
},
|
|
getMessageStatusIcon(message) {
|
if (message.is_read) {
|
return 'icon-eye';
|
} else {
|
return 'icon-paperplane';
|
}
|
},
|
|
formatTime(timeStr) {
|
if (!timeStr) return '';
|
|
// 处理日期字符串,将yyyy-MM-dd转换为yyyy/MM/dd以兼容iOS
|
const formattedTimeStr = typeof timeStr === 'string' ? timeStr.replace(/\-/g, '/') : timeStr * 1000;
|
const date = new Date(formattedTimeStr);
|
const now = new Date();
|
|
// 今天的消息显示时间
|
if (date.toDateString() === now.toDateString()) {
|
return this.formatTimeOnly(date);
|
}
|
|
// 昨天的消息显示"昨天"
|
const yesterday = new Date(now);
|
yesterday.setDate(now.getDate() - 1);
|
if (date.toDateString() === yesterday.toDateString()) {
|
return `昨天 ${this.formatTimeOnly(date)}`;
|
}
|
|
// 其他时间显示完整日期
|
return `${date.getMonth() + 1}/${date.getDate()} ${this.formatTimeOnly(date)}`;
|
},
|
|
formatTimeOnly(date) {
|
const hours = date.getHours().toString().padStart(2, '0');
|
const minutes = date.getMinutes().toString().padStart(2, '0');
|
return `${hours}:${minutes}`;
|
}
|
},
|
|
computed: {
|
placeholderText() {
|
return `和${this.card?this.card.name:this.targetUser.nickName}聊天`;
|
}
|
}
|
};
|
</script>
|
|
<style lang="scss" scoped>
|
@import '../../../../common/iconfont.css';
|
|
// 原有样式保持不变
|
.chat-container {
|
display: flex;
|
flex-direction: column;
|
height: 100vh;
|
background-color: #f5f5f5;
|
}
|
|
// 头部样式
|
.chat-header {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
height: 88rpx;
|
background-color: #fff;
|
border-bottom: 1rpx solid #e5e5e5;
|
padding: 0 30rpx;
|
|
.header-left,
|
.header-right {
|
width: 80rpx;
|
|
.back-btn,
|
.more-btn {
|
width: 60rpx;
|
height: 60rpx;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
|
.iconfont {
|
font-size: 36rpx;
|
color: #333;
|
}
|
}
|
}
|
|
.header-center {
|
flex: 1;
|
display: flex;
|
justify-content: center;
|
|
.user-info {
|
display: flex;
|
align-items: center;
|
|
.user-avatar {
|
width: 60rpx;
|
height: 60rpx;
|
border-radius: 50%;
|
margin-right: 20rpx;
|
}
|
|
.user-details {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
|
.user-name {
|
font-size: 32rpx;
|
font-weight: bold;
|
color: #333;
|
margin-bottom: 4rpx;
|
}
|
|
.user-status {
|
font-size: 24rpx;
|
color: #999;
|
|
&.online {
|
color: #07c160;
|
}
|
}
|
}
|
}
|
}
|
}
|
|
// 消息区域样式
|
.chat-messages {
|
height: calc(100vh - 200rpx);
|
flex: 1;
|
background-color: #f5f5f5;
|
|
.messages-container {
|
padding: 30rpx;
|
|
.load-more {
|
text-align: center;
|
padding: 20rpx 0;
|
|
.load-text {
|
font-size: 28rpx;
|
color: #999;
|
}
|
}
|
|
.message-item {
|
margin-bottom: 30rpx;
|
|
.message-other {
|
display: flex;
|
align-items: flex-start;
|
|
.avatar {
|
width: 80rpx;
|
height: 80rpx;
|
border-radius: 50%;
|
margin-right: 20rpx;
|
}
|
|
.message-content {
|
flex: 1;
|
|
.message-status {
|
margin-left: 10rpx;
|
width: 22rpx;
|
height: 22rpx;
|
border-radius: 22rpx;
|
border: 1rpx solid #ff1d1d;
|
margin-bottom: 10rpx;
|
}
|
|
.read-read {
|
border-color: #07c16000;
|
}
|
|
.message-bubble {
|
background: #fff;
|
border-radius: 10rpx;
|
padding: 20rpx;
|
max-width: 500rpx;
|
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
|
|
.message-text {
|
font-size: 30rpx;
|
line-height: 1.5;
|
color: #333;
|
word-wrap: break-word;
|
word-break: break-all;
|
}
|
}
|
|
.message-info {
|
display: flex;
|
align-items: center;
|
margin-top: 10rpx;
|
margin-left: 100rpx;
|
|
.message-time {
|
font-size: 24rpx;
|
color: #999;
|
}
|
}
|
}
|
}
|
|
.message-mine {
|
display: flex;
|
align-items: flex-start;
|
justify-content: flex-end;
|
|
.avatar {
|
width: 80rpx;
|
height: 80rpx;
|
border-radius: 50%;
|
margin-left: 20rpx;
|
}
|
|
.message-content {
|
display: flex;
|
flex-direction: column;
|
align-items: flex-end;
|
|
.message-status {
|
margin-right: 10rpx;
|
width: 22rpx;
|
height: 22rpx;
|
border-radius: 22rpx;
|
border: 1rpx solid #ff1d1d;
|
margin-bottom: 10rpx;
|
}
|
|
.read-read {
|
border-color: #07c16000;
|
}
|
|
.message-info {
|
display: flex;
|
align-items: center;
|
margin-bottom: 10rpx;
|
|
.message-time {
|
font-size: 24rpx;
|
margin-right: 120rpx;
|
color: #999;
|
}
|
}
|
|
.message-bubble {
|
background: #95ec69;
|
border-radius: 10rpx;
|
padding: 20rpx;
|
max-width: 500rpx;
|
|
.message-text {
|
font-size: 30rpx;
|
line-height: 1.5;
|
color: #333;
|
word-wrap: break-word;
|
word-break: break-all;
|
}
|
}
|
}
|
}
|
}
|
|
.typing-indicator {
|
display: flex;
|
align-items: flex-start;
|
margin-bottom: 30rpx;
|
|
.avatar {
|
width: 80rpx;
|
height: 80rpx;
|
border-radius: 50%;
|
margin-right: 20rpx;
|
}
|
|
.typing-bubble {
|
background: #fff;
|
border-radius: 10rpx;
|
padding: 20rpx;
|
max-width: 200rpx;
|
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
|
|
.typing-dots {
|
display: flex;
|
align-items: center;
|
margin-bottom: 10rpx;
|
|
.dot {
|
width: 12rpx;
|
height: 12rpx;
|
border-radius: 50%;
|
background-color: #999;
|
margin-right: 8rpx;
|
animation: typing 1.4s infinite ease-in-out;
|
|
&:nth-child(1) {
|
animation-delay: -0.32s;
|
}
|
|
&:nth-child(2) {
|
animation-delay: -0.16s;
|
}
|
}
|
}
|
|
.typing-text {
|
font-size: 26rpx;
|
color: #999;
|
}
|
}
|
}
|
}
|
}
|
|
// 输入区域样式
|
.chat-input-area {
|
background-color: #fff;
|
border-top: 1rpx solid #e5e5e5;
|
|
.input-container {
|
display: flex;
|
align-items: center;
|
padding: 20rpx 30rpx;
|
|
.input-left {
|
display: flex;
|
align-items: center;
|
width: 160rpx;
|
|
.emoji-btn,
|
.more-btn {
|
width: 60rpx;
|
height: 60rpx;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
margin-right: 20rpx;
|
|
.iconfont {
|
font-size: 40rpx;
|
color: #666;
|
}
|
}
|
}
|
|
.input-main {
|
flex: 1;
|
margin: 0 20rpx;
|
|
.message-input {
|
background: #f5f5f5;
|
border-radius: 10rpx;
|
padding: 20rpx;
|
width: 500rpx;
|
max-height: 200rpx;
|
font-size: 30rpx;
|
line-height: 1.5;
|
}
|
}
|
|
.input-right {
|
width: 120rpx;
|
|
.send-btn {
|
width: 120rpx;
|
height: 60rpx;
|
background: #e5e5e5;
|
border: none;
|
border-radius: 10rpx;
|
|
&.active {
|
background: #07c160;
|
|
.send-text {
|
color: #fff;
|
}
|
}
|
|
.send-text {
|
font-size: 28rpx;
|
color: #999;
|
}
|
}
|
}
|
}
|
|
// 更多功能面板
|
.more-panel {
|
background: #fff;
|
border-top: 1rpx solid #e5e5e5;
|
padding: 30rpx;
|
|
.panel-grid {
|
display: flex;
|
justify-content: space-around;
|
|
.panel-item {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
|
.item-icon {
|
width: 100rpx;
|
height: 100rpx;
|
background: #f5f5f5;
|
border-radius: 20rpx;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
margin-bottom: 10rpx;
|
|
/* .iconfont {
|
font-size: 50rpx;
|
color: #666;
|
} */
|
}
|
|
.item-text {
|
font-size: 24rpx;
|
color: #666;
|
}
|
}
|
}
|
}
|
|
// 表情面板
|
.emoji-panel {
|
background: #fff;
|
border-top: 1rpx solid #e5e5e5;
|
height: 400rpx;
|
|
.emoji-tabs {
|
display: flex;
|
border-bottom: 1rpx solid #e5e5e5;
|
|
.tab-item {
|
flex: 1;
|
text-align: center;
|
padding: 20rpx 0;
|
font-size: 28rpx;
|
color: #666;
|
|
&.active {
|
color: #07c160;
|
border-bottom: 4rpx solid #07c160;
|
}
|
}
|
}
|
|
.emoji-list {
|
height: 340rpx;
|
|
.emoji-grid {
|
display: flex;
|
flex-wrap: wrap;
|
padding: 20rpx;
|
|
.emoji-item {
|
width: 60rpx;
|
height: 60rpx;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
margin: 10rpx;
|
font-size: 36rpx;
|
}
|
}
|
}
|
}
|
}
|
|
// 操作菜单
|
.action-menu {
|
background: #fff;
|
border-radius: 20rpx 20rpx 0 0;
|
padding: 0 30rpx;
|
|
.menu-item {
|
text-align: center;
|
padding: 30rpx 0;
|
font-size: 32rpx;
|
color: #333;
|
border-bottom: 1rpx solid #e5e5e5;
|
|
&:last-child {
|
border-bottom: none;
|
}
|
|
&.cancel {
|
color: #999;
|
margin-top: 10rpx;
|
border-top: 10rpx solid #f5f5f5;
|
}
|
}
|
}
|
|
// 动画
|
@keyframes typing {
|
|
0%,
|
60%,
|
100% {
|
transform: translateY(0);
|
}
|
|
30% {
|
transform: translateY(-10rpx);
|
}
|
}
|
</style>
|