Skip to content

Commit

Permalink
feat: Add new range
Browse files Browse the repository at this point in the history
  • Loading branch information
linhf committed Nov 12, 2023
1 parent debd02b commit f4536ad
Show file tree
Hide file tree
Showing 19 changed files with 1,040 additions and 0 deletions.
1 change: 1 addition & 0 deletions .dumirc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,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' },
Expand Down
128 changes: 128 additions & 0 deletions packages/ui/src/DateRanger/QuickPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { ClockCircleOutlined, DownOutlined } from '@oceanbase/icons';
import { Dropdown, Menu, Select, Space } from '@oceanbase/design';
import classnames from 'classnames';
import dayjs from 'dayjs';
import { noop } from 'lodash';
import moment from 'moment';
import React, { useEffect, useState } from 'react';
import type { LocaleWrapperProps } from '../locale/LocaleWrapper';
import { getPrefix } from '../_util';
import { CUSTOMIZE } from './constant';
import type { RangeValue } from './Ranger';
import type { RangeOption } from './typing';

interface SelectProps {
selects: RangeOption[];
value: string;
onChange: (name: string) => void;
customable?: boolean;
locale?: Record<string, string>;
size?: 'small' | 'large' | 'middle';
}

export type QuickType = 'select' | 'dropdown';

interface QuickPickerProps extends LocaleWrapperProps {
selects: RangeOption[];
type?: QuickType;
onChange: (range: RangeValue, rangeName?: string) => void;
onNameChange?: (name: string) => void;
customable?: boolean;
name?: string;
defaultName?: string;
isMoment?: boolean;
size?: 'small' | 'large' | 'middle';
}

const prefix = getPrefix('ranger-quick-picker');

const RangeDropdown = ({ selects, onChange, value, customable, locale = {} }: SelectProps) => {
const menu = (
<Menu
onClick={e => {
onChange(e.key as string);
}}
style={{ minWidth: 120 }}
>
{selects.map(item => (
<Menu.Item key={item.name}>{locale[item.name] || item.name}</Menu.Item>
))}
{customable && <Menu.Item key={CUSTOMIZE}>{locale.customize}</Menu.Item>}
</Menu>
);

const match = selects.find(item => item.name === value);

return (
<Dropdown overlay={menu}>
<Space style={{ cursor: 'pointer' }} className={classnames(prefix, `${prefix}-dropdown`)}>
<ClockCircleOutlined />
{locale[match?.name] || match?.name}
<DownOutlined />
</Space>
</Dropdown>
);
};

const RangeSelect = ({ selects, onChange, value, customable, locale = {}, size }: SelectProps) => {
const handleChange = (nextValue: string) => {
onChange(nextValue);
};
return (
<Select
className={classnames(prefix, `${prefix}-select`)}
style={{ minWidth: 120 }}
onSelect={handleChange}
value={value}
size={size}
>
{selects.map(item => {
return (
<Select.Option key={item.name} value={item.name}>
{locale[item.name] || item.name}
</Select.Option>
);
})}
{customable && (
<Select.Option key={CUSTOMIZE} value={CUSTOMIZE}>
{locale.customize}
</Select.Option>
)}
</Select>
);
};

export default (props: QuickPickerProps) => {
const {
type = 'select',
name,
defaultName,
selects,
onChange,
onNameChange = noop,
isMoment,
...rest
} = props;
const [innerName, setInnerName] = useState(name ?? defaultName ?? selects?.[0]?.name);

useEffect(() => {
if (name) {
setInnerName(name);
}
}, [name]);

const handleChange = (value: string) => {
const selected = selects.find(item => item.name === value);
setInnerName(value);
if (value !== CUSTOMIZE) {
onChange(selected.range(isMoment ? moment() : dayjs()) as RangeValue, value);
}
onNameChange(value);
};

return type === 'select' ? (
<RangeSelect value={innerName} selects={selects} onChange={handleChange} {...rest} />
) : (
<RangeDropdown value={innerName} selects={selects} onChange={handleChange} {...rest} />
);
};
233 changes: 233 additions & 0 deletions packages/ui/src/DateRanger/Ranger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import { Button, DatePicker, Radio, Space } from '@oceanbase/design';
import type { RangePickerProps } from '@oceanbase/design/es/date-picker';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import { isNil, noop, omit } from 'lodash';
import type { Moment } from 'moment';
import moment from 'moment';
import classNames from 'classnames';
import { useInterval } from 'ahooks';
import React, { useEffect, useMemo, useState } from 'react';
import LocaleWrapper from '../locale/LocaleWrapper';
import { getPrefix } from '../_util';
import {
CUSTOMIZE,
DATE_TIME_FORMAT,
NEAR_1_HOURS,
NEAR_1_MINUTES,
NEAR_30_MINUTES,
NEAR_TIME_LIST,
} from './constant';
import './index.less';
import zhCN from './locale/zh-CN';
import type { QuickType } from './QuickPicker';
import type { RangeOption } from './typing';
import {
DoubleLeftOutlined,
PauseOutlined,
CaretRightOutlined,
DoubleRightOutlined,
} from '@oceanbase/icons';
import QuickPicker from './QuickPicker';

