AI 问答界面如何布局?
在豆包的AI问答聊天界面,为什么输入框总是会跟随在最底部?左边有导航栏,无论怎么缩小放大屏幕都会在当前问答展示界面的水平线中间?
难道是通过 position: fixed; 来实现的?但是它怎么能够解决第二个问题呢?先打开控制台看看。
在问答界面,输入框是一直被挤在最下方的,通过检查控制台会发现输入框好像会一直跟随在屏幕最下方?
但是随着控制台一直向上拉长,输入框又会被控制台覆盖?
说明根本不是通过固定定位来实现的效果。
下面来实现一下它的这种效果:这里展示的是最外层容器的布局。
<!-- 根容器 -->
<div class="chat">
<!-- 展示容器 -->
<div v-show='!isChat' class="chat-content">
</div>
<!-- 对话界面 -->
<div v-show='isChat' class="chat-scroll-container">
</div>
<!-- 输入框 -->
<div class="input-section">
</div>
</div>
.chat {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.chat-container {
width: 100%;
max-width: 1000px;
flex: 1;
overflow-y: auto;
}
.chat-scroll-container {
height: 100%;
width: 100%;
overflow-y: auto;
flex: 1;
/* 隐藏滚动条但保留功能 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
.input-section {
width: 100%;
height: 150px;
flex-shrink: 0;
}
可以看到核心的实现,其实就是让输入框的兄弟容器将剩余空间全部占据,而 input-section 本身只需要不压缩自身的高度就可以,当屏幕缩小后,flex:1;能占据的空间变小,而输入框高度不变将一直在外层容器最下方,当空间展示不下时会出现的可视区域外。
flex-shrink
- 父容器必须是弹性容器。
- 默认表示子元素固定宽度,不被压缩。
- 在父元素使用
flex-direction: column;改变了弹性方向后,表示子元素固定高度,不被压缩。 - 为 1 时,表示容器会适应父容器高度被压缩。
如何让视线跟随 AI 生成的内容
下方父容器为滚动容器,子元素为主要内容展示容器。以下介绍两种 AI 打字跟随的监听方法,控制跟随与用户操作停止跟随。
<div class="chat-scroll-container" ref="scrollContainerRef" @scroll="handleScroll">
<div class="chat-messages" ref="chatMessagesRef">
</div>
</div>
const chatMessagesRef = ref(null);
// 滚动容器引用
const scrollContainerRef = ref(null);
// 是否启用自动滚动跟随
const enableAutoScroll = ref(true);
// 上次滚动位置
let lastScrollTop = 0;
const handleScroll = throttle(() => {
const el = scrollContainerRef.value;
if (!el) return;
const { scrollTop, scrollHeight, clientHeight } = el;
// 判断当前是否已经在底部(留20px容差)
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 20;
// 如果用户在向上滚动超过阈值,取消自动跟随
if (isAtBottom === false && scrollTop < lastScrollTop) {
const upDistance = lastScrollTop - scrollTop;
if (upDistance > 10) {
enableAutoScroll.value = false;
}
}
// 如果滚动到底部,重新开启自动跟随
if (isAtBottom) {
enableAutoScroll.value = true;
}
// 记录本次滚动位置
lastScrollTop = scrollTop;
}, 100);
.chat-scroll-container{
height: 1000px;
.chat-messages{
// 高度由内容支撑
}
}
MutationObserver
监听容器的变化,包括高度、内容变化、DOM的操作等等。大多都抛弃该做法
-
触发次数极多
-
性能开销大
-
容易抖动、重复触发
-
性能不如ResizeObserver
let observer = null;
onMounted(() => {
initObserver();
});
// 初始化 MutationObserver
const initObserver = () => {
if (!scrollContainerRef.value) return;
// 断开旧的 observer
if (observer) {
observer.disconnect();
}
observer = new MutationObserver(() => {
if (!enableAutoScroll.value) return;
// 内容变化时,自动滚动到底部
scrollToBottom();
});
observer.observe(scrollContainerRef.value, {
childList: true,
subtree: true,
characterData: true,
});
};
// 滚动操作
const scrollToBottom = (smooth = true) => {
nextTick(() => {
if (!scrollContainerRef.value) return;
scrollContainerRef.value.scrollTo({
top: scrollContainerRef.value.scrollHeight,
behavior: smooth ? 'smooth' : 'instant',
});
});
};
ResizeObserver
监听容器是否发生尺寸变化,而不是滚动容器。操作跟随需要操作滚动容器。
// 启用监听
onMounted(() => {
const ro = new ResizeObserver(() => {
if (enableAutoScroll.value) {
scrollToBottom();
}
});
ro.observe(chatMessagesRef.value); // 监听高度变化容器
});
// 滚动操作,操作滚动容器
const scrollToBottom = (smooth = true) => {
nextTick(() => {
if (!scrollContainerRef.value) return;
scrollContainerRef.value.scrollTo({
top: scrollContainerRef.value.scrollHeight,
behavior: smooth ? 'smooth' : 'instant',
});
});
};