利用vue完成一个ai问答框组件。需要时直接引用。


vue3实现上面逻辑 用到的el-drawer组件,从页面右下角弹出问答框,用户可以输入问题。
如果用户咨询的问题,后台返回了很多答案,需要用户选择问题描述,这部分逻辑在前台处理
组件的整体代码如下,包装成一个弹窗组件,需要的时候引用就行。
 

<template>
  <div class="ai-drawer-box">
    <el-drawer class="ai-drawer" :modal-append-to-body="false" v-model="aiDrawer" z-index="2005" :direction="direction" :before-close="handleClose" placement="right-end">
      <template #header>
        <div class="custom-header">
          <div class="ai-img">
            <img :src="aiLogo" />
          </div>
          <div class="title">
            <h3>{{ aiTitle }}</h3>
          </div>
        </div>
      </template>
      <template #default>
        <div class="talkShow" id="talkShow">
          <div v-for="(item, index) in talkList" :key="index">
            <!-- 机器人回答 -->
            <div v-show="item.person == 'mechaniCal' && !item.multipleAnswers" class="mechanicalTalk">
              <span class="talk_header"><img :src="aiLogo" alt="" /></span>
              <span class="talk_detail">
                <span class="time">
                  <span style="padding-right: 5px; font-size: 14px">{{ '问答小助手' }}</span>
                  <span>{{ item.time }} </span>
                </span>
                <span class="talk_text" v-show="Array.isArray(item.say)">
                  <span v-for="(str, i) in item.say" :key="i">
                    <span v-html="findAndColorText(str)"></span>
                    <br v-show="i != item.say.length - 1" />
                  </span>
                </span>
                <span class="talk_text" v-show="typeof item.say == 'string'">{{ item.say }}</span>
              </span>
            </div>
            <!-- 用户提问 -->
            <div v-show="item.person == 'mineTalk'" class="mineTalk">
              <span class="talk_detail">
                <div style="margin-bottom: 5px; width: 100%">{{ item.time }}</div>
                <div class="talk_text">{{ item.say }}</div>
              </span>
            </div>
            <!-- 机器人回答数组 需要用户选择具体序号进行回复 -->
            <div v-show="item.person == 'mechaniCal' && item.multipleAnswers" class="mechanicalTalk mechanicalTalkChhoose">
              <span class="talk_header"><img :src="aiLogo" alt="" /></span>
              <span class="talk_detail">
                <span class="time">
                  <span style="padding-right: 5px">{{ '问答小助手' }}</span>
                  <span>{{ item.time }} </span>
                </span>
                <span class="talk_text">
                  <span class="desc_tips">{{ '您咨询的问题可能是以下哪个?请输入序号获取回答。' }}</span
                  ><br />
                  <span class="loop_answer" v-for="(k, ind) in item.answerArr" :key="ind">
                    <span> {{ k.orderSeq + ':' }}</span>
                    <span style="color: #0e8eff; padding-left: 3px">{{ k.issueDesc }}</span>
                    <br />
                  </span>
                </span>
              </span>
            </div>
          </div>
        </div>
      </template>
      <template #footer>
        <div class="chat-input">
          <el-input type="textarea" style="border: none" class="pass_input" rows="4" @keyup.enter.native="sendMessage" v-model.trim="message" placeholder="很高兴为您服务,请描述您的问题"> </el-input>
          <el-button style="position: absolute; bottom: 6px; right: 8px" class="send-btn" @click="sendMessage">发送</el-button>
        </div>
      </template>
    </el-drawer>
  </div>
</template>
<script lang="ts">
import { toRefs, defineComponent, reactive } from 'vue';
import aiLogo from '@/assets/images/ai-person.png';  // ai的头像 可以自定义
import baseService from '@/service/base-service'; // 包装好的请求逻辑 替换成你们自己项目的
import { ElMessage } from 'element-plus';

