实现chatgpt聊天机器人打字机效果

思路: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 `
${lang}">${str}
`
} 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>

你可能感兴趣的:(chatgpt)