基于antd的DatePicker 组件封装业务组件

37 篇文章 1 订阅
7 篇文章 0 订阅

先看一下我写的目录结构:

依次来看业务代码;

(1)RangeTime.tsx 

import {useState,uesCallback} from 'react';
import {DatePicker} from 'antd';
import {RangePickerProps as AntdRangePickerProps} from 'antd/es/date-picker';
import {Moment} from 'moment';
import type {RangeValue} from 'rc-picker/es/interface';
import {createPastTimeRange} from './utils/date';
import {toMomentRange} from './utils';
import Panel from './components/Panel';
import type {MomentRange} from './components/interface';

const OSUIRangePicker = DatePicker.RangePicker;

export type RangeValueMoment = Parameters<Parameters<
      NonNullable<React.ComponentProps<typeof OSUIRangePicker>['onChange']>
>[0];

export const DATE_RANGE_FUNC_PRESETS={
    '30分钟':()=>toMomentRange(createPastTimeRange({minutes:30})),
    '1小时':()=>toMomentRange(createPastTimeRange({hours:1})),
    '3小时':()=>toMomentRange(createPastTimeRange({hours:3})),
    '1天':()=>toMomentRange(createPastTimeRange({days:1})),
    '7天':()=>toMomentRange(createPastTimeRange({days:7})),
}

interface RangePickerProps extends Omit <AntdRangePickerProps ,'value'> {
    value:RangeValue<Moment> | [Moment,Moment];
}

export default function RangePicker({value,onChange,...props}:RangePickerProps){
    const [stateRangeValue,setRange]=useState<MomentRange | []>([]);
    const [open,setOpen]=useState(false);

    const handleChange = uesCallback(
        (value,dateString)=>{
            setRange(value);
            setOpen(false);
            onChange?.(value,dateString);
        },
        [onChange,setRange]
    );

    const handleQuickRangeSelect=uesCallback(
        (rangeFunc:()=>MomentRange)=>{
            const range = rangeFunc();
            const dateString = range.map(d=>d.format('YYYY-MM-DD HH:mm:ss'));
            handleChange(range,dateString);
        } 
    )

    const panelRender = uesCallback(
        panelNode =>(
            <Panel 
               rangeFunctionRecord={DATE_RANGE_FUNC_PRESETS}
               panelNode={panelNode}
               onQuickRangeSelect={handleQuickRangeSelect}
            />
        ),
        [handleQuickRangeSelect]
    )

    const handleFocus = uesCallback(
        ()=>{
            setOpen(true);
        },
        []
    )

    //当panel打开时,点击panel内的input,会触发datePicker的blur事件,如果panel是打开状态,则保持打开状态
    //依赖onOpenChange时序,onOpenChange会先于blur触发,所以可以成功
    const handleBlur = uesCallback(
        ()=>{
            if(open){
                setOpen(true);
            }
        },
        [open]
    )

    const handleOpenChange=uesCallback(
        open=>{
            setOpen(open);
        },
        []
    )

    const format = uesCallback(
        value =>{
            return value.format('YYYY-MM-DD HH:mm:ss');
        },
        []
    )

    const innerValue = (value || stateRangeValue) as RangeValue<Moment>;

    return (
        <OSUIRangePicker 
           {...props}
           showTime
           open={open}
           format={format}
           value={innerValue}
           onFocus={handleFocus}
           onBlur={handleBlur}
           onChange={handleChange}
           onOpenChange={handleOpenChange}
           panelRender={panelRender}
         />
    )
}

(2)封装的时间日期模块组件

1.utils/date/index.ts

export * from './common';
export * from './manipulate';
export * from './formatAsString';
export * from './formatAsTimeStamp';
export * from './formatAsMoment';

2.common.ts

export const isMilliSecond = (time:number)=>String(time).length === 13;

3.manipulate.ts

import moment,{Moment} from 'moment';
import {subDays,subHours,subMinutes} from 'date-fns';
import {MomentRange} from '../../components/interface';

type TimeRangeBy ={days:number} | {hours:number} | {minutes:number};

/**
 * 从开始时间到结束时间,返回一个时间范围
 * 也可提供一个till Date参数,表示到给定时间结束
 * eg: createPastTimeRange({minutes:60}),就是从60分钟开始到现在
 * @param by
 * @param till 默认是now
 * @returns
 */

 export function createPastTimeRange(by:{days:number},till?:Date):[Date,Date];
 export function createPastTimeRange(by:{hours:number},till?:Date):[Date,Date];
 export function createPastTimeRange(by:{minutes:number},till?:Date):[Date,Date];
 export function createPastTimeRange(by:TimeRangeBy,till?:Date = new Date()):[Date,Date]{
    if('days' in by){
     return [subDays(till,by.days),till];
    }
    if('hours' in by){
        return [subHours(till,by.hours),till];
    }
    if('minutes' in by){
        return [subMinutes(till,by.minutes),till];
    }
    return [till,till];
 };

 /**
 * [end-start]时间段转换成[天-小时-分钟-秒的格式] string
 * @param start Moment
 * @param end Moment
 * @returns string
 */
