没冲突的文件先提交

master
jiangjunjie 2 weeks ago
parent 497c5196f8
commit 731781b19a
  1. 2
      src/components/chat-main.vue
  2. 67
      src/components/quick-access.vue
  3. 153
      src/components/upload-image.vue
  4. 28
      src/utils/api.js
  5. 73
      src/utils/config.js
  6. 29
      src/utils/conversationApi.js
  7. 9
      src/utils/imageService.js
  8. 27
      src/utils/socket.js
  9. 2
      src/utils/sseService.js
  10. 5
      vite.config.js

@ -1,6 +1,6 @@
<template> <template>
<div class="chat wrapper"> <div class="chat wrapper">
<nav-bar :isWX="isWX" :tokenExist="tokenExist" @isNoticeHandle="isNoticeFn"></nav-bar> <!-- <nav-bar :isWX="isWX" :tokenExist="tokenExist" @isNoticeHandle="isNoticeFn"></nav-bar> -->
<!-- 内容部分 --> <!-- 内容部分 -->
<div class="app-content" @touchend.stop="handleAppCont" ref="appContent" :class="elder ? 'elder-content' : ''"> <div class="app-content" @touchend.stop="handleAppCont" ref="appContent" :class="elder ? 'elder-content' : ''">
<!--mescroll滚动区域的基本结构,为避免id重复导致的多次初始化,这里使用ref--> <!--mescroll滚动区域的基本结构,为避免id重复导致的多次初始化,这里使用ref-->

@ -1,21 +1,12 @@
<template> <template>
<div <div class="quick-access-box" :class="$elder ? 'elder-wrapper' : ''"
class="quick-access-box" v-if="quickData.activityList && quickData.activityList.length > 0">
:class="$elder ? 'elder-wrapper' : ''"
v-if="quickData.activityList && quickData.activityList.length > 0"
>
<div class="quick-btn-box an-move-left"> <div class="quick-btn-box an-move-left">
<a <a class="btn-item opacity" href="javascript:;" v-for="(item, index) in normalActivityList" :key="index"
class="btn-item opacity" @click.stop="quickBtnClick(item)" v-saclick="{
href="javascript:;"
v-for="(item, index) in normalActivityList"
:key="index"
@click.stop="quickBtnClick(item)"
v-saclick="{
clickName: 'quick_access', clickName: 'quick_access',
clickData: Object.assign({ buttonName: item.name }, saData), clickData: Object.assign({ buttonName: item.name }, saData),
}" }">
>
<!-- 开放标签 --> <!-- 开放标签 -->
<!-- <wx-open-launch-weapp <!-- <wx-open-launch-weapp
@click.stop="quickBtnClick(item)" @click.stop="quickBtnClick(item)"
@ -32,33 +23,19 @@
<button class="btn"></button> <button class="btn"></button>
</script> </script>
</wx-open-launch-weapp> --> </wx-open-launch-weapp> -->
<div <div class="btn-icon" :data-type="item.category" :data-url="item.url" :data-params="item.funcData">
class="btn-icon"
:data-type="item.category"
:data-url="item.url"
:data-params="item.funcData"
>
<img class="quick_btn_icon" v-lazy="getIconUrl(item.content)" :alt="item.name" /> <img class="quick_btn_icon" v-lazy="getIconUrl(item.content)" :alt="item.name" />
</div> </div>
<div class="btn-name">{{ item.name }}</div> <div class="btn-name">{{ item.name }}</div>
</a> </a>
<a <a v-if="$elder" class="btn-item opacity" href="javascript:;" @click.stop="fixedBtnClick($event, 0)">
v-if="$elder"
class="btn-item opacity"
href="javascript:;"
@click.stop="fixedBtnClick($event, 0)"
>
<div class="btn-icon"> <div class="btn-icon">
<img class="quick_btn_icon" src="../assets/images/human-guider.png" /> <img class="quick_btn_icon" src="../assets/images/human-guider.png" />
</div> </div>
<div class="btn-name fixed-name">人工客服</div> <div class="btn-name fixed-name">人工客服</div>
</a> </a>
<a <a v-if="$elder && source != '95290000'" class="btn-item opacity" href="javascript:;"
v-if="$elder && source != '95290000'" @click.stop="fixedBtnClick($event, 1)">
class="btn-item opacity"
href="javascript:;"
@click.stop="fixedBtnClick($event, 1)"
>
<div class="btn-icon"> <div class="btn-icon">
<img class="quick_btn_icon" src="../assets/images/tel-guider.png" /> <img class="quick_btn_icon" src="../assets/images/tel-guider.png" />
</div> </div>
@ -67,27 +44,13 @@
</div> </div>
<!-- 第二排特殊快捷入口 --> <!-- 第二排特殊快捷入口 -->
<div <div class="special-quick-btn-box an-move-left" v-if="specialActivityList && specialActivityList.length > 0">
class="special-quick-btn-box an-move-left" <a class="btn-item opacity" href="javascript:;" v-for="(item, index) in specialActivityList"
v-if="specialActivityList && specialActivityList.length > 0" :key="'special-' + index" @click.stop="specialBtnClick(item)" v-saclick="{
>
<a
class="btn-item opacity"
href="javascript:;"
v-for="(item, index) in specialActivityList"
:key="'special-' + index"
@click.stop="specialBtnClick(item)"
v-saclick="{
clickName: 'special_quick_access', clickName: 'special_quick_access',
clickData: Object.assign({ buttonName: item.name }, saData), clickData: Object.assign({ buttonName: item.name }, saData),
}" }">
> <div class="btn-icon" :data-type="item.category" :data-url="item.url" :data-params="item.funcData">
<div
class="btn-icon"
:data-type="item.category"
:data-url="item.url"
:data-params="item.funcData"
>
<img class="quick_btn_icon" v-lazy="getIconUrl(item.content)" :alt="item.name" /> <img class="quick_btn_icon" v-lazy="getIconUrl(item.content)" :alt="item.name" />
</div> </div>
<div class="btn-name">{{ item.name }}</div> <div class="btn-name">{{ item.name }}</div>
@ -97,7 +60,7 @@
</template> </template>
<script setup> <script setup>
import { ref, inject, onMounted } from 'vue' import { ref, inject, onMounted, computed } from 'vue'
import { showToast, showLoadingToast, closeToast } from 'vant' import { showToast, showLoadingToast, closeToast } from 'vant'
import utils from '@/utils/utils' import utils from '@/utils/utils'
import { emitter } from '@/utils/mitt' import { emitter } from '@/utils/mitt'

