发送按钮样式优化和代码联调

master
wujingling0908 2 weeks ago
parent f22713668a
commit 3f88043322
  1. 312
      src/App.vue
  2. 1
      src/assets/images/chat/todo-icon.svg
  3. 8
      src/assets/style/common.css
  4. 641
      src/components/abstract.vue
  5. 379
      src/components/business-handle.vue
  6. 4777
      src/components/chat-content.vue
  7. 2
      src/components/chat-main.vue
  8. 1201
      src/components/chat-new.vue
  9. 11
      src/components/hot-question-new.vue
  10. 914
      src/components/text-content.vue
  11. 1145
      src/components/text-folding.vue
  12. 12
      src/components/welcomes.vue
  13. 71
      src/router/index.js
  14. 11
      src/store/app.js
  15. 1376
      src/utils/index.js
  16. 930
      src/utils/socket.js
  17. 2020
      src/utils/utils.js
  18. 1257
      src/views/chat.vue
  19. 10
      src/views/word.vue
  20. 8
      vite.config.js

@ -1,154 +1,162 @@
<script setup>
import { RouterView, useRoute } from 'vue-router'
import { onMounted, onUnmounted, provide } from 'vue'
import { storeToRefs } from 'pinia'
import { useAppStore } from '@/store/app.js'
// import { leaveProject } from '@/utils/api.js'
import wx from 'weixin-js-sdk'
const route = useRoute()
import { isIOS, isAndroid, isPureHarmonyOS, handleFontSize, detectFontSize } from '@/utils/common'
console.log('navigator.userAgent', navigator.userAgent, navigator.userAgentData)
window.wx = wx
// 全局 provide 接线:子组件统一通过 inject('appStore') / inject('xxx') 使用 store
const appStore = useAppStore()
const appStoreRefs = storeToRefs(appStore)
provide('appStore', appStore)
Object.keys(appStoreRefs).forEach((key) => {
provide(key, appStoreRefs[key])
})
const handleGlobalBrowserExit = (reason, extra = {}) => {
const payload = {
reason,
href: window.location.href,
route: route.fullPath,
timestamp: Date.now(),
...extra,
}
// leaveProject()
// alert('关闭页面')
console.log('📴 全局检测到浏览器关闭/返回动作', payload)
// 在这里统一处理关闭浏览器、手势返回、刷新离开等动作。
// 如果后续需要接业务方法,直接在这里调用即可。
// example:
// someBusinessMethod(payload)
}
const handleBrowserPopState = () => {
handleGlobalBrowserExit('popstate')
}
const handleBrowserPageHide = (event) => {
handleGlobalBrowserExit('pagehide', {
persisted: Boolean(event?.persisted),
})
}
const handleBrowserBeforeUnload = () => {
handleGlobalBrowserExit('beforeunload')
}
onMounted(() => {
var html = document.getElementsByTagName('html')[0]
// 初始化
function init() {
console.log('navigator.userAgent', navigator.userAgent, navigator.userAgentData)
console.log('handleFontSize called with:', localStorage.getItem('bigFont'))
if (localStorage.getItem('bigFont') === 'true')
document.getElementsByTagName('html')[0].style.fontSize = '0.79333275px'
if (localStorage.getItem('localfontSize'))
html.style.fontSize = localStorage.getItem('localfontSize')
// 监听WeixinJSBridge
if (typeof WeixinJSBridge == 'object' && typeof WeixinJSBridge.invoke == 'function') {
detectFontSize()
handleFontSize()
} else {
if (document.addEventListener) {
document.addEventListener(
'WeixinJSBridgeReady',
function () {
detectFontSize()
handleFontSize()
},
false
)
} else if (document.attachEvent) {
document.attachEvent('WeixinJSBridgeReady', function () {
detectFontSize()
handleFontSize()
})
}
}
}
init()
console.log(isPureHarmonyOS(), 'isPureHarmonyOS')
window.onresize = function () {
if (!isIOS() && !isAndroid() && !isPureHarmonyOS()) init()
}
window.addEventListener('popstate', handleBrowserPopState)
window.addEventListener('pagehide', handleBrowserPageHide)
window.addEventListener('beforeunload', handleBrowserBeforeUnload)
})
onUnmounted(() => {
window.removeEventListener('popstate', handleBrowserPopState)
window.removeEventListener('pagehide', handleBrowserPageHide)
window.removeEventListener('beforeunload', handleBrowserBeforeUnload)
})
</script>
<template>
<div id="app" class="app-wrapper opacity">
<RouterView v-slot="{ Component }">
<keep-alive include="chat">
<component :is="Component" />
<script setup>
import { RouterView, useRoute } from 'vue-router'
import { onMounted, onUnmounted, provide } from 'vue'
import { storeToRefs } from 'pinia'
import { useAppStore } from '@/store/app.js'
// import { leaveProject } from '@/utils/api.js'
import wx from 'weixin-js-sdk'
const route = useRoute()
import { isIOS, isAndroid, isPureHarmonyOS, handleFontSize, detectFontSize } from '@/utils/common'
console.log('navigator.userAgent', navigator.userAgent, navigator.userAgentData)
window.wx = wx
// 全局 provide 接线:子组件统一通过 inject('appStore') / inject('xxx') 使用 store
const appStore = useAppStore()
const appStoreRefs = storeToRefs(appStore)
provide('appStore', appStore)
// Object.keys(appStoreRefs).forEach((key) => {
// provide(key, appStoreRefs[key])
// })
const handleGlobalBrowserExit = (reason, extra = {}) => {
const payload = {
reason,
href: window.location.href,
route: route.fullPath,
timestamp: Date.now(),
...extra,
}
// leaveProject()
// alert('关闭页面')
console.log('📴 全局检测到浏览器关闭/返回动作', payload)
// 在这里统一处理关闭浏览器、手势返回、刷新离开等动作。
// 如果后续需要接业务方法,直接在这里调用即可。
// example:
// someBusinessMethod(payload)
}
const handleBrowserPopState = () => {
handleGlobalBrowserExit('popstate')
}
const handleBrowserPageHide = (event) => {
handleGlobalBrowserExit('pagehide', {
persisted: Boolean(event?.persisted),
})
}
const handleBrowserBeforeUnload = () => {
handleGlobalBrowserExit('beforeunload')
}
onMounted(() => {
var html = document.getElementsByTagName('html')[0]
// 初始化
function init() {
console.log('navigator.userAgent', navigator.userAgent, navigator.userAgentData)
console.log('handleFontSize called with:', localStorage.getItem('bigFont'))
if (localStorage.getItem('bigFont') === 'true')
document.getElementsByTagName('html')[0].style.fontSize = '0.79333275px'
if (localStorage.getItem('localfontSize'))
html.style.fontSize = localStorage.getItem('localfontSize')
// 监听WeixinJSBridge
if (typeof WeixinJSBridge == 'object' && typeof WeixinJSBridge.invoke == 'function') {
detectFontSize()
handleFontSize()
} else {
if (document.addEventListener) {
document.addEventListener(
'WeixinJSBridgeReady',
function () {
detectFontSize()
handleFontSize()
},
false
)
} else if (document.attachEvent) {
document.attachEvent('WeixinJSBridgeReady', function () {
detectFontSize()
handleFontSize()
})
}
}
}
init()
console.log(isPureHarmonyOS(), 'isPureHarmonyOS')
window.onresize = function () {
if (!isIOS() && !isAndroid() && !isPureHarmonyOS()) init()
}
window.addEventListener('popstate', handleBrowserPopState)
window.addEventListener('pagehide', handleBrowserPageHide)
window.addEventListener('beforeunload', handleBrowserBeforeUnload)
})
onUnmounted(() => {
window.removeEventListener('popstate', handleBrowserPopState)
window.removeEventListener('pagehide', handleBrowserPageHide)
window.removeEventListener('beforeunload', handleBrowserBeforeUnload)
})
</script>
<template>
<div class="app-wrapper opacity">
<RouterView v-slot="{ Component, route }">
<keep-alive>
<component
:is="Component"
v-if="Component && route.meta.keepAlive"
:key="route.name"
/>
</keep-alive>
<component
:is="Component"
v-if="Component && !route.meta.keepAlive"
:key="route.name"
/>
</RouterView>
</div>
</template>
<style scoped>
.header-box {
width: 100%;
height: 88rem;
display: flex;
align-items: center;
background: #e8f2ff;
padding: 0 24rem 0 54rem;
}
.view {
width: 100vw;
height: 100vh;
overflow: hidden;
}
.left-box {
display: flex;
align-items: center;
}
.back-icon {
display: block;
width: 18rem;
height: 32rem;
}
.back-icon img {
width: 100%;
height: 100%;
}
</style>
<style scoped lang="scss">
#app {
.con {
color: red;
}
}
</style>
</div>
</template>
<style scoped>
.header-box {
width: 100%;
height: 88rem;
display: flex;
align-items: center;
background: #e8f2ff;
padding: 0 24rem 0 54rem;
}
.view {
width: 100vw;
height: 100vh;
overflow: hidden;
}
.left-box {
display: flex;
align-items: center;
}
.back-icon {
display: block;
width: 18rem;
height: 32rem;
}
.back-icon img {
width: 100%;
height: 100%;
}
</style>
<style scoped lang="scss">
#app {
.con {
color: red;
}
}
</style>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.4 KiB

