需求:
开发一款能够选择年份和月份的移动端选择器,其中可选择的最大日期不能超过当前的时间.
最终结果:
具体实现:
1.创建一个文件夹DateSel作为日期组件的目录,在里面分别创建两个文件 index.js和style.scss.手指滑动使用了better-scroll插件作为移动端滚动插件,需要先安装依赖better-scroll.
npm install better-scroll --save
在移动端使用了rem布局,为了保证计算所得的rem值相同需要在react的入口文件index.js中导入rem.js.创建一个util文件夹并在其中创建一个js文件rem.js,将下面的rem.js代码复制其中.紧接着在react的入口文件index.js中将rem.js引入.
import "./util/rem.js";
如此便可以在750px的设计稿中只需要测量出相应的值除以100就可以得到相应的rem值.
rem.js
(function (doc, win) {
var docEl = doc.documentElement,
resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize',
recalc = function () {
var clientWidth = docEl.clientWidth;
if (!clientWidth) return;
if (clientWidth >= 750) {
docEl.style.fontSize = '100px';
} else {
docEl.style.fontSize = 100 * (clientWidth / 750) + 'px';
}
};
if (!doc.addEventListener) return;
win.addEventListener(resizeEvt, recalc, false);
doc.addEventListener('DOMContentLoaded', recalc, false);
})(document, window);
index.js是实现该组件的核心部分,我们可以深入分析一下具体的实现过程.
1.在constructor函数中获取左右两边年和月滑动空间的dom引用,接下来计算出每一栏的高度columnHeight.页面上四根分割线是通过定位定死的,它们之间的高度就等于columnHeight.所以我们想实现的就是每次滑动结束后滑动的距离是columnHeight的整数倍,让滑动后的元素刚好处于分割线之内的空间.
2.组件初始化时会执行computedDate,computedDate函数的作用是计算出与年份相关的数据:this.year,this.current_year_idx.
this.year是一个数组,里面存储的是所有的年份, this.current_year_idx是指向选中的年份的索引.
3.componentDidMount表示组件挂载完毕的生命周期,挂载后便执行了this.computedMonth(true).this.computedMonth此方法会根据当前选中的年份计算出所有月份的数组和选中的月的索引:this.state.month,this.current_month_idx.(如果选中的年份是当前年,那么获得的月份就不是12,而是要小于和等于当前月,因为根据需求只能选择当前和过去的日期)
4.scrollInit是组件第一次在界面上显示出来执行的函数,此函数中主要是创建年份和月份的better-scroll的实例,并让其滚动到相应的位置.(为什么非要在显示状态下创建better-scroll实例呢,经过测试发现在dom的display为none时创建better-scroll会出现滚动的bug)
5.scrollHandler是每次滑动结束后执行的函数,该函数的作用是让滑动的距离是columnHeight的整数倍,让滑动后的数字正好位于分割线的中心位置.每次滑动年份后会触发月份的重新计算.
6.点击确定会根据当前的选中的年份索引和月份索引获取到对应的值发送给父组件.
index.js
import React,{Component} from "react";
import "./style.scss";
import BScroll from 'better-scroll';
export default class DateSel extends Component {
constructor(props){
super(props);
this.left_warpper = React.createRef();
this.right_warpper = React.createRef();
this.columnHeight = this.remToPx(0.84);
this.hasInit = false;
this.year_num = 50;
this.computedDate();
this.state = {
month: ["","01","02","03","04","05","06","07","08","09","10","11","12",""]
}
}
/**
* 将rem转化为px
*/
remToPx = (rem)=>{
let clientWidth = document.documentElement.clientWidth;
return rem*100 * (clientWidth / 750)
}
/**
* 计算年
*/
computedDate = ()=>{
this.date = new Date();
let year = this.date.getFullYear(),year_arr=[];
for(let i=this.year_num;i>=0;i--){
year_arr.push(year);
year--;
}
year_arr.reverse();
year_arr.unshift("");
year_arr.push("");
this.year=year_arr;
this.current_year_idx = this.year.length-2;
}
componentDidUpdate(){
this.scrollInit();
}
scrollInit = ()=>{
if(this.props.getModalStatus && !this.left_scroll){
this.left_scroll = new BScroll(this.left_warpper.current,{
momentum:false,
bounceTime:250
});
let bottom__distance = -1*this.columnHeight*(this.year.length-3);
this.left_scroll.on("scrollEnd",this.scrollHandler.bind(this,this.left_scroll,1));
this.left_scroll.scrollTo(0,bottom__distance,0);
}
if(this.props.getModalStatus && !this.right_scroll){
this.right_scroll = new BScroll(this.right_warpper.current,{
momentum:false,
bounceTime:250
});
this.right_scroll.on("scrollEnd",this.scrollHandler.bind(this,this.right_scroll,2));
this.right_scroll.scrollTo(0,-1*this.columnHeight*(this.state.month.length-3),0);
}
}
componentDidMount(){
this.computedMonth(true);
}
computedMonth = (init = false)=>{ //init 是否是初始化状态下执行
let selectedYear = this.year[this.current_year_idx];
let now_year = this.date.getFullYear();
let new_month = this.date.getMonth()-0+1;
if(selectedYear == now_year){ //表述选中的年是当前年
let arr =[];
for(let i=1;i<=new_month;i++){
arr.push(this.monthHander(i));
}
arr.unshift("");
arr.push("");
if(init){
this.current_month_idx = arr.length-2;
}else{
this.current_month_idx = 1;
}
this.setState({
month:arr
})
}else{
this.current_month_idx = 1;
this.setState({
month:["","01","02","03","04","05","06","07","08","09","10","11","12",""]
})
}
}
monthHander = (month)=>{
if(month<10){
return "0"+month;
}else{
return month;
}
}
scrollHandler = (scroll,type)=>{
let y = Math.abs(scroll.y);
let columnHeight = this.columnHeight;
let arr = (y/columnHeight).toString().split(".");
let index = parseInt(arr[0]);
if(arr.length == 2 && ("0."+arr[1])>=0.5){
index++;
}
if(type == 1){ //要计算月份的值
this.current_year_idx = index+1;
this.computedMonth();
if(this.right_scroll){
this.right_scroll.scrollTo(0,0);
}
}else if(type == 2){
this.current_month_idx = index+1;
this.setState((prevState)=>{
return {
month:prevState.month
}
})
}
scroll.scrollTo(0,-1*columnHeight*index,100);
}
componentWillUnmount(){
if(this.left_scroll && this.right_scroll){
this.left_scroll.destroy();
this.right_scroll.destroy();
this.left_scroll = null;
this.right_scroll = null;
}
}
ensure= ()=>{
this.props.getDate(this.year[this.current_year_idx],this.state.month[this.current_month_idx]);
}
monthFilter = (v)=>{
if(!v){
return "";
}
return parseInt(v)+"月"
}
render(){
return (
<div className="DateSel">
<div className="middle_region">
<div className="bar">
<div>选择日期</div>
<i className="iconfont icon-cha close" onClick={this.props.close}></i>
</div>
<div className="change_time">
<div className="left_month">
<div>
<span className="line_one"></span>
<span className="line_two"></span>
</div>
<div className="left-warpper" ref={this.left_warpper}>
<div className="scroll-warper">
{
this.year.map((v,index)=>{
return (
<div className={this.current_year_idx == index?"text current":"text"} key={index}>{v}</div>
)
})
}
</div>
</div>
</div>
<div className="right_day">
<div>
<span className="line_one"></span>
<span className="line_two"></span>
</div>
<div className="right-warpper" ref={this.right_warpper}>
<div className="scroll-warper">
{this.state.month.map((v,index)=>{
return (
<div className={this.current_month_idx == index?"text current":"text"} key={index}>{this.monthFilter(v)}</div>
)
})}
</div>
</div>
</div>
</div>
<div className="sure" onClick={this.ensure}>确定</div>
</div>
</div>
)
}
}
style.scss
.DateSel{
background: rgba($color: #000000, $alpha: 0.6);
width: 100%;
height:100%;
position: fixed;
top:0;
left:0;
z-index: 100;
.middle_region{
position: absolute;
top:50%;
left:50%;
width:4.6rem ;
height:4.36rem ;
transform: translate(-50%,-50%);
background-color: #fff;
border-radius:0.15rem;
.bar{
background-color: #728DAA;
height:0.88rem;
display:flex;
justify-content: flex-start;
align-items: center;
padding: 0 0.2rem;
font-size: 0.30rem;
color:#fff;
border-radius:0.15rem 0.15rem 0 0;
div{
flex:0 0 50%;
}
i{
flex:0 0 50%;
text-align: right;
}
}
.change_time{
width: 100%;
height: 2.51rem;
border-bottom:1px solid #ccc;
position: relative;
font-size: 0.35rem;
.left_month{
width: 50%;
position: absolute;
top:0;
left: 0;
height: 100%;
text-align: center;
div{
height:0.84rem;
line-height: 0.83rem;
}
.up{
position: absolute;
top:0;
right: 0;
color:#CCCCCC;
padding: 0 0.2rem;
}
.line_one{
width:1.23rem ;
height:0.03rem ;
background: #708EAA;
position: absolute;
top:0.83rem;
right: 0;
}
.middle{
position: absolute;
top:0.86rem;
right: 0;
color:#333;
padding: 0 0.2rem;
}
.line_two{
width:1.23rem ;
height:0.03rem ;
background: #708EAA;
position: absolute;
top:1.69rem;
right: 0;
}
.down{
position: absolute;
top:1.72rem;
right: 0;
color:#CCCCCC;
padding: 0 0.2rem;
}
.text{
color:#CCCCCC;
padding: 0 0.2rem;
}
.current{
color:#333;
}
.left-warpper{
position: absolute;
top: 0;
right: 0;
width: 50%;
height: 100%;
overflow: hidden;
.scroll-warper{
height:auto;
}
}
}
.right_day{
width: 50%;
position: absolute;
top:0 ;
right: 0;
height: 100%;
text-align: center;
div{
height:0.84rem;
line-height: 0.83rem;
}
.up{
position: absolute;
top:0;
left:0.4rem;
color:#CCCCCC;
padding: 0 0.2rem;
}
.line_one{
width:0.93rem ;
height:0.03rem ;
background: #708EAA;
position: absolute;
top:0.83rem;
left:0.3rem;
}
.middle{
position: absolute;
top:0.86rem;
left:0.4rem;
color:#333;
padding: 0 0.2rem;
}
.line_two{
width:0.93rem ;
height:0.03rem ;
background: #708EAA;
position: absolute;
top:1.69rem;
left:0.3rem;
}
.down{
position: absolute;
top:1.72rem;
left:0.4rem;
color:#CCCCCC;
padding: 0 0.2rem;
}
.right-warpper{
position: absolute;
top: 0;
left: 50%;
width: 50%;
height: 100%;
overflow: hidden;
transform: translate(-80%, 0%);
.scroll-warper{
height:auto;
}
.current{
color:#333;
}
}
.text{
color:#CCCCCC;
padding: 0 0.2rem;
}
}
}
.sure{
width: 100%;
line-height: 0.95rem;
font-size:0.3rem;
text-align: center ;
color:#333;
}
}
}
2.在页面中具体使用日期组件
import React,{Component} from "react";
import DateSel from "./../../components/DateSel";
class AllOrder extends Component{
state={
getModalStatus:false //控制日期组件的显示和隐藏
}
/**
* 点击筛选日期
*/
openDate = ()=>{
this.setState({
getModalStatus:true
})
}
/**
* 关闭筛选日期模态框
*/
close= ()=>{
this.setState({
getModalStatus:false
})
}
/**
* 点击日期框的确定,获取到选择后的年和月份
*/
getDate = (year,month)=>{
}
render(){
return (
<div className="all-order">
<span onClick={this.openDate}>打开日期框</span>
<div style={{display:this.state.getModalStatus?"block":"none"}}>
<DateSel close={this.close} getDate={this.getDate}
getModalStatus={getModalStatus}/>
</div>
</div>
)
}
}
export default AllOrder;