Browse Source

修复客服列表404错误并实现聊天功能

pull/1/head
徐飞洋 3 months ago
parent
commit
fccc3a2f83
  1. 3
      .gitignore
  2. 165
      app.js
  3. 221
      pages/customer-service/detail/index.js
  4. 12
      pages/customer-service/detail/index.json
  5. 126
      pages/customer-service/detail/index.wxml
  6. 323
      pages/customer-service/detail/index.wxss
  7. 396
      pages/customer-service/index.js
  8. 6
      pages/customer-service/index.json
  9. 113
      pages/customer-service/index.wxml
  10. 381
      pages/customer-service/index.wxss
  11. 153
      server-example/server-mysql.js
  12. 27
      utils/api.js

3
.gitignore

@ -1,3 +1,4 @@
node_modules
pages/test/
pages/test-tools/
pages/test-tools/
server-example/db-query.js

165
app.js

@ -1,165 +0,0 @@
App({
// 全局事件总线功能
eventBus: {
listeners: {},
// 监听事件
on: function (event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
},
// 触发事件
emit: function (event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach(callback => {
try {
callback(data);
} catch (error) {
console.error('事件处理错误:', error);
}
});
}
},
// 移除事件监听
off: function (event, callback) {
if (this.listeners[event]) {
if (callback) {
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
} else {
delete this.listeners[event];
}
}
}
},
onLaunch: function () {
// 初始化应用
console.log('App Launch')
// 初始化本地存储的标签和用户数据
if (!wx.getStorageSync('users')) {
wx.setStorageSync('users', {})
}
if (!wx.getStorageSync('tags')) {
wx.setStorageSync('tags', {})
}
if (!wx.getStorageSync('goods')) {
// 初始化空的商品列表,不预置默认数据,由服务器获取
wx.setStorageSync('goods', [])
}
if (!wx.getStorageSync('supplies')) {
// 初始化空的供应列表,不预置默认数据,由服务器获取
wx.setStorageSync('supplies', [])
}
// 检查是否是首次启动
const isFirstLaunch = !wx.getStorageSync('hasLaunched')
if (isFirstLaunch) {
// 标记应用已经启动过
wx.setStorageSync('hasLaunched', true)
// 只有在首次启动时才检查用户身份并可能跳转
const userId = wx.getStorageSync('userId')
if (userId) {
const users = wx.getStorageSync('users')
const user = users[userId]
if (user && user.type) {
// 延迟跳转,确保页面加载完成
setTimeout(() => {
try {
if (user.type === 'buyer') {
wx.switchTab({ url: '/pages/buyer/index' })
} else if (user.type === 'seller') {
wx.switchTab({ url: '/pages/seller/index' })
}
} catch (e) {
console.error('启动时页面跳转异常:', e)
// 即使跳转失败,也不影响应用正常启动
}
}, 100)
}
}
}
// 获取本地存储的用户信息和用户类型
const storedUserInfo = wx.getStorageSync('userInfo');
const storedUserType = wx.getStorageSync('userType');
if (storedUserInfo) {
this.globalData.userInfo = storedUserInfo;
}
if (storedUserType) {
this.globalData.userType = storedUserType;
}
console.log('App初始化 - 用户类型:', this.globalData.userType);
console.log('App初始化 - 用户信息:', this.globalData.userInfo);
// 获取用户信息
wx.getSetting({
success: res => {
if (res.authSetting['scope.userInfo']) {
// 已经授权,可以直接调用 getUserInfo 获取头像昵称
wx.getUserInfo({
success: res => {
// 将用户信息存储到本地和全局,不自动登录
// 登录流程将在用户点击登录按钮时触发
console.log('获取用户信息成功,暂不自动登录');
// 更新全局用户信息
this.globalData.userInfo = res.userInfo;
// 更新本地存储的用户信息
wx.setStorageSync('userInfo', res.userInfo);
},
fail: error => {
console.error('获取用户信息失败:', error);
}
});
}
},
fail: error => {
console.error('获取用户设置失败:', error);
}
});
},
onShow: function () {
console.log('App Show')
},
onHide: function () {
console.log('App Hide')
},
// 更新当前选中的tab
updateCurrentTab(tabKey) {
if (this.globalData) {
this.globalData.currentTab = tabKey
}
},
// 跳转到估价页面
goToEvaluatePage() {
wx.navigateTo({
url: '/pages/evaluate/index'
})
},
// 上传手机号数据
async uploadPhoneNumberData(phoneData) {
const API = require('./utils/api.js')
return await API.uploadPhoneNumberData(phoneData)
},
globalData: {
userInfo: null,
userType: 'customer', // 默认客户类型
currentTab: 'index', // 当前选中的tab
showTabBar: true, // 控制底部tab-bar显示状态
// 测试环境配置
isTestMode: false
}
})

