Diseño e implementación de componentes de calendario móvil.

prefacio

En la mayoría de las aplicaciones cliente, la selección y operación de la fecha es una función común, y usar el componente de calendario para implementar esta función suele ser una solución eficiente. Para el diseño y desarrollo de componentes de calendario, en proyectos comunes de código abierto, generalmente hay dos ideas de diseño:

  • Pantalla de cambio horizontal, un solo mes se representa de forma predeterminada, y el mes se puede cambiar presionando el botón o deslizando hacia la izquierda y hacia la derecha;
  • Cambie verticalmente la pantalla, la representación predeterminada muestra varios meses y deslice hacia arriba y hacia abajo para cambiar el mes;

Por ejemplo, agregar un selector para cambiar de vista, agregar botones personalizados, radio de fecha/selección múltiple, redacción personalizada, restricciones de rango de fechas, etc., estas son básicamente extensiones funcionales basadas en dos ideas.

Comparado

En la aplicación diaria, ambos métodos tienen sus propias ventajas y desventajas:

  • Conmutación horizontal, menos nodos para renderizado inicial y mejor rendimiento de renderizado;
  • Conmutación vertical, experiencia visual más intuitiva, mejor operación interactiva;

Sin embargo, es imposible tener ambos, y la elección entre experiencia interactiva y rendimiento es un problema que siempre debe afrontarse. Con el desarrollo continuo de los dispositivos móviles y la mejora continua de los navegadores móviles, la compatibilidad y la eficiencia operativa de los dispositivos de los usuarios han mejorado significativamente. Por lo tanto, este artículo describe principalmente el componente Calendario NutUI implementado mediante el cambio vertical. .

Introducción al tema

El tema de hoy es el diseño y la implementación del componente de calendario de NutUI. El componente de calendario es un componente de calendario de NutUI. Se utiliza para proporcionar a los usuarios una forma intuitiva de seleccionar fechas, cambiar de mes deslizando y admitir la selección de una sola fecha. y rango de fechas Contenido de fecha personalizado y otras funciones. Hoy, echemos un vistazo a cómo realizar la función del componente paso a paso en el proceso de desarrollo del componente.

Ejemplo

Ideas de diseño de componentes

El componente de calendario, no importa cómo se diseñe la interacción, el procesamiento de los datos de fecha y hora es fundamental, después de todo, la vista también sirve para la información de los datos. El método de visualización de conmutación vertical adoptado en este artículo también significa que debemos realizar algunos ajustes de optimización en el rendimiento de representación de los nodos. Así que nuestras ideas de implementación tienen principalmente los siguientes puntos:

mapa de ideas

  1. Fecha el procesamiento de datos, inicialice los datos originales a la vez y represente los elementos del nodo en segmentos en el área visible.
  2. Reduzca el costo de representación de los elementos del nodo mediante la aplicación de listas virtuales
  3. Manejo de eventos de desplazamiento y condiciones de contorno
  4. Completa funciones, enriquece Slots, Props, Eventos, etc., para mejorar la escalabilidad

Principio de implementación de componentes

Requisitos de parámetros básicos

在处理日期数据时,我们需要先明确我们所需的基本时间入参,例如:日历组件的可选时间范围,当前选中的时间。 通过对传入参数的解析处理,得到我们所需的数据内容,在之后的开发过程中,完成组件内容的渲染与事件处理。

这里我画了一张图方便大家更好理解:

procesamiento de datos

  • 原始日期数据:是我们根据日期范围计算的原始数据
  • 当前选中日期:可视范围的展示当前月份,需要判断选中日期是否在日期范围内
  • 展示范围区间:根据当前选中日期处理得出,为当前需要渲染的数据范围
  • 容器尺寸信息:用以计算日期滚动切换时的位移信息

日期数据处理

日期数据的计算,需要有多个处理过程。首先,我们需要先计算传入的日期范围是否存在,如果不存在,默认使用最近一年的时间范围。之后计算存在多少个月。在根据月的数量去遍历生成日期数据。

在计算单个月日期时,每个月的第一天最后一天的星期数是不同的,我们需要根据不同的星期数,以前一个月与后一个月的日期进行补全。这样既可以省去计算 1 号开始位置偏移量,也可以为功能扩展做出铺垫。

procesamiento de datos

