2
0
mirror of https://github.com/tenrok/vue2-datepicker.git synced 2026-06-14 21:02:27 +03:00
Files
vue2-datepicker/datepicker/calendar-panel.vue
T
2018-04-12 16:05:51 +08:00

674 lines
18 KiB
Vue

<template>
<div class="mx-calendar">
<div class="mx-calendar-header" v-if="currentPanel === 'time'">
<a @click="currentPanel = 'date'">{{now.toLocaleDateString()}}</a>
</div>
<div class="mx-calendar-header" v-else>
<a class="mx-calendar__prev-icon" @click="changeYear(-1)">&laquo;</a>
<a v-show="currentPanel === 'date'" class="mx-calendar__prev-icon" @click="changeMonth(-1)">&lsaquo;</a>
<a class="mx-calendar__next-icon" @click="changeYear(1)">&raquo;</a>
<a v-show="currentPanel === 'date'" class="mx-calendar__next-icon" @click="changeMonth(1)">&rsaquo;</a>
<a @click="showMonths">{{months[currentMonth]}}</a>
<a @click="showYears">{{currentYear}}</a>
</div>
<div class="mx-calendar-content">
<table class="mx-calendar-table" v-show="currentPanel === 'date'">
<thead>
<tr>
<th v-for="(day, index) in days" :key="index">{{day}}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in dates">
<td v-for="cell in row" :title="cell.title" :class="getDateClasses(cell)" @click="selectDate(cell)">{{cell.day}}</td>
</tr>
</tbody>
</table>
<div class="mx-calendar-year" v-show="currentPanel === 'years'">
<a v-for="year in years"
@click="selectYear(year)"
:class="{'current': currentYear === year, 'disabled': isDisabledYear(year)}">{{year}}</a>
</div>
<div class="mx-calendar-month" v-show="currentPanel === 'months'">
<a v-for="(month, index) in months"
@click="selectMonth(index)"
:class="{'current': currentMonth === index, 'disabled': isDisabledMonth(index)}">{{month}}</a>
</div>
<div class="mx-calendar-time"
v-show="currentPanel === 'time'" >
<div v-if="timeSelectOptions.length" class="mx-time-list-wrapper">
<ul class="mx-time-list">
<li class="mx-time-item mx-time-picker-item"
:class="getTimeClasses(item.value.hours * 60 + item.value.minutes, -1)"
@click="pickTime(item.value)"
v-for="item in timeSelectOptions">
{{item.label}}
</li>
</ul>
</div>
<div v-else class="mx-time-list-wrapper"
:style="{width: 100 / times.length + '%' }"
v-for="(time, index) in times"
:key="index">
<ul class="mx-time-list">
<li class="mx-time-item"
v-for="num in time"
:class="getTimeClasses(num, index)"
:key="num"
@click="selectTime(num, index)"
>{{num | timeText}}</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
const getTimeArray = function (len, step = 1) {
const length = parseInt(len / step)
return Array.apply(null, { length }).map((v, i) => i * step)
}
const parseTime = function(time) {
const values = (time || '').split(':');
if (values.length >= 2) {
const hours = parseInt(values[0], 10);
const minutes = parseInt(values[1], 10);
return {
hours,
minutes
}
}
return null;
}
const formatTime = function(time, type="24") {
let hours = time.hours
hours = (type === '24') ? hours : (hours % 12 || 12)
hours = hours < 10 ? '0' + hours : hours
let minutes = time.minutes < 10 ? '0' + time.minutes : time.minutes
let result = hours + ':' + minutes
if (type === '12') {
result += time.hours >= 12 ? ' pm' : ' am'
}
return result
}
export default {
props: {
startAt: null,
endAt: null,
value: null,
show: Boolean
},
data() {
const translation = this.$parent.translation
const minuteStep = this.$parent.minuteStep
const times = [getTimeArray(24, 1), getTimeArray(60, minuteStep || 1)]
if (minuteStep === 0) {
times.push(getTimeArray(60, 1))
}
return {
months: translation.months,
dates: [], // 日历面板
years: [], // 年代面板
now: new Date(), // calendar-header 显示的时间, 用于切换日历
currentPanel: 'date',
times: times
}
},
computed: {
// 日历显示头
days() {
const days = this.$parent.translation.days
const firstday = +this.$parent.firstDayOfWeek
return days.concat(days).slice(firstday, firstday + 7)
},
timeType () {
return /h+/.test(this.$parent.format) ? '12' : '24'
},
timeSelectOptions () {
const result = []
const options = this.$parent.timePickerOptions
if (!options) {
return []
}
const start = parseTime(options.start)
const end = parseTime(options.end)
const step = parseTime(options.step)
if (start && end && step) {
const startMinutes = start.minutes + start.hours * 60;
const endMinutes = end.minutes + end.hours * 60;
const stepMinutes = step.minutes + step.hours * 60
const len = Math.floor((endMinutes - startMinutes) / stepMinutes)
for (let i = 0; i <= len; i++) {
let timeMinutes = startMinutes + i * stepMinutes
let hours = Math.floor(timeMinutes/60)
let minutes = timeMinutes % 60
let value = {
hours, minutes
}
result.push({
value,
label: formatTime(value, this.timeType)
})
}
}
return result
},
currentYear() {
return this.now.getFullYear()
},
currentMonth() {
return this.now.getMonth()
},
curHour() {
return this.now.getHours()
},
curMinute() {
return this.now.getMinutes()
},
curSecond() {
return this.now.getSeconds()
}
},
created() {
this.updateCalendar()
},
watch: {
show(val) {
if (val) {
this.currentPanel = 'date'
this.updateNow()
}
},
value: {
handler: 'updateNow',
immediate: true
},
now: 'updateCalendar'
},
filters: {
timeText(value) {
return ('00' + value).slice(String(value).length)
}
},
methods: {
updateNow() {
this.now = this.value ? new Date(this.value) : new Date()
},
// 更新面板选择时间
updateCalendar() {
function getCalendar(time, firstday, length, classes) {
return Array.apply(null, { length }).map((v, i) => {
// eslint-disable-line
let day = firstday + i
const date = new Date(
time.getFullYear(),
time.getMonth(),
day,
0,
0,
0
)
date.setDate(day)
return {
title: date.toLocaleDateString(),
date,
day,
classes
}
})
}
const firstDayOfWeek = this.$parent.firstDayOfWeek
const time = new Date(this.now)
time.setDate(0) // 把时间切换到上个月最后一天
const lastMonthLength = (time.getDay() + 7 - firstDayOfWeek) % 7 + 1 // time.getDay() 0是星期天, 1是星期一 ...
const lastMonthfirst = time.getDate() - (lastMonthLength - 1)
const lastMonth = getCalendar(
time,
lastMonthfirst,
lastMonthLength,
'lastMonth'
)
time.setMonth(time.getMonth() + 2, 0) // 切换到这个月最后一天
const curMonthLength = time.getDate()
const curMonth = getCalendar(time, 1, curMonthLength, 'curMonth')
time.setMonth(time.getMonth() + 1, 1)
const nextMonthLength = 42 - (lastMonthLength + curMonthLength)
const nextMonth = getCalendar(time, 1, nextMonthLength, 'nextMonth')
// 分割数组
let index = 0
let resIndex = 0
const arr = lastMonth.concat(curMonth, nextMonth)
const result = new Array(6)
while (index < 42) {
result[resIndex++] = arr.slice(index, (index += 7))
}
this.dates = result
},
isDisabled (date) {
const now = new Date(date).getTime()
if (
this.$parent.disabledDays.some(v => new Date(v).setHours(0, 0, 0, 0) === now) ||
this.$parent.notBefore !== '' && now < new Date(this.$parent.notBefore).getTime() ||
this.$parent.notAfter !== '' && now > new Date(this.$parent.notAfter).getTime() ||
this.startAt && now < new Date(this.startAt).setHours(0, 0, 0, 0) ||
this.endAt && now > new Date(this.endAt).setHours(0, 0, 0, 0)
) {
return true
}
return false
},
getDateClasses(cell) {
const classes = []
const cellTime = new Date(cell.date).setHours(0, 0, 0, 0)
const cellEndTime = new Date(cell.date).setHours(23, 59, 59, 999)
const curTime = this.value ? new Date(this.value).setHours(0, 0, 0, 0) : 0
const startTime = this.startAt
? new Date(this.startAt).setHours(0, 0, 0, 0)
: 0
const endTime = this.endAt ? new Date(this.endAt).setHours(0, 0, 0, 0) : 0
const today = new Date().setHours(0, 0, 0, 0)
if (this.isDisabled(cellTime)) {
return 'disabled'
}
classes.push(cell.classes)
if (cellTime === today) {
classes.push('today')
}
// range classes
if (curTime) {
if (cellTime === curTime) {
classes.push('current')
} else if (startTime && cellTime <= curTime) {
classes.push('inrange')
} else if (endTime && cellTime >= curTime) {
classes.push('inrange')
}
}
return classes.join(' ')
},
getTimeClasses(value, index) {
let curValue
let cellTime
const startTime = this.startAt ? new Date(this.startAt) : 0
const endTime = this.endAt ? new Date(this.endAt) : 0
const classes = []
switch (index) {
case -1:
curValue = this.curHour * 60 + this.curMinute
cellTime = new Date(this.now).setHours(Math.floor(value / 60), value % 60, 0)
break
case 0:
curValue = this.curHour
cellTime = new Date(this.now).setHours(value)
break
case 1:
curValue = this.curMinute
cellTime = new Date(this.now).setMinutes(value)
break
case 2:
curValue = this.curSecond
cellTime = new Date(this.now).setSeconds(value)
break
}
if (
(this.$parent.notBefore !== '' &&
cellTime < new Date(this.$parent.notBefore).getTime()) ||
(this.$parent.notAfter !== '' &&
cellTime > new Date(this.$parent.notAfter).getTime())
) {
return 'disabled'
}
if (value === curValue) {
classes.push('cur-time')
} else if (startTime) {
if (cellTime < startTime) {
classes.push('disabled')
}
} else if (endTime) {
if (cellTime > endTime) {
classes.push('disabled')
}
}
return classes.join(' ')
},
showMonths() {
if (this.currentPanel === 'months') {
this.currentPanel = 'date'
} else {
this.currentPanel = 'months'
}
},
showYears() {
// 当前年代
if (this.currentPanel === 'years') {
this.currentPanel = 'date'
} else {
let firstYear = Math.floor(this.now.getFullYear() / 10) * 10
let years = []
for (let i = 0; i < 10; i++) {
years.push(firstYear + i)
}
this.years = years
this.currentPanel = 'years'
}
},
// 前进或后退一年
changeYear(flag) {
if (this.currentPanel === 'years') {
this.years = this.years.map(v => v + flag * 10)
} else {
const now = new Date(this.now)
now.setFullYear(now.getFullYear() + flag, now.getMonth(), 1)
this.now = now
}
},
changeMonth(flag) {
const now = new Date(this.now)
now.setMonth(now.getMonth() + flag, 1)
this.now = now
},
scrollIntoView(container, selected) {
if (!selected) {
container.scrollTop = 0
return
}
const top = selected.offsetTop
const bottom = selected.offsetTop + selected.offsetHeight
const viewRectTop = container.scrollTop
const viewRectBottom = viewRectTop + container.clientHeight
if (top < viewRectTop) {
container.scrollTop = top
} else if (bottom > viewRectBottom) {
container.scrollTop = bottom - container.clientHeight
}
},
selectDate(cell) {
const classes = this.getDateClasses(cell)
if (classes.indexOf('disabled') !== -1) {
return
}
let date = new Date(cell.date)
// datetime 跳转到 timepicker
if (this.$parent.type === 'datetime') {
// 保留时分秒
if (this.value instanceof Date) {
date.setHours(
this.value.getHours(),
this.value.getMinutes(),
this.value.getSeconds()
)
}
if (this.startAt && date.getTime() < new Date(this.startAt).getTime()) {
date = new Date(this.startAt)
} else if (
this.endAt &&
date.getTime() > new Date(this.endAt).getTime()
) {
date = new Date(this.endAt)
}
this.currentPanel = 'time'
this.$nextTick(() => {
Array.prototype.forEach.call(
this.$el.querySelectorAll('.mx-time-list-wrapper'),
(el) => {
this.scrollIntoView(el, el.querySelector('.cur-time'))
}
)
})
}
this.now = date
this.$emit('input', date)
this.$emit('select')
},
isDisabledYear (year) {
if (this.value) {
const now = new Date(this.now).setFullYear(year)
return this.isDisabled(now)
}
return false
},
isDisabledMonth (month) {
if (this.value) {
const now = new Date(this.now).setMonth(month)
return this.isDisabled(now)
}
return false
},
selectYear(year) {
if (this.isDisabledYear(year)) {
return
}
const now = new Date(this.now)
now.setFullYear(year)
this.now = now
if (this.value) {
this.$emit('input', now)
this.$emit('select', true)
}
this.currentPanel = 'months'
},
selectMonth(month) {
if (this.isDisabledMonth(month)) {
return
}
const now = new Date(this.now)
now.setMonth(month)
this.now = now
if (this.value) {
this.$emit('input', now)
this.$emit('select', true)
}
this.currentPanel = 'date'
},
selectTime(value, index) {
const classes = this.getTimeClasses(value, index)
if (classes.indexOf('disabled') !== -1) {
return
}
const date = new Date(this.now)
if (index === 0) {
date.setHours(value)
} else if (index === 1) {
date.setMinutes(value)
} else if (index === 2) {
date.setSeconds(value)
}
this.now = date
this.$emit('input', date)
this.$emit('select')
},
pickTime (value) {
const classes = this.getTimeClasses(value.hours * 60 + value.minutes, -1)
if (classes.indexOf('disabled') !== -1) {
return
}
const date = new Date(this.now)
date.setHours(value.hours, value.minutes, 0)
this.now = date
this.$emit('input', date)
this.$emit('select')
}
}
}
</script>
<style lang="scss">
.mx-calendar {
float: left;
width: 100%;
padding: 6px 12px;
a {
color: inherit;
text-decoration: none;
cursor: pointer;
}
}
.mx-calendar-header {
line-height: 34px;
text-align: center;
& > a:hover {
color: #1284e7;
}
}
.mx-calendar__next-icon,
.mx-calendar__prev-icon {
font-size: 20px;
padding: 0 6px;
}
.mx-calendar__prev-icon {
float: left;
}
.mx-calendar__next-icon {
float: right;
}
.mx-calendar-content {
height: 224px;
overflow: hidden;
}
.mx-calendar-table {
width: 100%;
font-size: 12px;
table-layout: fixed;
border-collapse: collapse;
border-spacing: 0;
td {
cursor: pointer;
}
.today {
color: #20a0ff;
}
.lastMonth,
.nextMonth {
color: #ddd;
}
}
.mx-calendar-table td,
.mx-calendar-table th {
width: 32px;
height: 32px;
text-align: center;
}
.mx-calendar-table td {
cursor: pointer;
}
.mx-calendar-table td.inrange,
.mx-calendar-table td:hover,
.mx-calendar-year > a:hover,
.mx-calendar-month > a:hover {
background-color: #eaf8fe;
}
.mx-calendar-table td.current,
.mx-calendar-year > a.current,
.mx-calendar-month > a.current {
color: #fff;
background-color: #1284e7;
}
.mx-calendar-table td.disabled,
.mx-time-item.disabled,
.mx-calendar-year a.disabled,
.mx-calendar-month a.disabled {
cursor: not-allowed;
color: #ccc;
background-color: #f3f3f3;
}
.mx-calendar-year,
.mx-calendar-month,
.mx-calendar-time {
width: 100%;
height: 100%;
padding: 7px 0;
text-align: center;
}
.mx-calendar-year > a {
display: inline-block;
width: 40%;
margin: 1px 5%;
line-height: 40px;
}
.mx-calendar-month > a {
display: inline-block;
width: 30%;
line-height: 40px;
margin: 8px 1.5%;
}
.mx-time-list-wrapper {
position: relative;
display: inline-block;
width: 100%;
height: 100%;
border-top: 1px solid rgba(0, 0, 0, 0.05);
border-left: 1px solid rgba(0, 0, 0, 0.05);
box-sizing: border-box;
overflow-y: auto;
}
.mx-time-list-wrapper::-webkit-scrollbar {
width: 8px;
height: 8px;
}
/* 滚动条滑块 */
.mx-time-list-wrapper::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.05);
border-radius: 10px;
box-shadow: inset 1px 1px 0 rgba(0, 0, 0, 0.1);
}
.mx-time-list-wrapper:hover::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
}
.mx-time-list-wrapper:first-child {
border-left: 0;
}
.mx-time-picker-item {
text-align: left;
padding-left: 10px;
}
.mx-time-list {
margin: 0;
padding: 0;
list-style: none;
}
.mx-time-item {
width: 100%;
font-size: 12px;
height: 30px;
line-height: 30px;
cursor: pointer;
}
.mx-time-item:hover {
background-color: #eaf8fe;
}
.mx-time-item.cur-time {
color: #fff;
background-color: #1284e7;
}
</style>