221
pages/customer-service/detail/index.js

@ -0,0 +1,221 @@
// pages/customer-service/detail/index.js
const api = require('../../../utils/api');
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) return;
// 获取当前用户的手机号
let userPhone = '';
try {
// 尝试从不同的存储位置获取手机号
const userInfo = wx.getStorageSync('userInfo');
if (userInfo && userInfo.phoneNumber) {
userPhone = userInfo.phoneNumber;
} else {
// 尝试从其他可能的存储位置获取
const users = wx.getStorageSync('users') || {};
const userId = wx.getStorageSync('userId');
if (userId && users[userId] && users[userId].phoneNumber) {
userPhone = users[userId].phoneNumber;
} else {
userPhone = wx.getStorageSync('phoneNumber');
}
}
} catch (e) {
console.error('获取用户手机号失败:', e);
}
console.log('当前用户手机号:', userPhone);
console.log('客服手机号:', customerData.phoneNumber);
// 验证手机号
if (!userPhone) {
wx.showToast({
title: '请先登录获取手机号',
icon: 'none'
});
return;
}
// 显示加载提示
wx.showLoading({
title: '正在建立聊天...',
});
// 调用API添加聊天记录
api.addChatRecord(userPhone, customerData.phoneNumber).then(res => {
console.log('添加聊天记录成功:', res);
// 隐藏加载提示
wx.hideLoading();
// 导航到聊天页面
wx.navigateTo({
url: `/pages/chat/index?id=${customerData.id}&name=${customerData.alias}&phone=${customerData.phoneNumber}`
});
}).catch(err => {
console.error('添加聊天记录失败:', err);
// 隐藏加载提示
wx.hideLoading();
// 显示错误提示
wx.showToast({
title: '建立聊天失败: ' + (err.message || '请稍后重试'),
icon: 'none'
});
});
},
// 拨打电话
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: ''
};
}
});

12
pages/customer-service/detail/index.json

@ -1,9 +1,7 @@
{
"navigationBarTitleText": "客服详情",
"navigationStyle": "custom",
"usingComponents": {
"custom-header": "../../components/custom-header/custom-header",
"service-card": "../../components/service-card/service-card",
"faq-item": "../../components/faq-item/faq-item"
}
}
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#f8f8f8",
"usingComponents": {}
}

126
pages/customer-service/detail/index.wxml

