浏览代码

feat:datepicker (#398)

* feat: add datepicker

* fix: 修复datepicker问题

* fix: datepicker解决导入相关问题
kaixuan 4 年之前
父节点
当前提交
0640302147

+ 10 - 0
src/config.json

@@ -175,6 +175,16 @@
           "sort": 5,
           "show": true,
           "author": "yangkaixuan"
+        },
+        {
+          "version": "3.0.0",
+          "name": "DatePicker",
+          "type": "component",
+          "cName": "选择器",
+          "desc": "提供多个选型集合供用户选择,支持单列选择和多列级联,通常与弹出层配合使用",
+          "sort": 5,
+          "show": true,
+          "author": "yangkaixuan"
         }
       ]
     },

+ 148 - 0
src/packages/datepicker/demo.vue

@@ -0,0 +1,148 @@
+<template>
+  <div class="demo">
+    <h2>每列不显示中文</h2>
+    <nut-cell title="日期选择" :desc="desc1" @click="open(0)"></nut-cell>
+    <h2>限制开始结束时间</h2>
+    <nut-cell title="日期选择" :desc="desc2" @click="open(1)"></nut-cell>
+    <h2>限制开始结束时间(有默认值)</h2>
+    <nut-cell title="日期时间选择" :desc="desc3" @click="open(2)"></nut-cell>
+    <h2>12小时制</h2>
+    <nut-cell title="日期选择" :desc="desc4" @click="open(3)"></nut-cell>
+    <h2>限制开始结束小时</h2>
+    <nut-cell title="时间选择" :desc="desc5" @click="open(4)"></nut-cell>
+    <h2>分钟数递增步长设置</h2>
+    <nut-cell title="时间选择" :desc="desc6" @click="open(5)"></nut-cell>
+
+    <nut-datepicker
+      v-model="currentDate"
+      title="日期选择"
+      @confirm="
+        val => {
+          confirm(0, val);
+        }
+      "
+      v-model:is-visible="show"
+      :is-show-chinese="false"
+    ></nut-datepicker>
+    <nut-datepicker
+      v-model="currentDate"
+      title="日期选择"
+      :minDate="minDate"
+      :maxDate="maxDate"
+      @confirm="
+        val => {
+          confirm(1, val);
+        }
+      "
+      v-model:is-visible="show2"
+      :is-show-chinese="false"
+    ></nut-datepicker>
+    <nut-datepicker
+      v-model="currentDate"
+      title="日期时间选择"
+      type="datetime"
+      :minDate="minDate"
+      :maxDate="maxDate"
+      @confirm="
+        val => {
+          confirm(2, val);
+        }
+      "
+      v-model:is-visible="show3"
+    ></nut-datepicker>
+    <nut-datepicker
+      v-model="currentDate"
+      title="时间选择"
+      type="time"
+      :minDate="minDate"
+      :maxDate="maxDate"
+      :is-use12-hours="true"
+      @confirm="
+        val => {
+          confirm(3, val);
+        }
+      "
+      v-model:is-visible="show4"
+    ></nut-datepicker>
+    <nut-datepicker
+      v-model="currentDate"
+      title="时间选择"
+      type="time"
+      :minDate="minDate"
+      :maxDate="maxDate"
+      @confirm="
+        val => {
+          confirm(4, val);
+        }
+      "
+      v-model:is-visible="show5"
+    ></nut-datepicker>
+    <nut-datepicker
+      v-model="currentDate"
+      title="时间选择"
+      type="time"
+      :minDate="minDate"
+      :minute-step="5"
+      :maxDate="maxDate"
+      @confirm="
+        val => {
+          confirm(5, val);
+        }
+      "
+      v-model:is-visible="show6"
+    ></nut-datepicker>
+  </div>
+</template>
+
+<script lang="ts">
+import { toRefs, watch, ref } from 'vue';
+import { createComponent } from '@/utils/create';
+const { createDemo } = createComponent('datepicker');
+export default createDemo({
+  props: {},
+  setup() {
+    const show = ref(false);
+    const show2 = ref(false);
+    const show3 = ref(false);
+    const show4 = ref(false);
+    const show5 = ref(false);
+    const show6 = ref(false);
+    const showList = [show, show2, show3, show4, show5, show6];
+    const currentDate = ref(new Date(2020, 0, 1));
+
+    const today = currentDate.value;
+    const desc1 = ref('2020-1-1');
+    const desc2 = ref('2020-1-1');
+    const desc3 = ref('2020年-1月-1日-0时-0分');
+    const desc4 = ref('0时-0分-上午');
+    const desc5 = ref('0时-0分-0秒');
+    const desc6 = ref('0时-0分-0秒');
+    const descList = [desc1, desc2, desc3, desc4, desc5, desc6];
+    return {
+      show,
+      show2,
+      show3,
+      show4,
+      show5,
+      show6,
+      desc1,
+      desc2,
+      desc3,
+      desc4,
+      desc5,
+      desc6,
+      currentDate,
+      minDate: new Date(2020, 0, 1),
+      maxDate: new Date(2025, 10, 1),
+      open: index => {
+        showList[index].value = true;
+      },
+      confirm: (index, val) => {
+        console.log(val);
+        descList[index].value = val.join('-');
+      }
+    };
+  }
+});
+</script>
+<style lang="scss" scoped></style>

