Files
cashier_wx/pageChat/chat.vue
2025-12-06 17:54:21 +08:00

843 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="min-page bg-f7 u-font-28 color-333" :style="pageHeight">
<view class="top u-flex u-row-between">
<text style="max-width: 600rpx" class="u-line-1 u-font-32">
{{ groupInfo.name }}
</text>
<text class="u-m-l-22">{{ membersRes?.user_list?.length || 0 }}</text>
<!-- <view class="" @click="toMore()">
<view class="u-flex u-row-center">
<up-icon name="more-dot-fill" color="#333" size="14"></up-icon>
</view>
<view class="color-666">更多</view>
</view> -->
</view>
<view class="box-1">
<!-- @refresherrefresh="refresherrefresh" -->
<scroll-view
scroll-y
refresher-background="transparent"
style="height: 100%"
@scrolltoupper="refresherrefresh"
:refresher-enabled="false"
:scroll-with-animation="false"
:refresher-triggered="scrollView.refresherTriggered"
:scroll-into-view="scrollView.intoView"
>
<view class="scroll-view-box">
<view class="talk-list">
<view :id="'msg-' + index" v-for="(item, index) in chatStore.chatList" :key="index">
<!-- 发消息 -->
<template v-if="item.operate_type == 'sendMsg' || item.is_user_send == 1">
<view class="user u-flex u-row-right">
<view class="u-p-r-18">
<view class="name u-line-1 text-right">{{ item.nick_name }}</view>
<view class="u-m-t-14 msg u-p-l-30" :class="['type' + item.msg_type]">
<chatItem :item="item" @previewImage="previewImage"></chatItem>
</view>
</view>
<up-avatar size="70rpx" :src="item.avatar" shape="square" bg-color="#fff"></up-avatar>
</view>
</template>
<!-- 商家消息 -->
<template v-else>
<view class="shop u-flex">
<up-avatar :src="item.avatar" size="70rpx" shape="square" bg-color="#fff"></up-avatar>
<view class="u-p-l-18">
<view class="u-flex">
<template v-if="item.is_shop == 1">
<view class="tag">商家</view>
<view class="name">{{ item.nick_name }}</view>
</template>
<template v-else>
<view class="color-000">{{ item.nick }}</view>
</template>
</view>
<view class="u-m-t-14 msg" :class="['type' + item.msg_type]">
<chatItem :item="item" @getCoupon="getCoupon" @previewImage="previewImage"></chatItem>
</view>
</view>
</view>
</template>
</view>
</view>
<up-loadmore v-if="isEnd" :status="isEnd ? 'nomore' : 'loading'"></up-loadmore>
</view>
</scroll-view>
</view>
<!-- <view :style="bottomSlotHeight"></view> -->
<view class="bottom" :class="[showMoreBtn ? '' : 'safe-bottom']">
<view class="u-flex" style="padding: 14rpx 28rpx" v-if="groupInfo && !groupInfo.is_mute">
<!-- <input
type="text"
class="u-flex-1 u-m-r-52 iput"
placeholder="请输入内容"
v-model="msg"
/> -->
<view class="u-flex-1 u-m-r-52 iput">
<up-input v-model="msg" border="none" placeholder="请输入内容"></up-input>
</view>
<button class="send-btn" v-if="msg.trim().length > 0" @click="sendText">
<text>发送</text>
</button>
<up-icon name="plus-circle" color="#666" size="60rpx" @click="showMoreBtnToggle" v-else></up-icon>
</view>
<view class="color-666 u-font-28 text-center u-m-t-28" v-else>商家已禁言</view>
<view class="more-btn" v-if="showMoreBtn">
<view v-for="(item, index) in moreBtns" @click="moreBtnsClick(item, index)" class="u-flex-col u-row-center u-col-center">
<view class="u-flex icon">
<image :src="item.icon" class="img" mode="aspectFill"></image>
</view>
<view class="u-m-t-8">{{ item.title }}</view>
</view>
</view>
</view>
</view>
<!-- 退出群聊 -->
<u-popup :show="popupShow" :safe-area-inset-bottom="false" mode="center" @close="popupShow = false">
<view class="popup-content">
<view class="header-wrap">
<text class="t">退出群{{ '{' + groupInfo.name + '}' }}</text>
<view class="close" @click="popupShow = false">
<u-icon name="close" size="20" color="#666"></u-icon>
</view>
</view>
<view class="u-p-40 text-center u-font-28 color-333 border-bottom">是否确认退出退出后将会错失更多活动和优惠</view>
<view class="btn-content">
<view class="btn">
<u-button color="#E8AD7B" plain="" shape="circle" @click="popupShow = false">取消</u-button>
</view>
<view class="btn">
<u-button color="#E8AD7B" shape="circle" @click="exitGroup">退出</u-button>
</view>
</view>
</view>
</u-popup>
<!-- 预览图 -->
<xbSwiperPreview :visable="showPrveImg" :imgs="prveImgsList" @update:visable="showPrveImg = $event"></xbSwiperPreview>
</template>
<script setup>
import { onReady, onShow, onReachBottom, onLoad, onPageScroll } from '@dcloudio/uni-app';
import { ref, inject, onMounted, nextTick, reactive, watch, onUnmounted, computed } from 'vue';
import { useChatStore } from '@/stores/chat';
import * as chatApi from '@/http/php/chat';
import { uploadFile } from '@/common/api/upload.js';
import * as javaChatApi from '@/common/api/market/chat';
import xbSwiperPreview from '@/components/xb-swiper-preview/index.vue';
const showPrveImg = ref(false);
const prveImgsList = ref([]);
function exitGroup() {
chatApi
.groupQuit({
group_id: groupInfo.value.id
})
.then((res) => {
if (res) {
popupShow.value = false;
uni.showToast({
title: '退出成功',
icon: 'none',
duration: 1500
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
});
}
function getCoupon(item) {
javaChatApi
.couponGrant({
id: item.coupon.activity_id,
// shopUserId: shopUserInfo.id,
shopId: groupInfo.value.shop_id,
userId: uni.cache.get('userInfo').id
})
.then((res) => {
if (res) {
uni.showToast({
title: '领取成功',
icon: 'none',
duration: 2000
});
}
});
}
function refresh() {
query.page = 1;
getMsgList();
}
const go = {
to(item) {}
};
const popupShow = ref(false);
import Modal from './components/modal.vue';
import chatItem from './components/chat-item.vue';
const placeholderStyle = {
color: '#999',
fontSize: '28rpx'
};
function refresherrefresh() {
console.log('refresherrefresh');
// 关键:同时判断「正在加载」和「无更多数据」,只要一个为真就关闭刷新
if (isLoading.value || isEnd.value) {
scrollView.refresherTriggered = false; // 立即关闭刷新状态
if (isEnd.value) {
uni.showToast({ title: '没有更多了', icon: 'none' });
}
return; // 终止后续逻辑
}
query.page++;
getMsgList();
}
const scrollView = reactive({
refresherTriggered: false,
intoView: '',
safeAreaHeight: 0
});
const showMoreBtn = ref(false);
function showMoreBtnToggle() {
showMoreBtn.value = !showMoreBtn.value;
if (showMoreBtn.value) {
}
}
const modalData = reactive({
show: false,
form: {
title: '',
getLimit: 1,
giveNum: 1,
couponId: ''
}
});
const chatStore = useChatStore();
const msg = ref('');
const shopInfo = uni.cache.get('shopInfo');
const shopUserInfo = uni.cache.get('shopUserInfo');
const moreBtns = ref([
{
icon: '/pageChat/static/pic.png',
title: '发送照片',
value: 'pic'
},
{
icon: '/pageChat/static/video.png',
title: '发送视频',
value: 'video'
},
{
icon: '/pageChat/static/exit.png',
title: '退出群聊',
value: 'exit'
}
]);
function moreBtnsClick(item, index) {
if (item.value == 'exit') {
popupShow.value = true;
return;
}
if (item.value == 'pic') {
sendImg();
}
if (item.value == 'video') {
sendVideo();
}
}
// 通用相机权限校验函数参考openLocationAuth逻辑
async function checkCameraAuth() {
return new Promise((resolve, reject) => {
try {
// 1. 检查当前相机权限状态
uni.getSetting({
success: (settingRes) => {
const authSetting = settingRes.authSetting || {};
if (authSetting['scope.camera']) {
// 2. 已授权:直接返回成功
resolve(true);
} else if (authSetting['scope.camera'] === undefined) {
// 3. 未请求过授权:发起授权请求
uni.authorize({
scope: 'scope.camera',
success: () => {
// 授权成功
resolve(true);
},
fail: (authErr) => {
// 授权失败(用户拒绝)
console.error('相机授权失败:', authErr);
reject(false);
}
});
} else {
// 4. 已拒绝授权:提示用户去设置页开启
uni.showModal({
title: '开启相机权限',
content: '请允许“零点八零”使用您的相机,方便您拍摄图片/视频',
confirmText: '去设置',
cancelText: '取消',
success: (modalRes) => {
if (modalRes.confirm) {
// 跳转微信小程序授权设置页
uni.openSetting({
success: (openRes) => {
if (openRes.authSetting['scope.camera']) {
// 用户在设置页开启授权
resolve(true);
} else {
// 用户未开启授权
reject(false);
}
},
fail: (openErr) => {
console.error('打开设置页失败:', openErr);
reject(false);
}
});
} else {
// 用户取消设置
reject(false);
}
},
fail: (modalErr) => {
console.error('弹窗失败:', modalErr);
reject(false);
}
});
}
},
fail: (settingErr) => {
console.error('获取权限设置失败:', settingErr);
reject(false);
}
});
} catch (err) {
console.error('相机权限校验异常:', err);
reject(false);
}
});
}
// 图片选择与发送(优化后)
async function sendImg() {
try {
// 1. 调用相机权限校验
const isAuth = await checkCameraAuth();
if (!isAuth) {
// 权限校验失败,终止流程
return;
}
// 2. 权限校验通过调用图片选择API
const res = await new Promise((resolve, reject) => {
uni.chooseImage({
count: 3,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: resolve,
fail: (err) => {
// 捕获选择失败(如用户取消选择)
if (err.errMsg && err.errMsg.includes('cancel')) {
// 用户主动取消,静默处理
reject(new Error('user cancel'));
} else {
// 其他错误(如系统异常)
reject(err);
}
}
});
});
uni.showLoading({ title: '发送中' });
console.log('选择图片成功', res);
// 3. 批量上传图片
for (let i = 0; i < res.tempFiles.length; i++) {
const fileRes = await uploadFile(res.tempFiles[i]);
if (fileRes) {
sendMsg({
image_url: fileRes,
msg_type: 2
});
}
}
} catch (err) {
console.error('图片选择/发送失败', err);
// 仅处理非用户取消的错误
if (err.message !== 'user cancel') {
uni.showToast({
title: '图片发送失败',
icon: 'none',
duration: 2000
});
}
} finally {
uni.hideLoading();
}
}
// 视频选择与发送(优化后)
async function sendVideo() {
try {
// 1. 调用相机权限校验(与图片共用)
const isAuth = await checkCameraAuth();
if (!isAuth) {
// 权限校验失败,终止流程
return;
}
// 2. 权限校验通过调用视频选择API
const res = await new Promise((resolve, reject) => {
uni.chooseVideo({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: resolve,
fail: (err) => {
// 捕获选择失败(如用户取消选择)
if (err.errMsg && err.errMsg.includes('cancel')) {
// 用户主动取消,静默处理
reject(new Error('user cancel'));
} else {
// 其他错误(如系统异常)
reject(err);
}
}
});
});
uni.showLoading({ title: '发送中' });
console.log('选择视频成功', res);
// 3. 上传视频
const fileRes = await uploadFile({ path: res.tempFilePath });
if (fileRes) {
sendMsg({
image_url: fileRes,
msg_type: 5
});
} else {
uni.showToast({
title: '视频发送失败',
icon: 'none',
duration: 2000
});
}
} catch (err) {
console.error('视频选择/发送失败', err);
// 仅处理非用户取消的错误
if (err.message !== 'user cancel') {
uni.showToast({
title: '视频发送失败',
icon: 'none',
duration: 2000
});
}
} finally {
uni.hideLoading();
}
}
const groupInfo = ref({});
const handleReceiveMsg = (msg) => {
nextTick(() => {
scrollView.intoView = 'msg-0';
});
};
// 注册消息回调
chatStore.registerReceiveMsgCallback(handleReceiveMsg);
async function init() {
const res = await chatApi.groupInfo({
group_id: options.group_id
});
console.log(res);
groupInfo.value = res || {};
if (res) {
chatStore.shop_id = groupInfo.value.shop_id;
chatStore.connectSocket();
chatStore.init();
}
getMsgList();
}
const query = reactive({
page: 1,
size: 10
});
async function getMsgList() {
// 提前拦截:已无更多数据,直接关闭状态
if (isEnd.value) {
scrollView.refresherTriggered = false;
isLoading.value = false;
return;
}
isLoading.value = true;
try {
const msgRes = await chatApi.messageHistory({
...query,
chat_type: '2',
session_id: options.session_id,
to_id: options.group_id,
shop_id: options.group_id,
group_id: options.group_id
});
const data = msgRes.list || [];
const selector = `msg-${query.page == 1 ? 0 : chatStore.chatList.length}`;
if (query.page == 1) {
chatStore.chatList = data;
} else {
chatStore.chatList.push(...data);
}
isEnd.value = chatStore.chatList.length >= msgRes.total;
nextTick(() => {
scrollView.intoView = selector;
console.log(selector);
});
} catch (err) {
console.error('加载历史消息异常', err);
// 加载失败回退页码,避免页码错乱
if (query.page > 1) query.page--;
} finally {
console.log('scrollView', scrollView);
setTimeout(() => {
scrollView.refresherTriggered = false;
isLoading.value = false;
}, 100);
}
}
const options = reactive({});
const membersRes = ref(null);
onLoad((opt) => {
Object.assign(options, opt);
init();
chatApi.messageMarkReadAll({
session_ids: options.group_id
});
chatStore.group_id = options.group_id;
chatApi.groupMembers({ group_id: options.group_id }).then((res) => {
console.log(res);
membersRes.value = res;
});
// #ifdef H5
scrollView.safeAreaHeight = uni.getSystemInfoSync().safeArea.height;
// #endif
});
function toMore() {
go.to('PAGES_CHAT_GROUP_INFO', {
group_id: groupInfo.value.id,
session_id: options.session_id
});
}
function sendText() {
if (!msg.value.trim().length) return;
sendMsg({ content: msg.value, msg_type: 1 });
msg.value = '';
}
function sendMsg(msg) {
chatStore.sendMessage({
to_id: groupInfo.value.id,
to_user_type: groupInfo.value.id,
chat_type: 2,
content: msg.value,
image_url: '',
order_id: '',
session_id: groupInfo.value.session_id || options.session_id,
...msg
});
}
function closeModal() {
modalData.form = {
title: '',
getLimit: 1,
giveNum: 1,
couponId: ''
};
}
function confirmCoupon() {
console.log(modalData.form);
if (!modalData.form.title) {
uni.showToast({
title: '请输入自定义文案',
icon: 'none'
});
return;
}
if (!modalData.form.couponId) {
uni.showToast({
title: '请选择优惠券',
icon: 'none'
});
return;
}
if (!modalData.form.giveNum) {
uni.showToast({
title: '请输入发放数量',
icon: 'none'
});
return;
}
if (!modalData.form.getLimit) {
uni.showToast({
title: '请输入每人限领量',
icon: 'none'
});
return;
}
}
function previewImage(url) {
prveImgsList.value = [url];
showPrveImg.value = true;
}
//是否到底了
const isBottom = ref(false);
const isLoading = ref(false);
//消息是否完了
const isEnd = ref(false);
watch(
() => chatStore.chatList.length,
(newVal, oldVal) => {}
);
const bottomSlotHeight = computed(() => {
if (showMoreBtn.value) {
return {
height: '180px'
};
} else {
return {
height: '88px'
};
}
});
onReady(() => {});
const pageHeight = computed(() => {
const safeAreaHeight = scrollView.safeAreaHeight;
if (safeAreaHeight > 0) {
return `height: calc(${safeAreaHeight}px - var(--window-top));`;
}
return '';
});
onShow(() => {
// 页面显示时,检查连接状态
if (!chatStore.isConnect || !chatStore.socketTask) {
console.log('聊天页显示检查Socket连接');
chatStore.forceReconnect();
}
});
onUnmounted(() => {
// 组件卸载时,移除消息回调
chatStore.removeReceiveMsgCallback(handleReceiveMsg);
});
</script>
<style lang="scss" scoped>
$grayColor: #9e9e9e;
$grayBorderColor: #bebebe;
.top {
background-color: #fff;
padding: 40rpx 32rpx;
}
.min-page {
height: calc(100vh - var(--window-top));
display: flex;
flex-direction: column;
flex-wrap: nowrap;
align-content: center;
justify-content: space-between;
align-items: stretch;
}
.talk-list {
padding: 30rpx 28rpx 30rpx 26rpx;
display: flex;
flex-direction: column-reverse;
flex-wrap: nowrap;
align-content: flex-start;
justify-content: flex-end;
align-items: stretch;
// 添加弹性容器,让内容自动在顶部
&::before {
content: '.';
display: inline;
visibility: hidden;
line-height: 0;
font-size: 0;
flex: 1 0 auto;
height: 1px;
}
}
.box-1 {
width: 100%;
height: 0;
flex: 1 0 auto;
box-sizing: content-box;
}
.user {
margin-bottom: 88rpx;
.name {
margin-top: -4upx;
color: $grayColor;
font-size: 24upx;
}
.msg {
padding: 16rpx 20rpx;
background-color: #fff;
border-radius: 20rpx;
}
}
.shop {
margin-bottom: 88rpx;
.tag {
margin-right: 32rpx;
padding: 4rpx 12rpx;
border: 1px solid $grayBorderColor;
border-radius: 8rpx;
font-size: 20rpx;
color: $grayColor;
}
.name {
color: $grayColor;
font-size: 24upx;
}
.msg {
padding: 16rpx 20rpx;
border-radius: 20rpx;
&.type1 {
background-color: #fff;
}
&.type4 {
background-color: #fff;
}
.img {
width: 50vw;
}
}
}
.bottom {
height: auto;
z-index: 2;
border-top: #e5e5e5 solid 1px;
box-sizing: content-box;
background-color: #f3f3f3;
/* 兼容iPhoneX */
padding-bottom: 0;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
&.safe-bottom {
padding-bottom: calc(env(safe-area-inset-bottom) + 4rpx);
}
.iput {
background-color: #f8f8f8;
padding: 16rpx 20rpx;
}
}
.send-btn {
background-color: $my-main-color;
color: #fff;
line-height: 1;
padding: 16rpx 20rpx;
border-radius: 8rpx;
font-size: 28rpx;
display: flex;
align-items: center;
}
.more-btn {
display: flex;
justify-content: center;
align-items: center;
padding: 30rpx;
background-color: #f0f0f0;
gap: 74rpx;
text-align: center;
color: #666;
padding-bottom: calc(env(safe-area-inset-bottom) + 4rpx);
.icon {
width: 100rpx;
height: 100rpx;
background-color: #fff;
border-radius: 16rpx;
display: flex;
justify-content: center;
align-items: center;
.img {
height: 70rpx;
width: 70rpx;
}
}
}
.scroll-view-box {
display: flex;
flex-direction: column-reverse;
}
.popup-content {
width: 90vw;
background-color: #fff;
border-radius: 8px;
.header-wrap {
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #ececec;
padding: 0 28upx;
.t {
font-size: 32upx;
color: #333;
}
.close {
$size: 60upx;
width: $size;
height: $size;
display: flex;
align-items: center;
justify-content: center;
}
}
.btn-content {
height: 86px;
display: flex;
align-items: center;
justify-content: center;
gap: 60upx;
.btn {
width: 248upx;
}
}
}
</style>