点击查看更新记录

足迹

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. 好玩还好看

教程

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

// 这个语句的作用就是取代了BF原生的悬浮窗,不想要的话可以删掉(不确保没BUG)
// 注意:如果你使用了这段代码,请务必保证它在比较靠后的位置执行,否则可能会出现代码执行的时候btf还没有被定义的问题
btf.snackbarShow = (text, time = 3500) => kms.pushInfo(text, null, time)

const kms = {

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

/** 缓存 */
_cache: {
win: new Map(),
winCode: 0
},
/**
* 在右上角弹出悬浮窗
* @param text {string} 悬浮窗文本
* @param button {{icon: string?, text: string, desc: string?, onclick: function?}}
* 传入null表示没有按钮(icon: 图标,text: 按钮文本,desc: 描述文本, cb: 点击按钮时触发的回调)
* @param time {number} 持续时间
* @return {function(void)} 返回一个函数对象,不接受任何参数,调用可手动关闭悬浮窗
*/
pushInfo: (text, button = null, time = 3500) => {
const idMap = kms._cache.win
/**
* 移动指定悬浮窗
* @param id {string} 悬浮窗ID
* @param direct {boolean} 移动方向,true为上,false为下
*/
const moveWin = (id, direct) => {
const list = document.getElementsByClassName('float-win')
const moveHeight = document.getElementById(id).offsetHeight + 10
for (let i = list.length - 1; i !== -1; --i) {
const div = list[i]
if (div.id === id) break
const value = parseInt(div.getAttribute('move')) + (direct ? -moveHeight : moveHeight)
div.setAttribute('move', value)
div.style.transform = `translateY(${value}px)`
}
}
/**
* 关闭指定悬浮窗
* @param id {string} 悬浮窗ID
* @param move {boolean} 是否移动其余悬浮窗
*/
const closeWin = (id, move = true) => new Promise(() => {
const div = document.getElementById(id)
if (!div || div.classList.contains('delete')) return
div.onanimationend = () => {
idMap.delete(div.id)
document.body.removeChild(div)
}
div.classList.add('delete')
div.style.transform = ''
if (move) moveWin(id, true)
})
/** 关闭多余的悬浮窗 */
const closeRedundantWin = maxCount => new Promise(() => {
const list = document.getElementsByClassName('float-win')
if (list && list.length > maxCount) {
const count = list.length - maxCount
for (let i = 0; i !== count; ++i) {
closeWin(list[list.length - i - 1].id, false)
}
}
})
/** 构建html代码 */
const buildHTML = id => {
const cardID = `float-win-${id}`
const actionID = `float-action-${id}`
const exitID = `float-exit-${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 ${button ? 'click' : ''
}" id="${cardID}" move="0"><i class="fa fa-info-circle"></i><button class="exit" id="${exitID}"><i class="fa fa-times"></i></button><p class="text">${text}</p>${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 id = kms._cache.winCode++
document.body.insertAdjacentHTML('afterbegin', buildHTML(id))
const actionButton = document.getElementById(`float-action-${id}`)
const exitButton = document.getElementById(`float-exit-${id}`)
const cardID = `float-win-${id}`
actionButton && actionButton.addEventListener('click', () => {
closeWin(cardID)
if (button.onclick) button.onclick()
})
exitButton.addEventListener('click', () => closeWin(cardID))
const div = document.getElementById(cardID)
div.onmouseover = () => div.setAttribute('over', true)
div.onmouseleave = () => div.removeAttribute('over')
moveWin(cardID, false)
closeRedundantWin(3)
const task = setInterval(() => {
const win = document.getElementById(cardID)
if (win) {
if (win.hasAttribute('over')) return idMap.set(cardID, 0)
const age = (idMap.get(cardID) || 0) + 100
idMap.set(cardID, age)
if (age < time) return
}
clearInterval(task)
closeWin(cardID)
}, 100)
// noinspection CommaExpressionJS
return () => (closeWin(cardID), undefined)
}
}

  接着我们新建一个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
.float-win

position fixed
top 70px
width 310px
z-index 9997
background-color var(--km-float-win-bg)
color var(--km-globle-font)
border-radius 15px
box-shadow 2px 2px 5px 3px var(--km-button-shadow)
transition all 1s
animation win-entrance 1.3s forwards

&.delete
animation win-exit 1.2s forwards

@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 0
margin-bottom 20px
word-break break-word
word-wrap break-word

&.click
& > .text
margin-bottom 0

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

&:hover
color deepskyblue
transform rotate(90deg)

& > .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 9998
transition all 1s

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

& > .action
display inline-block
position relative
float right
z-index 9999
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)

@keyframes win-entrance
from
right -310px
opacity 0.5
to
right 20px
opacity 1

@keyframes win-exit
from
right 20px
to
right -500px
opacity 0.3

  styl中用到了一些变量,变量表如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
:root
--km-float-win-bg whitesmoke
--km-globle-font black
--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-globle-font white
--km-button-dark-hover #49505d
--km-button-dark-bg #1f1f1f
--km-button-font #66ccff
--km-button-shadow #212121

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

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

笔记

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

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

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

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

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

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

参考资料


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