@ -117,14 +117,6 @@ body .van-toast--text {
color: #0074e1;
}
body .van-popup--bottom {
max-width: 9.98rem;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
}
/* .van-picker__toolbar{
position: relative;
}

@ -1,301 +1,340 @@
<script setup>
import { ref, onMounted, computed, inject } from 'vue';
import { Post } from '@/utils/https'
import { messageSend } from '@/utils/api'
import { getHiddenProp, getVisibilityState, decryptByVersionNo1, isSit, Encrypt, isDev, isProd } from "@/utils";
const store = inject('appStore', null)
const businessHandleInfo = ref({})
const orderCount = ref(0)
const abstractList = computed(() => [
{
title: '保障状态',
num: orderCount.value,
text: '保单保障中',
unit: '张',
name: '查保单',
btnText: '查保单',
btnBg: 'linear-gradient(90deg, #025cea 0%, #0595f6 100%)',
},
{
title: '待办任务',
num: businessHandleInfo.value?.contents?.length || 0,
text: '待办需处理',
unit: '个',
name: '去处理',
btnText: '去处理',
btnBg: 'linear-gradient(111deg, #ff752a 44%, #feb52d 136%)',
},
{
title: '权益领取',
num: 0,
text: '生存金权益',
unit: '项',
name: '去领取',
btnText: '去领取',
btnBg: 'linear-gradient(261deg, #2ed0c6 3%, #08b7ac 65%)',
},
].filter(item => item.num > 0))
const businessHandle = () => {
if (
store.source == "95270000" ||
store.source == "95320000" ||
store.source == "95350000" ||
store.source == "96060000"
) {
Post("notify/info", { userInfoId: store.userId })
.then((res) => {
if (res.data?.contents.length > 0) {
businessHandleInfo.value = res.data;
}
})
.catch((error) => {
console.log(error);
});
}
}
const isIdentityNo = (value) => /^\d{15}$|^\d{17}[\dXx]$/.test(String(value || '').trim())
// 与 liics 一致:明文身份证来自解密 userInfo.pid(encryptPid 是给外部系统用的密文,不能直接解)
const getIdentityCardno = () => {
const versionNo = store.userInfo?.versionNo1 || ''
const pidCipher = store.userInfo?.pid || ''
if (!versionNo || !pidCipher) return ''
const identityCardno = decryptByVersionNo1(versionNo, pidCipher)
console.log(decryptByVersionNo1(versionNo, '0FLzZwhD08nkUE4BjRGOSEri4KWO'),id);
return isIdentityNo(identityCardno) ? identityCardno.trim() : ''
}
const getOrderCount = () => {
try {
const identityCardno = getIdentityCardno()
if (!identityCardno) {
console.warn('证件号解密失败', {
hasPid: Boolean(store.userInfo?.pid),
hasVersionNo1: Boolean(store.userInfo?.versionNo1),
})
return
}
Post('policy/queryCount', {
dentityCardno: identityCardno,
userId: store.userId
}).then(res => {
orderCount.value = res?.data?.policyCount || 0
})
} catch (error) {
console.log(error)
}
}
const toDetail = (record) => {
const text = record?.name
console.log('text', text);
if (!text) return
messageSend(text).catch((err) => {
console.log(err)
})
}
onMounted(async () => {
businessHandle()
getOrderCount()
})
</script>
<template>
<div class="abstract-con" v-if="abstractList.length">
<div class="title">
<span>今日皮卡丘摘要</span><img src="@/assets/images/chat/star.svg" alt="" srcset="" />
</div>
<div class="list">
<div :class="`item ${(abstractList || []).length === 1 ? 'singleItem' : ''}`"
v-for="(item, index) in abstractList || []" :key="index">
<div>
<div class="title">{{ item.title }}</div>
<div class="content" :style="{ display: (abstractList || []).length > 2 ? 'block' : 'flex' }">
<div class="nums">
<div class="num">
{{ item.num }}<span>{{ item.unit }}</span>
</div>
</div>
<div class="text" :style="{ marginTop: (abstractList || []).length > 2 ? '8rem' : '0' }">
{{ item.text }}
</div>
</div>
</div>
<div class="btn" :style="{ background: item.btnBg }" @click="toDetail(item)">{{ item.btnText }}</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.abstract-con {
position: relative;
z-index: 1;
flex-direction: column;
justify-content: center;
padding: 10rem 12rem;
gap: 12rem;
align-self: stretch;
margin-top: 10rem;
background: linear-gradient(77deg, #fcfdfd80 -1%, #f5f7fee6 99%);
box-sizing: border-box;
border: 1rem solid rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10rem);
border-radius: 12rem;
.title {
font-family: PingFang SC;
font-size: 16rem;
font-weight: 500;
line-height: normal;
letter-spacing: 0rem;
/* 科个中性/正文-标题 17204D */
/* 样式描述:用于正文标题 */
color: #17204d;
img {
width: 20rem;
margin-left: 4.6rem;
}
}
.list {
margin-top: 13rem;
display: flex;
gap: 8rem;
.item {
display: flex;
flex-direction: column;
gap: 12rem;
flex: 1;
border-radius: 8rem;
display: flex;
flex-direction: column;
padding: 12rem;
gap: 12rem;
background: #fcfdfd;
box-shadow: 0rem 4rem 10rem 0rem #e7effd;
.title {
margin-left: 18rem;
font-family: PingFang SC;
font-size: 12rem;
font-weight: normal;
line-height: 16rem;
letter-spacing: 0rem;
/* 科个中性/正文-副标题 7E849F */
color: #7e849f;
}
.content {
.nums {
font-family: DIN-Medium;
font-size: 24rem;
font-weight: bold;
line-height: 24rem;
letter-spacing: 0rem;
font-variation-settings: 'opsz' auto;
color: #17204d;
.num {
display: flex;
align-items: flex-end;
}
span {
font-family: PingFang SC;
font-size: 12rem;
font-weight: normal;
line-height: 16rem;
letter-spacing: 0rem;
margin: 0 3rem 0 3rem;
/* 中性/重要文本色 */
/* 样式描述:17204D */
color: #17204d;
}
}
.text {
font-family: PingFang SC;
font-size: 12rem;
font-weight: normal;
line-height: 16rem;
letter-spacing: 0rem;
/* 中性/次要文本色 */
/* 样式描述:7E849F */
color: #7e849f;
align-self: flex-end;
}
}
.btn {
border-radius: 100rem;
font-family: PingFang SC;
font-size: 14rem;
font-weight: 500;
line-height: 19rem;
text-align: center;
letter-spacing: 0rem;
color: #ffffff;
padding: 4rem 12rem;
height: 28rem;
cursor: pointer;
}
}
.singleItem {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
>div {
margin-left: 36rem;
.title {
font-family: PingFang SC;
font-size: 14rem;
font-weight: 500;
line-height: normal;
letter-spacing: 0rem;
margin-left: 0;
/* 科个中性/正文-标题 17204D */
/* 样式描述:用于正文标题 */
color: #17204d;
margin-bottom: 4rem;
}
.content {
.nums {
font-size: 12rem;
font-weight: normal;
line-height: 16rem;
letter-spacing: 0rem;
z-index: 1;
font-family: PingFang SC;
font-weight: 500;
color: #17204d;
span {
color: #7e849f;
margin-left: 0;
}
}
}
}
}
}
}
</style>
<script setup>
import { ref, onMounted, onActivated, computed, inject } from 'vue';
import { Post } from '@/utils/https'
import { messageSend } from '@/utils/api'
import { getHiddenProp, getVisibilityState, decryptByVersionNo1, isSit, Encrypt, isDev, isProd } from "@/utils";
const store = inject('appStore', null)
const businessHandleInfo = ref({})
const orderCount = ref(0)
const emit = defineEmits(['ready', 'businessClick'])
const abstractList = computed(() => [
{
title: '保障状态',
num: orderCount.value,
text: '保单保障中',
unit: '张',
name: '查保单',
btnText: '查保单',
btnBg: 'linear-gradient(90deg, #025cea 0%, #0595f6 100%)',
},
{
title: '待办任务',
num: businessHandleInfo.value?.contents?.length || 0,
text: '待办需处理',
unit: '个',
name: '去处理',
btnText: '去处理',
btnBg: 'linear-gradient(111deg, #ff752a 44%, #feb52d 136%)',
},
{
title: '权益领取',
num: 0,
text: '生存金权益',
unit: '项',
name: '去领取',
btnText: '去领取',
btnBg: 'linear-gradient(261deg, #2ed0c6 3%, #08b7ac 65%)',
},
].filter(item => item.num > 0))
const businessHandle = (userId) => {
if (
store.source == "95270000" ||
store.source == "95320000" ||
store.source == "95350000" ||
store.source == "96060000"
) {
return Post("notify/info", { userInfoId: userId })
.then((res) => {
if (res.data?.contents?.length > 0) {
businessHandleInfo.value = res.data
} else {
businessHandleInfo.value = {}
}
store?.setBusinessHandleInfo?.(businessHandleInfo.value)
})
.catch((error) => {
console.log(error);
});
}
return Promise.resolve()
}
const businessClick = () => {
emit('businessClick')
}
const isIdentityNo = (value) => /^\d{15}$|^\d{17}[\dXx]$/.test(String(value || '').trim())
// 与 liics 一致:明文身份证来自解密 userInfo.pid(encryptPid 是给外部系统用的密文,不能直接解)
const getIdentityCardno = () => {
const versionNo = store.userInfo?.versionNo1 || ''
const pidCipher = store.userInfo?.pid || ''
if (!versionNo || !pidCipher) return ''
const identityCardno = decryptByVersionNo1(versionNo, pidCipher)
console.log('decrypt pid =>', JSON.stringify(identityCardno))
return isIdentityNo(identityCardno) ? identityCardno.trim() : ''
}
const getOrderCount = () => {
try {
const identityCardno = getIdentityCardno()
if (!identityCardno) {
console.warn('证件号解密失败', {
hasPid: Boolean(store.userInfo?.pid),
hasVersionNo1: Boolean(store.userInfo?.versionNo1),
})
return Promise.resolve()
}
return Post('policy/queryCount', {
dentityCardno: identityCardno,
userId: store.userId
}).then(res => {
orderCount.value = res?.data?.policyCount || 0
}).catch((error) => {
console.log(error)
})
} catch (error) {
console.log(error)
return Promise.resolve()
}
}
const toDetail = (record) => {
if (record.title === '待办任务') {
businessClick()
}
if (record.title === '保障状态') {
// 只把问题写入对话气泡,不走对话接口
store.addSendMessageList({ msgText: record.name, reqAndResType: '1', sendLoading: false })
}
}
const refreshAbstractData = async () => {
await Promise.all([businessHandle(store?.userId), getOrderCount()])
}
const hasLoadedOnce = ref(false)
onMounted(async () => {
try {
await refreshAbstractData()
} finally {
hasLoadedOnce.value = true
emit('ready')
}
})
// keep-alive 返回 chat 时刷新待办和保单数量
onActivated(async () => {
if (!hasLoadedOnce.value) return
await refreshAbstractData()
})
</script>
<template>
<div class="abstract-con" v-if="abstractList.length">
<div class="title">
<span>今日皮卡丘摘要</span><img src="@/assets/images/chat/star.svg" alt="" srcset="" />
</div>
<div class="list">
<div
:class="`item ${(abstractList || []).length === 1 ? 'singleItem' : ''}`"
v-for="(item, index) in abstractList || []"
:key="index"
>
<div>
<div class="title">{{ item.title }}</div>
<div
class="content"
:style="{ display: (abstractList || []).length > 2 ? 'block' : 'flex' }"
>
<div class="nums">
<div class="num">
{{ item.num }}<span>{{ item.unit }}</span>
</div>
</div>
<div
class="text"
:style="{ marginTop: (abstractList || []).length > 2 ? '8rem' : '0' }"
>
{{ item.text }}
</div>
</div>
</div>
<div class="btn" :style="{ background: item.btnBg }" @click="toDetail(item)">
{{ item.btnText }}
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.abstract-con {
position: relative;
z-index: 1;
flex-direction: column;
justify-content: center;
padding: 10rem 12rem;
gap: 12rem;
align-self: stretch;
background: linear-gradient(77deg, #fcfdfd80 -1%, #f5f7fee6 99%);
margin-bottom: 16rem;
box-sizing: border-box;
border: 1rem solid rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10rem);
border-radius: 12rem;
.title {
font-family: PingFang SC;
font-size: 16rem;
font-weight: 500;
line-height: normal;
letter-spacing: 0rem;
/* 科个中性/正文-标题 17204D */
/* 样式描述:用于正文标题 */
color: #17204d;
img {
width: 20rem;
margin-left: 4.6rem;
}
}
.list {
margin-top: 13rem;
display: flex;
gap: 8rem;
.item {
display: flex;
flex-direction: column;
gap: 12rem;
flex: 1;
border-radius: 8rem;
display: flex;
flex-direction: column;
padding: 12rem;
gap: 12rem;
background: #fcfdfd;
box-shadow: 0rem 4rem 10rem 0rem #e7effd;
.title {
margin-left: 18rem;
font-family: PingFang SC;
font-size: 12rem;
font-weight: normal;
line-height: 16rem;
letter-spacing: 0rem;
/* 科个中性/正文-副标题 7E849F */
color: #7e849f;
}
.content {
.nums {
font-family: DIN-Medium;
font-size: 24rem;
font-weight: bold;
line-height: 24rem;
letter-spacing: 0rem;
font-variation-settings: 'opsz' auto;
color: #17204d;
.num {
display: flex;
align-items: flex-end;
}
span {
font-family: PingFang SC;
font-size: 12rem;
font-weight: normal;
line-height: 16rem;
letter-spacing: 0rem;
margin: 0 3rem 0 3rem;
/* 中性/重要文本色 */
/* 样式描述:17204D */
color: #17204d;
}
}
.text {
font-family: PingFang SC;
font-size: 12rem;
font-weight: normal;
line-height: 16rem;
letter-spacing: 0rem;
/* 中性/次要文本色 */
/* 样式描述:7E849F */
color: #7e849f;
align-self: flex-end;
}
}
.btn {
border-radius: 100rem;
font-family: PingFang SC;
font-size: 14rem;
font-weight: 500;
line-height: 19rem;
text-align: center;
letter-spacing: 0rem;
color: #ffffff;
padding: 4rem 12rem;
height: 28rem;
cursor: pointer;
}
}
.singleItem {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
> div {
margin-left: 36rem;
.title {
font-family: PingFang SC;
font-size: 14rem;
font-weight: 500;
line-height: normal;
letter-spacing: 0rem;
margin-left: 0;
/* 科个中性/正文-标题 17204D */
/* 样式描述:用于正文标题 */
color: #17204d;
margin-bottom: 4rem;
}
.content {
.nums {
font-size: 12rem;
font-weight: normal;
line-height: 16rem;
letter-spacing: 0rem;
z-index: 1;
font-family: PingFang SC;
font-weight: 500;
color: #17204d;
span {
color: #7e849f;
margin-left: 0;
}
}
}
}
}
}
}
</style>

