效果图
实现
左侧渐变条通过 CSS 实现,利用 background
属性进行背景定位结合 linear-gradient
线性渐变函数实现
右侧滑块引入 ElementUI 的 el-slider
组件,自定义样式,其中 marks
展开标记通过定位修改其默认位置达到刻度值的效果
封装成组件 VolumeBar.vue
这时候产品经理又说了,每个音频组件需要动态地控制显示和隐藏,如何在动刀最小的前提下满足它呢,总不至于通过 js 给一个个组件单独绑定事件去控制吧。这里用到了伪元素
结合 :hover 伪类
,当鼠标悬浮到每个组件上时显示伪元素样式,并通过 content: attr(data-hide)
动态指定其要显示的文本,需要在对应的元素上添加自定义属性,这里是 data-hide
代码如下:
<template>
<div v-if="!isHide || isEdit" :class="{ 'hide-wrapper': isEdit }" :data-hide="isHide ? '显示' : '隐藏'"
@click="showOrHide">
<div :class="['volume-panel', isHide ? 'hide-pannel' : '', isEdit ? 'prevent' : '']">
<div class="label">{{ name }}</div>
<div class="volume">
<!-- 渐变音柱 -->
<div class="volume-bar">
<div class="volume-bar-inner top-bar" :style="{ height: `${100 - sliderValue}%` }"></div>
<div class="volume-bar-inner bottom-bar" :style="{ height: `${sliderValue}%` }"></div>
</div>
<!-- 音量控制滑块 -->
<el-slider v-model="sliderValue" @input="sliderValueChange" vertical height="200px" :show-tooltip="false"
:marks="marks"></el-slider>
</div>
<el-input-number v-model="volumeInput" @change="volumeInputChange" :min="min" :max="max"></el-input-number>
</div>
</div>
</template>
<script>
export default {
props: {
name: String,
min: Number,
max: Number,
isEdit: Boolean,
},
data() {
return {
sliderValue: 0,
volumeInput: 0,
isHide: false,
}
},
computed: {
marks() {
let obj = {}
let step = (this.max - this.min) / 10
for (let i = 0; i <= 10; i++) {
let value = this.min
value += step * i
obj[i * 10] = Math.round(value).toString()
}
return obj
},
},
methods: {
showOrHide() {
if (!this.isEdit) return
this.isHide = !this.isHide
},
transferToSliderValue(volumeInput) {
this.sliderValue = Math.abs(this.min - volumeInput) / (this.max - this.min) * 100
},
transferToVolumeInput(sliderValue) {
this.volumeInput = Math.round(this.min + ((this.max - this.min) / 100) * sliderValue)
},
sliderValueChange() {
this.transferToVolumeInput(this.sliderValue)
},
volumeInputChange() {
this.transferToSliderValue(this.volumeInput)
}
}
}
</script>
<style lang="scss" scoped>
.hide-wrapper {
position: relative;
&:hover {
border: 1px solid #0C87F0;
&::before {
content: attr(data-hide);
position: absolute;
z-index: 1;
height: 70px;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(12, 135, 240, .8);
}
}
}
.volume-panel {
width: 108px;
background: url('@/assets/images/video/bg_sound.png') no-repeat center / 100% 100%;
display: flex;
flex-direction: column;
align-items: center;
.label {
line-height: 40px;
text-align: center;
}
.volume {
margin-bottom: 10px;
opacity: .95;
display: flex;
.volume-bar {
transform: translateX(-10px);
width: 10px;
background: linear-gradient(180deg, #E54948 2%, #FFB637 43%, #0BD4FF 97%);
position: relative;
.volume-bar-inner {
position: absolute;
left: 0;
width: 100%;
height: 50%;
}
.bottom-bar {
bottom: 0;
height: 70%;
background: linear-gradient(180deg, #333C49 50%, rgba(0, 0, 0, .01) 50%) bottom;
background-size: 100% 4px;
}
.top-bar {
height: 30%;
top: 0;
background: linear-gradient(0deg, #333C49 50%, #5D6878 50%) top;
background-size: 100% 4px;
}
}
}
}
.hide-pannel {
background: rgb(40, 47, 60);
}
.prevent {
pointer-events: none;
}
:deep(.el-slider) {
.el-slider__runway {
background-color: #1D2128;
border-top: 4px solid #1D2128;
border-bottom: 4px solid #1D2128;
.el-slider__stop {
width: 30px;
height: 1px;
background-color: #8695A6;
z-index: -1;
left: -13px;
}
.el-slider__marks-text {
left: 20px;
font-size: 10px;
color: #657181;
}
}
.el-slider__bar {
background-image: linear-gradient(160deg, #2673DC 18%, #62B6FF 90%);
}
.el-slider__button {
width: 20px;
height: 26px;
border: none;
border-radius: 0;
background: url('@/assets/images/video/btn_sound_hand.png') center / 100% 100%;
}
}
:deep(.el-input-number) {
width: 100px;
margin: 8px;
line-height: 30px;
.el-input__inner {
padding: 0;
height: 32px;
background-color: #454f61;
border: 1px solid #303a48;
color: #fff;
}
.el-input-number__decrease,
.el-input-number__increase {
width: 30px;
color: #fff;
background-color: #454f61;
}
.el-input-number__decrease {
border-right: 1px solid #303a48;
}
.el-input-number__increase {
border-left: 1px solid #303a48;
}
}
</style>
在父组件中引入:
<template>
<div id="app">
<el-button type="text" @click="isEdit = !isEdit">{{ isEdit ? '退出编辑' : '编辑' }}</el-button>
<div class="volumes">
<volume-bar v-for="(item, index) in volumes" :key="index" :isEdit="isEdit" :name="item.name" :min="0" :max="100" />
</div>
</div>
</template>
<script>
import VolumeBar from '@/components/VolumeBar.vue'
export default {
name: 'App',
components: {
VolumeBar
},
data() {
return {
volumes: [
{ name: 'OUT1' },
{ name: 'OUT2' },
{ name: 'OUT3' },
],
isEdit: false
}
}
}
</script>
<style>
#app {
width: 100%;
height: 100%;
padding: 20px;
background-color: #242a35;
color: #fff;
}
.volumes {
display: flex;
gap: 20px;
}
</style>