export const formatDurationTime = (start:Moment,end:Moment)=>{
    const diff = moment(end).diff(moment(start)) / 1000;
    const d = Math.floor(diff / (60 * 60 * 24));
    const h = Math.floor(diff % (60 * 60 * 24) / (60 * 60));
    const m = Math.floor(diff % (60 * 60) / 60);
    const s = Math.floor(diff % 60);
    const days = d ? `${h}天` : '';
    const hours = h ? `${h}小时` : '';
    const minutes = m ? `${m}分钟` : '';
    const seconds = s ? `${s}秒` : '';
    return `${days}${hours}${minutes}${seconds}`;
}

export function timeRangeLengrh(
   [startTime,endTime]:MomentRange,
   unitOfTime:'days' | 'hours' | 'minutes' | 'seconds' | 'ms' = 'seconds'
){ 
   return moment(endTime).diff(moment(startTime),unitOfTime);
}

这块附上date-fns插件的链接,date-fns的github地址 

4.formatAsString

import {chunk} from 'lodash';
import moment,{Moment} from 'moment';
import {assertNever} from './type';
import {timeStampToMoment} from './formatAsMoment';

/**
 * 返回ISO格式的时间字符串
 * @param time
 * @example '2024-05-16T07:05:10.658Z'
 */

 export function timeStampToISOString(time:number | Moment){
    if(typeof time === 'number'){
       return timeStampToMoment(time).toISOString();
    }
    return moment(time).toISOString();
 }

 /**
 * 返回ISO格式的时间字符串,但是没有毫秒
 * @param time
 * @example '2024-05-16T07:05:10.13Z'
 */

export function timeStampToShortISOString(time:number | Moment){
    return timeStampToISOString(time).replace(/\.\d+/,'');
}

export const formatMomentAsLong = (m:Moment)=>{
    return m.format('YYYY-MM-DD HH:mm:ss');
}

export const formatMomentAsTime = (m:Moment)=>{
    return m.format('HH:mm:ss');
}

export const formatDateStringToLong = (isoString:string)=>{
    return formatMomentAsLong(moment(isoString));
}

export const formatDateStringToTime = (isoString:string)=>{
    return formatMomentAsTime(moment(isoString));
}

export function transToLocalTimeByDateString(
    dateString:string,
    formatBy: 'date' | 'time' = 'date'
){
    if(formatBy === 'date'){
        return formatDateStringToLong(dateString);
    }
    if(formatBy === 'time'){
        return formatDateStringToTime(dateString);
    }
    assertNever(formatBy);
}

/**
 * @param serverForm {string} `001122`
 * @returns `09:12:22`
 */
export const transformTimeString = (serverForm:string) =>{
   const hourMinutesSecondList = chunk(serverForm,2);
   return hourMinutesSecondList.map(x=>x.join('')).join(':');
}

/**
 * @param clientTime 前端时间点
 * @param deltaInHours 需要增加的小时数,可以是负数 . 绝对值小于等于24
 * @returns {string} 调整小时之后的时间点的前端形式
 */
export const addHourForClientTime = (clientTime:string,deltaInHours:number) => {
   const [hour,minute,second] = clientTime.split(':').map(Number);
   return [(hour + deltaInHours + 24) % 24,minute,second]
      .map(num =>String(num).padStart(2,'0'))
      .join(':')
} 

/**
 * 将服务端时间转换成前端需要时间
 * @description 服务端形式的时间范围UTC+0,eg:[`000022`,`000033`]
 * @param timeRange {[string,string]}
 * @returns {[string,string]} 前端形式的时间范围UTC+8,eg:[`09:00:22`,`09:00:33`]
 */
export const normalizeServerTimeRangeToClientForm = (timeRange:string[]) =>{
   return timeRange.map(time=>addHourForClientTime(transformTimeString(time),8));
}

/**
 * 将前端时间范围标准化为服务端形式
 * @param clientTimeRange
 * @returns 服务端形式的时间范围,UTC+0,[`000022`,`000033`]
 */
export const normalizeClientTimeRangeToServerForm = (clientTimeRange:string[])=>{
   return clientTimeRange.map(time=>addHourForClientTime(transformTimeString(time),16));
}

