前言
日期選擇肯定很多人都做過,最近在做移動端的專案,產品想做一個類似這樣的日期時間選擇介面,詳看下圖:
透過這個介面來看,我們正常的日期時間選擇的元件庫肯定不能直接滿足,但是有些部分可以滿足要求;像右邊的時間選擇就可以,但是左邊的這個日期和星期幾怎麼做呢?
介面上沒有年份?這裡做一個伏筆,後面再看。
下面我們一步步來實現這個需求,我使用的是 Vue + Vant,Vant官網。
開始
基礎元件選擇
首先我們需要檢視相應的 UI 元件庫找到可以基本滿足需要的元件,我主要找了兩個元件一個是 DatetimePicker 時間選擇,另一個是Picker 選擇器。
這兩個按理說應該都可以,我選擇的是 Picker 選擇器元件,DatetimePicker 時間選擇 元件怎麼實現可以自己試試。
資料構造
從圖片上我們可以知道這個需要三列,第一列顯示日期和星期,第二列顯示小時,最後一列顯示分鐘。
1、html
title=“標題” show-toolbar :columns=“dateColumns” @confirm=“onConfirm” @cancel=“onCancel” @change=“onChange” /> 複製程式碼 2、vue 我們在進頁面後初始化一個 dateColumns,包括預設的第一列以及後面兩列時間。後面的 onChange 和 getNewDateArray 方法就是我們後面邏輯實現的地方。 export default { 。。。 data() { dateColumns: [], defaultYear: new Date()。getFullYear(), // 儲存年份,預設當前年 dateArray: [], // 儲存第一列的values }, mounted() { this。dateColumns = [ this。getNewDateArray(undefined, true),// 第一列 { values: Array。from({ length: 24 }, (v, k) => { if (k < 10) { k = `0${k}` } return k; }), defaultIndex: 1, }, { // 第二列 values: Array。from({ length: 60 }, (v, k) => { if (k < 10) { k = `0${k}` } return k; }), defaultIndex: 1, }]; // 第三列 }, method:{ onConfirm(picker, values) {。。。}, onChange(picker, values) { console。log(‘piker’, picker, values) // 。。。 }, onCancel() { 。。。 }, getNewDateArray(date, flag, picker, type) { 。。。 } 複製程式碼邏輯分析 首先我們看第一列的資料結構,是XX月XX日 XX,第一個XX是月份,第二個XX是多少日,第三個XX是星期幾,我們需要用到的就是月份(onChange 中 getNewDateArray 的呼叫為什麼不加第幾天而預設使用 1,這個後面‘疑問’目錄中會詳細說明),每次滾動到頭或者到尾的時候我們需要透過這個來判斷是第一個月還是最後一個月,從而對月份和年份重新設定。 1、當滾動到 dateColumns 第一列 values 陣列的第一個的時候,需要載入上一個月的資料; 2、當滾動到 dateColumns 第一列 values 陣列的最後一個的時候,需要載入後一個月的資料; 3、當滾動到 dateColumns 第一列 values 陣列的第一個的時候,需要判斷 month(月份)是否為第一個月,如果是第一個月我們需要將月份重置為第 12 個月,並且將預設的年份 -1; 4、當滾動到 dateColumns 第一列 values 陣列的最後一個的時候,需要判斷 month(月份)是否為最後一個月,如果是最後一個月我們需要將月份重置為第 1 個月,並且將預設的年份 +1。 相關程式碼如下 onChange(picker, values) { console。log(‘piker’, picker, values) const dateArr = values[0]。text。split(‘ ’)[0]。slice(0, -1)。split(‘月’); const month = dateArr[0]; // 當前的月份 if(values[0] === this。dateArray[0]) { // 判斷`dateColumns` 第一列 `values` 陣列的第一個 let newMonth; if(month === ‘1’) { // 判斷 `month`(月份)是否為`第一個月` newMonth = 12; this。defaultYear -= 1; } else { newMonth = Number(month) - 1; } this。getNewDateArray(`${this。defaultYear}-${newMonth}-1`, false, picker, true) } else if (values[0] === this。dateArray[this。dateArray。length - 1]) { // 判斷`dateColumns` 第一列 `values` 陣列的最後一個 let newMonth; if(month === ‘12’) { // 判斷 `month`(月份)是否為`最後一個月` newMonth = 1; this。defaultYear += 1; } else { newMonth = Number(month) + 1 } this。getNewDateArray(`${this。defaultYear}-${newMonth}-1`, false, picker, false) } }, 複製程式碼初始化 第一次進入頁面我們初始化當前月份的資料。 1、我們需要拿到當前月份的總天數; 2、生成 UI 上對應的資料格式,每次需要通過當前的日期拿到星期幾; 3、定義一個週一到週日的陣列方便對應取值,注意星期日是返回 0 ; 4、透過 flag 判斷為 true 返回物件。 程式碼如下: const weekArray = [‘週日’, ‘週一’, ‘週二’, ‘週三’, ‘週四’, ‘週五’, ‘週六’]; /** * date: 需要增加的資料 * flag:是否是第一次進入 * picker:Picker的例項 * type:資料push的方向 */ getNewDateArray(date, flag, picker, type) { date = date ? new Date(date) : new Date(); let month = date。getMonth() + 1; // 儲存當前月份 const monthDays = new Date(date。getFullYear(), month, 0)。getDate(); // 獲取當前月份的總天數 let arr = []; let index = 0; for (let i = 1; i <= monthDays; i ++) { let str = `${month}月${i}日 ${weekArray[new Date(`${date。getFullYear()}-${month}-${i}`)。getDay()]}` arr。push(str); } 。。。 // code if(flag) { return { values: arr, defaultIndex: date。getDate() - 1 // 設定預設選中 } } 。。。 // code } 複製程式碼後續上下滾動載入 什麼的程式碼只是第一次進入的時候初始化的資料,上下滑動到開始或結尾並不會新增內容,所以對什麼的程式碼進行補充和修改。 1、透過 type 判斷是向下滾動到一個還是向上滾動到最後一個; 2、向下滾動到一個將預設值設為新增的上一個月的總天數,並將新月份新增到 dateArray 陣列之前; 3、向上滾動到最後一個將預設值設為為新增新月份的陣列長度 -1,因為 column 的索引是 0 開始的,這裡需要注意一下 ;並將新月份新增到 dateArray 陣列之後; 4、透過 flag 判斷為 false,呼叫Picker例項修改已存在的values及預設選中的索引。 程式碼如下: 。。。 // code + if(type) { + this。dateArray = arr。concat(this。dateArray); // 新增到dateArray陣列之前 + index = monthDays; + } else { + index = this。dateArray。length - 1; // 設定新陣列前設定預設選中索引 + this。dateArray = this。dateArray。concat(arr); // 新增到dateArray陣列之後 + } if(flag) { return { + values: this。dateArray, - values: arr, defaultIndex: date。getDate() - 1 } + } else { + picker。setColumnValues(0, this。dateArray) + picker。setColumnIndex(0, index) // 設定預設選中 } 複製程式碼大功告成? 剛剛我們看到只是選擇的時候的 UI 介面,那選擇完成後需要怎麼顯示呢?我們再看一下選完後的 UI 介面? 哈哈哈哈哈,wtf,年份呢?居然不顯示年份?為了安全起見,我們還是把年份加上,當然後端也會用到這個。 查閱文件,我發現數組的值可以是一個物件,顯示的是 text 欄位,那我們把生成的的資料 str 那個結構改一下。 程式碼如下: onChange() { - if(values[0] === this。dateArray[0]) { + if(values[0]。text === this。dateArray[0]。text) { 。。。 - } else if (values[0] === this。dateArray[this。dateArray。length - 1]) { + } else if (values[0]。text === this。dateArray[this。dateArray。length - 1]。text) { 。。。 } }, getNewDateArray(date, flag, picker, type) { - let str = `${month}月${i}日 ${weekArray[new Date(`${date。getFullYear()}-${month}-${i}`)。getDay()]}` + let str = { + year: date。getFullYear(),, + text: `${month}月${i}日 ${weekArray[new Date(`${newYear}-${month}-${i}`)。getDay()]}` + }; } 複製程式碼 資料結構是加上了,那生不生效呢?我們看一下介面; 介面倒是沒什麼問題,那是否能拿到資料呢?我們在 onChange 中列印一下當前選擇的 values; ok,完美!年的問題就解決了,是不是覺得 so easy。 再試試? 試試當前日期是當前月的第一天或者最後一天。 是不是發現什麼了?如果當前是第一天,你下拉是不會載入前一個月的資料的,因為沒有觸發 onChange 事件,你可以上拉到新的日期,再下拉到第一個,這樣就會重新整理了,當然如果你們產品能接受那也是可以的;最後一天是相同的道理。 解決方案 我們可以透過判斷初始時是否是今天是當月的第一天或者最後一天,來多載入前一個月或者後一個月; 1、增加一個識別符號變數 getMoreMonth ,false:不需要獲取更多, 1:獲取前一個月,2:獲取後一個月; 2、增加一個長度標識變數 moreLen 初始化為當前日期的,用來儲存對預設選中需要增加的長度; 3、獲取當前年份和月份,判斷獲取之前還是之後的月份,對年份和月份進行重置; 4、生成新的一個月的陣列,判斷是之前還是之後的月份,生成之前的月份將所有資料放到新陣列中;之後的月份直接向之前的 arr 陣列中 push 就可以了; 5、迴圈結束後,設定索引需要增加的長度,如果是獲取之前的一個月(getMoreMonth === 1),將 moreLen 加上之前一個月的總天數;並將新的陣列和之前的陣列進行拼接; 新增程式碼如下: getNewDateArray(date, flag, picker, type) { + let getMoreMonth = false; // 不需要獲取更多 1、獲取前一個月;2、獲取後一個月 + if(flag) { // 初次渲染 + const newDate = new Date(); + const monthDays = new Date(newDate。getFullYear(), newDate。getMonth() + 1, 0)。getDate() + if(new Date()。getDate() === 1) { // 當月第一天 + getMoreMonth = 1; + } else if (new Date()。getDate() === monthDays) { // 當月最後一天 + getMoreMonth = 2; + } + } date = date ? new Date(date) : new Date(); 。。。 for (let i = 1; i <= monthDays; i ++) { let str = { year: date。getFullYear(), text: `${month}月${i}日 ${weekArray[new Date(`${date。getFullYear()}-${month}-${i}`)。getDay()]}` }; arr。push(str); } + let moreLen = date。getDate() - 1; + if(getMoreMonth) { + let newYear = date。getFullYear(); + if (getMoreMonth === 1) { // 當月第一天 + if (month === 1) { + month = 12; + newYear -= 1; + } else { + month -= 1; + } + } else { + if (month === 12) {// 當月最後一天 + month = 1; + newYear += 1; + } else { + month += 1; + } + } + const moreMonthDays = new Date(newYear, month, 0)。getDate(); + let beforeArray = []; + for (let i = 1; i <= moreMonthDays; i ++) { + let str = { + year: newYear, + text: `${month}月${i}日 ${weekArray[new Date(`${newYear}-${month}-${i}`)。getDay()]}` + }; + if (getMoreMonth === 2) { // 獲取後一個月直接push + arr。push(str); + } else { + beforeArray。push(str); // 獲取前一個月存入新的陣列 + } + } + moreLen = getMoreMonth === 1 ? (moreLen + beforeArray。length) : moreLen; // 獲取前一個月索引增加前一個月總天數 + arr = beforeArray。concat(arr); + } if(flag) { return { values: this。dateArray, - defaultIndex: date。getDate() - 1 + defaultIndex: moreLen } } else { 。。。 複製程式碼測試一下 修改 mounted 中的首次呼叫入參為當前月份的第一日,並修改 getNewDateArray 中第一個判斷是否首次進入的邏輯。 程式碼修改如下: mounted() { this。dateColumns = [ - this。getNewDateArray(undefined, true),// 第一列 + this。getNewDateArray(‘2020-11-1’, true),// 第一列 。。。 }, getNewDateArray(date, flag, picker, type) { let getMoreMonth = false; // 不需要獲取更多 1、獲取前一個月;2、獲取後一個月 if(flag) { - const newDate = new Date(); + const newDate = new Date(date); const monthDays = new Date(newDate。getFullYear(), newDate。getMonth() + 1, 0)。getDate() - if(new Date()。getDate() === 1) { + if(new Date(date)。getDate() === 1) { + console。log(‘first day’) getMoreMonth = 1; } else if (new Date()。getDate() === monthDays) { getMoreMonth = 2; } } 。。。 } 複製程式碼 看一下測試的效果,獲取前面的一個月的沒有問題,相應的我們再看看獲取下一個月。 獲取下一個月,修改程式碼如下: mounted() { this。dateColumns = [ - this。getNewDateArray(‘2020-11-1’, true),// 第一列 + this。getNewDateArray(‘2020-11-30’, true),// 第一列 。。。 }, getNewDateArray(date, flag, picker, type) { let getMoreMonth = false; // 不需要獲取更多 1、獲取前一個月;2、獲取後一個月 if(flag) { - const newDate = new Date(date); + const newDate = new Date(); const monthDays = new Date(newDate。getFullYear(), newDate。getMonth() + 1, 0)。getDate() + if(new Date()。getDate() === 1) { - if(new Date(date)。getDate() === 1) { - console。log(‘first day’) getMoreMonth = 1; - } else if (new Date()。getDate() === monthDays) { + } else if (new Date(date)。getDate() === monthDays) { + console。log(‘last day’) getMoreMonth = 2; } } 。。。 } 複製程式碼 好的,也沒有問題,針對目前的問題就基本上解決了。 疑問 1、為什麼 this。getNewDateArray(${this。defaultYear}-${newMonth}-1, false, picker, true)這裡不用選擇的資料中的日作為第一個引數日期的最後日子傳進去呢?這裡有個坑,之前我也以為需要將這個日子用上,所以我穿了這個過去;但是後面在使用時,我發現滾動到1月份底的時候,載入的是 3 月份,並沒有載入 2 月份。 將如果是當月第一天或最後一天預載入兩個月的程式碼全部註釋(便於測試),並修改程式碼測試如下就會得到上述結果: mounted() { this。dateColumns = [ - this。getNewDateArray(‘2020-11-30’, true),// 第一列 + this。getNewDateArray(undefined, true),// 第一列 。。。 ] }, onChange(picker, values) { 。。。 const month = dateArr[0]; // 當前的月份 const day = dateArr[1]; // 當月第幾天 if(。。。) { 。。。 this。getNewDateArray(`${this。defaultYear}-${newMonth}-${day}`, false, picker, true) } else if (。。。) { 。。。 this。getNewDateArray(`${this。defaultYear}-${newMonth}-${day}`, false, picker, false) } } 複製程式碼 原因是因為這裡獲取到 2020 年的 1 月份的最後一天是 31 號,但是 2 月只有 29 天,所以 getMonth() 拿到的就是 2,再 +1 就變成了 3。 可最佳化點 1、 每次會到第一個或最後一個再去載入,是否可以最佳化為滾動去載入? 2、程式碼由於比較寫得比較急,有些點是可以最佳化的。 總結 透過產品出的原型和 UI,如果現有的UI庫不能完全滿足需求,我們可以找一個相似度比較高的進行修改。 在功能做完之後,需要針對一些臨界點做一些測試。 都看到這兒了點個讚唄,男帥女美,謝謝大家。