点击查看更新记录

足迹

2023/02/03

  • 悬浮窗允许自定义位置(左上、左下、右上、右下)
  • 修改函数参数格式
  • 修改悬浮窗显示顺序

2023/01/01

  • 微调样式
  • 修改函数的返回值

2022/08/11

  • 重构代码

2022/08/07

  • 修改悬浮窗ID的存储方式
  • 移动端按钮描述文本改为常显

2022/07/04

  • 修复带按钮的悬浮窗显示效果异常的问题

2022/07/01

  • 压缩文件体积

2022/05/27

  • 修复一些小漏洞

2022/05/25

  • 发布初版

预览

点击下方按钮就可以查看带按钮的悬浮窗的样式了

点击触发

特性

  1. 打开后超时自动关闭
  2. 附加关闭按钮,可手动关闭
  3. 鼠标悬浮在悬浮窗上会重置自动关闭计时器
  4. 附带全套动画
  5. 可以在悬浮窗上添加一个附带文字描述的按钮并自定义点击功能
  6. 可以同时显示多个悬浮窗(有上限,超过上限会直接关闭额外的悬浮窗)
  7. 适配黑白两色主题
  8. 好玩还好看

教程

  修改[theme]/layout/includes/layout.pug(注意缩进):

  上面这个文件是butterfly主题用户的路径,其它主题的用户需要自行判断代码插入位置。实际上这个代码只需要保证每个页面都存在且存在于display不是none的元素中即可,插入的位置不固定,可以变动。

1
2
3
4
5
6
7
8
9
10
   body
+ .float-box.left.top
+ .float-box.left.bottom
+ .float-box.right.top
+ .float-box.right.bottom

if theme.preloader
!=partial('includes/loading/loading', {}, {cache: true})
if theme.background
#web_bg

  首先我们新建一个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
// noinspection JSIgnoredPromiseFromCall

// 这个语句的作用就是取代了BF原生的悬浮窗,不想要的话可以删掉(不确保没BUG)
document.addEventListener(
'DOMContentLoaded',
() => btf.snackbarShow = (text, time = 3500) => kms.pushInfo({text, time}, null)
)

const kms = {

/** 是否为移动端 */
isMobile: 'ontouchstart' in document.documentElement,

/** 缓存 */
_cache: {
winCode: 0
},
/**
* 在右上角弹出悬浮窗
* @param {{text: string, time:number?, left:boolean?, bottom:boolean?}|string} optional
* 配置项(text: 要显示的内容,time: 持续时间,left: 是否靠左显示,bottom: 是否靠下显示
* @param button {?{icon:string?, text:string, desc:string?, onclick:function?}}
* 传入null表示没有按钮(icon: 图标,text: 按钮文本,desc: 描述文本, onclick: 点击按钮时触发的回调)
* @return {{close:function, onclose:function}} 一个对象,其中有两个函数对象,调用`close`可手动关闭悬浮窗,添加`onclose`可监听悬浮窗的关闭
*/

pushInfo: (optional, button = null) => {
let {text, time, left, bottom} = optional
if (typeof optional === 'string') text = optional
if (!time) time = 3500
const externalApi = {} // 对外部暴露的接口
const id = kms._cache.winCode++
const cardID = `float-win-${id}`
const actionID = `float-action-${id}`
const exitID = `float-exit-${id}`
/**
* 关闭指定悬浮窗
* @param id {string} 悬浮窗ID
*/

const closeWin = id => {
const div = document.getElementById(id)
if (div) {
const {classList, style} = div
if (classList.contains('close')) return
if (externalApi.onclose) externalApi.onclose()
style.maxHeight = `${div.clientHeight + 10}px`
classList.add('close')
setTimeout(() => div.remove(), 1000)
setTimeout(() => {
style.maxHeight = style.marginBottom = '0'
classList.remove('show')
}, 25)
}
}
/** 关闭多余的悬浮窗 */
const closeRedundantWin = className => {
// noinspection SpellCheckingInspection
const list = document.querySelector(`.float-box${className}`).children
const count = list.length - 3
for (let k = 0, i = 0; k < count && i !== list.length; ++i) {
closeWin(list[i].id)
++k
}
}
/** 构建html代码 */
const buildHTML = id => {
const buttonDesc = (button && button.desc) ? `<div class="descr"><p ${kms.isMobile ? 'class="open"' : ''}>${button.desc}</p></div>` : ''
// noinspection HtmlUnknownAttribute
return `<div class="float-win ${left ? 'left' : 'right'} ${bottom ? 'bottom' : 'top'} ${button ? 'click' : ''
}
" id="${cardID}" move="0" style="z-index:${id};"><button class="exit" id="${exitID}"><i class="iconfont icon-close"></i></button><div class="text">${text}</div>${button ?
'<div class="select"><button class="action" id="' + actionID + '">' + (button.icon ? '<i class="' + button.icon + '">' : '') +
'</i><p class="text">' + button.text + `</p></button>${buttonDesc}` : ''}
</div></div>`

}
const className = `${left ? '.left' : '.right'}${bottom ? '.bottom' : '.top'}`
// noinspection SpellCheckingInspection
document.querySelector(`.float-box${className}`).insertAdjacentHTML('beforeend', buildHTML(id))
const cardDiv = document.getElementById(cardID)
const actionButton = document.getElementById(actionID)
const exitButton = document.getElementById(exitID)
if (actionButton) {
actionButton.onclick = () => {
if (button.onclick) button.onclick()
closeWin(cardID)
}
}
exitButton.onclick = () => closeWin(cardID)
cardDiv.onmouseover = () => cardDiv.setAttribute('over', '1')
cardDiv.onmouseleave = () => cardDiv.removeAttribute('over')
closeRedundantWin(className)
setTimeout(() => cardDiv.classList.add('show'), 25)
let age = 0
const task = setInterval(() => {
const win = document.getElementById(cardID)
if (win) {
if (win.hasAttribute('over')) return age = 0
age += 100
if (age < time) return
}
clearInterval(task)
closeWin(cardID)
}, 100)
externalApi.close = () => closeWin(cardID)
return externalApi
}
}

  接着我们新建一个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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
