Prechádzať zdrojové kódy

DatePicker: support multiple dates selection (#10650)

* DatePicker: add multiselect feature

* DatePicker: add multiselect feature

* DatePicker: add multiselect feature

* DatePicker: add multiselect feature

* DatePicker: add multiselect feature

* Datepicker: fix can't clear bug

*  Datepicker: add unit test

* Datepicker: add unit test

* Datepicker: add unit test

* Datepicker: add unit test

* Datepicker: add unit test

* Datepicker: add unit test

* DatePicker: support multiple dates selection

* reflect review comments
杨奕 7 rokov pred
rodič
commit
1f4adb7c08

+ 1 - 0
.gitignore

@@ -6,6 +6,7 @@ npm-debug.log.*
 lerna-debug.log.*
 lib
 .idea
+.vscode
 examples/element-ui
 examples/pages/en-US
 examples/pages/zh-CN

+ 55 - 28
examples/docs/en-US/date-picker.md

@@ -66,7 +66,8 @@
         value10: '',
         value11: '',
         value12: '',
-        value13: []
+        value13: [],
+        value14: []
       };
     }
   };
@@ -88,6 +89,20 @@
     }
   }
 
+  .demo-date-picker .container {
+    flex: 1;
+    border-right: solid 1px #EFF2F6;
+    .block {
+      border-right: none;
+      &:last-child {
+        border-top: solid 1px #EFF2F6;
+      }
+    }
+    &:last-child {
+      border-right: none;
+    }
+  }
+
   .demo-date-picker .demonstration {
     display: block;
     color: #8492a6;
@@ -163,40 +178,51 @@ Basic date picker measured by 'day'.
   };
 </script>
 ```
-
 :::
 
 ### Other measurements
 
-You can choose week, month or year by extending the standard date picker component.
+You can choose week, month, year or multiple dates by extending the standard date picker component.
 
 :::demo
 
 ```html
-<div class="block">
-  <span class="demonstration">Week</span>
-  <el-date-picker
-    v-model="value3"
-    type="week"
-    format="Week WW"
-    placeholder="Pick a week">
-  </el-date-picker>
-</div>
-<div class="block">
-  <span class="demonstration">Month</span>
-  <el-date-picker
-    v-model="value4"
-    type="month"
-    placeholder="Pick a month">
-  </el-date-picker>
+<div class="container">
+  <div class="block">
+    <span class="demonstration">Week</span>
+    <el-date-picker
+      v-model="value3"
+      type="week"
+      format="Week WW"
+      placeholder="Pick a week">
+    </el-date-picker>
+  </div>
+  <div class="block">
+    <span class="demonstration">Month</span>
+    <el-date-picker
+      v-model="value4"
+      type="month"
+      placeholder="Pick a month">
+    </el-date-picker>
+  </div>
 </div>
-<div class="block">
-  <span class="demonstration">Year</span>
-  <el-date-picker
-    v-model="value5"
-    type="year"
-    placeholder="Pick a year">
-  </el-date-picker>
+<div class="container">
+  <div class="block">
+    <span class="demonstration">Year</span>
+    <el-date-picker
+      v-model="value5"
+      type="year"
+      placeholder="Pick a year">
+    </el-date-picker>
+  </div>
+  <div class="block">
+    <span class="demonstration">Dates</span>
+    <el-date-picker
+      type="dates"
+      v-model="value14"
+      placeholder="Pick one or more dates">
+    </el-date-picker>
+  </div>
 </div>
 
 <script>
@@ -205,7 +231,8 @@ You can choose week, month or year by extending the standard date picker compone
       return {
         value3: '',
         value4: '',
-        value5: ''
+        value5: '',
+        value14: []
       };
     }
   };
@@ -455,7 +482,7 @@ When picking a date range, you can assign the time part for start date and end d
 | placeholder | placeholder in non-range mode | string | — | — |
 | start-placeholder | placeholder for the start date in range mode | string | — | — |
 | end-placeholder | placeholder for the end date in range mode | string | — | — |
