forked from SmartBench/CMVR-IOT-UI
feat: 优化任务日志自动滚动
This commit is contained in:
parent
36016684a3
commit
1edb97f606
@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<div ref="messageContainer" class="task-log" :style="{ height: `${height}px` }">
|
||||
<div class="task-log" :style="{ height: `${height}px` }">
|
||||
<!-- 独立滚动层负责监听用户位置;外层仅提供边框和悬浮按钮的定位上下文。 -->
|
||||
<div ref="messageContainer" class="task-log__scroll" @scroll.passive="handleScroll">
|
||||
<!-- 每条日志由固定宽度的时间列和自适应内容列组成;没有后端 ID 时使用数组下标兜底。 -->
|
||||
<div v-for="(log, index) in list" :key="log.id || index" class="task-log__item">
|
||||
<div class="task-log__time">{{ formatDate(log.logTime) }}</div>
|
||||
@ -79,10 +81,24 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户离开底部后显示悬浮入口;点击后回到最新日志并恢复自动跟随。 -->
|
||||
<el-tooltip v-if="!isAtBottom" content="跳到最新日志" placement="left">
|
||||
<el-button
|
||||
class="task-log__jump-latest"
|
||||
type="primary"
|
||||
:icon="Bottom"
|
||||
circle
|
||||
aria-label="跳到最新日志"
|
||||
@click.stop="scrollToBottom"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { Bottom } from '@element-plus/icons-vue'
|
||||
import { formatDate } from '@/utils/index'
|
||||
|
||||
/**
|
||||
@ -97,6 +113,12 @@ const props = defineProps({
|
||||
// 保存日志滚动容器 DOM,用于首次渲染和日志更新后滚动到最新一条。
|
||||
const messageContainer = ref(null)
|
||||
|
||||
// 使用少量像素容差吸收浏览器对子像素和缩放比例的取整误差,避免视觉上已到底部却仍显示按钮。
|
||||
const BOTTOM_THRESHOLD = 8
|
||||
|
||||
// 初始状态视为位于底部;组件挂载后会根据真实滚动尺寸完成第一次定位。
|
||||
const isAtBottom = ref(true)
|
||||
|
||||
// 以日志对象本身作为弱引用键缓存媒体解析结果;日志对象回收后缓存会自动释放,不产生长期内存占用。
|
||||
const mediaCache = new WeakMap()
|
||||
|
||||
@ -287,31 +309,84 @@ const imageUrlsOf = (log) => mediaOf(log)
|
||||
.map((item) => item.url)
|
||||
|
||||
/**
|
||||
* 将滚动位置移动到容器底部,让用户默认看到最新日志。
|
||||
* nextTick 等待 Vue 完成 DOM 更新,否则新日志节点尚未插入时 scrollHeight 仍是旧值。
|
||||
* 根据滚动层的剩余距离更新“是否位于底部”。
|
||||
* 用户拖动滚动条、滚轮滚动以及程序修改 scrollTop 都会触发 scroll 事件,因此只需要维护一个状态源。
|
||||
*/
|
||||
const scrollToBottom = () => {
|
||||
nextTick(() => {
|
||||
if (messageContainer.value) messageContainer.value.scrollTop = messageContainer.value.scrollHeight
|
||||
})
|
||||
const updateBottomState = () => {
|
||||
const container = messageContainer.value
|
||||
if (!container) {
|
||||
isAtBottom.value = true
|
||||
return
|
||||
}
|
||||
|
||||
const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight
|
||||
isAtBottom.value = distanceToBottom <= BOTTOM_THRESHOLD
|
||||
}
|
||||
|
||||
// deep 监听既覆盖追加/删除日志,也覆盖现有日志对象字段更新;组件挂载时同样执行一次初始定位。
|
||||
watch(() => props.list, scrollToBottom, { deep: true })
|
||||
// 滚动事件只做轻量尺寸计算,不写入 scrollTop,避免干扰用户正在进行的历史日志浏览。
|
||||
const handleScroll = () => updateBottomState()
|
||||
|
||||
/**
|
||||
* 将滚动位置移动到最新日志,并恢复自动跟随状态。
|
||||
* nextTick 确保新日志已经渲染,scrollHeight 是更新后的完整内容高度。
|
||||
*/
|
||||
const scrollToBottom = async () => {
|
||||
await nextTick()
|
||||
const container = messageContainer.value
|
||||
if (!container) return
|
||||
container.scrollTop = container.scrollHeight
|
||||
isAtBottom.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 日志变化时先读取更新前的滚动状态:
|
||||
* - 原本在底部,说明用户正在追踪最新日志,DOM 更新后继续滚到底部;
|
||||
* - 原本不在底部,说明用户正在查看历史内容,只更新按钮状态,不改变其 scrollTop。
|
||||
* deep 监听同时覆盖追加日志和现有日志内容更新。
|
||||
*/
|
||||
watch(() => props.list, () => {
|
||||
if (isAtBottom.value) {
|
||||
scrollToBottom()
|
||||
} else {
|
||||
nextTick(updateBottomState)
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// 首次进入日志视图仍默认展示最新一条,此时尚不存在用户主动选择的历史位置。
|
||||
onMounted(scrollToBottom)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 外层容器固定高度并独立滚动,避免持续增长的日志改变父页面布局。 */
|
||||
/* 外层容器固定高度并裁剪内容,同时作为“跳到最新日志”悬浮按钮的定位上下文。 */
|
||||
.task-log {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 只有内部区域负责滚动,悬浮按钮不会随日志内容一起移出可视区域。 */
|
||||
.task-log__scroll {
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 固定在滚动视口右下角,不参与日志排版;稳定尺寸避免按钮出现时挤压或移动内容。 */
|
||||
.task-log__jump-latest {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
z-index: 2;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 18%);
|
||||
}
|
||||
|
||||
/* 桌面端使用“时间 + 内容”两列布局;内容列 minmax(0, 1fr) 允许长文本正确收缩。 */
|
||||
.task-log__item {
|
||||
display: grid;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user