工具-抄了个掘金的日历签到组件

一个文笔一般,想到哪是哪的唯心论前端小白。

前言

接着分页来的,我那朋友又想要个签到的功能页面,嗯,需求就是这样了!!!

没错,需求只有几个字 【我想要个签到的页面!】,没有了,没有需求文档,没有产品说明,没有原型,没有效果图,后台也没有,只说了一句,我想要个签到的页面,后面补充了一句,你到时候说一下接口要什么样的数据,我找人搞,你只管搞定页面就行!

所以我就果断的打开掘金,截了个图发过去,问了一下这个行不行?

他说,看起来还凑合...

所以就呼哧呼哧的抄了一个出来,感觉日历这一块可以拿来凑一个小分享,以便以后复用,虽然可能性不大...

效果如下:

技术栈: jQuery + html + css

思路

如图所示,主要分为两个部分:

  1. 操作按钮,通过左右两个按钮可以切换月份
  2. 日历部分,将数据进行回显

大概就是这么个玩意了,操作按钮没什么说的,关键是日历部分,存在几个点需要考虑:

  1. 整个结构布局怎么来布?使用table布局,还是使用div+css来设计?
  2. 每个月的1号,不可能都在第一个,如何填充?以及最后几个没有填满的怎么处理?
  3. 每个日期的卡片间距如何把控?在浏览器宽度变化的时候显示效果是什么样的?

直接说我的选择了:

  1. 使用table布局,周日-周一放在 thead 里面,下面的内容放 tbody 中,每次切换月份后直接重绘tbody;
  2. 声明一个方法,这个方法的作用就是根据每个月1号,处于星期几,会在整个data中填充几个空值,当渲染每个格子的时候,对空值进行特殊处理,后面也一样。
  3. 查了一下表格的各个属性,间距没有专门的属性,所以给每个 td 加了一个和背景颜色一致的边框,宽度根据效果图来定。浏览器宽度变化的时候,table自有其处理逻辑,不用过分关注。

这样一来,一个table就省去了很多行用来定位的css样式,只需要关注td就好了。

开发

开发分为三个阶段:初始化阶段,切换按钮中间的内容,渲染日历

初始化阶段

初始化阶段主要做的是获取到今天是哪一天,属于哪个月份,将其显示在按钮中间的位置,并且要调用渲染日历的方法。

这个组件我声明了一个全局的数据 _GLOBAL_MONTH,有两个值字段 ym,分别对应 年和月。

切换月份

切换月份需要考虑到跨年的场景。更新按钮中间的内容并重绘日历。

渲染日历

根据接口返回的日历,绘制日历:

  1. 高亮今天,高亮方式为背景转换为一个原型的蓝底实心圆,字体颜色改为白色。
  2. 已经打卡的日期会出现一个对钩标识。
  3. 将返回的数据整齐划,按照周为单位进行分组,用来渲染tr>td。

代码分享

下面的代码主要展示的是一个思路性质的东西,如果真的要用在实际业务中,可能要改一点东西,主要是样式上的。

html 部分

就是简单粗暴的把所有需要的元素都一字摆开,放在了html里面。

html 复制代码
<div class="sign-content">
        <div class="sign-content-left fl">
            <button class="sign-button" id="lastMonthBtn"><i
                    class="iconfont icon-arrow-left"></i></button>
            <span id="signMonth"></span>
            <button class="sign-button" id="nextMonthBtn"><i
                    class="iconfont icon-arrow-right"></i></button>
        </div>
        <div class="sign-content-right fr"><img src="./img/card.png" alt="" srcset=""> 补签卡
            <span>2</span> 张
        </div>
    </div>
    <div class="sign-calendar">
        <table>
            <thead>
                <tr>
                    <th>周日</th>
                    <th>周一</th>
                    <th>周二</th>
                    <th>周三</th>
                    <th>周四</th>
                    <th>周五</th>
                    <th>周六</th>
                </tr>
            </thead>
            <tbody></tbody>
        </table>
    </div>
</div>

css 部分

css 复制代码
.sign-content {
  height: 40px;
  line-height: 40px;
  padding: 20px 0;
}

.sign-content-left button {
  background: #e8f3ff;
  border: none;
  outline: none;

  height: 32px;
  width: 32px;

  cursor: pointer;
}
.sign-content-left span {
  font-size: 20px;
  margin: 0 6px;
}