+ 164 - 0
src/packages/datepicker/doc.md

@@ -0,0 +1,164 @@
+#  datepicker组件
+
+### 介绍
+    
+时间选择器,支持日期、年月、时分等维度,通常与弹出层组件配合使用。
+    
+### 安装
+    
+```javascript
+import { createApp } from 'vue';
+import { Picker } from '@nutui/nutui';
+
+const app = createApp();
+app.use(Picker);
+```
+    
+## 代码演示
+    
+### 日期选择-每列不显示中文
+```html
+<nut-datepicker
+    v-model="currentDate"
+    @confirm="confirm"
+    v-model:is-visible="show"
+    :is-show-chinese="false"
+></nut-datepicker> 
+```
+```javascript
+<script>
+export default createDemo({
+  setup(props, { emit }) {
+    const show = ref(false);
+    const desc = ref('2020-1-1');
+
+    return {
+      show,
+      desc
+      open: (index) => {
+        show.value = true;
+      },
+      confirm: (res) => {
+        desc.value = val.join('-');
+      }
+    };
+  }
+});
+</script>
+```
+### 日期选择-限制开始结束时间
+```html
+<nut-datepicker
+    v-model="currentDate"
+    :minDate="minDate"
+    :maxDate="maxDate"
+    @confirm="confirm"
+    v-model:is-visible="show"
+    :is-show-chinese="false"
+></nut-datepicker> 
+```
+```javascript
+<script>
+export default createDemo({
+  setup(props, { emit }) {
+    const show = ref(false);
+    const desc = ref('2020-1-1');
+
+    return {
+      show,
+      desc,
+      minDate: new Date(2020, 0, 1),
+      maxDate: new Date(2025, 10, 1),
+      open: (index) => {
+        show.value = true;
+      },
+      confirm: (res) => {
+        desc.value = val.join('-');
+      }
+    };
+  }
+});
+</script>
+```
+### 日期时间-限制开始结束时间(有默认值)
+```html
+<nut-datepicker
+    v-model="currentDate"
+    :minDate="minDate"
+    :maxDate="maxDate"
+    type="datetime"
+    @confirm="confirm"
+    v-model:is-visible="show" 
+></nut-datepicker> 
+```
+```javascript
+<script>
+export default createDemo({
+  setup(props, { emit }) {
+    const show = ref(false);
+    const desc = ref('2020年-1月-1日-0时-0分');
+
+    return {
+      show,
+      desc,
+      minDate: new Date(2020, 0, 1),
+      maxDate: new Date(2025, 10, 1),
+      open: (index) => {
+        show.value = true;
+      },
+      confirm: (res) => {
+        desc.value = val.join('-');
+      }
+    };
+  }
+});
+</script>
+```
+### 时间选择-12小时制
+```html
+<nut-datepicker
+    v-model="currentDate"
+    type="time"
+    :minDate="minDate"
+    :maxDate="maxDate"
+    :is-use12-hours="true"
+    @confirm="confirm"
+    v-model:is-visible="show"
+></nut-datepicker>
+``` 
+### 时间选择-分钟数递增步长设置
+```html
+<nut-datepicker
+    v-model="currentDate"
+    type="time"
+    :minute-step="5"
+    :minDate="minDate"
+    :maxDate="maxDate"
+    :is-use12-hours="true"
+    @confirm="confirm"
+    v-model:is-visible="show"
+></nut-datepicker>
+```  
+
+## API
+    
+### Props
+    
+| 参数         | 说明                             | 类型   | 默认值           |
+|--------------|----------------------------------|--------|------------------|
+|  type        |    类型,日期'date', 日期时间'datetime',时间'time'            | String |'date'               |isVisible     |     是否可见    |  Boolean    | boolean | false  |
+|     isUse12Hours     | 是否十二小时制度,只限类型为'time'时使用 | boolean | false              |
+| minuteStep | 分钟步进值  | number | 1 |
+|   isShowChinese        |       每列是否展示中文 | boolean | false           |
+|   title | 设置标题 | string | null |
+|   startDate | 开始日期 | Date | 十年前 |
+|   endDate | 结束日期 | Date | 十年后 |
+
+
+
+### Events
+    
+| 事件名 | 说明           | 回调参数     |
+|--------|----------------|--------------|
+| confirm  | 点击确定按钮时触发 | event: Event |
+| close  | 关闭时触发 | event: Event |

