点击查看更新记录

更新记录

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

  1. 1.0版本正式版发布。最终确定此帖为SAO UI右半边菜单,适合可用菜单项较少且习惯精简风格的用户。

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

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

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

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

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

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

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

  1. 实现SAO风格的右键菜单
  2. 添加了点击音效,默认使用本站同款,可以自定义配置
  3. 支持添加链接或者自定义脚本动作
  4. 添加Ctrl+右键转换原生菜单功能
  5. 几个常用脚本分享
点击查看参考教程
参考方向教程原贴
菜单边框风格伪类样式实现方案codepen-Pure CSS SAO Menu Thing
右键菜单显隐逻辑和原生实现方案
样式风格参考,图标、音效资源采集SAO Utils
图标采集fontawesome
pjax适配参考

资源下载

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

写在最前

来自店长的碎碎念

2021.01.24

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

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

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

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

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

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

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

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

米娜桑,Link Start!

教程正文

SAO UI PLAN 相关项目为本站原创项目,因此均为内测版,在样式适配上仅针对本站进行调整,因此在泛用性上存在缺漏。对于可能遇到的bug,欢迎在评论区进行讨论。

在进行本帖的魔改前,请务必做好备份以便回退。

点击查看教程正文
  1. 新建[Blogroot]\themes\butterfly\layout\includes\SAO-menu.pug
    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
    #SAO-back
    #SAO-menu
    #SAO-menu-content
    #menu-list
    if theme.SAO_Utils.menu_list
    each item in theme.SAO_Utils.menu_list
    .menu-list-item(onclick='clickAudio()' onmouseover='Mouseover()' onmouseout='Mouseout()')
    if item.link
    i(class=item.icon onclick=`setTimeout(function(){linkStart('` +url_for(item.link)+ `')},500);`)= item.name
    else if item.action
    i(class=item.icon onclick=`setTimeout(function(){` + item.action + `},500);`)= item.name
    else
    i(class=item.icon)= item.name
    if item.child_list
    .menu-child(style=`top: -` + (30 * (item.child_list.length + 1) ) + `px;`)
    each child in item.child_list
    .menu-list-child(onclick='clickAudio()')
    if child.link
    i(class=child.icon onclick=`setTimeout(function(){linkStart('` +url_for(child.link)+ `')},500);`)= child.name
    else if child.action
    i(class=child.icon onclick=`setTimeout(function(){` + child.action + `},500);`)= child.name
    else
    i(class=child.icon)= child.name
    if theme.SAO_Utils.music.enable
    - var Launcher = theme.SAO_Utils.music.Launcher ? url_for(theme.SAO_Utils.music.Launcher) : 'https://npm.elemecdn.com/测试-candyassets/audio/Launcher.wav'
    - var Click = theme.SAO_Utils.music.Click ? url_for(theme.SAO_Utils.music.Click) : 'https://npm.elemecdn.com/测试-candyassets/audio/Click.wav'
    audio#SAOlauncher(src=Launcher)
    audio#SAOClick(src=Click)
    script(async src=url_for(theme.CDN.SAO_Utils))
  2. 新建[Blogroot]\themes\butterfly\source\css\_layout\SAO_Menu.styl:
    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
    if hexo-config('SAO_Utils.enable')
    #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
    if hexo-config('SAO_Utils.ThreeD')
    .left
    transform rotate3d(-1, -1, 0, 35deg)!important
    .top
    transform rotate3d(1, 1, 0, 35deg)!important
    #SAO-menu
    display none
    position absolute
    border 1px solid
    border-image linear-gradient(to top, transparent, #f9f9f9 20%, #f9f9f9 80%, transparent) 0 0 0 1
    margin-left 30px
    width auto
    font-family Langar,-apple-system, sans-serif
    if hexo-config('SAO_Utils.ThreeD')
    transform rotate3d(-1, 1, 0, 35deg)
    animation Updown 1s linear infinite alternate
    @keyframes Updown
    from
    margin-top 20px
    to
    margin-top 10px
    &: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%)

    #SAO-menu-content
    max-height 300px
    overflow scroll
    width 430px
    padding-top 165px
    padding-bottom 15px
    &::-webkit-scrollbar
    display none

    #menu-list
    list-style-type none
    margin 0 5px
    padding 0
    width auto

    .menu-list-item
    background-color rgba(249, 249, 249, 0.79)
    width 200px
    padding 15px 25px
    margin-bottom 5px
    font-weight bolder
    color rgb(77, 72, 73)
    box-shadow 3px 3px 2px #888888
    height 50px
    border-radius 5px
    .active
    display: inline-block;
    i
    vertical-align super
    &::before
    margin-right 15px
    color: white;
    background: rgb(77, 72, 73);
    padding: 5px;
    border-radius: 50%;
    &:hover
    cursor pointer
    background-color #eda60c
    color #f9f9f9
    i
    &::before
    color: #eda60c;
    background: white;
    .menu-child
    display inline-block
    &:last-child
    margin-bottom 0

    .menu-list-child
    display display
    background-color rgba(249, 249, 249, 0.79)
    color #494748
    width 200px
    padding 15px 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 50px
    border-radius 5px
    i
    vertical-align super
    &::before
    margin-right 15px
    color: white!important;
    background: rgb(77, 72, 73)!important;
    padding: 5px;
    border-radius: 50%;
    &:hover
    cursor pointer
    background-color #eda60c
    color #f9f9f9
    i
    &::before
    color: #eda60c!important;
    background: white!important;
    .menu-child
    display none
    position relative
    border 1px solid
    left 170px
    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%)
  3. 修改[Blogroot]\themes\butterfly\layout\includes\additional-js.pug,引入右键菜单网页元素,注意butterfly_v3.6.0取消了缓存配置,转为完全默认,需要将{cache:theme.fragment_cache}改为{cache: true}:
    1
    2
    3
    4
    5
    6
    7
      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异步加载。因为全部都是触发类函数,在监听到相应的点击或悬停事件之前不会执行,所以甚至不会有加载完成后执行脚本的那段阻塞时间。
    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
    window.document.oncontextmenu = function(event) {
    if (event.ctrlKey) return true; //ctrl+右键 使用原生右键
    if (/Android|webOS|BlackBerry/i.test(navigator.userAgent)) return true; //媒体选择
    return popMenu(event); //打开右键菜单
    };
    document.addEventListener("click", function(event) {
    var mymenu = document.getElementById('SAO-back');
    mymenu.style.display = "none";
    });
    //处理链接跳转的请求;调用了主题自带的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 Mouseover() {
    var thisChild = event.target.querySelector('.menu-child');
    if (thisChild) {
    thisChild.classList.add('active');
    }
    }
    function Mouseout() {
    var thisChild = event.target.querySelector('.menu-child');
    if (thisChild && thisChild.className.indexOf('active') > -1) {
    setTimeout(function() {
    thisChild.classList.remove('active');
    }, 100);
    }
    }
    function popMenu(event) {
    //播放菜单打开音乐
    var audio = document.getElementById("SAOlauncher");
    if (audio) {
    audio.play();//有音乐时打开
    }
    document.getElementById('SAO-back').style.display = "block";
    var mymenu = document.getElementById('SAO-menu');
    var menuContent = document.getElementById('SAO-menu-content');
    var screenWidth = document.documentElement.clientWidth || document.body.clientWidth;
    var screenHeight = document.documentElement.clientHeight || document.body.clientHeight;
    // 菜单显示
    mymenu.style.display = 'block';
    menuContent.scrollTop = '150';
    //根据当前位置决定菜单出现位置,确保菜单可完整显示
    if (event.clientX * 2 > screenWidth) {
    if ((event.clientX - menuContent.clientWidth) * 2 > screenWidth) {
    mymenu.style.left = (event.clientX - menuContent.clientWidth) + "px";//偏右时左移
    }
    else {
    mymenu.style.left = event.clientX + "px";
    }
    mymenu.classList.add('left');
    } else {
    mymenu.style.left = event.clientX + "px";
    mymenu.classList.remove('left');
    }
    if (event.clientY * 2 > screenHeight) {
    mymenu.style.top = (event.clientY - menuContent.clientHeight) + "px";//偏高时下降
    mymenu.classList.add('top');
    } else {
    mymenu.style.top = event.clientY + "px";
    mymenu.classList.remove('top');
    }
    if ((event.clientY * 2 > screenHeight) && (event.clientX * 2 > screenWidth)) {
    if (mymenu.className.indexOf('top') > -1) {
    mymenu.classList.remove('top');
    }
    if (mymenu.className.indexOf('left') > -1) {
    mymenu.classList.remove('left');
    }
    }
    return false; //屏蔽原生菜单
    }
  5. 修改[Blogroot]\_config.butterfly.yml,添加CDN配置项和菜单选项:
    • CDN配置项:
      1
      2
      3
      4
      5
      6
      7
      8
        CDN:
      # main
      main_css: /css/index.css
      jquery: https://npm.elemecdn.com/jquery@latest/dist/jquery.min.js
      main: /js/main.js
      utils: /js/utils.js
      + # SAO_Utils
      + SAO_Utils: /js/SAO_Menu.js
    • SAO_Utils菜单配置项示例:
      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
      # SAO_Utils右键菜单
      SAO_Utils:
      enable: true
      ThreeD: true
      music:
      enable: true
      Launcher:
      Click:
      menu_list:
      - name: Link Start
      icon: fa fa-link
      link: /link/
      action:
      child_list:
      - name: Message
      icon: fa fa-envelope
      link:
      action:
      child_list:
      - name: Tidio
      icon: fa fa-server
      link:
      action: openTidio()
      - name: Comments
      icon: fa fa-comments
      link: /comments/
      action:
  6. 因为这次的配置逻辑较为繁复,所以参数解释会比较多:
