添加季度选择器 Quarter (#1382)

* quarter 相关

* 优化 time 选择器

* 优化 time 选择器样式

* quarter 部分代码

* 添加 Quarter 组件

* rm debugger

* chart 默认不可见时不销毁
This commit is contained in:
liaoxuezhi 2021-01-14 09:21:16 +08:00 committed by GitHub
parent dbf3581bb9
commit 625f2f1691
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 708 additions and 56 deletions

View File

@ -0,0 +1,27 @@
---
title: Quarter 季度
description:
type: 0
group: null
menuName: Quarter 季度
icon:
order: 62
---
## 基本用法
```schema:height="400" scope="body"
{
"type": "form",
"api": "https://houtai.baidu.com/api/mock2/form/saveForm",
"controls": [
{
"type": "quarter",
"name": "quarter",
"label": "季度"
}
]
}
```
更多用法和配置可以参考 [Date 日期](date)quarter 就是 data 的特定配置,所以 data 的所有配置都能使用。

View File

@ -685,6 +685,15 @@ export default [
import('../../docs/zh-CN/components/form/year.md').then(
makeMarkdownRenderer
)
},
{
label: 'Quarter 年',
path: '/zh-CN/docs/components/form/quarter',
getComponent: () =>
// @ts-ignore
import('../../docs/zh-CN/components/form/quarter.md').then(
makeMarkdownRenderer
)
}
]
},

View File

