Browse Source

feat: calendar组件支持弹出和平铺

songchenglin3 5 years ago
parent
commit
75c02f92fd

+ 23 - 31
src/packages/calendar/demo.vue

@@ -6,20 +6,18 @@
         :showIcon="true"
         title="选择单个日期"
         :desc="date ? `${date} ${dateWeek}` : '请选择'"
-        @click.native="openSwitch('isVisible')"
+        @click="openSwitch('isVisible')"
       >
-      </nut-cell
-      ><div class="test-calendar-wrapper">
-        <nut-calendar
-          :is-visible="isVisible"
-          :default-value="date"
-          @close="closeSwitch('isVisible')"
-          @choose="setChooseValue"
-          :start-date="`2019-10-11`"
-          :end-date="`2022-11-11`"
-        >
-        </nut-calendar
-      ></div>
+      </nut-cell>
+      <nut-calendar
+        :is-visible="isVisible"
+        :default-value="date"
+        @close="closeSwitch('isVisible')"
+        @choose="setChooseValue"
+        :start-date="`2019-10-11`"
+        :end-date="`2022-11-11`"
+      >
+      </nut-calendar>
     </div>
 
     <div>
@@ -27,21 +25,19 @@
         :showIcon="true"
         title="选择日期区间"
         :desc="date1 ? `${date1[0]}至${date1[1]}` : '请选择'"
-        @click.native="openSwitch('isVisible1')"
+        @click="openSwitch('isVisible1')"
       >
-      </nut-cell
-      ><div class="test-calendar-wrapper">
-        <nut-calendar
-          :is-visible="isVisible1"
-          :default-value="date1"
-          type="range"
-          :start-date="`2019-12-22`"
-          :end-date="`2021-01-08`"
-          @close="closeSwitch('isVisible1')"
-          @choose="setChooseValue1"
-        >
-        </nut-calendar
-      ></div>
+      </nut-cell>
+      <nut-calendar
+        :is-visible="isVisible1"
+        :default-value="date1"
+        type="range"
+        :start-date="`2019-12-22`"
+        :end-date="`2021-01-08`"
+        @close="closeSwitch('isVisible1')"
+        @choose="setChooseValue1"
+      >
+      </nut-calendar>
     </div>
     <h2>自定义日历</h2>
     <h2>平铺展示</h2>
