calendar.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. <template>
  2. <view class="u-calendar">
  3. <view class="u-calendar__action u-flex u-row-center">
  4. <view class="u-calendar__action__icon">
  5. <u-icon v-if="changeYear" name="arrow-left-double" :color="yearArrowColor" @click="changeYearHandler(0)"></u-icon>
  6. </view>
  7. <view class="u-calendar__action__icon">
  8. <u-icon v-if="changeMonth" name="arrow-left" :color="monthArrowColor" @click="changeMonthHandler(0)"></u-icon>
  9. </view>
  10. <view class="u-calendar__action__text">{{ showTitle }}</view>
  11. <view class="u-calendar__action__icon">
  12. <u-icon v-if="changeMonth" name="arrow-right" :color="monthArrowColor" @click="changeMonthHandler(1)"></u-icon>
  13. </view>
  14. <view class="u-calendar__action__icon">
  15. <u-icon v-if="changeYear" name="arrow-right-double" :color="yearArrowColor" @click="changeYearHandler(1)"></u-icon>
  16. </view>
  17. <view class="u-calendar__action__back" @click="init">
  18. 回到今天
  19. </view>
  20. </view>
  21. <view class="u-calendar__week-day">
  22. <view class="u-calendar__week-day__text" v-for="(item, index) in weekDayZh" :key="index">{{item}}</view>
  23. </view>
  24. <view class="u-calendar__content">
  25. <!-- 前置空白部分 -->
  26. <block v-for="(item, index) in weekdayArr" :key="index">
  27. <view class="u-calendar__content__item"></view>
  28. </block>
  29. <view class="u-calendar__content__item u-calendar__content--start-date u-calendar__content--end-date" :class="{
  30. 'u-hover-class':openDisAbled(year,month,index+1)
  31. }" :style="{backgroundColor: getColor(index,1)}" v-for="(item, index) in daysArr" :key="index"
  32. @tap="dateClick(index)">
  33. <view class="u-calendar__content__item__inner" :style="{color: getColor(index,2)}">
  34. <view>{{ index + 1 }}</view>
  35. </view>
  36. </view>
  37. <view class="u-calendar__content__bg-month">{{month}}</view>
  38. </view>
  39. </view>
  40. </template>
  41. <script>
  42. /**
  43. * calendar 日历
  44. * @property {Boolean} change-year 是否显示顶部的切换年份方向的按钮(默认true)
  45. * @property {Boolean} change-month 是否显示顶部的切换月份方向的按钮(默认true)
  46. * @property {String Number} max-year 可切换的最大年份(默认2050)
  47. * @property {String Number} min-year 可切换的最小年份(默认1950)
  48. * @property {String Number} min-date 最小可选日期(默认1950-01-01)
  49. * @property {String Number} max-date 最大可选日期(默认当前日期)
  50. * @property {String Number} 弹窗顶部左右两边的圆角值,单位rpx(默认20)
  51. * @property {String} month-arrow-color 月份切换按钮箭头颜色(默认#606266)
  52. * @property {String} year-arrow-color 年份切换按钮箭头颜色(默认#909399)
  53. * @property {String} color 日期字体的默认颜色(默认#303133)
  54. * @property {String} active-bg-color 起始/结束日期按钮的背景色(默认#2979ff)
  55. * @property {String} active-color 起始/结束日期按钮的字体颜色(默认#ffffff)
  56. */
  57. export default {
  58. name: 'calendar',
  59. props: {
  60. // 是否允许切换年份
  61. changeYear: {
  62. type: Boolean,
  63. default: true
  64. },
  65. // 是否允许切换月份
  66. changeMonth: {
  67. type: Boolean,
  68. default: true
  69. },
  70. // date-单个日期选择,range-开始日期+结束日期选择
  71. mode: {
  72. type: String,
  73. default: 'date'
  74. },
  75. // 可切换的最大年份
  76. maxYear: {
  77. type: [Number, String],
  78. default: 2050
  79. },
  80. // 可切换的最小年份
  81. minYear: {
  82. type: [Number, String],
  83. default: 1950
  84. },
  85. // 最小可选日期(不在范围内日期禁用不可选)
  86. minDate: {
  87. type: [Number, String],
  88. default: '1950-01-01'
  89. },
  90. /**
  91. * 最大可选日期
  92. * 默认最大值为今天,之后的日期不可选
  93. * 2030-12-31
  94. * */
  95. maxDate: {
  96. type: [Number, String],
  97. default: '2030-12-31'
  98. },
  99. // 弹窗顶部左右两边的圆角值
  100. borderRadius: {
  101. type: [String, Number],
  102. default: 20
  103. },
  104. // 月份切换按钮箭头颜色
  105. monthArrowColor: {
  106. type: String,
  107. default: '#606266'
  108. },
  109. // 年份切换按钮箭头颜色
  110. yearArrowColor: {
  111. type: String,
  112. default: '#909399'
  113. },
  114. // 默认日期字体颜色
  115. color: {
  116. type: String,
  117. default: '#303133'
  118. },
  119. // 选中|起始结束日期背景色
  120. activeBgColor: {
  121. type: String,
  122. default: '#19be6b'
  123. },
  124. // 选中|起始结束日期字体颜色
  125. activeColor: {
  126. type: String,
  127. default: '#ffffff'
  128. },
  129. // 范围内日期背景色
  130. rangeBgColor: {
  131. type: String,
  132. default: 'rgba(41,121,255,0.13)'
  133. },
  134. // 当前选中日期带选中效果
  135. isActiveCurrent: {
  136. type: Boolean,
  137. default: true
  138. },
  139. // 切换年月是否触发事件 mode=date时生效
  140. isChange: {
  141. type: Boolean,
  142. default: false
  143. }
  144. },
  145. data() {
  146. return {
  147. // 星期几,值为1-7
  148. weekday: 1,
  149. weekdayArr:[],
  150. // 当前月有多少天
  151. days: 0,
  152. daysArr:[],
  153. showTitle: '',
  154. year: 2020,
  155. month: 0,
  156. day: 0,
  157. startYear: 0,
  158. startMonth: 0,
  159. startDay: 0,
  160. endYear: 0,
  161. endMonth: 0,
  162. endDay: 0,
  163. today: '',
  164. activeDate: '',
  165. startDate: '',
  166. endDate: '',
  167. isStart: true,
  168. min: null,
  169. max: null,
  170. weekDayZh: ['日', '一', '二', '三', '四', '五', '六']
  171. };
  172. },
  173. computed: {
  174. dataChange() {
  175. return `${this.mode}-${this.minDate}-${this.maxDate}`;
  176. },
  177. uZIndex() {
  178. // 如果用户有传递z-index值,优先使用
  179. return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
  180. }
  181. },
  182. watch: {
  183. dataChange(val) {
  184. this.init()
  185. }
  186. },
  187. created() {
  188. this.init()
  189. },
  190. methods: {
  191. getColor(index, type) {
  192. let color = type == 1 ? '' : this.color;
  193. let day = index + 1
  194. let date = `${this.year}-${this.month}-${day}`
  195. let timestamp = new Date(date.replace(/\-/g, '/')).getTime();
  196. let start = this.startDate.replace(/\-/g, '/')
  197. let end = this.endDate.replace(/\-/g, '/')
  198. if ((this.isActiveCurrent && this.activeDate == date) || this.startDate == date || this.endDate == date) {
  199. color = type == 1 ? this.activeBgColor : this.activeColor;
  200. } else if (this.endDate && timestamp > new Date(start).getTime() && timestamp < new Date(end).getTime()) {
  201. color = type == 1 ? this.rangeBgColor : this.rangeColor;
  202. }
  203. return color;
  204. },
  205. init() {
  206. let now = new Date();
  207. this.year = now.getFullYear();
  208. this.month = now.getMonth() + 1;
  209. this.day = now.getDate();
  210. this.today = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`;
  211. this.activeDate = this.today;
  212. this.min = this.initDate(this.minDate);
  213. this.max = this.initDate(this.maxDate || this.today);
  214. this.startDate = "";
  215. this.startYear = 0;
  216. this.startMonth = 0;
  217. this.startDay = 0;
  218. this.endYear = 0;
  219. this.endMonth = 0;
  220. this.endDay = 0;
  221. this.endDate = "";
  222. this.isStart = true;
  223. this.changeData();
  224. },
  225. //日期处理
  226. initDate(date) {
  227. let fdate = date.split('-');
  228. return {
  229. year: Number(fdate[0] || 1920),
  230. month: Number(fdate[1] || 1),
  231. day: Number(fdate[2] || 1)
  232. }
  233. },
  234. openDisAbled: function(year, month, day) {
  235. let bool = true;
  236. let date = `${year}/${month}/${day}`;
  237. // let today = this.today.replace(/\-/g, '/');
  238. let min = `${this.min.year}/${this.min.month}/${this.min.day}`;
  239. let max = `${this.max.year}/${this.max.month}/${this.max.day}`;
  240. let timestamp = new Date(date).getTime();
  241. if (timestamp >= new Date(min).getTime() && timestamp <= new Date(max).getTime()) {
  242. bool = false;
  243. }
  244. return bool;
  245. },
  246. generateArray: function(start, end) {
  247. return Array.from(new Array(end + 1).keys()).slice(start);
  248. },
  249. formatNum: function(num) {
  250. return num < 10 ? '0' + num : num + '';
  251. },
  252. //一个月有多少天
  253. getMonthDay(year, month) {
  254. let days = new Date(year, month, 0).getDate();
  255. return days;
  256. },
  257. getWeekday(year, month) {
  258. let date = new Date(`${year}/${month}/01 00:00:00`);
  259. return date.getDay();
  260. },
  261. checkRange(year) {
  262. let overstep = false;
  263. if (year < this.minYear || year > this.maxYear) {
  264. uni.showToast({
  265. title: "日期超出范围啦~",
  266. icon: 'none'
  267. })
  268. overstep = true;
  269. }
  270. return overstep;
  271. },
  272. changeMonthHandler(isAdd) {
  273. if (isAdd) {
  274. let month = this.month + 1;
  275. let year = month > 12 ? this.year + 1 : this.year;
  276. if (!this.checkRange(year)) {
  277. this.month = month > 12 ? 1 : month;
  278. this.year = year;
  279. this.changeData();
  280. }
  281. } else {
  282. let month = this.month - 1;
  283. let year = month < 1 ? this.year - 1 : this.year;
  284. if (!this.checkRange(year)) {
  285. this.month = month < 1 ? 12 : month;
  286. this.year = year;
  287. this.changeData();
  288. }
  289. }
  290. },
  291. changeYearHandler(isAdd) {
  292. let year = isAdd ? this.year + 1 : this.year - 1;
  293. if (!this.checkRange(year)) {
  294. this.year = year;
  295. this.changeData();
  296. }
  297. },
  298. changeData() {
  299. this.days = this.getMonthDay(this.year, this.month);
  300. this.daysArr=this.generateArray(1,this.days)
  301. this.weekday = this.getWeekday(this.year, this.month);
  302. this.weekdayArr=this.generateArray(1,this.weekday)
  303. this.showTitle = `${this.year}年${this.month}月`;
  304. if (this.isChange && this.mode == 'date') {
  305. this.btnFix(true);
  306. }
  307. },
  308. dateClick: function(day) {
  309. day += 1;
  310. if (!this.openDisAbled(this.year, this.month, day)) {
  311. this.day = day;
  312. let date = `${this.year}-${this.month}-${day}`;
  313. if (this.mode == 'date') {
  314. this.activeDate = date;
  315. } else {
  316. let compare = new Date(date.replace(/\-/g, '/')).getTime() < new Date(this.startDate.replace(/\-/g, '/')).getTime()
  317. if (this.isStart || compare) {
  318. this.startDate = date;
  319. this.startYear = this.year;
  320. this.startMonth = this.month;
  321. this.startDay = this.day;
  322. this.endYear = 0;
  323. this.endMonth = 0;
  324. this.endDay = 0;
  325. this.endDate = "";
  326. this.activeDate = "";
  327. this.isStart = false;
  328. } else {
  329. this.endDate = date;
  330. this.endYear = this.year;
  331. this.endMonth = this.month;
  332. this.endDay = this.day;
  333. this.isStart = true;
  334. }
  335. }
  336. }
  337. },
  338. close() {
  339. // 修改通过v-model绑定的父组件变量的值为false,从而隐藏日历弹窗
  340. this.$emit('input', false);
  341. },
  342. getWeekText(date) {
  343. date = new Date(`${date.replace(/\-/g, '/')} 00:00:00`);
  344. let week = date.getDay();
  345. return '星期' + ['日', '一', '二', '三', '四', '五', '六'][week];
  346. },
  347. btnFix(show) {
  348. if (!show) {
  349. this.close();
  350. }
  351. if (this.mode == 'date') {
  352. let arr = this.activeDate.split('-')
  353. let year = this.isChange ? this.year : Number(arr[0]);
  354. let month = this.isChange ? this.month : Number(arr[1]);
  355. let day = this.isChange ? this.day : Number(arr[2]);
  356. //当前月有多少天
  357. let days = this.getMonthDay(year, month);
  358. let result = `${year}-${this.formatNum(month)}-${this.formatNum(day)}`;
  359. let weekText = this.getWeekText(result);
  360. let isToday = false;
  361. if (`${year}-${month}-${day}` == this.today) {
  362. //今天
  363. isToday = true;
  364. }
  365. this.$emit('change', {
  366. year: year,
  367. month: month,
  368. day: day,
  369. days: days,
  370. result: result,
  371. week: weekText,
  372. isToday: isToday,
  373. // switch: show //是否是切换年月操作
  374. });
  375. } else {
  376. if (!this.startDate || !this.endDate) return;
  377. let startMonth = this.formatNum(this.startMonth);
  378. let startDay = this.formatNum(this.startDay);
  379. let startDate = `${this.startYear}-${startMonth}-${startDay}`;
  380. let startWeek = this.getWeekText(startDate)
  381. let endMonth = this.formatNum(this.endMonth);
  382. let endDay = this.formatNum(this.endDay);
  383. let endDate = `${this.endYear}-${endMonth}-${endDay}`;
  384. let endWeek = this.getWeekText(endDate);
  385. this.$emit('change', {
  386. startYear: this.startYear,
  387. startMonth: this.startMonth,
  388. startDay: this.startDay,
  389. startDate: startDate,
  390. startWeek: startWeek,
  391. endYear: this.endYear,
  392. endMonth: this.endMonth,
  393. endDay: this.endDay,
  394. endDate: endDate,
  395. endWeek: endWeek
  396. });
  397. }
  398. }
  399. }
  400. };
  401. </script>
  402. <style scoped lang="scss">
  403. // 定义混入指令,用于在非nvue环境下的flex定义,因为nvue没有display属性,会报错
  404. @mixin vue-flex($direction: row) {
  405. /* #ifndef APP-NVUE */
  406. display: flex;
  407. flex-direction: $direction;
  408. /* #endif */
  409. }
  410. .u-calendar {
  411. color: $u-content-color;
  412. &__header {
  413. width: 100%;
  414. box-sizing: border-box;
  415. font-size: 30rpx;
  416. background-color: #fff;
  417. color: $u-main-color;
  418. &__text {
  419. margin-top: 30rpx;
  420. padding: 0 60rpx;
  421. @include vue-flex;
  422. justify-content: center;
  423. align-items: center;
  424. }
  425. }
  426. &__action {
  427. padding: 40rpx 0 40rpx 0;
  428. position: relative;
  429. &__icon {
  430. margin: 0 16rpx;
  431. }
  432. &__text {
  433. padding: 0 16rpx;
  434. color: $u-main-color;
  435. font-size: 32rpx;
  436. line-height: 32rpx;
  437. font-weight: bold;
  438. }
  439. }
  440. &__week-day {
  441. @include vue-flex;
  442. align-items: center;
  443. justify-content: center;
  444. padding: 6px 0;
  445. overflow: hidden;
  446. &__text {
  447. flex: 1;
  448. text-align: center;
  449. }
  450. }
  451. &__content {
  452. width: 100%;
  453. @include vue-flex;
  454. flex-wrap: wrap;
  455. padding: 6px 0;
  456. box-sizing: border-box;
  457. background-color: #fff;
  458. position: relative;
  459. &--end-date {
  460. border-top-right-radius: 8rpx;
  461. border-bottom-right-radius: 8rpx;
  462. }
  463. &--start-date {
  464. border-top-left-radius: 8rpx;
  465. border-bottom-left-radius: 8rpx;
  466. }
  467. &__item {
  468. width: 14.2857%;
  469. @include vue-flex;
  470. align-items: center;
  471. justify-content: center;
  472. padding: 6px 0;
  473. overflow: hidden;
  474. position: relative;
  475. z-index: 2;
  476. &__inner {
  477. height: 84rpx;
  478. @include vue-flex;
  479. align-items: center;
  480. justify-content: center;
  481. flex-direction: column;
  482. font-size: 32rpx;
  483. position: relative;
  484. border-radius: 50%;
  485. &__desc {
  486. width: 100%;
  487. font-size: 24rpx;
  488. line-height: 24rpx;
  489. transform: scale(0.75);
  490. transform-origin: center center;
  491. position: absolute;
  492. left: 0;
  493. text-align: center;
  494. bottom: 2rpx;
  495. }
  496. }
  497. &__tips {
  498. width: 100%;
  499. font-size: 24rpx;
  500. line-height: 24rpx;
  501. position: absolute;
  502. left: 0;
  503. transform: scale(0.8);
  504. transform-origin: center center;
  505. text-align: center;
  506. bottom: 8rpx;
  507. z-index: 2;
  508. }
  509. }
  510. &__bg-month {
  511. position: absolute;
  512. font-size: 130px;
  513. line-height: 130px;
  514. left: 50%;
  515. top: 50%;
  516. transform: translate(-50%, -50%);
  517. color: #e4e7ed;
  518. z-index: 1;
  519. }
  520. }
  521. &__bottom {
  522. width: 100%;
  523. @include vue-flex;
  524. align-items: center;
  525. justify-content: center;
  526. flex-direction: column;
  527. background-color: #fff;
  528. padding: 0 40rpx 30rpx;
  529. box-sizing: border-box;
  530. font-size: 24rpx;
  531. color: $u-tips-color;
  532. &__choose {
  533. height: 50rpx;
  534. }
  535. &__btn {
  536. width: 100%;
  537. }
  538. }
  539. .u-calendar__action__back {
  540. position: absolute;
  541. color: #fff;
  542. font-size: 24rpx;
  543. right: -10rpx;
  544. background-color: #19be6b;
  545. padding: 5rpx 12rpx;
  546. border-radius: 20rpx 0 0 20rpx;
  547. }
  548. }
  549. </style>