@ -1,65 +1,93 @@
<!-- pages/customer-service/detail/index.wxml -->
<view class="container">
<view class="service-detail-header">
<view class="header-back" bindtap="onBack">
<text>←</text>
<!-- 顶部导航栏 -->
<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>
<text class="header-title">客服详情</text>
<view class="header-placeholder"></view>
</view>
<view class="service-detail-content">
<view class="service-info-section">
<view class="service-avatar">
<text>💁</text>
<!-- 客服基本信息 -->
<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="service-basic-info">
<text class="service-name">客服小王</text>
<text class="service-status">在线</text>
<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 class="service-details">
<view class="detail-item">
<text class="detail-label">服务类型</text>
<text class="detail-value">在线客服</text>
</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="detail-item">
<text class="detail-label">服务时间</text>
<text class="detail-value">24小时在线</text>
<view class="info-item">
<text class="info-label">工作经验</text>
<text class="info-value">服务平台{{customerData.experience}}</text>
</view>
<view class="detail-item">
<text class="detail-label">专业领域</text>
<text class="detail-value">产品咨询、订单处理、售后支持</text>
<view class="info-item">
<text class="info-label">服务规模</text>
<text class="info-value">服务{{customerData.serviceCount}}家鸡场</text>
</view>
</view>
<view class="contact-section">
<text class="section-title">联系方式</text>
<button class="contact-button" bindtap="startChat">
<text>💬 开始聊天</text>
</button>
<button class="contact-button phone-button" open-type="makePhoneCall" phone-number="400-123-4567">
<text>📞 电话联系</text>
</button>
<button class="contact-button email-button" bindtap="sendEmail">
<text>📧 发送邮件</text>
</button>
</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 class="faq-section">
<text class="section-title">常见问题</text>
<view class="faq-item" bindtap="onFaqItemTap">
<text class="faq-question">如何查询订单状态?</text>
<text class="faq-arrow">></text>
</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="faq-item" bindtap="onFaqItemTap">
<text class="faq-question">如何申请退款?</text>
<text class="faq-arrow">></text>
<view class="performance-card">
<view class="performance-number">{{customerData.profitFarmCount}}</view>
<view class="performance-label">累计服务鸡场(家)</view>
</view>
<view class="faq-item" bindtap="onFaqItemTap">
<text class="faq-question">物流配送时间是多久?</text>
<text class="faq-arrow">></text>
<view class="performance-card">
<view class="performance-number profit-rate">{{customerData.profitIncreaseRate}}%</view>
<view class="performance-label">平均盈利增长</view>
</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>

323
pages/customer-service/detail/index.wxss