-| type | type of the picker | string | year/month/date/datetime/ week/datetimerange/daterange | date |
+| type | type of the picker | string | year/month/date/dates/datetime/ week/datetimerange/daterange | date |
 | format | format of the displayed value in the input box | string | see [date formats](#/en-US/component/date-picker#date-formats) | yyyy-MM-dd |
 | align | alignment | left/center/right | left |
 | popper-class | custom class name for DatePicker's dropdown | string | — | — |

+ 55 - 27
examples/docs/es/date-picker.md

@@ -66,7 +66,8 @@
         value10: '',
         value11: '',
         value12: '',
-        value13: []
+        value13: [],
+        value14: []
       };
     }
   };
@@ -88,6 +89,20 @@
     }
   }
 
+  .demo-date-picker .container {
+    flex: 1;
+    border-right: solid 1px #EFF2F6;
+    .block {
+      border-right: none;
+      &:last-child {
+        border-top: solid 1px #EFF2F6;
+      }
+    }
+    &:last-child {
+      border-right: none;
+    }
+  }
+
   .demo-date-picker .demonstration {
     display: block;
     color: #8492a6;
@@ -169,35 +184,47 @@ Date Picker básico por "día".
 
 ### Otras mediciones
 
-Puede seleccionar la semana, el mes o el año extendiendo el componente date picker estándar.
+You can choose week, month, year or multiple dates by extending the standard date picker component.
 
 :::demo
 
 ```html
-<div class="block">
-  <span class="demonstration">Week</span>
-  <el-date-picker
-    v-model="value3"
-    type="week"
-    format="Week WW"
-    placeholder="Pick a week">
-  </el-date-picker>
-</div>
-<div class="block">
-  <span class="demonstration">Month</span>
-  <el-date-picker
-    v-model="value4"
-    type="month"
-    placeholder="Pick a month">
-  </el-date-picker>
+<div class="container">
+  <div class="block">
+    <span class="demonstration">Week</span>
+    <el-date-picker
+      v-model="value3"
+      type="week"
+      format="Week WW"
+      placeholder="Pick a week">
+    </el-date-picker>
+  </div>
+  <div class="block">
+    <span class="demonstration">Month</span>
+    <el-date-picker
+      v-model="value4"
+      type="month"
+      placeholder="Pick a month">
+    </el-date-picker>
+  </div>
 </div>
-<div class="block">
-  <span class="demonstration">Year</span>
-  <el-date-picker
-    v-model="value5"
-    type="year"
-    placeholder="Pick a year">
-  </el-date-picker>
+<div class="container">
+  <div class="block">
+    <span class="demonstration">Year</span>
+    <el-date-picker
+      v-model="value5"
+      type="year"
+      placeholder="Pick a year">
+    </el-date-picker>
+  </div>
+  <div class="block">
+    <span class="demonstration">Dates</span>
+    <el-date-picker
+      type="dates"
+      v-model="value14"
+      placeholder="Pick one or more dates">
+    </el-date-picker>
+  </div>
 </div>
 
 <script>
@@ -206,7 +233,8 @@ Puede seleccionar la semana, el mes o el año extendiendo el componente date pic
       return {
         value3: '',
         value4: '',
-        value5: ''
+        value5: '',
+        value14: []
       };
     }
   };
@@ -457,7 +485,7 @@ Al seleccionar un intervalo de fechas, puede asignar la hora para la fecha de in
 | placeholder       | placeholder cuando el modo NO es rango   | string            | —                                        | —                    |
 | start-placeholder | placeholder para la fecha de inicio en modo rango | string            | —                                        | —                    |
 | end-placeholder   | placeholder para la fecha final en modo rango | string            | —                                        | —                    |
-| type              | tipo de picker                           | string            | year/month/date/datetime/ week/datetimerange/daterange | date                 |
+| type              | tipo de picker                           | string            | year/month/date/dates/datetime/ week/datetimerange/daterange | date                 |
 | format            | formato en que se muestra el valor en el input | string            | ver [date formats](#/es/component/date-picker#date-formats) | yyyy-MM-dd           |
 | align             | alineación                               | left/center/right | left                                     |                      |
 | popper-class      | nombre de clase personalizada para el dropdown de DatePicker | string            | —                                        | —                    |

+ 57 - 28
examples/docs/zh-CN/date-picker.md

@@ -66,7 +66,8 @@
         value10: '',
         value11: '',
         value12: '',
-        value13: []
+        value13: [],
+        value14: []
       };
     }
   };
@@ -76,6 +77,7 @@
   .demo-block.demo-date-picker .source {
     padding: 0;
     display: flex;
+    flex-wrap: wrap;
   }
 
   .demo-date-picker .block {
@@ -87,6 +89,20 @@
       border-right: none;
     }
   }
+  
+  .demo-date-picker .container {
+    flex: 1;
+    border-right: solid 1px #EFF2F6;
+    .block {
+      border-right: none;
+      &:last-child {
+        border-top: solid 1px #EFF2F6;
+      }
+    }
+    &:last-child {
+      border-right: none;
+    }
+  }
 
   .demo-date-picker .demonstration {
     display: block;
@@ -167,35 +183,46 @@
 
 ###  其他日期单位
 
-通过扩展基础的日期选择,可以选择周、月、年
+通过扩展基础的日期选择,可以选择周、月、年或多个日期
 
 :::demo
 ```html
-<div class="block">
-  <span class="demonstration">周</span>
-  <el-date-picker
-    v-model="value3"
-    type="week"
-    format="yyyy 第 WW 周"
-    placeholder="选择周">
-  </el-date-picker>
-</div>
-<div class="block">
-  <span class="demonstration">月</span>
-  <el-date-picker
-    v-model="value4"
-    type="month"
-    placeholder="选择月">
-  </el-date-picker>
+<div class="container">
+  <div class="block">
+    <span class="demonstration">周</span>
+    <el-date-picker
+      v-model="value3"
+      type="week"
+      format="yyyy 第 WW 周"
+      placeholder="选择周">
+    </el-date-picker>
+  </div>
+  <div class="block">
+    <span class="demonstration">月</span>
+    <el-date-picker
+      v-model="value4"
+      type="month"
+      placeholder="选择月">
+    </el-date-picker>
+  </div>
 </div>
-<div class="block">
-  <span class="demonstration">年</span>
-  <el-date-picker
-    v-model="value5"
-    align="right"
-    type="year"
-    placeholder="选择年">
-  </el-date-picker>
+<div class="container">
+  <div class="block">
+    <span class="demonstration">年</span>
+    <el-date-picker
+      v-model="value5"
+      type="year"
+      placeholder="选择年">
+    </el-date-picker>
+  </div>
+  <div class="block">
+    <span class="demonstration">多个日期</span>
+    <el-date-picker
+      type="dates"
+      v-model="value14"
+      placeholder="选择一个或多个日期">
+    </el-date-picker>
+  </div>
 </div>
 
 <script>
@@ -204,7 +231,8 @@
       return {
         value3: '',
         value4: '',
-        value5: ''
+        value5: '',
+        value14: []
       };
     }
   };