@ -193,24 +193,6 @@
text-align: center;
}
input {
outline: none;
width: 42px;
font-size: var(--Calendar-input-fontSize);
color: var(--Calendar-input-color);
border: 1px solid var(--Calendar-input-borderColor);
border-radius: var(--Calendar-input-borderRadius);
height: var(--Calendar-input-height);
line-height: var(--Calendar-input-lineHeight);
padding: var(--Calendar-input-paddingY) var(--Calendar-input-paddingX);
box-shadow: none;
&:focus {
border-color: var(--Calendar-input-onFocused-borderColor);
box-shadow: none;
}
}
.rdtActions {
margin-top: var(--gap-sm);
text-align: right;
@ -218,18 +200,87 @@
}
}
.rdtCounter {
.rdtBtn {
height: 30%;
line-height: px2rem(20px);
// .rdtCounter {
// .rdtBtn {
// height: 30%;
// line-height: px2rem(20px);
// }
// .rdtCount {
// height: 40%;
// display: flex;
// align-items: center;
// justify-content: center;
// }
// }
}
.#{$ns}CalendarInput {
outline: none;
width: 40px;
font-size: var(--Calendar-input-fontSize);
color: var(--Calendar-input-color);
border: 1px solid var(--Calendar-input-borderColor);
border-radius: var(--Calendar-input-borderRadius);
height: var(--Calendar-input-height);
line-height: var(--Calendar-input-lineHeight);
padding: var(--Calendar-input-paddingY) var(--Calendar-input-paddingX);
box-shadow: none;
&:focus {
border-color: var(--Calendar-input-onFocused-borderColor);
box-shadow: none;
}
}
.#{$ns}CalendarTime {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.#{$ns}CalendarCounter {
&-btn {
display: flex;
justify-content: center;
align-items: center;
height: 25px;
text-align: center;
color: var(--Button--primary-bg);
&:hover {
color: var(--Button--primary-onActive-bg);
}
.rdtCount {
height: 40%;
display: flex;
align-items: center;
justify-content: center;
> svg {
width: 16px;
height: 16px;
}
&--up > svg {
transform: rotate(-90deg);
}
&--down > svg {
transform: rotate(90deg);
}
}
&-sep {
width: 15px;
text-align: center;
}
&--daypart {
margin-left: 10px;
}
&--daypart &-value {
height: 30px;
display: flex;
align-items: center;
padding: 0 5px;
}
}
@ -347,7 +398,8 @@
}
td.rdtMonth,
td.rdtYear {
td.rdtYear,
td.rdtQuarter {
width: px2rem(50px);
height: px2rem(40px);

View File

@ -228,7 +228,7 @@ export type ShortCuts =
| ShortCutDateRange;
export interface DateProps extends LocaleProps, ThemeProps {
viewMode: 'years' | 'months' | 'days' | 'time';
viewMode: 'years' | 'months' | 'days' | 'time' | 'quarters';
className?: string;
placeholder?: string;
inputFormat?: string;
@ -248,7 +248,23 @@ export interface DateProps extends LocaleProps, ThemeProps {
minTime?: moment.Moment;
maxTime?: moment.Moment;
dateFormat?: string;
timeConstraints?: any;
timeConstraints?: {
hours?: {
min: number;
max: number;
step: number;
};
minutes?: {
min: number;
max: number;
step: number;
};
seconds: {
min: number;
max: number;
step: number;
};
};
popOverContainer?: any;
// 是否为内嵌模式,如果开启就不是 picker 了,直接页面点选。
@ -381,7 +397,8 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
dateFormat,
timeFormat,
closeOnSelect,
utc
utc,
viewMode
} = this.props;
if (!moment.isMoment(value)) {

View File

@ -577,6 +577,8 @@ export class DateRangePicker extends React.Component<
embed
} = this.props;
const __ = this.props.translate;
let viewMode: 'days' | 'months' | 'years' | 'months' | 'days' | 'time' =
'days';
const {startDate, endDate} = this.state;
return (
@ -591,7 +593,7 @@ export class DateRangePicker extends React.Component<
dateFormat={dateFormat}
timeFormat={timeFormat}
isValidDate={this.checkStartIsValidDate}
viewMode="days"
viewMode={viewMode}
input={false}
onClose={this.close}
renderDay={this.renderDay}
@ -608,7 +610,7 @@ export class DateRangePicker extends React.Component<
viewDate={this.nextMonth}
isEndDate
isValidDate={this.checkEndIsValidDate}
viewMode="days"
viewMode={viewMode}
input={false}
onClose={this.close}
renderDay={this.renderDay}

View File

@ -6,8 +6,11 @@ import React from 'react';
import CustomCalendarContainer from './CalendarContainer';
import cx from 'classnames';
import moment from 'moment';
import {themeable, ThemeOutterProps, ThemeProps} from '../../theme';
interface BaseDatePickerProps extends ReactDatePicker.DatetimepickerProps {
interface BaseDatePickerProps
extends Omit<ReactDatePicker.DatetimepickerProps, 'viewMode'> {
viewMode?: 'years' | 'months' | 'days' | 'time' | 'quarters';
inputFormat?: string;
onViewModeChange?: (type: string) => void;
requiredConfirm?: boolean;
@ -22,8 +25,41 @@ interface BaseDatePickerProps extends ReactDatePicker.DatetimepickerProps {
class BaseDatePicker extends ReactDatePicker {
state: any;
props: BaseDatePickerProps;
props: any;
setState: (state: any) => void;
getStateFromProps: any;
constructor(props: any) {
super(props);
const state = this.getStateFromProps(this.props);
if (state.open === undefined) {
state.open = !this.props.input;
}
state.currentView = this.props.dateFormat
? this.props.viewMode || state.updateOn || 'days'
: 'time';
this.state = state;
}
static propTypes = {};
getUpdateOn = (formats: any) => {
if (formats.date.match(/[lLD]/)) {
return 'days';
} else if (formats.date.indexOf('M') !== -1) {
return 'months';
} else if (formats.date.indexOf('Q') !== -1) {
return 'quarters';
} else if (formats.date.indexOf('Y') !== -1) {
return 'years';
}
return 'days';
};
getComponentProps = ((origin: Function) => {
return () => {
const props = origin.call(this);
@ -37,22 +73,29 @@ class BaseDatePicker extends ReactDatePicker {
'classPrefix',
'prevIcon',
'nextIcon',
'isEndDate'
'isEndDate',
'classnames'
].forEach(key => (props[key] = (this.props as any)[key]));
return props;
};
})((this as any).getComponentProps);
setDate = (type: 'month' | 'year') => {
setDate = (type: 'month' | 'year' | 'quarters') => {
// todo 没看懂这个是啥意思,好像没啥用
const currentShould =
this.props.viewMode === 'months' &&
!/^mm$/i.test(this.props.inputFormat || '');
const nextViews = {
month: currentShould ? 'months' : 'days',
year: currentShould ? 'months' : 'days'
year: currentShould ? 'months' : 'days',
quarters: ''
};
if ((this.props.viewMode as any) === 'quarters') {
nextViews.year = 'quarters';
}
return (e: any) => {
this.setState({
viewDate: this.state.viewDate
@ -67,6 +110,67 @@ class BaseDatePicker extends ReactDatePicker {
};
};
updateSelectedDate = (e: React.MouseEvent, close?: boolean) => {
const that: any = this;
let target = e.currentTarget,
modifier = 0,
viewDate = this.state.viewDate,
currentDate = this.state.selectedDate || viewDate,
date;
if (target.className.indexOf('rdtDay') !== -1) {
if (target.className.indexOf('rdtNew') !== -1) modifier = 1;
else if (target.className.indexOf('rdtOld') !== -1) modifier = -1;
date = viewDate
.clone()
.month(viewDate.month() + modifier)
.date(parseInt(target.getAttribute('data-value')!, 10));
} else if (target.className.indexOf('rdtMonth') !== -1) {
date = viewDate
.clone()
.month(parseInt(target.getAttribute('data-value')!, 10))
.date(currentDate.date());
} else if (target.className.indexOf('rdtQuarter') !== -1) {
date = viewDate
.clone()
.quarter(parseInt(target.getAttribute('data-value')!, 10))
.date(currentDate.date());
} else if (target.className.indexOf('rdtYear') !== -1) {
date = viewDate
.clone()
.month(currentDate.month())
.date(currentDate.date())
.year(parseInt(target.getAttribute('data-value')!, 10));
}
date
.hours(currentDate.hours())
.minutes(currentDate.minutes())
.seconds(currentDate.seconds())
.milliseconds(currentDate.milliseconds());
if (!this.props.value) {
var open = !(this.props.closeOnSelect && close);
if (!open) {
that.props.onBlur(date);
}
this.setState({
selectedDate: date,
viewDate: date.clone().startOf('month'),
inputValue: date.format(this.state.inputFormat),
open: open
});
} else {
if (this.props.closeOnSelect && close) {
that.closeCalendar();
}
}
that.props.onChange(date);
};
render() {
const Component = CustomCalendarContainer as any;
return (
@ -82,5 +186,5 @@ class BaseDatePicker extends ReactDatePicker {
}
}
const Calendar: any = BaseDatePicker;
const Calendar: any = themeable(BaseDatePicker as any);
export default Calendar as React.ComponentType<BaseDatePickerProps>;

View File

@ -4,12 +4,16 @@ import CalendarContainer from 'react-datetime/src/CalendarContainer';
import CustomDaysView from './DaysView';
import CustomYearsView from './YearsView';
import CustomMonthsView from './MonthsView';
import CustomTimeView from './TimeView';
import QuartersView from './QuartersView';
export default class CustomCalendarContainer extends CalendarContainer {
viewComponents: any = {
...(this as any).viewComponents,
days: CustomDaysView,
years: CustomYearsView,
months: CustomMonthsView
months: CustomMonthsView,
time: CustomTimeView,
quarters: QuartersView
};
}

View File

@ -3,6 +3,7 @@ import moment from 'moment';
import DaysView from 'react-datetime/src/DaysView';
import React from 'react';
import {LocaleProps, localeable} from '../../locale';
import {ClassNamesFn} from '../../theme';
interface CustomDaysViewProps extends LocaleProps {
classPrefix?: string;
@ -35,6 +36,7 @@ interface CustomDaysViewProps extends LocaleProps {
showView: (view: string) => () => void;
updateSelectedDate: (event: React.MouseEvent<any>, close?: boolean) => void;
handleClickOutside: () => void;
classnames: ClassNamesFn;
}
export class CustomDaysView extends DaysView {
@ -112,7 +114,13 @@ export class CustomDaysView extends DaysView {
};
renderTimes = () => {
const {timeFormat, selectedDate, viewDate, isEndDate} = this.props;
const {
timeFormat,
selectedDate,
viewDate,
isEndDate,
classnames: cx
} = this.props;
const date = selectedDate || (isEndDate ? viewDate.endOf('day') : viewDate);
const inputs: Array<React.ReactNode> = [];
@ -131,6 +139,7 @@ export class CustomDaysView extends DaysView {
key={i + 'input'}
type="text"
value={date.format(format)}
className={cx('CalendarInput')}
min={min}
max={max}
onChange={e =>

View File

@ -47,8 +47,8 @@ export class CustomMonthsView extends MonthsView {
return (
<div className="rdtMonths">
<table>
{showYearHead && (
{showYearHead && (
<table>
<thead>
<tr>
<th
@ -78,8 +78,9 @@ export class CustomMonthsView extends MonthsView {
</th>
</tr>
</thead>
)}
</table>
</table>
)}
<table>
<tbody>{this.renderMonths()}</tbody>
</table>

View File

@ -0,0 +1,171 @@
import React from 'react';
import {localeable, LocaleProps} from '../../locale';
import {ThemeProps} from '../../theme';
export interface QuarterViewProps extends LocaleProps, ThemeProps {
viewDate: moment.Moment;
selectedDate: moment.Moment;
inputFormat?: string;
updateOn: string;
subtractTime: (
amount: number,
type: string,
toSelected?: moment.Moment
) => () => void;
addTime: (
amount: number,
type: string,
toSelected?: moment.Moment
) => () => void;
setDate: (type: string) => () => void;
showView: (view: string) => () => void;
updateSelectedDate: (e: any, close?: boolean) => void;
renderQuarter: any;
isValidDate: any;
}
export class QuarterView extends React.Component<QuarterViewProps> {
alwaysValidDate: any;
renderYear() {
const __ = this.props.translate;
const showYearHead = !/^mm$/i.test(this.props.inputFormat || '');
if (!showYearHead) {
return null;
}
const canClick = /yy/i.test(this.props.inputFormat || '');
return (
<table>
<thead>
<tr>
<th
className="rdtPrev"
onClick={this.props.subtractTime(1, 'years')}
>
«
</th>
{canClick ? (
<th className="rdtSwitch" onClick={this.props.showView('years')}>
{this.props.viewDate.format(__('YYYY年'))}
</th>
) : (
<th className="rdtSwitch">
{this.props.viewDate.format(__('YYYY年'))}
</th>
)}
<th className="rdtNext" onClick={this.props.addTime(1, 'years')}>
»
</th>
</tr>
</thead>
</table>
);
}
renderQuarters() {
let date = this.props.selectedDate,
month = this.props.viewDate.month(),
year = this.props.viewDate.year(),
rows = [],
i = 1,
months = [],
renderer = this.props.renderQuarter || this.renderQuarter,
isValid = this.props.isValidDate || this.alwaysValidDate,
classes,
props: any,
currentMonth: moment.Moment,
isDisabled,
noOfDaysInMonth,
daysInMonth,
validDay,
// Date is irrelevant because we're only interested in month
irrelevantDate = 1;
while (i < 5) {
classes = 'rdtQuarter';
currentMonth = this.props.viewDate
.clone()
.set({year: year, quarter: i, date: irrelevantDate});
noOfDaysInMonth = currentMonth.endOf('quarter').format('Q');
daysInMonth = Array.from(
{length: parseInt(noOfDaysInMonth, 10)},
function (e, i) {
return i + 1;
}
);
validDay = daysInMonth.find(function (d) {
var day = currentMonth.clone().set('date', d);
return isValid(day);
});
isDisabled = validDay === undefined;
if (isDisabled) classes += ' rdtDisabled';
if (date && i === date.quarter() && year === date.year())
classes += ' rdtActive';
props = {
'key': i,
'data-value': i,
'className': classes
};
if (!isDisabled) {
props.onClick =
this.props.updateOn === 'quarters'
? this.updateSelectedQuarter
: this.props.setDate('quarter');
}
months.push(renderer(props, i, year, date && date.clone()));
if (months.length === 2) {
rows.push(
React.createElement('tr', {key: month + '_' + rows.length}, months)
);
months = [];
}
i++;
}
return rows;
}
renderQuarter = (
props: any,
quartar: number,
year: number,
date: moment.Moment
) => {
return (
<td {...props}>
<span>Q{quartar}</span>
</td>
);
};
updateSelectedQuarter = (event: any) => {
this.props.updateSelectedDate(event);
};
render() {
const {classnames: cx} = this.props;
return (
<div className={cx('ClalendarQuarter')}>
{this.renderYear()}
<table>
<tbody>{this.renderQuarters()}</tbody>
</table>
</div>
);
}
}
export default localeable(QuarterView);

View File

@ -0,0 +1,183 @@
// @ts-ignore
import TimeView from 'react-datetime/src/TimeView';
import moment from 'moment';
import React from 'react';
import {LocaleProps, localeable} from '../../locale';
import {Icon} from '../..';
import {ClassNamesFn} from '../../theme';
export class CustomTimeView extends TimeView {
props: {
viewDate: moment.Moment;
subtractTime: (
amount: number,
type: string,
toSelected?: moment.Moment
) => () => void;
addTime: (
amount: number,
type: string,
toSelected?: moment.Moment
) => () => void;
showView: (view: string) => () => void;
timeFormat: string;
classnames: ClassNamesFn;
setTime: (type: string, value: any) => void;
} & LocaleProps;
onStartClicking: any;
disableContextMenu: any;
updateMilli: any;
renderHeader: any;
pad: any;
state: {daypart: any; counters: Array<string>; [propName: string]: any};
timeConstraints: any;
padValues = {
hours: 2,
minutes: 2,
seconds: 2,
milliseconds: 3
};
renderDayPart = () => {
const {translate: __, classnames: cx} = this.props;
return (
<div
key="dayPart"
className={cx('CalendarCounter CalendarCounter--daypart')}
>
<span
key="up"
className={cx('CalendarCounter-btn CalendarCounter-btn--up')}
onClick={this.onStartClicking('toggleDayPart', 'hours')}
onContextMenu={this.disableContextMenu}
>
<Icon icon="right-arrow-bold" />
</span>
<div className={cx('CalendarCounter-value')} key={this.state.daypart}>
{__(this.state.daypart)}
</div>
<span
key="down"
className={cx('CalendarCounter-btn CalendarCounter-btn--down')}
onClick={this.onStartClicking('toggleDayPart', 'hours')}
onContextMenu={this.disableContextMenu}
>
<Icon icon="right-arrow-bold" />
</span>
</div>
);
};
renderCounter = (type: string) => {
const cx = this.props.classnames;
if (type !== 'daypart') {
var value = this.state[type];
if (
type === 'hours' &&
this.props.timeFormat.toLowerCase().indexOf(' a') !== -1
) {
value = ((value - 1) % 12) + 1;
if (value === 0) {
value = 12;
}
}
const {min, max, step} = this.timeConstraints[type];
return (
<div key={type} className={cx('CalendarCounter')}>
<span
key="up"
className={cx('CalendarCounter-btn CalendarCounter-btn--up')}
onMouseDown={this.onStartClicking('increase', type)}
onContextMenu={this.disableContextMenu}
>
<Icon icon="right-arrow-bold" />
</span>
<div key="c" className={cx('CalendarCounter-value')}>
<input
type="text"
value={this.pad(type, value)}
className={cx('CalendarInput')}
min={min}
max={max}
step={step}
onChange={e =>
this.props.setTime(
type,
Math.max(
min,
Math.min(
parseInt(e.currentTarget.value.replace(/\D/g, ''), 10) ||
0,
max
)
)
)
}
/>
</div>
<span
key="do"
className={cx('CalendarCounter-btn CalendarCounter-btn--down')}
onMouseDown={this.onStartClicking('decrease', type)}
onContextMenu={this.disableContextMenu}
>
<Icon icon="right-arrow-bold" />
</span>
</div>
);
}
return null;
};
render() {
const counters: Array<JSX.Element | null> = [];
const cx = this.props.classnames;
this.state.counters.forEach(c => {
if (counters.length) {
counters.push(
<div
key={`sep${counters.length}`}
className={cx('CalendarCounter-sep')}
>
:
</div>
);
}
counters.push(this.renderCounter(c));
});
if (this.state.daypart !== false) {
counters.push(this.renderDayPart());
}
if (
this.state.counters.length === 3 &&
this.props.timeFormat.indexOf('S') !== -1
) {
counters.push(
<div className={cx('CalendarCounter-sep')} key="sep5">
:
</div>
);
counters.push(
<div className={cx('CalendarCounter CalendarCounter--milli')}>
<input
value={this.state.milliseconds}
type="text"
onChange={this.updateMilli}
/>
</div>
);
}
return <div className={cx('CalendarTime')}>{counters}</div>;
}
}
export default localeable(CustomTimeView as any);

View File

@ -154,7 +154,7 @@ export interface ChartProps extends RendererProps, Omit<ChartSchema, 'type'> {
export class Chart extends React.Component<ChartProps> {
static defaultProps: Partial<ChartProps> = {
replaceChartOption: false,
unMountOnHidden: true
unMountOnHidden: false
};
static propsList: Array<string> = [];

View File

@ -10,7 +10,7 @@ export interface DateBaseControlSchema extends FormBaseControl {
/**
*
*/
type: 'date' | 'datetime' | 'time' | 'month';
type: 'date' | 'datetime' | 'time' | 'month' | 'quarter';
/**
*
@ -179,11 +179,49 @@ export interface MonthControlSchema extends DateBaseControlSchema {
inputFormat?: string;
}
/**
*
*/
export interface QuarterControlSchema extends DateBaseControlSchema {
/**
*
*/
type: 'quarter';
/**
*
* @default X
*/
format?: string;
/**
*
* @default YYYY-MM
*/
inputFormat?: string;
}
export interface DateProps extends FormControlProps {
inputFormat?: string;
timeFormat?: string;
format?: string;
timeConstraints?: object;
timeConstraints?: {
hours?: {
min: number;
max: number;
step: number;
};
minutes?: {
min: number;
max: number;
step: number;
};
seconds: {
min: number;
max: number;
step: number;
};
};
closeOnSelect?: boolean;
disabled: boolean;
iconClassName?: string;
@ -267,19 +305,32 @@ export default class DateControl extends React.PureComponent<
}
render() {
const {
let {
className,
defaultValue,
defaultData,
classnames: cx,
minDate,
maxDate,
type,
format,
timeFormat,
...rest
} = this.props;
if (type === 'time' && timeFormat) {
format = timeFormat;
}
return (
<div className={cx(`DateControl`, className)}>
<DatePicker {...rest} {...this.state} classnames={cx} />
<DatePicker
{...rest}
timeFormat={timeFormat}
format={format}
{...this.state}
classnames={cx}
/>
</div>
);
}
@ -344,6 +395,21 @@ export class MonthControlRenderer extends DateControl {
};
}
@FormItem({
type: 'quarter'
})
export class QuarterControlRenderer extends DateControl {
static defaultProps = {
...DateControl.defaultProps,
placeholder: '请选择季度',
inputFormat: 'YYYY [Q]Q',
dateFormat: 'YYYY [Q]Q',
timeFormat: '',
viewMode: 'quarters',
closeOnSelect: true
};
}
@FormItem({
type: 'year'
})

View File

@ -36,6 +36,7 @@ import {
DateControlSchema,
DateTimeControlSchema,
MonthControlSchema,
QuarterControlSchema,
TimeControlSchema
} from './Date';
import {DateRangeControlSchema} from './DateRange';
@ -98,6 +99,7 @@ export type FormControlType =
| 'date'
| 'datetime'
| 'time'
| 'quarter'
| 'month'
| 'date-range'
| 'diff'
@ -210,6 +212,8 @@ export type FormControlSchema =
| DateTimeControlSchema
| TimeControlSchema
| MonthControlSchema
| MonthControlSchema
| QuarterControlSchema
| DateRangeControlSchema
| DiffControlSchema
| EditorControlSchema

View File

@ -123,6 +123,13 @@ export interface ThemeProps {
theme?: string;
}
export interface ThemeOutterProps {
theme?: string;
className?: string;
classPrefix?: string;
classnames?: ClassNamesFn;
}
export let defaultTheme: string = 'default';
export const ThemeContext = React.createContext('');
@ -134,12 +141,8 @@ export function themeable<
type OuterProps = JSX.LibraryManagedAttributes<
T,
Omit<React.ComponentProps<T>, keyof ThemeProps>
> & {
theme?: string;
className?: string;
classPrefix?: string;
classnames?: ClassNamesFn;
};
> &
ThemeOutterProps;
const result = hoistNonReactStatic(
class extends React.Component<OuterProps> {