文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

移动端日历组件设计与实现

2024-12-02 00:49

关注

例如添加 picker 进行视图切换,添加自定义按钮,日期单选/多选,自定义文案,日期范围限制等等功能,这些基本都是在两种思路的基础上进行的功能扩展。

在日常的应用中,两种方式各有优劣:

然而,鱼和熊掌不可兼得,交互体验与性能上的取舍,是一个始终都要直面的问题。随着移动端设备的不断发展,移动端浏览器不断的完善,用户设备在兼容性与运行效率上都有明显提升,因此,本文主要阐述的,是以竖向切换方式实现的 NutUI Calendar 日历组件。

主题介绍

今天的主题是 NutUI Calendar 组件的设计与实现,Calendar 组件是 NutUI 的一个日历组件,它用以为用户提供一个直观的日期选择方式,以滑动的方式切换月份,支持单个日期与日期范围的选择,支持自定义日期内容等功能。今天,让我们一起来看看,在组件的开发过程中,是如何一步步实现组件功能的。

组件设计思路

日历组件,不管以何种方式设计交互,日期时间数据的处理都是必不可少的,毕竟视图也是为数据信息服务的。在本文中采取的竖向切换展示的方式,也意味着我们要在节点的渲染性能上做一些优化调整。所以我们的实现思路主要有以下几点:

组件的实现原理

01基本参数需求

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

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

原始日期数据:是我们根据日期范围计算的原始数据

当前选中日期:可视范围的展示当前月份,需要判断选中日期是否在日期范围内

展示范围区间:根据当前选中日期处理得出,为当前需要渲染的数据范围

容器尺寸信息:用以计算日期滚动切换时的位移信息

02日期数据处理

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

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

// 获取单个月的日期与状态
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);
};
};

处理后的数据如下:

03虚拟列表

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

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

如上图所示,

当滚动事件触发后,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)"
>

<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>

事件处理与边界状态

01事件选择

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

02边界条件

确定好事件后,边界条件的判断,就是我们需要考虑的一个问题:每个月所占高度,不一定相同。每个月包含有几个星期,不一定相同。导致每个月所占据的高度也不一定相同。所以要准确到判断当前滚动的位置信息,就需要找到一个相同点来进行判断。

这里我们以单个日期的高度作为基准值,通过单个日期的高度计算月份的高度,在得出平均单个月份的高度。滚动位置除以平均高度取得近似 current。如下图所示:

在计算高度过程中,因为小程序的单位为 rpx,h5 为 rem,所以需要对 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;

当我们得到当前的平均 current,就可以进行边界条件的判断。

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;
};

让我们来看一看效果吧:

功能完善

通过以上过程,我们已经完成了一个基本的滚动日历组件。在这个基础上,我们需要进行一些完善,以扩展组件的通用性。

// 未传入的slot不进行加载,减少无意义的dom
<view
class="calendar-curr-tips calendar-curr-tips-top"
v-if="topInfo"
>
<slot name="topInfo" :date="day.type == 'curr' ? day : ''"></slot>
</view>

结语

本文介绍了 NutUI 中 Calendar 组件的设计思路与实现原理,希望可以为大家提供一些灵感与思路。最后再提一下我们的 NutUI 组件库,长期以来,团队的小伙伴都在尽心尽力地维护着 NutUI。在之后的日子里,这种坚持也不会放弃,我们依然会积极地维护与迭代,为有需要的同学提供技术支持,也会不定时地发布一些相关的文章帮助大家更好地理解与使用我们的组件库。

来源:京东零售技术内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