@@ -221,6 +249,7 @@
 <template>
   <div class="block">
     <span class="demonstration">默认</span>
+    {{value6}}
     <el-date-picker
       v-model="value6"
       type="daterange"
@@ -408,7 +437,7 @@
 | placeholder | 非范围选择时的占位内容 | string | — | — |
 | start-placeholder | 范围选择时开始日期的占位内容 | string | — | — |
 | end-placeholder | 范围选择时结束日期的占位内容 | string | — | — |
-| type | 显示类型 | string | year/month/date/week/ datetime/datetimerange/daterange | date |
+| type | 显示类型 | string | year/month/date/dates/ week/datetime/datetimerange/daterange | date |
 | format | 显示在输入框中的格式 | string | 见[日期格式](#/zh-CN/component/date-picker#ri-qi-ge-shi) | yyyy-MM-dd |
 | align | 对齐方式 | string | left, center, right | left |
 | popper-class | DatePicker 下拉框的类名 | string | — | — |

+ 27 - 1
packages/date-picker/src/basic/date-table.vue

@@ -73,6 +73,10 @@
 
       disabledDate: {},
 
+      selectedDate: {
+        type: Array
+      },
+
       minDate: {},
 
       maxDate: {},
@@ -129,6 +133,7 @@
 
         const startDate = this.startDate;
         const disabledDate = this.disabledDate;
+        const selectedDate = this.selectedDate || this.value;
         const now = clearHours(new Date());
 
         for (let i = 0; i < 6; i++) {
@@ -181,7 +186,10 @@
               }
             }
 