@ -1,168 +1,211 @@
<template>
<div>
<van-swipe style="height:30rem;margin-top:10rem" class="businessBox" vertical :scrollable="false" :autoplay="3000"
:show-indicators="false">
<van-swipe-item @click="swiperClick" v-for="(item, index) in businessHandleInfo.contents" :key="index">
<div class="item">
<img :src="getIconUrl(item.icon)" alt="" style="width:30rem;height:30rem;">
<div class="item-content">
<div class="line">{{ item.name }}</div>
<van-icon name="arrow-down" />
</div>
</div>
</van-swipe-item>
</van-swipe>
<van-popup get-container="#app" v-model:show="Popupshow" round position="bottom"
:style="{ padding: '15rem', boxSizing: ' border-box' }">
<div class="displayFlex">
<div style="font-size: 13rem;color: #878D99;">
您有{{ businessHandleInfo.contents.length }}个待办{{ businessHandleInfo.tip }}</div>
<van-icon name="close" color="#5A9CFF" @click="Popupshow = false" />
</div>
<div class="popupContent">
<div class="item popupContentItem" v-for="(item, index) in businessHandleInfo.contents" :key="index"
@click="richCallAnswer(item)">
<img :src="getIconUrl(item.icon)" alt="" style="width:30rem;height:30rem;">
<div class="item-content">
<div>{{ item.name }}</div>
<van-icon name="arrow" color="#5A9CFF" />
</div>
</div>
</div>
</van-popup>
</div>
</template>
<script setup>
import { ref, inject, computed } from 'vue'
import { Icon, Swipe, SwipeItem, Popup } from 'vant'
defineProps({
businessHandleInfo: { type: Object, default: () => ({}) }
})
const store = inject('appStore', null)
const userId = inject('userId', ref(''))
const mescrollContent = inject('mescrollContent', ref(null))
const manualService = inject('manualService', ref('1'))
const Popupshow = ref(false)
const isBtnsubmit = ref(true)
function getIconUrl(icon) {
try {
return require(`../assets/images/${icon}.png`)
} catch (e) {
return ''
}
}
function swiperClick() {
Popupshow.value = true
}
function richCallAnswer(item) {
const text = item.name
const serialNumber = item.serialNumber
const sessionId = item.sessionId
const mdParmas = {
userInfoId: userId.value,
sessionId,
serialNumber,
clickTime: new Date().getTime()
}
store?.dispatch?.('updateClickTime', mdParmas).catch(err => {
console.error(err)
})
let sendParmas = {
cmd: 'business-handle',
reqAndResType: '1',
msgText: '&' + text + '&',
intentId: '',
msgSource: manualService.value,
questionType: 6
}
if (isBtnsubmit.value) {
isBtnsubmit.value = false
Popupshow.value = false
scrollTopBody()
store?.dispatch?.('sendingRobot', sendParmas).then(() => {
isBtnsubmit.value = true
}).catch(err => {
isBtnsubmit.value = true
console.log(err)
})
}
}
function scrollTopBody() {
setTimeout(() => {
try {
if (mescrollContent.value) {
mescrollContent.value.scrollTop = mescrollContent.value.scrollHeight || 0
}
} catch (error) {
console.log(error.message)
}
}, 300)
}
</script>
<style scoped>
.businessBox {
background: #fff;
padding: 10rem;
border-radius: 16rem 16rem 16rem 2rem;
}
.item {
display: flex;
align-items: center;
justify-content: space-around;
}
.item>img {
width: 30rem;
height: 30rem;
margin-right: 10rem;
}
.item-content {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 15rem;
}
.line {
font-size: 14rem;
}
.displayFlex {
display: flex;
align-items: center;
justify-content: space-between;
}
.popupContent {
min-height: 180rem;
max-height: 290rem;
overflow: auto;
}
.popupContentItem {
height: 50rem;
background: #FFFFFF;
border: 1rem solid #F1F3F6;
padding: 13rem;
box-shadow: 0 2rem 8rem 0 rgba(229, 234, 237, 0.50);
border-radius: 8rem;
box-sizing: border-box;
margin: 7rem 5rem;
}
</style>
<template>
<div>
<!-- <van-swipe style="height:30rem;margin-top:10rem" class="businessBox" vertical :scrollable="false" :autoplay="3000"
:show-indicators="false">
<van-swipe-item @click="swiperClick" v-for="(item, index) in businessHandleInfo.contents" :key="index">
<div class="item">
<img :src="getIconUrl(item.icon)" alt="" style="width:30rem;height:30rem;">
<div class="item-content">
<div class="line">{{ item.name }}</div>
<van-icon name="arrow-down" />
</div>
</div>
</van-swipe-item>
</van-swipe> -->
<van-popup
v-model:show="Popupshow"
teleport="body"
round
position="bottom"
closeable
:z-index="3000"
>
<div class="displayFlex">
<div class="title">您有{{ displayInfo?.contents?.length || 0 }}个待办</div>
<div class="popupContent">
<div
class="item popupContentItem"
v-for="(item, index) in displayInfo.contents || []"
:key="index"
@click="richCallAnswer(item)"
>
<img
src="@/assets/images/chat/todo-icon.svg"
alt=""
style="width: 22rem; height: 22rem"
/>
<div class="item-content">
<div>{{ item.name }}</div>
<van-icon name="arrow" color="#5A9CFF" />
</div>
</div>
</div>
</div>
</van-popup>
</div>
</template>
<script setup>
import { ref, inject, computed, watch } from 'vue'
import { Icon, Swipe, SwipeItem, Popup } from 'vant'
const props = defineProps({
businessHandleInfo: { type: Object, default: () => ({}) },
autoOpen: { type: Boolean, default: true },
})
const store = inject('appStore', null)
const userId = inject('userId', ref(''))
const mescrollContent = inject('mescrollContent', ref(null))
const manualService = inject('manualService', ref('1'))
const Popupshow = ref(false)
const isBtnsubmit = ref(true)
const hasAutoOpened = ref(false)
const displayInfo = computed(() => {
const fromProp = props.businessHandleInfo
if (fromProp && Array.isArray(fromProp.contents) && fromProp.contents.length) return fromProp
return store?.businessHandleInfo || {}
})
function getIconUrl(icon) {
try {
return new URL(`../assets/images/${icon}.png`, import.meta.url).href
} catch (e) {
return ''
}
}
function swiperClick() {
Popupshow.value = true
}
function richCallAnswer(item) {
const text = item.name
const serialNumber = item.serialNumber
const sessionId = item.sessionId
const mdParmas = {
userInfoId: userId.value,
sessionId,
serialNumber,
clickTime: new Date().getTime()
}
store?.dispatch?.('updateClickTime', mdParmas).catch(err => {
console.error(err)
})
let sendParmas = {
cmd: 'business-handle',
reqAndResType: '1',
msgText: '&' + text + '&',
intentId: '',
msgSource: manualService.value,
questionType: 6
}
if (isBtnsubmit.value) {
isBtnsubmit.value = false
Popupshow.value = false
scrollTopBody()
const sending = store?.sendingRobot?.(sendParmas) || store?.dispatch?.('sendingRobot', sendParmas)
Promise.resolve(sending).then(() => {
isBtnsubmit.value = true
}).catch(err => {
isBtnsubmit.value = true
console.log(err)
})
}
}
function scrollTopBody() {
setTimeout(() => {
try {
if (mescrollContent.value) {
mescrollContent.value.scrollTop = mescrollContent.value.scrollHeight || 0
}
} catch (error) {
console.log(error.message)
}
}, 300)
}
defineExpose({
swiperClick
})
</script>
<style scoped lang="scss">
.businessBox {
background: #fff;
padding: 10rem;
border-radius: 16rem 16rem 16rem 2rem;
}
.item {
display: flex;
justify-content: space-around;
}
.item > img {
width: 30rem;
height: 30rem;
margin-right: 10rem;
}
.item-content {
flex: 1;
display: flex;
justify-content: space-between;
font-size: 15rem;
i{
padding-top: 4rem;
}
}
.line {
font-size: 14rem;
}
.displayFlex {
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 16rem 16rem 0rem 0rem;
display: flex;
flex-direction: column;
align-items: center;
padding: 16rem 16rem 0rem 16rem;
gap: 24rem;
width: 100vw;
background: linear-gradient(180deg, #f0f8ff 0%, #f6f9ff 100%);
.title {
font-family: PingFang SC;
font-size: 16rem;
font-weight: 500;
line-height: normal;
text-align: center;
letter-spacing: 0rem;
font-variation-settings: 'opsz' auto;
}
}
.popupContent {
display: flex;
flex-direction: column;
max-height: 300rem;
overflow: auto;
width: 100%;
gap: 12rem;
padding-bottom: 12rem;
}
.popupContentItem {
background: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.9);
padding: 12px;
gap: 12px;
border-radius: 12px;
}
</style>

File diff suppressed because it is too large Load Diff

@ -96,7 +96,7 @@
<script setup>
// 原 Vue2 Options API 组件 chat-main(keep-alive 缓存名沿用 "chat")
defineOptions({ name: 'chat' })
defineOptions({ name: 'chat-main' })
import { ref, computed, watch, onMounted, onUnmounted, onActivated, nextTick } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'

File diff suppressed because it is too large Load Diff

