2
0
mirror of https://github.com/tenrok/vue2-datepicker.git synced 2026-06-13 07:22:26 +03:00

refactor: 2.0

This commit is contained in:
mxie
2018-06-16 10:11:13 +08:00
parent d28d307def
commit 96fffcab04
39 changed files with 10988 additions and 2969 deletions
+302
View File
@@ -0,0 +1,302 @@
<template>
<div class="mx-calendar">
<div class="mx-calendar-header">
<a
v-show="panel !== 'TIME'"
class="mx-icon-last-year"
@click="handleIconYear(-1)">&laquo;</a>
<a
v-show="panel === 'DATE'"
class="mx-icon-last-month"
@click="handleIconMonth(-1)">&lsaquo;</a>
<a
v-show="panel !== 'TIME'"
class="mx-icon-next-year"
@click="handleIconYear(1)">&raquo;</a>
<a
v-show="panel === 'DATE'"
class="mx-icon-next-month"
@click="handleIconMonth(1)">&rsaquo;</a>
<a
v-show="panel !== 'TIME'"
class="mx-current-month"
@click="handleBtnMonth">{{months[calendarMonth]}}</a>
<a
v-show="panel !== 'TIME'"
class="mx-current-year"
@click="handleBtnYear">{{calendarYear}}</a>
<a
v-show="panel === 'TIME'"
class="mx-time-header"
@click="showPanelDate">{{timeHeader}}</a>
</div>
<div class="mx-calendar-content">
<panel-date
v-show="panel === 'DATE'"
:value="value"
:calendar-month="calendarMonth"
:calendar-year="calendarYear"
:start-at="startAt"
:end-at="endAt"
:first-day-of-week="firstDayOfWeek"
:disabled-date="isDisabledDate"
@select="selectDate"/>
<panel-year
v-show="panel === 'YEAR'"
:value="value"
:first-year="firstYear"
@select="selectYear" />
<panel-month
v-show="panel === 'MONTH'"
:value="value"
:calendar-year="calendarYear"
@select="selectMonth" />
<panel-time
v-show="panel === 'TIME'"
:minute-step="minuteStep"
:time-picker-options="timePickerOptions"
:value="value"
:disabled-time="isDisabledTime"
@select="selectTime" />
</div>
</div>
</template>
<script>
import { isValidDate, isDateObejct } from '@/utils/index'
import { t } from '@/locale/index'
import scrollIntoView from '@/utils/scroll-into-view'
import PanelDate from '@/panel/date'
import PanelYear from '@/panel/year'
import PanelMonth from '@/panel/month'
import PanelTime from '@/panel/time'
export default {
name: 'CalendarPanel',
components: { PanelDate, PanelYear, PanelMonth, PanelTime },
props: {
value: {
default: null,
validator: function (val) {
return val === null || isValidDate(val)
}
},
startAt: null,
endAt: null,
visible: {
type: Boolean,
default: false
},
// below user set
type: {
type: String,
default: 'date' // ['date', 'datetime']
},
firstDayOfWeek: {
default: 7,
type: Number,
validator: val => val >= 1 && val <= 7
},
notBefore: {
default: null,
validator: function (val) {
return !val || isValidDate(val)
}
},
notAfter: {
default: null,
validator: function (val) {
return !val || isValidDate(val)
}
},
disabledDays: {
type: [Array, Function],
default: function () {
return []
}
},
minuteStep: {
type: Number,
default: 0,
validator: val => val >= 0 && val <= 60
},
timePickerOptions: {
type: [Object, Function],
default () {
return null
}
}
},
data () {
const now = new Date()
const calendarYear = now.getFullYear()
const calendarMonth = now.getMonth()
const firstYear = Math.floor(calendarYear / 10) * 10
const months = t('months')
return {
panel: 'DATE',
dates: [],
months,
calendarMonth,
calendarYear,
firstYear
}
},
computed: {
now: {
get () {
return new Date(this.calendarYear, this.calendarMonth).getTime()
},
set (val) {
const now = new Date(val)
this.calendarYear = now.getFullYear()
this.calendarMonth = now.getMonth()
}
},
timeHeader () {
return this.value && new Date(this.value).toLocaleDateString()
}
},
watch: {
value: {
immediate: true,
handler: 'updateNow'
},
visible: {
immediate: true,
handler: 'init'
},
panel: {
immediate: true,
handler: 'handelPanelChange'
}
},
methods: {
handelPanelChange (panel) {
if (panel === 'YEAR') {
this.firstYear = Math.floor(this.calendarYear / 10) * 10
} else if (panel === 'TIME') {
this.$nextTick(() => {
[...this.$el.querySelectorAll('.mx-panel-time .mx-time-list')].forEach(el => {
scrollIntoView(el, el.querySelector('.actived'))
})
})
}
},
init () {
this.panel = 'DATE'
this.updateNow(this.value)
},
// 根据value更新日历
updateNow (value) {
this.now = value ? new Date(value) : new Date()
},
isDisabledTime (date, startAt, endAt) {
const time = new Date(date).getTime()
const notBefore = this.notBefore && (time < new Date(this.notBefore))
const notAfter = this.notAfter && (time > new Date(this.notAfter))
startAt = startAt === undefined ? this.startAt : startAt
startAt = startAt && (time < new Date(startAt))
endAt = endAt === undefined ? this.endAt : endAt
endAt = endAt && (time > new Date(endAt))
return notBefore || notAfter || startAt || endAt
},
isDisabledDate (date, startAt, endAt) {
const time = new Date(date).getTime()
const notBefore = this.notBefore && (time < new Date(this.notBefore).setHours(0, 0, 0, 0))
const notAfter = this.notAfter && (time > new Date(this.notAfter).setHours(0, 0, 0, 0))
startAt = startAt === undefined ? this.startAt : startAt
startAt = startAt && (time < new Date(startAt).setHours(0, 0, 0, 0))
endAt = endAt === undefined ? this.endAt : endAt
endAt = endAt && (time > new Date(endAt).setHours(0, 0, 0, 0))
let disabledDays = false
if (Array.isArray(this.disabledDays)) {
disabledDays = this.disabledDays.some(v => new Date(v).setHours(0, 0, 0, 0) === time)
} else if (typeof this.disabledDays === 'function') {
disabledDays = this.disabledDays(new Date(date))
}
return notBefore || notAfter || disabledDays || startAt || endAt
},
selectDate (date) {
if (this.type === 'datetime') {
let time = new Date(date)
if (isDateObejct(this.value)) {
time.setHours(
this.value.getHours(),
this.value.getMinutes(),
this.value.getSeconds()
)
}
if (this.isDisabledTime(time)) {
time.setHours(0, 0, 0, 0)
if (this.notBefore && time.getTime() < new Date(this.notBefore).getTime()) {
time = new Date(this.notBefore)
}
if (this.startAt && time.getTime() < new Date(this.startAt).getTime()) {
time = new Date(this.startAt)
}
}
this.$emit('select-time', time)
this.panel = 'TIME'
return
}
this.$emit('select-date', date)
},
selectYear (year) {
this.changeCalendarYear(year)
this.showPanelMonth()
},
selectMonth (month) {
this.changeCalendarMonth(month)
this.showPanelDate()
},
selectTime (time) {
this.$emit('select-time', time)
},
changeCalendarYear (year) {
this.now = new Date(year, this.calendarMonth)
},
changeCalendarMonth (month) {
this.now = new Date(this.calendarYear, month)
},
handleIconMonth (flag) {
this.changeCalendarMonth(this.calendarMonth + flag)
},
handleIconYear (flag) {
if (this.panel === 'YEAR') {
this.changePanelYears(flag)
} else {
this.changeCalendarYear(this.calendarYear + flag)
}
},
handleBtnYear () {
if (this.panel === 'YEAR') {
this.showPanelDate()
} else {
this.showPanelYear()
}
},
handleBtnMonth () {
if (this.panel === 'MONTH') {
this.showPanelDate()
} else {
this.showPanelMonth()
}
},
changePanelYears (flag) {
this.firstYear = this.firstYear + flag * 10
},
showPanelDate () {
this.panel = 'DATE'
},
showPanelYear () {
this.panel = 'YEAR'
},
showPanelMonth () {
this.panel = 'MONTH'
}
}
}
</script>
+18
View File
@@ -0,0 +1,18 @@
export default {
bind (el, binding, vnode) {
el['@clickoutside'] = e => {
if (
!el.contains(e.target) &&
binding.expression &&
vnode.context[binding.expression]
) {
binding.value()
}
}
document.addEventListener('click', el['@clickoutside'], true)
},
unbind (el) {
document.removeEventListener('click', el['@clickoutside'], true)
}
}
+8
View File
@@ -0,0 +1,8 @@
import DatePicker from './index.vue'
import './index.scss'
DatePicker.install = function (Vue) {
Vue.component(DatePicker.name, DatePicker)
}
export default DatePicker
+319
View File
@@ -0,0 +1,319 @@
$default-color: #73879c;
$primary-color: #1284e7;
.mx-datepicker {
position: relative;
display: inline-block;
width: 210px;
color: $default-color;
font: 14px/1.5 'Helvetica Neue', Helvetica, Arial, 'Microsoft Yahei', sans-serif;
* {
box-sizing: border-box;
}
&.disabled {
opacity: 0.7;
cursor: not-allowed;
}
}
.mx-datepicker-range {
width: 320px;
}
.mx-datepicker-popup {
position: absolute;
margin-top: 1px;
margin-bottom: 1px;
border: 1px solid #d9d9d9;
background-color: #fff;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
z-index: 1000;
}
.mx-input-wrapper {
position: relative;
.mx-clear-wrapper {
display: none;
}
&:hover {
.mx-clear-wrapper {
display: block;
}
}
}
.mx-input {
display: inline-block;
width: 100%;
height: 34px;
padding: 6px 30px;
padding-left: 10px;
font-size: 14px;
line-height: 1.4;
color: #555;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
&:disabled,
&.disabled {
opacity: 0.7;
cursor: not-allowed;
}
&:focus {
outline: none;
}
}
.mx-input-append {
position: absolute;
top: 0;
right: 0;
width: 30px;
height: 100%;
padding: 6px;
background-color: #fff;
background-clip: content-box;
}
.mx-input-icon {
display: inline-block;
width: 100%;
height: 100%;
font-style: normal;
color: #555;
text-align: center;
cursor: pointer;
}
.mx-calendar-icon {
width: 100%;
height: 100%;
color: #555;
stroke-width: 8px;
stroke: currentColor;
fill: currentColor;
}
.mx-clear-icon {
&::before {
display: inline-block;
content: '\2716';
vertical-align: middle;
}
&::after {
content: '';
display: inline-block;
width: 0;
height: 100%;
vertical-align: middle;
}
}
.mx-range-wrapper {
width: 248px * 2;
overflow: hidden;
}
.mx-shortcuts-wrapper {
text-align: left;
padding: 0 12px;
line-height: 34px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
.mx-shortcuts {
white-space: nowrap;
cursor: pointer;
&:hover {
color: mix(#fff, $primary-color, 20%);
}
&:after {
content: '|';
margin: 0 10px;
color: #48576a;
}
}
}
.mx-datepicker-footer {
padding: 4px;
clear: both;
text-align: right;
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
.mx-datepicker-btn {
font-size: 12px;
line-height: 1;
padding: 7px 15px;
margin: 0 5px;
cursor: pointer;
background-color: transparent;
outline: none;
border: none;
border-radius: 3px;
}
.mx-datepicker-btn-confirm {
border: 1px solid rgba(0, 0, 0, 0.1);
color: #73879c;
&:hover {
color: #1284e7;
border-color: #1284e7;
}
}
/* 日历组件 */
.mx-calendar {
float: left;
color: $default-color;
padding: 6px 12px;
font: 14px/1.5 Helvetica Neue,Helvetica,Arial,Microsoft Yahei,sans-serif;
* {
box-sizing: border-box;
}
}
.mx-calendar-header {
padding: 0 4px;
height: 34px;
line-height: 34px;
text-align: center;
overflow: hidden;
> a {
color: inherit;
text-decoration: none;
cursor: pointer;
&:hover {
color: mix(#fff, $primary-color, 20%);
}
}
@at-root {
.mx-icon-last-month,
.mx-icon-next-month {
padding: 0 6px;
font-size: 20px;
line-height: 30px;
}
.mx-icon-last-month {
float: left;
}
.mx-icon-next-month {
float: right;
}
.mx-icon-last-year {
@extend .mx-icon-last-month
}
.mx-icon-next-year {
@extend .mx-icon-next-month
}
}
}
.mx-calendar-content {
width: 32px * 7;
height: 32px * 7;
.cell {
cursor: pointer;
&:hover {
background-color: #eaf8fe;
}
&.actived {
color: #fff;
background-color: $primary-color;
}
&.inrange {
background-color: #eaf8fe;
}
&.disabled {
cursor: not-allowed;
color: #ccc;
background-color: #f3f3f3;
}
}
}
.mx-panel {
width: 100%;
height: 100%;
text-align: center;
}
.mx-panel-date {
table-layout: fixed;
border-collapse: collapse;
border-spacing: 0;
td, th {
font-size: 12px;
width: 32px;
height: 32px;
padding: 0;
overflow: hidden;
text-align: center;
}
td {
&.today {
color: mix(#fff, $primary-color, 10%);
}
&.last-month,
&.next-month {
color: #ddd;
}
}
}
.mx-panel-year {
padding: 7px 0;
.cell {
display: inline-block;
width: 40%;
margin: 1px 5%;
line-height: 40px;
}
}
.mx-panel-month {
.cell {
display: inline-block;
width: 30%;
line-height: 40px;
margin: 8px 1.5%;
}
}
.mx-time-list {
position: relative; // 定位 offsetParent
float: left;
margin: 0;
padding: 0;
list-style: none;
width: 100%;
height: 100%;
border-top: 1px solid rgba(0,0,0,.05);
border-left: 1px solid rgba(0,0,0,.05);
overflow-y: auto;
.mx-time-picker-item {
display: block;
text-align: left;
padding-left: 10px;
}
&:first-child {
border-left: 0;
}
.cell {
width: 100%;
font-size: 12px;
height: 30px;
line-height: 30px;
}
/* 滚动条滑块 */
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
&::-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);
}
&:hover::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
}
}
+409
View File
@@ -0,0 +1,409 @@
<template>
<div
class="mx-datepicker"
:class="{
'mx-datepicker-range': range,
'disabled': disabled
}"
:style="{
'width': computedWidth
}"
v-clickoutside="closePopup">
<div class="mx-input-wrapper"
@click="showPopup">
<input
:class="inputClass"
ref="input"
type="text"
:name="inputName"
:disabled="disabled"
:readonly="!editable"
:value="text"
:placeholder="innerPlaceholder"
@input="handleInput"
@change="handleChange">
<span class="mx-input-append">
<slot name="calendar-icon">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 200 200" class="mx-calendar-icon">
<rect x="13" y="29" rx="14" ry="14" width="174" height="158" fill="transparent" />
<line x1="46" x2="46" y1="8" y2="50" />
<line x1="154" x2="154" y1="8" y2="50" />
<line x1="13" x2="187" y1="70" y2="70" />
<text x="50%" y="135" font-size="90" stroke-width="1" text-anchor="middle" dominant-baseline="middle">{{new Date().getDate()}}</text>
</svg>
</slot>
</span>
<span
v-if="showClearIcon"
class="mx-input-append mx-clear-wrapper"
@click.stop="clearDate">
<slot name="mx-clear-icon">
<i class="mx-input-icon mx-clear-icon"></i>
</slot>
</span>
</div>
<div class="mx-datepicker-popup"
:style="position"
v-show="popupVisible"
ref="calendar">
<slot name="header">
<div class="mx-shortcuts-wrapper"
v-if="range && innnerShortcuts.length">
<span
class="mx-shortcuts"
v-for="(range, index) in innnerShortcuts"
:key="index"
@click="selectRange(range)">{{range.text}}</span>
</div>
</slot>
<calendar-panel
v-if="!range"
v-bind="$attrs"
:value="currentValue"
:visible="popupVisible"
@select-date="selectDate"
@select-time="selectTime"></calendar-panel>
<div class="mx-range-wrapper"
v-else>
<calendar-panel
style="box-shadow:1px 0 rgba(0, 0, 0, .1)"
v-bind="$attrs"
:value="currentValue[0]"
:end-at="currentValue[1]"
:start-at="null"
:visible="popupVisible"
@select-date="selectStartDate"
@select-time="selectStartTime"></calendar-panel>
<calendar-panel
v-bind="$attrs"
:value="currentValue[1]"
:start-at="currentValue[0]"
:end-at="null"
:visible="popupVisible"
@select-date="selectEndDate"
@select-time="selectEndTime"></calendar-panel>
</div>
<slot name="footer">
<div class="mx-datepicker-footer"
v-if="confirm">
<button type="button"
class="mx-datepicker-btn mx-datepicker-btn-confirm"
@click="confirmDate">{{ confirmText }}</button>
</div>
</slot>
</div>
</div>
</template>
<script>
import fecha from 'fecha'
import clickoutside from '@/directives/clickoutside'
import { isValidDate, isValidRange, isDateObejct } from '@/utils/index'
import { use, t } from '@/locale/index'
import CalendarPanel from './calendar.vue'
export default {
name: 'DatePicker',
components: { CalendarPanel },
directives: {
clickoutside
},
props: {
value: null,
placeholder: {
type: String,
default: null
},
lang: {
type: [String, Object],
default: 'zh'
},
format: {
type: String,
default: 'YYYY-MM-DD'
},
range: {
type: Boolean,
default: false
},
rangeSeparator: {
type: String,
default: '~'
},
width: {
type: [String, Number],
default: null
},
confirmText: {
type: String,
default: 'OK'
},
confirm: {
type: Boolean,
default: false
},
editable: {
type: Boolean,
default: true
},
disabled: {
type: Boolean,
default: false
},
clearable: {
type: Boolean,
default: true
},
shortcuts: {
type: [Boolean, Array],
default: true
},
inputName: {
type: String,
default: 'date'
},
inputClass: {
type: [String, Array],
default: 'mx-input'
}
},
data () {
return {
currentValue: this.range ? [null, null] : null,
userInput: null,
popupVisible: false,
position: {}
}
},
watch: {
value: {
immediate: true,
handler: 'handleValueChange'
},
popupVisible (val) {
if (val) {
this.initCalendar()
} else {
this.userInput = null
}
}
},
computed: {
innerPlaceholder () {
if (typeof this.placeholder === 'string') {
return this.placeholder
}
return this.range ? t('placeholder.dateRange') : t('placeholder.date')
},
text () {
if (this.userInput !== null) {
return this.userInput
}
if (!this.range) {
return isValidDate(this.value) ? this.stringify(this.value) : ''
}
return isValidRange(this.value)
? `${this.stringify(this.value[0])} ${this.rangeSeparator} ${this.stringify(this.value[1])}`
: ''
},
computedWidth () {
if (typeof this.width === 'number' || (typeof this.width === 'string' && /^\d+$/.test(this.width))) {
return this.width + 'px'
}
return this.width
},
showClearIcon () {
return !this.disabled && this.clearable && (this.range ? isValidRange(this.value) : isValidDate(this.value))
},
innnerShortcuts () {
if (Array.isArray(this.shortcuts)) {
return this.shortcuts
}
if (this.shortcuts === false) {
return []
}
const pickers = t('pickers')
const arr = [
{
text: pickers[0],
start: new Date(),
end: new Date(Date.now() + 3600 * 1000 * 24 * 7)
},
{
text: pickers[1],
start: new Date(),
end: new Date(Date.now() + 3600 * 1000 * 24 * 30)
},
{
text: pickers[2],
start: new Date(Date.now() - 3600 * 1000 * 24 * 7),
end: new Date()
},
{
text: pickers[3],
start: new Date(Date.now() - 3600 * 1000 * 24 * 30),
end: new Date()
}
]
return arr
}
},
created () {
use(this.lang)
},
methods: {
initCalendar () {
this.handleValueChange(this.value)
this.displayPopup()
},
stringify (date, format) {
format = format || this.format
return fecha.format(date, format)
},
parseDate (value, format) {
try {
format = format || this.format
return fecha.parse(value, format)
} catch (e) {
return false
}
},
dateEqual (a, b) {
return isDateObejct(a) && isDateObejct(b) && a.getTime() === b.getTime()
},
rangeEqual (a, b) {
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => this.dateEqual(item, b[index]))
},
selectRange (range) {
this.currentValue = [ new Date(range.start), new Date(range.end) ]
this.updateDate(true)
},
clearDate () {
const date = this.range ? [null, null] : null
this.currentValue = date
this.updateDate(true)
},
confirmDate () {
const valid = this.range ? isValidRange(this.currentValue) : isValidDate(this.currentValue)
if (valid) {
this.updateDate(true)
}
this.$emit('confirm', this.currentValue)
this.closePopup()
},
updateDate (confirm = false) {
if ((this.confirm && !confirm) || this.disabled) {
return false
}
const equal = this.range ? this.rangeEqual(this.value, this.currentValue) : this.dateEqual(this.value, this.currentValue)
if (equal) {
return false
}
this.$emit('input', this.currentValue)
this.$emit('change', this.currentValue)
return true
},
handleValueChange (value) {
if (!this.range) {
this.currentValue = isValidDate(value) ? new Date(value) : null
} else {
this.currentValue = isValidRange(value) ? [new Date(value[0]), new Date(value[1])] : [null, null]
}
},
selectDate (date) {
this.currentValue = date
this.updateDate() && this.closePopup()
},
selectStartDate (date) {
this.$set(this.currentValue, 0, date)
if (this.currentValue[1]) {
this.updateDate()
}
},
selectEndDate (date) {
this.$set(this.currentValue, 1, date)
if (this.currentValue[0]) {
this.updateDate()
}
},
selectTime (time) {
this.currentValue = time
this.updateDate()
},
selectStartTime (time) {
this.selectStartDate(time)
},
selectEndTime (time) {
this.selectEndDate(time)
},
showPopup () {
if (this.disabled) {
return
}
this.popupVisible = true
},
closePopup () {
this.popupVisible = false
},
displayPopup () {
const dw = document.documentElement.clientWidth
const dh = document.documentElement.clientHeight
const InputRect = this.$el.getBoundingClientRect()
const PopupRect = this.$refs.calendar.getBoundingClientRect()
this.position = {}
if (
dw - InputRect.left < PopupRect.width &&
InputRect.right < PopupRect.width
) {
this.position.left = 1 - InputRect.left + 'px'
} else if (InputRect.left + InputRect.width / 2 <= dw / 2) {
this.position.left = 0
} else {
this.position.right = 0
}
if (
InputRect.top <= PopupRect.height + 1 &&
dh - InputRect.bottom <= PopupRect.height + 1
) {
this.position.top = dh - InputRect.top - PopupRect.height - 1 + 'px'
} else if (InputRect.top + InputRect.height / 2 <= dh / 2) {
this.position.top = '100%'
} else {
this.position.bottom = '100%'
}
},
handleInput (event) {
this.userInput = event.target.value
},
handleChange (event) {
const value = event.target.value
if (this.editable && this.userInput !== null) {
const calendar = this.$children[0]
const checkDate = calendar.type === 'date' ? calendar.isDisabledDate : calendar.isDisabledTime
if (this.range) {
const range = value.split(` ${this.rangeSeparator} `)
if (range.length === 2) {
const start = this.parseDate(range[0], this.format)
const end = this.parseDate(range[1], this.format)
if (start && end && !checkDate(start, null, end) && !checkDate(end, start, null)) {
this.currentValue = [ start, end ]
this.updateDate(true)
this.closePopup()
return
}
}
} else {
const date = this.parseDate(value, this.format)
if (date && !checkDate(date, null, null)) {
this.currentValue = date
this.updateDate(true)
this.closePopup()
return
}
}
this.$emit('input-error', value)
}
}
}
}
</script>
+30
View File
@@ -0,0 +1,30 @@
import Languages from './languages'
import { isPlainObject } from '@/utils/index'
let lang = 'zh'
export function use (target) {
if (isPlainObject(target)) {
lang = { ...Languages.en, ...target }
} else {
lang = Languages[target] || Languages.en
}
}
export function t (path) {
const arr = path.split('.')
let current = lang
let value
for (let i = 0, len = arr.length; i < len; i++) {
const prop = arr[i]
value = current[prop]
if (i === len - 1) {
return value
}
if (!value) {
return ''
}
current = value
}
return ''
}
+101
View File
@@ -0,0 +1,101 @@
export default {
'zh': {
'days': ['日', '一', '二', '三', '四', '五', '六'],
'months': ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
'pickers': ['未来7天', '未来30天', '最近7天', '最近30天'],
'placeholder': {
'date': '请选择日期',
'dateRange': '请选择日期范围'
}
},
'en': {
'days': ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
'months': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
'pickers': ['next 7 days', 'next 30 days', 'previous 7 days', 'previous 30 days'],
'placeholder': {
'date': 'Select Date',
'dateRange': 'Select Date Range'
}
},
'ro': {
'days': ['Lun', 'Mar', 'Mie', 'Joi', 'Vin', 'Sâm', 'Dum'],
'months': ['Ian', 'Feb', 'Mar', 'Apr', 'Mai', 'Iun', 'Iul', 'Aug', 'Sep', 'Oct', 'Noi', 'Dec'],
'pickers': ['urmatoarele 7 zile', 'urmatoarele 30 zile', 'ultimele 7 zile', 'ultimele 30 zile'],
'placeholder': {
'date': 'Selectați Data',
'dateRange': 'Selectați Intervalul De Date'
}
},
'fr': {
'days': ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'],
'months': ['Jan', 'Fev', 'Mar', 'Avr', 'Mai', 'Juin', 'Juil', 'Aout', 'Sep', 'Oct', 'Nov', 'Dec'],
'pickers': ['7 jours suivants', '30 jours suivants', '7 jours précédents', '30 jours précédents'],
'placeholder': {
'date': 'Sélectionnez une date',
'dateRange': 'Sélectionnez une période'
}
},
'es': {
'days': ['Dom', 'Lun', 'mar', 'Mie', 'Jue', 'Vie', 'Sab'],
'months': ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'],
'pickers': ['próximos 7 días', 'próximos 30 días', '7 días anteriores', '30 días anteriores'],
'placeholder': {
'date': 'Seleccionar fecha',
'dateRange': 'Seleccionar un rango de fechas'
}
},
'pt-br': {
'days': ['Dom', 'Seg', 'Ter', 'Qua', 'Quin', 'Sex', 'Sáb'],
'months': ['Jan', 'Fev', 'Mar', 'Abr', 'Maio', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'],
'pickers': ['próximos 7 dias', 'próximos 30 dias', '7 dias anteriores', ' 30 dias anteriores'],
'placeholder': {
'date': 'Selecione uma data',
'dateRange': 'Selecione um período'
}
},
'ru': {
'days': ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'],
'months': ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек'],
'pickers': ['след. 7 дней', 'след. 30 дней', 'прош. 7 дней', 'прош. 30 дней'],
'placeholder': {
'date': 'Выберите дату',
'dateRange': 'Выберите период'
}
},
'de': {
'days': ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'],
'months': ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
'pickers': ['nächsten 7 Tage', 'nächsten 30 Tage', 'vorigen 7 Tage', 'vorigen 30 Tage'],
'placeholder': {
'date': 'Datum auswählen',
'dateRange': 'Zeitraum auswählen'
}
},
'it': {
'days': ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'],
'months': ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic'],
'pickers': ['successivi 7 giorni', 'successivi 30 giorni', 'precedenti 7 giorni', 'precedenti 30 giorni'],
'placeholder': {
'date': 'Seleziona una data',
'dateRange': 'Seleziona un intervallo date'
}
},
'cs': {
'days': ['Ned', 'Pon', 'Úte', 'Stř', 'Čtv', 'Pát', 'Sob'],
'months': ['Led', 'Úno', 'Bře', 'Dub', 'Kvě', 'Čer', 'Čerc', 'Srp', 'Zář', 'Říj', 'Lis', 'Pro'],
'pickers': ['příštích 7 dní', 'příštích 30 dní', 'předchozích 7 dní', 'předchozích 30 dní'],
'placeholder': {
'date': 'Vyberte datum',
'dateRange': 'Vyberte časové rozmezí'
}
},
'sl': {
'days': ['Ned', 'Pon', 'Tor', 'Sre', 'Čet', 'Pet', 'Sob'],
'months': ['Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Avg', 'Sep', 'Okt', 'Nov', 'Dec'],
'pickers': ['naslednjih 7 dni', 'naslednjih 30 dni', 'prejšnjih 7 dni', 'prejšnjih 30 dni'],
'placeholder': {
'date': 'Izberite datum',
'dateRange': 'Izberite razpon med 2 datumoma'
}
}
}
+140
View File
@@ -0,0 +1,140 @@
import { t } from '@/locale/index'
export default {
name: 'panelDate',
props: {
value: null,
startAt: null,
endAt: null,
calendarMonth: {
default: new Date().getMonth()
},
calendarYear: {
default: new Date().getFullYear()
},
firstDayOfWeek: {
default: 7,
type: Number,
validator: val => val >= 1 && val <= 7
},
disabledDate: {
type: Function,
default: () => {
return false
}
}
},
methods: {
selectDate ({ year, month, day }) {
const date = new Date(year, month, day)
if (this.disabledDate(date)) {
return
}
this.$emit('select', date)
},
getDays (firstDayOfWeek) {
const days = t('days')
const firstday = parseInt(firstDayOfWeek, 10)
return days.concat(days).slice(firstday, firstday + 7)
},
getDates (year, month, firstDayOfWeek) {
const arr = []
const time = new Date(year, month)
time.setDate(0) // 把时间切换到上个月最后一天
const lastMonthLength = (time.getDay() + 7 - firstDayOfWeek) % 7 + 1 // time.getDay() 0是星期天, 1是星期一 ...
const lastMonthfirst = time.getDate() - (lastMonthLength - 1)
for (let i = 0; i < lastMonthLength; i++) {
arr.push({ year, month: month - 1, day: lastMonthfirst + i })
}
time.setMonth(time.getMonth() + 2, 0) // 切换到这个月最后一天
const curMonthLength = time.getDate()
for (let i = 0; i < curMonthLength; i++) {
arr.push({ year, month, day: 1 + i })
}
time.setMonth(time.getMonth() + 1, 1) // 切换到下个月第一天
const nextMonthLength = 42 - (lastMonthLength + curMonthLength)
for (let i = 0; i < nextMonthLength; i++) {
arr.push({ year, month: month + 1, day: 1 + i })
}
return arr
},
getCellClasses ({ year, month, day }) {
const classes = []
const cellTime = new Date(year, month, day).getTime()
const today = new Date().setHours(0, 0, 0, 0)
const curTime = this.value && new Date(this.value).setHours(0, 0, 0, 0)
const startTime = this.startAt && new Date(this.startAt).setHours(0, 0, 0, 0)
const endTime = this.endAt && new Date(this.endAt).setHours(0, 0, 0, 0)
if (month < this.calendarMonth) {
classes.push('last-month')
} else if (month > this.calendarMonth) {
classes.push('next-month')
} else {
classes.push('cur-month')
}
if (cellTime === today) {
classes.push('today')
}
if (this.disabledDate(cellTime)) {
classes.push('disabled')
}
if (curTime) {
if (cellTime === curTime) {
classes.push('actived')
} else if (startTime && cellTime <= curTime) {
classes.push('inrange')
} else if (endTime && cellTime >= curTime) {
classes.push('inrange')
}
}
return classes
},
getCellTitle ({ year, month, day }) {
return new Date(year, month, day).toLocaleDateString()
}
},
render (h) {
const ths = this.getDays(this.firstDayOfWeek).map(day => {
return <th>{day}</th>
})
const dates = this.getDates(this.calendarYear, this.calendarMonth, this.firstDayOfWeek)
const tbody = Array.apply(null, { length: 6 }).map((week, i) => {
const tds = dates.slice(7 * i, 7 * i + 7).map(date => {
const attrs = {
class: this.getCellClasses(date)
}
return (
<td
class="cell"
{...attrs}
title={this.getCellTitle(date)}
onClick={this.selectDate.bind(this, date)}>
{date.day}
</td>
)
})
return <tr>{tds}</tr>
})
return (
<table class="mx-panel mx-panel-date">
<thead>
<tr>{ths}</tr>
</thead>
<tbody>
{tbody}
</tbody>
</table>
)
}
}
+32
View File
@@ -0,0 +1,32 @@
import { t } from '@/locale/index'
export default {
name: 'panelMonth',
props: {
value: null,
calendarYear: {
default: new Date().getFullYear()
}
},
methods: {
selectMonth (month) {
this.$emit('select', month)
}
},
render (h) {
let months = t('months')
const currentYear = this.value && new Date(this.value).getFullYear()
const currentMonth = this.value && new Date(this.value).getMonth()
months = months.map((v, i) => {
return <span
class={{
'cell': true,
'actived': currentYear === this.calendarYear && currentMonth === i
}}
onClick={this.selectMonth.bind(this, i)}>
{v}
</span>
})
return <div class="mx-panel mx-panel-month">{months}</div>
}
}
+162
View File
@@ -0,0 +1,162 @@
import { formatTime, parseTime } from '@/utils/index'
export default {
name: 'panelTime',
props: {
timePickerOptions: {
type: [Object, Function],
default () {
return null
}
},
minuteStep: {
type: Number,
default: 0,
validator: val => val >= 0 && val <= 60
},
value: null,
disabledTime: Function
},
computed: {
currentHours () {
return new Date(this.value).getHours()
},
currentMinutes () {
return new Date(this.value).getMinutes()
},
currentSeconds () {
return new Date(this.value).getSeconds()
}
},
methods: {
stringifyText (value) {
return ('00' + value).slice(String(value).length)
},
selectTime (time) {
if (typeof this.disabledTime === 'function' && this.disabledTime(time)) {
return
}
this.$emit('select', new Date(time))
},
getTimeSelectOptions () {
const result = []
const options = this.timePickerOptions
if (!options) {
return []
}
if (typeof options === 'function') {
return options() || []
}
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)
})
}
}
return result
}
},
render (h) {
const date = new Date(this.value)
const disabledTime = typeof this.disabledTime === 'function' && this.disabledTime
let pickers = this.getTimeSelectOptions()
if (Array.isArray(pickers) && pickers.length) {
pickers = pickers.map(picker => {
const pickHours = picker.value.hours
const pickMinutes = picker.value.minutes
const time = new Date(date).setHours(pickHours, pickMinutes, 0)
return (
<li
class={{
'mx-time-picker-item': true,
'cell': true,
'actived': pickHours === this.currentHours && pickMinutes === this.currentMinutes,
'disabled': disabledTime && disabledTime(time)
}}
onClick={this.selectTime.bind(this, time)}>{picker.label}</li>
)
})
return (
<div class="mx-panel mx-panel-time">
<ul class="mx-time-list">
{pickers}
</ul>
</div>
)
}
const hours = Array.apply(null, { length: 24 }).map((_, i) => {
const time = new Date(date).setHours(i)
return <li
class={{
'cell': true,
'actived': i === this.currentHours,
'disabled': disabledTime && disabledTime(time)
}}
onClick={this.selectTime.bind(this, time)}
>{this.stringifyText(i)}</li>
})
const step = this.minuteStep || 1
const length = parseInt(60 / step)
const minutes = Array.apply(null, { length }).map((_, i) => {
const value = i * step
const time = new Date(date).setMinutes(value)
return <li
class={{
'cell': true,
'actived': value === this.currentMinutes,
'disabled': disabledTime && disabledTime(time)
}}
onClick={this.selectTime.bind(this, time)}
>{this.stringifyText(value)}</li>
})
const seconds = Array.apply(null, { length: 60 }).map((_, i) => {
const time = new Date(date).setSeconds(i)
return <li
class={{
'cell': true,
'actived': i === this.currentSeconds,
'disabled': disabledTime && disabledTime(time)
}}
onClick={this.selectTime.bind(this, time)}
>{this.stringifyText(i)}</li>
})
let times = [hours, minutes]
if (this.minuteStep === 0) {
times.push(seconds)
}
times = times.map(list => (
<ul class="mx-time-list"
style={{ width: 100 / times.length + '%' }}>
{list}
</ul>
))
return (
<div class="mx-panel mx-panel-time">
{times}
</div>
)
}
}
+28
View File
@@ -0,0 +1,28 @@
export default {
name: 'panelYear',
props: {
value: null,
firstYear: Number
},
methods: {
selectYear (year) {
this.$emit('select', year)
}
},
render (h) {
// 当前年代
const firstYear = Math.floor(this.firstYear / 10) * 10
const currentYear = this.value && new Date(this.value).getFullYear()
const years = Array.apply(null, { length: 10 }).map((_, i) => {
const year = firstYear + i
return <span
class={{
'cell': true,
'actived': currentYear === year
}}
onClick={this.selectYear.bind(this, year)}
>{year}</span>
})
return <div class="mx-panel mx-panel-year">{years}</div>
}
}
+49
View File
@@ -0,0 +1,49 @@
export function isPlainObject (obj) {
return Object.prototype.toString.call(obj) === '[object Object]'
}
export function isDateObejct (value) {
return value instanceof Date
}
export function isValidDate (date) {
if (date === null || date === undefined) {
return false
}
return !!new Date(date).getTime()
}
export function isValidRange (date) {
return (
Array.isArray(date) &&
date.length === 2 &&
isValidDate(date[0]) &&
isValidDate(date[1]) &&
(new Date(date[1]).getTime() >= new Date(date[0]).getTime())
)
}
export function parseTime (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
}
export function formatTime (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
}
+23
View File
@@ -0,0 +1,23 @@
export default function scrollIntoView (container, selected) {
if (!selected) {
container.scrollTop = 0
return
}
const offsetParents = []
let pointer = selected.offsetParent
while (pointer && container !== pointer && container.contains(pointer)) {
offsetParents.push(pointer)
pointer = pointer.offsetParent
}
const top = selected.offsetTop + offsetParents.reduce((prev, curr) => (prev + curr.offsetTop), 0)
const bottom = top + 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
}
}