:root
--km-float-win-bg whitesmoke
--km-button-dark-hover #0084ff
--km-button-dark-bg #66ccff
--km-button-font white
--km-button-shadow hsla(0, 0%, 22%, 0.2)

[data-theme="dark"]
--km-float-win-bg #141414
--km-button-dark-hover #49505d
--km-button-dark-bg #1f1f1f
--km-button-font #66ccff
--km-button-shadow #212121

.float-box
position fixed
width 310px
display flex
flex-wrap wrap
flex-direction column
z-index 120

&.left
left 20px
align-items flex-start

&.right
right 20px
align-items flex-end

&.top
top 70px

&.bottom
bottom 35px

.float-win
position relative
overflow hidden
width 100%
background-color var(--km-float-win-bg)
color var(--font-color)
border-radius 15px
box-shadow 2px 2px 5px 3px var(--km-button-shadow)
transition all 1s
opacity 0
margin-bottom 15px

&.show
opacity 1

&.left
left -400px

&.show
left 0

&.right
right -400px

&.show
right 0

@media screen and (max-width: 550px) and (min-width: 250px)
width calc(65%)

@media screen and (max-width: 250px)
width calc(80%)

& > .select > .descr
color transparent

& > i
margin-left 10px

& > .text
position relative
max-width 84%
margin-left 8%
text-align center
margin-top 25px
margin-bottom 20px
word-break break-word
word-wrap break-word

img
max-width 90%
object-fit contain
border-radius 10px

&.click
& > .text
margin-bottom 0

& > .exit
position absolute
color var(--text-highlight-color)
right 0
top 0
width 30px
height 30px
transition all 0.5s

&:hover
color deepskyblue
transform rotate(90deg)

i
font-size 1em

& > .select
position relative
text-align right
margin-bottom 10px
height 30px

& > .descr
position relative
margin-top 0
margin-right 6px
display inline-block
height 30px
overflow hidden

& > p
position relative
margin 0
height 30px
z-index 121
transition all 1s

&:not(.open)
left 100px
opacity 0

& > .action
display inline-block
position relative
float right
z-index 121
height 30px
margin-right 10px
background-color var(--km-button-dark-bg)
color var(--km-button-font)
box-shadow 1px 1px 2px var(--km-button-shadow)
padding 10px
border-radius 7px
transition all 0.5s

& > .text
display inline
position relative
bottom 4px
font-weight bold

& > i
position relative
bottom 4px
margin-right 5px

&:hover
background-color var(--km-button-dark-hover)

& + .descr > p
left 0
opacity 1

[data-theme='light']
.float-win
box-shadow 3px 4px 6px 5px var(--km-button-shadow)

  最后在合适的地方引入刚刚编写的文件即可,具体用法我就说一点,注释里已经写的很详细了。

  在这里写的几个函数中,所有以下划线(_)开头的函数和变量均为内部调用,外部不应当调用。至于为什么外露出来,一是JS没有像其它语言那样的可见域控制(也有可能是有但是我不知道),二是方便调试。

笔记

  虽然这个悬浮窗实现并不复杂,不过对于这种基本不懂前端开发,只会照葫芦画瓢的人来说还是比较有难度的,用了一下午才把悬浮窗弄好。 开发中也遇到了不少问题(不然也不会花这么长时间),难为我最长时间的就是悬浮窗内部元素的排版了,光想办法把按钮放在右边,提示文本放在左边就花费了至少一个小时。

  不过通过这次实践,我也学到了不少东西,比如了解了更多的CSS 选择器、了解了setInterval的用法……

  这的确是应了那句老话:“实践出真知。”多实践就能不断巩固已有的知识,同时学习新的知识。

下面的话主要实给同学看的

  现在有不少人都称呼我为“大佬”,实际上我也就是在代码编写方面有稍微多一点的经验,看了我上面的话应该也不难发现,前端开发我就是个渣渣。实际上不仅是我,任何“大佬”都不会擅长所有领域。同时大佬也一定不是一天养成的,现在的我前端是渣渣,说不定以后我就是前端的大牛了呢?

  所以也不要因为身边有那么几个水平超出自己非常多的人就自暴自弃,只要自己不放弃就有赶上甚至超越那些人的希望,一旦放弃就毫无希望了。

参考资料


创作不易,扫描下方打赏二维码支持一下吧ヾ(≧▽≦*)o