新增聊天系统

This commit is contained in:
gyq
2025-06-16 17:51:09 +08:00
parent f103df619b
commit b7e12cd0d1
12 changed files with 1656 additions and 505 deletions

441
pages/contact/contact.vue Normal file
View File

@@ -0,0 +1,441 @@
<template>
<view class="container">
<!-- #ifdef H5 -->
<view class="header-wrap">
<view class="header">
<view class="item" @click="back()">
<uni-icons type="left" color="#111" size="22"></uni-icons>
</view>
<view class="title">
<text class="t">咨询</text>
</view>
<view class="item"></view>
</view>
</view>
<!-- #endif -->
<view class="message-wrap" id="message-wrap">
<view class="more" v-if="list.length >= 10">
<text class="t" v-if="!finish">下拉加载更多历史消息</text>
<text class="t" v-else>没有更多消息了~</text>
</view>
<view class="more" v-if="status">
<text class="t">{{ contactInfo }}</text>
</view>
<view class="item" :class="[`item${item.user_type}`]" v-for="item in list" :key="item.id">
<image class="avatar" src="/static/icon_contact_avatar.svg" mode="aspectFill" v-if="item.user_type == 2"></image>
<view class="msg-wrap">
<view class="msg" v-if="item.type == 1">
<text class="t">{{ item.content }}</text>
</view>
<image class="img" :src="item.content" mode="heightFix" @click="previewImageHandle([item.content])" v-else></image>
<view class="time-wrap">
<text class="t">{{ item.add_time | formatTime }}</text>
</view>
</view>
</view>
</view>
<view class="focus-wrap" v-if="iosFocus"></view>
<view class="footer-wrap" :class="{ focus: iosFocus }">
<view class="footer">
<view class="item" @click="selectImg">
<uni-icons type="image-filled" size="30" color="#666"></uni-icons>
</view>
<view class="input-wrap">
<input :focus="focus" adjust-position class="input" type="text" placeholder="输入消息..." v-model.trim="messageValue" @confirm="sendMessageHandle" @focus="inputFocus" @blur="inputBlur" />
</view>
<view class="item" style="transform: rotate(280deg)" @click="sendMessageHandle">
<uni-icons type="paperplane-filled" size="30" color="#5C6BC0"></uni-icons>
</view>
</view>
</view>
</view>
</template>
<script>
const app = getApp();
import dayjs from 'dayjs';
import socket from '@/utils/socket';
import { uploadImage, previewImage } from '@/utils/uploadFile.js';
export default {
mixins: [socket.createSocketMixin()],
data() {
return {
iosFocus: false,
focus: false,
messageValue: '',
userInfo: '',
status: 0,
contactInfo: '',
finish: false,
page: 1,
list: [],
platform: '',
};
},
filters: {
formatTime(timeStr) {
const now = dayjs(); // 当前时间
const targetTime = dayjs(timeStr); // 目标时间
// 获取日期部分(忽略时分秒)
const nowDate = now.startOf('day');
const targetDate = targetTime.startOf('day');
// 计算日期差(按天计算)
const diffDays = nowDate.diff(targetDate, 'day');
// 星期几的中文映射
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const targetWeekDay = weekDays[targetTime.day()];
// 获取当前周的第一天(周日)
const firstDayOfWeek = now.startOf('week');
// 根据天数差判断格式化规则
if (diffDays === 0) {
// 今天
return targetTime.format('HH:mm');
} else if (diffDays === 1) {
// 昨天
return '昨天 ' + targetTime.format('HH:mm');
} else if (targetDate.isSame(firstDayOfWeek, 'week')) {
// 本周内(但不是今天和昨天)
return targetWeekDay + ' ' + targetTime.format('HH:mm');
} else {
// 更早之前
return targetTime.format('YYYY年M月D日 HH:mm');
}
},
},
onLoad() {
this.userInfo = uni.getStorageSync('cache_shop_user_info_key');
if (this.userInfo.id) {
// 自动管理生命周期的消息监听
this.socketOnMessage((data) => {
this.messageListener(data);
});
socket.sendMessage({
operate_type: 'chat_list',
type: 'user_msg',
user_id: this.userInfo.id,
});
}
// 获取系统信息
const systemInfo = uni.getSystemInfoSync();
this.platform = systemInfo.platform;
},
onPullDownRefresh() {
this.page++;
this.getHistoryMessage();
},
methods: {
// 发消息
sendMessageHandle() {
if (this.messageValue) {
this.sendMessageSocket(this.messageValue);
this.messageValue = '';
setTimeout(() => {
this.focus = true;
}, 10);
}
},
// 发送消息 1文本 2图文
sendMessageSocket(content = '', type = 1) {
socket.sendMessage({
operate_type: 'send_msg',
type: 'user_msg',
user_id: this.userInfo.id,
content: content,
content_type: type,
});
},
// 监听消息
messageListener(data) {
// console.log('监听消息===', data);
if (data.operate_type == 'chat_list' && this.page == 1) {
this.list = data.data.chat_list.reverse();
this.status = 1;
this.contactInfo = data.service;
this.$nextTick(() => {
this.scrollToBottom();
});
}
if (data.operate_type == 'chat_list' && this.page != 1) {
if (data.data.chat_list.length) {
this.list.unshift(...data.data.chat_list.reverse());
uni.showToast({
title: '获取成功',
icon: 'none',
});
} else {
this.finish = true;
}
setTimeout(() => {
uni.stopPullDownRefresh();
}, 300);
// this.$nextTick(() => {
// uni.pageScrollTo({
// scrollTop: 0, // 滚动到页面顶部
// duration: 100, // 滚动动画时长(毫秒)
// });
// });
}
if (data.operate_type == 'service_msg') {
this.list.push(data.data);
this.$nextTick(() => {
this.scrollToBottom();
});
}
if (data.operate_type == 'user_msg') {
this.list.push(data.data[0]);
this.$nextTick(() => {
this.scrollToBottom();
});
}
if (data.operate_type == 'error') {
uni.showModal({
title: '注意',
content: data.msg,
showCancel: false,
success: (res) => {
if (res.confirm) {
uni.navigateBack();
}
},
});
}
},
// 获取历史消息
getHistoryMessage() {
socket.sendMessage({
operate_type: 'chat_list',
type: 'user_msg',
user_id: this.userInfo.id,
page: this.page,
});
},
// 上传图片
async selectImg() {
try {
const [url] = await uploadImage(1, true);
console.log('selectImg===', url);
this.sendMessageSocket(url, 2);
} catch (error) {
console.log(error);
}
},
// 预览图片
previewImageHandle(urls) {
previewImage(urls);
},
// 返回上一页
back() {
uni.navigateBack();
},
inputBlur() {
this.focus = false;
this.iosFocus = false;
},
inputFocus() {
if (this.platform == 'ios') {
this.iosFocus = true;
}
this.scrollToBottom();
},
// 滚动到页面底部
scrollToBottom() {
// 创建节点查询器
const query = uni.createSelectorQuery();
// 获取底部元素的位置(相对于页面顶部的距离)
query.select('#message-wrap').boundingClientRect();
query.exec((res) => {
console.log('scrollToBottom===', res);
if (res && res[0]) {
const bottomHeight = res[0].height; // 元素顶部距离页面顶部的距离
// 滚动到指定位置(注意:不同平台参数可能不同)
uni.pageScrollTo({
scrollTop: bottomHeight + 1000, // 滚动距离单位px
duration: 100, // 滚动动画时长(可选)
});
}
});
},
},
};
</script>
<style scoped lang="scss">
.container {
--header-height: 44px;
--footer-height: 60px;
$height: 40px;
/* #ifdef H5 */
padding: calc(var(--header-height) + var(--status-bar-height)) 0 calc(var(--footer-height) + env(safe-area-inset-bottom)) 0;
/* #endif */
/* #ifdef MP-ALIPAY */
padding: 28upx 0 calc(var(--footer-height) + env(safe-area-inset-bottom)) 0;
/* #endif */
position: relative;
.header-wrap {
width: 100%;
padding-top: var(--status-bar-height);
position: fixed;
left: 0;
top: 0;
z-index: 99;
background-color: #fff;
.header {
height: var(--header-height);
display: flex;
align-items: center;
.item {
width: var(--header-height);
height: var(--header-height);
display: flex;
align-items: center;
justify-content: center;
}
.title {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
.t {
font-size: 32upx;
font-weight: bold;
color: #333;
}
}
}
}
.message-wrap {
padding: 28upx;
.more {
display: flex;
justify-content: center;
padding-bottom: 28upx;
.t {
font-size: 24upx;
color: #999;
}
}
.item {
display: flex;
gap: 20upx;
padding-bottom: 36upx;
&.item1 {
flex-direction: row-reverse;
.msg-wrap {
.msg {
background-color: #8e99f3;
.t {
color: #fff;
}
}
}
}
&.item2 {
.msg-wrap {
.time-wrap {
justify-content: flex-start;
}
}
}
.avatar {
$size: 80upx;
width: $size;
height: $size;
margin-top: 7upx;
flex-shrink: 0;
}
.msg-wrap {
.msg {
padding: 28upx;
border-radius: 28upx;
background-color: #fff;
.t {
display: block;
max-width: 450upx;
font-size: 28upx;
color: #333;
word-break: break-all;
}
}
.img {
display: block;
max-width: 500upx;
max-height: 200upx;
border-radius: 12upx;
background-color: #fefefe;
}
.time-wrap {
display: flex;
justify-content: flex-end;
padding-top: 12upx;
.t {
font-size: 24upx;
color: #999;
}
}
}
}
}
.focus-wrap {
height: calc(var(--footer-height) + env(safe-area-inset-bottom));
}
.footer-wrap {
width: 100%;
position: fixed;
left: 0;
bottom: 0;
z-index: 99;
padding-bottom: env(safe-area-inset-bottom);
background-color: #fcfcfc;
transform: translateY(0);
&.focus {
transform: translateY(-100%);
}
.footer {
height: var(--footer-height);
display: flex;
align-items: center;
.item {
width: $height + 20px;
height: $height;
display: flex;
align-items: center;
justify-content: center;
}
.input-wrap {
flex: 1;
.input {
box-sizing: border-box;
width: 100%;
height: $height;
border-radius: $height;
background-color: #efefef;
padding-left: 20px;
overflow: hidden;
}
}
}
}
}
</style>