参数备选值参数释义
enabletrue , falsetrue为开启右键菜单,false为关闭右键菜单
ThreeDtrue , falsetrue为开启3D效果,false为关闭3D效果
music.enabletrue , falsetrue为开启点击音效,false为关闭点击音效
music.Launcher音乐文件的相对路径或外链右键点击打开菜单时的音效,留空则使用默认音效
music.Click音乐文件的相对路径或外链左键点击菜单选项时的音效,留空则使用默认音效
menu_list见下文菜单选项
menu_list.nametext菜单选项标题
menu_list.iconeg:fa fa-link菜单选项图标,使用fontawesome,也可以使用iconfont
menu_list.linkurl链接,站内建议使用相对路径,站外需要使用带协议的绝对路径,与action互斥,只能填写一个
menu_list.actionfunction点击动作,详见本帖拓展内容,与link互斥,只能填写一个
menu_list.child_list类似于menu_list仅一级菜单支持该配置项,其余下辖配置项与menu_list相同
child_list.nametext菜单选项标题
child_list.iconeg:fa fa-link菜单选项图标,使用fontawesome,也可以使用iconfont
child_list.linkurl链接,站内建议使用相对路径,站外需要使用带协议的绝对路径,与action互斥,只能填写一个
child_list.actionfunction点击动作,详见本帖拓展内容,与link互斥,只能填写一个

自定义脚本拓展

点击查看脚本拓展内容

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

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

1
2
3
4
5
6
7
8
9
- 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镜像站和当前站点的同篇文章跳转,记得更改链接。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Mirror() {
let pathname;
let hostname;
let url;
pathname = window.location.pathname;
hostname = window.location.hostname;
if (hostname === '') {
url = "https://测试.gitee.io" + pathname;
window.alert("即将前往资源宝分店🍬");
window.location.href = url;
}
else if(hostname === '测试.gitee.io') {
url = "https://" + pathname;
window.alert("正在返回资源宝本部🍭!");
window.location.href = url;
}
else {
window.alert("Master,本地调试不需要跳转哦!🍫");
}
}

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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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源码,其他主题可能不生效)。


1
2
3
4
5
6
7
8
9
10
11
12
13
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源码,其他主题可能不生效)


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

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


1
2
3
4
5
6
7
8
9
10
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)


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

TO DO

SAO风格的右键菜单

二级菜单显隐逻辑适配

Ctrl+右键恢复原生菜单

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

补全左侧圆形列表;详见2.0

补全左侧角色属性栏样式;详见2.0

3D显示效果

添加浮动动态动画