-            cell.disabled = typeof disabledDate === 'function' && disabledDate(new Date(time));
+            let newDate = new Date(time);
+            cell.disabled = typeof disabledDate === 'function' && disabledDate(newDate);
+            cell.selected = Array.isArray(selectedDate) &&
+              selectedDate.filter(date => date.toString() === newDate.toString())[0];
 
             this.$set(row, this.showWeekNumber ? j + 1 : j, cell);
           }
@@ -285,6 +293,10 @@
           classes.push('disabled');
         }
 
+        if (cell.selected) {
+          classes.push('selected');
+        }
+
         return classes.join(' ');
       },
 
@@ -472,6 +484,20 @@
             value: value,
             date: newDate
           });
+        } else if (selectionMode === 'dates') {
+          let selectedDate = this.selectedDate;
+
+          if (!cell.selected) {
+            selectedDate.push(newDate);
+          } else {
+            selectedDate.forEach((date, index) => {
+              if (date.toString() === newDate.toString()) {
+                selectedDate.splice(index, 1);
+              }
+            });
+          }
+
+          this.$emit('select', selectedDate);
         }
       }
     }

+ 24 - 5
packages/date-picker/src/panel/date.vue

@@ -90,12 +90,14 @@
             <date-table
               v-show="currentView === 'date'"
               @pick="handleDatePick"
+              @select="handleDateSelect"
               :selection-mode="selectionMode"
               :first-day-of-week="firstDayOfWeek"
               :value="new Date(value)"
               :default-value="defaultValue ? new Date(defaultValue) : null"
               :date="date"
-              :disabled-date="disabledDate">
+              :disabled-date="disabledDate"
+              :selected-date="selectedDate">
             </date-table>
             <year-table
               v-show="currentView === 'year'"
@@ -124,7 +126,8 @@
           size="mini"
           type="text"
           class="el-picker-panel__link-btn"
-          @click="changeToNow">
+          @click="changeToNow"
+          v-show="selectionMode !== 'dates'">
           {{ t('el.datepicker.now') }}
         </el-button>
         <el-button
@@ -208,6 +211,8 @@
           if (this.currentView !== 'year' || this.currentView !== 'month') {
             this.currentView = 'month';
           }
+        } else if (newVal === 'dates') {
+          this.currentView = 'date';
         }
       }
     },
@@ -234,6 +239,9 @@
       emit(value, ...args) {
         if (!value) {
           this.$emit('pick', value, ...args);
+        } else if (Array.isArray(value)) {
+          const dates = value.map(date => this.showTime ? clearMilliseconds(date) : clearTime(date));
+          this.$emit('pick', dates, ...args);
         } else {
           this.$emit('pick', this.showTime ? clearMilliseconds(value) : clearTime(value), ...args);
         }
@@ -317,6 +325,12 @@
         }
       },
 
