diff --git a/.dumirc.ts b/.dumirc.ts index 8317d22b6..a06bc4e03 100644 --- a/.dumirc.ts +++ b/.dumirc.ts @@ -222,6 +222,7 @@ export default defineConfig({ link: '/biz-components/content-with-icon', }, { title: 'Ranger 日期快速选择', link: '/biz-components/ranger' }, + { title: 'New Ranger 日期快速选择', link: '/biz-components/date-ranger' }, { title: 'TreeSearch 树搜索', link: '/biz-components/tree-search' }, { title: 'Password 密码输入框', link: '/biz-components/password' }, { title: 'Boundary 错误兜底', link: '/biz-components/boundary' }, diff --git a/packages/design/src/config-provider/__tests__/__snapshots__/prefixCls.tsx.snap b/packages/design/src/config-provider/__tests__/__snapshots__/prefixCls.tsx.snap index eb13988cf..bc311316e 100644 --- a/packages/design/src/config-provider/__tests__/__snapshots__/prefixCls.tsx.snap +++ b/packages/design/src/config-provider/__tests__/__snapshots__/prefixCls.tsx.snap @@ -1,30 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ConfigProvider prefixCls iconPrefixCls 1`] = ` - - - -`; - exports[`ConfigProvider prefixCls prefixCls 1`] = ` + + + + ); +}; + +export default InternalPickerPanel; diff --git a/packages/ui/src/DateRanger/Ranger.tsx b/packages/ui/src/DateRanger/Ranger.tsx new file mode 100644 index 000000000..bb6091b25 --- /dev/null +++ b/packages/ui/src/DateRanger/Ranger.tsx @@ -0,0 +1,523 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Button, DatePicker, Dropdown, Radio, Space, Tooltip, theme } from '@oceanbase/design'; +import type { TooltipProps, FormItemProps } from '@oceanbase/design'; +import { + LeftOutlined, + PauseOutlined, + CaretRightOutlined, + RightOutlined, + ZoomOutOutlined, +} from '@oceanbase/icons'; +import type { RangePickerProps } from '@oceanbase/design/es/date-picker'; +import type { Dayjs } from 'dayjs'; +import dayjs from 'dayjs'; +import { findIndex, isNil, noop, omit } from 'lodash'; +import type { Moment } from 'moment'; +import moment from 'moment'; +import classNames from 'classnames'; +import LocaleWrapper from '../locale/LocaleWrapper'; +import { getPrefix } from '../_util'; +import { + CUSTOMIZE, + DATE_TIME_FORMAT, + NEAR_1_MINUTES, + NEAR_30_MINUTES, + NEAR_1_HOURS, + NEAR_3_HOURS, + NEAR_6_HOURS, + TODAY, + NEAR_TIME_LIST, + YEAR_DATE_TIME_FORMAT, + LAST_3_DAYS, +} from './constant'; +import type { RangeOption } from './typing'; +import InternalPickerPanel, { Rule } from './PickerPanel'; +import zhCN from './locale/zh-CN'; +import enUS from './locale/en-US'; +import './index.less'; + +export type RangeName = 'customize' | string; + +export type RangeValue = [Moment, Moment] | [Dayjs, Dayjs]; + +export type RangeDateValue = { + name: RangeName; + range: RangeValue; +}; + +export interface DateRangerProps + extends Omit { + // 数据相关 + selects?: RangeOption[]; + defaultQuickValue?: string; + // ui 相关 + hasRewind?: boolean; + hasPlay?: boolean; + hasNow?: boolean; + hasForward?: boolean; + hasZoomOut?: boolean; + // 时间选择提示 + tip?: string; + rules?: Rule[]; + /** 是否只允许选择过去时间 */ + pastOnly?: boolean; + isMoment?: boolean; + //固定 rangeName + stickRangeName?: boolean; + value?: RangeValue; + defaultValue?: RangeValue; + size?: 'small' | 'large' | 'middle'; + tooltipProps?: TooltipProps; + locale: any; +} + +const prefix = getPrefix('date-ranger'); + +const Ranger = (props: DateRangerProps) => { + const { + selects = [ + NEAR_1_MINUTES, + NEAR_30_MINUTES, + NEAR_1_HOURS, + NEAR_3_HOURS, + NEAR_6_HOURS, + TODAY, + LAST_3_DAYS, + ], + value, + defaultValue, + defaultQuickValue, + hasRewind = true, + hasPlay = false, + hasNow = true, + hasForward = true, + hasZoomOut = false, + pastOnly = false, + onChange = noop, + disabledDate, + locale, + size, + //固定 rangeName + stickRangeName = false, + tooltipProps, + isMoment: isMomentProps, + rules, + tip, + ...rest + } = props; + + console.log(locale, 'locale'); + const { token } = theme.useToken(); + + // 是否为 moment 时间对象 + const isMoment = + moment.isMoment(defaultValue?.[0]) || + moment.isMoment(defaultValue?.[1]) || + moment.isMoment(value?.[0]) || + moment.isMoment(value?.[1]) || + isMomentProps; + + const defaultRangeName = + value || defaultValue ? CUSTOMIZE : defaultQuickValue ?? selects?.[0]?.name; + const [rangeName, setRangeName] = useState(defaultRangeName); + + const [innerValue, setInnerValue] = useState( + value ?? + defaultValue ?? + (selects + .find(item => item.name === defaultRangeName) + ?.range(isMoment ? moment() : dayjs()) as RangeValue) + ); + + const [open, setOpen] = useState(false); + const [tooltipOpen, setTooltipOpen] = useState(false); + const refState = useRef({ + tooltipOpen, + }); + refState.current.tooltipOpen = tooltipOpen; + + // 没有 selects 时,回退到普通 RangePicker, 当前时间选项为自定义时,应该显示 RangePicker + const [isPlay, setIsPlay] = useState(rangeName !== CUSTOMIZE); + + const compare = (m1: RangeValue, m2: RangeValue) => { + if (Array.isArray(m1) && !Array.isArray(m2)) return false; + if (Array.isArray(m2) && !Array.isArray(m1)) return false; + return value[0] === innerValue[0] || value[1] === innerValue[1]; + }; + + useEffect(() => { + if (isNil(value) && isNil(innerValue)) return; + // FIXME: 当前存在值的时候赋空值给组件,不好处理先 workaround 绕过,后面再想一个整体的方案 + if (isNil(value) && !isNil(innerValue)) return; + const isEqual = compare(value, innerValue as RangeValue); + // 前后时间有差异时,进行赋值 + if (!isEqual) { + setInnerValue(value); + if (!stickRangeName) { + setRangeName(CUSTOMIZE); + } + } + }, [value, stickRangeName]); + + const closeTooltip = () => { + setOpen(false); + setTooltipOpen(false); + }; + + const handleNameChange = (name: string) => { + if (name !== CUSTOMIZE) { + closeTooltip(); + } + setRangeName(name); + }; + + const rangeChange = (range: RangeValue) => { + setInnerValue(range); + onChange(range); + }; + + const datePickerChange = (range: RangeValue) => { + rangeChange(range); + setRangeName(CUSTOMIZE); + }; + + const disabledFuture = (current: Moment | Dayjs) => { + const futureDay = moment.isMoment(current) ? moment().endOf('day') : dayjs().endOf('day'); + // 禁止选择未来日期 + return current && futureDay && current > futureDay; + }; + + const startTime = innerValue?.[0]; + const endTime = innerValue?.[1]; + const differenceMs = endTime?.diff(startTime as any); + + const differenceSeconds = endTime?.diff(startTime as any, 'seconds'); + const differenceMinutes = endTime?.diff(startTime as any, 'minutes'); + const differenceHours = endTime?.diff(startTime as any, 'hours'); + const differenceDays = endTime?.diff(startTime as any, 'days'); + const differenceWeeks = endTime?.diff(startTime as any, 'weeks'); + const differenceMonths = endTime?.diff(startTime as any, 'months'); + const differenceYears = endTime?.diff(startTime as any, 'years'); + + const getCustomizeRangeLabel = () => { + if (differenceYears > 0) { + return `${differenceYears}y`; + } + + if (differenceMonths > 0) { + return `${differenceMonths}mon`; + } + + if (differenceWeeks > 0) { + return `${differenceWeeks}w`; + } + + if (differenceDays > 0) { + return `${differenceDays}d`; + } + + if (differenceHours > 0) { + return `${differenceHours}h`; + } + + if (differenceMinutes > 0) { + return `${differenceMinutes}m`; + } + + return `${differenceSeconds}s`; + }; + + const getCustomizeLabel = () => { + if (differenceYears > 0) { + return `近 ${differenceYears} 年`; + } + + // if (differenceQuarters > 0) { + // return `近 ${differenceQuarters} 季度`; + // } + + if (differenceMonths > 0) { + return `近 ${differenceMonths} 月`; + } + + if (differenceWeeks > 0) { + return `近 ${differenceWeeks} 周`; + } + + if (differenceDays > 0) { + return `近 ${differenceDays} 天`; + } + + if (differenceHours > 0) { + return `近 ${differenceHours} 时`; + } + + if (differenceMinutes > 0) { + return `近 ${differenceMinutes} 分`; + } + + return `近 ${differenceSeconds} 秒`; + }; + + const setNow = () => { + const selected = NEAR_TIME_LIST.find(item => item.name === rangeName); + if (selected?.range) { + rangeChange(selected.range(isMoment ? moment() : dayjs()) as RangeValue); + } + if (rangeName === CUSTOMIZE) { + const eTime = isMoment ? moment() : dayjs(); + rangeChange([(eTime as Dayjs)?.clone().subtract(differenceMs), eTime] as RangeValue); + } + }; + + const rangeLabel = + rangeName === CUSTOMIZE + ? getCustomizeRangeLabel() + : selects.find(_item => _item.name === rangeName)?.rangeLabel; + + const label = + rangeName === CUSTOMIZE + ? getCustomizeLabel() + : selects.find(_item => _item.name === rangeName)?.label; + + const thisYear = new Date().getFullYear(); + const isThisYear = startTime?.year() === thisYear && endTime?.year() === thisYear; + const rangeNameIndex = findIndex(selects, item => item.name === rangeName); + + const nextRangeItem = + rangeNameIndex === -1 + ? selects.find(item => { + const [s, e] = item.range(isMoment ? moment() : dayjs()) as RangeValue; + // 自定义模式下,对比毫秒来选出比当前范围大一级的 rangeItem + const diffMs = e.diff(s as any); + return diffMs > differenceMs; + }) + : selects[rangeNameIndex + 1]; + + return ( + + +
+ { + if (o === false && refState.current.tooltipOpen) { + return; + } + + setOpen(o); + }} + menu={{ + onClick: ({ key, domEvent }) => { + const selected = NEAR_TIME_LIST.find(_item => _item.name === key); + // 存在快捷选项切换为极简模式 + if (selected?.range) { + handleNameChange(key); + setIsPlay(true); + rangeChange(selected.range(isMoment ? moment() : dayjs()) as RangeValue); + } + }, + items: [ + ...selects, + { + name: CUSTOMIZE, + rangeLabel: locale.customize, + label: locale.customTime, + }, + ] + .filter(item => { + return !!item; + }) + .map(item => { + return { + key: item.name, + label: + item.name === CUSTOMIZE ? ( + { + if (o) { + setTooltipOpen(true); + } + }} + placement="right" + {...tooltipProps} + overlayStyle={{ + maxWidth: 336, + ...tooltipProps?.overlayStyle, + }} + overlayInnerStyle={{ + background: '#fff', + maxHeight: 'none', + margin: 16, + ...tooltipProps?.overlayInnerStyle, + }} + title={ + { + setIsPlay(false); + rangeChange( + vList.map(v => { + return isMoment ? moment(v) : dayjs(v); + }) as RangeValue + ); + + closeTooltip(); + }} + onCancel={() => { + closeTooltip(); + }} + /> + } + > + + {item.rangeLabel} + {/* @ts-ignore */} + {locale[item.label] || item.label} + + + ) : ( + + {item.rangeLabel} + {/* @ts-ignore */} + {locale[item.label] || item.label} + + ), + }; + }), + }} + > + + + {rangeLabel} + + {isPlay &&
{label}
} +
+
+ {!isPlay && ( + { + setOpen(true); + }} + > + {/* @ts-ignore */} + { + // format 会影响布局,原先采用 v.year() === new Date().getFullYear() 进行判断,value 一共会传入三次(range0 range1 now), 会传入最新的时间导致判断异常 + return isThisYear ? v.format(DATE_TIME_FORMAT) : v.format(YEAR_DATE_TIME_FORMAT); + }} + // @ts-ignore + value={innerValue} + onChange={datePickerChange} + allowClear={false} + size={size} + // 透传 props 到 antd Ranger + {...omit(rest, 'value', 'onChange')} + /> + + )} +
+ + {hasRewind && ( + { + if (isPlay) { + setIsPlay(false); + } + + if (startTime && endTime) { + const newStartTime = (startTime as Dayjs) + .clone() + .subtract(differenceMs, 'milliseconds'); + const newEndTime = startTime?.clone() as Dayjs; + rangeChange([newStartTime, newEndTime]); + } + }} + > + + + )} + {hasForward && ( + { + if (startTime && endTime) { + const newStartTime = endTime.clone() as Dayjs; + const newEndTime = (endTime as Dayjs).clone().add(differenceMs); + + if (newEndTime.isBefore(new Date())) { + rangeChange([newStartTime, newEndTime]); + } else { + setIsPlay(true); + setNow(); + } + } + }} + > + + + )} + +
+ {hasNow && ( + + )} + {hasZoomOut && ( +