@ -1,33 +1,146 @@
<template>
<input
ref="fileInput"
class="file-input"
type="file"
accept="image/jpeg,image/png,image/gif,image/webp"
@change="handleFileChange"
/>
</template>
<script setup> <script setup>
import { ref, inject, onMounted } from 'vue' import { ref, inject, unref } from 'vue'
import { showToast } from 'vant' import { showToast } from 'vant'
import utils from '@/utils/utils'
import { compressImage } from '@/utils/CompressImage' import { compressImage } from '@/utils/CompressImage'
import { Post } from '@/utils/https' import { upLoadImgHttp } from '@/utils/https'
const emit = defineEmits(['image-selected', 'image-sent', 'image-failed'])
const store = inject('appStore', null)
const userId = inject('userId', ref('')) const userId = inject('userId', ref(''))
const token = inject('token', ref(''))
const manualService = inject('manualService', ref('1')) const manualService = inject('manualService', ref('1'))
const sessionId = inject('sessionId', ref('')) const sessionId = inject('sessionId', ref(''))
const source = inject('source', ref('')) const source = inject('source', ref(''))
const mescrollContent = inject('mescrollContent', null)
const elder = inject('elder', false)
const upImage = ref(true) const fileInput = ref(null)
const showImage = ref(false) const uploading = ref(false)
const srcImg = ref('')
const isWxAndroid = ref(false) const MAX_IMAGE_SIZE = 5 * 1024 * 1024
const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
function openFilePicker() {
if (uploading.value) {
showToast('图片正在发送,请稍候')
return
}
fileInput.value?.click()
}
function readAsDataUrl(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result)
reader.onerror = reject
reader.readAsDataURL(file)
})
}
async function getUploadFile(file, dataUrl) {
try {
const result = await compressImage(dataUrl, {
file: { file },
quality: 0.8,
maxWidth: 1600,
maxHeight: 1600,
})
if (!result?.blob || result.blob.size >= file.size) return file
return new File([result.blob], file.name, {
type: result.blob.type || file.type,
lastModified: Date.now(),
})
} catch (error) {
console.log('图片压缩失败,使用原图上传', error)
return file
}
}
function getResponseData(response) {
let data = response?.data ?? response
if (typeof data === 'string') {
try {
data = JSON.parse(data)
} catch (error) {
return { value: data }
}
}
return data?.data ?? data ?? {}
}
function isBusinessFailure(response) {
if (response?.success === false) return true
const code = Number(response?.code)
return Number.isFinite(code) && (code < 0 || code >= 400)
}
async function handleFileChange(event) {
const input = event.target
const file = input.files?.[0]
input.value = ''
if (!file || uploading.value) return
if (!ALLOWED_IMAGE_TYPES.includes(file.type)) {
showToast('请选择 JPG、PNG、GIF 或 WebP 图片')
return
}
if (file.size > MAX_IMAGE_SIZE) {
showToast('图片大小不能超过5M')
return
}
const localId = `image_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
let previewUrl = ''
try {
previewUrl = await readAsDataUrl(file)
emit('image-selected', { localId, imageUrl: previewUrl, fileName: file.name })
uploading.value = true
const uploadFile = await getUploadFile(file, previewUrl)
const formData = new FormData()
formData.append('file', uploadFile, file.name)
formData.append('fromFlag', '1')
formData.append('userId', unref(userId) || '')
formData.append('sessionId', unref(sessionId) || '')
formData.append('source', unref(source) || '')
formData.append('reqAndResType', '2')
formData.append('msgSource', String(unref(manualService) || '1'))
formData.append('timestamp', String(Date.now()))
onMounted(() => { const response = await upLoadImgHttp('FileUpLoad/uploadImgAndVideo', formData)
if (utils.isWeiXin() && utils.isAndroid() === 'isAndroid') { if (isBusinessFailure(response)) {
isWxAndroid.value = true throw new Error(response?.msg || '图片发送失败')
}
const data = getResponseData(response)
emit('image-sent', {
localId,
imageUrl: data?.imgUrl || data?.url || previewUrl,
imageKey: data?.imgKey || data?.fileKey || data?.key || '',
response,
})
} catch (error) {
console.log('图片发送失败', error)
if (previewUrl) emit('image-failed', { localId })
showToast(error?.message || '图片发送失败,请稍后重试')
} finally {
uploading.value = false
} }
}) }
// ... 方法中: defineExpose({ openFilePicker })
// this.$store.commit('set_sendLoading') → store?.set_sendLoading?.(value)
// this.$post('FileUpLoad/uploadImgAndVideo') → Post('FileUpLoad/uploadImgAndVideo', ...)
// this.mescrollContent → mescrollContent.value
// this.$elder → elder
</script> </script>
<style scoped>
.file-input {
display: none;
}
</style>

@ -178,26 +178,16 @@ export function eventPageFunction(payload, text) {
return Post(`/yyfw/api/interactive-sessions/${appStore.sessionId}/events`, obj) return Post(`/yyfw/api/interactive-sessions/${appStore.sessionId}/events`, obj)
} }
// 用户输入 // 用户输入:统一复用 liics 迁移后的发送动作。
export function messageSend(text) { // 该动作在机器人模式下调用 /liics_api/semantic/qaChat,人工模式下走原有 WebSocket。
export function messageSend(text, options = {}) {
const appStore = getAppStore() const appStore = getAppStore()
return appStore.sendingRobot({
let obj = { reqAndResType: '1',
eventId: `card_action_${getTimestamp()}${Math.floor(Math.random() * 9000) + 1000}_USER_TEXT`, msgText: text,
eventType: 'USER_TEXT', msgSource: appStore.manualService,
channel: appStore.channel, quoteData: options.quoteData || null,
userId: appStore.userId, })
currentStage: appStore.currentStage || 'SESSION_INIT',
currentNode: appStore.currentNode,
idempotentKey: '',
payload: {
text,
},
occurredAt: getCurrentISOTime(),
displayText: text,
}
return Post(`/yyfw/api/interactive-sessions/${appStore.sessionId}/events`, obj)
} }
// 获取出险人选择列表(userStr 由 SSE 下发) // 获取出险人选择列表(userStr 由 SSE 下发)

@ -1,64 +1,17 @@
let curWwwPath = window.location.href; const currentUrl = new URL(window.location.href)
let pathName = ''; // window.location.pathname const host = currentUrl.host
let pos = curWwwPath.indexOf(pathName); const protocol = currentUrl.protocol
let host = window.location.host; const isWs = protocol === 'https:' ? 'wss' : 'ws'
let protocol = window.location.protocol;
let localhostPath = curWwwPath // downSource.substring(0, pos);
let projectName = pathName.substring(0, pathName.substr(1).indexOf('/') + 1);
let isWs = protocol == 'https:' ? 'wss' : 'ws';
let baseUrl = ""; // liics 的 HTTP/WebSocket 服务与页面使用同一域名;本地开发也由 Vite
let WSURL = ''; // 直接代理 /liics_api,因此浏览器里看到的请求路径在各环境保持一致。
let envType = ''; const config = {
baseUrl: '/liics_api/',
//开发环境 WSURL: `${isWs}://${host}/liics_api/webSocket/`,
if(localhostPath.indexOf("localhost") >=0 || localhostPath.indexOf("172.16") >=0){ envType: import.meta.env.DEV ? 'dev' : 'production',
baseUrl = '/api'; BASEURL: currentUrl.origin,
WSURL = `${isWs}://${host}/api/liics_api/webSocket/`; //ws://ssp-dev.trace.com.cn/liics_api/webSocket/
envType = 'dev';
//测试环境
}else if(
localhostPath.includes("-dev") ||
localhostPath.includes("-sit") ||
localhostPath.includes("-uat") ||
localhostPath.includes("-pt")
){
baseUrl = localhostPath;
WSURL = `${isWs}://${host}/liics_api/webSocket/`; //ws://ssp-dev.trace.com.cn/liics_api/webSocket/
envType = 'sit';
//生产环境
}else{
baseUrl = localhostPath;
WSURL = `${isWs}://${host}/liics_api/webSocket/`; //ws://ssp-dev.trace.com.cn/liics_api/webSocket/
envType = 'production';
} }
console.log('baseUrl', baseUrl, localhostPath, window.location.href, window.location.pathname);
// switch (process.env.NODE_ENV) {
// case 'development':
// baseUrl = process.env.VUE_APP_BASE_API //这里是本地的请求url
// WSURL = process.env.VUE_APP_WEBSOCKET_URL;
// break
// case 'sit': // 注意这里的名字要和步骤二中设置的环境名字对应起来
// baseUrl = process.env.VUE_APP_BASE_API //这里是测试环境中的url
// WSURL = process.env.VUE_APP_WEBSOCKET_URL;
// break
// case 'production':
// baseUrl = process.env.VUE_APP_BASE_API //生产环境url
// WSURL = process.env.VUE_APP_WEBSOCKET_URL;
// break
// default:
// baseUrl = process.env.VUE_APP_BASE_API //生产环境url
// WSURL = process.env.VUE_APP_WEBSOCKET_URL;
// }
console.log('liics api config', config)
let config = { export default config
baseUrl: `${baseUrl}/liics_api/`,//后台请求链接
WSURL: WSURL,//websocket链接
envType: envType,//环境类型
BASEURL: baseUrl,
}
export default config;