+      handleDateSelect(value) {
+        if (this.selectionMode === 'dates') {
+          this.selectedDate = value;
+        }
+      },
+
       handleDatePick(value) {
         if (this.selectionMode === 'day') {
           this.date = this.value ? modifyDate(this.date, value.getFullYear(), value.getMonth(), value.getDate()) : modifyWithDefaultTime(value, this.defaultTime);
@@ -348,8 +362,12 @@
       },
 
       confirm() {
-        const date = this.value ? this.date : modifyWithDefaultTime(this.date, this.defaultTime);
-        this.emit(date);
+        if (this.selectionMode === 'dates') {
+          this.emit(this.selectedDate);
+        } else {
+          const date = this.value ? this.date : modifyWithDefaultTime(this.date, this.defaultTime);
+          this.emit(date);
+        }
       },
 
       resetView() {
@@ -467,6 +485,7 @@
         visible: false,
         currentView: 'date',
         disabledDate: '',
+        selectedDate: [],
         firstDayOfWeek: 7,
         showWeekNumber: false,
         timePickerVisible: false,
@@ -495,7 +514,7 @@
       },
 
       footerVisible() {
-        return this.showTime;
+        return this.showTime || this.selectionMode === 'dates';
       },
 
       visibleTime() {

+ 40 - 7
packages/date-picker/src/picker.vue

@@ -2,7 +2,7 @@
   <el-input
     class="el-date-editor"
     :class="'el-date-editor--' + type"
-    :readonly="!editable || readonly"
+    :readonly="!editable || readonly || type === 'dates'"
     :disabled="pickerDisabled"
     :size="pickerSize"
     :name="name"
@@ -123,7 +123,8 @@ const HAVE_TRIGGER_TYPES = [
   'year',
   'daterange',
   'timerange',
-  'datetimerange'
+  'datetimerange',
+  'dates'
 ];
 const DATE_FORMATTER = function(value, format) {
   if (format === 'timestamp') return value.getTime();
@@ -242,6 +243,15 @@ const TYPE_VALUE_RESOLVER_MAP = {
         return null;
       }
     }
+  },
+  dates: {
+    formatter(value, format) {
+      return value.map(date => DATE_FORMATTER(date, format));
+    },
+    parser(value, format) {
+      return (typeof value === 'string' ? value.split(', ') : value)
+        .map(date => date instanceof Date ? date : DATE_PARSER(date, format));
+    }
   }
 };
 const PLACEMENT_MAP = {
@@ -275,8 +285,10 @@ const valueEquals = function(a, b) {
   const aIsArray = a instanceof Array;
   const bIsArray = b instanceof Array;
   if (aIsArray && bIsArray) {
-    return new Date(a[0]).getTime() === new Date(b[0]).getTime() &&
-           new Date(a[1]).getTime() === new Date(b[1]).getTime();
+    if (a.length !== b.length) {
+      return false;
+    }
+    return a.every((item, index) => new Date(item).getTime() === new Date(b[index]).getTime());
   }
   if (!aIsArray && !bIsArray) {
     return new Date(a).getTime() === new Date(b).getTime();
@@ -374,7 +386,7 @@ export default {
       if (this.readonly || this.pickerDisabled) return;
       if (val) {
         this.showPicker();
-        this.valueOnOpen = this.value;
+        this.valueOnOpen = Array.isArray(this.value) ? [...this.value] : this.value;
       } else {
         this.hidePicker();
         this.emitChange(this.value);
@@ -394,6 +406,7 @@ export default {
       handler(val) {
         if (this.picker) {
           this.picker.value = val;
+          this.picker.selectedDate = Array.isArray(val) ? val : [];
         }
       }
     },
@@ -449,6 +462,8 @@ export default {
         return 'month';
       } else if (this.type === 'year') {
         return 'year';
+      } else if (this.type === 'dates') {
+        return 'dates';
       }
 
       return 'day';
@@ -468,8 +483,14 @@ export default {
           this.userInput[0] || (formattedValue && formattedValue[0]) || '',
           this.userInput[1] || (formattedValue && formattedValue[1]) || ''
         ];
+      } else if (this.userInput !== null) {
+        return this.userInput;
+      } else if (formattedValue) {
+        return this.type === 'dates'
+          ? formattedValue.join(', ')
+          : formattedValue;
       } else {
-        return this.userInput !== null ? this.userInput : formattedValue || '';
+        return '';
       }
     },
 
@@ -655,7 +676,18 @@ export default {
     },
 
     handleClose() {
+      if (!this.pickerVisible) return;
       this.pickerVisible = false;
+      const {
+        type,
+        valueOnOpen,
+        valueFormat,
+        rangeSeparator
+      } = this;
+      if (type === 'dates' && this.picker) {
+        this.picker.selectedDate = parseAsFormatAndType(valueOnOpen, valueFormat, type, rangeSeparator) || valueOnOpen;
+        this.emitInput(this.picker.selectedDate);
+      }
     },
 
     handleFieldReset(initialValue) {
@@ -769,6 +801,7 @@ export default {
       this.picker.selectionMode = this.selectionMode;
       this.picker.unlinkPanels = this.unlinkPanels;
       this.picker.arrowControl = this.arrowControl || this.timeArrowControl || false;
+      this.picker.selectedDate = Array.isArray(this.value) && this.value || [];
       this.$watch('format', (format) => {
         this.picker.format = format;
       });
@@ -846,7 +879,7 @@ export default {
 
     emitInput(val) {
       const formatted = this.formatToValue(val);
-      if (!valueEquals(this.value, formatted)) {
+      if (!valueEquals(this.value, formatted) || this.type === 'dates') {
         this.$emit('input', formatted);
       }
     },

+ 16 - 0
packages/theme-chalk/src/date-picker/date-table.scss

@@ -120,6 +120,22 @@
       color: $--color-text-placeholder;
     }
 
+    &.selected div {
+      margin-left: 5px;
+      margin-right: 5px;
+      background-color: $--datepicker-inrange-color;
+      border-radius: 15px;
+      &:hover {
+        background-color: $--datepicker-inrange-hover-color;
+      }
+    }
+
+    &.selected span {
+      background-color: $--datepicker-active-color;
+      color: $--color-white;
+      border-radius: 15px;
+    }
+
     &.week {
       font-size: 80%;
       color: $--datepicker-header-color;

+ 7 - 0
packages/theme-chalk/src/date-picker/picker.scss

@@ -26,6 +26,13 @@
     }
   }
 
+  @include m(dates) {
+    .el-input__inner {
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+  }
+
   .el-icon-circle-close {
     cursor: pointer;
   }

+ 57 - 0
test/unit/specs/date-picker.spec.js

@@ -1400,6 +1400,63 @@ describe('DatePicker', () => {
     });
   });
 
+  describe('type:dates', () => {
+    let vm;
+
+    beforeEach(done => {
+      vm = createVue({
+        template: '<el-date-picker type="dates" value-format="timestamp" v-model="value" ref="compo" />',
+        data() {
+          return {
+            value: []
+          };
+        }
+      }, true);
+      const input = vm.$el.querySelector('input');
+
+      input.blur();
+      input.focus();
+      setTimeout(done, DELAY);
+    });
+
+    afterEach(() => destroyVM(vm));
+
+    it('click cell', done => {
+      const td = vm.$refs.compo.picker.$el.querySelector('.el-date-table__row .available');
+      td.click();
+      setTimeout(_ => {
+        expect(vm.$refs.compo.picker.selectedDate).to.exist;
+        expect(vm.value.length).to.equal(1);
+        done();
+      }, DELAY);
+    });
+
+    it('value format', done => {
+      const td = vm.$refs.compo.picker.$el.querySelector('.el-date-table__row .available');
+      td.click();
+      setTimeout(_ => {
+        vm.$refs.compo.picker.$el.querySelector('.el-button--default').click();
+        setTimeout(() => {
+          expect(vm.$refs.compo.picker.selectedDate).to.exist;
+          expect(vm.value.length).to.equal(1);
+          done();
+        }, DELAY);
+      }, DELAY);
+    });
+
+    it('restore value when cancel', done => {
+      const td = vm.$refs.compo.picker.$el.querySelector('.el-date-table__row .available');
+      td.click();
+      setTimeout(_ => {
+        vm.$refs.compo.handleClose();
+        setTimeout(() => {
+          expect(vm.value.length).to.equal(0);
+          done();
+        }, DELAY);
+      }, DELAY);
+    });
+  });
+
   describe('type:daterange', () => {
     it('works', done => {
       vm = createVue({

+ 1 - 1
types/date-picker.d.ts

@@ -1,6 +1,6 @@
 import { ElementUIComponent, ElementUIComponentSize, ElementUIHorizontalAlignment } from './component'
 
-export type DatePickerType = 'year' | 'month' | 'date' | 'datetime' | 'week' | 'datetimerange' | 'daterange'
+export type DatePickerType = 'year' | 'month' | 'date' | 'datetime' | 'week' | 'datetimerange' | 'daterange' | 'dates'
 export type FirstDayOfWeek = 1 | 2 | 3 | 4 | 5 | 6 | 7
 
 export interface DisabledDateChecker {