+ 2 - 0
src/packages/datepicker/index.scss

@@ -0,0 +1,2 @@
+.nut-datepicker {
+}

+ 309 - 0
src/packages/datepicker/index.vue

@@ -0,0 +1,309 @@
+<template>
+  <view-block>
+    <nut-picker
+      :is-visible="show"
+      @close="closeHandler"
+      :list-data="columns"
+      @change="changeHandler"
+      :title="title"
+      @confirm="confirm"
+    ></nut-picker>
+  </view-block>
+</template>
+<script lang="ts">
+import { toRefs, watch, ref, computed } from 'vue';
+import picker from '@/packages/picker/index.vue';
+import { createComponent } from '@/utils/create';
+const { componentName, create } = createComponent('datepicker');
+const currentYear = new Date().getFullYear();
+function isDate(val: Date): val is Date {
+  return (
+    Object.prototype.toString.call(val) === '[object Date]' &&
+    !isNaN(val.getTime())
+  );
+}
+
+const zhCNType = {
+  day: '日',
+  year: '年',
+  month: '月',
+  hour: '时',
+  minute: '分',
+  seconds: '秒'
+};
+export default create({
+  children: [picker],
+  props: {
+    modelValue: null,
+    isVisible: {
+      type: Boolean,
+      default: false
+    },
+    title: {
+      type: String,
+      default: ''
+    },
+    type: {
+      type: String,
+      default: 'date'
+    },
+    isUse12Hours: {
+      type: Boolean,
+      default: false
+    },
+    isShowChinese: {
+      type: Boolean,
+      default: true
+    },
+    minuteStep: {
+      type: Number,
+      default: 1
+    },
+    minDate: {
+      type: Date,
+      default: () => new Date(currentYear - 10, 0, 1),
+      validator: isDate
+    },
+    maxDate: {
+      type: Date,
+      default: () => new Date(currentYear + 10, 11, 31),
+      validator: isDate
+    }
+  },
+  components: {},
+  emits: ['click', 'close', 'update:isVisible', 'confirm'],
+
+  setup(props, { emit }) {
+    const show = ref(false);
+    const title = ref(props.title);
+    const formatValue = value => {
+      if (!isDate(value)) {
+        value = props.minDate;
+      }
+
+      value = Math.max(value, (props.minDate as any).getTime());
+      value = Math.min(value, (props.maxDate as any).getTime());
+
+      return new Date(value);
+    };
+    const currentDate = ref(formatValue(props.modelValue));
+    watch(
+      () => props.title,
+      val => {
+        title.value = val;
+      }
+    );
+    watch(
+      () => props.isVisible,
+      val => {
+        show.value = val;
+      }
+    );
+    function getMonthEndDay(year: number, month: number): number {
+      return 32 - new Date(year, month - 1, 32).getDate();
+    }
+    const getBoundary = (type, value) => {
+      const boundary = props[`${type}Date`];
+      const year = boundary.getFullYear();
+      let month = 1;
+      let date = 1;
+      let hour = 0;
+      let minute = 0;
+
+      if (type === 'max') {
+        month = 12;
+        date = getMonthEndDay(value.getFullYear(), value.getMonth() + 1);
+        hour = 23;
+        minute = 59;
+      }
+      const seconds = minute;
+      if (value.getFullYear() === year) {
+        month = boundary.getMonth() + 1;
+        if (value.getMonth() + 1 === month) {
+          date = boundary.getDate();
+          if (value.getDate() === date) {
+            hour = boundary.getHours();
+            if (value.getHours() === hour) {
+              minute = boundary.getMinutes();
+            }
+          }
+        }
+      }
+
+      return {
+        [`${type}Year`]: year,
+        [`${type}Month`]: month,
+        [`${type}Date`]: date,
+        [`${type}Hour`]: hour,
+        [`${type}Minute`]: minute,
+        [`${type}Seconds`]: seconds
+      };
+    };
+
+    const ranges = computed(() => {
+      const {
+        maxYear,
+        maxDate,
+        maxMonth,
+        maxHour,
+        maxMinute,
+        maxSeconds
+      } = getBoundary('max', currentDate.value);
+
+      const {
+        minYear,
+        minDate,
+        minMonth,
+        minHour,
+        minMinute,
+        minSeconds
+      } = getBoundary('min', currentDate.value);
+
+      let result = [
+        {
+          type: 'year',
+          range: [minYear, maxYear]
+        },
+        {
+          type: 'month',
+          range: [minMonth, maxMonth]
+        },
+        {
+          type: 'day',
+          range: [minDate, maxDate]
+        },
+        {
+          type: 'hour',
+          range: [minHour, maxHour]
+        },
+        {
+          type: 'minute',
+          range: [minMinute, maxMinute]
+        },
+        {
+          type: 'seconds',
+          range: [minSeconds, maxSeconds]
+        }
+      ];
+
+      switch (props.type) {
+        case 'date':
+          result = result.slice(0, 3);
+          break;
+        case 'datetime':
+          result = result.slice(0, 5);
+          break;
+        case 'time':
+          if (props.isUse12Hours) {
+            result = result.slice(3, 5);
+          } else {
+            result = result.slice(3, 6);
+          }
+          break;
+        case 'month-day':
+          result = result.slice(1, 3);
+          break;
+        case 'datehour':
+          result = result.slice(0, 4);
+          break;
+      }
+      return result;
+    });
+
+    const changeHandler = val => {
+      let formatDate = [];
+      if (props.isShowChinese) {
+        formatDate = val.forEach((res: string) => {
+          Number(res.slice(0, res.length - 2));
+        });
+      } else {
+        formatDate = val;
+      }
+      currentDate.value = formatValue(
+        new Date(formatDate[0], formatDate[1] - 1, formatDate[2])
+      );
+    };
+    const generateValue = (
+      min: number,
+      max: number,
+      val: number,
+      type: string
+    ) => {
+      if (!(max > min)) return;
+      const arr: Array<number | string> = [];
+      let index = 0;
+      // let stopAdd = false;
+      while (min <= max) {
+        if (props.isShowChinese) {
+          arr.push(min + zhCNType[type]);
+        } else {
+          arr.push(min);
+        }
+
+        if (type === 'minute') {
+          min += props.minuteStep;
+        } else {
+          min++;
+        }
+
+        if (min <= val) {
+          index++;
+        }
+      }
+
+      return { values: arr, defaultIndex: index };
+    };
+    const getDateIndex = type => {
+      if (type === 'year') {
+        return currentDate.value.getFullYear();
+      } else if (type === 'month') {
+        return currentDate.value.getMonth() + 1;
+      } else if (type === 'day') {
+        return currentDate.value.getDate();
+      } else if (type === 'hour') {
+        return currentDate.value.getHours();
+      } else if (type === 'minute') {
+        return currentDate.value.getMinutes();
+      } else if (type === 'seconds') {
+        return currentDate.value.getSeconds();
+      }
+      return 0;
+    };
+    const columns = computed(() => {
+      const val = ranges.value.map(res => {
+        return generateValue(
+          res.range[0],
+          res.range[1],
+          getDateIndex(res.type),
+          res.type
+        );
+      });
+      if (props.type === 'time' && props.isUse12Hours) {
+        val.push({ values: ['上午', '下午'], defaultIndex: 0 });
+      }
+      return val;
+    });
+    const handleClick = (event: Event) => {
+      emit('click', event);
+    };
+
+    return {
+      show,
+      title,
+      changeHandler,
+      closeHandler: () => {
+        emit('update:isVisible', false);
+      },
+      confirm: val => {
+        emit('update:isVisible', false);
+        emit('confirm', val);
+      },
+      columns
+    };
+  }
+});
+</script>
+
+<style lang="scss">
+@import 'index.scss';
+</style>

