Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 

508 Zeilen
13 KiB

  1. <template>
  2. <view class="u-calendar-strip">
  3. <view v-if="!fullCalendar || !innerShowFull">
  4. <view class="u-calendar-strip__header u-border-bottom">
  5. <text
  6. class="u-calendar-strip__header__switch"
  7. :class="{ 'u-calendar-strip__header__switch--disabled': switchPrevDisabled }"
  8. @tap="prevMonth"
  9. >‹</text>
  10. <text class="u-calendar-strip__header__title">{{ monthLabel }}</text>
  11. <text
  12. class="u-calendar-strip__header__switch"
  13. :class="{ 'u-calendar-strip__header__switch--disabled': switchNextDisabled }"
  14. @tap="nextMonth"
  15. >›</text>
  16. <text
  17. v-if="fullCalendar"
  18. class="u-calendar-strip__header__toggle"
  19. @click="toggleFull('button')"
  20. >▾</text>
  21. </view>
  22. <view
  23. class="u-calendar-strip__scroll-wrap"
  24. @touchstart="onTouchStart"
  25. @touchend="onTouchEnd"
  26. >
  27. <scroll-view
  28. class="u-calendar-strip__scroll"
  29. scroll-x
  30. enable-flex
  31. :show-scrollbar="false"
  32. :scroll-into-view="scrollIntoView"
  33. >
  34. <view class="u-calendar-strip__scroll__inner">
  35. <view
  36. v-for="(item, index) in monthDays"
  37. :key="index"
  38. class="u-calendar-strip__day"
  39. :id="getDateId(item.date)"
  40. :class="[
  41. item.disabled && 'u-calendar-strip__day--disabled',
  42. item.selected && 'u-calendar-strip__day--selected',
  43. item.today && showToday && !item.selected && 'u-calendar-strip__day--today',
  44. ]"
  45. :style="[dayStyle(item)]"
  46. @tap="onDayTap(item)"
  47. >
  48. <text class="u-calendar-strip__day__date">{{ item.day }}</text>
  49. <text class="u-calendar-strip__day__week">{{ getWeekLabel(item.week) }}</text>
  50. </view>
  51. </view>
  52. </scroll-view>
  53. </view>
  54. </view>
  55. <view
  56. v-if="fullCalendar && innerShowFull"
  57. class="u-calendar-strip__panel-wrap"
  58. @touchstart="onTouchStart"
  59. @touchend="onTouchEnd"
  60. >
  61. <view class="u-calendar-strip__panel">
  62. <u-calendar
  63. :show="true"
  64. :pageInline="true"
  65. :showTitle="false"
  66. :showConfirm="false"
  67. :closeOnClickOverlay="false"
  68. :monthSwitch="true"
  69. mode="single"
  70. :defaultDate="innerSelectedDate"
  71. :minDate="panelMinDate"
  72. :maxDate="panelMaxDate"
  73. :monthNum="panelMonthNum"
  74. :readonly="readonly"
  75. :showToday="showToday"
  76. :color="color"
  77. v-bind="fullCalendarProps"
  78. @confirm="onPanelConfirm"
  79. ></u-calendar>
  80. </view>
  81. <view class="u-calendar-strip__hint u-calendar-strip__hint--panel">
  82. <text class="u-calendar-strip__hint__text" @click="toggleFull('hint')">{{ collapseHint }}</text>
  83. </view>
  84. </view>
  85. </view>
  86. </template>
  87. <script>
  88. import { props } from './props'
  89. import { mpMixin } from '../../libs/mixin/mpMixin'
  90. import { mixin } from '../../libs/mixin/mixin'
  91. import dayjs from '../u-datetime-picker/dayjs.esm.min.js'
  92. import test from '../../libs/function/test'
  93. /**
  94. * CalendarStrip 单行日历
  95. * @description 单行横向日期日历,支持切月、下拉展开完整月历
  96. */
  97. export default {
  98. name: 'u-calendar-strip',
  99. mixins: [mpMixin, mixin, props],
  100. data() {
  101. return {
  102. innerSelectedDate: '',
  103. currentMonth: '',
  104. scrollIntoView: '',
  105. innerShowFull: false,
  106. touchStartX: 0,
  107. touchStartY: 0
  108. }
  109. },
  110. computed: {
  111. innerMaxDate() {
  112. return test.number(this.maxDate) ? Number(this.maxDate) : this.maxDate
  113. },
  114. innerMinDate() {
  115. return test.number(this.minDate) ? Number(this.minDate) : this.minDate
  116. },
  117. rangeChange() {
  118. return [this.innerMinDate, this.innerMaxDate]
  119. },
  120. hasMaxDate() {
  121. return !!this.innerMaxDate && dayjs(this.innerMaxDate).isValid()
  122. },
  123. hasMinDate() {
  124. return !!this.innerMinDate && dayjs(this.innerMinDate).isValid()
  125. },
  126. minDateDay() {
  127. if (!this.hasMinDate) return ''
  128. return dayjs(this.innerMinDate).format('YYYY-MM-DD')
  129. },
  130. maxDateDay() {
  131. if (!this.hasMaxDate) return ''
  132. return dayjs(this.innerMaxDate).format('YYYY-MM-DD')
  133. },
  134. todayDate() {
  135. return dayjs().format('YYYY-MM-DD')
  136. },
  137. monthLabel() {
  138. if (!this.currentMonth) return ''
  139. const date = dayjs(`${this.currentMonth}-01`)
  140. if (this.monthFormat) return date.format(this.monthFormat)
  141. if (uni.getLocale() == 'zh-Hans' || uni.getLocale() == 'zh-Hant') {
  142. return date.format('YYYY年MM月')
  143. }
  144. return date.format('MM/YYYY')
  145. },
  146. monthDays() {
  147. if (!this.currentMonth) return []
  148. const start = dayjs(`${this.currentMonth}-01`)
  149. const days = start.daysInMonth()
  150. return new Array(days).fill(0).map((item, index) => {
  151. const date = start.date(index + 1).format('YYYY-MM-DD')
  152. return {
  153. day: index + 1,
  154. date,
  155. week: start.date(index + 1).day(),
  156. disabled: this.isDateDisabled(date),
  157. selected: this.dateSame(date, this.innerSelectedDate),
  158. today: this.dateSame(date, this.todayDate)
  159. }
  160. })
  161. },
  162. switchPrevDisabled() {
  163. if (!this.hasMinDate || !this.currentMonth) return false
  164. const current = dayjs(`${this.currentMonth}-01`)
  165. const minMonth = dayjs(this.minDateDay).startOf('month')
  166. return current.isSame(minMonth, 'month') || current.isBefore(minMonth, 'month')
  167. },
  168. switchNextDisabled() {
  169. if (!this.hasMaxDate || !this.currentMonth) return false
  170. const current = dayjs(`${this.currentMonth}-01`)
  171. const maxMonth = dayjs(this.maxDateDay).startOf('month')
  172. return current.isSame(maxMonth, 'month') || current.isAfter(maxMonth, 'month')
  173. },
  174. panelMinDate() {
  175. if (this.hasMinDate) return this.minDateDay
  176. const monthNum = Math.max(1, Number(this.fullMonthNum) || 24)
  177. const anchor = this.currentMonth || dayjs().format('YYYY-MM')
  178. return dayjs(`${anchor}-01`).subtract(monthNum - 1, 'month').startOf('month').format('YYYY-MM-DD')
  179. },
  180. panelMaxDate() {
  181. if (this.hasMaxDate) return this.maxDateDay
  182. const monthNum = Math.max(1, Number(this.fullMonthNum) || 24)
  183. const anchor = this.currentMonth || dayjs().format('YYYY-MM')
  184. return dayjs(`${anchor}-01`).add(monthNum - 1, 'month').endOf('month').format('YYYY-MM-DD')
  185. },
  186. panelMonthNum() {
  187. return this.getMonths(this.panelMinDate, this.panelMaxDate)
  188. },
  189. pullHintText() {
  190. return this.innerShowFull ? this.collapseHint : this.expandHint
  191. }
  192. },
  193. watch: {
  194. modelValue: {
  195. immediate: true,
  196. handler(n) {
  197. this.syncByValue(n, false, 'sync')
  198. }
  199. },
  200. rangeChange() {
  201. this.syncByValue(this.innerSelectedDate || this.modelValue, true, 'range')
  202. }
  203. },
  204. emits: ['change', 'confirm', 'monthChange', 'toggleFull', 'update:modelValue'],
  205. methods: {
  206. dateSame(date1, date2) {
  207. if (!date1 || !date2) return false
  208. return dayjs(date1).isSame(dayjs(date2), 'day')
  209. },
  210. normalizeDate(value) {
  211. if (!value) return ''
  212. const parsed = dayjs(value)
  213. if (!parsed.isValid()) return ''
  214. return parsed.format('YYYY-MM-DD')
  215. },
  216. clampDate(date) {
  217. let next = this.normalizeDate(date)
  218. if (!next) return ''
  219. if (this.hasMinDate && dayjs(next).isBefore(dayjs(this.minDateDay), 'day')) {
  220. next = this.minDateDay
  221. }
  222. if (this.hasMaxDate && dayjs(next).isAfter(dayjs(this.maxDateDay), 'day')) {
  223. next = this.maxDateDay
  224. }
  225. return next
  226. },
  227. isDateDisabled(date) {
  228. if (!date) return true
  229. if (this.hasMinDate && dayjs(date).isBefore(dayjs(this.minDateDay), 'day')) {
  230. return true
  231. }
  232. if (this.hasMaxDate && dayjs(date).isAfter(dayjs(this.maxDateDay), 'day')) {
  233. return true
  234. }
  235. return false
  236. },
  237. getDateId(date) {
  238. return `u-calendar-strip-day-${dayjs(date).format('YYYYMMDD')}`
  239. },
  240. getWeekLabel(week) {
  241. const index = week === 0 ? 6 : week - 1
  242. return this.weekText[index] || ''
  243. },
  244. dayStyle(item) {
  245. const style = {}
  246. if (item.selected) {
  247. style.backgroundColor = this.color
  248. }
  249. if (!item.selected && item.today && this.showToday) {
  250. style.borderColor = this.color
  251. }
  252. return style
  253. },
  254. scrollToDate(date) {
  255. if (!date) {
  256. this.scrollIntoView = ''
  257. return
  258. }
  259. const target = this.getDateId(date)
  260. this.scrollIntoView = ''
  261. this.$nextTick(() => {
  262. this.scrollIntoView = target
  263. })
  264. },
  265. syncByValue(value, emit = false, scene = 'sync') {
  266. let next = this.clampDate(value)
  267. if (!next) {
  268. next = this.clampDate(this.todayDate)
  269. }
  270. if (!next) return
  271. this.setSelectedDate(next, scene, emit)
  272. },
  273. setSelectedDate(date, scene = 'tap', emit = true) {
  274. const next = this.clampDate(date)
  275. if (!next || this.isDateDisabled(next)) return
  276. const prevDate = this.innerSelectedDate
  277. const prevMonth = this.currentMonth
  278. this.innerSelectedDate = next
  279. this.currentMonth = dayjs(next).format('YYYY-MM')
  280. this.scrollToDate(next)
  281. if (!emit) return
  282. const payload = {
  283. date: next,
  284. month: this.currentMonth,
  285. scene
  286. }
  287. if (!this.dateSame(prevDate, next)) {
  288. this.$emit('update:modelValue', next)
  289. }
  290. this.$emit('change', payload)
  291. this.$emit('confirm', payload)
  292. if (prevMonth !== this.currentMonth) {
  293. this.$emit('monthChange', {
  294. month: this.currentMonth,
  295. scene
  296. })
  297. }
  298. },
  299. getMonths(minDate, maxDate) {
  300. const minYear = dayjs(minDate).year()
  301. const minMonth = dayjs(minDate).month() + 1
  302. const maxYear = dayjs(maxDate).year()
  303. const maxMonth = dayjs(maxDate).month() + 1
  304. return Math.max(1, (maxYear - minYear) * 12 + (maxMonth - minMonth) + 1)
  305. },
  306. findFirstEnabledDate(month) {
  307. const start = dayjs(`${month}-01`)
  308. const days = start.daysInMonth()
  309. for (let i = 1; i <= days; i++) {
  310. const date = start.date(i).format('YYYY-MM-DD')
  311. if (!this.isDateDisabled(date)) {
  312. return date
  313. }
  314. }
  315. return ''
  316. },
  317. switchMonth(step = 0) {
  318. if (step < 0 && this.switchPrevDisabled) return
  319. if (step > 0 && this.switchNextDisabled) return
  320. const baseMonth = this.currentMonth || dayjs(this.todayDate).format('YYYY-MM')
  321. const target = dayjs(`${baseMonth}-01`).add(step, 'month')
  322. const targetMonth = target.format('YYYY-MM')
  323. const selectedDay = dayjs(this.innerSelectedDate || this.todayDate).date()
  324. const dayInTargetMonth = Math.min(selectedDay, target.daysInMonth())
  325. let next = target.date(dayInTargetMonth).format('YYYY-MM-DD')
  326. next = this.clampDate(next)
  327. if (!next || !dayjs(next).isSame(target, 'month') || this.isDateDisabled(next)) {
  328. next = this.findFirstEnabledDate(targetMonth)
  329. }
  330. if (!next) return
  331. this.setSelectedDate(next, 'switch', true)
  332. },
  333. prevMonth() {
  334. this.switchMonth(-1)
  335. },
  336. nextMonth() {
  337. this.switchMonth(1)
  338. },
  339. onDayTap(item) {
  340. if (this.readonly || item.disabled) return
  341. this.setSelectedDate(item.date, 'tap', true)
  342. },
  343. setFullVisible(show, source = 'button') {
  344. if (!this.fullCalendar) return
  345. if (this.innerShowFull === show) return
  346. this.innerShowFull = show
  347. this.$emit('toggleFull', {
  348. show,
  349. source
  350. })
  351. },
  352. toggleFull(source = 'button') {
  353. this.setFullVisible(!this.innerShowFull, source)
  354. },
  355. onPanelConfirm(e) {
  356. if (!Array.isArray(e) || !e.length) return
  357. this.setSelectedDate(e[0], 'full', true)
  358. if (this.collapseAfterSelect) {
  359. this.setFullVisible(false, 'auto')
  360. }
  361. },
  362. onTouchStart(event) {
  363. if (!this.fullCalendar) return
  364. const point = event?.changedTouches?.[0] || event?.touches?.[0]
  365. if (!point) return
  366. this.touchStartX = point.clientX
  367. this.touchStartY = point.clientY
  368. },
  369. onTouchEnd(event) {
  370. if (!this.fullCalendar) return
  371. const point = event?.changedTouches?.[0] || event?.touches?.[0]
  372. if (!point) return
  373. const deltaX = point.clientX - this.touchStartX
  374. const deltaY = point.clientY - this.touchStartY
  375. const threshold = Number(this.pullDownThreshold) || 40
  376. if (Math.abs(deltaY) < threshold || Math.abs(deltaY) <= Math.abs(deltaX)) {
  377. return
  378. }
  379. if (deltaY > 0 && !this.innerShowFull) {
  380. this.setFullVisible(true, 'pull-down')
  381. }
  382. if (deltaY < 0 && this.innerShowFull) {
  383. this.setFullVisible(false, 'pull-up')
  384. }
  385. }
  386. }
  387. }
  388. </script>
  389. <style lang="scss" scoped>
  390. .u-calendar-strip {
  391. background-color: var(--up-card-bg-color, #ffffff);
  392. border-radius: 10px;
  393. margin: 0 8px;
  394. &__header {
  395. @include flex;
  396. align-items: center;
  397. padding: 0 4px;
  398. height: 40px;
  399. &__switch {
  400. width: 40px;
  401. text-align: center;
  402. font-size: 20px;
  403. line-height: 40px;
  404. color: var(--up-main-color, $u-main-color);
  405. &--disabled {
  406. opacity: 0.35;
  407. }
  408. }
  409. &__title {
  410. flex: 1;
  411. text-align: center;
  412. font-size: 14px;
  413. font-weight: bold;
  414. color: var(--up-main-color, $u-main-color);
  415. }
  416. &__toggle {
  417. width: 40px;
  418. text-align: center;
  419. font-size: 18px;
  420. line-height: 40px;
  421. color: var(--up-content-color, $u-content-color);
  422. }
  423. }
  424. &__scroll-wrap {
  425. padding: 10px 0 6px;
  426. }
  427. &__scroll {
  428. &__inner {
  429. @include flex;
  430. padding: 0 8px;
  431. }
  432. }
  433. &__day {
  434. width: 54px;
  435. height: 62px;
  436. margin-right: 8px;
  437. border-radius: 8px;
  438. border: 1px solid transparent;
  439. @include flex;
  440. flex-direction: column;
  441. align-items: center;
  442. justify-content: center;
  443. flex-shrink: 0;
  444. &__date {
  445. font-size: 18px;
  446. line-height: 22px;
  447. color: var(--up-main-color, $u-main-color);
  448. }
  449. &__week {
  450. margin-top: 4px;
  451. font-size: 12px;
  452. line-height: 16px;
  453. color: var(--up-content-color, $u-content-color);
  454. }
  455. &--selected {
  456. .u-calendar-strip__day__date,
  457. .u-calendar-strip__day__week {
  458. color: #ffffff;
  459. }
  460. }
  461. &--today {
  462. border-style: solid;
  463. }
  464. &--disabled {
  465. opacity: 0.4;
  466. }
  467. }
  468. &__hint {
  469. padding: 4px 0 10px;
  470. text-align: center;
  471. &__text {
  472. font-size: 12px;
  473. color: var(--up-tips-color, $u-tips-color);
  474. }
  475. }
  476. &__panel {
  477. padding: 0 8px 8px;
  478. }
  479. &__panel-wrap {
  480. padding-top: 4px;
  481. }
  482. &__hint--panel {
  483. padding: 0 0 8px;
  484. }
  485. }
  486. </style>