this.clickCalendarDay(e, date)}
987 | >
988 |
1005 | {(daySlot &&
1006 | daySlot(date, {
1007 | isMarked: !!(
1008 | this.markDateColor(date, "circle") ||
1009 | this.markDateColor(date, "dot")
1010 | ),
1011 | isDisabledDate: this.formatDisabledDate(date),
1012 | isToday: this.isToday(date),
1013 | isChecked: this.isCheckedDay(date),
1014 | isCurrentMonthDay: !this.isNotCurrentMonthDay(date, mIndex),
1015 | isFirstDayOfMonth: this.isFirstDayOfMonth(date, mIndex),
1016 | })) ||
1017 | (this.isFirstDayOfMonth(date, mIndex)
1018 | ? language.MONTH && language.MONTH[date.month]
1019 | : date.day)}
1020 |
1021 |
1025 |
1026 | ));
1027 | };
1028 |
1029 | const calendarGroupNode = (
1030 | ;
93 | type State = { calendarRef?: any } & typeof state;
94 |
95 | class ReactHashCalendar extends React.Component<
96 | DateTimeProps & typeof defaultProps,
97 | State,
98 | {}
99 | > {
100 | static defaultProps = defaultProps;
101 |
102 | public state: State = state;
103 |
104 | componentDidMount() {
105 | const { model, lang, onVisibleChange } = this.props;
106 | if (model === 'inline') {
107 | this.setState({ isShowDatetimePicker: true });
108 | onVisibleChange && onVisibleChange(true);
109 | }
110 |
111 | this.setState({ language: languageUtil[lang] });
112 |
113 | setTimeout(() => {
114 | this.setState({ isShowCalendar: true });
115 | });
116 | }
117 |
118 | componentDidUpdate(prevProps: DateTimeProps) {
119 | const { pickerType } = prevProps;
120 | const { isShowAction, visible } = this.props;
121 | const {
122 | isShowCalendar,
123 | isShowDatetimePicker,
124 | calendarTitleRefHeight,
125 | calendarBodyHeight,
126 | calendarTitleHeight,
127 | calendarContentHeight,
128 | } = this.state;
129 |
130 | if (isShowCalendar && pickerType === 'time') {
131 | this.showTime();
132 | }
133 |
134 | if (visible && !isShowDatetimePicker) {
135 | this.show();
136 | }
137 |
138 | if (
139 | calendarTitleHeight !== calendarTitleRefHeight ||
140 | calendarContentHeight !== calendarTitleRefHeight + calendarBodyHeight
141 | ) {
142 | if (!isShowAction) {
143 | this.setState({ calendarTitleHeight: 0 });
144 | } else {
145 | this.setState({
146 | calendarTitleHeight: calendarTitleRefHeight,
147 | calendarContentHeight: calendarTitleRefHeight + calendarBodyHeight,
148 | });
149 | }
150 | }
151 | }
152 |
153 | // 显示时间选择控件
154 | showTime = () => {
155 | this.setState({ isShowCalendar: false });
156 | };
157 |
158 | showCalendar = () => {
159 | this.setState({ isShowCalendar: true });
160 | };
161 |
162 | show = () => {
163 | const { onVisibleChange } = this.props;
164 | this.setState({ isShowDatetimePicker: true });
165 | this.setState({ isShowCalendar: true });
166 | onVisibleChange && onVisibleChange(true);
167 | };
168 |
169 | close = () => {
170 | const { onVisibleChange } = this.props;
171 | this.setState({ isShowDatetimePicker: false });
172 | onVisibleChange && onVisibleChange(false);
173 | };
174 |
175 | formatDate(time: string, format: string) {
176 | const { lang } = this.props;
177 | return formatDate(time, format, lang);
178 | }
179 |
180 | today = () => {
181 | const { disabledDate } = this.props;
182 | if (disabledDate(new Date())) return;
183 |
184 | const { calendarRef } = this.state;
185 | calendarRef && calendarRef.today();
186 | };
187 |
188 | confirm = () => {
189 | const { format, model, lang, dateConfirmCallback } = this.props;
190 | const { checkedDate } = this.state;
191 | let date: Date | string = new Date(
192 | `${checkedDate.year}/${checkedDate.month + 1}/${checkedDate.day} ${
193 | checkedDate.hours
194 | }:${checkedDate.minutes}`
195 | );
196 | if (format) {
197 | date = formatDate(date, format, lang);
198 | }
199 | dateConfirmCallback && dateConfirmCallback(date);
200 |
201 | if (model === 'dialog') {
202 | this.close();
203 | }
204 | };
205 |
206 | dateChange = (date: IDate) => {
207 | const { checkedDate } = this.state;
208 | this.setState({
209 | checkedDate: {
210 | ...checkedDate,
211 | ...date,
212 | },
213 | });
214 | };
215 |
216 | timeChange = (time: ITime) => {
217 | const { checkedDate } = this.state;
218 | this.setState({
219 | checkedDate: {
220 | ...checkedDate,
221 | ...time,
222 | },
223 | });
224 | };
225 |
226 | heightChange = (height: number) => {
227 | const { firstTimes, calendarTitleHeight } = this.state;
228 | const { model } = this.props;
229 |
230 | if (!firstTimes && model === 'dialog') return;
231 |
232 | this.setState({
233 | calendarBodyHeight: height,
234 | calendarContentHeight: height + calendarTitleHeight,
235 | firstTimes: false,
236 | });
237 | };
238 |
239 | // 小于10,在前面补0
240 | fillNumber = (val: number) => (val > 9 ? val : '0' + val);
241 |
242 | calendarTitleRef = (ref: HTMLDivElement): void => {
243 | if (!ref) return;
244 | const height = ref.offsetHeight;
245 |
246 | this.setState({
247 | calendarTitleRefHeight: height,
248 | });
249 | };
250 |
251 | stopEvent = (e: React.MouseEvent) => {
252 | e.stopPropagation();
253 | };
254 |
255 | onCalendarRef = (ref: any) => {
256 | this.setState({
257 | calendarRef: ref,
258 | });
259 | };
260 |
261 | // 监听手指开始滑动事件
262 | touchStart = (event: React.TouchEvent) => {
263 | const { touchStartCallback } = this.props;
264 | touchStartCallback && touchStartCallback(event);
265 | };
266 |
267 | // 监听手指开始滑动事件
268 | touchMove = (event: React.TouchEvent) => {
269 | const { touchMoveCallback } = this.props;
270 | touchMoveCallback && touchMoveCallback(event);
271 | };
272 |
273 | // 监听手指开始滑动事件
274 | touchEnd = (event: React.TouchEvent) => {
275 | const { touchEndCallback } = this.props;
276 | touchEndCallback && touchEndCallback(event);
277 | };
278 |
279 | // 滑动方向改变
280 | slideChange = (direction: string) => {
281 | const { slideChangeCallback } = this.props;
282 | slideChangeCallback && slideChangeCallback(direction);
283 | };
284 |
285 | dateClick = (date: IDate) => {
286 | const { checkedDate } = this.state;
287 | const { dateClickCallback, format, lang } = this.props;
288 | let _checkedDate = {
289 | ...checkedDate,
290 | ...date,
291 | };
292 |
293 | let fDate: Date | string = new Date(
294 | `${_checkedDate.year}/${_checkedDate.month + 1}/${_checkedDate.day} ${
295 | _checkedDate.hours
296 | }:${_checkedDate.minutes}`
297 | );
298 | if (format) {
299 | fDate = formatDate(fDate, format, lang);
300 | }
301 |
302 | this.setState({
303 | checkedDate: _checkedDate,
304 | });
305 |
306 | dateClickCallback && dateClickCallback(fDate);
307 | };
308 |
309 | render() {
310 | const {
311 | model,
312 | isShowAction,
313 | disabledDate,
314 | showTodayButton,
315 | pickerType,
316 | todaySlot,
317 | actionSlot,
318 | confirmSlot,
319 | defaultDatetime,
320 | } = this.props;
321 |
322 | const {
323 | calendarTitleHeight,
324 | calendarContentHeight,
325 | isShowDatetimePicker,
326 | isShowCalendar,
327 | checkedDate,
328 | language,
329 | } = this.state;
330 | const dateNode: React.ReactNode = (
331 |
337 | {this.formatDate(
338 | `${checkedDate.year}/${checkedDate.month + 1}/${checkedDate.day}`,
339 | language.DEFAULT_DATE_FORMAT
340 | )}
341 |
342 | );
343 |
344 | const timeNode: React.ReactNode = (
345 |
351 | {this.formatDate(
352 | `${checkedDate.year}/${checkedDate.month + 1}/${
353 | checkedDate.day
354 | } ${this.fillNumber(checkedDate.hours)}:${this.fillNumber(
355 | checkedDate.minutes
356 | )}`,
357 | language.DEFAULT_TIME_FORMAT
358 | )}
359 |
360 | );
361 |
362 | const actionNode: React.ReactNode = actionSlot || (
363 |
364 |
365 | {pickerType !== 'time' ? dateNode : ''}
366 | {pickerType !== 'date' ? timeNode : ''}
367 |
368 | {showTodayButton ? (
369 |
375 | {todaySlot || language.TODAY}
376 |
377 | ) : null}
378 | {model === 'dialog' ? (
379 |
380 | {confirmSlot || language.CONFIRM}
381 |
382 | ) : null}
383 |
384 | );
385 |
386 | return isShowDatetimePicker ? (
387 |
396 |
401 | {isShowAction ? actionNode : null}
402 |
416 | {pickerType !== 'date' ? (
417 |
423 | ) : null}
424 |
425 |
426 | ) : null;
427 | }
428 | }
429 |
430 | export default ReactHashCalendar;
431 |
--------------------------------------------------------------------------------
/src/components/datetimePicker/style.styl:
--------------------------------------------------------------------------------
1 | @import '../../style/common.styl';
2 |
3 | .hash-calendar {
4 | position: fixed;
5 | width: 100vw;
6 | height: 100vh;
7 | top: 0;
8 | left: 0;
9 | background: rgba(0, 0, 0, 0.6);
10 | z-index: 999;
11 | &.calendar_inline {
12 | position: relative;
13 | width: 100%;
14 | height: auto;
15 | background: none;
16 | height: px2vw(710px);
17 | z-index: 1;
18 | }
19 | .calendar_content {
20 | position: absolute;
21 | width: 100%;
22 | left: 0;
23 | bottom: 0;
24 | display: flex;
25 | padding-bottom: px2vw(26px);
26 | flex-wrap: wrap;
27 | background: white;
28 | height: px2vw(710px);
29 | overflow: hidden;
30 | }
31 | .calendar_title {
32 | position: absolute;
33 | width: 100%;
34 | left: 0;
35 | top: 0;
36 | background: bg-color;
37 | borderBottom();
38 | display: flex;
39 | align-items: center;
40 | justify-content: space-between;
41 | z-index: 1;
42 | }
43 | .calendar_title_date {
44 | color: vice-font-color;
45 | background: white;
46 | padding: px2vw(30px) px2vw(15px);
47 | }
48 | .calendar_title_date_active {
49 | color: main-font-color;
50 | font-weight: bold;
51 | }
52 | .calendar_title_date_time {
53 | margin-left: px2vw(20px);
54 | }
55 | .calendar_confirm {
56 | color: main-color;
57 | margin-right: px2vw(34px);
58 | }
59 | .today_disable {
60 | color: disabled-font-color;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as ReactHashCalendar } from './datetimePicker';
2 | export { default as Calendar } from './calendar';
3 | export { default as TimePicker } from './timePicker';
--------------------------------------------------------------------------------
/src/components/timePicker/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './style.styl';
3 | import classNames from 'classnames';
4 | import { checkPlatform } from '../../utils/util';
5 | import { ITime } from '../../utils/type';
6 |
7 | const defaultProps = {
8 | show: false,
9 | defaultTime: new Date(),
10 | minuteStep: 1,
11 | };
12 |
13 | export type TimePickerProps = {
14 | timeChangeCallback?: (date: ITime) => void;
15 | } & Partial;
16 |
17 | const state = {
18 | hashID: [''],
19 | hashClass: '',
20 | timeRange: [''],
21 | timeOptions: {
22 | minHours: 24,
23 | minMinutes: 59,
24 | maxHours: 0,
25 | maxMinutes: 0,
26 | },
27 | checkedDate: {
28 | hours: new Date().getHours(),
29 | minutes: new Date().getMinutes(),
30 | },
31 | timeHeight: 0,
32 | timeStartY: 0,
33 | timeStartUp: 0,
34 | };
35 |
36 | type State = {
37 | timeArray?: number[][];
38 | } & typeof state;
39 |
40 | class TimePicker extends React.Component<
41 | TimePickerProps & typeof defaultProps,
42 | State,
43 | {}
44 | > {
45 | static defaultProps = defaultProps;
46 | public state: State = state;
47 |
48 | componentDidMount() {
49 | const { defaultTime, timeChangeCallback } = this.props;
50 | const { checkedDate } = this.state;
51 |
52 | this.setState({
53 | hashID: [
54 | `time${Math.floor(Math.random() * 1000000)}`,
55 | `time${Math.floor(Math.random() * 1000000)}`,
56 | ],
57 | hashClass: `time_item_${Math.floor(Math.random() * 1000000)}`,
58 | });
59 |
60 | if (defaultTime) {
61 | let _checkedDate: ITime = {
62 | ...checkedDate,
63 | hours: defaultTime.getHours(),
64 | minutes: defaultTime.getMinutes(),
65 | };
66 | this.setState({ checkedDate: _checkedDate });
67 |
68 | timeChangeCallback && timeChangeCallback(_checkedDate);
69 | }
70 | }
71 |
72 | componentDidUpdate(
73 | prevProps: TimePickerProps & typeof defaultProps,
74 | prevState: State
75 | ) {
76 | const { show: showPrev } = prevProps;
77 | const { show } = this.props;
78 |
79 | if (show !== showPrev && show) {
80 | setTimeout(() => {
81 | this.initTimeArray();
82 | });
83 | }
84 | }
85 |
86 | initTimeArray = () => {
87 | const { minuteStep } = this.props;
88 | const { checkedDate, hashClass, hashID } = this.state;
89 |
90 | let hours: number[] = [];
91 | let timeArray: number[][] = [];
92 | for (let i = 0; i < 24; i++) {
93 | hours.push(i);
94 | }
95 | let minutes: number[] = [];
96 | for (let i = 0; i < 60; i++) {
97 | if (i % minuteStep === 0) {
98 | minutes.push(i);
99 | }
100 | }
101 | timeArray.push(hours, minutes);
102 |
103 | this.setState({ timeArray });
104 |
105 | let checkHours = checkedDate.hours;
106 | let checkMinutes = checkedDate.minutes;
107 |
108 | let timeEle = document.querySelector(`.${hashClass}`);
109 | if (!timeEle) return;
110 |
111 | let _timeHeight: string = getComputedStyle(timeEle).height || '';
112 | let timeHeight = parseFloat(_timeHeight.split('px')[0]);
113 |
114 | this.setState({ timeHeight });
115 |
116 | let hoursUp = (2 - checkHours) * timeHeight;
117 | let hourEle = document.querySelector(`#${hashID[0]}`) as HTMLElement;
118 | let minuteEle = document.querySelector(`#${hashID[1]}`) as HTMLElement;
119 |
120 | if (!hourEle || !minuteEle) return;
121 |
122 | let minutesUp = (2 - checkMinutes / minuteStep) * timeHeight;
123 | hourEle.style.webkitTransform = 'translate3d(0px,' + hoursUp + 'px,0px)';
124 | minuteEle.style.webkitTransform =
125 | 'translate3d(0px,' + minutesUp + 'px,0px)';
126 | };
127 |
128 | timeTouchStart = (e: React.TouchEvent) => {
129 | e.preventDefault();
130 | let timeStartY = e.changedTouches[0].pageY;
131 | this.setState({ timeStartY });
132 |
133 | let eventEl = e.currentTarget as HTMLElement;
134 | let transform = eventEl.style.webkitTransform;
135 | if (transform) {
136 | let timeStartUp = parseFloat(transform.split(' ')[1].split('px')[0]);
137 | this.setState({ timeStartUp });
138 | }
139 | };
140 |
141 | timeTouchMove = (e: React.TouchEvent, index: number) => {
142 | const { timeStartY, timeStartUp } = this.state;
143 |
144 | let moveEndY = e.changedTouches[0].pageY;
145 | let Y = moveEndY - timeStartY;
146 |
147 | let eventEl = e.currentTarget as HTMLElement;
148 | eventEl.style.webkitTransform =
149 | 'translate3d(0px,' + (Y + timeStartUp) + 'px,0px)';
150 |
151 | if (checkPlatform() === '2') {
152 | this.timeTouchEnd(e, index);
153 | return false;
154 | }
155 | };
156 |
157 | timeTouchEnd = (e: React.TouchEvent, index: number) => {
158 | const { minuteStep, timeChangeCallback } = this.props;
159 | const { checkedDate, timeStartUp, timeHeight, timeArray } = this.state;
160 |
161 | let eventEl = e.currentTarget as HTMLElement;
162 | let transform = eventEl.style.webkitTransform;
163 | let endUp = timeStartUp;
164 | if (transform) {
165 | endUp = parseFloat(
166 | eventEl.style.webkitTransform.split(' ')[1].split('px')[0]
167 | );
168 | }
169 |
170 | let distance = Math.abs(endUp - timeStartUp);
171 | let upCount = Math.floor(distance / timeHeight) || 1;
172 | let halfWinWith = timeHeight / 2;
173 | let up = timeStartUp;
174 |
175 | if (endUp <= timeStartUp) {
176 | // 向上滑动 未过临界值
177 | if (distance <= halfWinWith) {
178 | up = timeStartUp;
179 | } else {
180 | up = timeStartUp - timeHeight * upCount;
181 |
182 | if (timeArray && up < -(timeArray[index].length - 3) * timeHeight) {
183 | up = -(timeArray[index].length - 3) * timeHeight;
184 | }
185 | }
186 | } else {
187 | // 向下滑动 未过临界值
188 | if (distance <= halfWinWith) {
189 | up = timeStartUp;
190 | } else {
191 | up = timeStartUp + timeHeight * upCount;
192 | if (up > timeHeight * 2) {
193 | up = timeHeight * 2;
194 | }
195 | }
196 | }
197 |
198 | let _checkedDate: ITime;
199 | if (index === 0) {
200 | let hours = 2 - Math.round(up / timeHeight);
201 | _checkedDate = { ...checkedDate, hours };
202 | } else {
203 | let minute = 2 - Math.round(up / timeHeight);
204 | _checkedDate = { ...checkedDate, minutes: minute * minuteStep };
205 | }
206 | this.setState({ checkedDate: _checkedDate });
207 | timeChangeCallback && timeChangeCallback(_checkedDate);
208 |
209 | eventEl.style.webkitTransition = 'transform 300ms';
210 | eventEl.style.webkitTransform = 'translate3d(0px,' + up + 'px,0px)';
211 | };
212 |
213 | isBeSelectedTime = (time: number, index: number) => {
214 | // 是否为当前选中的时间
215 | const { checkedDate } = this.state;
216 |
217 | return (
218 | (index === 0 && time === checkedDate.hours) ||
219 | (index === 1 && time === checkedDate.minutes)
220 | );
221 | };
222 |
223 | // 小于10,在前面补0
224 | fillNumber = (val: number) => {
225 | return val > 9 ? val : '0' + val;
226 | };
227 |
228 | render() {
229 | const { show } = this.props;
230 | const { timeArray, hashID, hashClass } = this.state;
231 |
232 | const timeItemNode = (
233 | timeArr: number[],
234 | parentIndex: number
235 | ): React.ReactNode => {
236 | return timeArr.map((time: number, index: number) => (
237 |
245 | {this.fillNumber(time)}
246 |
247 | ));
248 | };
249 |
250 | const timeContentNode = (timeArray?: number[][]): React.ReactNode => {
251 | return (
252 | timeArray &&
253 | timeArray.map((item: number[], index: number) => (
254 | {
260 | this.timeTouchMove(event, index);
261 | }}
262 | onTouchEnd={(event) => {
263 | this.timeTouchEnd(event, index);
264 | }}
265 | >
266 | {timeItemNode(item, index)}
267 |
268 | ))
269 | );
270 | };
271 |
272 | return show ? (
273 |
274 |
{timeContentNode(timeArray)}
275 |
276 | ) : null;
277 | }
278 | }
279 |
280 | export default TimePicker;
281 |
--------------------------------------------------------------------------------
/src/components/timePicker/style.styl:
--------------------------------------------------------------------------------
1 | @import '../../style/common.styl';
2 |
3 | .hash-calendar {
4 | .time_body {
5 | width: 100%;
6 | margin-top: px2vw(100px);
7 | }
8 | .time_group {
9 | width: 100%;
10 | display: flex;
11 | align-items: flex-start;
12 | justify-content: center;
13 | height: px2vw(360px);
14 | margin-top: px2vw(100px);
15 | -webkit-overflow-scrolling: touch;
16 | overflow: hidden;
17 | }
18 | .time_content {
19 | touch-action: none;
20 | padding: 0 px2vw(40px);
21 | -webkit-overflow-scrolling: touch;
22 | }
23 | .time_item {
24 | padding: px2vw(20px) 0;
25 | color: vice-font-color;
26 | }
27 | .time_item_show {
28 | color: main-font-color;
29 | }
30 | .time_disabled {
31 | color: red;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/examples/index.tsx:
--------------------------------------------------------------------------------
1 | // import { ReactHashCalendar } from '../components';
2 | import React from 'react';
3 |
4 | const ReactHashCalendar = require('../components').ReactHashCalendar;
5 |
6 | const state = {
7 | defaultDatetime: new Date(),
8 | isShowCalendar: false,
9 | markDate: [
10 | '2020/11/24',
11 | '2020/11/22',
12 | {
13 | color: 'red',
14 | type: 'dot',
15 | date: [
16 | '0',
17 | '2020/02/25',
18 | '2020/03/25',
19 | '2020/04/01',
20 | '2020/05/25',
21 | '2020/06/25',
22 | '2020/07/25',
23 | '2020/08/25',
24 | '2020/09/25',
25 | '2020/10/25',
26 | '2020/11/25',
27 | '2020/12/25',
28 | ],
29 | },
30 | {
31 | color: 'blue',
32 | type: 'circle',
33 | date: [
34 | '2020/01/20',
35 | '2020/02/20',
36 | '2020/03/20',
37 | '2020/04/20',
38 | '2020/05/20',
39 | '2020/06/20',
40 | '2020/07/20',
41 | '2020/08/20',
42 | '2020/09/20',
43 | '2020/10/20',
44 | '2020/11/20',
45 | '2020/12/20',
46 | ],
47 | },
48 | {
49 | color: 'pink',
50 | date: [
51 | '2020/01/12',
52 | '2020/02/12',
53 | '2020/03/12',
54 | '2020/04/12',
55 | '2020/05/12',
56 | '2020/06/12',
57 | '2020/07/12',
58 | '2020/08/12',
59 | '2020/09/12',
60 | '2020/10/12',
61 | '2020/11/12',
62 | '2020/12/12',
63 | ],
64 | },
65 | {
66 | color: '#000000',
67 | date: [
68 | '2020/01/29',
69 | '2020/02/29',
70 | '2020/03/29',
71 | '2020/04/29',
72 | '2020/05/29',
73 | '2020/06/29',
74 | '2020/07/29',
75 | '2020/08/29',
76 | '2020/09/29',
77 | '2020/10/29',
78 | '2020/11/29',
79 | '2020/12/29',
80 | ],
81 | },
82 | ],
83 | };
84 |
85 | type State = typeof state;
86 |
87 | class Examples extends React.Component<{}, State, {}> {
88 | public state: State = state;
89 |
90 | handleVisibleChange = (isShowCalendar: boolean) => {
91 | this.setState({ isShowCalendar });
92 | };
93 |
94 | showCalendar = () => {
95 | this.setState({ isShowCalendar: true });
96 | };
97 |
98 | dateClick = (date?: string | Date) => {
99 | console.log('Examples -> dateClick -> date', date);
100 | };
101 |
102 | dateConfirm = (date?: string | Date) => {
103 | console.log('Examples -> dateConfirm -> date', date);
104 | };
105 |
106 | disabledDate = (date: Date): boolean => {
107 | let timestamp = date.getTime();
108 | let oneDay = 24 * 60 * 60 * 1000;
109 |
110 | if (timestamp < new Date().getTime() - oneDay) {
111 | return true;
112 | }
113 | return false;
114 | };
115 |
116 | render() {
117 | const { isShowCalendar, markDate, defaultDatetime } = this.state;
118 | return (
119 |
120 |
121 |
143 |
144 | );
145 | }
146 | }
147 |
148 | export default Examples;
149 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Examples from './examples/index';
4 | import './style/reset.css';
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById('root')
11 | );
12 |
--------------------------------------------------------------------------------
/src/language/cn.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Description:中文
3 | * @Author: TSY
4 | * @Date: 2020-09-08 23:12:02
5 | * @LastEditTime: 2020-09-09 22:22:01
6 | */
7 |
8 | export default {
9 | CONFIRM: '确定',
10 | TODAY: '今天',
11 | WEEK: ['日', '一', '二', '三', '四', '五', '六'],
12 | MONTH: [
13 | '1月',
14 | '2月',
15 | '3月',
16 | '4月',
17 | '5月',
18 | '6月',
19 | '7月',
20 | '8月',
21 | '9月',
22 | '10月',
23 | '11月',
24 | '12月',
25 | ],
26 | DEFAULT_DATE_FORMAT: 'YY年MM月DD日',
27 | DEFAULT_TIME_FORMAT: 'hh:mm',
28 | };
29 |
--------------------------------------------------------------------------------
/src/language/en.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @Description: 英文
3 | * @Author: TSY
4 | * @CreateDate: 2020/3/22 21:59
5 | */
6 |
7 | export default {
8 | CONFIRM: 'CONFIRM',
9 | TODAY: 'TODAY',
10 | WEEK: ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'],
11 | MONTH: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec'],
12 | DEFAULT_DATE_FORMAT: 'MM DD,YY',
13 | DEFAULT_TIME_FORMAT: 'at hh:mm F'
14 | }
15 |
--------------------------------------------------------------------------------
/src/language/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @Description: 统一导出所有语言文件
3 | * @Author: TSY
4 | * @CreateDate: 2020/3/22 22:01
5 | */
6 |
7 | import CN from './cn';
8 | import EN from './en';
9 |
10 | export default {
11 | CN,
12 | EN,
13 | };
14 |
--------------------------------------------------------------------------------
/src/style/common.css:
--------------------------------------------------------------------------------
1 | /**
2 | * @Description:
3 | * @Author: TSY
4 | * @CreateDate: 2018/6/9 13:28
5 | */
6 | .click_item:active {
7 | background: #eee;
8 | }
9 | .mask {
10 | position: fixed;
11 | top: 0;
12 | left: 0;
13 | width: 100%;
14 | height: 100%;
15 | background: rgba(0,0,0,0.5);
16 | z-index: 999;
17 | }
18 | .pulldown-wrapper {
19 | top: -150px;
20 | }
21 | .iconfont {
22 | font-size: 34px;
23 | font-size: 4.533333333333333vw;
24 | }
25 |
--------------------------------------------------------------------------------
/src/style/common.styl:
--------------------------------------------------------------------------------
1 | /**
2 | * @Description:
3 | * @Author: TSY
4 | * @CreateDate: 2018/6/9 13:28
5 | */
6 |
7 | //设计稿尺寸
8 | designSize = 750px
9 | // 页面主色
10 | main-color = #1c71fb
11 | //页面背景色
12 | bg-color = #f4f4f4
13 | //主文字颜色
14 | main-font-color = #4c4c4c
15 | //副文字颜色
16 | vice-font-color = #898989
17 | //禁用背景颜色
18 | disabled-bg-color = #f5f7fa
19 | //禁用文字颜色
20 | disabled-font-color = #c0c4cc
21 | //绿色
22 | green-color = #00cb69
23 | //红色
24 | red-color = #f80f30
25 | //橙色
26 | orange-color = #FF7800
27 | // 1px 实线边框
28 | solidBorder(color = bg-color) {
29 | border 1px solid color
30 | }
31 |
32 | borderBottom(color = bg-color) {
33 | border-bottom 1px solid color
34 | }
35 |
36 | borderTop(color = bg-color) {
37 | border-top 1px solid color
38 | }
39 |
40 | borderLeft(color = bg-color) {
41 | border-left 1px solid color
42 | }
43 |
44 | borderRight(color = bg-color) {
45 | border-right 1px solid color
46 | }
47 |
48 | bottomLine(color = bg-color) {
49 | border-bottom px2vw(16px) solid color
50 | }
51 |
52 | topLine(color = bg-color) {
53 | border-top px2vw(16px) solid color
54 | }
55 |
56 | //浏览器私有前缀属性扩展
57 | vendor(prop, args) {
58 | -webkit-{prop} args
59 | -moz-{prop} args
60 | {prop} args
61 | }
62 |
63 | //文字溢出显示省略号
64 | textOverflow() {
65 | overflow hidden
66 | text-overflow ellipsis
67 | white-space nowrap
68 | }
69 |
70 | //px转vw
71 | px2vw(size) {
72 | return (size /designSize * 100) vw
73 | }
74 |
75 | fontSize(size, isMoblie = true)
76 | if isMobile
77 | font-size size
78 | font-size px2vw(size)
79 | else
80 | font-size size
81 |
82 | paddingAround() {
83 | padding px2vw(30px) px2vw(34px)
84 | }
85 |
86 | paddingSmall() {
87 | padding px2vw(16px) px2vw(34px)
88 | }
89 |
90 | fixedTop() {
91 | position: fixed
92 | width 100%
93 | top 0
94 | left 0
95 | z-index 2
96 | }
97 |
98 | fixedBottom() {
99 | position: fixed
100 | width 100%
101 | bottom 0
102 | left 0
103 | z-index 2
104 | }
105 |
106 | flexAlign(align = center) {
107 | display flex
108 | align-items align
109 | }
110 |
111 | flexContent(align = center, justify = center) {
112 | display flex
113 | align-items align
114 | justify-content justify
115 | }
116 |
117 | flexBetween(align = center) {
118 | display flex
119 | align-items align
120 | justify-content space-between
121 | }
122 |
123 | flexAround(align = center) {
124 | display flex
125 | align-items align
126 | justify-content space-around
127 | }
128 |
129 | .click_item:active {
130 | background: #eee;
131 | }
132 |
133 | .mask {
134 | position fixed
135 | top 0
136 | left 0
137 | width 100%
138 | height 100%
139 | background rgba(0,0,0,0.5)
140 | z-index 999
141 | }
142 |
143 | .pulldown-wrapper {
144 | top: -150px;
145 | }
146 |
147 | .iconfont {
148 | fontSize(34px)
149 | }
--------------------------------------------------------------------------------
/src/style/reset.css:
--------------------------------------------------------------------------------
1 | /**
2 | * @Description:
3 | * @Author: TSY
4 | * @CreateDate: 2018/6/9 13:28
5 | */
6 | html,
7 | body {
8 | width: 100%;
9 | border: 0;
10 | font-family: 'Helvetica-Neue', 'Helvetica', Arial, sans-serif;
11 | margin: 0;
12 | padding: 0;
13 | overflow-x: hidden;
14 | font-size: 4vw;
15 | background: #fff;
16 | color: #191919;
17 | }
18 | div,
19 | span,
20 | object,
21 | iframe,
22 | img,
23 | table,
24 | caption,
25 | thead,
26 | tbody,
27 | tfoot,
28 | tr,
29 | td,
30 | article,
31 | aside,
32 | canvas,
33 | details,
34 | figure,
35 | hgroup,
36 | menu,
37 | nav,
38 | footer,
39 | header,
40 | section,
41 | summary,
42 | mark,
43 | audio,
44 | video {
45 | border: 0;
46 | margin: 0;
47 | padding: 0;
48 | }
49 | h1,
50 | h2,
51 | h3,
52 | h4,
53 | h5,
54 | h6,
55 | p,
56 | blockquote,
57 | pre,
58 | a,
59 | abbr,
60 | address,
61 | cit,
62 | code,
63 | del,
64 | dfn,
65 | em,
66 | ins,
67 | q,
68 | samp,
69 | small,
70 | strong,
71 | sub,
72 | sup,
73 | b,
74 | i,
75 | hr,
76 | dl,
77 | dt,
78 | dd,
79 | ol,
80 | ul,
81 | li,
82 | fieldset,
83 | legend,
84 | label {
85 | border: 0;
86 | vertical-align: baseline;
87 | margin: 0;
88 | padding: 0;
89 | }
90 | article,
91 | aside,
92 | canvas,
93 | figure,
94 | figure img,
95 | figcaption,
96 | hgroup,
97 | footer,
98 | header,
99 | nav,
100 | section,
101 | audio,
102 | video {
103 | display: inline-block;
104 | }
105 | table {
106 | border-collapse: separate;
107 | border-spacing: 0;
108 | }
109 | table caption,
110 | table th,
111 | table td {
112 | text-align: left;
113 | vertical-align: middle;
114 | }
115 | li {
116 | list-style: none;
117 | }
118 | a {
119 | text-decoration: none;
120 | outline: none;
121 | display: inline-block;
122 | }
123 | a img {
124 | border: 0;
125 | }
126 | img {
127 | width: 100%;
128 | }
129 | :focus {
130 | outline: 0;
131 | }
132 | * {
133 | box-sizing: border-box;
134 | }
135 | textarea {
136 | resize: none;
137 | appearance: none;
138 | font-size: 4vw;
139 | }
140 | input {
141 | appearance: none;
142 | -webkit-appearance: none;
143 | font-size: 4vw;
144 | }
145 | select {
146 | appearance: none;
147 | background-color: #fff;
148 | font-size: 4vw;
149 | }
150 | button {
151 | border: none;
152 | font-size: 4vw;
153 | }
154 | p,
155 | span {
156 | letter-spacing: 1px;
157 | }
158 | .mescroll-totop-self {
159 | bottom: 50px !important;
160 | }
161 | .mint-indicator-wrapper {
162 | z-index: 99;
163 | }
164 |
--------------------------------------------------------------------------------
/src/style/reset.styl:
--------------------------------------------------------------------------------
1 | /**
2 | * @Description:
3 | * @Author: TSY
4 | * @CreateDate: 2018/6/9 13:28
5 | */
6 | html, body {
7 | .hash-calendar {
8 | width: 100%;
9 | border: 0;
10 | font-family: "Helvetica-Neue", "Helvetica", Arial, sans-serif;
11 | margin: 0;
12 | padding: 0;
13 | overflow-x: hidden;
14 | font-size: 4vw;
15 | background: #fff;
16 | color: #191919
17 | }
18 | }
19 |
20 | .hash-calendar {
21 | div, span, object, iframe, img, table, caption, thead, tbody,
22 | tfoot, tr, tr, td, article, aside, canvas, details, figure, hgroup, menu,
23 | nav, footer, header, section, summary, mark, audio, video {
24 | border: 0;
25 | margin: 0;
26 | padding: 0;
27 | }
28 |
29 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, address, cit, code,
30 | del, dfn, em, ins, q, samp, small, strong, sub, sup, b, i, hr, dl, dt, dd,
31 | ol, ul, li, fieldset, legend, label {
32 | border: 0;
33 | vertical-align: baseline;
34 | margin: 0;
35 | padding: 0;
36 | }
37 |
38 | li {
39 | list-style: none;
40 | }
41 |
42 | * {
43 | box-sizing: border-box;
44 |
45 | &:focus {
46 | outline: 0;
47 | }
48 | }
49 |
50 | button {
51 | border: none;
52 | font-size: 4vw;
53 | }
54 |
55 | p, span {
56 | letter-spacing: 1px;
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/src/utils/constant.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Description:
3 | * @Author: TSY
4 | * @Date: 2020-10-28 23:32:59
5 | * @LastEditTime: 2020-10-28 23:35:13
6 | */
7 | import { tuple } from './type';
8 |
9 | export const SCROLL_DIRECTION_LIST = tuple('left', 'right', 'up', 'down');
10 |
11 | export const WEEK_LIST = tuple(
12 | 'Sunday',
13 | 'Monday',
14 | 'Tuesday',
15 | 'Wednesday',
16 | 'Thursday',
17 | 'Friday',
18 | 'Saturday'
19 | );
20 |
21 | export const DIRECTION_LIST = tuple(
22 | '',
23 | 'all',
24 | 'left',
25 | 'right',
26 | 'up',
27 | 'down',
28 | 'horizontal',
29 | 'vertical'
30 | );
31 |
--------------------------------------------------------------------------------
/src/utils/eq.ts:
--------------------------------------------------------------------------------
1 | const toString = Object.prototype.toString;
2 |
3 | function isFunction(obj: any) {
4 | return toString.call(obj) === '[object Function]';
5 | }
6 |
7 | export const eq = (a: any, b: any, aStack?: any, bStack?: any): boolean => {
8 | // === 结果为 true 的区别出 +0 和 -0
9 | if (a === b) return a !== 0 || 1 / a === 1 / b;
10 |
11 | // typeof null 的结果为 object ,这里做判断,是为了让有 null 的情况尽早退出函数
12 | if (a == null || b == null) return false;
13 |
14 | // 判断 NaN
15 | // eslint-disable-next-line
16 | if (a !== a) return b !== b;
17 |
18 | // 判断参数 a 类型,如果是基本类型,在这里可以直接返回 false
19 | var type = typeof a;
20 | if (type !== 'function' && type !== 'object' && typeof b != 'object')
21 | return false;
22 |
23 | // 更复杂的对象使用 deepEq 函数进行深度比较
24 | return deepEq(a, b, aStack, bStack);
25 | };
26 |
27 | function deepEq(a: any, b: any, aStack: any, bStack: any) {
28 | // a 和 b 的内部属性 [[class]] 相同时 返回 true
29 | var className = toString.call(a);
30 | if (className !== toString.call(b)) return false;
31 |
32 | switch (className) {
33 | case '[object RegExp]':
34 | case '[object String]':
35 | return '' + a === '' + b;
36 | case '[object Number]':
37 | // eslint-disable-next-line
38 | if (+a !== +a) return +b !== +b;
39 | return +a === 0 ? 1 / +a === 1 / b : +a === +b;
40 | case '[object Date]':
41 | case '[object Boolean]':
42 | return +a === +b;
43 | }
44 |
45 | var areArrays = className === '[object Array]';
46 | // 不是数组
47 | if (!areArrays) {
48 | // 过滤掉两个函数的情况
49 | if (typeof a != 'object' || typeof b != 'object') return false;
50 |
51 | var aCtor = a.constructor,
52 | bCtor = b.constructor;
53 | // aCtor 和 bCtor 必须都存在并且都不是 Object 构造函数的情况下,aCtor 不等于 bCtor, 那这两个对象就真的不相等啦
54 | if (
55 | aCtor === bCtor &&
56 | !(
57 | isFunction(aCtor) &&
58 | aCtor instanceof aCtor &&
59 | isFunction(bCtor) &&
60 | bCtor instanceof bCtor
61 | ) &&
62 | 'constructor' in a &&
63 | 'constructor' in b
64 | ) {
65 | return false;
66 | }
67 | }
68 |
69 | aStack = aStack || [];
70 | bStack = bStack || [];
71 | var length = aStack.length;
72 |
73 | // 检查是否有循环引用的部分
74 | while (length--) {
75 | if (aStack[length] === a) {
76 | return bStack[length] === b;
77 | }
78 | }
79 |
80 | aStack.push(a);
81 | bStack.push(b);
82 |
83 | // 数组判断
84 | if (areArrays) {
85 | length = a.length;
86 | if (length !== b.length) return false;
87 |
88 | while (length--) {
89 | if (!eq(a[length], b[length], aStack, bStack)) return false;
90 | }
91 | }
92 | // 对象判断
93 | else {
94 | var keys = Object.keys(a),
95 | key;
96 | length = keys.length;
97 |
98 | if (Object.keys(b).length !== length) return false;
99 | while (length--) {
100 | key = keys[length];
101 | if (!(b.hasOwnProperty(key) && eq(a[key], b[key], aStack, bStack)))
102 | return false;
103 | }
104 | }
105 |
106 | aStack.pop();
107 | bStack.pop();
108 | return true;
109 | }
110 |
--------------------------------------------------------------------------------
/src/utils/type.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Description:
3 | * @Author: TSY
4 | * @Date: 2020-09-18 22:15:06
5 | * @LastEditTime: 2020-10-10 22:58:29
6 | */
7 | export type Omit = Pick>;
8 | // https://stackoverflow.com/questions/46176165/ways-to-get-string-literal-type-of-array-values-without-enum-overhead
9 | export const tuple = (...args: T) => args;
10 |
11 | export const tupleNum = (...args: T) => args;
12 |
13 | /**
14 | * https://stackoverflow.com/a/59187769
15 | * Extract the type of an element of an array/tuple without performing indexing
16 | */
17 | export type ElementOf = T extends (infer E)[]
18 | ? E
19 | : T extends readonly (infer E)[]
20 | ? E
21 | : never;
22 |
23 | /**
24 | * https://github.com/Microsoft/TypeScript/issues/29729
25 | */
26 | export type LiteralUnion = T | (U & {});
27 |
28 | export interface IDate {
29 | year: number;
30 | month: number;
31 | day: number;
32 | }
33 |
34 | export interface ITime {
35 | hours: number;
36 | minutes: number;
37 | }
38 |
--------------------------------------------------------------------------------
/src/utils/util.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Description: 各种工具类
3 | * @Author: TSY
4 | * @Date: 2020-09-08 23:13:30
5 | * @LastEditTime: 2020-10-10 22:18:15
6 | */
7 |
8 | /**
9 | * 判断安卓与IOS平台
10 | * @returns {string}
11 | */
12 | export const checkPlatform = function () {
13 | if (/android/i.test(navigator.userAgent)) {
14 | return '1';
15 | }
16 | if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) {
17 | return '2';
18 | }
19 | };
20 |
21 | /**
22 | * 日期格式化
23 | * @param time {string}
24 | * @param format {string}
25 | * @returns {string}
26 | */
27 | export let formatDate = (
28 | time: string | Date,
29 | format?: string,
30 | lang = 'CN'
31 | ): string => {
32 | lang = lang.toUpperCase();
33 | let language = require('../language').default[lang] || {};
34 | format =
35 | format || `${language.DEFAULT_DATE_FORMAT} ${language.DEFAULT_TIME_FORMAT}`;
36 | let date = time ? new Date(time) : new Date();
37 | let year = date.getFullYear();
38 | let month = date.getMonth() + 1; // 月份是从0开始的
39 | let day = date.getDate();
40 | let hour = date.getHours();
41 | let min = date.getMinutes();
42 | let sec = date.getSeconds();
43 | let preArr = Array.apply(null, Array(10)).map(function (elem, index) {
44 | return '0' + index;
45 | }); /// /开个长度为10的数组 格式为 00 01 02 03
46 |
47 | let newTime = format
48 | .replace(/YY/g, year + '')
49 | .replace(/F/g, hour >= 12 ? 'pm' : 'am')
50 | .replace(/ss/g, preArr[sec] || sec + '')
51 | .replace(/mm/g, preArr[min] || min + '')
52 | .replace(
53 | /hh/g,
54 | hour > 12 && format.includes('F')
55 | ? hour - 12 + ''
56 | : format.includes('F')
57 | ? hour + ''
58 | : preArr[hour] || hour + ''
59 | )
60 | .replace(/DD/g, preArr[day] || day + '')
61 | .replace(
62 | /MM/g,
63 | lang === 'EN' ? language.MONTH[month - 1] : preArr[month] || month
64 | );
65 |
66 | return newTime;
67 | };
68 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | "target": "es5",
5 | "lib": [
6 | "dom",
7 | "dom.iterable",
8 | "esnext"
9 | ],
10 | "allowJs": false,
11 | "skipLibCheck": true,
12 | "esModuleInterop": true,
13 | "allowSyntheticDefaultImports": true,
14 | "strict": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react",
22 | "sourceMap": true,
23 | "noImplicitAny": false,
24 | },
25 | "include": [
26 | "src"
27 | ]
28 | }
--------------------------------------------------------------------------------
/type.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'ReactHashCalendar';
2 | declare module 'Calendar';
3 | declare module 'TimePicker';
4 |
--------------------------------------------------------------------------------