点击查看更新记录

更新记录

2021-01-24:内测版v0.07

  1. 实现SAO风格的右键菜单
  2. 添加了点击音效,默认使用本站同款,可以自定义配置
  3. 支持添加链接或者自定义脚本动作
  4. 添加Ctrl+右键转换原生菜单功能
  5. 几个常用脚本分享

2021-01-25:内测版v0.14

  1. 新增了3D模式开关配置项

2021-01-26:内测版v0.21

  1. 新增了3D模式下浮动效果
  2. 适配了pjax。调用了Butterfly主题自带的pjax对象。其他主题需要另外适配。

2021-01-28:内测版v0.37

  1. 正式版待发布,因为显隐逻辑完全改变,预备转至新帖;

2021-01-29:正式版v1.0

  1. 1.0版本正式版发布。
  2. 确定1.0版本为半边菜单。

2021-01-29:正式版v2.0

  1. 继承1.0版本的绝大多数配置项。
  2. 废弃最大高度,重写样式。全部调整为自动。完全显示菜单。
  3. 重写显隐逻辑,默认只显示1级菜单。
  4. 通过点击1级菜单展开2级菜单。
  5. 通过悬停2级菜单显示3级菜单。
  6. 优化显示逻辑,识别边缘调整菜单出现位置。确保主要内容完整可见。
  7. 优化显示逻辑,新增拖动动作监听。可以通过点按拖动菜单调整菜单位置。
  8. 将1级菜单最后一个按钮默认设置为退出菜单动作。
  9. 优化退出逻辑。点按空白处也可以退出菜单。
  10. 2.0版本正式版发布。实现完整UI风格效果。

2021-01-30:正式版v2.1

  1. 修改显隐逻辑,所有次级元素均可以通过点按上级元素来实现常显。
  2. 优化了左侧属性面板的内容排布,使用绝对高度。
  3. 属性面板内容超出自动转为滚动条。
  4. 改动三级菜单的显隐逻辑,也为点按显示
  5. 新增次级菜单的偏移量调整。
  6. 新增了说明书配置项,内附本帖链接。希望可以开启帮我做下宣传。
  7. 调整了3D风格的形变程度。
  8. 更新了字体,忠于原著。
  9. 新增悬停显示配置项,可以自己决定是否使用悬停显示

2021-01-31:正式版v2.2

  1. 新增退出按钮。
  2. 无痕模式下退出窗口功能会被拦截,变相致敬原著设定。
  3. 将说明书内容移入默认按钮,可以关闭。
点击查看参考教程
参考方向教程原贴
菜单边框风格伪类样式实现方案codepen-Pure CSS SAO Menu Thing
右键菜单显隐逻辑和原生实现方案
UI风格参考,图标、音效资源采集SAO Utils
图标采集fontawesome
pjax适配参考
原生js实现拖拽效果js拖拽:简单五步实现元素拖拽功能

资源下载

由于本教程涉及的所有修改对缩进格式等有严格要求,担心自己控制不好的可以直接下载静态资源。参照教程进行修改。

写在最前

来自店长的碎碎念

写一个SAO风格的右键菜单算是我的一个执念了,但是搜遍全网页找不到网页版的内容,于是我想起来多年前就用过的一款软件SAO Utils,可惜它虽然有完整的菜单逻辑,但是却是基于C语言写的。至多只能参考一些音效。

最后兜兜转转,在魔改博客时看到了Volantis的右键菜单。学习了一下右键菜单的魔改原理。决定自己来从零开始做一个SAO风格的右键菜单。

因为这个项目,魔怔了大概半个月,好在那半个月单位工作基本划水。(嘛,总之摸鱼也是为了给大家写好看的魔改教程嘛)一直被二级菜单的显隐逻辑所困扰,因为用到了相对定位,中间有一段元素是空白的,没法在不破坏菜单项显示效果的情况下直接依靠hover实现持续显示二级菜单的效果,最后是用定时函数控制绝对显隐,用hover控制持续显隐。总算是有了一个相对舒适的显隐体验。

在一开始,因为想到以前一直被一些读者喷右键菜单占用了原生菜单很讨厌(说实话这样的读者也很讨厌)。所以这次在@卓越科技建议下添加了ctrl+右键打开原生右键菜单的功能。然后考虑到菜单界面对手机不友好,干脆对手机不生效了。

在添加音效时,因为直接链接跳转的话,会来不及启动点击音效,所以只能使用超时函数设置了0.5秒的延迟,给音效播放留点时间。

也正是因为不是依赖于a标签的超链接,而是使用window.location.href来实现页面重定向,所以目前对于pjax的适配还是有些许不好。会在切换页面时打断全局音乐。
不过塞翁失马焉知非福,也正是因为如此,我又添加了自定义脚本的配置项接口。可以让读者自己开发各种各样的脚本来丰富菜单功能啦。

说到底,既然可以自定义脚本了,那应该也可以适配pjax实现无伤跳转才对。唉,果然还是太菜了

总之,这次的项目就先告一段落啦!

米娜桑,Link Start!

既然已经制作完成了右半边的SAO风格UI,肯定不可能满足于此的啦。

说起来我当初执意要选择软件工程专业的时候,有个动机就是被刀剑神域里的VRMMO吸引,想着能有一天可以真的体验上这种完全沉浸式的游戏。不过理想和现实还真的是差距鲜明啊。

于是开始尝试制作左半侧的UI。球形风格还是很好实现的。大概只花了半天摸鱼时间,就基本实现了球形样式UI和悬停变色效果,得益于之前做右半边的经验,不管是构建主体还是引入图标都是顺风顺水。

