客服部分

This commit is contained in:
赵毅 2025-09-24 08:37:16 +08:00
parent 6b9ad175d6
commit 5f6e76ea71
21 changed files with 3591 additions and 5307 deletions

6
api/customerService.js Normal file
View File

@ -0,0 +1,6 @@
export const apiArr = {
csGetMsgList: "/api/v2/wechat/contactServer/getMsgList", //获取联系客服消息列表
csGetMsgRecord: "/api/v2/wechat/contactServer/getMsgRecord", //获取联系客服消息记录
csGetToClientId: "/api/v2/wechat/contactServer/getToClientId", //获取联系客服客户端ID
csGetMchContactList: "/api/v2/wechat/contactServer/mchContactList", //商户客服列表
};

View File

@ -21,6 +21,8 @@ export const apiArr = {
createPay:"/api/v2/wechat/quick-payment-record-crud/creat",//创建支付信息 createPay:"/api/v2/wechat/quick-payment-record-crud/creat",//创建支付信息
getPayInfo:"/api/v2/wechat/quick-payment-record-crud/info", //支付记录 getPayInfo:"/api/v2/wechat/quick-payment-record-crud/info", //支付记录
getPreOrderInfo:"/api/v2/wechat/quick-payment-record-crud/preorder", //预下单
tradeQuery:"/api/v2/wechat/quick-payment-record-crud/trade-query", //查单
createStore:"/api/v2/wechat/store-info-crud/creat",//门店信息创建 createStore:"/api/v2/wechat/store-info-crud/creat",//门店信息创建

5244
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,5 +15,9 @@
"前端组件", "前端组件",
"通用组件" "通用组件"
] ]
},
"dependencies": {
"abort-controller": "^3.0.0",
"mqtt": "^3.0.0"
} }
} }

View File

