要实现的表格样式
- 问题:
为什么要每个月后面和前面要在加上上个月和下个月的日期?
原因:为了表格的宽度固定,不会一会高一会第。
那么我们该如何确定这一个月前面要加几个数,后面要加几个数哪?
首先我们的日历是7*6的,也就是42个,我们可以把他拆成三个数组,中间的是你当月的所有日期,前面是上个月的,后面是下个月的如下图:
上图中首先将当月的所有日期放在中间的数组中
然后根据1号是星期几,比如说星期2那前面数组就添加一项,星期日那前面就加6项,也就是(n-1)项;
最后用n-1+m,m就是这个月的所有日期数量,用42-(n-1+m)就可以得到最后一个数组有多少项了
- 我们如何拿到这个月的最后一天哪?
方法:当前月的最后一天就是下个月的前一天,也就是把当前月加一,然后取0号(就等同雨下个月的前一天)
但是上面的代码会有bug,比如我们在当前日期是一月三十一号的时候我们加一个月再取它的上一天,按理说我们应该拿到一月三十一号,但是我们取到的确实二月二十八号
bug原因:js里当前月份加一,并不是直接到下个月对应日期,比如说一月三十一号加一并不是二月二十八号,而是当前日期+当月天数,当前日期是一月三十一所以加上三十一天也就是到了三月三号,取它的0号也就是二月二十八
解决方法:把31回拨到小于29的数,因为每个月都会有这一天的话就加起来就不会超出这个月
<template>
<div>
<lf-popover position="bottom">
<lf-input type="text"></lf-input>
<template slot="content">
<div class="lifa-date-picker-pop">
<div class="lifa-date-picker-nav">
<span><lf-icon name="settings"></lf-icon></span>
<span><lf-icon name="settings"></lf-icon></span>
<span @click="onClickYear">2019年</span>
<span @click="onClickMonth">8月</span>
</div>
<div class="lifa-date-picker-panels">
<div v-if="mode==='years'" class="lifa-date-picker-content">年</div>
<div v-else-if="mode === 'months'" class="lifa-date-picker-content">月</div>
<div v-else class="lifa-date-picker-content">日</div>
</div>
<div class="lifa-date-picker-actions"></div>
</div>
</template>
</lf-popover>
</div>
</template>
<script>
import LfInput from '../input'
import LfIcon from '../icon'
import LfPopover from '../popover'
export default {
name: "LiFaDatePicker",
components: {LfIcon, LfInput, LfPopover},
data () {
return {
mode: 'days',
value: new Date()
}
},
mounted () {
let date = this.value
let firstDay = date.setDate(1)
date.setDate(28) // 回拨到29号之前,解决bug
date.setMonth(date.getMonth()+1)
let lastDay = date.setDate(0)
},
methods: {
onClickMonth() {
this.mode = 'months'
},
onClickYear() {
this.mode = 'years'
}
}
}
</script>
<style scoped>
</style>
日期渲染
<div v-else class="lifa-date-picker-content">
<div v-for="item in 6">
<span v-for="day in visibleDays.slice(item*7-7, item*7)">
{{day.getDate()}}
</span>
</div>
</div>
import helper from './helper'
data () {
return {
mode: 'days',
value: new Date()
}
},
computed: {
visibleDays () {
let date = this.value
let firstDay = helper.firstDayOfMonth(date)
let lastDay = helper.lastDayOfMonth(date)
let [year, month, day] = helper.getYearMonthDate(date)
let arr = []
// firstDay.getDate()得到这个月的第一天也就是1,lastDay.getDate()得到最后一天如31
for (let i = firstDay.getDate(); i <= lastDay.getDate(); i++) {
arr.push(new Date(year, month, i))
}
let arr1 = []
// 如果1号是周日那么firstDay.getDay() = 0,前面要加上个月的6天,否则直接减1
let arr1Length = firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1
for (let i = 0; i <= arr1Length; i++) {
// 当前月的天数从1号开始往上减就到了上个月,如2月1号减1,就是1月31
arr1.push(new Date(year, month, -i))
}
arr1.reverse()
let arr2 = []
for (let i = 0; i< 42- (arr.length + arr1.length);i++) {
// 因为每个月都是从1号开始所以加1
arr2.push(new Date(year, month+1, 1+i))
}
return [...arr1, ...arr, ...arr2]
}
}
- helper.js
export default {
firstDayOfMonth(date) {
let [year, month, day] = getYearMonthDate(date)
return new Date(year, month, 1)
},
lastDayOfMonth(date) {
let [year, month, day] = getYearMonthDate(date)
return new Date(year, month + 1, 0)
},
getYearMonthDate
}
function getYearMonthDate(date) {
let year = date.getFullYear()
let month = date.getMonth()
let day = date.getDate()
return [year, month, day]
}
我们现在的日期选择器两边都有padding我们需要去掉,但是因为我们用的是我们的popover,这个padding也是这个组件的,所以我们需要再让popover接受一个class
- popover.vue
<div ref="content" class="content-wrapper" v-if="visibility"
:class="[`position-${position}`, popClassName]"
>
props: {
popClassName: {
type: String
}
- data-picker.vue
<lf-popover position="bottom" :pop-class-name="c('popWrapper')">
methods: {
c(className) {
return `lifa-data-picker-${className}`
},
}
<style scoped lang="scss">
.lifa-date-picker {
&-nav {
background: red;
}
&-popWrapper {
padding: 0;
}
}
</style>
我们加上样式后发现并没起作用,原因是我们popover是添加到body里的,不是在当前组件里,所以我们在当前组件里写样式是没用的
所以我们需要再给popover加一个参数,指定它的挂载位置,默认是挂到body
- popover.vue
props: {
container: {
type: Object
}
},
methods: {
positionContent(){
let {content,button} = this.$refs
// 如果this.container存在就挂载到this.container否则就是body
(this.container || document.body).appendChild(content)
}
上面代码报错,但是我们没有把this.$res当做函数用
- 相关知识拓展
如果你代码开头是'('或'['或'`' js都会认为是你上一行的它会往你的上一行去拼
const {contentWrapper, triggerWrapper } = this.$refs
(this.container || document.body).appendChild(contentWrapper)
// 等价于
const {contentWrapper, triggerWrapper } = this.$refs(this.container || document.body).appendChild(contentWrapper)
解决办法在上一行加分号
- date-picker.vue
<div ref="wrapper">
<lf-popover position="bottom" :pop-class-name="c('popWrapper')" :container="this.$refs.wrapper">
问题
可我们发现上面还是挂载到了body里,并且我们在popover里打印this.container是undefined
原因:是因为异步,因为我们的popover组件是在页面挂载前就已经引入了,而这时候dom元素也就是this.$res.wrapper并没有生成,我们可以直接在dom里测试
<lf-popover position="bottom" :pop-class-name="c('popWrapper')" :container="console.log(this.$refs)">
data: {
console
}
一开始是空对象,点开里面就能拿到我们的dom了,所以证明这确实是异步造成的
解决办法
使用data绑定属性,在mounted后重新把this.$res.wrapper赋值给这个属性
- date-picker.vue
<div ref="wrapper">
<lf-popover position="bottom" :pop-class-name="c('popWrapper')" :container="x">
data: {
x: undefined
},
mounted () {
this.x = this.$refs.wrapper
},
- popover.vue
props: {
container: {
type: Element
}
}
现在我们已经正确的把popover挂载到了对应的元素里,但是我们发现我们之前写的样式还是没有生效,那是因为我们是要在当前组件修改popover组件的,所以我们要加一个/deep/意思是可以跨组件设置样式
<style scoped lang="scss">
.lifa-date-picker {
&-nav {
background: red;
}
/deep/ &-popWrapper {
padding: 0;
}
}
</style>
vue生成dom的顺序是从里往外的,也就是先生成子组件,然后把子组件放到父组件中
比如:
<div>
<lf-popover>
<lf-input>
</lf-popover>
</div>
这个组件中就是先生成lf-input然后把它放到lf-popover里,再生成lf-popover再放到当前组件里
直接通过拿到这个月的第一天来得到完整的日历
最上面我们通过三个数组,上一个月的天数和当前月的天数以及下一个月的天数来拿到42天,其实我们只需要通过这个月的第一天是星期几然后来确定星期一定义的日期后面直接累加拿到42天就可以
visibleDays () {
let date = this.value
let firstDay = helper.firstDayOfMonth(date)
let lastDay = helper.lastDayOfMonth(date)
let [year, month, day] = helper.getYearMonthDate(date)
// 获取1号是星期几
let n = firstDay.getDay()
let arr = []
// 获取日历显示中的第一天;因为是按照星期一到星期天排的星期天是0,所以如果一号是星期天前面应该有6天
// (也就是应该减去6天可以得到日历现实的第一天),否则就是n-1天,所以需要再乘以3600 * 24 * 1000得到每天的毫秒数
let x = firstDay - (n === 0 ? 6 : n - 1) * 3600 * 24 * 1000
for(let i = 0; i < 42; i++) {
// 因为一共是42天所以每次都在第一天后面加加
arr.push(new Date(x + i * 3600 * 24 * 1000 ))
}
return arr
}
让定义的类支持传入多个类名
<span :class="c('prevYear', 'navItem')"><lf-icon name="leftleft"></lf-icon></span>
c(...classNames) {
return classNames.map(className => `lifa-date-picker-${className}`)
},
实现点击日期input回显
- date-picker.vue
<lf-input type="text" :value="filterValue"></lf-input>
<div v-for="item in 6" :class="c('row')" :key="item">
<span v-for="(day, index) in visibleDays.slice(item*7-7, item*7)" :key="index"
:class="c('cell')" @click="onGetDay(day)">
{{day.getDate()}}
</span>
</div>
props: {
value: {
type: Date,
default: () => new Date()
}
},
methods: {
onGetDay(day) {
this.$emit('input', day)
}
},
computed: {
filterValue () {
const [year, month, day] = helper.getYearMonthDate(this.value)
return `${year}-${month+1}-${day}`
}
}
- demo.vue
<lf-date-picker :value="value" @input="value = $event"></lf-date-picker>
data() {
return {
value: new Date()
}
}
实现点击当前月的日期切换,不是当前的点击无效
思路:主要是要判断当前选中的日期和当前的日期是不是同一年同一个月
<div v-for="item in 6" :class="c('row')" :key="item">
<span v-for="(day, index) in visibleDays.slice(item*7-7, item*7)" :key="index"
:class="[c('cell'), {currentMonth: isCurrentMonth(day)}]" @click="onGetDay(day)">
{{day.getDate()}}
</span>
</div>
methods: {
onGetDay(date) {
// 如果是当前月就可以点击
if (this.isCurrentMonth(date)) {
this.$emit('input', date)
}
},
isCurrentMonth(date) {
let [year1, month1] = helper.getYearMonthDate(date)
let [year2, month2] = helper.getYearMonthDate(this.value)
// 如果是当前月的日期那么月和年都应该等于当前日期的
return year1 === year2 && month1 === month2
}
}
问题:我们用一个value来表示当前的日期是有一点问题的,我们现在value表示的是当前日期,而value有可能是当前选中的日期,也有可能是当前显示的日期比如说当前选中的是一号当我们点上一个月那么展示的就应该是上一个月,也就是说我选中的是某一天而展示的是上一个月的一整个月;所以这里我们用最初的value表示具体的哪一天,然后再拓展一个value用来表示月和年,可以是一个对象{year: 2019, month: 10},也可以是一个数组[2019, 10],这里我们用对象的形式
默认展示的年和月的初始值是根据我们选中的日期来的,所以我们这里通过value获取年和月,然后我们的visbleDays因为是用户看到的日期也就是展示的所以也要根据我们这个新的value来确定
<span @click="onClickYear">{{display.year}}年</span>
<span @click="onClickMonth">{{display.month+1}}月</span>
data () {
let [year, month] = helper.getYearMonthDate(this.value)
return {
display: {year, month}
}
},
computed: {
visibleDays () {
// 界面展示的当前月的日期,所以也根据display来确定
let date = new Date(this.display.year, this.display.month, 1)
}
}
实现点击显示上一个月日期
<span :class="c('prevMonth', 'navItem')" @click="onClickPrevMonth"><lf-icon name="left"></lf-icon></span>
onClickPrevMonth() {
this.display.month -= 1
},
上面代码的两个问题
- 当我们点击上一个月的时候日期和月份的确都变了,但是日期对应的高亮并不对,主要是因为我们是否是当前月的判断条件之前是根据value来判断的,也就是说是根据选中的日期来判断的,而我们现在应该根据展示的日期来判断
解决代码如下:
isCurrentMonth(date) {
let [year1, month1] = helper.getYearMonthDate(date)
// 如果是当前选中的年和月等于展示的年和月
return year1 === this.display.year && month1 === this.display.month
},
- 因为我们是直接每次拿月这个数字减1,所以会出现0和负数的情况
解决办法:
通过日期的形式修改,在我们的helper.js里添加一个addMonth的方法,它接收我们传进来的date,还有每次我们减或者加的月数
- helper.js
addMonth(date, n) {
const [year, month, day] = getYearMonthDate(date)
const newMonth = month + n
// 因为date是用户传进来的所以我们不能直接修改,要重新生成一个
const copy = new Date(date)
copy.setMonth(newMonth)
return copy
}
- date-picker.vue
onClickPrevMonth() {
this.display.month = helper.addMonth(new Date(this.display.year, this.display.month, 1), -1)
.getMonth()
},
问题:月份的问题解决了但是年并不会跟着一起变
解决方法:每次将展示的年和月都同时重新赋值
onClickPrevMonth() {
// 当前日期
const oldDate = new Date(this.display.year, this.display.month, 1)
// 点击上一个月的日期
const newDate = helper.addMonth(oldDate, -1)
const [year, month] = helper.getYearMonthDate(newDate)
this.display = {year, month}
},
实现点击年的切换
- helper.js
addYear(date, n) {
const [year, month, day] = getYearMonthDate(date)
const newMonth = year + n
const copy = new Date(date)
copy.setFullYear(newMonth)
return copy
}
- date-picker.vue
onClickPrevMonth() {
// 当前日期
const oldDate = new Date(this.display.year, this.display.month, 1)
// 点击上一个月的日期
const newDate = helper.addMonth(oldDate, -1)
const [year, month] = helper.getYearMonthDate(newDate)
this.display = {year, month}
},
onClickPrevYear() {
const oldDate = new Date(this.display.year, this.display.month, 1)
const newDate = helper.addYear(oldDate, -1)
const [year, month] = helper.getYearMonthDate(newDate)
this.display = {year, month}
},
onClickNextMonth() {
const oldDate = new Date(this.display.year, this.display.month, 1)
const newDate = helper.addMonth(oldDate, 1)
const [year, month] = helper.getYearMonthDate(newDate)
this.display = {year, month}
},
onClickNextYear() {
const oldDate = new Date(this.display.year, this.display.month, 1)
const newDate = helper.addYear(oldDate, 1)
const [year, month] = helper.getYearMonthDate(newDate)
this.display = {year, month}
}
特别补充:为什么我们点击上一年上一月对应的日期也会变哪?
主要是因为我们点击修改的是display,而我们的日期显示依赖的就是display,所以他变了,每次显示的对应日期也会变
选中年和月
当我们点击日历头部的时候来回切换会默认鼠标选中,我们不想让他选中,所以可以加一个@selectstart.prevent
<div class="lifa-date-picker-pop" @selectstart.prevent>
获取年和月
可以接受传进来的年份区间,默认是从1990年到当前年份后100年
<select name="" id="" @change="onSelectYear" v-model="display.year">
<option v-for="list in currentYear" :value="list" :key="list">{{list}}</option>
</select>
<select name="" id="" @change="onSelectMonth" v-model="display.month">
<option v-for="item in 12" :value="item-1" :key="item">{{item}}</option>
</select>
props: {
startAndEndDate: {
type: Array,
default: () => [new Date(1990, 0, 1), helper.addYear(new Date(), 100)]
}
},
compouted: {
currentYear () {
return helper.range([this.startAndEndDate[0].getFullYear(), this.startAndEndDate[1].getFullYear()])
},
methods: {
// 选中年和月的时候对传进来的日期进行判断是否在当前区间内,如果不在就做限制
onSelectYear (e) {
const year = e.target.value
const d = new Date(year, this.display.month)
if ( d >= this.startAndEndDate[0] && d <= this.startAndEndDate[1]) {
this.display.year = year
} else {
e.target.value = this.display.year
}
},
onSelectMonth (e) {
const month = e.target.value
const d = new Date(this.display.year, month)
if ( d >= this.startAndEndDate[0] && d <= this.startAndEndDate[1]) {
this.display.month = month
} else {
e.target.value = this.display.month
}
},
}
}
选中今天和清除
<span v-for="(day, index) in visibleDays.slice(item*7-7, item*7)" :key="index"
:class="[c('cell'), {currentMonth: isCurrentMonth(day),
selected: isSelected(day), today: isToday(day)}]"
@click="onGetDay(day)">
{{day.getDate()}}
</span>
<div class="lifa-date-picker-actions">
<lf-button style="margin-right: 4px" @click="onClickClear">清除</lf-button>
<lf-button @click="onClickToday">今天</lf-button>
</div>
isToday(date) {
let [y,m,d] = helper.getYearMonthDate(date)
let [y1,m1,d1] = helper.getYearMonthDate(new Date())
return y === y1 && m === m1 && d === d1
},
onClickToday() {
const today = new Date()
// 让当前展示的月和年都变成当天对应的
const [year, month] = helper.getYearMonthDate(today)
this.display = {year, month}
this.$emit('update:value', today)
},
onClickClear() {
this.$emit('update:value', undefined)
}
功能优化
- 每次选中日期后展开面板关闭
只需要在每次点击后面触发popover的close事件即可
onGetDay(date) {
// 如果是当前月就可以点击
if (this.isCurrentMonth(date)) {
this.$emit('input', date)
this.$refs.popover.close()
}
},
- 当我们切换到选择年和月的时候点击关闭下次打开还应该是显示天而不应该还是显示年和月
在popover组件里添加面板关闭时触发close事件,展开时触发open事件
然后在datepicker组件里监听open,每次都设置mode为year
<lf-popover position="bottom" :pop-class-name="c('popWrapper')" :container="x" ref="popover"
@open="onOpen"
>
onOpen() {
this.mode = 'year'
},
- 对日期补零
- helper.js
pad2(number) {
if (typeof number !== 'number') {
throw new Error('wrong param')
}
return (number >= 10 ? '' : '0') + number
}
- date-picker.vue
computed() {
filterValue() {
if (!this.value) return
const [year, month, day] = helper.getYearMonthDate(this.value)
return `${year}-${helper.pad2(month + 1)}-${helper.pad2(day)}`
}
}
- 直接输入日期对不符合的格式过滤
<lf-input type="text" :value="filterValue" @input="onInput" @change="onChange" ref="input"></lf-input>
onInput(value) {
let regex = /^\d{4}-\d{2}-\d{2}$/g
// 如果日期格式匹配就更新面板
if (value.match(regex)) {
let [year, month, day] = value.split('-')
month = month - 1
this.display = {year, month}
this.$emit('input', new Date(year, month, day))
}
},
onChange() {
// 输入完成的时候将当前的日期设置为符合格式的,也就是一开始修改的
this.$refs.input.setRawValue(this.filterValue)
},
注意事项:因为我们的input不是原生的input而是我们自己的组件所以我们不能直接修改它的value,而要对我们组件input里的原生的input的value进行修改
- input.vue
<input ref="input" type="text" :value="value" :disabled="disabled" :readonly="readonly"
@change="$emit('change',$event.target.value)"
@input="$emit('input',$event.target.value)"
@focus="$emit('focus',$event.target.value)"
@blur="$emit('blur',$event.target.value)"
>
methods: {
setRawValue(value) {
this.$refs.input.value = value
}
}
网友评论