前言
在工作过程中date-picker作为常用的时间选择器经常被用到,观察了公司对date-picker的写法后,发现大多是都是使用Jquery去写的,通过传入一个配置来指定生成Swiper的页面元素ID、swiper的列表,以及默认选中项等等。
之所以这样写可能是由于项目时间较久,前辈们在开发时还不是react、vue的时代,也可能是Jq写的兼容性更好、通用性更强。在使用过程中,经常有一些附加的需求是组件内部没有封装的,因此逐渐有了自己写一个这样组件的想法。
因为自己比较喜欢react的编码方式以及JSX,所以自己写用到的是最新的react18以及最新的swiper8版本。现在的大多数项目所安装的依赖肯定是没有这么新,所以只当是锻炼自己的一个小demo。而且不管框架和版本是什么,其中的内在逻辑掌握即可,在针对不同环境写不同的代码。
dependencies
"lodash": "^4.17.21",
"moment": "^2.29.3",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-scripts": "5.0.1",
"sass": "^1.52.1",
"swiper": "^8.1.5",
需求
工作中针对date-picker常用的需求就体现在对于日期选择范围的把控上,比如在选择某些订单的日期、或者是选择某些人的生日方面,都要求只能选择当前日期之前的日期。也会出现只支持几月前,半年前,一年前的日期(例如查询订单的范围、券商查询交割单)。
虽然校验一般都是通过后端来校验,但是如果前端在选择时就能限定了范围,也是极好的!(在工作中遇到选择生日时还可以选择当天之后的日期,给我的体验是相当不好。)
所以刚开始定义的需求如下:
/**
* 1. 什么都不传,默认日期为当前日期,以当前日期前后推50年
* 2. 只传yearLength, 默认日期为当前日期,以当前日期前后推yearLength年
* 3. 传了date 和 yearLength,默认日期为date,以当前日期前后推yearLength年
* 4. before 默认日期传了为默认日期,没传为当前日期, 从当前日期往前推
* 5. after 默认日期传了为默认日期,没传为当前日期, 从当前日期往后推
* 6. duration 传入一个日期前后多少年 或者是给定前后日期
*/
// 之前多少年或之前多少月
interface Before {
type: "year" | "month"; // 之前的类型
value: number; // int 或 0.5的倍数, 0.5表示半月
initialDate: string; // 'yyyy-mm-dd' 初始日期,一打开swiper的初始日期
}
// 之后多少年或之后多少月
interface After {
type: "year" | "month"; // 之后的类型
value: number; // int 或 0.5的倍数, 0.5表示半月
initialDate: string; // 'yyyy-mm-dd' 初始日期,一打开swiper的初始日期
}
// 期间
interface Duration {
startTime: string | "now"; // 开始时间 "yyyy-mm-dd"
endTime: string | "now"; // 结束时间 ”yyyy-mm-dd"
initialDate: string; // 'yyyy-mm-dd' 初始日期,一打开swiper的初始日期
}
// 组件参数
interface DateProps {
date?: string;
before?: Before;
after?: After;
duration?: Duration;
}
后来在开发before模式过程中,发现无论是哪一种模式本质是都是一个duration的范围,由三个主要的时间来把控,最大时间、最小时间、初始时间。因此即使是不同的模式,在实现起来有些细微差别,但主要逻辑都是相同的,所以在做完before后其余模式就没再开发。
构建页面
页面的主要结构就是中间的Swiper,可以通过弹性盒的方式自己扩展swiper的个数,然后通过原生的方式插入swiper。我这里为了方便是直接写死的三个swiper,本来打算做时、分两个swiper但是发现逻辑大同小异就没做。
JSX
// app.js
import DateEight from './dateEight';
import './App.scss';
import "swiper/scss";
function App() {
return (
<DateEight />
);
}
export default App;
//src/dateEight/index.js
function DateEight(props) {
return (
<div className="App">
<div className="date-box" onClick={openPicker}>
<span className="date-box-content">
{ selectedValue }
</span>
<p className="date-arrow">
<span className="icon icon-down"></span>
</p >
</div>
{
showPicker ? (
<section className="date-wrapper">
<div className="date-wrapper-blank" onClick={ closePicker }></div>
<div className="date-picker">
<div className="date-picker-button">
<div onClick={ cancelHandler }>取消</div>
<div onClick={ confirmHandler }>确定</div>
</div>
<div className="date-picker-container">
<Swiper
direction='vertical'
spaceBetween={0}
slidesPerView={5}
initialSlide={yearIndex}
centeredSlides={true}
onSwiper={setYearSwiper}
onSlideChange={yearSlideChange}
>
{
yearSlide.map(year => {
const { key, value } = year;
return (
<SwiperSlide key={key}>{ value }年</SwiperSlide>
)
})
}
</Swiper>
<Swiper
direction='vertical'
spaceBetween={0}
slidesPerView={5}
initialSlide={monthIndex}
centeredSlides={true}
onSwiper={setMonthSwiper}
onSlideChange={monthSlideChange}
>
{
monthSlide.map(month => {
const { key, value } = month;
return (
<SwiperSlide key={key}>{ value }月</SwiperSlide>
)
})
}
</Swiper>
<Swiper
direction='vertical'
spaceBetween={0}
slidesPerView={5}
initialSlide={dayIndex}
centeredSlides={true}
onSwiper={setDaySwiper}
// onSlideChange={() => console.log('month change')}
>
{
daySlide.map(day => {
const { key, value } = day;
return (
<SwiperSlide key={key}>{ value }日</SwiperSlide>
)
})
}
</Swiper>
{
hourShow ? (
<>
<div className="swiper-container" id="date-swiper-hour">
<div className="swiper-wrapper">
{
hourSlide.map(hour => {
const { key, value } = hour;
return (
<SwiperSlide key={key}>{ value }时</SwiperSlide>
)
})
}
</div>
</div>
<div className="swiper-container" id="date-swiper-minute">
<div className="swiper-wrapper">
{
minuteSlide.map(minute => {
const { key, value } = minute;
return (
<SwiperSlide key={key}>{ value }分</SwiperSlide>
)
})
}
</div>
</div>
</>) : null
}
</div>
<div className="date-picker-focus">
<div></div>
</div>
</div>
</section>
) : null
}
</div>
);
}
app.scss
.date-box {
color: #5f606d;
height: 50px;
display: flex;
align-items: center;
&-content {
color: #2C2C3C;
}
.date-arrow {
width: 25px;
height: 25px;
display: flex;
justify-content: center;
align-items: center;
margin: 0;
.icon {
position: static;
width: 0.75rem;
height: 0.75rem;
cursor: pointer;
&.icon-down {
content: url('./down.jpg')
}
}
}
}
@mixin flexx {
display: flex;
justify-content: center;
align-items: center;
}
.date-wrapper {
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
color: #5f606d;
z-index: 999;
.date-picker {
width: 100%;
height: 15rem;
background-color: #e5e8eb;
position: relative;
&-button {
width: 100%;
height: 2.5rem;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #f9f9f9;
border-top: solid 1px #c5c5c5;
> div {
width: 4rem;
height: 100%;
@include flexx;
font-size: 1rem;
color: #00abf3;
}
}
&-container {
width: 90%;
height: 12.5rem;
display: flex;
margin: 0 auto;
> div {
height: 100%;
flex: 1;
overflow: hidden;
.swiper-slide {
color: #c3c3c3;
line-height: 2rem;
font-size: 1rem;
@include flexx;
&-active {
color: #202020;
font-size: 1.5rem;
hr {
width: 100%;
height: 1px;
border: none;
border-top: 1px solid #c5c5c5;
margin-top: 0.5rem !important;
margin-bottom: 0.5rem !important;
}
}
}
}
}
&-focus {
width: 100%;
height: 3rem;
border-top: 1px solid #c5c5c5;
border-bottom: 1px solid #c5c5c5;
position: absolute;
left: 0;;
top: 7.25rem;
}
}
&-blank { width: 100%; height: calc(100vh - 15rem);}
}