|
|
|
@ -1,5 +1,6 @@ |
|
|
|
<script setup> |
|
|
|
<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 { showToast } from 'vant' |
|
|
|
import _ from 'lodash' |
|
|
|
import _ from 'lodash' |
|
|
|
import { showImagePreview } from 'vant' |
|
|
|
import { showImagePreview } from 'vant' |
|
|
|
@ -10,6 +11,7 @@ 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 chatContent from '@/components/chat-content.vue' |
|
|
|
import DynamicImage from '@/components/common/DynamicImage.vue' |
|
|
|
import DynamicImage from '@/components/common/DynamicImage.vue' |
|
|
|
|
|
|
|
import bussinessHandle from '@/components/business-handle.vue' |
|
|
|
import imageService from '@/utils/imageService' |
|
|
|
import imageService from '@/utils/imageService' |
|
|
|
import { messageSend } from '@/utils/api' |
|
|
|
import { messageSend } from '@/utils/api' |
|
|
|
import { Post } from '@/utils/https' |
|
|
|
import { Post } from '@/utils/https' |
|
|
|
@ -44,6 +46,31 @@ const mainContainerRef = ref(null) |
|
|
|
|
|
|
|
|
|
|
|
const historylist = ref([]) |
|
|
|
const historylist = ref([]) |
|
|
|
const historyLoading = ref(false) |
|
|
|
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 pageNum = ref(1) |
|
|
|
const pageSize = ref(10) |
|
|
|
const pageSize = ref(10) |
|
|
|
const nolist = ref(false) |
|
|
|
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) { |
|
|
|
function scrollMainTo(top) { |
|
|
|
const el = mainContainerRef.value |
|
|
|
const el = getMainEl() |
|
|
|
if (!el) return |
|
|
|
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 |
|
|
|
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) { |
|
|
|
function getHeaderUrl(context, payload) { |
|
|
|
let params = { |
|
|
|
let params = { |
|
|
|
"source": store.source |
|
|
|
"source": store.source |
|
|
|
@ -222,10 +353,15 @@ const getChatHistory = async () => { |
|
|
|
userId: userId.value || store?.userId, |
|
|
|
userId: userId.value || store?.userId, |
|
|
|
} |
|
|
|
} |
|
|
|
historyLoading.value = true |
|
|
|
historyLoading.value = true |
|
|
|
|
|
|
|
if (pageNum.value === 1) { |
|
|
|
|
|
|
|
historyReady.value = false |
|
|
|
|
|
|
|
introReadyCount.value = 0 |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Post('history/chatInfo', params) |
|
|
|
Post('history/chatInfo', params) |
|
|
|
.then((response) => { |
|
|
|
.then((response) => { |
|
|
|
historyLoading.value = false |
|
|
|
historyLoading.value = false |
|
|
|
|
|
|
|
historyReady.value = true |
|
|
|
if (response.code && response.code != 0) { |
|
|
|
if (response.code && response.code != 0) { |
|
|
|
showToast(response.msg) |
|
|
|
showToast(response.msg) |
|
|
|
return |
|
|
|
return |
|
|
|
@ -302,23 +438,31 @@ const getChatHistory = async () => { |
|
|
|
}) |
|
|
|
}) |
|
|
|
.catch(() => { |
|
|
|
.catch(() => { |
|
|
|
historyLoading.value = false |
|
|
|
historyLoading.value = false |
|
|
|
|
|
|
|
historyReady.value = true |
|
|
|
pageNum.value-- |
|
|
|
pageNum.value-- |
|
|
|
nolist.value = true |
|
|
|
nolist.value = true |
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
onMounted(async () => { |
|
|
|
onMounted(async () => { |
|
|
|
|
|
|
|
getHeaderUrl() |
|
|
|
|
|
|
|
getChatHistory() |
|
|
|
await nextTick() |
|
|
|
await nextTick() |
|
|
|
if (mainContainerRef.value) { |
|
|
|
if (mainContainerRef.value) { |
|
|
|
store?.setMescrollContent?.(mainContainerRef.value) |
|
|
|
store?.setMescrollContent?.(mainContainerRef.value) |
|
|
|
} |
|
|
|
} |
|
|
|
getHeaderUrl() |
|
|
|
const persisted = readPersistedScrollTop() |
|
|
|
getChatHistory() |
|
|
|
if (persisted != null && persisted > 0) { |
|
|
|
|
|
|
|
shouldRestoreScroll.value = true |
|
|
|
|
|
|
|
restoreScrollPosition() |
|
|
|
|
|
|
|
} |
|
|
|
}) |
|
|
|
}) |
|
|
|
watch( |
|
|
|
watch( |
|
|
|
chatArr, |
|
|
|
chatArr, |
|
|
|
() => { |
|
|
|
() => { |
|
|
|
|
|
|
|
// 从其他路由返回、正在恢复滚动位置时,不要强制滚到底部 |
|
|
|
|
|
|
|
if (isRestoringScroll.value || shouldRestoreScroll.value) return |
|
|
|
nextTick(() => { |
|
|
|
nextTick(() => { |
|
|
|
const container = mainContainerRef.value |
|
|
|
const container = getMainEl() |
|
|
|
if (container) container.scrollTop = container.scrollHeight |
|
|
|
if (container) container.scrollTop = container.scrollHeight |
|
|
|
}) |
|
|
|
}) |
|
|
|
}, |
|
|
|
}, |
|
|
|
@ -328,25 +472,38 @@ watch( |
|
|
|
|
|
|
|
|
|
|
|
<template> |
|
|
|
<template> |
|
|
|
<div class="chat-new"> |
|
|
|
<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"> |
|
|
|
<div class="history-loading" v-if="historyLoading"> |
|
|
|
<van-loading type="spinner" size="18rem" /> |
|
|
|
<van-loading type="spinner" size="18rem" /> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div class="historicRecords" v-if="historylist.length > 0"> |
|
|
|
<div class="historicRecords" v-if="historylist.length > 0"> |
|
|
|
<chatContent :historylist="historylist" :isWX="isWX" :tokenExist="tokenExist" :isHistory="true" |
|
|
|
<chatContent |
|
|
|
:lostChatInfoNum="lostChatInfoNum" /> |
|
|
|
:historylist="historylist" |
|
|
|
|
|
|
|
:isWX="isWX" |
|
|
|
|
|
|
|
:tokenExist="tokenExist" |
|
|
|
|
|
|
|
:isHistory="true" |
|
|
|
|
|
|
|
:lostChatInfoNum="lostChatInfoNum" |
|
|
|
|
|
|
|
/> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div class="this-news-box opacity" v-if="historylist.length > 0"> |
|
|
|
<div class="this-news-box opacity" v-if="historylist.length > 0"> |
|
|
|
<div class="this-news-tips">以下是本次消息</div> |
|
|
|
<div class="this-news-tips">以下是本次消息</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<welcomes /> |
|
|
|
<template v-if="historyReady"> |
|
|
|
<abstract /> |
|
|
|
<welcomes @ready="onIntroReady" /> |
|
|
|
<hotQuestionNew /> |
|
|
|
<abstract @ready="onIntroReady" @businessClick="businessClick" /> |
|
|
|
<chatContent :historylist="chatArr" :isWX="isWX" :tokenExist="tokenExist" :lostChatInfoNum="lostChatInfoNum" /> |
|
|
|
<hotQuestionNew @ready="onIntroReady" /> |
|
|
|
|
|
|
|
</template> |
|
|
|
|
|
|
|
<chatContent |
|
|
|
|
|
|
|
:historylist="chatArr" |
|
|
|
|
|
|
|
:isWX="isWX" |
|
|
|
|
|
|
|
:tokenExist="tokenExist" |
|
|
|
|
|
|
|
:lostChatInfoNum="lostChatInfoNum" |
|
|
|
|
|
|
|
/> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div class="chat-footer"> |
|
|
|
<div class="chat-footer"> |
|
|
|
<footer-bar v-if="tokenExist" :tokenExist="tokenExist" /> |
|
|
|
<footer-bar v-if="tokenExist" :tokenExist="tokenExist" /> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<bussinessHandle :businessHandleInfo="store.businessHandleInfo" ref="businessHandleRef" /> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
</template> |
|
|
|
<style lang="scss" scoped> |
|
|
|
<style lang="scss" scoped> |
|
|
|
@ -373,6 +530,7 @@ watch( |
|
|
|
|
|
|
|
|
|
|
|
.main-container { |
|
|
|
.main-container { |
|
|
|
flex: 1; |
|
|
|
flex: 1; |
|
|
|
|
|
|
|
min-height: 0; |
|
|
|
overflow-y: auto; |
|
|
|
overflow-y: auto; |
|
|
|
overflow-x: hidden; |
|
|
|
overflow-x: hidden; |
|
|
|
|
|
|
|
|
|
|
|
@ -505,7 +663,6 @@ watch( |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@keyframes typing-blink { |
|
|
|
@keyframes typing-blink { |
|
|
|
|
|
|
|
|
|
|
|
0%, |
|
|
|
0%, |
|
|
|
80%, |
|
|
|
80%, |
|
|
|
100% { |
|
|
|
100% { |
|
|
|
|