@ -0,0 +1,29 @@
import { fetchPost, upLoadImgHttp } from '@/utils/https'
// The legacy liics chat used a small set of HTTP endpoints. Keeping those
// calls here gives the Vue 3 store a single transport boundary and preserves
// the payload contract expected by the existing chat components.
export const postConversation = (url, data = {}) => fetchPost(url, data)
export const sendRobotMessage = (payload) => postConversation('semantic/qaChat', payload)
export const sendVoiceMessage = (payload) => postConversation('upload/hxVoice', payload)
export const uploadConversationImage = (formData) => upLoadImgHttp('upload/image', formData)
export const connectHumanService = (payload) => postConversation('user/connectToEasemob', payload)
export const updateReadMessages = (payload) => postConversation('HumanChat/msgCallBack', payload)
export const submitUserBehavior = (payload) => postConversation('user/interactive', payload)
export const submitSatisfaction = (payload) => postConversation('semantic/savePaperResult', payload)
export const loadSystemConfig = (payload) => postConversation('config/sysConfig', payload)
export const loadHeaderAvatar = (payload) => postConversation('config/avatar', payload)
export const loadTipsConfig = (payload) =>
postConversation('insuranceQuery/longTermRiskConfigValue', payload)
export const getLegacyApi = (url, params) => postConversation(url, params)

