mand-mobile/components/steps/index.vue

407 lines
10 KiB
Vue

<template>
<div
class="md-steps"
:class="{
'md-steps-vertical': direction == 'vertical',
'md-steps-horizontal': direction == 'horizontal',
'vertical-adaptive': direction == 'vertical' && verticalAdaptive,
'no-current': currentLength % 1 !== 0
}"
>
<template v-for="(step, index) of steps">
<div class="step-wrapper"
:class="[$_getStepStatusClass(index)]"
:key="`steps-${index}`"
>
<div class="icon-wrapper">
<slot
v-if="index < currentLength && (($scopedSlots.reached) || $slots.reached)"
name="reached"
:index="index"
></slot>
<slot
v-else-if="index === currentLength && (($scopedSlots.current) || $slots.current)"
name="current"
:index="index"
></slot>
<md-icon
v-else-if="index === currentLength"
name="success"
></md-icon>
<div v-else class="step-node-default">
<div class="step-node-default-icon" style="width: 6px;height: 6px;border-radius: 50%;"></div>
</div>
</div>
<div class="text-wrapper">
<slot
v-if="$scopedSlots.content"
name="content"
:index="index"
:step="step"
></slot>
<template v-else>
<div class="name">
{{step.name}}
</div>
<div class="desc" v-if="step.text">
{{step.text}}
</div>
</template>
</div>
</div>
<div class="bar"
:class="[direction === 'horizontal' ? 'horizontal-bar' : 'vertical-bar']"
:style="$_getStepSizeForStyle(index)"
:key="`bar-${index}`"
>
<i
class="bar-inner"
v-if="progress[index]"
:style="$_barInnerStyle(index)"
></i>
</div>
</template>
</div>
</template>
<script>
import Icon from '../icon'
import {toArray} from '../_util'
export default {
name: 'md-steps',
components: {
[Icon.name]: Icon,
},
props: {
steps: {
type: Array,
default() {
return []
},
},
current: {
type: Number,
default: 0,
validator(val) {
return val >= 0
},
},
direction: {
type: String,
default: 'horizontal',
},
transition: {
type: Boolean,
default: false,
},
verticalAdaptive: {
type: Boolean,
default: false,
},
},
data() {
return {
initialed: false,
progress: [],
stepsSize: [],
currentLength: 0,
duration: 0.3,
timer: null,
}
},
computed: {
$_barInnerStyle() {
return index => {
const {progress} = this
const transform =
this.direction === 'horizontal'
? `(${(progress[index]['len'] - 1) * 100}%, 0, 0)`
: `(0, ${(progress[index]['len'] - 1) * 100}%, 0)`
return {
transform: `translate3d${transform}`,
transition: `all ${progress[index]['time']}s linear`,
}
}
},
},
watch: {
current(val, oldVal) {
const currentStep = this.$_formatValue(val)
const newProgress = this.$_sliceProgress(currentStep)
if (this.transition) {
const isAdd = currentStep >= oldVal
this.timer && clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.$_doTransition(newProgress, isAdd, len => {
if ((isAdd && len > this.currentLength) || (!isAdd && len < this.currentLength)) {
this.currentLength = len
}
})
}, 100)
} else {
this.progress = newProgress
this.currentLength = currentStep
}
},
},
created() {
const currentStep = this.$_formatValue(this.current)
this.currentLength = currentStep
this.progress = this.$_sliceProgress(currentStep)
},
mounted() {
this.$_initStepSize()
},
updated() {
this.$nextTick(() => {
this.$_initStepSize()
})
},
methods: {
// MARK: private methods
$_initStepSize() {
if (this.direction !== 'vertical' || this.verticalAdaptive) {
return
}
const iconWrappers = this.$el.querySelectorAll('.icon-wrapper')
const textWrappers = this.$el.querySelectorAll('.text-wrapper')
const stepsSize = toArray(textWrappers).map((wrapper, index) => {
let stepHeight = wrapper.clientHeight
const iconHeight = iconWrappers[index].clientHeight
if (index === textWrappers.length - 1) {
// The last step needs to subtract floated height
stepHeight -= iconHeight
} else {
// Add spacing between steps to prevent distance too close
stepHeight += 40
}
return stepHeight > 0 ? stepHeight : 0
})
if (stepsSize.toString() !== this.stepsSize.toString()) {
this.stepsSize = stepsSize
}
},
$_getStepSizeForStyle(index) {
const size = this.direction === 'vertical' && !this.verticalAdaptive ? this.stepsSize[index] : 0
return size
? {
height: `${size}px`,
}
: null
},
$_getStepStatusClass(index) {
const currentLength = this.currentLength
let status = []
if (index < currentLength) {
status.push('reached')
}
if (index === Math.floor(currentLength)) {
status.push('current')
}
return status.join(' ')
},
$_formatValue(val) {
if (val < 0) {
return 0
} else if (val > this.steps.length - 1) {
return this.steps.length - 1
} else {
return val
}
},
$_sliceProgress(current) {
return this.steps.slice(0, this.steps.length - 1).map((step, index) => {
const offset = current - index
const progress = this.progress[index]
const isNewProgress = progress === undefined
let len, time
if (offset <= 0) {
len = 0
} else if (offset >= 1) {
len = 1
} else {
len = offset
}
time = (isNewProgress ? len : Math.abs(progress.len - len)) * this.duration
return {
len,
time,
}
})
},
$_doTransition(progress, isAdd, step) {
let currentLength = isAdd ? 0 : this.currentLength
const walk = index => {
if ((index < progress.length) & (index > -1) && progress[index]) {
if (isAdd) {
currentLength += progress[index].len
} else {
currentLength -= this.progress[index].len - progress[index].len
}
setTimeout(() => {
index += isAdd ? 1 : -1
step(currentLength)
walk(index)
}, progress[index].time * 1000)
}
this.$set(this.progress, index, progress[index])
}
walk(isAdd ? 0 : progress.length - 1)
},
},
}
</script>
<style lang="stylus">
.md-steps
display flex
justify-content space-around
font-size 28px
&.md-steps-horizontal
align-items center
padding 40px 100px 100px
.step-wrapper
margin 0 4px
justify-content center
align-items center
flex-direction column
&.reached
.text-wrapper .name
color steps-text-color
&.current
.text-wrapper .name
color steps-color-active
.text-wrapper
top 100%
padding-top steps-text-gap-horizontal
text-align center
.name
color steps-desc-color
.desc
margin-top 10px
color steps-desc-color
&.no-current
.reached:last-of-type
display none !important
&.md-steps-vertical
align-items flex-start
padding 40px
flex-direction column
&.vertical-adaptive
justify-content normal
padding 40px 40px 8px
.bar.vertical-bar
flex 1
.step-wrapper
width 100%
margin 4px 0
align-items stretch
.icon-wrapper
position relative
justify-content flex-start
.step-node-default
min-width steps-icon-size
min-height steps-icon-size
.text-wrapper
left steps-icon-size
padding-left steps-text-gap-vertical
.name, .desc
white-space normal
.name
color steps-text-color
.desc
margin-top 18px
color steps-desc-color
.icon-wrapper
display flex
justify-content center
align-items center
color steps-color-active
>div
display flex
justify-content center
align-items center
&:nth-child(2)
display none
.step-node-default-icon
background steps-color
.step-wrapper
display flex
position relative
min-width steps-icon-size
min-height steps-icon-size
.icon-wrapper
min-width steps-icon-size
min-height steps-icon-size
.md-icon
width steps-icon-size
height steps-icon-size
font-size steps-icon-size
line-height steps-icon-size
.text-wrapper
position absolute
.name, .desc
white-space nowrap
.name
line-height steps-text-font-size
font-size steps-text-font-size
.desc
line-height steps-text-font-size
font-size steps-desc-font-size
&.reached
.icon-wrapper .step-node-default-icon
background steps-color-active
.bar
position relative
background-color steps-color
overflow hidden
.bar-inner
z-index 10
position absolute
top 0
left 0
display block
content ''
transition all linear 1s
&.horizontal-bar
flex 1
height steps-border-size
.bar-inner
width 100%
height steps-border-size
background-color steps-color-active
&.vertical-bar
left 16px
width steps-border-size
transform translateX(-50%)
.bar-inner
width steps-border-size
height 100%
background-color steps-color-active
&:last-of-type
&.horizontal-bar
display none
&.vertical-bar
visibility hidden
</style>