要實現(xiàn)的表格樣式
- 問題:
為什么要每個月后面和前面要在加上上個月和下個月的日期?
原因:為了表格的寬度固定巴帮,不會一會高一會第溯泣。
那么我們該如何確定這一個月前面要加幾個數(shù),后面要加幾個數(shù)哪榕茧?
首先我們的日歷是7*6的垃沦,也就是42個,我們可以把他拆成三個數(shù)組用押,中間的是你當(dāng)月的所有日期肢簿,前面是上個月的,后面是下個月的如下圖:
上圖中首先將當(dāng)月的所有日期放在中間的數(shù)組中
然后根據(jù)1號是星期幾蜻拨,比如說星期2那前面數(shù)組就添加一項池充,星期日那前面就加6項,也就是(n-1)項缎讼;
最后用n-1+m纵菌,m就是這個月的所有日期數(shù)量,用42-(n-1+m)就可以得到最后一個數(shù)組有多少項了
- 我們?nèi)绾文玫竭@個月的最后一天哪休涤?
方法:當(dāng)前月的最后一天就是下個月的前一天咱圆,也就是把當(dāng)前月加一,然后取0號(就等同雨下個月的前一天)
但是上面的代碼會有bug功氨,比如我們在當(dāng)前日期是一月三十一號的時候我們加一個月再取它的上一天序苏,按理說我們應(yīng)該拿到一月三十一號,但是我們?nèi)〉降拇_實二月二十八號
bug原因:js里當(dāng)前月份加一捷凄,并不是直接到下個月對應(yīng)日期忱详,比如說一月三十一號加一并不是二月二十八號,而是當(dāng)前日期+當(dāng)月天數(shù)跺涤,當(dāng)前日期是一月三十一所以加上三十一天也就是到了三月三號匈睁,取它的0號也就是二月二十八
解決方法:把31回撥到小于29的數(shù)监透,因為每個月都會有這一天的話就加起來就不會超出這個月
<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++) {
// 當(dāng)前月的天數(shù)從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]
}
我們現(xiàn)在的日期選擇器兩邊都有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>
我們加上樣式后發(fā)現(xiàn)并沒起作用,原因是我們popover是添加到body里的享潜,不是在當(dāng)前組件里困鸥,所以我們在當(dāng)前組件里寫樣式是沒用的
所以我們需要再給popover加一個參數(shù),指定它的掛載位置剑按,默認是掛到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當(dāng)做函數(shù)用
- 相關(guān)知識拓展
如果你代碼開頭是'('或'['或'`' 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">
問題
可我們發(fā)現(xiàn)上面還是掛載到了body里,并且我們在popover里打印this.container是undefined
原因:是因為異步吕座,因為我們的popover組件是在頁面掛載前就已經(jīng)引入了虐译,而這時候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
}
}
現(xiàn)在我們已經(jīng)正確的把popover掛載到了對應(yīng)的元素里锣枝,但是我們發(fā)現(xiàn)我們之前寫的樣式還是沒有生效厢拭,那是因為我們是要在當(dāng)前組件修改popover組件的,所以我們要加一個/deep/意思是可以跨組件設(shè)置樣式
<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再放到當(dāng)前組件里
直接通過拿到這個月的第一天來得到完整的日歷
最上面我們通過三個數(shù)組摔癣,上一個月的天數(shù)和當(dāng)前月的天數(shù)以及下一個月的天數(shù)來拿到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趋厉,所以如果一號是星期天前面應(yīng)該有6天
// (也就是應(yīng)該減去6天可以得到日歷現(xiàn)實的第一天)寨闹,否則就是n-1天,所以需要再乘以3600 * 24 * 1000得到每天的毫秒數(shù)
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}`)
},
實現(xiàn)點擊日期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()
}
}
實現(xiàn)點擊當(dāng)前月的日期切換君账,不是當(dāng)前的點擊無效
思路:主要是要判斷當(dāng)前選中的日期和當(dāng)前的日期是不是同一年同一個月
<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) {
// 如果是當(dāng)前月就可以點擊
if (this.isCurrentMonth(date)) {
this.$emit('input', date)
}
},
isCurrentMonth(date) {
let [year1, month1] = helper.getYearMonthDate(date)
let [year2, month2] = helper.getYearMonthDate(this.value)
// 如果是當(dāng)前月的日期那么月和年都應(yīng)該等于當(dāng)前日期的
return year1 === year2 && month1 === month2
}
}
問題:我們用一個value來表示當(dāng)前的日期是有一點問題的繁堡,我們現(xiàn)在value表示的是當(dāng)前日期,而value有可能是當(dāng)前選中的日期,也有可能是當(dāng)前顯示的日期比如說當(dāng)前選中的是一號當(dāng)我們點上一個月那么展示的就應(yīng)該是上一個月椭蹄,也就是說我選中的是某一天而展示的是上一個月的一整個月闻牡;所以這里我們用最初的value表示具體的哪一天,然后再拓展一個value用來表示月和年绳矩,可以是一個對象{year: 2019, month: 10}罩润,也可以是一個數(shù)組[2019, 10],這里我們用對象的形式
默認展示的年和月的初始值是根據(jù)我們選中的日期來的埋酬,所以我們這里通過value獲取年和月,然后我們的visbleDays因為是用戶看到的日期也就是展示的所以也要根據(jù)我們這個新的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 () {
// 界面展示的當(dāng)前月的日期,所以也根據(jù)display來確定
let date = new Date(this.display.year, this.display.month, 1)
}
}
實現(xiàn)點擊顯示上一個月日期
<span :class="c('prevMonth', 'navItem')" @click="onClickPrevMonth"><lf-icon name="left"></lf-icon></span>
onClickPrevMonth() {
this.display.month -= 1
},
上面代碼的兩個問題
- 當(dāng)我們點擊上一個月的時候日期和月份的確都變了烧栋,但是日期對應(yīng)的高亮并不對写妥,主要是因為我們是否是當(dāng)前月的判斷條件之前是根據(jù)value來判斷的,也就是說是根據(jù)選中的日期來判斷的审姓,而我們現(xiàn)在應(yīng)該根據(jù)展示的日期來判斷
解決代碼如下:
isCurrentMonth(date) {
let [year1, month1] = helper.getYearMonthDate(date)
// 如果是當(dāng)前選中的年和月等于展示的年和月
return year1 === this.display.year && month1 === this.display.month
},
- 因為我們是直接每次拿月這個數(shù)字減1珍特,所以會出現(xiàn)0和負數(shù)的情況
解決辦法:
通過日期的形式修改,在我們的helper.js里添加一個addMonth的方法魔吐,它接收我們傳進來的date扎筒,還有每次我們減或者加的月數(shù)
- 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() {
// 當(dāng)前日期
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}
},
實現(xiàn)點擊年的切換
- 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() {
// 當(dāng)前日期
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}
}
特別補充:為什么我們點擊上一年上一月對應(yīng)的日期也會變哪酬姆?
主要是因為我們點擊修改的是display嗜桌,而我們的日期顯示依賴的就是display,所以他變了辞色,每次顯示的對應(yīng)日期也會變
選中年和月
當(dāng)我們點擊日歷頭部的時候來回切換會默認鼠標(biāo)選中骨宠,我們不想讓他選中,所以可以加一個@selectstart.prevent
<div class="lifa-date-picker-pop" @selectstart.prevent>
獲取年和月
可以接受傳進來的年份區(qū)間相满,默認是從1990年到當(dāng)前年份后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: {
// 選中年和月的時候?qū)鬟M來的日期進行判斷是否在當(dāng)前區(qū)間內(nèi)层亿,如果不在就做限制
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()
// 讓當(dāng)前展示的月和年都變成當(dāng)天對應(yīng)的
const [year, month] = helper.getYearMonthDate(today)
this.display = {year, month}
this.$emit('update:value', today)
},
onClickClear() {
this.$emit('update:value', undefined)
}
功能優(yōu)化
- 每次選中日期后展開面板關(guān)閉
只需要在每次點擊后面觸發(fā)popover的close事件即可
onGetDay(date) {
// 如果是當(dāng)前月就可以點擊
if (this.isCurrentMonth(date)) {
this.$emit('input', date)
this.$refs.popover.close()
}
},
- 當(dāng)我們切換到選擇年和月的時候點擊關(guān)閉下次打開還應(yīng)該是顯示天而不應(yīng)該還是顯示年和月
在popover組件里添加面板關(guān)閉時觸發(fā)close事件,展開時觸發(fā)open事件
然后在datepicker組件里監(jiān)聽open立美,每次都設(shè)置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() {
// 輸入完成的時候?qū)?dāng)前的日期設(shè)置為符合格式的匿又,也就是一開始修改的
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
}
}