export default defineComponent({
  name: 'myAiAnswer',
  props: {},
  setup(props, context) {
    const formatTime = (date: any) => {
      const hours = date.getHours().toString().padStart(2, '0');
      const minutes = date.getMinutes().toString().padStart(2, '0');
      const seconds = date.getSeconds().toString().padStart(2, '0');
      return `${hours}:${minutes}:${seconds}`;
    };
    const state = reactive({
      direction: 'rtl',
      aiTitle: '问答小助手',
      aiDrawer: false,
      message: '',
      answerUrl: '这里写你们请求调用的后台接口',
      initMechaniCal: { time: formatTime(new Date()), person: 'mechaniCal', say: '您好,我是ai小助手,有什么可以帮您的?' },
      talkList: [] as any,
      hasAnswer: false,
      // 这部分注释内容是聊天记录数组的数据格式 请参考
      // { time: formatTime(new Date()), person: 'mineTalk', say: '我问报销' },
      // {
      //   time: formatTime(new Date()),
      //   multipleAnswers: true,
      //   person: 'mechaniCal',
      //   answerArr: [
      //     { orderSeq: 1, issueDesc: '招待费申请晚于招待费报销怎么办' },
      //     { orderSeq: 2, issueDesc: '在差旅报销单据中出差类型选择培训出差' },
      //     { orderSeq: 3, issueDesc: '差旅报销是否可以多人一起提报销单' },
      //     { orderSeq: 4, issueDesc: '报销次数以及时间有没有限制' },
      //   ],
      // },
    });
    const handleClose = () => {
      state.aiDrawer = false; // 关掉弹窗
      state.talkList = []; // 清掉聊天记录
      state.message = ''; // 清掉对话框输入内容
      state.hasAnswer = false; // 清掉答案请求状态
    };
    // 初始化ai问答
    const aiInit = () => {
      state.message = '';
      state.aiDrawer = true;
      state.talkList.push(state.initMechaniCal);
    };
    const getLastItem = (arr: any) => {
      const filteredArr = arr.filter((item: any) => {
        return item.multipleAnswers;
      });
      return filteredArr.pop();
    };
    const findAndColorText = (str: any) => {
      // 匹配连续的英文或数字 这个一般返回的是单据编号或者是微信号码
      const regex = /(\b[A-Za-z0-9]+\b)/g;
      const emailRegex = /(\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b)/g;
      // 假设手机号为11位数字
      const phoneRegex = /(\b\d{11}\b)/g;
      // 假设电话号码格式为xxx-xxx-xxxx或xxx.xxx.xxxx或xxx xxx xxxx
      const phoneNumberRegex = /(\b\d{3}[-\.\s]??\d{3}[-\.\s]??\d{4}\b)/g;
      const matches = [...str.matchAll(emailRegex), ...str.matchAll(phoneRegex), ...str.matchAll(phoneNumberRegex), ...str.matchAll(regex)];
      if (matches.length === 0) {
        return str;
      }

      let formattedText = str;
      matches.forEach((match) => {
        const matchedText = match[0];
        const replacement = `<span style="color: #0e8eff">${matchedText}</span>`;
        formattedText = formattedText.replace(matchedText, replacement);
      });

      return formattedText;
    };

    // 发送问题
    const sendMessage = () => {
      if (state.message == '' || !state.message) {
        return false;
      }
      // 不需要请求接口 : 当返回多条 需要用户输入序号 咨询某个问题时 直接取前台的回答数组里返回
      const lastMultipleAnswers = getLastItem(state.talkList); // 聊天记录里 最后一组数组形式的answer
      if (lastMultipleAnswers != undefined) {
        // tempory前台匹配的答案 如有 直接返回  没有则请求接口
        const tempory = lastMultipleAnswers.answerArr.filter((K: any) => {
          return K.orderSeq == state.message;
        });
        if (tempory && tempory.length == 1 && tempory[0].say && state.hasAnswer) {
          state.hasAnswer = true;
          state.talkList.push({ time: formatTime(new Date()), person: 'mineTalk', say: state.message });
          state.talkList.push({ time: formatTime(new Date()), person: 'mechaniCal', say: tempory[0].say });
          state.message = '';
          setBoxPosition();
          return false;
        } else {
          state.hasAnswer = false;
        }
      }
      //  正常提问 前台没有已知答案 需要请求接口 处理接口回答
      if (!state.hasAnswer) {
        // 先问
        state.talkList.push({ time: formatTime(new Date()), person: 'mineTalk', say: state.message });
        setBoxPosition();

        const data = {
          issueDesc: state.message,
        };
        state.message = '';
        baseService.post(state.answerUrl, data).then((res) => {
          if (res.code != 0) {
           return ElMessage.error(res.msg)
          }
          if (res.data.length == 0) {
            state.talkList.push({ time: formatTime(new Date()), person: 'mechaniCal', say: '我没听清楚您说了什么,请您换一个说法试试!' });
            setBoxPosition();
          } else if (res.data.length == 1) {
            // 不需要处理
            // if (res.data[0].issueAnswer.indexOf('。') != -1) {
            //   res.data[0].issueAnswer = res.data[0].issueAnswer.split('。');
            //   console.log(res.data[0].issueAnswer);
            // }

            state.talkList.push({ time: formatTime(new Date()), person: 'mechaniCal', say: res.data[0].issueAnswer });
            setBoxPosition();
          } else if (res.data.length > 1) {
            const obj = {
              time: formatTime(new Date()),
              multipleAnswers: true,
              person: 'mechaniCal',
              answerArr: [],
            } as any;
            res.data.map((ans: any) => {
              const data = {
                orderSeq: ans.orderSeq,
                issueDesc: ans.issueDesc,
                say: ans.issueAnswer,
                id: ans.id,
              };
              obj.answerArr.push(data);
            });
            state.talkList.push(obj);
            setBoxPosition();
            state.hasAnswer = true;
          }
        });
      }
    };

    const setBoxPosition = () => {
      setTimeout(() => {
        let container: any = document.getElementById('talkShow');
        container.scrollIntoView({ behavior: 'smooth', block: 'end' });
      }, 20);
    };
    // 流式输出文字逐个展示
    const flowShowing = async (data: any) => {
      let text = '';
      const length = state.talkList.length;
      for (let i = 0; i < data.length; i++) {
        text += data.charAt(i);
        await new Promise((resolve) => {
          setTimeout(resolve, 50);
        });
        state.talkList[length - 1].say = text;
      }
    };

    return {
      ...toRefs(state),
      handleClose,
      aiInit,
      aiLogo,
      sendMessage,
      formatTime,
      findAndColorText,
    };
  },
});
</script>
<style scoped lang="less">
.ai-drawer-box:deep(.el-drawer) {
  width: 30% !important;
  height: 60% !important;
  top: auto !important;
  padding: 10px;
  background-color: rgb(226, 226, 226);
  box-shadow: 0px 0 22px #d6d6d6 inset;
}
.ai-drawer-box:deep(.el-drawer__header) {
  margin-bottom: 0 !important;
  padding: 0;
  border-radius: 5px 5px 0 0;
  background-color: rgb(255, 255, 255);
  padding-right: 10px;
}
.ai-drawer-box:deep(.el-drawer__body) {
  margin-bottom: 0 !important;
  background-color: #f5f5f5;
  padding: 10px 5px 10px 2px;
  overflow-y: auto;
  border-top: 12px solid #f5f5f5;
}
.ai-drawer-box:deep(.el-drawer__footer) {
  padding: 0 !important;
}
.custom-header {
  display: flex;
  justify-content: flex-start;
  align-items: center;
  padding-left: 10px;
  .ai-img {
    width: 43px;
    height: 43px;
    background-color: #383838;
    //  ~ 'var(--color-primary)'
    text-align: center;
    display: flex;
    flex-direction: column;
    justify-content: space-around;
    border-radius: 27px;
    margin-right: 20px;
    img {
      width: 35px;
      height: 42px;
      border-radius: 50%;
      margin: 0 auto;
    }
  }
  .title {
    color: #383838;
  }
}
.chat-input {
  height: 90px;
  position: relative;
  :deep(.el-textarea__inner) {
    border: none !important;
    box-shadow: none !important;
    resize: none;
    border-radius: 0 0 5px 5px;
  }
}
.talkShow {
  padding-bottom: 10px;
  // 机器人回答样式
  .mechanicalTalk {
    margin: 10px;
    display: flex;
    .talk_header {
      flex-shrink: 0;
      width: 43px;
      height: 43px;
      background-color: #383838;
      text-align: center;
      display: flex;
      flex-direction: column;
      justify-content: space-around;
      border-radius: 27px;
      img {
        width: 35px;
        height: 42px;
        border-radius: 50%;
        margin: 0 auto;
      }
    }
    .talk_detail {
      margin-left: 8px;
      .time {
        width: 100%;
        margin-bottom: 5px;
        display: inline-block;
      }
      .talk_text {
        display: inline-block;
        background: white;
        border-radius: 5px;
        padding: 10px 10px;
        border: 1px solid rgb(214, 216, 219);
        border-top-left-radius: 0px;
        word-break: break-all;
        text-align: left;
        color: #383838;
      }
    }
  }
  // 用户提问
  .mineTalk {
    margin: 10px;
    text-align: right;
    .talk_detail {
      .talk_text {
        display: inline-block;
        border-radius: 5px;
        border-top-right-radius: 0px;
        background: #0088ff;
        color: #fff;
        padding: 10px 10px;
        word-break: break-all;
        text-align: left;
      }
    }
  }
  .mechanicalTalkChhoose {
    .talk_text {
      span {
        display: inline-block;
      }
      .desc_tips {
        margin-bottom: 4px;
      }
      .loop_answer {
        margin-bottom: 2px;
        width: 100%;
      }
    }
  }
}
</style>

上面的代码还处理了多选逻辑,对话内容如下:

用户问的问题有多种回复时,后端会返回数组,前台只需要输入序号,就可以得到对应答案,这部分逻辑是在前台处理,详情看代码就可以。需要用的可以复制,不用的这段逻辑删掉就行。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值