自己動(dòng)手寫一個(gè)移動(dòng)端日期選擇器組件

背景

本文寫的組件是基于 uni-app 框架下的,但是其實(shí)框架不重要,思路都是一樣的纱耻。

有同學(xué)可能會(huì)問(wèn)了,uni-app 本身不是就有 picker险耀,mode=time 的時(shí)候就是時(shí)間選擇器了嗎弄喘,為什么還要自己寫一個(gè)?那是因?yàn)槲覀儺a(chǎn)品大佬說(shuō)甩牺,不要固定在底部彈出選擇的蘑志,想嵌套在頁(yè)面篩選條件里,因?yàn)榭紤]到交互blabla的……我想了想贬派,好吧急但,給時(shí)間啥都好說(shuō),咱就自己造個(gè)輪子唄~

效果演示

先來(lái)看看效果~

完整功能
年月日模式
年月日時(shí)分秒模式
年月模式

思路

開始動(dòng)手之前先捋一下思路搞乏。

移動(dòng)端的日期篩選器交互方式比較常見(jiàn)的都是多列滾動(dòng)的波桩,所以我們可以用 picker-view 來(lái)實(shí)現(xiàn)。除了基礎(chǔ)交互请敦,組件需要注意的點(diǎn)就是年月日之間的相互關(guān)聯(lián)镐躲,比如1月有31天,4月是30天侍筛,閏年2月是29天等這些萤皂,也就是年月日需要相互關(guān)聯(lián)動(dòng)態(tài)變化。此外還可以添加支持配置最大最小時(shí)間范圍匣椰,支持切換不同的時(shí)間模式(比如年月日/年月/年月日時(shí)分秒)等裆熙。

一個(gè)常用的日期選擇器組件主要的功能就是以上這些了。

完整代碼見(jiàn):https://github.com/Dandelion-drq/uniapp-datetime-picker

歡迎喜歡的朋友給個(gè)star哈~

實(shí)現(xiàn)

1. picker-view 實(shí)現(xiàn)基礎(chǔ)交互

先封裝一個(gè)接受多個(gè)數(shù)組的多列滾動(dòng)選擇組件,方便后面支持不同日期模式切換弛车。

<template>
  <picker-view class="picker-view" :value="indexArr" @change="onChange">
    <picker-view-column class="picker-view-column" v-for="(col, colIdx) in columns" :key="colIdx">
      <view v-for="(item, idx) in col" :key="idx">{{ item }}</view>
    </picker-view-column>
  </picker-view>
</template>

<script src="./index.js"></script>

<style lang="css" scoped src="./index.css"></style>
.picker-view {
  height: 356rpx;
}

.picker-view-column {
  font-size: 14px;
  line-height: 34px;
  text-align: center;
  color: #333;
}
export default {
  data() {
    return {};
  },
  props: {
    // 所有列選項(xiàng)數(shù)據(jù)
    columns: {
      type: Array,
      default: () => []
    },
    // 每一列默認(rèn)選中值數(shù)組齐媒,不傳默認(rèn)選中第一項(xiàng)
    selectVals: {
      type: Array,
      default: () => []
    }
  },
  computed: {
    // 每一列選中項(xiàng)的索引,當(dāng)默認(rèn)選中值變化的時(shí)候這個(gè)值也要變化
    indexArr: {
      // 多維數(shù)組纷跛,深度監(jiān)聽(tīng)
      cache: false,
      get() {
        // console.log('indexArr', this.selectVals, this.columns);
        if (this.selectVals.length > 0) {
          return this.columns.map((col, cIdx) => {
            return col.findIndex((i) => i == this.selectVals[cIdx]);
          });
        } else {
          return [].fill(0, 0, this.columns.length);
        }
      }
    }
  },
  methods: {
    onChange(e) {
      const { value } = e.detail;
      // console.log('pickerview改變', value, this.columns);

      let ret = this.columns.map((item, index) => {
        let idx = value[index];
        if (idx < 0) {
          idx = 0;
        }
        if (idx > item.length - 1) {
          idx = item.length - 1;
        }
        return item[idx];
      });
      // console.log('選中值', ret);

      this.$emit('onChange', {
        value: ret
      });
    }
  }
};

