zsy-calendar.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. <template>
  2. <!-- 日历滚动插件 -->
  3. <view class="zsy_calendar">
  4. <!-- 日历顶部信息 -->
  5. <view class="calendar_info">
  6. <text class="title">打卡记录</text>
  7. <text class="desc">
  8. [{{ getAssignDateInfo(false, 0) === getAssignDateInfo(true, 0) ? '' : getAssignDateInfo(false, 0) + '年' }}{{ getAssignDateInfo(false, 1) }}月]
  9. </text>
  10. <text v-show="showBackToTodayBtn" class="backToToday" :style="{color: dateActiveColor}"
  11. @tap="goToDate()">回到今天</text>
  12. </view>
  13. <!-- 日历周数 -->
  14. <view class="calendar_week">
  15. <view v-for="(item, index) in week" :key="index" class="calendar_week__item">{{ item }}</view>
  16. </view>
  17. <!-- 日历轮播 -->
  18. <view class="calendar_swiper">
  19. <!-- 展开情况下的日历轮播 -->
  20. <swiper v-if="swiperMode === 'open'" key="normalSwiper" circular :style="{height: swiperHeight('open')}"
  21. :current="current" :duration="duration" :skip-hidden-item-layout="true"
  22. @change="e => current = e.detail.current">
  23. <swiper-item v-for="(swiper, swiperIndex) in 3" :key="swiperIndex" class="swiper-item">
  24. <DateBox :dates="calendarSwiperDates[swiperIndex]" :cellHeight="cellHeight"
  25. :dateActiveColor="dateActiveColor" :swiperMode="swiperMode" @chooseDate="chooseDate" />
  26. </swiper-item>
  27. </swiper>
  28. </view>
  29. </view>
  30. </template>
  31. <script>
  32. import {
  33. mapState,
  34. mapMutations
  35. } from 'vuex'
  36. import {
  37. parseTime,
  38. deepClone
  39. } from './js/utils.js'
  40. import DateBox from './dateBox.vue'
  41. export default {
  42. name: 'ZsyCalendar',
  43. components: {
  44. DateBox
  45. },
  46. props: {
  47. duration: { // 轮播图动画时长
  48. type: Number,
  49. default: 300
  50. },
  51. cellHeight: { // 一列的高度
  52. type: Number,
  53. default: 75
  54. },
  55. dateActiveColor: { // 日期选中颜色
  56. type: String,
  57. default: '#4169E1'
  58. },
  59. sundayIndex: { // 星期天所在索引,0表示第一个、6表示最后一个
  60. type: Number,
  61. default: 6
  62. },
  63. mode: { // 日历模式
  64. type: String,
  65. default: 'open'
  66. },
  67. changeSetDefault: { // 月份切换时是否显示一号还是当前月份选中高亮
  68. type: Boolean,
  69. default: true
  70. }
  71. },
  72. data() {
  73. return {
  74. today: parseTime(new Date(), '{y}-{m}-{d}'), // 今天日期
  75. selectedDate: null, // 选中日期
  76. week: [], // 日历周数
  77. current: 1, // 当前日历轮播默认显示索引
  78. // shrinkCurrent: 1, // 缩放日历轮播默认显示索引
  79. calendarSwiperDates: [], // 日历轮播日期信息
  80. calendarSwiperShrinkDates: [], // 日历轮播收缩时的日期信息
  81. dateActive: -1, // 日期选中索引
  82. swiperByClick: false, // 是否通过点击上月份或下月份的日期进行轮播切换
  83. shrinkSwiperByClick: false, // 是否通过点击上月份或下月份的日期进行收缩日历的轮播切换
  84. swiperMode: this.mode, // 日历轮播显示模式 open:展开 close:收缩
  85. dateCache: {}, // 日期缓存
  86. emitTimer: null, // 日期改变向父级传递当前选中日期计时器
  87. dateClick: false, // 是否进行了日期的点击选择
  88. }
  89. },
  90. computed: {
  91. ...mapState('m_children', ['records']),
  92. /* 获取指定日期信息
  93. isCurDate: 是否获取当天的信息还是选中日期的信息
  94. index: 0 表示年份 1 表示月份 2 表示日期 */
  95. getAssignDateInfo() {
  96. return (isCurDate, index) => {
  97. return (isCurDate ? this.today : this.selectedDate).split('-')[index] * 1
  98. }
  99. },
  100. // 是否显示回到今天按钮
  101. showBackToTodayBtn() {
  102. return this.getAssignDateInfo(false, 0) !== this.getAssignDateInfo(true, 0) || this.getAssignDateInfo(
  103. false, 1) !== this.getAssignDateInfo(true, 1)
  104. },
  105. // 返回轮播图高度
  106. swiperHeight() {
  107. return (swiperMode) => {
  108. const normalHeight = (this.calendarSwiperDates[this.current] || []).length / 7 * (this.cellHeight +
  109. 20) + 'rpx'
  110. const shrinkHeight = this.cellHeight + 20 + 'rpx'
  111. return swiperMode === 'open' ? normalHeight : shrinkHeight
  112. }
  113. }
  114. },
  115. watch: {
  116. // 展开日历轮播切换
  117. current(newV, oldV) {
  118. if (newV === 0 && oldV === 2) { // 右滑
  119. this.swiperChange(1)
  120. return
  121. }
  122. if (newV === 2 && oldV === 0) { // 左滑
  123. this.swiperChange(-1)
  124. return
  125. }
  126. if (newV > oldV) { // 右滑
  127. this.swiperChange(1)
  128. } else { // 左滑
  129. this.swiperChange(-1)
  130. }
  131. },
  132. selectedDate: {
  133. deep: true,
  134. handler(newV, oldV) {
  135. if (newV && (oldV === null || this.dateClick)) { // 初始化/日历点击选择时直接返回
  136. this.emitDate()
  137. this.dateClick = false
  138. } else { // 其它情况做防抖处理
  139. if (this.emitTimer !== null) {
  140. clearTimeout(this.emitTimer)
  141. }
  142. this.emitTimer = setTimeout(() => {
  143. this.emitDate()
  144. }, this.duration + 1)
  145. }
  146. }
  147. }
  148. },
  149. created() {
  150. this.init() // 初始化数据
  151. },
  152. methods: {
  153. ...mapMutations('m_children', ['updateNoAttendNum']),
  154. // 初始化数据
  155. init() {
  156. if (this.selectedDate === null) this.selectedDate = this.today // 默认选中日期为当天
  157. this.initWeek() // 初始化要显示的周数
  158. this.initCalendarSwiperDates() // 初始化日历轮播日期信息
  159. // 解决swiperMode初始化为收缩时没有初始化日历收缩轮播日期信息
  160. },
  161. // 初始化周数
  162. initWeek() {
  163. const normalWeek = ['日', '一', '二', '三', '四', '五', '六'] // 正常周数
  164. const sIndex = this.sundayIndex < 0 ? 0 : this.sundayIndex >= normalWeek.length ? normalWeek.length - 1 :
  165. this.sundayIndex
  166. normalWeek.unshift(...normalWeek.slice(-sIndex))
  167. normalWeek.length = 7
  168. this.week = normalWeek
  169. },
  170. // 初始化展开时的日历轮播日期信息
  171. initCalendarSwiperDates(cb) {
  172. const year = this.getAssignDateInfo(false, 0)
  173. const month = this.getAssignDateInfo(false, 1)
  174. const cur = this.generateCalendar(year, month)
  175. const prev = this.generateCalendar(month === 1 ? year - 1 : year, month === 1 ? 12 : month - 1)
  176. const next = this.generateCalendar(month === 12 ? year + 1 : year, month === 12 ? 1 : month + 1)
  177. // 根据current来判断相邻的轮播存放哪些日历数据
  178. if (this.current === 0) {
  179. this.calendarSwiperDates = [cur, next, prev]
  180. } else if (this.current === 1) {
  181. this.calendarSwiperDates = [prev, cur, next]
  182. } else if (this.current === 2) {
  183. this.calendarSwiperDates = [next, prev, cur]
  184. }
  185. this.swiperByClick = false
  186. // 初始化日期信息完毕执行回调函数
  187. cb && cb()
  188. },
  189. // 生成展开的日历数据
  190. generateCalendar(year, month) {
  191. let calendarDate = []
  192. // 先获取缓存里面有没有该月的日期数据
  193. if (this.dateCache[`${year}-${month}`]) {
  194. calendarDate = deepClone(this.dateCache[`${year}-${month}`])
  195. } else { // 进行月份日期的计算
  196. const monthDates = new Date(year, month, 0).getDate() // 获取此月份总天数
  197. const normalWeek = ['一', '二', '三', '四', '五', '六', '日'] // 正常周数
  198. const monthFirstDay = normalWeek[new Date(year, month - 1, 0).getDay()] // 获取本月一号为星期几
  199. const monthFirstDayIndex = this.week.indexOf(monthFirstDay) // 计算本月一号在日历周数中的索引,索引之前的填充上个月的后几天
  200. // 本月一号在日历中不是第一个位置,需要进行填充
  201. if (monthFirstDayIndex !== 0) {
  202. const prevMonthDates = new Date(year, month - 1, 0).getDate() // 获取上一个月份的总天数
  203. // 填充本月一号之前的数据
  204. for (let i = 0; i < monthFirstDayIndex; i++) {
  205. const item = {
  206. year: month === 1 ? year - 1 : year,
  207. month: month === 1 ? 12 : month - 1,
  208. date: prevMonthDates - i,
  209. type: 'prev',
  210. isAttend: 0,//未打卡
  211. }
  212. this.theDateIsToday(item)
  213. calendarDate.unshift(item)
  214. }
  215. }
  216. // 循环生成当月所有日期
  217. for (let i = 1; i <= monthDates; i++) {
  218. let item = {
  219. year,
  220. month,
  221. date: i,
  222. isSelected: false,
  223. isToday: false,
  224. type: 'cur',
  225. isAttend: 0, //未打卡
  226. }
  227. // 今天的日期在不在里面
  228. this.theDateIsToday(item)
  229. calendarDate.push(item)
  230. }
  231. const residue = calendarDate.length % 7
  232. // 判断是否需要填充下个月的前几天
  233. if (residue !== 0) {
  234. for (let i = 1; i <= 7 - residue; i++) {
  235. const item = {
  236. year: month === 12 ? year + 1 : year,
  237. month: month === 12 ? 1 : month + 1,
  238. date: i,
  239. type: 'next',
  240. isAttend: 0, //未打卡
  241. }
  242. this.theDateIsToday(item)
  243. calendarDate.push(item)
  244. }
  245. }
  246. this.dateCache[`${year}-${month}`] = deepClone(calendarDate)
  247. }
  248. // 进行日期的默认选中
  249. if (year === this.getAssignDateInfo(false, 0) && month === this.getAssignDateInfo(false, 1)) {
  250. for (let i = 0, len = calendarDate.length; i < len; i++) {
  251. if (calendarDate[i].type === 'cur' && calendarDate[i].date === this.getAssignDateInfo(false, 2)) {
  252. calendarDate[i].isSelected = true
  253. this.dateActive = i
  254. break
  255. }
  256. }
  257. }
  258. //当月时间
  259. let currentMonth = (new Date).getMonth() + 1;
  260. //判断是否打卡和时间对比并修改数据
  261. let todayDate = Date.now() //毫秒数
  262. calendarDate.forEach(item => {
  263. if (this.records.find(x => x.month === item.month && x.year === item.year && x.date === item
  264. .date)) {
  265. item.isAttend = 1 //已打卡
  266. }
  267. //判断是否在当前日期之后
  268. let valueTime = (new Date(`${item.year}/${item.month}/${item.date}`)).getTime();
  269. if (valueTime > todayDate) {
  270. item.isAttend = -1 //时间未到
  271. }
  272. //判断是否为周末
  273. // if(){}
  274. })
  275. return calendarDate
  276. },
  277. // 判断日期是否为今天
  278. theDateIsToday(item) {
  279. if (item.year + '-' + item.month + '-' + item.date === this.getAssignDateInfo(true, 0) + '-' + this
  280. .getAssignDateInfo(true, 1) + '-' + this.getAssignDateInfo(true, 2)) {
  281. item.isToday = true
  282. }
  283. },
  284. // 展开日历轮播切换
  285. swiperChange(type) {
  286. // 通过点击上个月/下个月日期进行切换,不需要默认选中下个月的一号,直接选中点击的那个日期
  287. if (!this.swiperByClick && this.swiperMode === 'open') {
  288. this.getPrevOrNextDate(type)
  289. }
  290. setTimeout(() => { // 设置定时器是为了防止轮播切换时生成数据造成页面卡顿
  291. this.initCalendarSwiperDates(() => {
  292. this.swiperMode === 'close'
  293. }) // 初始化日历轮播日期信息
  294. }, this.swiperMode === 'open' ? this.duration : 0)
  295. },
  296. // 获取上一个月/下一个月的一号日期
  297. getPrevOrNextDate(type) {
  298. const year = this.getAssignDateInfo(false, 0)
  299. let month = this.getAssignDateInfo(false, 1)
  300. month = month + type
  301. // 判断切换月份时选中当前日期高亮还是一号,若选中当前日期高亮需进行大小判断
  302. const curActiveDate = this.getAssignDateInfo(false, 2)
  303. const maxDate = new Date(year, month, 0).getDate()
  304. const date = this.changeSetDefault ? 1 : curActiveDate > maxDate ? maxDate : curActiveDate
  305. this.selectedDate = parseTime(new Date(year, month - 1, date), '{y}-{m}-{d}')
  306. },
  307. // 获取上个星期/下一星期的开始日期
  308. getPrevOrNextStartDate(type) {
  309. const date = this.calendarSwiperShrinkDates[this.shrinkCurrent][0]
  310. this.selectedDate = parseTime(new Date(date.year, date.month - 1, date.date), '{y}-{m}-{d}')
  311. },
  312. // 前往某一天 格式 YYYY-MM | YYYY-MM-DD
  313. goToDate(date = this.today) {
  314. try {
  315. if (date.split('-').length < 2 || date.split('-').length > 3) throw '参数有误'
  316. if (date.split('-').length === 2) {
  317. date += '-01'
  318. }
  319. } catch (err) {
  320. throw Error('请检查参数是否符合规范')
  321. }
  322. this.selectedDate = date
  323. this.initCalendarSwiperDates(() => {
  324. })
  325. },
  326. // 日历轮播展开的情况下选择日期
  327. chooseDate(dateInfo, dateIndex) {
  328. // 重复点击后续不做处理
  329. if (dateInfo.isSelected) return false
  330. // 是否点击了上个月份的后几天或者点击了下个月份的前几天
  331. if (dateInfo.type !== 'cur') {
  332. if (dateInfo.type === 'prev') { // 点击了上个月份的后几天,滑到上个月
  333. this.current = this.current === 0 ? 2 : this.current - 1
  334. } else { // 点击了下个月份的前几天,滑到下个月
  335. this.current = this.current === 2 ? 0 : this.current + 1
  336. }
  337. // 将选中日期赋值为当前点击的那个日期
  338. this.selectedDate = parseTime(new Date(dateInfo.year, dateInfo.month - 1, dateInfo.date),
  339. '{y}-{m}-{d}')
  340. this.swiperByClick = true
  341. this.$emit('chooseDate', dateInfo, dateIndex)
  342. return false
  343. }
  344. // 将当前选中的日期清空并选中最新的日期
  345. this.calendarSwiperDates[this.current][this.dateActive].isSelected = false
  346. this.dateActive = dateIndex
  347. const date = this.calendarSwiperDates[this.current][this.dateActive]
  348. date.isSelected = true
  349. this.selectedDate = parseTime(new Date(date.year, date.month - 1, date.date), '{y}-{m}-{d}')
  350. this.dateClick = true
  351. this.$emit('chooseDate', dateInfo, dateIndex)
  352. },
  353. // 向父组件传递当前选中数据
  354. emitDate() {
  355. const {
  356. year,
  357. month,
  358. date
  359. } = this.calendarSwiperDates[this.current][this.dateActive]
  360. const e = {
  361. selectedDate: this.selectedDate,
  362. year,
  363. month,
  364. date
  365. }
  366. this.$emit('change', e)
  367. },
  368. }
  369. }
  370. </script>
  371. <style>
  372. .zsy_calendar {
  373. width: 100%;
  374. padding: 30rpx 10rpx 0rpx 10rpx;
  375. box-sizing: border-box;
  376. background-color: #fff;
  377. border-radius: 20rpx;
  378. }
  379. /* 日历顶部信息 */
  380. .calendar_info {
  381. display: flex;
  382. align-items: center;
  383. padding: 0 20rpx;
  384. }
  385. .calendar_info .title {
  386. font-size: 32rpx;
  387. font-weight: bold;
  388. color: $color-title;
  389. }
  390. .calendar_info .desc {
  391. margin-left: 29rpx;
  392. font-size: 28rpx;
  393. color: #959595;
  394. }
  395. .calendar_info .backToToday {
  396. margin-left: auto;
  397. font-size: 26rpx;
  398. }
  399. /* 日历顶部信息 */
  400. /* 日历周数 */
  401. .calendar_week {
  402. display: flex;
  403. align-items: center;
  404. justify-content: space-between;
  405. font-size: 26rpx;
  406. color: #959595;
  407. margin: 20rpx 0rpx;
  408. }
  409. .calendar_week .calendar_week__item {
  410. width: calc(100% / 7);
  411. text-align: center;
  412. }
  413. /* 日历周数 */
  414. /* 日历切换模式 */
  415. .calendar_toggle {
  416. position: relative;
  417. padding: 10rpx 0;
  418. margin: 10rpx 20rpx 0;
  419. display: flex;
  420. justify-content: center;
  421. }
  422. /* .calendar_toggle .icon {
  423. width: 30rpx;
  424. height: 30rpx;
  425. background-image: url('../../subpkg/zsy-calendar/icon/arrow.png');
  426. background-size: contain;
  427. background-repeat: no-repeat;
  428. margin: 0 auto;
  429. transition: all .3s;
  430. }
  431. .icon.down {
  432. transform: rotate(180deg);
  433. } */
  434. .calendar_toggle::before,
  435. .calendar_toggle::after {
  436. width: calc(50% - 30rpx);
  437. border-top: solid 2rpx #EAEAEA;
  438. content: '';
  439. display: block;
  440. position: absolute;
  441. top: 50%;
  442. transform: translateY(-50%);
  443. }
  444. .calendar_toggle::before {
  445. left: 0;
  446. }
  447. .calendar_toggle::after {
  448. right: 0;
  449. }
  450. /* 日历切换模式 */
  451. </style>