聊天功能联调

master
wujingling0908 2 weeks ago
parent 59c04e5fb8
commit ad90552de9
  1. 478
      src/assets/style/chat.css
  2. 11
      src/components/abstract.vue
  3. 2380
      src/components/chat-content.vue
  4. 181
      src/components/chat-new.vue
  5. 7
      src/components/hot-question-new.vue
  6. 430
      src/components/product/product.js
  7. 1
      src/components/text-folding.vue
  8. 54
      src/store/app.js
  9. 91
      src/store/pageOverlay.js
  10. 180
      src/utils/api.js
  11. 74
      src/utils/bus.js
  12. 95
      src/utils/filters.js
  13. 556
      src/utils/http.js
  14. 2
      src/utils/index.js

File diff suppressed because one or more lines are too long

@ -1,6 +1,7 @@
<script setup> <script setup>
import { ref, onMounted, computed, inject } from 'vue'; import { ref, onMounted, computed, inject } from 'vue';
import { Post } from '@/utils/https' import { Post } from '@/utils/https'
import { messageSend } from '@/utils/api'
const store = inject('appStore', null) const store = inject('appStore', null)
const businessHandleInfo = ref({}) const businessHandleInfo = ref({})
const abstractList = computed(() => [ const abstractList = computed(() => [
@ -9,6 +10,7 @@ const abstractList = computed(() => [
num: 3, num: 3,
text: '保单保障中', text: '保单保障中',
unit: '张', unit: '张',
name: '查保单',
btnText: '查保单', btnText: '查保单',
btnBg: 'linear-gradient(90deg, #025cea 0%, #0595f6 100%)', btnBg: 'linear-gradient(90deg, #025cea 0%, #0595f6 100%)',
}, },
@ -17,6 +19,7 @@ const abstractList = computed(() => [
num: businessHandleInfo.value?.contents?.length || 0, num: businessHandleInfo.value?.contents?.length || 0,
text: '待办需处理', text: '待办需处理',
unit: '个', unit: '个',
name: '去处理',
btnText: '去处理', btnText: '去处理',
btnBg: 'linear-gradient(111deg, #ff752a 44%, #feb52d 136%)', btnBg: 'linear-gradient(111deg, #ff752a 44%, #feb52d 136%)',
}, },
@ -25,6 +28,7 @@ const abstractList = computed(() => [
num: 0, num: 0,
text: '生存金权益', text: '生存金权益',
unit: '项', unit: '项',
name: '去领取',
btnText: '去领取', btnText: '去领取',
btnBg: 'linear-gradient(261deg, #2ed0c6 3%, #08b7ac 65%)', btnBg: 'linear-gradient(261deg, #2ed0c6 3%, #08b7ac 65%)',
}, },
@ -48,7 +52,12 @@ const businessHandle = () => {
} }
} }
const toDetail = (record) => { const toDetail = (record) => {
console.log(window); const text = record?.name
console.log('text',text);
if (!text) return
messageSend(text).catch((err) => {
console.log(err)
})
} }
onMounted(async () => { onMounted(async () => {
businessHandle() businessHandle()

File diff suppressed because it is too large Load Diff

@ -1,20 +1,34 @@
<script setup> <script setup>
import { ref, computed, inject, watch, nextTick, onMounted } from 'vue' import { ref, computed, inject, watch, nextTick, onMounted } from 'vue'
import { showToast } from 'vant'; import { showToast } from 'vant'
import _ from 'lodash'
import footerBarInput from '@/components/footer-bar-input.vue' import footerBarInput from '@/components/footer-bar-input.vue'
import hotQuestionNew from '@/components/hot-question-new.vue' import hotQuestionNew from '@/components/hot-question-new.vue'
import welcomes from '@/components/welcomes.vue' import welcomes from '@/components/welcomes.vue'
import abstract from '@/components/abstract.vue' import abstract from '@/components/abstract.vue'
import chatContent from '@/components/chat-content.vue'
import { messageSend } from '@/utils/api' import { messageSend } from '@/utils/api'
import { Post } from '@/utils/https'
import { SJHLCmdList } from '@/utils/constant'
defineOptions({ defineOptions({
name: 'chat-new', name: 'chat-new',
}) })
import { Post } from '@/utils/https'
const store = inject('appStore', null) const store = inject('appStore', null)
const chatArr = inject('chatArr', ref([])) const chatArr = inject('chatArr', ref([]))
const isChatLoading = inject('isChatLoading', ref(false)) const isChatLoading = inject('isChatLoading', ref(false))
const sessionId = inject('sessionId', ref(''))
const userId = inject('userId', ref(''))
const isLostMessage = inject('isLostMessage', ref(null))
const mainContainerRef = ref(null) const mainContainerRef = ref(null)
const historylist = ref([])
const pageNum = ref(1)
const pageSize = ref(10)
const nolist = ref(false)
const nolistTxt = ref('没有更多数据了')
function getItemText(item) { function getItemText(item) {
return item?.message?.[0]?.options?.label || '' return item?.message?.[0]?.options?.label || ''
} }
@ -22,8 +36,8 @@ function getItemText(item) {
const showTyping = computed( const showTyping = computed(
() => () =>
isChatLoading.value && isChatLoading.value &&
chatArr.value.length > 0 && store.chatArr.length > 0 &&
chatArr.value[chatArr.value.length - 1].type === 'user', store.chatArr[store.chatArr.length - 1].type === 'user',
) )
function sendText(text) { function sendText(text) {
@ -38,7 +52,161 @@ function sendText(text) {
function handleAskClick(item) { function handleAskClick(item) {
sendText(item) sendText(item)
} }
// 更新未读状态
function updateReadStatus(datalist) {
if (isLostMessage.value != 1) {
// 是不是当通会话
return
}
if (datalist && datalist.length > 0) {
const readedStatus = []
datalist.forEach((item) => {
if (item.chatId) {
if (sessionId.value == item.sessionId && item.fromFlag == 2) {
readedStatus.push(item.chatId)
}
}
})
// 提交到后台
store
?.updateReadStatus?.(readedStatus)
?.then(() => {
console.log('更新成功', readedStatus)
})
?.catch((err) => {
console.log(err)
})
}
}
function scrollMainTo(top) {
const el = mainContainerRef.value
if (!el) return
el.scrollTop = top
}
function getHeaderUrl(context, payload) {
let params = {
"source": store.source
}
return new Promise((resolve, reject) => {
Post(
"config/avatar",
params
).then(res => {
resolve(res.data);
console.log("获取头像URL==", res)
if (res.code && res.code != 0) {
// Toast(res.msg);
reject(res.msg)
return;
}
//本地环境显示图片
// if (window.location.href.indexOf('localhost') != -1) {
// res.data = res.data.replaceAll('../', `//ssp-dev.cpic.com.cn/`)
// }
store.setHeaderAvatar(JSON.parse(res.data));
resolve(res.data);
})
.catch(error => {
reject(error)
});
});
}
const getChatHistory = async () => {
const params = {
page: pageNum.value,
pageSize: pageSize.value,
sessionId: sessionId.value || store?.sessionId,
userId: userId.value || store?.userId,
}
Post('history/chatInfo', params)
.then((response) => {
if (response.code && response.code != 0) {
showToast(response.msg)
return
}
// 成功返回
if (response.data?.list?.length > 0) {
const dataList = response.data.list
pageNum.value++
// 不满一页,说明已经无更多消息
if (dataList.length < pageSize.value) {
nolist.value = true
nolistTxt.value = '没有更多数据了'
}
// 生成 VIEW_ID,大写,避免污染源数据
dataList.forEach((val) => {
val.VIEW_ID = 'msg' + val.timestamp
})
// 获取当前最顶部的 VIEW_ID(注意写在 concat 前面)
let topMsg = historylist.value[0]
// 处理隐藏的数据
if (topMsg && !topMsg.msgText) {
topMsg = historylist.value[1]
if (topMsg && !topMsg.msgText) {
topMsg = historylist.value[2]
}
}
// 设置列表数据
store.chatArr = dataList.concat(store.chatArr)
_.map(store.chatArr, (item) => {
// 历史记录不展示数据互联
if (SJHLCmdList.includes(item.cmd) && !_.isEmpty(item.VIEW_ID)) {
item.msgText = ''
}
// 历史问句去掉 & 符号
if (item.fromFlag === 1 && item.msgText && item.msgText.indexOf('&') !== -1) {
item.msgText = item.msgText.replace(/&/g, '')
}
})
// 更新未读状态
updateReadStatus(dataList)
nextTick(() => {
if (pageNum.value <= 2) {
if (isLostMessage.value === 3) {
showToast('您有新消息未读,请及时查看历史消息')
}
setTimeout(() => {
scrollMainTo(99999)
}, 150)
} else if (topMsg) {
const view = document.querySelector('#' + topMsg.VIEW_ID)
console.log(topMsg.VIEW_ID + ' 节点离页面顶部的距离=' + view?.offsetTop)
setTimeout(() => {
if (view) scrollMainTo(view.offsetTop - 20)
}, 150)
}
})
} else {
nolist.value = true
nolistTxt.value = pageNum.value === 1 ? '当前还没有您的聊天记录' : '没有更多数据了'
}
})
.catch(() => {
pageNum.value--
nolist.value = true
})
}
onMounted(async () => { onMounted(async () => {
getHeaderUrl()
getChatHistory()
}) })
</script> </script>
@ -48,7 +216,7 @@ onMounted(async () => {
<welcomes /> <welcomes />
<abstract /> <abstract />
<hotQuestionNew /> <hotQuestionNew />
<div class="message-con" v-if="chatArr.length || showTyping"> <!-- <div class="message-con" v-if="chatArr.length || showTyping">
<div v-for="(item, index) in chatArr" :key="index" <div v-for="(item, index) in chatArr" :key="index"
:class="['msg-item', item.type === 'user' ? 'from-user' : 'from-bot']"> :class="['msg-item', item.type === 'user' ? 'from-user' : 'from-bot']">
<div class="bubble">{{ getItemText(item) }}</div> <div class="bubble">{{ getItemText(item) }}</div>
@ -56,7 +224,8 @@ onMounted(async () => {
<div class="msg-item from-bot" v-if="showTyping"> <div class="msg-item from-bot" v-if="showTyping">
<div class="bubble typing"><span></span><span></span><span></span></div> <div class="bubble typing"><span></span><span></span><span></span></div>
</div> </div>
</div> </div> -->
<chatContent :historylist="chatArr" />
</div> </div>
<div class="chat-footer"> <div class="chat-footer">
<footer-bar-input /> <footer-bar-input />

@ -3,6 +3,7 @@ import { ref, computed, inject, watch, nextTick, onMounted } from 'vue'
import { showToast } from 'vant'; import { showToast } from 'vant';
import { Post } from '@/utils/https' import { Post } from '@/utils/https'
const store = inject('appStore', null) const store = inject('appStore', null)
import { messageSend } from '@/utils/api'
const questionList = ref([]) const questionList = ref([])
const initHotQuestion = async () => { const initHotQuestion = async () => {
let param = { let param = {
@ -46,7 +47,11 @@ const initHotQuestion = async () => {
}); });
} }
const handleAskClick=(record)=>{ const handleAskClick=(record)=>{
// store.setAskRecord(record) const text = record?.name
if (!text) return
messageSend(text).catch((err) => {
console.log(err)
})
} }
onMounted(async () => { onMounted(async () => {
await initHotQuestion() await initHotQuestion()

@ -1,233 +1,221 @@
import { ref, computed, inject, defineComponent } from 'vue'
import { useRouter } from 'vue-router'
import _ from 'lodash' import _ from 'lodash'
import wx from 'weixin-js-sdk' import Utils from '@/utils/utils'
import { mapState } from "vuex"; import labelesImg from '@/assets/images/product/icon_yes.png'
import Utils from "@/utils/utils" import bottomTkywImg from '@/assets/images/product/bottom_tkyw.png'
const name = "product"
const props = ["responseData","intents","itemlist"] export default defineComponent({
const labelesImg= require('@/assets/images/product/icon_yes.png') name: 'product',
const data =function() { props: {
return { responseData: { type: Object, default: () => ({}) },
labelesImg, intents: { type: Array, default: () => [] },
resultData:{}, //处理后的数据 itemlist: { type: Object, default: () => ({}) },
flagNav:true, },
baseModuleData:[], setup(props) {
popUpFlag:false, const store = inject('appStore', null)
productImgUrl:"", const manualService = inject('manualService', ref('1'))
bottomModulesData:[ const mescrollContent = inject('mescrollContent', ref(null))
{ id: 1, name: 'TKYW', src: require('@/assets/images/product/bottom_tkyw.png'), url: '' }, const router = useRouter()
],
exchangeButton: true, // 是否显示展开收起按钮 const resultData = ref({})
showExchangeButton: true, const flagNav = ref(true)
showTotal: true, // 显示展开还是收起 const baseModuleData = ref([])
isBtnsubmit: true, //防止关联问发送 const popUpFlag = ref(false)
nowPopType:"", const productImgUrl = ref('')
tkywValue:"", const bottomModulesData = ref([
detaillTitle:"", { id: 1, name: 'TKYW', src: bottomTkywImg, url: '' },
hasClicked:false ])
} const exchangeButton = ref(true)
} const showExchangeButton = ref(true)
function created() { const showTotal = ref(true)
const allData = _.cloneDeep(_.get(this.responseData,'productInfo')) //产品模板数据 const isBtnsubmit = ref(true)
const originProDuctData= allData.insDuty const nowPopType = ref('')
const originalText = _.cloneDeep(_.get(this.responseData,'originalText')) //图谱数据 const tkywValue = ref('')
// console.log("产品、图谱数据===",allData,originalText) const detaillTitle = ref('')
// console.log('保险责任==',originProDuctData) const hasClicked = ref(false)
this.resultData = allData
//格式化 const bottomModuleNum = computed(
const afterData = [] () => bottomModulesData.value.filter((item) => !!item.url).length > 3,
for (let i in originProDuctData) { )
//结果对象
const result = {} function initProductData() {
result.key = i const allData = _.cloneDeep(_.get(props.responseData, 'productInfo'))
//判断是否直接渲染 if (!allData) return
const productVal = originProDuctData[i]
if (productVal.includes('|')) { const originProDuctData = allData.insDuty || {}
//包含| const originalText = _.cloneDeep(_.get(props.responseData, 'originalText'))
result.isRender = false resultData.value = allData
//分隔|拿到要处理的数据
const beforeData = productVal.split('|') const afterData = []
//继续遍历是否为链接 for (const i in originProDuctData) {
const afterData = beforeData.map((item) => { const result = {}
if (item.includes(':')) { result.key = i
//链接形式 const productVal = originProDuctData[i]
const urlData = item.split(';') if (typeof productVal === 'string' && productVal.includes('|')) {
return { urlkey: urlData[0], urlValue: urlData[1] } result.isRender = false
const beforeData = productVal.split('|')
const completedData = beforeData.map((item) => {
if (item.includes(':')) {
const urlData = item.split(';')
return { urlkey: urlData[0], urlValue: urlData[1] }
}
return item
})
result.completedData = completedData
} else { } else {
// 非链接 result.isRender = true
return item result.completedData = originProDuctData[i]
} }
afterData.push(result)
}
allData.insDuty = afterData
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) {
baseModuleData.value.forEach((item) => {
originalArr.forEach((tkyw) => {
if (tkyw.propName === item.key) {
item.tkywValue = tkyw.propValue
}
})
})
}
console.log('处理后结果===', baseModuleData.value)
bottomModulesData.value.forEach((item) => {
if (item.name === 'TKYW') item.url = resultData.value.clauseText
}) })
result.completedData = afterData
} else {
//直接v-html渲染
result.isRender = true
result.completedData = originProDuctData[i]
} }
afterData.push(result)
} function showImgPopup(url) {
allData.insDuty = afterData popUpFlag.value = true
this.resultData = allData productImgUrl.value = url
//设置基本保额数据,其中包括对应条款原文的数据 nowPopType.value = 'img'
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}
]
const originalArr = _.get(originalText,'[0].properties')
if(Array.isArray(originalArr)&&originalArr.length>0){
this.baseModuleData.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
})
}
const computed = {
bottomModuleNum(){
return this.bottomModulesData.filter(item=>!!item.url).length > 3
},
...mapState([
"manualService",
"mescrollContent",
]),
}
const methods = {
showImgPopup(url){
this.popUpFlag = true
this.productImgUrl = url
this.nowPopType = "img"
},
clickBottomBtn(name){
let url = '';
let productname = null;
if (name === 'TKYW') {
url = this.resultData.clauseText
productname = encodeURI(this.resultData.productName);
} }
let fileTypesReg = new RegExp(/\.pdf$/)
if (fileTypesReg.test(url)) { function clickBottomBtn(name) {
// const valSrc = _.clone(this.urlMsg) let url = ''
// let msg = '' if (name === 'TKYW') {
// if (_rules.IsNull(valSrc)) return url = resultData.value.clauseText
// Object.keys(valSrc).map((item) => { }
// msg += `${item}=${valSrc[item]}&`
// return msg const fileTypesReg = /\.pdf$/
// }) if (fileTypesReg.test(url)) {
// msg = msg.substr(0, msg.length - 1) const systemType = Utils.getSystemType()
// if (url.indexOf('?') === -1) { const isMiniProgram = Utils.isMiniProgram()
// url += `?${msg}${!!productname ? `&productname=${productname}` : ''}` if ((systemType === 'android' || systemType === 'harmonyos') && isMiniProgram) {
// } else { router.push({
// url += `&${msg}${!!productname ? `&productname=${productname}` : ''}` name: 'previewPdf',
// } params: { url },
// 安卓小程序环境跳转预览页 })
let isMiniProgram = false; } else {
wx.miniProgram.getEnv((res) => { location.href = url
if (res.miniprogram) { }
isMiniProgram = true;
}
});
if((Utils.getSystemType() === 'android' || Utils.getSystemType() === 'harmonyos') && isMiniProgram){
this.$router.push({
name: 'previewPdf',
params: {
url: url
}
})
}else{
location.href = url;
} }
return;
} }
// console.log(url);
//非文档则跳转 function showTotalIntro() {
// location.href = url; showTotal.value = !showTotal.value
// openWebView(this.urlMsg.src, url, title, '5') exchangeButton.value = !exchangeButton.value
}, }
showTotalIntro() {
this.showTotal = !this.showTotal; function richCallAnswer(item) {
this.exchangeButton = !this.exchangeButton; const sendParmas = {
}, reqAndResType: '1',
//点击问题按钮 msgText: item.intent,
richCallAnswer(item) { intentId: item.intentId,
var that = this; msgSource: manualService.value,
// if(this.hasClicked){ questionType:
// return props.itemlist.reqAndResType == '02'
// } ? 5
// if(this.itemlist.cmd === onceClickCmd){ : props.itemlist.reqAndResType == '06'
// this.hasClicked = true ? 4
// } : props.itemlist.reqAndResType == '11'
let sendParmas = { ? 8
reqAndResType: "1", //1文本 2图片 3问提评价 : '',
msgText: item.intent, }
intentId: item.intentId,
msgSource: this.manualService, //1机器人 2人工 if (!isBtnsubmit.value) return
questionType: isBtnsubmit.value = false
this.itemlist.reqAndResType == "02" scrollTopBody()
? 5 store
: this.itemlist.reqAndResType == "06" ?.dispatch?.('sendingRobot', sendParmas)
? 4 .then(() => {
: this.itemlist.reqAndResType == "11" isBtnsubmit.value = true
? 8
: "",
};
if (that.isBtnsubmit) {
that.isBtnsubmit = false;
this.scrollTopBody();
this.$store
.dispatch("sendingRobot", sendParmas)
.then((res) => {
that.isBtnsubmit = true;
}) })
.catch((err) => { .catch((err) => {
that.isBtnsubmit = true; isBtnsubmit.value = true
console.log(err); console.log(err)
}); })
}
function scrollTopBody() {
setTimeout(() => {
try {
if (mescrollContent.value) {
mescrollContent.value.scrollTop = mescrollContent.value.scrollHeight || 0
}
} catch (error) {
console.log(error.message)
}
}, 300)
}
function showGraph(item) {
popUpFlag.value = true
nowPopType.value = 'tkyw'
detaillTitle.value = item.key
tkywValue.value = (item.tkywValue || '').replace(/(\n)/g, '<br/>')
}
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,
} }
}, },
scrollTopBody() { })
setTimeout(() => {
//滚动到底部
try {
this.mescrollContent.scrollTop =
this.mescrollContent.scrollHeight || 0;
} catch (error) {
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/>')
}
}
export default {
name,
data,
props,
created,
methods,
computed
}

@ -90,6 +90,7 @@ const manualService = inject('manualService', ref('1'))
const sendLoading = inject('sendLoading', ref(false)) const sendLoading = inject('sendLoading', ref(false))
const publicPath = import.meta.env.BASE_URL const publicPath = import.meta.env.BASE_URL
const desc = ref(null)
const showTotal = ref(true) const showTotal = ref(true)
const exchangeButton = ref(true) const exchangeButton = ref(true)
const showExchangeButton = ref(false) const showExchangeButton = ref(false)

@ -336,10 +336,16 @@ export const useAppStore = defineStore('app', () => {
function pushUserMessage(content, messageType = 'user', systemType = 'CARD') { function pushUserMessage(content, messageType = 'user', systemType = 'CARD') {
if (!content) return null if (!content) return null
const timestamp = Date.now()
const message = { const message = {
type: messageType, type: messageType,
system: systemType, system: systemType,
isSendSuccess: true, isSendSuccess: true,
msgText: content,
fromFlag: messageType === 'user' ? '1' : '2',
reqAndResType: '1',
timestamp,
VIEW_ID: 'msg' + timestamp,
message: [ message: [
{ {
value: '', value: '',
@ -347,7 +353,7 @@ export const useAppStore = defineStore('app', () => {
type: 'text', type: 'text',
options: { options: {
label: content, label: content,
readOnly: false, readOnly: messageType !== 'user',
placeholder: '', placeholder: '',
}, },
}, },
@ -364,6 +370,7 @@ export const useAppStore = defineStore('app', () => {
return ( return (
payload?.msgText ?? payload?.msgText ??
payload?.answer ?? payload?.answer ??
payload?.answerText ??
payload?.content ?? payload?.content ??
payload?.text ?? payload?.text ??
payload?.message?.[0]?.options?.label ?? payload?.message?.[0]?.options?.label ??
@ -388,12 +395,43 @@ export const useAppStore = defineStore('app', () => {
const fromFlag = typeof item === 'object' ? String(item.fromFlag || '') : '' const fromFlag = typeof item === 'object' ? String(item.fromFlag || '') : ''
const messageType = fromFlag === '1' ? 'user' : fromFlag === '2' ? 'bot' : fallbackType const messageType = fromFlag === '1' ? 'user' : fromFlag === '2' ? 'bot' : fallbackType
const resolvedFromFlag =
fromFlag || (messageType === 'user' ? '1' : messageType === 'bot' ? '2' : '')
const resolvedTimestamp =
(typeof item === 'object' && item.timestamp) || Date.now()
const hasReqType =
typeof item === 'object' && item.reqAndResType !== undefined && item.reqAndResType !== null && item.reqAndResType !== ''
const hasMsgType =
typeof item === 'object' && item.msgType !== undefined && item.msgType !== null && item.msgType !== ''
// chat-content 机器人文本分支依赖 reqAndResType=='01' && msgType=='01'
const resolvedReqAndResType = hasReqType
? item.reqAndResType
: imageUrl
? (messageType === 'user' ? '2' : '2')
: messageType === 'bot'
? '01'
: '1'
const resolvedMsgType = hasMsgType
? item.msgType
: imageUrl
? '02'
: '01'
const message = { const message = {
...(typeof item === 'object' ? item : {}),
type: messageType, type: messageType,
system: item?.system || 'LIICS', system: (typeof item === 'object' && item.system) || 'LIICS',
isSendSuccess: item?.isSendSuccess !== false, isSendSuccess: typeof item === 'object' ? item.isSendSuccess !== false : true,
imageUrl, imageUrl,
raw: item, raw: item,
// 兼容 chat-content:保留/补齐 liics 字段
msgText: content || (typeof item === 'object' ? item.msgText : '') || '',
fromFlag: resolvedFromFlag,
reqAndResType: resolvedReqAndResType,
msgType: resolvedMsgType,
timestamp: resolvedTimestamp,
imgUrl: (typeof item === 'object' && (item.imgUrl || item.imageUrl)) || imageUrl || '',
VIEW_ID:
(typeof item === 'object' && item.VIEW_ID) || ('msg' + resolvedTimestamp),
message: [ message: [
{ {
value: '', value: '',
@ -580,9 +618,13 @@ export const useAppStore = defineStore('app', () => {
// 消息列表 // 消息列表
function setChatList(payload) { function setChatList(payload) {
const normalizedItem = utils.normalizeMessage(payload) const list = Array.isArray(payload) ? payload : [payload]
chatList.value.push(normalizedItem) list.forEach((entry) => {
pushConversationMessage(normalizedItem, 'bot') if (entry == null) return
const normalizedItem = utils.normalizeMessage(entry)
chatList.value.push(normalizedItem)
pushConversationMessage(normalizedItem, 'bot')
})
} }
function delChatList(index) { function delChatList(index) {

@ -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,
}
})

@ -1,4 +1,4 @@
import { Get, Post } from './http.js' import { Get, Post } from './https.js'
import { useAppStore } from '@/store/app.js' import { useAppStore } from '@/store/app.js'
import CryptoJS from 'crypto-js' import CryptoJS from 'crypto-js'
@ -42,142 +42,6 @@ function getAppStore() {
return useAppStore() return useAppStore()
} }
// 创建会话
export function createSessions(data) {
const appStore = getAppStore()
// appStore.setUserInfo({ channel: data.channel, code: data.subject.code })
return Post('/yyfw/api/interactive-sessions', data).then((res) => {
if (res.data && res.data.token) {
sessionStorage.setItem('token', res.data.token)
console.log('Token stored in sessionStorage:', res.data.token)
}
return res
})
}
// 事件发送
export function getSessions() {
const appStore = getAppStore()
let data = {
eventId: `flow-start_${getTimestamp()}${Math.floor(Math.random() * 9000) + 1000}_0001`,
eventType: 'FLOW_START',
sessionId: appStore.sessionId,
idempotentKey: `flow-start-${getTimestamp()}${Math.floor(Math.random() * 9000) + 1000}`,
payload: {},
}
return Post(`/yyfw/api/interactive-sessions/${appStore.sessionId}/events`, data)
}
// 通用事件发送
export function eventFunction(payload, text) {
const appStore = getAppStore()
payload.cardType = appStore.cardType
// 存储 certNo 到 sessionStorage
if (payload.actionPayload && payload.actionPayload.certNo) {
sessionStorage.setItem('certNo', payload.actionPayload.certNo)
console.log('CertNo stored in sessionStorage:', payload.actionPayload.certNo)
}
let obj = {
eventId: `card_action_${getTimestamp()}${Math.floor(Math.random() * 9000) + 1000}_CARD_ACTION`,
eventType: 'CARD_ACTION',
channel: appStore.channel,
userId: appStore.userId,
currentStage: appStore.currentStage || 'SESSION_INIT',
currentNode: appStore.currentNode,
idempotentKey: appStore.idempotentKey,
occurredAt: getCurrentISOTime(),
payload: import.meta.env.VITE_USER_NODE_ENV === 'development' ? payload : undefined,
displayText: text,
}
// 判断是否需要加密
if (shouldEncrypt('CARD_ACTION')) {
console.log('加密前:', payload)
obj.encryptedPayload = encryptPayload(payload)
} else {
console.log('不加密:', payload)
obj.payload = payload
}
return Post(`/yyfw/api/interactive-sessions/${appStore.sessionId}/events`, obj)
}
// 事件重新发送
export function reEventFunction(obj) {
const appStore = getAppStore()
return Post(`/yyfw/api/interactive-sessions/${appStore.sessionId}/events`, obj)
}
// 图片事件发送
export function imgEventFunction(payload) {
const appStore = getAppStore()
payload.cardType = appStore.cardType
// 存储 certNo 到 sessionStorage
if (payload.actionPayload && payload.actionPayload.certNo) {
sessionStorage.setItem('certNo', payload.actionPayload.certNo)
console.log('CertNo stored in sessionStorage:', payload.actionPayload.certNo)
}
let obj = {
eventId: `page_action_${getTimestamp()}${Math.floor(Math.random() * 9000) + 1000}_PAGE_ACTION`,
eventType: 'PAGE_ACTION',
channel: appStore.channel,
userId: appStore.userId,
currentStage: appStore.currentStage || 'SESSION_INIT',
currentNode: appStore.currentNode,
idempotentKey: '',
occurredAt: getCurrentISOTime(),
payload: import.meta.env.VITE_USER_NODE_ENV === 'development' ? payload : undefined,
}
// 判断是否需要加密
if (shouldEncrypt('CARD_ACTION')) {
console.log('加密前:', payload)
obj.encryptedPayload = encryptPayload(payload)
} else {
console.log('不加密:', payload)
obj.payload = payload
}
return Post(`/yyfw/api/interactive-sessions/${appStore.sessionId}/events`, obj)
}
// page事件发送
export function eventPageFunction(payload, text) {
const appStore = getAppStore()
payload.cardType = appStore.cardType
// 存储 certNo 到 sessionStorage
if (payload.actionPayload && payload.actionPayload.certNo) {
sessionStorage.setItem('certNo', payload.actionPayload.certNo)
console.log('CertNo stored in sessionStorage:', payload.actionPayload.certNo)
}
let obj = {
eventId: `page_action_${getTimestamp()}${Math.floor(Math.random() * 9000) + 1000}_PAGE_ACTION`,
eventType: 'PAGE_ACTION',
channel: appStore.channel,
userId: appStore.userId,
currentStage: appStore.currentStage || 'SESSION_INIT',
currentNode: appStore.currentNode,
idempotentKey: '',
occurredAt: getCurrentISOTime(),
payload: import.meta.env.VITE_USER_NODE_ENV === 'development' ? payload : undefined,
displayText: text,
}
// 判断是否需要加密
if (shouldEncrypt('PAGE_ACTION')) {
console.log('加密前:', payload)
obj.encryptedPayload = encryptPayload(payload)
} else {
console.log('不加密:', payload)
obj.payload = payload
}
return Post(`/yyfw/api/interactive-sessions/${appStore.sessionId}/events`, obj)
}
// 用户输入:统一复用 liics 迁移后的发送动作。 // 用户输入:统一复用 liics 迁移后的发送动作。
// 该动作在机器人模式下调用 /liics_api/semantic/qaChat,人工模式下走原有 WebSocket。 // 该动作在机器人模式下调用 /liics_api/semantic/qaChat,人工模式下走原有 WebSocket。
export function messageSend(text, options = {}) { export function messageSend(text, options = {}) {
@ -189,45 +53,3 @@ export function messageSend(text, options = {}) {
quoteData: options.quoteData || null, quoteData: options.quoteData || null,
}) })
} }
// 获取出险人选择列表(userStr 由 SSE 下发)
export function getInsuredList(userStr) {
return Get(`/yyfw/api/select/query`, { userStr })
}
// 获取医院列表
export function getHospitals(text) {
const appStore = getAppStore()
return Get(`/yyfw/api/accident/hospitals?keyword=${text}&sfFlag=${appStore.sfFlag}`)
}
// 根据录入的意外类型获取意外原因(点击意外原因调用)
export function getAccidentReasons(remark) {
if (remark) {
return Get(`/yyfw/api/accident/accidentReasons?remark=${remark}`)
} else {
return Get(`/yyfw/api/accident/accidentReasons`)
}
}
// 校验事故日期
export function checkDate(data) {
return Post(`/yyfw/api/accident/checkDate`, data)
}
// 离开项目埋点
export function leaveProject() {
const appStore = getAppStore()
return Post(`/yyfw/api/interactive-sessions/${appStore.sessionId}/exit`, {})
}
// 获取服务器时钟
export function getServerTime() {
return Get(`/yyfw/api/interactive-sessions/server-time`, {})
}
// 上报网络耗时监控数据
export function reportNetworkMetrics(data) {
return Post(`/yyfw/api/interactive-sessions/network-metrics`, data)
}
// 查询协议
export function queryProtocol() {
return Get(`/yyfw/api/authprotocol/query`, {})
}

@ -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

@ -6,61 +6,62 @@
* 例子: * 例子:
* (new Date()).Format("yyyy-MM-dd hh:mm:ss.S") ==> 2006-07-02 08:09:04.423 * (new Date()).Format("yyyy-MM-dd hh:mm:ss.S") ==> 2006-07-02 08:09:04.423
* (new Date()).Format("yyyy-M-d h:m:s.S") ==> 2006-7-2 8:9:4.18 * (new Date()).Format("yyyy-M-d h:m:s.S") ==> 2006-7-2 8:9:4.18
* item.timestamp | FormatDate('yyyy-MM-dd hh:mm:ss') * FormatDate(item.timestamp, 'yyyy-MM-dd hh:mm:ss')
* //author: guhaibin * //author: guhaibin
*/ */
const _filters = {} export function FormatDate(_date, fmt) {
_filters.FormatDate = function (_date, fmt) { if (!_date) {
if (!_date) { return ''
return ''; }
}
if (typeof (fmt) === "undefined") { if (typeof fmt === 'undefined') {
fmt = "yyyy-MM-dd HH:mm:ss"; fmt = 'yyyy-MM-dd HH:mm:ss'
} }
var date = new Date(_date); var date = new Date(_date)
var o = { var o = {
"M+": date.getMonth() + 1, //月份 'M+': date.getMonth() + 1, //月份
"d+": date.getDate(), //日 'd+': date.getDate(), //日
"h+": date.getHours(), //小时 'h+': date.getHours(), //小时
"m+": date.getMinutes(), //分 'm+': date.getMinutes(), //分
"s+": date.getSeconds(), //秒 's+': date.getSeconds(), //秒
"q+": Math.floor((date.getMonth() + 3) / 3), //季度 'q+': Math.floor((date.getMonth() + 3) / 3), //季度
"S": date.getMilliseconds() //毫秒 S: date.getMilliseconds(), //毫秒
}; }
if (/(y+)/.test(fmt)) if (/(y+)/.test(fmt))
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length)); fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
for (var k in o) for (var k in o)
if (new RegExp("(" + k + ")").test(fmt)) if (new RegExp('(' + k + ')').test(fmt))
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); fmt = fmt.replace(
return fmt; RegExp.$1,
RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length),
)
return fmt
} }
//时间格式化 //时间格式化
Date.prototype.format = function (fmt) { Date.prototype.format = function (fmt) {
var o = { var o = {
"M+": this.getMonth() + 1, //月份 'M+': this.getMonth() + 1, //月份
"d+": this.getDate(), //日 'd+': this.getDate(), //日
"h+": this.getHours(), //小时 'h+': this.getHours(), //小时
"m+": this.getMinutes(), //分 'm+': this.getMinutes(), //分
"s+": this.getSeconds(), //秒 's+': this.getSeconds(), //秒
"q+": Math.floor((this.getMonth() + 3) / 3), //季度 'q+': Math.floor((this.getMonth() + 3) / 3), //季度
"S": this.getMilliseconds() //毫秒 S: this.getMilliseconds(), //毫秒
}; }
if (/(y+)/.test(fmt)) { if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length)); 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),
)
} }
for (var k in o) { }
if (new RegExp("(" + k + ")").test(fmt)) { return fmt
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
}
}
return fmt;
} }
export default (Vue) => { export default FormatDate
Object.keys(_filters).forEach(key => {
Vue.filter(key, _filters[key])
})
}

@ -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);
});
});
}

@ -374,7 +374,7 @@ export function getHost() {
export function Encrypt(Okey, Oiv, payload) { export function Encrypt(Okey, Oiv, payload) {
const key = CryptoJS.enc.Utf8.parse(Okey); const key = CryptoJS.enc.Utf8.parse(Okey);
const iv = CryptoJS.enc.Utf8.parse(Oiv); const iv = CryptoJS.enc.Utf8.parse(Oiv);
let srcs = srcs = CryptoJS.enc.Utf8.parse(payload) let srcs = CryptoJS.enc.Utf8.parse(payload)
const encrypted = CryptoJS.AES.encrypt(srcs, key, { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }) const encrypted = CryptoJS.AES.encrypt(srcs, key, { iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 })
const base64str = CryptoJS.enc.Base64.stringify(encrypted.ciphertext) const base64str = CryptoJS.enc.Base64.stringify(encrypted.ciphertext)
// return encrypted.ciphertext.toString().toUpperCase(); // return encrypted.ciphertext.toString().toUpperCase();

Loading…
Cancel
Save