2. 年月日動(dòng)態(tài)配置以及支持最大最小日期

年份比較簡(jiǎn)單,從配置的最小日期年份到最大日期年份生成數(shù)組就好邀杏。月份要注意當(dāng)如果選中的年份剛好是最小/最大可選日期的年份時(shí)贫奠,月份要從最小/最大可選日期開始/結(jié)束,其他時(shí)候月份都是1~12望蜡。日就先列出正常一年每個(gè)人的天數(shù)配置唤崭,然后注意閏年2月是29天,還有同樣跟月一樣要注意的是當(dāng)如果選中的年份和月份剛好是最小/最大可選日期的年月時(shí)脖律,日要從最小/最大可選日期開始/結(jié)束谢肾。時(shí)分秒同理。

<template>
  <view class="datetime-picker">
    <CustomPickerView :columns="dateConfig" :selectVals="selectVals" @onChange="onChangePickerValue" />
  </view>
</template>

<script src="./index.js"></script>
import CustomPickerView from '../customPickerView/index.vue';
import DateUtil from '../dateTimePicker/dateUtil';

export default {
  components: {
    CustomPickerView
  },
  data() {
    return {
      selectYear: new Date().getFullYear(),
      selectMonth: new Date().getMonth() + 1, // 選中的月份小泉,1~12
      selectDay: new Date().getDate(),
      selectHour: new Date().getHours(),
      selectMinute: new Date().getMinutes(),
      selectSecond: new Date().getSeconds()
    };
  },
  props: {
    // 可選的最小日期芦疏,默認(rèn)十年前
    minDate: {
      type: String,
      default: ''
    },
    // 可選的最大日期,默認(rèn)十年后
    maxDate: {
      type: String,
      default: ''
    }
  },
  computed: {
    minDateObj() {
      let minDate = this.minDate;
      if (minDate) {
        if (this.mode == 2 && minDate.replace(/\-/g, '/').split('/').length == 2) {
          // 日期模式為年月時(shí)有可能傳進(jìn)來(lái)的minDate是2022-02這樣的格式微姊,在ios下new Date會(huì)報(bào)錯(cuò)酸茴,加上日期部分做兼容
          minDate += '-01';
        }
        return new Date(DateUtil.handleDateStr(minDate));
      } else {
        // 沒(méi)有傳最小日期,默認(rèn)十年前
        minDate = new Date();
        minDate.setFullYear(minDate.getFullYear() - 10);
        return minDate;
      }
    },
    maxDateObj() {
      let maxDate = this.maxDate;
      if (maxDate) {
        if (this.mode == 2 && maxDate.replace(/\-/g, '/').split('/').length == 2) {
          // 日期模式為年月時(shí)有可能傳進(jìn)來(lái)的maxDate是2022-02這樣的格式兢交,在ios下new Date會(huì)報(bào)錯(cuò)薪捍,加上日期部分做兼容
          maxDate += '-01';
        }
        return new Date(DateUtil.handleDateStr(maxDate));
      } else {
        // 沒(méi)有傳最小日期,默認(rèn)十年后
        maxDate = new Date();
        maxDate.setFullYear(maxDate.getFullYear() + 10);
        return maxDate;
      }
    },    
    years() {
      let years = [];
      let minYear = this.minDateObj.getFullYear();
      let maxYear = this.maxDateObj.getFullYear();
      for (let i = minYear; i <= maxYear; i++) {
        years.push(i);
      }

      return years;
    },
    months() {
      let months = [];
      let minMonth = 1;
      let maxMonth = 12;

      // 如果選中的年份剛好是最小可選日期的年份配喳,那月份就要從最小日期的月份開始
      if (this.selectYear == this.minDateObj.getFullYear()) {
        minMonth = this.minDateObj.getMonth() + 1;
      }
      // 如果選中的年份剛好是最大可選日期的年份酪穿,那月份就要在最大日期的月份結(jié)束
      if (this.selectYear == this.maxDateObj.getFullYear()) {
        maxMonth = this.maxDateObj.getMonth() + 1;
      }

      for (let i = minMonth; i <= maxMonth; i++) {
        months.push(i);
      }

      return months;
    },
    days() {
      // 一年中12個(gè)月每個(gè)月的天數(shù)
      let monthDaysConfig = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
      // 閏年2月有29天
      if (this.selectMonth == 2 && this.selectYear % 4 == 0) {
        monthDaysConfig[1] = 29;
      }

      let minDay = 1;
      let maxDay = monthDaysConfig[this.selectMonth - 1];

      if (this.selectYear == this.minDateObj.getFullYear() && this.selectMonth == this.minDateObj.getMonth() + 1) {
        minDay = this.minDateObj.getDate();
      }
      if (this.selectYear == this.maxDateObj.getFullYear() && this.selectMonth == this.maxDateObj.getMonth() + 1) {
        maxDay = this.maxDateObj.getDate();
      }

      let days = [];
      for (let i = minDay; i <= maxDay; i++) {
        days.push(i);
      }

      return days;
    },
    hours() {
      let hours = [];
      let minHour = 0;
      let maxHour = 23;

      if (
        this.selectYear == this.minDateObj.getFullYear() &&
        this.selectMonth == this.minDateObj.getMonth() + 1 &&
        this.selectDay == this.minDateObj.getDate()
      ) {
        minHour = this.minDateObj.getHours();
      }
      if (
        this.selectYear == this.maxDateObj.getFullYear() &&
        this.selectMonth == this.maxDateObj.getMonth() + 1 &&
        this.selectDay == this.maxDateObj.getDate()
      ) {
        maxHour = this.maxDateObj.getHours();
      }

      for (let i = minHour; i <= maxHour; i++) {
        hours.push(i);
      }

      return hours;
    },
    minutes() {
      let mins = [];
      let minMin = 0;
      let maxMin = 59;

      if (
        this.selectYear == this.minDateObj.getFullYear() &&
        this.selectMonth == this.minDateObj.getMonth() + 1 &&
        this.selectDay == this.minDateObj.getDate() &&
        this.selectHour == this.minDateObj.getHours()
      ) {
        minMin = this.minDateObj.getMinutes();
      }
      if (
        this.selectYear == this.maxDateObj.getFullYear() &&
        this.selectMonth == this.maxDateObj.getMonth() + 1 &&
        this.selectDay == this.maxDateObj.getDate() &&
        this.selectHour == this.maxDateObj.getHours()
      ) {
        maxMin = this.maxDateObj.getMinutes();
      }

      for (let i = minMin; i <= maxMin; i++) {
        mins.push(i);
      }

      return mins;
    },
    seconds() {
      let seconds = [];
      let minSecond = 0;
      let maxSecond = 59;

      if (
        this.selectYear == this.minDateObj.getFullYear() &&
        this.selectMonth == this.minDateObj.getMonth() + 1 &&
        this.selectDay == this.minDateObj.getDate() &&
        this.selectHour == this.minDateObj.getHours() &&
        this.selectMinute == this.minDateObj.getMinutes()
      ) {
        minSecond = this.minDateObj.getSeconds();
      }
      if (
        this.selectYear == this.maxDateObj.getFullYear() &&
        this.selectMonth == this.maxDateObj.getMonth() + 1 &&
        this.selectDay == this.maxDateObj.getDate() &&
        this.selectHour == this.maxDateObj.getHours() &&
        this.selectMinute == this.maxDateObj.getMinutes()
      ) {
        maxSecond = this.maxDateObj.getSeconds();
      }

      for (let i = minSecond; i <= maxSecond; i++) {
        seconds.push(i);
      }

      return seconds;
    }
  }
}
// DateUtil.js

