From 42faef34194c466f03e29d75a63ae502e4213044 Mon Sep 17 00:00:00 2001
From: admin <344137771@qq.com>
Date: Tue, 06 Jan 2026 10:38:46 +0800
Subject: [PATCH] 上一版是10.10的, 这版才是原始源码
---
src/views/customerService/index.vue | 749 ++++++++++++++++++++++++++-------------------------------
1 files changed, 344 insertions(+), 405 deletions(-)
diff --git a/src/views/customerService/index.vue b/src/views/customerService/index.vue
index 126fd61..1d77954 100644
--- a/src/views/customerService/index.vue
+++ b/src/views/customerService/index.vue
@@ -1,438 +1,377 @@
<template>
- <div class="service-box flex flex-col" :class="homesStore.kefu_url ? '' : 'pb-40'">
- <div>
- <van-nav-bar ref="navEl" :title="$t('onLineService') + ' Jason David'" left-arrow @click-left="onClickLeft"
- fixed />
- <div class="px-3.5 py-5" v-if="state == 0">
- <div class="white">{{ $t('OrdersWill') }} <span style="color: #1194F7">{{ dayjs.duration(remainingTime,
- 'seconds').format('mm:ss') }}</span> {{
- $t('afterCancel')
- }}</div>
- <div class="mt-3">
- <span class="mr-1" style="color: #8A919E">{{ $t('lumpSum') }}</span>
- <span class="white">{{ payInfo.currency }} {{ payInfo.amount }}</span>
- </div>
- <div class="mt-5">
- <van-button class="w-full" type="primary" @click="router.back()">{{ $t('toPay') }}</van-button>
- </div>
- </div>
- </div>
- <div class="flex-1" v-if="homesStore.kefu_url">
- <iframe :src="generateExtranetLink()" width="100%" height="100%" frameborder="0"></iframe>
- </div>
- <div class="localKefu flex-1 flex" v-else>
- <div class="flex flex-col px-10 box-border" ref="boxScrollEl" style="overflow:auto;">
- <div class="w-full py-4 text-grey text-center pt-30 text-30" @click="onMore"
- :style="{ 'display': finished ? 'none' : 'block' }">
- {{ $t('historyMessage') }}
- </div>
- <ul class="flex flex-col pt-3">
- <li v-for="(item, index) in list" :key="index" class="flex flex-col my-3">
- <!-- <p class="text-center py-2 text-grey text-30" v-if="showTime(index)">{{
- item.createtime &&
- item.createtime.split(' ')[0]
- }}</p> -->
- <template v-if="item.delete_status != -1">
- <p class="text-center pb-3 text-grey text-30">{{
- item.createtime
- }}</p>
- <div class="flex" :class="item.send_receive === 'send' ? 'justify-end' : ''">
- <template v-if="item.send_receive === 'receive'">
- <img src="@/assets/image/service/responser.png" class="w-20 h-20 mr-5" />
- <div class="responser px-12 py-8 text-30 left-chatBg">
- <p class="break-word textColor text-30" style="max-width: 200px;"
- v-if="item.content_type === 'text' || item.type === 'text'">
- {{ item.content }}</p>
- <img v-else :src="item.content" class="w-200 h-200" @click="onPreview(item.content)" />
- </div>
- </template>
- <div class="py-8 px-12 rounded-md flex flex-col right-chatBg" v-else>
- <img :src="`${item.content}`" class="w-200 h-200"
- v-if="item.content_type === 'img' || item.type === 'img'" @click="onPreview(item.content)" />
- <p class="break-word text-white text-30" v-else style="max-width: 200px;">{{ item.content }}</p>
- </div>
- </div>
- </template>
- </li>
- </ul>
- </div>
-
- <div
- class="bottom bottomBox flex justify-between items-center w-full fixed bottom-0 borderTop px-4 box-border bgBottom">
- <van-uploader :max-size="10000 * 1024" @oversize="onOversize" :after-read="afterRead">
- <img src="@/assets/image/service/photo2.png" class="w-12 h-12" />
- </van-uploader>
- <input type="text" v-model="message" :placeholder="$t('entryYouMessage')"
- class="flex-1 mx-3 h-full border-none textColor chatBg" maxlength="500" />
- <img src="@/assets/image/service/send2.png" class="w-12 h-12" @click="throttleSend(message)" />
- </div>
- </div>
- </div>
+ <div class="service-box flex flex-col pb-40">
+ <iframe height="100%" src="https://cdn.chat20gm.cfd/chat_online/index?channelId=1958101585091772416"></iframe>
+ </div>
</template>
<script setup>
-import { Uploader, showImagePreview } from 'vant'
-import { _getMsg, _getUnreadMsg, _sendMsg } from '@/service/im.api'
-import { _uploadImage } from '@/service/upload.api'
-import { ref, onMounted, onUnmounted, onBeforeMount, nextTick } from "vue";
-import { useI18n } from "vue-i18n";
-import { throttle } from '@/utils/index'
-import { closeToast, showToast, showLoadingToast } from "vant";
-import { useRoute, useRouter } from 'vue-router';
-import { getc2cOrder } from "@/service/recharge.api.js";
-import { useHomesStore } from "@/store/homes.store";
-import { SET_KEFU } from "@/store/types.store";
-import { useUserStore } from '@/store/user';
-import dayjs from 'dayjs';
-import duration from 'dayjs/plugin/duration';
-dayjs.extend(duration);
+ import {
+ Uploader,
+ showImagePreview
+ } from 'vant'
+ import {
+ _getMsg,
+ _getUnreadMsg,
+ _sendMsg
+ } from '@/service/im.api'
+ import {
+ _uploadImage
+ } from '@/service/upload.api'
+ import {
+ ref,
+ onMounted,
+ onUnmounted,
+ onBeforeMount
+ } from "vue";
+ import {
+ useI18n
+ } from "vue-i18n";
+ import {
+ throttle
+ } from '@/utils/index'
+ import {
+ closeToast,
+ showToast,
+ showLoadingToast
+ } from "vant";
+ import {
+ useRoute,
+ useRouter
+ } from 'vue-router';
+ import {
+ getc2cOrder
+ } from "@/service/recharge.api.js";
+ import {
+ useHomesStore
+ } from "@/store/homes.store";
+ import {
+ SET_KEFU
+ } from "@/store/types.store";
+ import {
+ useUserStore
+ } from '@/store/user';
+ import dayjs from 'dayjs';
+ import duration from 'dayjs/plugin/duration';
+ dayjs.extend(duration);
-const userStore = useUserStore()
-const { t } = useI18n()
-const homesStore = useHomesStore();
-const router = useRouter()
-const route = useRoute()
-const list = ref([])
-const message = ref('')
-const lastMsgId = ref('')
-const interval = ref(null)
-const unread = ref(0)
-const finished = ref(false)
-const isScrollBottom = ref(true)
-const currentScrollTop = ref(0)
-const androidAttrs = ref(null)
-const navEl = ref(null);
-const boxScrollEl = ref(null);
-const navHeight = ref(0);
-const payInfo = ref({})
-const remainingTime = ref(0)
+ const userStore = useUserStore()
+ const {
+ t
+ } = useI18n()
+ const homesStore = useHomesStore();
+ const router = useRouter()
+ const route = useRoute()
+ const list = ref([])
+ const message = ref('')
+ const lastMsgId = ref('')
+ const interval = ref(null)
+ const unread = ref(0)
+ const finished = ref(false)
+ const isScrollBottom = ref(true)
+ const currentScrollTop = ref(0)
+ const androidAttrs = ref(null)
+ const navEl = ref(null);
+ const boxScrollEl = ref(null);
+ const navHeight = ref(0);
+ const payInfo = ref({})
+ const remainingTime = ref(0)
-let state = ref(null)
-let orderNo = ""
-let partyId = ""
-// onMounted(() => {
-// // 美洽客服
-// // const _ll="&id="+_id.value+"&name="+_name.value
-// const _ll = ""
-// // console.log(_ll)
-// const _uull = 'https://cdn.chat20gm.cfd/chat_online/index?channelId=1958101585091772416' + _ll;
-// // window.location.href=_uull
+ let state = ref(null)
+ let orderNo = ""
+ let partyId = ""
+ onMounted(() => {
+ // 美洽客服
+ // const _ll="&id="+_id.value+"&name="+_name.value
+ // const _ll=""
+ // console.log(_ll)
+ // const _uull='https://cdn.chat20gm.cfd/chat_online/index?channelId=1958101585091772416'+_ll;
+ // window.location.href=_uull
-// // 绑定滚动事件监听器,使用 nextTick 确保 DOM 渲染完成
-// nextTick(() => {
-// if (boxScrollEl.value) {
-// boxScrollEl.value.addEventListener('scroll', handleScroll)
-// // 初始化时检查是否在底部
-// setTimeout(() => {
-// handleScroll()
-// }, 100)
-// }
-// })
-// })
-onMounted(() => {
- orderNo = ""
- partyId = ""
- navHeight.value = navEl.value.$el.getBoundingClientRect().height
- if (route.query.order_no) {
- getc2cOrderDetails(route.query.order_no, (data) => {
- console.log("getc2cOrderDetails = " + JSON.stringify(data))
- orderNo = data.orderNo;
- partyId = data.partyId;
- fetchList()
- })
- } else {
- if (!homesStore.kefu_url) {
- fetchList()
- }
- }
- setInterval(() => {
- getCountdown()
- }, 1000)
- const model = navigator.userAgent;
- // 判断是否是安卓手机,是则是true
- androidAttrs.value = model.indexOf('Android') > -1 || model.indexOf('Linux') > -1
- window.addEventListener('scroll', handleScroll, true)
-})
+ })
+ // onMounted_bak(() => {
+ // orderNo = ""
+ // partyId = ""
+ // navHeight.value = navEl.value.$el.getBoundingClientRect().height
+ // if (route.query.order_no) {
+ // getc2cOrderDetails(route.query.order_no, (data) => {
+ // console.log("getc2cOrderDetails = " + JSON.stringify(data))
+ // orderNo = data.orderNo;
+ // partyId = data.partyId;
+ // fetchList()
+ // })
+ // } else {
+ // if (!homesStore.kefu_url) {
+ // fetchList()
+ // }
+ // }
+ // setInterval(() => {
+ // getCountdown()
+ // }, 1000)
+ // const model = navigator.userAgent;
+ // // 判断是否是安卓手机,是则是true
+ // androidAttrs.value = model.indexOf('Android') > -1 || model.indexOf('Linux') > -1
+ // window.addEventListener('scroll', handleScroll, true)
+ // })
-onBeforeMount(() => {
- homesStore[SET_KEFU]()
-})
+ onBeforeMount(() => {
+ homesStore[SET_KEFU]()
+ })
-//获取订单详情
-const getc2cOrderDetails = (orderNo, call) => {
- getc2cOrder({ order_no: orderNo }).then((res) => {
- payInfo.value = res
- state.value = payInfo.value.state
- remainingTime.value = res.autoCancelTimeRemain
- if (call) { call(res) }
- })
-}
+ //获取订单详情
+ const getc2cOrderDetails = (orderNo, call) => {
+ getc2cOrder({
+ order_no: orderNo
+ }).then((res) => {
+ payInfo.value = res
+ state.value = payInfo.value.state
+ remainingTime.value = res.autoCancelTimeRemain
+ if (call) {
+ call(res)
+ }
+ })
+ }
-//第三方客服带用户id
-const generateExtranetLink = () => {
- let extranetLink = ''
- if (userStore.userInfo && userStore.userInfo.usercode) {
- const userData = encodeURIComponent(JSON.stringify({ name: userStore.userInfo.usercode, comment: '' }))
- let params = `&clientid=${userStore.userInfo.usercode}&metadata=${userData}`;
- extranetLink = homesStore.kefu_url + params;
- } else {
- extranetLink = homesStore.kefu_url
- }
- // extranetLink = homesStore.kefu_url + params;
- console.log('generateExtranetLink', extranetLink)
- // console.log('extranetLink',extranetLink)
- return extranetLink;
-}
+ //第三方客服带用户id
+ const generateExtranetLink = () => {
+ let extranetLink = ''
+ if (userStore.userInfo && userStore.userInfo.usercode) {
+ const userData = encodeURIComponent(JSON.stringify({
+ name: userStore.userInfo.usercode,
+ comment: ''
+ }))
+ let params = `&clientid=${userStore.userInfo.usercode}&metadata=${userData}`;
+ extranetLink = homesStore.kefu_url + params;
+ } else {
+ extranetLink = homesStore.kefu_url
+ }
+ // extranetLink = homesStore.kefu_url + params;
+ console.log('generateExtranetLink', extranetLink)
+ // console.log('extranetLink',extranetLink)
+ return extranetLink;
+ }
-const throttleSend = throttle((message) => {
- send('text', message)
-}, 500)
+ const throttleSend = throttle((message) => {
+ send('text', message)
+ }, 500)
-const onOversize = (file) => {
- showToast(t('fileMaxLimit'));
-}
-const onPreview = (url) => { // 预览
- showImagePreview([url])
-}
-const showTime = (index) => { // 时间显示
- if (index === 0) {
- return true
- }
- if (index === list.value.length - 1) {
- return false
- }
- if (list.value[index].createtime.split(' ')[0] === list.value[index + 1].createtime.split(' ')[1]) {
- return false
- }
-}
-const afterRead = (file) => { // 文件上传
- showLoadingToast({ duration: 0 })
- _uploadImage(file, (percent) => {
- console.log(percent)
- }).then(data => {
- closeToast()
- send('img', data)
- }).catch(() => {
- showToast(t('失败'))
- })
-}
-const fetchList = async (message_id = '') => { // 获取消息列表
- console.log("orderNo = " + orderNo);
- console.log("partyId = " + partyId);
- // 保存更新前的滚动位置和滚动高度
- let previousScrollTop = 0
- let previousScrollHeight = 0
- if (boxScrollEl.value && !message_id) {
- previousScrollTop = boxScrollEl.value.scrollTop
- previousScrollHeight = boxScrollEl.value.scrollHeight
- }
+ const onOversize = (file) => {
+ showToast(t('fileMaxLimit'));
+ }
+ const onPreview = (url) => { // 预览
+ showImagePreview([url])
+ }
+ const showTime = (index) => { // 时间显示
+ if (index === 0) {
+ return true
+ }
+ if (index === list.value.length - 1) {
+ return false
+ }
+ if (list.value[index].createtime.split(' ')[0] === list.value[index + 1].createtime.split(' ')[1]) {
+ return false
+ }
+ }
+ const afterRead = (file) => { // 文件上传
+ showLoadingToast({
+ duration: 0
+ })
+ _uploadImage(file, (percent) => {
+ console.log(percent)
+ }).then(data => {
+ closeToast()
+ send('img', data)
+ }).catch(() => {
+ showToast(t('失败'))
+ })
+ }
+ const fetchList = async (message_id = '') => { // 获取消息列表
+ console.log("orderNo = " + orderNo);
+ console.log("partyId = " + partyId);
+ _getMsg({
+ message_id
+ }, orderNo, partyId).then(data => { //
+ if (!lastMsgId.value) {
+ lastMsgId.value = data.length && data[data.length - 1]['id']
+ }
+ if (data.length) {
+ if (message_id) { // 加载更多
+ lastMsgId.value = data[data.length - 1]['id']
+ list.value = [...data.reverse(), ...list.value]
+ } else {
+ list.value = [...list.value, ...data.reverse()]
+ let hash = {};
+ list.value = list.value.reduce(function(preVal, curVal) {
+ hash[curVal.id] ? ' ' : hash[curVal.id] = true && preVal.push(curVal);
+ return preVal
+ }, []);
+ list.value.forEach((item, index) => {
+ data.forEach((item2, index2) => {
+ if (item.id === item2.id) {
+ item.delete_status = item2.delete_status
+ }
+ })
+ })
- _getMsg({ message_id }, orderNo, partyId).then(data => { //
- if (!lastMsgId.value) {
- lastMsgId.value = data.length && data[data.length - 1]['id']
- }
- if (data.length) {
- if (message_id) { // 加载更多
- lastMsgId.value = data[data.length - 1]['id']
- list.value = [...data.reverse(), ...list.value]
- } else {
- list.value = [...list.value, ...data.reverse()]
- let hash = {};
- list.value = list.value.reduce(function (preVal, curVal) {
- hash[curVal.id] ? ' ' : hash[curVal.id] = true && preVal.push(curVal);
- return preVal
- }, []);
- list.value.forEach((item, index) => {
- data.forEach((item2, index2) => {
- if (item.id === item2.id) {
- item.delete_status = item2.delete_status
- }
- })
- })
+ }
- }
+ if (isScrollBottom.value) {
+ boxScrollEl.value.scrollTop = boxScrollEl.value.scrollHeight - boxScrollEl.value
+ .offsetHeight
+ }
+ currentScrollTop.value = boxScrollEl.value.scrollTop;
+ if (data.length < 10) {
+ finished.value = true
+ }
+ }
+ if (!message_id) {
+ clearIntervalTimer()
+ interval.value = setInterval(() => {
+ fetchList()
+ }, 1000)
+ }
+ })
+ }
- // 使用 nextTick 确保 DOM 更新完成后再操作滚动
- setTimeout(() => {
- if (boxScrollEl.value) {
- if (message_id) {
- // 加载更多时,保持滚动位置(通过计算新增内容的高度)
- const newScrollHeight = boxScrollEl.value.scrollHeight
- const heightDiff = newScrollHeight - previousScrollHeight
- boxScrollEl.value.scrollTop = previousScrollTop + heightDiff
- } else {
- // 轮询新消息时,只有用户在底部才滚动到底部
- if (isScrollBottom.value) {
- boxScrollEl.value.scrollTop = boxScrollEl.value.scrollHeight - boxScrollEl.value.offsetHeight
- } else {
- // 如果用户不在底部,保持当前滚动位置(通过计算新增内容的高度)
- const newScrollHeight = boxScrollEl.value.scrollHeight
- const heightDiff = newScrollHeight - previousScrollHeight
- boxScrollEl.value.scrollTop = previousScrollTop + heightDiff
- }
- }
- currentScrollTop.value = boxScrollEl.value.scrollTop
- }
- }, 0)
+ const handleScroll = () => {
+ if (boxScrollEl.value) {
+ if (boxScrollEl.value.scrollTop === currentScrollTop.value) {
+ isScrollBottom.value = true
+ } else {
+ isScrollBottom.value = false
+ }
+ }
+ }
- if (data.length < 10) {
- finished.value = true
- }
- }
- if (!message_id) {
- clearIntervalTimer()
- interval.value = setInterval(() => {
- fetchList()
- }, 1000)
- }
- })
-}
+ const onMore = () => { // 加载更多
+ fetchList(lastMsgId.value)
+ }
+ const clearIntervalTimer = () => {
+ if (interval.value) {
+ clearInterval(interval.value)
+ interval.value = null
+ }
+ }
+ const fetchUnread = () => { // 获取未读
+ _getUnreadMsg(orderNo, partyId).then(data => {
+ unread.value = data
+ })
+ }
+ const onClickLeft = () => { // 返回
+ router.go(-1);
+ }
+ const send = (type = 'text', content = '') => { // 发送消息, img 也当消息text
+ if (!content) {
+ showToast(t('entryMessageContent'))
+ return
+ }
+ _sendMsg(type, content, orderNo, partyId).then(async data => {
+ message.value = ''
+ isScrollBottom.value = true
+ // document.getElementById('bottom').click()
+ // await fetchList()
+ })
+ }
+ const getCountdown = () => {
+ if (remainingTime.value > 0) {
+ remainingTime.value = remainingTime.value - 1
+ } else {
+ remainingTime.value = 0
+ }
+ }
-const handleScroll = () => {
- if (boxScrollEl.value) {
- const { scrollTop, scrollHeight, offsetHeight } = boxScrollEl.value
- // 判断是否在底部(允许5px的误差)
- const isAtBottom = scrollHeight - scrollTop - offsetHeight <= 5
- isScrollBottom.value = isAtBottom
- currentScrollTop.value = scrollTop
- }
-}
-
-const onMore = () => { // 加载更多
- fetchList(lastMsgId.value)
-}
-const clearIntervalTimer = () => {
- if (interval.value) {
- clearInterval(interval.value)
- interval.value = null
- }
-}
-const fetchUnread = () => { // 获取未读
- _getUnreadMsg(orderNo, partyId).then(data => {
- unread.value = data
- })
-}
-const onClickLeft = () => { // 返回
- router.go(-1);
-}
-const send = (type = 'text', content = '') => { // 发送消息, img 也当消息text
- if (!content) {
- showToast(t('entryMessageContent'))
- return
- }
- _sendMsg(type, content, orderNo, partyId).then(async data => {
- message.value = ''
- isScrollBottom.value = true
- // document.getElementById('bottom').click()
- await fetchList()
- })
-}
-const getCountdown = () => {
- if (remainingTime.value > 0) {
- remainingTime.value = remainingTime.value - 1
- } else {
- remainingTime.value = 0
- }
-}
-
-onUnmounted(() => {
- clearIntervalTimer()
- // 移除滚动事件监听器
- if (boxScrollEl.value) {
- boxScrollEl.value.removeEventListener('scroll', handleScroll)
- }
-})
-
+ onUnmounted(() => {
+ clearIntervalTimer()
+ })
</script>
<style lang="scss" scoped>
-.service-box {
- font-size: 14px;
- width: 100%;
- height: 100vh;
- box-sizing: border-box;
- background: $mainBgColor;
- overflow: hidden;
+ .service-box {
+ font-size: 14px;
+ width: 100%;
+ height: 100vh;
+ box-sizing: border-box;
+ background: $mainBgColor;
+ overflow: hidden;
- :deep(.van-hairline--bottom::after) {
- border-color: $mainBgColor;
- }
-}
+ :deep(.van-hairline--bottom::after) {
+ border-color: $mainBgColor;
+ }
+ }
-.break-word {
- word-wrap: break-word;
- white-space: pre-wrap;
-}
+ .break-word {
+ word-wrap: break-word;
+ white-space: pre-wrap;
+ }
-.max-w-230 {
- max-width: 115px;
-}
+ .max-w-230 {
+ max-width: 115px;
+ }
-.responser {
- position: relative;
+ .responser {
+ position: relative;
- &::after {
- content: '';
- width: 0;
- height: 0;
- border-top: 5px solid transparent;
- border-bottom: 5px solid transparent;
- border-right: 10px solid $input_background;
- position: absolute;
- left: -10px;
- top: 10px;
- }
-}
+ &::after {
+ content: '';
+ width: 0;
+ height: 0;
+ border-top: 5px solid transparent;
+ border-bottom: 5px solid transparent;
+ border-right: 10px solid $input_background;
+ position: absolute;
+ left: -10px;
+ top: 10px;
+ }
+ }
-.borderTop {
- border-top: 1px solid $border_color;
-}
+ .borderTop {
+ border-top: 1px solid $border_color;
+ }
-.bottomBox {
- height: 65px;
-}
+ .bottomBox {
+ height: 65px;
+ }
-.white {
- color: $text_color;
-}
+ .white {
+ color: $text_color;
+ }
-.chatBg {
- background: $input_background;
- height: 36px;
- padding: 8px 18px;
- border-radius: 18px;
- color: $text_color;
- font-size: 14px;
- border: 1px solid $chat-border;
-}
+ .chatBg {
+ background: $input_background;
+ height: 36px;
+ padding: 8px 18px;
+ border-radius: 18px;
+ color: $text_color;
+ font-size: 14px;
+ border: 1px solid $chat-border;
+ }
-.right-chatBg {
- position: relative;
- background: $color_main;
- color: $text_color;
+ .right-chatBg {
+ position: relative;
+ background: $color_main;
+ color: $text_color;
- &::after {
- content: '';
- width: 0;
- height: 0;
- border-top: 5px solid transparent;
- border-bottom: 5px solid transparent;
- border-left: 10px solid $color_main;
- position: absolute;
- right: -8px;
- top: 14px;
- }
-}
+ &::after {
+ content: '';
+ width: 0;
+ height: 0;
+ border-top: 5px solid transparent;
+ border-bottom: 5px solid transparent;
+ border-left: 10px solid $color_main;
+ position: absolute;
+ right: -8px;
+ top: 14px;
+ }
+ }
-.left-chatBg {
- background: $input_background;
-}
+ .left-chatBg {
+ background: $input_background;
+ }
-.localKefu {
- overflow: auto;
- flex-direction: column;
-}
+ .localKefu {
+ overflow: auto;
+ flex-direction: column;
+ }
-.van-nav-bar--fixed {
- position: relative !important;
-}
-</style>
+ .van-nav-bar--fixed {
+ position: relative !important;
+ }
+</style>
\ No newline at end of file
--
Gitblit v1.9.3