// 获取单个月的日期与状态
const getDaysStatus = (currMonthDays: number,  dateInfo: any) => {
  let { year, month } = dateInfo;
  return Array.from(Array(currMonthDays), (v, k) => {
    return {
      day: k + 1,
      type: "curr",
      year,
      month,
    };
  });
  // 获取上一个月的最后一周天数,填充当月空白
  const getPreDaysStatus = (
    preCurrMonthDays: number
    weekNum: number,
    dateInfo: any,
  ) => {
    let { year, month } = dateInfo;
    if ( weekNum >= 7) {
      weekNum -= 7;
    }
    let months = Array.from(Array(preCurrMonthDays), (v, k) => {
      return {
        day: k + 1,
        type: "prev",
        year,
        month,
      };
    });
    return months.slice(preCurrMonthDays - weekNum);
  };
};
复制代码

处理后的数据如下:

procesamiento de datos

虚拟列表

当我们生成或加载的数据量非常大时,可能会产生严重的性能问题,导致视图无法响应操作一段时间。在小程序中视图的渲染问题更为明显,为了解决这个问题,虚拟列表是一种不错的解决方案:比起全量渲染数据生成的视图,可以只渲染当前可视区域(visible viewport)的视图,非可视区域的视图在用户滚动到可视区域再渲染。 例如,Taro中的长列表渲染(虚拟列表):

lista virtual

当然以上只是一个简单的应用,日历组件的构建需要在这个的基础上进行一定的优化。如下图,months wrapper 为需要展示月份的容器。这样设置,是因为在我们的视口范围内,会存在不止一个月份。同时因为单个月份包含的节点较多,当通过 视口边界 后在进行渲染,可能会存在留白现象,所以我们可以预留部分月份内容,在不可视区域进行节点变更与渲染。

lista virtual

如上图所示,

  • scrollWarpper:是一个高度为总月份高度的容器,主要用来作为 viewport 中的滚动容器;
  • monthsWrapper:内为当前渲染出的月份的容器;
  • viewport:为当前视口范围;

当滚动事件触发后,scrollWrapper 进行向下或向上移动。到达边界后,monthsWrapper 内的月份信息改变,其总体高度也可能发生变化。通过对 monthsWrapper 的 transition 进行修改,保障在月份变更后,视口中内容不变,视口外数据更新。

在应用虚拟列表的同时,结合当前的主流框架,将数据加入框架的响应式数据中,框架使用 diff 算法或其它机制根据数据的不同,可以对 DOM 节点进行一定程度上的复用,减少 DOM 节点元素的新增与删除操作。毕竟频繁的进行 DOM 增删操作是一件较为消耗性能的事情。

<!-- 视口 -->
<view class="nut-calendar-content" ref="months" @scroll="mothsViewScroll">
  <!-- 整体容器-设置一个总体高度用以撑起视口 -->
  <view class="calendar-months-panel" ref="monthsPanel">
    <!-- 月份容器 -->
    <view
      class="viewArea"
      ref="viewArea"
      :style="{ transform: `translateY(${translateY}px)` }"
    >
      <view
        class="calendar-month"
        v-for="(month, index) of compConthsData"
        :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)"
              >
                <!-- 日期显示slot -->
                <view class="calendar-day">
                  <slot name="day" :date="day.type == 'curr' ? day : ''">
                    {{ day.type == 'curr' ? day.day : '' }}
                  </slot>
                </view>
                <view
                  class="calendar-curr-tip-curr"
                  v-if="!bottomInfo && showToday && isCurrDay(day)"
                >
                  今天
                </view>
                <view
                  class="calendar-day-tip"
                  :class="{ 'calendar-curr-tips-top': rangeTip(day, month) }"
                  v-if="isStartTip(day, month)"
                >
                  {{ startText }}
                </view>
                <view class="calendar-day-tip" v-if="isEndTip(day, month)"
                  >{{ endText }}</view
                >
              </view>
            </template>
          </view>
        </view>
      </view>
    </view>
  </view>
</view>
复制代码

事件处理与边界状态

事件选择

在 Calendar 组件中,月份的切换变更是通过对滚动事件监听实现的。 考虑使用滚动事件,是因为考虑到对于 Taro 转换为微信小程序的兼容处理。touchmove 事件同样可以实现加载切换交互,但是 touch 事件要实现滚动效果,需要频繁的触发事件修改元素位置,在小程序中就表现为频繁的setData,而这会导致较大的性能开销,使得页面卡顿。

边界条件

Después de determinar el evento, el juicio de las condiciones de contorno es un problema que debemos considerar: la altura ocupada por cada mes no es necesariamente la misma. Cada mes contiene varias semanas, no necesariamente las mismas. Como resultado, la altura ocupada por cada mes no es necesariamente la misma. Por lo tanto, para juzgar con precisión la información de posición del desplazamiento actual, es necesario encontrar un punto similar para juzgar.

Condiciones de borde

Aquí usamos la altura de una sola fecha como valor de referencia, calculamos la altura del mes a través de la altura de una sola fecha y obtenemos la altura promedio de un solo mes. La posición de desplazamiento se divide por la altura promedio para aproximar la corriente. Como se muestra abajo:

