feat:日历组件展示日程 (#3058)

* feat:日历组件展示日程

* fix: 修改schema

* fix: schedule参数说明修改

* fix: 修改schedules参数描述

* fix: 代码调整

* fix: 参数color改为classname

* fix: readme文档补充

* feat: 日历组件支持从上下文中获取数据

* fix: 日历组件data获取条件修改

Co-authored-by: hongyang03 <hongyang03@baidu.com>
This commit is contained in:
HongYang 2021-12-06 17:53:02 +08:00 committed by GitHub
parent 6d5016d526
commit eac8e7e533
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 499 additions and 6 deletions

View File

@ -312,6 +312,185 @@ order: 13
}
```
## 日历日程
```schema: scope="body"
{
"type": "input-date",
"embed": true,
"schedules": [
{
"startTime": "2021-12-11 05:14:00",
"endTime": "2021-12-11 06:14:00",
"content": "这是一个日程1"
},
{
"startTime": "2021-12-21 05:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程2"
}
]
}
```
## 日历日程-自定义颜色
```schema: scope="body"
{
"type": "input-date",
"embed": true,
"schedules": [
{
"startTime": "2021-12-11 05:14:00",
"endTime": "2021-12-11 06:14:00",
"content": "这是一个日程1",
"className": "bg-success"
},
{
"startTime": "2021-12-21 05:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程2",
"className": "bg-info"
}
]
}
```
```schema: scope="body"
{
"type": "input-date",
"embed": true,
"scheduleClassNames": ["bg-success", "bg-info"],
"schedules": [
{
"startTime": "2021-12-11 05:14:00",
"endTime": "2021-12-11 06:14:00",
"content": "这是一个日程1"
},
{
"startTime": "2021-12-21 05:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程2"
}
]
}
```
## 日历日程-自定义日程展示
```schema: scope="body"
{
"type": "input-date",
"embed": true,
"schedules": [
{
"startTime": "2021-12-11 05:14:00",
"endTime": "2021-12-11 06:14:00",
"content": "这是一个日程1"
},
{
"startTime": "2021-12-21 05:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程2"
}
],
"scheduleAction": {
"actionType": "drawer",
"drawer": {
"title": "日程",
"body": {
"type": "table",
"columns": [
{
"name": "time",
"label": "时间"
},
{
"name": "content",
"label": "内容"
}
],
"data": "${scheduleData}"
}
}
}
}
```
## 日历日程-支持从数据源中获取日程
```schema
{
"type": "page",
"data": {
"schedules": [
{
"startTime": "2021-12-11 05:14:00",
"endTime": "2021-12-11 06:14:00",
"content": "这是一个日程1"
},
{
"startTime": "2021-12-21 05:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程2"
}
]
},
"body": [
{
"type": "input-date",
"embed": true,
"schedules": "${schedules}"
}
]
}
```
## 放大模式
```schema: scope="body"
{
"type": "input-date",
"embed": true,
"largeMode": true,
"schedules": [
{
"startTime": "2021-12-11 05:14:00",
"endTime": "2021-12-11 06:14:00",
"content": "这是一个日程1"
},
{
"startTime": "2021-12-12 02:14:00",
"endTime": "2021-12-13 05:14:00",
"content": "这是一个日程2"
},
{
"startTime": "2021-12-20 05:14:00",
"endTime": "2021-12-21 05:14:00",
"content": "这是一个日程3"
},
{
"startTime": "2021-12-21 05:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程4"
},
{
"startTime": "2021-12-22 02:14:00",
"endTime": "2021-12-23 05:14:00",
"content": "这是一个日程5"
},
{
"startTime": "2021-12-22 02:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程6"
},
{
"startTime": "2021-12-22 02:14:00",
"endTime": "2021-12-22 05:14:00",
"content": "这是一个日程7"
}
]
}
```
## 原生日期组件
原生数字日期将直接使用浏览器的实现,最终展现效果和浏览器有关,而且只支持 `min`、`max`、`step` 这几个属性设置。
@ -348,3 +527,7 @@ order: 13
| clearable | `boolean` | `true` | 是否可清除 |
| embed | `boolean` | `false` | 是否内联模式 |
| timeConstraints | `object` | `true` | 请参考: [react-datetime](https://github.com/YouCanBookMe/react-datetime) |
| schedules | `Array<{startTime: Date, endTime: Date, content: any, className?: string}> \| string` | | 日历中展示日程可设置静态数据或从上下文中取数据className参考[背景色](https://baidu.gitee.io/amis/zh-CN/style/background/background-color) |
| scheduleClassNames | `Array<string>` | `['bg-warning', 'bg-danger', 'bg-success', 'bg-info', 'bg-secondary']` | 日历中展示日程的颜色,参考[背景色](https://baidu.gitee.io/amis/zh-CN/style/background/background-color) |
| scheduleAction | `SchemaNode` | | 自定义日程展示 |
| largeMode | `boolean` | `false` | 放大模式 |

View File

@ -0,0 +1,81 @@
.#{$ns}ScheduleCalendar {
&-icon {
position: absolute;
bottom: var(--Calendar-icon-bottom);
left: 50%;
transform: translateX(-50%);
display: block;
width: var(--Calendar-icon-width);
height: var(--Calendar-icon-height);
border-radius: 50%;
z-index: 10;
}
&-action {
display: block;
padding: 0;
width: 100%;
height: 100%;
border: none;
background: transparent;
color: inherit;
&:not(:disabled):not(.is-disabled):hover {
color: inherit;
background: transparent;
border-color: transparent;
}
}
.rdtDay {
position: relative;
}
&-text-overflow {
white-space:nowrap;
overflow:hidden;
text-overflow:ellipsis;
position: absolute;
width: 100%;
}
}
.#{$ns}ScheduleCalendar-large {
width: 100%;
.rdtPicker {
width: 100%;
table {
border-collapse: collapse;
border-spacing: 0;
td {
border: var(--Calendar-borderWidth) solid var(--borderColor);
}
}
}
.rdtDay {
height: var(--Calendar-rdt-day);
vertical-align: top;
}
.#{$ns}ScheduleCalendar-large-day-wrap {
position: absolute;
top: 0;
left: 0;
min-width: 100%;
height: 100%;
.#{$ns}ScheduleCalendar-large-schedule-content {
position: relative;
z-index: 10;
border-radius: var(--borderRadius);
text-align: left;
padding: var(--Calendar-schedule-content-padding);
height: var(--Calendar-schedule-content-height);
color: var(--Calendar-schedule-content-color);
}
}
.#{$ns}ScheduleCalendar-action {
z-index: 20;
position: relative;
}
}

View File

@ -37,6 +37,7 @@
@import '../components/button-group';
@import '../components/dropdown';
@import '../components/each';
@import '../components/calendar';
@import '../components/collapse';
@import '../components/collapse-group';
@import '../components/color';

View File

@ -221,6 +221,15 @@ $L1: 0px 4px 6px 0px rgba(8, 14, 26, 0.06),
--Switch-onDisabled-color: #{$G11};
// --Switch-onDisabled-circle-BackgroundColor: #fff;
--Calendar-icon-bottom: #{px2rem(-4px)};
--Calendar-icon-width: #{px2rem(10px)};
--Calendar-icon-height: #{px2rem(10px)};
--Calendar-borderWidth: #{px2rem(1px)};
--Calendar-rdt-day: #{px2rem(100px)};
--Calendar-schedule-content-padding: 0 #{px2rem(4px)};
--Calendar-schedule-content-height: #{px2rem(20px)};
--Calendar-schedule-content-color: #{$white};
--ColorPicker-borderWidth: #{px2rem(1px)};
--ColorPicker-borderRadius: #{$R3};
--ColorPicker-bg: var(--white);

View File

@ -278,6 +278,15 @@ export interface DateProps extends LocaleProps, ThemeProps {
borderMode?: 'full' | 'half' | 'none';
// 是否为内嵌模式,如果开启就不是 picker 了,直接页面点选。
embed?: boolean;
schedules?: Array<{
startTime: Date,
endTime: Date,
content: any,
className?: string
}>;
scheduleClassNames?: Array<string>;
largeMode?: boolean;
onScheduleClick?: (scheduleData: any) => void;
// 下面那个千万不要写,写了就会导致 keyof DateProps 得到的结果是 string | number;
// [propName: string]: any;
@ -302,7 +311,8 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
viewMode: 'days' as 'years' | 'months' | 'days' | 'time',
shortcuts: '',
closeOnSelect: true,
overlayPlacement: 'auto'
overlayPlacement: 'auto',
scheduleClassNames: ['bg-warning', 'bg-danger', 'bg-success', 'bg-info', 'bg-secondary']
};
state: DatePickerState = {
isOpened: false,
@ -546,7 +556,11 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
format,
borderMode,
embed,
minDate
minDate,
schedules,
largeMode,
scheduleClassNames,
onScheduleClick
} = this.props;
const __ = this.props.translate;
@ -554,12 +568,33 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
let date: moment.Moment | undefined = this.state.value;
if (embed) {
let schedulesData: DateProps['schedules'] = undefined;
if (schedules && Array.isArray(schedules)) {
// 设置日程颜色
let index = 0;
schedulesData = schedules.map((schedule: any) => {
let className = schedule.className;
if (!className && scheduleClassNames) {
className = scheduleClassNames[index];
index++;
if (index >= scheduleClassNames.length) {
index = 0;
}
}
return {
...schedule,
className
};
});
}
return (
<div
className={cx(
`DateCalendar`,
{
'is-disabled': disabled
'is-disabled': disabled,
'ScheduleCalendar': schedulesData,
'ScheduleCalendar-large': largeMode
},
className
)}
@ -578,6 +613,9 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
locale={locale}
minDate={minDate}
// utc={utc}
schedules={schedulesData}
largeMode={largeMode}
onScheduleClick={onScheduleClick}
/>
</div>
);

View File

@ -28,6 +28,14 @@ interface BaseDatePickerProps
year?: number,
date?: moment.Moment
) => JSX.Element;
schedules?: Array<{
startTime: Date;
endTime: Date;
content: string | React.ReactElement;
color?: string;
}>;
largeMode?: boolean;
onScheduleClick?: (scheduleData: any) => void;
}
class BaseDatePicker extends ReactDatePicker {
@ -82,7 +90,10 @@ class BaseDatePicker extends ReactDatePicker {
'nextIcon',
'isEndDate',
'classnames',
'minDate'
'minDate',
'schedules',
'largeMode',
'onScheduleClick'
].forEach(key => (props[key] = (this.props as any)[key]));
return props;

View File

@ -5,6 +5,7 @@ import React from 'react';
import Downshift from 'downshift';
import {LocaleProps, localeable} from '../../locale';
import {ClassNamesFn} from '../../theme';
import find from 'lodash/find';
interface CustomDaysViewProps extends LocaleProps {
classPrefix?: string;
@ -39,6 +40,14 @@ interface CustomDaysViewProps extends LocaleProps {
updateSelectedDate: (event: React.MouseEvent<any>, close?: boolean) => void;
handleClickOutside: () => void;
classnames: ClassNamesFn;
schedules?: Array<{
startTime: Date,
endTime: Date,
content: any,
className?: string
}>;
largeMode?: boolean;
onScheduleClick?: (scheduleData: any) => void;
}
export class CustomDaysView extends DaysView {
@ -117,6 +126,86 @@ export class CustomDaysView extends DaysView {
};
renderDay = (props: any, currentDate: moment.Moment) => {
if (this.props.schedules) {
let schedule: any[] = [];
this.props.schedules.forEach((item: any) => {
if (currentDate.isSameOrAfter(moment(item.startTime).subtract(1, 'days')) && currentDate.isSameOrBefore(item.endTime)) {
schedule.push(item);
}
});
if (schedule.length > 0) {
const cx = this.props.classnames;
const __ = this.props.translate;
// 日程数据
const scheduleData = {
scheduleData: schedule.map((item: any) => {
return {
...item,
time: moment(item.startTime).format('YYYY-MM-DD HH:mm:ss') + ' - ' + moment(item.endTime).format('YYYY-MM-DD HH:mm:ss'),
}
}),
currentDate
};
// 放大模式
if (this.props.largeMode) {
let showSchedule: any[] = [];
for (let i = 0; i < schedule.length; i++) {
if (showSchedule.length > 3) {
break;
}
if (moment(schedule[i].startTime).isSame(currentDate, 'day')) {
showSchedule.push(schedule[i]);
}
else if (currentDate.weekday() === 0) {
// 周一重新设置日程
showSchedule.push({
...schedule[i],
width: moment(schedule[i].endTime).date() - currentDate.date()
});
}
}
[0, 1, 2].forEach((i: number) => {
const findSchedule = find(schedule, (item: any) => item.height === i);
if (findSchedule && findSchedule !== showSchedule[i] && currentDate.weekday() !== 0) {
// 生成一个空白格占位
showSchedule.splice(i, 0, {
width: 1,
className: 'bg-transparent',
content: ''
});
}
else {
showSchedule[i] && (showSchedule[i].height = i);
}
});
// 最多展示3个
showSchedule = showSchedule.slice(0, 3);
const scheduleDiv = showSchedule.map((item: any, index: number) => {
const width = item.width || Math.min(moment(item.endTime).diff(moment(item.startTime), 'days') + 1, 7 - moment(item.startTime).weekday());
return <div key={props.key + 'content' + index}
className={cx('ScheduleCalendar-large-schedule-content', item.className)}
style={{width: width + '00%'}}>
<div className={cx('ScheduleCalendar-text-overflow')}>{item.content}</div>
</div>;
});
return <td {...props} onClick={() => this.props.onScheduleClick && this.props.onScheduleClick(scheduleData)}>
<div className={cx('ScheduleCalendar-large-day-wrap')}>
<div className={cx('ScheduleCalendar-large-schedule-header')}>{currentDate.date()}</div>
{scheduleDiv}
{schedule.length > 3 && <div className={cx('ScheduleCalendar-large-schedule-footer')}>{schedule.length - 3} {__('more')}</div>}
</div>
</td>
}
// 正常模式
const ScheduleIcon = <span className={cx('ScheduleCalendar-icon', schedule[0].className)}></span>;
return <td {...props} onClick={() => this.props.onScheduleClick && this.props.onScheduleClick(scheduleData)}>
{currentDate.date()}
{ScheduleIcon}
</td>;
}
}
return <td {...props}>{currentDate.date()}</td>;
};

View File

@ -1,10 +1,12 @@
import React from 'react';
import {FormItem, FormControlProps, FormBaseControl} from './Item';
import cx from 'classnames';
import {filterDate} from '../../utils/tpl-builtin';
import {filterDate, isPureVariable, resolveVariableAndFilter} from '../../utils/tpl-builtin';
import moment from 'moment';
import 'moment/locale/zh-cn';
import DatePicker from '../../components/DatePicker';
import {SchemaObject} from '../../Schema';
import {createObject, anyChanged} from '../../utils/helper';
export interface InputDateBaseControlSchema extends FormBaseControl {
/**
@ -85,6 +87,24 @@ export interface DateControlSchema extends InputDateBaseControlSchema {
*
*/
maxDate?: string;
/**
*
*/
schedules?: Array<{
startTime: Date,
endTime: Date,
content: any,
className?: string
}> | string;
/**
*
*/
scheduleClassNames?: Array<string>;
/**
*
*/
scheduleAction?: SchemaObject;
}
/**
@ -267,6 +287,12 @@ export interface DateProps extends FormControlProps {
interface DateControlState {
minDate?: moment.Moment;
maxDate?: moment.Moment;
schedules?: Array<{
startTime: Date,
endTime: Date,
content: any,
className?: string
}>;
}
export default class DateControl extends React.PureComponent<
@ -304,9 +330,18 @@ export default class DateControl extends React.PureComponent<
setPrinstineValue((utc ? moment.utc(date) : date).format(format));
}
let schedulesData = props.schedules;
if (typeof schedulesData === 'string') {
const resolved = resolveVariableAndFilter(schedulesData, data, '| raw');
if (Array.isArray(resolved)) {
schedulesData = resolved;
}
}
this.state = {
minDate: minDate ? filterDate(minDate, data, format) : undefined,
maxDate: maxDate ? filterDate(maxDate, data, format) : undefined
maxDate: maxDate ? filterDate(maxDate, data, format) : undefined,
schedules: schedulesData
};
}
@ -334,6 +369,47 @@ export default class DateControl extends React.PureComponent<
: undefined
});
}
if (anyChanged(['schedules', 'data'], prevProps, props)
&& (typeof props.schedules === 'string' && isPureVariable(props.schedules))
) {
const schedulesData = resolveVariableAndFilter(props.schedules, props.data, '| raw');
const preSchedulesData = resolveVariableAndFilter(prevProps.schedules, prevProps.data, '| raw');
if (Array.isArray(schedulesData) && preSchedulesData !== schedulesData) {
this.setState({
schedules: schedulesData
})
}
}
}
// 日程点击事件
onScheduleClick(scheduleData: any) {
const {scheduleAction, onAction, data, translate: __} = this.props;
const defaultscheduleAction = {
actionType: 'dialog',
dialog: {
title: __('Schedule'),
actions: [],
body: {
type: 'table',
columns: [
{
name: 'time',
label: __('Time')
},
{
name: 'content',
label: __('Content')
}
],
data: '${scheduleData}'
}
}
};
onAction && onAction(null, scheduleAction || defaultscheduleAction, createObject(data, scheduleData));
}
render() {
@ -348,6 +424,8 @@ export default class DateControl extends React.PureComponent<
format,
timeFormat,
valueFormat,
largeMode,
render,
...rest
} = this.props;
@ -363,6 +441,9 @@ export default class DateControl extends React.PureComponent<
format={valueFormat || format}
{...this.state}
classnames={cx}
schedules={this.state.schedules}
largeMode={largeMode}
onScheduleClick={this.onScheduleClick.bind(this)}
/>
</div>
);