55 changed files with 10371 additions and 607 deletions
|
Before Width: | Height: | Size: 1.2 MiB |
File diff suppressed because it is too large
@ -0,0 +1,8 @@ |
|||
{ |
|||
"navigationBarTitleText": "聊天详情", |
|||
"navigationBarBackgroundColor": "#ffffff", |
|||
"navigationBarTextStyle": "black", |
|||
"usingComponents": {}, |
|||
"enablePullDownRefresh": false, |
|||
"navigationBarBackButtonText": "返回" |
|||
} |
|||
@ -0,0 +1,102 @@ |
|||
<view class="chat-detail-container"> |
|||
<!-- 连接状态显示 --> |
|||
<view wx:if="{{!isMockMode}}" class="connection-status {{connectionStatus}}"> |
|||
<text class="status-text">{{connectionMessage}}</text> |
|||
<button wx:if="{{connectionStatus === 'error' || connectionStatus === 'disconnected'}}" |
|||
class="reconnect-btn" |
|||
size="mini" |
|||
bindtap="reconnect"> |
|||
重新连接 |
|||
</button> |
|||
</view> |
|||
|
|||
<!-- 测试模式切换 --> |
|||
<view class="test-mode-switch"> |
|||
<switch wx:if="{{!isMockMode}}" checked="{{!isMockMode}}" bindchange="toggleMockMode" /> |
|||
<text wx:if="{{!isMockMode}}" class="mode-label">模拟模式切换</text> |
|||
</view> |
|||
|
|||
<!-- 消息列表区域 - 微信风格 --> |
|||
<scroll-view |
|||
id="message-list" |
|||
class="wechat-message-list" |
|||
scroll-y |
|||
bindscrolltolower="loadMoreMessages" |
|||
scroll-into-view="{{scrollToMessage}}" |
|||
scroll-with-animation |
|||
> |
|||
<!-- 消息容器 --> |
|||
<view class="wechat-message-container"> |
|||
<!-- 遍历消息列表 --> |
|||
<block wx:for="{{messages}}" wx:key="index"> |
|||
<!-- 时间显示 - 条件显示 --> |
|||
<view wx:if="{{item.showTime}}" class="wechat-time-display"> |
|||
<text class="wechat-time-text">{{item.time}}</text> |
|||
</view> |
|||
|
|||
<!-- 系统消息(如诈骗提示)--> |
|||
<view wx:if="{{item.type === 'system'}}" class="wechat-system-message"> |
|||
<view class="wechat-system-content"> |
|||
<view wx:if="{{item.isWarning}}" class="wechat-warning-title">谨防诈骗</view> |
|||
<text class="wechat-system-text">{{item.content}}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 对方消息 --> |
|||
<view wx:elif="{{item.type === 'message' && item.sender === 'other'}}" class="wechat-message-item wechat-other-message"> |
|||
<view class="wechat-avatar"> |
|||
<text class="wechat-avatar-text">{{userName ? userName.charAt(0) : (avatar || '用')}}</text> |
|||
</view> |
|||
<!-- 图片消息直接显示 --> |
|||
<image wx:if="{{item.isImage}}" src="{{item.content}}" class="wechat-message-image wechat-other-image" mode="aspectFit" bindtap="previewImage" data-src="{{item.content}}" /> |
|||
<!-- 文本消息仍然显示在气泡中 --> |
|||
<view wx:elif="{{!item.isImage}}" class="wechat-message-wrapper"> |
|||
<view class="wechat-message-name">{{userName}}</view> |
|||
<view class="wechat-message-bubble wechat-other-bubble"> |
|||
<text class="wechat-message-text">{{item.content}}</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<!-- 我的消息 --> |
|||
<view wx:elif="{{item.type === 'message' && item.sender === 'me'}}" class="wechat-message-item wechat-my-message"> |
|||
<!-- 图片消息直接显示 --> |
|||
<image wx:if="{{item.isImage}}" src="{{item.content}}" class="wechat-message-image wechat-my-image" mode="aspectFit" bindtap="previewImage" data-src="{{item.content}}" /> |
|||
<!-- 文本消息仍然显示在气泡中 --> |
|||
<view wx:elif="{{!item.isImage}}" class="wechat-message-wrapper wechat-my-wrapper"> |
|||
<view class="wechat-message-bubble wechat-my-bubble"> |
|||
<text class="wechat-message-text wechat-my-text">{{item.content}}</text> |
|||
</view> |
|||
</view> |
|||
<view class="wechat-avatar wechat-my-avatar"> |
|||
<text class="wechat-avatar-text">我</text> |
|||
</view> |
|||
</view> |
|||
</block> |
|||
</view> |
|||
</scroll-view> |
|||
|
|||
<!-- 输入区域 - 微信风格 --> |
|||
<view class="wechat-input-area"> |
|||
<view class="wechat-input-toolbar"> |
|||
<view class="wechat-input-left"> |
|||
<view class="wechat-emoji-btn" bindtap="showEmoji">😊</view> |
|||
<view class="wechat-voice-btn" bindtap="toggleVoiceMode">🔊</view> |
|||
</view> |
|||
<view class="wechat-input-wrapper"> |
|||
<input |
|||
class="wechat-message-input" |
|||
value="{{inputValue}}" |
|||
bindinput="onInput" |
|||
placeholder="消息" |
|||
placeholder-style="color: #999999;" |
|||
adjust-position |
|||
maxlength="500" |
|||
/> |
|||
</view> |
|||
<view class="wechat-input-right"> |
|||
<view class="wechat-more-btn" bindtap="showMoreOptions">+</view> |
|||
<view class="wechat-send-btn" bindtap="sendMessage" disabled="{{!inputValue.trim()}}">发送</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
@ -0,0 +1,468 @@ |
|||
/* 微信聊天界面样式 */ |
|||
.chat-detail-container { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100vh; |
|||
background-color: #e5e5e5; |
|||
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; |
|||
} |
|||
|
|||
/* 连接状态显示样式 */ |
|||
.connection-status { |
|||
padding: 8rpx 0; |
|||
text-align: center; |
|||
font-size: 24rpx; |
|||
color: #fff; |
|||
position: relative; |
|||
z-index: 10; |
|||
} |
|||
|
|||
.connection-status.connecting { |
|||
background-color: #FF9800; |
|||
} |
|||
|
|||
.connection-status.connected { |
|||
background-color: #4CAF50; |
|||
} |
|||
|
|||
.connection-status.error { |
|||
background-color: #F44336; |
|||
} |
|||
|
|||
.connection-status.disconnected { |
|||
background-color: #9E9E9E; |
|||
} |
|||
|
|||
.status-text { |
|||
display: inline-block; |
|||
padding: 0 20rpx; |
|||
border-radius: 10rpx; |
|||
} |
|||
|
|||
.reconnect-btn { |
|||
display: inline-block; |
|||
margin-left: 20rpx; |
|||
padding: 0 20rpx; |
|||
font-size: 24rpx; |
|||
line-height: 40rpx; |
|||
background-color: rgba(255, 255, 255, 0.3); |
|||
color: #fff; |
|||
border: 1px solid rgba(255, 255, 255, 0.5); |
|||
border-radius: 16rpx; |
|||
} |
|||
|
|||
.test-mode-switch { |
|||
padding: 20rpx; |
|||
background-color: #f8f8f8; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
border-bottom: 1rpx solid #e8e8e8; |
|||
} |
|||
|
|||
.mode-label { |
|||
margin-left: 20rpx; |
|||
font-size: 28rpx; |
|||
color: #666; |
|||
} |
|||
|
|||
/* 消息列表区域 - 微信风格 */ |
|||
.wechat-message-list { |
|||
flex: 1; |
|||
padding: 20rpx 20rpx 40rpx; |
|||
box-sizing: border-box; |
|||
overflow-y: auto; |
|||
background-color: #e5e5e5; |
|||
} |
|||
|
|||
/* 消息容器 */ |
|||
.wechat-message-container { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 20rpx; |
|||
} |
|||
|
|||
/* 时间显示 */ |
|||
.wechat-time-display { |
|||
display: flex; |
|||
justify-content: center; |
|||
margin: 20rpx 0; |
|||
} |
|||
|
|||
.wechat-time-text { |
|||
background-color: rgba(0, 0, 0, 0.1); |
|||
color: #fff; |
|||
font-size: 24rpx; |
|||
padding: 6rpx 20rpx; |
|||
border-radius: 12rpx; |
|||
} |
|||
|
|||
/* 系统消息 */ |
|||
.wechat-system-message { |
|||
display: flex; |
|||
justify-content: center; |
|||
margin: 10rpx 0; |
|||
} |
|||
|
|||
.wechat-system-content { |
|||
background-color: rgba(255, 255, 255, 0.8); |
|||
border-radius: 18rpx; |
|||
padding: 16rpx 24rpx; |
|||
max-width: 80%; |
|||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05); |
|||
} |
|||
|
|||
.wechat-warning-title { |
|||
color: #ff0000; |
|||
font-size: 28rpx; |
|||
font-weight: bold; |
|||
margin-bottom: 10rpx; |
|||
} |
|||
|
|||
.wechat-system-text { |
|||
color: #606266; |
|||
font-size: 26rpx; |
|||
line-height: 36rpx; |
|||
} |
|||
|
|||
/* 商品信息卡片 */ |
|||
.wechat-goods-card { |
|||
display: flex; |
|||
background-color: #ffffff; |
|||
border-radius: 18rpx; |
|||
padding: 20rpx; |
|||
margin: 10rpx 0; |
|||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05); |
|||
align-self: flex-start; |
|||
max-width: 80%; |
|||
} |
|||
|
|||
.wechat-goods-avatar { |
|||
width: 120rpx; |
|||
height: 120rpx; |
|||
background-color: #f0f0f0; |
|||
border-radius: 8rpx; |
|||
flex-shrink: 0; |
|||
margin-right: 20rpx; |
|||
} |
|||
|
|||
.wechat-goods-content { |
|||
flex: 1; |
|||
min-width: 0; |
|||
} |
|||
|
|||
.wechat-goods-title { |
|||
font-size: 28rpx; |
|||
color: #303133; |
|||
font-weight: 500; |
|||
display: -webkit-box; |
|||
-webkit-line-clamp: 2; |
|||
-webkit-box-orient: vertical; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
margin-bottom: 10rpx; |
|||
} |
|||
|
|||
.wechat-goods-desc { |
|||
font-size: 26rpx; |
|||
color: #606266; |
|||
display: block; |
|||
margin-bottom: 10rpx; |
|||
line-height: 36rpx; |
|||
} |
|||
|
|||
.wechat-goods-footer { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
|
|||
.wechat-goods-price { |
|||
font-size: 32rpx; |
|||
color: #e64340; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.wechat-goods-type { |
|||
font-size: 24rpx; |
|||
color: #909399; |
|||
background-color: #f0f9ff; |
|||
padding: 4rpx 16rpx; |
|||
border-radius: 12rpx; |
|||
} |
|||
|
|||
/* 消息项 */ |
|||
.wechat-message-item { |
|||
display: flex; |
|||
margin: 10rpx 0; |
|||
align-items: flex-end; |
|||
} |
|||
|
|||
/* 对方消息 */ |
|||
.wechat-other-message { |
|||
flex-direction: row; |
|||
} |
|||
|
|||
/* 我的消息 */ |
|||
.wechat-my-message { |
|||
flex-direction: row-reverse; |
|||
} |
|||
|
|||
/* 头像 - 圆形 */ |
|||
.wechat-avatar { |
|||
width: 76rpx; |
|||
height: 76rpx; |
|||
border-radius: 50%; |
|||
background-color: #9aa5b1; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
margin: 0 12rpx; |
|||
flex-shrink: 0; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.wechat-my-avatar { |
|||
background-color: #d8d8d8; |
|||
} |
|||
|
|||
.wechat-avatar-text { |
|||
color: #ffffff; |
|||
font-size: 32rpx; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
/* 消息包装器 */ |
|||
.wechat-message-wrapper { |
|||
max-width: 75%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: flex-start; |
|||
gap: 6rpx; |
|||
} |
|||
|
|||
/* 客服姓名显示 */ |
|||
.wechat-message-name { |
|||
font-size: 24rpx; |
|||
color: #909399; |
|||
margin-left: 4rpx; |
|||
margin-bottom: 2rpx; |
|||
} |
|||
|
|||
.wechat-my-wrapper { |
|||
align-items: flex-end; |
|||
} |
|||
|
|||
/* 消息气泡 - 添加三角形箭头 */ |
|||
.wechat-message-bubble { |
|||
position: relative; |
|||
padding: 14rpx 20rpx; |
|||
max-width: 100%; |
|||
word-wrap: break-word; |
|||
word-break: break-all; |
|||
} |
|||
|
|||
.wechat-other-bubble { |
|||
background-color: #ffffff; |
|||
border-radius: 18rpx; |
|||
} |
|||
|
|||
.wechat-my-bubble { |
|||
background-color: #07c160; |
|||
border-radius: 18rpx; |
|||
} |
|||
|
|||
/* 对方消息气泡三角形 */ |
|||
.wechat-other-bubble::before { |
|||
content: ''; |
|||
position: absolute; |
|||
left: -10rpx; |
|||
bottom: 18rpx; |
|||
width: 0; |
|||
height: 0; |
|||
border-style: solid; |
|||
border-width: 10rpx 10rpx 10rpx 0; |
|||
border-color: transparent #ffffff transparent transparent; |
|||
} |
|||
|
|||
/* 我的消息气泡三角形 */ |
|||
.wechat-my-bubble::before { |
|||
content: ''; |
|||
position: absolute; |
|||
right: -10rpx; |
|||
bottom: 18rpx; |
|||
width: 0; |
|||
height: 0; |
|||
border-style: solid; |
|||
border-width: 10rpx 0 10rpx 10rpx; |
|||
border-color: transparent transparent transparent #07c160; |
|||
} |
|||
|
|||
/* 消息文本 */ |
|||
.wechat-message-text { |
|||
font-size: 32rpx; |
|||
line-height: 44rpx; |
|||
color: #303133; |
|||
} |
|||
|
|||
.wechat-my-text { |
|||
color: #ffffff; |
|||
} |
|||
|
|||
/* 图片消息样式 - 使用微信插件风格 */ |
|||
.wechat-message-image { |
|||
width: 280rpx; |
|||
height: 280rpx; |
|||
max-width: 75%; |
|||
max-height: 500rpx; |
|||
border-radius: 16rpx; |
|||
margin: 8rpx 0; |
|||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); |
|||
background-color: #f5f5f5; |
|||
} |
|||
|
|||
/* 对方图片消息样式 - 左侧缩进 */ |
|||
.wechat-other-image { |
|||
margin-left: 16rpx; |
|||
align-self: flex-start; |
|||
} |
|||
|
|||
/* 我的图片消息样式 - 右侧缩进 */ |
|||
.wechat-my-image { |
|||
margin-right: 16rpx; |
|||
align-self: flex-end; |
|||
} |
|||
|
|||
/* 消息项调整,确保图片和文本消息对齐 */ |
|||
.wechat-message-item { |
|||
align-items: flex-start; |
|||
padding: 4rpx 0; |
|||
} |
|||
|
|||
/* 消息时间 */ |
|||
.wechat-message-time { |
|||
font-size: 22rpx; |
|||
color: #909399; |
|||
padding: 0 4rpx; |
|||
} |
|||
|
|||
.wechat-my-time { |
|||
text-align: right; |
|||
} |
|||
|
|||
/* 输入区域 - 微信风格 */ |
|||
.wechat-input-area { |
|||
background-color: #f5f5f5; |
|||
border-top: 1rpx solid #d8d8d8; |
|||
padding: 12rpx 20rpx; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
/* 输入工具栏 */ |
|||
.wechat-input-toolbar { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 16rpx; |
|||
} |
|||
|
|||
/* 底部安全区域适配 */ |
|||
@media screen and (min-height: 812px) { |
|||
.wechat-input-area { |
|||
padding-bottom: calc(12rpx + env(safe-area-inset-bottom, 0rpx)); |
|||
} |
|||
} |
|||
|
|||
/* 左侧按钮 */ |
|||
.wechat-input-left { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 10rpx; |
|||
} |
|||
|
|||
/* 右侧按钮 */ |
|||
.wechat-input-right { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 10rpx; |
|||
} |
|||
|
|||
/* 表情按钮 */ |
|||
.wechat-emoji-btn { |
|||
width: 64rpx; |
|||
height: 64rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
font-size: 36rpx; |
|||
} |
|||
|
|||
/* 语音按钮 */ |
|||
.wechat-voice-btn { |
|||
width: 64rpx; |
|||
height: 64rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
font-size: 36rpx; |
|||
} |
|||
|
|||
/* 更多按钮 */ |
|||
.wechat-more-btn { |
|||
width: 64rpx; |
|||
height: 64rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
font-size: 44rpx; |
|||
color: #606266; |
|||
} |
|||
|
|||
/* 发送按钮 */ |
|||
.wechat-send-btn { |
|||
background-color: #07c160; |
|||
color: #ffffff; |
|||
font-size: 28rpx; |
|||
padding: 0 28rpx; |
|||
height: 64rpx; |
|||
line-height: 64rpx; |
|||
border-radius: 32rpx; |
|||
text-align: center; |
|||
} |
|||
|
|||
/* 发送按钮禁用状态 */ |
|||
.wechat-send-btn:disabled { |
|||
background-color: #c8c8c8; |
|||
color: #ffffff; |
|||
cursor: not-allowed; |
|||
} |
|||
|
|||
/* 发送按钮点击状态 */ |
|||
.wechat-send-btn:active:not(:disabled) { |
|||
background-color: #06b354; |
|||
} |
|||
|
|||
/* 输入框包装器 */ |
|||
.wechat-input-wrapper { |
|||
flex: 1; |
|||
background-color: #ffffff; |
|||
border: 1rpx solid #d8d8d8; |
|||
border-radius: 32rpx; |
|||
padding: 0 20rpx; |
|||
min-height: 64rpx; |
|||
max-height: 180rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
box-shadow: inset 0 2rpx 4rpx rgba(0, 0, 0, 0.05); |
|||
} |
|||
|
|||
/* 消息输入框 */ |
|||
.wechat-message-input { |
|||
flex: 1; |
|||
font-size: 32rpx; |
|||
color: #303133; |
|||
min-height: 44rpx; |
|||
max-height: 160rpx; |
|||
line-height: 44rpx; |
|||
padding: 0; |
|||
margin: 10rpx 0; |
|||
} |
|||
@ -0,0 +1,474 @@ |
|||
// pages/chat/index.js
|
|||
import socketManager from '../../utils/websocket.js'; |
|||
|
|||
Page({ |
|||
|
|||
/** |
|||
* 页面的初始数据 |
|||
*/ |
|||
data: { |
|||
messages: [], // 初始为空数组,后续通过数据获取和WebSocket更新
|
|||
activeTab: 'all', |
|||
webSocketUrl: '' // WebSocket服务器地址
|
|||
}, |
|||
|
|||
// WebSocket消息处理函数
|
|||
handleWebSocketMessage: function(message) { |
|||
console.log('聊天列表页面接收到消息:', message); |
|||
|
|||
// 判断是否为新消息 - 支持'new_message'类型(服务器实际发送的类型)
|
|||
if (message.type === 'new_message') { |
|||
const newMessage = message.payload; |
|||
this.updateMessageList(newMessage); |
|||
} else if (message.type === 'chat_message') { |
|||
// 保留原有的'chat_message'类型支持,兼容不同的消息格式
|
|||
const newMessage = message.data; |
|||
this.updateMessageList(newMessage); |
|||
} |
|||
}, |
|||
|
|||
// 更新消息列表
|
|||
updateMessageList: function(newMessage) { |
|||
console.log('更新消息列表:', newMessage); |
|||
const messages = [...this.data.messages]; |
|||
|
|||
// 确定消息发送者ID - 处理服务器返回的数据格式
|
|||
const senderId = newMessage.senderId; |
|||
const existingIndex = messages.findIndex(item => item.id === senderId); |
|||
|
|||
// 格式化消息时间 - 服务器使用createdAt字段
|
|||
const now = new Date(); |
|||
const messageDate = new Date(newMessage.createdAt || newMessage.timestamp || Date.now()); |
|||
let displayTime = ''; |
|||
|
|||
if (messageDate.toDateString() === now.toDateString()) { |
|||
// 今天的消息只显示时间
|
|||
displayTime = messageDate.getHours().toString().padStart(2, '0') + ':' + |
|||
messageDate.getMinutes().toString().padStart(2, '0'); |
|||
} else if (messageDate.toDateString() === new Date(now.getTime() - 86400000).toDateString()) { |
|||
// 昨天的消息
|
|||
displayTime = '昨天'; |
|||
} else { |
|||
// 其他日期显示月日
|
|||
displayTime = (messageDate.getMonth() + 1) + '月' + messageDate.getDate() + '日'; |
|||
} |
|||
|
|||
// 获取当前用户ID,确定消息方向
|
|||
const app = getApp(); |
|||
const currentUserId = app.globalData.userInfo?.userId || wx.getStorageSync('userId') || 'unknown'; |
|||
|
|||
// 确定显示的用户ID(如果是自己发送的消息,显示接收者ID)
|
|||
const displayUserId = senderId === currentUserId ? newMessage.receiverId : senderId; |
|||
|
|||
if (existingIndex >= 0) { |
|||
// 存在该用户的消息,更新内容和时间
|
|||
messages[existingIndex] = { |
|||
...messages[existingIndex], |
|||
content: newMessage.content || '', |
|||
time: displayTime, |
|||
isRead: false |
|||
}; |
|||
// 将更新的消息移到列表顶部
|
|||
const [updatedMessage] = messages.splice(existingIndex, 1); |
|||
messages.unshift(updatedMessage); |
|||
} else { |
|||
// 新用户消息,添加到列表顶部
|
|||
// 这里暂时使用ID作为用户名,实际应用中应该从用户信息中获取
|
|||
const displayName = `用户${displayUserId}`; |
|||
messages.unshift({ |
|||
id: displayUserId, |
|||
name: displayName, |
|||
avatar: displayName.charAt(0), |
|||
content: newMessage.content || '', |
|||
time: displayTime, |
|||
isRead: false |
|||
}); |
|||
} |
|||
|
|||
// 使用setData更新视图
|
|||
this.setData({ messages }); |
|||
|
|||
// 触发消息提示振动(可选)
|
|||
wx.vibrateShort(); |
|||
|
|||
// 更新TabBar未读消息数(如果需要)
|
|||
this.updateTabBarBadge(); |
|||
}, |
|||
|
|||
// 更新TabBar未读消息提示 - 使用自定义TabBar兼容方式
|
|||
updateTabBarBadge: function() { |
|||
console.log('更新TabBar未读提示,当前消息数:', this.data.messages.length); |
|||
// 检查是否有未读消息
|
|||
const hasUnread = this.data.messages.some(msg => !msg.isRead); |
|||
console.log('是否有未读消息:', hasUnread); |
|||
|
|||
// 对于自定义TabBar,使用全局状态来管理未读标记
|
|||
const app = getApp(); |
|||
if (app && app.globalData) { |
|||
app.globalData.tabBarBadge = { |
|||
chat: hasUnread ? ' ' : '' |
|||
}; |
|||
console.log('已更新全局TabBar未读标记状态:', hasUnread ? '显示' : '隐藏'); |
|||
|
|||
// 尝试通过getTabBar方法通知自定义TabBar更新
|
|||
try { |
|||
const tabBar = this.getTabBar(); |
|||
if (tabBar) { |
|||
tabBar.setData({ |
|||
selected: 'buyer', // 假设聊天页是buyer tab
|
|||
badge: hasUnread ? ' ' : '' |
|||
}); |
|||
} |
|||
} catch (e) { |
|||
console.log('TabBar更新失败,将在下一次页面显示时自动更新'); |
|||
} |
|||
} |
|||
}, |
|||
|
|||
// 清理所有未读消息状态
|
|||
clearAllUnreadStatus: function() { |
|||
console.log('清理所有未读消息状态'); |
|||
try { |
|||
// 1. 更新本地消息列表中的未读状态
|
|||
const updatedMessages = this.data.messages.map(msg => ({ |
|||
...msg, |
|||
isRead: true |
|||
})); |
|||
|
|||
this.setData({ |
|||
messages: updatedMessages |
|||
}); |
|||
|
|||
// 2. 对于自定义TabBar,使用全局状态来管理未读标记
|
|||
const app = getApp(); |
|||
if (app && app.globalData) { |
|||
app.globalData.tabBarBadge = { |
|||
chat: '' |
|||
}; |
|||
console.log('已清理全局TabBar未读标记'); |
|||
|
|||
// 尝试通过getTabBar方法通知自定义TabBar更新
|
|||
try { |
|||
const tabBar = this.getTabBar(); |
|||
if (tabBar) { |
|||
tabBar.setData({ |
|||
selected: 'buyer', // 假设聊天页是buyer tab
|
|||
badge: '' |
|||
}); |
|||
} |
|||
} catch (e) { |
|||
console.log('TabBar更新失败,将在下一次页面显示时自动更新'); |
|||
} |
|||
} |
|||
|
|||
// 3. 显示成功提示
|
|||
wx.showToast({ |
|||
title: '已清除所有未读提示', |
|||
icon: 'success', |
|||
duration: 2000 |
|||
}); |
|||
} catch (error) { |
|||
console.error('清理未读状态失败:', error); |
|||
wx.showToast({ |
|||
title: '清理失败', |
|||
icon: 'none', |
|||
duration: 2000 |
|||
}); |
|||
} |
|||
}, |
|||
|
|||
// 初始化WebSocket连接
|
|||
initWebSocket: function() { |
|||
try { |
|||
const app = getApp(); |
|||
|
|||
// 使用正确的WebSocket服务器地址 - 开发环境通常是ws://localhost:3003
|
|||
// 动态构建WebSocket URL,基于全局配置或环境
|
|||
let wsProtocol = 'ws://'; |
|||
let wsHost = app.globalData.webSocketUrl || 'localhost:3003'; |
|||
let wsUrl; |
|||
|
|||
// 如果wsHost已经包含协议,直接使用
|
|||
if (wsHost.startsWith('ws://') || wsHost.startsWith('wss://')) { |
|||
wsUrl = wsHost; |
|||
} else { |
|||
// 否则添加协议前缀
|
|||
wsUrl = `${wsProtocol}${wsHost}`; |
|||
} |
|||
|
|||
this.setData({ webSocketUrl: wsUrl }); |
|||
|
|||
console.log('WebSocket连接初始化,使用地址:', wsUrl); |
|||
|
|||
// 连接WebSocket
|
|||
socketManager.connect(wsUrl); |
|||
|
|||
// 添加消息监听
|
|||
socketManager.on('message', this.handleWebSocketMessage.bind(this)); |
|||
|
|||
// 添加状态监听,以便调试
|
|||
socketManager.on('status', (status) => { |
|||
console.log('WebSocket状态更新:', status); |
|||
}); |
|||
|
|||
console.log('聊天列表页面WebSocket已初始化'); |
|||
} catch (error) { |
|||
console.error('初始化WebSocket失败:', error); |
|||
wx.showToast({ |
|||
title: 'WebSocket初始化失败', |
|||
icon: 'none' |
|||
}); |
|||
} |
|||
}, |
|||
|
|||
// 清理WebSocket连接
|
|||
cleanupWebSocket: function() { |
|||
socketManager.off('message', this.handleWebSocketMessage); |
|||
}, |
|||
|
|||
// 加载聊天列表数据
|
|||
loadChatList: function() { |
|||
wx.showLoading({ title: '加载中' }); |
|||
|
|||
try { |
|||
// 从服务器获取真实的聊天列表数据
|
|||
const app = getApp(); |
|||
const token = app.globalData.token || wx.getStorageSync('token'); |
|||
|
|||
// 使用正确的API配置,兼容开发环境
|
|||
const baseUrl = app.globalData.baseUrl || 'http://localhost:3003'; |
|||
const currentUserId = app.globalData.userInfo?.userId || wx.getStorageSync('userId') || 'unknown'; |
|||
console.log('使用API地址:', baseUrl); |
|||
console.log('当前用户ID:', currentUserId); |
|||
|
|||
// 使用正确的API端点 - /api/conversations/user/:userId
|
|||
wx.request({ |
|||
url: `${baseUrl}/api/conversations/user/${currentUserId}`, |
|||
method: 'GET', |
|||
header: { |
|||
'Authorization': token ? `Bearer ${token}` : '', |
|||
'content-type': 'application/json' |
|||
}, |
|||
success: (res) => { |
|||
console.log('获取聊天列表成功:', res.data); |
|||
// 处理不同的API响应格式
|
|||
let chatData = []; |
|||
|
|||
if (res.data.code === 0 && res.data.data) { |
|||
chatData = res.data.data; |
|||
} else if (Array.isArray(res.data)) { |
|||
// 如果直接返回数组
|
|||
chatData = res.data; |
|||
} else if (res.data) { |
|||
// 如果返回的是对象但不是标准格式,尝试直接使用
|
|||
chatData = [res.data]; |
|||
} |
|||
|
|||
if (chatData.length > 0) { |
|||
// 格式化聊天列表数据
|
|||
const formattedMessages = chatData.map(item => { |
|||
// 格式化时间
|
|||
const now = new Date(); |
|||
const messageDate = new Date(item.lastMessageTime || item.createdAt || Date.now()); |
|||
let displayTime = ''; |
|||
|
|||
if (messageDate.toDateString() === now.toDateString()) { |
|||
// 今天的消息只显示时间
|
|||
displayTime = messageDate.getHours().toString().padStart(2, '0') + ':' + |
|||
messageDate.getMinutes().toString().padStart(2, '0'); |
|||
} else if (messageDate.toDateString() === new Date(now.getTime() - 86400000).toDateString()) { |
|||
// 昨天的消息
|
|||
displayTime = '昨天'; |
|||
} else { |
|||
// 其他日期显示月日
|
|||
displayTime = (messageDate.getMonth() + 1) + '月' + messageDate.getDate() + '日'; |
|||
} |
|||
|
|||
// 获取当前用户ID,确定显示哪个用户
|
|||
const displayUserId = item.userId === currentUserId ? item.managerId : item.userId || item.id; |
|||
const displayName = `用户${displayUserId}`; |
|||
|
|||
return { |
|||
id: displayUserId, |
|||
name: item.userName || item.name || displayName, |
|||
avatar: item.avatar || (item.userName || displayName).charAt(0) || '用', |
|||
content: item.lastMessage || item.content || '', |
|||
time: displayTime, |
|||
isRead: item.isRead || false, |
|||
unreadCount: item.unreadCount || 0 |
|||
}; |
|||
}); |
|||
|
|||
this.setData({ messages: formattedMessages }); |
|||
} else { |
|||
console.log('暂无聊天消息'); |
|||
this.setData({ messages: [] }); |
|||
} |
|||
}, |
|||
fail: (err) => { |
|||
console.error('网络请求失败:', err); |
|||
wx.showToast({ |
|||
title: '网络请求失败,使用本地数据', |
|||
icon: 'none', |
|||
duration: 3000 |
|||
}); |
|||
|
|||
// 失败时使用模拟数据,确保页面能够正常显示
|
|||
this.setData({ |
|||
messages: [ |
|||
{ |
|||
id: '1', |
|||
name: '系统消息', |
|||
avatar: '系', |
|||
content: '欢迎使用聊天功能', |
|||
time: '刚刚', |
|||
isRead: false |
|||
} |
|||
] |
|||
}); |
|||
}, |
|||
complete: () => { |
|||
wx.hideLoading(); |
|||
} |
|||
}); |
|||
} catch (error) { |
|||
console.error('加载聊天列表异常:', error); |
|||
wx.hideLoading(); |
|||
wx.showToast({ title: '加载异常', icon: 'none' }); |
|||
} |
|||
}, |
|||
|
|||
// 返回上一页
|
|||
onBack: function() { |
|||
wx.navigateBack({ |
|||
delta: 1 |
|||
}); |
|||
}, |
|||
|
|||
/** |
|||
* 生命周期函数--监听页面加载 |
|||
*/ |
|||
onLoad(options) { |
|||
this.loadChatList(); |
|||
}, |
|||
|
|||
/** |
|||
* 生命周期函数--监听页面初次渲染完成 |
|||
*/ |
|||
onReady() { |
|||
// 设置导航栏右侧按钮的点击事件
|
|||
wx.showNavigationBarLoading(); |
|||
}, |
|||
|
|||
// 导航栏左侧按钮点击事件
|
|||
onNavigationBarButtonTap(e) { |
|||
if (e.type === 'left') { |
|||
this.onBack(); |
|||
} else if (e.type === 'right') { |
|||
// 处理管理按钮点击
|
|||
wx.showToast({ |
|||
title: '管理功能待开发', |
|||
icon: 'none' |
|||
}); |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 生命周期函数--监听页面显示 |
|||
*/ |
|||
onShow() { |
|||
// 页面显示时初始化WebSocket连接
|
|||
this.initWebSocket(); |
|||
}, |
|||
|
|||
/** |
|||
* 生命周期函数--监听页面隐藏 |
|||
*/ |
|||
onHide() { |
|||
// 页面隐藏时清理WebSocket连接
|
|||
this.cleanupWebSocket(); |
|||
}, |
|||
|
|||
/** |
|||
* 生命周期函数--监听页面卸载 |
|||
*/ |
|||
onUnload() { |
|||
// 页面卸载时清理WebSocket连接
|
|||
this.cleanupWebSocket(); |
|||
}, |
|||
|
|||
/** |
|||
* 页面相关事件处理函数--监听用户下拉动作 |
|||
*/ |
|||
onPullDownRefresh() { |
|||
|
|||
}, |
|||
|
|||
/** |
|||
* 页面上拉触底事件的处理函数 |
|||
*/ |
|||
onReachBottom() { |
|||
|
|||
}, |
|||
|
|||
/** |
|||
* 用户点击右上角分享 |
|||
*/ |
|||
onShareAppMessage: function () { |
|||
// 页面分享配置
|
|||
return { |
|||
title: '聊天列表', |
|||
path: '/pages/chat/index' |
|||
} |
|||
}, |
|||
|
|||
// 跳转到对话详情页面
|
|||
navigateToChatDetail: function(e) { |
|||
const userId = e.currentTarget.dataset.userId; |
|||
const userName = e.currentTarget.dataset.userName; |
|||
|
|||
// 将该聊天标记为已读
|
|||
const messages = [...this.data.messages]; |
|||
const messageIndex = messages.findIndex(item => item.id === userId); |
|||
|
|||
if (messageIndex >= 0) { |
|||
messages[messageIndex].isRead = true; |
|||
messages[messageIndex].unreadCount = 0; |
|||
this.setData({ messages }); |
|||
|
|||
// 更新TabBar未读消息数
|
|||
this.updateTabBarBadge(); |
|||
|
|||
// 通知服务器已读状态(可选)
|
|||
this.markAsRead(userId); |
|||
} |
|||
|
|||
wx.navigateTo({ |
|||
url: `/pages/chat-detail/index?userId=${userId}&userName=${encodeURIComponent(userName)}` |
|||
}); |
|||
}, |
|||
|
|||
// 通知服务器消息已读
|
|||
markAsRead: function(userId) { |
|||
const app = getApp(); |
|||
const token = app.globalData.token || wx.getStorageSync('token'); |
|||
|
|||
wx.request({ |
|||
url: `${app.globalData.baseUrl || 'https://your-server.com'}/api/chat/read`, |
|||
method: 'POST', |
|||
header: { |
|||
'Authorization': `Bearer ${token}`, |
|||
'content-type': 'application/json' |
|||
}, |
|||
data: { |
|||
userId: userId |
|||
}, |
|||
success: (res) => { |
|||
console.log('标记消息已读成功:', res.data); |
|||
}, |
|||
fail: (err) => { |
|||
console.error('标记消息已读失败:', err); |
|||
} |
|||
}); |
|||
} |
|||
}) |
|||
@ -0,0 +1,8 @@ |
|||
{ |
|||
"navigationBarTitleText": "消息", |
|||
"navigationBarBackgroundColor": "#ffffff", |
|||
"navigationBarTextStyle": "black", |
|||
"navigationBarLeftButtonText": "返回", |
|||
"navigationBarRightButtonText": "管理", |
|||
"usingComponents": {} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
<view class="chat-container"> |
|||
<!-- 页面顶部导航已在index.json中配置 --> |
|||
|
|||
<!-- 消息类型切换 --> |
|||
<view class="message-tabs"> |
|||
<view class="tab-item active">全部</view> |
|||
<view class="tab-item">未读</view> |
|||
</view> |
|||
|
|||
<!-- 清除未读提示 --> |
|||
<view class="clear-unread"> |
|||
<text class="clear-btn" bindtap="clearAllUnreadStatus">清除未读</text> |
|||
</view> |
|||
|
|||
<!-- 消息列表 --> |
|||
<view class="message-list"> |
|||
<!-- 提示消息 --> |
|||
<view class="message-tips"> |
|||
以下为3天前的消息,提示将弱化 |
|||
</view> |
|||
|
|||
<!-- 动态消息列表 --> |
|||
<block wx:if="{{messages.length > 0}}"> |
|||
<view wx:for="{{messages}}" wx:key="id" class="message-item" bindtap="navigateToChatDetail" data-user-id="{{item.id}}" data-user-name="{{item.name}}"> |
|||
<view class="message-avatar"> |
|||
<text class="avatar-icon">{{item.avatar}}</text> |
|||
<view wx:if="{{!item.isRead}}" class="unread-dot"></view> |
|||
</view> |
|||
<view class="message-content"> |
|||
<view class="message-header"> |
|||
<text class="message-name">{{item.name}}</text> |
|||
<text class="message-time">{{item.time}}</text> |
|||
</view> |
|||
<text class="message-text {{!item.isRead ? 'unread' : ''}}">{{item.content}}</text> |
|||
</view> |
|||
</view> |
|||
</block> |
|||
<!-- 空状态提示 --> |
|||
<view wx:else class="empty-state"> |
|||
<text class="empty-text">暂无消息</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
@ -0,0 +1,182 @@ |
|||
.chat-container { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100vh; |
|||
background-color: #f5f5f5; |
|||
padding-top: 0; |
|||
} |
|||
|
|||
/* 消息类型切换 */ |
|||
.message-tabs { |
|||
display: flex; |
|||
background-color: #ffffff; |
|||
border-bottom: 1rpx solid #eeeeee; |
|||
width: 100%; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
.tab-item { |
|||
flex: 1; |
|||
text-align: center; |
|||
padding: 28rpx 0; |
|||
font-size: 32rpx; |
|||
color: #666666; |
|||
position: relative; |
|||
} |
|||
|
|||
.tab-item.active { |
|||
color: #07c160; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.tab-item.active::after { |
|||
content: ''; |
|||
position: absolute; |
|||
bottom: 0; |
|||
left: 40%; |
|||
width: 20%; |
|||
height: 4rpx; |
|||
background-color: #07c160; |
|||
} |
|||
|
|||
/* 清除未读提示 */ |
|||
.clear-unread { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
padding: 20rpx 30rpx; |
|||
background-color: #ffffff; |
|||
box-sizing: border-box; |
|||
width: 100%; |
|||
} |
|||
|
|||
.clear-btn { |
|||
font-size: 28rpx; |
|||
color: #1677ff; |
|||
} |
|||
|
|||
/* 消息列表 */ |
|||
.message-list { |
|||
flex: 1; |
|||
padding-bottom: 30rpx; |
|||
overflow-y: auto; |
|||
} |
|||
|
|||
/* 提示消息 */ |
|||
.message-tips { |
|||
text-align: center; |
|||
padding: 20rpx 0; |
|||
margin-bottom: 20rpx; |
|||
color: #999999; |
|||
font-size: 28rpx; |
|||
} |
|||
|
|||
/* 消息项 */ |
|||
.message-item { |
|||
display: flex; |
|||
padding: 24rpx 30rpx; |
|||
background-color: #ffffff; |
|||
position: relative; |
|||
} |
|||
|
|||
/* 添加底部边框线,模仿微信分隔线 */ |
|||
.message-item::after { |
|||
content: ''; |
|||
position: absolute; |
|||
left: 150rpx; |
|||
right: 0; |
|||
bottom: 0; |
|||
height: 1rpx; |
|||
background-color: #f0f0f0; |
|||
} |
|||
|
|||
/* 头像 */ |
|||
.message-avatar { |
|||
width: 100rpx; |
|||
height: 100rpx; |
|||
border-radius: 8rpx; |
|||
background-color: #07c160; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
margin-right: 24rpx; |
|||
position: relative; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
/* 未读消息红点 */ |
|||
.unread-dot { |
|||
position: absolute; |
|||
top: -6rpx; |
|||
right: -6rpx; |
|||
width: 28rpx; |
|||
height: 28rpx; |
|||
border-radius: 14rpx; |
|||
background-color: #ff3b30; |
|||
border: 2rpx solid #ffffff; |
|||
} |
|||
|
|||
.avatar-icon { |
|||
font-size: 40rpx; |
|||
color: #ffffff; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
/* 消息内容 */ |
|||
.message-content { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
padding: 6rpx 0; |
|||
} |
|||
|
|||
.message-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 8rpx; |
|||
} |
|||
|
|||
.message-name { |
|||
font-size: 32rpx; |
|||
color: #000000; |
|||
font-weight: 500; |
|||
flex: 1; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.message-time { |
|||
font-size: 24rpx; |
|||
color: #999999; |
|||
margin-left: 12rpx; |
|||
} |
|||
|
|||
.message-text { |
|||
font-size: 28rpx; |
|||
color: #999999; |
|||
line-height: 40rpx; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
/* 未读消息文本样式 */ |
|||
.message-text.unread { |
|||
color: #000000; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
/* 空状态样式 */ |
|||
.empty-state { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
padding: 100rpx 0; |
|||
color: #999999; |
|||
} |
|||
|
|||
.empty-text { |
|||
font-size: 32rpx; |
|||
color: #999999; |
|||
} |
|||
@ -0,0 +1,165 @@ |
|||
// pages/customer-service/detail/index.js
|
|||
Page({ |
|||
data: { |
|||
customerData: null, |
|||
customerServices: [ |
|||
{ |
|||
id: 1, |
|||
managerId: 'PM001', |
|||
managercompany: '鸡蛋贸易有限公司', |
|||
managerdepartment: '采购部', |
|||
organization: '鸡蛋采购组', |
|||
projectName: '高级采购经理', |
|||
name: '张三', |
|||
alias: '张经理', |
|||
phoneNumber: '13800138001', |
|||
avatarUrl: '', |
|||
score: 999, |
|||
isOnline: true, |
|||
responsibleArea: '华北区鸡蛋采购', |
|||
experience: '1-3年', |
|||
serviceCount: 200, |
|||
purchaseCount: 15000, |
|||
profitIncreaseRate: 15, |
|||
profitFarmCount: 120, |
|||
skills: ['渠道拓展', '供应商维护', '质量把控', '精准把控市场价格'] |
|||
}, |
|||
{ |
|||
id: 2, |
|||
managerId: 'PM002', |
|||
managercompany: '鸡蛋贸易有限公司', |
|||
managerdepartment: '采购部', |
|||
organization: '全国采购组', |
|||
projectName: '采购经理', |
|||
name: '李四', |
|||
alias: '李经理', |
|||
phoneNumber: '13900139002', |
|||
avatarUrl: '', |
|||
score: 998, |
|||
isOnline: true, |
|||
responsibleArea: '全国鸡蛋采购', |
|||
experience: '2-3年', |
|||
serviceCount: 200, |
|||
purchaseCount: 20000, |
|||
profitIncreaseRate: 18, |
|||
profitFarmCount: 150, |
|||
skills: ['精准把控市场价格', '渠道拓展', '供应商维护'] |
|||
}, |
|||
{ |
|||
id: 3, |
|||
managerId: 'PM003', |
|||
managercompany: '鸡蛋贸易有限公司', |
|||
managerdepartment: '采购部', |
|||
organization: '华东采购组', |
|||
projectName: '采购专员', |
|||
name: '王五', |
|||
alias: '王专员', |
|||
phoneNumber: '13700137003', |
|||
avatarUrl: '', |
|||
score: 997, |
|||
isOnline: false, |
|||
responsibleArea: '华东区鸡蛋采购', |
|||
experience: '1-2年', |
|||
serviceCount: 150, |
|||
purchaseCount: 12000, |
|||
profitIncreaseRate: 12, |
|||
profitFarmCount: 80, |
|||
skills: ['质量把控', '供应商维护'] |
|||
}, |
|||
{ |
|||
id: 4, |
|||
managerId: 'PM004', |
|||
managercompany: '鸡蛋贸易有限公司', |
|||
managerdepartment: '采购部', |
|||
organization: '华南采购组', |
|||
projectName: '高级采购经理', |
|||
name: '赵六', |
|||
alias: '赵经理', |
|||
phoneNumber: '13600136004', |
|||
avatarUrl: '', |
|||
score: 996, |
|||
isOnline: true, |
|||
responsibleArea: '华南区鸡蛋采购', |
|||
experience: '3-5年', |
|||
serviceCount: 250, |
|||
purchaseCount: 25000, |
|||
profitIncreaseRate: 20, |
|||
profitFarmCount: 180, |
|||
skills: ['精准把控市场价格', '渠道拓展', '质量把控', '供应商维护'] |
|||
} |
|||
] |
|||
}, |
|||
|
|||
onLoad: function (options) { |
|||
// 获取传递过来的客服ID
|
|||
const { id } = options; |
|||
// 根据ID查找客服数据
|
|||
const customerData = this.data.customerServices.find(item => item.id === parseInt(id)); |
|||
|
|||
if (customerData) { |
|||
this.setData({ |
|||
customerData: customerData |
|||
}); |
|||
// 设置导航栏标题
|
|||
wx.setNavigationBarTitle({ |
|||
title: `${customerData.alias} - 客服详情`, |
|||
}); |
|||
} else { |
|||
// 如果找不到对应ID的客服,显示错误提示
|
|||
wx.showToast({ |
|||
title: '未找到客服信息', |
|||
icon: 'none' |
|||
}); |
|||
} |
|||
}, |
|||
|
|||
onShow() { |
|||
// 更新自定义tabBar状态
|
|||
if (typeof this.getTabBar === 'function' && this.getTabBar()) { |
|||
this.getTabBar().setData({ |
|||
selected: -1 // 不选中任何tab
|
|||
}); |
|||
} |
|||
}, |
|||
|
|||
// 返回上一页
|
|||
onBack: function () { |
|||
wx.navigateBack(); |
|||
}, |
|||
|
|||
// 在线沟通
|
|||
onChat: function () { |
|||
const { customerData } = this.data; |
|||
if (customerData) { |
|||
wx.navigateTo({ |
|||
url: `/pages/chat/index?id=${customerData.id}&name=${customerData.alias}&phone=${customerData.phoneNumber}` |
|||
}); |
|||
} |
|||
}, |
|||
|
|||
// 拨打电话
|
|||
onCall: function () { |
|||
const { customerData } = this.data; |
|||
if (customerData && customerData.phoneNumber) { |
|||
wx.makePhoneCall({ |
|||
phoneNumber: customerData.phoneNumber, |
|||
success: function () { |
|||
console.log('拨打电话成功'); |
|||
}, |
|||
fail: function () { |
|||
console.log('拨打电话失败'); |
|||
} |
|||
}); |
|||
} |
|||
}, |
|||
|
|||
// 分享功能
|
|||
onShareAppMessage: function () { |
|||
const { customerData } = this.data; |
|||
return { |
|||
title: `${customerData?.alias || '优秀客服'} - 鸡蛋贸易平台`, |
|||
path: `/pages/customer-service/detail/index?id=${customerData?.id}`, |
|||
imageUrl: '' |
|||
}; |
|||
} |
|||
}); |
|||
@ -0,0 +1,7 @@ |
|||
{ |
|||
"navigationBarTitleText": "客服详情", |
|||
"navigationBarBackgroundColor": "#ffffff", |
|||
"navigationBarTextStyle": "black", |
|||
"backgroundColor": "#f8f8f8", |
|||
"usingComponents": {} |
|||
} |
|||
@ -0,0 +1,93 @@ |
|||
<!-- pages/customer-service/detail/index.wxml --> |
|||
<view class="container"> |
|||
<!-- 顶部导航栏 --> |
|||
<view class="nav-bar"> |
|||
<view class="nav-left" bindtap="onBack"> |
|||
<text class="back-icon">返回</text> |
|||
</view> |
|||
<view class="nav-title">客服详情</view> |
|||
<view class="nav-right"> |
|||
<text class="share-icon">📤</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 客服基本信息 --> |
|||
<view class="info-section"> |
|||
<view class="header-info"> |
|||
<view class="avatar-container"> |
|||
<image class="avatar" src="{{customerData.avatarUrl || '/images/default-avatar.png'}}" mode="aspectFill" /> |
|||
<view wx:if="{{customerData.isOnline}}" class="online-indicator-large">在线</view> |
|||
<view wx:else class="offline-indicator-large">离线</view> |
|||
</view> |
|||
<view class="header-details"> |
|||
<view class="name-score-row"> |
|||
<text class="name-large">{{customerData.alias}}</text> |
|||
<text class="score-large">{{customerData.score}} 鸡蛋分</text> |
|||
</view> |
|||
<text class="position">{{customerData.projectName}}</text> |
|||
<text class="company-large">{{customerData.managercompany}}</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 核心信息卡片 --> |
|||
<view class="core-info-section"> |
|||
<view class="info-card"> |
|||
<view class="info-item"> |
|||
<text class="info-label">负责区域</text> |
|||
<text class="info-value">{{customerData.responsibleArea}}</text> |
|||
</view> |
|||
<view class="info-item"> |
|||
<text class="info-label">联系电话</text> |
|||
<text class="info-value phone-number" bindtap="onCall">{{customerData.phoneNumber}}</text> |
|||
</view> |
|||
<view class="info-item"> |
|||
<text class="info-label">工作经验</text> |
|||
<text class="info-value">服务平台{{customerData.experience}}</text> |
|||
</view> |
|||
<view class="info-item"> |
|||
<text class="info-label">服务规模</text> |
|||
<text class="info-value">服务{{customerData.serviceCount}}家鸡场</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 专业技能标签 --> |
|||
<view class="skills-section"> |
|||
<view class="section-title">专业技能</view> |
|||
<view class="skills-container"> |
|||
<view wx:for="{{customerData.skills}}" wx:key="index" class="skill-tag"> |
|||
{{item}} |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 业绩数据 --> |
|||
<view class="performance-section"> |
|||
<view class="section-title">业绩数据</view> |
|||
<view class="performance-cards"> |
|||
<view class="performance-card"> |
|||
<view class="performance-number">{{customerData.purchaseCount}}</view> |
|||
<view class="performance-label">累计采购鸡蛋(件)</view> |
|||
</view> |
|||
<view class="performance-card"> |
|||
<view class="performance-number">{{customerData.profitFarmCount}}</view> |
|||
<view class="performance-label">累计服务鸡场(家)</view> |
|||
</view> |
|||
<view class="performance-card"> |
|||
<view class="performance-number profit-rate">{{customerData.profitIncreaseRate}}%</view> |
|||
<view class="performance-label">平均盈利增长</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 底部操作按钮 --> |
|||
<view class="bottom-actions"> |
|||
<view class="action-button chat-button" bindtap="onChat"> |
|||
<text class="button-text">💬 在线沟通</text> |
|||
</view> |
|||
<view class="action-button call-button" bindtap="onCall"> |
|||
<text class="button-text">📞 电话联系</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
@ -0,0 +1,317 @@ |
|||
/* pages/customer-service/detail/index.wxss */ |
|||
.container { |
|||
padding-bottom: 100rpx; |
|||
background-color: #f8f8f8; |
|||
min-height: 100vh; |
|||
} |
|||
|
|||
/* 顶部导航栏 */ |
|||
.nav-bar { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 44rpx 30rpx 20rpx; |
|||
background-color: #fff; |
|||
border-bottom: 1rpx solid #f0f0f0; |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
z-index: 1000; |
|||
width: 100%; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
.nav-left { |
|||
width: 80rpx; |
|||
display: flex; |
|||
justify-content: flex-start; |
|||
} |
|||
|
|||
.back-icon { |
|||
font-size: 32rpx; |
|||
color: #333; |
|||
font-weight: normal; |
|||
} |
|||
|
|||
.nav-title { |
|||
font-size: 36rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
flex: 1; |
|||
text-align: center; |
|||
} |
|||
|
|||
.nav-right { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 30rpx; |
|||
} |
|||
|
|||
.share-icon { |
|||
font-size: 32rpx; |
|||
color: #333; |
|||
} |
|||
|
|||
/* 客服基本信息 */ |
|||
.info-section { |
|||
background-color: #fff; |
|||
padding: 120rpx 30rpx 30rpx; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.header-info { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.avatar-container { |
|||
width: 160rpx; |
|||
height: 160rpx; |
|||
margin-right: 30rpx; |
|||
position: relative; |
|||
} |
|||
|
|||
.avatar { |
|||
width: 100%; |
|||
height: 100%; |
|||
border-radius: 50%; |
|||
background-color: #f0f0f0; |
|||
border: 4rpx solid #f0f0f0; |
|||
} |
|||
|
|||
.online-indicator-large { |
|||
position: absolute; |
|||
bottom: 0; |
|||
right: 0; |
|||
background-color: #52c41a; |
|||
color: white; |
|||
font-size: 24rpx; |
|||
padding: 8rpx 20rpx; |
|||
border-radius: 20rpx; |
|||
border: 4rpx solid white; |
|||
} |
|||
|
|||
.offline-indicator-large { |
|||
position: absolute; |
|||
bottom: 0; |
|||
right: 0; |
|||
background-color: #999; |
|||
color: white; |
|||
font-size: 24rpx; |
|||
padding: 8rpx 20rpx; |
|||
border-radius: 20rpx; |
|||
border: 4rpx solid white; |
|||
} |
|||
|
|||
.header-details { |
|||
flex: 1; |
|||
} |
|||
|
|||
.name-score-row { |
|||
display: flex; |
|||
align-items: center; |
|||
margin-bottom: 12rpx; |
|||
} |
|||
|
|||
.name-large { |
|||
font-size: 40rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
margin-right: 16rpx; |
|||
} |
|||
|
|||
.score-large { |
|||
font-size: 28rpx; |
|||
color: #fff; |
|||
background: linear-gradient(135deg, #ffb800, #ff7700); |
|||
padding: 6rpx 16rpx; |
|||
border-radius: 20rpx; |
|||
} |
|||
|
|||
.position { |
|||
font-size: 32rpx; |
|||
color: #666; |
|||
margin-bottom: 8rpx; |
|||
} |
|||
|
|||
.company-large { |
|||
font-size: 28rpx; |
|||
color: #999; |
|||
} |
|||
|
|||
/* 核心信息卡片 */ |
|||
.core-info-section { |
|||
padding: 0 30rpx; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.info-card { |
|||
background-color: #fff; |
|||
border-radius: 24rpx; |
|||
padding: 30rpx; |
|||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); |
|||
} |
|||
|
|||
.info-item { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 20rpx 0; |
|||
border-bottom: 1rpx solid #f0f0f0; |
|||
} |
|||
|
|||
.info-item:last-child { |
|||
border-bottom: none; |
|||
} |
|||
|
|||
.info-label { |
|||
font-size: 30rpx; |
|||
color: #666; |
|||
} |
|||
|
|||
.info-value { |
|||
font-size: 30rpx; |
|||
color: #333; |
|||
text-align: right; |
|||
flex: 1; |
|||
margin-left: 30rpx; |
|||
} |
|||
|
|||
.phone-number { |
|||
color: #1890ff; |
|||
} |
|||
|
|||
/* 专业技能标签 */ |
|||
.skills-section { |
|||
background-color: #fff; |
|||
padding: 30rpx; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.section-title { |
|||
font-size: 32rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.skills-container { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 16rpx; |
|||
padding-bottom: 10rpx; |
|||
} |
|||
|
|||
.skill-tag { |
|||
background-color: #f0f9ff; |
|||
color: #1890ff; |
|||
font-size: 26rpx; |
|||
padding: 12rpx 24rpx; |
|||
border-radius: 20rpx; |
|||
border: 1rpx solid #bae7ff; |
|||
transition: all 0.2s ease; |
|||
} |
|||
|
|||
.skill-tag:active { |
|||
background-color: #bae7ff; |
|||
transform: scale(0.98); |
|||
} |
|||
|
|||
/* 业绩数据 */ |
|||
.performance-section { |
|||
background-color: #fff; |
|||
padding: 30rpx; |
|||
margin-bottom: 120rpx; |
|||
} |
|||
|
|||
.performance-cards { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
gap: 20rpx; |
|||
} |
|||
|
|||
.performance-card { |
|||
flex: 1; |
|||
background-color: #f9f9f9; |
|||
padding: 24rpx; |
|||
border-radius: 16rpx; |
|||
text-align: center; |
|||
transition: all 0.2s ease; |
|||
position: relative; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.performance-card:active { |
|||
transform: translateY(2rpx); |
|||
background-color: #f0f0f0; |
|||
} |
|||
|
|||
.performance-number { |
|||
font-size: 36rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
margin-bottom: 12rpx; |
|||
} |
|||
|
|||
.performance-number.profit-rate { |
|||
color: #52c41a; |
|||
} |
|||
|
|||
.performance-label { |
|||
font-size: 24rpx; |
|||
color: #666; |
|||
line-height: 1.4; |
|||
} |
|||
|
|||
/* 底部操作按钮 */ |
|||
.bottom-actions { |
|||
position: fixed; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
display: flex; |
|||
background-color: #fff; |
|||
padding: 20rpx 30rpx; |
|||
border-top: 1rpx solid #f0f0f0; |
|||
gap: 20rpx; |
|||
} |
|||
|
|||
.action-button { |
|||
flex: 1; |
|||
padding: 24rpx; |
|||
border-radius: 40rpx; |
|||
text-align: center; |
|||
font-size: 32rpx; |
|||
font-weight: 500; |
|||
transition: all 0.2s ease; |
|||
position: relative; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.action-button:active { |
|||
transform: scale(0.98); |
|||
opacity: 0.9; |
|||
} |
|||
|
|||
.chat-button:active { |
|||
background-color: #096dd9; |
|||
} |
|||
|
|||
.call-button:active { |
|||
background-color: #389e0d; |
|||
} |
|||
|
|||
.chat-button { |
|||
background-color: #1890ff; |
|||
color: white; |
|||
} |
|||
|
|||
.call-button { |
|||
background-color: #52c41a; |
|||
color: white; |
|||
} |
|||
|
|||
.button-text { |
|||
font-size: 30rpx; |
|||
} |
|||
@ -0,0 +1,362 @@ |
|||
// pages/customer-service/index.js
|
|||
Page({ |
|||
// 客服数据,将从后端动态获取
|
|||
data: { |
|||
customerServices: [], |
|||
filteredServices: [], |
|||
searchKeyword: '', |
|||
selectedArea: '全部', |
|||
totalCount: 0, |
|||
onlineCount: 0 |
|||
}, |
|||
|
|||
// 获取客服列表的方法
|
|||
async fetchCustomerServices() { |
|||
try { |
|||
console.log('开始请求客服列表...'); |
|||
// 导入API工具并使用正确的请求方法
|
|||
const api = require('../../utils/api'); |
|||
|
|||
const res = await new Promise((resolve, reject) => { |
|||
wx.request({ |
|||
url: 'http://localhost:3003/api/managers', |
|||
method: 'GET', |
|||
timeout: 15000, |
|||
header: { |
|||
'content-type': 'application/json' |
|||
}, |
|||
success: resolve, |
|||
fail: (error) => { |
|||
console.error('网络请求失败:', error); |
|||
reject(error); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
console.log('API响应状态码:', res?.statusCode); |
|||
console.log('API响应数据:', res?.data ? JSON.stringify(res.data) : 'undefined'); |
|||
|
|||
// 更宽松的响应检查,确保能处理各种有效的响应格式
|
|||
if (res && res.statusCode === 200 && res.data) { |
|||
// 无论success字段是否存在,只要有data字段就尝试处理
|
|||
const dataSource = res.data.data || res.data; |
|||
if (Array.isArray(dataSource)) { |
|||
const processedData = dataSource.map(item => ({ |
|||
id: item.id || `id_${Date.now()}_${Math.random()}`, // 确保有id
|
|||
managerId: item.managerId || '', |
|||
managercompany: item.managercompany || '', |
|||
managerdepartment: item.managerdepartment || '', |
|||
organization: item.organization || '', |
|||
projectName: item.projectName || '', |
|||
name: item.name || '未知', |
|||
alias: item.alias || item.name || '未知', |
|||
phoneNumber: item.phoneNumber || '', |
|||
avatarUrl: item.avatar || item.avatarUrl || '', // 兼容avatar和avatarUrl
|
|||
score: Math.floor(Math.random() * 20) + 980, // 随机生成分数
|
|||
isOnline: !!item.online, // 转换为布尔值
|
|||
responsibleArea: `${this.getRandomArea()}鸡蛋采购`, |
|||
experience: this.getRandomExperience(), |
|||
serviceCount: this.getRandomNumber(100, 300), |
|||
purchaseCount: this.getRandomNumber(10000, 30000), |
|||
profitIncreaseRate: this.getRandomNumber(10, 25), |
|||
profitFarmCount: this.getRandomNumber(50, 200), |
|||
skills: this.getRandomSkills() |
|||
})); |
|||
console.log('处理后的数据数量:', processedData.length); |
|||
return processedData; |
|||
} else { |
|||
console.error('响应数据格式错误,不是预期的数组格式:', dataSource); |
|||
return []; |
|||
} |
|||
} else { |
|||
console.error('获取客服列表失败,状态码:', res?.statusCode, '响应数据:', res?.data); |
|||
return []; |
|||
} |
|||
} catch (error) { |
|||
console.error('请求客服列表出错:', error); |
|||
// 网络错误时显示提示
|
|||
wx.showToast({ |
|||
title: '网络请求失败,请检查网络连接', |
|||
icon: 'none' |
|||
}); |
|||
return []; |
|||
} |
|||
}, |
|||
|
|||
// 辅助函数:生成随机区域
|
|||
getRandomArea() { |
|||
const areas = ['华北区', '华东区', '华南区', '全国', '西南区', '西北区', '东北区']; |
|||
return areas[Math.floor(Math.random() * areas.length)]; |
|||
}, |
|||
|
|||
// 辅助函数:生成随机工作经验
|
|||
getRandomExperience() { |
|||
const experiences = ['1-2年', '1-3年', '2-3年', '3-5年', '5年以上']; |
|||
return experiences[Math.floor(Math.random() * experiences.length)]; |
|||
}, |
|||
|
|||
// 辅助函数:生成随机数字
|
|||
getRandomNumber(min, max) { |
|||
return Math.floor(Math.random() * (max - min + 1)) + min; |
|||
}, |
|||
|
|||
// 辅助函数:生成随机技能
|
|||
getRandomSkills() { |
|||
const allSkills = ['渠道拓展', '供应商维护', '质量把控', '精准把控市场价格', '谈判技巧', '库存管理']; |
|||
const skillCount = Math.floor(Math.random() * 3) + 2; // 2-4个技能
|
|||
const selectedSkills = []; |
|||
|
|||
while (selectedSkills.length < skillCount) { |
|||
const skill = allSkills[Math.floor(Math.random() * allSkills.length)]; |
|||
if (!selectedSkills.includes(skill)) { |
|||
selectedSkills.push(skill); |
|||
} |
|||
} |
|||
|
|||
return selectedSkills; |
|||
}, |
|||
|
|||
// 设置WebSocket监听客服状态变化
|
|||
setupWebSocketListener() { |
|||
const ws = getApp().globalData.webSocketManager; |
|||
const app = getApp(); |
|||
const userInfo = app.globalData.userInfo || {}; |
|||
const isManager = userInfo.userType === 'manager' || userInfo.type === 'manager'; |
|||
|
|||
if (ws) { |
|||
// 监听客服状态更新消息
|
|||
ws.on('customerServiceStatusUpdate', (data) => { |
|||
console.log('收到客服状态更新:', data); |
|||
// 更新对应客服的在线状态
|
|||
const customerServiceList = this.data.customerServices; |
|||
const updatedList = customerServiceList.map(item => { |
|||
if (item.id === data.id || item.managerId === data.managerId) { |
|||
return { ...item, isOnline: data.isOnline }; |
|||
} |
|||
return item; |
|||
}); |
|||
|
|||
const onlineCount = updatedList.filter(item => item.isOnline).length; |
|||
|
|||
this.setData({ |
|||
customerServices: updatedList, |
|||
onlineCount: onlineCount |
|||
}); |
|||
|
|||
// 重新应用筛选
|
|||
this.filterServices(); |
|||
}); |
|||
|
|||
// 如果当前用户是客服,立即进行认证
|
|||
if (isManager && userInfo.userId) { |
|||
console.log('客服用户登录,进行WebSocket认证:', userInfo.userId); |
|||
// 使用userId作为managerId进行认证,确保与服务器端onlineManagers中的键匹配
|
|||
ws.authenticate(); |
|||
} |
|||
} |
|||
}, |
|||
|
|||
// 定期刷新客服列表(每30秒)
|
|||
startPeriodicRefresh() { |
|||
this.refreshTimer = setInterval(() => { |
|||
console.log('定期刷新客服列表...'); |
|||
this.loadCustomerServices(); |
|||
}, 30000); |
|||
}, |
|||
|
|||
// 停止定期刷新
|
|||
stopPeriodicRefresh() { |
|||
if (this.refreshTimer) { |
|||
clearInterval(this.refreshTimer); |
|||
this.refreshTimer = null; |
|||
} |
|||
}, |
|||
|
|||
// 加载客服列表
|
|||
async loadCustomerServices() { |
|||
console.log('开始加载客服列表...'); |
|||
// 确保在开始时调用hideLoading,防止重复调用showLoading
|
|||
try { |
|||
wx.hideLoading(); |
|||
} catch (e) { |
|||
console.log('没有正在显示的loading'); |
|||
} |
|||
|
|||
wx.showLoading({ title: '加载中...' }); |
|||
try { |
|||
const services = await this.fetchCustomerServices(); |
|||
console.log('获取到的客服数量:', services.length); |
|||
|
|||
// 计算在线数量
|
|||
const onlineCount = services.filter(item => item.isOnline).length; |
|||
console.log('在线客服数量:', onlineCount); |
|||
|
|||
// 更新数据
|
|||
this.setData({ |
|||
customerServices: services, |
|||
totalCount: services.length, |
|||
onlineCount: onlineCount |
|||
}); |
|||
console.log('数据更新成功'); |
|||
|
|||
// 应用当前的筛选条件
|
|||
this.filterServices(); |
|||
console.log('筛选条件应用完成'); |
|||
|
|||
// 如果没有数据,显示提示(确保在hideLoading后显示)
|
|||
if (services.length === 0) { |
|||
setTimeout(() => { |
|||
wx.showToast({ |
|||
title: '暂无客服数据', |
|||
icon: 'none', |
|||
duration: 2000 |
|||
}); |
|||
}, 100); |
|||
} |
|||
} catch (error) { |
|||
console.error('加载客服列表失败:', error); |
|||
// 确保在hideLoading后显示错误提示
|
|||
setTimeout(() => { |
|||
wx.showToast({ |
|||
title: '加载失败,请重试', |
|||
icon: 'none', |
|||
duration: 2000 |
|||
}); |
|||
}, 100); |
|||
} finally { |
|||
wx.hideLoading(); |
|||
} |
|||
}, |
|||
|
|||
onLoad: function () { |
|||
// 初始化WebSocket连接
|
|||
const app = getApp(); |
|||
if (!app.globalData.webSocketManager) { |
|||
// 如果WebSocket管理器还没初始化,从utils导入
|
|||
const WebSocketManager = require('../../utils/websocket').default; |
|||
app.globalData.webSocketManager = WebSocketManager; |
|||
|
|||
// 尝试连接WebSocket
|
|||
WebSocketManager.connect('ws://localhost:3003'); |
|||
} |
|||
|
|||
// 设置WebSocket监听
|
|||
this.setupWebSocketListener(); |
|||
|
|||
// 加载客服列表
|
|||
this.loadCustomerServices(); |
|||
|
|||
// 启动定期刷新
|
|||
this.startPeriodicRefresh(); |
|||
}, |
|||
|
|||
onShow() { |
|||
// 更新自定义tabBar状态
|
|||
if (typeof this.getTabBar === 'function' && this.getTabBar()) { |
|||
this.getTabBar().setData({ |
|||
selected: -1 // 不选中任何tab
|
|||
}); |
|||
} |
|||
|
|||
// 当页面显示时重新加载数据,确保数据最新
|
|||
this.loadCustomerServices(); |
|||
}, |
|||
|
|||
onUnload: function() { |
|||
// 停止定期刷新
|
|||
this.stopPeriodicRefresh(); |
|||
|
|||
// 清理WebSocket事件监听
|
|||
const ws = getApp().globalData.webSocketManager; |
|||
if (ws) { |
|||
ws.off('customerServiceStatusUpdate'); |
|||
} |
|||
}, |
|||
|
|||
onSearch: function (e) { |
|||
const keyword = e.detail.value; |
|||
this.setData({ |
|||
searchKeyword: keyword |
|||
}); |
|||
this.filterServices(); |
|||
}, |
|||
|
|||
onAreaFilter: function () { |
|||
// 区域筛选弹窗 - 鸡蛋采购区域
|
|||
wx.showActionSheet({ |
|||
itemList: ['全部', '华北区', '华东区', '华南区', '全国', '西南区', '西北区', '东北区'], |
|||
success: res => { |
|||
const areas = ['全部', '华北区', '华东区', '华南区', '全国', '西南区', '西北区', '东北区']; |
|||
const selectedArea = areas[res.tapIndex]; |
|||
this.setData({ |
|||
selectedArea: selectedArea |
|||
}); |
|||
this.filterServices(); |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
filterServices: function () { |
|||
const { customerServices, searchKeyword, selectedArea } = this.data; |
|||
|
|||
let filtered = customerServices; |
|||
|
|||
// 关键词搜索
|
|||
if (searchKeyword) { |
|||
const keyword = searchKeyword.toLowerCase(); |
|||
filtered = filtered.filter(item => { |
|||
return item.alias?.toLowerCase().includes(keyword) || |
|||
item.name.toLowerCase().includes(keyword) || |
|||
item.phoneNumber?.includes(keyword) || |
|||
item.managercompany?.toLowerCase().includes(keyword); |
|||
}); |
|||
} |
|||
|
|||
// 区域筛选
|
|||
if (selectedArea && selectedArea !== '全部') { |
|||
filtered = filtered.filter(item => { |
|||
return item.responsibleArea?.includes(selectedArea); |
|||
}); |
|||
} |
|||
|
|||
this.setData({ |
|||
filteredServices: filtered |
|||
}); |
|||
}, |
|||
|
|||
onChat: function (e) { |
|||
const id = e.currentTarget.dataset.id; |
|||
const service = this.data.customerServices.find(item => item.id === id); |
|||
// 确保使用managerId作为聊天对象的唯一标识符
|
|||
const chatUserId = service?.managerId || id; |
|||
wx.navigateTo({ |
|||
url: `/pages/chat-detail/index?userId=${chatUserId}&userName=${encodeURIComponent(service?.alias || '')}&phone=${service?.phoneNumber || ''}&isManager=true` |
|||
}); |
|||
console.log('跳转到聊天页面:', { chatUserId, userName: service?.alias }); |
|||
}, |
|||
|
|||
onCall: function (e) { |
|||
const phone = e.currentTarget.dataset.phone; |
|||
wx.makePhoneCall({ |
|||
phoneNumber: phone, |
|||
success: function () { |
|||
console.log('拨打电话成功'); |
|||
}, |
|||
fail: function () { |
|||
console.log('拨打电话失败'); |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
// 查看客服详情
|
|||
onViewDetail: function (e) { |
|||
const id = e.currentTarget.dataset.id; |
|||
wx.navigateTo({ |
|||
url: `/pages/customer-service/detail?id=${id}` |
|||
}); |
|||
}, |
|||
|
|||
onBack: function () { |
|||
wx.navigateBack(); |
|||
} |
|||
}); |
|||
@ -0,0 +1,5 @@ |
|||
{ |
|||
"navigationBarBackgroundColor": "#f8f8f8", |
|||
"navigationBarTextStyle": "black", |
|||
"usingComponents": {} |
|||
} |
|||
@ -0,0 +1,89 @@ |
|||
<!-- pages/customer-service/index.wxml --> |
|||
<view class="container"> |
|||
<!-- 顶部导航栏 --> |
|||
<view class="nav-bar"> |
|||
<view class="nav-left" bindtap="onBack"> |
|||
<text class="back-icon">返回</text> |
|||
</view> |
|||
<view class="nav-title">客服列表</view> |
|||
<view class="nav-right"> |
|||
<text class="settings-icon">⚙️</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 搜索区域 --> |
|||
<view class="search-container"> |
|||
<view class="search-bar"> |
|||
<text class="search-icon-small">🔍</text> |
|||
<input class="search-input" placeholder="客服人称或手机号" bindinput="onSearch" value="{{searchKeyword}}" /> |
|||
</view> |
|||
<view class="filter-area"> |
|||
<view class="area-picker" bindtap="onAreaFilter"> |
|||
<text>区域</text> |
|||
<text class="picker-arrow">▼</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 客服列表 --> |
|||
<view class="broker-list"> |
|||
<block wx:if="{{filteredServices.length > 0}}"> |
|||
<view class="broker-item" wx:for="{{filteredServices}}" wx:key="id" bindtap="onViewDetail" data-id="{{item.id}}"> |
|||
<view class="broker-info"> |
|||
<view class="avatar-container"> |
|||
<image class="avatar" src="{{item.avatarUrl || '/images/default-avatar.png'}}" mode="aspectFill" /> |
|||
<view wx:if="{{item.isOnline}}" class="online-indicator">在线</view> |
|||
<view wx:else class="offline-indicator">离线</view> |
|||
</view> |
|||
<view class="broker-details"> |
|||
<view class="name-row"> |
|||
<text class="name">{{item.alias}}</text> |
|||
<text class="score-text">{{item.score}} 鸡蛋分</text> |
|||
<text wx:if="{{item.isOnline}}" class="online-status">(在线)</text> |
|||
</view> |
|||
<text class="company">{{item.managercompany || '暂无公司信息'}}</text> |
|||
<text class="department">{{item.managerdepartment}} · {{item.projectName}}</text> |
|||
<text class="area">负责区域:{{item.responsibleArea}}</text> |
|||
<text class="experience">服务平台{{item.experience}} 服务{{item.serviceCount}}家鸡场</text> |
|||
<!-- 业绩数据统计 --> |
|||
<view class="performance-stats"> |
|||
<view class="stat-item"> |
|||
<text class="stat-value">{{item.purchaseCount}}</text> |
|||
<text class="stat-label">累计采购(件)</text> |
|||
</view> |
|||
<view class="stat-divider">|</view> |
|||
<view class="stat-item"> |
|||
<text class="stat-value profit-rate">{{item.profitFarmCount}}</text> |
|||
<text class="stat-label">服务盈利鸡场(家)</text> |
|||
</view> |
|||
<view class="stat-divider">|</view> |
|||
<view class="stat-item"> |
|||
<text class="stat-value profit-rate">{{item.profitIncreaseRate}}%</text> |
|||
<text class="stat-label">平均盈利增长</text> |
|||
</view> |
|||
</view> |
|||
<!-- 专业技能标签 --> |
|||
<view class="skills-preview"> |
|||
<view wx:for="{{item.skills}}" wx:key="index" wx:if="{{index < 3}}" class="skill-tag-small"> |
|||
{{item}}</view> |
|||
<view wx:if="{{item.skills.length > 3}}" class="skill-more"> |
|||
+{{item.skills.length - 3}}</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view class="action-buttons"> |
|||
<view class="button-chat" bindtap="onChat" data-id="{{item.id}}"> |
|||
<text class="button-icon">💬</text> |
|||
</view> |
|||
<view class="button-call" bindtap="onCall" data-phone="{{item.phoneNumber}}"> |
|||
<text class="button-icon">📞</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</block> |
|||
<view wx:else class="empty-state"> |
|||
<text>👤</text> |
|||
<text class="empty-text">暂无匹配的经纪人</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
@ -0,0 +1,383 @@ |
|||
/* pages/customer-service/index.wxss */ |
|||
.container { |
|||
padding-bottom: 100rpx; |
|||
background-color: #f8f8f8; |
|||
min-height: 100vh; |
|||
} |
|||
|
|||
/* 顶部导航栏 */ |
|||
.nav-bar { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 44rpx 30rpx 20rpx; |
|||
background-color: #fff; |
|||
border-bottom: 1rpx solid #f0f0f0; |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
z-index: 1000; |
|||
width: 100%; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
.nav-left { |
|||
width: 80rpx; |
|||
display: flex; |
|||
justify-content: flex-start; |
|||
} |
|||
|
|||
.back-icon { |
|||
font-size: 32rpx; |
|||
color: #333; |
|||
font-weight: normal; |
|||
} |
|||
|
|||
.nav-title { |
|||
font-size: 36rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
flex: 1; |
|||
text-align: center; |
|||
} |
|||
|
|||
.nav-right { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 30rpx; |
|||
} |
|||
|
|||
.search-icon, .settings-icon { |
|||
font-size: 32rpx; |
|||
color: #333; |
|||
} |
|||
|
|||
/* 搜索区域 */ |
|||
.search-container { |
|||
background-color: #fff; |
|||
padding: 20rpx 30rpx; |
|||
border-bottom: 10rpx solid #f8f8f8; |
|||
position: fixed; |
|||
top: 110rpx; |
|||
left: 0; |
|||
right: 0; |
|||
z-index: 999; |
|||
width: 100%; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
.search-bar { |
|||
display: flex; |
|||
align-items: center; |
|||
background-color: #f5f5f5; |
|||
border-radius: 40rpx; |
|||
padding: 16rpx 24rpx; |
|||
margin-bottom: 20rpx; |
|||
transition: all 0.3s ease; |
|||
} |
|||
|
|||
.search-bar:focus-within { |
|||
background-color: #e6f7ff; |
|||
box-shadow: 0 0 0 4rpx rgba(24, 144, 255, 0.1); |
|||
} |
|||
|
|||
.search-icon-small { |
|||
font-size: 28rpx; |
|||
color: #999; |
|||
margin-right: 16rpx; |
|||
} |
|||
|
|||
.search-input { |
|||
flex: 1; |
|||
font-size: 28rpx; |
|||
color: #333; |
|||
background: none; |
|||
padding: 0; |
|||
} |
|||
|
|||
.search-input::placeholder { |
|||
color: #999; |
|||
} |
|||
|
|||
.filter-area { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.area-picker { |
|||
display: flex; |
|||
align-items: center; |
|||
background-color: #f5f5f5; |
|||
padding: 12rpx 24rpx; |
|||
border-radius: 24rpx; |
|||
font-size: 28rpx; |
|||
color: #333; |
|||
} |
|||
|
|||
.picker-arrow { |
|||
margin-left: 8rpx; |
|||
font-size: 20rpx; |
|||
color: #999; |
|||
} |
|||
|
|||
/* 经纪人列表 */ |
|||
.broker-list { |
|||
background-color: #f8f8f8; |
|||
padding: 0 30rpx; |
|||
margin-top: 280rpx; /* 为固定导航和搜索区域留出空间 */ |
|||
} |
|||
|
|||
.broker-item { |
|||
background-color: #fff; |
|||
border-radius: 24rpx; |
|||
margin: 20rpx 0; |
|||
padding: 28rpx; |
|||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); |
|||
transition: all 0.2s ease; |
|||
} |
|||
|
|||
.broker-item:active { |
|||
transform: translateY(2rpx); |
|||
box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.03); |
|||
background-color: #fafafa; |
|||
} |
|||
|
|||
.broker-info { |
|||
display: flex; |
|||
margin-bottom: 24rpx; |
|||
} |
|||
|
|||
.avatar-container { |
|||
width: 100rpx; |
|||
height: 100rpx; |
|||
margin-right: 24rpx; |
|||
position: relative; |
|||
} |
|||
|
|||
.avatar { |
|||
width: 100%; |
|||
height: 100%; |
|||
border-radius: 50%; |
|||
background-color: #f0f0f0; |
|||
border: 2rpx solid #f0f0f0; |
|||
} |
|||
|
|||
.online-indicator { |
|||
position: absolute; |
|||
bottom: 0; |
|||
right: 0; |
|||
background-color: #52c41a; |
|||
color: white; |
|||
font-size: 18rpx; |
|||
padding: 4rpx 12rpx; |
|||
border-radius: 12rpx; |
|||
border: 2rpx solid white; |
|||
} |
|||
|
|||
.offline-indicator { |
|||
position: absolute; |
|||
bottom: 0; |
|||
right: 0; |
|||
background-color: #999; |
|||
color: white; |
|||
font-size: 18rpx; |
|||
padding: 4rpx 12rpx; |
|||
border-radius: 12rpx; |
|||
border: 2rpx solid white; |
|||
} |
|||
|
|||
.broker-details { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.name-row { |
|||
display: flex; |
|||
align-items: center; |
|||
margin-bottom: 8rpx; |
|||
flex-wrap: wrap; |
|||
} |
|||
|
|||
.name { |
|||
font-size: 32rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
margin-right: 12rpx; |
|||
} |
|||
|
|||
.score-text { |
|||
font-size: 24rpx; |
|||
color: #fff; |
|||
background: linear-gradient(135deg, #ffb800, #ff7700); |
|||
padding: 4rpx 12rpx; |
|||
border-radius: 12rpx; |
|||
margin-right: 8rpx; |
|||
} |
|||
|
|||
.online-status { |
|||
font-size: 22rpx; |
|||
color: #52c41a; |
|||
} |
|||
|
|||
.company { |
|||
font-size: 28rpx; |
|||
color: #666; |
|||
margin-bottom: 6rpx; |
|||
line-height: 1.4; |
|||
} |
|||
|
|||
.department { |
|||
font-size: 26rpx; |
|||
color: #999; |
|||
margin-bottom: 6rpx; |
|||
line-height: 1.4; |
|||
} |
|||
|
|||
.area { |
|||
font-size: 26rpx; |
|||
color: #333; |
|||
margin-bottom: 6rpx; |
|||
line-height: 1.4; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.experience { |
|||
font-size: 24rpx; |
|||
color: #999; |
|||
margin-top: 8rpx; |
|||
} |
|||
|
|||
/* 专业技能标签预览 */ |
|||
.skills-preview { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 10rpx; |
|||
margin-top: 12rpx; |
|||
} |
|||
|
|||
.skill-tag-small { |
|||
background-color: #f0f9ff; |
|||
color: #1890ff; |
|||
font-size: 22rpx; |
|||
padding: 8rpx 16rpx; |
|||
border-radius: 16rpx; |
|||
border: 1rpx solid #bae7ff; |
|||
} |
|||
|
|||
.skill-more { |
|||
background-color: #f5f5f5; |
|||
color: #999; |
|||
font-size: 22rpx; |
|||
padding: 8rpx 16rpx; |
|||
border-radius: 16rpx; |
|||
border: 1rpx solid #e0e0e0; |
|||
} |
|||
|
|||
/* 业绩数据统计样式 */ |
|||
.performance-stats { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
margin-top: 16rpx; |
|||
padding-top: 16rpx; |
|||
border-top: 1rpx solid #f0f0f0; |
|||
} |
|||
|
|||
.stat-item { |
|||
flex: 1; |
|||
text-align: center; |
|||
} |
|||
|
|||
.stat-value { |
|||
font-size: 28rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
display: block; |
|||
} |
|||
|
|||
.stat-value.profit-rate { |
|||
color: #52c41a; |
|||
} |
|||
|
|||
.stat-label { |
|||
font-size: 22rpx; |
|||
color: #999; |
|||
margin-top: 4rpx; |
|||
display: block; |
|||
} |
|||
|
|||
.stat-divider { |
|||
color: #e0e0e0; |
|||
font-size: 24rpx; |
|||
margin: 0 10rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.action-buttons { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
gap: 24rpx; |
|||
padding-top: 20rpx; |
|||
border-top: 1rpx solid #f0f0f0; |
|||
} |
|||
|
|||
.button-chat, .button-call { |
|||
width: 72rpx; |
|||
height: 72rpx; |
|||
border-radius: 50%; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
transition: all 0.2s ease; |
|||
position: relative; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.button-chat { |
|||
background-color: #e6f7f0; |
|||
} |
|||
|
|||
.button-call { |
|||
background-color: #e6f0f7; |
|||
} |
|||
|
|||
.button-chat:active { |
|||
transform: scale(0.95); |
|||
opacity: 0.9; |
|||
background-color: #c3e6cb; |
|||
} |
|||
|
|||
.button-call:active { |
|||
transform: scale(0.95); |
|||
opacity: 0.9; |
|||
background-color: #b3d8ff; |
|||
} |
|||
|
|||
.button-icon { |
|||
font-size: 36rpx; |
|||
} |
|||
|
|||
/* 空状态 */ |
|||
.empty-state { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 100rpx 0; |
|||
background-color: #fff; |
|||
margin: 20rpx 0; |
|||
border-radius: 24rpx; |
|||
} |
|||
|
|||
.empty-state text:first-child { |
|||
font-size: 100rpx; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.empty-text { |
|||
font-size: 28rpx; |
|||
color: #999; |
|||
} |
|||
@ -0,0 +1,326 @@ |
|||
// 消息列表页面
|
|||
Page({ |
|||
data: { |
|||
messageList: [] |
|||
}, |
|||
|
|||
onLoad: function() { |
|||
this.loadChatList(); |
|||
// 注册全局新消息处理函数
|
|||
this.registerGlobalMessageHandler(); |
|||
}, |
|||
|
|||
/** |
|||
* 注册全局新消息处理函数 |
|||
*/ |
|||
registerGlobalMessageHandler: function() { |
|||
try { |
|||
const app = getApp(); |
|||
const that = this; |
|||
|
|||
// 保存原有的处理函数(如果有)
|
|||
this.originalMessageHandler = app.globalData.onNewMessage; |
|||
|
|||
// 注册新的处理函数
|
|||
app.globalData.onNewMessage = function(message) { |
|||
console.log('消息列表页面收到新消息:', message); |
|||
// 重新加载聊天列表
|
|||
that.loadChatList(); |
|||
|
|||
// 调用原始处理函数(如果有)
|
|||
if (that.originalMessageHandler && typeof that.originalMessageHandler === 'function') { |
|||
that.originalMessageHandler(message); |
|||
} |
|||
}; |
|||
|
|||
console.log('已注册全局新消息处理函数'); |
|||
} catch (e) { |
|||
console.error('注册全局新消息处理函数失败:', e); |
|||
} |
|||
}, |
|||
|
|||
onShow: function() { |
|||
// 每次显示页面时刷新聊天列表
|
|||
this.loadChatList(); |
|||
}, |
|||
|
|||
/** |
|||
* 加载聊天列表 |
|||
*/ |
|||
loadChatList: function() { |
|||
try { |
|||
// 获取所有存储的聊天记录键
|
|||
const storageInfo = wx.getStorageInfoSync(); |
|||
const chatKeys = storageInfo.keys.filter(key => key.startsWith('chat_messages_')); |
|||
|
|||
const messageList = []; |
|||
// 获取当前用户信息
|
|||
const app = getApp(); |
|||
const userInfo = app.globalData.userInfo || {}; |
|||
const isCurrentUserManager = userInfo.userType === 'manager' || userInfo.type === 'manager'; |
|||
const currentManagerId = userInfo.managerId || ''; |
|||
const currentUserId = wx.getStorageSync('userId') || ''; |
|||
|
|||
// 临时存储已处理的会话,避免重复
|
|||
const processedConversations = new Set(); |
|||
|
|||
// 遍历每个聊天记录
|
|||
chatKeys.forEach(key => { |
|||
const chatUserId = key.replace('chat_messages_', ''); |
|||
|
|||
// 获取消息列表
|
|||
const messages = wx.getStorageSync(key); |
|||
|
|||
// 跳过空消息列表
|
|||
if (!messages || messages.length === 0) return; |
|||
|
|||
// 对于客服,需要特殊处理,确保能看到所有相关消息
|
|||
if (isCurrentUserManager) { |
|||
// 避免处理自己与自己的聊天
|
|||
if (chatUserId === currentManagerId) return; |
|||
|
|||
// 客服需要看到所有有消息的对话,包括用户发送的消息
|
|||
if (!processedConversations.has(chatUserId)) { |
|||
// 获取最后一条消息
|
|||
const lastMessage = messages[messages.length - 1]; |
|||
|
|||
messageList.push({ |
|||
userId: chatUserId, |
|||
userName: this.getUserNameById(chatUserId), |
|||
avatar: '', |
|||
lastMessage: this.formatMessagePreview(lastMessage), |
|||
lastMessageTime: this.formatMessageTime(lastMessage.time), |
|||
messageCount: messages.length |
|||
}); |
|||
|
|||
processedConversations.add(chatUserId); |
|||
} |
|||
} else { |
|||
// 普通用户,正常处理
|
|||
// 避免处理自己与自己的聊天
|
|||
if (chatUserId === currentUserId) return; |
|||
|
|||
// 获取最后一条消息
|
|||
const lastMessage = messages[messages.length - 1]; |
|||
|
|||
messageList.push({ |
|||
userId: chatUserId, |
|||
userName: this.getUserNameById(chatUserId), |
|||
avatar: '', |
|||
lastMessage: this.formatMessagePreview(lastMessage), |
|||
lastMessageTime: this.formatMessageTime(lastMessage.time), |
|||
messageCount: messages.length |
|||
}); |
|||
} |
|||
}); |
|||
|
|||
// 按最后消息时间排序(最新的在前)
|
|||
messageList.sort((a, b) => { |
|||
return new Date(b.lastMessageTime) - new Date(a.lastMessageTime); |
|||
}); |
|||
|
|||
this.setData({ |
|||
messageList: messageList |
|||
}); |
|||
} catch (e) { |
|||
console.error('加载聊天列表失败:', e); |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 根据用户ID获取用户名 |
|||
*/ |
|||
getUserNameById: function(userId) { |
|||
try { |
|||
// 参数有效性检查
|
|||
if (!userId || typeof userId === 'undefined') { |
|||
return '未知用户'; |
|||
} |
|||
|
|||
// 确保userId是字符串类型
|
|||
const safeUserId = String(userId); |
|||
|
|||
// 尝试从全局客服列表中获取用户名
|
|||
const app = getApp(); |
|||
|
|||
// 尝试从本地缓存的客服列表中查找
|
|||
const cachedCustomerServices = wx.getStorageSync('cached_customer_services') || []; |
|||
const service = cachedCustomerServices.find(item => |
|||
item.id === safeUserId || item.managerId === safeUserId || |
|||
String(item.id) === safeUserId || String(item.managerId) === safeUserId |
|||
); |
|||
|
|||
if (service) { |
|||
return service.alias || service.name || '客服'; |
|||
} |
|||
|
|||
// 固定用户名映射
|
|||
const userMap = { |
|||
'user1': '客服专员', |
|||
'user2': '商家客服', |
|||
'user_1': '张三', |
|||
'user_2': '李四', |
|||
'user_3': '王五', |
|||
'user_4': '赵六', |
|||
'user_5': '钱七', |
|||
'customer_service_1': '客服小王', |
|||
'customer_service_2': '客服小李', |
|||
'customer_service_3': '客服小张' |
|||
}; |
|||
|
|||
if (userMap[safeUserId]) { |
|||
return userMap[safeUserId]; |
|||
} |
|||
|
|||
// 对于manager_开头的ID,显示为客服
|
|||
if (safeUserId.startsWith('manager_') || safeUserId.includes('manager')) { |
|||
return '客服-' + (safeUserId.length >= 4 ? safeUserId.slice(-4) : safeUserId); |
|||
} |
|||
|
|||
// 如果都没有找到,返回默认名称,避免出现undefined
|
|||
if (safeUserId.length >= 4) { |
|||
return '用户' + safeUserId.slice(-4); |
|||
} else { |
|||
return '用户' + safeUserId; |
|||
} |
|||
} catch (e) { |
|||
console.error('获取用户名失败:', e); |
|||
return '未知用户'; |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 格式化消息预览 |
|||
*/ |
|||
formatMessagePreview: function(message) { |
|||
if (message.type === 'system') { |
|||
return '[系统消息] ' + message.content; |
|||
} else { |
|||
const senderPrefix = message.sender === 'me' ? '我: ' : ''; |
|||
// 限制预览长度
|
|||
let preview = senderPrefix + message.content; |
|||
if (preview.length > 30) { |
|||
preview = preview.substring(0, 30) + '...'; |
|||
} |
|||
return preview; |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 格式化消息时间 |
|||
*/ |
|||
formatMessageTime: function(timeStr) { |
|||
if (!timeStr) return ''; |
|||
|
|||
// 假设时间格式为 "9-21 10:50" 或类似格式
|
|||
const now = new Date(); |
|||
let dateStr = timeStr; |
|||
|
|||
// 如果没有包含年份,添加当前年份
|
|||
if (!timeStr.includes('-') || timeStr.split('-').length < 3) { |
|||
const currentYear = now.getFullYear(); |
|||
dateStr = timeStr.includes(' ') ? |
|||
`${currentYear}-${timeStr.split(' ')[0]} ${timeStr.split(' ')[1]}` : |
|||
`${currentYear}-${timeStr}`; |
|||
} |
|||
|
|||
const messageDate = new Date(dateStr); |
|||
const diffTime = Math.abs(now - messageDate); |
|||
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); |
|||
|
|||
if (diffDays === 0) { |
|||
// 今天的消息只显示时间
|
|||
return timeStr.split(' ')[1] || timeStr; |
|||
} else if (diffDays === 1) { |
|||
// 昨天的消息
|
|||
return '昨天'; |
|||
} else if (diffDays < 7) { |
|||
// 一周内的消息显示星期
|
|||
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']; |
|||
return weekdays[messageDate.getDay()]; |
|||
} else { |
|||
// 超过一周的消息显示日期
|
|||
return timeStr.split(' ')[0] || timeStr; |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 跳转到聊天详情页 |
|||
*/ |
|||
goToChat: function(e) { |
|||
const userId = e.currentTarget.dataset.userId; |
|||
// 查找对应的用户信息
|
|||
const userInfo = this.data.messageList.find(item => item.userId === userId); |
|||
|
|||
wx.navigateTo({ |
|||
url: `/pages/chat-detail/index?userId=${userId}&userName=${encodeURIComponent(userInfo?.userName || '')}` |
|||
}); |
|||
}, |
|||
|
|||
/** |
|||
* 页面相关事件处理函数--监听用户下拉动作 |
|||
*/ |
|||
onPullDownRefresh: function() { |
|||
this.loadChatList(); |
|||
// 停止下拉刷新
|
|||
wx.stopPullDownRefresh(); |
|||
}, |
|||
|
|||
/** |
|||
* 处理清除聊天记录 |
|||
*/ |
|||
onUnload: function() { |
|||
// 页面卸载时的清理工作
|
|||
// 恢复原始的全局消息处理函数
|
|||
try { |
|||
const app = getApp(); |
|||
if (this.originalMessageHandler) { |
|||
app.globalData.onNewMessage = this.originalMessageHandler; |
|||
} else { |
|||
// 如果没有原始处理函数,则清除当前的
|
|||
delete app.globalData.onNewMessage; |
|||
} |
|||
console.log('已清理全局新消息处理函数'); |
|||
} catch (e) { |
|||
console.error('清理全局新消息处理函数失败:', e); |
|||
} |
|||
}, |
|||
|
|||
handleClearChat: function(e) { |
|||
const userId = e.currentTarget.dataset.userId; |
|||
|
|||
wx.showModal({ |
|||
title: '确认清空', |
|||
content: '确定要清空与该用户的所有聊天记录吗?此操作不可恢复。', |
|||
success: (res) => { |
|||
if (res.confirm) { |
|||
this.clearChatHistory(userId); |
|||
} |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
/** |
|||
* 清空指定用户的聊天记录 |
|||
*/ |
|||
clearChatHistory: function(userId) { |
|||
try { |
|||
wx.removeStorageSync(`chat_messages_${userId}`); |
|||
console.log('已清空用户', userId, '的聊天记录'); |
|||
|
|||
// 刷新消息列表
|
|||
this.loadChatList(); |
|||
|
|||
wx.showToast({ |
|||
title: '聊天记录已清空', |
|||
icon: 'success' |
|||
}); |
|||
} catch (e) { |
|||
console.error('清空聊天记录失败:', e); |
|||
wx.showToast({ |
|||
title: '清空失败', |
|||
icon: 'none' |
|||
}); |
|||
} |
|||
} |
|||
}); |
|||
@ -0,0 +1,4 @@ |
|||
{ |
|||
"navigationBarTitleText": "消息列表", |
|||
"usingComponents": {} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
<view class="message-list-container"> |
|||
<view class="page-header"> |
|||
<text class="page-title">消息列表</text> |
|||
</view> |
|||
|
|||
<view class="message-list"> |
|||
<!-- 消息列表项 --> |
|||
<block wx:for="{{messageList}}" wx:key="userId"> |
|||
<view class="message-item" bindtap="goToChat" data-user-id="{{item.userId}}"> |
|||
<view class="avatar"> |
|||
<image src="{{item.avatar || '/images/logo.svg'}}" mode="aspectFit"></image> |
|||
</view> |
|||
<view class="message-content"> |
|||
<view class="message-header"> |
|||
<text class="user-name">{{item.userName}}</text> |
|||
<text class="message-time">{{item.lastMessageTime}}</text> |
|||
</view> |
|||
<view class="message-preview">{{item.lastMessage || '暂无消息'}}</view> |
|||
</view> |
|||
<view class="message-actions"> |
|||
<button size="mini" type="warn" bindtap="handleClearChat" data-user-id="{{item.userId}}" catchtap="true">清空</button> |
|||
</view> |
|||
</view> |
|||
</block> |
|||
|
|||
<view wx:if="{{messageList.length === 0}}" class="empty-state"> |
|||
<text>暂无聊天记录</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
@ -0,0 +1,101 @@ |
|||
.message-list-container { |
|||
height: 100vh; |
|||
background-color: #f5f5f5; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.page-header { |
|||
padding: 20rpx 30rpx; |
|||
background-color: #fff; |
|||
border-bottom: 1rpx solid #eee; |
|||
} |
|||
|
|||
.page-title { |
|||
font-size: 36rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
} |
|||
|
|||
.message-list { |
|||
flex: 1; |
|||
overflow-y: auto; |
|||
} |
|||
|
|||
.message-item { |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 20rpx 30rpx; |
|||
background-color: #fff; |
|||
border-bottom: 1rpx solid #f0f0f0; |
|||
transition: background-color 0.2s; |
|||
} |
|||
|
|||
.message-item:active { |
|||
background-color: #f8f8f8; |
|||
} |
|||
|
|||
.avatar { |
|||
width: 100rpx; |
|||
height: 100rpx; |
|||
border-radius: 50%; |
|||
overflow: hidden; |
|||
margin-right: 20rpx; |
|||
background-color: #f0f0f0; |
|||
} |
|||
|
|||
.avatar image { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
|
|||
.message-content { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.message-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 8rpx; |
|||
} |
|||
|
|||
.user-name { |
|||
font-size: 32rpx; |
|||
font-weight: 500; |
|||
color: #333; |
|||
} |
|||
|
|||
.message-time { |
|||
font-size: 24rpx; |
|||
color: #999; |
|||
} |
|||
|
|||
.message-preview { |
|||
font-size: 28rpx; |
|||
color: #666; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.message-actions { |
|||
margin-left: 20rpx; |
|||
} |
|||
|
|||
.message-actions button { |
|||
font-size: 24rpx; |
|||
padding: 0 20rpx; |
|||
min-width: 80rpx; |
|||
line-height: 50rpx; |
|||
} |
|||
|
|||
.empty-state { |
|||
padding: 100rpx 0; |
|||
text-align: center; |
|||
color: #999; |
|||
font-size: 28rpx; |
|||
} |
|||
@ -0,0 +1,382 @@ |
|||
// 客服功能综合测试页面
|
|||
|
|||
const socketManager = require('../../utils/websocket'); |
|||
const API = require('../../utils/api'); |
|||
|
|||
Page({ |
|||
data: { |
|||
// 测试状态
|
|||
testResults: [], |
|||
currentTest: '', |
|||
isTesting: false, |
|||
|
|||
// 用户信息
|
|||
userInfo: null, |
|||
userType: 'unknown', |
|||
isAuthenticated: false, |
|||
|
|||
// WebSocket状态
|
|||
wsConnected: false, |
|||
wsAuthenticated: false, |
|||
|
|||
// 测试消息
|
|||
testMessage: '测试消息', |
|||
receivedMessage: null, |
|||
|
|||
// 测试模式
|
|||
testMode: 'customer', // customer 或 customer_service
|
|||
|
|||
// 模拟客服ID
|
|||
mockServiceId: 'test_service_001' |
|||
}, |
|||
|
|||
onLoad: function() { |
|||
console.log('测试页面加载'); |
|||
this.setData({ |
|||
userInfo: getApp().globalData.userInfo, |
|||
userType: getApp().globalData.userType |
|||
}); |
|||
|
|||
// 设置WebSocket事件监听
|
|||
this.setupWebSocketListeners(); |
|||
}, |
|||
|
|||
// 设置WebSocket事件监听
|
|||
setupWebSocketListeners: function() { |
|||
// 连接状态监听
|
|||
socketManager.on('status', (status) => { |
|||
console.log('WebSocket状态:', status); |
|||
this.setData({ |
|||
wsConnected: status.type === 'connected' |
|||
}); |
|||
this.addTestResult(`WebSocket状态: ${status.type} - ${status.message || ''}`); |
|||
}); |
|||
|
|||
// 认证状态监听
|
|||
socketManager.on('authenticated', (data) => { |
|||
console.log('WebSocket认证成功:', data); |
|||
this.setData({ |
|||
wsAuthenticated: true, |
|||
isAuthenticated: true |
|||
}); |
|||
this.addTestResult(`WebSocket认证成功,用户类型: ${data.userType || 'unknown'}`); |
|||
}); |
|||
|
|||
// 消息接收监听
|
|||
socketManager.on('message', (message) => { |
|||
console.log('收到WebSocket消息:', message); |
|||
this.setData({ |
|||
receivedMessage: message |
|||
}); |
|||
this.addTestResult(`收到消息: ${JSON.stringify(message).substring(0, 100)}...`); |
|||
}); |
|||
|
|||
// 错误监听
|
|||
socketManager.on('error', (error) => { |
|||
console.error('WebSocket错误:', error); |
|||
this.addTestResult(`错误: ${error.message || JSON.stringify(error)}`, true); |
|||
}); |
|||
}, |
|||
|
|||
// 开始综合测试
|
|||
startTest: function() { |
|||
if (this.data.isTesting) { |
|||
wx.showToast({ title: '测试正在进行中', icon: 'none' }); |
|||
return; |
|||
} |
|||
|
|||
this.setData({ |
|||
isTesting: true, |
|||
testResults: [], |
|||
currentTest: '' |
|||
}); |
|||
|
|||
this.addTestResult('===== 开始综合测试 ====='); |
|||
|
|||
// 按顺序执行测试用例
|
|||
setTimeout(() => { |
|||
this.testUserTypeDetection(); |
|||
}, 500); |
|||
}, |
|||
|
|||
// 测试1: 用户类型检测
|
|||
testUserTypeDetection: function() { |
|||
this.setData({ currentTest: '用户类型检测' }); |
|||
this.addTestResult('测试1: 验证用户类型检测功能'); |
|||
|
|||
const app = getApp(); |
|||
const userType = app.globalData.userType || 'unknown'; |
|||
const storedType = wx.getStorageSync('userType') || 'unknown'; |
|||
|
|||
this.addTestResult(`全局用户类型: ${userType}`); |
|||
this.addTestResult(`本地存储用户类型: ${storedType}`); |
|||
|
|||
if (userType === storedType && userType !== 'unknown') { |
|||
this.addTestResult('✓ 用户类型检测通过'); |
|||
} else { |
|||
this.addTestResult('✗ 用户类型检测失败', true); |
|||
} |
|||
|
|||
setTimeout(() => { |
|||
this.testWebSocketConnection(); |
|||
}, 1000); |
|||
}, |
|||
|
|||
// 测试2: WebSocket连接和认证
|
|||
testWebSocketConnection: function() { |
|||
this.setData({ currentTest: 'WebSocket连接' }); |
|||
this.addTestResult('测试2: 验证WebSocket连接和认证功能'); |
|||
|
|||
// 检查是否已连接
|
|||
if (socketManager.getConnectionStatus()) { |
|||
this.addTestResult('✓ WebSocket已连接'); |
|||
setTimeout(() => { |
|||
this.testAuthentication(); |
|||
}, 500); |
|||
} else { |
|||
this.addTestResult('尝试建立WebSocket连接...'); |
|||
// 构建连接URL
|
|||
const app = getApp(); |
|||
const userType = app.globalData.userType || 'customer'; |
|||
const userId = wx.getStorageSync('userId') || `test_${Date.now()}`; |
|||
|
|||
const wsUrl = `ws://localhost:3003?userId=${userId}&userType=${userType}`; |
|||
this.addTestResult(`连接URL: ${wsUrl}`); |
|||
|
|||
socketManager.connect(wsUrl); |
|||
|
|||
// 等待连接建立
|
|||
setTimeout(() => { |
|||
if (this.data.wsConnected) { |
|||
this.addTestResult('✓ WebSocket连接成功'); |
|||
this.testAuthentication(); |
|||
} else { |
|||
this.addTestResult('✗ WebSocket连接失败', true); |
|||
this.completeTest(); |
|||
} |
|||
}, 3000); |
|||
} |
|||
}, |
|||
|
|||
// 测试3: 认证功能
|
|||
testAuthentication: function() { |
|||
this.setData({ currentTest: '认证功能' }); |
|||
this.addTestResult('测试3: 验证WebSocket认证功能'); |
|||
|
|||
if (this.data.wsAuthenticated) { |
|||
this.addTestResult('✓ WebSocket认证成功'); |
|||
setTimeout(() => { |
|||
this.testMessageSending(); |
|||
}, 500); |
|||
} else { |
|||
this.addTestResult('尝试手动认证...'); |
|||
const app = getApp(); |
|||
const userType = app.globalData.userType || this.data.testMode; |
|||
const userId = wx.getStorageSync('userId') || `test_${Date.now()}`; |
|||
|
|||
socketManager.authenticate(userType, userId); |
|||
|
|||
// 等待认证结果
|
|||
setTimeout(() => { |
|||
if (this.data.wsAuthenticated) { |
|||
this.addTestResult('✓ 手动认证成功'); |
|||
this.testMessageSending(); |
|||
} else { |
|||
this.addTestResult('✗ 认证失败', true); |
|||
this.completeTest(); |
|||
} |
|||
}, 2000); |
|||
} |
|||
}, |
|||
|
|||
// 测试4: 消息发送功能
|
|||
testMessageSending: function() { |
|||
this.setData({ currentTest: '消息发送' }); |
|||
this.addTestResult('测试4: 验证消息发送功能'); |
|||
|
|||
const app = getApp(); |
|||
const userType = app.globalData.userType || this.data.testMode; |
|||
const targetId = userType === 'customer_service' ? |
|||
`test_customer_${Date.now()}` : |
|||
this.data.mockServiceId; |
|||
|
|||
this.addTestResult(`当前用户类型: ${userType}, 目标ID: ${targetId}`); |
|||
|
|||
const testMessage = { |
|||
type: 'chat_message', |
|||
direction: userType === 'customer_service' ? 'service_to_customer' : 'customer_to_service', |
|||
data: { |
|||
receiverId: targetId, |
|||
senderId: wx.getStorageSync('userId') || `test_${Date.now()}`, |
|||
senderType: userType, |
|||
content: this.data.testMessage || '测试消息_' + Date.now(), |
|||
contentType: 1, |
|||
timestamp: Date.now() |
|||
} |
|||
}; |
|||
|
|||
const sent = socketManager.send(testMessage); |
|||
if (sent) { |
|||
this.addTestResult('✓ 消息发送成功'); |
|||
// 等待接收消息(如果有回复)
|
|||
setTimeout(() => { |
|||
this.testBidirectionalCommunication(); |
|||
}, 3000); |
|||
} else { |
|||
this.addTestResult('✗ 消息发送失败', true); |
|||
this.completeTest(); |
|||
} |
|||
}, |
|||
|
|||
// 测试5: 双向通信功能
|
|||
testBidirectionalCommunication: function() { |
|||
this.setData({ currentTest: '双向通信' }); |
|||
this.addTestResult('测试5: 验证双向通信功能'); |
|||
|
|||
if (this.data.receivedMessage) { |
|||
this.addTestResult('✓ 收到响应消息'); |
|||
this.addTestResult(`消息内容: ${JSON.stringify(this.data.receivedMessage).substring(0, 150)}...`); |
|||
} else { |
|||
this.addTestResult('⚠ 未收到响应消息(可能是正常的,取决于服务器配置)'); |
|||
} |
|||
|
|||
setTimeout(() => { |
|||
this.testUserTypeSwitching(); |
|||
}, 1000); |
|||
}, |
|||
|
|||
// 测试6: 用户类型切换
|
|||
testUserTypeSwitching: function() { |
|||
this.setData({ currentTest: '用户类型切换' }); |
|||
this.addTestResult('测试6: 验证用户类型切换功能'); |
|||
|
|||
try { |
|||
// 模拟切换用户类型
|
|||
const app = getApp(); |
|||
const originalType = app.globalData.userType; |
|||
const newType = originalType === 'customer' ? 'customer_service' : 'customer'; |
|||
|
|||
app.globalData.userType = newType; |
|||
wx.setStorageSync('userType', newType); |
|||
|
|||
this.addTestResult(`✓ 用户类型切换成功: ${originalType} → ${newType}`); |
|||
this.addTestResult(`新的全局用户类型: ${app.globalData.userType}`); |
|||
this.addTestResult(`新的存储用户类型: ${wx.getStorageSync('userType')}`); |
|||
|
|||
// 恢复原始类型
|
|||
setTimeout(() => { |
|||
app.globalData.userType = originalType; |
|||
wx.setStorageSync('userType', originalType); |
|||
this.addTestResult(`恢复原始用户类型: ${originalType}`); |
|||
this.completeTest(); |
|||
}, 1000); |
|||
} catch (error) { |
|||
this.addTestResult(`✗ 用户类型切换失败: ${error.message}`, true); |
|||
this.completeTest(); |
|||
} |
|||
}, |
|||
|
|||
// 完成测试
|
|||
completeTest: function() { |
|||
this.addTestResult('===== 测试完成 ====='); |
|||
|
|||
// 统计测试结果
|
|||
const results = this.data.testResults; |
|||
const successCount = results.filter(r => r.includes('✓')).length; |
|||
const failCount = results.filter(r => r.includes('✗')).length; |
|||
const warnCount = results.filter(r => r.includes('⚠')).length; |
|||
|
|||
this.addTestResult(`测试统计: 成功=${successCount}, 失败=${failCount}, 警告=${warnCount}`); |
|||
|
|||
if (failCount === 0) { |
|||
this.addTestResult('🎉 所有测试通过!'); |
|||
} else { |
|||
this.addTestResult('❌ 测试中有失败项,请检查', true); |
|||
} |
|||
|
|||
this.setData({ |
|||
isTesting: false, |
|||
currentTest: '测试完成' |
|||
}); |
|||
}, |
|||
|
|||
// 添加测试结果
|
|||
addTestResult: function(message, isError = false) { |
|||
const timestamp = new Date().toLocaleTimeString(); |
|||
const resultItem = { |
|||
id: Date.now(), |
|||
time: timestamp, |
|||
message: message, |
|||
isError: isError |
|||
}; |
|||
|
|||
this.setData({ |
|||
testResults: [...this.data.testResults, resultItem] |
|||
}); |
|||
|
|||
console.log(`[${timestamp}] ${message}`); |
|||
}, |
|||
|
|||
// 切换测试模式
|
|||
switchTestMode: function() { |
|||
const newMode = this.data.testMode === 'customer' ? 'customer_service' : 'customer'; |
|||
this.setData({ testMode: newMode }); |
|||
wx.showToast({ title: `已切换到${newMode === 'customer' ? '客户' : '客服'}模式` }); |
|||
}, |
|||
|
|||
// 输入测试消息
|
|||
onInputChange: function(e) { |
|||
this.setData({ testMessage: e.detail.value }); |
|||
}, |
|||
|
|||
// 清理WebSocket连接
|
|||
cleanup: function() { |
|||
try { |
|||
socketManager.close(); |
|||
this.addTestResult('WebSocket连接已关闭'); |
|||
} catch (error) { |
|||
this.addTestResult(`关闭连接失败: ${error.message}`, true); |
|||
} |
|||
}, |
|||
|
|||
// 手动发送消息
|
|||
sendTestMessage: function() { |
|||
if (!this.data.testMessage.trim()) { |
|||
wx.showToast({ title: '请输入测试消息', icon: 'none' }); |
|||
return; |
|||
} |
|||
|
|||
const app = getApp(); |
|||
const userType = app.globalData.userType || this.data.testMode; |
|||
const targetId = userType === 'customer_service' ? |
|||
`test_customer_${Date.now()}` : |
|||
this.data.mockServiceId; |
|||
|
|||
const message = { |
|||
type: 'chat_message', |
|||
direction: userType === 'customer_service' ? 'service_to_customer' : 'customer_to_service', |
|||
data: { |
|||
receiverId: targetId, |
|||
senderId: wx.getStorageSync('userId') || `test_${Date.now()}`, |
|||
senderType: userType, |
|||
content: this.data.testMessage.trim(), |
|||
contentType: 1, |
|||
timestamp: Date.now() |
|||
} |
|||
}; |
|||
|
|||
const sent = socketManager.send(message); |
|||
if (sent) { |
|||
wx.showToast({ title: '消息已发送' }); |
|||
this.addTestResult(`手动发送消息: ${this.data.testMessage}`); |
|||
} else { |
|||
wx.showToast({ title: '发送失败', icon: 'none' }); |
|||
this.addTestResult(`手动发送失败`, true); |
|||
} |
|||
}, |
|||
|
|||
onUnload: function() { |
|||
// 清理监听器和连接
|
|||
this.cleanup(); |
|||
} |
|||
}); |
|||
@ -0,0 +1,97 @@ |
|||
<view class="test-container"> |
|||
<view class="header"> |
|||
<text class="title">客服功能综合测试</text> |
|||
<text class="subtitle">验证客服认证、身份判断和双向沟通功能</text> |
|||
</view> |
|||
|
|||
<view class="test-status"> |
|||
<view class="status-item {{isTesting ? 'testing' : ''}}"> |
|||
<text class="status-label">当前测试:</text> |
|||
<text class="status-value">{{currentTest || '未开始'}}</text> |
|||
</view> |
|||
<view class="status-item"> |
|||
<text class="status-label">用户类型:</text> |
|||
<text class="status-value">{{userType}}</text> |
|||
</view> |
|||
<view class="status-item {{wsConnected ? 'connected' : ''}}"> |
|||
<text class="status-label">WebSocket:</text> |
|||
<text class="status-value">{{wsConnected ? '已连接' : '未连接'}}</text> |
|||
</view> |
|||
<view class="status-item {{wsAuthenticated ? 'authenticated' : ''}}"> |
|||
<text class="status-label">认证状态:</text> |
|||
<text class="status-value">{{wsAuthenticated ? '已认证' : '未认证'}}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="test-controls"> |
|||
<view class="control-item"> |
|||
<text class="control-label">测试模式:</text> |
|||
<view class="control-buttons"> |
|||
<button type="{{testMode === 'customer' ? 'primary' : 'default'}}" bind:tap="switchTestMode"> |
|||
{{testMode === 'customer' ? '客户模式 ✓' : '客户模式'}} |
|||
</button> |
|||
<button type="{{testMode === 'customer_service' ? 'primary' : 'default'}}" bind:tap="switchTestMode"> |
|||
{{testMode === 'customer_service' ? '客服模式 ✓' : '客服模式'}} |
|||
</button> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="control-item"> |
|||
<text class="control-label">测试消息:</text> |
|||
<input |
|||
class="message-input" |
|||
placeholder="请输入测试消息" |
|||
value="{{testMessage}}" |
|||
bindinput="onInputChange" |
|||
/> |
|||
<button type="primary" bind:tap="sendTestMessage">发送测试消息</button> |
|||
</view> |
|||
|
|||
<view class="action-buttons"> |
|||
<button |
|||
class="start-button" |
|||
type="primary" |
|||
size="mini" |
|||
bind:tap="startTest" |
|||
disabled="{{isTesting}}" |
|||
> |
|||
{{isTesting ? '测试进行中...' : '开始综合测试'}} |
|||
</button> |
|||
<button |
|||
class="cleanup-button" |
|||
type="warn" |
|||
size="mini" |
|||
bind:tap="cleanup" |
|||
> |
|||
清理连接 |
|||
</button> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="test-results"> |
|||
<text class="results-title">测试结果:</text> |
|||
<scroll-view class="results-list" scroll-y> |
|||
<view |
|||
wx:for="{{testResults}}" |
|||
wx:key="id" |
|||
class="result-item {{item.isError ? 'error' : ''}}" |
|||
> |
|||
<text class="result-time">{{item.time}}</text> |
|||
<text class="result-message">{{item.message}}</text> |
|||
</view> |
|||
<view class="empty-result" wx:if="{{testResults.length === 0}}"> |
|||
暂无测试结果,点击开始综合测试 |
|||
</view> |
|||
</scroll-view> |
|||
</view> |
|||
|
|||
<view class="test-tips"> |
|||
<text class="tips-title">测试提示:</text> |
|||
<view class="tips-content"> |
|||
<text>1. 测试前请确保已完成登录</text> |
|||
<text>2. WebSocket服务需要正常运行</text> |
|||
<text>3. 测试将验证用户类型检测、WebSocket连接、认证和消息发送功能</text> |
|||
<text>4. 双向通信测试依赖于服务器配置,可能不会收到响应消息</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
@ -0,0 +1,249 @@ |
|||
.test-container { |
|||
padding: 20rpx; |
|||
background-color: #f8f8f8; |
|||
min-height: 100vh; |
|||
} |
|||
|
|||
.header { |
|||
text-align: center; |
|||
margin-bottom: 30rpx; |
|||
padding: 20rpx 0; |
|||
background-color: #fff; |
|||
border-radius: 12rpx; |
|||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
.title { |
|||
font-size: 36rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
display: block; |
|||
margin-bottom: 10rpx; |
|||
} |
|||
|
|||
.subtitle { |
|||
font-size: 24rpx; |
|||
color: #666; |
|||
display: block; |
|||
} |
|||
|
|||
.test-status { |
|||
background-color: #fff; |
|||
border-radius: 12rpx; |
|||
padding: 20rpx; |
|||
margin-bottom: 30rpx; |
|||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
.status-item { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
padding: 15rpx 0; |
|||
border-bottom: 1rpx solid #f0f0f0; |
|||
} |
|||
|
|||
.status-item:last-child { |
|||
border-bottom: none; |
|||
} |
|||
|
|||
.status-label { |
|||
font-size: 28rpx; |
|||
color: #666; |
|||
} |
|||
|
|||
.status-value { |
|||
font-size: 28rpx; |
|||
color: #333; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.status-item.testing .status-value { |
|||
color: #07c160; |
|||
animation: pulse 1s infinite; |
|||
} |
|||
|
|||
.status-item.connected .status-value { |
|||
color: #07c160; |
|||
} |
|||
|
|||
.status-item.authenticated .status-value { |
|||
color: #1989fa; |
|||
} |
|||
|
|||
@keyframes pulse { |
|||
0% { |
|||
opacity: 1; |
|||
} |
|||
50% { |
|||
opacity: 0.6; |
|||
} |
|||
100% { |
|||
opacity: 1; |
|||
} |
|||
} |
|||
|
|||
.test-controls { |
|||
background-color: #fff; |
|||
border-radius: 12rpx; |
|||
padding: 20rpx; |
|||
margin-bottom: 30rpx; |
|||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
.control-item { |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.control-item:last-child { |
|||
margin-bottom: 0; |
|||
} |
|||
|
|||
.control-label { |
|||
font-size: 28rpx; |
|||
color: #333; |
|||
display: block; |
|||
margin-bottom: 15rpx; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.control-buttons { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
gap: 20rpx; |
|||
} |
|||
|
|||
.control-buttons button { |
|||
flex: 1; |
|||
font-size: 26rpx; |
|||
} |
|||
|
|||
.message-input { |
|||
border: 1rpx solid #ddd; |
|||
border-radius: 8rpx; |
|||
padding: 20rpx; |
|||
font-size: 28rpx; |
|||
margin-bottom: 15rpx; |
|||
background-color: #f9f9f9; |
|||
} |
|||
|
|||
.action-buttons { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
margin-top: 20rpx; |
|||
} |
|||
|
|||
.action-buttons button { |
|||
flex: 1; |
|||
margin: 0 10rpx; |
|||
} |
|||
|
|||
.start-button { |
|||
background-color: #07c160; |
|||
} |
|||
|
|||
.cleanup-button { |
|||
background-color: #ee0a24; |
|||
} |
|||
|
|||
.test-results { |
|||
background-color: #fff; |
|||
border-radius: 12rpx; |
|||
padding: 20rpx; |
|||
margin-bottom: 30rpx; |
|||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
.results-title { |
|||
font-size: 28rpx; |
|||
color: #333; |
|||
font-weight: 500; |
|||
display: block; |
|||
margin-bottom: 15rpx; |
|||
} |
|||
|
|||
.results-list { |
|||
height: 400rpx; |
|||
background-color: #f9f9f9; |
|||
border-radius: 8rpx; |
|||
padding: 10rpx; |
|||
} |
|||
|
|||
.result-item { |
|||
padding: 15rpx; |
|||
margin-bottom: 10rpx; |
|||
background-color: #fff; |
|||
border-radius: 6rpx; |
|||
border-left: 4rpx solid #1989fa; |
|||
font-size: 24rpx; |
|||
} |
|||
|
|||
.result-item.error { |
|||
border-left-color: #ee0a24; |
|||
background-color: #fff1f0; |
|||
} |
|||
|
|||
.result-time { |
|||
color: #999; |
|||
font-size: 20rpx; |
|||
display: block; |
|||
margin-bottom: 5rpx; |
|||
} |
|||
|
|||
.result-message { |
|||
color: #333; |
|||
word-break: break-all; |
|||
line-height: 1.5; |
|||
} |
|||
|
|||
.empty-result { |
|||
text-align: center; |
|||
color: #999; |
|||
padding: 60rpx 0; |
|||
font-size: 26rpx; |
|||
} |
|||
|
|||
.test-tips { |
|||
background-color: #fff; |
|||
border-radius: 12rpx; |
|||
padding: 20rpx; |
|||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
.tips-title { |
|||
font-size: 28rpx; |
|||
color: #333; |
|||
font-weight: 500; |
|||
display: block; |
|||
margin-bottom: 15rpx; |
|||
} |
|||
|
|||
.tips-content { |
|||
background-color: #f0f9ff; |
|||
border-radius: 8rpx; |
|||
padding: 20rpx; |
|||
} |
|||
|
|||
.tips-content text { |
|||
display: block; |
|||
margin-bottom: 10rpx; |
|||
font-size: 24rpx; |
|||
color: #666; |
|||
line-height: 1.5; |
|||
} |
|||
|
|||
.tips-content text:last-child { |
|||
margin-bottom: 0; |
|||
} |
|||
|
|||
/* 适配不同屏幕尺寸 */ |
|||
@media screen and (min-width: 768px) { |
|||
.test-container { |
|||
max-width: 900rpx; |
|||
margin: 0 auto; |
|||
padding: 30rpx; |
|||
} |
|||
|
|||
.results-list { |
|||
height: 600rpx; |
|||
} |
|||
} |
|||
@ -0,0 +1,125 @@ |
|||
// 查询userlogin数据库中的personnel表,获取所有采购员数据
|
|||
require('dotenv').config({ path: 'd:\\xt\\mian_ly\\server-example\\.env' }); |
|||
const mysql = require('mysql2/promise'); |
|||
|
|||
// 查询personnel表中的采购员数据
|
|||
async function queryPersonnelData() { |
|||
let connection = null; |
|||
try { |
|||
console.log('连接到userlogin数据库...'); |
|||
connection = await mysql.createConnection({ |
|||
host: '1.95.162.61', // 直接使用正确的主机地址
|
|||
user: process.env.DB_USER || 'root', |
|||
password: process.env.DB_PASSWORD || 'schl@2025', // 直接使用默认密码
|
|||
database: 'userlogin', |
|||
port: process.env.DB_PORT || 3306, |
|||
timezone: '+08:00' // 设置时区为UTC+8
|
|||
}); |
|||
console.log('数据库连接成功\n'); |
|||
|
|||
// 首先查询personnel表结构
|
|||
console.log('=== personnel表结构 ==='); |
|||
const [columns] = await connection.execute( |
|||
'SHOW COLUMNS FROM personnel' |
|||
); |
|||
console.log('字段列表:', columns.map(col => col.Field).join(', ')); |
|||
console.log(); |
|||
|
|||
// 查询projectName为采购员的数据
|
|||
console.log('=== 所有采购员数据 ==='); |
|||
const [personnelData] = await connection.execute( |
|||
'SELECT * FROM personnel WHERE projectName = ?', |
|||
['采购员'] |
|||
); |
|||
|
|||
console.log(`找到 ${personnelData.length} 名采购员记录`); |
|||
console.log('\n采购员数据详情:'); |
|||
|
|||
// 格式化输出数据
|
|||
personnelData.forEach((person, index) => { |
|||
console.log(`\n${index + 1}. 采购员信息:`); |
|||
console.log(` - ID: ${person.id || 'N/A'}`); |
|||
console.log(` - 用户ID: ${person.userId || 'N/A'}`); |
|||
console.log(` - 姓名: ${person.name || 'N/A'}`); |
|||
console.log(` - 别名: ${person.alias || 'N/A'}`); |
|||
console.log(` - 电话号码: ${person.phoneNumber || 'N/A'}`); |
|||
console.log(` - 公司: ${person.managercompany || 'N/A'}`); |
|||
console.log(` - 部门: ${person.managerdepartment || 'N/A'}`); |
|||
console.log(` - 角色: ${person.role || 'N/A'}`); |
|||
}); |
|||
|
|||
// 输出JSON格式,便于复制使用
|
|||
console.log('\n=== JSON格式数据 (用于客服页面) ==='); |
|||
const customerServiceData = personnelData.map((person, index) => ({ |
|||
id: index + 1, |
|||
managerId: person.userId || `PM${String(index + 1).padStart(3, '0')}`, |
|||
managercompany: person.managercompany || '未知公司', |
|||
managerdepartment: person.managerdepartment || '采购部', |
|||
organization: person.organization || '采购组', |
|||
projectName: person.role || '采购员', |
|||
name: person.name || '未知', |
|||
alias: person.alias || person.name || '未知', |
|||
phoneNumber: person.phoneNumber || '', |
|||
avatarUrl: '', |
|||
score: 990 + (index % 10), |
|||
isOnline: index % 4 !== 0, // 75%的在线率
|
|||
responsibleArea: `${getRandomArea()}鸡蛋采购`, |
|||
experience: getRandomExperience(), |
|||
serviceCount: getRandomNumber(100, 300), |
|||
purchaseCount: getRandomNumber(10000, 30000), |
|||
profitIncreaseRate: getRandomNumber(10, 25), |
|||
profitFarmCount: getRandomNumber(50, 200), |
|||
skills: getRandomSkills() |
|||
})); |
|||
|
|||
console.log(JSON.stringify(customerServiceData, null, 2)); |
|||
|
|||
return customerServiceData; |
|||
|
|||
} catch (error) { |
|||
console.error('查询过程中发生错误:', error); |
|||
console.error('错误详情:', error.stack); |
|||
return null; |
|||
} finally { |
|||
if (connection) { |
|||
await connection.end(); |
|||
console.log('\n数据库连接已关闭'); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 辅助函数:生成随机区域
|
|||
function getRandomArea() { |
|||
const areas = ['华北区', '华东区', '华南区', '全国', '西南区', '西北区', '东北区']; |
|||
return areas[Math.floor(Math.random() * areas.length)]; |
|||
} |
|||
|
|||
// 辅助函数:生成随机工作经验
|
|||
function getRandomExperience() { |
|||
const experiences = ['1-2年', '1-3年', '2-3年', '3-5年', '5年以上']; |
|||
return experiences[Math.floor(Math.random() * experiences.length)]; |
|||
} |
|||
|
|||
// 辅助函数:生成随机数字
|
|||
function getRandomNumber(min, max) { |
|||
return Math.floor(Math.random() * (max - min + 1)) + min; |
|||
} |
|||
|
|||
// 辅助函数:生成随机技能
|
|||
function getRandomSkills() { |
|||
const allSkills = ['渠道拓展', '供应商维护', '质量把控', '精准把控市场价格', '谈判技巧', '库存管理']; |
|||
const skillCount = Math.floor(Math.random() * 3) + 2; // 2-4个技能
|
|||
const selectedSkills = []; |
|||
|
|||
while (selectedSkills.length < skillCount) { |
|||
const skill = allSkills[Math.floor(Math.random() * allSkills.length)]; |
|||
if (!selectedSkills.includes(skill)) { |
|||
selectedSkills.push(skill); |
|||
} |
|||
} |
|||
|
|||
return selectedSkills; |
|||
} |
|||
|
|||
// 运行查询
|
|||
queryPersonnelData(); |
|||
@ -0,0 +1,79 @@ |
|||
// 测试修复后的/api/managers接口
|
|||
const http = require('http'); |
|||
|
|||
console.log('开始测试/api/managers接口...'); |
|||
|
|||
const options = { |
|||
hostname: 'localhost', |
|||
port: 3003, |
|||
path: '/api/managers', |
|||
method: 'GET', |
|||
headers: { |
|||
'Content-Type': 'application/json' |
|||
} |
|||
}; |
|||
|
|||
const req = http.request(options, (res) => { |
|||
let data = ''; |
|||
|
|||
res.on('data', (chunk) => { |
|||
data += chunk; |
|||
}); |
|||
|
|||
res.on('end', () => { |
|||
console.log(`状态码: ${res.statusCode}`); |
|||
|
|||
try { |
|||
const responseData = JSON.parse(data); |
|||
console.log('响应数据:', JSON.stringify(responseData, null, 2)); |
|||
|
|||
// 验证响应格式
|
|||
if (res.statusCode === 200) { |
|||
if (responseData.success === true || responseData.code === 200) { |
|||
console.log('✅ API接口返回成功状态'); |
|||
|
|||
// 检查数据字段
|
|||
const dataArray = responseData.data || responseData; |
|||
if (Array.isArray(dataArray)) { |
|||
console.log(`✅ 获取到 ${dataArray.length} 条客服数据`); |
|||
|
|||
// 检查数据结构
|
|||
if (dataArray.length > 0) { |
|||
const firstItem = dataArray[0]; |
|||
console.log('第一条数据结构:', Object.keys(firstItem)); |
|||
|
|||
// 检查必要字段
|
|||
const requiredFields = ['id', 'managerId', 'name', 'phoneNumber']; |
|||
const missingFields = requiredFields.filter(field => !(field in firstItem)); |
|||
|
|||
if (missingFields.length === 0) { |
|||
console.log('✅ 所有必要字段都存在'); |
|||
} else { |
|||
console.warn('⚠️ 缺少必要字段:', missingFields); |
|||
} |
|||
} |
|||
|
|||
console.log('🎉 测试通过!API接口正常工作'); |
|||
} else { |
|||
console.error('❌ 响应数据不是预期的数组格式'); |
|||
} |
|||
} else { |
|||
console.error('❌ API返回非成功状态:', responseData); |
|||
} |
|||
} else { |
|||
console.error(`❌ 请求失败,状态码: ${res.statusCode}`); |
|||
} |
|||
} catch (parseError) { |
|||
console.error('❌ JSON解析错误:', parseError.message); |
|||
console.error('原始响应数据:', data); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
req.on('error', (e) => { |
|||
console.error('❌ 请求错误:', e.message); |
|||
}); |
|||
|
|||
req.end(); |
|||
|
|||
console.log('测试脚本已启动,请等待测试结果...'); |
|||
@ -0,0 +1,138 @@ |
|||
// 简化版聊天功能测试
|
|||
|
|||
// 服务器配置
|
|||
const SERVER_URL = 'ws://localhost:3003'; |
|||
|
|||
// 测试数据
|
|||
const managerData = { |
|||
userId: 'manager_001', |
|||
type: 'manager', |
|||
name: '客服小刘' |
|||
}; |
|||
|
|||
const userData = { |
|||
userId: 'user_001', |
|||
type: 'user', |
|||
name: '测试用户' |
|||
}; |
|||
|
|||
// 测试结果跟踪
|
|||
const testResults = { |
|||
managerConnection: false, |
|||
managerAuth: false, |
|||
userConnection: false, |
|||
userAuth: false, |
|||
messageExchange: false, |
|||
onlineStatusDetection: false, |
|||
messageCenterFunctionality: false |
|||
}; |
|||
|
|||
function runSimpleChatTest() { |
|||
console.log('=== 开始简化版聊天功能测试 ==='); |
|||
|
|||
// 模拟客服连接
|
|||
try { |
|||
const WebSocket = require('ws'); |
|||
const managerSocket = new WebSocket(SERVER_URL); |
|||
|
|||
managerSocket.on('open', () => { |
|||
console.log('[✅] 客服连接已建立'); |
|||
testResults.managerConnection = true; |
|||
|
|||
// 发送客服认证
|
|||
const authMessage = { |
|||
type: 'auth', |
|||
data: { |
|||
userId: managerData.userId, |
|||
type: managerData.type, |
|||
name: managerData.name |
|||
} |
|||
}; |
|||
console.log('发送客服认证:', authMessage); |
|||
managerSocket.send(JSON.stringify(authMessage)); |
|||
}); |
|||
|
|||
managerSocket.on('message', (data) => { |
|||
console.log('[客服收到消息]:', data.toString()); |
|||
const message = JSON.parse(data); |
|||
|
|||
// 检查认证结果
|
|||
if (message.type === 'auth_success' || message.action === 'auth_response') { |
|||
console.log('[✅] 客服认证成功'); |
|||
testResults.managerAuth = true; |
|||
} |
|||
}); |
|||
|
|||
managerSocket.on('error', (error) => { |
|||
console.error('[❌] 客服连接错误:', error.message); |
|||
}); |
|||
|
|||
managerSocket.on('close', () => { |
|||
console.log('[🔌] 客服连接已关闭'); |
|||
}); |
|||
|
|||
// 延迟创建用户连接
|
|||
setTimeout(() => { |
|||
const userSocket = new WebSocket(SERVER_URL); |
|||
|
|||
userSocket.on('open', () => { |
|||
console.log('[✅] 用户连接已建立'); |
|||
testResults.userConnection = true; |
|||
|
|||
// 发送用户认证
|
|||
const userAuth = { |
|||
type: 'auth', |
|||
data: { |
|||
userId: userData.userId, |
|||
type: userData.type, |
|||
name: userData.name |
|||
} |
|||
}; |
|||
console.log('发送用户认证:', userAuth); |
|||
userSocket.send(JSON.stringify(userAuth)); |
|||
}); |
|||
|
|||
userSocket.on('message', (data) => { |
|||
console.log('[用户收到消息]:', data.toString()); |
|||
}); |
|||
|
|||
// 5秒后发送测试消息
|
|||
setTimeout(() => { |
|||
if (userSocket.readyState === WebSocket.OPEN) { |
|||
const testMessage = { |
|||
type: 'chat', |
|||
from: userData.userId, |
|||
to: managerData.userId, |
|||
content: '你好,这是一条测试消息', |
|||
timestamp: Date.now() |
|||
}; |
|||
console.log('用户发送测试消息:', testMessage); |
|||
userSocket.send(JSON.stringify(testMessage)); |
|||
} |
|||
}, 5000); |
|||
|
|||
}, 3000); |
|||
|
|||
// 15秒后显示测试结果
|
|||
setTimeout(() => { |
|||
console.log('\n=== 测试结果 ==='); |
|||
console.log('客服连接:', testResults.managerConnection ? '✅ 成功' : '❌ 失败'); |
|||
console.log('客服认证:', testResults.managerAuth ? '✅ 成功' : '❌ 失败'); |
|||
console.log('用户连接:', testResults.userConnection ? '✅ 成功' : '❌ 失败'); |
|||
console.log('\n测试完成!'); |
|||
|
|||
// 关闭连接
|
|||
managerSocket.close(); |
|||
process.exit(0); |
|||
|
|||
}, 15000); |
|||
|
|||
} catch (error) { |
|||
console.error('测试运行失败:', error.message); |
|||
} |
|||
} |
|||
|
|||
// 运行测试
|
|||
if (require.main === module) { |
|||
runSimpleChatTest(); |
|||
} |
|||
@ -0,0 +1,333 @@ |
|||
// 客服功能测试脚本
|
|||
// 用于验证客服认证、身份判断和双向沟通功能
|
|||
|
|||
console.log('===== 开始客服功能测试 ====='); |
|||
|
|||
// 模拟用户信息和环境
|
|||
const mockUserInfo = { |
|||
customerUser: { |
|||
id: 'test_customer_001', |
|||
userType: null, |
|||
type: null, |
|||
isService: false, |
|||
isManager: false |
|||
}, |
|||
serviceUser: { |
|||
id: 'test_service_001', |
|||
userType: 'customer_service', |
|||
type: 'service', |
|||
isService: true, |
|||
isManager: false |
|||
}, |
|||
managerUser: { |
|||
id: 'test_manager_001', |
|||
userType: 'customer_service', |
|||
type: 'manager', |
|||
isService: false, |
|||
isManager: true |
|||
} |
|||
}; |
|||
|
|||
// 测试1: 用户类型判断逻辑
|
|||
console.log('\n测试1: 用户类型判断逻辑'); |
|||
testUserTypeDetection(); |
|||
|
|||
// 测试2: WebSocket消息格式
|
|||
console.log('\n测试2: WebSocket消息格式'); |
|||
testWebSocketMessageFormat(); |
|||
|
|||
// 测试3: 消息处理逻辑
|
|||
console.log('\n测试3: 消息处理逻辑'); |
|||
testMessageProcessing(); |
|||
|
|||
// 测试4: 双向通信模式
|
|||
console.log('\n测试4: 双向通信模式'); |
|||
testBidirectionalCommunication(); |
|||
|
|||
console.log('\n===== 测试完成 ====='); |
|||
|
|||
// 测试用户类型判断逻辑
|
|||
function testUserTypeDetection() { |
|||
console.log('- 测试用户类型判断函数'); |
|||
|
|||
// 模拟用户类型判断函数
|
|||
function detectUserType(userInfo) { |
|||
if (!userInfo) return 'customer'; |
|||
|
|||
if (userInfo.userType === 'customer_service' || |
|||
userInfo.type === 'service' || |
|||
userInfo.type === 'manager' || |
|||
userInfo.isService || |
|||
userInfo.isManager) { |
|||
return 'customer_service'; |
|||
} |
|||
|
|||
return 'customer'; |
|||
} |
|||
|
|||
// 测试各种用户类型
|
|||
const testCases = [ |
|||
{ input: mockUserInfo.customerUser, expected: 'customer', desc: '普通用户' }, |
|||
{ input: mockUserInfo.serviceUser, expected: 'customer_service', desc: '客服用户' }, |
|||
{ input: mockUserInfo.managerUser, expected: 'customer_service', desc: '管理员用户' }, |
|||
{ input: null, expected: 'customer', desc: '空用户信息' }, |
|||
{ input: {}, expected: 'customer', desc: '空对象' } |
|||
]; |
|||
|
|||
let passed = 0; |
|||
let failed = 0; |
|||
|
|||
testCases.forEach((testCase, index) => { |
|||
const result = detectUserType(testCase.input); |
|||
const isPass = result === testCase.expected; |
|||
|
|||
if (isPass) { |
|||
passed++; |
|||
console.log(` ✓ 测试${index + 1} (${testCase.desc}): 期望 ${testCase.expected}, 结果 ${result}`); |
|||
} else { |
|||
failed++; |
|||
console.log(` ✗ 测试${index + 1} (${testCase.desc}): 期望 ${testCase.expected}, 结果 ${result}`); |
|||
} |
|||
}); |
|||
|
|||
console.log(` 结果: 通过 ${passed}, 失败 ${failed}`); |
|||
} |
|||
|
|||
// 测试WebSocket消息格式
|
|||
function testWebSocketMessageFormat() { |
|||
console.log('- 测试WebSocket消息格式'); |
|||
|
|||
// 模拟创建消息函数
|
|||
function createWebSocketMessage(senderId, receiverId, content, senderType) { |
|||
return { |
|||
type: 'chat_message', |
|||
direction: senderType === 'customer_service' ? 'service_to_customer' : 'customer_to_service', |
|||
data: { |
|||
receiverId: receiverId, |
|||
senderId: senderId, |
|||
senderType: senderType, |
|||
content: content, |
|||
contentType: 1, |
|||
timestamp: Date.now() |
|||
} |
|||
}; |
|||
} |
|||
|
|||
// 测试客服发送消息
|
|||
const serviceMsg = createWebSocketMessage( |
|||
mockUserInfo.serviceUser.id, |
|||
mockUserInfo.customerUser.id, |
|||
'您好,有什么可以帮助您的吗?', |
|||
'customer_service' |
|||
); |
|||
|
|||
// 测试客户发送消息
|
|||
const customerMsg = createWebSocketMessage( |
|||
mockUserInfo.customerUser.id, |
|||
mockUserInfo.serviceUser.id, |
|||
'我想咨询一下产品信息', |
|||
'customer' |
|||
); |
|||
|
|||
console.log(' 客服消息格式:'); |
|||
console.log(` - type: ${serviceMsg.type}`); |
|||
console.log(` - direction: ${serviceMsg.direction}`); |
|||
console.log(` - senderId: ${serviceMsg.data.senderId}`); |
|||
console.log(` - receiverId: ${serviceMsg.data.receiverId}`); |
|||
console.log(` - senderType: ${serviceMsg.data.senderType}`); |
|||
|
|||
console.log(' 客户消息格式:'); |
|||
console.log(` - type: ${customerMsg.type}`); |
|||
console.log(` - direction: ${customerMsg.direction}`); |
|||
console.log(` - senderId: ${customerMsg.data.senderId}`); |
|||
console.log(` - receiverId: ${customerMsg.data.receiverId}`); |
|||
console.log(` - senderType: ${customerMsg.data.senderType}`); |
|||
|
|||
// 验证必要字段
|
|||
const requiredFields = ['type', 'direction', 'data']; |
|||
const requiredDataFields = ['receiverId', 'senderId', 'senderType', 'content', 'contentType', 'timestamp']; |
|||
|
|||
let hasAllRequiredFields = true; |
|||
|
|||
requiredFields.forEach(field => { |
|||
if (!(field in serviceMsg)) { |
|||
console.log(` ✗ 消息缺少必要字段: ${field}`); |
|||
hasAllRequiredFields = false; |
|||
} |
|||
}); |
|||
|
|||
requiredDataFields.forEach(field => { |
|||
if (!(field in serviceMsg.data)) { |
|||
console.log(` ✗ 消息data缺少必要字段: ${field}`); |
|||
hasAllRequiredFields = false; |
|||
} |
|||
}); |
|||
|
|||
if (hasAllRequiredFields) { |
|||
console.log(' ✓ 消息格式验证通过'); |
|||
} else { |
|||
console.log(' ✗ 消息格式验证失败'); |
|||
} |
|||
} |
|||
|
|||
// 测试消息处理逻辑
|
|||
function testMessageProcessing() { |
|||
console.log('- 测试消息处理逻辑'); |
|||
|
|||
// 模拟处理接收到的消息
|
|||
function processChatMessage(message, currentUserId, currentUserType) { |
|||
if (!message || !message.data) { |
|||
return null; |
|||
} |
|||
|
|||
// 判断消息方向
|
|||
const isFromMe = message.data.senderId === currentUserId; |
|||
const isFromService = message.data.senderType === 'customer_service'; |
|||
const isFromCustomer = message.data.senderType === 'customer'; |
|||
|
|||
// 构建本地消息对象
|
|||
const localMessage = { |
|||
id: message.id || Date.now().toString(), |
|||
content: message.data.content || '', |
|||
contentType: message.data.contentType || 1, |
|||
timestamp: message.data.timestamp || Date.now(), |
|||
isFromMe: isFromMe, |
|||
senderType: message.data.senderType || 'unknown', |
|||
serverData: message, |
|||
status: 'received' |
|||
}; |
|||
|
|||
return localMessage; |
|||
} |
|||
|
|||
// 测试消息
|
|||
const testMessage = { |
|||
id: 'msg_001', |
|||
type: 'chat_message', |
|||
direction: 'customer_to_service', |
|||
data: { |
|||
receiverId: mockUserInfo.serviceUser.id, |
|||
senderId: mockUserInfo.customerUser.id, |
|||
senderType: 'customer', |
|||
content: '测试消息', |
|||
contentType: 1, |
|||
timestamp: Date.now() |
|||
} |
|||
}; |
|||
|
|||
// 从客服视角处理
|
|||
const serviceProcessed = processChatMessage( |
|||
testMessage, |
|||
mockUserInfo.serviceUser.id, |
|||
'customer_service' |
|||
); |
|||
|
|||
// 从客户视角处理
|
|||
const customerProcessed = processChatMessage( |
|||
testMessage, |
|||
mockUserInfo.customerUser.id, |
|||
'customer' |
|||
); |
|||
|
|||
console.log(' 客服视角处理结果:'); |
|||
console.log(` - 是否来自自己: ${serviceProcessed.isFromMe}`); |
|||
console.log(` - 发送方类型: ${serviceProcessed.senderType}`); |
|||
console.log(` - 内容: ${serviceProcessed.content}`); |
|||
|
|||
console.log(' 客户视角处理结果:'); |
|||
console.log(` - 是否来自自己: ${customerProcessed.isFromMe}`); |
|||
console.log(` - 发送方类型: ${customerProcessed.senderType}`); |
|||
console.log(` - 内容: ${customerProcessed.content}`); |
|||
|
|||
// 验证处理逻辑
|
|||
const isServiceLogicCorrect = !serviceProcessed.isFromMe && serviceProcessed.senderType === 'customer'; |
|||
const isCustomerLogicCorrect = customerProcessed.isFromMe && customerProcessed.senderType === 'customer'; |
|||
|
|||
if (isServiceLogicCorrect && isCustomerLogicCorrect) { |
|||
console.log(' ✓ 消息处理逻辑验证通过'); |
|||
} else { |
|||
console.log(' ✗ 消息处理逻辑验证失败'); |
|||
if (!isServiceLogicCorrect) console.log(' - 客服视角处理错误'); |
|||
if (!isCustomerLogicCorrect) console.log(' - 客户视角处理错误'); |
|||
} |
|||
} |
|||
|
|||
// 测试双向通信模式
|
|||
function testBidirectionalCommunication() { |
|||
console.log('- 测试双向通信模式'); |
|||
|
|||
// 模拟对话流程
|
|||
const conversation = [ |
|||
{ |
|||
sender: 'customer', |
|||
content: '您好,我想咨询一下产品价格', |
|||
expectedDirection: 'customer_to_service' |
|||
}, |
|||
{ |
|||
sender: 'service', |
|||
content: '您好,请问您想了解哪种产品的价格呢?', |
|||
expectedDirection: 'service_to_customer' |
|||
}, |
|||
{ |
|||
sender: 'customer', |
|||
content: '就是你们的主打产品', |
|||
expectedDirection: 'customer_to_service' |
|||
}, |
|||
{ |
|||
sender: 'service', |
|||
content: '我们的主打产品价格是¥199,现在有优惠活动', |
|||
expectedDirection: 'service_to_customer' |
|||
} |
|||
]; |
|||
|
|||
let conversationLog = []; |
|||
|
|||
conversation.forEach((msg, index) => { |
|||
const isFromService = msg.sender === 'service'; |
|||
const senderId = isFromService ? mockUserInfo.serviceUser.id : mockUserInfo.customerUser.id; |
|||
const receiverId = isFromService ? mockUserInfo.customerUser.id : mockUserInfo.serviceUser.id; |
|||
const senderType = isFromService ? 'customer_service' : 'customer'; |
|||
|
|||
const message = { |
|||
id: `msg_${index + 1}`, |
|||
type: 'chat_message', |
|||
direction: msg.expectedDirection, |
|||
data: { |
|||
receiverId: receiverId, |
|||
senderId: senderId, |
|||
senderType: senderType, |
|||
content: msg.content, |
|||
contentType: 1, |
|||
timestamp: Date.now() + index |
|||
} |
|||
}; |
|||
|
|||
conversationLog.push({ |
|||
role: isFromService ? '客服' : '客户', |
|||
content: msg.content, |
|||
direction: msg.expectedDirection |
|||
}); |
|||
|
|||
// 验证消息方向
|
|||
if (message.direction !== msg.expectedDirection) { |
|||
console.log(` ✗ 消息${index + 1}方向错误: 期望${msg.expectedDirection}, 实际${message.direction}`); |
|||
} |
|||
}); |
|||
|
|||
// 打印对话流程
|
|||
console.log(' 双向对话流程:'); |
|||
conversationLog.forEach((msg, index) => { |
|||
console.log(` ${index + 1}. [${msg.role}] ${msg.content} (${msg.direction})`); |
|||
}); |
|||
|
|||
console.log(' ✓ 双向通信模式验证完成'); |
|||
} |
|||
|
|||
// 导出测试结果
|
|||
module.exports = { |
|||
mockUserInfo, |
|||
testUserTypeDetection, |
|||
testWebSocketMessageFormat, |
|||
testMessageProcessing, |
|||
testBidirectionalCommunication |
|||
}; |
|||
@ -0,0 +1,96 @@ |
|||
// 测试聊天功能连接的脚本
|
|||
const WebSocket = require('ws'); |
|||
|
|||
// 假设服务器WebSocket地址
|
|||
const SERVER_URL = 'ws://localhost:3000'; // 根据实际服务器地址调整
|
|||
|
|||
// 模拟用户和客服的连接
|
|||
function testUserToManagerCommunication() { |
|||
console.log('开始测试用户和客服之间的消息传递...'); |
|||
|
|||
// 模拟客服连接
|
|||
const managerSocket = new WebSocket(SERVER_URL); |
|||
|
|||
managerSocket.on('open', () => { |
|||
console.log('客服连接已建立'); |
|||
|
|||
// 客服认证
|
|||
managerSocket.send(JSON.stringify({ |
|||
type: 'auth', |
|||
data: { |
|||
userId: 'manager_1', |
|||
type: 'manager', |
|||
name: '测试客服' |
|||
} |
|||
})); |
|||
}); |
|||
|
|||
managerSocket.on('message', (data) => { |
|||
try { |
|||
const message = JSON.parse(data.toString()); |
|||
console.log('客服收到消息:', message); |
|||
} catch (e) { |
|||
console.error('客服解析消息失败:', e); |
|||
} |
|||
}); |
|||
|
|||
managerSocket.on('error', (error) => { |
|||
console.error('客服连接错误:', error); |
|||
}); |
|||
|
|||
// 延迟2秒后创建用户连接
|
|||
setTimeout(() => { |
|||
const userSocket = new WebSocket(SERVER_URL); |
|||
|
|||
userSocket.on('open', () => { |
|||
console.log('用户连接已建立'); |
|||
|
|||
// 用户认证
|
|||
userSocket.send(JSON.stringify({ |
|||
type: 'auth', |
|||
data: { |
|||
userId: 'user_1', |
|||
type: 'user', |
|||
name: '测试用户' |
|||
} |
|||
})); |
|||
|
|||
// 再延迟1秒后发送消息
|
|||
setTimeout(() => { |
|||
console.log('用户发送测试消息...'); |
|||
userSocket.send(JSON.stringify({ |
|||
type: 'chat_message', |
|||
data: { |
|||
managerId: 'manager_1', |
|||
content: '这是一条测试消息', |
|||
contentType: 1, // 文本消息
|
|||
timestamp: Date.now() |
|||
} |
|||
})); |
|||
}, 1000); |
|||
}); |
|||
|
|||
userSocket.on('message', (data) => { |
|||
try { |
|||
const message = JSON.parse(data.toString()); |
|||
console.log('用户收到消息:', message); |
|||
} catch (e) { |
|||
console.error('用户解析消息失败:', e); |
|||
} |
|||
}); |
|||
|
|||
userSocket.on('error', (error) => { |
|||
console.error('用户连接错误:', error); |
|||
}); |
|||
|
|||
// 清理连接
|
|||
setTimeout(() => { |
|||
console.log('测试完成,关闭连接'); |
|||
userSocket.close(); |
|||
managerSocket.close(); |
|||
}, 10000); |
|||
}, 2000); |
|||
} |
|||
|
|||
// 运行测试
|
|||
testUserToManagerCommunication(); |
|||
File diff suppressed because it is too large
@ -0,0 +1,75 @@ |
|||
// 更新products表结构,添加联系人相关字段
|
|||
const mysql = require('mysql2/promise'); |
|||
|
|||
async function updateProductTable() { |
|||
let connection; |
|||
try { |
|||
// 连接数据库 - 使用正确的密码
|
|||
connection = await mysql.createConnection({ |
|||
host: '1.95.162.61', |
|||
port: 3306, |
|||
user: 'root', |
|||
password: 'schl@2025', // 从.env文件中获取的密码
|
|||
database: 'wechat_app' |
|||
}); |
|||
console.log('✅ 数据库连接成功'); |
|||
|
|||
// 检查product_contact字段是否存在
|
|||
const [rows] = await connection.query( |
|||
"SELECT column_name FROM information_schema.columns WHERE table_schema = 'wechat_app' AND table_name = 'products' AND column_name = 'product_contact'" |
|||
); |
|||
|
|||
if (rows.length === 0) { |
|||
// 添加product_contact字段
|
|||
await connection.query("ALTER TABLE products ADD COLUMN product_contact VARCHAR(100) DEFAULT ''"); |
|||
console.log('✅ 已添加product_contact字段'); |
|||
} else { |
|||
console.log('ℹ️ product_contact字段已存在'); |
|||
} |
|||
|
|||
// 检查contact_phone字段是否存在
|
|||
const [phoneRows] = await connection.query( |
|||
"SELECT column_name FROM information_schema.columns WHERE table_schema = 'wechat_app' AND table_name = 'products' AND column_name = 'contact_phone'" |
|||
); |
|||
|
|||
if (phoneRows.length === 0) { |
|||
// 添加contact_phone字段
|
|||
await connection.query("ALTER TABLE products ADD COLUMN contact_phone VARCHAR(20) DEFAULT ''"); |
|||
console.log('✅ 已添加contact_phone字段'); |
|||
} else { |
|||
console.log('ℹ️ contact_phone字段已存在'); |
|||
} |
|||
|
|||
// 查询所有已发布商品的数量
|
|||
const [productRows] = await connection.query( |
|||
"SELECT COUNT(*) as count FROM products WHERE status = 'published'" |
|||
); |
|||
console.log(`📊 已发布商品数量: ${productRows[0].count}`); |
|||
|
|||
// 查询需要更新联系人信息的商品数量
|
|||
const [pendingRows] = await connection.query( |
|||
"SELECT COUNT(*) as count FROM products WHERE status = 'published' AND (product_contact = '' OR product_contact IS NULL OR contact_phone = '' OR contact_phone IS NULL)" |
|||
); |
|||
console.log(`⚠️ 需要更新联系人信息的商品数量: ${pendingRows[0].count}`); |
|||
|
|||
// 显示一些商品数据作为示例
|
|||
const [sampleProducts] = await connection.query( |
|||
"SELECT productId, productName, product_contact, contact_phone FROM products WHERE status = 'published' LIMIT 5" |
|||
); |
|||
console.log('\n📋 示例商品数据:'); |
|||
sampleProducts.forEach(product => { |
|||
console.log(`- ${product.productName}: 联系人=${product.product_contact || '空'}, 电话=${product.contact_phone || '空'}`); |
|||
}); |
|||
|
|||
} catch (error) { |
|||
console.error('❌ 操作失败:', error.message); |
|||
} finally { |
|||
if (connection) { |
|||
await connection.end(); |
|||
console.log('\n✅ 数据库连接已关闭'); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 执行更新
|
|||
updateProductTable(); |
|||
@ -0,0 +1,492 @@ |
|||
// utils/websocket.js
|
|||
// WebSocket连接管理器
|
|||
|
|||
class WebSocketManager { |
|||
constructor() { |
|||
this.socket = null; |
|||
this.url = ''; |
|||
this.isConnected = false; |
|||
this.isAuthenticated = false; // 新增:认证状态标记
|
|||
this.reconnectAttempts = 0; |
|||
this.maxReconnectAttempts = 5; |
|||
this.reconnectInterval = 3000; // 3秒后重连
|
|||
this.heartbeatInterval = null; |
|||
this.heartbeatTime = 30000; // 30秒心跳
|
|||
this.messageQueue = []; // 未发送的消息队列
|
|||
this.listeners = {}; // 事件监听器
|
|||
this.lastHeartbeatTime = 0; // 最后一次心跳响应时间
|
|||
this.isManualDisconnect = false; // 是否手动断开连接
|
|||
|
|||
// 清理可能导致端口错误的存储配置
|
|||
this._cleanupStorage(); |
|||
} |
|||
|
|||
// 清理可能导致端口错误的存储配置
|
|||
_cleanupStorage() { |
|||
try { |
|||
// 尝试在小程序环境中清理存储
|
|||
if (typeof wx !== 'undefined' && wx.removeStorageSync) { |
|||
wx.removeStorageSync('__TEST_MODE__'); |
|||
wx.removeStorageSync('__TEST_SERVER_IP__'); |
|||
wx.removeStorageSync('__DEVICE_TYPE__'); |
|||
console.log('WebSocket: 已清理可能导致端口错误的本地存储配置'); |
|||
} |
|||
} catch (e) { |
|||
console.warn('WebSocket: 清理存储时出错:', e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 初始化WebSocket连接 |
|||
* @param {string} url - WebSocket服务器地址 |
|||
* @param {object} options - 配置选项 |
|||
*/ |
|||
connect(url, options = {}) { |
|||
if (this.socket && this.isConnected) { |
|||
console.log('WebSocket已经连接'); |
|||
return; |
|||
} |
|||
|
|||
this.url = url; |
|||
this.maxReconnectAttempts = options.maxReconnectAttempts || this.maxReconnectAttempts; |
|||
this.reconnectInterval = options.reconnectInterval || this.reconnectInterval; |
|||
this.heartbeatTime = options.heartbeatTime || this.heartbeatTime; |
|||
this.isManualDisconnect = false; // 重置手动断开标志
|
|||
|
|||
try { |
|||
console.log('尝试连接WebSocket:', url); |
|||
this._trigger('status', { type: 'connecting', message: '正在连接服务器...' }); |
|||
|
|||
this.socket = wx.connectSocket({ |
|||
url: url, |
|||
success: () => { |
|||
console.log('WebSocket连接请求已发送'); |
|||
}, |
|||
fail: (error) => { |
|||
console.error('WebSocket连接请求失败:', error); |
|||
this._trigger('error', error); |
|||
this._trigger('status', { type: 'error', message: '连接服务器失败' }); |
|||
this._reconnect(); |
|||
} |
|||
}); |
|||
|
|||
this._setupEventHandlers(); |
|||
} catch (error) { |
|||
console.error('WebSocket初始化失败:', error); |
|||
this._trigger('error', error); |
|||
this._trigger('status', { type: 'error', message: '连接异常' }); |
|||
this._reconnect(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 设置WebSocket事件处理器 |
|||
*/ |
|||
_setupEventHandlers() { |
|||
if (!this.socket) return; |
|||
|
|||
// 连接成功
|
|||
this.socket.onOpen(() => { |
|||
console.log('WebSocket连接已打开'); |
|||
this.isConnected = true; |
|||
this.isAuthenticated = false; // 重置认证状态
|
|||
this.reconnectAttempts = 0; |
|||
this.lastHeartbeatTime = Date.now(); // 记录最后心跳时间
|
|||
this._trigger('open'); |
|||
this._trigger('status', { type: 'connected', message: '连接成功' }); |
|||
|
|||
// 连接成功后立即进行认证
|
|||
this.authenticate(); |
|||
|
|||
this._startHeartbeat(); |
|||
}); |
|||
|
|||
// 接收消息
|
|||
this.socket.onMessage((res) => { |
|||
try { |
|||
let data = JSON.parse(res.data); |
|||
// 处理心跳响应
|
|||
if (data.type === 'pong') { |
|||
this.lastHeartbeatTime = Date.now(); // 更新心跳时间
|
|||
return; |
|||
} |
|||
|
|||
// 处理认证响应
|
|||
if (data.type === 'auth_response') { |
|||
if (data.success) { |
|||
console.log('WebSocket认证成功'); |
|||
this.isAuthenticated = true; |
|||
// 触发认证成功事件,并传递用户类型信息
|
|||
this._trigger('authenticated', { userType: data.userType || 'customer' }); |
|||
// 认证成功后发送队列中的消息
|
|||
this._flushMessageQueue(); |
|||
} else { |
|||
console.error('WebSocket认证失败:', data.message); |
|||
this.isAuthenticated = false; |
|||
this._trigger('authFailed', { message: data.message, userType: data.userType || 'unknown' }); |
|||
} |
|||
return; |
|||
} |
|||
|
|||
// 处理客服状态更新消息
|
|||
if (data.type === 'customerServiceStatusUpdate') { |
|||
console.log('处理客服状态更新:', data); |
|||
this._trigger('customerServiceStatusUpdate', data); |
|||
return; |
|||
} |
|||
|
|||
console.log('接收到消息:', data); |
|||
this._trigger('message', data); |
|||
} catch (error) { |
|||
console.error('消息解析失败:', error); |
|||
this._trigger('error', error); |
|||
} |
|||
}); |
|||
|
|||
// 连接关闭
|
|||
this.socket.onClose((res) => { |
|||
console.log('WebSocket连接已关闭:', res); |
|||
this.isConnected = false; |
|||
this._stopHeartbeat(); |
|||
this._trigger('close', res); |
|||
this._trigger('status', { type: 'disconnected', message: '连接已关闭' }); |
|||
// 尝试重连
|
|||
if (res.code !== 1000 && !this.isManualDisconnect) { // 非正常关闭且不是手动断开
|
|||
this._reconnect(); |
|||
} |
|||
}); |
|||
|
|||
// 连接错误
|
|||
this.socket.onError((error) => { |
|||
console.error('WebSocket错误:', error); |
|||
this._trigger('error', error); |
|||
this._trigger('status', { type: 'error', message: '连接发生错误' }); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 发送认证消息 |
|||
* @param {string} userType - 用户类型,如'user'或'customerService' |
|||
* @param {string} userId - 用户ID |
|||
*/ |
|||
authenticate(userType = null, userId = null) { |
|||
try { |
|||
// 获取登录用户信息或token
|
|||
const app = getApp(); |
|||
const globalUserInfo = app.globalData.userInfo || {}; |
|||
|
|||
// 如果传入了参数,优先使用传入的参数
|
|||
const finalUserType = userType || globalUserInfo.userType || globalUserInfo.type || 'customer'; |
|||
const finalUserId = userId || globalUserInfo.userId || wx.getStorageSync('userId') || `temp_${Date.now()}`; |
|||
|
|||
console.log('发送WebSocket认证消息:', { userId: finalUserId, userType: finalUserType }); |
|||
|
|||
// 构建认证消息
|
|||
const authMessage = { |
|||
type: 'auth', |
|||
timestamp: Date.now(), |
|||
data: { |
|||
userId: finalUserId, |
|||
userType: finalUserType, |
|||
// 可以根据实际需求添加更多认证信息
|
|||
} |
|||
}; |
|||
|
|||
// 直接发送认证消息,不经过常规消息队列
|
|||
if (this.isConnected && this.socket) { |
|||
this.socket.send({ |
|||
data: JSON.stringify(authMessage), |
|||
success: () => { |
|||
console.log('认证消息发送成功'); |
|||
}, |
|||
fail: (error) => { |
|||
console.error('认证消息发送失败:', error); |
|||
// 认证失败后尝试重新认证
|
|||
setTimeout(() => { |
|||
this.authenticate(userType, userId); |
|||
}, 2000); |
|||
} |
|||
}); |
|||
} |
|||
} catch (error) { |
|||
console.error('发送认证消息异常:', error); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 发送消息 |
|||
* @param {object} data - 要发送的数据 |
|||
* @returns {boolean} 消息是否已成功放入发送队列(不保证实际发送成功) |
|||
*/ |
|||
send(data) { |
|||
// 验证消息格式
|
|||
if (!data || typeof data !== 'object') { |
|||
console.error('WebSocket发送消息失败: 消息格式不正确'); |
|||
return false; |
|||
} |
|||
|
|||
// 为消息添加时间戳
|
|||
if (!data.timestamp) { |
|||
data.timestamp = Date.now(); |
|||
} |
|||
|
|||
// 如果是认证消息或连接未建立,直接处理
|
|||
if (data.type === 'auth' || data.type === 'ping') { |
|||
// 认证消息和心跳消息不需要等待认证
|
|||
if (this.isConnected && this.socket) { |
|||
try { |
|||
this.socket.send({ |
|||
data: JSON.stringify(data), |
|||
success: () => { |
|||
console.log('特殊消息发送成功:', data); |
|||
this._trigger('sendSuccess', data); |
|||
}, |
|||
fail: (error) => { |
|||
console.error('特殊消息发送失败:', error); |
|||
this._trigger('sendError', error); |
|||
} |
|||
}); |
|||
return true; |
|||
} catch (error) { |
|||
console.error('发送特殊消息异常:', error); |
|||
this._trigger('error', error); |
|||
return false; |
|||
} |
|||
} |
|||
} else if (this.isConnected && this.socket) { |
|||
// 非特殊消息需要检查认证状态
|
|||
if (!this.isAuthenticated) { |
|||
console.log('WebSocket未认证,消息已加入队列等待认证'); |
|||
this.messageQueue.push(data); |
|||
// 如果未认证,尝试重新认证
|
|||
if (!this.isAuthenticated) { |
|||
this.authenticate(); |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
try { |
|||
this.socket.send({ |
|||
data: JSON.stringify(data), |
|||
success: () => { |
|||
console.log('消息发送成功:', data); |
|||
this._trigger('sendSuccess', data); |
|||
}, |
|||
fail: (error) => { |
|||
console.error('消息发送失败:', error); |
|||
// 将失败的消息加入队列
|
|||
this.messageQueue.push(data); |
|||
this._trigger('sendError', error); |
|||
} |
|||
}); |
|||
return true; |
|||
} catch (error) { |
|||
console.error('发送消息异常:', error); |
|||
this.messageQueue.push(data); |
|||
this._trigger('error', error); |
|||
return false; |
|||
} |
|||
} else { |
|||
// 连接未建立,加入消息队列
|
|||
console.log('WebSocket未连接,消息已加入队列'); |
|||
this.messageQueue.push(data); |
|||
// 尝试重连
|
|||
if (!this.isConnected) { |
|||
this._reconnect(); |
|||
} |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 关闭WebSocket连接 |
|||
*/ |
|||
close() { |
|||
if (this.socket) { |
|||
this._stopHeartbeat(); |
|||
this.isManualDisconnect = true; // 标记为手动断开
|
|||
this.socket.close(); |
|||
this.socket = null; |
|||
this.isConnected = false; |
|||
this.isAuthenticated = false; // 重置认证状态
|
|||
console.log('WebSocket已主动关闭'); |
|||
this._trigger('status', { type: 'disconnected', message: '连接已断开' }); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 开始心跳检测 |
|||
*/ |
|||
_startHeartbeat() { |
|||
this._stopHeartbeat(); |
|||
this.heartbeatInterval = setInterval(() => { |
|||
// 检查是否超过3倍心跳间隔未收到心跳响应
|
|||
if (Date.now() - this.lastHeartbeatTime > this.heartbeatTime * 3) { |
|||
console.warn('WebSocket心跳超时,可能已断开连接'); |
|||
this._stopHeartbeat(); |
|||
this._reconnect(); |
|||
return; |
|||
} |
|||
|
|||
if (this.isConnected) { |
|||
this.send({ type: 'ping', timestamp: Date.now() }); |
|||
console.log('发送心跳包'); |
|||
} |
|||
}, this.heartbeatTime); |
|||
} |
|||
|
|||
/** |
|||
* 停止心跳检测 |
|||
*/ |
|||
_stopHeartbeat() { |
|||
if (this.heartbeatInterval) { |
|||
clearInterval(this.heartbeatInterval); |
|||
this.heartbeatInterval = null; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 尝试重新连接 |
|||
*/ |
|||
_reconnect() { |
|||
if (this.reconnectAttempts >= this.maxReconnectAttempts) { |
|||
console.error('WebSocket重连次数已达上限,停止重连'); |
|||
this._trigger('reconnectFailed'); |
|||
this._trigger('status', { |
|||
type: 'error', |
|||
isWarning: true, |
|||
message: `已达到最大重连次数(${this.maxReconnectAttempts}次)` |
|||
}); |
|||
return; |
|||
} |
|||
|
|||
this.reconnectAttempts++; |
|||
// 增加重连时间间隔(指数退避)
|
|||
const currentInterval = this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1); |
|||
|
|||
console.log(`WebSocket第${this.reconnectAttempts}次重连... 间隔: ${currentInterval}ms`); |
|||
this._trigger('reconnecting', this.reconnectAttempts); |
|||
this._trigger('status', { |
|||
type: 'reconnecting', |
|||
message: `正在重连(${this.reconnectAttempts}/${this.maxReconnectAttempts})` |
|||
}); |
|||
|
|||
setTimeout(() => { |
|||
this.connect(this.url); |
|||
}, currentInterval); |
|||
} |
|||
|
|||
/** |
|||
* 发送队列中的消息 |
|||
*/ |
|||
_flushMessageQueue() { |
|||
if (this.messageQueue.length > 0) { |
|||
console.log('发送队列中的消息,队列长度:', this.messageQueue.length); |
|||
|
|||
// 循环发送队列中的消息,使用小延迟避免消息发送过快
|
|||
const sendMessage = () => { |
|||
if (this.messageQueue.length === 0 || !this.isConnected) { |
|||
return; |
|||
} |
|||
|
|||
const messageData = this.messageQueue.shift(); |
|||
const message = JSON.stringify(messageData); |
|||
|
|||
this.socket.send({ |
|||
data: message, |
|||
success: () => { |
|||
console.log('队列消息发送成功:', messageData); |
|||
// 继续发送下一条消息,添加小延迟
|
|||
setTimeout(sendMessage, 50); |
|||
}, |
|||
fail: (error) => { |
|||
console.error('队列消息发送失败:', error); |
|||
// 发送失败,重新加入队列
|
|||
this.messageQueue.unshift(messageData); |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
// 开始发送队列中的第一条消息
|
|||
sendMessage(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 触发事件 |
|||
* @param {string} event - 事件名称 |
|||
* @param {*} data - 事件数据 |
|||
*/ |
|||
_trigger(event, data = null) { |
|||
if (this.listeners[event]) { |
|||
this.listeners[event].forEach(callback => { |
|||
try { |
|||
callback(data); |
|||
} catch (error) { |
|||
console.error(`事件处理错误 [${event}]:`, error); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 监听事件 |
|||
* @param {string} event - 事件名称 |
|||
* @param {Function} callback - 回调函数 |
|||
*/ |
|||
on(event, callback) { |
|||
if (!this.listeners[event]) { |
|||
this.listeners[event] = []; |
|||
} |
|||
this.listeners[event].push(callback); |
|||
} |
|||
|
|||
/** |
|||
* 移除事件监听 |
|||
* @param {string} event - 事件名称 |
|||
* @param {Function} callback - 回调函数,不传则移除所有该事件的监听器 |
|||
*/ |
|||
off(event, callback) { |
|||
if (!this.listeners[event]) return; |
|||
|
|||
if (callback) { |
|||
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback); |
|||
} else { |
|||
this.listeners[event] = []; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取连接状态 |
|||
* @returns {boolean} 是否连接 |
|||
*/ |
|||
getConnectionStatus() { |
|||
return this.isConnected; |
|||
} |
|||
|
|||
/** |
|||
* 获取认证状态 |
|||
* @returns {boolean} 是否已认证 |
|||
*/ |
|||
getAuthStatus() { |
|||
return this.isAuthenticated; |
|||
} |
|||
|
|||
/** |
|||
* 获取重连次数 |
|||
* @returns {number} 重连次数 |
|||
*/ |
|||
getReconnectAttempts() { |
|||
return this.reconnectAttempts; |
|||
} |
|||
|
|||
/** |
|||
* 清空消息队列 |
|||
*/ |
|||
clearMessageQueue() { |
|||
this.messageQueue = []; |
|||
} |
|||
} |
|||
|
|||
// 导出单例
|
|||
export default new WebSocketManager(); |
|||
Loading…
Reference in new issue