procesamiento de datos

En el proceso de cálculo de la altura, debido a que la unidad del applet es rpx y h5 es rem, es necesario convertir el px.

let titleHeight, itemHeight;
//计算单个日期高度
//对小程序与H5,rpx与rem转换px处理
if (TARO_ENV === "h5") {
  titleHeight = 46 * scalePx.value + 16 * scalePx.value * 2;
  itemHeight = 128 * scalePx.value;
} else {
  titleHeight =
    Math.floor(46 * scalePx.value) + Math.floor(16 * scalePx.value) * 2;
  itemHeight = Math.floor(128 * scalePx.value);
}
monthInfo.cssHeight =
  titleHeight +
  (monthInfo.monthData.length > 35 ? itemHeight * 6 : itemHeight * 5);
let cssScrollHeight = 0;
//保存月份位置信息
if (state.monthsData.length > 0) {
  cssScrollHeight =
    state.monthsData[state.monthsData.length - 1].cssScrollHeight +
    state.monthsData[state.monthsData.length - 1].cssHeight;
}
monthInfo.cssScrollHeight = cssScrollHeight;
复制代码

Cuando obtenemos la corriente promedio actual, podemos juzgar las condiciones de contorno.

const mothsViewScroll = (e: any) => {
  const currentScrollTop = e.target.scrollTop;
  // 获取平均current
  let current = Math.floor(currentScrollTop / state.avgHeight);
  if (current == 0) {
    if (currentScrollTop >= state.monthsData[current + 1].cssScrollHeight) {
      current += 1;
    }
  } else if (current > 0 && current < state.monthsNum - 1) {
    if (currentScrollTop >= state.monthsData[current + 1].cssScrollHeight) {
      current += 1;
    }
    if (currentScrollTop < state.monthsData[current].cssScrollHeight) {
      current -= 1;
    }
  } else {
    // 获取视口高度 判断是否已经到最后一个月
    const viewPosition = Math.round(currentScrollTop + viewHeight.value);
    if (
      viewPosition <
        state.monthsData[current].cssScrollHeight +
          state.monthsData[current].cssHeight &&
      currentScrollTop < state.monthsData[current].cssScrollHeight
    ) {
      current -= 1;
    }
    if (
      current + 1 <= state.monthsNum &&
      viewPosition >=
        state.monthsData[current + 1].cssScrollHeight +
          state.monthsData[current + 1].cssHeight
    ) {
      current += 1;
    }
    if (currentScrollTop < state.monthsData[current - 1].cssScrollHeight) {
      current -= 1;
    }
  }
  if (state.currentIndex !== current) {
    state.currentIndex = current;
    setDefaultRange(state.monthsNum, current);
  }
  //设置月份标题信息
  state.yearMonthTitle = state.monthsData[current].title;
};
复制代码

Echemos un vistazo al efecto:

representaciones

función perfecta

A través del proceso anterior, hemos completado un componente básico del calendario móvil. Sobre esta base, necesitamos hacer algunas mejoras para extender la generalidad del componente.

  1. Agregue ranuras para la información de la fecha, lo que permite una visualización personalizada de la información de la fecha
  2. Las ranuras se proporcionan en el título. Inserción fácil de usar de acciones personalizadas
  3. El título, el botón, la copia del rango de fechas y otra información brindan configuraciones de accesorios
  4. Agregue métodos de devolución de llamada, como seleccionar una fecha, hacer clic en una fecha, cerrar el calendario, etc.
// 未传入的slot不进行加载,减少无意义的dom
<view
  class="calendar-curr-tips calendar-curr-tips-top"
  v-if="topInfo"
>
  <slot name="topInfo" :date="day.type == 'curr' ? day : ''"></slot>
</view>

复制代码

pantalla de función

Epílogo

Este artículo presenta las ideas de diseño y los principios de implementación del componente Calendar en NutUI, con la esperanza de brindarle algo de inspiración e ideas. Finalmente, mencionemos nuestra biblioteca de componentes NutUI Durante mucho tiempo, los pequeños socios del equipo se han dedicado a mantener NutUI. En los días venideros, este tipo de persistencia no se rendirá. Seguiremos manteniendo e iterando activamente, brindando soporte técnico a los estudiantes que lo necesiten y también publicaremos algunos artículos relacionados de vez en cuando para ayudar a todos a comprender y usar mejor Nuestro biblioteca de componentes.

Ven y apóyanos con una Estrella ❤️~

Supongo que te gusta

Origin juejin.im/post/7087550518595977252
Recomendado
Clasificación