.sign-content-right img {
  vertical-align: sub;
  height: 24px;
}
.sign-calendar table {
  width: 100%;
  text-align: center;
  font-size: 16px;
}
.sign-calendar table th {
  font-weight: 500;
}

.sign-calendar table td {
  background: #e8f3ff;
  padding: 20px 0;
  border: 5px solid #fff;
  position: relative;
}
.sign-calendar table td span {
  display: block;

  position: absolute;

  top: 15px;
  right: 15px;
}

.sign-calendar table td p:nth-of-type(1) {
  font-size: 16px;
  font-weight: bold;
  line-height: 30px;
}
.sign-calendar table td p:nth-of-type(2) {
  font-size: 12px;
}
.sign-calendar table td p:nth-of-type(2) svg {
  vertical-align: middle;
}

.sign-calendar table td.today p:nth-of-type(1) {
  background: #1e80ff;
  width: 32px;
  border-radius: 50%;
  margin: 0 auto;
  color: #fff;
}

.sign-calendar table td.empty {
  background: transparent;
}

js 部分

js 部分分为三块,第一块为页面加载完成要初始化日历,第二块为绘制日历的核心方法,第三块则是切换月份时进行重绘日历的具体操作方法。

第一块:初始化各个功能

js 复制代码
const _GLOBAL_MONTH = {
    y: '',
    m: ''
}

// 初始化月份并绘制日历
function initSignMonth() {
    const y = _GLOBAL_MONTH.y
    const m = _GLOBAL_MONTH.m
    
    // 填写html内容
    $('#signMonth').html(`${y}年${m + 1}月`)

    // 生成开始时间和结束时间,真实场景应该不需要这一步,因为只需要提交 2023-7 后台就可以返回整月的数据了,这里是为了后面mock数据使用
    const start = `${y}-${m + 1}-1`
    const end = m + 1 > 11 ? `${y + 1}-1-1` : `${y}-${m + 1 + 1}-1`
    initCalendar(start, end)
}

// 初始化 全局月份数据
function initGloBalData() {
    const today = new Date()

    _GLOBAL_MONTH.y = today.getFullYear()
    _GLOBAL_MONTH.m = today.getMonth()
}

$(function () {
    initGloBalData()
    initSignMonth()
})

第二块:绘制日历

js 复制代码
/* 工具方法 - 模拟接口数据
 * @params { start: string } 开始时间
 * @params { end: string } 结束时间
 * @result { result: { date: string, fen: string, isSign: boolean }[] }
 * */
function mockData(start, end) {
    var start = new Date(start)
    var end = new Date(end)
    var dayLength = (end.getTime() - start.getTime()) / (24 * 60 * 60 * 1000)

    var result = []
    for (let i = 0; i < dayLength; i++) {
        let newDate = new Date(start.getTime() + i * (24 * 60 * 60 * 1000))
        let y = newDate.getFullYear()
        let m = newDate.getMonth() + 1
        let d = newDate.getDate()

        result.push({
            date: `${y}-${m}-${d}`,
            fen: parseInt(Math.random() * 1000),
            isSign: Math.random() > .5 ? true : false
        })
    }
    return result
}

/* 工具方法 - 对齐数据
 * 1- 按照开头和结尾,填充空数据
 * 2- 以周为单位,切割分组
 * */
function alignData(data) {
    // 获取第一天和最后一天的星期数,0-6
    let firstDay = new Date(data[0].date)
    let lastDay = new Date(data[data.length - 1].date)

    // 根据第一天和最后一天,向源数据中拼入空数据
    let unshiftArr = Array(firstDay.getDay()).fill({ date: '' })
    let pushArr = Array(6 - lastDay.getDay()).fill({ date: '' })

    // 切割分片
    return clipByNumber(7, [...unshiftArr, ...data, ...pushArr])
}

/* 工具方法 - 根据数字将数组切割分片
 * @params { num: number } 每个分片的长度
 * @params { arr: any[] }  源数据
 * @results { result: any[any[]]} 
 * */
function clipByNumber(num, arr) {
    // 获取分组数,并初始化 result
    var group = arr.length / num
    var result = Array(group).fill(Array(num).fill(null))

    // 填充数据
    return result.map((item, i) => {
        return item.map((ele, j) => {
            return arr[i * item.length + j]
        })
    })

}