@@ -111,10 +107,6 @@ export default createDemo({
       state.date1 = [...[param[0][3], param[1][3]]];
     };
 
-    setTimeout(() => {
-      state.date = '2021-01-03';
-    }, 3000);
-
     return {
       ...toRefs(state),
       openSwitch,

+ 70 - 566
src/packages/calendar/index.vue

@@ -1,120 +1,49 @@
 <template>
-  <view class="nut-calendar" :class="{ 'nut-calendar-tile': !poppable }">
-    <!-- header -->
-    <view
-      class="nut-calendar-header"
-      :class="{ 'nut-calendar-header-tile': !poppable }"
+  <nut-popup
+    v-model:show="state.childIsVisible"
+    position="bottom"
+    round
+    @click="closeActionSheet"
+    v-if="poppable"
+  >
+    <nut-calendar-item
+      props
+      ref="mychild"
+      :type="type"
+      :is-auto-back-fill="isAutoBackFill"
+      :poppable="poppable"
+      :title="title"
+      :default-value="defaultValue"
+      :start-date="startDate"
+      :end-date="endDate"
+      @update="update"
+      @close="close"
+      @choose="choose"
     >
-      <template v-if="poppable">
-        <view class="calendar-title">{{ title }}</view>
-        <view class="calendar-curr-month">2020年11月</view>
-      </template>
-      <view class="calendar-weeks">
-        <view
-          class="calendar-week-item"
-          v-for="(item, index) of weeks"
-          :key="index"
-          >{{ item }}</view
-        >
-      </view>
-    </view>
-    <!-- content-->
-    <view
-      class="nut-calendar-content"
-      ref="months"
-      @touchstart.stop="touchStart"
-      @touchmove.stop.prevent="touchMove"
-      @touchend.stop="touchEnd"
-    >
-      <view class="calendar-months-panel" ref="monthsPanel">
-        <view class="calendar-loading-tip">{{
-          !unLoadPrev ? '加载上一个月' : '没有更早月份'
-        }}</view>
-        <view
-          class="calendar-month"
-          v-for="(month, index) of monthsData"
-          :key="index"
-        >
-          <view class="calendar-month-title">{{ month.title }}</view>
-          <view class="calendar-month-con">
-            <view
-              class="calendar-month-item"
-              :class="type === 'range' ? 'month-item-range' : ''"
-            >
-              <template v-for="(day, i) of month.monthData" :key="i">
-                <view
-                  class="calendar-month-day"
-                  :class="getClass(day, month)"
-                  @click="chooseDay(day, month)"
-                >
-                  <view class="calendar-day">{{
-                    day.type == 'curr' ? day.day : ''
-                  }}</view>
-                  <view class="curr-tips" v-if="isCurrDay(month, day.day)"
-                    >今天</view
-                  >
-                  <view
-                    class="calendar-day-tip"
-                    v-if="isStartTip(day, month)"
-                    >{{ '开始' }}</view
-                  >
-                  <view
-                    class="calendar-day-tip"
-                    v-else-if="isEndTip(day, month)"
-                    >{{ '结束' }}</view
-                  >
-                </view>
-              </template>
-            </view>
-          </view>
-        </view>
-      </view>
-    </view>
-    <!-- footer-->
-    <view class="nut-calendar-footer" v-if="poppable">
-      <view class="calendar-confirm-btn" @click="confirm">确定</view>
-    </view>
-  </view>
+    </nut-calendar-item>
+  </nut-popup>
+  <nut-calendar-item
+    v-else
+    :type="type"
+    :is-auto-back-fill="isAutoBackFill"
+    :poppable="poppable"
+    :title="title"
+    :default-value="defaultValue"
+    :start-date="startDate"
+    :end-date="endDate"
+    @close="close"
+    @choose="choose"
+  >
+  </nut-calendar-item>
 </template>
 <script lang="ts">
-// import {
-//   PropType,
-//   computed,
-//   watch,
-//   reactive,
-//   ref,
-//   toRefs,
-//   readonly
-// } from 'vue';
-import {
-  PropType,
-  computed,
-  reactive,
-  ref,
-  watch,
-  toRefs,
-  onMounted
-} from 'vue';
+import { PropType, reactive, ref, watch, toRefs } from 'vue';
 import { createComponent } from '@/utils/create';
 const { componentName, create } = createComponent('calendar');
+import Popup from '@/packages/popup/index.vue';
+import CalendarItem from '@/packages/calendar-item/index.vue';
 import Utils from '@/utils/date';
 type InputDate = string | string[];
-interface CalendarState {
-  childIsVisible: boolean;
-  currDate: InputDate;
-  unLoadPrev: boolean;
-  touchParams: any;
-  transformY: number;
-  translateY: number;
-  scrollDistance: number;
-  defaultData: InputDate;
-  chooseData: any;
-  monthsData: any[];
-  dayPrefix: string;
-  startData: InputDate;
-  endData: InputDate;
-  isRange: boolean;
-}
 
 export default create({
   props: {
@@ -130,13 +59,15 @@ export default create({
       type: Boolean,
       default: true
     },
+    isVisible: {
+      type: Boolean
+    },
     title: {
       type: String,
       default: '日历选择'
     },
     defaultValue: {
-      type: String as PropType<InputDate>,
-      default: null
+      type: String as PropType<InputDate>
     },
     startDate: {
       type: String,
@@ -148,480 +79,51 @@ export default create({
     }
   },
   components: {},
-  emits: ['choose', 'update', 'close'],
+  emits: ['choose', 'close'],
 
-  setup(props, { emit, slots }) {
-    const weeks = ref(['日', '一', '二', '三', '四', '五', '六']);
+  setup(props, { emit }) {
     // element refs
-    const months = ref<null | HTMLElement>(null);
-    const monthsPanel = ref<null | HTMLElement>(null);
+    const mychild = ref<null | HTMLElement>(null);
+
     // state
-    let state: CalendarState = reactive({
-      childIsVisible: false,
-      currDate: '',
-      unLoadPrev: false,
-      touchParams: {
-        startY: 0,
-        endY: 0,
-        startTime: 0,
-        endTime: 0,
-        lastY: 0,
-        lastTime: 0
-      },
-      transformY: 0,
-      translateY: 0,
-      scrollDistance: 0,
-      defaultData: [],
-      chooseData: [],
-      monthsData: [],
-      dayPrefix: 'calendar-month-day',
-      startData: '',
-      endData: '',
-      isRange: props.type === 'range'
+    const state = reactive({
+      childIsVisible: false
     });
 
-    // 日期转化成数组
-    const splitDate = date => {
-      return date.split('-');
-    };
-
-    const isStart = currDate => {
-      return Utils.isEqual(state.currDate[0], currDate);
-    };
-
-    const isEnd = currDate => {
-      return Utils.isEqual(state.currDate[1], currDate);
-    };
-
-    // 获取当前数据
-    const getCurrDate = (day, month, isRange?) => {
-      return isRange
-        ? month.curData[3] +
-            '-' +
-            month.curData[4] +
-            '-' +
-            Utils.getNumTwoBit(day.day)
-        : month.curData[0] +
-            '-' +
-            month.curData[1] +
-            '-' +
-            Utils.getNumTwoBit(day.day);
-    };
-
-    // 获取样式
-    const getClass = (day, month, isRange?) => {
-      let currDate = getCurrDate(day, month, isRange);
-      if (day.type == 'curr') {
-        if (
-          (!state.isRange && Utils.isEqual(state.currDate, currDate)) ||
-          (state.isRange && (isStart(currDate) || isEnd(currDate)))
-        ) {
-          return `${state.dayPrefix}-active`;
-        } else if (
-          (props.startDate && Utils.compareDate(currDate, props.startDate)) ||
-          (props.endDate && Utils.compareDate(props.endDate, currDate))
-        ) {
-          return `${state.dayPrefix}-disabled`;
-        } else if (
-          state.isRange &&
-          Array.isArray(state.currDate) &&
-          Object.values(state.currDate).length == 2 &&
-          Utils.compareDate(state.currDate[0], currDate) &&
-          Utils.compareDate(currDate, state.currDate[1])
-        ) {
-          return `${state.dayPrefix}-choose`;
-        } else {
-          return null;
-        }
-      } else {
-        return `${state.dayPrefix}-disabled`;
-      }
-    };
-
-    // 选中数据
-    const chooseDay = (day, month, isFirst, isRange?) => {
-      if (getClass(day, month, isRange) != `${state.dayPrefix}-disabled`) {
-        let days = [...month.curData];
-        days = isRange ? days.splice(3) : days.splice(0, 3);
-        days[2] =
-          typeof day.day == 'number' ? Utils.getNumTwoBit(day.day) : day.day;
-        days[3] = `${days[0]}-${days[1]}-${days[2]}`;
-        days[4] = Utils.getWhatDay(days[0], days[1], days[2]);
-        if (!state.isRange) {
-          state.currDate = days[3];
-          state.chooseData = [...days];
-        } else {
-          if (Object.values(state.currDate).length == 2) {
-            state.currDate = [days[3]];
-          } else {
-            if (Utils.compareDate(state.currDate[0], days[3])) {
-              Array.isArray(state.currDate) && state.currDate.push(days[3]);
-            } else {
-              Array.isArray(state.currDate) && state.currDate.unshift(days[3]);
-            }
-          }
-          if (state.chooseData.length == 2 || !state.chooseData.length) {
-            state.chooseData = [...days];
-          } else {
-            if (Utils.compareDate(state.chooseData[3], days[3])) {
-              state.chooseData = [[...state.chooseData], [...days]];
-            } else {
-              state.chooseData = [[...days], [...state.chooseData]];
-            }
-          }
-        }
-
-        if (props.isAutoBackFill && !isFirst) {
-          confirm();
-        }
-      }
-    };
-
-    // 获取当前月数据
-    const getCurrData = type => {
-      let monthData =
-        type == 'prev'
-          ? state.monthsData[0]
-          : state.monthsData[state.monthsData.length - 1];
-      let year = parseInt(monthData.curData[0]);
-      let month = parseInt(monthData.curData[1].toString().replace(/^0/, ''));
-      switch (type) {
-        case 'prev':
-          month == 1 && (year -= 1);
-          month = month == 1 ? 12 : --month;
-          break;
-        case 'next':
-          month == 12 && (year += 1);
-          month = month == 12 ? 1 : ++month;
-          break;
-      }
-      return [year, Utils.getNumTwoBit(month), monthData.curData[2]];
-    };
-
-    // 获取日期状态
-    const getDaysStatus = (days, type) => {
-      // 修复:当某个月的1号是周日时,月份下方会空出来一行
-      if (type == 'prev' && days >= 7) {
-        days -= 7;
-      }
-      return Array.from(Array(days), (v, k) => {
-        return {
-          day: k + 1,
-          type: type
-        };
-      });
-    };
-
-    // 获取月数据
-    const getMonth = (curData, type) => {
-      const preMonthDays = Utils.getMonthPreDay(curData[0], curData[1]);
-      const currMonthDays = Utils.getMonthDays(curData[0], curData[1]);
-      const nextMonthDays = 42 - preMonthDays - currMonthDays;
-      const title = {
-        year: curData[0],
-        month: curData[1]
-      };
-      const monthInfo = {
-        curData: curData,
-        title: `${title.year}年${title.month}月`,
-        monthData: [
-          ...getDaysStatus(preMonthDays, 'prev'),
-          ...getDaysStatus(currMonthDays, 'curr')
-        ]
-      };
-      if (type == 'next') {
-        if (
-          !state.endData ||
-          !Utils.compareDate(
-            `${state.endData[0]}-${state.endData[1]}-${Utils.getMonthDays(
-              state.endData[0],
-              state.endData[1]
-            )}`,
-            `${curData[0]}-${curData[1]}-${curData[2]}`
-          )
-        ) {
-          state.monthsData.push(monthInfo);
-        }
-      } else {
-        if (
-          !state.startData ||
-          !Utils.compareDate(
-            `${curData[0]}-${curData[1]}-${curData[2]}`,
-            `${state.startData[0]}-${state.startData[1]}-01`
-          )
-        ) {
-          state.monthsData.unshift(monthInfo);
-        } else {
-          state.unLoadPrev = true;
-        }
-      }
-    };
-
-    // 初始化数据
-    const initData = () => {
-      // 初始化开始结束数据
-      state.startData = props.startDate ? splitDate(props.startDate) : null;
-      state.endData = props.endDate ? splitDate(props.endDate) : null;
-
-      // 初始化当前日期
-      if (!props.defaultValue) {
-        state.currDate = state.isRange
-          ? [Utils.date2Str(new Date()), Utils.getDay(1)]
-          : Utils.date2Str(new Date());
-      } else {
-        state.currDate = state.isRange
-          ? [...props.defaultValue]
-          : props.defaultValue;
-      }
-
-      // 日期转化为数组
-      if (state.isRange && Array.isArray(state.currDate)) {
-        if (
-          props.startDate &&
-          Utils.compareDate(state.currDate[0], props.startDate)
-        ) {
-          state.currDate.splice(0, 1, props.startDate);
-        }
-        if (
-          props.endDate &&
-          Utils.compareDate(props.endDate, state.currDate[1])
-        ) {
-          state.currDate.splice(1, 1, props.endDate);
-        }
-        state.defaultData = [
-          ...splitDate(state.currDate[0]),
-          ...splitDate(state.currDate[1])
-        ];
-      } else {
-        if (
-          props.startDate &&
-          Utils.compareDate(state.currDate, props.startDate)
-        ) {
-          state.currDate = props.startDate;
-        } else if (
-          props.endDate &&
-          !Utils.compareDate(state.currDate, props.endDate)
-        ) {
-          state.currDate = props.endDate;
-        }
-
-        state.defaultData = [...splitDate(state.currDate)];
-      }
-
-      getMonth(state.defaultData, 'next');
-
-      let i = 1;
-      do {
-        getMonth(getCurrData('next'), 'next');
-      } while (i++ < 4);
-
-      if (state.isRange) {
-        chooseDay(
-          { day: state.defaultData[2], type: 'curr' },
-          state.monthsData[0],
-          true
-        );
-        chooseDay(
-          { day: state.defaultData[5], type: 'curr' },
-          state.monthsData[0],
-          true,
-          true
-        );
-      } else {
-        chooseDay(
-          { day: state.defaultData[2], type: 'curr' },
-          state.monthsData[0],
-          true
-        );
-      }
-      console.log(state.currDate, 'state.currDate');
-    };
-
-    // 区间选择&&当前月&&选中态
-    const isActive = (day, month) => {
-      return (
-        state.isRange &&
-        day.type == 'curr' &&
-        getClass(day, month) == 'calendar-month-day-active'
-      );
-    };
-
-    const isStartTip = (day, month) => {
-      if (isActive(day, month)) {
-        return isStart(getCurrDate(day, month));
-      } else {
-        return false;
-      }
-    };
-
-    const isEndTip = (day, month) => {
-      return isActive(day, month);
-    };
-
-    const isCurrDay = (month, day) => {
-      let date = `${month.curData[0]}-${month.curData[1]}-${day}`;
-      return Utils.isEqual(date, Utils.date2Str(new Date()));
-    };
-
-    const confirm = () => {
-      if ((state.isRange && state.chooseData.length == 2) || !state.isRange) {
-        emit('choose', state.chooseData);
-        if (props.poppable) {
-          state.childIsVisible = false;
-          emit('update');
-        }
-      }
-    };
-
-    const touchStart = event => {
-      let changedTouches = event.changedTouches[0];
-      state.touchParams.startY = changedTouches.pageY;
-      state.touchParams.startTime = event.timestamp || Date.now();
-      state.transformY = state.scrollDistance;
-    };
-
-    const touchMove = event => {
-      //event.preventDefault();
-      let changedTouches = event.changedTouches[0];
-      state.touchParams.lastY = changedTouches.pageY;
-      state.touchParams.lastTime = event.timestamp || Date.now();
-      let move = state.touchParams.lastY - state.touchParams.startY;
-      if (Math.abs(move) < 5) {
-        return false;
-      }
-      setMove(move);
-    };
-
-    const touchEnd = event => {
-      let changedTouches = event.changedTouches[0];
-      state.touchParams.lastY = changedTouches.pageY;
-      state.touchParams.lastTime = event.timestamp || Date.now();
-      let move = state.touchParams.lastY - state.touchParams.startY;
-      if (Math.abs(move) < 5) {
-        return false;
-      }
-      let updateMove = move + state.transformY;
-      let h = months.value?.offsetHeight || 0;
-      let offsetHeight = monthsPanel.value?.offsetHeight || 0;
-
-      if (updateMove > 0) {
-        getMonth(getCurrData('prev'), 'prev');
-      } else if (updateMove < -offsetHeight + h * 2) {
-        getMonth(getCurrData('next'), 'next');
-        if (Math.abs(move) >= 300) {
-          getMonth(getCurrData('next'), 'next');
-        }
-      }
-
-      let moveTime = state.touchParams.lastTime - state.touchParams.startTime;
-      if (moveTime <= 300) {
-        move = move * 2;
-        moveTime = moveTime + 1000;
-        setMove(move, 'end', moveTime);
-      } else {
-        setMove(move, 'end');
-      }
-    };
-
-    const setTransform = (translateY = 0, type?, time = 1000) => {
-      if (type === 'end') {
-        monthsPanel?.value &&
-          (monthsPanel.value.style.webkitTransition = `transform ${time}ms cubic-bezier(0.19, 1, 0.22, 1)`);
-      } else {
-        monthsPanel?.value && (monthsPanel.value.style.webkitTransition = '');
-      }
-
-      monthsPanel?.value &&
-        (monthsPanel.value.style.webkitTransform = `translateY(${translateY}px)`);
-      state.scrollDistance = translateY;
+    // methods
+    const closeActionSheet = () => {
+      //mychild.value && mychild.value.closeActionSheet();
     };
 
-    const setMove = (move, type?, time?) => {
-      let updateMove = move + state.transformY;
-      let h = months.value?.offsetHeight || 0;
-
-      let offsetHeight = monthsPanel.value?.offsetHeight || 0;
-
-      if (type === 'end') {
-        // 限定滚动距离
-        if (updateMove > 0) {
-          updateMove = 0;
-        }
-        if (updateMove < 0 && updateMove < -offsetHeight + h) {
-          updateMove = -offsetHeight + h;
-        }
-        if (offsetHeight <= h && state.monthsData.length == 1) {
-          updateMove = 0;
-        }
-        let endMove = updateMove;
-        setTransform(endMove, type, time);
-      } else {
-        if (updateMove > 0 && updateMove > 100) {
-          updateMove = 100;
-        }
-        if (
-          updateMove < -offsetHeight + h - 100 &&
-          state.monthsData.length > 1
-        ) {
-          updateMove = -offsetHeight + h - 100;
-        }
-        if (
-          updateMove < 0 &&
-          updateMove < -100 &&
-          state.monthsData.length == 1
-        ) {
-          updateMove = -100;
-        }
-        setTransform(updateMove);
-      }
+    const update = () => {
+      state.childIsVisible = false;
     };
 
-    const resetRender = () => {
-      state.chooseData.splice(0);
-      state.monthsData.splice(0);
-      state.scrollDistance = 0;
-      state.translateY = 0;
-      setTransform(state.scrollDistance);
-      initData();
+    const close = () => {
+      emit('close');
     };
 
-    const closeActionSheet = () => {
-      if (props.poppable) {
-        state.childIsVisible = false;
-        emit('update');
-        emit('close');
-      }
-      resetRender();
+    const choose = param => {
+      close();
+      emit('choose', param);
     };
 
-    // 初始化
-    initData();
-
-    //监听 默认值更改
     watch(
-      () => props.defaultValue,
-      (val, prevVal) => {
+      () => props.isVisible,
+      val => {
         if (val) {
-          console.log(val, 'init');
-          resetRender();
+          state.childIsVisible = true;
         }
       }
     );
 
     return {
-      weeks,
-      touchStart,
-      touchMove,
-      touchEnd,
-      getClass,
-      isStartTip,
-      isEndTip,
-      chooseDay,
-      isCurrDay,
-      confirm,
-      monthsPanel,
-      months,
-      ...toRefs(state),
+      closeActionSheet,
+      update,
+      close,
+      choose,
+      mychild,
+      state,
       ...toRefs(props)
     };
   }
@@ -629,5 +131,7 @@ export default create({
 </script>
 
 <style lang="scss">
-@import 'index.scss';
+.popup-box {
+  height: 518px;
+}
 </style>

+ 188 - 0
src/packages/calendaritem/index.scss

@@ -0,0 +1,188 @@
+.nut-calendar {
+  position: relative;
+  display: flex;
+  flex: 1;
+  height: 100%;
+  padding-top: 132px;
+  padding-bottom: 78px;
+  color: $calendar-base-color;
+  font-size: $calendar-base-font;
+  background-color: $white;
+  overflow: hidden;
+
+  &.nut-calendar-tile {
+    padding-top: 46px;
+    padding-bottom: 0;
+
+    .nut-calendar-header {
+      .calendar-title {
+        font-size: $calendar-base-font;
+      }
+    }
+  }
+
+  // 头部导航
+  .nut-calendar-header {
+    position: absolute;
+    top: -1px;
+    left: 0;
+    right: 0;
+    display: flex;
+    flex-direction: column;
+    text-align: center;
+    padding-top: 1px;
+    background-color: $white;
+    z-index: 1;
+
+    .calendar-title {
+      padding-top: 22px;
+      font-size: $calendar-title-font;
+      line-height: 25px;
+      border-radius: 12px 12px 0 0;
+    }
+
+    .calendar-curr-month {
+      padding: 10px 0 7px;
+      line-height: 22px;
+    }
+
+    .calendar-weeks {
+      display: flex;
+      align-items: center;
+      justify-content: space-around;
+      height: 46px;
+      border-radius: 0px 0px 12px 12px;
+      box-shadow: 0px 4px 10px 0px rgba($color: #000000, $alpha: 0.06);
+
+      .calendar-week-item {
+        &:first-of-type,
+        &:last-of-type {
+          color: $calendar-primary-color;
+        }
+      }
+    }
+  }
+
+  // 月份
+  .nut-calendar-content {
+    flex: 1;
+
+    .calendar-months-panel {
+      position: relative;
+      width: 100%;
+      height: auto;
+      display: block;
+
+      .calendar-month {
+        display: flex;
+        flex-direction: column;
+        text-align: center;
+      }
+
+      div:nth-of-type(2) {
+        .calendar-month-title {
+          padding-top: 0;
+        }
+      }
+
+      .calendar-loading-tip {
+        height: 50px;
+        line-height: 50px;
+        text-align: center;
+        position: absolute;
+        top: -50px;
+        left: 0;
+        right: 0;
+        font-size: $calendar-text-font;
+        color: $text-color;
+      }
+
+      .calendar-month-title {
+        height: 23px;
+        line-height: 23px;
+        margin: 8px 0;
+      }
+
+      .calendar-month-con {
+        overflow: hidden;
+
+        .calendar-month-item {
+          .calendar-month-day:nth-child(7n + 0),
+          .calendar-month-day:nth-child(7n + 1) {
+            color: $calendar-primary-color;
+          }
+        }
+
+        .calendar-month-day {
+          float: left;
+          width: 14.28%;
+          height: 64px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          flex-direction: column;
+          position: relative;
+
+          .curr-tips,
+          .calendar-day-tip {
+            position: absolute;
+            top: 10px;
+            width: 100%;
+            font-size: 11px;
+            line-height: 12px;
+            color: $calendar-primary-color;
+          }
+
+          &-active {
+            background-color: $calendar-primary-color;
+            color: $white !important;
+
+            .curr-tips {
+              visibility: hidden;
+            }
+
+            .calendar-day-tip {
+              color: $white;
+            }
+          }
+
+          &-disabled {
+            color: $calendar-disable-color !important;
+          }
+
+          &-choose {
+            background-color: $calendar-choose-color;
+          }
+
+          .calendar-day {
+            padding: 4px 0;
+            font-size: $calendar-day-font;
+          }
+        }
+      }
+    }
+  }
+
+  // 底部导航
+  .nut-calendar-footer {
+    position: absolute;
+    left: 0;
+    right: 0;
+    bottom: -1px;
+    display: flex;
+    height: 78px;
+    width: 100%;
+    background-color: $white;
+
+    .calendar-confirm-btn {
+      height: 44px;
+      width: 100%;
+      margin: 14px 18px;
+      border-radius: 22px;
+      background: $button-primary-background-color;
+      color: $white;
+      text-align: center;
+      line-height: 44px;
+    }
+  }
+}

+ 626 - 0
src/packages/calendaritem/index.vue

@@ -0,0 +1,626 @@
+<template>
+  <view class="nut-calendar" :class="{ 'nut-calendar-tile': !poppable }">
+    <!-- header -->
+    <view
+      class="nut-calendar-header"
+      :class="{ 'nut-calendar-header-tile': !poppable }"
+    >
+      <template v-if="poppable">
+        <view class="calendar-title">{{ title }}</view>
+        <view class="calendar-curr-month">2020年11月</view>
+      </template>
+      <view class="calendar-weeks">
+        <view
+          class="calendar-week-item"
+          v-for="(item, index) of weeks"
+          :key="index"
+          >{{ item }}</view
+        >
+      </view>
+    </view>
+    <!-- content-->
+    <view
+      class="nut-calendar-content"
+      ref="months"
+      @touchstart.stop="touchStart"
+      @touchmove.stop.prevent="touchMove"
+      @touchend.stop="touchEnd"
+    >
+      <view class="calendar-months-panel" ref="monthsPanel">
+        <view class="calendar-loading-tip">{{
+          !unLoadPrev ? '加载上一个月' : '没有更早月份'
+        }}</view>
+        <view
+          class="calendar-month"
+          v-for="(month, index) of monthsData"
+          :key="index"
+        >
+          <view class="calendar-month-title">{{ month.title }}</view>
+          <view class="calendar-month-con">
+            <view
+              class="calendar-month-item"
+              :class="type === 'range' ? 'month-item-range' : ''"
+            >
+              <template v-for="(day, i) of month.monthData" :key="i">
+                <view
+                  class="calendar-month-day"
+                  :class="getClass(day, month)"
+                  @click="chooseDay(day, month)"
+                >
+                  <view class="calendar-day">{{
+                    day.type == 'curr' ? day.day : ''
+                  }}</view>
+                  <view class="curr-tips" v-if="isCurrDay(month, day.day)"
+                    >今天</view
+                  >
+                  <view
+                    class="calendar-day-tip"
+                    v-if="isStartTip(day, month)"
+                    >{{ '开始' }}</view
+                  >
+                  <view
+                    class="calendar-day-tip"
+                    v-else-if="isEndTip(day, month)"
+                    >{{ '结束' }}</view
+                  >
+                </view>
+              </template>
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+    <!-- footer-->
+    <view class="nut-calendar-footer" v-if="poppable">
+      <view class="calendar-confirm-btn" @click="confirm">确定</view>
+    </view>
+  </view>
+</template>
+<script lang="ts">
+// import {
+//   PropType,
+//   computed,
+//   watch,
+//   reactive,
+//   ref,
+//   toRefs,
+//   readonly
+// } from 'vue';
+import { PropType, reactive, ref, watch, toRefs } from 'vue';
+import { createComponent } from '@/utils/create';
+const { create } = createComponent('calendar-item');
+import Utils from '@/utils/date';
+type InputDate = string | string[];
+interface CalendarState {
+  childIsVisible: boolean;
+  currDate: InputDate;
+  unLoadPrev: boolean;
+  touchParams: any;
+  transformY: number;
+  translateY: number;
+  scrollDistance: number;
+  defaultData: InputDate;
+  chooseData: any;
+  monthsData: any[];
+  dayPrefix: string;
+  startData: InputDate;
+  endData: InputDate;
+  isRange: boolean;
+}
+
+export default create({
+  props: {
+    type: {
+      type: String,
+      default: 'one'
+    },
+    isAutoBackFill: {
+      type: Boolean,
+      default: false
+    },
+    poppable: {
+      type: Boolean,
+      default: true
+    },
+    title: {
+      type: String,
+      default: '日历选择'
+    },
+    defaultValue: {
+      type: String as PropType<InputDate>,
+      default: null
+    },
+    startDate: {
+      type: String,
+      default: Utils.getDay(0)
+    },
+    endDate: {
+      type: String,
+      default: Utils.getDay(365)
+    }
+  },
+  components: {},
+  emits: ['choose', 'update', 'close'],
+
+  setup(props, { emit }) {
+    const weeks = ref(['日', '一', '二', '三', '四', '五', '六']);
+    // element refs
+    const months = ref<null | HTMLElement>(null);
+    const monthsPanel = ref<null | HTMLElement>(null);
+
+    // state
+    const state: CalendarState = reactive({
+      childIsVisible: false,
+      currDate: '',
+      unLoadPrev: false,
+      touchParams: {
+        startY: 0,
+        endY: 0,
+        startTime: 0,
+        endTime: 0,
+        lastY: 0,
+        lastTime: 0
+      },
+      transformY: 0,
+      translateY: 0,
+      scrollDistance: 0,
+      defaultData: [],
+      chooseData: [],
+      monthsData: [],
+      dayPrefix: 'calendar-month-day',
+      startData: '',
+      endData: '',
+      isRange: props.type === 'range'
+    });
+
+    // 日期转化成数组
+    const splitDate = date => {
+      return date.split('-');
+    };
+
+    const isStart = currDate => {
+      return Utils.isEqual(state.currDate[0], currDate);
+    };
+
+    const isEnd = currDate => {
+      return Utils.isEqual(state.currDate[1], currDate);
+    };
+
+    // 获取当前数据
+    const getCurrDate = (day, month, isRange?) => {
+      return isRange
+        ? month.curData[3] +
+            '-' +
+            month.curData[4] +
+            '-' +
+            Utils.getNumTwoBit(day.day)
+        : month.curData[0] +
+            '-' +
+            month.curData[1] +
+            '-' +
+            Utils.getNumTwoBit(day.day);
+    };
+
+    // 获取样式
+    const getClass = (day, month, isRange?) => {
+      const currDate = getCurrDate(day, month, isRange);
+      if (day.type == 'curr') {
+        if (
+          (!state.isRange && Utils.isEqual(state.currDate, currDate)) ||
+          (state.isRange && (isStart(currDate) || isEnd(currDate)))
+        ) {
+          return `${state.dayPrefix}-active`;
+        } else if (
+          (props.startDate && Utils.compareDate(currDate, props.startDate)) ||
+          (props.endDate && Utils.compareDate(props.endDate, currDate))
+        ) {
+          return `${state.dayPrefix}-disabled`;
+        } else if (
+          state.isRange &&
+          Array.isArray(state.currDate) &&
+          Object.values(state.currDate).length == 2 &&
+          Utils.compareDate(state.currDate[0], currDate) &&
+          Utils.compareDate(currDate, state.currDate[1])
+        ) {
+          return `${state.dayPrefix}-choose`;
+        } else {
+          return null;
+        }
+      } else {
+        return `${state.dayPrefix}-disabled`;
+      }
+    };
+
+    const confirm = () => {
+      if ((state.isRange && state.chooseData.length == 2) || !state.isRange) {
+        emit('choose', state.chooseData);
+        if (props.poppable) {
+          state.childIsVisible = false;
+          emit('update');
+        }
+      }
+    };
+
+    // 选中数据
+    const chooseDay = (day, month, isFirst, isRange?) => {
+      if (getClass(day, month, isRange) != `${state.dayPrefix}-disabled`) {
+        let days = [...month.curData];
+        days = isRange ? days.splice(3) : days.splice(0, 3);
+        days[2] =
+          typeof day.day == 'number' ? Utils.getNumTwoBit(day.day) : day.day;
+        days[3] = `${days[0]}-${days[1]}-${days[2]}`;
+        days[4] = Utils.getWhatDay(days[0], days[1], days[2]);
+        if (!state.isRange) {
+          state.currDate = days[3];
+          state.chooseData = [...days];
+        } else {
+          if (Object.values(state.currDate).length == 2) {
+            state.currDate = [days[3]];
+          } else {
+            if (Utils.compareDate(state.currDate[0], days[3])) {
+              Array.isArray(state.currDate) && state.currDate.push(days[3]);
+            } else {
+              Array.isArray(state.currDate) && state.currDate.unshift(days[3]);
+            }
+          }
+          if (state.chooseData.length == 2 || !state.chooseData.length) {
+            state.chooseData = [...days];
+          } else {
+            if (Utils.compareDate(state.chooseData[3], days[3])) {
+              state.chooseData = [[...state.chooseData], [...days]];
+            } else {
+              state.chooseData = [[...days], [...state.chooseData]];
+            }
+          }
+        }
+
+        if (props.isAutoBackFill && !isFirst) {
+          confirm();
+        }
+      }
+    };
+
+    // 获取当前月数据
+    const getCurrData = type => {
+      const monthData =
+        type == 'prev'
+          ? state.monthsData[0]
+          : state.monthsData[state.monthsData.length - 1];
+      let year = parseInt(monthData.curData[0]);
+      let month = parseInt(monthData.curData[1].toString().replace(/^0/, ''));
+      switch (type) {
+        case 'prev':
+          month == 1 && (year -= 1);
+          month = month == 1 ? 12 : --month;
+          break;
+        case 'next':
+          month == 12 && (year += 1);
+          month = month == 12 ? 1 : ++month;
+          break;
+      }
+      return [year, Utils.getNumTwoBit(month), monthData.curData[2]];
+    };
+
+    // 获取日期状态
+    const getDaysStatus = (days, type) => {
+      // 修复:当某个月的1号是周日时,月份下方会空出来一行
+      if (type == 'prev' && days >= 7) {
+        days -= 7;
+      }
+      return Array.from(Array(days), (v, k) => {
+        return {
+          day: k + 1,
+          type: type
+        };
+      });
+    };
+
+    // 获取月数据
+    const getMonth = (curData, type) => {
+      const preMonthDays = Utils.getMonthPreDay(curData[0], curData[1]);
+      const currMonthDays = Utils.getMonthDays(curData[0], curData[1]);
+      const title = {
+        year: curData[0],
+        month: curData[1]
+      };
+      const monthInfo = {
+        curData: curData,
+        title: `${title.year}年${title.month}月`,
+        monthData: [
+          ...getDaysStatus(preMonthDays, 'prev'),
+          ...getDaysStatus(currMonthDays, 'curr')
+        ]
+      };
+      if (type == 'next') {
+        if (
+          !state.endData ||
+          !Utils.compareDate(
+            `${state.endData[0]}-${state.endData[1]}-${Utils.getMonthDays(
+              state.endData[0],
+              state.endData[1]
+            )}`,
+            `${curData[0]}-${curData[1]}-${curData[2]}`
+          )
+        ) {
+          state.monthsData.push(monthInfo);
+        }
+      } else {
+        if (
+          !state.startData ||
+          !Utils.compareDate(
+            `${curData[0]}-${curData[1]}-${curData[2]}`,
+            `${state.startData[0]}-${state.startData[1]}-01`
+          )
+        ) {
+          state.monthsData.unshift(monthInfo);
+        } else {
+          state.unLoadPrev = true;
+        }
+      }
+    };
+
+    // 初始化数据
+    const initData = () => {
+      // 初始化开始结束数据
+      state.startData = props.startDate ? splitDate(props.startDate) : null;
+      state.endData = props.endDate ? splitDate(props.endDate) : null;
+
+      // 初始化当前日期
+      if (!props.defaultValue) {
+        state.currDate = state.isRange
+          ? [Utils.date2Str(new Date()), Utils.getDay(1)]
+          : Utils.date2Str(new Date());
+      } else {
+        state.currDate = state.isRange
+          ? [...props.defaultValue]
+          : props.defaultValue;
+      }
+
+      // 日期转化为数组
+      if (state.isRange && Array.isArray(state.currDate)) {
+        if (
+          props.startDate &&
+          Utils.compareDate(state.currDate[0], props.startDate)
+        ) {
+          state.currDate.splice(0, 1, props.startDate);
+        }
+        if (
+          props.endDate &&
+          Utils.compareDate(props.endDate, state.currDate[1])
+        ) {
+          state.currDate.splice(1, 1, props.endDate);
+        }
+        state.defaultData = [
+          ...splitDate(state.currDate[0]),
+          ...splitDate(state.currDate[1])
+        ];
+      } else {
+        if (
+          props.startDate &&
+          Utils.compareDate(state.currDate, props.startDate)
+        ) {
+          state.currDate = props.startDate;
+        } else if (
+          props.endDate &&
+          !Utils.compareDate(state.currDate, props.endDate)
+        ) {
+          state.currDate = props.endDate;
+        }
+
+        state.defaultData = [...splitDate(state.currDate)];
+      }
+
+      getMonth(state.defaultData, 'next');
+
+      let i = 1;
+      do {
+        getMonth(getCurrData('next'), 'next');
+      } while (i++ < 4);
+
+      if (state.isRange) {
+        chooseDay(
+          { day: state.defaultData[2], type: 'curr' },
+          state.monthsData[0],
+          true
+        );
+        chooseDay(
+          { day: state.defaultData[5], type: 'curr' },
+          state.monthsData[0],
+          true,
+          true
+        );
+      } else {
+        chooseDay(
+          { day: state.defaultData[2], type: 'curr' },
+          state.monthsData[0],
+          true
+        );
+      }
+      console.log(state.currDate, 'state.currDate');
+    };
+
+    // 区间选择&&当前月&&选中态
+    const isActive = (day, month) => {
+      return (
+        state.isRange &&
+        day.type == 'curr' &&
+        getClass(day, month) == 'calendar-month-day-active'
+      );
+    };
+
+    const isStartTip = (day, month) => {
+      if (isActive(day, month)) {
+        return isStart(getCurrDate(day, month));
+      } else {
+        return false;
+      }
+    };
+
+    const isEndTip = (day, month) => {
+      return isActive(day, month);
+    };
+
+    const isCurrDay = (month, day) => {
+      const date = `${month.curData[0]}-${month.curData[1]}-${day}`;
+      return Utils.isEqual(date, Utils.date2Str(new Date()));
+    };
+
+    const setTransform = (translateY = 0, type?, time = 1000) => {
+      if (type === 'end') {
+        monthsPanel?.value &&
+          (monthsPanel.value.style.webkitTransition = `transform ${time}ms cubic-bezier(0.19, 1, 0.22, 1)`);
+      } else {
+        monthsPanel?.value && (monthsPanel.value.style.webkitTransition = '');
+      }
+
+      monthsPanel?.value &&
+        (monthsPanel.value.style.webkitTransform = `translateY(${translateY}px)`);
+      state.scrollDistance = translateY;
+    };
+
+    const setMove = (move, type?, time?) => {
+      let updateMove = move + state.transformY;
+      const h = months.value?.offsetHeight || 0;
+
+      const offsetHeight = monthsPanel.value?.offsetHeight || 0;
+
+      if (type === 'end') {
+        // 限定滚动距离
+        if (updateMove > 0) {
+          updateMove = 0;
+        }
+        if (updateMove < 0 && updateMove < -offsetHeight + h) {
+          updateMove = -offsetHeight + h;
+        }
+        if (offsetHeight <= h && state.monthsData.length == 1) {
+          updateMove = 0;
+        }
+        const endMove = updateMove;
+        setTransform(endMove, type, time);
+      } else {
+        if (updateMove > 0 && updateMove > 100) {
+          updateMove = 100;
+        }
+        if (
+          updateMove < -offsetHeight + h - 100 &&
+          state.monthsData.length > 1
+        ) {
+          updateMove = -offsetHeight + h - 100;
+        }
+        if (
+          updateMove < 0 &&
+          updateMove < -100 &&
+          state.monthsData.length == 1
+        ) {
+          updateMove = -100;
+        }
+        setTransform(updateMove);
+      }
+    };
+
+    const touchStart = event => {
+      const changedTouches = event.changedTouches[0];
+      state.touchParams.startY = changedTouches.pageY;
+      state.touchParams.startTime = event.timestamp || Date.now();
+      state.transformY = state.scrollDistance;
+    };
+
+    const touchMove = event => {
+      //event.preventDefault();
+      const changedTouches = event.changedTouches[0];
+      state.touchParams.lastY = changedTouches.pageY;
+      state.touchParams.lastTime = event.timestamp || Date.now();
+      const move = state.touchParams.lastY - state.touchParams.startY;
+      if (Math.abs(move) < 5) {
+        return false;
+      }
+      setMove(move);
+    };
+
+    const touchEnd = event => {
+      const changedTouches = event.changedTouches[0];
+      state.touchParams.lastY = changedTouches.pageY;
+      state.touchParams.lastTime = event.timestamp || Date.now();
+      let move = state.touchParams.lastY - state.touchParams.startY;
+      if (Math.abs(move) < 5) {
+        return false;
+      }
+      const updateMove = move + state.transformY;
+      const h = months.value?.offsetHeight || 0;
+      const offsetHeight = monthsPanel.value?.offsetHeight || 0;
+
+      if (updateMove > 0) {
+        getMonth(getCurrData('prev'), 'prev');
+      } else if (updateMove < -offsetHeight + h * 2) {
+        getMonth(getCurrData('next'), 'next');
+        if (Math.abs(move) >= 300) {
+          getMonth(getCurrData('next'), 'next');
+        }
+      }
+
+      let moveTime = state.touchParams.lastTime - state.touchParams.startTime;
+      if (moveTime <= 300) {
+        move = move * 2;
+        moveTime = moveTime + 1000;
+        setMove(move, 'end', moveTime);
+      } else {
+        setMove(move, 'end');
+      }
+    };
+
+    const resetRender = () => {
+      state.chooseData.splice(0);
+      state.monthsData.splice(0);
+      state.scrollDistance = 0;
+      state.translateY = 0;
+      setTransform(state.scrollDistance);
+      initData();
+    };
+
+    const closeActionSheet = () => {
+      if (props.poppable) {
+        state.childIsVisible = false;
+        emit('update');
+        emit('close');
+      }
+      resetRender();
+    };
+
+    // 初始化
+    initData();
+
+    //监听 默认值更改
+    watch(
+      () => props.defaultValue,
+      val => {
+        if (val) {
+          console.log(val, 'init');
+          resetRender();
+        }
+      }
+    );
+
+    return {
+      weeks,
+      touchStart,
+      touchMove,
+      touchEnd,
+      getClass,
+      isStartTip,
+      isEndTip,
+      chooseDay,
+      isCurrDay,
+      confirm,
+      monthsPanel,
+      months,
+      closeActionSheet,
+      ...toRefs(state),
+      ...toRefs(props)
+    };
+  }
+});
+</script>
+
+<style lang="scss">
+@import 'index.scss';
+</style>