然后在追番考古时发现左侧菜单还有一个属性面板的界面。OK,话不多说。继续加。
总的来说,在熟练掌握相对定位的关系以后,适配起来还是很容易的。因为右键菜单是动态出现的,故而我用的都是绝对长度,自然也不会去考虑和窗口整体大小比例自适应的问题。

一开始是在静态页面上写的纯静态效果,依靠hover动作控制显隐属性。但是这就导致我遇到了一系列问题。relative定位下,100%这个概念居然是相对于父元素的,依靠各种偏移量搭建的菜单一下子就乱了套。为了调整各个子菜单,重新捡起了初中数学知识,列了一堆二元方程组,最后还真的让我整出了一套计算公式。不过静态的css是不支持这种玩法的啦,好在到时候实装时还有pugstylus可以添加计算变量动态调整。小case啦。

然后左半边菜单就搞定啦,悬停显示效果和动画里那是一模一样啊。开心!

然后,好吧,要把左半边菜单和右半边菜单组装起来。淦!一想到之前为了调整右侧菜单的显隐逻辑付出的血泪我就恨不得吐血三升。网上的参考内容都是针对于子菜单在父级元素内部的情况,那确实可以靠hover轻松搞定,但是我设置了一堆偏移量和伪类,导致子菜单和父菜单关键的连接轴是个伪类,hover无效啊喂!。在体验了两天的天下武功唯快不破以后设置了三种逻辑。

一种是全部通过点击来展开子菜单。但是这样子一来每次点击都要记得关闭,用清空已激活项来初始化的话,二级和三级又要写另一套逻辑。Pass

一种是依靠悬停加延时消失来控制显隐。虽然也算是靠谱了,但是二级消失连带着三级也消失了,还是得天下武功,唯快不破!把延时消失时间设长,观感又很差。Pass

最后一种是被读者点醒的,所以说当局者迷旁观者清啊,既然前两种方案都是半吊子,那把它们组合起来。一级菜单用点击来控制二级菜单显隐,二级菜单用悬停延时来控制三级菜单显隐,那不就没问题了嘛!说起来SAO Utils就是这个显隐逻辑。啊,顿悟的感觉真爽,我感觉我现在起码能烧出十颗舍利子。

显隐逻辑处理好以后,再做边缘判断。为了确保完整可见,要充分考虑每次点击的情况,感觉又回到了初中数学课堂,分类讨论,列算式,化简,emmm,我应该还算对得起我初中的数学老师吧。这时候又想起洪哥之前吐槽的,点击位置和菜单出现位置偏移有点大了。

没办法,那就再加个可以拖动的吧。诶?那我还做边缘判断干啥?

总之,磕磕绊绊的,总算是可以把完整的SAO UI复现到网页中了。

嗯,再来一次。Link Start

教程正文

