没冲突的文件先提交

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>
<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' : ''">
<!--mescroll滚动区域的基本结构,为避免id重复导致的多次初始化,这里使用ref-->

@ -1,21 +1,12 @@
<template>
<div
class="quick-access-box"
:class="$elder ? 'elder-wrapper' : ''"
v-if="quickData.activityList && quickData.activityList.length > 0"
>
<div class="quick-access-box" :class="$elder ? 'elder-wrapper' : ''"
v-if="quickData.activityList && quickData.activityList.length > 0">
<div class="quick-btn-box an-move-left">
<a
class="btn-item opacity"
href="javascript:;"
v-for="(item, index) in normalActivityList"
:key="index"
@click.stop="quickBtnClick(item)"
v-saclick="{
<a class="btn-item opacity" href="javascript:;" v-for="(item, index) in normalActivityList" :key="index"
@click.stop="quickBtnClick(item)" v-saclick="{
clickName: 'quick_access',
clickData: Object.assign({ buttonName: item.name }, saData),
}"
>
}">
<!-- 开放标签 -->
<!-- <wx-open-launch-weapp
@click.stop="quickBtnClick(item)"
@ -32,33 +23,19 @@
<button class="btn"></button>
</script>
</wx-open-launch-weapp> -->
<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" />
</div>
<div class="btn-name">{{ item.name }}</div>
</a>
<a
v-if="$elder"
class="btn-item opacity"
href="javascript:;"
@click.stop="fixedBtnClick($event, 0)"
>
<a v-if="$elder" class="btn-item opacity" href="javascript:;" @click.stop="fixedBtnClick($event, 0)">
<div class="btn-icon">
<img class="quick_btn_icon" src="../assets/images/human-guider.png" />
</div>
<div class="btn-name fixed-name">人工客服</div>
</a>
<a
v-if="$elder && source != '95290000'"
class="btn-item opacity"
href="javascript:;"
@click.stop="fixedBtnClick($event, 1)"
>
<a v-if="$elder && source != '95290000'" class="btn-item opacity" href="javascript:;"
@click.stop="fixedBtnClick($event, 1)">
<div class="btn-icon">
<img class="quick_btn_icon" src="../assets/images/tel-guider.png" />
</div>
@ -67,27 +44,13 @@
</div>
<!-- 第二排特殊快捷入口 -->
<div
class="special-quick-btn-box an-move-left"
v-if="specialActivityList && specialActivityList.length > 0"
>
<a
class="btn-item opacity"
href="javascript:;"
v-for="(item, index) in specialActivityList"
:key="'special-' + index"
@click.stop="specialBtnClick(item)"
v-saclick="{
<div class="special-quick-btn-box an-move-left" v-if="specialActivityList && specialActivityList.length > 0">
<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',
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" />
</div>
<div class="btn-name">{{ item.name }}</div>
@ -97,7 +60,7 @@
</template>
<script setup>
import { ref, inject, onMounted } from 'vue'
import { ref, inject, onMounted, computed } from 'vue'
import { showToast, showLoadingToast, closeToast } from 'vant'
import utils from '@/utils/utils'
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>
import { ref, inject, onMounted } from 'vue'
import { ref, inject, unref } from 'vue'
import { showToast } from 'vant'
import utils from '@/utils/utils'
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 token = inject('token', ref(''))
const manualService = inject('manualService', ref('1'))
const sessionId = inject('sessionId', ref(''))
const source = inject('source', ref(''))
const mescrollContent = inject('mescrollContent', null)
const elder = inject('elder', false)
const upImage = ref(true)
const showImage = ref(false)
const srcImg = ref('')
const isWxAndroid = ref(false)
const fileInput = ref(null)
const uploading = 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 = ''
onMounted(() => {
if (utils.isWeiXin() && utils.isAndroid() === 'isAndroid') {
isWxAndroid.value = true
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
}
})
// ... 方法中:
// this.$store.commit('set_sendLoading') → store?.set_sendLoading?.(value)
// this.$post('FileUpLoad/uploadImgAndVideo') → Post('FileUpLoad/uploadImgAndVideo', ...)
// this.mescrollContent → mescrollContent.value
// this.$elder → elder
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()))
const response = await upLoadImgHttp('FileUpLoad/uploadImgAndVideo', formData)
if (isBusinessFailure(response)) {
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 })
</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)
}
// 用户输入
export function messageSend(text) {
// 用户输入:统一复用 liics 迁移后的发送动作。
// 该动作在机器人模式下调用 /liics_api/semantic/qaChat,人工模式下走原有 WebSocket。
export function messageSend(text, options = {}) {
const appStore = getAppStore()
let obj = {
eventId: `card_action_${getTimestamp()}${Math.floor(Math.random() * 9000) + 1000}_USER_TEXT`,
eventType: 'USER_TEXT',
channel: appStore.channel,
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)
return appStore.sendingRobot({
reqAndResType: '1',
msgText: text,
msgSource: appStore.manualService,
quoteData: options.quoteData || null,
})
}
// 获取出险人选择列表(userStr 由 SSE 下发)

@ -1,64 +1,17 @@
let curWwwPath = window.location.href;
let pathName = ''; // window.location.pathname
let pos = curWwwPath.indexOf(pathName);
let host = window.location.host;
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';
const currentUrl = new URL(window.location.href)
const host = currentUrl.host
const protocol = currentUrl.protocol
const isWs = protocol === 'https:' ? 'wss' : 'ws'
let baseUrl = "";
let WSURL = '';
let envType = '';
//开发环境
if(localhostPath.indexOf("localhost") >=0 || localhostPath.indexOf("172.16") >=0){
baseUrl = '/api';
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';
// liics 的 HTTP/WebSocket 服务与页面使用同一域名;本地开发也由 Vite
// 直接代理 /liics_api,因此浏览器里看到的请求路径在各环境保持一致。
const config = {
baseUrl: '/liics_api/',
WSURL: `${isWs}://${host}/liics_api/webSocket/`,
envType: import.meta.env.DEV ? 'dev' : 'production',
BASEURL: currentUrl.origin,
}
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 = {
baseUrl: `${baseUrl}/liics_api/`,//后台请求链接
WSURL: WSURL,//websocket链接
envType: envType,//环境类型
BASEURL: baseUrl,
}
export default config;
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 store from '@/store'
import { useAppStore } from '@/store/app.js'
const getAppStore = () => useAppStore()
class ImageCacheManager {
constructor(maxSize = 100) {
@ -184,7 +186,7 @@ class ImageService {
const imageData = await fetchImageData('config/getFile', {
filekey: key,
type: 'img',
userInfoId: store.state.userId || ''
userInfoId: getAppStore().userId || ''
}, {
timeout
})
@ -368,8 +370,7 @@ class ImageService {
*/
_getToken() {
if (typeof window !== 'undefined') {
const store = require('@/store').default
return store.state.token
return getAppStore().token
}
return ''
}

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

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

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

Loading…
Cancel
Save