/**
 * 日期時(shí)間格式化
 * @param {Date} date 要格式化的日期對(duì)象
 * @param {String} fmt 格式化字符串,eg:YYYY-MM-DD HH:mm:ss
 * @returns 格式化后的日期字符串
 */
function formatDate(date, fmt) {
  if (typeof date == 'string') {
    date = new Date(handleDateStr(date));
  }

  var o = {
    'M+': date.getMonth() + 1, // 月份
    'd+': date.getDate(), // 日
    'D+': date.getDate(), // 日
    'H+': date.getHours(), // 小時(shí)
    'h+': date.getHours(), // 小時(shí)
    'm+': date.getMinutes(), // 分
    's+': date.getSeconds(), // 秒
    'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
    S: date.getMilliseconds() // 毫秒
  };

  if (/([y|Y]+)/.test(fmt)) {
    fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').slice(4 - RegExp.$1.length));
  }
  for (var k in o) {
    if (new RegExp('(' + k + ')').test(fmt)) {
      fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).slice(('' + o[k]).length));
    }
  }

  return fmt;
}

/**
 * 處理時(shí)間字符串晴裹,兼容ios下new Date()返回NaN問(wèn)題
 * @param {*} dateStr 日期字符串
 * @returns
 */
function handleDateStr(dateStr) {
  return dateStr.replace(/\-/g, '/');
}

