view.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. <template>
  2. <view class="u-calendar">
  3. <uHeader
  4. :title="title"
  5. :subtitle="subtitle"
  6. :showSubtitle="showSubtitle"
  7. :showTitle="showTitle"
  8. :weekdays="weekdays"
  9. ></uHeader>
  10. <scroll-view
  11. :style="{
  12. height: $u.addUnit(listHeight),
  13. }"
  14. enable-flex
  15. scroll-y
  16. @scroll="onScroll"
  17. :scroll-top="scrollTop"
  18. :scrollIntoView="scrollIntoView"
  19. >
  20. <uMonth
  21. :shape="shape"
  22. :color="color"
  23. :rowHeight="rowHeight"
  24. :showMark="showMark"
  25. :months="months"
  26. :mode="mode"
  27. :maxCount="maxCount"
  28. :startText="startText"
  29. :endText="endText"
  30. :defaultDate="defaultDate"
  31. :minDate="innerMinDate"
  32. :maxDate="innerMaxDate"
  33. :maxMonth="monthNum"
  34. :readonly="readonly"
  35. :maxRange="maxRange"
  36. :rangePrompt="rangePrompt"
  37. :showRangePrompt="showRangePrompt"
  38. :allowSameDay="allowSameDay"
  39. ref="month"
  40. @monthSelected="monthSelected"
  41. @updateMonthTop="updateMonthTop"
  42. ></uMonth>
  43. </scroll-view>
  44. </view>
  45. </template>
  46. <script>
  47. import uHeader from './header.vue';
  48. import uMonth from './month.vue';
  49. import util from './util.js';
  50. import dayjs from '../../libs/util/dayjs.js';
  51. import Calendar from '../../libs/util/calendar.js';
  52. import mixin from '../../libs/mixin/mixin';
  53. /**
  54. * Calendar 日历
  55. * @property {String} title 标题内容 (默认 日期选择 )
  56. * @property {Boolean} showTitle 是否显示标题 (默认 true )
  57. * @property {Boolean} showSubtitle 是否显示副标题 (默认 true )
  58. * @property {String} mode 日期类型选择 single-选择单个日期,multiple-可以选择多个日期,range-选择日期范围 ( 默认 'single' )
  59. * @property {String} startText mode=range时,第一个日期底部的提示文字 (默认 '开始' )
  60. * @property {String} endText mode=range时,最后一个日期底部的提示文字 (默认 '结束' )
  61. * @property {Array} customList 自定义列表
  62. * @property {String} color 主题色,对底部按钮和选中日期有效 (默认 ‘#3c9cff' )
  63. * @property {String | Number} minDate 最小的可选日期 (默认 0 )
  64. * @property {String | Number} maxDate 最大可选日期 (默认 0 )
  65. * @property {Array | String| Date} defaultDate 默认选中的日期,mode为multiple或range是必须为数组格式
  66. * @property {String | Number} maxCount mode=multiple时,最多可选多少个日期 (默认 Number.MAX_SAFE_INTEGER )
  67. * @property {String | Number} rowHeight 日期行高 (默认 56 )
  68. * @property {Function} formatter 日期格式化函数
  69. * @property {Boolean} showLunar 是否显示农历 (默认 false )
  70. * @property {Boolean} showMark 是否显示月份背景色 (默认 true )
  71. * @property {Boolean} show 是否显示日历弹窗 (默认 false )
  72. * @property {Boolean} closeOnClickOverlay 是否允许点击遮罩关闭日历 (默认 false )
  73. * @property {Boolean} readonly 是否为只读状态,只读状态下禁止选择日期 (默认 false )
  74. * @property {String | Number} maxRange 日期区间最多可选天数,默认无限制,mode = range时有效
  75. * @property {String} rangePrompt 范围选择超过最多可选天数时的提示文案,mode = range时有效
  76. * @property {Boolean} showRangePrompt 范围选择超过最多可选天数时,是否展示提示文案,mode = range时有效 (默认 true )
  77. * @property {Boolean} allowSameDay 是否允许日期范围的起止时间为同一天,mode = range时有效 (默认 false )
  78. * @property {Number|String} monthNum 最多展示的月份数量 (默认 3 )
  79. *
  80. * @event {Function()} confirm 点击确定按钮时触发 选择日期相关的返回参数
  81. * @event {Function()} close 日历关闭时触发 可定义页面关闭时的回调事件
  82. * @example <u-calendar :defaultDate="defaultDateMultiple" :show="show" mode="multiple" @confirm="confirm">
  83. </u-calendar>
  84. * */
  85. export default {
  86. name: 'u-calendar-view',
  87. mixins: [mixin],
  88. props: {
  89. // 日历顶部标题
  90. title: {
  91. type: String,
  92. default: '',
  93. },
  94. // 是否显示标题
  95. showTitle: {
  96. type: Boolean,
  97. default: true,
  98. },
  99. // 是否显示副标题
  100. showSubtitle: {
  101. type: Boolean,
  102. default: true,
  103. },
  104. // 日期类型选择,single-选择单个日期,multiple-可以选择多个日期,range-选择日期范围
  105. mode: {
  106. type: String,
  107. default: 'single',
  108. },
  109. // 选中的日期的形状,circle(圆形),square(带圆角) (默认 'square' )
  110. shape: {
  111. type: String,
  112. default: 'square',
  113. },
  114. // mode=range时,第一个日期底部的提示文字
  115. startText: {
  116. type: String,
  117. default: '开始',
  118. },
  119. // mode=range时,最后一个日期底部的提示文字
  120. endText: {
  121. type: String,
  122. default: '结束',
  123. },
  124. // 自定义列表
  125. customList: {
  126. type: Array,
  127. default: () => [],
  128. },
  129. // 主题色,对底部按钮和选中日期有效
  130. color: {
  131. type: String,
  132. default: '#3c9cff',
  133. },
  134. // 最小的可选日期
  135. minDate: {
  136. type: [String, Number],
  137. default: 0,
  138. },
  139. // 最大可选日期
  140. maxDate: {
  141. type: [String, Number],
  142. default: 0,
  143. },
  144. // 默认选中的日期,mode为multiple或range是必须为数组格式
  145. defaultDate: {
  146. type: [Array, String, Date, null],
  147. default: null,
  148. },
  149. // mode=multiple时,最多可选多少个日期
  150. maxCount: {
  151. type: [String, Number],
  152. default: Number.MAX_SAFE_INTEGER,
  153. },
  154. // 日期行高
  155. rowHeight: {
  156. type: [String, Number],
  157. default: 56,
  158. },
  159. // 日期格式化函数
  160. formatter: {
  161. type: [Function, null],
  162. default: null,
  163. },
  164. // 是否显示农历
  165. showLunar: {
  166. type: Boolean,
  167. default: false,
  168. },
  169. // 是否显示月份背景色
  170. showMark: {
  171. type: Boolean,
  172. default: true,
  173. },
  174. // 是否为只读状态,只读状态下禁止选择日期
  175. readonly: {
  176. type: Boolean,
  177. default: false,
  178. },
  179. // 日期区间最多可选天数,默认无限制,mode = range时有效
  180. maxRange: {
  181. type: [Number, String],
  182. default: 0,
  183. },
  184. // 范围选择超过最多可选天数时的提示文案,mode = range时有效
  185. rangePrompt: {
  186. type: String,
  187. default: '',
  188. },
  189. // 范围选择超过最多可选天数时,是否展示提示文案,mode = range时有效
  190. showRangePrompt: {
  191. type: Boolean,
  192. default: true,
  193. },
  194. // 是否允许日期范围的起止时间为同一天,mode = range时有效
  195. allowSameDay: {
  196. type: Boolean,
  197. default: false,
  198. },
  199. // 最多展示月份数量
  200. monthNum: {
  201. type: [Number, String],
  202. default: 3,
  203. },
  204. // 禁止选择的日期
  205. disabledDate: {
  206. type: [Array, String, null],
  207. default: null,
  208. },
  209. // 禁止选择的日期函数
  210. disabledFun: {
  211. type: [Function, null],
  212. default: null,
  213. },
  214. // 星期几
  215. weekdays: {
  216. type: String,
  217. default: '',
  218. },
  219. },
  220. components: {
  221. uHeader,
  222. uMonth,
  223. },
  224. data() {
  225. return {
  226. subtitle: '',
  227. // 需要显示的月份的数组
  228. months: [],
  229. // 在月份滚动区域中,当前视图中月份的index索引
  230. monthIndex: 0,
  231. // 月份滚动区域的高度
  232. listHeight: 0,
  233. scrollIntoView: '',
  234. scrollTop: 0,
  235. // 过滤处理方法
  236. innerFormatter: (value) => value,
  237. };
  238. },
  239. watch: {
  240. selectedChange: {
  241. immediate: true,
  242. handler(n) {
  243. this.setMonth();
  244. },
  245. },
  246. //修复vue2微信小程序报警告
  247. monthIndex: {
  248. immediate: true,
  249. handler(newVal,oldVal) {
  250. this.subtitle = `${this.months[newVal].year}年${this.months[newVal].month}月`;
  251. }
  252. },
  253. },
  254. computed: {
  255. // 由于maxDate和minDate可以为字符串(2021-10-10),或者数值(时间戳),但是dayjs如果接受字符串形式的时间戳会有问题,这里进行处理
  256. innerMaxDate() {
  257. return uni.$u.test.number(this.maxDate)
  258. ? Number(this.maxDate)
  259. : this.maxDate;
  260. },
  261. innerMinDate() {
  262. return uni.$u.test.number(this.minDate)
  263. ? Number(this.minDate)
  264. : this.minDate;
  265. },
  266. // 多个条件的变化,会引起选中日期的变化,这里统一管理监听
  267. selectedChange() {
  268. return [this.innerMinDate, this.innerMaxDate, this.defaultDate];
  269. },
  270. },
  271. mounted() {
  272. this.start = Date.now();
  273. this.init();
  274. },
  275. // #ifdef VUE3
  276. emits: ['monthSelected'],
  277. // #endif
  278. methods: {
  279. // 在微信小程序中,不支持将函数当做props参数,故只能通过ref形式调用
  280. setFormatter(e) {
  281. this.innerFormatter = e;
  282. },
  283. // month组件内部选择日期后,通过事件通知给父组件
  284. monthSelected(e) {
  285. this.$emit('monthSelected', e);
  286. },
  287. init() {
  288. // 校验maxDate,不能小于minDate
  289. if (
  290. this.innerMaxDate &&
  291. this.innerMinDate &&
  292. new Date(this.innerMaxDate).getTime() <
  293. new Date(this.innerMinDate).getTime()
  294. ) {
  295. return uni.$u.error('maxDate不能小于minDate');
  296. }
  297. // 滚动区域的高度
  298. this.listHeight = this.rowHeight * 5 - 1;
  299. this.setMonth();
  300. },
  301. // 获得两个日期之间的月份数
  302. getMonths(minDate, maxDate) {
  303. const minYear = dayjs(minDate).year();
  304. const minMonth = dayjs(minDate).month() + 1;
  305. const maxYear = dayjs(maxDate).year();
  306. const maxMonth = dayjs(maxDate).month() + 1;
  307. return (maxYear - minYear) * 12 + (maxMonth - minMonth) + 1;
  308. },
  309. // 设置月份数据
  310. setMonth() {
  311. // 最小日期的毫秒数
  312. const minDate = this.innerMinDate || dayjs().valueOf();
  313. // 如果没有指定最大日期,则往后推3个月
  314. const maxDate =
  315. this.innerMaxDate ||
  316. dayjs(minDate)
  317. .add(this.monthNum - 1, 'month')
  318. .valueOf();
  319. // 最大最小月份之间的共有多少个月份,
  320. const months = uni.$u.range(
  321. 1,
  322. this.monthNum,
  323. this.getMonths(minDate, maxDate),
  324. );
  325. // 先清空数组
  326. let monthsArr = [];
  327. for (let i = 0; i < months; i++) {
  328. monthsArr.push({
  329. date: new Array(
  330. dayjs(minDate).add(i, 'month').daysInMonth(),
  331. )
  332. .fill(1)
  333. .map((item, index) => {
  334. // 日期,取值1-31
  335. let day = index + 1;
  336. // 星期,0-6,0为周日
  337. const week = dayjs(minDate)
  338. .add(i, 'month')
  339. .date(day)
  340. .day();
  341. const date = dayjs(minDate)
  342. .add(i, 'month')
  343. .date(day)
  344. .format('YYYY-MM-DD');
  345. let bottomInfo = '';
  346. if (this.showLunar) {
  347. // 将日期转为农历格式
  348. const lunar = Calendar.solar2lunar(
  349. dayjs(date).year(),
  350. dayjs(date).month() + 1,
  351. dayjs(date).date(),
  352. );
  353. bottomInfo = lunar.IDayCn;
  354. }
  355. let dateObj = new Date(date);
  356. let disabled =
  357. dayjs(date).isBefore(
  358. dayjs(minDate).format('YYYY-MM-DD'),
  359. ) ||
  360. dayjs(date).isAfter(
  361. dayjs(maxDate).format('YYYY-MM-DD'),
  362. );
  363. if (!disabled) {
  364. if (this.disabledDate) {
  365. if (
  366. uni.$u.test.string(
  367. this.disabledDate,
  368. )
  369. ) {
  370. this.disabledDate = [
  371. this.disabledDate,
  372. ];
  373. }
  374. this.disabledDate.forEach((item) => {
  375. if (
  376. dayjs(item).format(
  377. 'YYYY-MM-DD',
  378. ) === date
  379. ) {
  380. disabled = true;
  381. }
  382. });
  383. }
  384. if (
  385. this.disabledFun &&
  386. uni.$u.test.func(this.disabledFun)
  387. ) {
  388. let result = this.disabledFun(dateObj);
  389. if (uni.$u.test.array(result)) {
  390. disabled = result[0];
  391. bottomInfo = result[1];
  392. } else {
  393. disabled = result;
  394. }
  395. }
  396. }
  397. let config = {
  398. day,
  399. week,
  400. // 小于最小允许的日期,或者大于最大的日期,则设置为disabled状态
  401. disabled,
  402. // 返回一个日期对象,供外部的formatter获取当前日期的年月日等信息,进行加工处理
  403. date: dateObj,
  404. bottomInfo,
  405. dot: false,
  406. month:
  407. dayjs(minDate).add(i, 'month').month() +
  408. 1,
  409. };
  410. const formatter =
  411. this.formatter || this.innerFormatter;
  412. return formatter(config);
  413. }),
  414. // 当前所属的月份
  415. month: dayjs(minDate).add(i, 'month').month() + 1,
  416. // 当前年份
  417. year: dayjs(minDate).add(i, 'month').year(),
  418. });
  419. }
  420. this.months = monthsArr;
  421. },
  422. // 滚动到默认设置的月份
  423. scrollIntoDefaultMonth(selected) {
  424. // 查询默认日期在可选列表的下标
  425. const _index = this.months.findIndex(({ year, month }) => {
  426. month = uni.$u.padZero(month);
  427. return `${year}-${month}` === selected;
  428. });
  429. if (_index !== -1) {
  430. this.$nextTick(() => {
  431. // #ifndef MP-WEIXIN
  432. this.scrollIntoView = `month-${_index}`;
  433. // #endif
  434. // #ifdef MP-WEIXIN
  435. this.scrollTop = 0;
  436. this.scrollTop = this.months[_index].top || 0;
  437. // #endif
  438. });
  439. }
  440. },
  441. // scroll-view滚动监听
  442. onScroll(event) {
  443. // 不允许小于0的滚动值,如果scroll-view到顶了,继续下拉,会出现负数值
  444. const scrollTop = Math.max(0, event.detail.scrollTop);
  445. // 将当前滚动条数值,除以滚动区域的高度,可以得出当前滚动到了哪一个月份的索引
  446. for (let i = 0; i < this.months.length; i++) {
  447. if (scrollTop >= (this.months[i].top || this.listHeight)) {
  448. this.monthIndex = i;
  449. }
  450. }
  451. },
  452. // 更新月份的top值
  453. updateMonthTop(topArr = []) {
  454. // 设置对应月份的top值,用于onScroll方法更新月份
  455. topArr.map((item, index) => {
  456. this.months[index].top = item;
  457. });
  458. // 获取默认日期的下标
  459. if (!this.defaultDate) {
  460. // 如果没有设置默认日期,则将当天日期设置为默认选中的日期
  461. const selected = dayjs().format('YYYY-MM');
  462. this.scrollIntoDefaultMonth(selected);
  463. return;
  464. }
  465. let selected = dayjs().format('YYYY-MM');
  466. // 单选模式,可以是字符串或数组,Date对象等
  467. if (!uni.$u.test.array(this.defaultDate)) {
  468. selected = dayjs(this.defaultDate).format('YYYY-MM');
  469. } else {
  470. selected = dayjs(this.defaultDate[0]).format('YYYY-MM');
  471. }
  472. this.scrollIntoDefaultMonth(selected);
  473. },
  474. },
  475. };
  476. </script>