开发记录

开发记录

2020-10-22:第一版搭建

实现了侧栏插入

修复了原生botui.js每次跳动到顶部的bug

添加了pjax重载。避免换页消失

2020-10-25:添加按钮

修复了每次进入页面优先跳到页面正中的不良体验。

新增翻转按钮,移除pjax重载。

移除data-pjax,将自动加载改为手动加载。将对话主动权交给用户。

2020-11-16:版本优化

增加butterfly_v3.3.0配置方案。

botui.js简介

botui.js是一个简单的聊天机器人框架,使用它可以完成简易的脚本对话式交流。缺点是只能在自己设定的逻辑内进行有限问答,而不是像真正的AI那样智能会话。

静态资源下载

由于本教程涉及的所有修改对缩进格式等有严格要求,担心自己控制不好的可以直接下载静态资源,将压缩包内的butterfly文件夹复制到[Blogroot]\theme\目录下覆盖现有主题文件夹即可跳过以下教程的前4步,直接到主题配置文件_config.butterfly.yml中参照第5、6两步修改配置项。(已更新butterfly_v3.3.0的配置方案,根据自己的版本选择相应文件夹。资源包中的v3.3.0只提供了给所有页面都添加card_botui的index.pug写法,其他写法还是得看教程。)

修改步骤

  1. ~\[blogroot]\themes\butterfly\layout\includes\widget\目录下新建card_botui.pug,注意对齐格式。可以自定义修改按钮显示的内容。

    .card-widget.card-botui
    .card-content(style='height:320px;')
    .item-headline
    i.fas.fa-comments
    span= _p('aside.card_botui')
    #hello-akilar.botui-app-container(style='width:100%;padding:0.5px')
    bot-ui
    .facemain
    figure
    div
    span.face 要来和我聊聊么?
    span.face
    a(href='javascript:void(0);' onclick='botui_init()') 欢迎光临糖果屋
  2. 修改~\[blogroot]\themes\butterfly\layout\includes\widget\index.pug,注意对齐格式。

    对xwcker疑问的解答:

    • 关于include ./card_botui.pug!=partial(‘includes/widget/card_botui’, {}, {cache:theme.fragment_cache})两种写法的区别,其实本质上都是可以引入card_botui的,最终呈现的效果并无区别。不过后者使用了hexo自带的缓存,能够更快的生成页面。

    • 用于区分是否是butterfly_v3.3.0的关键,在于index.pug中是否存在if is_post()这个判断方法。

        #aside_content.aside_content
    if theme.aside.card_author.enable
    include ./card_author.pug
    .sticky_layout
    + if theme.aside.card_botui.enable
    + include ./card_botui.pug
    if theme.aside.card_announcement.enable
    include ./card_announcement.pug
    if theme.aside.card_recent_post.enable
    include ./card_recent_post.pug
    if theme.newest_comments.enable
    include ./card_newest_comment.pug
    if theme.ad && theme.ad.aside
          #aside_content.aside_content
    if theme.aside.card_author.enable
    !=partial('includes/widget/card_author', {}, {cache:theme.fragment_cache})
    if theme.aside.card_announcement.enable
    !=partial('includes/widget/card_announcement', {}, {cache:theme.fragment_cache})
    + if theme.aside.card_botui.enable
    + !=partial('includes/widget/card_botui', {}, {cache:theme.fragment_cache})
    .sticky_layout
    if is_post()
    if showToc
    include ./card_post_toc.pug
    if theme.aside.card_recent_post.enable
    !=partial('includes/widget/card_recent_post', {}, {cache:theme.fragment_cache})
          #aside_content.aside_content
    if theme.aside.card_author.enable
    !=partial('includes/widget/card_author', {}, {cache:theme.fragment_cache})
    if theme.aside.card_announcement.enable
    !=partial('includes/widget/card_announcement', {}, {cache:theme.fragment_cache})
    .sticky_layout
    if is_post()
    if showToc
    include ./card_post_toc.pug
    + if theme.aside.card_botui.enable
    + !=partial('includes/widget/card_botui', {}, {cache:theme.fragment_cache})
    if theme.aside.card_recent_post.enable
    !=partial('includes/widget/card_recent_post', {}, {cache:theme.fragment_cache})
        #aside_content.aside_content
    if theme.aside.card_author.enable
    !=partial('includes/widget/card_author', {}, {cache:theme.fragment_cache})
    if theme.aside.card_announcement.enable
    !=partial('includes/widget/card_announcement', {}, {cache:theme.fragment_cache})
    .sticky_layout
    if is_post()
    if showToc
    include ./card_post_toc.pug
    if theme.aside.card_recent_post.enable
    !=partial('includes/widget/card_recent_post', {}, {cache:theme.fragment_cache})
    if theme.ad && theme.ad.aside
    !=partial('includes/widget/card_ad', {}, {cache:theme.fragment_cache})
    else
    + if theme.aside.card_botui.enable
    + !=partial('includes/widget/card_botui', {}, {cache:theme.fragment_cache})
    if theme.aside.card_recent_post.enable
    !=partial('includes/widget/card_recent_post', {}, {cache:theme.fragment_cache})
    if theme.ad && theme.ad.aside
    !=partial('includes/widget/card_ad', {}, {cache:theme.fragment_cache})
    if theme.newest_comments.enable
    !=partial('includes/widget/card_newest_comment', {}, {cache:theme.fragment_cache})
    if theme.aside.card_categories.enable
    !=partial('includes/widget/card_categories', {}, {cache:theme.fragment_cache})
  3. ~\[blogroot]\themes\butterfly\source\css\目录下新建card_botui.css,

    @import url(https://fonts.googleapis.com/css?family=Open+Sans);
    .botui-container{
    font-size:14px;
    background-color:#fff;
    font-family:"Open Sans",sans-serif
    }
    .botui-messages-container{
    padding:10px 20px
    }
    .botui-actions-container{
    padding:10px 20px
    }
    .botui-message{
    min-height:30px
    }
    .botui-message-content{
    padding:7px 13px;
    border-radius:15px;
    color:#595a5a;
    background-color:#ebebeb
    }
    .botui-message-content.human{
    color:#f7f8f8;
    background-color:#919292
    }
    .botui-message-content.text{
    line-height:1.3
    }
    .botui-message-content.loading{
    background-color:rgba(206,206,206,.5);
    line-height:1.3;
    text-align:center
    }
    .botui-message-content.embed{
    padding:5px;
    border-radius:5px
    }
    .botui-message-content-link{
    color:#919292
    }
    .botui-actions-text-input{
    border:0;
    outline:0;
    border-radius:0;
    padding:5px 7px;
    font-family:"Open Sans",sans-serif;
    background-color:transparent;
    color:#595a5a;
    border-bottom:1px solid #919292
    }
    .botui-actions-text-submit{
    color:#fff;
    width:30px;
    padding:5px;
    height:30px;
    line-height:1;
    border-radius:50%;
    border:1px solid #919292;
    background:#777979
    }
    .botui-actions-buttons-button{
    border:0;
    color:#fff;
    line-height:1;
    cursor:pointer;
    font-size:14px;
    font-weight:500;
    padding:7px 15px;
    border-radius:4px;
    font-family:"Open Sans",sans-serif;
    background:#777979;
    box-shadow:2px 3px 4px 0 rgba(0,0,0,.25)
    }
    .botui-actions-text-select{
    border:0;
    outline:0;
    border-radius:0;
    padding:5px 7px;
    font-family:"Open Sans",sans-serif;
    background-color:transparent;
    color:#595a5a;
    border-bottom:1px solid #919292
    }
    .botui-actions-text-searchselect{
    border:0;
    outline:0;
    border-radius:0;
    padding:5px 7px;
    font-family:"Open Sans",sans-serif;
    background-color:transparent;
    color:#595a5a;
    border-bottom:1px solid #919292
    }
    .botui-actions-text-searchselect .dropdown-toggle{
    border:none!important
    }
    .botui-actions-text-searchselect .selected-tag{
    background-color:transparent!important;
    border:0!important
    }
    .slide-fade-enter-active{
    transition:all .3s ease
    }
    .slide-fade-enter,.slide-fade-leave-to{
    opacity:0;
    transform:translateX(-10px)
    }
    .dot{
    width:.5rem;
    height:.5rem;
    border-radius:.5rem;
    display:inline-block;
    background-color:#919292
    }
    .dot:nth-last-child(1){
    margin-left:.3rem;
    animation:loading .6s .3s linear infinite
    }
    .dot:nth-last-child(2){
    margin-left:.3rem;
    animation:loading .6s .2s linear infinite
    }
    .dot:nth-last-child(3){
    animation:loading .6s .1s linear infinite
    }
    @keyframes loading{
    0%{transform:translate(0,0);
    background-color:#ababab
    }
    25%{transform:translate(0,-3px)
    }
    50%{transform:translate(0,0);
    background-color:#ababab
    }
    75%{transform:translate(0,3px)
    }
    100%{transform:translate(0,0)}
    }

    /*
    * botui 0.3.9
    * A JS library to build the UI for your bot
    * https://botui.org
    *
    * Copyright 2019, Moin Uddin
    * Released under the MIT license.
    */
    a.botui-message-content-link:focus {
    outline: thin dotted
    }

    a.botui-message-content-link:focus:active, a.botui-message-content-link:focus:hover {
    outline: 0
    }

    form.botui-actions-text {
    margin: 0
    }

    button.botui-actions-buttons-button, input.botui-actions-text-input {
    margin: 0;
    font-size: 100%;
    line-height: normal;
    vertical-align: baseline
    }

    button.botui-actions-buttons-button::-moz-focus-inner, input.botui-actions-text-input::-moz-focus-inner {
    border: 0;
    padding: 0
    }

    button.botui-actions-buttons-button {
    cursor: pointer;
    -webkit-appearance: button
    }

    .botui-app-container {
    width: 100%;
    height: 100%;
    line-height: 1
    }

    .botui-container {
    width: 100%;
    height: 100%;
    overflow-y: auto;
    overflow-x: hidden
    }

    .botui-message {
    margin-top: 10px;
    margin-bottom:10px
    min-height: 20px
    }

    .botui-message:after {
    display: block;
    content: "";
    clear: both
    }

    .botui-message-content {
    width: auto;
    max-width: 85%;
    display: inline-block
    }

    .botui-message-content.human {
    float: right
    }

    .botui-message-content iframe {
    width: 100%
    }

    .botui-message-content-image {
    margin: 2px 0;
    display: block;
    max-width: 200px;
    max-height: 200px
    }

    .botui-message-content-link {
    text-decoration: underline
    }

    .profil {
    position: relative;
    border-radius: 50%
    }

    .profil.human {
    float: right;
    margin-left: 0
    }

    .profil.agent {
    float: left;
    margin-right: 0
    }

    .profil>img {
    width: 26px;
    height: 26px;
    border: 1px solid #e8e8e8
    }

    .profil>img.agent {
    content: url(http://decodemoji.com/img/logos/blue_moji_hat.svg);
    border-radius:50%
    }
    button.botui-actions-buttons-button{
    margin-top:10px;
    margin-bottom:10px
    }
    button.botui-actions-buttons-button:not(:last-child){
    margin-right:10px
    }
    @media (min-width:400px){
    .botui-actions-text-submit{
    display:none
    }
    }

    .botui.botui-container::-webkit-scrollbar {
    width: 0 !important;
    }
    /* 按钮动效 */
    .facemain { font-size: 100%; padding: 0; margin: 0;}

    /* Reset */
    .facemain *,
    .facemain *:after,
    .facemain *:before {
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
    }

    /* Clearfix hack by Nicolas Gallagher: http://nicolasgallagher.com/micro-clearfix-hack/ */
    .clearfix:before,
    .clearfix:after {
    content: " ";
    display: table;
    }

    .clearfix:after {
    clear: both;
    }

    .facemain{
    height: 250px;
    width:300px;
    margin: 0 auto;
    border-radius: 10px;
    vertical-align:middle;
    display:table-cell;
    background: #494A5F;
    color: #D5D6E2;
    font-weight: 500;
    font-size: 1.05em;
    font-family: "Microsoft YaHei","Segoe UI", "Lucida Grande", Helvetica, Arial,sans-serif;
    }
    .facemain a{ color: rgba(255, 255, 255, 0.6);outline: none;text-decoration: none;-webkit-transition: 0.2s;transition: 0.2s;}
    .facemain a:hover,a:focus{color:#74777b;text-decoration: none;}

    .facemain figure {
    width: 200px;
    height: 60px;
    margin: 50px auto;
    cursor: pointer;
    perspective: 500px;
    -webkit-perspective: 500px;
    }

    .facemain figure div {
    height: 100%;
    transform-style: preserve-3d;
    -webkit-transform-style: preserve-3d;
    transition: 0.25s;
    -webkit-transition: 0.25s;
    }

    .facemain figure:hover div {
    transform: rotateX(-90deg);
    }

    .facemain span.face {
    width: 100%;
    height: 100%;
    position: absolute;
    box-sizing: border-box;
    border: 5px solid #fff;
    font-family: 'Source Sans Pro',sans-serif;
    line-height: 50px;
    font-size: 17pt;
    text-align: center;
    text-transform: uppercase;
    }

    .facemain span.face:nth-child(1) {
    color: #fff;
    transform: translate3d(0, 0, 30px);
    -webkit-transform: translate3d(0, 0, 30px);
    }

    .facemain span.face:nth-child(2) {
    color: #094b2c;
    background: #fff;
    transform: rotateX(90deg) translate3d(0, 0, 30px);
    -webkit-transform: rotateX(90deg) translate3d(0, 0, 30px);
    }
  4. ~\[blogroot]\themes\butterfly\source\js\目录下新建botui.jsbotui_init.js,

    • botui.js

      /*
      * botui 0.3.9
      * A JS library to build the UI for your bot
      * https://botui.org
      *
      * Copyright 2019, Moin Uddin
      * Released under the MIT license.
      */

      (function (root, factory) {
      "use strict";
      if (typeof define === 'function' && define.amd) {
      define([], function () {
      return (root.BotUI = factory(root));
      });
      } else {
      root.BotUI = factory(root);
      }
      }(typeof window !== 'undefined' ? window : this, function (root, undefined) {
      "use strict";

      var BotUI = (function (id, opts) {

      opts = opts || {};

      if(!id) {
      throw Error('BotUI: Container id is required as first argument.');
      }

      if(!document.getElementById(id)) {
      throw Error('BotUI: Element with id #' + id + ' does not exist.');
      }

      if(!root.Vue && !opts.vue) {
      throw Error('BotUI: Vue is required but not found.');
      }

      var _botApp, // current vue instance.
      _options = {
      debug: false,
      fontawesome: true,
      searchselect: true
      },
      _container, // the outermost Element. Needed to scroll to bottom, for now.
      _interface = {}, // methods returned by a BotUI() instance.
      _actionResolve,
      _markDownRegex = {
      icon: /!\(([^\)]+)\)/igm, // !(icon)
      image: /!\[(.*?)\]\((.*?)\)/igm, // ![aleternate text](src)
      link: /\[([^\[]+)\]\(([^\)]+)\)(\^?)/igm // [text](link) ^ can be added at end to set the target as 'blank'
      },
      _fontAwesome = 'https://use.fontawesome.com/ea731dcb6f.js',
      _esPromisePollyfill = 'https://cdn.jsdelivr.net/es6-promise/4.1.0/es6-promise.min.js', // mostly for IE
      _searchselect = "https://unpkg.com/vue-select@2.4.0/dist/vue-select.js";

      root.Vue = root.Vue || opts.vue;

      // merge opts passed to constructor with _options
      for (var prop in _options) {
      if (opts.hasOwnProperty(prop)) {
      _options[prop] = opts[prop];
      }
      }

      if(!root.Promise && typeof Promise === "undefined" && !opts.promise) {
      loadScript(_esPromisePollyfill);
      }

      function _linkReplacer(match, $1, $2, $3) {
      var _target = $3 ? 'blank' : ''; // check if '^' sign is present with link syntax
      return "<a class='botui-message-content-link' target='" + _target + "' href='" + $2 +"'>" + $1 + "</a>";
      }

      function _parseMarkDown(text) {
      return text
      .replace(_markDownRegex.image, "<img class='botui-message-content-image' src='$2' alt='$1' />")
      .replace(_markDownRegex.icon, "<i class='botui-icon botui-message-content-icon fa fa-$1'></i>")
      .replace(_markDownRegex.link, _linkReplacer);
      }

      function loadScript(src, cb) {
      var script = document.createElement('script');
      script.type = 'text/javascript';
      script.src = src;

      if(cb) {
      script.onload = cb;
      }

      document.body.appendChild(script);
      }

      function _handleAction(text) {
      if(_instance.action.addMessage) {
      _interface.message.human({
      delay: 100,
      content: text
      });
      }
      _instance.action.show = !_instance.action.autoHide;
      }

      var _botuiComponent = {
      template: '<div class=\"botui botui-container\" v-botui-container><div class=\"botui-messages-container\"><div v-for=\"msg in messages\" class=\"botui-message\" :class=\"msg.cssClass\" v-botui-scroll><transition name=\"slide-fade\"><div v-if=\"msg.visible\"><div v-if=\"msg.photo && !msg.loading\" :class=\"[\'profil\', \'profile\', {human: msg.human, \'agent\': !msg.human}]\"> <img :src=\"msg.photo\" :class=\"[{human: msg.human, \'agent\': !msg.human}]\"></div><div :class=\"[{human: msg.human, \'botui-message-content\': true}, msg.type]\"><span v-if=\"msg.type == \'text\'\" v-text=\"msg.content\" v-botui-markdown></span><span v-if=\"msg.type == \'html\'\" v-html=\"msg.content\"></span> <iframe v-if=\"msg.type == \'embed\'\" :src=\"msg.content\" frameborder=\"0\" allowfullscreen></iframe></div></div></transition><div v-if=\"msg.photo && msg.loading && !msg.human\" :class=\"[\'profil\', \'profile\', {human: msg.human, \'agent\': !msg.human}]\"> <img :src=\"msg.photo\" :class=\"[{human: msg.human, \'agent\': !msg.human}]\"></div><div v-if=\"msg.loading\" class=\"botui-message-content loading\"><i class=\"dot\"></i><i class=\"dot\"></i><i class=\"dot\"></i></div></div></div><div class=\"botui-actions-container\"><transition name=\"slide-fade\"><div v-if=\"action.show\" v-botui-scroll><form v-if=\"action.type == \'text\'\" class=\"botui-actions-text\" @submit.prevent=\"handle_action_text()\" :class=\"action.cssClass\"><i v-if=\"action.text.icon\" class=\"botui-icon botui-action-text-icon fa\" :class=\"\'fa-\' + action.text.icon\"></i> <input type=\"text\" ref=\"input\" :type=\"action.text.sub_type\" v-model=\"action.text.value\" class=\"botui-actions-text-input\" :placeholder=\"action.text.placeholder\" :size=\"action.text.size\" :value=\" action.text.value\" :class=\"action.text.cssClass\" required v-focus/> <button type=\"submit\" :class=\"{\'botui-actions-buttons-button\': !!action.text.button, \'botui-actions-text-submit\': !action.text.button}\"><i v-if=\"action.text.button && action.text.button.icon\" class=\"botui-icon botui-action-button-icon fa\" :class=\"\'fa-\' + action.text.button.icon\"></i> <span>{{(action.text.button && action.text.button.label) || \'Go\'}}</span></button></form><form v-if=\"action.type == \'select\'\" class=\"botui-actions-select\" @submit.prevent=\"handle_action_select()\" :class=\"action.cssClass\"><i v-if=\"action.select.icon\" class=\"botui-icon botui-action-select-icon fa\" :class=\"\'fa-\' + action.select.icon\"></i><v-select v-if=\"action.select.searchselect && !action.select.multipleselect\" v-model=\"action.select.value\" :value=\"action.select.value\" :placeholder=\"action.select.placeholder\" class=\"botui-actions-text-searchselect\" :label=\"action.select.label\" :options=\"action.select.options\"></v-select><v-select v-else-if=\"action.select.searchselect && action.select.multipleselect\" multiple v-model=\"action.select.value\" :value=\"action.select.value\" :placeholder=\"action.select.placeholder\" class=\"botui-actions-text-searchselect\" :label=\"action.select.label\" :options=\"action.select.options\"></v-select> <select v-else v-model=\"action.select.value\" class=\"botui-actions-text-select\" :placeholder=\"action.select.placeholder\" :size=\"action.select.size\" :class=\"action.select.cssClass\" required v-focus><option v-for=\"option in action.select.options\" :class=\"action.select.optionClass\" v-bind:value=\"option.value\" :disabled=\"(option.value == \'\')?true:false\" :selected=\"(action.select.value == option.value)?\'selected\':\'\'\"> {{ option.text }}</option></select> <button type=\"submit\" :class=\"{\'botui-actions-buttons-button\': !!action.select.button, \'botui-actions-select-submit\': !action.select.button}\"><i v-if=\"action.select.button && action.select.button.icon\" class=\"botui-icon botui-action-button-icon fa\" :class=\"\'fa-\' + action.select.button.icon\"></i> <span>{{(action.select.button && action.select.button.label) || \'Ok\'}}</span></button></form><div v-if=\"action.type == \'button\'\" class=\"botui-actions-buttons\" :class=\"action.cssClass\"> <button type=\"button\" :class=\"button.cssClass\" class=\"botui-actions-buttons-button\" v-botui-scroll v-for=\"button in action.button.buttons\" @click=\"handle_action_button(button)\"><i v-if=\"button.icon\" class=\"botui-icon botui-action-button-icon fa\" :class=\"\'fa-\' + button.icon\"></i> {{button.text}}</button></div><form v-if=\"action.type == \'buttontext\'\" class=\"botui-actions-text\" @submit.prevent=\"handle_action_text()\" :class=\"action.cssClass\"><i v-if=\"action.text.icon\" class=\"botui-icon botui-action-text-icon fa\" :class=\"\'fa-\' + action.text.icon\"></i> <input type=\"text\" ref=\"input\" :type=\"action.text.sub_type\" v-model=\"action.text.value\" class=\"botui-actions-text-input\" :placeholder=\"action.text.placeholder\" :size=\"action.text.size\" :value=\"action.text.value\" :class=\"action.text.cssClass\" required v-focus/> <button type=\"submit\" :class=\"{\'botui-actions-buttons-button\': !!action.text.button, \'botui-actions-text-submit\': !action.text.button}\"><i v-if=\"action.text.button && action.text.button.icon\" class=\"botui-icon botui-action-button-icon fa\" :class=\"\'fa-\' + action.text.button.icon\"></i> <span>{{(action.text.button && action.text.button.label) || \'Go\'}}</span></button><div class=\"botui-actions-buttons\" :class=\"action.cssClass\"> <button type=\"button\" :class=\"button.cssClass\" class=\"botui-actions-buttons-button\" v-for=\"button in action.button.buttons\" @click=\"handle_action_button(button)\" autofocus><i v-if=\"button.icon\" class=\"botui-icon botui-action-button-icon fa\" :class=\"\'fa-\' + button.icon\"></i> {{button.text}}</button></div></form></div></transition></div></div>', // replaced by HTML template during build. see Gulpfile.js
      data: function () {
      return {
      action: {
      text: {
      size: 30,
      placeholder: 'Write here ..'
      },
      button: {},
      show: false,
      type: 'text',
      autoHide: true,
      addMessage: true
      },
      messages: []
      };
      },
      computed: {
      isMobile: function () {
      return root.innerWidth && root.innerWidth <= 768;
      }
      },
      methods: {
      handle_action_button: function (button) {
      for (var i = 0; i < this.action.button.buttons.length; i++) {
      if(this.action.button.buttons[i].value == button.value && typeof(this.action.button.buttons[i].event) == 'function') {
      this.action.button.buttons[i].event(button);
      if (this.action.button.buttons[i].actionStop) return false;
      break;
      }
      }

      _handleAction(button.text);

      var defaultActionObj = {
      type: 'button',
      text: button.text,
      value: button.value
      };

      for (var eachProperty in button) {
      if (button.hasOwnProperty(eachProperty)) {
      if (eachProperty !== 'type' && eachProperty !== 'text' && eachProperty !== 'value') {
      defaultActionObj[eachProperty] = button[eachProperty];
      }
      }
      }

      _actionResolve(defaultActionObj);
      },
      handle_action_text: function () {
      if(!this.action.text.value) return;
      _handleAction(this.action.text.value);
      _actionResolve({
      type: 'text',
      value: this.action.text.value
      });
      this.action.text.value = '';
      },
      handle_action_select: function () {
      if(this.action.select.searchselect && !this.action.select.multipleselect) {
      if(!this.action.select.value.value) return;
      _handleAction(this.action.select.value[this.action.select.label]);
      _actionResolve({
      type: 'text',
      value: this.action.select.value.value,
      text: this.action.select.value.text,
      obj: this.action.select.value
      });
      }
      if(this.action.select.searchselect && this.action.select.multipleselect) {
      if(!this.action.select.value) return;
      var values = new Array();
      var labels = new Array();
      for (var i = 0; i < this.action.select.value.length; i++) {
      values.push(this.action.select.value[i].value);
      labels.push(this.action.select.value[i][this.action.select.label]);
      }
      _handleAction(labels.join(', '));
      _actionResolve({
      type: 'text',
      value: values.join(', '),
      text: labels.join(', '),
      obj: this.action.select.value
      });
      }
      else {
      if(!this.action.select.value) return;
      for (var i = 0; i < this.action.select.options.length; i++) { // Find select title
      if (this.action.select.options[i].value == this.action.select.value) {
      _handleAction(this.action.select.options[i].text);
      _actionResolve({
      type: 'text',
      value: this.action.select.value,
      text: this.action.select.options[i].text
      });
      }
      }
      }
      }
      }
      };

      root.Vue.directive('botui-markdown', function (el, binding) {
      if(binding.value == 'false') return; // v-botui-markdown="false"
      el.innerHTML = _parseMarkDown(el.textContent);
      });

      root.Vue.directive('botui-scroll', {
      inserted: function (el) {
      _container.scrollTop = _container.scrollHeight;
      // 弹弹乐问题定位
      el.scrollIntoView({behavior: "smooth", block: "center", inline: "nearest"});
      }
      });

      root.Vue.directive('focus', {
      inserted: function (el) {
      el.focus();
      }
      });

      root.Vue.directive('botui-container', {
      inserted: function (el) {
      _container = el;
      }
      });

      _botApp = new root.Vue({
      components: {
      'bot-ui': _botuiComponent
      }
      }).$mount('#' + id);

      var _instance = _botApp.$children[0]; // to access the component's data

      function _addMessage(_msg) {

      if(!_msg.loading && !_msg.content) {
      throw Error('BotUI: "content" is required in a non-loading message object.');
      }

      _msg.type = _msg.type || 'text';
      _msg.visible = (_msg.delay || _msg.loading) ? false : true;
      var _index = _instance.messages.push(_msg) - 1;

      return new Promise(function (resolve, reject) {
      setTimeout(function () {
      if(_msg.delay) {
      _msg.visible = true;

      if(_msg.loading) {
      _msg.loading = false;
      }
      }
      resolve(_index);
      }, _msg.delay || 0);
      });
      }

      function _checkOpts(_opts) {
      if(typeof _opts === 'string') {
      _opts = {
      content: _opts
      };
      }
      return _opts || {};
      }

      _interface.message = {
      add: function (addOpts) {
      return _addMessage( _checkOpts(addOpts) );
      },
      bot: function (addOpts) {
      addOpts = _checkOpts(addOpts);
      return _addMessage(addOpts);
      },
      human: function (addOpts) {
      addOpts = _checkOpts(addOpts);
      addOpts.human = true;
      return _addMessage(addOpts);
      },
      get: function (index) {
      return Promise.resolve(_instance.messages[index]);
      },
      remove: function (index) {
      _instance.messages.splice(index, 1);
      return Promise.resolve();
      },
      update: function (index, msg) { // only content can be updated, not the message type.
      var _msg = _instance.messages[index];
      _msg.content = msg.content;
      _msg.visible = !msg.loading;
      _msg.loading = !!msg.loading;
      return Promise.resolve(msg.content);
      },
      removeAll: function () {
      _instance.messages.splice(0, _instance.messages.length);
      return Promise.resolve();
      }
      };

      function mergeAtoB(objA, objB) {
      for (var prop in objA) {
      if (!objB.hasOwnProperty(prop)) {
      objB[prop] = objA[prop];
      }
      }
      }

      function _checkAction(_opts) {
      if(!_opts.action && !_opts.actionButton && !_opts.actionText) {
      throw Error('BotUI: "action" property is required.');
      }
      }

      function _showActions(_opts) {

      _checkAction(_opts);

      mergeAtoB({
      type: 'text',
      cssClass: '',
      autoHide: true,
      addMessage: true
      }, _opts);

      _instance.action.type = _opts.type;
      _instance.action.cssClass = _opts.cssClass;
      _instance.action.autoHide = _opts.autoHide;
      _instance.action.addMessage = _opts.addMessage;

      return new Promise(function(resolve, reject) {
      _actionResolve = resolve; // resolved when action is performed, i.e: button clicked, text submitted, etc.
      setTimeout(function () {
      _instance.action.show = true;
      }, _opts.delay || 0);
      });
      };

      _interface.action = {
      show: _showActions,
      hide: function () {
      _instance.action.show = false;
      return Promise.resolve();
      },
      text: function (_opts) {
      _checkAction(_opts);
      _instance.action.text = _opts.action;
      return _showActions(_opts);
      },
      button: function (_opts) {
      _checkAction(_opts);
      _opts.type = 'button';
      _instance.action.button.buttons = _opts.action;
      return _showActions(_opts);
      },
      select: function (_opts) {
      _checkAction(_opts);
      _opts.type = 'select';
      _opts.action.label = _opts.action.label || 'text';
      _opts.action.value = _opts.action.value || '';
      _opts.action.searchselect = typeof _opts.action.searchselect !== 'undefined' ? _opts.action.searchselect : _options.searchselect;
      _opts.action.multipleselect = _opts.action.multipleselect || false;
      if (_opts.action.searchselect && typeof(_opts.action.value) == 'string') {
      if (!_opts.action.multipleselect) {
      for (var i = 0; i < _opts.action.options.length; i++) { // Find object
      if (_opts.action.options[i].value == _opts.action.value) {
      _opts.action.value = _opts.action.options[i]
      }
      }
      }
      else {
      var vals = _opts.action.value.split(',');
      _opts.action.value = new Array();
      for (var i = 0; i < _opts.action.options.length; i++) { // Find object
      for (var j = 0; j < vals.length; j++) { // Search values
      if (_opts.action.options[i].value == vals[j]) {
      _opts.action.value.push(_opts.action.options[i]);
      }
      }
      }
      }
      }
      if (!_opts.action.searchselect) { _opts.action.options.unshift({value:'',text : _opts.action.placeholder}); }
      _instance.action.button = _opts.action.button;
      _instance.action.select = _opts.action;
      return _showActions(_opts);
      },
      buttontext: function (_opts) {
      _checkAction(_opts);
      _opts.type = 'buttontext';
      _instance.action.button.buttons = _opts.actionButton;
      _instance.action.text = _opts.actionText;
      return _showActions(_opts);
      }
      };

      if(_options.fontawesome) {
      loadScript(_fontAwesome);
      }

      if(_options.searchselect) {
      loadScript(_searchselect, function() {
      Vue.component('v-select', VueSelect.VueSelect);
      });
      }

      if(_options.debug) {
      _interface._botApp = _botApp; // current Vue instance
      }

      return _interface;
      });

      return BotUI;

      }));
    • botui_init.js

      这个是整个项目的关键,聊天内容全部在这里进行设计,此处仅以我的项目作为示例,可以参阅botui的github仓库查阅使用文档,或者在我的项目上进行内容修改。

      function botui_init() {
      var botui = new BotUI("hello-akilar");
      botui.message.add({
      delay: 800,
      content: "Hi, 欢迎光临Akilarの糖果屋😊"
      }).then(function() {
      botui.message.add({
      delay: 1100,
      content: "我是店长Akilar😄"
      }).then(function() {
      botui.message.add({
      delay: 1100,
      content: "你也可以叫我Aki~😋"
      }).then(function() {
      botui.action.button({
      delay: 1600,
      action: [{
      text: "我想知道更多关于糖果屋的故事!😃",
      value: "sure"
      }, {
      text: "好的,就这样吧,拜拜!🙄",
      value: "skip"
      }]
      }).then(function(a) {
      "sure" == a.value && sure();
      "skip" == a.value && end()
      })
      })
      })
      });
      var sure = function() {
      botui.message.add({
      delay: 600,
      content: "🎉🎉🎉🎉🎉🎉"
      }).then(function() {
      secondpart()
      })
      },
      end = function() {
      botui.message.add({
      delay: 600,
      content: "w(゚Д゚)w 不要走!再看看嘛!"
      })
      },
      secondpart = function() {
      botui.message.add({
      delay: 5000,
      content: "首先呢,很感谢您肯在这里驻足片刻❤️。Akilarの糖果屋是一个个人性质的博客,我会在这里发表各种各样的内容。"
      }).then(function() {
      botui.message.add({
      delay: 15000,
      content: "起这个名字是因为想到了安卓的命名方式,安卓历代版本都用甜品的名字命名🍰,例如9是Pineapple cake(菠萝蛋糕)🍰,8是Oreo(奥利奥)🍩,那我干脆就甜到底了。因此可以看到我的分类里面都是糖。之后就发现了一个很纠结的问题,除了巧克力,我想不到其他的不带糖字的糖果。当然了,无伤大雅。才怪咯!超难受的好么!偏偏我那么喜欢巧克力🍫,我是不会把它删掉的。"
      }).then(function() {
      botui.message.add({
      delay: 5000,
      content: "分类也有一点我的恶趣味在。👀"
      }).then(function() {
      botui.message.add({
      delay: 8000,
      content: "比如巧克力是Ubuntu的教程,棉花糖是windows的教程,糖葫芦就是各种通用教程啦!🎉"
      }).then(function() {
      botui.message.add({
      delay: 5000,
      content: "泡泡糖是个人日记哦,流水账一样的,不要看,很羞耻的。😶"
      }).then(function() {
      botui.message.add({
      delay: 4000,
      content: "我个人最推荐的是太妃糖版块哦,这里可都是我引以为豪的作品呢💝!马卡龙酌情观看吧,长篇连载对我来说是个挑战,很可能断更。👻"
      }).then(function() {
      botui.action.button({
      delay: 1100,
      action: [{
      text: "为什么叫Akilarの糖果屋呢?🤔",
      value: "why-mashiro"
      }]
      }).then(function(a) {
      thirdpart()
      })
      })
      })
      })
      })
      })
      })
      },
      thirdpart = function() {
      botui.message.add({
      delay: 1e3,
      content: "诶?Akilar是我的英文名啊😏,糖果屋,emm🤔,大概是因为我在现实中也很想开一家糖果屋吧。"
      }).then(function() {
      botui.action.button({
      delay: 1500,
      action: [{
      text: "😲,那英文名为什么叫Akilar呢?",
      value: "why-cat"
      }]
      }).then(function(a) {
      fourthpart()
      })
      })
      },
      fourthpart = function() {
      botui.message.add({
      delay: 3000,
      content: "这个是因为我的名字的释义用日文发音,其中有一节是Akira,用英文谐音拼写就是Akilar了 "
      }).then(function() {
      botui.message.add({
      delay: 3000,
      content: "灵感来自于刀剑神域~"
      }).then(function() {
      botui.action.button({
      delay: 1500,
      action: [{
      text: "方便透露一下真名吗?👀",
      value: "why-domain"
      }]
      }).then(function(a) {
      fifthpart()
      })
      })
      })
      },
      fifthpart = function() {
      botui.message.add({
      delay: 5000,
      content: "emmmm,流水幽吟绕耳边,煦风馨语抚心弦,挥臂欲揽冰钩月,银星斟酌醉人涎~"
      }).then(function() {
      botui.message.add({
      delay: 3000,
      content: "只是一介无名小卒而已^_^"
      })
      })
      }
      }
  5. 修改~\[blogroot]\_config.butterfly.yml,注意对齐格式。

        aside:
    enable: true
    mobile: false # display on mobile
    position: right # left or right
    card_author:
    enable: true
    description:
    button:
    icon:
    text:
    link:
    + card_botui:
    + enable: true #侧栏聊天窗口
    card_announcement:
    enable: true

    inject:
    head:
    # 侧栏聊天窗口
    + - <link rel="stylesheet" href="/css/card_botui.css" />
    bottom:
    + # Veu.js依赖
    + - <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    + # 侧栏聊天窗
    + - <script src="/js/botui.js"></script>
    + - <script data-pjax src="/js/botui_init.js"></script>
  6. ~\[blogroot]\themes\butterfly\languages\zh-CN.yml中添加相应译名

        aside:
    articles: 文章
    tags: 标签
    categories: 分类
    Link: 友人帐
    + card_botui: 聊天窗
    card_announcement: 告示牌
    card_categories: 分类
    card_tags: 标签
    card_archives: 时间轴
    card_recent_post: 最新文章
    card_webinfo:

可能遇到的bug

  1. 无法显示

    • botui.js依赖vue.js,添加依赖即可。(教程已更新相关内容)
        inject:
      head:
      bottom:
      + # Veu.js依赖
      + - <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
  2. 切换页面侧栏就变成空白

    • 添加pjax重载(仅限于butterfly主题)
        inject:
      head:
      bottom:
      - - <script src="/js/botui_init.js"></script>
      + - <script data-pjax src="/js/botui_init.js"></script>