/**
 * 判斷日期1是否在日期2之前被济,即日期1小于日期2
 * @param {Date} date1
 * @param {Date} date2
 * @returns
 */
function isBefore(date1, date2) {
  if (typeof date1 == 'string') {
    date1 = new Date(handleDateStr(date1));
  }
  if (typeof date2 == 'string') {
    date2 = new Date(handleDateStr(date2));
  }
  return date1.getTime() < date2.getTime();
}

/**
 * 判斷日期1是否在日期2之后,即日期1大于日期2
 * @param {Date} date1
 * @param {Date} date2
 * @returns
 */
function isAfter(date1, date2) {
  if (typeof date1 == 'string') {
    date1 = new Date(handleDateStr(date1));
  }
  if (typeof date2 == 'string') {
    date2 = new Date(handleDateStr(date2));
  }
  return date1.getTime() > date2.getTime();
}

export default {
  formatDate,
  handleDateStr,
  isBefore,
  isAfter
};

3. 支持不同日期模式

支持多種不同的日期模式息拜,包括年月日(默認(rèn))溉潭、年月、年份少欺、年月日時(shí)分秒喳瓣。主要的處理邏輯是要根據(jù) mode 的變化,來(lái)動(dòng)態(tài)生成傳給 pickerView 組件的數(shù)組赞别,以及其默認(rèn)選中值畏陕,還有注意 pickerView 組件 onChange 事件的處理也需要考慮不同日期模式的情況。

<template>
  <view class="datetime-picker">
    <PickerView :columns="dateConfig" :selectVals="selectVals" @onChange="onChangePickerValue" />
  </view>
</template>

<script src="./index.js"></script>

