|
|
|
|
@ -1,13 +1,32 @@
|
|
|
|
|
<template>
|
|
|
|
|
<view class="chat-window">
|
|
|
|
|
<scroll-view :scroll-y="true" class="message-list" :scroll-into-view="lastMessageId">
|
|
|
|
|
<view v-for="message in messages" :key="message.id" :id="'msg-' + message.id"
|
|
|
|
|
:class="['message-item', message.from === 'me' ? 'my-message' : 'their-message']">
|
|
|
|
|
<u-avatar v-if="message.from !== 'me'" :src="theirAvatar" size="80"></u-avatar>
|
|
|
|
|
<view class="message-content">
|
|
|
|
|
<text>{{ message.content }}</text>
|
|
|
|
|
<scroll-view
|
|
|
|
|
:scroll-y="true"
|
|
|
|
|
class="message-list"
|
|
|
|
|
:scroll-into-view="lastMessageId"
|
|
|
|
|
@scrolltoupper="onScrollToUpper"
|
|
|
|
|
:scroll-with-animation="false"
|
|
|
|
|
:upper-threshold="50">
|
|
|
|
|
<!-- 加载更多提示 -->
|
|
|
|
|
<view v-if="loading" class="loading-more">
|
|
|
|
|
<u-loading mode="circle" size="20"></u-loading>
|
|
|
|
|
<text class="loading-text">加载中...</text>
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
<view v-for="(message, index) in messages" :key="message.id">
|
|
|
|
|
<!-- 时间分割线 -->
|
|
|
|
|
<view v-if="shouldShowTimeDivider(message, index)" class="time-divider">
|
|
|
|
|
<text class="time-text">{{ formatTime(message.timestamp) }}</text>
|
|
|
|
|
</view>
|
|
|
|
|
<!-- 消息内容 -->
|
|
|
|
|
<view :id="'msg-' + message.id"
|
|
|
|
|
:class="['message-item', message.from === 'me' ? 'my-message' : 'their-message']">
|
|
|
|
|
<u-avatar v-if="message.from !== 'me'" :src="theirAvatar" size="80"></u-avatar>
|
|
|
|
|
<view class="message-content">
|
|
|
|
|
<text class="message-text">{{ message.content }}</text>
|
|
|
|
|
</view>
|
|
|
|
|
<u-avatar v-if="message.from === 'me'" :src="myAvatar" size="80"></u-avatar>
|
|
|
|
|
</view>
|
|
|
|
|
<u-avatar v-if="message.from === 'me'" :src="myAvatar" size="80"></u-avatar>
|
|
|
|
|
</view>
|
|
|
|
|
</scroll-view>
|
|
|
|
|
|
|
|
|
|
@ -22,64 +41,208 @@
|
|
|
|
|
export default {
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
myAvatar: 'https://via.placeholder.com/80',
|
|
|
|
|
theirAvatar: 'https://via.placeholder.com/80',
|
|
|
|
|
myAvatar: '',
|
|
|
|
|
theirAvatar: '',
|
|
|
|
|
newMessage: '',
|
|
|
|
|
messages: [],
|
|
|
|
|
lastMessageId: '',
|
|
|
|
|
supplyDemandId: '',
|
|
|
|
|
theirUserId: '',
|
|
|
|
|
loading: false,
|
|
|
|
|
currentPage: 1,
|
|
|
|
|
hasMore: true,
|
|
|
|
|
pageSize: 20
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onLoad(options) {
|
|
|
|
|
const userId = options.userId;
|
|
|
|
|
const userName = options.userName;
|
|
|
|
|
const supplyDemandId = options.supplyDemandId;
|
|
|
|
|
const supplyDemandTitle = options.supplyDemandTitle ? decodeURIComponent(options.supplyDemandTitle) : '';
|
|
|
|
|
|
|
|
|
|
// 保存供应需求ID
|
|
|
|
|
this.supplyDemandId = supplyDemandId;
|
|
|
|
|
this.theirUserId = userId;
|
|
|
|
|
|
|
|
|
|
// 调试:输出当前用户信息
|
|
|
|
|
console.log('当前用户信息:', this.$store.state.vuex_user);
|
|
|
|
|
|
|
|
|
|
// 动态设置导航栏标题
|
|
|
|
|
const title = supplyDemandTitle ? `${supplyDemandTitle} - ${userName || userId}` : `与 ${userName || userId} 的对话`;
|
|
|
|
|
uni.setNavigationBarTitle({
|
|
|
|
|
title: `与 ${userId} 的对话`
|
|
|
|
|
title: title
|
|
|
|
|
});
|
|
|
|
|
this.fetchMessages(userId);
|
|
|
|
|
this.fetchMessages(supplyDemandId);
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
|
fetchMessages(userId) {
|
|
|
|
|
// 模拟获取历史消息
|
|
|
|
|
this.messages = [{
|
|
|
|
|
id: 1,
|
|
|
|
|
from: 'them',
|
|
|
|
|
content: '你好!很高兴认识你。'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 2,
|
|
|
|
|
from: 'me',
|
|
|
|
|
content: '你好!我也是。'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 3,
|
|
|
|
|
from: 'them',
|
|
|
|
|
content: '我们下周一开会讨论一下那个项目吧,你觉得怎么样?'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 4,
|
|
|
|
|
from: 'me',
|
|
|
|
|
content: '好啊,没问题。'
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
this.scrollToBottom();
|
|
|
|
|
},
|
|
|
|
|
fetchMessages(supplyDemandId, page = 1, append = false) {
|
|
|
|
|
if (this.loading || (!append && !this.hasMore)) return;
|
|
|
|
|
|
|
|
|
|
this.loading = true;
|
|
|
|
|
|
|
|
|
|
// 使用正确的API调用方式
|
|
|
|
|
this.$u.api.supplyDemandMessageList({
|
|
|
|
|
supply_demand_id: supplyDemandId,
|
|
|
|
|
page: page,
|
|
|
|
|
per_page: this.pageSize
|
|
|
|
|
}).then(res => {
|
|
|
|
|
console.log('获取消息列表成功:', res);
|
|
|
|
|
// 处理返回的消息数据,数据在 res.message.data 中
|
|
|
|
|
if (res.message && res.message.data) {
|
|
|
|
|
const newMessages = res.message.data.map((item, index) => ({
|
|
|
|
|
id: item.id,
|
|
|
|
|
from: item.user_id === this.$store.state.vuex_user.id ? 'me' : 'them',
|
|
|
|
|
content: item.content,
|
|
|
|
|
timestamp: item.created_at,
|
|
|
|
|
user: item.user,
|
|
|
|
|
to_user: item.to_user
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
if (append) {
|
|
|
|
|
// 上拉加载更多,老消息插入到前面
|
|
|
|
|
this.messages.unshift(...newMessages);
|
|
|
|
|
} else {
|
|
|
|
|
// 首次加载或刷新,按时间正序排列(老消息在前,新消息在后)
|
|
|
|
|
this.messages = newMessages.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查是否还有更多数据
|
|
|
|
|
this.hasMore = res.message.current_page < res.message.last_page;
|
|
|
|
|
this.currentPage = res.message.current_page;
|
|
|
|
|
|
|
|
|
|
// 设置头像
|
|
|
|
|
if (this.messages.length > 0) {
|
|
|
|
|
const firstMessage = this.messages[0];
|
|
|
|
|
if (firstMessage.from === 'me') {
|
|
|
|
|
this.myAvatar = firstMessage.user?.headimgurl || '';
|
|
|
|
|
this.theirAvatar = firstMessage.to_user?.headimgurl || '';
|
|
|
|
|
} else {
|
|
|
|
|
this.myAvatar = firstMessage.to_user?.headimgurl || '';
|
|
|
|
|
this.theirAvatar = firstMessage.user?.headimgurl || '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新导航栏标题,使用实际的用户名
|
|
|
|
|
const theirUser = firstMessage.from === 'me' ? firstMessage.to_user : firstMessage.user;
|
|
|
|
|
if (theirUser) {
|
|
|
|
|
const displayName = theirUser.nickname || theirUser.name || `用户${theirUser.id}`;
|
|
|
|
|
uni.setNavigationBarTitle({
|
|
|
|
|
title: `与 ${displayName} 的对话`
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!append) {
|
|
|
|
|
this.scrollToBottom();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}).catch(err => {
|
|
|
|
|
console.error('获取消息列表失败:', err);
|
|
|
|
|
}).finally(() => {
|
|
|
|
|
this.loading = false;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
sendMessage() {
|
|
|
|
|
if (this.newMessage.trim() === '') return;
|
|
|
|
|
|
|
|
|
|
const messageContent = this.newMessage.trim();
|
|
|
|
|
this.newMessage = '';
|
|
|
|
|
|
|
|
|
|
// 生成临时ID(使用时间戳确保唯一性)
|
|
|
|
|
const tempId = Date.now();
|
|
|
|
|
|
|
|
|
|
// 先添加到本地消息列表
|
|
|
|
|
const newMsg = {
|
|
|
|
|
id: this.messages.length + 1,
|
|
|
|
|
id: tempId,
|
|
|
|
|
from: 'me',
|
|
|
|
|
content: this.newMessage.trim()
|
|
|
|
|
content: messageContent,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
user: this.$store.state.vuex_user,
|
|
|
|
|
to_user: null
|
|
|
|
|
};
|
|
|
|
|
// 新消息添加到列表末尾(最新消息)
|
|
|
|
|
this.messages.push(newMsg);
|
|
|
|
|
this.newMessage = '';
|
|
|
|
|
this.scrollToBottom();
|
|
|
|
|
|
|
|
|
|
// 调用发送消息API
|
|
|
|
|
this.$u.api.supplyDemandSendMessage({
|
|
|
|
|
supply_demand_id: this.supplyDemandId,
|
|
|
|
|
content: messageContent,
|
|
|
|
|
to_user_id: this.theirUserId
|
|
|
|
|
}).then(res => {
|
|
|
|
|
console.log('发送消息成功:', res);
|
|
|
|
|
// 发送成功后,更新临时消息的ID为服务器返回的真实ID
|
|
|
|
|
if (res && res.id) {
|
|
|
|
|
const lastMessage = this.messages[this.messages.length - 1];
|
|
|
|
|
if (lastMessage.id === tempId) {
|
|
|
|
|
lastMessage.id = res.id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}).catch(err => {
|
|
|
|
|
console.error('发送消息失败:', err);
|
|
|
|
|
// 发送失败时,可以从消息列表中移除临时消息
|
|
|
|
|
this.messages = this.messages.filter(msg => msg.id !== tempId);
|
|
|
|
|
// 可选:提示用户发送失败
|
|
|
|
|
uni.showToast({
|
|
|
|
|
title: '发送失败,请重试',
|
|
|
|
|
icon: 'none'
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
scrollToBottom() {
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
if (this.messages.length > 0) {
|
|
|
|
|
// 滚动到最新消息(列表末尾)
|
|
|
|
|
this.lastMessageId = `msg-${this.messages[this.messages.length - 1].id}`;
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
formatTime(timestamp) {
|
|
|
|
|
if (!timestamp) return '';
|
|
|
|
|
|
|
|
|
|
const date = new Date(timestamp);
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const diff = now - date;
|
|
|
|
|
|
|
|
|
|
// 今天的消息只显示时间
|
|
|
|
|
if (diff < 24 * 60 * 60 * 1000 && date.getDate() === now.getDate()) {
|
|
|
|
|
return date.toLocaleTimeString('zh-CN', {
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
minute: '2-digit'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 昨天的消息显示"昨天 + 时间"
|
|
|
|
|
if (diff < 48 * 60 * 60 * 1000 && date.getDate() === now.getDate() - 1) {
|
|
|
|
|
return `昨天 ${date.toLocaleTimeString('zh-CN', {
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
minute: '2-digit'
|
|
|
|
|
})}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 其他显示完整日期和时间
|
|
|
|
|
return date.toLocaleString('zh-CN', {
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
day: '2-digit',
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
minute: '2-digit'
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
shouldShowTimeDivider(message, index) {
|
|
|
|
|
// 第一条消息总是显示时间
|
|
|
|
|
if (index === 0) return true;
|
|
|
|
|
|
|
|
|
|
const currentTime = new Date(message.timestamp);
|
|
|
|
|
const prevTime = new Date(this.messages[index - 1].timestamp);
|
|
|
|
|
|
|
|
|
|
// 如果时间间隔超过5分钟,显示时间分割线
|
|
|
|
|
const timeDiff = Math.abs(currentTime - prevTime);
|
|
|
|
|
return timeDiff > 5 * 60 * 1000; // 5分钟
|
|
|
|
|
},
|
|
|
|
|
onScrollToUpper() {
|
|
|
|
|
// 上拉加载更多历史消息
|
|
|
|
|
if (this.hasMore && !this.loading) {
|
|
|
|
|
this.fetchMessages(this.supplyDemandId, this.currentPage + 1, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -133,6 +296,47 @@
|
|
|
|
|
max-width: 70%;
|
|
|
|
|
font-size: 30rpx;
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-text {
|
|
|
|
|
margin-bottom: 8rpx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-time {
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
color: #999;
|
|
|
|
|
opacity: 0.8;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.time-divider {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin: 30rpx 0 20rpx 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.time-text {
|
|
|
|
|
background-color: rgba(0, 0, 0, 0.1);
|
|
|
|
|
color: #999;
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
padding: 8rpx 16rpx;
|
|
|
|
|
border-radius: 20rpx;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.loading-more {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 20rpx 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.loading-text {
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
color: #999;
|
|
|
|
|
margin-left: 10rpx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-area {
|
|
|
|
|
|