View File

@@ -452,6 +452,10 @@
</block>
</block>
</block>
<view class="cp" hover-class="none" data-value="/pages/contact/contact" @tap="url_event">
<image src="/static/icon_contact.svg" mode="scaleToFill" class="image" style="filter: invert(80%)"></image>
<text class="dis-block text-size-xs cr-grey">客服</text>
</view>
<!-- 购物车 -->
<view v-if="is_opt_cart == 1" class="cp pr" data-value="/pages/cart-page/cart-page" @tap="url_event">
<view class="badge-icon">
@@ -1460,7 +1464,24 @@
// url事件
url_event(e) {
app.globalData.url_event(e);
var login = e.currentTarget.dataset.login;
if (login === undefined || login == 1) {
if (this.is_login()) {
app.globalData.url_event(e);
}
} else {
app.globalData.url_event(e);
}
},
// 是否登录
is_login() {
const user = app.globalData.get_user_cache_info() || null;
if ((user || null) == null) {
app.globalData.url_open('/pages/login/login?event_callback=init');
return false;
}
return true;
},
// 底部导航操作返回事件

View File

@@ -15,9 +15,7 @@
<iconfont name="icon-login-down-arrow" size="16rpx"></iconfont>
</view>
</view> -->
<view style="height: 100upx;">
</view>
<view style="height: 100upx"> </view>
</view>
<!-- 绑定手机 -->
@@ -415,6 +413,7 @@
</template>
<script>
const app = getApp();
import socket from '@/utils/socket';
import base64 from '@/common/js/lib/base64.js';
import componentCommon from '@/components/common/common';
import componentPopup from '@/components/popup/popup';
@@ -1369,6 +1368,7 @@
app.globalData.showToast(msg, 'success');
var event_callback = this.params.event_callback || null;
setTimeout(function () {
socket.connectSocket();
var pages = getCurrentPages();
if (pages.length > 1) {
// 触发回调函数

View File

@@ -44,7 +44,13 @@
<!-- 商品列表 -->
<view class="goods bg-white padding-main" style="border-radius: 8px 8px 0 0">
<view class="br-b padding-bottom-main fw-b text-size">{{ $t('user-order-detail.user-order-detail.7f8p26') }}</view>
<view class="br-b padding-bottom-main fw-b text-size" style="display: flex; justify-content: space-between; align-items: center">
<text>{{ $t('user-order-detail.user-order-detail.7f8p26') }}</text>
<view style="color: #555; font-size: 28upx;display: flex;align-items: center;gap: 4upx;" data-value="/pages/contact/contact" @click="url_event">
<uni-icons type="chat" size="18" color="#666"></uni-icons>
联系客服
</view>
</view>
<view v-for="(item, index) in detail.items" :key="index" class="goods-item br-b-dashed oh padding-main">
<view :data-value="item.goods_url" @tap="url_event" class="cp">
<image class="goods-image fl radius" :src="item.images" mode="aspectFill"></image>

View File

@@ -53,6 +53,7 @@
>
</view>
<view v-if="item.operate_data.is_cancel + item.operate_data.is_pay + item.operate_data.is_collect + item.operate_data.is_comments + item.operate_data.is_delete + (item.plugins_is_order_allot_button || 0) + (item.plugins_is_order_batch_button || 0) + (item.plugins_is_order_frequencycard_button || 0) + (item.plugins_delivery_data || 0) + (item.plugins_ordergoodsform_data || 0) + (item.plugins_orderresources_data || 0) > 0 || (item.status == 2 && item.order_model != 2) || ((item.plugins_express_data || 0) == 1 && (item.express_data || null) != null) || ((item.plugins_intellectstools_data || null) != null && (item.plugins_intellectstools_data.continue_buy_data || null) != null && item.plugins_intellectstools_data.continue_buy_data.length > 0)" class="item-operation tr br-t padding-top-main">
<button class="round bg-white cr-green br-green margin-bottom-main" type="default" size="mini" data-value="/pages/contact/contact" @tap="url_event" hover-class="none">客服</button>
<button v-if="item.operate_data.is_cancel == 1" class="round bg-white cr-yellow br-yellow margin-bottom-main" type="default" size="mini" @tap="cancel_event" :data-value="item.id" :data-index="index" hover-class="none">{{ $t('common.cancel') }}</button>
<button v-if="item.operate_data.is_pay == 1" class="round bg-white cr-green br-green margin-bottom-main" type="default" size="mini" @tap="pay_event" :data-value="item.id" :data-index="index" :data-price="item.total_price" :data-payment="item.payment_id" :data-currency-symbol="item.currency_data.currency_symbol" hover-class="none">{{ $t('order.order.1i873j') }}</button>
<button v-if="item.operate_data.is_collect == 1" class="round bg-white cr-green br-green margin-bottom-main" type="default" size="mini" @tap="collect_event" :data-transactionid="item.weixin_collect_data || ''" :data-value="item.id" :data-index="index" hover-class="none">{{ $t('orderallot-list.orderallot-list.w2w2w4') }}</button>

File diff suppressed because it is too large Load Diff