// 绘制日历
function initCalendar(start, end) {

    // 获取并对其数据
    var data = mockData(start, end)
    data = alignData(data)

    // 一言不合直接开画
    $('.sign-calendar table tbody').html(data.map(item => `<tr>
            ${item.map(ele => {
        if (!ele.date) {
            return `<td class="empty"></td>`  // 空的块
        }
        var today = new Date()
        var todayStr = `${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}`
        var svg = `<svg t="1690722299548" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3649" width="16" height="16"><path d="M512 528m-398 0a398 398 0 1 0 796 0 398 398 0 1 0-796 0Z" fill="#F2814E" p-id="3650"></path><path d="M512 496m-398 0a398 398 0 1 0 796 0 398 398 0 1 0-796 0Z" fill="#FFBE4E" p-id="3651"></path><path d="M512 496m-282 0a282 282 0 1 0 564 0 282 282 0 1 0-564 0Z" fill="#FFBC54" p-id="3652"></path><path d="M512 180.668c174.036 0 315.332 141.296 315.332 315.332S686.036 811.332 512 811.332 196.668 670.036 196.668 496 337.964 180.668 512 180.668z m0 33.332c-155.64 0-282 126.36-282 282s126.36 282 282 282 282-126.36 282-282-126.36-282-282-282z" fill="#FA7B4D" p-id="3653"></path><path d="M512 180.668c174.036 0 315.332 141.296 315.332 315.332S686.036 811.332 512 811.332 196.668 670.036 196.668 496 337.964 180.668 512 180.668z m0 33.332c-155.64 0-282 126.36-282 282s126.36 282 282 282 282-126.36 282-282-126.36-282-282-282z" fill="#FA7B4D" p-id="3654"></path><path d="M494.108 705.168h55.352v-47.672c56.296-10.416 84.208-44.868 84.208-88.936 0-86.132-141.928-84.132-141.928-124.592 0-20.032 13.248-27.644 38.32-27.644 21.764 0 38.796 7.612 58.192 22.436l41.632-40.864c-20.816-19.228-45.888-33.248-80.424-37.656V313.768h-55.352V361.04c-51.568 8.816-82.32 40.464-82.32 86.132 0 81.724 141.456 82.928 141.456 126.596 0 19.628-12.776 29.244-42.104 29.244-24.604 0-48.256-8.412-76.168-26.44l-36.904 48.072c26.496 20.032 64.34 32.048 96.04 35.256v45.268z" fill="#F97950" p-id="3655"></path><path d="M494.108 685.168h55.352v-47.672c56.296-10.416 84.208-44.868 84.208-88.936 0-86.132-141.928-84.132-141.928-124.592 0-20.032 13.248-27.644 38.32-27.644 21.764 0 38.796 7.612 58.192 22.436l41.632-40.864c-20.816-19.228-45.888-33.248-80.424-37.656V293.768h-55.352V341.04c-51.568 8.816-82.32 40.464-82.32 86.132 0 81.724 141.456 82.928 141.456 126.596 0 19.628-12.776 29.244-42.104 29.244-24.604 0-48.256-8.412-76.168-26.44l-36.904 48.072c26.496 20.032 64.34 32.048 96.04 35.256v45.268z" fill="#FFF89F" p-id="3656"></path><path d="M257.4 801.828c14.168-7.124 31.124-14.42 36.6-35.828 20.132-78.676 424-508 424-508s0.372-52.644 29.852-82.536C846.172 247.96 910 364.584 910 496c0 219.664-178.336 398-398 398-96.8 0-185.572-34.632-254.6-92.172z" fill="#F47A38" fill-opacity=".4" p-id="3657"></path><path d="M716.296 301.688a30.104 30.104 0 0 1-0.228-41.464c1.24-1.488 1.932-2.224 1.932-2.224v-0.144a0.188 0.188 0 0 1 0.044-0.24 0.188 0.188 0 0 1 0.244 0c66.784 57.816 109.044 143.204 109.044 238.384 0 174.036-141.296 315.332-315.332 315.332-72.76 0-139.8-24.696-193.18-66.156a14.96 14.96 0 0 1-3.216-19.928l-0.036-0.02a18.172 18.172 0 0 1 26.248-4.452C389.112 756.692 448.088 778 512 778c155.64 0 282-126.36 282-282 0-75.284-29.564-143.716-77.704-194.312z" fill="#FFBA58" p-id="3658"></path><path d="M787.368 557.016a17.312 17.312 0 0 1 21.992-12.948c0.376 0.124 0.752 0.236 1.124 0.356a14.752 14.752 0 0 1 10.016 17.036c-18.42 87.196-72.936 161.14-147.148 205.464a15.988 15.988 0 0 1-21.116-4.48l0.016-0.016a17.24 17.24 0 0 1 5.224-24.844c65.064-39.296 113.004-104.128 129.892-180.568z" fill="#FFED8C" p-id="3659"></path></svg>`
        var isSignedSVG = `<svg t="1690768496247" class="icon" viewBox="0 0 1088 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3106" width="16" height="16"><path d="M502.464 832a3980.224 3980.224 0 0 1 230.656-309.248 3539.584 3539.584 0 0 1 106.048-123.84c30.4-33.6 60.48-64.448 89.472-93.44 49.344-49.472 107.072-104.256 143.36-127.68l-46.336-65.792c-67.2 44.992-131.84 93.056-181.824 129.984-29.12 21.568-56.128 42.24-81.6 61.952-25.216 19.52-51.2 40.96-78.72 63.424a4260.48 4260.48 0 0 0-168.128 145.792L366.72 362.752 192 510.08 502.464 832z" fill="#FEBE4F" p-id="3107"></path></svg>`

        var d = new Date(ele.date)
        if (todayStr === ele.date) { // 高亮今天
            return `<td class="today">
                <span>${ele.isSign ? isSignedSVG : ``
                }</span>
                <p>${d.getDate()}</p>
                <p>${svg}+${ele.fen}</p>
                </td>`
        } else {  // 正常数据
            return `<td>
                <span>${ele.isSign ? isSignedSVG : `` // 打对钩逻辑
                }</span>
                <p>${d.getDate()}</p>
                <p>${svg}+${ele.fen}</p>
                </td>`
        }
    })}
        </tr>`
    ).join())
    }

第三块:按钮交互

按钮交互很简单,只需要注意一下跨年的场景即可。

js 复制代码
function nextMonth() {
    if (_GLOBAL_MONTH.m + 1 > 11) {
        _GLOBAL_MONTH.y += 1
        _GLOBAL_MONTH.m = 0
    } else {
        _GLOBAL_MONTH.m += 1
    }
    initSignMonth()
}
function lastMonth() {
    if (_GLOBAL_MONTH.m - 1 < 0) {
        _GLOBAL_MONTH.y -= 1
        _GLOBAL_MONTH.m = 11
    } else {
        _GLOBAL_MONTH.m -= 1
    }
    initSignMonth()
}

后记

如上代码所示,就实现了展示中的那个日历的效果。

只有下面的绘制日历部分可以单独提出来,这只是提出了一个简单的思路。

总结一下所得:

  1. 得到一个任何场景都可以使用的日历绘制方法。
  2. 得到一个工具函数,可以根据开始和结束日期填充中间的的日期,并map成任何想要的数据结构。
  3. 得到一个分片的工具函数。
  4. 再有签到的场景,可以拿来直接用了,而且改造vue和react组件会很简单。

see U!

相关推荐
小白小白从不日白13 分钟前
react hooks--useCallback
前端·react.js·前端框架
恩婧21 分钟前
React项目中使用发布订阅模式
前端·react.js·前端框架·发布订阅模式
mez_Blog22 分钟前
个人小结(2.0)
前端·javascript·vue.js·学习·typescript
珊珊而川30 分钟前
【浏览器面试真题】sessionStorage和localStorage
前端·javascript·面试
森叶40 分钟前
Electron 安装包 asar 解压定位问题实战
前端·javascript·electron
drebander44 分钟前
ubuntu 安装 chrome 及 版本匹配的 chromedriver
前端·chrome
软件技术NINI1 小时前
html知识点框架
前端·html
深情废杨杨1 小时前
前端vue-插值表达式和v-html的区别
前端·javascript·vue.js
GHUIJS1 小时前
【vue3】vue3.3新特性真香
前端·javascript·vue.js
markzzw1 小时前
我在 Thoughtworks 被裁前后的经历
前端·javascript·面试