先看一下我写的目录结构:
依次来看业务代码;
(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}showTimeopen={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如果传进来的是毫秒,需要加上000return 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.RangeItemkey={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是从接口返回的数据并封装的hooksconst [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 (<RangeTimeComponentisConnectNulls={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是从接口返回的数据并封装的hooksconst 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}/>)
}