<style scoped></style>
{
  props: {
    // 日期模式仿滔,1:年月日惠毁,2:年月犹芹,3:年份,4:年月日時(shí)分秒
    mode: {
      type: Number,
      default: 1
    },
    // 默認(rèn)選中日期(注意要跟日期模式對(duì)應(yīng))
    defaultDate: {
      type: String,
      default: ''
    }
  }
  computed: {
    // 傳給pickerView組件的數(shù)組鞠绰,根據(jù)mode來(lái)生成不同的數(shù)據(jù)
    dateConfig() {
      if (this.mode == 2) {
        // 年月模式
        let years = this.years.map((y) => y + '年');
        let months = this.months.map((m) => m + '月');
        return [years, months];
      } else if (this.mode == 3) {
        // 只有年份模式
        let years = this.years.map((y) => y + '年');
        return [years];
      } else if (this.mode == 4) {
        // 年月日時(shí)分秒模式
        let years = this.years.map((y) => y + '年');
        let months = this.months.map((m) => m + '月');
        let days = this.days.map((d) => d + '日');
        let hours = this.hours.map((h) => h + '時(shí)');
        let minutes = this.minutes.map((m) => m + '分');
        let seconds = this.seconds.map((s) => s + '秒');
        return [years, months, days, hours, minutes, seconds];
      } else {
        // 默認(rèn)腰埂,年月日模式
        let years = this.years.map((y) => y + '年');
        let months = this.months.map((m) => m + '月');
        let days = this.days.map((d) => d + '日');
        return [years, months, days];
      }
    },
    // pickerView默認(rèn)值,根據(jù)mode的切換來(lái)變換值
    selectVals() {
      if (this.mode == 2) {
        return [this.selectYear + '年', this.selectMonth + '月'];
      } else if (this.mode == 3) {
        return [this.selectYear + '年'];
      } else if (this.mode == 4) {
        return [
          this.selectYear + '年',
          this.selectMonth + '月',
          this.selectDay + '日',
          this.selectHour + '時(shí)',
          this.selectMinute + '分',
          this.selectSecond + '秒'
        ];
      } else {
        return [this.selectYear + '年', this.selectMonth + '月', this.selectDay + '日'];
      }
    }
  },
  methods: {
        onChangePickerValue(e) {
      const { value } = e;
      // console.log('onChangePickerValue', value);

      if (this.mode == 2 && value[0] && value[1]) {
        // 年月模式
        this.selectYear = Number(value[0].replace('年', ''));
        this.selectMonth = Number(value[1].replace('月', ''));
      } else if (this.mode == 3 && value[0]) {
        // 只有年份模式
        this.selectYear = Number(value[0].replace('年', ''));
      } else if (this.mode == 4 && value[0] && value[1] && value[2] != '' && value[3] && value[4] && value[5]) {
        // 年月日時(shí)分秒模式
        this.selectYear = Number(value[0].replace('年', ''));
        this.selectMonth = Number(value[1].replace('月', ''));
        this.selectDay = Number(value[2].replace('日', ''));
        this.selectHour = Number(value[3].replace('時(shí)', ''));
        this.selectMinute = Number(value[4].replace('分', ''));
        this.selectSecond = Number(value[5].replace('秒', ''));
      } else if (value[0] && value[1] && value[2]) {
        // 默認(rèn)蜈膨,年月日模式
        this.selectYear = Number(value[0].replace('年', ''));
        this.selectMonth = Number(value[1].replace('月', ''));
        this.selectDay = Number(value[2].replace('日', ''));
      } else {
        // 其他情況可能是pickerView返回的數(shù)據(jù)有問(wèn)題屿笼,不處理
        console.log('onChangePickerValue其他情況');
        return;
      }

      let formatTmpl = 'YYYY-MM-DD';
      if (this.mode == 2) {
        formatTmpl = 'YYYY-MM';
      } else if (this.mode == 3) {
        formatTmpl = 'YYYY';
      } else if (this.mode == 4) {
        formatTmpl = 'YYYY-MM-DD HH:mm:ss';
      }

      this.$emit(
        'onChange',
        DateUtil.formatDate(
          new Date(`${this.selectYear}/${this.selectMonth}/${this.selectDay} ${this.selectHour}:${this.selectMinute}:${this.selectSecond}`),
          formatTmpl
        )
      );
    }
  }
}

完成了以上3點(diǎn),日期選擇器組件就寫好了翁巍,完整代碼以及使用demo見(jiàn):https://github.com/Dandelion-drq/uniapp-datetime-picker

歡迎喜歡的朋友給個(gè)star~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末驴一,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子灶壶,更是在濱河造成了極大的恐慌肝断,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件驰凛,死亡現(xiàn)場(chǎng)離奇詭異胸懈,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)洒嗤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門箫荡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人渔隶,你說(shuō)我怎么就攤上這事羔挡。” “怎么了间唉?”我有些...
    開封第一講書人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵绞灼,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我呈野,道長(zhǎng)低矮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任被冒,我火速辦了婚禮军掂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘昨悼。我一直安慰自己蝗锥,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開白布率触。 她就那樣靜靜地躺著终议,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上穴张,一...
    開封第一講書人閱讀 51,365評(píng)論 1 302
  • 那天细燎,我揣著相機(jī)與錄音,去河邊找鬼皂甘。 笑死玻驻,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的偿枕。 我是一名探鬼主播击狮,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼益老!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起寸莫,我...
    開封第一講書人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤捺萌,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后膘茎,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體桃纯,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年披坏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了态坦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡棒拂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情寸士,我是刑警寧澤慕趴,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站攻旦,受9級(jí)特大地震影響喻旷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜牢屋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一且预、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧烙无,春花似錦锋谐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春多搀,著一層夾襖步出監(jiān)牢的瞬間歧蕉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工康铭, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留惯退,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓从藤,卻偏偏與公主長(zhǎng)得像催跪,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子夷野,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容