|
- <template>
- <view class="u-calendar-strip">
- <view v-if="!fullCalendar || !innerShowFull">
- <view class="u-calendar-strip__header u-border-bottom">
- <text
- class="u-calendar-strip__header__switch"
- :class="{ 'u-calendar-strip__header__switch--disabled': switchPrevDisabled }"
- @tap="prevMonth"
- >‹</text>
- <text class="u-calendar-strip__header__title">{{ monthLabel }}</text>
- <text
- class="u-calendar-strip__header__switch"
- :class="{ 'u-calendar-strip__header__switch--disabled': switchNextDisabled }"
- @tap="nextMonth"
- >›</text>
- <text
- v-if="fullCalendar"
- class="u-calendar-strip__header__toggle"
- @click="toggleFull('button')"
- >▾</text>
- </view>
- <view
- class="u-calendar-strip__scroll-wrap"
- @touchstart="onTouchStart"
- @touchend="onTouchEnd"
- >
- <scroll-view
- class="u-calendar-strip__scroll"
- scroll-x
- enable-flex
- :show-scrollbar="false"
- :scroll-into-view="scrollIntoView"
- >
- <view class="u-calendar-strip__scroll__inner">
- <view
- v-for="(item, index) in monthDays"
- :key="index"
- class="u-calendar-strip__day"
- :id="getDateId(item.date)"
- :class="[
- item.disabled && 'u-calendar-strip__day--disabled',
- item.selected && 'u-calendar-strip__day--selected',
- item.today && showToday && !item.selected && 'u-calendar-strip__day--today',
- ]"
- :style="[dayStyle(item)]"
- @tap="onDayTap(item)"
- >
- <text class="u-calendar-strip__day__date">{{ item.day }}</text>
- <text class="u-calendar-strip__day__week">{{ getWeekLabel(item.week) }}</text>
- </view>
- </view>
- </scroll-view>
- </view>
- </view>
- <view
- v-if="fullCalendar && innerShowFull"
- class="u-calendar-strip__panel-wrap"
- @touchstart="onTouchStart"
- @touchend="onTouchEnd"
- >
- <view class="u-calendar-strip__panel">
- <u-calendar
- :show="true"
- :pageInline="true"
- :showTitle="false"
- :showConfirm="false"
- :closeOnClickOverlay="false"
- :monthSwitch="true"
- mode="single"
- :defaultDate="innerSelectedDate"
- :minDate="panelMinDate"
- :maxDate="panelMaxDate"
- :monthNum="panelMonthNum"
- :readonly="readonly"
- :showToday="showToday"
- :color="color"
- v-bind="fullCalendarProps"
- @confirm="onPanelConfirm"
- ></u-calendar>
- </view>
- <view class="u-calendar-strip__hint u-calendar-strip__hint--panel">
- <text class="u-calendar-strip__hint__text" @click="toggleFull('hint')">{{ collapseHint }}</text>
- </view>
- </view>
- </view>
- </template>
-
- <script>
- import { props } from './props'
- import { mpMixin } from '../../libs/mixin/mpMixin'
- import { mixin } from '../../libs/mixin/mixin'
- import dayjs from '../u-datetime-picker/dayjs.esm.min.js'
- import test from '../../libs/function/test'
-
- /**
- * CalendarStrip 单行日历
- * @description 单行横向日期日历,支持切月、下拉展开完整月历
- */
- export default {
- name: 'u-calendar-strip',
- mixins: [mpMixin, mixin, props],
- data() {
- return {
- innerSelectedDate: '',
- currentMonth: '',
- scrollIntoView: '',
- innerShowFull: false,
- touchStartX: 0,
- touchStartY: 0
- }
- },
- computed: {
- innerMaxDate() {
- return test.number(this.maxDate) ? Number(this.maxDate) : this.maxDate
- },
- innerMinDate() {
- return test.number(this.minDate) ? Number(this.minDate) : this.minDate
- },
- rangeChange() {
- return [this.innerMinDate, this.innerMaxDate]
- },
- hasMaxDate() {
- return !!this.innerMaxDate && dayjs(this.innerMaxDate).isValid()
- },
- hasMinDate() {
- return !!this.innerMinDate && dayjs(this.innerMinDate).isValid()
- },
- minDateDay() {
- if (!this.hasMinDate) return ''
- return dayjs(this.innerMinDate).format('YYYY-MM-DD')
- },
- maxDateDay() {
- if (!this.hasMaxDate) return ''
- return dayjs(this.innerMaxDate).format('YYYY-MM-DD')
- },
- todayDate() {
- return dayjs().format('YYYY-MM-DD')
- },
- monthLabel() {
- if (!this.currentMonth) return ''
- const date = dayjs(`${this.currentMonth}-01`)
- if (this.monthFormat) return date.format(this.monthFormat)
- if (uni.getLocale() == 'zh-Hans' || uni.getLocale() == 'zh-Hant') {
- return date.format('YYYY年MM月')
- }
- return date.format('MM/YYYY')
- },
- monthDays() {
- if (!this.currentMonth) return []
- const start = dayjs(`${this.currentMonth}-01`)
- const days = start.daysInMonth()
- return new Array(days).fill(0).map((item, index) => {
- const date = start.date(index + 1).format('YYYY-MM-DD')
- return {
- day: index + 1,
- date,
- week: start.date(index + 1).day(),
- disabled: this.isDateDisabled(date),
- selected: this.dateSame(date, this.innerSelectedDate),
- today: this.dateSame(date, this.todayDate)
- }
- })
- },
- switchPrevDisabled() {
- if (!this.hasMinDate || !this.currentMonth) return false
- const current = dayjs(`${this.currentMonth}-01`)
- const minMonth = dayjs(this.minDateDay).startOf('month')
- return current.isSame(minMonth, 'month') || current.isBefore(minMonth, 'month')
- },
- switchNextDisabled() {
- if (!this.hasMaxDate || !this.currentMonth) return false
- const current = dayjs(`${this.currentMonth}-01`)
- const maxMonth = dayjs(this.maxDateDay).startOf('month')
- return current.isSame(maxMonth, 'month') || current.isAfter(maxMonth, 'month')
- },
- panelMinDate() {
- if (this.hasMinDate) return this.minDateDay
- const monthNum = Math.max(1, Number(this.fullMonthNum) || 24)
- const anchor = this.currentMonth || dayjs().format('YYYY-MM')
- return dayjs(`${anchor}-01`).subtract(monthNum - 1, 'month').startOf('month').format('YYYY-MM-DD')
- },
- panelMaxDate() {
- if (this.hasMaxDate) return this.maxDateDay
- const monthNum = Math.max(1, Number(this.fullMonthNum) || 24)
- const anchor = this.currentMonth || dayjs().format('YYYY-MM')
- return dayjs(`${anchor}-01`).add(monthNum - 1, 'month').endOf('month').format('YYYY-MM-DD')
- },
- panelMonthNum() {
- return this.getMonths(this.panelMinDate, this.panelMaxDate)
- },
- pullHintText() {
- return this.innerShowFull ? this.collapseHint : this.expandHint
- }
- },
- watch: {
- modelValue: {
- immediate: true,
- handler(n) {
- this.syncByValue(n, false, 'sync')
- }
- },
- rangeChange() {
- this.syncByValue(this.innerSelectedDate || this.modelValue, true, 'range')
- }
- },
- emits: ['change', 'confirm', 'monthChange', 'toggleFull', 'update:modelValue'],
- methods: {
- dateSame(date1, date2) {
- if (!date1 || !date2) return false
- return dayjs(date1).isSame(dayjs(date2), 'day')
- },
- normalizeDate(value) {
- if (!value) return ''
- const parsed = dayjs(value)
- if (!parsed.isValid()) return ''
- return parsed.format('YYYY-MM-DD')
- },
- clampDate(date) {
- let next = this.normalizeDate(date)
- if (!next) return ''
- if (this.hasMinDate && dayjs(next).isBefore(dayjs(this.minDateDay), 'day')) {
- next = this.minDateDay
- }
- if (this.hasMaxDate && dayjs(next).isAfter(dayjs(this.maxDateDay), 'day')) {
- next = this.maxDateDay
- }
- return next
- },
- isDateDisabled(date) {
- if (!date) return true
- if (this.hasMinDate && dayjs(date).isBefore(dayjs(this.minDateDay), 'day')) {
- return true
- }
- if (this.hasMaxDate && dayjs(date).isAfter(dayjs(this.maxDateDay), 'day')) {
- return true
- }
- return false
- },
- getDateId(date) {
- return `u-calendar-strip-day-${dayjs(date).format('YYYYMMDD')}`
- },
- getWeekLabel(week) {
- const index = week === 0 ? 6 : week - 1
- return this.weekText[index] || ''
- },
- dayStyle(item) {
- const style = {}
- if (item.selected) {
- style.backgroundColor = this.color
- }
- if (!item.selected && item.today && this.showToday) {
- style.borderColor = this.color
- }
- return style
- },
- scrollToDate(date) {
- if (!date) {
- this.scrollIntoView = ''
- return
- }
- const target = this.getDateId(date)
- this.scrollIntoView = ''
- this.$nextTick(() => {
- this.scrollIntoView = target
- })
- },
- syncByValue(value, emit = false, scene = 'sync') {
- let next = this.clampDate(value)
- if (!next) {
- next = this.clampDate(this.todayDate)
- }
- if (!next) return
- this.setSelectedDate(next, scene, emit)
- },
- setSelectedDate(date, scene = 'tap', emit = true) {
- const next = this.clampDate(date)
- if (!next || this.isDateDisabled(next)) return
- const prevDate = this.innerSelectedDate
- const prevMonth = this.currentMonth
- this.innerSelectedDate = next
- this.currentMonth = dayjs(next).format('YYYY-MM')
- this.scrollToDate(next)
- if (!emit) return
- const payload = {
- date: next,
- month: this.currentMonth,
- scene
- }
- if (!this.dateSame(prevDate, next)) {
- this.$emit('update:modelValue', next)
- }
- this.$emit('change', payload)
- this.$emit('confirm', payload)
- if (prevMonth !== this.currentMonth) {
- this.$emit('monthChange', {
- month: this.currentMonth,
- scene
- })
- }
- },
- 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 Math.max(1, (maxYear - minYear) * 12 + (maxMonth - minMonth) + 1)
- },
- findFirstEnabledDate(month) {
- const start = dayjs(`${month}-01`)
- const days = start.daysInMonth()
- for (let i = 1; i <= days; i++) {
- const date = start.date(i).format('YYYY-MM-DD')
- if (!this.isDateDisabled(date)) {
- return date
- }
- }
- return ''
- },
- switchMonth(step = 0) {
- if (step < 0 && this.switchPrevDisabled) return
- if (step > 0 && this.switchNextDisabled) return
- const baseMonth = this.currentMonth || dayjs(this.todayDate).format('YYYY-MM')
- const target = dayjs(`${baseMonth}-01`).add(step, 'month')
- const targetMonth = target.format('YYYY-MM')
- const selectedDay = dayjs(this.innerSelectedDate || this.todayDate).date()
- const dayInTargetMonth = Math.min(selectedDay, target.daysInMonth())
- let next = target.date(dayInTargetMonth).format('YYYY-MM-DD')
- next = this.clampDate(next)
- if (!next || !dayjs(next).isSame(target, 'month') || this.isDateDisabled(next)) {
- next = this.findFirstEnabledDate(targetMonth)
- }
- if (!next) return
- this.setSelectedDate(next, 'switch', true)
- },
- prevMonth() {
- this.switchMonth(-1)
- },
- nextMonth() {
- this.switchMonth(1)
- },
- onDayTap(item) {
- if (this.readonly || item.disabled) return
- this.setSelectedDate(item.date, 'tap', true)
- },
- setFullVisible(show, source = 'button') {
- if (!this.fullCalendar) return
- if (this.innerShowFull === show) return
- this.innerShowFull = show
- this.$emit('toggleFull', {
- show,
- source
- })
- },
- toggleFull(source = 'button') {
- this.setFullVisible(!this.innerShowFull, source)
- },
- onPanelConfirm(e) {
- if (!Array.isArray(e) || !e.length) return
- this.setSelectedDate(e[0], 'full', true)
- if (this.collapseAfterSelect) {
- this.setFullVisible(false, 'auto')
- }
- },
- onTouchStart(event) {
- if (!this.fullCalendar) return
- const point = event?.changedTouches?.[0] || event?.touches?.[0]
- if (!point) return
- this.touchStartX = point.clientX
- this.touchStartY = point.clientY
- },
- onTouchEnd(event) {
- if (!this.fullCalendar) return
- const point = event?.changedTouches?.[0] || event?.touches?.[0]
- if (!point) return
- const deltaX = point.clientX - this.touchStartX
- const deltaY = point.clientY - this.touchStartY
- const threshold = Number(this.pullDownThreshold) || 40
- if (Math.abs(deltaY) < threshold || Math.abs(deltaY) <= Math.abs(deltaX)) {
- return
- }
- if (deltaY > 0 && !this.innerShowFull) {
- this.setFullVisible(true, 'pull-down')
- }
- if (deltaY < 0 && this.innerShowFull) {
- this.setFullVisible(false, 'pull-up')
- }
- }
- }
- }
- </script>
-
- <style lang="scss" scoped>
- .u-calendar-strip {
- background-color: var(--up-card-bg-color, #ffffff);
- border-radius: 10px;
- margin: 0 8px;
-
- &__header {
- @include flex;
- align-items: center;
- padding: 0 4px;
- height: 40px;
-
- &__switch {
- width: 40px;
- text-align: center;
- font-size: 20px;
- line-height: 40px;
- color: var(--up-main-color, $u-main-color);
-
- &--disabled {
- opacity: 0.35;
- }
- }
-
- &__title {
- flex: 1;
- text-align: center;
- font-size: 14px;
- font-weight: bold;
- color: var(--up-main-color, $u-main-color);
- }
-
- &__toggle {
- width: 40px;
- text-align: center;
- font-size: 18px;
- line-height: 40px;
- color: var(--up-content-color, $u-content-color);
- }
- }
-
- &__scroll-wrap {
- padding: 10px 0 6px;
- }
-
- &__scroll {
- &__inner {
- @include flex;
- padding: 0 8px;
- }
- }
-
- &__day {
- width: 54px;
- height: 62px;
- margin-right: 8px;
- border-radius: 8px;
- border: 1px solid transparent;
- @include flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
-
- &__date {
- font-size: 18px;
- line-height: 22px;
- color: var(--up-main-color, $u-main-color);
- }
-
- &__week {
- margin-top: 4px;
- font-size: 12px;
- line-height: 16px;
- color: var(--up-content-color, $u-content-color);
- }
-
- &--selected {
- .u-calendar-strip__day__date,
- .u-calendar-strip__day__week {
- color: #ffffff;
- }
- }
-
- &--today {
- border-style: solid;
- }
-
- &--disabled {
- opacity: 0.4;
- }
- }
-
- &__hint {
- padding: 4px 0 10px;
- text-align: center;
-
- &__text {
- font-size: 12px;
- color: var(--up-tips-color, $u-tips-color);
- }
- }
-
- &__panel {
- padding: 0 8px 8px;
- }
-
- &__panel-wrap {
- padding-top: 4px;
- }
-
- &__hint--panel {
- padding: 0 0 8px;
- }
- }
- </style>
|