发送按钮样式优化和代码联调

master
wujingling0908 2 weeks ago
parent f22713668a
commit 3f88043322
  1. 24
      src/App.vue
  2. 1
      src/assets/images/chat/todo-icon.svg
  3. 8
      src/assets/style/common.css
  4. 87
      src/components/abstract.vue
  5. 113
      src/components/business-handle.vue
  6. 5
      src/components/chat-content.vue
  7. 2
      src/components/chat-main.vue
  8. 187
      src/components/chat-new.vue
  9. 11
      src/components/hot-question-new.vue
  10. 17
      src/components/text-folding.vue
  11. 12
      src/components/welcomes.vue
  12. 33
      src/router/index.js
  13. 11
      src/store/app.js
  14. 109
      src/views/chat.vue
  15. 10
      src/views/word.vue
  16. 8
      vite.config.js

@ -14,9 +14,9 @@ window.wx = wx
const appStore = useAppStore()
const appStoreRefs = storeToRefs(appStore)
provide('appStore', appStore)
Object.keys(appStoreRefs).forEach((key) => {
provide(key, appStoreRefs[key])
})
// Object.keys(appStoreRefs).forEach((key) => {
// provide(key, appStoreRefs[key])
// })
const handleGlobalBrowserExit = (reason, extra = {}) => {
const payload = {
@ -86,7 +86,6 @@ onMounted(() => {
}
init()
console.log(isPureHarmonyOS(), 'isPureHarmonyOS')
window.onresize = function () {
if (!isIOS() && !isAndroid() && !isPureHarmonyOS()) init()
}
@ -104,11 +103,20 @@ onUnmounted(() => {
</script>
<template>
<div id="app" class="app-wrapper opacity">
<RouterView v-slot="{ Component }">
<keep-alive include="chat">
<component :is="Component" />
<div class="app-wrapper opacity">
<RouterView v-slot="{ Component, route }">
<keep-alive>
<component
:is="Component"
v-if="Component && route.meta.keepAlive"
:key="route.name"
/>
</keep-alive>
<component
:is="Component"
v-if="Component && !route.meta.keepAlive"
:key="route.name"
/>
</RouterView>
</div>
</template>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.4 KiB

@ -117,14 +117,6 @@ body .van-toast--text {
color: #0074e1;
}
body .van-popup--bottom {
max-width: 9.98rem;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
}
/* .van-picker__toolbar{
position: relative;
}

@ -1,11 +1,12 @@
<script setup>
import { ref, onMounted, computed, inject } from 'vue';
import { ref, onMounted, onActivated, computed, inject } from 'vue';
import { Post } from '@/utils/https'
import { messageSend } from '@/utils/api'
import { getHiddenProp, getVisibilityState, decryptByVersionNo1, isSit, Encrypt, isDev, isProd } from "@/utils";
const store = inject('appStore', null)
const businessHandleInfo = ref({})
const orderCount = ref(0)
const emit = defineEmits(['ready', 'businessClick'])
const abstractList = computed(() => [
{
title: '保障状态',
@ -35,23 +36,31 @@ const abstractList = computed(() => [
btnBg: 'linear-gradient(261deg, #2ed0c6 3%, #08b7ac 65%)',
},
].filter(item => item.num > 0))
const businessHandle = () => {
const businessHandle = (userId) => {
if (
store.source == "95270000" ||
store.source == "95320000" ||
store.source == "95350000" ||
store.source == "96060000"
) {
Post("notify/info", { userInfoId: store.userId })
return Post("notify/info", { userInfoId: userId })
.then((res) => {
if (res.data?.contents.length > 0) {
businessHandleInfo.value = res.data;
if (res.data?.contents?.length > 0) {
businessHandleInfo.value = res.data
} else {
businessHandleInfo.value = {}
}
store?.setBusinessHandleInfo?.(businessHandleInfo.value)
})
.catch((error) => {
console.log(error);
});
}
return Promise.resolve()
}
const businessClick = () => {
emit('businessClick')
}
const isIdentityNo = (value) => /^\d{15}$|^\d{17}[\dXx]$/.test(String(value || '').trim())
@ -62,7 +71,7 @@ const getIdentityCardno = () => {
if (!versionNo || !pidCipher) return ''
const identityCardno = decryptByVersionNo1(versionNo, pidCipher)
console.log(decryptByVersionNo1(versionNo, '0FLzZwhD08nkUE4BjRGOSEri4KWO'),id);
console.log('decrypt pid =>', JSON.stringify(identityCardno))
return isIdentityNo(identityCardno) ? identityCardno.trim() : ''
}
@ -74,30 +83,50 @@ const getOrderCount = () => {
hasPid: Boolean(store.userInfo?.pid),
hasVersionNo1: Boolean(store.userInfo?.versionNo1),
})
return
return Promise.resolve()
}
Post('policy/queryCount', {
return Post('policy/queryCount', {
dentityCardno: identityCardno,
userId: store.userId
}).then(res => {
orderCount.value = res?.data?.policyCount || 0
}).catch((error) => {
console.log(error)
})
} catch (error) {
console.log(error)
return Promise.resolve()
}
}
const toDetail = (record) => {
const text = record?.name
console.log('text', text);
if (!text) return
messageSend(text).catch((err) => {
console.log(err)
})
if (record.title === '待办任务') {
businessClick()
}
if (record.title === '保障状态') {
// 只把问题写入对话气泡,不走对话接口
store.addSendMessageList({ msgText: record.name, reqAndResType: '1', sendLoading: false })
}
}
const refreshAbstractData = async () => {
await Promise.all([businessHandle(store?.userId), getOrderCount()])
}
const hasLoadedOnce = ref(false)
onMounted(async () => {
businessHandle()
getOrderCount()
try {
await refreshAbstractData()
} finally {
hasLoadedOnce.value = true
emit('ready')
}
})
// keep-alive 返回 chat 时刷新待办和保单数量
onActivated(async () => {
if (!hasLoadedOnce.value) return
await refreshAbstractData()
})
</script>
<template>
@ -106,22 +135,33 @@ onMounted(async () => {
<span>今日皮卡丘摘要</span><img src="@/assets/images/chat/star.svg" alt="" srcset="" />
</div>
<div class="list">
<div :class="`item ${(abstractList || []).length === 1 ? 'singleItem' : ''}`"
v-for="(item, index) in abstractList || []" :key="index">
<div
:class="`item ${(abstractList || []).length === 1 ? 'singleItem' : ''}`"
v-for="(item, index) in abstractList || []"
:key="index"
>
<div>
<div class="title">{{ item.title }}</div>
<div class="content" :style="{ display: (abstractList || []).length > 2 ? 'block' : 'flex' }">
<div
class="content"
:style="{ display: (abstractList || []).length > 2 ? 'block' : 'flex' }"
>
<div class="nums">
<div class="num">
{{ item.num }}<span>{{ item.unit }}</span>
</div>
</div>
<div class="text" :style="{ marginTop: (abstractList || []).length > 2 ? '8rem' : '0' }">
<div
class="text"
:style="{ marginTop: (abstractList || []).length > 2 ? '8rem' : '0' }"
>
{{ item.text }}
</div>
</div>
</div>
<div class="btn" :style="{ background: item.btnBg }" @click="toDetail(item)">{{ item.btnText }}</div>
<div class="btn" :style="{ background: item.btnBg }" @click="toDetail(item)">
{{ item.btnText }}
</div>
</div>
</div>
</div>
@ -135,9 +175,8 @@ onMounted(async () => {
padding: 10rem 12rem;
gap: 12rem;
align-self: stretch;
margin-top: 10rem;
background: linear-gradient(77deg, #fcfdfd80 -1%, #f5f7fee6 99%);
margin-bottom: 16rem;
box-sizing: border-box;
border: 1rem solid rgba(255, 255, 255, 0.9);
@ -259,7 +298,7 @@ onMounted(async () => {
justify-content: space-between;
align-items: center;
>div {
> div {
margin-left: 36rem;
.title {

@ -1,6 +1,6 @@
<template>
<div>
<van-swipe style="height:30rem;margin-top:10rem" class="businessBox" vertical :scrollable="false" :autoplay="3000"
<!-- <van-swipe style="height:30rem;margin-top:10rem" class="businessBox" vertical :scrollable="false" :autoplay="3000"
:show-indicators="false">
<van-swipe-item @click="swiperClick" v-for="(item, index) in businessHandleInfo.contents" :key="index">
<div class="item">
@ -11,22 +11,33 @@
</div>
</div>
</van-swipe-item>
</van-swipe>
<van-popup get-container="#app" v-model:show="Popupshow" round position="bottom"
:style="{ padding: '15rem', boxSizing: ' border-box' }">
</van-swipe> -->
<van-popup
v-model:show="Popupshow"
teleport="body"
round
position="bottom"
closeable
:z-index="3000"
>
<div class="displayFlex">
<div style="font-size: 13rem;color: #878D99;">
您有{{ businessHandleInfo.contents.length }}个待办{{ businessHandleInfo.tip }}</div>
<van-icon name="close" color="#5A9CFF" @click="Popupshow = false" />
</div>
<div class="popupContent">
<div class="item popupContentItem" v-for="(item, index) in businessHandleInfo.contents" :key="index"
@click="richCallAnswer(item)">
<img :src="getIconUrl(item.icon)" alt="" style="width:30rem;height:30rem;">
<div class="item-content">
<div>{{ item.name }}</div>
<van-icon name="arrow" color="#5A9CFF" />
<div class="title">您有{{ displayInfo?.contents?.length || 0 }}个待办</div>
<div class="popupContent">
<div
class="item popupContentItem"
v-for="(item, index) in displayInfo.contents || []"
:key="index"
@click="richCallAnswer(item)"
>
<img
src="@/assets/images/chat/todo-icon.svg"
alt=""
style="width: 22rem; height: 22rem"
/>
<div class="item-content">
<div>{{ item.name }}</div>
<van-icon name="arrow" color="#5A9CFF" />
</div>
</div>
</div>
</div>
@ -35,11 +46,12 @@
</template>
<script setup>
import { ref, inject, computed } from 'vue'
import { ref, inject, computed, watch } from 'vue'
import { Icon, Swipe, SwipeItem, Popup } from 'vant'
defineProps({
businessHandleInfo: { type: Object, default: () => ({}) }
const props = defineProps({
businessHandleInfo: { type: Object, default: () => ({}) },
autoOpen: { type: Boolean, default: true },
})
const store = inject('appStore', null)
@ -49,10 +61,17 @@ const manualService = inject('manualService', ref('1'))
const Popupshow = ref(false)
const isBtnsubmit = ref(true)
const hasAutoOpened = ref(false)
const displayInfo = computed(() => {
const fromProp = props.businessHandleInfo
if (fromProp && Array.isArray(fromProp.contents) && fromProp.contents.length) return fromProp
return store?.businessHandleInfo || {}
})
function getIconUrl(icon) {
try {
return require(`../assets/images/${icon}.png`)
return new URL(`../assets/images/${icon}.png`, import.meta.url).href
} catch (e) {
return ''
}
@ -90,7 +109,8 @@ function richCallAnswer(item) {
isBtnsubmit.value = false
Popupshow.value = false
scrollTopBody()
store?.dispatch?.('sendingRobot', sendParmas).then(() => {
const sending = store?.sendingRobot?.(sendParmas) || store?.dispatch?.('sendingRobot', sendParmas)
Promise.resolve(sending).then(() => {
isBtnsubmit.value = true
}).catch(err => {
isBtnsubmit.value = true
@ -110,9 +130,12 @@ function scrollTopBody() {
}
}, 300)
}
defineExpose({
swiperClick
})
</script>
<style scoped>
<style scoped lang="scss">
.businessBox {
background: #fff;
padding: 10rem;
@ -121,11 +144,10 @@ function scrollTopBody() {
.item {
display: flex;
align-items: center;
justify-content: space-around;
}
.item>img {
.item > img {
width: 30rem;
height: 30rem;
margin-right: 10rem;
@ -134,9 +156,11 @@ function scrollTopBody() {
.item-content {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 15rem;
i{
padding-top: 4rem;
}
}
.line {
@ -147,22 +171,41 @@ function scrollTopBody() {
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 16rem 16rem 0rem 0rem;
display: flex;
flex-direction: column;
align-items: center;
padding: 16rem 16rem 0rem 16rem;
gap: 24rem;
width: 100vw;
background: linear-gradient(180deg, #f0f8ff 0%, #f6f9ff 100%);
.title {
font-family: PingFang SC;
font-size: 16rem;
font-weight: 500;
line-height: normal;
text-align: center;
letter-spacing: 0rem;
font-variation-settings: 'opsz' auto;
}
}
.popupContent {
min-height: 180rem;
max-height: 290rem;
display: flex;
flex-direction: column;
max-height: 300rem;
overflow: auto;
width: 100%;
gap: 12rem;
padding-bottom: 12rem;
}
.popupContentItem {
height: 50rem;
background: #FFFFFF;
border: 1rem solid #F1F3F6;
padding: 13rem;
box-shadow: 0 2rem 8rem 0 rgba(229, 234, 237, 0.50);
border-radius: 8rem;
box-sizing: border-box;
margin: 7rem 5rem;
background: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.9);
padding: 12px;
gap: 12px;
border-radius: 12px;
}
</style>

@ -1111,11 +1111,12 @@
<div class="chat-items">
<div class="content-left-main">
<div class="chat-left-txt connectionArtificial" v-html="replacementRag(item.msgText)"></div>
<div class="question-answer-help" v-if="item.cmd && item.cmd == 'RGFWHS'">
<!-- <div class="question-answer-help" v-if="item.cmd && item.cmd == 'RGFWHS'">
如需更多帮助,请点击<button class="answer-help-btn" disabled="disabled"
@click.stop="connectionArtificial($event)">
进入人工服务</button>。
</div>
</div> -->
<!-- 本次注释进入人工服务 -->
</div>
</div>
</div>

@ -96,7 +96,7 @@
<script setup>
// 原 Vue2 Options API 组件 chat-main(keep-alive 缓存名沿用 "chat")
defineOptions({ name: 'chat' })
defineOptions({ name: 'chat-main' })
import { ref, computed, watch, onMounted, onUnmounted, onActivated, nextTick } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'

@ -1,5 +1,6 @@
<script setup>
import { ref, computed, inject, watch, nextTick, onMounted } from 'vue'
import { ref, computed, inject, watch, nextTick, onMounted, onActivated, onDeactivated } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
import { showToast } from 'vant'
import _ from 'lodash'
import { showImagePreview } from 'vant'
@ -10,6 +11,7 @@ import welcomes from '@/components/welcomes.vue'
import abstract from '@/components/abstract.vue'
import chatContent from '@/components/chat-content.vue'
import DynamicImage from '@/components/common/DynamicImage.vue'
import bussinessHandle from '@/components/business-handle.vue'
import imageService from '@/utils/imageService'
import { messageSend } from '@/utils/api'
import { Post } from '@/utils/https'
@ -44,6 +46,31 @@ const mainContainerRef = ref(null)
const historylist = ref([])
const historyLoading = ref(false)
const historyReady = ref(false)
const introReadyCount = ref(0)
const businessHandleRef = ref(null)
const businessClick = () => {
businessHandleRef.value.swiperClick()
console.log('businessHandleRef');
}
const scrollMainToBottom = async () => {
// 返回恢复滚动中时,避免欢迎区/摘要 ready 再次强制滚到底部
if (isRestoringScroll.value || shouldRestoreScroll.value) return
await nextTick()
const el = mainContainerRef.value
if (!el) return
el.scrollTop = el.scrollHeight
}
const onIntroReady = async () => {
introReadyCount.value += 1
if (introReadyCount.value < 2) return
await scrollMainToBottom()
// 内容渲染(尤其 abstract 列表)可能晚一帧,再补一次
setTimeout(() => {
scrollMainToBottom()
}, 150)
}
const pageNum = ref(1)
const pageSize = ref(10)
const nolist = ref(false)
@ -178,11 +205,115 @@ function updateReadStatus(datalist) {
}
}
const CHAT_SCROLL_KEY = 'yykf_chat_main_scroll_top'
// 模块级缓存:避免 keep-alive 隐藏 DOM 后 scrollTop 被置 0 覆盖掉真实位置
let memoryScrollTop = 0
function getMainEl() {
return mainContainerRef.value || store?.mescrollContent || null
}
function scrollMainTo(top) {
const el = mainContainerRef.value
if (!el) return
el.scrollTop = top
const el = getMainEl()
if (!el) return false
const value = Math.max(0, Number(top) || 0)
el.scrollTop = value
return Math.abs((el.scrollTop || 0) - value) < 2 || value === 0
}
const savedScrollTop = ref(0)
const shouldRestoreScroll = ref(false)
const isRestoringScroll = ref(false)
function readPersistedScrollTop() {
if (memoryScrollTop > 0) return memoryScrollTop
if (savedScrollTop.value > 0) return savedScrollTop.value
try {
const raw = sessionStorage.getItem(CHAT_SCROLL_KEY)
if (raw == null || raw === '') return null
const top = Number(raw)
return Number.isFinite(top) ? top : null
} catch (e) {
return null
}
}
function persistScrollTop(top, { allowZero = false } = {}) {
const value = Math.max(0, Number(top) || 0)
// display:none 后常被读成 0,禁止用 0 覆盖已记录的有效位置
if (!allowZero && value <= 0 && (memoryScrollTop > 0 || savedScrollTop.value > 0)) {
return memoryScrollTop || savedScrollTop.value
}
memoryScrollTop = value
savedScrollTop.value = value
try {
sessionStorage.setItem(CHAT_SCROLL_KEY, String(value))
} catch (e) {}
return value
}
function saveScrollPosition(markRestore = true) {
const el = getMainEl()
// 优先用离开前仍可见时的 scrollTop;读到 0 时保留 memory 中的旧值
if (el) persistScrollTop(el.scrollTop)
if (markRestore) shouldRestoreScroll.value = true
}
function restoreScrollPosition(forceTop) {
const top = forceTop != null ? forceTop : (readPersistedScrollTop() ?? 0)
console.log('[chat-scroll] restore to', top, {
memoryScrollTop,
saved: savedScrollTop.value,
el: !!getMainEl(),
})
shouldRestoreScroll.value = true
isRestoringScroll.value = true
const apply = () => {
const el = getMainEl()
if (!el) return false
// 内容高度不够时先撑不住,稍后重试
if (top > 0 && el.scrollHeight <= el.clientHeight + 2) return false
el.scrollTop = top
return true
}
nextTick(() => {
apply()
requestAnimationFrame(apply)
const delays = [16, 50, 100, 200, 350, 500, 800]
delays.forEach((delay) => setTimeout(apply, delay))
setTimeout(() => {
apply()
isRestoringScroll.value = false
shouldRestoreScroll.value = false
}, 850)
})
}
function onMainScroll(event) {
if (isRestoringScroll.value) return
const top = event?.target?.scrollTop
if (top == null) return
persistScrollTop(top, { allowZero: true })
}
onBeforeRouteLeave(() => {
// 此时 DOM 仍可见,最适合记录真实滚动位置
saveScrollPosition(true)
})
onDeactivated(() => {
// 只标记需要恢复,不再用可能已被置 0 的 scrollTop 覆盖
shouldRestoreScroll.value = true
const el = getMainEl()
if (el && el.scrollTop > 0) persistScrollTop(el.scrollTop)
})
onActivated(() => {
const top = readPersistedScrollTop()
console.log('[chat-scroll] onActivated', { top, shouldRestoreScroll: shouldRestoreScroll.value })
if (!shouldRestoreScroll.value && (top == null || top <= 0)) return
restoreScrollPosition(top)
})
function getHeaderUrl(context, payload) {
let params = {
"source": store.source
@ -222,10 +353,15 @@ const getChatHistory = async () => {
userId: userId.value || store?.userId,
}
historyLoading.value = true
if (pageNum.value === 1) {
historyReady.value = false
introReadyCount.value = 0
}
Post('history/chatInfo', params)
.then((response) => {
historyLoading.value = false
historyReady.value = true
if (response.code && response.code != 0) {
showToast(response.msg)
return
@ -302,23 +438,31 @@ const getChatHistory = async () => {
})
.catch(() => {
historyLoading.value = false
historyReady.value = true
pageNum.value--
nolist.value = true
})
}
onMounted(async () => {
getHeaderUrl()
getChatHistory()
await nextTick()
if (mainContainerRef.value) {
store?.setMescrollContent?.(mainContainerRef.value)
}
getHeaderUrl()
getChatHistory()
const persisted = readPersistedScrollTop()
if (persisted != null && persisted > 0) {
shouldRestoreScroll.value = true
restoreScrollPosition()
}
})
watch(
chatArr,
() => {
// 从其他路由返回、正在恢复滚动位置时,不要强制滚到底部
if (isRestoringScroll.value || shouldRestoreScroll.value) return
nextTick(() => {
const container = mainContainerRef.value
const container = getMainEl()
if (container) container.scrollTop = container.scrollHeight
})
},
@ -328,25 +472,38 @@ watch(
<template>
<div class="chat-new">
<div class="main-container" ref="mainContainerRef">
<div class="main-container" ref="mainContainerRef" @scroll.passive="onMainScroll">
<div class="history-loading" v-if="historyLoading">
<van-loading type="spinner" size="18rem" />
</div>
<div class="historicRecords" v-if="historylist.length > 0">
<chatContent :historylist="historylist" :isWX="isWX" :tokenExist="tokenExist" :isHistory="true"
:lostChatInfoNum="lostChatInfoNum" />
<chatContent
:historylist="historylist"
:isWX="isWX"
:tokenExist="tokenExist"
:isHistory="true"
:lostChatInfoNum="lostChatInfoNum"
/>
</div>
<div class="this-news-box opacity" v-if="historylist.length > 0">
<div class="this-news-tips">以下是本次消息</div>
</div>
<welcomes />
<abstract />
<hotQuestionNew />
<chatContent :historylist="chatArr" :isWX="isWX" :tokenExist="tokenExist" :lostChatInfoNum="lostChatInfoNum" />
<template v-if="historyReady">
<welcomes @ready="onIntroReady" />
<abstract @ready="onIntroReady" @businessClick="businessClick" />
<hotQuestionNew @ready="onIntroReady" />
</template>
<chatContent
:historylist="chatArr"
:isWX="isWX"
:tokenExist="tokenExist"
:lostChatInfoNum="lostChatInfoNum"
/>
</div>
<div class="chat-footer">
<footer-bar v-if="tokenExist" :tokenExist="tokenExist" />
</div>
<bussinessHandle :businessHandleInfo="store.businessHandleInfo" ref="businessHandleRef" />
</div>
</template>
<style lang="scss" scoped>
@ -373,6 +530,7 @@ watch(
.main-container {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
@ -505,7 +663,6 @@ watch(
}
@keyframes typing-blink {
0%,
80%,
100% {

@ -14,7 +14,7 @@ const initHotQuestion = async () => {
sceneId: store.userInfo.sceneId || "",
};
Post("config/activity", param)
return Post("config/activity", param)
.then((res) => {
if (res.code && res.code != 0) {
showToast(res.msg);
@ -72,8 +72,13 @@ const handleAskClick = (record) => {
isItemsubmit.value = true
})
}
const emit = defineEmits(['ready'])
onMounted(async () => {
await initHotQuestion()
try {
await initHotQuestion()
} finally {
emit('ready')
}
})
</script>
<template>
@ -95,7 +100,7 @@ onMounted(async () => {
display: flex;
flex-direction: column;
gap: 10rem;
margin-top: 16rem;
margin-bottom: 16rem;
position: relative;
z-index: 1;

@ -35,7 +35,7 @@
{{ items.intent }}
</button>
</div>
<div class="question-answer-help" v-if="itemlist.cmd && itemlist.cmd == 'RGFWHS'">
<!-- <div class="question-answer-help" v-if="itemlist.cmd && itemlist.cmd == 'RGFWHS'">
如需更多帮助,请点击<button
class="answer-help-btn"
:disabled="disabled"
@ -43,7 +43,8 @@
>
进入人工服务</button
>。
</div>
</div> -->
<!-- 本次注释进入人工服务 -->
</div>
<div
class="unfold-box"
@ -187,7 +188,7 @@ function initGlobalCallbacks() {
if (!res.data) {
utils.AppToHttpHtml(
url +
`&idnum=1&openId=${userInfo.value?.sourceUserId}&entryOpenid=${userInfo.value?.entryOpenid}`
`&idnum=1&openId=${userInfo.value?.sourceUserId}&entryOpenid=${userInfo.value?.entryOpenid}`
)
} else {
utils.AppToHttpHtml(url + `&accessTicket=${res.data}`)
@ -364,13 +365,11 @@ function emojiToImage(str) {
let regex = /\[[^\]]+\]/g
return str.replace(regex, (item) => {
if (expressionProcessing.value[item]) {
return `<img src="${
publicPath + expressionProcessing.value[item]
}" alt="small-image" title="${item}" />`
return `<img src="${publicPath + expressionProcessing.value[item]
}" alt="small-image" title="${item}" />`
} else if (bigExpressionProcessing.value[item]) {
return `<img src="${
publicPath + bigExpressionProcessing.value[item]
}" alt="big-image" title="${item}"/>`
return `<img src="${publicPath + bigExpressionProcessing.value[item]
}" alt="big-image" title="${item}"/>`
}
return item
})

@ -3,6 +3,7 @@ import { ref, onMounted, inject } from 'vue';
import { Post } from '@/utils/https'
const store = inject('appStore')
const emit = defineEmits(['ready'])
const welcomeWord = ref('')
const welcomeSubWord = ref('我是您的服务管家皮卡丘')
onMounted(() => {
@ -11,8 +12,12 @@ onMounted(() => {
userInfoId: store.userId
}).then(res => {
if (res.code === 0 && res.data) {
welcomeWord.value = `${res.data?.greetings || ''}${res.data?.greetings ? ',' : ''}${res.data?.welcomeName || ''}`
welcomeWord.value = `${res.data?.greetings || ''}${res.data?.welcomeName ? ',' : ''}${res.data?.welcomeName || ''}`
}
}).catch((error) => {
console.log(error)
}).finally(() => {
emit('ready')
})
})
</script>
@ -25,7 +30,7 @@ onMounted(() => {
</template>
<style lang="scss" scoped>
.welcome-con {
padding: 33rem 8rem;
padding: 38rem 8rem;
position: relative;
.welcome-title {
font-family: PingFang SC;
@ -44,7 +49,7 @@ onMounted(() => {
}
.sub-title {
width: calc(100% - 100rem);
max-width: calc(100% - 100rem);
font-family: PingFang SC;
font-size: 18rem;
font-weight: 600;
@ -56,6 +61,7 @@ onMounted(() => {
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
width: fit-content;
text-fill-color: transparent;
}

@ -5,26 +5,23 @@ const router = createRouter({
history: createWebHashHistory(),
routes: [
{ path: '/', redirect: '/chat' },
{ path: '/chat', name: 'chat', component: () => import('../views/chat.vue') },
{
path: '/history',
name: 'history',
component: () => import('../views/history.vue'),
meta: { title: '洋洋客服-历史记录查询' },
},
{
path: '/previewPdf',
name: 'previewPdf',
component: () => import('../components/previewPDF/index.vue'),
meta: { title: '预览PDF' },
},
// { path: '/uploadPage', name: 'uploadPage', component: () => import('../views/uploadPage.vue') },
// { path: '/claimPage', name: 'claimPage', component: () => import('../views/claimPage/index.vue'), },
// { path: '/outView', name: 'outView', component: () => import('../views/outView.vue') },
// { path: '/sign', name: 'sign', component: () => import('../components/input/sign.vue') },
{ path: '/chat', name: 'chat', component: () => import('../views/chat.vue'), meta: { keepAlive: true, title: '小蓝管家' } },
{ path: '/word', name: 'word', component: () => import('../views/word.vue'), meta: { keepAlive: true, title: '小蓝管家' } },
// {
// path: '/history',
// name: 'history',
// component: () => import('../views/history.vue'),
// meta: { title: '小蓝管家-历史记录查询' },
// },
// {
// path: '/previewPdf',
// name: 'previewPdf',
// component: () => import('../components/previewPDF/index.vue'),
// meta: { title: '预览PDF' },
// },
],
})
const defaultTitle = '洋洋客服'
const defaultTitle = '小蓝管家'
// 全局前置路由守卫
router.beforeEach(async (to, from, next) => {

@ -117,6 +117,7 @@ export const useAppStore = defineStore('app', () => {
const isBackstage = ref(false)
const sdkLoaded = ref(false)
const chatArr = ref([])
const businessHandleInfo = ref({})
const isChatLoading = ref(false)
const bpErrorInfo = ref([])
const voiceDataList = ref([])
@ -194,6 +195,7 @@ export const useAppStore = defineStore('app', () => {
userInfo.value = {}
chatList.value = []
chatArr.value = []
businessHandleInfo.value = {}
isChatLoading.value = false
sendMessageList.value = []
receiveMessageList.value = []
@ -752,12 +754,17 @@ export const useAppStore = defineStore('app', () => {
chatList.value.splice(index, 1)
}
function setBusinessHandleInfo(payload = {}) {
businessHandleInfo.value = payload || {}
}
function delAllChatList() {
chatList.value.splice(0, chatList.value.length)
chatArr.value.splice(0, chatArr.value.length)
isChatLoading.value = false
}
function delSendFailListById(delId) {
if (sendFailList.value && sendFailList.value.length > 0) {
sendFailList.value.forEach((item, index) => {
@ -1345,6 +1352,8 @@ export const useAppStore = defineStore('app', () => {
set_hxOpenSuccesed: setHxOpenSuccesed, set_timestamp: setTimestamp,
set_sendParmas: setSendParmas, set_queuedTimeout: setQueuedTimeout,
set_chatList: setChatList, del_chatList: delChatList, delall_chatList: delAllChatList,
set_businessHandleInfo: setBusinessHandleInfo,
setBusinessHandleInfo,
set_chatId: setChatId, set_currentItemId: setCurrentItemId,
set_findActiveItem: setFindActiveItem, set_activeItemExtra: setActiveItemExtra,
set_reSendMessage: setReSendMessage, set_isBackstage: setIsBackstage,
@ -1433,6 +1442,7 @@ export const useAppStore = defineStore('app', () => {
isBackstage,
sdkLoaded,
chatArr,
businessHandleInfo,
isChatLoading,
bpErrorInfo,
voiceDataList,
@ -1511,6 +1521,7 @@ export const useAppStore = defineStore('app', () => {
setChatList,
delChatList,
delAllChatList,
setBusinessHandleInfo,
setChatId,
setCurrentItemId,
setFindActiveItem,

@ -6,14 +6,20 @@
:isWX="isWX"
:lostChatInfoNum="lostChatInfoNum"
/> -->
<chat-new v-if="isToken" :tokenExist="isToken" :isWX="isWX" :lostChatInfoNum="lostChatInfoNum" />
<chat-new
v-if="isToken"
:tokenExist="isToken"
:isWX="isWX"
:lostChatInfoNum="lostChatInfoNum"
/>
<!-- <button @click="router.push('/word')">
跳转word
</button> -->
<!-- <human-appraise-popup /> -->
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useAppStore } from '@/store/app.js'
import { ref, onMounted, onActivated, inject, nextTick } from 'vue'
import {
showLoadingToast,
closeToast,
@ -28,34 +34,66 @@ import chatNew from '@/components/chat-new.vue'
import Utils from '@/utils/utils'
import sensors from '@/utils/sensors'
import { Decryption, DecryptionECB, Encrypt, isDev, isProd } from '@/utils'
import { useRouter, onBeforeRouteLeave } from 'vue-router'
const router = useRouter()
defineOptions({
name: 'chat',
})
// 状态管理
const store = useAppStore()
const {
setSource,
setUserInfo,
setNotitle,
setConfigId,
setEntrance,
setAppentry,
setHtmlExtParams,
setComplaint,
setNavigationCorpus,
setSessionId,
} = store
const store = inject('appStore', null)
// 响应式数据
const isToken = ref(false)
const hasInitialized = ref(false)
const isWX = ref(false)
const lostChatInfoNum = ref(0)
const urlParams = ref({})
// 生命周期
onActivated(() => {
// keep-alive 返回时不重新初始化
console.log('[chat] activated, hasInitialized=', hasInitialized.value, 'isToken=', isToken.value)
// 路由组件激活后再补一次滚动恢复,确保子组件 DOM 已显示
try {
const raw = sessionStorage.getItem('yykf_chat_main_scroll_top')
const top = raw == null ? 0 : Number(raw)
if (!Number.isFinite(top) || top <= 0) return
const apply = () => {
const el = store?.mescrollContent
if (!el) return
el.scrollTop = top
}
nextTick(() => {
apply()
;[50, 120, 240, 480].forEach((d) => setTimeout(apply, d))
})
} catch (e) {}
})
onBeforeRouteLeave(() => {
try {
const el = store?.mescrollContent
if (!el) return
const top = el.scrollTop || 0
// 避免隐藏后读到 0 覆盖有效位置
if (top > 0) {
sessionStorage.setItem('yykf_chat_main_scroll_top', String(top))
}
} catch (e) {}
})
onMounted(async () => {
if (hasInitialized.value) return
hasInitialized.value = true
var liccsAds = localStorage.getItem('liccsAds') // 记录用户弹窗提示
localStorage.clear()
localStorage.liccsAds = liccsAds || ''
//清除session 清除上次的记录
//清除session 清除上次的记录(保留聊天滚动位置)
const chatScrollTop = sessionStorage.getItem('yykf_chat_main_scroll_top')
sessionStorage.clear()
if (chatScrollTop != null) sessionStorage.setItem('yykf_chat_main_scroll_top', chatScrollTop)
urlParams.value = Utils.getUrlParams()
console.log('txfModules参数', urlParams.value.txfModules)
@ -93,16 +131,16 @@ onMounted(async () => {
}
isWX.value = Utils.isWeiXin()
setUserInfo(userInfo);//用户信息
setSource(urlParams.value.ht || ""); //来源
setNotitle(urlParams.value.notitle || ""); //是否展示头部
setConfigId(urlParams.value.configId || ""); //环信参数
setEntrance(urlParams.value.entrance || ""); //入口标识
setAppentry(urlParams.value.appentry || ""); // 是否为老版本app
setHtmlExtParams(urlParams.value.htmlExtParams || ""); //扩展参数
setComplaint(urlParams.value.complaint || ""); //扩展参数
setNavigationCorpus(urlParams.value.navigationCorpus || ""); //扩展参数
store.setUserInfo(userInfo);//用户信息
store.setSource(urlParams.value.ht || ""); //来源
store.setNotitle(urlParams.value.notitle || ""); //是否展示头部
store.setConfigId(urlParams.value.configId || ""); //环信参数
store.setEntrance(urlParams.value.entrance || ""); //入口标识
store.setAppentry(urlParams.value.appentry || ""); // 是否为老版本app
store.setHtmlExtParams(urlParams.value.htmlExtParams || ""); //扩展参数
store.setComplaint(urlParams.value.complaint || ""); //扩展参数
store.setNavigationCorpus(urlParams.value.navigationCorpus || ""); //扩展参数
wx.miniProgram.getEnv((res) => {
console.log(res, 'res');
})
@ -138,6 +176,21 @@ onMounted(async () => {
urlParams.value = Utils.getUrlParams()
initToken()
// 渠道校验
try {
const checkRes = await Post("config/downSource", {
source: urlParams.value.ht || "96060000"
});
if (checkRes.code === 0 && checkRes.data !== null) {
showDialog({
title: "提示",
message: checkRes.data?.toast,
confirmButtonText: "确定"
})
}
} catch (error) {
console.log("渠道校验异常:", error);
}
})
// 方法

@ -0,0 +1,10 @@
<template>
<div>
vue3
</div>
</template>
<script setup>
defineOptions({
name: 'word',
})
</script>

@ -19,14 +19,6 @@ export default defineConfig(({ command }) => {
changeOrigin: true,
ws: true,
},
'/api': {
// target: 'https://aioblu-sit.cpic.com.cn',
// target: 'https://aioblu-dev.cpic.com.cn/yyfw',
target: 'https://ssp-sit.cpic.com.cn',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
plugins: [

Loading…
Cancel
Save