@ -0,0 +1,291 @@
.group-purchase-container {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
max-width: 750rpx;
margin: 0 auto;
background-color: #ffffff;
}
/* 顶部横幅 */
.banner-content {
z-index: 1;
}
.banner-title {
font-size: 36rpx;
font-weight: bold;
margin-bottom: 10rpx;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
}
.banner-subtitle {
font-size: 18rpx;
background-color: rgba(255, 255, 255, 0.2);
padding: 5rpx 10rpx;
border-radius: 4rpx;
display: inline-block;
}
/* 商品列表 */
.goods-list {
background-color: #ffffff;
padding: 30rpx 20rpx;
height: 80vh;
overflow-y: auto;
}
.goods-item {
border-radius: 10rpx;
padding: 15rpx;
margin-bottom: 25rpx;
display: flex;
}
.goods-item2 {
border-radius: 10rpx;
padding: 15rpx;
margin-bottom: 25rpx;
display: flex;
}
.goods-image {
width: 140rpx;
height: 140rpx;
border-radius: 15rpx;
overflow: hidden;
margin-right: 15rpx;
}
.goods-image image {
width: 100%;
height: 100%;
object-fit: cover;
}
.goods-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
}
.goods-name {
font-size: 28rpx;
font-weight: bold;
margin-bottom: 8rpx;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.goods-desc {
font-size: 22rpx;
color: #666;
margin-bottom: 10rpx;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.goods-price {
font-size: 28rpx;
color: #e63946;
margin-bottom: 10rpx;
}
.price-container {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10rpx;
}
.group-price {
font-size: 28rpx;
color: #e63946;
margin-right: 10rpx;
border-radius: 4rpx;
display: flex;
}
.group-price1{
width: 100rpx;
padding: 10rpx 15rpx;
color: #ffffff;
background-color: #fc5d15;
border-radius: 15rpx 0 0 15rpx;
}
.group-price2{
width: auto;
padding: 10rpx 15rpx;
background: linear-gradient(to bottom, #fef6d6, #fee8a9);
border-radius: 0 15rpx 15rpx 0;
}
.original-price {
font-size: 26rpx;
color: #999;
}
.countdown {
width: auto;
font-size: 22rpx;
padding: 10rpx 20rpx;
color: #ffffff;
margin-bottom: 15rpx;
border-radius: 50rpx;
background-color: #fe2f01;
position: absolute;
/* top: 120rpx; */
bottom: -30rpx;
right: 0;
}
.quantity-control {
display: flex;
align-items: center;
justify-content: flex-end;
}
.decrease-btn {
width: 40rpx;
height: 40rpx;
background-color: #f5f5f5;
border: 1rpx solid #ccc;
border-radius: 50%;
line-height: 33rpx;
text-align: center;
font-size: 28rpx;
color: #333;
}
.increase-btn {
width: 40rpx;
height: 40rpx;
background-color: #ff502a;
border-radius: 50%;
line-height: 33rpx;
text-align: center;
font-size: 28rpx;
color: #ffffff;
}
.quantity {
margin: 0 15rpx;
font-size: 24rpx;
width: 40rpx;
text-align: center;
}
/* 购物车按钮 */
.shop_car {
width: 140rpx;
height: 140rpx;
position: fixed;
right: 33rpx;
bottom: 80rpx;
z-index: 10;
}
.shop_car image {
width: 100%;
height: 100%;
}
.u-badge {
position: absolute;
right: 0;
top: -10rpx;
}
/* 规格标签样式 */
.specification-tag {
width: 130rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #feeceb;
padding: 10rpx 20rpx;
border-radius: 40rpx;
margin: 15rpx 0;
font-size: 26rpx;
color: #dd4020;
}
.specification-tag text {
margin-right: 5rpx;
}
/* 货品列表样式 */
.sku-list {
margin-top: 10rpx;
padding: 15rpx;
background-color: #f9f9f9;
border-radius: 10rpx;
}
.sku-item{
position: relative;
margin-bottom: 30rpx;
}
.sku-info{
display: flex;
align-items: center;
justify-content: center;
}
.sku-image{
width: 100rpx;
height: 100rpx;
border-radius: 15rpx;
overflow: hidden;
margin-right: 15rpx;
}
.sku-price {
font-size: 26rpx;
color: #e63946;
margin-right: 10rpx;
border-radius: 4rpx;
display: flex;
margin-top: 15rpx;
}
.sku-price1{
width: auto;
padding: 10rpx 15rpx;
color: #ffffff;
background-color: #fc5d15;
border-radius: 15rpx 0 0 15rpx;
}
.sku-price2{
width: auto;
padding: 10rpx 15rpx;
background: linear-gradient(to bottom, #fef6d6, #fee8a9);
border-radius: 0 15rpx 15rpx 0;
}
.sku-control {
display: flex;
align-items: center;
justify-content: flex-end;
margin-top: 15rpx;
}
.sku-countdown {
width: auto;
font-size: 22rpx;
padding: 5rpx 20rpx;
color: #ffffff;
margin-bottom: 15rpx;
border-radius: 50rpx;
background-color: #fe2f01;
position: absolute;
top: 170rpx;
right: 0;
}

View File

@ -0,0 +1,345 @@
<template>
<view class="group-purchase-container">
<!-- 商品列表 -->
<view class="goods-list">
<!-- 商品项 -->
<view v-for="(item, index) in goodsList" :key="index">
<!-- 有多个货品 -->
<view class="goods-item" v-if="item.group_buy_goods_list.length > 1">
<view class="goods-image">
<image :src="item.commodity_pic" mode="aspectFill"></image>
</view>
<view class="goods-info">
<view class="goods-name">{{ item.commodity_name }}</view>
<view class="goods-desc">{{ item.commodity_intro }}</view>
<view class="goods-price"> {{ getPriceRange(item.group_buy_goods_list) }} </view>
<!-- 选择规格标签 -->
<view class="specification-tag" @click.stop="toggleSkuList(index)">
<text>{{ item.showSkuList ? '收起' : '选择规格' }}</text>
<u-icon name="arrow-down" size="26rpx" color="#FF370B" v-if="!item.showSkuList"></u-icon>
<u-icon name="arrow-up" size="26rpx" color="#FF370B" v-else></u-icon>
</view>
<!-- 货品列表 -->
<view class="sku-list" v-if="item.showSkuList">
<view v-for="(sku, skuIndex) in item.group_buy_goods_list" :key="skuIndex">
<view class="sku-item" @click="toDetail(sku)">
<view class="sku-info">
<view class="sku-image">
<image :src="sku.commodity_pic" mode="aspectFill"></image>
</view>
<view class="goods-info">
<view class="goods-name">{{ sku.goods_name }}</view>
<view class="goods-desc">{{ sku.goods_spec }}</view>
</view>
</view>
<view class="price-container">
<view class="sku-price">
<view class="sku-price1">团购价</view>
<view class="sku-price2">¥{{ sku.group_buy_price }}/{{ sku.goods_unit }}
</view>
</view>
<view class="sku-control">
<view class="decrease-btn" @tap.stop="decreaseQuantity(index, skuIndex)">-
</view>
<view class="quantity">{{ sku.quantity }}</view>
<view class="increase-btn" @tap.stop="increaseQuantity(index, skuIndex)">+
</view>
</view>
</view>
<view class="original-price">单买价 ¥{{ sku.sales_price }}/{{ sku.goods_unit }}</view>
<view class="sku-countdown">
{{
getEndTheCountdown(sku.group_buy_activity_info.end_time)
}}
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 只有一个货品 -->
<view v-else>
<view class="goods-item2" @click="toDetail(item)">
<view class="goods-image">
<image :src="item.commodity_pic" mode="aspectFill"></image>
</view>
<view class="goods-info">
<view class="goods-name">{{ item.commodity_name }}</view>
<view class="goods-desc">{{ item.commodity_intro }}</view>
<view class="price-container">
<view class="group-price">
<view class="group-price1">团购价</view>
<view class="group-price2">¥{{ item.group_buy_goods_list[0].group_buy_price }}/{{
item.group_buy_goods_list[0].goods_unit }}</view>
</view>
<view class="quantity-control">
<view class="decrease-btn" @tap.stop="decreaseQuantity(index, 0)">-</view>
<view class="quantity">{{ item.group_buy_goods_list[0].quantity }}</view>
<view class="increase-btn" @tap.stop="increaseQuantity(index, 0)">+</view>
</view>
</view>
<view class="original-price">单买价 ¥{{ item.group_buy_goods_list[0].sales_price }}/{{
item.group_buy_goods_list[0].goods_unit }}</view>
<view class="countdown">
{{ getEndTheCountdown(item.group_buy_goods_list[0].group_buy_activity_info.end_time) }}
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 购物车按钮 -->
<view class="shop_car" @click="shopCar">
<u-badge numberType="limit" type="error" max="99" :value="carNum"></u-badge>
<image src="https://wechat-img-file.oss-cn-beijing.aliyuncs.com/property-img-file/shop_car_num.png"></image>
</view>
</view>
</template>
<script>
import { picUrl, menuButtonInfo, request, NavgateTo } from "../../../utils";
import { apiArr } from '@/api/groupPurchase.js'
import { apiArr as shopApi } from "../../../api/shop.js";
export default {
data() {
return {
goodsList: [],
carNum: 0,
quantity: 0,
timer: null, // 定时器ID
endTime: '', // 初始化结束时间
updateTime: Date.now(), // 用于触发倒计时更新的时间戳
goodsDetail: []
};
},
onLoad() {
// this.getGoodsList()
},
onShow() {
// 在页面显示时启动定时器
if (!this.timer) {
this.timer = setInterval(() => {
// 更新时间戳,触发页面重新渲染
this.updateTime = Date.now();
}, 1000);
}
// 确保getGoodsList和getShopdetail都执行完毕后再执行getGoodsNum
Promise.all([
// 确保getGoodsList已完成
this.goodsList.length > 0 ? Promise.resolve() : this.getGoodsList(),
// 调用getShopdetail并等待其完成
this.getShopdetail()
]).then(() => {
this.getGoodsNum();//获取货品在购物车中的数量
});
},
methods: {
getGoodsList() {
if(!uni.getStorageSync('userId')){
uni.showToast({
title: '请先登录',
icon: 'none'
})
return
}
const params = {
user_id: uni.getStorageSync('userId')
}
return request(apiArr.groupBuyList, 'POST', params).then(res => {
const list = res.group_buy_list.map(item => {
// 为每个货品初始化quantity
const group_buy_goods_list = item.group_buy_goods_list.map(sku => ({
...sku,
commodity_pic: picUrl + sku.commodity_pic,
quantity: 0
}));
return {
...item,
commodity_pic: picUrl + item.commodity_pic,
showSkuList: false,
group_buy_goods_list
}
})
this.goodsList = list
return res;
})
},
getShopdetail() {
const params = {
is_group_buy: 1,
}
return request(shopApi.getCar, "POST", params).then((res) => {
this.carNum = res.total;
// 合并当日达和普通商品数据
this.goodsDetail = [].concat(res.same_day_cart_list, res.normal_cart_list)
.flatMap(supplier => supplier.commodity_cart_and_goods_model);
return res;
});
},
getGoodsNum() {
if (!this.goodsDetail || !this.goodsList || this.goodsList.length === 0) {
return;
}
// 遍历所有商品
this.goodsList.forEach(goods => {
// 遍历商品的所有货品
goods.group_buy_goods_list.forEach(sku => {
// 在购物车数据中查找对应的货品
const matchedItem = this.goodsDetail.find(item => item.goods_id === sku.goods_id);
// 如果找到匹配项,更新quantity
if (matchedItem) {
sku.quantity = matchedItem.count;
} else {
// 如果没有找到,设置为0
sku.quantity = 0;
}
});
});
},
toDetail(itemObj) {
const item = {
...itemObj,
groupById: itemObj.group_buy_activity_info.id
};
NavgateTo(`/packages/shop/groupPurchaseDetail/index?item=${JSON.stringify(item)}`)
},
// 获取商品价格范围
getPriceRange(goodsList) {
if (!goodsList || goodsList.length === 0) return '¥0';
const prices = goodsList.map(item => Number(item.sales_price));
const minPrice = Math.min(...prices);
const maxPrice = Math.max(...prices);
return minPrice === maxPrice ? `¥${minPrice}` : `¥${minPrice} ~ ¥${maxPrice}`;
},
// 展开/收起货品列表
toggleSkuList(index) {
this.goodsList[index].showSkuList = !this.goodsList[index].showSkuList;
},
// 增加货品数量
increaseQuantity(goodsIndex, skuIndex) {
if (this.goodsList[goodsIndex].group_buy_goods_list[skuIndex].quantity == 0) {
this.goodsList[goodsIndex].group_buy_goods_list[skuIndex].quantity += this.goodsList[goodsIndex].group_buy_goods_list[skuIndex].min_order_quantity
this.carNum += this.goodsList[goodsIndex].group_buy_goods_list[skuIndex].min_order_quantity
} else {
if (this.goodsList[goodsIndex].group_buy_goods_list[skuIndex].quantity == this.goodsList[goodsIndex].group_buy_goods_list[skuIndex].total_stock) {
uni.showToast({
title: '库存不足',
icon: 'none'
});
return
}
if (this.goodsList[goodsIndex].group_buy_goods_list[skuIndex].quantity == this.goodsList[goodsIndex].group_buy_goods_list[skuIndex].max_limit_quantity) {
uni.showToast({
title: '一次最多购买' + this.goodsList[goodsIndex].group_buy_goods_list[skuIndex].max_limit_quantity + '件',
icon: 'none'
});
return
}
this.goodsList[goodsIndex].group_buy_goods_list[skuIndex].quantity++;
this.carNum++;
}
const params = {
goods_id_and_count: [
{
goods_id: this.goodsList[goodsIndex].group_buy_goods_list[skuIndex].id,
count: this.goodsList[goodsIndex].group_buy_goods_list[skuIndex].quantity,
},
],
group_buy_id: this.goodsList[goodsIndex].group_buy_goods_list[skuIndex].group_buy_activity_info.id
}
this.updateCar(params);
},
// 减少货品数量
decreaseQuantity(goodsIndex, skuIndex) {
if (this.goodsList[goodsIndex].group_buy_goods_list[skuIndex].quantity > 0) {
if (this.goodsList[goodsIndex].group_buy_goods_list[skuIndex].quantity == this.goodsList[goodsIndex].group_buy_goods_list[skuIndex].min_order_quantity) {
this.goodsList[goodsIndex].group_buy_goods_list[skuIndex].quantity = 0
this.carNum = 0
} else {
this.goodsList[goodsIndex].group_buy_goods_list[skuIndex].quantity--;
this.carNum--;
}
const params = {
goods_id_and_count: [
{
goods_id: this.goodsList[goodsIndex].group_buy_goods_list[skuIndex].id,
count: this.goodsList[goodsIndex].group_buy_goods_list[skuIndex].quantity,
},
],
group_buy_id: this.goodsList[goodsIndex].group_buy_goods_list[skuIndex].group_buy_activity_info.id
}
this.updateCar(params);
} else {
uni.showToast({
title: '已经没有了...',
icon: 'none'
});
}
},
// 请求更改购物车接口
async updateCar(params) {
return request(shopApi.updateCar, "POST", params).then((res) => {
this.getShopdetail();
uni.showToast({
title: "操作成功!",
success() { },
});
});
},
// 跳转到购物车
shopCar() {
const item = {
is_group_buy: 1,
}
NavgateTo("../shopCar/index?item=" + JSON.stringify(item));
},
// 计算距离结束日期的剩余时间
getEndTheCountdown(endTime) {
// 获取当前时间和结束时间的时间戳
const now = new Date().getTime();
const end = new Date(endTime).getTime();
// 计算时间差(毫秒)
let diff = end - now;
// 如果已经结束,返回提示
if (diff <= 0) {
return '团购已结束';
}
// 计算天、小时、分钟
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
diff -= days * (1000 * 60 * 60 * 24);
const hours = Math.floor(diff / (1000 * 60 * 60));
diff -= hours * (1000 * 60 * 60);
const minutes = Math.floor(diff / (1000 * 60));
diff -= minutes * (1000 * 60);
// 返回格式化的字符串
return `${days}天${hours}小时${minutes}分钟后结束`;
}
},
onHide() {
// 清除定时器
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
}
};
</script>
<style>
@import url("./index.css");
</style>

View File

@ -0,0 +1,26 @@
{
"defaultCategoryList": [
{
"id": 1,
"category_name": "物业介绍"
},
{
"id": 2,
"category_name": "物业缴费"
},
{
"id": 3,
"category_name": "物业公积金"
},
{
"id": 4,
"category_name": "报事报修"
}
],
"defaultInfoList": {
"1": "11",
"2": "22",
"3": "33",
"4": "44"
}
}

View File

@ -43,6 +43,26 @@
</u-grid-item> </u-grid-item>
</u-grid> </u-grid>
</view> </view>
<!-- <view class="tabs" v-if="defaultCategoryList.length > 0">
<view v-for="(item, index) in defaultCategoryList" :key="index"
:class="['tabItem', selectedTab === index ? 'active2' : '']" @click="selectTab(index, item)">
{{ item.category_name }}
</view>
</view>
<view class="newsList" v-if="defaultCategoryList.length > 0">
<view class="newsItem" v-for="item in defaultInfoList" @click="detail(item)" :key="item.id">
<view class="newsItem_left">
<view class="newsItem_left_tit">{{ item.title }}</view>
<view class="newsItem_left_sub">{{ item.author }}</view>
</view>
<view class="newsItem_right">
<image :src="item.list_image" mode="aspectFill" />
</view>
</view>
</view> -->
</view> </view>
<view v-else> <view v-else>
<view class="searchBox" :style="{ height: localHeight + 'px', paddingTop: top + 'px' }"> <view class="searchBox" :style="{ height: localHeight + 'px', paddingTop: top + 'px' }">
@ -158,6 +178,9 @@ import {
import { apiArr } from "../../../api/v2Community"; import { apiArr } from "../../../api/v2Community";
import { apiArr as apiArr2 } from "../../../api/community"; import { apiArr as apiArr2 } from "../../../api/community";
// 引入数据文件
import dataJson from './data.json';
export default { export default {
data() { data() {
return { return {
@ -242,6 +265,10 @@ export default {
isShowBill: false, isShowBill: false,
houseVal: "", houseVal: "",
// 从JSON文件中获取默认数据
defaultCategoryList: dataJson.defaultCategoryList,
defaultInfoList: dataJson.defaultInfoList,
}; };
}, },
async onLoad(options) { async onLoad(options) {

View File

@ -485,13 +485,43 @@ export default {
// this.payInfoId // this.payInfoId
await request(apiArr.OrderPay, "POST", { order_pay_id: this.payInfoId }).then( await request(apiArr.OrderPay, "POST", { order_pay_id: this.payInfoId }).then(
async (res) => { async (res) => {
const params = { if (res && res.timeStamp && res.nonceStr && res.package && res.signType && res.paySign) {
order_pay_id: this.payInfoId, // 调用微信支付
} uni.requestPayment({
await request(apiArr.OrderPay, "POST", params).then( timeStamp: res.timeStamp,
(res) => { nonceStr: res.nonceStr,
package: res.package,
signType: res.signType,
paySign: res.paySign,
success: (payRes) => {
const params = {
order_pay_id: this.payInfoId,
}
request(apiArr.tradeQuery, "POST", params).then(res => {
})
},
fail: (payErr) => {
uni.showToast({
title: payErr.errMsg == 'requestPayment:fail cancel' ? '已取消支付' : '支付失败',
icon: 'none'
})
const params = {
order_ids: orderId,
}
request(apiArr.cancelPay, "POST", params).then(res => {
})
},
complete: () => {
// 支付完成后的回调,无论成功失败都会执行
}
})
} else {
console.error("获取支付参数失败,缺少必要参数")
uni.showToast({
title: '获取支付信息失败',
icon: 'none'
})
} }
);
} }
); );
}, },

View File

@ -0,0 +1,160 @@
page{
background-color: #f6f7fb;
}
/* 切换客服页面样式 */
.change-service-container {
display: flex;
flex-direction: column;
}
/* 头部 */
.change-service-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 50px;
padding: 0 15px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding-bottom: 15rpx;
}
.back-btn {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.page-title {
font-size: 16px;
font-weight: 500;
color: #333;
}
.empty-header {
width: 40px;
}
/* 客服列表 */
.service-list {
flex: 1;
padding: 10px;
overflow-y: auto;
}
/* 加载状态 */
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 100px;
font-size: 14px;
color: #999;
}
/* 空状态 */
.empty-service {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
font-size: 14px;
color: #999;
}
/* 客服项 */
.service-item {
display: flex;
align-items: center;
padding: 15px;
margin-bottom: 10px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.service-item:active {
background-color: #f0f0f0;
}
.service-item.selected {
border: 2px solid #07c160;
}
/* 客服头像 */
.service-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
margin-right: 15px;
}
/* 客服信息 */
.service-info {
flex: 1;
}
.service-name {
display: block;
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 5px;
}
.service-desc {
font-size: 14px;
color: #666;
line-height: 1.4;
}
/* 选中图标 */
.selected-icon {
margin-left: 10px;
}
/* 确认按钮区域 */
.confirm-section {
padding: 15px;
background-color: #fff;
border-top: 1px solid #eee;
}
.confirm-btn {
width: 100%;
height: 45px;
background-color: #07c160;
color: #fff;
font-size: 16px;
border-radius: 25px;
display: flex;
align-items: center;
justify-content: center;
}
.confirm-btn:disabled {
background-color: #ccc;
color: #fff;
}
/* 滚动条样式 */
.service-list::-webkit-scrollbar {
width: 6px;
}
.service-list::-webkit-scrollbar-track {
background-color: #f1f1f1;
border-radius: 3px;
}
.service-list::-webkit-scrollbar-thumb {
background-color: #c1c1c1;
border-radius: 3px;
}
.service-list::-webkit-scrollbar-thumb:hover {
background-color: #a8a8a8;
}

View File

@ -0,0 +1,124 @@
<template>
<view class="change-service-container">
<view class="change-service-header" :style="{ paddingTop: top + 'px', height: localHeight + 'px' }">
<view class="back-btn" @tap="goBack">
<uni-icons type="left" size="28" color="#333"></uni-icons>
</view>
<view class="page-title">选择客服</view>
<view class="empty-header"></view>
</view>
<!-- 客服列表 -->
<view class="service-list">
<view v-if="isLoading" class="loading">加载中...</view>
<view v-else-if="serviceList.length === 0" class="empty-service">暂无客服</view>
<view v-else>
<view v-for="service in serviceList" :key="service.id">
<view class="service-item" @tap="confirmChange(service)">
<image class="service-avatar" :src="service.employee_image" mode="aspectFill"></image>
<view class="service-info">
<text class="service-name">{{ service.employee_name }}</text>
<text class="service-desc">{{ service.expertise || '专业客服为您服务' }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import {
request,
picUrl,
uniqueByField,
menuButtonInfo,
NavgateTo
} from '../../../utils';
import { apiArr } from '@/api/customerService';
export default {
data() {
return {
localHeight: "",
top: "",
// 客服列表
serviceList: [],
// 是否加载中
isLoading: false,
// 当前选中的客服ID
selectedServiceId: ''
};
},
onLoad(options) {
const meun = menuButtonInfo();
this.top = meun.top;
this.localHeight = meun.height;
// 从选项中获取当前选中的客服ID(如果有)
if (options.currentMchId) {
this.selectedServiceId = options.currentMchId;
}
// 加载客服列表
this.loadServiceList();
},
methods: {
// 加载客服列表
async loadServiceList() {
try {
this.isLoading = true;
request(apiArr.csGetMchContactList, "POST", {
mch_id: uni.getStorageSync("merchantInfo").id,
}).then((res) => {
if (res.rows && res.rows.length > 0) {
res.rows.map(item => {
item.employee_image = picUrl + item.employee_image;
})
this.serviceList = res.rows
} else {
console.log("没有获取到客服列表数据");
}
})
} catch (error) {
console.error('加载客服列表失败', error);
} finally {
this.isLoading = false;
}
},
// 选择客服
selectService(service) {
this.selectedServiceId = service.mchId;
},
// 确认切换客服
confirmChange(item) {
if (!item) {
uni.showToast({
title: '请选择客服',
icon: 'none'
});
return;
}
// 存储当前聊天对象的信息
// uni.setStorageSync('currentChatTarget', item);
// 跳转到聊天页面
uni.navigateTo({
url: '/packages/customerService/index/index?item=' + JSON.stringify(item)
});
},
// 返回上一页
goBack() {
uni.navigateBack();
}
}
};
</script>
<style>
@import url("./index.css");
</style>

View File

@ -0,0 +1,136 @@
/* 聊天记录页面样式 */
page{
background-color: #f6f7fb;
}
/* 聊天记录容器 */
.chatting-records-container {
display: flex;
flex-direction: column;
background-color: #f6f7fb;
}
/* 聊天记录头部 */
.records-header {
height: 50px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 500;
color: #333;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* 聊天记录列表 */
.records-list {
flex: 1;
padding: 10px;
overflow-y: auto;
}
/* 加载状态 */
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 100px;
font-size: 14px;
color: #999;
}
/* 空状态 */
.empty-records {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
font-size: 14px;
color: #999;
}
/* 聊天记录项 */
.record-item {
display: flex;
padding: 12px;
margin-bottom: 10px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.record-item:active {
background-color: #f0f0f0;
}
/* 头像 */
.record-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 12px;
}
/* 记录信息 */
.record-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 50px;
}
/* 标题行 */
.record-title-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
/* 标题 */
.record-title {
font-size: 15px;
font-weight: 500;
color: #333;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 8px;
}
/* 时间 */
.record-time {
font-size: 12px;
color: #999;
}
/* 最后一条消息 */
.record-last-msg {
font-size: 14px;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 滚动条样式 */
.records-list::-webkit-scrollbar {
width: 6px;
}
.records-list::-webkit-scrollbar-track {
background-color: #f1f1f1;
border-radius: 3px;
}
.records-list::-webkit-scrollbar-thumb {
background-color: #c1c1c1;
border-radius: 3px;
}
.records-list::-webkit-scrollbar-thumb:hover {
background-color: #a8a8a8;
}

View File

@ -0,0 +1,169 @@
<template>
<view class="chatting-records-container">
<!-- 聊天记录列表 -->
<view class="records-list">
<view v-if="isLoading" class="loading">加载中...</view>
<view v-else-if="recordsList.length === 0" class="empty-records">暂无聊天记录</view>
<view v-else>
<view v-for="record in recordsList" :key="record.mchId" class="record-item" @tap="goToChatPage(record)">
<image class="record-avatar" :src="record.avatar || '/static/logo.png'" mode="aspectFill"></image>
<view class="record-info">
<view class="record-title-row">
<text class="record-title">{{ record.title }}</text>
<text class="record-time">{{ formatTime(record.lastMsgTime) }}</text>
</view>
<text class="record-last-msg">{{ record.lastMsg || '暂无消息' }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import {
request,
picUrl,
NavgateTo,
menuButtonInfo,
} from "../../../utils/index";
import { apiArr } from "../../../api/customerService";
export default {
data() {
return {
// 聊天记录列表
recordsList: [],
// 是否加载中
isLoading: false,
page_num: 1,
page_size: 10,
};
},
onLoad() {
// 加载聊天记录列表
this.loadChattingRecords();
},
onShow() {
// 页面显示时重新加载聊天记录
this.loadChattingRecords();
},
methods: {
// 加载聊天记录列表
async loadChattingRecords() {
try {
this.isLoading = true;
// 获取聊天记录列表
request(apiArr.csGetMsgList, "POST", {
open_id: uni.getStorageSync("openId"),
page_num: this.page_num,
page_size: this.page_size,
}).then((res) => {
console.log("🚀 ~ loadChattingRecords ~ res:", res)
})
// const response = await this.$http.get(apiArr.csGetMsgList);
// if (response.success && response.data && response.data.list) {
// this.recordsList = response.data.list.map(item => ({
// mchId: item.mchId,
// title: item.title || `客服${item.mchId}`,
// avatar: item.avatar,
// lastMsg: item.lastMsg,
// lastMsgTime: item.lastMsgTime || Date.now(),
// unreadCount: item.unreadCount || 0
// }));
// } else {
// // 使用模拟数据
// this.recordsList = this.getMockRecords();
// }
} catch (error) {
console.error('加载聊天记录失败', error);
// 使用模拟数据
this.recordsList = this.getMockRecords();
} finally {
this.isLoading = false;
}
},
// 获取模拟数据
getMockRecords() {
return [
{
mchId: '1001',
title: '客服小明',
avatar: '/static/logo.png',
lastMsg: '您好,请问有什么可以帮助您的吗?',
lastMsgTime: Date.now() - 30 * 60 * 1000,
unreadCount: 0
},
{
mchId: '1002',
title: '客服小丽',
avatar: '/static/logo.png',
lastMsg: '您的问题我已经记录,稍后会有专人与您联系。',
lastMsgTime: Date.now() - 2 * 60 * 60 * 1000,
unreadCount: 1
},
{
mchId: '1003',
title: '客服小张',
avatar: '/static/logo.png',
lastMsg: '感谢您的咨询,还有其他问题吗?',
lastMsgTime: Date.now() - 5 * 60 * 60 * 1000,
unreadCount: 0
}
];
},
// 格式化时间
formatTime(time) {
const date = new Date(time);
const now = new Date();
const diff = now - date;
// 小于1分钟显示"刚刚"
if (diff < 60 * 1000) {
return '刚刚';
}
// 小于1小时显示"XX分钟前"
if (diff < 60 * 60 * 1000) {
return Math.floor(diff / (60 * 1000)) + '分钟前';
}
// 小于24小时显示"XX小时前"
if (diff < 24 * 60 * 60 * 1000) {
return Math.floor(diff / (60 * 60 * 1000)) + '小时前';
}
// 小于7天显示"XX天前"
if (diff < 7 * 24 * 60 * 60 * 1000) {
return Math.floor(diff / (24 * 60 * 60 * 1000)) + '天前';
}
// 其他情况显示具体日期
const month = date.getMonth() + 1;
const day = date.getDate();
return month + '-' + day;
},
// 跳转到聊天页面
goToChatPage(record) {
// 存储当前聊天对象的信息,供聊天页面使用
uni.setStorageSync('currentChatTarget', record);
// 跳转到聊天页面
uni.navigateTo({
url: '/packages/customerService/index/index?mchId=' + record.mchId
});
}
}
};
</script>
<style>
@import url("./index.css");
</style>

View File

@ -0,0 +1,187 @@
/* 客服聊天页面样式 */
page {
background-color: #f6f7fb;
overflow-y: hidden;
}
/* 聊天容器 */
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f6f7fb;
}
/* 聊天头部 */
.chat-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 50px;
padding: 0 15px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding-bottom: 15rpx;
}
.back-btn {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.chat-title {
font-size: 16px;
font-weight: 500;
color: #333;
}
.empty-header {
width: 40px;
}
/* 切换客服按钮 */
.change-service-btn {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
/* 连接状态提示 */
.connecting-status {
padding: 5px 15px;
background-color: #fff3cd;
color: #856404;
font-size: 14px;
text-align: center;
}
/* 聊天消息区域 */
.chat-messages {
width: 97%;
margin: 0 auto;
flex: 1;
padding: 10px;
overflow-y: auto;
}
/* 时间分割线 */
.message-time {
margin: 10px 0;
text-align: center;
font-size: 12px;
color: #999;
}
/* 消息项 */
.message-item {
display: flex;
margin-bottom: 15px;
align-items: flex-start;
}
.message-item.self {
flex-direction: row-reverse;
}
/* 头像 */
.message-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin: 0 10px;
}
/* 消息内容 */
.message-content {
max-width: 70%;
padding: 10px 15px;
border-radius: 10px;
word-break: break-word;
font-size: 15px;
line-height: 1.5;
}
.message-item.self .message-content {
background-color: #91d5ff;
color: #333;
border-bottom-right-radius: 4px;
}
.message-item.other .message-content {
background-color: #fff;
color: #333;
border-bottom-left-radius: 4px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
/* 加载状态 */
.message-item.loading .message-content {
opacity: 0.7;
}
/* 输入区域 */
.chat-input-area {
background-color: #fff;
padding: 10px 15px;
border-top: 1px solid #eee;
margin-bottom: 20rpx;
}
.input-container {
display: flex;
align-items: flex-end;
}
.message-input {
flex: 1;
height: 20px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 20px;
font-size: 15px;
line-height: 1.5;
resize: none;
background-color: #f9f9f9;
}
.send-btn {
margin-left: 10px;
padding: 0 20px;
height: 40px;
background-color: #07c160;
color: #fff;
font-size: 15px;
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.send-btn:disabled {
background-color: #ccc;
color: #fff;
}
/* 滚动条样式 */
.chat-messages::-webkit-scrollbar {
width: 6px;
}
.chat-messages::-webkit-scrollbar-track {
background-color: #f1f1f1;
border-radius: 3px;
}
.chat-messages::-webkit-scrollbar-thumb {
background-color: #c1c1c1;
border-radius: 3px;
}
.chat-messages::-webkit-scrollbar-thumb:hover {
background-color: #a8a8a8;
}

View File

@ -0,0 +1,596 @@
<template>
<view class="chat-container">
<!-- 聊天头部 -->
<view class="chat-header" :style="{ paddingTop: top + 'px', height: localHeight + 'px' }">
<view class="back-btn" @tap="goBack">
<uni-icons type="left" size="28" color="#333"></uni-icons>
</view>
<view class="chat-title">{{ chatTarget.title || '客服' }}</view>
<view class="change-service-btn" @tap="goToChangeService">
<uni-icons type="switch" size="22" color="#333"></uni-icons>
</view>
</view>
<!-- 连接状态提示 -->
<view v-if="connectingStatus" class="connecting-status">{{ connectingStatus }}</view>
<!-- 聊天消息区域 -->
<scroll-view class="chat-messages" scroll-y="true" :scroll-into-view="scrollToView"
@scrolltoupper="loadMoreHistory">
<!-- 加载历史消息提示 -->
<view v-if="isLoadingHistory" class="message-time">加载历史消息...</view>
<!-- 消息列表 -->
<block v-for="(message, index) in messages" :key="index">
<!-- 时间分割线 -->
<view v-if="needShowTime(index)" class="message-time">{{ formatTime(message.time) }}</view>
<!-- 消息项 -->
<view class="message-item" :class="{
'self': message.isSelf,
'other': !message.isSelf,
'loading': message.isLoading
}" :id="'msg-' + index">
<image class="message-avatar" :src="message.isSelf ? userAvatar : (chatTarget.avatar || '/static/logo.png')"
mode="aspectFill"></image>
<view class="message-content">
{{ message.content }}
</view>
</view>
</block>
</scroll-view>
<!-- 输入区域 -->
<view class="chat-input-area">
<view class="input-container">
<textarea class="message-input" placeholder="请输入消息..." v-model="inputMessage" @input="handleInput"
@confirm="sendMessage" :adjust-position="true"></textarea>
<button class="send-btn" :disabled="!canSend || (!mqttUtils || !mqttUtils.getIsConnected())" @tap="sendMessage">
发送
</button>
</view>
</view>
</view>
</template>
<script>
import {
request,
picUrl,
uniqueByField,
menuButtonInfo,
NavgateTo
} from '../../../utils';
import { apiArr } from '../../../api/customerService';
// 修改导入路径
import mqtt from 'mqtt'
import MqttUtils from '../../../utils/mqtt';
import awaitRated from "@/packages/myOrders/awaitRated/awaitRated.vue";
export default {
data() {
return {
localHeight: "",
top: "",
// 聊天目标信息
chatTarget: {
mchId: '',
title: '',
avatar: '',
openId: '' // 接收方的open_id
},
// 用户头像
userAvatar: '/static/logo.png',
// 消息列表
messages: [],
// 输入的消息
inputMessage: '',
// 是否可以发送消息
canSend: false,
// 连接状态
isConnected: false,
// 连接状态文本
connectingStatus: '',
// 滚动到指定视图
scrollToView: '',
// 是否加载历史消息
isLoadingHistory: false,
// 心跳包定时器
keepaliveTimer: null,
// MQTT工具实例
client: null,
// 重连失败提示定时器
reconnectFailedTimer: null
};
},
onLoad(options) {
const meun = menuButtonInfo();
this.top = meun.top;
this.localHeight = meun.height;
// 获取聊天对象信息
if (options.item) {
this.chatTarget = JSON.parse(options.item);
this.chatTarget.title = `客服${this.chatTarget.employee_name}`;
}
// 创建MQTT工具实例
// this.mqttUtils = new MqttUtils({
// username: 'dev01',
// password: '211561',
// clientId: 'mqtt_8812321421_' + Math.random().toString(16).substr(2, 8),
// protocolVersion: 3,
// connectTimeout: 30 * 1000,
// reconnectPeriod: 5000,
// keepalive: 60,
// clean: true,
// // 配置重连参数
// maxReconnectAttempts: 15,
// reconnectExponentialBackoff: true
// });
// this.mqttUtils.connect()
// 初始化MQTT连接
this.initChat();
},
onShow() {
// 检查MQTT连接状态
// if (this.mqttUtils && !this.mqttUtils.getIsConnected()) {
// this.connectingStatus = '连接中...';
// }
},
methods: {
async connect() {
const options = {
clientId: 'mqtt_821321321',
username: 'dev01',
password: '211561',
keepalive: 30,
protocolId: 'MQTT',
protocolVersion: 3,
reconnectPeriod: 1000,
connectTimeout: 15 * 1000,
clean: true,
path: '/',
pathname: 'mqtt'
}
// #ifdef H5
this.client = mqtt.connect('ws://api.hshuishang.com:8084', options)
console.log('H5',this.client)
// #endif
// #ifdef MP-WEIXIN||APP-PLUS
this.client = mqtt.connect('wx://api.hshuishang.com:8084', options)
console.log('WX',this.client)
// #endif
this.client.on('connect', async function () {
this.isConnected = true
console.log('连接成功')
this.subscribe()
}).on('reconnect', function () {
console.log('正在重新连接.....')
}).on('error', function (err) {
console.log('on error',err)
}).on('end', function () {
console.log('on end')
}).on('message', function (topic, message) {
console.log(message)
console.log(message.toString())
})
},
async subscribe() {
if (this.isConnected && this.client) {
this.client.subscribe('contact/message/receive_msg',{qos:0}, function (err) {
if (!err) {
console.log('订阅成功')
this.client.publish({'contact/message/send_msg': 0}, 'hello mqtt')
} else {
console.log('订阅失败:', err);
this.connectingStatus = '订阅失败,请重试';
}
})
} else {
console.log('连接失败', this.isConnected, this.client)
}
},
// 初始化聊天
async initChat() {
try {
// 显示连接状态
this.connectingStatus = '连接中...';
this.connect()
// // 获取MQTT连接配置
// await this.getMqttConfig();
// // 连接MQTT服务器
// this.mqttUtils.connect();
// // 监听事件
// this.mqttUtils.on('connect', (connack) => {
// console.log('MQTT连接成功:', connack);
// this.isConnected = true;
// this.connectingStatus = '';
// // 订阅主题
// this.mqttUtils.subscribe('contact/message/receive_msg', (err) => {
// if (err) {
// console.error('订阅失败:', err);
// this.connectingStatus = '订阅失败,请重试';
// } else {
// console.log('订阅成功: contact/message/receive_msg');
// // 加载历史消息
// this.loadHistoryMessages();
// // 启动心跳包
// this.startKeepalive();
// }
// });
// });
// this.mqttUtils.on('error', (error) => {
// console.log('MQTT连接错误:', error);
// console.error('错误码:', error.code);
// console.error('错误详情:', error.message);
// this.isConnected = false;
// this.connectingStatus = '连接失败,请检查网络';
// });
// this.mqttUtils.on('disconnect', () => {
// console.log('MQTT连接已断开');
// })
// this.mqttUtils.on('message', (topic, message) => {
// console.log('收到消息:', topic, message.toString());
// try {
// // 解析消息
// let msgData;
// if (typeof message === 'string') {
// try {
// msgData = JSON.parse(message);
// } catch (e) {
// console.error('消息解析失败', e);
// return;
// }
// } else {
// msgData = JSON.parse(message.toString());
// }
// // 只处理与当前聊天对象相关的消息
// if (msgData.receive_client === this.mqttConfig.clientId &&
// msgData.send_client === this.chatTarget.openId) {
// // 添加到消息列表
// this.messages.push({
// id: `msg_${Date.now()}_${Math.random()}`,
// content: msgData.content,
// isSelf: false,
// time: Date.now(),
// isLoading: false
// });
// // 滚动到底部
// this.scrollToBottom();
// }
// } catch (error) {
// console.error('处理消息失败', error);
// }
// });
// this.mqttUtils.on('reconnect', (attempts, delay) => {
// console.log('MQTT正在重连...', attempts, delay);
// this.connectingStatus = `连接已断开,正在重连(${attempts})...`;
// });
// this.mqttUtils.on('close', () => {
// console.log('MQTT连接关闭');
// this.isConnected = false;
// this.connectingStatus = '连接已关闭';
// this.stopKeepalive();
// });
// this.mqttUtils.on('offline', () => {
// console.log('MQTT离线');
// this.isConnected = false;
// this.connectingStatus = '离线,请检查网络';
// this.stopKeepalive();
// });
// this.mqttUtils.on('end', () => {
// console.log('MQTT客户端结束');
// this.isConnected = false;
// this.connectingStatus = '客户端已结束';
// this.stopKeepalive();
// });
// // 添加重连失败事件监听
// this.mqttUtils.on('reconnectFailed', () => {
// console.error('MQTT重连失败,已达到最大重连次数');
// this.connectingStatus = '连接失败,请检查网络后重试';
// // 5秒后尝试重新初始化连接
// this.reconnectFailedTimer = setTimeout(() => {
// console.log('重连失败后,尝试重新初始化连接');
// this.initChat();
// }, 5000);
// });
} catch (error) {
console.error('初始化聊天失败', error);
this.connectingStatus = '连接失败,请重试';
// 失败后尝试重新连接
setTimeout(() => {
this.initChat();
}, 3000);
}
},
// 获取MQTT连接配置
async getMqttConfig() {
console.log("🚀 ~ getMqttConfig ~ getMqttConfig:", 11111)
try {
// // 直接使用已创建的MQTT实例的clientId
// if (this.mqttUtils && this.mqttUtils.options.clientId) {
// this.mqttConfig.clientId = this.mqttUtils.options.clientId;
// return Promise.resolve();
// }
// 如果没有已创建的实例或clientId,则通过API获取
return new Promise((resolve, reject) => {
const params = {
worker_id: this.chatTarget.id || '',
open_id: uni.getStorageSync('openId') || '',
}
request(apiArr.csGetToClientId, "POST", params).then((res) => {
// 检查响应数据格式是否正确
if (res && res.client_bind && res.client_bind.client_id_one && res.client_bind.client_id_two) {
this.mqttConfig.clientId = res.client_bind.client_id_one; // 登录人的open_id
this.chatTarget.openId = res.client_bind.client_id_two; // 接收方的open_id
// 如果MQTT实例已创建,更新clientId
if (this.mqttUtils) {
this.mqttUtils.options.clientId = this.mqttConfig.clientId;
}
resolve();
} else {
console.error('MQTT配置响应格式不正确:', res);
reject(new Error('未获取到有效的MQTT配置'));
}
}).catch(error => {
console.error('获取MQTT配置失败', error);
reject(error);
})
});
} catch (error) {
console.error('获取MQTT配置失败', error);
throw error;
}
},
// MQTT断开连接回调 (为了兼容旧的调用)
onDisconnect(packet) {
console.log('MQTT连接断开', packet);
this.isConnected = false;
// 根据断开原因设置不同的连接状态文本
if (packet.error) {
// 连接失败的情况
this.connectingStatus = '连接失败,请检查网络或服务器';
} else if (packet.reconnecting) {
// 正在重连的情况
this.connectingStatus = '连接已断开,正在重连...';
} else {
// 其他断开连接的情况
this.connectingStatus = '连接已断开,正在重连...';
}
// 停止心跳包
this.stopKeepalive();
},
// 加载历史消息
async loadHistoryMessages() {
try {
this.isLoadingHistory = true;
// 调用API获取历史消息
// const response = await this.$http.get(apiArr.csGetMsgRecord, {
// mchId: this.chatTarget.mchId,
// pageSize: 20,
// pageNum: 1
// });
// if (response.success && response.data && response.data.list) {
// // 处理历史消息
// const historyMessages = response.data.list.map(item => ({
// id: item.id || `msg_${item.time}_${Math.random()}`,
// content: item.content,
// isSelf: item.from === 'self',
// time: item.time,
// isLoading: false
// }));
// // 添加到消息列表开头
// this.messages = [...historyMessages, ...this.messages];
// // 滚动到底部
// this.scrollToBottom();
// }
} catch (error) {
console.error('加载历史消息失败', error);
} finally {
this.isLoadingHistory = false;
}
},
// 加载更多历史消息
loadMoreHistory() {
if (this.isLoadingHistory) return;
// 这里可以实现加载更多历史消息的逻辑
},
// 发送消息
sendMessage() {
const content = this.inputMessage.trim();
if (!content || !this.mqttUtils || !this.mqttUtils.getIsConnected()) return;
// 创建消息对象
const message = {
id: `msg_${Date.now()}_${Math.random()}`,
content: content,
isSelf: true,
time: Date.now(),
isLoading: true
};
// 添加到消息列表
// this.messages.push(message);
// 滚动到底部
this.scrollToBottom();
// 按照用户提供的格式构建发送消息
const msgData = {
bind_id: 1, // 聊天列表的ID,这里暂时固定为1
send_client: this.mqttConfig.clientId, // 消息发送方open_id
receive_client: this.chatTarget.openId, // 消息接收方open_id
type: 1, // 消息类型,1表示文字消息
content: content, // 消息内容
receive_read_status: 2, // 接收方阅读状态
};
this.mqttUtils.publish(
'contact/message/send_msg', // 使用指定的发送消息主题
JSON.stringify(msgData),
{ Qos: 0 },
(err) => {
// 更新消息状态
const index = this.messages.findIndex(m => m.id === message.id);
if (index !== -1) {
this.$set(this.messages[index], 'isLoading', false);
}
if (err) {
console.error('发送消息失败', err);
// 可以在这里添加消息发送失败的处理逻辑
} else {
console.log('发送消息成功');
}
}
);
// 清空输入框
this.inputMessage = '';
this.canSend = false;
},
// 处理输入事件
handleInput() {
this.canSend = this.inputMessage.trim().length > 0;
},
// 是否需要显示时间分割线
needShowTime(index) {
if (index === 0) return true;
const currentMsg = this.messages[index];
const prevMsg = this.messages[index - 1];
// 如果两条消息间隔超过5分钟,则显示时间分割线
return (currentMsg.time - prevMsg.time) > 5 * 60 * 1000;
},
// 格式化时间
formatTime(time) {
const date = new Date(time);
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
},
// 滚动到底部
scrollToBottom() {
setTimeout(() => {
this.scrollToView = 'msg-' + (this.messages.length - 1);
}, 100);
},
// 返回上一页
goBack() {
uni.navigateBack();
},
// 启动心跳包 - 增强版:添加错误处理和状态检查
startKeepalive() {
// 停止之前的定时器
this.stopKeepalive();
// 每30秒发送一次心跳包
this.keepaliveTimer = setInterval(() => {
if (this.mqttUtils && this.mqttUtils.getIsConnected()) {
const keepaliveData = {
client_id: this.mqttConfig.clientId
};
this.mqttUtils.publish(
'contact/message/keep_time',
JSON.stringify(keepaliveData),
{},
(err) => {
if (err) {
console.error('发送心跳包失败', err);
// 心跳包发送失败可能表示连接有问题,可以考虑触发重连
if (!this.isConnected) {
return;
}
console.log('心跳包发送失败,尝试检查连接状态');
// 这里可以添加额外的连接检查逻辑
}
}
);
} else {
console.warn('MQTT未连接,停止心跳包');
this.stopKeepalive();
}
}, 30000);
},
// 停止心跳包
stopKeepalive() {
if (this.keepaliveTimer) {
clearInterval(this.keepaliveTimer);
this.keepaliveTimer = null;
}
},
// 跳转到切换客服页面
goToChangeService() {
uni.navigateTo({
url: '/packages/customerService/changeService/index?currentMchId=' + this.chatTarget.mchId
});
}
},
// 页面卸载时停止心跳包
onUnload() {
// 断开MQTT连接
if (this.mqttUtils) {
this.mqttUtils.disconnect();
}
// 停止心跳包
this.stopKeepalive();
// 清除重连失败提示定时器
if (this.reconnectFailedTimer) {
clearTimeout(this.reconnectFailedTimer);
this.reconnectFailedTimer = null;
}
}
};
</script>
<style>
@import url("./index.css");
</style>

View File

@ -109,6 +109,12 @@
mode="" /> mode="" />
<view>点评</view> <view>点评</view>
</view> </view>
<view class="left_label" @click="handleKfClick">
<image
src="https://wechat-img-file.oss-cn-beijing.aliyuncs.com/kefu0.png"
mode="" />
<view>客服</view>
</view>
</view> </view>
<text v-if="isShow" class="btn" @click="handleQuickPayClick">快捷买单</text> <text v-if="isShow" class="btn" @click="handleQuickPayClick">快捷买单</text>
</view> </view>
@ -233,6 +239,11 @@ export default {
NavgateTo('../comment/index'); NavgateTo('../comment/index');
}, },
// 客服
handleKfClick() {
NavgateTo('/packages/customerService/changeService/index');
},
like(e) { like(e) {
request(apiArr.merChantCommentLike, "POST", { request(apiArr.merChantCommentLike, "POST", {
evaluation_id: e.id, evaluation_id: e.id,

View File

@ -18,9 +18,8 @@
<view class="Msg"> <view class="Msg">
<view class="payMony"> <view class="payMony">
¥<input type="number" v-model="Money" placeholder="付款金额" placeholder-style="font-size: 50rpx;" ¥<input type="digit" v-model="Money" placeholder="付款金额" placeholder-style="font-size: 50rpx;"
@blur="handleMoneyInput" @focus="showKeyboard" step="0.01" min="0.01"> @blur="handleMoneyInput" step="0.01" min="0.01">
<cu-keyboard ref="cukeyboard" @change="change" @confirm="keyboardConfirm" @hide="hide"></cu-keyboard>
</view> </view>
<view class="payRemark"> <view class="payRemark">
<input type="text" v-model="remarks" placeholder="付款备注"> <input type="text" v-model="remarks" placeholder="付款备注">
@ -138,48 +137,7 @@ export default {
methods: { methods: {
showKeyboard() {
this.$refs.cukeyboard.open();
},
// 键盘输入内容变化时触发
change(value) {
// 先进行基本的输入格式化
let formattedValue = value.toString().replace(/[^\d.]/g, '');
// 如果第一个数字是0且后面还有其他数字,则删除这个0
if (formattedValue.length >= 2 && formattedValue[0] === '0' && formattedValue[1] !== '.') {
formattedValue = formattedValue.substring(1);
}
// 限制只能有一个小数点
formattedValue = formattedValue.replace(/\.{2,}/g, '.');
// 确保小数点后最多两位
const parts = formattedValue.split('.');
if (parts.length > 1) {
formattedValue = parts[0] + '.' + parts[1].substring(0, 2);
}
// 处理开头是小数点的情况
if (formattedValue.startsWith('.')) {
formattedValue = '0' + formattedValue;
}
this.Money = formattedValue;
},
// 点击键盘确认按钮时触发
keyboardConfirm(value) {
// 如果最后一位是小数点,则删除小数点
if (value && value.endsWith('.')) {
value = value.slice(0, -1);
}
this.Money = value;
this.$refs.cukeyboard.close();
},
// 键盘隐藏时触发
hide() {
// 可以在这里添加键盘隐藏后的处理逻辑
},
handleMoneyInput() { handleMoneyInput() {
// 清除非数字和非小数点的字符 // 清除非数字和非小数点的字符
let value = this.Money.toString().replace(/[^\d.]/g, ''); let value = this.Money.toString().replace(/[^\d.]/g, '');

View File

@ -28,7 +28,7 @@
<view class="grid-item"> <view class="grid-item">
<image class="nav_icon" :src="item.image" mode=""></image> <image class="nav_icon" :src="item.image" mode=""></image>
</view> </view>
<text class="grid-text">{{ item.name }}</text> <text class="grid-text">{{ item.name }}</text>
</u-grid-item> </u-grid-item>
</u-grid> </u-grid>
</view> </view>
@ -85,25 +85,29 @@ import {
export default { export default {
data() { data() {
return { return {
top: "", top: "",
localHeight: "", localHeight: "",
active: 1, active: 1,
show: false, show: false,
show2: false, show2: false,
baseList: [ baseList: [
{ {
image: "https://wechat-img-file.oss-cn-beijing.aliyuncs.com/property-img-file/page_user_Group_1568.png", image: "https://wechat-img-file.oss-cn-beijing.aliyuncs.com/property-img-file/page_user_Group_1568.png",
name: "订单", name: "订单",
url: "", url: "/packages/storeManagement/order/index",
}, { }, {
image: "https://wechat-img-file.oss-cn-beijing.aliyuncs.com/gp_cancelAfterVerification.png", image: "https://wechat-img-file.oss-cn-beijing.aliyuncs.com/gp_cancelAfterVerification.png",
name: "到店核销", name: "到店核销",
url: "", url: "/packages/storeManagement/orderVerification/index",
} }, {
], image: "https://wechat-img-file.oss-cn-beijing.aliyuncs.com/kefu0.png",
} name: "客服",
}, url: "/packages/customerService/chattingRecords/index",
}
],
}
},
onLoad(options) { onLoad(options) {
const meun = menuButtonInfo(); const meun = menuButtonInfo();
this.top = meun.top; this.top = meun.top;
@ -129,10 +133,12 @@ export default {
}, },
click(item) { click(item) {
console.log("🚀 ~ click ~ item:", item) console.log("🚀 ~ click ~ item:", item)
if(item.name === '到店核销'){ // 根据item中的url进行跳转
NavgateTo('/packages/storeManagement/orderVerification/index') if (item.url) {
}else{ NavgateTo(item.url)
NavgateTo('/packages/storeManagement/order/index') } else {
console.warn('未配置跳转URL');
// 可添加默认跳转或提示
} }
} }
}, },

View File

@ -109,6 +109,48 @@
"navigationBarTitleText": "选择小区", "navigationBarTitleText": "选择小区",
"navigationBarBackgroundColor": "#fff" "navigationBarBackgroundColor": "#fff"
} }
},
{
"path": "routingInspection/index",
"style": {
"navigationBarTitleText": "巡更巡检",
"navigationBarBackgroundColor": "#fff"
}
},
{
"path": "addRoutingInspection/index",
"style": {
"navigationBarTitleText": "任务详情",
"navigationBarBackgroundColor": "#fff"
}
}
]
},
{
"root": "packages/customerService",
"pages": [
{
"path": "index/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#F9F9F9"
}
},
{
"path": "chattingRecords/index",
"style": {
"navigationBarTitleText": "客服聊天记录",
"navigationBarBackgroundColor": "#fff"
}
},
{
"path": "changeService/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#F9F9F9"
}
} }
] ]
}, },

0
utils/mqtt.js Normal file
View File

1384
yarn.lock

File diff suppressed because it is too large Load Diff