Util.js
export const Util = {
trim: function (str) {
if (str == null) {
return '';
}
return (str + '').trim();
},
vendorPropName: (function () {
var cssPrefixes = ['Webkit', 'Moz', 'O', 'ms'],
emptyStyle = document.createElement('div').style;
return function (name) {
if (name in emptyStyle) {
return name;
}
var capName = name[0].toUpperCase() + name.slice(1),
length = cssPrefixes.length;
for (var i = 0; i < length; i++) {
name = cssPrefixes[i] + capName;
if (name in emptyStyle) {
return name;
}
}
};
})(),
getStyle: function (elem, attr, isNumber) {
if (!elem) {
return;
}
let style;
if (getComputedStyle) {
style = getComputedStyle(elem)[attr];
} else if (document.documentElement.currentStyle) {
style = elem.currentStyle[attr];
}
elem = null;
if (isNumber) {
return parseFloat(style);
}
return style;
},
multip: function (n, bit = 1) {
if (!this.trim(n) || Number.isNaN(+n)) {
return '0';
}
n += '';
let nArr = n.split('.'),
digitN = (n.split('.')[1] || '').length,
res = '';
if (digitN > bit) {
res = nArr[0] + nArr[1].slice(0, bit) + '.' + nArr[1].slice(bit);
} else {
res = nArr[0] + (nArr[1] || '') + new Array(bit - digitN).fill(0).join('');
}
return res.replace(/^0+(?!\.)/, '');
},
};
Ruler.js
import './style.less';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Util } from '../../libs/utils';
export default class Ruler extends PureComponent {
constructor(props) {
super(props);
this.state = {
value: props.value || '',
liStyle: null,
rulerMax: 0,
rulerMin: 0,
rulerStep: 0,
};
this.isFirstClick = true;
this.lastPosition = 0;
this.eventLastPosition = 0;
this.transform = Util.vendorPropName('transform');
}
transformOffsetToValue = (offset) => {
return (((this.rulerMiddle - offset) / this.rulerStepWidth) * this.state.rulerStep + this.state.rulerMin).toFixed(this.valueDecimalDigit);
};
transformValueToOffset = (value) => {
return this.rulerMiddle - ((value - this.state.rulerMin) / this.state.rulerStep) * this.rulerStepWidth;
};
move = (offset = 0, setValue = true, triggerChange = true) => {
if (offset > this.minMove || offset < this.maxMove) {
return;
}
this.lastPosition = offset;
let state = {
style: {
[this.transform]: `translateX(${offset}px)`,
},
};
if (!this.focusing && setValue) {
state.value = this.transformOffsetToValue(offset);
}
this.setState(state, () => {
const { onChange } = this.props;
if (triggerChange && typeof onChange === 'function') {
onChange(this.state.value);
}
});
};
touchStart = (e) => {
if (this.noNeedMove) {
return;
}
this.focusing = false;
e.stopPropagation();
this.endX = this.startX = e.targetTouches[0].pageX;
if (this.startX <= 0) {
return;
}
this.eventMove = true;
this.endY = this.startY = e.targetTouches[0].pageY;
this.eventLastPosition = this.lastPosition;
};
touchMove = (e) => {
if (!this.eventMove) {
return;
}
this.endX = e.targetTouches[0].pageX;
this.endY = e.targetTouches[0].pageY;
if (this.touchEndTimer) {
clearTimeout(this.touchEndTimer);
}
this.touchEndTimer = setTimeout(this.touchEnd, 1000);
if (Math.abs(this.endX - this.startX) - Math.abs(this.endY - this.startY) > 0) {
this.move(this.eventLastPosition + this.endX - this.startX);
}
};
touchEnd = () => {
if (this.state.value >= '0') {
this.move(this.transformValueToOffset(this.state.value));
}
if (!this.eventMove) {
return;
}
if (this.touchEndTimer) {
clearTimeout(this.touchEndTimer);
}
if (this.changeTimer) {
clearTimeout(this.changeTimer);
}
this.eventMove = false;
};
componentDidMount() {
const { view, max, min, step, defaultValue, value, digits } = this.props;
let width = Util.getStyle(this.elem, 'width', true),
liWidth = Math.round(width / view / 10) * 10,
rulerStep = Util.multip(step),
rulerMax = Math.ceil(max / rulerStep) * rulerStep,
rulerMin = Math.floor(min / rulerStep) * rulerStep;
this.rulerMiddle = width / 2;
this.rulerStepWidth = liWidth;
this.valueDecimalDigit = digits;
this.minMove = this.rulerMiddle - ((min - rulerMin) / rulerStep) * liWidth;
this.maxMove = this.rulerMiddle - ((rulerMax - rulerMin) / rulerStep) * liWidth;
this.noNeedMove = (rulerMax - rulerMin) / rulerStep <= view;
let firstValue = +(value || defaultValue) || 0;
if (firstValue < min) {
firstValue = min;
} else if (firstValue > max) {
firstValue = max;
}
this.setState(
{
rulerMax,
rulerMin,
rulerStep,
liStyle: {
minWidth: liWidth + 'px',
maxWidth: liWidth + 'px',
},
},
() => {
this.move(this.transformValueToOffset(firstValue), !!value, false);
}
);
}
componentDidUpdate(prevProps) {
const { defaultValue, value } = this.props;
if (!this.state.value && prevProps.defaultValue !== defaultValue) {
this.move(this.transformValueToOffset(defaultValue), false, false);
} else if (value !== prevProps.value) {
this.move(this.transformValueToOffset(value), true, false);
}
}
componentWillUnmount() {
if (this.touchEndTimer) {
clearTimeout(this.touchEndTimer);
}
if (this.inputChangeTimer) {
clearTimeout(this.inputChangeTimer);
}
}
render() {
const { className, unit, id, unitName, min, disabled, digits, defaultValue } = this.props;
const { value, style, liStyle, rulerMax, rulerMin, rulerStep } = this.state;
return (
<div className={`component-ruler ${className}`} data-ubtid="sect-ruler" id={id}>
<header>
<div className="input-wrap empty">
{unitName} <span>{value || defaultValue}</span> {unit}
</div>
</header>
<section
ref={(el) => {
this.elem = el;
}}
onTouchStart={(!disabled && this.touchStart) || null}
onTouchMove={(!disabled && this.touchMove) || null}
onTouchEnd={(!disabled && this.touchEnd) || null}
onTouchCancel={(!disabled && this.touchEnd) || null}
>
<ul className="ruler" style={style}>
{new Array((rulerMax - rulerMin) / rulerStep || 0).fill(0).map((item, index, arr) => {
let isLast = index === arr.length - 1;
return (
<li style={liStyle} key={index}>
{isLast && <i></i>}
</li>
);
})}
</ul>
<ul className="mark" style={style}>
{new Array((rulerMax - rulerMin) / rulerStep || 0).fill(0).map((item, index, arr) => {
let isLast = index === arr.length - 1;
return (
<li style={liStyle} key={index}>
<span>{(min + index * rulerStep).toFixed(Math.max(digits - 1, 0))}</span>
{isLast && <span className="last">{(rulerMin + (index + 1) * rulerStep).toFixed(Math.max(digits - 1, 0))}</span>}
</li>
);
})}
</ul>
</section>
</div>
);
}
}
Ruler.defaultProps = {
disabled: false,
className: '',
min: 1,
max: 35,
step: 0.1,
digits: 1,
low: 4.4,
high: 10,
view: 3,
unitName: '',
unit: 'ml',
defaultValue: 6.5,
};
Ruler.propTypes = {
id: PropTypes.number,
disabled: PropTypes.bool,
className: PropTypes.string,
placeholder: PropTypes.string,
min: PropTypes.number,
max: PropTypes.number,
step: PropTypes.number,
digits: PropTypes.number,
low: PropTypes.number,
high: PropTypes.number,
view: PropTypes.number,
unit: PropTypes.string,
unitName: PropTypes.string,
defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
onChange: PropTypes.func,
};
调用方式:
<Modal visible={show} popup animationType={'slide-up'} onClose={onClose}>
<div className="water-modal-box">
<DrinkRuler onChange={(val) => getValue(val)} defaultValue={500} min={200} max={500} id={1} key={1} step={1} digits={0} />
<div className="water-modal-btn">
<span onClick={onClose}>取消</span>
<span onClick={waterAdd}>确认</span>
</div>
</div>
</Modal>
ruler.less
.component-ruler {
text-align: center;
header {
display: flex;
padding: 0 17px;
}
.img-wrap {
padding: 10px 18px;
img {
width: 30px;
height: 30px;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2);
border-radius: 100%;
}
}
.input-wrap {
overflow: hidden;
flex: 1;
margin: 0 0 12px 0;
text-align: center;
font-size: 14px;
color: #4a4a4a;
font-weight: 500;
span {
// font-family: DINAlternate-Bold;
font-size: 56px;
color: #333333;
// letter-spacing: 0;
// line-height: 56px;
}
input {
width: 100%;
height: 100%;
line-height: 30px;
text-align: center;
vertical-align: top;
color: inherit;
&::placeholder {
color: #333;
}
}
}
p {
margin-bottom: 1em;
color: #999;
font-size: 12px;
}
section {
position: relative;
overflow: hidden;
padding-bottom: 30px;
&:before {
content: '';
z-index: 1;
position: absolute;
top: 0;
top: 0;
left: 50%;
width: 3px;
height: 50%;
margin-left: 0px;
background: #03c067;
border-radius: 1px;
}
}
ul {
display: flex;
li {
position: relative;
flex: 1;
min-width: 25%;
}
}
.ruler {
li {
opacity: 1;
height: 40px;
background: linear-gradient(90deg, #e0e0e0, #e0e0e0, transparent 1px) repeat-x;
background-size: 10% 50%;
&:before {
content: '';
position: absolute;
top: 0;
left: 0px;
width: 2px;
height: 72%;
background: #e0e0e0;
}
&:after {
content: '';
position: absolute;
top: 0;
left: 50%;
width: 1px;
height: 70%;
background: #e0e0e0;
}
i {
position: absolute;
top: 0;
right: -0.5px;
width: 2px;
height: 100%;
background: #ccc;
}
}
}
.mark {
li {
span {
position: absolute;
left: -3em;
bottom: -20px;
width: 6em;
line-height: 1;
color: #ccc;
&.last {
left: auto;
right: -3em;
}
}
}
}
&.high,
&.low,
&.normal {
section {
color: #fff;
&:before {
background: #fff !important;
}
}
li span {
color: #fff;
}
}
&.high {
header {
color: #ff2f2f;
}
section {
background-image: linear-gradient(90deg, #ff6d6d, #ff2f2f);
}
}
&.low {
header {
color: #ffa100;
}
section {
background-image: linear-gradient(90deg, #ffbe4f, #ffa100);
}
}
&.normal {
header {
color: #20ba32;
}
section {
background-image: linear-gradient(90deg, #65de74, #20ba32);
}
}
}