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