export type RangeName = 'customize' | string;

export type RangeValue = [Moment, Moment] | [Dayjs, Dayjs];

export type RangeDateValue = {
name: RangeName;
range: RangeValue;
};

interface RangerProps extends Omit<RangePickerProps, 'mode' | 'picker' | 'value' | 'defaultValue'> {
// 数据相关
selects?: RangeOption[];
defaultQuickValue?: string;
// ui 相关
mode?: 'default' | 'mini';
quickType?: 'select' | 'dropdown';
/** 是否只允许选择过去时间 */
pastOnly?: boolean;
//固定rangeName
stickRangeName?: boolean;
value?: RangeValue;
defaultValue?: RangeValue;
size?: 'small' | 'large' | 'middle';
}

const prefix = getPrefix('ranger');

const Ranger = (props: RangerProps) => {
const {
selects = [NEAR_1_MINUTES, NEAR_30_MINUTES, NEAR_1_HOURS],
value,
defaultValue,
defaultQuickValue,
mode = 'default',
quickType = 'select',
pastOnly = false,
onChange = noop,
disabledDate,
locale,
size,
//固定rangeName
stickRangeName = false,
...rest
} = props;

console.log(props.onChange, 'props props props');

// 是否为 moment 时间对象
const isMoment =
moment.isMoment(defaultValue?.[0]) ||
moment.isMoment(defaultValue?.[1]) ||
moment.isMoment(value?.[0]) ||
moment.isMoment(value?.[1]);

const [innerValue, setInnerValue] = useState<RangeValue>(value ?? defaultValue);

const [rangeName, setRangeName] = useState(
value || defaultValue ? CUSTOMIZE : defaultQuickValue ?? selects?.[0]?.name
);

const [isPlay, setIsPlay] = useState(false);
const [radioValue, setRadioValue] = useState('');

const defaultInternalValue = useMemo(() => {
return selects
.find(item => item.name === rangeName)
?.range(isMoment ? moment() : dayjs()) as RangeValue;
}, [defaultValue]);

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]);

useEffect(() => {
if (defaultInternalValue) {
rangeChange(defaultInternalValue);
}
}, []);

const handleNameChange = (name: string) => {
setRangeName(name);
};

const rangeChange = (range: RangeValue, rName?: string) => {
setInnerValue(range);
onChange(range);
};

const datePickerChange = (range: RangeValue) => {
rangeChange(range, CUSTOMIZE);
setRangeName(CUSTOMIZE);
};

const disabledFuture = (current: Moment | Dayjs) => {
const futureDay = moment.isMoment(current) ? moment().endOf('day') : dayjs().endOf('day');
// 禁止选择未来日期
return current && futureDay && current > futureDay;
};

let internalQuickType!: QuickType;
if (quickType === 'dropdown' && rangeName !== CUSTOMIZE) {
internalQuickType = 'dropdown';
} else {
internalQuickType = 'select';
}
// 普通模式或者当前时间选项为自定义时,应该显示 rangePicker
const showRange = mode === 'default' || rangeName === CUSTOMIZE;
// 没有 selects 时,回退到普通 RangePicker
const showQuickPicker = selects.length !== 0;

useInterval(
() => {
const selected = NEAR_TIME_LIST.find(item => item.name === rangeName);
if (selected.range) {
rangeChange(selected.range(isMoment ? moment() : dayjs()) as RangeValue);
}
},
isPlay ? 1000 : null
);

return (
<Space
size={0}
className={classNames(
{
[`${prefix}-show-range`]: showRange,
},
prefix
)}
style={rest.style}
>
{showQuickPicker && (
<QuickPicker
customable
type={internalQuickType}
onChange={rangeChange}
onNameChange={handleNameChange}
selects={selects}
name={rangeName}
locale={locale}
isMoment={isMoment}
size={size}
/>
)}
{showRange && (
// @ts-ignore
<DatePicker.RangePicker
disabledDate={pastOnly ? disabledFuture : disabledDate}
format={DATE_TIME_FORMAT}
defaultValue={defaultValue}
value={innerValue || defaultInternalValue}
onChange={datePickerChange}
showTime={true}
className={`${prefix}-range-picker`}
size={size}
// 透传 props 到 antd Ranger
{...omit(rest, 'value', 'onChange')}
/>
)}
<Radio.Group value={radioValue} className={`${prefix}-playback-control`} buttonStyle="solid">
<Radio.Button value="stepBack">
<DoubleLeftOutlined />
</Radio.Button>
<Radio.Button
value={'play'}
onClick={() => {
const newPlay = !isPlay;
setRadioValue(newPlay ? 'play' : '');
setIsPlay(newPlay);
}}
>
{isPlay ? <PauseOutlined /> : <CaretRightOutlined />}
</Radio.Button>
<Radio.Button value="stepForward">
<DoubleRightOutlined />
</Radio.Button>
</Radio.Group>
</Space>
);
};

export default LocaleWrapper({
componentName: 'Ranger',
defaultLocale: zhCN,
})(Ranger);
Loading

0 comments on commit f4536ad

Please sign in to comment.