@ -1,180 +1,317 @@
/* pages/customer-service/detail/index.wxss */
.container {
background-color: #f5f5f5;
padding-bottom: 100rpx;
background-color: #f8f8f8;
min-height: 100vh;
}
.service-detail-header {
background-color: #fff;
/* 顶部导航栏 */
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 24rpx;
border-bottom: 1rpx solid #e8e8e8;
position: sticky;
align-items: center;
padding: 44rpx 30rpx 20rpx;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
position: fixed;
top: 0;
z-index: 100;
left: 0;
right: 0;
z-index: 1000;
width: 100%;
box-sizing: border-box;
}
.header-back, .header-placeholder {
width: 60rpx;
height: 60rpx;
.nav-left {
width: 80rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: bold;
justify-content: flex-start;
}
.header-title {
.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;
}
.service-detail-content {
padding: 20rpx;
.nav-right {
display: flex;
align-items: center;
gap: 30rpx;
}
.service-info-section {
.share-icon {
font-size: 32rpx;
color: #333;
}
/* 客服基本信息 */
.info-section {
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx;
padding: 120rpx 30rpx 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.header-info {
display: flex;
align-items: center;
}
.service-avatar {
width: 120rpx;
height: 120rpx;
background-color: #f0f0f0;
.avatar-container {
width: 160rpx;
height: 160rpx;
margin-right: 30rpx;
position: relative;
}
.avatar {
width: 100%;
height: 100%;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 64rpx;
margin-right: 32rpx;
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;
}
.service-basic-info {
.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;
}
.service-name {
display: block;
font-size: 36rpx;
.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;
}
.service-status {
display: block;
.company-large {
font-size: 28rpx;
color: #07C160;
color: #999;
}
.service-details {
background-color: #fff;
border-radius: 16rpx;
padding: 24rpx;
/* 核心信息卡片 */
.core-info-section {
padding: 0 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.detail-item {
.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: 16rpx 0;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.detail-item:last-child {
.info-item:last-child {
border-bottom: none;
}
.detail-label {
font-size: 28rpx;
.info-label {
font-size: 30rpx;
color: #666;
}
.detail-value {
font-size: 28rpx;
.info-value {
font-size: 30rpx;
color: #333;
text-align: right;
flex: 1;
margin-left: 40rpx;
margin-left: 30rpx;
}
.phone-number {
color: #1890ff;
}
.contact-section, .faq-section {
/* 专业技能标签 */
.skills-section {
background-color: #fff;
border-radius: 16rpx;
padding: 24rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.section-title {
display: block;
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 24rpx;
}
.contact-button {
display: block;
width: 100%;
background-color: #FF6B81;
color: #fff;
font-size: 32rpx;
font-weight: bold;
padding: 24rpx;
border-radius: 12rpx;
border: none;
margin-bottom: 16rpx;
line-height: normal;
height: auto;
margin-bottom: 20rpx;
}
.contact-button:last-child {
margin-bottom: 0;
.skills-container {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
padding-bottom: 10rpx;
}
.contact-button.phone-button {
background-color: #07C160;
.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;
}
.contact-button.email-button {
background-color: #1989fa;
.skill-tag:active {
background-color: #bae7ff;
transform: scale(0.98);
}
.contact-button:hover {
opacity: 0.9;
/* 业绩数据 */
.performance-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 120rpx;
}
.faq-item {
.performance-cards {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 0;
border-bottom: 1rpx solid #f0f0f0;
gap: 20rpx;
}
.faq-item:last-child {
border-bottom: none;
.performance-card {
flex: 1;
background-color: #f9f9f9;
padding: 24rpx;
border-radius: 16rpx;
text-align: center;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.faq-question {
font-size: 28rpx;
.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;
}
.faq-arrow {
font-size: 28rpx;
color: #999;
margin-left: 20rpx;
}
.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;
}

396
pages/customer-service/index.js

@ -1,55 +1,377 @@
// pages/customer-service/index.js
const api = require('../../utils/api');
Page({
// 客服数据,将从后端动态获取
data: {
serviceList: []
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 [];
}
},
onLoad: function (options) {
this.loadServiceInfo();
// 辅助函数:生成随机区域
getRandomArea() {
const areas = ['华北区', '华东区', '华南区', '全国', '西南区', '西北区', '东北区'];
return areas[Math.floor(Math.random() * areas.length)];
},
loadServiceInfo: function () {
// 模拟加载客服信息
const serviceList = [
{
id: 1,
type: 'phone',
name: '电话客服',
desc: '周一至周日 9:00-18:00',
number: '400-123-4567'
},
{
id: 2,
type: 'online',
name: '在线客服',
desc: '24小时在线为您服务'
},
{
id: 3,
type: 'email',
name: '邮箱客服',
desc: 'service@example.com'
// 辅助函数:生成随机工作经验
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;
},
// 定期刷新客服列表(每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 () {
// 加载客服列表
this.loadCustomerServices();
// 启动定期刷新
this.startPeriodicRefresh();
// 检查当前用户身份
this.checkUserType();
},
/**
* 检查当前用户身份
*/
checkUserType: function() {
const app = getApp();
const userInfo = app.globalData.userInfo || {};
const isManager = userInfo.userType === 'manager' || userInfo.type === 'manager';
console.log('当前用户身份检查:', { isManager, userType: userInfo.userType });
this.setData({
isCurrentUserManager: isManager
});
},
onShow() {
// 更新自定义tabBar状态
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({
selected: -1 // 不选中任何tab
});
}
// 当页面显示时重新加载数据,确保数据最新
this.loadCustomerServices();
},
onUnload: function() {
// 停止定期刷新
this.stopPeriodicRefresh();
},
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({
serviceList: serviceList
filteredServices: filtered
});
},
startOnlineChat: function () {
// 跳转到在线聊天页面
wx.showToast({
title: '在线客服功能开发中',
icon: 'none'
onChat: function (e) {
const id = e.currentTarget.dataset.id;
const service = this.data.customerServices.find(item => item.id === id);
if (!service) return;
// 获取当前用户的手机号
let userPhone = '';
try {
// 尝试从不同的存储位置获取手机号
const userInfo = wx.getStorageSync('userInfo');
if (userInfo && userInfo.phoneNumber) {
userPhone = userInfo.phoneNumber;
} else {
// 尝试从其他可能的存储位置获取
const users = wx.getStorageSync('users') || {};
const userId = wx.getStorageSync('userId');
if (userId && users[userId] && users[userId].phoneNumber) {
userPhone = users[userId].phoneNumber;
} else {
userPhone = wx.getStorageSync('phoneNumber');
}
}
} catch (e) {
console.error('获取用户手机号失败:', e);
}
console.log('当前用户手机号:', userPhone);
console.log('客服手机号:', service.phoneNumber);
// 验证手机号
if (!userPhone) {
wx.showToast({
title: '请先登录获取手机号',
icon: 'none'
});
return;
}
// 显示加载提示
wx.showLoading({
title: '正在建立聊天...',
});
// 调用API添加聊天记录
api.addChatRecord(userPhone, service.phoneNumber).then(res => {
console.log('添加聊天记录成功:', res);
// 隐藏加载提示
wx.hideLoading();
// 确保使用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 });
}).catch(err => {
console.error('添加聊天记录失败:', err);
// 隐藏加载提示
wx.hideLoading();
wx.showToast({
title: '建立聊天失败,请重试',
icon: 'none'
});
});
},
sendEmail: function () {
// 打开邮件客户端
wx.showToast({
title: '邮箱客服功能开发中',
icon: 'none'
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();
}
});
});

6
pages/customer-service/index.json

@ -1,3 +1,5 @@
{
"navigationBarTitleText": "客服中心"
}
"navigationBarBackgroundColor": "#f8f8f8",
"navigationBarTextStyle": "black",
"usingComponents": {}
}

113
pages/customer-service/index.wxml

@ -1,40 +1,89 @@
<!-- pages/customer-service/index.wxml -->
<view class="container">
<view class="service-header">
<text class="service-title">客服中心</text>
<!-- 顶部导航栏 -->
<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="service-content">
<view class="service-item">
<view class="service-icon">
<text>📞</text>
</view>
<view class="service-info">
<text class="service-name">电话客服</text>
<text class="service-desc">周一至周日 9:00-18:00</text>
</view>
<button class="call-button" open-type="makePhoneCall" phone-number="400-123-4567">呼叫</button>
<!-- 搜索区域 -->
<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="service-item">
<view class="service-icon">
<text>💬</text>
<view class="filter-area">
<view class="area-picker" bindtap="onAreaFilter">
<text>区域</text>
<text class="picker-arrow">▼</text>
</view>
<view class="service-info">
<text class="service-name">在线客服</text>
<text class="service-desc">24小时在线为您服务</text>
</view>
<button class="chat-button" bindtap="startOnlineChat">在线咨询</button>
</view>
<view class="service-item">
<view class="service-icon">
<text>📧</text>
</view>
<view class="service-info">
<text class="service-name">邮箱客服</text>
<text class="service-desc">service@example.com</text>
</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 wx:if="{{!isCurrentUserManager}}" 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>
<button class="email-button" bindtap="sendEmail">发送邮件</button>
</block>
<view wx:else class="empty-state">
<text>👤</text>
<text class="empty-text">暂无匹配的经纪人</text>
</view>
</view>
</view>
</view>

381
pages/customer-service/index.wxss

@ -1,80 +1,383 @@
/* pages/customer-service/index.wxss */
.container {
background-color: #f5f5f5;
padding-bottom: 100rpx;
background-color: #f8f8f8;
min-height: 100vh;
}
.service-header {
background-color: #fff;
padding: 20rpx;
border-bottom: 1rpx solid #e8e8e8;
/* 顶部导航栏 */
.nav-bar {
display: flex;
justify-content: space-between;
align-items: center;
justify-content: 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;
}
.service-title {
.nav-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
flex: 1;
text-align: center;
}
.nav-right {
display: flex;
align-items: center;
gap: 30rpx;
}
.service-content {
padding: 20rpx;
.search-icon, .settings-icon {
font-size: 32rpx;
color: #333;
}
.service-item {
/* 搜索区域 */
.search-container {
background-color: #fff;
border-radius: 12rpx;
padding: 24rpx;
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;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
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;
}
.service-icon {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background-color: #f0f0f0;
.area-picker {
display: flex;
align-items: center;
justify-content: 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;
font-size: 40rpx;
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;
}
.service-info {
.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;
}
.service-name {
display: block;
.name-row {
display: flex;
align-items: center;
margin-bottom: 8rpx;
flex-wrap: wrap;
}
.name {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
margin-right: 12rpx;
}
.service-desc {
display: block;
font-size: 26rpx;
.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;
}
.call-button, .chat-button, .email-button {
padding: 12rpx 32rpx;
border-radius: 24rpx;
.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;
background-color: #FF6B81;
color: #fff;
border: none;
margin: 0;
line-height: normal;
height: auto;
color: #333;
display: block;
}
.call-button:hover, .chat-button:hover, .email-button:hover {
background-color: #FF526D;
}
.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;
}

153
server-example/server-mysql.js

@ -6481,9 +6481,92 @@ app.get('/api/online-stats', async (req, res) => {
});
}
});
// REST API接口 - 获取客服列表 - 修改为按照用户需求:工位名为采购员且有电话号码
app.get('/api/managers', async (req, res) => {
try {
// 查询userlogin数据库中的personnel表,获取工位为销售员且有电话号码的用户
// 根据表结构选择所有需要的字段
const [personnelData] = await sequelize.query(
'SELECT id, managerId, managercompany, managerdepartment, organization, projectName, name, alias, phoneNumber, avatarUrl FROM userlogin.personnel WHERE projectName = ? AND phoneNumber IS NOT NULL AND phoneNumber != "" ORDER BY id ASC',
{ replacements: ['销售员'] }
);
// 查询chat_online_status表获取客服在线状态(添加错误处理)
let onlineStatusData = [];
try {
const [statusResults] = await sequelize.query(
'SELECT userId, is_online FROM chat_online_status WHERE type = 2', // type=2表示客服
{ type: sequelize.QueryTypes.SELECT }
);
onlineStatusData = statusResults || [];
} catch (statusError) {
console.warn('获取在线状态失败(可能是表不存在):', statusError.message);
// 表不存在或查询失败时,使用空数组
onlineStatusData = [];
}
// 创建在线状态映射
const onlineStatusMap = {};
// 检查onlineStatusData是否存在,防止undefined调用forEach
if (Array.isArray(onlineStatusData)) {
onlineStatusData.forEach(status => {
onlineStatusMap[status.userId] = status.is_online === 1;
});
}
// 将获取的数据映射为前端需要的格式,添加online状态(综合考虑内存中的onlineManagers和数据库中的状态)
const isManagerOnline = (id, managerId) => {
// 转换ID为字符串以便正确比较
const stringId = String(id);
const stringManagerId = managerId ? String(managerId) : null;
console.log(`检查客服在线状态: id=${id}(${stringId}), managerId=${managerId}(${stringManagerId})`);
// 首先从内存中的onlineManagers检查(实时性更好)
if (onlineManagers && typeof onlineManagers.has === 'function') {
// 检查id或managerId是否在onlineManagers中
if (onlineManagers.has(stringId) || (stringManagerId && onlineManagers.has(stringManagerId))) {
console.log(`客服在线(内存检查): id=${id}`);
return true;
}
}
// 其次从数据库查询结果检查
const dbStatus = onlineStatusMap[stringId] || (stringManagerId && onlineStatusMap[stringManagerId]) || false;
console.log(`客服在线状态(数据库): id=${id}, status=${dbStatus}`);
return dbStatus;
};
const managers = personnelData.map((person, index) => ({
id: person.id,
managerId: person.managerId || `PM${String(index + 1).padStart(3, '0')}`,
managercompany: person.managercompany || '未知公司',
managerdepartment: person.managerdepartment || '采购部',
organization: person.organization || '采购组',
projectName: person.projectName || '采购员',
name: person.name || '未知',
alias: person.alias || person.name || '未知',
phoneNumber: person.phoneNumber || '',
avatar: person.avatarUrl || '', // 使用表中的avatarUrl字段
online: isManagerOnline(person.id, person.managerId) // 综合检查在线状态
}));
res.status(200).json({
success: true,
code: 200,
data: managers
});
} catch (error) {
console.error('获取客服列表失败:', error);
res.status(500).json({
success: false,
code: 500,
message: '获取客服列表失败: ' + error.message
});
}
});
// 认证处理函数
async function handleAuth(ws, data) {
@ -7975,6 +8058,72 @@ app.post('/api/chat/list', async (req, res) => {
}
});
// 新增:添加聊天记录到chat_list表
app.post('/api/chat/add', async (req, res) => {
try {
const { user_phone, manager_phone } = req.body;
console.log('添加聊天记录 - user_phone:', user_phone, 'manager_phone:', manager_phone);
if (!user_phone || !manager_phone) {
return res.status(400).json({
success: false,
code: 400,
message: '用户手机号和业务员手机号不能为空'
});
}
// 检查chat_list表是否存在,如果不存在则创建
try {
await sequelize.query(
`CREATE TABLE IF NOT EXISTS chat_list (
id INT AUTO_INCREMENT PRIMARY KEY,
user_phone VARCHAR(20) NOT NULL,
manager_phone VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_phone (user_phone),
INDEX idx_manager_phone (manager_phone)
) ENGINE=InnoDB;`
);
console.log('✅ chat_list表检查/创建成功');
} catch (createError) {
console.warn('创建chat_list表时出错(可能已存在):', createError.message);
}
// 插入两条记录:
// 1. 用户手机号 -> 业务员手机号
await sequelize.query(
'INSERT INTO chat_list (user_phone, manager_phone) VALUES (?, ?)',
{ replacements: [user_phone, manager_phone] }
);
// 2. 业务员手机号 -> 用户手机号
await sequelize.query(
'INSERT INTO chat_list (user_phone, manager_phone) VALUES (?, ?)',
{ replacements: [manager_phone, user_phone] }
);
console.log('✅ 成功插入两条聊天记录');
res.status(200).json({
success: true,
code: 200,
message: '聊天记录添加成功',
data: {
user_phone,
manager_phone
}
});
} catch (error) {
console.error('添加聊天记录失败:', error);
res.status(500).json({
success: false,
code: 500,
message: '添加聊天记录失败: ' + error.message
});
}
});
// 新增:获取业务员信息接口 - 根据手机号查询
app.post('/api/personnel/get', async (req, res) => {
try {

27
utils/api.js

@ -924,6 +924,33 @@ module.exports = {
return this.uploadProductWithRecursiveImages(uploadData, imageUrls);
},
// 添加聊天记录 - 为用户和客服创建双向聊天记录
addChatRecord: function (user_phone, manager_phone) {
console.log('API.addChatRecord - user_phone:', user_phone, 'manager_phone:', manager_phone);
if (!user_phone || !manager_phone) {
return Promise.reject(new Error('用户手机号和客服手机号不能为空'));
}
return request('/api/chat/add', 'POST', {
user_phone: user_phone,
manager_phone: manager_phone
}).then(res => {
console.log('添加聊天记录服务器响应:', res);
if (res && (res.success || res.code === 200)) {
console.log('添加聊天记录成功');
return res;
} else {
console.error('添加聊天记录失败,服务器返回:', res);
let errorMessage = res && res.message ? res.message : '添加聊天记录失败';
return Promise.reject(new Error(errorMessage));
}
}).catch(err => {
console.error('添加聊天记录请求失败:', err);
console.error('错误详情:', { message: err.message, statusCode: err.statusCode, responseData: err.responseData });
return Promise.reject(new Error('添加聊天记录失败,请稍后重试'));
});
},
// 上传带图片的商品 - 改进版,确保所有图片都被实际上传到服务器
uploadProductWithImages: function (productData, imageUrls) {
console.log('===== 开始上传带图片的商品 =====');

Loading…
Cancel
Save