@ -4,7 +4,9 @@
*/ */
import { fetchImageData, base64ToBlob } from './https' import { fetchImageData, base64ToBlob } from './https'
import store from '@/store' import { useAppStore } from '@/store/app.js'
const getAppStore = () => useAppStore()
class ImageCacheManager { class ImageCacheManager {
constructor(maxSize = 100) { constructor(maxSize = 100) {
@ -184,7 +186,7 @@ class ImageService {
const imageData = await fetchImageData('config/getFile', { const imageData = await fetchImageData('config/getFile', {
filekey: key, filekey: key,
type: 'img', type: 'img',
userInfoId: store.state.userId || '' userInfoId: getAppStore().userId || ''
}, { }, {
timeout timeout
}) })
@ -368,8 +370,7 @@ class ImageService {
*/ */
_getToken() { _getToken() {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const store = require('@/store').default return getAppStore().token
return store.state.token
} }
return '' return ''
} }

@ -1,13 +1,28 @@
import config from './config' import config from './config'
import { Toast, Dialog } from 'vant' import { showToast, showDialog, showConfirmDialog } from 'vant'
import store from '@/store' import { useAppStore } from '@/store/app.js'
import utils from './utils' import utils from './utils'
// Keep the legacy socket implementation independent from Vuex. The proxy
// exposes the old state/commit/dispatch shape while reading the live Pinia
// store instance for every call.
const store = {
get state() {
return useAppStore().state
},
commit(type, payload) {
return useAppStore().commit(type, payload)
},
dispatch(type, payload) {
return useAppStore().dispatch(type, payload)
},
}
/** /**
* 提示函数 * 提示函数
* 禁止点击蒙层、显示3秒后关闭 * 禁止点击蒙层、显示3秒后关闭
*/ */
const toast = msg => { const toast = msg => {
Toast({ showToast({
message: msg, message: msg,
duration: 5000, duration: 5000,
position: 'top', position: 'top',
@ -79,7 +94,7 @@ let wsConnection = {
// this.$ws = new MozWebSocket(this.WSS_URL + userId + "," + sessionId); // this.$ws = new MozWebSocket(this.WSS_URL + userId + "," + sessionId);
this.$ws = new MozWebSocket(`${this.WSS_URL}${userId}&${encodeURIComponent(checkParam)}`); this.$ws = new MozWebSocket(`${this.WSS_URL}${userId}&${encodeURIComponent(checkParam)}`);
} else { } else {
Dialog.alert({ showDialog({
message: '您的浏览器不支持websocket协议,建议您使用Android系统版本大于4.0,ios系统版本大于7.0。' message: '您的浏览器不支持websocket协议,建议您使用Android系统版本大于4.0,ios系统版本大于7.0。'
}).then(() => { }).then(() => {
utils.closeWindow(); //关闭页面 utils.closeWindow(); //关闭页面
@ -180,7 +195,7 @@ let wsConnection = {
return return
} }
Dialog.confirm({ showConfirmDialog({
title: '提示', title: '提示',
message: '当前会话已结束,请刷新页面或关闭页面重新进入', message: '当前会话已结束,请刷新页面或关闭页面重新进入',
confirmButtonText: '取消', confirmButtonText: '取消',
@ -308,7 +323,7 @@ let wsConnection = {
}, 4000); }, 4000);
} else { } else {
// toast('重连次数超出设定值,不再重连,请检查配置项'); // toast('重连次数超出设定值,不再重连,请检查配置项');
Dialog.alert({ showDialog({
message: '重连次数超出设定值,不再重连。请检查您的网络设置,点击“确定”刷新页面,重新建立会话。' message: '重连次数超出设定值,不再重连。请检查您的网络设置,点击“确定”刷新页面,重新建立会话。'
}).then(() => { }).then(() => {
// window.location.reload(); // window.location.reload();

@ -1,7 +1,7 @@
import CryptoJS from 'crypto-js' import CryptoJS from 'crypto-js'
import pinia from '@/store' import pinia from '@/store'
import { useAppStore } from '@/store/app.js' import { useAppStore } from '@/store/app.js'
const appStore = useAppStore() const appStore = useAppStore(pinia)
// AES 密钥(与 api.js 保持一致,从环境变量读取) // AES 密钥(与 api.js 保持一致,从环境变量读取)
const AES_KEY = import.meta.env.VITE_AES_KEY || '3a7b9c2d5e8f1a4b6c9d8e7f5a3b2c1d' const AES_KEY = import.meta.env.VITE_AES_KEY || '3a7b9c2d5e8f1a4b6c9d8e7f5a3b2c1d'

@ -14,6 +14,11 @@ export default defineConfig(({ command }) => {
base: "./", base: "./",
server: { server: {
proxy: { proxy: {
'/liics_api': {
target: 'https://ssp-sit.cpic.com.cn',
changeOrigin: true,
ws: true,
},
'/api': { '/api': {
// target: 'https://aioblu-sit.cpic.com.cn', // target: 'https://aioblu-sit.cpic.com.cn',
// target: 'https://aioblu-dev.cpic.com.cn/yyfw', // target: 'https://aioblu-dev.cpic.com.cn/yyfw',

Loading…
Cancel
Save