点击查看教程正文
  1. 新建[Blogroot]\themes\butterfly\layout\includes\SAO-menu.pug
    #SAO-back
    #SAO-menu
    #SAO-menu-content
    .utils-list
    if theme.SAO_Utils.util_list
    each util in theme.SAO_Utils.util_list
    .utils-list-item
    if util.panel
    .user-panel(style=`top:`+ util.panel.top + `;`)
    .user-panel-name!= util.panel.title
    .user-panel-img
    - var userImg = util.panel.img ? url_for(util.panel.img) : 'https://cdn.jsdelivr.net/gh/Akilarlxh/Akilarlxh.github.io@bf_3.5.1_6/assets/info.png'
    img(src=userImg)
    .user-panel-properties!=`${util.panel.prop}`
    if util.link
    i(class=util.icon onclick=`clickAudio();setTimeout(function(){SAOclose();linkStart('` +url_for(util.link)+ `')},500);`)
    else if util.action
    i(class=util.icon onclick=`clickAudio();setTimeout(function(){SAOclose();` + util.action + `},500);`)
    else
    i(class=util.icon onclick="panelAudio();UtilsClick()")
    if util.menu_list
    - var menuTop = util.menutop ? util.menutop : -((util.menu_list.length)*40/2+43)
    .menu-list(style=`top:` + menuTop + `px;`)
    each item in util.menu_list
    .menu-list-item
    if item.link
    i(class=item.icon onclick=`clickAudio();setTimeout(function(){SAOclose();linkStart('` +url_for(item.link)+ `')},500);`)= item.name
    else if item.action
    i(class=item.icon onclick=`clickAudio();setTimeout(function(){SAOclose();` + item.action + `},500);`)= item.name
    else
    i(class=item.icon onclick='panelAudio();MenusClick()')= item.name
    if item.child_list
    - var childTop = item.childtop ? item.childtop : -((item.child_list.length)*40/2+20)
    .menu-child(style=`top:` + childTop + `px;`)
    each child in item.child_list
    .menu-list-child
    if child.link
    i(class=child.icon onclick=`clickAudio();setTimeout(function(){SAOclose();linkStart('` +url_for(child.link)+ `')},500);`)= child.name
    else if child.action
    i(class=child.icon onclick=`clickAudio();setTimeout(function(){SAOclose();` + child.action + `},500);`)= child.name
    else
    i(class=child.icon)= child.name
    if theme.SAO_Utils.Instructions ? theme.SAO_Utils.Instructions : true
    .utils-list-item
    .user-panel
    .user-panel-name Instructions
    .user-panel-img
    img(src='https://akilar.top/img/siteicon/favicon.png')
    .user-panel-properties
    h4 欢迎使用SAO_Utils_Web 2.0
    p 点按选项可以持续显示下级菜单。您可以通过按住<kbd>ctrl</kbd>+<kbd>右键</kbd>来恢复使用原生右键菜单,更多内容可点击右侧Option按钮访问教程或Help按钮加入糖果屋QQ群。
    center ©Akilarの糖果屋
    i.fa.fa-cog(onclick="panelAudio();UtilsClick()")
    .menu-list(style="top:-103px;")
    .menu-list-item
    i.fa.fa-tools(onclick="clickAudio();setTimeout(function(){SAOclose();linkStart('https://akilar.top/posts/fd243d7/')},500);") Option
    .menu-list-item
    i.fa.fa-question-circle(onclick="clickAudio();setTimeout(function(){SAOclose();linkStart('https://jq.qq.com/?_wv=1027&k=a08BZRzs')},500);") Help
    .menu-list-item
    i.fa.fa-sign-out-alt(onclick="alertAudio();openLogout()") Logout
    .utils-list-item
    i.fa.fa-power-off(onclick='clickAudio();SAOclose()')
    if theme.SAO_Utils.Instructions ? theme.SAO_Utils.Instructions : true
    #SAO-logout
    .logout-title Alert
    .logout-alert 是否确认退出?
    .logout-button
    span.logout-confirm
    button.far.fa-circle(type="button" name="confirm" onclick="clickAudio();confirmLogout()")
    span.logout-cancel
    button.fa.fa-times(type="button" name="cancel" onclick="panelAudio();cancelLogout()")
    if theme.SAO_Utils.music.enable
    - var Launcher = theme.SAO_Utils.music.Launcher ? url_for(theme.SAO_Utils.music.Launcher) : 'https://cdn.jsdelivr.net/gh/Akilarlxh/Akilarlxh.github.io@bf_3.5.1_6/assets/Launcher.wav'
    - var Click = theme.SAO_Utils.music.Click ? url_for(theme.SAO_Utils.music.Click) : 'https://cdn.jsdelivr.net/gh/Akilarlxh/Akilarlxh.github.io@bf_3.5.1_6/assets/Click.wav'
    - var Panel = theme.SAO_Utils.music.Panel ? url_for(theme.SAO_Utils.music.Panel) : 'https://cdn.jsdelivr.net/gh/Akilarlxh/Akilarlxh.github.io@bf_3.5.1_6/assets/Panel.wav'
    - var Alert = theme.SAO_Utils.music.Alert ? url_for(theme.SAO_Utils.music.Alert) : 'https://cdn.jsdelivr.net/gh/Akilarlxh/Akilarlxh.github.io@bf_3.5.1_6/assets/Alert.wav'
    audio#SAOlauncher(src=Launcher)
    audio#SAOClick(src=Click)
    audio#SAOPanel(src=Panel)
    audio#SAOAlert(src=Alert)
    script(async src=url_for(theme.CDN.SAO_Utils))
  2. 新建[Blogroot]\themes\butterfly\source\css\_layout\SAO_Menu.styl:
    if hexo-config('SAO_Utils.enable')
    @font-face
    font-family 'SAOUI'
    src url('https://cdn.jsdelivr.net/gh/Akilarlxh/Akilarlxh.github.io@bf_3.5.1_6/assets/SAOUI.ttf')
    font-display swap
    #SAO-back
    display none
    position fixed
    width 100%
    top 0
    left 0
    height 100%
    background rgba(3, 3, 3, 0.5)
    z-index 9999
    vertical-align super

    #SAO-menu
    display block
    position absolute

    #SAO-menu-content
    font-family 'SAOUI' ,Langar,-apple-system, sans-serif
    display block

    .utils-list-item
    font-weight bolder
    margin-top 10px
    width 66px
    height 66px
    background rgba(255,255,255,0)
    text-align center
    vertical-align super
    position relative
    display block
    border-radius 50%
    color rgba(255,255,255,0.5)
    border 3px solid
    box-shadow 2px 2px 2px #888888
    i
    width 52px
    height 52px
    background rgba(255,255,255,0.5)
    display block
    margin auto
    vertical-align middle
    top 4px
    position relative
    line-height 100%
    border-radius 50%
    &::before
    color rgba(60, 60, 61,0.7)
    font-size 30px
    text-align center
    font-weight bolder
    top 20px
    position relative
    &:hover
    color #eda60c
    i
    background #eda60c
    &::before
    color white
    .user-panel
    transform rotate3d(-1,1,0,20deg)
    display none
    position absolute
    width 250px
    height auto
    background rgba(231, 231, 232,0.9)
    border-radius 10px
    left -290px
    top -170px
    box-shadow 3px 3px 2px #888888
    &:before
    position absolute
    content ''
    border-left 25px solid #f9f9f9
    border-top 10px solid transparent
    border-bottom 10px solid transparent
    left 250px
    top calc(50% - 10px)

    .user-panel
    .user-panel-name
    font-size 20px
    color rgba(60, 60, 61,0.7)!important
    display block
    width 100%
    height 30px
    position relative
    background #f9f9f9
    border-top-left-radius 10px
    border-top-right-radius 10px
    .user-panel-img
    padding-inline 30px
    padding-bottom 0px
    padding-top 20px
    width 100%
    height 50%
    overflow hidden
    background #f9f9f9
    text-align center
    box-shadow -1px 10px 15px #888888
    img
    width 100%
    &::after
    position absolute
    content ''
    border-top 15px solid #f9f9f9
    border-right 10px solid transparent
    border-left 10px solid transparent
    left 30%
    top calc(50% + 45px)
    .user-panel-properties
    font-size 18px
    padding 20px
    margin 0 auto
    height 150px
    width 100%
    overflow scroll
    display inline-block
    color rgba(60, 60, 61,0.7)!important
    text-align left
    &::-webkit-scrollbar
    display none
    .user-panel-name
    &::after
    display block
    width 80%
    margin-left 10%
    height 2px
    content ''
    background rgba(160, 159, 160,0.9)
    border-radius 5px

    .menu-list
    display none
    position relative
    border 1px solid
    left 90px
    border-image linear-gradient(to top, transparent, #f9f9f9 20%, #f9f9f9 80%, transparent) 0 0 0 1
    margin-left 10px
    width auto
    height auto
    padding 10px 30px
    &:before
    position absolute
    content ''
    border-right 25px solid #f9f9f9
    border-top 10px solid transparent
    border-bottom 10px solid transparent
    left -30px
    top calc(50% - 10px)
    &:after
    position absolute
    content ''
    border 3px solid #333
    border-radius 50%
    left -20px
    top 50%
    transform translateY(-50%)

    .menu-list-item
    display block
    border-radius 3px
    background rgba(249, 249, 249, 0.79)
    color #494748
    width 180px
    padding 20px 25px
    margin-bottom 5px
    position relative
    left -25px
    font-weight bolder
    color rgb(77, 72, 73)
    box-shadow 3px 3px 2px #888888
    height 40px
    text-align left
    &:last-child
    margin-bottom 0
    &:hover
    cursor pointer
    background-color #eda60c
    color #f9f9f9
    .menu-child
    display block
    z-index 10000
    i
    &::before
    cursor pointer
    background-color #f9f9f9
    color #eda60c
    i
    display block
    vertical-align super
    font-family 'SAOUI' ,Langar,-apple-system, sans-serif
    font-size 20px
    position absolute
    border-radius 0
    background rgba(77, 72, 73,0)!important
    left -10px
    width 100%
    margin-left 15px
    top -2px
    left -15px
    padding-top 10px
    &::before
    margin-inline 15px
    font-size 16px
    color white
    background rgb(77, 72, 73)
    padding 5px
    border-radius 50%
    top -2px
    @extend .fontawesomeIcon
    .menu-child
    display none
    position relative
    border 1px solid
    left 180px
    border-image linear-gradient(to top, transparent, #f9f9f9 20%, #f9f9f9 80%, transparent) 0 0 0 1
    margin-left 20px
    width auto
    height auto
    padding 10px 30px
    &:before
    position absolute
    content ''
    border-right 25px solid #f9f9f9
    border-top 10px solid transparent
    border-bottom 10px solid transparent
    left -30px
    top calc(50% - 10px)
    &:after
    position absolute
    content ''
    border 3px solid #333
    border-radius 50%
    left -20px
    top 50%
    transform translateY(-50%)

    .menu-list-child
    display block
    border-radius 3px
    background rgba(249, 249, 249, 0.79)!important
    color #494748
    width 180px
    padding 20px 25px
    margin-bottom 5px
    position relative
    left -25px
    top 0px
    font-weight bolder
    color rgb(77, 72, 73)
    box-shadow 3px 3px 2px #888888
    height 40px
    &:last-child
    margin-bottom 0
    i
    display block
    font-family 'SAOUI' ,Langar,-apple-system, sans-serif
    font-size 20px
    vertical-align super
    position absolute
    border-radius 0
    background rgba(77, 72, 73,0)!important
    left -10px
    width 100%
    height 40px
    margin-left 15px
    top -2px
    left -15px
    padding-top 10px
    &::before
    margin-inline 15px
    font-size 16px
    color white!important
    width 20px
    height 20px
    background rgb(77, 72, 73)!important
    padding 5px
    border-radius 50%
    top -2px
    @extend .fontawesomeIcon
    &:hover
    cursor pointer
    background-color #eda60c!important
    color #f9f9f9!important
    i
    &::before
    cursor pointer
    background-color #f9f9f9!important
    color #eda60c!important

    .active
    display block!important
    if hexo-config('SAO_Utils.ThreeD')
    #SAO-menu
    animation Updown 1s linear infinite alternate
    .utils-list-item
    .user-panel
    transform rotate3d(0,-1,0,20deg)

    .menu-list
    transform rotate3d(0,1,0,20deg)

    .menu-child
    transform rotate3d(0,1,0,5deg)
    .user-panel
    .user-panel-img
    box-shadow -1px 10px 15px #888888
    @keyframes Updown
    from
    margin-top 20px
    to
    margin-top 10px
    if hexo-config('SAO_Utils.hoverShow') ? hexo-config('SAO_Utils.hoverShow') : 1
    .utils-list-item
    &:hover
    .user-panel
    display block
    z-index 10000
    .menu-list-item
    &:hover
    .menu-child
    display block
    z-index 10000

    if hexo-config('SAO_Utils.Instructions') ? hexo-config('SAO_Utils.Instructions') : 1
    #SAO-logout
    z-index 9999
    background rgba(204, 204, 207,0.8)
    font-family 'SAOUI' ,Langar,-apple-system, sans-serif
    font-weight bolder
    text-shadow 1px 1px 1px #888888
    height 240px
    width 350px
    display none
    position fixed
    left calc(50% - 150px)
    top calc(50% - 100px)
    border-radius 5px
    box-shadow 2px 2px 10px #888888

    .logout-title
    background rgba(249, 249, 249, 0.8)
    color rgba(60, 60, 61,0.7)
    height 60px
    width 100%
    display block
    font-size 20px
    text-align center
    border-top-left-radius 5px
    border-top-right-radius 5px
    padding-top 10px

    .logout-alert
    background rgba(220, 220, 220,0.8)
    color rgba(60, 60, 61,0.7)
    height 90px
    width 100%
    display block
    text-align center
    padding-top 30px
    box-shadow 0px 0px 15px rgb(188, 188, 188) inset

    .logout-button
    background rgba(249, 249, 249, 0.8)
    height 90px
    width 100%
    display block
    text-align center
    border-bottom-left-radius 5px
    border-bottom-right-radius 5px
    padding-top 30px

    .logout-confirm
    background rgba(#2f79d4,0)
    border-radius 50%
    display inline-block
    width 36px
    height 36px
    margin-inline 60px
    border 1px solid
    border-color #2f79d4
    button
    background #2f79d4
    text-align center
    border-radius 50%
    font-size 18px
    color white
    display block
    width 30px
    height 30px
    margin 2px

    .logout-cancel
    background rgba(#cb3749,0)
    border-radius 50%
    display inline-block
    width 36px
    height 36px
    margin-inline 60px
    border 1px solid
    border-color #cb3749
    button
    background #cb3749
    text-align center
    border-radius 50%
    font-size 18px
    font-weight bolder
    color white
    display block
    width 30px
    height 30px
    margin 2px

    .activeLogout
    display block!important
    animation flashOpen 1s ease alternate

    @keyframes flashOpen
    from
    transform rotateX(90deg)
    to
    transform rotateX(0deg)
  3. 修改[Blogroot]\themes\butterfly\layout\includes\additional-js.pug,引入右键菜单网页元素,注意butterfly_v3.6.0取消了缓存配置,转为完全默认,需要将{cache:theme.fragment_cache}改为{cache: true}:
      if theme.pjax.enable
    !=partial('includes/third-party/pjax', {}, {cache:theme.fragment_cache})

    !=partial('includes/third-party/baidu_push', {}, {cache:theme.fragment_cache})

    + if theme.SAO_Utils.enable
    + !=partial('includes/SAO-menu', {}, {cache:theme.fragment_cache})
  4. 新建[Blogroot]\themes\butterfly\source\js\SAO_Menu.js,控制右键菜单的显隐。此处的脚本引入使用了async异步加载。因为全部都是触发类函数,在监听到相应的点击或悬停事件之前不会执行,所以甚至不会有加载完成后执行脚本的那段阻塞时间。
    window.document.oncontextmenu = function(event) {
    if (event.ctrlKey) return true; //ctrl+右键 使用原生右键
    if (/Android|webOS|BlackBerry/i.test(navigator.userAgent)) return true; //媒体选择
    return popMenu(event); //打开右键菜单
    };

    //监听位移动作,实现拖动效果
    var dv = document.getElementById("SAO-menu");
    var x = 0;
    var y = 0;
    var l = 0;
    var t = 0;
    var isDown = false;
    dv.onmousedown = function(e) {
    x = e.clientX;
    y = e.clientY;
    l = dv.offsetLeft;
    t = dv.offsetTop;
    isDown = true;
    dv.style.cursor = "move"
    };
    window.onmousemove = function(e) {
    if (isDown == false) {
    return
    }
    var nx = e.clientX;
    var ny = e.clientY;
    var nl = nx - (x - l);
    var nt = ny - (y - t);
    dv.style.left = nl + "px";
    dv.style.top = nt + "px"
    };
    dv.onmouseup = function() {
    isDown = false;
    dv.style.cursor = "default"
    };
    //监听鼠标点击动作,点击空白处关闭菜单
    document.addEventListener("click", function(event) {
    if (event.target.id === 'SAO-back'){
    SAOclose()
    }
    });
    //关闭菜单
    function SAOclose() {
    var mymenu = document.getElementById('SAO-back');
    mymenu.style.display = "none";
    }

    //初始化,隐藏所有active类
    function SAOinit() {
    var activedItems = document.querySelectorAll('.active');
    if (activedItems) {
    for (i = 0; i < activedItems.length; i++) {
    activedItems[i].classList.remove('active');
    }
    }
    }
    //二级菜单初始化,隐藏所有三级菜单的active类
    function Menuinit(menu) {
    var activedChild = menu.querySelectorAll('.active');
    if (activedChild) {
    for (i = 0; i < activedChild.length; i++) {
    activedChild[i].classList.remove('active');
    }
    }
    }
    //link start,处理链接跳转的pjax请求;调用了主题自带的pjax对象。其他主题需要另外适配。
    function linkStart(link){
    if (link.includes('https://') || link.includes('http://') ){
    window.location.href = link;
    }
    else{
    if (pjax){
    pjax.loadUrl(link);
    }
    else{
    window.location.href = link;
    }
    }
    }
    //点击菜单内元素播放点击音频
    //无子菜单时,点击音效
    function clickAudio() {
    var clickAudio = document.getElementById("SAOClick");
    if (clickAudio) {
    clickAudio.play();//有音频时播放
    }
    }
    //有子菜单时,展开音效
    function panelAudio() {
    var panelAudio = document.getElementById("SAOPanel");
    if (panelAudio) {
    panelAudio.play();//有音频时播放
    }
    }
    //警告音效
    function alertAudio() {
    var alertAudio = document.getElementById("SAOAlert");
    if (alertAudio) {
    alertAudio.play();//有音频时播放
    }
    }
    //关闭当前页面
    function confirmLogout(){
    setTimeout(function(){
    window.opener=null;
    window.open('','_self');
    window.close();
    },500);
    }
    function openLogout(){
    SAOclose();
    var logoutWindow = document.getElementById('SAO-logout');
    logoutWindow.classList.add('activeLogout');
    }
    function cancelLogout(){
    var logoutWindow = document.getElementById('SAO-logout');
    logoutWindow.classList.remove('activeLogout');
    }

    //监听一级菜单点击动作,控制二级菜单显隐
    function UtilsClick(){
    var thisElement = event.target;
    // console.log('click at', thisElement);
    var hasMenu = thisElement.parentElement.querySelector('.menu-list');
    var hasPanel = thisElement.parentElement.querySelector('.user-panel');
    var hasActived = thisElement.parentElement.querySelector('.active');
    //当有二级菜单或属性栏被激活时
    if (hasActived){
    // console.log('hasmenu or haspanel]');
    //如果有二级菜单且已经激活,则重新隐藏二级菜单
    if (hasMenu && (hasMenu.className.indexOf('active') > -1) ){
    hasMenu.classList.remove('active');
    }
    //如果有属性栏且已经激活,则重新隐藏属性栏
    if (hasPanel && (hasPanel.className.indexOf('active') > -1) ){
    hasPanel.classList.remove('active');
    }
    }
    else{
    SAOinit();//先初始化以清除其他的激活项
    //若有二级菜单就显示二级菜单
    if (hasMenu) {
    hasMenu.classList.add('active');
    }
    //若有属性栏,就显示属性栏
    if (hasPanel) {
    hasPanel.classList.add('active');
    }
    }
    }
    //监听二级菜单点击动作,控制三级菜单显隐
    function MenusClick(){
    var thisElement = event.target;
    // console.log('click at', thisElement);
    //当前按钮指向的三级菜单
    var hasChild = thisElement.parentElement.querySelector('.menu-child');
    //当前按钮所在的二级菜单
    var menuParent = thisElement.parentElement.parentElement;
    if (hasChild) {
    //若已经激活了,则再点击就隐藏;
    if (hasChild.className.indexOf('active') > -1) {
    hasChild.classList.remove('active');
    }
    //若未激活
    else{
    Menuinit(menuParent);//先初始化以清除其他的激活项
    hasChild.classList.add('active');//再给当前子菜单添加激活
    }
    }
    }

    function popMenu(event) {
    //播放菜单打开音乐
    SAOinit();
    var audio = document.getElementById("SAOlauncher");
    if (audio) {
    audio.play();//有音乐时打开
    }
    document.getElementById('SAO-back').style.display = "block";
    var mymenu = document.getElementById('SAO-menu');
    var screenWidth = document.documentElement.clientWidth || document.body.clientWidth;
    var screenHeight = document.documentElement.clientHeight || document.body.clientHeight;
    // 菜单显示
    mymenu.style.display = 'block';

    //---- start 点击位置旁边生成。不自动判断位置
    // mymenu.style.left = (event.clientX + mymenu.clientWidth/2) + "px";
    // mymenu.style.top = (event.clientY - mymenu.clientHeight/2) + "px";
    //--- end 点击位置旁边生成。不自动判断位置
    //---- start 自动判断确保给菜单边缘留下足够位置
    //根据当前位置决定菜单出现位置,确保菜单可完整显示
    var currentX = event.clientX;
    var currentY = event.clientY;
    //X轴判断
    //偏左
    if (currentX < 230){
    mymenu.style.left = (mymenu.clientWidth/2 + 230) + "px";
    }
    //偏右
    else if ((screenWidth - currentX) < 320 ){
    mymenu.style.left = (mymenu.clientWidth/2 - 320 + screenWidth) + "px";
    }
    //默认情况
    else{
    mymenu.style.left = (currentX + mymenu.clientWidth/2) + "px";
    }
    //Y轴判断
    //偏高
    if (currentY < (mymenu.clientHeight/2 + 130)){
    mymenu.style.top = "130px";
    }
    //偏低
    else if ((screenHeight - currentY) < (80 + mymenu.clientHeight/2)){
    mymenu.style.top = ( screenHeight - mymenu.clientHeight - 80) + "px";
    }
    //默认情况
    else {
    mymenu.style.top = (event.clientY - mymenu.clientHeight/2) + "px";
    }
    //---- end 自动判断确保给菜单边缘留下足够位置
    //屏蔽原生菜单
    return false;
    }
    //自定义动作
    //搜索按钮动作
    function openSearch() {
    document.body.style.cssText = 'width: 100%;overflow: hidden'
    document.querySelector('#local-search .search-dialog').style.display = 'block'
    document.querySelector('#local-search-input input').focus()
    btf.fadeIn(document.getElementById('search-mask'), 0.5)
    if (!loadFlag) {
    search(GLOBAL_CONFIG.localSearch.path)
    loadFlag = true
    }
    // shortcut: ESC
    document.addEventListener('keydown', function f (event) {
    if (event.code === 'Escape') {
    closeSearch()
    document.removeEventListener('keydown', f)
    }
    })
    }
    //在线聊天按钮动作
    function openTidio() {
    window.tidioChatApi.show();window.tidioChatApi.open();
    }
    //镜像站跳转动作
    function Mirror() {
    let pathname;
    let hostname;
    let url;
    pathname = window.location.pathname;
    hostname = window.location.hostname;
    if (hostname === 'akilar.top') {
    url = "https://akilar.gitee.io" + pathname;
    window.alert("即将前往糖果屋分店🍬");
    window.location.href = url;
    }
    else if(hostname === 'akilar.gitee.io') {
    url = "https://akilar.top" + pathname;
    window.alert("正在返回糖果屋本部🍭!");
    window.location.href = url;
    }
    else {
    window.alert("Master,本地调试不需要跳转哦!🍫");
    }
    }
    //评论窗口跳转
    function ToComment(){
    var hasComment = document.getElementById('post-comment');
    if (hasComment){
    window.location.href = '#post-comment'; //如果有评论区就跳转到评论区
    }
    else{
    linkStart('/comments/');//如果没有,就跳转到留言板
    };
    }
  5. 修改[Blogroot]\_config.butterfly.yml,添加CDN配置项和菜单选项:
    • CDN配置项:
        CDN:
      # main
      main_css: /css/index.css
      jquery: https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js
      main: /js/main.js
      utils: /js/utils.js
      + # SAO_Utils
      + SAO_Utils: /js/SAO_Menu.js
    • SAO_Utils菜单配置项示例:
      # SAO_Utils右键菜单
      SAO_Utils:
      # 基础选项
      enable: true # 总开关
      ThreeD: true # 3D效果
      Instructions: true # 说明书
      hoverShow: true # 悬停显示属性面板
      music:
      enable: true # 点击音效开关
      Launcher: /assets/Launcher.wav #启动音效,留空则使用默认项
      Click: /assets/Click.wav # 点击音效,留空则使用默认项
      # 菜单显示
      # 一级菜单
      util_list:
      # 配置图标选项、属性面板和二级菜单
      - icon: fa fa-question-circle
      panel: # 选填,左侧属性面板
      title: Help # 选填,面板标题
      img: /img/info.png # 选填,面板图片,留空则使用默认项
      prop: Anything can I help you ? #选填,面板内容,支持html语法
      top: -170 # 选填,面板偏移量,用来控制和一级菜单按钮的对齐,留空则使用默认项
      link: # 链接,和action、menu_list互斥,只能填一个
      action: # 自定义动作,和link、menu_list互斥,只能填一个
      menutop: #控制二级菜单的偏移量来调整和一级菜单的对齐,一般不用填。
      menu_list: # 二级菜单,和action、link互斥
      - name: Tidio # 名称
      icon: fa fa-comment-dots # 图标
      action: openTidio() # 自定义动作
      - name: Candyhome
      icon: fa fa-user-friends
      link: https://jq.qq.com/?_wv=1027&k=a08BZRzs
      - name: Document
      icon: fa fa-folder-open
      childtop: #控制三级菜单的偏移量来调整和二级菜单的对齐,一般不用填。
      child_list: # 三级菜单
      - name: Beautify # 名称
      icon: fa fa-file-invoice # 图标
      link: /posts/f99b208/ # 链接
      # SAO_Utils右键菜单
      SAO_Utils:
      enable: true
      ThreeD: true
      Instructions: true
      hoverShow: true
      music:
      enable: true
      Launcher: /assets/Launcher.wav
      Click: /assets/Click.wav
      Alert: /assets/Alert.wav
      Panel: /assets/Panel.wav
      util_list:
      - icon: fa fa-link
      link: /link/
      panel:
      title: Friends
      img:
      prop: Welcome to Sword Art Online !
      top:
      - icon: fa fa-question-circle
      panel:
      title: Help
      img:
      prop: Anything can I help you ?
      top:
      menu_list:
      - name: Tidio
      icon: fa fa-comment-dots
      action: openTidio()
      - name: Comments
      icon: fa fa-comments
      action: ToComment()
      - name: Candyhome
      icon: fa fa-user-friends
      link: https://jq.qq.com/?_wv=1027&k=a08BZRzs
      - icon: fa fa-tasks
      panel:
      title: Menu
      img:
      prop: The menu of my blog
      top:
      menu_list:
      - name: Home
      icon: fa fa-home
      link: https://blog.akilar.top/
      - name: Document
      icon: fa fa-folder-open
      child_list:
      - name: Beautify
      icon: fa fa-file-invoice
      link: /posts/f99b208/
      - name: Optimize
      icon: fa fa-file-invoice
      link: /posts/7c16c4bb/
      - name: Tag_Plugins
      icon: fa fa-file-invoice
      link: /posts/615e2dec/
      - name: Construct
      icon: fa fa-file-invoice
      link: /posts/6ef63e2d/
      - name: Blog
      icon: fa fa-blog
      link: /
      - name: Archives
      icon: fa fa-archive
      link: /archives/
      - name: Categoties
      icon: fa fa-folder-open
      link: /categories/
      - name: Tags
      icon: fa fa-tags
      link: /tags/
      - icon: fa fa-tools
      panel:
      title: Options
      img:
      prop: Some userful scripts
      top:
      menu_list:
      - name: Mirror
      icon: fa fa-indent
      action: Mirror()
      - name: Random
      icon: fa fa-random
      link: /random/
      - name: Search
      icon: fa fa-search
      action: openSearch()
  6. 因为这次的配置逻辑较为繁复,所以参数解释会比较多:
序号参数备选值参数释义
0enabletrue , falsetrue为开启右键菜单,false为关闭右键菜单
1ThreeDtrue , falsetrue为开启3D效果,false为关闭3D效果
2Instructionstrue , falsetrue为开启说明书,false为关闭说明书。默认开启。内附本帖链接,可能的话,希望可以开着帮我做下宣传
3hoverShowtrue , falsetrue为开启悬停显示,false为关闭悬停显示。默认开启。控制属性栏和三级菜单的悬停显隐。
4.1music.enabletrue , falsetrue为开启点击音效,false为关闭点击音效
4.2music.LauncherUrl,音乐文件的相对路径或外链右键点击打开菜单时的音效,留空则使用默认音效
4.3music.ClickUrl,音乐文件的相对路径或外链左键点击菜单选项时的音效,留空则使用默认音效
4.4music.AlertUrl,音乐文件的相对路径或外链右键点击退出按钮时的音效,留空则使用默认音效
4.5music.PanelUrl,音乐文件的相对路径或外链左键点击含子菜单的选项时的音效,留空则使用默认音效
5util_list见下文一级菜单选项
5.1util_list.iconeg:fa fa-link菜单选项图标,使用fontawesome,也可以使用iconfont
5.2util_list.panel见下文角色属性面板
5.2.1util_list.panel.titleText面板标题
5.2.2util_list.panel.imgUrl,图片相对链接或图床绝对链接面板图片,默认使用角色属性面板图
5.2.3util_list.panel.propHtml面板内容,支持Html语法
5.2.4util_list.panel.topNumber eg:-150属性面板偏移量,确保面板右侧箭头对着相应的一级菜单,填写数字即可,不需要加单位
5.3util_list.linkUrl链接,站内建议使用相对路径,站外需要使用带协议的绝对路径,与actionmenu_list互斥,只能填写一个
5.4util_list.actionFunction点击动作,详见本帖拓展内容,与linkmenu_list互斥,只能填写一个
5.5util_list.menutopNumber eg:-150二级菜单面板偏移量,确保面板右侧箭头对着相应的一级菜单,填写数字即可,不需要加单位可选,当前偏移量是根据公式计算的,一般都会对齐
5.6util_list.menu_list见下文仅一级菜单支持该配置项
5.6.1menu_list.nameText菜单选项标题
5.6.2menu_list.iconeg:fa fa-link菜单选项图标,使用fontawesome,也可以使用iconfont
5.6.3menu_list.linkUrl链接,站内建议使用相对路径,站外需要使用带协议的绝对路径,与actionchild_list互斥,只能填写一个
5.6.4menu_list.actionFunction点击动作,详见本帖拓展内容,与linkchild_list互斥,只能填写一个
5.6.5menu_list.childtopNumber eg:-150二级菜单面板偏移量,确保面板右侧箭头对着相应的一级菜单,填写数字即可,不需要加单位。可选,当前偏移量是根据公式计算的,一般都会对齐
5.6.6menu_list.child_list类似于menu_list仅二级菜单支持该配置项,其余下辖配置项与menu_list相同
5.6.6.1child_list.nameText菜单选项标题
5.6.6.2child_list.iconeg:fa fa-link菜单选项图标,使用fontawesome,也可以使用iconfont
5.6.6.3child_list.linkUrl链接,站内建议使用相对路径,站外需要使用带协议的绝对路径,与action互斥,只能填写一个
5.6.6.4child_list.actionFunction点击动作,详见本帖拓展内容,与link互斥,只能填写一个

自定义脚本拓展

点击查看脚本拓展内容

糖果屋出品的右键菜单提供了自定义js配置,读者可以通过封装自己的js脚本,直接通过菜单选项调用。以下会分享几个简单示例。更多内容可以自行探索。希望可以启发读者,在评论区留下更多有趣的脚本。

使用方法:在上文的menu_list或者child_list配置项的action填写函数名即可正常调用。注意actionlink互斥。所以写了action就不要写link

- name: Mirror
icon: fa fa-indent
action: Mirror()
- name: Search
icon: fa fa-search
action: openSearch()
- name: Tidio
icon: fa fa-server
action: openTidio()

功能:针对gitee镜像站和当前站点的同篇文章跳转,记得更改链接。


function Mirror() {
let pathname;
let hostname;
let url;
pathname = window.location.pathname;
hostname = window.location.hostname;
if (hostname === 'akilar.top') {
url = "https://akilar.gitee.io" + pathname;
window.alert("即将前往糖果屋分店🍬");
window.location.href = url;
}
else if(hostname === 'akilar.gitee.io') {
url = "https://akilar.top" + pathname;
window.alert("正在返回糖果屋本部🍭!");
window.location.href = url;
}
else {
window.alert("Master,本地调试不需要跳转哦!🍫");
}
}

功能:打开local-search搜索按钮(提取自Butterfly源码,其他主题可能不生效)。


function openSearch() {
document.body.style.cssText = 'width: 100%;overflow: hidden'
document.querySelector('#local-search .search-dialog').style.display = 'block'
document.querySelector('#local-search-input input').focus()
btf.fadeIn(document.getElementById('search-mask'), 0.5)
if (!loadFlag) {
search(GLOBAL_CONFIG.localSearch.path)
loadFlag = true
}
// shortcut: ESC
document.addEventListener('keydown', function f (event) {
if (event.code === 'Escape') {
closeSearch()
document.removeEventListener('keydown', f)
}
})
}

功能:打开algolia搜索按钮(提取自Butterfly源码,其他主题可能不生效)。


function openAlgolia() {
document.body.style.cssText = 'width: 100%;overflow: hidden'
document.querySelector('#algolia-search .search-dialog').style.display = 'block'
document.querySelector('#algolia-search .ais-search-box--input').focus()
btf.fadeIn(document.getElementById('search-mask'), 0.5)
// shortcut: ESC
document.addEventListener('keydown', function f (event) {
if (event.code === 'Escape') {
closeSearch()
document.removeEventListener('keydown', f)
}
})
}

功能:打开Tidio在线聊天界面(提取自Butterfly源码,其他主题可能不生效)


function openTidio() {
window.tidioChatApi.show();window.tidioChatApi.open();
}

功能:若当前页面有评论区,则跳转评论区,若没有,则跳转到留言板页面,评论区的挂载ID和留言板路径可能不一致,请自己根据实际情况替换。


function ToComment(){
var hasComment = document.getElementById('post-comment');
if (hasComment){
window.location.href = '#post-comment'; //如果有评论区就跳转到评论区
}
else{
linkStart('/comments/');//如果没有,就跳转到留言板
//linkStart是本帖的SAO_Menu.js中带的跳转函数,用于pjax适配
}
}

功能:关闭当前页面。既然是SAO,怎么可以不致敬一下登出键呢?(对无痕窗口不生效,会提示scripts may close only the windows that were opened by them)


//关闭当前页面
function Logout(){
window.opener=null;
window.open('','_self');
window.close();
}

TO DO

SAO风格的右键菜单

二级菜单显隐逻辑适配

Ctrl+右键恢复原生菜单

适配pjax,站内跳转不打断全局音乐

补全左侧圆形列表

补全左侧角色属性栏样式

3D显示效果

添加浮动动态动画