parent
59c04e5fb8
commit
ad90552de9
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -1,233 +1,221 @@ |
||||
import { ref, computed, inject, defineComponent } from 'vue' |
||||
import { useRouter } from 'vue-router' |
||||
import _ from 'lodash' |
||||
import wx from 'weixin-js-sdk' |
||||
import { mapState } from "vuex"; |
||||
import Utils from "@/utils/utils" |
||||
const name = "product" |
||||
const props = ["responseData","intents","itemlist"] |
||||
const labelesImg= require('@/assets/images/product/icon_yes.png') |
||||
const data =function() { |
||||
return { |
||||
labelesImg, |
||||
resultData:{}, //处理后的数据
|
||||
flagNav:true, |
||||
baseModuleData:[], |
||||
popUpFlag:false, |
||||
productImgUrl:"", |
||||
bottomModulesData:[ |
||||
{ id: 1, name: 'TKYW', src: require('@/assets/images/product/bottom_tkyw.png'), url: '' }, |
||||
], |
||||
exchangeButton: true, // 是否显示展开收起按钮
|
||||
showExchangeButton: true, |
||||
showTotal: true, // 显示展开还是收起
|
||||
isBtnsubmit: true, //防止关联问发送
|
||||
nowPopType:"", |
||||
tkywValue:"", |
||||
detaillTitle:"", |
||||
hasClicked:false |
||||
} |
||||
} |
||||
function created() { |
||||
const allData = _.cloneDeep(_.get(this.responseData,'productInfo')) //产品模板数据
|
||||
const originProDuctData= allData.insDuty |
||||
const originalText = _.cloneDeep(_.get(this.responseData,'originalText')) //图谱数据
|
||||
// console.log("产品、图谱数据===",allData,originalText)
|
||||
// console.log('保险责任==',originProDuctData)
|
||||
this.resultData = allData |
||||
//格式化
|
||||
import Utils from '@/utils/utils' |
||||
import labelesImg from '@/assets/images/product/icon_yes.png' |
||||
import bottomTkywImg from '@/assets/images/product/bottom_tkyw.png' |
||||
|
||||
export default defineComponent({ |
||||
name: 'product', |
||||
props: { |
||||
responseData: { type: Object, default: () => ({}) }, |
||||
intents: { type: Array, default: () => [] }, |
||||
itemlist: { type: Object, default: () => ({}) }, |
||||
}, |
||||
setup(props) { |
||||
const store = inject('appStore', null) |
||||
const manualService = inject('manualService', ref('1')) |
||||
const mescrollContent = inject('mescrollContent', ref(null)) |
||||
const router = useRouter() |
||||
|
||||
const resultData = ref({}) |
||||
const flagNav = ref(true) |
||||
const baseModuleData = ref([]) |
||||
const popUpFlag = ref(false) |
||||
const productImgUrl = ref('') |
||||
const bottomModulesData = ref([ |
||||
{ id: 1, name: 'TKYW', src: bottomTkywImg, url: '' }, |
||||
]) |
||||
const exchangeButton = ref(true) |
||||
const showExchangeButton = ref(true) |
||||
const showTotal = ref(true) |
||||
const isBtnsubmit = ref(true) |
||||
const nowPopType = ref('') |
||||
const tkywValue = ref('') |
||||
const detaillTitle = ref('') |
||||
const hasClicked = ref(false) |
||||
|
||||
const bottomModuleNum = computed( |
||||
() => bottomModulesData.value.filter((item) => !!item.url).length > 3, |
||||
) |
||||
|
||||
function initProductData() { |
||||
const allData = _.cloneDeep(_.get(props.responseData, 'productInfo')) |
||||
if (!allData) return |
||||
|
||||
const originProDuctData = allData.insDuty || {} |
||||
const originalText = _.cloneDeep(_.get(props.responseData, 'originalText')) |
||||
resultData.value = allData |
||||
|
||||
const afterData = [] |
||||
for (let i in originProDuctData) { |
||||
//结果对象
|
||||
for (const i in originProDuctData) { |
||||
const result = {} |
||||
result.key = i |
||||
//判断是否直接渲染
|
||||
const productVal = originProDuctData[i] |
||||
if (productVal.includes('|')) { |
||||
//包含|
|
||||
if (typeof productVal === 'string' && productVal.includes('|')) { |
||||
result.isRender = false |
||||
//分隔|拿到要处理的数据
|
||||
const beforeData = productVal.split('|') |
||||
//继续遍历是否为链接
|
||||
const afterData = beforeData.map((item) => { |
||||
const completedData = beforeData.map((item) => { |
||||
if (item.includes(':')) { |
||||
//链接形式
|
||||
const urlData = item.split(';') |
||||
return { urlkey: urlData[0], urlValue: urlData[1] } |
||||
} else { |
||||
// 非链接
|
||||
return item |
||||
} |
||||
return item |
||||
}) |
||||
result.completedData = afterData |
||||
result.completedData = completedData |
||||
} else { |
||||
//直接v-html渲染
|
||||
result.isRender = true |
||||
result.completedData = originProDuctData[i] |
||||
} |
||||
afterData.push(result) |
||||
} |
||||
|
||||
allData.insDuty = afterData |
||||
this.resultData = allData |
||||
//设置基本保额数据,其中包括对应条款原文的数据
|
||||
this.baseModuleData = [ |
||||
{key:"保险类型",val:_.get(this.resultData,'insType'),id:0}, |
||||
{key:"开停办时间",val:_.get(this.resultData,'osTime'),id:1}, |
||||
{key:"投保年龄",val:_.get(this.resultData,'insdAge'),id:2}, |
||||
{key:"保险期间",val:_.get(this.resultData,'insPeriod'),id:3}, |
||||
{key:"交费方式",val:_.get(this.resultData,'payType'),id:4}, |
||||
{key:"犹豫期",val:_.get(this.resultData,'hesPeriod'),id:5}, |
||||
{key:"观察期",val:_.get(this.resultData,'obsPeriod'),id:6}, |
||||
{key:"销售地区",val:_.get(this.resultData,'saleArea'),id:7}, |
||||
{key:"销售渠道",val:_.get(this.resultData,'salesChannels'),id:8}, |
||||
{key:"基本保额",val:_.get(this.resultData,'basicInsAmo'),id:9}, |
||||
{key:"每份保费",val:_.get(this.resultData,'basicPremium'),id:10}, |
||||
{key:"最低保证利率",val:_.get(this.resultData,'minGuaRate'),id:11}, |
||||
{key:"红利领取方式",val:_.get(this.resultData,'bonClaType'),id:12}, |
||||
{key:"免赔额",val:_.get(this.resultData,'deductible'),id:13} |
||||
resultData.value = allData |
||||
|
||||
baseModuleData.value = [ |
||||
{ key: '保险类型', val: _.get(resultData.value, 'insType'), id: 0 }, |
||||
{ key: '开停办时间', val: _.get(resultData.value, 'osTime'), id: 1 }, |
||||
{ key: '投保年龄', val: _.get(resultData.value, 'insdAge'), id: 2 }, |
||||
{ key: '保险期间', val: _.get(resultData.value, 'insPeriod'), id: 3 }, |
||||
{ key: '交费方式', val: _.get(resultData.value, 'payType'), id: 4 }, |
||||
{ key: '犹豫期', val: _.get(resultData.value, 'hesPeriod'), id: 5 }, |
||||
{ key: '观察期', val: _.get(resultData.value, 'obsPeriod'), id: 6 }, |
||||
{ key: '销售地区', val: _.get(resultData.value, 'saleArea'), id: 7 }, |
||||
{ key: '销售渠道', val: _.get(resultData.value, 'salesChannels'), id: 8 }, |
||||
{ key: '基本保额', val: _.get(resultData.value, 'basicInsAmo'), id: 9 }, |
||||
{ key: '每份保费', val: _.get(resultData.value, 'basicPremium'), id: 10 }, |
||||
{ key: '最低保证利率', val: _.get(resultData.value, 'minGuaRate'), id: 11 }, |
||||
{ key: '红利领取方式', val: _.get(resultData.value, 'bonClaType'), id: 12 }, |
||||
{ key: '免赔额', val: _.get(resultData.value, 'deductible'), id: 13 }, |
||||
] |
||||
|
||||
const originalArr = _.get(originalText, '[0].properties') |
||||
if (Array.isArray(originalArr) && originalArr.length > 0) { |
||||
this.baseModuleData.forEach(item=>{ |
||||
originalArr.forEach(tkyw=>{ |
||||
baseModuleData.value.forEach((item) => { |
||||
originalArr.forEach((tkyw) => { |
||||
if (tkyw.propName === item.key) { |
||||
item.tkywValue = tkyw.propValue |
||||
} |
||||
}) |
||||
}) |
||||
} |
||||
console.log("处理后结果===",this.baseModuleData) |
||||
//设置模块的url
|
||||
this.bottomModulesData.forEach(item => { |
||||
if (item.name === "TKYW") item.url = this.resultData.clauseText |
||||
|
||||
console.log('处理后结果===', baseModuleData.value) |
||||
|
||||
bottomModulesData.value.forEach((item) => { |
||||
if (item.name === 'TKYW') item.url = resultData.value.clauseText |
||||
}) |
||||
} |
||||
|
||||
const computed = { |
||||
bottomModuleNum(){ |
||||
return this.bottomModulesData.filter(item=>!!item.url).length > 3 |
||||
}, |
||||
...mapState([ |
||||
"manualService", |
||||
"mescrollContent", |
||||
]), |
||||
function showImgPopup(url) { |
||||
popUpFlag.value = true |
||||
productImgUrl.value = url |
||||
nowPopType.value = 'img' |
||||
} |
||||
|
||||
const methods = { |
||||
showImgPopup(url){ |
||||
this.popUpFlag = true |
||||
this.productImgUrl = url |
||||
this.nowPopType = "img" |
||||
}, |
||||
clickBottomBtn(name){ |
||||
let url = ''; |
||||
let productname = null; |
||||
function clickBottomBtn(name) { |
||||
let url = '' |
||||
if (name === 'TKYW') { |
||||
url = this.resultData.clauseText |
||||
productname = encodeURI(this.resultData.productName); |
||||
url = resultData.value.clauseText |
||||
} |
||||
let fileTypesReg = new RegExp(/\.pdf$/) |
||||
|
||||
const fileTypesReg = /\.pdf$/ |
||||
if (fileTypesReg.test(url)) { |
||||
// const valSrc = _.clone(this.urlMsg)
|
||||
// let msg = ''
|
||||
// if (_rules.IsNull(valSrc)) return
|
||||
// Object.keys(valSrc).map((item) => {
|
||||
// msg += `${item}=${valSrc[item]}&`
|
||||
// return msg
|
||||
// })
|
||||
// msg = msg.substr(0, msg.length - 1)
|
||||
// if (url.indexOf('?') === -1) {
|
||||
// url += `?${msg}${!!productname ? `&productname=${productname}` : ''}`
|
||||
// } else {
|
||||
// url += `&${msg}${!!productname ? `&productname=${productname}` : ''}`
|
||||
// }
|
||||
// 安卓小程序环境跳转预览页
|
||||
let isMiniProgram = false; |
||||
wx.miniProgram.getEnv((res) => { |
||||
if (res.miniprogram) { |
||||
isMiniProgram = true; |
||||
} |
||||
}); |
||||
if((Utils.getSystemType() === 'android' || Utils.getSystemType() === 'harmonyos') && isMiniProgram){ |
||||
this.$router.push({ |
||||
const systemType = Utils.getSystemType() |
||||
const isMiniProgram = Utils.isMiniProgram() |
||||
if ((systemType === 'android' || systemType === 'harmonyos') && isMiniProgram) { |
||||
router.push({ |
||||
name: 'previewPdf', |
||||
params: { |
||||
url: url |
||||
} |
||||
params: { url }, |
||||
}) |
||||
} else { |
||||
location.href = url; |
||||
location.href = url |
||||
} |
||||
return; |
||||
} |
||||
// console.log(url);
|
||||
//非文档则跳转
|
||||
// location.href = url;
|
||||
// openWebView(this.urlMsg.src, url, title, '5')
|
||||
}, |
||||
showTotalIntro() { |
||||
this.showTotal = !this.showTotal; |
||||
this.exchangeButton = !this.exchangeButton; |
||||
}, |
||||
//点击问题按钮
|
||||
richCallAnswer(item) { |
||||
var that = this; |
||||
// if(this.hasClicked){
|
||||
// return
|
||||
// }
|
||||
// if(this.itemlist.cmd === onceClickCmd){
|
||||
// this.hasClicked = true
|
||||
// }
|
||||
let sendParmas = { |
||||
reqAndResType: "1", //1文本 2图片 3问提评价
|
||||
} |
||||
|
||||
function showTotalIntro() { |
||||
showTotal.value = !showTotal.value |
||||
exchangeButton.value = !exchangeButton.value |
||||
} |
||||
|
||||
function richCallAnswer(item) { |
||||
const sendParmas = { |
||||
reqAndResType: '1', |
||||
msgText: item.intent, |
||||
intentId: item.intentId, |
||||
msgSource: this.manualService, //1机器人 2人工
|
||||
msgSource: manualService.value, |
||||
questionType: |
||||
this.itemlist.reqAndResType == "02" |
||||
props.itemlist.reqAndResType == '02' |
||||
? 5 |
||||
: this.itemlist.reqAndResType == "06" |
||||
: props.itemlist.reqAndResType == '06' |
||||
? 4 |
||||
: this.itemlist.reqAndResType == "11" |
||||
: props.itemlist.reqAndResType == '11' |
||||
? 8 |
||||
: "", |
||||
}; |
||||
if (that.isBtnsubmit) { |
||||
that.isBtnsubmit = false; |
||||
this.scrollTopBody(); |
||||
this.$store |
||||
.dispatch("sendingRobot", sendParmas) |
||||
.then((res) => { |
||||
that.isBtnsubmit = true; |
||||
: '', |
||||
} |
||||
|
||||
if (!isBtnsubmit.value) return |
||||
isBtnsubmit.value = false |
||||
scrollTopBody() |
||||
store |
||||
?.dispatch?.('sendingRobot', sendParmas) |
||||
.then(() => { |
||||
isBtnsubmit.value = true |
||||
}) |
||||
.catch((err) => { |
||||
that.isBtnsubmit = true; |
||||
console.log(err); |
||||
}); |
||||
isBtnsubmit.value = true |
||||
console.log(err) |
||||
}) |
||||
} |
||||
}, |
||||
scrollTopBody() { |
||||
|
||||
function scrollTopBody() { |
||||
setTimeout(() => { |
||||
//滚动到底部
|
||||
try { |
||||
this.mescrollContent.scrollTop = |
||||
this.mescrollContent.scrollHeight || 0; |
||||
if (mescrollContent.value) { |
||||
mescrollContent.value.scrollTop = mescrollContent.value.scrollHeight || 0 |
||||
} |
||||
} catch (error) { |
||||
console.log(error.message); |
||||
console.log(error.message) |
||||
} |
||||
}, 300); |
||||
}, |
||||
showGraph(item){ |
||||
this.popUpFlag = true; |
||||
this.nowPopType = "tkyw" |
||||
this.detaillTitle = item.key |
||||
this.tkywValue = item.tkywValue.replace(/(\n)/g, '<br/>') |
||||
}, 300) |
||||
} |
||||
|
||||
function showGraph(item) { |
||||
popUpFlag.value = true |
||||
nowPopType.value = 'tkyw' |
||||
detaillTitle.value = item.key |
||||
tkywValue.value = (item.tkywValue || '').replace(/(\n)/g, '<br/>') |
||||
} |
||||
|
||||
export default { |
||||
name, |
||||
data, |
||||
props, |
||||
created, |
||||
methods, |
||||
computed |
||||
initProductData() |
||||
|
||||
return { |
||||
labelesImg, |
||||
resultData, |
||||
flagNav, |
||||
baseModuleData, |
||||
popUpFlag, |
||||
productImgUrl, |
||||
bottomModulesData, |
||||
exchangeButton, |
||||
showExchangeButton, |
||||
showTotal, |
||||
isBtnsubmit, |
||||
nowPopType, |
||||
tkywValue, |
||||
detaillTitle, |
||||
hasClicked, |
||||
bottomModuleNum, |
||||
manualService, |
||||
mescrollContent, |
||||
showImgPopup, |
||||
clickBottomBtn, |
||||
showTotalIntro, |
||||
richCallAnswer, |
||||
scrollTopBody, |
||||
showGraph, |
||||
} |
||||
}, |
||||
}) |
||||
|
||||
@ -1,91 +0,0 @@ |
||||
import { defineStore } from 'pinia' |
||||
import { computed, ref } from 'vue' |
||||
import { clearCurrentPageContext } from '@/utils/pageMessage.js' |
||||
|
||||
export const usePageOverlayStore = defineStore('pageOverlay', () => { |
||||
// PAGE 承载态统一放在 Pinia,chat.vue 只负责分发 SSE 与选择承载模式。
|
||||
// 这样浮层模式和路由调试模式可以共用同一份 pageContext / actionResponse。
|
||||
const pageOverlayVisible = ref(false) |
||||
const currentPageOverlayContext = ref(null) |
||||
const currentPageOverlayActionResponse = ref(null) |
||||
|
||||
const hasCurrentPageContext = computed(() => Boolean(currentPageOverlayContext.value)) |
||||
|
||||
/** |
||||
* 默认浮层模式:保存当前 PAGE 上下文并显示 PageOverlayShell。 |
||||
* 每次打开新 PAGE 都先清空上一条动作回流,避免旧 UPDATE/MESSAGE 被新页面重复消费。 |
||||
* |
||||
* @param {object} pageContext 标准化后的 PAGE 上下文 |
||||
* @returns {boolean} |
||||
*/ |
||||
function openPageOverlay(pageContext) { |
||||
currentPageOverlayContext.value = pageContext |
||||
currentPageOverlayActionResponse.value = null |
||||
pageOverlayVisible.value = true |
||||
return true |
||||
} |
||||
|
||||
/** |
||||
* 路由调试模式:只保存 PAGE 上下文,不显示浮层。 |
||||
* 后续由 chat.vue 执行 router.push,目标路由页再从 Pinia/sessionStorage 读取上下文。 |
||||
* |
||||
* @param {object} pageContext 标准化后的 PAGE 上下文 |
||||
* @returns {boolean} |
||||
*/ |
||||
function openPageRoute(pageContext) { |
||||
currentPageOverlayContext.value = pageContext |
||||
currentPageOverlayActionResponse.value = null |
||||
pageOverlayVisible.value = false |
||||
return true |
||||
} |
||||
|
||||
/** |
||||
* 判断 PAGE UPDATE/MESSAGE 是否属于当前承载的页面。 |
||||
* 注意路由模式下 pageOverlayVisible 为 false,所以这里不能只看 visible, |
||||
* 还要允许存在 currentPageOverlayContext 的路由承载态继续接收回流。 |
||||
* |
||||
* @param {object} page SSE payload.page |
||||
* @returns {boolean} |
||||
*/ |
||||
function isCurrentPageActionMessage(page) { |
||||
const currentContext = currentPageOverlayContext.value |
||||
|
||||
return Boolean( |
||||
currentContext |
||||
&& (pageOverlayVisible.value || hasCurrentPageContext.value) |
||||
&& (!page?.pageId || !currentContext.pageId || page.pageId === currentContext.pageId) |
||||
) |
||||
} |
||||
|
||||
/** |
||||
* 保存当前 PAGE 的动作级回流,ClaimPagePanel 会通过 watcher 消费。 |
||||
* |
||||
* @param {object | null} actionResponse 原始 PAGE UPDATE/MESSAGE 报文 |
||||
*/ |
||||
function setPageActionResponse(actionResponse) { |
||||
currentPageOverlayActionResponse.value = actionResponse || null |
||||
} |
||||
|
||||
/** |
||||
* PAGE 生命周期结束时统一清理内存态和 sessionStorage 兜底缓存。 |
||||
* NODE_EXIT 命中当前 stage/node 时会调用这里,避免后续旧 UPDATE 继续落到已结束页面。 |
||||
*/ |
||||
function closePageOverlay() { |
||||
pageOverlayVisible.value = false |
||||
currentPageOverlayActionResponse.value = null |
||||
currentPageOverlayContext.value = null |
||||
clearCurrentPageContext() |
||||
} |
||||
|
||||
return { |
||||
pageOverlayVisible, |
||||
currentPageOverlayContext, |
||||
currentPageOverlayActionResponse, |
||||
hasCurrentPageContext, |
||||
openPageOverlay, |
||||
openPageRoute, |
||||
isCurrentPageActionMessage, |
||||
setPageActionResponse, |
||||
closePageOverlay, |
||||
} |
||||
}) |
||||
@ -0,0 +1,74 @@ |
||||
// Vue 3 兼容事件总线(替代 Vue 2 的 new Vue() Event Bus)
|
||||
// 用法保持不变:Bus.$on / Bus.$off / Bus.$emit / Bus.$once
|
||||
|
||||
const eventMap = new Map() |
||||
|
||||
function getHandlers(type) { |
||||
let handlers = eventMap.get(type) |
||||
if (!handlers) { |
||||
handlers = [] |
||||
eventMap.set(type, handlers) |
||||
} |
||||
return handlers |
||||
} |
||||
|
||||
const bus = { |
||||
$on(type, handler) { |
||||
if (!type || typeof handler !== 'function') return this |
||||
getHandlers(type).push(handler) |
||||
return this |
||||
}, |
||||
|
||||
$once(type, handler) { |
||||
if (!type || typeof handler !== 'function') return this |
||||
const onceHandler = (...args) => { |
||||
bus.$off(type, onceHandler) |
||||
handler(...args) |
||||
} |
||||
onceHandler.__original = handler |
||||
getHandlers(type).push(onceHandler) |
||||
return this |
||||
}, |
||||
|
||||
$off(type, handler) { |
||||
// Bus.$off() 清空全部
|
||||
if (type == null) { |
||||
eventMap.clear() |
||||
return this |
||||
} |
||||
|
||||
const handlers = eventMap.get(type) |
||||
if (!handlers) return this |
||||
|
||||
// Bus.$off(type) 移除该事件全部监听
|
||||
if (!handler) { |
||||
eventMap.delete(type) |
||||
return this |
||||
} |
||||
|
||||
for (let i = handlers.length - 1; i >= 0; i--) { |
||||
const fn = handlers[i] |
||||
if (fn === handler || fn.__original === handler) { |
||||
handlers.splice(i, 1) |
||||
} |
||||
} |
||||
if (!handlers.length) eventMap.delete(type) |
||||
return this |
||||
}, |
||||
|
||||
$emit(type, ...args) { |
||||
const handlers = eventMap.get(type) |
||||
if (!handlers || !handlers.length) return this |
||||
// 拷贝后执行,避免回调里 $off 影响本次遍历
|
||||
;[...handlers].forEach((fn) => { |
||||
try { |
||||
fn(...args) |
||||
} catch (error) { |
||||
console.error(`[bus] event "${type}" handler error:`, error) |
||||
} |
||||
}) |
||||
return this |
||||
}, |
||||
} |
||||
|
||||
export default bus |
||||
@ -1,556 +0,0 @@ |
||||
'use strict' |
||||
|
||||
//引入axios
|
||||
import axios from 'axios' |
||||
import router from '@/router' |
||||
import pinia from '@/store' |
||||
import { useAppStore } from '@/store/app.js' |
||||
|
||||
import { showToast, showConfirmDialog } from 'vant' |
||||
|
||||
// Toast.allowMultiple();
|
||||
// Toast.setDefaultOptions({ forbidClick: true });
|
||||
|
||||
const base = { |
||||
baseUrl: 'https://ssp-sit.cpic.com.cn', |
||||
timeout: 300000, |
||||
} |
||||
|
||||
const IDEMPOTENT_REQUEST_TIMEOUT = 100000 |
||||
|
||||
/** |
||||
* 提示函数 |
||||
* 禁止点击蒙层、显示3秒后关闭 |
||||
*/ |
||||
/** |
||||
* 显示提示消息 |
||||
* @param {string} msg - 提示消息内容 |
||||
*/ |
||||
const toast = msg => { |
||||
showToast({ |
||||
message: msg, |
||||
duration: 4000, |
||||
forbidClick: true |
||||
}); |
||||
} |
||||
|
||||
const getAppStore = () => useAppStore(pinia) |
||||
|
||||
// 封装浏览器网络信息读取,兼容不同内核前缀实现。
|
||||
const getNetworkConnection = () => globalThis.navigator?.connection |
||||
|| globalThis.navigator?.mozConnection |
||||
|| globalThis.navigator?.webkitConnection; |
||||
|
||||
// 与 SSE 侧统一弱网判断逻辑。
|
||||
// 带幂等键的关键请求在断网/弱网下直接判定失败,避免用户长时间等待后才发现发送失败。
|
||||
const isPoorNetworkEnvironment = () => { |
||||
if (globalThis.navigator?.onLine === false) { |
||||
return true; |
||||
} |
||||
|
||||
const connection = getNetworkConnection(); |
||||
if (!connection) { |
||||
return false; |
||||
} |
||||
|
||||
const effectiveType = connection.effectiveType || ''; |
||||
const saveData = Boolean(connection.saveData); |
||||
const rtt = Number(connection.rtt || 0); |
||||
const downlink = Number(connection.downlink || 0); |
||||
|
||||
return saveData |
||||
|| effectiveType === 'slow-2g' |
||||
|| effectiveType === '2g' |
||||
|| rtt >= 800 |
||||
|| (downlink > 0 && downlink < 0.5); |
||||
} |
||||
|
||||
// 只有当前 store 中仍然持有本次请求的幂等键时才刷新,
|
||||
// 避免并发请求把后续已经替换的新 key 又覆盖掉。
|
||||
const tryRefreshIdempotentKey = (requestIdempotentKey) => { |
||||
const appStore = getAppStore(); |
||||
if (appStore.getIdempotentKey() === requestIdempotentKey) { |
||||
appStore.refreshIdempotentKey(); |
||||
} |
||||
} |
||||
|
||||
// 发送成功后回写聊天消息状态,主要用于把之前失败过的同一条消息恢复为成功态。
|
||||
const markIdempotentMessageSuccess = (requestIdempotentKey) => { |
||||
if (!requestIdempotentKey) { |
||||
return; |
||||
} |
||||
const appStore = getAppStore(); |
||||
appStore.updateChatMessageSendStatus(requestIdempotentKey, true); |
||||
} |
||||
|
||||
// 发送失败时除了标记失败,还把原始请求参数挂回消息,
|
||||
// 方便后续 UI 展示“失败重试”并直接复用这份请求体。
|
||||
const markIdempotentMessageFailed = (requestIdempotentKey, requestParams) => { |
||||
if (!requestIdempotentKey) { |
||||
return; |
||||
} |
||||
const appStore = getAppStore(); |
||||
appStore.updateChatMessageSendStatus(requestIdempotentKey, false, requestParams); |
||||
} |
||||
|
||||
// axios 响应拦截器里有的分支会直接 reject(response),
|
||||
// 这里统一兼容原生 error.response 和“直接被 reject 的 response 对象”两种形态。
|
||||
const getRejectedResponse = (error) => { |
||||
if (error?.response) { |
||||
return error.response; |
||||
} |
||||
|
||||
if (typeof error?.status === 'number' || error?.statusText || error?.data) { |
||||
return error; |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
// 项目里大部分接口通过 msg/code 表示业务结果。
|
||||
// 这里只在明确识别出失败时返回 false,避免误伤没有统一字段的响应结构。
|
||||
const isBusinessSuccessResponse = (data) => { |
||||
if (!data || typeof data !== 'object') { |
||||
return true; |
||||
} |
||||
|
||||
if (Object.prototype.hasOwnProperty.call(data, 'msg')) { |
||||
return data.msg === 'success'; |
||||
} |
||||
|
||||
if (Object.prototype.hasOwnProperty.call(data, 'code')) { |
||||
return data.code === 0 || data.code === 200; |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
// 仅对带幂等键的关键请求启用增强逻辑:
|
||||
// 1. 断网/弱网直接失败;
|
||||
// 2. 单次请求超时时间固定为 10 秒;
|
||||
// 3. 请求成功后刷新下一次要用的幂等键;
|
||||
// 4. 超时、HTTP 错误、业务失败都回写 chatArr 中对应消息的失败状态。
|
||||
/** |
||||
* 发送带幂等保障的 POST 请求,在弱网环境下直接标记失败,请求成功或业务失败时自动更新幂等消息状态 |
||||
* @param {string} url - 请求地址 |
||||
* @param {Object} params - 请求参数,需包含 idempotentKey 字段用于幂等标识 |
||||
* @param {string} params.idempotentKey - 幂等请求的唯一标识键 |
||||
* @returns {Promise<Object>} 返回 axios 响应对象;弱网环境下直接返回 rejected Promise |
||||
* @throws {Error} 弱网环境时抛出"当前网络环境较差,请稍后重试"错误;请求超时或服务端拒绝时抛出对应错误 |
||||
*/ |
||||
function postWithIdempotentRequest(url, params) { |
||||
const requestIdempotentKey = params?.idempotentKey; |
||||
console.log('当前请求带幂等键--------->', url , params) |
||||
|
||||
if (isPoorNetworkEnvironment() && import.meta.env.VITE_ENABLE_TEST !== 'true') { |
||||
console.warn('当前处于断网或弱网环境,幂等请求直接标记为失败', requestIdempotentKey); |
||||
markIdempotentMessageFailed(requestIdempotentKey, params); |
||||
return Promise.reject(new Error('当前网络环境较差,请稍后重试')); |
||||
} |
||||
|
||||
// 这里用“拿到响应结果”来近似判定本次请求已经成功发出,
|
||||
// 并在成功后把之前可能失败过的同一条消息恢复为成功态。
|
||||
const markRequestSent = () => { |
||||
markIdempotentMessageSuccess(requestIdempotentKey); |
||||
tryRefreshIdempotentKey(requestIdempotentKey); |
||||
} |
||||
|
||||
return axios.post(url, params, { |
||||
timeout: IDEMPOTENT_REQUEST_TIMEOUT, |
||||
_skipNetworkErrorToast: true |
||||
}).then(res => { |
||||
if (!isBusinessSuccessResponse(res?.data)) { |
||||
markIdempotentMessageFailed(requestIdempotentKey, params); |
||||
return res; |
||||
} |
||||
markRequestSent(); |
||||
return res; |
||||
}).catch(error => { |
||||
const rejectedResponse = getRejectedResponse(error); |
||||
|
||||
if (error?.code === 'ECONNABORTED' || error?.message?.includes('timeout')) { |
||||
console.error('Post 请求超时,标记消息发送失败', requestIdempotentKey); |
||||
markIdempotentMessageFailed(requestIdempotentKey, params); |
||||
} |
||||
|
||||
if (rejectedResponse) { |
||||
markIdempotentMessageFailed(requestIdempotentKey, params); |
||||
} |
||||
|
||||
return Promise.reject(error); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* 跳转登录页 |
||||
* 携带当前页面路由,以期在登录页面完成登录后返回当前页面 |
||||
*/ |
||||
const toLogin = () => { |
||||
router.replace({ |
||||
path: '/liics/login', |
||||
query: { |
||||
redirect: router.currentRoute.fullPath |
||||
} |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* 请求失败后的错误统一处理 |
||||
* @param {Number} status 请求失败的状态码 |
||||
*/ |
||||
const errorHandle = (status, msg) => { |
||||
// 状态码判断
|
||||
switch (status) { |
||||
//未登录状态,跳转登录页 跳转登录
|
||||
case 401: |
||||
// toLogin();
|
||||
// 清除token
|
||||
toast('【401】当前会话超时,请刷新页面'); |
||||
// store.commit('del_token');
|
||||
sessionStorage.removeItem('token'); |
||||
break; |
||||
// 403 token过期
|
||||
// 清除token并跳转登录页
|
||||
case 403: |
||||
toast('【403】当前会话超时,请刷新页面'); |
||||
// 清除token
|
||||
// store.commit('del_token');
|
||||
sessionStorage.removeItem('token'); |
||||
setTimeout(() => { |
||||
// toLogin();
|
||||
}, 1000); |
||||
break; |
||||
// 404请求不存在
|
||||
case 404: |
||||
toast('哎呦~404了~休息一下'); |
||||
break; |
||||
case 408: |
||||
toast('请求超时'); |
||||
break; |
||||
case 500: |
||||
toast('当前网络不给力,请检查你的网络设置'); |
||||
break; |
||||
case 503: |
||||
toast('当前服务器不可用,请稍后重试'); |
||||
break; |
||||
// 其他错误,直接抛出错误提示
|
||||
default: |
||||
toast(`连接错误${msg}`); |
||||
} |
||||
} |
||||
|
||||
// 请求超时时间
|
||||
axios.defaults.timeout = base.timeout |
||||
|
||||
//设置默认请求头
|
||||
// axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
|
||||
axios.defaults.headers.post['Content-Type'] = 'application/json' |
||||
|
||||
// 请求拦截器
|
||||
axios.interceptors.request.use( |
||||
config => { |
||||
// 每次发送请求之前判断是否存在token,如果存在,则统一在http请求的header都加上token,不用每次请求都手动添加了
|
||||
// 即使本地存在token,也有可能token是过期的,所以在响应拦截器中要对返回状态进行判断
|
||||
const token = sessionStorage.getItem('token'); |
||||
if (token) { |
||||
config.headers.Authorization = `Bearer ${token}`; |
||||
} |
||||
return config; |
||||
}, |
||||
error => { |
||||
return Promise.reject(error); |
||||
}) |
||||
|
||||
|
||||
// 响应拦截器
|
||||
axios.interceptors.response.use( |
||||
// 请求成功
|
||||
(res) => { |
||||
if (res.data.code === 10001) { |
||||
// store.commit('del_token');
|
||||
sessionStorage.removeItem("token"); // 删除已经失效或过期的token(不删除也可以,因为登录后覆盖)
|
||||
|
||||
|
||||
|
||||
showConfirmDialog({ |
||||
title: '提示', |
||||
message: '当前会话已结束,请刷新页面或关闭页面重新进入', |
||||
confirmButtonText: '取消', |
||||
cancelButtonText: '确认', |
||||
confirmButtonColor: '#000000', |
||||
cancelButtonColor: '#000000' |
||||
}).then(() => { |
||||
|
||||
}).catch(() => { |
||||
|
||||
}); |
||||
|
||||
return Promise.resolve(res); |
||||
} |
||||
|
||||
res.status === 200 ? Promise.resolve(res) : Promise.reject(res); |
||||
|
||||
return res |
||||
}, |
||||
// 服务器状态码不是200的情况
|
||||
// 请求失败
|
||||
async error => { |
||||
const { |
||||
response |
||||
} = error; |
||||
if (response) { |
||||
//过滤排队接口弹窗问题
|
||||
if (response.config.url.indexOf('HumanChat/queueNumbers') != -1 || response.config.url.indexOf('HumanChat/msgCallBack') != -1) { |
||||
return Promise.reject(response); |
||||
} |
||||
|
||||
if (response.data.code === 10001) { |
||||
// store.commit('del_token');
|
||||
sessionStorage.removeItem("token"); // 删除已经失效或过期的token(不删除也可以,因为登录后覆盖)
|
||||
|
||||
|
||||
|
||||
showConfirmDialog({ |
||||
title: '提示', |
||||
message: '当前会话已结束,请刷新页面或关闭页面重新进入', |
||||
confirmButtonText: '取消', |
||||
cancelButtonText: '确认', |
||||
confirmButtonColor: '#000000', |
||||
cancelButtonColor: '#000000' |
||||
}).then(() => { |
||||
|
||||
}).catch(() => { |
||||
|
||||
}); |
||||
|
||||
return Promise.reject(response); |
||||
} |
||||
|
||||
// 请求已发出,但是不在2xx的范围
|
||||
errorHandle(response.status, response.statusText); |
||||
return Promise.reject(response); |
||||
} else { |
||||
if (error?.config?._skipNetworkErrorToast || error?.code === 'ERR_CANCELED') { |
||||
return Promise.reject(error); |
||||
} |
||||
// 处理断网的情况
|
||||
// eg:请求超时或断网时,更新state的network状态
|
||||
// network状态在app.vue中控制着一个全局的断网提示组件的显示隐藏
|
||||
// 关于断网组件中的刷新重新获取数据,会在断网组件中说明
|
||||
// store.commit('changeNetwork', false);
|
||||
if (error?.message?.includes('timeout')) { // 判断请求异常信息中是否含有超时timeout字符串
|
||||
toast('请求超时,稍后重试!'); |
||||
return Promise.reject(error); |
||||
} |
||||
|
||||
toast('当前网络不给力,请检查你的网络设置'); |
||||
return Promise.reject(error); |
||||
} |
||||
}); |
||||
|
||||
/** |
||||
* get方法,对应get请求 |
||||
* @param {String} url [请求的url地址] |
||||
* @param {Object} params [请求时携带的参数] |
||||
*/ |
||||
export function Get(url, params) { |
||||
return new Promise((resolve, reject) => { |
||||
url = base.baseUrl + url; |
||||
|
||||
axios.get(url, { params }) |
||||
.then(res => { |
||||
resolve(res.data); |
||||
}) |
||||
.catch(err => { |
||||
reject(err.status) |
||||
}) |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* post方法,对应post请求 |
||||
* @param {String} url [请求的url地址] |
||||
* @param {Object} params [请求时携带的参数] |
||||
*/ |
||||
export function Post(url, params) { |
||||
return new Promise((resolve, reject) => { |
||||
url = base.baseUrl + url; |
||||
console.log('url', url,base); |
||||
const request = params?.idempotentKey |
||||
? postWithIdempotentRequest(url, params) |
||||
: axios.post(url, params); |
||||
|
||||
request |
||||
.then(res => { |
||||
resolve(res.data); |
||||
}) |
||||
.catch(err => { |
||||
reject(err?.status || err) |
||||
}) |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* post方法,对应post请求 上传图片专用 |
||||
* @param {String} url [请求的url地址] |
||||
* @param {Object} params [请求时携带的参数] |
||||
*/ |
||||
export function upLoadImgHttp(url, params) { |
||||
return new Promise((resolve, reject) => { |
||||
url = base.baseUrl + url; |
||||
const instance = axios.create({ |
||||
withCredentials: true, |
||||
}) |
||||
|
||||
instance.post(url, params, { |
||||
headers: { |
||||
'Content-Type': 'multipart/form-data', |
||||
// token: store.state.token
|
||||
} |
||||
}) |
||||
.then(res => { |
||||
resolve(res.data); |
||||
}) |
||||
.catch(err => { |
||||
reject(err.status) |
||||
}) |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* 获取图片数据(支持base64格式) |
||||
* @param {String} url [请求的url地址] |
||||
* @param {Object} params [请求时携带的参数] |
||||
* @param {Object} options [额外配置选项] |
||||
*/ |
||||
export function fetchImageData(url, params, options = {}) { |
||||
return new Promise((resolve, reject) => { |
||||
url = base.baseUrl + url; |
||||
|
||||
const { |
||||
timeout = 10000 |
||||
} = options; |
||||
|
||||
const instance = axios.create({ |
||||
timeout, |
||||
withCredentials: true, |
||||
}); |
||||
|
||||
instance.post(url, params, { |
||||
headers: { |
||||
'Content-Type': 'application/json', |
||||
// token: store.state.token
|
||||
} |
||||
}) |
||||
.then(res => { |
||||
// 检查响应格式
|
||||
if (res.data.code !== 0) { |
||||
reject(new Error(res.data.msg || 'Request failed')); |
||||
return; |
||||
} |
||||
|
||||
const imageData = res.data.data; |
||||
if (!imageData || !imageData.content) { |
||||
reject(new Error('Invalid response: missing image content')); |
||||
return; |
||||
} |
||||
|
||||
// 验证文件扩展名是否为图片格式
|
||||
const validImageExts = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp']; |
||||
const fileExt = imageData.fileNameExt?.toLowerCase(); |
||||
if (!validImageExts.includes(fileExt)) { |
||||
reject(new Error(`Unsupported image format: ${fileExt}`)); |
||||
return; |
||||
} |
||||
|
||||
resolve({ |
||||
fileName: imageData.fileName, |
||||
fileCrc: imageData.fileCrc, |
||||
fileNameExt: imageData.fileNameExt, |
||||
content: imageData.content, |
||||
// 构建完整的data URL
|
||||
dataUrl: `data:image/${fileExt};base64,${imageData.content}` |
||||
}); |
||||
}) |
||||
.catch(err => { |
||||
reject(err); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* 获取图片文件流(保持向后兼容) |
||||
* @deprecated 使用 fetchImageData 替代 |
||||
*/ |
||||
export function fetchImageStream(url, params, options = {}) { |
||||
return fetchImageData(url, params, options).then(imageData => { |
||||
// 将base64转换为blob以保持兼容性
|
||||
return base64ToBlob(imageData.content, `image/${imageData.fileNameExt}`); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* 将base64字符串转换为Blob对象 |
||||
* @param {String} base64Data - base64编码的数据(不包含前缀) |
||||
* @param {String} contentType - MIME类型 |
||||
* @returns {Blob} Blob对象 |
||||
*/ |
||||
export function base64ToBlob(base64Data, contentType = 'image/png') { |
||||
try { |
||||
// 解码base64数据
|
||||
const byteCharacters = atob(base64Data); |
||||
const byteNumbers = new Array(byteCharacters.length); |
||||
|
||||
for (let i = 0; i < byteCharacters.length; i++) { |
||||
byteNumbers[i] = byteCharacters.charCodeAt(i); |
||||
} |
||||
|
||||
const byteArray = new Uint8Array(byteNumbers); |
||||
return new Blob([byteArray], { type: contentType }); |
||||
} catch (error) { |
||||
throw new Error(`Failed to convert base64 to blob: ${error.message}`); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 通用文件下载方法 |
||||
* @param {String} url [请求的url地址] |
||||
* @param {Object} params [请求时携带的参数] |
||||
* @param {Object} options [配置选项] |
||||
*/ |
||||
export function downloadFile(url, params, options = {}) { |
||||
return new Promise((resolve, reject) => { |
||||
url = base.baseUrl + url; |
||||
|
||||
const { |
||||
timeout = 30000, |
||||
responseType = 'blob', |
||||
method = 'POST' |
||||
} = options; |
||||
|
||||
const config = { |
||||
url, |
||||
method, |
||||
headers: { |
||||
'Content-Type': 'application/json', |
||||
// token: store.state.token
|
||||
}, |
||||
responseType, |
||||
timeout |
||||
}; |
||||
|
||||
if (method.toUpperCase() === 'POST') { |
||||
config.data = params; |
||||
} else { |
||||
config.params = params; |
||||
} |
||||
|
||||
axios(config) |
||||
.then(res => { |
||||
resolve(res.data); |
||||
}) |
||||
.catch(err => { |
||||
reject(err); |
||||
}); |
||||
}); |
||||
} |
||||
Loading…
Reference in new issue