背景
在教育考勤业务中,需要配置一段考勤范围,例如:在上课的前5分钟-上课后的5分钟时间考勤时间,在上课5分钟之后,算是迟到。
效果图
改造步骤
- 项目依赖中安装el-slider组件(需要使用其样式)
- 将组件主要源码拿过来,封成自定义组件(需要调整气泡的展示)
- 上课前使用一个slider,上课后使用一个slider,组合得到大致模样
- 修改时间抽和滑块按钮样式
原代码
// index.vue
<template>
<div class="custom-slider-box">
<div class="top-legend">
<div class="class-attend-time">
<div class="left-time-bar"></div>
<div class="right-time-text">考勤时间</div>
</div>
<div class="late-come-time">
<div class="left-time-bar"></div>
<div class="right-time-text">迟到时间</div>
</div>
</div>
<div class="top-config-slider">
<div class="course-begin-before">
<customSlider :max="20" v-model="value1" :format-tooltip="formatTooltip1"></customSlider>
</div>
<div class="course-begin-after">
<span class="class-begin-time">{{'上课'}}</span>
<customSlider :max="50" v-model="value2" :format-tooltip="formatTooltip2"></customSlider>
</div>
</div>
</div>
</template>
<script>
import customSlider from '@/EduMyCourse/components/customSlider/main.vue'
export default {
name: '1111',
components:{
customSlider
},
props:{},
data(){
return {
value1:15,
value2:5
}
},
watch:{},
computed:{},
methods:{
formatTooltip1(val){
return `前${20-val}min`
},
formatTooltip2(val){
return `后${val}min`
}
},
created(){},
mounted(){}
}
</script>
<style lang="less" scoped>
.legend-common{
display: flex;
align-items: center;
margin-right: 30px;
.left-time-bar{
width: 20px;
height: 10px;
margin-right: 6px;
}
}
.slider-common{
border-right: 1px solid #c1c1c1;
/deep/ .el-slider{
.el-slider__runway{
height: 32px;
border-radius: 0;
margin: 4px 0;
.el-slider__runway-click-area{
height: 32px;
top:0;
}
.el-slider__bar{
height: 32px;
border-radius: 0;
}
.el-slider__button-wrapper{
top: -2px;
.el-slider__button{
width: 10px;
height: 36px;
border-radius: 2px;
position: relative;
border: none;
box-shadow: 0 0 3px;
&:hover::after{
width: 0;
height: 0;
background-color: transparent;
}
}
}
}
}
}
.custom-slider-box{
padding:50px;
.top-legend{
display: flex;
margin-bottom: 50px;
.class-attend-time{
.legend-common();
.left-time-bar{
background-color: rgb(2, 191, 15);
}
}
.late-come-time{
.legend-common();
.left-time-bar{
background-color: rgb(255, 204, 0);
}
}
}
.top-config-slider{
display: flex;
height: 40px;
align-items: center;
}
.course-begin-before{
width: 270px;
.slider-common();
border-left: 1px solid #c1c1c1;
/deep/ .el-slider{
.el-slider__runway{
background-color: rgb(2, 191, 15);
.el-slider__bar{
background-color: #f2f2f2;
}
}
}
}
.course-begin-after{
width: 450px;
.slider-common();
position: relative;
.class-begin-time{
height: 20px;
line-height: 20px;
position: absolute;
left: 0;
bottom: -30px;
transform: translate(-50%);
color: #999;
}
/deep/ .el-slider{
.el-slider__runway{
background-color: rgb(255, 204, 0);
.el-slider__bar{
background-color: rgb(2, 191, 15);
}
}
}
}
}
</style>
// main.vue
<template>
<div
:class="{
'is-vertical': vertical,
'el-slider--with-input': showInput,
'el-slider--with-button': internalShowButton
}"
class="el-slider"
>
<!-- v-repeat-click="onFirstButtonClick" -->
<el-button
v-if="internalShowButton"
:icon="`h-icon-${vertical ? 'add' : 'minus'}`"
/>
<div
ref="slider"
:class="{
'show-input': showInput,
'show-button': internalShowButton,
disabled: disabled
}"
:style="runwayStyle"
class="el-slider__runway"
>
<!-- @click="onSliderClick" -->
<div
class="el-slider__runway-click-area"
style="cursor: default;"
/>
<div
:style="barStyle"
class="el-slider__bar"
/>
<slider-button
ref="button1"
v-model="firstValue"
:vertical="vertical"
@slider-click-value-change="sliderClickValueChange"
@drag-end="dragEnd"
@button-down="buttonDown(sliderInput)"
/>
<slider-button
v-if="range"
ref="button2"
v-model="secondValue"
:vertical="vertical"
@drag-end="dragEnd"
@button-down="buttonDown(sliderInput2)"
/>
<div v-if="showStops">
<template v-for="(item, index) in stops">
<div
:key="index"
class="el-slider__stop-wrap"
>
<div
:style="
vertical
? { bottom: item.stepWidth + '%' }
: { left: item.stepWidth + '%' }
"
class="el-slider__stop el-slider__stop--top el-slider__stop--left"
/>
<div
:style="
vertical
? { bottom: item.stepWidth + '%' }
: { left: item.stepWidth + '%' }
"
class="el-slider__stop el-slider__stop--bottom el-slider__stop--right"
/>
<div
v-show="showStopsNumber"
:style="
vertical
? { bottom: item.stepWidth + '%' }
: { left: item.stepWidth + '%' }
"
class="el-slider__mark"
>
{{ item.stepValue }}
</div>
</div>
</template>
</div>
</div>
<el-button
v-if="internalShowButton"
v-repeat-click="onSecondButtonClick"
:icon="`h-icon-${vertical ? 'minus' : 'add'}`"
/>
</div>
</template>
<script>
import SliderButton from './button.vue';
// import RepeatClick from 'hui/src/directives/repeat-click';
export default {
name: 'custom-Slider',
components: {
SliderButton
},
// directives: {
// RepeatClick
// },
props: {
min: {
type: Number,
default: 0
},
max: {
type: Number,
default: 100
},
step: {
type: Number,
default: 1
},
marks: {
type: [Array, Object],
default: () => []
},
value: {
type: [Number, Array],
default: 0
},
showInput: {
type: Boolean,
default: false
},
showButton: {
type: Boolean,
default: false
},
showInputControls: {
type: Boolean,
default: true
},
showStops: {
type: Boolean,
default: false
},
showStopsNumber: {
type: Boolean,
default: false
},
showTooltip: {
type: Boolean,
default: true
},
formatTooltip: {
type: Function,
default: null
},
disabled: {
type: Boolean,
default: false
},
range: {
type: Boolean,
default: false
},
vertical: {
type: Boolean,
default: false
},
height: {
type: String,
default: ''
}
},
data() {
return {
firstValue: null,
secondValue: null,
oldValue: null,
dragging: false,
sliderSize: 1,
sliderInput: null,
sliderInput2: null
};
},
computed: {
stops() {
const result = [];
const totalWidth = 100;
if (
this.marks.length > 0 ||
(Object.keys(this.marks) && Object.keys(this.marks).length > 0)
) {
// TODO: 判断数据正确性
const markObj = Array.isArray(this.marks)
? this.marks.reduce((prev, curr) => {
prev[curr] = curr;
return prev;
}, {})
: this.marks;
for (const key in markObj) {
result.push({
stepWidth: ((key - this.min) * totalWidth) / (this.max - this.min),
stepValue: markObj[key]
});
}
return result;
}
if (this.step === 0) {
process.env.NODE_ENV !== 'production' &&
console.warn('[HUI Warn][Slider]step should not be 0.');
return [];
}
const stopCount = (this.max - this.min) / this.step;
const stepWidth = (totalWidth * this.step) / (this.max - this.min);
for (let i = 0; i <= stopCount; i++) {
result.push({
stepWidth: i * stepWidth,
stepValue: this.min + i * this.step
});
}
return result;
},
minValue() {
return Math.min(this.firstValue, this.secondValue);
},
maxValue() {
return Math.max(this.firstValue, this.secondValue);
},
barSize() {
return this.range
? `${(100 * (this.maxValue - this.minValue)) / (this.max - this.min)}%`
: `${(100 * (this.firstValue - this.min)) / (this.max - this.min)}%`;
},
barStart() {
return this.range
? `${(100 * (this.minValue - this.min)) / (this.max - this.min)}%`
: '0%';
},
precision() {
const precisions = [this.min, this.max, this.step].map(item => {
const decimal = ('' + item).split('.')[1];
return decimal ? decimal.length : 0;
});
return Math.max.apply(null, precisions);
},
runwayStyle() {
return this.vertical ? { height: this.height } : {};
},
barStyle() {
return this.vertical
? {
height: this.barSize,
bottom: this.barStart
}
: {
width: this.barSize,
left: this.barStart
};
},
internalShowButton() {
return this.showButton && !this.range && !this.showInput;
}
},
watch: {
value(val, oldVal) {
if (
this.dragging ||
(Array.isArray(val) &&
Array.isArray(oldVal) &&
val.every((item, index) => item === oldVal[index]))
) {
return;
}
this.setValues();
},
dragging(val) {
if (!val) {
this.setValues();
}
},
firstValue(val) {
if (this.range) {
this.$emit('input', [this.minValue, this.maxValue]);
} else {
this.$emit('input', val);
}
},
secondValue() {
if (this.range) {
this.$emit('input', [this.minValue, this.maxValue]);
}
},
min() {
this.setValues();
},
max() {
this.setValues();
}
},
mounted() {
if (this.range) {
if (Array.isArray(this.value)) {
this.firstValue = Math.max(this.min, this.value[0]);
this.secondValue = Math.min(this.max, this.value[1]);
} else {
this.firstValue = this.min;
this.secondValue = this.max;
}
this.oldValue = [this.firstValue, this.secondValue];
} else {
if (typeof this.value !== 'number' || isNaN(this.value)) {
this.firstValue = this.min;
} else {
this.firstValue = Math.min(this.max, Math.max(this.min, this.value));
}
this.oldValue = this.firstValue;
}
this.resetSize();
window.addEventListener('resize', this.resetSize);
},
beforeDestroy() {
window.removeEventListener('resize', this.resetSize);
if (this.sliderInput) {
this.sliderInput.$destroy(true);
}
if (this.sliderInput2) {
this.sliderInput2.$destroy(true);
}
},
methods: {
valueChanged() {
if (this.range) {
return ![this.minValue, this.maxValue].every(
(item, index) => item === this.oldValue[index]
);
}
return this.value !== this.oldValue;
},
setValues() {
if (this.min > this.max) {
console.error('[Hui Error][Slider]min should not be greater than max.');
return;
}
const val = this.value;
if (this.range && Array.isArray(val)) {
if (val[1] < this.min) {
this.$emit('input', [this.min, this.min]);
} else if (val[0] > this.max) {
this.$emit('input', [this.max, this.max]);
} else if (val[0] < this.min) {
this.$emit('input', [this.min, val[1]]);
} else if (val[1] > this.max) {
this.$emit('input', [val[0], this.max]);
} else {
this.firstValue = val[0];
this.secondValue = val[1];
if (this.valueChanged()) {
this.$emit('change', [this.minValue, this.maxValue]);
this.oldValue = val.slice();
}
}
} else if (!this.range && typeof val === 'number' && !isNaN(val)) {
if (val < this.min) {
this.$emit('input', this.min);
} else if (val > this.max) {
this.$emit('input', this.max);
} else {
this.firstValue = val;
if (this.valueChanged()) {
this.$emit('change', val);
this.oldValue = val;
}
}
}
},
setPosition(percent, type) {
const targetValue = this.min + (percent * (this.max - this.min)) / 100;
if (!this.range) {
this.$refs.button1.setPosition(percent, type);
return;
}
let button;
if (
Math.abs(this.minValue - targetValue) <
Math.abs(this.maxValue - targetValue)
) {
button = this.firstValue < this.secondValue ? 'button1' : 'button2';
} else {
button = this.firstValue > this.secondValue ? 'button1' : 'button2';
}
this.$refs[button].setPosition(percent, type);
},
onSliderClick(event) {
if (this.disabled || this.dragging) {
return;
}
// add by zhangxiaogang
this.$emit(
'before-click',
event,
this.range && Array.isArray(this.value)
? [this.minValue, this.maxValue]
: this.value
);
this.resetSize();
if (this.vertical) {
const sliderOffsetBottom = this.$refs.slider.getBoundingClientRect()
.bottom;
this.setPosition(
((sliderOffsetBottom - event.clientY) / this.sliderSize) * 100,
'slider-click-value-change'
);
} else {
const sliderOffsetLeft = this.$refs.slider.getBoundingClientRect().left;
this.setPosition(
((event.clientX - sliderOffsetLeft) / this.sliderSize) * 100,
'slider-click-value-change'
);
}
},
sliderClickValueChange(value) {
if (this.range && Array.isArray(value)) {
this.$emit('slider-click', [this.minValue, this.maxValue]);
} else {
this.$emit('slider-click', value);
}
},
onSecondButtonClick() {
this.vertical
? (this.firstValue -= this.step)
: (this.firstValue += this.step);
},
onFirstButtonClick() {
this.vertical
? (this.firstValue += this.step)
: (this.firstValue -= this.step);
},
resetSize() {
if (this.$refs.slider) {
this.sliderSize = this.$refs.slider[
`client${this.vertical ? 'Height' : 'Width'}`
];
}
},
// add by zhangxiaogang
dragEnd() {
if (this.range && Array.isArray(this.value)) {
this.$emit('drag-end', [this.minValue, this.maxValue]);
} else {
this.$emit('drag-end', this.value);
}
},
// range show-input 情况下,点击 button聚焦 input add by yangzhini
buttonDown(sliderInput) {
// 点击失去焦点
if (this.$refs.input && this.$refs.input.$refs.input.$refs.input) {
this.$refs.input.$refs.input.$refs.input.blur();
}
if (!sliderInput) {
return;
}
this.sliderInputSelect(sliderInput);
},
sliderInputSelect(sliderInput) {
if (!sliderInput) {
return;
}
sliderInput.$refs.rangeInput.$refs.input.$refs.input.select();
},
handleBlur(event) {
this.$emit('blur', event);
}
}
};
</script>
// button.vue
<template>
<div
ref="button"
:style="wrapperStyle"
class="el-slider__button-wrapper"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@mousedown="onButtonDown"
>
<el-tooltip
ref="tooltip"
:disabled="!showTooltip"
effect="light"
placement="top"
v-model="toolTipShow"
:manual="true"
>
<span slot="content">
{{ formatValue }}
</span>
<div
class="el-slider__button"
/>
</el-tooltip>
</div>
</template>
<script>
export default {
name: 'CustomSliderButton',
props: {
value: {
type: Number,
default: 0
},
vertical: {
type: Boolean,
default: false
}
},
data() {
return {
hovering: false,
dragging: false,
startX: 0,
currentX: 0,
startY: 0,
currentY: 0,
startPosition: 0,
newPosition: null,
oldValue: this.value,
toolTipShow: true
};
},
computed: {
disabled() {
return this.$parent.disabled;
},
max() {
return this.$parent.max;
},
min() {
return this.$parent.min;
},
step() {
return this.$parent.step;
},
showTooltip() {
return (
this.$parent.showTooltip &&
!(this.$parent.showInput && this.$parent.range)
);
},
precision() {
return this.$parent.precision;
},
currentPosition() {
return `${((this.value - this.min) / (this.max - this.min)) * 100}%`;
},
enableFormat() {
return this.$parent.formatTooltip instanceof Function;
},
formatValue() {
return (
(this.enableFormat && this.$parent.formatTooltip(this.value)) ||
this.value
);
},
wrapperStyle() {
return this.vertical
? { bottom: this.currentPosition }
: { left: this.currentPosition };
}
},
watch: {
dragging(val) {
this.$parent.dragging = val;
}
},
methods: {
displayTooltip() {
// this.$refs.tooltip && (this.$refs.tooltip.showPopper = true);
},
hideTooltip() {
// this.$refs.tooltip && (this.$refs.tooltip.showPopper = false);
},
handleMouseEnter() {
// this.hovering = true;
// this.displayTooltip();
},
handleMouseLeave() {
// this.hovering = false;
// this.hideTooltip();
},
onButtonDown(event) {
if (this.disabled) {
return;
}
event.preventDefault();
this.$emit('button-down');
this.onDragStart(event);
window.addEventListener('mousemove', this.onDragging);
window.addEventListener('mouseup', this.onDragEnd);
window.addEventListener('contextmenu', this.onDragEnd);
},
onDragStart(event) {
this.dragging = true;
if (this.vertical) {
this.startY = event.clientY;
} else {
this.startX = event.clientX;
}
this.startPosition = parseFloat(this.currentPosition);
// add by zhangxiaogang
this.$parent.$emit('drag-start', event, this.oldValue);
},
onDragging(event) {
if (this.dragging) {
// this.displayTooltip();
this.$parent.resetSize();
let diff = 0;
if (this.vertical) {
this.currentY = event.clientY;
diff =
((this.startY - this.currentY) / this.$parent.sliderSize) * 100;
} else {
this.currentX = event.clientX;
diff =
((this.currentX - this.startX) / this.$parent.sliderSize) * 100;
}
this.newPosition = this.startPosition + diff;
this.setPosition(this.newPosition);
}
},
onDragEnd() {
if (this.dragging) {
/*
* 防止在 mouseup 后立即触发 click,导致滑块有几率产生一小段位移
* 不使用 preventDefault 是因为 mouseup 和 click 没有注册在同一个 DOM 上
*/
setTimeout(() => {
this.dragging = false;
// this.hideTooltip();
this.setPosition(this.newPosition);
// add by zhangxiaogang
this.$emit('drag-end');
}, 0);
window.removeEventListener('mousemove', this.onDragging);
window.removeEventListener('mouseup', this.onDragEnd);
window.removeEventListener('contextmenu', this.onDragEnd);
}
},
setPosition(newPosition, type) {
if (newPosition === null) {
return;
}
if (newPosition < 0) {
newPosition = 0;
} else if (newPosition > 100) {
newPosition = 100;
}
const lengthPerStep = 100 / ((this.max - this.min) / this.step);
const steps = Math.round(newPosition / lengthPerStep);
let value =
steps * lengthPerStep * (this.max - this.min) * 0.01 + this.min;
value = parseFloat(value.toFixed(this.precision));
this.newPosition = newPosition;
this.$emit('input', value);
type && this.$emit(type, value);
this.$refs.tooltip && this.$refs.tooltip.updatePopper();
if (!this.dragging && this.value !== this.oldValue) {
this.oldValue = this.value;
}
}
}
};
</script>