一个文笔一般,想到哪是哪的唯心论前端小白。
前言
接着分页来的,我那朋友又想要个签到的功能页面,嗯,需求就是这样了!!!
没错,需求只有几个字 【我想要个签到的页面!】,没有了,没有需求文档,没有产品说明,没有原型,没有效果图,后台也没有,只说了一句,我想要个签到的页面,后面补充了一句,你到时候说一下接口要什么样的数据,我找人搞,你只管搞定页面就行!
所以我就果断的打开掘金,截了个图发过去,问了一下这个行不行?
他说,看起来还凑合...
所以就呼哧呼哧的抄了一个出来,感觉日历这一块可以拿来凑一个小分享,以便以后复用,虽然可能性不大...
效果如下:
技术栈: jQuery + html + css
思路
如图所示,主要分为两个部分:
- 操作按钮,通过左右两个按钮可以切换月份
- 日历部分,将数据进行回显
大概就是这么个玩意了,操作按钮没什么说的,关键是日历部分,存在几个点需要考虑:
- 整个结构布局怎么来布?使用table布局,还是使用div+css来设计?
- 每个月的1号,不可能都在第一个,如何填充?以及最后几个没有填满的怎么处理?
- 每个日期的卡片间距如何把控?在浏览器宽度变化的时候显示效果是什么样的?
直接说我的选择了:
- 使用table布局,周日-周一放在 thead 里面,下面的内容放 tbody 中,每次切换月份后直接重绘tbody;
- 声明一个方法,这个方法的作用就是根据每个月1号,处于星期几,会在整个data中填充几个空值,当渲染每个格子的时候,对空值进行特殊处理,后面也一样。
- 查了一下表格的各个属性,间距没有专门的属性,所以给每个 td 加了一个和背景颜色一致的边框,宽度根据效果图来定。浏览器宽度变化的时候,table自有其处理逻辑,不用过分关注。
这样一来,一个table就省去了很多行用来定位的css样式,只需要关注td就好了。
开发
开发分为三个阶段:初始化阶段,切换按钮中间的内容,渲染日历
初始化阶段
初始化阶段主要做的是获取到今天是哪一天,属于哪个月份,将其显示在按钮中间的位置,并且要调用渲染日历的方法。
这个组件我声明了一个全局的数据 _GLOBAL_MONTH
,有两个值字段 y
和m
,分别对应 年和月。
切换月份
切换月份需要考虑到跨年的场景。更新按钮中间的内容并重绘日历。
渲染日历
根据接口返回的日历,绘制日历:
- 高亮今天,高亮方式为背景转换为一个原型的蓝底实心圆,字体颜色改为白色。
- 已经打卡的日期会出现一个对钩标识。
- 将返回的数据整齐划,按照周为单位进行分组,用来渲染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()
}
后记
如上代码所示,就实现了展示中的那个日历的效果。
只有下面的绘制日历部分可以单独提出来,这只是提出了一个简单的思路。
总结一下所得:
- 得到一个任何场景都可以使用的日历绘制方法。
- 得到一个工具函数,可以根据开始和结束日期填充中间的的日期,并map成任何想要的数据结构。
- 得到一个分片的工具函数。
- 再有签到的场景,可以拿来直接用了,而且改造vue和react组件会很简单。
see U!