u-cascader.vue
29.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
<template>
<u-popup
:show="show"
mode="bottom"
:overlay="overlay"
:duration="duration"
:overlayStyle="overlayStyle"
:overlayOpacity="overlayOpacity"
:zIndex="zIndex"
:safeAreaInsetBottom="safeAreaInsetBottom"
:safeAreaInsetTop="safeAreaInsetTop"
:bgColor="bgColor"
:closeable="!pageInline"
@close="close"
@closed="$emit('closed')"
:round="round"
:pageInline="pageInline"
:closeOnClickOverlay="closeOnClickOverlay"
>
<view class="u-calendar">
<uHeader
:title="title"
:subtitle="subtitle"
:showSubtitle="showSubtitle"
:showTitle="showTitle"
:weekText="weekText"
:showSwitch="monthSwitch"
:showToday="showToday"
:todayText="todayText"
:todayDisabled="todayDisabled"
:prevDisabled="switchPrevDisabled"
:nextDisabled="switchNextDisabled"
:prevYearDisabled="switchPrevYearDisabled"
:nextYearDisabled="switchNextYearDisabled"
@prev="prevMonth"
@next="nextMonth"
@prevYear="prevYear"
@nextYear="nextYear"
@today="jumpToToday"
></uHeader>
<view v-if="showTimePanel" class="u-calendar__time-panel">
<view v-if="mode === 'single'" class="u-calendar__time-row">
<text class="u-calendar__time-date">{{ singleDateLabel }}</text>
<view class="u-calendar__time-trigger" @click="openTimePicker('single')">
<text class="u-calendar__time-text">{{ singleTime }}</text>
</view>
</view>
<view v-else-if="mode === 'range' && rangeResultMode === 'boundary'">
<view class="u-calendar__time-row">
<text class="u-calendar__time-date">{{ rangeStartDateLabel }}</text>
<view class="u-calendar__time-trigger" @click="openTimePicker('start')">
<text class="u-calendar__time-text">{{ rangeStartTime }}</text>
</view>
</view>
<view class="u-calendar__time-row">
<text class="u-calendar__time-date">{{ rangeEndDateLabel }}</text>
<view class="u-calendar__time-trigger" @click="openTimePicker('end')">
<text class="u-calendar__time-text">{{ rangeEndTime }}</text>
</view>
</view>
</view>
</view>
<scroll-view
v-if="!monthSwitch"
:style="{
height: addUnit(listHeight, 'px')
}"
scroll-y
@scroll="onScroll"
:scroll-top="scrollTop"
:scrollIntoView="scrollIntoView"
>
<uMonth
:color="color"
:rowHeight="rowHeight"
:showMark="showMark"
:months="months"
:mode="mode"
:maxCount="maxCount"
:startText="startText"
:endText="endText"
:defaultDate="defaultDate"
:minDate="innerMinDate"
:maxDate="innerMaxDate"
:maxMonth="monthNum"
:readonly="readonly"
:maxRange="maxRange"
:rangePrompt="rangePrompt"
:showRangePrompt="showRangePrompt"
:allowSameDay="allowSameDay"
:forbidDays="forbidDays"
:forbidDaysToast="forbidDaysToast"
:monthFormat="monthFormat"
:todayDate="todayDate"
:todayColor="todayColor"
ref="month"
@monthSelected="monthSelected"
@updateMonthTop="onUpdateMonthTop"
></uMonth>
</scroll-view>
<view
v-else
:style="{
height: addUnit(listHeight, 'px')
}"
>
<uMonth
:color="color"
:rowHeight="rowHeight"
:showMark="showMark"
:months="currentMonths"
:mode="mode"
:maxCount="maxCount"
:startText="startText"
:endText="endText"
:defaultDate="defaultDate"
:minDate="innerMinDate"
:maxDate="innerMaxDate"
:maxMonth="monthNum"
:readonly="readonly"
:maxRange="maxRange"
:rangePrompt="rangePrompt"
:showRangePrompt="showRangePrompt"
:allowSameDay="allowSameDay"
:forbidDays="forbidDays"
:forbidDaysToast="forbidDaysToast"
:monthFormat="monthFormat"
:todayDate="todayDate"
:todayColor="todayColor"
ref="month"
@monthSelected="monthSelected"
@updateMonthTop="onUpdateMonthTop"
></uMonth>
</view>
<slot name="footer" v-if="showConfirm">
<view class="u-calendar__confirm">
<u-button
shape="circle"
:text="
buttonDisabled ? confirmDisabledText : confirmText
"
:color="color"
@click="confirm"
:disabled="buttonDisabled"
></u-button>
</view>
</slot>
</view>
</u-popup>
<u-popup
:show="timePickerShow"
mode="center"
:round="8"
:closeOnClickOverlay="true"
@close="closeTimePicker"
>
<view class="u-calendar__time-picker">
<view class="u-calendar__time-picker__header">
<text class="u-calendar__time-picker__cancel" @click="closeTimePicker">取消</text>
<text class="u-calendar__time-picker__title">选择时间</text>
<text class="u-calendar__time-picker__confirm" @click="confirmTimePicker">确定</text>
</view>
<picker-view class="u-calendar__time-picker__body" :value="timePickerValue" @change="onTimePickerChange">
<picker-view-column>
<view v-for="(item, index) in hourOptions" :key="`h-${index}`" class="u-calendar__time-picker__item">{{ item }}</view>
</picker-view-column>
<picker-view-column v-if="timePrecision !== 'hour'">
<view v-for="(item, index) in minuteOptions" :key="`m-${index}`" class="u-calendar__time-picker__item">{{ item }}</view>
</picker-view-column>
<picker-view-column v-if="timePrecision === 'second'">
<view v-for="(item, index) in secondOptions" :key="`s-${index}`" class="u-calendar__time-picker__item">{{ item }}</view>
</picker-view-column>
</picker-view>
</view>
</u-popup>
</template>
<script>
import uHeader from './header.vue'
import uMonth from './month.vue'
import { props } from './props.js'
import util from './util.js'
import dayjs from '../u-datetime-picker/dayjs.esm.min.js';
import Calendar from '../../libs/util/calendar.js'
import { mpMixin } from '../../libs/mixin/mpMixin.js'
import { mixin } from '../../libs/mixin/mixin.js'
import { addUnit, getPx, range, error, padZero } from '../../libs/function/index';
import test from '../../libs/function/test';
import { t } from '../../libs/i18n'
/**
* Calendar 日历
* @description 此组件用于单个选择日期,范围选择日期等,日历被包裹在底部弹起的容器中.
* @tutorial https://uview-plus.jiangruyi.com/components/calendar.html
*
* @property {String} title 标题内容 (默认 日期选择 )
* @property {Boolean} showTitle 是否显示标题 (默认 true )
* @property {Boolean} showSubtitle 是否显示副标题 (默认 true )
* @property {String} mode 日期类型选择 single-选择单个日期,multiple-可以选择多个日期,range-选择日期范围 ( 默认 'single' )
* @property {String} startText mode=range时,第一个日期底部的提示文字 (默认 '开始' )
* @property {String} endText mode=range时,最后一个日期底部的提示文字 (默认 '结束' )
* @property {Array} customList 自定义列表
* @property {String} color 主题色,对底部按钮和选中日期有效 (默认 ‘#3c9cff' )
* @property {String | Number} minDate 最小的可选日期 (默认 0 )
* @property {String | Number} maxDate 最大可选日期 (默认 0 )
* @property {Array | String| Date} defaultDate 默认选中的日期,mode为multiple或range是必须为数组格式
* @property {String | Number} maxCount mode=multiple时,最多可选多少个日期 (默认 Number.MAX_SAFE_INTEGER )
* @property {String | Number} rowHeight 日期行高 (默认 56 )
* @property {Function} formatter 日期格式化函数
* @property {Boolean} showLunar 是否显示农历 (默认 false )
* @property {Boolean} showMark 是否显示月份背景色 (默认 true )
* @property {String} confirmText 确定按钮的文字 (默认 '确定' )
* @property {String} confirmDisabledText 确认按钮处于禁用状态时的文字 (默认 '确定' )
* @property {Boolean} show 是否显示日历弹窗 (默认 false )
* @property {Boolean} overlay 是否显示遮罩 (默认 true )
* @property {String | Number} duration 动画时长,单位ms (默认 300 )
* @property {Object | String} overlayStyle 自定义遮罩的样式
* @property {Number | String} overlayOpacity 遮罩透明度,0-1之间 (默认 0.5 )
* @property {String | Number} zIndex 弹出层的z-index值 (默认 10075 )
* @property {Boolean} safeAreaInsetBottom 是否留出底部安全距离 (默认 true )
* @property {Boolean} safeAreaInsetTop 是否留出顶部安全距离(状态栏高度) (默认 false )
* @property {String} bgColor 弹窗背景色,设置为transparent可去除白色背景
* @property {Boolean} closeOnClickOverlay 是否允许点击遮罩关闭日历 (默认 false )
* @property {Boolean} readonly 是否为只读状态,只读状态下禁止选择日期 (默认 false )
* @property {String | Number} maxRange 日期区间最多可选天数,默认无限制,mode = range时有效
* @property {String} rangePrompt 范围选择超过最多可选天数时的提示文案,mode = range时有效
* @property {Boolean} showRangePrompt 范围选择超过最多可选天数时,是否展示提示文案,mode = range时有效 (默认 true )
* @property {Boolean} allowSameDay 是否允许日期范围的起止时间为同一天,mode = range时有效 (默认 false )
* @property {String} rangeResultMode 区间模式下确认返回值格式,all-返回区间内所有日期,boundary-仅返回起止日期 (默认 'all' )
* @property {Boolean} enableTime 是否开启时分秒选择 (默认 false )
* @property {String} timePrecision 时间精度,hour-仅时,minute-时分,second-时分秒 (默认 'minute' )
* @property {String} defaultTime 默认时间,支持HH/HH:mm/HH:mm:ss,不传则按00:00补齐
* @property {Number|String} round 圆角值,默认无圆角 (默认 0 )
* @property {Number|String} monthNum 最多展示的月份数量 (默认 3 )
* @property {Boolean} monthSwitch 是否启用非滚动的单月切换模式 (默认 false )
* @property {String} todayColor 今天日期的独立高亮颜色,默认跟随主题色
* @property {Array} weekText 星期文案 (默认 ['一', '二', '三', '四', '五', '六', '日'] )
*
* @event {Function()} confirm 点击确定按钮时触发 选择日期相关的返回参数
* @event {Function()} close 日历关闭时触发 可定义页面关闭时的回调事件
* @event {Function()} closed 日历已关闭(离场动画结束、弹窗真正消失后)触发
* @example <u-calendar :defaultDate="defaultDateMultiple" :show="show" mode="multiple" @confirm="confirm">
</u-calendar>
* */
export default {
name: 'u-calendar',
mixins: [mpMixin, mixin, props],
components: {
uHeader,
uMonth
},
data() {
return {
// 需要显示的月份的数组
months: [],
// 在月份滚动区域中,当前视图中月份的index索引
monthIndex: 0,
// 月份滚动区域的高度
listHeight: 0,
// month组件中选择的日期数组
selected: [],
scrollIntoView: '',
scrollIntoViewScroll: '',
scrollTop:0,
timePickerShow: false,
timePickerTarget: 'single',
timePickerValue: [0, 0, 0],
hourOptions: [],
minuteOptions: [],
secondOptions: [],
singleTime: '00:00',
rangeStartTime: '00:00',
rangeEndTime: '00:00',
// 过滤处理方法
innerFormatter: (value) => value
}
},
watch: {
scrollIntoView: {
immediate: true,
handler(n) {
// console.log('scrollIntoView', n)
}
},
selectedChange: {
immediate: true,
handler(n) {
this.setMonth()
}
},
timePrecision() {
this.initTimeOptions()
this.initTimeValues()
},
defaultTime() {
this.initTimeValues()
},
// 打开弹窗时,设置月份数据
show: {
immediate: true,
handler(n) {
if (n) {
this.setMonth()
// 弹层每次打开都会重新挂载并滚动回默认月份,副标题指向的月份索引要一起复位,
// 否则会沿用上一次关闭前滚动到的月份,导致副标题与日历内容对应不上。
// 页面内模式的内容一直挂载着,滚动位置不会重置,所以不做处理
if (!this.pageInline) {
this.monthIndex = this.getDefaultMonthIndex()
}
} else {
// 关闭时重置scrollIntoView,否则会出现二次打开日历,当前月份数据显示不正确。
// scrollIntoView需要有一个值变动过程,才会产生作用。
this.scrollIntoView = ''
}
}
}
},
computed: {
// 由于maxDate和minDate可以为字符串(2021-10-10),或者数值(时间戳),但是dayjs如果接受字符串形式的时间戳会有问题,这里进行处理
innerMaxDate() {
return test.number(this.maxDate)
? Number(this.maxDate)
: this.maxDate
},
innerMinDate() {
return test.number(this.minDate)
? Number(this.minDate)
: this.minDate
},
todayDate() {
return dayjs().format('YYYY-MM-DD')
},
todayText() {
return t('up.calendar.today')
},
todayDisabled() {
const today = dayjs(this.todayDate)
const minDate = this.innerMinDate
? dayjs(this.innerMinDate)
: null
const maxDate = this.innerMaxDate
? dayjs(this.innerMaxDate)
: null
if (minDate && today.isBefore(minDate, 'day')) {
return true
}
if (maxDate && today.isAfter(maxDate, 'day')) {
return true
}
return false
},
// 多个条件的变化,会引起选中日期的变化,这里统一管理监听
selectedChange() {
return [this.innerMinDate, this.innerMaxDate, this.defaultDate]
},
subtitle() {
// 初始化时,this.months为空数组,所以需要特别判断处理
if (this.months.length) {
if (uni.getLocale() == 'zh-Hans' || uni.getLocale() == 'zh-Hant') {
return this.months[this.monthIndex].year + '年' + (this.months[this.monthIndex].month < 10 ? '0' + this.months[this.monthIndex].month : this.months[this.monthIndex].month) + '月'
} else {
return (this.months[this.monthIndex].month < 10 ? '0' + this.months[this.monthIndex].month : this.months[this.monthIndex].month) + '/' + this.months[this.monthIndex].year
}
} else {
return ''
}
},
currentMonths() {
if (this.monthSwitch && this.months.length) {
return [this.months[this.monthIndex]]
}
return this.months
},
switchPrevDisabled() {
return this.monthIndex <= 0
},
switchNextDisabled() {
return this.monthIndex >= this.months.length - 1
},
switchPrevYearDisabled() {
return this.monthIndex - 12 < 0
},
switchNextYearDisabled() {
return this.monthIndex + 12 > this.months.length - 1
},
showTimePanel() {
if (!this.enableTime) return false
if (this.mode === 'single') return true
if (this.mode === 'range' && this.rangeResultMode === 'boundary') {
return true
}
return false
},
singleDateLabel() {
return this.selected[0] || '--'
},
rangeStartDateLabel() {
return this.selected[0] || '--'
},
rangeEndDateLabel() {
if (this.selected.length >= 2) {
return this.selected[this.selected.length - 1]
}
return '--'
},
buttonDisabled() {
// 如果为range类型,且选择的日期个数不足1个时,让底部的按钮出于disabled状态
if (this.mode === 'range') {
if (this.selected.length <= 1) {
return true
} else {
return false
}
} else {
return false
}
}
},
mounted() {
this.start = Date.now()
this.init()
},
emits: ["confirm", "close", "closed"],
methods: {
addUnit,
padTime(num) {
return String(num).padStart(2, '0')
},
initTimeOptions() {
this.hourOptions = Array.from({ length: 24 }, (_, i) => this.padTime(i))
this.minuteOptions = Array.from({ length: 60 }, (_, i) => this.padTime(i))
this.secondOptions = Array.from({ length: 60 }, (_, i) => this.padTime(i))
},
parseTimeValue(value = '') {
const raw = String(value || '').trim()
const parts = raw.length ? raw.split(':') : []
const getNumber = (index, max) => {
const current = Number(parts[index] || 0)
if (Number.isNaN(current)) return 0
return Math.max(0, Math.min(max, current))
}
const hour = getNumber(0, 23)
const minute = getNumber(1, 59)
const second = getNumber(2, 59)
return [hour, minute, second]
},
getDefaultTimeValue() {
const [hour, minute, second] = this.parseTimeValue(this.defaultTime)
if (this.timePrecision === 'hour') {
return this.padTime(hour)
}
if (this.timePrecision === 'second') {
return `${this.padTime(hour)}:${this.padTime(minute)}:${this.padTime(second)}`
}
return `${this.padTime(hour)}:${this.padTime(minute)}`
},
initTimeValues() {
const value = this.getDefaultTimeValue()
this.singleTime = value
this.rangeStartTime = value
this.rangeEndTime = value
},
timeToPickerValue(value = '') {
const [hour, minute, second] = this.parseTimeValue(value)
return [hour, minute, second]
},
pickerValueToTime(value = []) {
const hour = Number(value[0] || 0)
const minute = Number(value[1] || 0)
const second = Number(value[2] || 0)
if (this.timePrecision === 'hour') {
return this.padTime(hour)
}
if (this.timePrecision === 'second') {
return `${this.padTime(hour)}:${this.padTime(minute)}:${this.padTime(second)}`
}
return `${this.padTime(hour)}:${this.padTime(minute)}`
},
openTimePicker(target) {
this.timePickerTarget = target
let currentValue = this.singleTime
if (target === 'start') currentValue = this.rangeStartTime
if (target === 'end') currentValue = this.rangeEndTime
this.timePickerValue = this.timeToPickerValue(currentValue)
this.timePickerShow = true
},
onTimePickerChange(e) {
this.timePickerValue = e.detail.value
},
closeTimePicker() {
this.timePickerShow = false
},
confirmTimePicker() {
const value = this.pickerValueToTime(this.timePickerValue)
if (this.timePickerTarget === 'single') this.singleTime = value
if (this.timePickerTarget === 'start') this.rangeStartTime = value
if (this.timePickerTarget === 'end') this.rangeEndTime = value
this.timePickerShow = false
},
timeToSecond(timeText = '') {
const [hour, minute, second] = this.parseTimeValue(timeText)
return hour * 3600 + minute * 60 + second
},
validateSameDayRangeTime() {
if (!this.enableTime || this.mode !== 'range' || this.rangeResultMode !== 'boundary') {
return true
}
if (this.selected.length < 2) return true
const startDate = this.selected[0]
const endDate = this.selected[this.selected.length - 1]
if (startDate !== endDate) return true
const startSeconds = this.timeToSecond(this.rangeStartTime)
const endSeconds = this.timeToSecond(this.rangeEndTime)
if (endSeconds < startSeconds) {
uni.showToast({
title: '结束时间不能早于开始时间',
icon: 'none'
})
return false
}
return true
},
appendTime(dateText, timeText) {
return `${dateText} ${timeText}`
},
getConfirmValue(selected = this.selected) {
let result = selected
if (
this.mode === 'range' &&
this.rangeResultMode === 'boundary' &&
selected.length >= 2
) {
const len = selected.length - 1
result = [selected[0], selected[len]]
}
if (!this.showTimePanel || !this.enableTime) {
return result
}
if (this.mode === 'single' && result.length >= 1) {
return [this.appendTime(result[0], this.singleTime)]
}
if (this.mode === 'range' && this.rangeResultMode === 'boundary' && result.length >= 2) {
return [
this.appendTime(result[0], this.rangeStartTime),
this.appendTime(result[1], this.rangeEndTime)
]
}
return result
},
// 在微信小程序中,不支持将函数当做props参数,故只能通过ref形式调用
setFormatter(e) {
this.innerFormatter = e
},
// month组件内部选择日期后,通过事件通知给父组件
monthSelected(e,scene ='init') {
this.selected = e
if (!this.showConfirm) {
// 在不需要确认按钮的情况下,如果为单选,或者范围多选且已选长度大于2,则直接进行返还
if (
this.mode === 'multiple' ||
this.mode === 'single' ||
(this.mode === 'range' && this.selected.length >= 2)
) {
if( scene === 'init'){
return
}
if( scene === 'tap') {
if (!this.validateSameDayRangeTime()) return
this.$emit('confirm', this.getConfirmValue())
}
}
}
},
init() {
// 校验maxDate,不能小于minDate。
if (
this.innerMaxDate &&
this.innerMinDate &&
new Date(this.innerMaxDate).getTime() < new Date(this.innerMinDate).getTime()
) {
return error('maxDate不能小于minDate时间')
}
// 滚动区域的高度
let bottomPadding = 0;
if (this.pageInline) {
bottomPadding = 0
} else {
bottomPadding = 30
}
this.listHeight = this.rowHeight * (this.monthSwitch ? 6 : 5) + bottomPadding
this.initTimeOptions()
this.initTimeValues()
this.setMonth()
},
close() {
this.$emit('close')
},
// 点击确定按钮
confirm() {
if (!this.buttonDisabled) {
if (!this.validateSameDayRangeTime()) return
this.$emit('confirm', this.getConfirmValue())
}
},
// 获得两个日期之间的月份数
getMonths(minDate, maxDate) {
const minYear = dayjs(minDate).year()
const minMonth = dayjs(minDate).month() + 1
const maxYear = dayjs(maxDate).year()
const maxMonth = dayjs(maxDate).month() + 1
return (maxYear - minYear) * 12 + (maxMonth - minMonth) + 1
},
// 设置月份数据
setMonth() {
// 最小日期的毫秒数
const minDate = this.innerMinDate || dayjs().valueOf()
// 如果没有指定最大日期,则往后推3个月
const maxDate =
this.innerMaxDate ||
dayjs(minDate)
.add(this.monthNum - 1, 'month')
.valueOf()
// 最大最小月份之间的共有多少个月份,
const months = range(
1,
this.monthNum,
this.getMonths(minDate, maxDate)
)
// 记录上一次测量到的各月份top值,月份未发生变化时高度也不会变,
// 复用后可避免重新生成数据后onScroll没有判断依据
const topCache = {}
this.months.forEach(item => {
if (typeof item.top === 'number') {
topCache[`${item.year}-${item.month}`] = item.top
}
})
// 先清空数组
this.months = []
// 最大最小日期只需格式化一次,YYYY-MM-DD的字典序即时间序,可直接比较字符串
const minDateStr = dayjs(minDate).format('YYYY-MM-DD')
const maxDateStr = dayjs(maxDate).format('YYYY-MM-DD')
const formatter = this.formatter || this.innerFormatter
for (let i = 0; i < months; i++) {
// 每个月只构造一个dayjs对象,当月的日期数据都由它派生,
// 避免按天重复解析(monthNum较大时这里是主要开销)
const monthStart = dayjs(minDate).add(i, 'month')
const monthValue = monthStart.month() + 1
this.months.push({
date: new Array(monthStart.daysInMonth())
.fill(1)
.map((item, index) => {
// 日期,取值1-31
let day = index + 1
const current = monthStart.date(day)
// 星期,0-6,0为周日
const week = current.day()
const date = current.format('YYYY-MM-DD')
let bottomInfo = ''
if (this.showLunar) {
// 将日期转为农历格式
const lunar = Calendar.solar2lunar(
current.year(),
monthValue,
day
)
bottomInfo = lunar.IDayCn
}
let config = {
day,
week,
// 小于最小允许的日期,或者大于最大的日期,则设置为disabled状态
disabled:
date < minDateStr || date > maxDateStr,
// 返回一个日期对象,供外部的formatter获取当前日期的年月日等信息,进行加工处理
date: new Date(date),
// 已格式化好的日期字符串,供month组件比较使用,
// 省掉渲染期对每个日期格子的重复解析
dateStr: date,
bottomInfo,
dot: false,
month: monthValue
}
return formatter(config)
}),
// 当前所属的月份
month: monthValue,
// 当前年份
year: monthStart.year()
})
}
this.months.forEach(item => {
const cachedTop = topCache[`${item.year}-${item.month}`]
if (typeof cachedTop === 'number') {
item.top = cachedTop
}
})
if (this.monthSwitch) {
this.monthIndex = this.getDefaultMonthIndex()
}
},
getDefaultMonthIndex() {
let selected = dayjs().format('YYYY-MM')
if (this.defaultDate) {
if (!test.array(this.defaultDate)) {
selected = dayjs(this.defaultDate).format('YYYY-MM')
} else if (this.defaultDate.length) {
selected = dayjs(this.defaultDate[0]).format('YYYY-MM')
}
}
const index = this.months.findIndex(({ year, month }) => {
return `${year}-${padZero(month)}` === selected
})
return index === -1 ? 0 : index
},
prevMonth() {
if (!this.switchPrevDisabled) {
this.monthIndex -= 1
}
},
nextMonth() {
if (!this.switchNextDisabled) {
this.monthIndex += 1
}
},
prevYear() {
if (!this.switchPrevYearDisabled) {
this.monthIndex -= 12
}
},
nextYear() {
if (!this.switchNextYearDisabled) {
this.monthIndex += 12
}
},
jumpToToday() {
if (this.todayDisabled) {
return
}
const targetMonth = dayjs(this.todayDate).format('YYYY-MM')
const selectToday = () => {
if (this.mode === 'range') {
return
}
this.$refs.month && this.$refs.month.selectDate(this.todayDate)
}
if (this.monthSwitch) {
const todayMonthIndex = this.months.findIndex(({ year, month }) => {
return `${year}-${padZero(month)}` === targetMonth
})
if (todayMonthIndex !== -1) {
this.monthIndex = todayMonthIndex
this.$nextTick(selectToday)
}
return
}
this.scrollIntoDefaultMonth(targetMonth)
this.$nextTick(selectToday)
},
// 滚动到默认设置的月份
scrollIntoDefaultMonth(selected) {
// 查询默认日期在可选列表的下标
const _index = this.months.findIndex(({
year,
month
}) => {
month = padZero(month)
return `${year}-${month}` === selected
})
if (_index !== -1) {
// 滚动位置是程序主动设置的,同步更新副标题指向的月份索引。
// 只依赖scroll事件的话,滚动位置没有真正变化时收不到事件,副标题就会停留在上一次的月份
this.monthIndex = _index
// #ifndef MP-WEIXIN
this.$nextTick(() => {
this.scrollIntoView = ''
this.scrollIntoView = `month-${_index}`
this.scrollIntoViewScroll = this.scrollIntoView
})
// #endif
// #ifdef MP-WEIXIN
// scrollTop与scrollIntoView同理,需要有一个值变动过程才会生效,
// 否则二次打开日历时沿用上次的数值,scroll-view不会滚动到默认月份
const _top = this.months[_index].top || 0;
this.scrollTop = 0;
if (_top !== 0) {
this.$nextTick(() => {
this.scrollTop = _top;
})
}
// #endif
}
},
// scroll-view滚动监听
onScroll(event) {
// 不允许小于0的滚动值,如果scroll-view到顶了,继续下拉,会出现负数值
const scrollTop = Math.max(0, event.detail.scrollTop)
// 月份的top值尚未测量出来时不做判断,否则所有月份的判断阈值相同,
// 会把副标题固定到最后一个月份上
if (!this.months.some(item => typeof item.top === 'number')) return
// 将当前滚动条数值,与各月份的top值比较,可以得出当前滚动到了哪一个月份的索引
let monthIndex = 0
for (let i = 0; i < this.months.length; i++) {
const top = this.months[i].top
if (typeof top !== 'number') continue
if (scrollTop >= top) {
monthIndex = i
} else {
break
}
}
this.monthIndex = monthIndex
this.scrollIntoViewScroll = `month-${monthIndex}`
},
// 更新月份的top值
onUpdateMonthTop(topArr = []) {
if (this.monthSwitch) {
return
}
this.updateMonthTop(topArr)
},
updateMonthTop(topArr = []) {
// 设置对应月份的top值,用于onScroll方法更新月份
topArr.map((item, index) => {
this.months[index].top = item
})
// 获取默认日期的下标
if (!this.defaultDate) {
// 如果没有设置默认日期,则将当天日期设置为默认选中的日期
const selected = dayjs().format("YYYY-MM")
this.scrollIntoDefaultMonth(selected)
return
}
let selected = dayjs().format("YYYY-MM");
// 单选模式,可以是字符串或数组,Date对象等
if (!test.array(this.defaultDate)) {
selected = dayjs(this.defaultDate).format("YYYY-MM")
} else {
selected = dayjs(this.defaultDate[0]).format("YYYY-MM");
}
this.scrollIntoDefaultMonth(selected)
}
}
}
</script>
<style lang="scss" scoped>
.u-calendar {
color: var(--up-main-color, $u-main-color);
background-color: var(--up-card-bg-color, #ffffff);
&__time-panel {
padding: 8px 16px 0;
}
&__time-row {
height: 34px;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
&__time-date {
font-size: 13px;
color: #303133;
}
&__time-trigger {
min-width: 92px;
height: 30px;
padding: 0 10px;
border-radius: 15px;
border: 1px solid #dcdfe6;
display: flex;
align-items: center;
justify-content: center;
}
&__time-text {
font-size: 14px;
color: #303133;
}
&__time-picker {
width: 320px;
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
}
&__time-picker__header {
height: 44px;
padding: 0 14px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f0f0f0;
}
&__time-picker__cancel {
font-size: 14px;
color: #909399;
}
&__time-picker__title {
font-size: 15px;
color: #303133;
font-weight: 600;
}
&__time-picker__confirm {
font-size: 14px;
color: #3c9cff;
}
&__time-picker__body {
width: 320px;
height: 200px;
}
&__time-picker__item {
height: 40px;
line-height: 40px;
text-align: center;
font-size: 16px;
color: #303133;
}
&__confirm {
padding: 7px 18px;
}
}
</style>