先看一下我写的目录结构:
依次来看业务代码;
(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}
/>
)
}