+ 6 - 6
src/packages/picker/Column.vue

@@ -1,5 +1,5 @@
 <template>
-  <view
+  <view-block
     class="nut-picker__content"
     :style="{ height: height + 'px' }"
     @touchstart="onTouchStart"
@@ -8,15 +8,15 @@
     @touchcancel="onTouchEnd"
     @transitionend="stopMomentum"
   >
-    <view class="nut-picker__wrapper" ref="wrapper" :style="wrapperStyle">
-      <view
+    <view-block class="nut-picker__wrapper" ref="wrapper" :style="wrapperStyle">
+      <view-block
         class="nut-picker__item"
         :key="index"
         v-for="(item, index) in state.options"
-        >{{ dataType === 'cascade' ? item.text : item }}</view
+        >{{ dataType === 'cascade' ? item.text : item }}</view-block
       >
-    </view>
-  </view>
+    </view-block>
+  </view-block>
 </template>
 <script lang="ts">
 import { reactive, ref, watch, computed } from 'vue';

+ 1 - 0
src/packages/picker/index.scss

@@ -38,6 +38,7 @@
     position: relative;
   }
   &__columnitem {
+    width: 0;
     flex-grow: 1;
     height: 100%;
   }

+ 49 - 26
src/packages/picker/index.vue

@@ -1,24 +1,27 @@
 <template>