/**
 * 格式化服务端形式时间段为字符串
 * @param serverTimeRange {[srting,string]},形如['1000000','120000']
 * @returns 形如`[10:00:00-12:00:00]`的字符串
 */
export const formatServerFormTimeRange = (serverTimeRange:string[])=>{
   const [start,end]=normalizeServerTimeRangeToClientForm(serverTimeRange);
   return `[${start}-${end}]`;
}

/**
 * @param serverTimeRange {[string,string]},形如[['100000','120000'],['100000','140000']]
 * @returns 形如`[10:00:00 - 12:00:00]`,[10:00:00-14:00:00]`的字符串
 */
export const formatTimeRangeList = (timeRanges:string[][]) =>{
   return timeRanges.map(formatServerFormTimeRange).join(', ');
}

export function transToLocalTimeByUnixTimeStamp(
    timeStamp:number,
    formatBy: 'date' | 'time' = 'date'
){
    if(formatBy === 'date'){
        return formatMomentAsLong(timeStampToMoment(timeStamp));
    }
    if(formatBy === 'time'){
        return formatMomentAsTime(timeStampToMoment(timeStamp));
    }
    assertNever(formatBy);
}

5.formatAsTimeStamp.ts

import moment,{Moment} from 'moment';
import {isMilliSecond} from './common';

export const momentToSeconds = (timeStamp:Moment)=>{
    if(isMilliSecond(timeStamp.valueOf())){
      return Math.floor(timeStamp.valueOf() / 1000);
    }
    return timeStamp.valueOf();
}

export const monentToTimeStamp = (date:Moment) =>{
    return moment(date).unix();
}

export const timeStampAsMillSeconds = (timeStamp:number) =>{
    if(isMilliSecond(timeStamp)){
        return timeStamp;
    }
    return timeStamp * 1000;
}

6.formatAsMoment.ts

import moment from 'moment';
import {MomentRange} from '../../components/interface';
import {isMilliSecond} from './common';

/**
 * timeStamp number 转换成monent
 * @param time  number
 * @returns Moment
 */
export function timeStampToMoment(time:number){
   let unixTimestamp = time;
   if(!isMilliSecond(time)){
      unixTimestamp = time * 1000;
   }
   // 和moment.unix不同,unix如果传进来的是毫秒,需要加上000
   return moment(unixTimestamp);
}

export function timeStampRangeToMomentRange(timeStampRange:[number,number]):MomentRange{
   return [timeStampToMoment(timeStampRange[0]),timeStampToMoment(timeStampRange[1])];
}

(3)处理时间utils模块

utils.tsx

import moment,{Moment} from 'moment';
import { Time } from './components/interface';

export const toMomentRange = ([start,end]:[Date,Date]):[Moment,Moment] =>[moment(start),moment(end)];

export const timeToString = ({hour,min,sec}:Time)=>{
   return `${hour}:${min}:${sec}`;
}

export const stringToTime = (timeString:string) =>{
   const [hour,min,sec] = timeString.split(':').map(v=>v.trim());
   return {hour,min,sec};
}

(4)业务组件Panel模块

1.components/Panel

import type {MomentRange} from './interface';
import * as Styled from './Styled';

type MomentRangeFunc = ()=>MomentRange;

interface PancelProps{
    rangeFunctionRecord:Record<string,MomentRangeFunc>;
    panelNode:React.ReactElement;
    onQuickRangeSelect:(rangeFun:MomentRangeFunc)=>void;
    // hover可以用来设置临时的时间范围,交互的话效果更好
    onQuickRangeHover?:(rangeFun:MomentRangeFunc)=>void;
}

export default function Pancel({
    rangeFunctionRecord,
    panelNode,
    onQuickRangeSelect,
    onQuickRangeHover
}:PancelProps){
    return (
        <>
          <Styled.PanelLayout>
              <Styled.RangeLayout>
                  {
                      Object.entries(rangeFunctionRecord).map(([label,rangeFunc])=>{
                          return (
                              <Styled.RangeItem
                                key={label}
                                onClick={()=>onQuickRangeSelect(rangeFunc)}
                                onMouseEnter={()=>
                                    onQuickRangeHover && onQuickRangeHover(rangeFunc)
                                }
                              >
                                  {label}
                              </Styled.RangeItem>
                          )
                      })
                  }
              </Styled.RangeLayout>
              {panelNode.props.children[0]}
          </Styled.PanelLayout>
          {panelNode.props.children[1]}
        </>
    )
}

2.components/Styled

import styled from '@emotion/styled';

export const PanelLayout = styled.div`
       display:flex;
`

