思路:markdown-it和highlight是为了解决打字机过程中,写代码时,没有高亮效果的,同时还解决了,在打字过程中,滚动条没有随内容而自动滚动的问题
<template>
<div class="wd1200">
<div class="expand">
<div class="shadow mt10">
<div class="pad2030 flex between border-b">
<div class="flex-se">
<div class="f22 mr20">智能咨询</div>
</div>
</div>
<div ref="scrollable" @scroll="handleScroll" class="pad2030 bfa scroll msg-container border-b f14 scrollbar">
<div class="flex-st mb10">
<Avatar name="数据库智能助手小墨" avatar="https://oss-emcsprod-public.modb.pro/image/avatar/401097_1676426340830.jpeg" class="wd50 mr10"></Avatar>
<div>
<div class="mt5 c6">数据库智能助手小墨</div>
<div class="msg-box pad10 bg-active mt10">
<div>Hi,我是数据库智能助手小墨,您可以向我描述问题。紧急问题请联系墨天轮小助手:modb666。</div>
<img class="block mt8" src="https://js-cdn.modb.cc/image/modb666.jpg" width="80px" alt="暂无图片">
</div>
</div>
</div>
<div class="mb10" v-for="(item, idx) in historyList" :key="idx">
<div class="vr" v-if="item.question">
<div class="msg-box border-own pad10 bg-blue cf p-wrap">{{ item.question }}</div>
</div>
<div class="flex-st" v-if="item.answer">
<Avatar name="数据库智能助手小墨" avatar="https://oss-emcsprod-public.modb.pro/image/avatar/401097_1676426340830.jpeg" class="wd50 mr10"></Avatar>
<div>
<div class="mt5 mb10">
<span class="c6 mr10">数据库智能助手小墨</span>
</div>
<div class="msg-box pad10 bg-active markdown-body" v-html="item.answer"></div>
</div>
</div>
</div>
<div class="mb10" v-if="currentText || btnLoading">
<div class="flex-st">
<Avatar name="数据库智能助手小墨" avatar="https://oss-emcsprod-public.modb.pro/image/avatar/401097_1676426340830.jpeg" class="wd50 mr10"></Avatar>
<div>
<div class="mt5 mb10">
<span class="c6 mr10">数据库智能助手小墨</span>
</div>
<div>
<div v-if="currentText" class="msg-box pad10 bg-active markdown-body" v-html="markdownText"></div>
<div v-else class="msg-box pad10 bg-active markdown-body">正在努力思考,请耐心等待...</div>
</div>
</div>
</div>
</div>
</div>
<div class="pad2030">
<el-input
class="no-border"
v-model.trim="consultInfo.question"
:rows="3"
type="textarea"
@keyup.enter.native="submitConsult"
:disabled="btnLoading"
placeholder="请清晰描述一下您的问题,如:有哪些课程"/>
<div class="vr mt20">
<div class="emcs-btn bold" @click="submitConsult">发送留言</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { loginmixin } from '@/mixins'
import { cbSuccess, isMobile } from '@/utils'
import { getMoaiApi, saveMoaiApi } from '@/apis'
import MarkdownIt from 'markdown-it'
import mdKatex from '@traptitech/markdown-it-katex'
import mila from 'markdown-it-link-attributes'
import hljs from 'highlight.js'
function highlightBlock(str, lang) {
return `<pre class="code-block-wrapper"><div class="code-block-header"></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`
}
const mdi = new MarkdownIt({
html: false,
linkify: true,
highlight(code, language) {
const validLang = Boolean(language && hljs.getLanguage(language));
if (validLang) {
const lang = language || '';
return highlightBlock(hljs.highlight(code, { language: lang }).value, lang);
}
return highlightBlock(hljs.highlightAuto(code).value, '');
},
})
mdi.use(mila, { attrs: { target: '_blank', rel: 'noopener' } })
mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' })
export default {
name: 'consult',
head: {
title: '智能咨询',
},
data () {
return {
consultInfo: {
question: '',
},
records: [],
historyList: [],
currentText: '',
mdText: '',
currentQuestion: '',
btnLoading: false,
isAutoScroll: true,
total: 0,
threshold: 100,
// 是否正在进行上啦加载调用
isFetching: false,
// 是否正在进行问题回答
isReadText: false,
params: {
pageNum: 1,
pageSize: 5
},
// 记录上一次滚动的高度
lashScrollTop: 0,
}
},
computed: {
...mapGetters(['isSys']),
markdownText() {
const value = this.currentText || '';
this.mdText = mdi.render(value)
return mdi.render(value);
}
},
mixins: [loginmixin],
methods: {
async fetchData () {
let { data } = await getMoaiApi(this.params)
let _historyList = data.operateCallBackObj.list
for (const item of _historyList) {
item.answer = mdi.render(item.answer)
}
this.historyList = _historyList
this.total = data.operateCallBackObj.total
},
async submitConsult(){
this.scrollToBottom();
this.isReadText = true; // 开始发送请求相当于正在进行打印
if (!this.$checkLogin()) return
let _consultInfo = JSON.parse(JSON.stringify(this.consultInfo))
if(this.btnLoading || !_consultInfo.question) return
this.btnLoading = true
let myInfo = {
action_id: 'my',
question: _consultInfo.question
}
this.currentQuestion = _consultInfo.question
this.historyList.push(myInfo)
this.consultInfo.question = ''
let { data } = await saveMoaiApi(_consultInfo)
let _this = this
if(!data.success && data.operateMessage == '发布频率太快了,先休息一下吧'){
let operateCallBackObj = {
answer: '发布频率太快了,先休息一下吧',
question: "_consultInfo.question"
}
data.operateCallBackObj = operateCallBackObj
_this.readWriter(data, 100)
}else{
cbSuccess(data, _ => {
_this.readWriter(data, 100)
})
}
},
async readWriter(data, delay) {
let _this = this
let _consult = data.operateCallBackObj
let _text = _consult.answer
for (let i = 0; i < _text.length; i++) {
_this.currentText += _text[i];
this.isReadText = true; // 正在进行问题回答
if (this.isAutoScroll) { // 只有在自动滚动模式下,才自动滚动
await this.scrollToBottom();
}
await this.sleep(delay);
if(i == _text.length - 1) {
_this.currentText = ''
this.btnLoading = false
let _actions = {
answer: _this.mdText
}
_this.historyList.push(_actions)
this.isReadText = false; // 问题回答结束
}
}
},
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
},
async scrollToBottom() {
await this.$nextTick();
// 判断当前对话框是否产生滚动条
const scrollable = this.$refs.scrollable
if(scrollable.scrollHeight <= scrollable.clientHeight) {
if (this.total > this.historyList.length) {
await this.fetchMoreData();
}
}
if (this.$refs.scrollable) this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
},
handleScroll() { // 新的滚动处理方法
const scrollable = this.$refs.scrollable;
const scrollTop = scrollable.scrollTop;
// 处理上拉加载逻辑
if (scrollTop <= 240 && !this.isReadText) {
// 判断滚轮是往下滑动 如果是往下滑动不做处理
if (scrollTop < this.lashScrollTop && this.total > this.historyList.length) {
this.fetchMoreData();
}
}
this.lashScrollTop = scrollTop;
const distanceToBottom = scrollable.scrollHeight - scrollable.scrollTop - scrollable.clientHeight;
if (distanceToBottom <= this.threshold) {
this.isAutoScroll = true; // 滚动到底部,启动自动滚动
} else {
this.isAutoScroll = false; // 用户手动滚动,停止自动滚动
}
},
// 上拉加载函数
async fetchMoreData() {
if (this.isFetching) return // 利用变量控制 防止重复触发上拉加载
if (this.total === this.historyList.length) return
this.isFetching = true; // 开始进行上拉加载
// 这里写上拉加载接口 接口回调完成之后记得重置状态
this.params.pageNum++
let { data } = await getMoaiApi(this.params) // 模拟请求状态
let _historyList = data.operateCallBackObj.list
for (const item of _historyList) {
item.answer = mdi.render(item.answer)
}
this.historyList.unshift(..._historyList)
if (this.params.pageNum > 2) {
this.$refs.scrollable.scrollTop = 200;
}
this.isFetching = false; // 上拉加载结束
},
submitItem(val){
this.consultInfo.question = val
this.submitConsult()
}
},
async mounted(){
this.isMobile = isMobile()
await this.fetchData()
this.scrollToBottom()
}
}
</script>
<style lang="stylus" scoped>
.wd1200{
min-width 1200px
box-sizing border-box
margin 0 auto
}
.msg-box {
max-width: 700px;
display: inline-block;
border-radius: 0px 16px 16px 16px;
text-align: left;
}
.border-own {
border-radius: 16px 16px 0 16px
}
.bg-blue{
background-color: #4285f4
}
.bg-active {
background-color: rgba(66,133,244,.08);
}
.border-own {
border-radius: 16px 16px 0px 16px;
}
.msg-container {
height: 60vh;
}
.scroll {
overflow-y: auto;
-ms-overflow-style: none;
scrollbar-width: none;
}
.markdown-body{
background-color rgba(66,133,244,0.08)
}
@media screen and (max-width 768px) {
.wd1200{
margin 0 auto
min-width 0px
max-width: 100%
}
.shadow{
margin-top 0
}
}
</style>