-  <view>
+  <view-block class="nut-picker">
     <nut-popup
       position="bottom"
       :style="{ height: height + 56 + 'px' }"
       v-model:show="show"
       @close="close"
     >
-      <view class="nut-picker__bar">
-        <view class="nut-picker__left" @click="close()"> 取消</view>
-        <view> {{ title }}</view>
-        <view @click="confirm()"> 确定</view>
-      </view>
+      <view-block class="nut-picker__bar">
+        <view-block class="nut-picker__left" @click="close()"> 取消</view-block>
+        <view-block> {{ title }}</view-block>
+        <view-block @click="confirm()"> 确定</view-block>
+      </view-block>
 
-      <view class="nut-picker__column">
-        <view
+      <view-block class="nut-picker__column">
+        <view-block
           class="nut-picker__mask"
           :style="{ backgroundSize: `100% ${top}px` }"
-        ></view>
-        <view class="nut-picker__hairline" :style="{ top: ` ${top}px` }"></view>
-        <view
+        ></view-block>
+        <view-block
+          class="nut-picker__hairline"
+          :style="{ top: ` ${top}px` }"
+        ></view-block>
+        <view-block
           class="nut-picker__columnitem"
           v-for="(item, columnIndex) in columnList"
           :key="columnIndex"
@@ -36,10 +39,10 @@
               }
             "
           ></nut-picker-column>
