You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
customAgent/src/views/chat.vue

655 lines
23 KiB

<template>
<div>
<!-- <chat-main
v-if="isToken"
:tokenExist="isToken"
:isWX="isWX"
:lostChatInfoNum="lostChatInfoNum"
/> -->
<chat-new
v-if="isToken"
:tokenExist="isToken"
:isWX="isWX"
:lostChatInfoNum="lostChatInfoNum"
/>
<!-- <button @click="router.push('/word')">
跳转word
</button> -->
<!-- <human-appraise-popup /> -->
</div>
</template>
<script setup>
import { ref, onMounted, onActivated, inject, nextTick } from 'vue'
import {
showLoadingToast,
closeToast,
showDialog,
showConfirmDialog,
showToast,
} from 'vant'
import { Post, Get } from '@/utils/https'
import chatMain from '@/components/chat-main.vue'
import chatNew from '@/components/chat-new.vue'
// import HumanAppraisePopup from "@/components/human-appraise-popup.vue";
import Utils from '@/utils/utils'
import sensors from '@/utils/sensors'
import { Decryption, DecryptionECB, Encrypt, isDev, isProd } from '@/utils'
import { useRouter, onBeforeRouteLeave } from 'vue-router'
const router = useRouter()
defineOptions({
name: 'chat',
})
// 状态管理
const store = inject('appStore', null)
// 响应式数据
const isToken = ref(false)
const hasInitialized = ref(false)
const isWX = ref(false)
const lostChatInfoNum = ref(0)
const urlParams = ref({})
// 生命周期
onActivated(() => {
// keep-alive 返回时不重新初始化
console.log('[chat] activated, hasInitialized=', hasInitialized.value, 'isToken=', isToken.value)
// 路由组件激活后再补一次滚动恢复,确保子组件 DOM 已显示
try {
const raw = sessionStorage.getItem('yykf_chat_main_scroll_top')
const top = raw == null ? 0 : Number(raw)
if (!Number.isFinite(top) || top <= 0) return
const apply = () => {
const el = store?.mescrollContent
if (!el) return
el.scrollTop = top
}
nextTick(() => {
apply()
;[50, 120, 240, 480].forEach((d) => setTimeout(apply, d))
})
} catch (e) {}
})
onBeforeRouteLeave(() => {
try {
const el = store?.mescrollContent
if (!el) return
const top = el.scrollTop || 0
// 避免隐藏后读到 0 覆盖有效位置
if (top > 0) {
sessionStorage.setItem('yykf_chat_main_scroll_top', String(top))
}
} catch (e) {}
})
onMounted(async () => {
if (hasInitialized.value) return
hasInitialized.value = true
var liccsAds = localStorage.getItem('liccsAds') // 记录用户弹窗提示
localStorage.clear()
localStorage.liccsAds = liccsAds || ''
//清除session 清除上次的记录(保留聊天滚动位置)
const chatScrollTop = sessionStorage.getItem('yykf_chat_main_scroll_top')
sessionStorage.clear()
if (chatScrollTop != null) sessionStorage.setItem('yykf_chat_main_scroll_top', chatScrollTop)
urlParams.value = Utils.getUrlParams()
console.log('txfModules参数', urlParams.value.txfModules)
console.log('this.urlParams--', urlParams.value)
let userInfo = {
phone: urlParams.value.phone || '',
name: urlParams.value.name || '',
sex: urlParams.value.sex || '',
openid: urlParams.value.openid || '',
idtype: urlParams.value.idtype || '',
pid: urlParams.value.pid || '',
auStatus: urlParams.value.auStatus || '', //实名认证状态 枚举:1、0、其他(1为已认证状态,其他为非认证认证状态)
bdStatus: urlParams.value.bStatus || urlParams.value.bdStatus || '', //老客户绑定状态 1为已绑定状态,其他为非绑定状态
bthdate: urlParams.value.bthdate || '', //出生日期
workNum: urlParams.value.workNum || '', //营销员工号
unit: urlParams.value.unit || '', //单位
orgname: urlParams.value.orgname || '', //机构名称
txfModules: urlParams.value.txfModules || '', //太享福渠道活动与功能显示
sceneId: urlParams.value.sceneId || '', //场景标识(微信跟随)
entrance: urlParams.value.entrance || '', // 入口标识
planCode: urlParams.value.planCode || urlParams.value.modelCode || '', //星云渠道产品代码
// 断点服务
bpErrorCode: urlParams.value.bErrorCode || urlParams.value.brErrorCode || '',
bpErrorContent: urlParams.value.bErrorContent || urlParams.value.errorMsg || '',
bpErrorModule: urlParams.value.bErrorModule || '', // 电子回访断点服务
bpErrorPicture: urlParams.value.bErrorPicture || '', // 微信给付断点服务
entryIdnum: '', // 查门店证件号加密传参
entryOpenid: '', // 查门店openid加密传参
userType: urlParams.value.usertype || '', // 95980000渠道(b2bi),说明一下是游客
}
isWX.value = Utils.isWeiXin()
store.setUserInfo(userInfo);//用户信息
store.setSource(urlParams.value.ht || ""); //来源
store.setNotitle(urlParams.value.notitle || ""); //是否展示头部
store.setConfigId(urlParams.value.configId || ""); //环信参数
store.setEntrance(urlParams.value.entrance || ""); //入口标识
store.setAppentry(urlParams.value.appentry || ""); // 是否为老版本app
store.setHtmlExtParams(urlParams.value.htmlExtParams || ""); //扩展参数
store.setComplaint(urlParams.value.complaint || ""); //扩展参数
store.setNavigationCorpus(urlParams.value.navigationCorpus || ""); //扩展参数
wx.miniProgram.getEnv((res) => {
console.log(res, 'res');
})
// 渠道校验
// let checkToast = null
// try {
// checkToast = showLoadingToast({
// message: '加载中...',
// duration: 0,
// loadingType: 'spinner',
// forbidClick: true,
// })
// const checkRes = await Post('config/downSource', {
// source: urlParams.value.ht || '96060000',
// })
// checkToast.close()
// if (checkRes.code === 0 && checkRes.data !== null) {
// showDialog({
// title: '提示',
// message: checkRes.data.toast,
// confirmButtonText: '确定',
// }).then((res) => {
// Utils.closeWindow()
// })
// return
// }
// } catch (error) {
// console.log('渠道校验异常:', error)
// if (checkToast) {
// checkToast.close();
// }
// }
urlParams.value = Utils.getUrlParams()
initToken()
// 渠道校验
try {
const checkRes = await Post("config/downSource", {
source: urlParams.value.ht || "96060000"
});
if (checkRes.code === 0 && checkRes.data !== null) {
showDialog({
title: "提示",
message: checkRes.data?.toast,
confirmButtonText: "确定"
})
}
} catch (error) {
console.log("渠道校验异常:", error);
}
})
// 方法
const initToken = async () => {
try {
const res = await getToken()
const data = res?.data
if (!data) {
throw new Error('获取用户信息失败:接口未返回 data')
}
// Pinia setup store 会自动解包 ref,组件内直接通过 action 更新即可。
store.setUserId(data.userId || '')
store.setSource(data.source || urlParams.value.ht || '')
store.setPlanCode(data.planCode || '')
store.setSessionId(data.sessionId || '')
store.setToken(data.token || '')
store.setManualService(data.msgSource ?? 1)
store.setMsgStatus(data.msgStatus || '')
store.setIsLostMessage(data.isLostChatInfo || '')
const elderChannels = ['95460000', '95290000', '95830000']
const isElder = Boolean(data.eldCustomer)
if (elderChannels.includes(data.source)) {
store.setElder(isElder)
if (!isElder && data.source === '95460000') {
window.location.href = Utils.changeURLArg(window.location.href, 'ht', '95270000')
}
}
const nextUserInfo = {
...(store.userInfo || {}),
versionNo1: data.versionNo1 || '',
versionHFHZ: data.versionHFHZ || '',
encryptPid: data.encryptPid || '',
pid: data.pid || '',
sourceUserId: data.sourceUserId || '',
encryptPhone: data.encryptPhone || '',
eldCustomer: isElder,
vip: data.vip,
planCode: data.planCode || '',
entryIdnum: data.entryIdnum || '',
entryOpenid: data.entryOpenid || '',
sourceName: data.sourceName || '',
blacklistFlag: data.blacklistFlag || '',
}
store.setUserInfo(nextUserInfo)
if (nextUserInfo.blacklistFlag === '1' && nextUserInfo.bpErrorContent) {
const query = new URLSearchParams({
ht: urlParams.value.ht || '',
openid: urlParams.value.openid || '',
})
history.replaceState({}, '', `${window.location.origin}${window.location.pathname}?${query}`)
}
// 尚未提供 setter 的少量 Pinia state 可以直接赋值,Pinia 会同步更新其内部 ref。
store.flagType = data.flagType || ''
store.setGaokeUser(Boolean(data.gaokeUser))
store.setHotUser(Boolean(data.gaowenUser))
store.mydFlag = Boolean(data.manyiduWhite)
store.setIntegratedUser(Boolean(data.white))
store.setReviewRestricted(Boolean(data.reviewRestricted))
store.setBranchCompany(data.branchCompany || '')
store.setBpErrorInfo(data.bpErrorInfo || [])
isToken.value = true
lostChatInfoNum.value = data.lostChatInfoNum || 0
if (store.hotUser) {
getHotUserData()
}
//保存分支机构
const branchCompany = res.data.branchCompany || '';
store.setBranchCompany(branchCompany);
// 微信鉴权异步执行,不阻塞 token 初始化主流程。
if (((Utils.isWeiXin() && !Utils.isMiniProgram()) || isDev()) && nextUserInfo.versionNo1) {
initWxAuth(nextUserInfo.versionNo1).catch((error) => {
console.warn('微信鉴权异步执行失败:', error)
})
}
// 获取 ASR 配置(elderlyData 必须是可解密的字符串)
try {
const elderlyData = data.elderlyData
if (isElder && typeof elderlyData === 'string' && elderlyData && nextUserInfo.versionNo1) {
const year = new Date().getFullYear()
const versionNo = DecryptionECB(
`${year}${year}${year}${year}`,
nextUserInfo.versionNo1,
)
const key = versionNo.substring(0, 16)
const iv = versionNo.substring(16, 32)
if (key.length === 16 && iv.length === 16) {
const decrypted = Decryption(key, iv, elderlyData)
const deAsrConfig = decrypted.length >= 2 ? decrypted.slice(1, -1) : decrypted
console.log('deAsrConfig', deAsrConfig)
if (deAsrConfig) {
store.setAsrConfig(JSON.parse(deAsrConfig))
}
}
}
} catch (error) {
console.warn('获取 ASR 配置失败:', error)
}
const deviceFingerprintSwitch = Boolean(data.deviceFingerprintSwitch)
if (deviceFingerprintSwitch) {
initBxSDK()
}
loadSdk()
void Post('config/getAllProvinces', {})
.then((response) => store.setPcaData(response?.data || []))
.catch((error) => console.warn('获取省市区数据失败:', error))
void Post('config/getQueueNumberTimeSource', {})
.then((response) => {
const sources = response?.data || []
store.setChannelReform(Array.isArray(sources) && sources.includes(data.source))
})
.catch((error) => console.warn('获取排队等待时间试点渠道失败:', error))
if (data.bpErrorCode) {
void callStoreAction('brokenPointCall', {
reqAndResType: '1',
msgText: data.bpErrorContent || '',
msgSource: data.msgSource,
bpErrorCode: data.bpErrorCode,
bpErrorContent: data.bpErrorContent,
})
}
if (data.msgSource == 2) {
if (store.userId) {
void callStoreAction('initWebSocket')
} else {
showConfirmDialog({
title: '初始化提示',
message: '初始化会话失败,请检查网络或页面必要参数;点击“确定”重新建立会话。',
})
.then(() => Utils.AppReloadHtml())
.catch(() => Utils.closeWindow())
}
void Post('HumanChat/humanChatIsQA', { sessionId: data.sessionId })
.then((response) => store.setIsHumanChat(response?.data || false))
.catch((error) => console.warn('获取人工会话状态失败:', error))
}
sensors.quick('isReady', () => {
sensors.login(String(store.userId || ''))
sensors.track('yykfPageInit', {
...nextUserInfo,
source: data.source || '',
})
})
if (deviceFingerprintSwitch && typeof globalThis.getBrowserID === 'function') {
try {
globalThis.getBrowserID(
'ozpw1c5ce6x6mx60pyz2o496frzgrnt6zlmj',
'h5_lpxlwwuyro',
'',
(matchId) => {
sensors.track('yykfDeviceFingerprint', {
matchId: matchId || 'NULL',
userId: data.userId || 'NULL',
source: data.sourceName || 'NULL',
encryptPid: data.pid || 'NULL',
encryptPhone: data.encryptPhone || 'NULL',
})
},
)
} catch (error) {
console.warn('获取设备指纹失败:', error)
}
}
// 关爱版自动转人工。对应 action 迁入 Pinia 后会直接执行;未迁移时不会阻断初始化。
if (isElder && elderChannels.includes(data.source) && String(store.manualService) !== '2') {
setTimeout(() => {
void callStoreAction('transferCustomer', {
reqAndResType: '1',
msgType: '01',
msgText: '转接寿险人工客服',
msgSource: store.manualService,
cmd: 'ZJSXRGFW',
})
}, 50)
}
const currentSource = data.source || urlParams.value.ht || ''
if (urlParams.value.bqQuestion && currentSource === '96060000') {
store.setIsBqCalcClaim(true)
const msgSource = data.msgSource || store.manualService || '1'
const isManual = String(msgSource) === '2' || String(store.manualService) === '2'
const sendParams = {
reqAndResType: '1',
msgText: urlParams.value.bqQuestion,
msgSource: String(msgSource),
questionType: '11',
}
const sendQuestion = () =>
callStoreAction(isManual ? 'sendmessageContent' : 'sendingRobot', sendParams)
if (isManual) {
setTimeout(sendQuestion, 500)
} else {
void sendQuestion()
}
}
} catch (error) {
isToken.value = false
console.error('初始化 token 失败:', error)
}
}
const callStoreAction = (actionName, payload) => {
const action = store[actionName]
if (typeof action !== 'function') {
console.warn(`Pinia action ${actionName} 尚未迁移,已跳过本次调用`)
return Promise.resolve(null)
}
return Promise.resolve(action(payload)).catch((error) => {
console.warn(`Pinia action ${actionName} 执行失败:`, error)
return null
})
}
const getToken = async () => {
const params = {
name: urlParams.value.name || '',
source: urlParams.value.ht || '',
phone: urlParams.value.phone || '',
userId: urlParams.value.openid || '',
sex: urlParams.value.sex || '',
pid: urlParams.value.pid || '',
idType: urlParams.value.idtype || urlParams.value.idType || '',
sessionId: store.sessionId, //取出本地存入的上次的id
auStatus: urlParams.value.auStatus || '',
bdStatus: urlParams.value.bStatus || urlParams.value.bdStatus || '',
bthDate: urlParams.value.bthdate || '',
workNum: urlParams.value.workNum || '',
htmlExtParams: urlParams.value.htmlExtParams || '',
unit: urlParams.value.unit || '', //单位
orgname: urlParams.value.orgname || '', //机构名称
sceneId: urlParams.value.sceneId || '', //场景标识(微信跟随)
planCode: urlParams.value.planCode || urlParams.value.modelCode || '', //星云渠道产品代码
// 断点服务
bpErrorCode: urlParams.value.bErrorCode || urlParams.value.brErrorCode || '',
bpErrorContent:
urlParams.value.bErrorContent ||
`${urlParams.value.policyNo ? urlParams.value.policyNo + '#' : ''}${urlParams.value.errorMsg ? urlParams.value.errorMsg : ''
}` ||
'',
bpErrorModule: urlParams.value.bErrorModule || '', // 电子回访断点服务
bpErrorPicture: urlParams.value.bErrorPicture || '', // 微信给付断点服务
userType: urlParams.value.usertype || '', // 95980000渠道(b2bi),说明一下是游客
// 非游客标识
sort: '1',
}
if (urlParams.value.ticket) {
params.userId = decodeURIComponent(urlParams.value.ticket).replace(/ /g, '+')
}
if (!!urlParams.value.param) {
//95985000 E家福渠道传递密文
params.param = urlParams.value.param
}
// APP处理appTicket参数
if (urlParams.value.appTicket) {
params.userId = decodeURIComponent(urlParams.value.appTicket).replace(/ /g, '+')
}
showLoadingToast({ message: '加载中...', duration: 0, loadingType: 'spinner', forbidClick: true })
try {
const res = await Post('user/info', params)
closeToast()
return res
} catch (error) {
console.log('error', error)
closeToast()
throw error
}
}
// 北极星SDK鉴权
const initBxSDK = () => {
// store.setConfig({
// url: isProd() ? "life-open.cpic.com.cn/fop/ui/poc/shumeng/h5" : "life-open-sit.cpic.com.cn/fop/ui/poc/shumeng/h5",
// deb: "false",
// })
}
// 异步微信鉴权方法
const initWxAuth = async (versionNo1) => {
try {
// 异步获取微信配置参数
const res = await getWxAuthConfig(versionNo1);
if (!res.success) {
console.warn('微信配置获取失败,跳过鉴权');
showToast('微信鉴权失败')
return;
}
const wxConfig = res.result
// document.addEventListener('WeixinOpenTagsError', function (e) {
// // console.error(e.detail.errMsg); // 无法使用开放标签的错误原因,需回退兼容。仅无法使用开放标签,JS-SDK其他功能不受影响
// Toast('开放标签错误=' + e.detail.errMsg)
// });
// 检查微信JS-SDK是否可用
if (typeof wx === 'undefined' || !wx || typeof wx.config !== 'function') {
console.error('微信JS-SDK未正确加载');
showToast('微信JS-SDK加载失败');
return;
}
// 配置微信JS-SDK
wx.config({
debug: false, // 生产环境关闭调试模式
appId: wxConfig.appId, // 必填,服务号的唯一标识
timestamp: wxConfig.timestamp, // 必填,生成签名的时间戳
nonceStr: wxConfig.nonceStr, // 必填,生成签名的随机串
signature: wxConfig.signature, // 必填,签名
jsApiList: ['previewImage'], // 必填,需要使用的JS接口列表
openTagList: ['wx-open-launch-weapp'] // 可选,需要使用的开放标签列表
});
// 鉴权成功回调
wx.ready(() => {
console.log('微信JS-SDK鉴权成功');
// this.onWxAuthSuccess();
});
// 鉴权失败回调
wx.error((res) => {
console.warn('微信JS-SDK鉴权失败:', res);
// Toast(`微信授权失败: ${JSON.stringify(res)}`);
});
} catch (error) {
console.warn('微信鉴权初始化失败:', error);
showToast('微信初始化失败');
}
}
// 获取微信鉴权配置参数
const getWxAuthConfig = async (versionNo1) => {
const v1 = versionNo1
try {
const url = encodeURIComponent(window.location.href.split('#')[0])
const year = new Date().getFullYear()
const versionNo1 = DecryptionECB(`${year}${year}${year}${year}`, v1)
const key = versionNo1.substring(0, 16)
const iv = versionNo1.substring(16, 32)
const encryptUrl = Encrypt(key, iv, url)
const response = await Post("thirdPartyAuth/getGwResultByUrl", {
url: encryptUrl
});
if (response.code === 0 && response.data) {
return response.data;
} else {
console.warn('获取微信配置失败:', response.msg);
return null;
}
} catch (error) {
console.warn('微信配置接口调用失败:', error);
return null;
}
}
// 微信鉴权成功后的回调
const onWxAuthSuccess = () => {
console.log('微信鉴权成功');
showToast('wx.ready回调')
}
const disableShare = () => {
try {
//禁止分享
if (typeof WeixinJSBridge == "undefined") {
if (document.addEventListener) {
document.addEventListener(
"WeixinJSBridgeReady",
onBridgeReady(),
false
);
} else if (document.attachEvent) {
document.attachEvent("WeixinJSBridgeReady", onBridgeReady());
document.attachEvent("onWeixinJSBridgeReady", onBridgeReady());
}
} else {
onBridgeReady();
}
} catch (error) {
console.log("禁止分享hideOptionMenu", error.message);
}
}
const onBridgeReady = () => {
WeixinJSBridge.call("hideOptionMenu");
}
const loadSdk = () => {
const script = document.createElement('script');
Post("config/mydConfig", {})
.then((res) => {
if (!!res.data && !!res.data.data) {
script.src = res.data.data.url + '?t=' + new Date().getTime()
}
script.onload = () => {
store.setSdkLoaded?.(true)
console.log('SDK successed');
};
script.onerror = () => {
console.error('Failed to load SDK');
};
document.head.appendChild(script)
})
.catch((error) => {
console.log(error);
})
}
//获取高温用户弹窗数据
const getHotUserData = () => {
Get("product/tip/select")
.then((res) => {
if (res.code === 0 && res.data) {
// 接口返回 rhetoricString 话术,productTipEntityList 产品列表
const hotUserData = {
tips: res.data.rhetoricString || '',
productList: res.data.productTipEntityList || []
};
if (res.data.rhetoricString && res.data.productTipEntityList.length > 0) {
store.setHotUserData?.(hotUserData);
}
// 获取当前时间戳
const now = new Date();
const clickTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
const userId = store.userId;
const source = store.source;
const sessionId = store.sessionId;
// 调用埋点接口
Post("product/record/save", {
userId: userId,
productName: "",
productCode: "",
channel: source,
sessionId: sessionId,
clickTime: clickTime,
buttonName: ""
}).then((res) => {
console.log("高温用户初始化埋点成功", res);
}).catch((error) => {
console.log("高温用户初始化埋点失败", error);
});
}
})
.catch((error) => {
console.log('获取高温用户弹窗数据失败', error);
});
}
</script>
<style scoped></style>