@ -14,7 +14,7 @@ const initHotQuestion = async () => {
sceneId: store.userInfo.sceneId || "",
};
Post("config/activity", param)
return Post("config/activity", param)
.then((res) => {
if (res.code && res.code != 0) {
showToast(res.msg);
@ -72,8 +72,13 @@ const handleAskClick = (record) => {
isItemsubmit.value = true
})
}
const emit = defineEmits(['ready'])
onMounted(async () => {
await initHotQuestion()
try {
await initHotQuestion()
} finally {
emit('ready')
}
})
</script>
<template>
@ -95,7 +100,7 @@ onMounted(async () => {
display: flex;
flex-direction: column;
gap: 10rem;
margin-top: 16rem;
margin-bottom: 16rem;
position: relative;
z-index: 1;

@ -1,457 +1,457 @@
<template>
<div class="text-template-box" v-if="introduce">
<div
class="question-answer-con"
v-if="introduce"
ref="desc"
:class="showTotal || heightMax == 'autoHeight' ? 'total-introduce' : 'detailed-introduce'"
>
<p
:class="{ connectionArtificial: disabled }"
v-html="replacementRagEmoticon(introduce)"
:data-qaid="itemlist && itemlist.qaId ? itemlist.qaId : ''"
></p>
</div>
<div class="content-question-answer" v-if="intents && intents.length > 0">
<button
class="answer-items"
v-for="(items, index) in intents"
:key="index"
:disabled="disabled ? true : false"
@click.stop="richCallAnswer(items)"
>
{{ items.intent }}
</button>
</div>
<div
class="unfold-box"
:class="exchangeButton ? 'unfold-bg' : 'unfold-style'"
v-if="(!intents || intents.length == 0) && showExchangeButton"
>
<a href="javascript:;" @click="showTotalIntro"
>{{ exchangeButton ? '展开' : '收起'
}}<em class="look-more-icon" :class="!exchangeButton ? 'active' : ''"></em
></a>
</div>
</div>
</template>
<script setup>
import { ref, inject, onMounted, nextTick } from 'vue'
import { showToast, showLoadingToast, closeToast } from 'vant'
import faceJSON from '@/mock/face.json'
import utils from '@/utils/utils'
import wx from 'weixin-js-sdk'
const props = defineProps({
introduce: { type: String, default: '' },
intents: { type: Array, default: () => [] },
disabled: { type: Boolean, default: false },
heightMax: { type: String, default: '' },
itemlist: { type: Object, default: () => ({}) },
})
const store = inject('appStore', null)
const userId = inject('userId', ref(''))
const userInfo = inject('userInfo', ref({}))
const mescrollContent = inject('mescrollContent', ref(null))
const manualService = inject('manualService', ref('1'))
const sendLoading = inject('sendLoading', ref(false))
const publicPath = import.meta.env.BASE_URL
const showTotal = ref(true)
const exchangeButton = ref(true)
const showExchangeButton = ref(false)
const expressionProcessing = ref({})
const bigExpressionProcessing = ref({})
const isBtnsubmit = ref(true)
const desc = ref(null)
onMounted(() => {
processedEmoji()
descHeightChange()
nextTick(() => {
initGlobalCallbacks()
})
})
function initGlobalCallbacks() {
try {
window.getLink = (item) => {
utils.AppToHttpHtml(item)
}
window.jumpOpenLink = (item) => {
utils.AppToHttpHtml(item)
}
window.jumpOpenWeb = (type, url, urlType) => {
let params = {}
if (type === 'YY_0000') {
params = {
idnum: '1',
ownerId: userInfo.value?.encryptPid,
openid: userInfo.value?.sourceUserId,
}
const completeURL = handleURL(url, params)
store
?.dispatch?.('post', {
url: 'semantic/getYdbqTicket',
data: { url: completeURL, userId: userId.value },
})
.then((res) => {
if (res.code === 0) {
if (!res.data) {
utils.openWxPage(url, params)
} else {
utils.openWxPage(url, { accessTicket: res.data })
}
} else {
showToast('网络异常,请重试')
}
})
} else if (type === 'YY_ydbq') {
params = {
mobile: userInfo.value?.phone,
pid: userInfo.value?.encryptPid,
openid: userInfo.value?.sourceUserId,
}
const completeURL = handleURL(url, params)
store
?.dispatch?.('post', {
url: 'semantic/getYdbqTicket',
data: { url: completeURL, userId: userId.value },
})
.then((res) => {
if (res.code === 0) {
if (!res.data) {
utils.openWxPage(url, params)
} else {
utils.openWxPage(url, { accessTicket: res.data })
}
} else {
showToast('网络异常,请重试')
}
})
} else if (type === 'YY_APP_H5') {
utils.AppOpenH5(url, { mobile: userInfo.value?.phone, pid: userInfo.value?.encryptPid })
} else if (type === 'ydbq_2') {
utils.openSpecPage({ url, generateType: type, userId: userId.value })
} else if (type === 'YY_CMD') {
const newUrl =
url +
`&idnum=1&openId=${userInfo.value?.sourceUserId}&entryOpenid=${userInfo.value?.entryOpenid}`
store
?.dispatch?.('post', {
url: 'semantic/getYdbqTicket',
data: { url: newUrl, userId: userId.value, isCmd: '1' },
})
.then((res) => {
if (res.code === 0) {
if (!res.data) {
utils.AppToHttpHtml(newUrl)
} else {
utils.AppToHttpHtml(url + `&accessTicket=${res.data}`)
}
} else {
showToast('网络异常,请重试')
}
})
} else if (type === 'YY_hfhz') {
store
?.dispatch?.('post', {
url: 'semantic/xdhfCreateToken',
data: { certNum: '1', systemSource: 'yykf', userId: userId.value },
})
.then((res) => {
const entryptedId = userInfo.value?.versionHFHZ
utils.openWxPage(url, {
sessionToken: res.data,
holderId: encodeURI(entryptedId),
holderType: 'a',
hzhfSystemSource: '0803',
ishwFlag: '1',
isqshzFlag: '1',
})
})
} else if (type === 'YY_1v1') {
store
?.dispatch?.('post', { url: 'thirdPartyAuth/getExclusiveServicesToken' })
.then((res) => {
if (res.code === 0 && res.data) {
utils.openWxPage(url, {
sour: 'yykf',
omoSource: userInfo.value?.source,
omoSourceName: encodeURIComponent(userInfo.value?.sourceName),
token: res.data,
})
} else {
showToast('网络异常,请重试')
}
})
} else if (type === 'YY_yzsfx') {
store
?.dispatch?.('post', {
url: 'semantic/getYzsUrl',
data: { userId: userId.value, bqType: url, urlType },
})
.then((res) => {
if (res.code === 0 && res.data) {
utils.openWxPage(res.data, '')
} else {
showToast('网络异常,请重试')
}
})
} else if (type === 'YY_GWXCX') {
wx.miniProgram.navigateTo({ url: url })
} else if (type === 'YY_GWXCX_HOME') {
wx.miniProgram.reLaunch({ url: url })
}
}
window.openAppPage = (params) => {
if (params) {
params = params.replace(/\'/g, '"')
utils.AppToOutModules(params)
}
}
window.connectionArtificial = (event) => {
let isClick = false,
isclassName = false
if (event.parentElement && !event.parentElement.className) {
isclassName = false
} else {
isclassName = true
}
if (isclassName && event.parentElement) {
isClick = utils.hasClass(event.parentElement, 'connectionArtificial')
}
if (!isclassName || !isClick) {
connectionArtificial(event)
}
}
} catch (error) {
console.log(error)
}
}
function handleURL(url, params) {
let flag = url.indexOf('?') != -1
let urlPar = ''
if (params) {
Object.getOwnPropertyNames(params).forEach((item, index) => {
if (index === 0 && !flag) {
urlPar += `?${item}=${params[item]}`
} else {
urlPar += `&${item}=${params[item]}`
}
})
}
return url + urlPar
}
function replacementRagEmoticon(str) {
str = str || ''
let res = str.replace(/\\n/gm, '<br/>')
return emojiToImage(res)
}
function processedEmoji() {
const tempExp = {}
const tempBigExp = {}
faceJSON.forEach((item) => {
item.faceList.forEach((items) => {
tempExp[`[${items.title}]`] = items.src
})
item.bigExpression.forEach((items) => {
tempBigExp[`[${items.title}]`] = items.src
})
})
expressionProcessing.value = tempExp
bigExpressionProcessing.value = tempBigExp
}
function emojiToImage(str) {
let srcImg = ''
let regex = /\[[^\]]+\]/g
srcImg = str.replace(regex, (item) => {
if (expressionProcessing.value[item]) {
return `<img src="${
publicPath + expressionProcessing.value[item]
}" alt="small-image" title="${item}" />`
} else if (bigExpressionProcessing.value[item]) {
return `<img src="${
publicPath + bigExpressionProcessing.value[item]
}" alt="big-image" title="${item}"/>`
}
return item
})
return srcImg
}
function showTotalIntro() {
showTotal.value = !showTotal.value
exchangeButton.value = !exchangeButton.value
}
function getRem() {
let winWidth = window.innerWidth
const defaultRem = document.body.style.fontSize.replace('rem', '')
let rem = (winWidth / 375) * defaultRem
return rem
}
function descHeightChange() {
nextTick(() => {
let rem = parseFloat(getRem())
if (!desc.value) return
let descHeight = window.getComputedStyle(desc.value).height.replace('rem', '')
descHeight = descHeight || desc.value.offsetHeight
if (descHeight > 2.325 * rem) {
showExchangeButton.value = true
exchangeButton.value = true
showTotal.value = false
} else {
showExchangeButton.value = false
showTotal.value = true
}
})
}
function scrollTopBody() {
setTimeout(() => {
try {
if (mescrollContent.value)
mescrollContent.value.scrollTop = mescrollContent.value.scrollHeight || 0
} catch (e) {
console.log(e.message)
}
}, 300)
}
function connectionArtificial(event) {
try {
let qaId = event.parentNode ? event.parentNode.dataset.qaid : ''
let sendParmas = { reqAndResType: '1', msgText: '&转人工服务&', msgSource: manualService.value }
scrollTopBody()
store
?.dispatch?.('connectionArtificial', sendParmas)
.then(() => {
event.disabled = true
})
.catch((err) => console.log(err))
store
?.dispatch?.('userBehavior', { qaId, eventType: '5', eventMsg: '知识库进入人工服务' })
.catch((err) => console.log(err))
} catch (error) {
console.log(error)
}
}
function richCallAnswer(item) {
if (!isBtnsubmit.value) return
isBtnsubmit.value = false
let sendParmas = {
reqAndResType: '1',
msgText: item.intent,
intentId: item.intentId,
msgSource: manualService.value,
}
scrollTopBody()
store
?.dispatch?.('sendingRobot', sendParmas)
.then(() => {
isBtnsubmit.value = true
})
.catch((err) => {
isBtnsubmit.value = true
console.log(err)
})
}
function manualAnswer(event, item) {
let sendParmas = { reqAndResType: '1', msgText: '&转人工服务&', msgSource: manualService.value }
const toast = showLoadingToast({
message: '加载中...',
duration: 0,
loadingType: 'spinner',
forbidClick: true,
})
if (!sendLoading.value) {
scrollTopBody()
store
?.dispatch?.('sendingRobot', sendParmas)
.then(() => {
closeToast()
event.disabled = true
})
.catch((err) => {
closeToast()
console.log(err)
})
} else {
closeToast()
}
}
</script>
<style scoped>
.text-template-box {
padding: 0;
}
.question-answer-con {
overflow: hidden;
}
.total-introduce {
max-height: none;
}
.detailed-introduce {
max-height: 2.325rem;
}
.content-question-answer {
display: flex;
flex-wrap: wrap;
gap: 8rem;
margin-top: 10rem;
}
.answer-items {
padding: 6rem 12rem;
background: #f0f6ff;
border: 1rem solid #d6e4ff;
border-radius: 16rem;
font-size: 13rem;
color: #025cea;
cursor: pointer;
}
.answer-items[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
.unfold-box {
text-align: center;
margin-top: 8rem;
}
.unfold-box a {
font-size: 13rem;
color: #025cea;
}
.look-more-icon {
display: inline-block;
width: 12rem;
height: 12rem;
margin-left: 4rem;
transition: transform 0.3s;
}
.look-more-icon.active {
transform: rotate(180deg);
}
</style>
<template>
<div class="text-template-box" v-if="introduce">
<div
class="question-answer-con"
v-if="introduce"
ref="desc"
:class="showTotal || heightMax == 'autoHeight' ? 'total-introduce' : 'detailed-introduce'"
>
<p
:class="{ connectionArtificial: disabled }"
v-html="replacementRagEmoticon(introduce)"
:data-qaid="itemlist && itemlist.qaId ? itemlist.qaId : ''"
></p>
</div>
<div class="content-question-answer" v-if="intents && intents.length > 0">
<button
class="answer-items"
v-for="(items, index) in intents"
:key="index"
:disabled="disabled ? true : false"
@click.stop="richCallAnswer(items)"
>
{{ items.intent }}
</button>
</div>
<div
class="unfold-box"
:class="exchangeButton ? 'unfold-bg' : 'unfold-style'"
v-if="(!intents || intents.length == 0) && showExchangeButton"
>
<a href="javascript:;" @click="showTotalIntro"
>{{ exchangeButton ? '展开' : '收起'
}}<em class="look-more-icon" :class="!exchangeButton ? 'active' : ''"></em
></a>
</div>
</div>
</template>
<script setup>
import { ref, inject, onMounted, nextTick } from 'vue'
import { showToast, showLoadingToast, closeToast } from 'vant'
import faceJSON from '@/mock/face.json'
import utils from '@/utils/utils'
import wx from 'weixin-js-sdk'
const props = defineProps({
introduce: { type: String, default: '' },
intents: { type: Array, default: () => [] },
disabled: { type: Boolean, default: false },
heightMax: { type: String, default: '' },
itemlist: { type: Object, default: () => ({}) },
})
const store = inject('appStore', null)
const userId = inject('userId', ref(''))
const userInfo = inject('userInfo', ref({}))
const mescrollContent = inject('mescrollContent', ref(null))
const manualService = inject('manualService', ref('1'))
const sendLoading = inject('sendLoading', ref(false))
const publicPath = import.meta.env.BASE_URL
const showTotal = ref(true)
const exchangeButton = ref(true)
const showExchangeButton = ref(false)
const expressionProcessing = ref({})
const bigExpressionProcessing = ref({})
const isBtnsubmit = ref(true)
const desc = ref(null)
onMounted(() => {
processedEmoji()
descHeightChange()
nextTick(() => {
initGlobalCallbacks()
})
})
function initGlobalCallbacks() {
try {
window.getLink = (item) => {
utils.AppToHttpHtml(item)
}
window.jumpOpenLink = (item) => {
utils.AppToHttpHtml(item)
}
window.jumpOpenWeb = (type, url, urlType) => {
let params = {}
if (type === 'YY_0000') {
params = {
idnum: '1',
ownerId: userInfo.value?.encryptPid,
openid: userInfo.value?.sourceUserId,
}
const completeURL = handleURL(url, params)
store
?.dispatch?.('post', {
url: 'semantic/getYdbqTicket',
data: { url: completeURL, userId: userId.value },
})
.then((res) => {
if (res.code === 0) {
if (!res.data) {
utils.openWxPage(url, params)
} else {
utils.openWxPage(url, { accessTicket: res.data })
}
} else {
showToast('网络异常,请重试')
}
})
} else if (type === 'YY_ydbq') {
params = {
mobile: userInfo.value?.phone,
pid: userInfo.value?.encryptPid,
openid: userInfo.value?.sourceUserId,
}
const completeURL = handleURL(url, params)
store
?.dispatch?.('post', {
url: 'semantic/getYdbqTicket',
data: { url: completeURL, userId: userId.value },
})
.then((res) => {
if (res.code === 0) {
if (!res.data) {
utils.openWxPage(url, params)
} else {
utils.openWxPage(url, { accessTicket: res.data })
}
} else {
showToast('网络异常,请重试')
}
})
} else if (type === 'YY_APP_H5') {
utils.AppOpenH5(url, { mobile: userInfo.value?.phone, pid: userInfo.value?.encryptPid })
} else if (type === 'ydbq_2') {
utils.openSpecPage({ url, generateType: type, userId: userId.value })
} else if (type === 'YY_CMD') {
const newUrl =
url +
`&idnum=1&openId=${userInfo.value?.sourceUserId}&entryOpenid=${userInfo.value?.entryOpenid}`
store
?.dispatch?.('post', {
url: 'semantic/getYdbqTicket',
data: { url: newUrl, userId: userId.value, isCmd: '1' },
})
.then((res) => {
if (res.code === 0) {
if (!res.data) {
utils.AppToHttpHtml(newUrl)
} else {
utils.AppToHttpHtml(url + `&accessTicket=${res.data}`)
}
} else {
showToast('网络异常,请重试')
}
})
} else if (type === 'YY_hfhz') {
store
?.dispatch?.('post', {
url: 'semantic/xdhfCreateToken',
data: { certNum: '1', systemSource: 'yykf', userId: userId.value },
})
.then((res) => {
const entryptedId = userInfo.value?.versionHFHZ
utils.openWxPage(url, {
sessionToken: res.data,
holderId: encodeURI(entryptedId),
holderType: 'a',
hzhfSystemSource: '0803',
ishwFlag: '1',
isqshzFlag: '1',
})
})
} else if (type === 'YY_1v1') {
store
?.dispatch?.('post', { url: 'thirdPartyAuth/getExclusiveServicesToken' })
.then((res) => {
if (res.code === 0 && res.data) {
utils.openWxPage(url, {
sour: 'yykf',
omoSource: userInfo.value?.source,
omoSourceName: encodeURIComponent(userInfo.value?.sourceName),
token: res.data,
})
} else {
showToast('网络异常,请重试')
}
})
} else if (type === 'YY_yzsfx') {
store
?.dispatch?.('post', {
url: 'semantic/getYzsUrl',
data: { userId: userId.value, bqType: url, urlType },
})
.then((res) => {
if (res.code === 0 && res.data) {
utils.openWxPage(res.data, '')
} else {
showToast('网络异常,请重试')
}
})
} else if (type === 'YY_GWXCX') {
wx.miniProgram.navigateTo({ url: url })
} else if (type === 'YY_GWXCX_HOME') {
wx.miniProgram.reLaunch({ url: url })
}
}
window.openAppPage = (params) => {
if (params) {
params = params.replace(/\'/g, '"')
utils.AppToOutModules(params)
}
}
window.connectionArtificial = (event) => {
let isClick = false,
isclassName = false
if (event.parentElement && !event.parentElement.className) {
isclassName = false
} else {
isclassName = true
}
if (isclassName && event.parentElement) {
isClick = utils.hasClass(event.parentElement, 'connectionArtificial')
}
if (!isclassName || !isClick) {
connectionArtificial(event)
}
}
} catch (error) {
console.log(error)
}
}
function handleURL(url, params) {
let flag = url.indexOf('?') != -1
let urlPar = ''
if (params) {
Object.getOwnPropertyNames(params).forEach((item, index) => {
if (index === 0 && !flag) {
urlPar += `?${item}=${params[item]}`
} else {
urlPar += `&${item}=${params[item]}`
}
})
}
return url + urlPar
}
function replacementRagEmoticon(str) {
str = str || ''
let res = str.replace(/\\n/gm, '<br/>')
return emojiToImage(res)
}
function processedEmoji() {
const tempExp = {}
const tempBigExp = {}
faceJSON.forEach((item) => {
item.faceList.forEach((items) => {
tempExp[`[${items.title}]`] = items.src
})
item.bigExpression.forEach((items) => {
tempBigExp[`[${items.title}]`] = items.src
})
})
expressionProcessing.value = tempExp
bigExpressionProcessing.value = tempBigExp
}
function emojiToImage(str) {
let srcImg = ''
let regex = /\[[^\]]+\]/g
srcImg = str.replace(regex, (item) => {
if (expressionProcessing.value[item]) {
return `<img src="${
publicPath + expressionProcessing.value[item]
}" alt="small-image" title="${item}" />`
} else if (bigExpressionProcessing.value[item]) {
return `<img src="${
publicPath + bigExpressionProcessing.value[item]
}" alt="big-image" title="${item}"/>`
}
return item
})
return srcImg
}
function showTotalIntro() {
showTotal.value = !showTotal.value
exchangeButton.value = !exchangeButton.value
}
function getRem() {
let winWidth = window.innerWidth
const defaultRem = document.body.style.fontSize.replace('rem', '')
let rem = (winWidth / 375) * defaultRem
return rem
}
function descHeightChange() {
nextTick(() => {
let rem = parseFloat(getRem())
if (!desc.value) return
let descHeight = window.getComputedStyle(desc.value).height.replace('rem', '')
descHeight = descHeight || desc.value.offsetHeight
if (descHeight > 2.325 * rem) {
showExchangeButton.value = true
exchangeButton.value = true
showTotal.value = false
} else {
showExchangeButton.value = false
showTotal.value = true
}
})
}
function scrollTopBody() {
setTimeout(() => {
try {
if (mescrollContent.value)
mescrollContent.value.scrollTop = mescrollContent.value.scrollHeight || 0
} catch (e) {
console.log(e.message)
}
}, 300)
}
function connectionArtificial(event) {
try {
let qaId = event.parentNode ? event.parentNode.dataset.qaid : ''
let sendParmas = { reqAndResType: '1', msgText: '&转人工服务&', msgSource: manualService.value }
scrollTopBody()
store
?.dispatch?.('connectionArtificial', sendParmas)
.then(() => {
event.disabled = true
})
.catch((err) => console.log(err))
store
?.dispatch?.('userBehavior', { qaId, eventType: '5', eventMsg: '知识库进入人工服务' })
.catch((err) => console.log(err))
} catch (error) {
console.log(error)
}
}
function richCallAnswer(item) {
if (!isBtnsubmit.value) return
isBtnsubmit.value = false
let sendParmas = {
reqAndResType: '1',
msgText: item.intent,
intentId: item.intentId,
msgSource: manualService.value,
}
scrollTopBody()
store
?.dispatch?.('sendingRobot', sendParmas)
.then(() => {
isBtnsubmit.value = true
})
.catch((err) => {
isBtnsubmit.value = true
console.log(err)
})
}
function manualAnswer(event, item) {
let sendParmas = { reqAndResType: '1', msgText: '&转人工服务&', msgSource: manualService.value }
const toast = showLoadingToast({
message: '加载中...',
duration: 0,
loadingType: 'spinner',
forbidClick: true,
})
if (!sendLoading.value) {
scrollTopBody()
store
?.dispatch?.('sendingRobot', sendParmas)
.then(() => {
closeToast()
event.disabled = true
})
.catch((err) => {
closeToast()
console.log(err)
})
} else {
closeToast()
}
}
</script>
<style scoped>
.text-template-box {
padding: 0;
}
.question-answer-con {
overflow: hidden;
}
.total-introduce {
max-height: none;
}
.detailed-introduce {
max-height: 2.325rem;
}
.content-question-answer {
display: flex;
flex-wrap: wrap;
gap: 8rem;
margin-top: 10rem;
}
.answer-items {
padding: 6rem 12rem;
background: #f0f6ff;
border: 1rem solid #d6e4ff;
border-radius: 16rem;
font-size: 13rem;
color: #025cea;
cursor: pointer;
}
.answer-items[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
.unfold-box {
text-align: center;
margin-top: 8rem;
}
.unfold-box a {
font-size: 13rem;
color: #025cea;
}
.look-more-icon {
display: inline-block;
width: 12rem;
height: 12rem;
margin-left: 4rem;
transition: transform 0.3s;
}
.look-more-icon.active {
transform: rotate(180deg);
}
</style>

File diff suppressed because it is too large Load Diff

@ -3,6 +3,7 @@ import { ref, onMounted, inject } from 'vue';
import { Post } from '@/utils/https'
const store = inject('appStore')
const emit = defineEmits(['ready'])
const welcomeWord = ref('')
const welcomeSubWord = ref('我是您的服务管家皮卡丘')
onMounted(() => {
@ -11,8 +12,12 @@ onMounted(() => {
userInfoId: store.userId
}).then(res => {
if (res.code === 0 && res.data) {
welcomeWord.value = `${res.data?.greetings || ''}${res.data?.greetings ? ',' : ''}${res.data?.welcomeName || ''}`
welcomeWord.value = `${res.data?.greetings || ''}${res.data?.welcomeName ? ',' : ''}${res.data?.welcomeName || ''}`
}
}).catch((error) => {
console.log(error)
}).finally(() => {
emit('ready')
})
})
</script>
@ -25,7 +30,7 @@ onMounted(() => {
</template>
<style lang="scss" scoped>
.welcome-con {
padding: 33rem 8rem;
padding: 38rem 8rem;
position: relative;
.welcome-title {
font-family: PingFang SC;
@ -44,7 +49,7 @@ onMounted(() => {
}
.sub-title {
width: calc(100% - 100rem);
max-width: calc(100% - 100rem);
font-family: PingFang SC;
font-size: 18rem;
font-weight: 600;
@ -56,6 +61,7 @@ onMounted(() => {
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
width: fit-content;
text-fill-color: transparent;
}

@ -1,37 +1,34 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import Utils from "@/utils/utils";
const router = createRouter({
history: createWebHashHistory(),
routes: [
{ path: '/', redirect: '/chat' },
{ path: '/chat', name: 'chat', component: () => import('../views/chat.vue') },
{
path: '/history',
name: 'history',
component: () => import('../views/history.vue'),
meta: { title: '洋洋客服-历史记录查询' },
},
{
path: '/previewPdf',
name: 'previewPdf',
component: () => import('../components/previewPDF/index.vue'),
meta: { title: '预览PDF' },
},
// { path: '/uploadPage', name: 'uploadPage', component: () => import('../views/uploadPage.vue') },
// { path: '/claimPage', name: 'claimPage', component: () => import('../views/claimPage/index.vue'), },
// { path: '/outView', name: 'outView', component: () => import('../views/outView.vue') },
// { path: '/sign', name: 'sign', component: () => import('../components/input/sign.vue') },
],
})
const defaultTitle = '洋洋客服'
// 全局前置路由守卫
router.beforeEach(async (to, from, next) => {
/* 路由发生变化修改页面title */
var title = to.meta.title ? to.meta.title : defaultTitle;
Utils.setTitle(title);//修改页面title
next();
})
export default router
import { createRouter, createWebHashHistory } from 'vue-router'
import Utils from "@/utils/utils";
const router = createRouter({
history: createWebHashHistory(),
routes: [
{ path: '/', redirect: '/chat' },
{ path: '/chat', name: 'chat', component: () => import('../views/chat.vue'), meta: { keepAlive: true, title: '小蓝管家' } },
{ path: '/word', name: 'word', component: () => import('../views/word.vue'), meta: { keepAlive: true, title: '小蓝管家' } },
// {
// path: '/history',
// name: 'history',
// component: () => import('../views/history.vue'),
// meta: { title: '小蓝管家-历史记录查询' },
// },
// {
// path: '/previewPdf',
// name: 'previewPdf',
// component: () => import('../components/previewPDF/index.vue'),
// meta: { title: '预览PDF' },
// },
],
})
const defaultTitle = '小蓝管家'
// 全局前置路由守卫
router.beforeEach(async (to, from, next) => {
/* 路由发生变化修改页面title */
var title = to.meta.title ? to.meta.title : defaultTitle;
Utils.setTitle(title);//修改页面title
next();
})
export default router

@ -117,6 +117,7 @@ export const useAppStore = defineStore('app', () => {
const isBackstage = ref(false)
const sdkLoaded = ref(false)
const chatArr = ref([])
const businessHandleInfo = ref({})
const isChatLoading = ref(false)
const bpErrorInfo = ref([])
const voiceDataList = ref([])
@ -194,6 +195,7 @@ export const useAppStore = defineStore('app', () => {
userInfo.value = {}
chatList.value = []
chatArr.value = []
businessHandleInfo.value = {}
isChatLoading.value = false
sendMessageList.value = []
receiveMessageList.value = []
@ -752,12 +754,17 @@ export const useAppStore = defineStore('app', () => {
chatList.value.splice(index, 1)
}
function setBusinessHandleInfo(payload = {}) {
businessHandleInfo.value = payload || {}
}
function delAllChatList() {
chatList.value.splice(0, chatList.value.length)
chatArr.value.splice(0, chatArr.value.length)
isChatLoading.value = false
}
function delSendFailListById(delId) {
if (sendFailList.value && sendFailList.value.length > 0) {
sendFailList.value.forEach((item, index) => {
@ -1345,6 +1352,8 @@ export const useAppStore = defineStore('app', () => {
set_hxOpenSuccesed: setHxOpenSuccesed, set_timestamp: setTimestamp,
set_sendParmas: setSendParmas, set_queuedTimeout: setQueuedTimeout,
set_chatList: setChatList, del_chatList: delChatList, delall_chatList: delAllChatList,
set_businessHandleInfo: setBusinessHandleInfo,
setBusinessHandleInfo,
set_chatId: setChatId, set_currentItemId: setCurrentItemId,
set_findActiveItem: setFindActiveItem, set_activeItemExtra: setActiveItemExtra,
set_reSendMessage: setReSendMessage, set_isBackstage: setIsBackstage,
@ -1433,6 +1442,7 @@ export const useAppStore = defineStore('app', () => {
isBackstage,
sdkLoaded,
chatArr,
businessHandleInfo,
isChatLoading,
bpErrorInfo,
voiceDataList,
@ -1511,6 +1521,7 @@ export const useAppStore = defineStore('app', () => {
setChatList,
delChatList,
delAllChatList,
setBusinessHandleInfo,
setChatId,
setCurrentItemId,
setFindActiveItem,

File diff suppressed because it is too large Load Diff

@ -1,465 +1,465 @@
import config from './config'
import { showToast, showDialog, showConfirmDialog } from 'vant'
import { useAppStore } from '@/store/app.js'
import utils from './utils'
// Keep the legacy socket implementation independent from Vuex. The proxy
// exposes the old state/commit/dispatch shape while reading the live Pinia
// store instance for every call.
const store = {
get state() {
return useAppStore().state
},
commit(type, payload) {
return useAppStore().commit(type, payload)
},
dispatch(type, payload) {
return useAppStore().dispatch(type, payload)
},
}
/**
* 提示函数
* 禁止点击蒙层、显示3秒后关闭
*/
const toast = msg => {
showToast({
message: msg,
duration: 5000,
position: 'top',
});
}
const sendMessageFunction = (msgText) => {
return {
token: store.state.token,
fromFlag: 'other',
userId: store.state.userId,
sessionId: store.state.sessionId,
reqAndResType: '',
msgType: '', //
msgText: msgText,
intentId: '',
source: store.state.source,
timestamp: (new Date()).valueOf(),
msgSource: store.state.manualService, //1机器人 2人工
cmd: ''
}
}
//设置title
document.setTitle = function (title) {
title = title || '洋洋客服';
store.commit('set_otherTips', title);
// document.title = title;
var iframe = document.createElement('iframe');
iframe.src = `${import.meta.env.BASE_URL}favicon.ico`;
iframe.style.display = 'none';
iframe.onload = function () {
setTimeout(function () {
iframe.remove();
}, 9)
}
document.body.appendChild(iframe);
}
let wsConnection = {
WSS_URL: config.WSURL,
$ws: null,
lockReturn: false,
//6s发一次心跳
timeout: 60 * 100 * 1,
timeoutObj: null,
timeoutNum: null,
serverTimeoutObj: null,
globalCallback: null,
sendMessageData: null,
reconectNum: 0,
//最大限制 重连次数
reconectTimes: 20,
closeSession: false,//是否关闭当前会话
//初始化webSocket长连接
initWebSocket: function (callback) {
var that = this;
var userId = store.state.userId;
var checkParam = store.state.token;
var sessionId = store.state.sessionId;
try {
if ('WebSocket' in window) {
// this.$ws = new WebSocket(this.WSS_URL + userId + "," + sessionId);
this.$ws = new WebSocket(`${this.WSS_URL}${userId}&${encodeURIComponent(checkParam)}`);
} else if ('MozWebSocket' in window) {
// this.$ws = new MozWebSocket(this.WSS_URL + userId + "," + sessionId);
this.$ws = new MozWebSocket(`${this.WSS_URL}${userId}&${encodeURIComponent(checkParam)}`);
} else {
showDialog({
message: '您的浏览器不支持websocket协议,建议您使用Android系统版本大于4.0,ios系统版本大于7.0。'
}).then(() => {
utils.closeWindow(); //关闭页面
});
}
// this.$ws.onopen = this.wsOpen;
this.$ws.addEventListener("open", event => {
this.wsOpen();
})
// this.$ws = new WebSocket(this.WSS_URL + userId + "," + sessionId);
this.$ws.onclose = this.wsClose;
this.$ws.onmessage = this.onMessage;
this.$ws.onerror = this.wsError;
callback && callback();
// store.commit('set_otherTips', "连接中...");
document.setTitle('连接中...')
} catch (e) {
wsConnection.reconnect()
console.log(e);
}
},
wsResendMessage() {
var sendFailList = store.state.sendFailList;//取出上次发送失败的消息
if (sendFailList.length > 0) {
for (let i = 0; i < sendFailList.length; i++) {
(function (i) {//这个匿名函数生成了闭包的效果,新建了一个作用域,这个作用域接收到每次循环的i值保存了下来,即使循环结束,闭包形成的作用域也不会被销毁
setTimeout(function () {
var dataResend = sendFailList[i];
console.log("发送失败的数据ID==", dataResend.timestamp);
wsConnection.sendMessage(dataResend, wsConnection.globalCallback);
store.commit('set_currentItemId', dataResend.timestamp);
store.commit('set_findActiveItem', false);//发送成功
}, 200);
})(i);
}
store.commit('del_sendFailList');//删除发送成功的
console.log(store.state.sendFailList);
}
},
//打开websocket
wsOpen: function (e) {
var that = this;
//开始websocket心跳
wsConnection.startWsHeartbeat();
// toast('会话建立成功');
// store.commit('set_otherTips', "洋洋客服");
document.setTitle('洋洋客服')
//执行重新发送
// if(wsConnection.isReconnet){
// wsConnection.wsResendMessage();
// wsConnection.isReconnet = false;
// }
console.log("ws success " + new Date().format("yyyy-MM-dd hh:mm:ss"));
},
onMessage: function (event) {
//每次接收到服务端消息后 重置websocket心跳
wsConnection.resetHeartbeat();
let res = event.data;
try {
if (event.data === 'pong') {
console.log("收到心跳了:", event.data);
return;
}
if (event.data.match(/^\d+$/)) {
wsConnection.globalCallback && wsConnection.globalCallback(event.data);
return;
}
res = JSON.parse(res);
console.log("收到消息啦:", res.data || res);
if (res.data.cmd === 'ServiceSessionClosedEvent' && store.state.hxOpenSucceseds.includes(res.data.sessionId)) {
if(store.state.integratedUser){
utils.useSdk(res.data)
}else{
setTimeout(() => {
utils.useSdk(res.data)
}, 100);
}
}
if (res.code != 0) {
// toast(data.msg);
if (res.code === 10001) {
// 删除已经失效或过期的token(不删除也可以,因为登录后覆盖)
sessionStorage.removeItem("token");
wsConnection.$ws.close();
if (store.state.notitle === '1') {
toast('当前会话已结束,请刷新页面或关闭页面重新进入');
return
}
showConfirmDialog({
title: '提示',
message: '当前会话已结束,请刷新页面或关闭页面重新进入',
confirmButtonText: '取消',
cancelButtonText: '确认',
confirmButtonColor: '#000000',
cancelButtonColor: '#000000'
}).then(() => {
// on cancel (现在是右边的取消按钮)
// utils.closeWindow();
// router.replace({
// path: '/404' // 到登录页重新获取token
// })
}).catch(() => {
// on confirm (现在是左边的确认按钮)
//确认重新刷新页面获取token
// window.location.reload();
utils.AppReloadHtml();
});
} else {
if (res.msg) {
toast(res.msg);
}
}
return;
}
//是否转接人工参数
// if (res.data.cmd && res.data.cmd == "ServiceSessionCreatedEvent") {
// store.dispatch('setTransferCustomer', 2);
// }
//客服接起不排队了
if (res.data.cmd && res.data.cmd == "ServiceSessionOpenedEvent") {
store.commit('set_queuedTimeout', true);
store.commit('set_msgStatus', '');//人工客服排队状态值 等待中:"1",其他:""
store.commit('set_msgReply', false);//人工客服排队是否调取自动回复话术
}
if (res.data.cmd && res.data.cmd == "ServiceSessionClosedEvent") {
wsConnection.closeSession = true;//是关闭会话
setTimeout(() => {
store.commit('set_queuedTimeout', true);
store.dispatch('setTransferCustomer', 1);//人工改为机器人
wsConnection.$ws.close();
}, 2500)
}
//服务端发送来的消息存到vuex
store.dispatch('sendingDrtificial', res.data).then((res) => {
}).catch(err => {
console.log(err)
});
//如果会话关闭不用推送消息回执
if (res.data && res.data.chatId) {
let chatParam = JSON.stringify(res.data);
// wsConnection.$ws.send(chatParam, '');
wsConnection.sendMessage(chatParam, '');
console.log("发送消息回执", chatParam)
}
} catch (error) {
console.log(error)
}
},
//发送数据,接收处理数据
sendMessage: function (data, callback) {
//存储事件
wsConnection.globalCallback = callback;
// console.log(data, callback)
switch (wsConnection.$ws.readyState) {
//CONNECTING:值为0,表示正在连接。
case wsConnection.$ws.CONNECTING:
setTimeout(function () {
wsConnection.sendMessage(data, callback);
}, 1000);
break;
//OPEN:值为1,表示连接成功,可以通信了。
case wsConnection.$ws.OPEN:
wsConnection.$ws.send(data);
break;
//CLOSING:值为2,表示连接正在关闭。
case wsConnection.$ws.CLOSING:
setTimeout(function () {
wsConnection.sendMessage(data, callback);
}, 1000);
break;
//CLOSED:值为3,表示连接已经关闭,或者打开连接失败。
case wsConnection.$ws.CLOSED:
// do something
break;
default:
// this never happens
break;
}
//重新监听onmessage,并把数据传给回调函数
// wsConnection.onmessage = ev =>{
// callback && callback(ev)
// }
},
//重启websocket
reconnect: function () {
let _this = this;
if (_this.lockReturn) {
return;
}
_this.isReconnet = true;
_this.lockReturn = true;
//没连接上会一直重连,设置延迟避免请求过多
if (_this.reconectNum < this.reconectTimes) {
_this.timeoutNum && clearTimeout(_this.timeoutNum);
_this.timeoutNum = setTimeout(function () {
_this.initWebSocket();
// toast('正在重连,请稍后...');
// store.commit('set_otherTips', "正在重连...");
document.setTitle('正在重连...')
console.info(`正在重连第${_this.reconectNum + 1}次`)
_this.reconectNum++
_this.lockReturn = false;
}, 4000);
} else {
// toast('重连次数超出设定值,不再重连,请检查配置项');
showDialog({
message: '重连次数超出设定值,不再重连。请检查您的网络设置,点击“确定”刷新页面,重新建立会话。'
}).then(() => {
// window.location.reload();
utils.AppReloadHtml();
});
}
},
//心跳检测,每5s心跳一次
startWsHeartbeat: function () {
let _this = this;
this.timeoutObj && clearTimeout(this.timeoutObj);
this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
this.timeoutObj = setTimeout(() => {
//这里发送一个心跳,后端收到后,返回一个心跳消息,
//onmessage拿到返回的心跳就说明连接正常
if (wsConnection.$ws && wsConnection.$ws.readyState != wsConnection.$ws.CLOSED) {
//关闭当前会话后不发送ping
if (!wsConnection.closeSession) {
_this.$ws.send("ping");
console.log("ping!", new Date().format("yyyy-MM-dd hh:mm:ss"));
}
}
//如果超过一定时间还没重置,说明后端主动断开了
_this.serverTimeoutObj = setTimeout(() => {
//如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
_this.$ws.close();
}, _this.timeout)
}, this.timeout);
},
//重置websocket心跳
resetHeartbeat: function () {
let _this = this;
this.reconectNum = 0;
clearTimeout(_this.timeoutObj);
clearTimeout(_this.serverTimeoutObj);
_this.startWsHeartbeat();
},
wsError: function (err) {
console.log(err, 'ws error');
// store.commit('set_otherTips', "洋洋客服(未连接)");
document.setTitle('洋洋客服(未连接)')
wsConnection.reconnect()
},
wsClose: function (e) {
// console.log(e, 'ws close')
console.log("连接关闭! " + new Date().format("yyyy-MM-dd hh:mm:ss"));
console.log('websocket 断开: ' + e.code + ' 关闭的原因:' + e.reason + ' 是否正常关闭' + e.wasClean);
if (e.code !== 1000) {
// toast('当前会话已结束');
this.lockReturn = false
clearTimeout(this.timeoutNum)
wsConnection.reconnect();
// store.commit('set_otherTips', "洋洋客服(未连接)");
document.setTitle('洋洋客服(未连接)')
//当前转成机器人
// store.dispatch('setTransferCustomer', 1);
} else {
this.$ws = null;
clearInterval(this.timeoutObj)
clearTimeout(this.serverTimeoutObj)
// toast('会话已关闭');
// store.commit('set_otherTips', "洋洋客服");
document.setTitle('洋洋客服')
wsConnection.$ws.close();
}
},
};
//时间格式化
Date.prototype.format = function (fmt) {
var o = {
"M+": this.getMonth() + 1, //月份
"d+": this.getDate(), //日
"h+": this.getHours(), //小时
"m+": this.getMinutes(), //分
"s+": this.getSeconds(), //秒
"q+": Math.floor((this.getMonth() + 3) / 3), //季度
"S": this.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
}
for (var k in o) {
if (new RegExp("(" + k + ")").test(fmt)) {
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
}
}
return fmt;
}
// 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function (event) {
event = event || window.event;
try {
// toast('onbeforeunload关闭当前会话窗口啦');
var oldSessionId = localStorage.getItem('sessionId');
var liccsAds = localStorage.getItem('liccsAds'); // 记录用户弹窗提示
// sessionStorage.removeItem("manualService");
if (wsConnection.$ws) {
wsConnection.$ws.close();
}
//清除本地缓存
localStorage.clear();
localStorage.sessionId = oldSessionId || '';
localStorage.liccsAds = liccsAds || '';
//清除session
sessionStorage.clear();
} catch (error) {
console.log(error.message)
}
}
//安卓处理解决方案
window.onunload = function (event) {
event = event || window.event;
try {
// toast('onunload关闭当前会话窗口啦');
var oldSessionId = localStorage.getItem('sessionId');
var liccsAds = localStorage.getItem('liccsAds'); // 记录用户弹窗提示
// sessionStorage.removeItem("manualService");
if (wsConnection.$ws) {
wsConnection.$ws.close();
}
//清除本地缓存
localStorage.clear();
localStorage.sessionId = oldSessionId || '';
localStorage.liccsAds = liccsAds || '';
//清除session
sessionStorage.clear();
} catch (error) {
console.log(error.message)
}
}
//抛出websocket对象
export default wsConnection
import config from './config'
import { showToast, showDialog, showConfirmDialog } from 'vant'
import { useAppStore } from '@/store/app.js'
import utils from './utils'
// Keep the legacy socket implementation independent from Vuex. The proxy
// exposes the old state/commit/dispatch shape while reading the live Pinia
// store instance for every call.
const store = {
get state() {
return useAppStore().state
},
commit(type, payload) {
return useAppStore().commit(type, payload)
},
dispatch(type, payload) {
return useAppStore().dispatch(type, payload)
},
}
/**
* 提示函数
* 禁止点击蒙层、显示3秒后关闭
*/
const toast = msg => {
showToast({
message: msg,
duration: 5000,
position: 'top',
});
}
const sendMessageFunction = (msgText) => {
return {
token: store.state.token,
fromFlag: 'other',
userId: store.state.userId,
sessionId: store.state.sessionId,
reqAndResType: '',
msgType: '', //
msgText: msgText,
intentId: '',
source: store.state.source,
timestamp: (new Date()).valueOf(),
msgSource: store.state.manualService, //1机器人 2人工
cmd: ''
}
}
//设置title
document.setTitle = function (title) {
title = title || '洋洋客服';
store.commit('set_otherTips', title);
// document.title = title;
var iframe = document.createElement('iframe');
iframe.src = `${import.meta.env.BASE_URL}favicon.ico`;
iframe.style.display = 'none';
iframe.onload = function () {
setTimeout(function () {
iframe.remove();
}, 9)
}
document.body.appendChild(iframe);
}
let wsConnection = {
WSS_URL: config.WSURL,
$ws: null,
lockReturn: false,
//6s发一次心跳
timeout: 60 * 100 * 1,
timeoutObj: null,
timeoutNum: null,
serverTimeoutObj: null,
globalCallback: null,
sendMessageData: null,
reconectNum: 0,
//最大限制 重连次数
reconectTimes: 20,
closeSession: false,//是否关闭当前会话
//初始化webSocket长连接
initWebSocket: function (callback) {
var that = this;
var userId = store.state.userId;
var checkParam = store.state.token;
var sessionId = store.state.sessionId;
try {
if ('WebSocket' in window) {
// this.$ws = new WebSocket(this.WSS_URL + userId + "," + sessionId);
this.$ws = new WebSocket(`${this.WSS_URL}${userId}&${encodeURIComponent(checkParam)}`);
} else if ('MozWebSocket' in window) {
// this.$ws = new MozWebSocket(this.WSS_URL + userId + "," + sessionId);
this.$ws = new MozWebSocket(`${this.WSS_URL}${userId}&${encodeURIComponent(checkParam)}`);
} else {
showDialog({
message: '您的浏览器不支持websocket协议,建议您使用Android系统版本大于4.0,ios系统版本大于7.0。'
}).then(() => {
utils.closeWindow(); //关闭页面
});
}
// this.$ws.onopen = this.wsOpen;
this.$ws.addEventListener("open", event => {
this.wsOpen();
})
// this.$ws = new WebSocket(this.WSS_URL + userId + "," + sessionId);
this.$ws.onclose = this.wsClose;
this.$ws.onmessage = this.onMessage;
this.$ws.onerror = this.wsError;
callback && callback();
// store.commit('set_otherTips', "连接中...");
document.setTitle('连接中...')
} catch (e) {
wsConnection.reconnect()
console.log(e);
}
},
wsResendMessage() {
var sendFailList = store.state.sendFailList;//取出上次发送失败的消息
if (sendFailList.length > 0) {
for (let i = 0; i < sendFailList.length; i++) {
(function (i) {//这个匿名函数生成了闭包的效果,新建了一个作用域,这个作用域接收到每次循环的i值保存了下来,即使循环结束,闭包形成的作用域也不会被销毁
setTimeout(function () {
var dataResend = sendFailList[i];
console.log("发送失败的数据ID==", dataResend.timestamp);
wsConnection.sendMessage(dataResend, wsConnection.globalCallback);
store.commit('set_currentItemId', dataResend.timestamp);
store.commit('set_findActiveItem', false);//发送成功
}, 200);
})(i);
}
store.commit('del_sendFailList');//删除发送成功的
console.log(store.state.sendFailList);
}
},
//打开websocket
wsOpen: function (e) {
var that = this;
//开始websocket心跳
wsConnection.startWsHeartbeat();
// toast('会话建立成功');
// store.commit('set_otherTips', "洋洋客服");
document.setTitle('洋洋客服')
//执行重新发送
// if(wsConnection.isReconnet){
// wsConnection.wsResendMessage();
// wsConnection.isReconnet = false;
// }
console.log("ws success " + new Date().format("yyyy-MM-dd hh:mm:ss"));
},
onMessage: function (event) {
//每次接收到服务端消息后 重置websocket心跳
wsConnection.resetHeartbeat();
let res = event.data;
try {
if (event.data === 'pong') {
console.log("收到心跳了:", event.data);
return;
}
if (event.data.match(/^\d+$/)) {
wsConnection.globalCallback && wsConnection.globalCallback(event.data);
return;
}
res = JSON.parse(res);
console.log("收到消息啦:", res.data || res);
if (res.data.cmd === 'ServiceSessionClosedEvent' && store.state.hxOpenSucceseds.includes(res.data.sessionId)) {
if(store.state.integratedUser){
utils.useSdk(res.data)
}else{
setTimeout(() => {
utils.useSdk(res.data)
}, 100);
}
}
if (res.code != 0) {
// toast(data.msg);
if (res.code === 10001) {
// 删除已经失效或过期的token(不删除也可以,因为登录后覆盖)
sessionStorage.removeItem("token");
wsConnection.$ws.close();
if (store.state.notitle === '1') {
toast('当前会话已结束,请刷新页面或关闭页面重新进入');
return
}
showConfirmDialog({
title: '提示',
message: '当前会话已结束,请刷新页面或关闭页面重新进入',
confirmButtonText: '取消',
cancelButtonText: '确认',
confirmButtonColor: '#000000',
cancelButtonColor: '#000000'
}).then(() => {
// on cancel (现在是右边的取消按钮)
// utils.closeWindow();
// router.replace({
// path: '/404' // 到登录页重新获取token
// })
}).catch(() => {
// on confirm (现在是左边的确认按钮)
//确认重新刷新页面获取token
// window.location.reload();
utils.AppReloadHtml();
});
} else {
if (res.msg) {
toast(res.msg);
}
}
return;
}
//是否转接人工参数
// if (res.data.cmd && res.data.cmd == "ServiceSessionCreatedEvent") {
// store.dispatch('setTransferCustomer', 2);
// }
//客服接起不排队了
if (res.data.cmd && res.data.cmd == "ServiceSessionOpenedEvent") {
store.commit('set_queuedTimeout', true);
store.commit('set_msgStatus', '');//人工客服排队状态值 等待中:"1",其他:""
store.commit('set_msgReply', false);//人工客服排队是否调取自动回复话术
}
if (res.data.cmd && res.data.cmd == "ServiceSessionClosedEvent") {
wsConnection.closeSession = true;//是关闭会话
setTimeout(() => {
store.commit('set_queuedTimeout', true);
store.dispatch('setTransferCustomer', 1);//人工改为机器人
wsConnection.$ws.close();
}, 2500)
}
//服务端发送来的消息存到vuex
store.dispatch('sendingDrtificial', res.data).then((res) => {
}).catch(err => {
console.log(err)
});
//如果会话关闭不用推送消息回执
if (res.data && res.data.chatId) {
let chatParam = JSON.stringify(res.data);
// wsConnection.$ws.send(chatParam, '');
wsConnection.sendMessage(chatParam, '');
console.log("发送消息回执", chatParam)
}
} catch (error) {
console.log(error)
}
},
//发送数据,接收处理数据
sendMessage: function (data, callback) {
//存储事件
wsConnection.globalCallback = callback;
// console.log(data, callback)
switch (wsConnection.$ws.readyState) {
//CONNECTING:值为0,表示正在连接。
case wsConnection.$ws.CONNECTING:
setTimeout(function () {
wsConnection.sendMessage(data, callback);
}, 1000);
break;
//OPEN:值为1,表示连接成功,可以通信了。
case wsConnection.$ws.OPEN:
wsConnection.$ws.send(data);
break;
//CLOSING:值为2,表示连接正在关闭。
case wsConnection.$ws.CLOSING:
setTimeout(function () {
wsConnection.sendMessage(data, callback);
}, 1000);
break;
//CLOSED:值为3,表示连接已经关闭,或者打开连接失败。
case wsConnection.$ws.CLOSED:
// do something
break;
default:
// this never happens
break;
}
//重新监听onmessage,并把数据传给回调函数
// wsConnection.onmessage = ev =>{
// callback && callback(ev)
// }
},
//重启websocket
reconnect: function () {
let _this = this;
if (_this.lockReturn) {
return;
}
_this.isReconnet = true;
_this.lockReturn = true;
//没连接上会一直重连,设置延迟避免请求过多
if (_this.reconectNum < this.reconectTimes) {
_this.timeoutNum && clearTimeout(_this.timeoutNum);
_this.timeoutNum = setTimeout(function () {
_this.initWebSocket();
// toast('正在重连,请稍后...');
// store.commit('set_otherTips', "正在重连...");
document.setTitle('正在重连...')
console.info(`正在重连第${_this.reconectNum + 1}次`)
_this.reconectNum++
_this.lockReturn = false;
}, 4000);
} else {
// toast('重连次数超出设定值,不再重连,请检查配置项');
showDialog({
message: '重连次数超出设定值,不再重连。请检查您的网络设置,点击“确定”刷新页面,重新建立会话。'
}).then(() => {
// window.location.reload();
utils.AppReloadHtml();
});
}
},
//心跳检测,每5s心跳一次
startWsHeartbeat: function () {
let _this = this;
this.timeoutObj && clearTimeout(this.timeoutObj);
this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
this.timeoutObj = setTimeout(() => {
//这里发送一个心跳,后端收到后,返回一个心跳消息,
//onmessage拿到返回的心跳就说明连接正常
if (wsConnection.$ws && wsConnection.$ws.readyState != wsConnection.$ws.CLOSED) {
//关闭当前会话后不发送ping
if (!wsConnection.closeSession) {
_this.$ws.send("ping");
console.log("ping!", new Date().format("yyyy-MM-dd hh:mm:ss"));
}
}
//如果超过一定时间还没重置,说明后端主动断开了
_this.serverTimeoutObj = setTimeout(() => {
//如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
_this.$ws.close();
}, _this.timeout)
}, this.timeout);
},
//重置websocket心跳
resetHeartbeat: function () {
let _this = this;
this.reconectNum = 0;
clearTimeout(_this.timeoutObj);
clearTimeout(_this.serverTimeoutObj);
_this.startWsHeartbeat();
},
wsError: function (err) {
console.log(err, 'ws error');
// store.commit('set_otherTips', "洋洋客服(未连接)");
document.setTitle('洋洋客服(未连接)')
wsConnection.reconnect()
},
wsClose: function (e) {
// console.log(e, 'ws close')
console.log("连接关闭! " + new Date().format("yyyy-MM-dd hh:mm:ss"));
console.log('websocket 断开: ' + e.code + ' 关闭的原因:' + e.reason + ' 是否正常关闭' + e.wasClean);
if (e.code !== 1000) {
// toast('当前会话已结束');
this.lockReturn = false
clearTimeout(this.timeoutNum)
wsConnection.reconnect();
// store.commit('set_otherTips', "洋洋客服(未连接)");
document.setTitle('洋洋客服(未连接)')
//当前转成机器人
// store.dispatch('setTransferCustomer', 1);
} else {
this.$ws = null;
clearInterval(this.timeoutObj)
clearTimeout(this.serverTimeoutObj)
// toast('会话已关闭');
// store.commit('set_otherTips', "洋洋客服");
document.setTitle('洋洋客服')
wsConnection.$ws.close();
}
},
};
//时间格式化
Date.prototype.format = function (fmt) {
var o = {
"M+": this.getMonth() + 1, //月份
"d+": this.getDate(), //日
"h+": this.getHours(), //小时
"m+": this.getMinutes(), //分
"s+": this.getSeconds(), //秒
"q+": Math.floor((this.getMonth() + 3) / 3), //季度
"S": this.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
}
for (var k in o) {
if (new RegExp("(" + k + ")").test(fmt)) {
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
}
}
return fmt;
}
// 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function (event) {
event = event || window.event;
try {
// toast('onbeforeunload关闭当前会话窗口啦');
var oldSessionId = localStorage.getItem('sessionId');
var liccsAds = localStorage.getItem('liccsAds'); // 记录用户弹窗提示
// sessionStorage.removeItem("manualService");
if (wsConnection.$ws) {
wsConnection.$ws.close();
}
//清除本地缓存
localStorage.clear();
localStorage.sessionId = oldSessionId || '';
localStorage.liccsAds = liccsAds || '';
//清除session
sessionStorage.clear();
} catch (error) {
console.log(error.message)
}
}
//安卓处理解决方案
window.onunload = function (event) {
event = event || window.event;
try {
// toast('onunload关闭当前会话窗口啦');
var oldSessionId = localStorage.getItem('sessionId');
var liccsAds = localStorage.getItem('liccsAds'); // 记录用户弹窗提示
// sessionStorage.removeItem("manualService");
if (wsConnection.$ws) {
wsConnection.$ws.close();
}
//清除本地缓存
localStorage.clear();
localStorage.sessionId = oldSessionId || '';
localStorage.liccsAds = liccsAds || '';
//清除session
sessionStorage.clear();
} catch (error) {
console.log(error.message)
}
}
//抛出websocket对象
export default wsConnection

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,10 @@
<template>
<div>
vue3
</div>
</template>
<script setup>
defineOptions({
name: 'word',
})
</script>

@ -19,14 +19,6 @@ export default defineConfig(({ command }) => {
changeOrigin: true,
ws: true,
},
'/api': {
// target: 'https://aioblu-sit.cpic.com.cn',
// target: 'https://aioblu-dev.cpic.com.cn/yyfw',
target: 'https://ssp-sit.cpic.com.cn',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
plugins: [

Loading…
Cancel
Save