-        </view>
-      </view>
+        </view-block>
+      </view-block>
     </nut-popup>
-  </view>
+  </view-block>
 </template>
 <script lang="ts">
 import { reactive, ref, watch, computed, toRaw } from 'vue';
@@ -62,12 +65,13 @@ export default create({
     },
     ...commonProps
   },
-  emits: ['close', 'confirm', 'update:isVisible'],
+  components: { column },
+  emits: ['close', 'change', 'confirm', 'update:isVisible'],
 
   setup(props, { emit }) {
     const show = ref(false);
     const defaultIndex = ref(props.defaultIndex);
-    const listData: any = reactive(props.listData);
+    const formattedColumns: any = ref(props.listData);
     //临时变量,当点击确定时候赋值
     let _defaultIndex = props.defaultIndex;
     const childrenKey = 'children';
@@ -81,6 +85,13 @@ export default create({
       }
     );
 
+    watch(
+      () => props.listData,
+      val => {
+        formattedColumns.value = val;
+      }
+    );
+
     const addDefaultIndexList = listData => {
       defaultIndexList = [];
       listData.forEach(res => {
@@ -88,7 +99,7 @@ export default create({
       });
     };
     const dataType = computed(() => {
-      const firstColumn = listData[0] || {};
+      const firstColumn = formattedColumns.value[0] || {};
 
       if (typeof firstColumn === 'object') {
         if (firstColumn?.[childrenKey]) {
@@ -118,13 +129,15 @@ export default create({
 
     const columnList = computed(() => {
       if (dataType.value === 'text') {
-        return [{ values: listData, defaultIndex: defaultIndex.value }];
+        return [
+          { values: formattedColumns.value, defaultIndex: defaultIndex.value }
+        ];
       } else if (dataType.value === 'multipleColumns') {
-        return listData;
+        return formattedColumns.value;
       } else if (dataType.value === 'cascade') {
-        return formatCascade(listData, defaultIndex.value);
+        return formatCascade(formattedColumns.value, defaultIndex.value);
       }
-      return listData;
+      return formattedColumns.value;
     });
     const getCascadeData = (listData, defaultIndex) => {
       let arr = listData;
@@ -152,7 +165,7 @@ export default create({
       },
       changeHandler: (columnIndex, dataIndex) => {
         if (dataType.value === 'cascade') {
-          let cursor: any = listData;
+          let cursor: any = toRaw(formattedColumns.value);
           //最外层使用props.defaultIndex作为初始index
           if (columnIndex === 0) {
             defaultIndex.value = dataIndex;
@@ -172,22 +185,32 @@ export default create({
           _defaultIndex = dataIndex;
         } else if (dataType.value === 'multipleColumns') {
           defaultIndexList[columnIndex] = dataIndex;
+          const val = defaultIndexList.map(
+            (res, i) => toRaw(formattedColumns.value)[i].values[res]
+          );
+          console.log('val', defaultIndexList);
+          emit('change', val);
         }
       },
+
       confirm: () => {
         if (dataType.value === 'text') {
           defaultIndex.value = _defaultIndex;
-          emit('confirm', listData[_defaultIndex]);
+          emit('confirm', formattedColumns.value[_defaultIndex]);
         } else if (dataType.value === 'multipleColumns') {
           for (let i = 0; i < defaultIndexList.length; i++) {
-            listData[i].defaultIndex = defaultIndexList[i];
+            formattedColumns.value[i].defaultIndex = defaultIndexList[i];
           }
-          const checkedArr = toRaw(listData).map(
+          const checkedArr = toRaw(formattedColumns.value).map(
             (res: any) => res.values[res.defaultIndex]
           );
+          console.log(formattedColumns.value);
           emit('confirm', checkedArr);
         } else if (dataType.value === 'cascade') {
-          emit('confirm', getCascadeData(toRaw(listData), defaultIndex.value));
+          emit(
+            'confirm',
+            getCascadeData(toRaw(formattedColumns.value), defaultIndex.value)
+          );
         }
 
         emit('update:isVisible', false);