开发记录

开发记录

2020-10-22:第一版搭建

实现了侧栏插入

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

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

</div></div>

2020-10-25:添加按钮

</div>

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

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

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

2020-11-16:版本优化

增加butterfly_v3.3.0配置方案。

2020-12-13:版本优化

增加butterfly_v3.4.0配置方案。

2021-1-31:适配3.6.0

  1. 更新v3.6.0适配方案

botui.js简介

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

静态资源下载

**由于本教程涉及的所有修改对缩进格式等有严格要求,担心自己控制不好的可以直接下载静态资源,将压缩包内的butterfly文件夹复制到[Blogroot]\theme\目录下覆盖现有主题文件夹即可跳过以下教程的前4步,直接到主题配置文件_config.butterfly.yml中参照第5、6两步修改配置项。如果不希望全局引入侧栏,请读者自行根据自己的主题版本,参看第2步进行修改。

修改步骤

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    .card-widget.card-botui
    .card-content(style='height:320px;')
    .item-headline
    i.fas.fa-comments
    span= _p('aside.card_botui')
    #hello-测试.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()这个判断方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
        #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

    此处写法是在站点页和文章页都添加了card_botui,只需要文章页有的就只写上面这个。只需要站点页有的就只写下面这个。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
        #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})
    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})
    if theme.aside.card_tags.enable
    !=partial('includes/widget/card_tags', {}, {cache:theme.fragment_cache})
    if theme.aside.card_archives.enable
    !=partial('includes/widget/card_archives', {}, {cache:theme.fragment_cache})
    if theme.aside.card_webinfo.enable
    !=partial('includes/widget/card_webinfo', {}, {cache:theme.fragment_cache})

    此处写法是在站点页和文章页都添加了card_botui,只需要文章页有的就只写上面这个。只需要站点页有的就只写下面这个。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
        #aside_content.aside_content
    //- post
    if is_post()
    if showToc && theme.toc.style_simple
    .sticky_layout
    include ./card_post_toc.pug
    else
    !=partial('includes/widget/card_author', {}, {cache:theme.fragment_cache})
    !=partial('includes/widget/card_announcement', {}, {cache:theme.fragment_cache})
    .sticky_layout
    if showToc
    include ./card_post_toc.pug
    + if theme.aside.card_botui.enable
    + !=partial('includes/widget/card_botui', {}, {cache:theme.fragment_cache})
    !=partial('includes/widget/card_recent_post', {}, {cache:theme.fragment_cache})
    !=partial('includes/widget/card_ad', {}, {cache:theme.fragment_cache})
    else
    //- page
    !=partial('includes/widget/card_author', {}, {cache:theme.fragment_cache})
    !=partial('includes/widget/card_announcement', {}, {cache:theme.fragment_cache})
    .sticky_layout
    + if theme.aside.card_botui.enable
    + !=partial('includes/widget/card_botui', {}, {cache:theme.fragment_cache})
    !=partial('includes/widget/card_recent_post', {}, {cache:theme.fragment_cache})
    !=partial('includes/widget/card_ad', {}, {cache:theme.fragment_cache})
    !=partial('includes/widget/card_newest_comment', {}, {cache:theme.fragment_cache})
    !=partial('includes/widget/card_categories', {}, {cache:theme.fragment_cache})
    !=partial('includes/widget/card_tags', {}, {cache:theme.fragment_cache})
    !=partial('includes/widget/card_archives', {}, {cache:theme.fragment_cache})
    !=partial('includes/widget/card_webinfo', {}, {cache:theme.fragment_cache})

    此处写法是在站点页和文章页都添加了card_botui,只需要文章页有的就只写上面这个。只需要站点页有的就只写下面这个。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
        #aside_content.aside_content
    //- post
    if is_post()
    if showToc && theme.toc.style_simple
    .sticky_layout
    include ./card_post_toc.pug
    else
    !=partial('includes/widget/card_author', {}, {cache: true})
    !=partial('includes/widget/card_announcement', {}, {cache: true})
    .sticky_layout
    if showToc
    include ./card_post_toc.pug
    + if theme.aside.card_botui.enable
    + !=partial('includes/widget/card_botui', {}, {cache: true})
    !=partial('includes/widget/card_recent_post', {}, {cache: true})
    !=partial('includes/widget/card_ad', {}, {cache: true})
    else
    //- page
    !=partial('includes/widget/card_author', {}, {cache: true})
    !=partial('includes/widget/card_announcement', {}, {cache: true})
    .sticky_layout
    + if theme.aside.card_botui.enable
    + !=partial('includes/widget/card_botui', {}, {cache: true})
    !=partial('includes/widget/card_recent_post', {}, {cache: true})
    !=partial('includes/widget/card_ad', {}, {cache: true})
    !=partial('includes/widget/card_newest_comment', {}, {cache: true})
    !=partial('includes/widget/card_categories', {}, {cache: true})
    !=partial('includes/widget/card_tags', {}, {cache: true})
    !=partial('includes/widget/card_archives', {}, {cache: true})
    !=partial('includes/widget/card_webinfo', {}, {cache: true})
  3. ~\[blogroot]\themes\butterfly\source\css\目录下新建card_botui.css,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    @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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
      275
      276
      277
      278
      279
      280
      281
      282
      283
      284
      285
      286
      287
      288
      289
      290
      291
      292
      293
      294
      295
      296
      297
      298
      299
      300
      301
      302
      303
      304
      305
      306
      307
      308
      309
      310
      311
      312
      313
      314
      315
      316
      317
      318
      319
      320
      321
      322
      323
      324
      325
      326
      327
      328
      329
      330
      331
      332
      333
      334
      335
      336
      337
      338
      339
      340
      341
      342
      343
      344
      345
      346
      347
      348
      349
      350
      351
      352
      353
      354
      355
      356
      357
      358
      359
      360
      361
      362
      363
      364
      365
      366
      367
      368
      369
      370
      371
      372
      373
      374
      375
      376
      377
      378
      379
      380
      381
      382
      383
      384
      385
      386
      387
      388
      389
      390
      391
      392
      393
      394
      395
      396
      397
      398
      399
      400
      401
      402
      403
      404
      405
      406
      407
      408
      409
      410
      411
      412
      413
      414
      415
      416
      417
      418
      419
      420
      421
      /*
      * 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仓库查阅使用文档,或者在我的项目上进行内容修改。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      function botui_init() {
      var botui = new BotUI("hello-测试");
      botui.message.add({
      delay: 800,
      content: "Hi, 欢迎光临测试の资源宝😊"
      }).then(function() {
      botui.message.add({
      delay: 1100,
      content: "我是店长测试😄"
      }).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: "首先呢,很感谢您肯在这里驻足片刻❤️。测试の资源宝是一个个人性质的博客,我会在这里发表各种各样的内容。"
      }).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: "为什么叫测试の资源宝呢?🤔",
      value: "why-mashiro"
      }]
      }).then(function(a) {
      thirdpart()
      })
      })
      })
      })
      })
      })
      })
      },
      thirdpart = function() {
      botui.message.add({
      delay: 1e3,
      content: "诶?测试是我的英文名啊😏,资源宝,emm🤔,大概是因为我在现实中也很想开一家资源宝吧。"
      }).then(function() {
      botui.action.button({
      delay: 1500,
      action: [{
      text: "😲,那英文名为什么叫测试呢?",
      value: "why-cat"
      }]
      }).then(function(a) {
      fourthpart()
      })
      })
      },
      fourthpart = function() {
      botui.message.add({
      delay: 3000,
      content: "这个是因为我的名字的释义用日文发音,其中有一节是Akira,用英文谐音拼写就是测试了 "
      }).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,注意对齐格式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
        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:
    + # vue.js依赖
    + - <script src="https://npm.elemecdn.com/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中添加相应译名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
        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,添加依赖即可。(教程已更新相关内容)
      1
      2
      3
      4
      5
        inject:
      head:
      bottom:
      + # vue.js依赖
      + - <script src="https://npm.elemecdn.com/vue@2.6.11"></script>
  2. 切换页面侧栏就变成空白

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