背景
随着人工智能技术的发展,AI 问答系统在各个领域得到了广泛应用。为了提升用户体验,增加互动性和趣味性,很多问答系统引入了逐字加载特效。这种特效模拟了人类打字的过程,使得回答内容的展示更加生动自然。
实现原理
逐字加载特效的核心在于逐步将内容添加到页面上,而不是一次性显示所有内容。这可以通过 JavaScript
的 setInterval
函数来实现。具体来说,我们可以在每次定时器触发时,将内容的一部分添加到页面上,直到全部内容加载完毕。
代码示例
1. 创建模拟接口
首先,我们需要创建一个模拟接口,返回需要逐字加载的内容。这里我们使用一个简单的 Promise 来模拟异步请求:
let data = `<h2>《钢铁是怎样炼成的》简介</h2><p>《钢铁是怎样炼成的》是前苏联作家尼古拉·奥斯特洛夫斯基创作的一部长篇小说,首次出版于1934年。这部小说以主人公保尔·柯察金的成长经历为主线,展现了他在革命斗争中的成长历程,以及他如何在艰苦的环境中锻炼成一名坚强的共产主义战士。</p><h2>主要内容</h2><ol><li>少年时期<ul><li>保尔·柯察金出生在一个贫穷的工人家庭,从小经历了许多苦难。他在学校受到不公正的待遇,后来辍学成为一名铁路工人。</li><li>他接触到了革命思想,逐渐认识到社会的不公和阶级压迫。</li></ul></li><li>革命斗争<ul><li>保尔加入了布尔什维克党,积极参与革命活动。他在战斗中表现英勇,多次负伤,但始终坚定地站在革命的最前线。</li><li>他在战争中结识了许多志同道合的战友,共同为推翻旧制度、建立新社会而奋斗。</li></ul></li><li>建设时期<ul><li>革命胜利后,保尔投身于国家的建设事业。他在铁路建设中发挥了重要作用,克服了种种困难,完成了艰巨的任务。</li><li>他在工作中表现出色,被提拔为共青团的重要干部。</li></ul></li><li>伤病与康复<ul><li>由于长期的劳累和战斗,保尔的身体逐渐恶化,最终因病重而无法继续工作。他多次住院治疗,但始终保持着乐观和坚强的精神。</li><li>在病床上,保尔开始写作,最终完成了自传体小说《钢铁是怎样炼成的》。</li></ul></li></ol><h2>主题思想</h2><ul><li>坚韧不拔的精神:保尔·柯察金在面对各种困难和挑战时,始终保持着坚定的信念和顽强的意志。他的故事激励了一代又一代的人,成为无数人心中的英雄。</li><li>革命理想:小说通过保尔的成长历程,展现了革命者的崇高理想和无私奉献精神。保尔为了实现共产主义理想,不惜牺牲个人的一切。</li><li>集体主义:保尔的成功离不开集体的支持和帮助。小说强调了集体的力量和个人奋斗的结合,展示了团结协作的重要性。</li></ul><h2>影响</h2><p>《钢铁是怎样炼成的》不仅在苏联广为流传,也被翻译成多种语言,在世界范围内产生了广泛的影响。它被誉为一部激励人们克服困难、追求理想的经典之作,对许多读者产生了深远的启发和鼓舞。</p><h2>结语</h2><p>《钢铁是怎样炼成的》是一部充满激情和力量的小说,通过保尔·柯察金的故事,展现了一个人在逆境中成长的历程。这部作品不仅是文学上的瑰宝,更是精神上的灯塔,激励着每一个读过它的人在生活的道路上勇往直前。</p>`
export function getAnswer() {
return new Promise((resolve, reject) => {
let res = {
msg: "success",
code: 0,
data
}
resolve(res);
}).catch(error => {
reject(error);
})
}
2. 创建 Vue 组件
创建一个 Vue 组件来展示问答界面,并实现逐字加载特效。
<template>
<div>
<el-dialog width="68%" :visible.sync="isShow" :before-close="close" class="kefu" :show-close="false"
:close-on-click-modal="false">
<div class="kefu-con">
<i class="el-icon-close close" @click="close" />
<div class="header">
<img :src="kefuImg" alt="">
<div class="title">小奶龙智能问答助手</div>
</div>
<div class="container" ref="container">
<div class="content" ref="content">
<div v-for="item in messageForm">
<div class="reply-container" v-if="item.type === 'reply'">
<div class="reply-content">
<div class="img-con">
<img :src="kefuImg" alt="">
</div>
<div class="reply" @click="handleProxyClick">
<div v-if="item.isXml" v-html="item.content"></div>
<p v-else>
{{ item.content }}
</p>
</div>
</div>
</div>
<div class="qs-container" v-if="item.type === 'qs'">
<div class="qs-content">
<div class="qs">
<p>
{{ item.content }}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="footer">
<img :src="kefuImg" alt="">
<div class="question-con">
<el-input class="ipt" v-model="question" placeholder="请输入想咨询的问题"></el-input>
<el-button class="btn" type="warning" round @click="send">发送</el-button>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script>
import { getAnswer } from '@/api/checkin.js';
export default {
data() {
return {
isShow: false,
kefuImg: require("@/assets/images/headshot.png"),
question: '',
messageForm: [{
content: "<p>您好, 我是您的小奶龙,你的智能助手。 你可以问我编码相关的问题,也可以一起更高效、更高质量地完成编码工作。比如 “<span class='quick' style='color: #1b6ef3;cursor: pointer;' @click='quick'>动态路由实现</span>”, “<span class='quick' style='color: #1b6ef3;cursor: pointer;' @click='quick'>移动端适配</span>” 等等一些问题。</p>",
type: "reply", // reply 回答 qs 问题
isXml: true,
},
{
content: '动态路由实现',
type: "qs", // reply 回答 qs 问题
isXml: false,
}],
}
},
methods: {
handleProxyClick(event) {
// 获取触发事件的目标元素 event 事件对象
const target = event.target;
// 判断目标元素是否包含指定类名
if (target.classList.contains('quick')) {
// 传递目标元素的文本内容
this.quick(target.outerText);
}
},
quick(text) {
console.log('quick方法触发了::: ');
this.question = text;
// 发送
this.send();
},
show() {
this.isShow = true;
},
close() {
this.isShow = false;
},
send() {
let NewQuestion = this.question.trim();
if (NewQuestion === '') {
return;
}
this.messageForm.push({
content: NewQuestion,
type: 'qs',
isXml: false,
});
setTimeout(async () => {
let response = await getAnswer(NewQuestion);
this.messageForm.push({
content: "",
type: 'reply',
isXml: true,
});
// 逐字加载效果
let content = response.data;
let index = 0;
let tempContent = ''; // 用于存储当前加载的部分内容
const interval = setInterval(() => {
if (index < content.length) {
tempContent += content[index];
index++;
// 检查是否可以安全地插入到 DOM 中
try {
const parser = new DOMParser();
const doc = parser.parseFromString(tempContent, 'text/html');
console.log('tempContent::: ', tempContent);
if (!doc.getElementsByTagName('parsererror').length) {
this.messageForm[this.messageForm.length - 1] = {
content: tempContent,
type: 'reply',
isXml: true,
};
this.$forceUpdate();
}
} catch (error) {
console.error('Error parsing XML:', error);
}
} else {
clearInterval(interval);
this.question = '';
this.$nextTick(() => {
this.scrollToBottom();
});
}
}, 100); // 调整间隔时间以控制加载速度
})
},
scrollToBottom() {
const content = this.$refs.content;
this.$refs.container.scrollTo(0, content.scrollHeight);
}
},
}
</script>
效果展示
总结
当用户点击“发送”按钮时,将用户的输入添加到消息列表中,并调用API获取需要渲染的数据。在loadReply
方法中,使用 setInterval
来逐字加载内容。每次加载一个字符后,使用 DOMParser
来解析当前的内容片段,确保其不会导致DOM错误。如果解析成功,将内容更新到消息列表中,并强制 Vue 重新渲染。为了确保用户始终能看到最新的消息,在加载完成后调用 scrollToBottom
方法,将滚动条滚动到底部。