export const RangeLayout = styled.div`
       display:flex;
       flex-direction: column;
       align-items: center;
       min-width: 56px;
`

export const RangeItem = styled.div`
       line-height: 1.5;
       font-size: 12px;
       padding: 0px 8px;
       &:not(:last-child){
            margin-bottom: 12px;
       }
       cursor: pointer;
       &:hover{
           background-color:'#dce1e3'
       }
`

3.components/interface.tsx

import type {Moment} from 'monent';
export interface Time{
    hour:string;
    min:string;
    sec:string;
}
export type MomentRange=[Moment,Moment];

(5)使用时

1.PanelComponent.tsx

import React,{useCallback,useState} from 'react';
import {useBoolean} from 'huse';
import {MomentRange} from './components/interface';
import {useTimeRange} from './hooks/useTimeRange';
export default function PanelComponent(){
    const {data:fetchData}= useFetchData();// eg:useFetchData是从接口返回的数据并封装的hooks
    const [isConnectNulls,{on:onConnectNulls,off:offConnectNulls}] = useBoolean(false);
    const onChangeConnectNulls = useCallback((checked:boolean)=>{
        if(checked){
            onConnectNulls();
        }else{
            offConnectNulls();
        }
    },
    [offConnectNulls,onConnectNulls]
    )

    const {eventStartTimeMoment,defaultRange}=useTimeRange();
    const [range,setRange] = useState<MomentRange>(defaultRange);

    return (
        <RangeTimeComponent
            isConnectNulls={isConnectNulls}
            onChangeConnectNulls={onChangeConnectNulls}
            momentRange={range}
            setRange={setRange}
            eventStartTimeMoment={eventStartTimeMoment}
        />
    )
};

2.处理时间模块hooks/useTimeRange.tsx

import {useMemo} from 'react';
import moment from 'moment';
import {MomentRange} from '../components/interface';
import {timeStampToMoment} from '../utils/date';

export const useTimeRange = () =>{
    const {data:fetchData}= useFetchData();// eg:useFetchData是从接口返回的数据并封装的hooks
    const eventStartTimeMoment = useMemo(
        ()=>timeStampToMoment(fetchData?.eventStartTime || 0),
        [fetchData?.eventStartTime ]
    );

    const defaultRange = useMemo(
        ():MomentRange =>{
            const startPointMoment = eventStartTimeMoment.clone().subtract(2,'hours');
            const endPointMoment = eventStartTimeMoment.clone().add(2,'hours');
            return [
                startPointMoment,
                endPointMoment.isBefore(moment()) ? endPointMoment : moment(),
            ]
        },
        [eventStartTimeMoment]
    )

    return {
        eventStartTimeMoment,
        defaultRange, 
    }
}

3.处理时间组件RangeTimeComponent.tsx

import React,{useCallback} from 'react';
import moment from 'moment';
import {range} from 'lodash';
import RangeTime from './RangeTime';
import {MomentRange} from './components/interface';

interface Props{
    isConnectNulls:boolean;
    momentRange:MomentRange;
    setRange:(range:MomentRange)=>void;
    onChangeConnectNulls:(isConnectNulls:boolean)=>void;
    eventStartTimeMoment:moment.Moment;
}

export default function RangeTimeComponent(props:Props){
    const {
        isConnectNulls,
        momentRange,
        onChangeConnectNulls,
        setRange,
        eventStartTimeMoment,
    }=props;

    const handleRangeChange = useCallback(
        value=>{
            setRange(value);
        },
        [setRange]
    );

    const disabledDateTime = useCallback((current:any)=>{
        const nowMoment=moment();
        if(current?.isSame(nowMoment,'days')){
            return {
                disabledHours:()=>range(nowMoment.hours() + 1 ,25),
                disabledMinutes:()=>range(nowMoment.minutes() + 1 ,61),
                disabledSeconds:()=>range(nowMoment.seconds() + 1 ,61),
            }
        }
        return {};
    },[]
    );

    const disabledDate = useCallback((current:Moment.Moment)=>{
        const currentMomentClock=moment(current.format('L'));
        const eventStartTimeMomentClock = moment(eventStartTimeMoment.format('L'));

        const toolate = currentMomentClock.diff(eventStartTimeMomentClock,'days') > 7
              || current > moment().endOf('day');
        const tooEarly = eventStartTimeMomentClock.diff(currentMomentClock,'days') > 7;
        return tooEarly || toolate;
    },[eventStartTimeMoment]
    );

    return (
        <RangeTime 
           allowClear={false}
           value={[...momentRange]}
           onChange={handleRangeChange}
           disabledDate={disabledDate}
           disabledDateTime={disabledDateTime}
        />
    )
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值