first commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* @Author: weisheng
|
||||
* @Date: 2023-06-12 10:04:19
|
||||
* @LastEditTime: 2023-07-15 16:16:34
|
||||
* @LastEditors: weisheng
|
||||
* @Description:
|
||||
* @FilePath: \wot-design-uni\src\uni_modules\wot-design-uni\components\wd-calendar-view\index.scss
|
||||
* 记得注释
|
||||
*/
|
||||
@@ -0,0 +1,162 @@
|
||||
@import '../../common/abstracts/variable';
|
||||
@import '../../common/abstracts/mixin';
|
||||
|
||||
.wot-theme-dark {
|
||||
@include b(month) {
|
||||
@include e(title) {
|
||||
color: $-dark-color;
|
||||
}
|
||||
|
||||
@include e(days) {
|
||||
color: $-dark-color;
|
||||
}
|
||||
|
||||
@include e(day) {
|
||||
@include when(disabled) {
|
||||
.wd-month__day-text {
|
||||
color: $-dark-color-gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include b(month) {
|
||||
@include e(title) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 45px;
|
||||
font-size: $-calendar-panel-title-fs;
|
||||
color: $-calendar-panel-title-color;
|
||||
}
|
||||
|
||||
@include e(days) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
font-size: $-calendar-day-fs;
|
||||
color: $-calendar-day-color;
|
||||
}
|
||||
|
||||
@include e(day) {
|
||||
position: relative;
|
||||
width: 14.285%;
|
||||
height: $-calendar-day-height;
|
||||
line-height: $-calendar-day-height;
|
||||
text-align: center;
|
||||
margin-bottom: $-calendar-item-margin-bottom;
|
||||
|
||||
@include when(disabled) {
|
||||
.wd-month__day-text {
|
||||
color: $-calendar-disabled-color;
|
||||
}
|
||||
}
|
||||
|
||||
@include when(current) {
|
||||
color: $-calendar-active-color;
|
||||
}
|
||||
|
||||
@include when(selected, multiple-selected) {
|
||||
.wd-month__day-container {
|
||||
border-radius: $-calendar-active-border;
|
||||
background: $-calendar-active-color;
|
||||
color: $-calendar-selected-color;
|
||||
}
|
||||
}
|
||||
|
||||
@include when(middle) {
|
||||
.wd-month__day-container {
|
||||
background: $-calendar-range-color;
|
||||
}
|
||||
}
|
||||
@include when(multiple-middle) {
|
||||
.wd-month__day-container {
|
||||
background: $-calendar-active-color;
|
||||
color: $-calendar-selected-color;
|
||||
}
|
||||
}
|
||||
|
||||
@include when(start) {
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: $-calendar-day-height;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 50%;
|
||||
background: $-calendar-range-color;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&.is-without-end::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wd-month__day-container {
|
||||
background: $-calendar-active-color;
|
||||
color: $-calendar-selected-color;
|
||||
border-radius: $-calendar-active-border 0 0 $-calendar-active-border;
|
||||
}
|
||||
}
|
||||
|
||||
@include when(end) {
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: $-calendar-day-height;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 50%;
|
||||
background: $-calendar-range-color;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.wd-month__day-container {
|
||||
background: $-calendar-active-color;
|
||||
color: $-calendar-selected-color;
|
||||
border-radius: 0 $-calendar-active-border $-calendar-active-border 0;
|
||||
}
|
||||
}
|
||||
|
||||
@include when(same) {
|
||||
.wd-month__day-container {
|
||||
background: $-calendar-active-color;
|
||||
color: $-calendar-selected-color;
|
||||
border-radius: $-calendar-active-border;
|
||||
}
|
||||
}
|
||||
|
||||
@include when(last-row){
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(day-container) {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@include e(day-text) {
|
||||
font-weight: $-calendar-day-fw;
|
||||
}
|
||||
|
||||
@include e(day-top) {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
line-height: 1.1;
|
||||
font-size: $-calendar-info-fs;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@include e(day-bottom) {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
line-height: 1.1;
|
||||
font-size: $-calendar-info-fs;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
<template>
|
||||
<view>
|
||||
<wd-toast selector="wd-month" />
|
||||
<view class="month">
|
||||
<view class="wd-month">
|
||||
<view class="wd-month__title" v-if="showTitle">{{ monthTitle(date) }}</view>
|
||||
<view class="wd-month__days">
|
||||
<view
|
||||
v-for="(item, index) in days"
|
||||
:key="index"
|
||||
:class="`wd-month__day ${item.disabled ? 'is-disabled' : ''} ${item.isLastRow ? 'is-last-row' : ''} ${
|
||||
item.type ? dayTypeClass(item.type) : ''
|
||||
}`"
|
||||
:style="index === 0 ? firstDayStyle : ''"
|
||||
@click="handleDateClick(index)"
|
||||
>
|
||||
<view class="wd-month__day-container">
|
||||
<view class="wd-month__day-top">{{ item.topInfo }}</view>
|
||||
<view class="wd-month__day-text">
|
||||
{{ item.text }}
|
||||
</view>
|
||||
<view class="wd-month__day-bottom">{{ item.bottomInfo }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
virtualHost: true,
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import wdToast from '../../wd-toast/wd-toast.vue'
|
||||
import { computed, ref, watch, type CSSProperties } from 'vue'
|
||||
import {
|
||||
compareDate,
|
||||
formatMonthTitle,
|
||||
getDateByDefaultTime,
|
||||
getDayByOffset,
|
||||
getDayOffset,
|
||||
getItemClass,
|
||||
getMonthEndDay,
|
||||
getNextDay,
|
||||
getPrevDay,
|
||||
getWeekRange
|
||||
} from '../utils'
|
||||
import { useToast } from '../../wd-toast'
|
||||
import { deepClone, isArray, isFunction, objToStyle } from '../../common/util'
|
||||
import { useTranslate } from '../../composables/useTranslate'
|
||||
import type { CalendarDayItem, CalendarDayType } from '../types'
|
||||
import { monthProps } from './types'
|
||||
|
||||
const props = defineProps(monthProps)
|
||||
const emit = defineEmits(['change'])
|
||||
|
||||
const { translate } = useTranslate('calendar-view')
|
||||
|
||||
const days = ref<Array<CalendarDayItem>>([])
|
||||
|
||||
const toast = useToast('wd-month')
|
||||
|
||||
const offset = computed(() => {
|
||||
const firstDayOfWeek = props.firstDayOfWeek >= 7 ? props.firstDayOfWeek % 7 : props.firstDayOfWeek
|
||||
const offset = (7 + new Date(props.date).getDay() - firstDayOfWeek) % 7
|
||||
return offset
|
||||
})
|
||||
|
||||
const dayTypeClass = computed(() => {
|
||||
return (monthType: CalendarDayType) => {
|
||||
return getItemClass(monthType, props.value, props.type)
|
||||
}
|
||||
})
|
||||
|
||||
const monthTitle = computed(() => {
|
||||
return (date: number) => {
|
||||
return formatMonthTitle(date)
|
||||
}
|
||||
})
|
||||
|
||||
const firstDayStyle = computed(() => {
|
||||
const dayStyle: CSSProperties = {}
|
||||
dayStyle.marginLeft = `${(100 / 7) * offset.value}%`
|
||||
return objToStyle(dayStyle)
|
||||
})
|
||||
|
||||
const isLastRow = (date: number) => {
|
||||
const currentDate = new Date(date)
|
||||
const currentDay = currentDate.getDate()
|
||||
const daysInMonth = getMonthEndDay(currentDate.getFullYear(), currentDate.getMonth() + 1)
|
||||
const totalDaysShown = offset.value + daysInMonth
|
||||
const totalRows = Math.ceil(totalDaysShown / 7)
|
||||
return Math.ceil((offset.value + currentDay) / 7) === totalRows
|
||||
}
|
||||
watch(
|
||||
[() => props.type, () => props.date, () => props.value, () => props.minDate, () => props.maxDate, () => props.formatter],
|
||||
() => {
|
||||
setDays()
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
function setDays() {
|
||||
const dayList: Array<CalendarDayItem> = []
|
||||
const date = new Date(props.date)
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth()
|
||||
const totalDay = getMonthEndDay(year, month + 1)
|
||||
let value = props.value
|
||||
if ((props.type === 'week' || props.type === 'weekrange') && value) {
|
||||
value = getWeekValue()
|
||||
}
|
||||
|
||||
for (let day = 1; day <= totalDay; day++) {
|
||||
const date = new Date(year, month, day).getTime()
|
||||
let type: CalendarDayType = getDayType(date, value as number | number[] | null)
|
||||
if (!type && compareDate(date, Date.now()) === 0) {
|
||||
type = 'current'
|
||||
}
|
||||
const dayObj = getFormatterDate(date, day, type)
|
||||
dayList.push(dayObj)
|
||||
}
|
||||
days.value = dayList
|
||||
}
|
||||
function getDayType(date: number, value: number | number[] | null): CalendarDayType {
|
||||
switch (props.type) {
|
||||
case 'date':
|
||||
case 'datetime':
|
||||
return getDateType(date)
|
||||
case 'dates':
|
||||
return getDatesType(date)
|
||||
case 'daterange':
|
||||
case 'datetimerange':
|
||||
return getDatetimeType(date, value)
|
||||
case 'week':
|
||||
return getWeektimeType(date, value)
|
||||
case 'weekrange':
|
||||
return getWeektimeType(date, value)
|
||||
default:
|
||||
return getDateType(date)
|
||||
}
|
||||
}
|
||||
function getDateType(date: number): CalendarDayType {
|
||||
if (props.value && compareDate(date, props.value as number) === 0) {
|
||||
return 'selected'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function getDatesType(date: number): CalendarDayType {
|
||||
const { value } = props
|
||||
let type: CalendarDayType = ''
|
||||
|
||||
if (!isArray(value)) return type
|
||||
const isSelected = (day: number) => {
|
||||
return value.some((item) => compareDate(day, item) === 0)
|
||||
}
|
||||
|
||||
if (isSelected(date)) {
|
||||
const prevDay = getPrevDay(date)
|
||||
const nextDay = getNextDay(date)
|
||||
const prevSelected = isSelected(prevDay)
|
||||
const nextSelected = isSelected(nextDay)
|
||||
if (prevSelected && nextSelected) {
|
||||
type = 'multiple-middle'
|
||||
} else if (prevSelected) {
|
||||
type = 'end'
|
||||
} else if (nextSelected) {
|
||||
type = 'start'
|
||||
} else {
|
||||
type = 'multiple-selected'
|
||||
}
|
||||
}
|
||||
|
||||
return type
|
||||
}
|
||||
function getDatetimeType(date: number, value: number | number[] | null) {
|
||||
const [startDate, endDate] = isArray(value) ? value : []
|
||||
|
||||
if (startDate && compareDate(date, startDate) === 0) {
|
||||
if (props.allowSameDay && endDate && compareDate(startDate, endDate) === 0) {
|
||||
return 'same'
|
||||
}
|
||||
return 'start'
|
||||
} else if (endDate && compareDate(date, endDate) === 0) {
|
||||
return 'end'
|
||||
} else if (startDate && endDate && compareDate(date, startDate) === 1 && compareDate(date, endDate) === -1) {
|
||||
return 'middle'
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
function getWeektimeType(date: number, value: number | number[] | null) {
|
||||
const [startDate, endDate] = isArray(value) ? value : []
|
||||
|
||||
if (startDate && compareDate(date, startDate) === 0) {
|
||||
return 'start'
|
||||
} else if (endDate && compareDate(date, endDate) === 0) {
|
||||
return 'end'
|
||||
} else if (startDate && endDate && compareDate(date, startDate) === 1 && compareDate(date, endDate) === -1) {
|
||||
return 'middle'
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
function getWeekValue() {
|
||||
if (props.type === 'week') {
|
||||
return getWeekRange(props.value as number, props.firstDayOfWeek)
|
||||
} else {
|
||||
const [startDate, endDate] = (props.value as any) || []
|
||||
|
||||
if (startDate) {
|
||||
const firstWeekRange = getWeekRange(startDate, props.firstDayOfWeek)
|
||||
|
||||
if (endDate) {
|
||||
const endWeekRange = getWeekRange(endDate, props.firstDayOfWeek)
|
||||
|
||||
return [firstWeekRange[0], endWeekRange[1]]
|
||||
} else {
|
||||
return firstWeekRange
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
function handleDateClick(index: number) {
|
||||
const date = days.value[index]
|
||||
switch (props.type) {
|
||||
case 'date':
|
||||
case 'datetime':
|
||||
handleDateChange(date)
|
||||
break
|
||||
case 'dates':
|
||||
handleDatesChange(date)
|
||||
break
|
||||
case 'daterange':
|
||||
case 'datetimerange':
|
||||
handleDateRangeChange(date)
|
||||
break
|
||||
case 'week':
|
||||
handleWeekChange(date)
|
||||
break
|
||||
case 'weekrange':
|
||||
handleWeekRangeChange(date)
|
||||
break
|
||||
default:
|
||||
handleDateChange(date)
|
||||
}
|
||||
}
|
||||
function getDate(date: number, isEnd: boolean = false) {
|
||||
date = props.defaultTime && props.defaultTime.length > 0 ? getDateByDefaultTime(date, isEnd ? props.defaultTime[1] : props.defaultTime[0]) : date
|
||||
|
||||
if (date < props.minDate) return props.minDate
|
||||
|
||||
if (date > props.maxDate) return props.maxDate
|
||||
|
||||
return date
|
||||
}
|
||||
|
||||
function handleDateChange(date: CalendarDayItem) {
|
||||
if (date.disabled) return
|
||||
|
||||
if (date.type !== 'selected') {
|
||||
emit('change', {
|
||||
value: getDate(date.date),
|
||||
type: 'start'
|
||||
})
|
||||
}
|
||||
}
|
||||
function handleDatesChange(date: CalendarDayItem) {
|
||||
if (date.disabled) return
|
||||
const currentValue = deepClone(isArray(props.value) ? props.value : [])
|
||||
const dateIndex = currentValue.findIndex((item) => item && compareDate(item, date.date) === 0)
|
||||
const value = dateIndex === -1 ? [...currentValue, getDate(date.date)] : currentValue.filter((_, index) => index !== dateIndex)
|
||||
emit('change', { value })
|
||||
}
|
||||
|
||||
function handleDateRangeChange(date: CalendarDayItem) {
|
||||
if (date.disabled) return
|
||||
|
||||
let value: (number | null)[] = []
|
||||
let type: CalendarDayType = ''
|
||||
const [startDate, endDate] = deepClone(isArray(props.value) ? props.value : [])
|
||||
const compare = compareDate(date.date, startDate)
|
||||
|
||||
// 禁止选择同个日期
|
||||
if (!props.allowSameDay && compare === 0 && (props.type === 'daterange' || props.type === 'datetimerange') && !endDate) {
|
||||
return
|
||||
}
|
||||
|
||||
if (startDate && !endDate && compare > -1) {
|
||||
// 不能选择超过最大范围的日期
|
||||
if (props.maxRange && getDayOffset(date.date, startDate) > props.maxRange) {
|
||||
const maxEndDate = getDayByOffset(startDate, props.maxRange - 1)
|
||||
value = [startDate, getDate(maxEndDate, true)]
|
||||
toast.show({
|
||||
msg: props.rangePrompt || translate('rangePrompt', props.maxRange)
|
||||
})
|
||||
} else {
|
||||
value = [startDate, getDate(date.date, true)]
|
||||
}
|
||||
} else if (props.type === 'datetimerange' && startDate && endDate) {
|
||||
// 时间范围类型,且有开始时间和结束时间,需要支持重新点击开始日期和结束日期可以重新修改时间
|
||||
if (compare === 0) {
|
||||
type = 'start'
|
||||
value = props.value as number[]
|
||||
} else if (compareDate(date.date, endDate) === 0) {
|
||||
type = 'end'
|
||||
value = props.value as number[]
|
||||
} else {
|
||||
value = [getDate(date.date), null]
|
||||
}
|
||||
} else {
|
||||
value = [getDate(date.date), null]
|
||||
}
|
||||
|
||||
emit('change', {
|
||||
value,
|
||||
type: type || (value[1] ? 'end' : 'start')
|
||||
})
|
||||
}
|
||||
function handleWeekChange(date: CalendarDayItem) {
|
||||
const [weekStart] = getWeekRange(date.date, props.firstDayOfWeek)
|
||||
|
||||
// 周的第一天如果是禁用状态,则不可选中
|
||||
if (getFormatterDate(weekStart, new Date(weekStart).getDate()).disabled) return
|
||||
|
||||
emit('change', {
|
||||
value: getDate(weekStart) + 24 * 60 * 60 * 1000
|
||||
})
|
||||
}
|
||||
function handleWeekRangeChange(date: CalendarDayItem) {
|
||||
const [weekStartDate] = getWeekRange(date.date, props.firstDayOfWeek)
|
||||
|
||||
// 周的第一天如果是禁用状态,则不可选中
|
||||
if (getFormatterDate(weekStartDate, new Date(weekStartDate).getDate()).disabled) return
|
||||
|
||||
let value: (number | null)[] = []
|
||||
const [startDate, endDate] = deepClone(isArray(props.value) ? props.value : [])
|
||||
const [startWeekStartDate] = startDate ? getWeekRange(startDate, props.firstDayOfWeek) : []
|
||||
const compare = compareDate(weekStartDate, startWeekStartDate)
|
||||
|
||||
if (startDate && !endDate && compare > -1) {
|
||||
if (!props.allowSameDay && compare === 0) return
|
||||
|
||||
value = [getDate(startWeekStartDate) + 24 * 60 * 60 * 1000, getDate(weekStartDate) + 24 * 60 * 60 * 1000]
|
||||
} else {
|
||||
value = [getDate(weekStartDate) + 24 * 60 * 60 * 1000, null]
|
||||
}
|
||||
|
||||
emit('change', {
|
||||
value
|
||||
})
|
||||
}
|
||||
function getFormatterDate(date: number, day: string | number, type?: CalendarDayType) {
|
||||
let dayObj: CalendarDayItem = {
|
||||
date: date,
|
||||
text: day,
|
||||
topInfo: '',
|
||||
bottomInfo: '',
|
||||
type,
|
||||
disabled: compareDate(date, props.minDate) === -1 || compareDate(date, props.maxDate) === 1,
|
||||
isLastRow: isLastRow(date)
|
||||
}
|
||||
if (props.formatter) {
|
||||
if (isFunction(props.formatter)) {
|
||||
dayObj = props.formatter(dayObj)
|
||||
} else {
|
||||
console.error('[wot-design] error(wd-calendar-view): the formatter prop of wd-calendar-view should be a function')
|
||||
}
|
||||
}
|
||||
return dayObj
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import './index.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { PropType } from 'vue'
|
||||
import { makeBooleanProp, makeRequiredProp } from '../../common/props'
|
||||
import type { CalendarFormatter, CalendarType } from '../types'
|
||||
|
||||
export const monthProps = {
|
||||
type: makeRequiredProp(String as PropType<CalendarType>),
|
||||
date: makeRequiredProp(Number),
|
||||
value: makeRequiredProp([Number, Array, null] as PropType<number | (number | null)[] | null>),
|
||||
minDate: makeRequiredProp(Number),
|
||||
maxDate: makeRequiredProp(Number),
|
||||
firstDayOfWeek: makeRequiredProp(Number),
|
||||
formatter: Function as PropType<CalendarFormatter>,
|
||||
maxRange: Number,
|
||||
rangePrompt: String,
|
||||
allowSameDay: makeBooleanProp(false),
|
||||
defaultTime: {
|
||||
type: [Array] as PropType<Array<number[]>>
|
||||
},
|
||||
showTitle: makeBooleanProp(true)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
@import '../../common/abstracts/variable';
|
||||
@import '../../common/abstracts/mixin';
|
||||
|
||||
.wot-theme-dark {
|
||||
@include b(month-panel) {
|
||||
@include e(title) {
|
||||
color: $-dark-color;
|
||||
}
|
||||
|
||||
@include e(weeks) {
|
||||
box-shadow: 0px 4px 8px 0 rgba(255, 255, 255, 0.02);
|
||||
color: $-dark-color;
|
||||
}
|
||||
|
||||
@include e(time-label) {
|
||||
color: $-dark-color;
|
||||
&::after{
|
||||
background: $-dark-background4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include b(month-panel) {
|
||||
font-size: $-calendar-fs;
|
||||
|
||||
@include e(title) {
|
||||
padding: 5px 0;
|
||||
text-align: center;
|
||||
font-size: $-calendar-panel-title-fs;
|
||||
color: $-calendar-panel-title-color;
|
||||
padding: $-calendar-panel-padding;
|
||||
}
|
||||
|
||||
@include e(weeks) {
|
||||
display: flex;
|
||||
height: $-calendar-week-height;
|
||||
line-height: $-calendar-week-height;
|
||||
box-shadow: 0px 4px 8px 0 rgba(0, 0, 0, 0.02);
|
||||
color: $-calendar-week-color;
|
||||
font-size: $-calendar-week-fs;
|
||||
padding: $-calendar-panel-padding;
|
||||
}
|
||||
|
||||
@include e(week) {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@include e(container) {
|
||||
padding: $-calendar-panel-padding;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@include e(time) {
|
||||
display: flex;
|
||||
box-shadow: 0px -4px 8px 0px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
@include e(time-label) {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
font-size: $-picker-column-fs;
|
||||
text-align: center;
|
||||
line-height: 125px;
|
||||
color: $-picker-column-color;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 35px;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
background: $-picker-column-select-bg;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(time-text) {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@include e(time-picker) {
|
||||
flex: 3;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
<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>
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { ComponentPublicInstance, ExtractPropTypes, PropType } from 'vue'
|
||||
import { makeBooleanProp, makeRequiredProp } from '../../common/props'
|
||||
import type { CalendarFormatter, CalendarTimeFilter, CalendarType } from '../types'
|
||||
|
||||
/**
|
||||
* 月份信息
|
||||
*/
|
||||
export interface MonthInfo {
|
||||
date: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export const monthPanelProps = {
|
||||
type: makeRequiredProp(String as PropType<CalendarType>),
|
||||
value: makeRequiredProp([Number, Array, null] as PropType<number | (number | null)[] | null>),
|
||||
minDate: makeRequiredProp(Number),
|
||||
maxDate: makeRequiredProp(Number),
|
||||
firstDayOfWeek: makeRequiredProp(Number),
|
||||
formatter: Function as PropType<CalendarFormatter>,
|
||||
maxRange: Number,
|
||||
rangePrompt: String,
|
||||
allowSameDay: makeBooleanProp(false),
|
||||
showPanelTitle: makeBooleanProp(false),
|
||||
defaultTime: {
|
||||
type: [Array] as PropType<Array<number[]>>
|
||||
},
|
||||
panelHeight: makeRequiredProp(Number),
|
||||
// type 为 'datetime' 或 'datetimerange' 时有效,用于过滤时间选择器的数据
|
||||
timeFilter: Function as PropType<CalendarTimeFilter>,
|
||||
hideSecond: makeBooleanProp(false),
|
||||
/**
|
||||
* 是否在手指松开时立即触发picker-view的 change 事件。若不开启则会在滚动动画结束后触发 change 事件,1.2.25版本起提供,仅微信小程序和支付宝小程序支持。
|
||||
*/
|
||||
immediateChange: makeBooleanProp(false)
|
||||
}
|
||||
|
||||
export type MonthPanelProps = ExtractPropTypes<typeof monthPanelProps>
|
||||
|
||||
export type MonthPanelTimeType = 'start' | 'end' | ''
|
||||
|
||||
export type MonthPanelExpose = {
|
||||
/**
|
||||
* 使当前日期或者选中日期滚动到可视区域
|
||||
*/
|
||||
scrollIntoView: () => void
|
||||
}
|
||||
|
||||
export type MonthPanelInstance = ComponentPublicInstance<MonthPanelProps, MonthPanelExpose>
|
||||
109
uni_modules/wot-design-uni/components/wd-calendar-view/types.ts
Normal file
109
uni_modules/wot-design-uni/components/wd-calendar-view/types.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import type { ComponentPublicInstance, ExtractPropTypes, PropType } from 'vue'
|
||||
import { baseProps, makeBooleanProp, makeNumberProp, makeRequiredProp, makeStringProp } from '../common/props'
|
||||
|
||||
export type CalendarType = 'date' | 'dates' | 'datetime' | 'week' | 'month' | 'daterange' | 'datetimerange' | 'weekrange' | 'monthrange'
|
||||
|
||||
export const calendarViewProps = {
|
||||
...baseProps,
|
||||
/**
|
||||
* 选中值,为 13 位时间戳或时间戳数组
|
||||
*/
|
||||
modelValue: makeRequiredProp([Number, Array, null] as PropType<number | number[] | null>),
|
||||
/**
|
||||
* 日期类型
|
||||
*/
|
||||
type: makeStringProp<CalendarType>('date'),
|
||||
/**
|
||||
* 最小日期,为 13 位时间戳
|
||||
*/
|
||||
minDate: makeNumberProp(new Date(new Date().getFullYear(), new Date().getMonth() - 6, new Date().getDate()).getTime()),
|
||||
/**
|
||||
* 最大日期,为 13 位时间戳
|
||||
*/
|
||||
maxDate: makeNumberProp(new Date(new Date().getFullYear(), new Date().getMonth() + 6, new Date().getDate(), 23, 59, 59).getTime()),
|
||||
/**
|
||||
* 周起始天
|
||||
*/
|
||||
firstDayOfWeek: makeNumberProp(0),
|
||||
/**
|
||||
* 日期格式化函数
|
||||
*/
|
||||
formatter: Function as PropType<CalendarFormatter>,
|
||||
/**
|
||||
* type 为范围选择时有效,最大日期范围
|
||||
*/
|
||||
maxRange: Number,
|
||||
/**
|
||||
* type 为范围选择时有效,选择超出最大日期范围时的错误提示文案
|
||||
*/
|
||||
rangePrompt: String,
|
||||
/**
|
||||
* type 为范围选择时有效,是否允许选择同一天
|
||||
*/
|
||||
allowSameDay: makeBooleanProp(false),
|
||||
// 是否展示面板标题,自动计算当前滚动的日期月份
|
||||
showPanelTitle: makeBooleanProp(true),
|
||||
/**
|
||||
* 选中日期所使用的当日内具体时刻
|
||||
*/
|
||||
defaultTime: {
|
||||
type: [String, Array] as PropType<string | string[]>,
|
||||
default: '00:00:00'
|
||||
},
|
||||
/**
|
||||
* 可滚动面板的高度
|
||||
*/
|
||||
panelHeight: makeNumberProp(378),
|
||||
/**
|
||||
* type 为 'datetime' 或 'datetimerange' 时有效,用于过滤时间选择器的数据
|
||||
*/
|
||||
timeFilter: Function as PropType<CalendarTimeFilter>,
|
||||
/**
|
||||
* type 为 'datetime' 或 'datetimerange' 时有效,是否不展示秒修改
|
||||
*/
|
||||
hideSecond: makeBooleanProp(false),
|
||||
/**
|
||||
* 是否在手指松开时立即触发picker-view的 change 事件。若不开启则会在滚动动画结束后触发 change 事件,1.2.25版本起提供,仅微信小程序和支付宝小程序支持。
|
||||
*/
|
||||
immediateChange: makeBooleanProp(false)
|
||||
}
|
||||
|
||||
export type CalendarViewProps = ExtractPropTypes<typeof calendarViewProps>
|
||||
|
||||
export type CalendarDayType = '' | 'start' | 'middle' | 'end' | 'selected' | 'same' | 'current' | 'multiple-middle' | 'multiple-selected'
|
||||
|
||||
export type CalendarDayItem = {
|
||||
date: number
|
||||
text?: number | string
|
||||
topInfo?: string
|
||||
bottomInfo?: string
|
||||
type?: CalendarDayType
|
||||
disabled?: boolean
|
||||
isLastRow?: boolean
|
||||
}
|
||||
|
||||
export type CalendarFormatter = (day: CalendarDayItem) => CalendarDayItem
|
||||
|
||||
export type CalendarTimeFilterOptionType = 'hour' | 'minute' | 'second'
|
||||
|
||||
export type CalendarTimeFilterOption = {
|
||||
type: CalendarTimeFilterOptionType
|
||||
values: CalendarItem[]
|
||||
}
|
||||
|
||||
export type CalendarTimeFilter = (option: CalendarTimeFilterOption) => CalendarItem[]
|
||||
|
||||
export type CalendarItem = {
|
||||
label: string
|
||||
value: number
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
export type CalendarViewExpose = {
|
||||
/**
|
||||
* 使当前日期或者选中日期滚动到可视区域
|
||||
*/
|
||||
scrollIntoView: () => void
|
||||
}
|
||||
|
||||
export type CalendarViewInstance = ComponentPublicInstance<CalendarViewExpose, CalendarViewProps>
|
||||
429
uni_modules/wot-design-uni/components/wd-calendar-view/utils.ts
Normal file
429
uni_modules/wot-design-uni/components/wd-calendar-view/utils.ts
Normal file
@@ -0,0 +1,429 @@
|
||||
import { computed } from 'vue'
|
||||
import dayjs from '../../dayjs'
|
||||
import { isArray, isFunction, padZero } from '../common/util'
|
||||
import { useTranslate } from '../composables/useTranslate'
|
||||
import type { CalendarDayType, CalendarItem, CalendarTimeFilter, CalendarType } from './types'
|
||||
const { translate } = useTranslate('calendar-view')
|
||||
|
||||
const weeks = computed(() => {
|
||||
return [
|
||||
translate('weeks.sun'),
|
||||
translate('weeks.mon'),
|
||||
translate('weeks.tue'),
|
||||
translate('weeks.wed'),
|
||||
translate('weeks.thu'),
|
||||
translate('weeks.fri'),
|
||||
translate('weeks.sat')
|
||||
]
|
||||
})
|
||||
|
||||
/**
|
||||
* 比较两个时间的日期是否相等
|
||||
* @param {timestamp} date1
|
||||
* @param {timestamp} date2
|
||||
*/
|
||||
export function compareDate(date1: number, date2: number | null) {
|
||||
const dateValue1 = new Date(date1)
|
||||
const dateValue2 = new Date(date2 || '')
|
||||
|
||||
const year1 = dateValue1.getFullYear()
|
||||
const year2 = dateValue2.getFullYear()
|
||||
const month1 = dateValue1.getMonth()
|
||||
const month2 = dateValue2.getMonth()
|
||||
const day1 = dateValue1.getDate()
|
||||
const day2 = dateValue2.getDate()
|
||||
|
||||
if (year1 === year2) {
|
||||
if (month1 === month2) {
|
||||
return day1 === day2 ? 0 : day1 > day2 ? 1 : -1
|
||||
}
|
||||
return month1 === month2 ? 0 : month1 > month2 ? 1 : -1
|
||||
}
|
||||
|
||||
return year1 > year2 ? 1 : -1
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否是范围选择
|
||||
* @param {string} type
|
||||
*/
|
||||
export function isRange(type: CalendarType) {
|
||||
return type.indexOf('range') > -1
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两个日期的月份是否相等
|
||||
* @param {timestamp} date1
|
||||
* @param {timestamp} date2
|
||||
*/
|
||||
export function compareMonth(date1: number, date2: number) {
|
||||
const dateValue1 = new Date(date1)
|
||||
const dateValue2 = new Date(date2)
|
||||
|
||||
const year1 = dateValue1.getFullYear()
|
||||
const year2 = dateValue2.getFullYear()
|
||||
const month1 = dateValue1.getMonth()
|
||||
const month2 = dateValue2.getMonth()
|
||||
|
||||
if (year1 === year2) {
|
||||
return month1 === month2 ? 0 : month1 > month2 ? 1 : -1
|
||||
}
|
||||
|
||||
return year1 > year2 ? 1 : -1
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两个日期的年份是否一致
|
||||
* @param {timestamp} date1
|
||||
* @param {timestamp} date2
|
||||
*/
|
||||
export function compareYear(date1: number, date2: number) {
|
||||
const dateValue1 = new Date(date1)
|
||||
const dateValue2 = new Date(date2)
|
||||
|
||||
const year1 = dateValue1.getFullYear()
|
||||
const year2 = dateValue2.getFullYear()
|
||||
|
||||
return year1 === year2 ? 0 : year1 > year2 ? 1 : -1
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个月的最后一天
|
||||
* @param {number} year
|
||||
* @param {number} month
|
||||
*/
|
||||
export function getMonthEndDay(year: number, month: number) {
|
||||
return 32 - new Date(year, month - 1, 32).getDate()
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化年月
|
||||
* @param {timestamp} date
|
||||
*/
|
||||
export function formatMonthTitle(date: number) {
|
||||
return dayjs(date).format(translate('monthTitle'))
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据下标获取星期
|
||||
* @param {number} index
|
||||
*/
|
||||
export function getWeekLabel(index: number) {
|
||||
if (index >= 7) {
|
||||
index = index % 7
|
||||
}
|
||||
|
||||
return weeks.value[index]
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化年份
|
||||
* @param {timestamp} date
|
||||
*/
|
||||
export function formatYearTitle(date: number) {
|
||||
return dayjs(date).format(translate('yearTitle'))
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据最小日期和最大日期获取这之间总共有几个月份
|
||||
* @param {timestamp} minDate
|
||||
* @param {timestamp} maxDate
|
||||
*/
|
||||
export function getMonths(minDate: number, maxDate: number) {
|
||||
const months: number[] = []
|
||||
const month = new Date(minDate)
|
||||
month.setDate(1)
|
||||
|
||||
while (compareMonth(month.getTime(), maxDate) < 1) {
|
||||
months.push(month.getTime())
|
||||
month.setMonth(month.getMonth() + 1)
|
||||
}
|
||||
|
||||
return months
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据最小日期和最大日期获取这之间总共有几年
|
||||
* @param {timestamp} minDate
|
||||
* @param {timestamp} maxDate
|
||||
*/
|
||||
export function getYears(minDate: number, maxDate: number) {
|
||||
const years: number[] = []
|
||||
const year = new Date(minDate)
|
||||
year.setMonth(0)
|
||||
year.setDate(1)
|
||||
|
||||
while (compareYear(year.getTime(), maxDate) < 1) {
|
||||
years.push(year.getTime())
|
||||
year.setFullYear(year.getFullYear() + 1)
|
||||
}
|
||||
|
||||
return years
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个日期所在周的第一天和最后一天
|
||||
* @param {timestamp} date
|
||||
*/
|
||||
export function getWeekRange(date: number, firstDayOfWeek: number) {
|
||||
if (firstDayOfWeek >= 7) {
|
||||
firstDayOfWeek = firstDayOfWeek % 7
|
||||
}
|
||||
|
||||
const dateValue = new Date(date)
|
||||
dateValue.setHours(0, 0, 0, 0)
|
||||
const year = dateValue.getFullYear()
|
||||
const month = dateValue.getMonth()
|
||||
const day = dateValue.getDate()
|
||||
const week = dateValue.getDay()
|
||||
|
||||
const weekStart = new Date(year, month, day - ((7 + week - firstDayOfWeek) % 7))
|
||||
const weekEnd = new Date(year, month, day + 6 - ((7 + week - firstDayOfWeek) % 7))
|
||||
|
||||
return [weekStart.getTime(), weekEnd.getTime()]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日期偏移量
|
||||
* @param {timestamp} date1
|
||||
* @param {timestamp} date2
|
||||
*/
|
||||
export function getDayOffset(date1: number, date2: number) {
|
||||
return (date1 - date2) / (24 * 60 * 60 * 1000) + 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取偏移日期
|
||||
* @param {timestamp} date
|
||||
* @param {number} offset
|
||||
*/
|
||||
export function getDayByOffset(date: number, offset: number) {
|
||||
const dateValue = new Date(date)
|
||||
dateValue.setDate(dateValue.getDate() + offset)
|
||||
|
||||
return dateValue.getTime()
|
||||
}
|
||||
|
||||
export const getPrevDay = (date: number) => getDayByOffset(date, -1)
|
||||
export const getNextDay = (date: number) => getDayByOffset(date, 1)
|
||||
|
||||
/**
|
||||
* 获取月份偏移量
|
||||
* @param {timestamp} date1
|
||||
* @param {timestamp} date2
|
||||
*/
|
||||
export function getMonthOffset(date1: number, date2: number) {
|
||||
const dateValue1 = new Date(date1)
|
||||
const dateValue2 = new Date(date2)
|
||||
|
||||
const year1 = dateValue1.getFullYear()
|
||||
const year2 = dateValue2.getFullYear()
|
||||
let month1 = dateValue1.getMonth()
|
||||
const month2 = dateValue2.getMonth()
|
||||
|
||||
month1 = (year1 - year2) * 12 + month1
|
||||
|
||||
return month1 - month2 + 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取偏移月份
|
||||
* @param {timestamp} date
|
||||
* @param {number} offset
|
||||
*/
|
||||
export function getMonthByOffset(date: number, offset: number) {
|
||||
const dateValue = new Date(date)
|
||||
dateValue.setMonth(dateValue.getMonth() + offset)
|
||||
|
||||
return dateValue.getTime()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认时间,格式化为数组
|
||||
* @param {array|string|null} defaultTime
|
||||
*/
|
||||
export function getDefaultTime(defaultTime: string[] | string | null) {
|
||||
if (isArray(defaultTime)) {
|
||||
const startTime = (defaultTime[0] || '00:00:00').split(':').map((item: string) => {
|
||||
return parseInt(item)
|
||||
})
|
||||
const endTime = (defaultTime[1] || '00:00:00').split(':').map((item) => {
|
||||
return parseInt(item)
|
||||
})
|
||||
return [startTime, endTime]
|
||||
} else {
|
||||
const time = (defaultTime || '00:00:00').split(':').map((item) => {
|
||||
return parseInt(item)
|
||||
})
|
||||
|
||||
return [time, time]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据默认时间获取日期
|
||||
* @param {timestamp} date
|
||||
* @param {array} defaultTime
|
||||
*/
|
||||
export function getDateByDefaultTime(date: number, defaultTime: number[]) {
|
||||
const dateValue = new Date(date)
|
||||
dateValue.setHours(defaultTime[0])
|
||||
dateValue.setMinutes(defaultTime[1])
|
||||
dateValue.setSeconds(defaultTime[2])
|
||||
|
||||
return dateValue.getTime()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取经过 iteratee 格式化后的长度为 n 的数组
|
||||
* @param {number} n
|
||||
* @param {function} iteratee
|
||||
*/
|
||||
const times = (n: number, iteratee: (index: number) => CalendarItem) => {
|
||||
let index: number = -1
|
||||
const result: CalendarItem[] = Array(n < 0 ? 0 : n)
|
||||
while (++index < n) {
|
||||
result[index] = iteratee(index)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取时分秒
|
||||
* @param {timestamp}} date
|
||||
*/
|
||||
const getTime = (date: number) => {
|
||||
const dateValue = new Date(date)
|
||||
return [dateValue.getHours(), dateValue.getMinutes(), dateValue.getSeconds()]
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据最小最大日期获取时间数据,用于填入picker
|
||||
* @param {*} param0
|
||||
*/
|
||||
export function getTimeData({
|
||||
date,
|
||||
minDate,
|
||||
maxDate,
|
||||
isHideSecond,
|
||||
filter
|
||||
}: {
|
||||
date: number
|
||||
minDate: number
|
||||
maxDate: number
|
||||
isHideSecond: boolean
|
||||
filter?: CalendarTimeFilter
|
||||
}) {
|
||||
const compareMin = compareDate(date, minDate)
|
||||
const compareMax = compareDate(date, maxDate)
|
||||
|
||||
let minHour = 0
|
||||
let maxHour = 23
|
||||
let minMinute = 0
|
||||
let maxMinute = 59
|
||||
let minSecond = 0
|
||||
let maxSecond = 59
|
||||
|
||||
if (compareMin === 0) {
|
||||
const minTime = getTime(minDate)
|
||||
const currentTime = getTime(date)
|
||||
|
||||
minHour = minTime[0]
|
||||
if (minTime[0] === currentTime[0]) {
|
||||
minMinute = minTime[1]
|
||||
|
||||
if (minTime[1] === currentTime[1]) {
|
||||
minSecond = minTime[2]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (compareMax === 0) {
|
||||
const maxTime = getTime(maxDate)
|
||||
const currentTime = getTime(date)
|
||||
|
||||
maxHour = maxTime[0]
|
||||
if (maxTime[0] === currentTime[0]) {
|
||||
maxMinute = maxTime[1]
|
||||
|
||||
if (maxTime[1] === currentTime[1]) {
|
||||
maxSecond = maxTime[2]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let columns: CalendarItem[][] = []
|
||||
let hours = times(24, (index) => {
|
||||
return {
|
||||
label: translate('hour', padZero(index)),
|
||||
value: index,
|
||||
disabled: index < minHour || index > maxHour
|
||||
}
|
||||
})
|
||||
let minutes = times(60, (index) => {
|
||||
return {
|
||||
label: translate('minute', padZero(index)),
|
||||
value: index,
|
||||
disabled: index < minMinute || index > maxMinute
|
||||
}
|
||||
})
|
||||
let seconds: CalendarItem[] = []
|
||||
if (filter && isFunction(filter)) {
|
||||
hours = filter({
|
||||
type: 'hour',
|
||||
values: hours
|
||||
})
|
||||
minutes = filter({
|
||||
type: 'minute',
|
||||
values: minutes
|
||||
})
|
||||
}
|
||||
|
||||
if (!isHideSecond) {
|
||||
seconds = times(60, (index) => {
|
||||
return {
|
||||
label: translate('second', padZero(index)),
|
||||
value: index,
|
||||
disabled: index < minSecond || index > maxSecond
|
||||
}
|
||||
})
|
||||
if (filter && isFunction(filter)) {
|
||||
seconds = filter({
|
||||
type: 'second',
|
||||
values: seconds
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
columns = isHideSecond ? [hours, minutes] : [hours, minutes, seconds]
|
||||
|
||||
return columns
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前是第几周
|
||||
* @param {timestamp} date
|
||||
*/
|
||||
export function getWeekNumber(date: number | Date) {
|
||||
date = new Date(date)
|
||||
date.setHours(0, 0, 0, 0)
|
||||
// Thursday in current week decides the year.
|
||||
date.setDate(date.getDate() + 3 - ((date.getDay() + 6) % 7))
|
||||
// January 4 is always in week 1.
|
||||
const week = new Date(date.getFullYear(), 0, 4)
|
||||
// Adjust to Thursday in week 1 and count number of weeks from date to week 1.
|
||||
// Rounding should be fine for Daylight Saving Time. Its shift should never be more than 12 hours.
|
||||
return 1 + Math.round(((date.getTime() - week.getTime()) / 86400000 - 3 + ((week.getDay() + 6) % 7)) / 7)
|
||||
}
|
||||
|
||||
export function getItemClass(monthType: CalendarDayType, value: number | null | (number | null)[], type: CalendarType) {
|
||||
const classList = ['is-' + monthType]
|
||||
|
||||
if (type.indexOf('range') > -1 && isArray(value)) {
|
||||
if (!value || !value[1]) {
|
||||
classList.push('is-without-end')
|
||||
}
|
||||
}
|
||||
|
||||
return classList.join(' ')
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<view :class="`wd-calendar-view ${customClass}`">
|
||||
<year-panel
|
||||
v-if="type === 'month' || type === 'monthrange'"
|
||||
ref="yearPanelRef"
|
||||
:type="type"
|
||||
:value="modelValue"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
:formatter="formatter"
|
||||
:max-range="maxRange"
|
||||
:range-prompt="rangePrompt"
|
||||
:allow-same-day="allowSameDay"
|
||||
:show-panel-title="showPanelTitle"
|
||||
:default-time="formatDefauleTime"
|
||||
:panel-height="panelHeight"
|
||||
@change="handleChange"
|
||||
/>
|
||||
<month-panel
|
||||
v-else
|
||||
ref="monthPanelRef"
|
||||
:type="type"
|
||||
:value="modelValue"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
:first-day-of-week="firstDayOfWeek"
|
||||
:formatter="formatter"
|
||||
:max-range="maxRange"
|
||||
:range-prompt="rangePrompt"
|
||||
:allow-same-day="allowSameDay"
|
||||
:show-panel-title="showPanelTitle"
|
||||
:default-time="formatDefauleTime"
|
||||
:panel-height="panelHeight"
|
||||
:immediate-change="immediateChange"
|
||||
:time-filter="timeFilter"
|
||||
:hide-second="hideSecond"
|
||||
@change="handleChange"
|
||||
@pickstart="handlePickStart"
|
||||
@pickend="handlePickEnd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'wd-calendar-view',
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
virtualHost: true,
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { getDefaultTime } from './utils'
|
||||
import yearPanel from './yearPanel/year-panel.vue'
|
||||
import MonthPanel from './monthPanel/month-panel.vue'
|
||||
import { calendarViewProps, type CalendarViewExpose } from './types'
|
||||
|
||||
const props = defineProps(calendarViewProps)
|
||||
const emit = defineEmits(['change', 'update:modelValue', 'pickstart', 'pickend'])
|
||||
const formatDefauleTime = ref<number[][]>([])
|
||||
|
||||
const yearPanelRef = ref()
|
||||
const monthPanelRef = ref()
|
||||
|
||||
watch(
|
||||
() => props.defaultTime,
|
||||
(newValue) => {
|
||||
formatDefauleTime.value = getDefaultTime(newValue)
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 使当前日期或者选中日期滚动到可视区域
|
||||
*/
|
||||
function scrollIntoView() {
|
||||
const panel = getPanel()
|
||||
panel.scrollIntoView && panel.scrollIntoView()
|
||||
}
|
||||
|
||||
function getPanel() {
|
||||
return props.type.indexOf('month') > -1 ? yearPanelRef.value : monthPanelRef.value
|
||||
}
|
||||
|
||||
function handleChange({ value }: { value: number | number[] | null }) {
|
||||
emit('update:modelValue', value)
|
||||
emit('change', {
|
||||
value
|
||||
})
|
||||
}
|
||||
function handlePickStart() {
|
||||
emit('pickstart')
|
||||
}
|
||||
function handlePickEnd() {
|
||||
emit('pickend')
|
||||
}
|
||||
|
||||
defineExpose<CalendarViewExpose>({
|
||||
scrollIntoView
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './index.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,153 @@
|
||||
@import '../../common/abstracts/variable';
|
||||
@import '../../common/abstracts/mixin';
|
||||
|
||||
.wot-theme-dark {
|
||||
@include b(year) {
|
||||
@include e(title) {
|
||||
color: $-dark-color;
|
||||
}
|
||||
|
||||
@include e(months) {
|
||||
color: $-dark-color;
|
||||
}
|
||||
|
||||
@include e(month) {
|
||||
|
||||
@include when(disabled) {
|
||||
.wd-year__month-text {
|
||||
color: $-dark-color-gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include b(year) {
|
||||
@include e(title) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 45px;
|
||||
font-size: $-calendar-panel-title-fs;
|
||||
color: $-calendar-panel-title-color;
|
||||
}
|
||||
|
||||
@include e(months) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
font-size: $-calendar-day-fs;
|
||||
color: $-calendar-day-color;
|
||||
}
|
||||
|
||||
@include e(month) {
|
||||
position: relative;
|
||||
width: 25%;
|
||||
height: $-calendar-day-height;
|
||||
line-height: $-calendar-day-height;
|
||||
text-align: center;
|
||||
margin-bottom: $-calendar-item-margin-bottom;
|
||||
|
||||
@include when(disabled) {
|
||||
.wd-year__month-text {
|
||||
color: $-calendar-disabled-color;
|
||||
}
|
||||
}
|
||||
|
||||
@include when(current) {
|
||||
color: $-calendar-active-color;
|
||||
}
|
||||
|
||||
@include when(selected) {
|
||||
color: #fff;
|
||||
|
||||
.wd-year__month-text {
|
||||
border-radius: $-calendar-active-border;
|
||||
background: $-calendar-active-color;
|
||||
}
|
||||
}
|
||||
|
||||
@include when(middle) {
|
||||
background: $-calendar-range-color;
|
||||
}
|
||||
|
||||
@include when(start) {
|
||||
color: $-calendar-selected-color;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
content: '';
|
||||
background: $-calendar-range-color;
|
||||
}
|
||||
|
||||
.wd-year__month-text {
|
||||
background: $-calendar-active-color;
|
||||
border-radius: $-calendar-active-border 0 0 $-calendar-active-border;
|
||||
}
|
||||
|
||||
&.is-without-end::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@include when(end) {
|
||||
color: $-calendar-selected-color;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 50%;
|
||||
bottom: 0;
|
||||
content: '';
|
||||
background: $-calendar-range-color;
|
||||
}
|
||||
|
||||
.wd-year__month-text {
|
||||
background: $-calendar-active-color;
|
||||
border-radius: 0 $-calendar-active-border $-calendar-active-border 0;
|
||||
}
|
||||
}
|
||||
|
||||
@include when(same) {
|
||||
color: $-calendar-selected-color;
|
||||
|
||||
.wd-year__month-text {
|
||||
background: $-calendar-active-color;
|
||||
border-radius: $-calendar-active-border;
|
||||
}
|
||||
}
|
||||
@include when(last-row){
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(month-text) {
|
||||
width: $-calendar-month-width;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@include e(month-top) {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
line-height: 1.1;
|
||||
font-size: $-calendar-info-fs;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@include e(month-bottom) {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
line-height: 1.1;
|
||||
font-size: $-calendar-info-fs;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { PropType } from 'vue'
|
||||
import { makeBooleanProp, makeRequiredProp } from '../../common/props'
|
||||
import type { CalendarFormatter, CalendarType } from '../types'
|
||||
|
||||
export const yearProps = {
|
||||
type: makeRequiredProp(String as PropType<CalendarType>),
|
||||
date: makeRequiredProp(Number),
|
||||
value: makeRequiredProp([Number, Array] as PropType<number | (number | null)[] | null>),
|
||||
minDate: makeRequiredProp(Number),
|
||||
maxDate: makeRequiredProp(Number),
|
||||
// 日期格式化函数
|
||||
formatter: Function as PropType<CalendarFormatter>,
|
||||
maxRange: Number,
|
||||
rangePrompt: String,
|
||||
allowSameDay: makeBooleanProp(false),
|
||||
defaultTime: {
|
||||
type: [Array] as PropType<Array<number[]>>
|
||||
},
|
||||
showTitle: makeBooleanProp(true)
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<wd-toast selector="wd-year" />
|
||||
|
||||
<view class="wd-year year">
|
||||
<view class="wd-year__title" v-if="showTitle">{{ yearTitle(date) }}</view>
|
||||
<view class="wd-year__months">
|
||||
<view
|
||||
v-for="(item, index) in months"
|
||||
:key="index"
|
||||
:class="`wd-year__month ${item.disabled ? 'is-disabled' : ''} ${item.isLastRow ? 'is-last-row' : ''} ${
|
||||
item.type ? monthTypeClass(item.type) : ''
|
||||
}`"
|
||||
@click="handleDateClick(index)"
|
||||
>
|
||||
<view class="wd-year__month-top">{{ item.topInfo }}</view>
|
||||
<view class="wd-year__month-text">{{ getMonthLabel(item.date) }}</view>
|
||||
<view class="wd-year__month-bottom">{{ item.bottomInfo }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
virtualHost: true,
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import wdToast from '../../wd-toast/wd-toast.vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { deepClone, isArray, isFunction } from '../../common/util'
|
||||
import { compareMonth, formatYearTitle, getDateByDefaultTime, getItemClass, getMonthByOffset, getMonthOffset } from '../utils'
|
||||
import { useToast } from '../../wd-toast'
|
||||
import { useTranslate } from '../../composables/useTranslate'
|
||||
import dayjs from '../../../dayjs'
|
||||
import { yearProps } from './types'
|
||||
import type { CalendarDayItem, CalendarDayType } from '../types'
|
||||
|
||||
const props = defineProps(yearProps)
|
||||
const emit = defineEmits(['change'])
|
||||
|
||||
const toast = useToast('wd-year')
|
||||
const { translate } = useTranslate('calendar-view')
|
||||
|
||||
const months = ref<CalendarDayItem[]>([])
|
||||
|
||||
const monthTypeClass = computed(() => {
|
||||
return (monthType: CalendarDayType) => {
|
||||
return getItemClass(monthType, props.value, props.type)
|
||||
}
|
||||
})
|
||||
|
||||
const yearTitle = computed(() => {
|
||||
return (date: number) => {
|
||||
return formatYearTitle(date)
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
[() => props.type, () => props.date, () => props.value, () => props.minDate, () => props.maxDate, () => props.formatter],
|
||||
() => {
|
||||
setMonths()
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
function getMonthLabel(date: number) {
|
||||
return dayjs(date).format(translate('month', date))
|
||||
}
|
||||
|
||||
function setMonths() {
|
||||
const monthList: CalendarDayItem[] = []
|
||||
const date = new Date(props.date)
|
||||
const year = date.getFullYear()
|
||||
const value = props.value
|
||||
|
||||
if (props.type.indexOf('range') > -1 && value && !isArray(value)) {
|
||||
console.error('[wot-design] value should be array when type is range')
|
||||
return
|
||||
}
|
||||
|
||||
for (let month = 0; month < 12; month++) {
|
||||
const date = new Date(year, month, 1).getTime()
|
||||
let type: CalendarDayType = getMonthType(date)
|
||||
if (!type && compareMonth(date, Date.now()) === 0) {
|
||||
type = 'current'
|
||||
}
|
||||
const monthObj = getFormatterDate(date, month, type)
|
||||
monthList.push(monthObj)
|
||||
}
|
||||
|
||||
months.value = deepClone(monthList)
|
||||
}
|
||||
function getMonthType(date: number) {
|
||||
if (props.type === 'monthrange' && isArray(props.value)) {
|
||||
const [startDate, endDate] = props.value || []
|
||||
|
||||
if (startDate && compareMonth(date, startDate) === 0) {
|
||||
if (endDate && compareMonth(startDate, endDate) === 0) {
|
||||
return 'same'
|
||||
}
|
||||
return 'start'
|
||||
} else if (endDate && compareMonth(date, endDate) === 0) {
|
||||
return 'end'
|
||||
} else if (startDate && endDate && compareMonth(date, startDate) === 1 && compareMonth(date, endDate) === -1) {
|
||||
return 'middle'
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
} else {
|
||||
if (props.value && compareMonth(date, props.value as number) === 0) {
|
||||
return 'selected'
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleDateClick(index: number) {
|
||||
const date = months.value[index]
|
||||
|
||||
if (date.disabled) return
|
||||
|
||||
switch (props.type) {
|
||||
case 'month':
|
||||
handleMonthChange(date)
|
||||
break
|
||||
case 'monthrange':
|
||||
handleMonthRangeChange(date)
|
||||
break
|
||||
default:
|
||||
handleMonthChange(date)
|
||||
}
|
||||
}
|
||||
function getDate(date: number) {
|
||||
return props.defaultTime && props.defaultTime.length > 0 ? getDateByDefaultTime(date, props.defaultTime[0]) : date
|
||||
}
|
||||
function handleMonthChange(date: CalendarDayItem) {
|
||||
if (date.type !== 'selected') {
|
||||
emit('change', {
|
||||
value: getDate(date.date)
|
||||
})
|
||||
}
|
||||
}
|
||||
function handleMonthRangeChange(date: CalendarDayItem) {
|
||||
let value: (number | null)[] = []
|
||||
const [startDate, endDate] = isArray(props.value) ? props.value || [] : []
|
||||
const compare = compareMonth(date.date, startDate!)
|
||||
|
||||
// 禁止选择同个日期
|
||||
if (!props.allowSameDay && !endDate && compare === 0) return
|
||||
|
||||
if (startDate && !endDate && compare > -1) {
|
||||
if (props.maxRange && getMonthOffset(date.date, startDate) > props.maxRange) {
|
||||
const maxEndDate = getMonthByOffset(startDate, props.maxRange - 1)
|
||||
value = [startDate, getDate(maxEndDate)]
|
||||
toast.show({
|
||||
msg: props.rangePrompt || translate('rangePromptMonth', props.maxRange)
|
||||
})
|
||||
} else {
|
||||
value = [startDate, getDate(date.date)]
|
||||
}
|
||||
} else {
|
||||
value = [getDate(date.date), null]
|
||||
}
|
||||
emit('change', {
|
||||
value
|
||||
})
|
||||
}
|
||||
|
||||
function getFormatterDate(date: number, month: number, type?: CalendarDayType) {
|
||||
let monthObj: CalendarDayItem = {
|
||||
date: date,
|
||||
text: month + 1,
|
||||
topInfo: '',
|
||||
bottomInfo: '',
|
||||
type,
|
||||
disabled: compareMonth(date, props.minDate) === -1 || compareMonth(date, props.maxDate) === 1,
|
||||
isLastRow: month >= 8
|
||||
}
|
||||
|
||||
if (props.formatter) {
|
||||
if (isFunction(props.formatter)) {
|
||||
monthObj = props.formatter(monthObj)
|
||||
} else {
|
||||
console.error('[wot-design] error(wd-calendar-view): the formatter prop of wd-calendar-view should be a function')
|
||||
}
|
||||
}
|
||||
|
||||
return monthObj
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './index.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
@import '../../common/abstracts/variable';
|
||||
@import '../../common/abstracts/mixin';
|
||||
|
||||
.wot-theme-dark {
|
||||
@include b(year-panel) {
|
||||
@include e(title) {
|
||||
color: $-dark-color;
|
||||
box-shadow: 0px 4px 8px 0 rgba(255, 255,255, 0.02);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include b(year-panel) {
|
||||
font-size: $-calendar-fs;
|
||||
padding: $-calendar-panel-padding;
|
||||
|
||||
@include e(title) {
|
||||
padding: 5px 0;
|
||||
text-align: center;
|
||||
font-size: $-calendar-panel-title-fs;
|
||||
color: $-calendar-panel-title-color;
|
||||
box-shadow: 0px 4px 8px 0 rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { ComponentPublicInstance, ExtractPropTypes, PropType } from 'vue'
|
||||
import { makeBooleanProp, makeRequiredProp } from '../../common/props'
|
||||
import type { CalendarFormatter, CalendarType } from '../types'
|
||||
|
||||
/**
|
||||
* 月份信息
|
||||
*/
|
||||
export interface YearInfo {
|
||||
date: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export const yearPanelProps = {
|
||||
type: makeRequiredProp(String as PropType<CalendarType>),
|
||||
value: makeRequiredProp([Number, Array] as PropType<number | (number | null)[] | null>),
|
||||
minDate: makeRequiredProp(Number),
|
||||
maxDate: makeRequiredProp(Number),
|
||||
formatter: Function as PropType<CalendarFormatter>,
|
||||
maxRange: Number,
|
||||
rangePrompt: String,
|
||||
allowSameDay: makeBooleanProp(false),
|
||||
showPanelTitle: makeBooleanProp(false),
|
||||
defaultTime: {
|
||||
type: [Array] as PropType<Array<number[]>>
|
||||
},
|
||||
panelHeight: makeRequiredProp(Number)
|
||||
}
|
||||
|
||||
export type YearPanelProps = ExtractPropTypes<typeof yearPanelProps>
|
||||
|
||||
export type YearPanelExpose = {
|
||||
/**
|
||||
* 使当前日期或者选中日期滚动到可视区域
|
||||
*/
|
||||
scrollIntoView: () => void
|
||||
}
|
||||
|
||||
export type YearPanelInstance = ComponentPublicInstance<YearPanelProps, YearPanelExpose>
|
||||
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<view class="wd-year-panel">
|
||||
<view v-if="showPanelTitle" class="wd-year-panel__title">{{ title }}</view>
|
||||
<scroll-view class="wd-year-panel__container" :style="`height: ${scrollHeight}px`" scroll-y @scroll="yearScroll" :scroll-top="scrollTop">
|
||||
<view v-for="(item, index) in years" :key="index" :id="`year${index}`">
|
||||
<year
|
||||
:type="type"
|
||||
:date="item.date"
|
||||
:value="value"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
:max-range="maxRange"
|
||||
:formatter="formatter"
|
||||
:range-prompt="rangePrompt"
|
||||
:allow-same-day="allowSameDay"
|
||||
:default-time="defaultTime"
|
||||
:showTitle="index !== 0"
|
||||
@change="handleDateChange"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
virtualHost: true,
|
||||
styleIsolation: 'shared'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { compareYear, formatYearTitle, getYears } from '../utils'
|
||||
import { isArray, isNumber, pause } from '../../common/util'
|
||||
import Year from '../year/year.vue'
|
||||
import { yearPanelProps, type YearInfo, type YearPanelExpose } from './types'
|
||||
|
||||
const props = defineProps(yearPanelProps)
|
||||
const emit = defineEmits(['change'])
|
||||
|
||||
const scrollTop = ref<number>(0) // 滚动位置
|
||||
const scrollIndex = ref<number>(0) // 当前显示的年份索引
|
||||
|
||||
// 滚动区域的高度
|
||||
const scrollHeight = computed(() => {
|
||||
const scrollHeight: number = props.panelHeight + (props.showPanelTitle ? 26 : 16)
|
||||
return scrollHeight
|
||||
})
|
||||
|
||||
// 年份信息
|
||||
const years = computed<YearInfo[]>(() => {
|
||||
return getYears(props.minDate, props.maxDate).map((year, index) => {
|
||||
return {
|
||||
date: year,
|
||||
height: index === 0 ? 200 : 245
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 标题
|
||||
const title = computed(() => {
|
||||
return formatYearTitle(years.value[scrollIndex.value].date)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
scrollIntoView()
|
||||
})
|
||||
|
||||
async function scrollIntoView() {
|
||||
await pause()
|
||||
let activeDate: number | null = null
|
||||
if (isArray(props.value)) {
|
||||
activeDate = props.value![0]
|
||||
} else if (isNumber(props.value)) {
|
||||
activeDate = props.value
|
||||
}
|
||||
|
||||
if (!activeDate) {
|
||||
activeDate = Date.now()
|
||||
}
|
||||
|
||||
let top: number = 0
|
||||
for (let index = 0; index < years.value.length; index++) {
|
||||
if (compareYear(years.value[index].date, activeDate) === 0) {
|
||||
break
|
||||
}
|
||||
top += years.value[index] ? Number(years.value[index].height) : 0
|
||||
}
|
||||
scrollTop.value = 0
|
||||
if (top > 0) {
|
||||
await pause()
|
||||
scrollTop.value = top + 45
|
||||
}
|
||||
}
|
||||
|
||||
const yearScroll = (event: { detail: { scrollTop: number } }) => {
|
||||
if (years.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 < years.value.length; index++) {
|
||||
height = height + years.value[index].height
|
||||
if (scrollTop < height) {
|
||||
scrollIndex.value = index
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleDateChange({ value }: { value: number[] }) {
|
||||
emit('change', {
|
||||
value
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose<YearPanelExpose>({
|
||||
scrollIntoView
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './index.scss';
|
||||
</style>
|
||||
Reference in New Issue
Block a user