| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- <template>
- <view class="wd-month-panel">
- <view v-if="showPanelTitle" class="wd-month-panel__title">
- {{ title }}
- </view>
- <view class="wd-month-panel__weeks">
- <view v-for="item in 7" :key="item" class="wd-month-panel__week">{{ weekLabel(item + firstDayOfWeek) }}</view>
- </view>
- <scroll-view
- :class="`wd-month-panel__container ${!!timeType ? 'wd-month-panel__container--time' : ''}`"
- :style="`height: ${scrollHeight}px`"
- scroll-y
- @scroll="monthScroll"
- :scroll-top="scrollTop"
- >
- <view v-for="(item, index) in months" :key="index" :id="`month${index}`">
- <month
- :type="type"
- :date="item.date"
- :value="value"
- :min-date="minDate"
- :max-date="maxDate"
- :first-day-of-week="firstDayOfWeek"
- :formatter="formatter"
- :max-range="maxRange"
- :range-prompt="rangePrompt"
- :allow-same-day="allowSameDay"
- :default-time="defaultTime"
- :showTitle="index !== 0"
- @change="handleDateChange"
- />
- </view>
- </scroll-view>
- <view v-if="timeType" class="wd-month-panel__time">
- <view v-if="type === 'datetimerange'" class="wd-month-panel__time-label">
- <view class="wd-month-panel__time-text">{{ timeType === 'start' ? translate('startTime') : translate('endTime') }}</view>
- </view>
- <view class="wd-month-panel__time-picker">
- <wd-picker-view
- v-if="timeData.length"
- v-model="timeValue"
- :columns="timeData"
- :columns-height="125"
- :immediate-change="immediateChange"
- @change="handleTimeChange"
- @pickstart="handlePickStart"
- @pickend="handlePickEnd"
- />
- </view>
- </view>
- </view>
- </template>
- <script lang="ts">
- export default {
- options: {
- addGlobalClass: true,
- virtualHost: true,
- styleIsolation: 'shared'
- }
- }
- </script>
- <script lang="ts" setup>
- import wdPickerView from '../../wd-picker-view/wd-picker-view.vue'
- import { computed, ref, watch, onMounted } from 'vue'
- import { debounce, isArray, isEqual, isNumber, pause } from '../../common/util'
- import { compareMonth, formatMonthTitle, getMonthEndDay, getMonths, getTimeData, getWeekLabel } from '../utils'
- import Month from '../month/month.vue'
- import { monthPanelProps, type MonthInfo, type MonthPanelTimeType, type MonthPanelExpose } from './types'
- import { useTranslate } from '../../composables/useTranslate'
- import type { CalendarItem } from '../types'
- const props = defineProps(monthPanelProps)
- const emit = defineEmits(['change', 'pickstart', 'pickend'])
- const { translate } = useTranslate('calendar-view')
- const scrollTop = ref<number>(0) // 滚动位置
- const scrollIndex = ref<number>(0) // 当前显示的月份索引
- const timeValue = ref<number[]>([]) // 当前选中的时分秒
- const timeType = ref<MonthPanelTimeType>('') // 当前时间类型,是开始还是结束
- const innerValue = ref<string | number | (number | null)[]>('') // 内部保存一个值,用于判断新老值,避免监听器触发
- const handleChange = debounce((value) => {
- emit('change', {
- value
- })
- }, 50)
- // 时间picker的列数据
- const timeData = computed<Array<CalendarItem[]>>(() => {
- let timeColumns: Array<CalendarItem[]> = []
- if (props.type === 'datetime' && isNumber(props.value)) {
- const date = new Date(props.value)
- date.setHours(timeValue.value[0])
- date.setMinutes(timeValue.value[1])
- date.setSeconds(props.hideSecond ? 0 : timeValue.value[2])
- const dateTime = date.getTime()
- timeColumns = getTime(dateTime) || []
- } else if (isArray(props.value) && props.type === 'datetimerange') {
- const [start, end] = props.value!
- const dataValue = timeType.value === 'start' ? start : end
- const date = new Date(dataValue || '')
- date.setHours(timeValue.value[0])
- date.setMinutes(timeValue.value[1])
- date.setSeconds(props.hideSecond ? 0 : timeValue.value[2])
- const dateTime = date.getTime()
- const finalValue = [start, end]
- if (timeType.value === 'start') {
- finalValue[0] = dateTime
- } else {
- finalValue[1] = dateTime
- }
- timeColumns = getTime(finalValue, timeType.value) || []
- }
- return timeColumns
- })
- // 标题
- const title = computed(() => {
- return formatMonthTitle(months.value[scrollIndex.value].date)
- })
- // 周标题
- const weekLabel = computed(() => {
- return (index: number) => {
- return getWeekLabel(index - 1)
- }
- })
- // 滚动区域的高度
- const scrollHeight = computed(() => {
- const scrollHeight: number = timeType.value ? props.panelHeight - 125 : props.panelHeight
- return scrollHeight
- })
- // 月份日期和月份高度
- const months = computed<MonthInfo[]>(() => {
- return getMonths(props.minDate, props.maxDate).map((month, index) => {
- const offset = (7 + new Date(month).getDay() - props.firstDayOfWeek) % 7
- const totalDay = getMonthEndDay(new Date(month).getFullYear(), new Date(month).getMonth() + 1)
- const rows = Math.ceil((offset + totalDay) / 7)
- return {
- height: rows * 64 + (rows - 1) * 4 + (index === 0 ? 0 : 45), // 每行64px高度,除最后一行外每行加4px margin,加上标题45px
- date: month
- }
- })
- })
- watch(
- () => props.type,
- (val) => {
- if (
- (val === 'datetime' && props.value) ||
- (val === 'datetimerange' && isArray(props.value) && props.value && props.value.length > 0 && props.value[0])
- ) {
- setTime(props.value, 'start')
- }
- },
- {
- deep: true,
- immediate: true
- }
- )
- watch(
- () => props.value,
- (val) => {
- if (isEqual(val, innerValue.value)) return
- if ((props.type === 'datetime' && val) || (props.type === 'datetimerange' && val && isArray(val) && val.length > 0 && val[0])) {
- setTime(val, 'start')
- }
- },
- {
- deep: true,
- immediate: true
- }
- )
- onMounted(() => {
- scrollIntoView()
- })
- /**
- * 使当前日期或者选中日期滚动到可视区域
- */
- async function scrollIntoView() {
- // 等待渲染完毕
- await pause()
- let activeDate: number | null = 0
- if (isArray(props.value)) {
- // 对数组按时间排序,取第一个值
- const sortedValue = [...props.value].sort((a, b) => (a || 0) - (b || 0))
- activeDate = sortedValue[0]
- } else if (isNumber(props.value)) {
- activeDate = props.value
- }
- if (!activeDate) {
- activeDate = Date.now()
- }
- let top: number = 0
- let activeMonthIndex = -1
- for (let index = 0; index < months.value.length; index++) {
- if (compareMonth(months.value[index].date, activeDate) === 0) {
- activeMonthIndex = index
- // 找到选中月份后,计算选中日期在月份中的位置
- const date = new Date(activeDate)
- const day = date.getDate()
- const firstDay = new Date(date.getFullYear(), date.getMonth(), 1)
- const offset = (7 + firstDay.getDay() - props.firstDayOfWeek) % 7
- const row = Math.floor((offset + day - 1) / 7)
- // 每行高度64px,每行加4px margin
- top += row * 64 + row * 4
- break
- }
- top += months.value[index] ? Number(months.value[index].height) : 0
- }
- scrollTop.value = 0
- if (top > 0) {
- await pause()
- // 如果不是第一个月才加45
- scrollTop.value = top + (activeMonthIndex > 0 ? 45 : 0)
- }
- }
- /**
- * 获取时间 picker 的数据
- * @param {timestamp|array} value 当前时间
- * @param {string} type 类型,是开始还是结束
- */
- function getTime(value: number | (number | null)[], type?: string) {
- if (props.type === 'datetime') {
- return getTimeData({
- date: value as number,
- minDate: props.minDate,
- maxDate: props.maxDate,
- filter: props.timeFilter,
- isHideSecond: props.hideSecond
- })
- } else {
- if (type === 'start' && isArray(props.value)) {
- return getTimeData({
- date: (value as Array<number>)[0],
- minDate: props.minDate,
- maxDate: props.value[1] ? props.value[1] : props.maxDate,
- filter: props.timeFilter,
- isHideSecond: props.hideSecond
- })
- } else {
- return getTimeData({
- date: (value as Array<number>)[1],
- minDate: (value as Array<number>)[0],
- maxDate: props.maxDate,
- filter: props.timeFilter,
- isHideSecond: props.hideSecond
- })
- }
- }
- }
- /**
- * 获取 date 的时分秒
- * @param {timestamp} date 时间
- * @param {string} type 类型,是开始还是结束
- */
- function getTimeValue(date: number | (number | null)[], type: MonthPanelTimeType) {
- let dateValue: Date = new Date()
- if (props.type === 'datetime') {
- dateValue = new Date(date as number)
- } else if (isArray(date)) {
- if (type === 'start') {
- dateValue = new Date(date[0] || '')
- } else {
- dateValue = new Date(date[1] || '')
- }
- }
- const hour = dateValue.getHours()
- const minute = dateValue.getMinutes()
- const second = dateValue.getSeconds()
- return props.hideSecond ? [hour, minute] : [hour, minute, second]
- }
- function setTime(value: number | (number | null)[], type?: MonthPanelTimeType) {
- if (isArray(value) && value[0] && value[1] && type === 'start' && timeType.value === 'start') {
- type = 'end'
- }
- timeType.value = type || ''
- timeValue.value = getTimeValue(value, type || '')
- }
- function handleDateChange({ value, type }: { value: number | (number | null)[]; type?: MonthPanelTimeType }) {
- if (!isEqual(value, props.value)) {
- // 内部保存一个值,用于判断新老值,避免监听器触发
- innerValue.value = value
- handleChange(value)
- }
- // datetime 和 datetimerange 类型,需要计算 timeData 并做展示
- if (props.type.indexOf('time') > -1) {
- setTime(value, type)
- }
- }
- function handleTimeChange({ value }: { value: any[] }) {
- if (!props.value) {
- return
- }
- if (props.type === 'datetime' && isNumber(props.value)) {
- const date = new Date(props.value)
- date.setHours(value[0])
- date.setMinutes(value[1])
- date.setSeconds(props.hideSecond ? 0 : value[2])
- const dateTime = date.getTime()
- handleChange(dateTime)
- } else if (isArray(props.value) && props.type === 'datetimerange') {
- const [start, end] = props.value!
- const dataValue = timeType.value === 'start' ? start : end
- const date = new Date(dataValue || '')
- date.setHours(value[0])
- date.setMinutes(value[1])
- date.setSeconds(props.hideSecond ? 0 : value[2])
- const dateTime = date.getTime()
- if (dateTime === dataValue) return
- const finalValue = [start, end]
- if (timeType.value === 'start') {
- finalValue[0] = dateTime
- } else {
- finalValue[1] = dateTime
- }
- innerValue.value = finalValue // 内部保存一个值,用于判断新老值,避免监听器触发
- handleChange(finalValue)
- }
- }
- function handlePickStart() {
- emit('pickstart')
- }
- function handlePickEnd() {
- emit('pickend')
- }
- const monthScroll = (event: { detail: { scrollTop: number } }) => {
- if (months.value.length <= 1) {
- return
- }
- const scrollTop = Math.max(0, event.detail.scrollTop)
- doSetSubtitle(scrollTop)
- }
- /**
- * 设置小标题
- * scrollTop 滚动条位置
- */
- function doSetSubtitle(scrollTop: number) {
- let height: number = 0 // 月份高度和
- for (let index = 0; index < months.value.length; index++) {
- height = height + months.value[index].height
- if (scrollTop < height) {
- scrollIndex.value = index
- return
- }
- }
- }
- defineExpose<MonthPanelExpose>({
- scrollIntoView
- })
- </script>
- <style lang="scss" scoped>
- @import './index.scss';
- </style>
|