已经好久没有更新过博客了,大概有两三年了吧,因为换了工作,工作也比较忙,所以也就没有时间来写技术博客,期间也一直想写,但自己又比较懒,就给耽误了。今天这篇先续上,下一篇什么时候写,我也不知道,随心所欲、随遇而安、安之若素、素不相识也就无所谓了吧。

一开始看到这个功能需求,我也很懵逼,因为从来没有做过啊,哈哈。。。但转念一想既然产品能提出这个需求,想必会有人实现过,就去网上查了查资料,果不其然,还真有人做过,但离我想要的效果还是差着十万八千里,所以按照网上大神的思路,结合我司的实际需求,自己就把它给捣鼓出来了。

其实刚做好的效果还是能实现产品的需求的,我就让同事帮忙测试一下看看有没有什么bug,因为我司对bug的要求以及对用户体验的要求还是很严格的,所以在同事的实际测试以及提醒下,后面我又参照电脑上基于ctrl、shift键来选中文件的实际效果做了改进,算是不给测试提bug的机会吧。

照旧先上两张最后实现的效果图:



先来看看实现单月的日期组件calendar.vue

<template>
<div class="tiled-calendar">
<div class="calendar-header">
<span :class="getWeekClass(year)">{{monthCN[month]}}月</span>
</div>
<ul class="calendar-week">
<li v-for="(item, index) in calendarWeek" :key="index" class="calendar-week-item" :class="getWeekClass(year, month + 1)">{{item}}</li>
</ul>
<ul class="calendar-body">
<li v-for="(item, index) in calendar" :key="index" class="calendar-day" @click="onDay(item)">
<template v-if="hasCurrentMonth">
<span class="calendar-day-item" :class="getCellClass(item, index)">{{item.day}}</span>
</template>
<template v-else>
<span class="calendar-day-item" :class="getCellClass(item, index)" v-if="isCurrentMonth(item.date)">{{item.day}}</span>
</template>
</li>
</ul>
</div>
</template> <script>
import dayjs from 'dayjs'
import { mapState, mapMutations } from 'vuex' const monthCN = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二'] const getNewDate = date => {
const year = date.getFullYear()
const month = date.getMonth()
const day = date.getDate() return { year, month, day }
} const getDate = (year, month, day) => new Date(year, month, day) export default {
props: {
year: {
type: [String, Number],
default: 2023
},
month: {
type: [String, Number],
default: 0
},
// 是否在当月日历中展示上个月和下个月的部分日期,默认不展示。
hasCurrentMonth: {
type: Boolean,
default: false
},
// 休息日
weekendList: Array,
// 工作日
workList: {
type: Array,
default: () => []
},
// 既非工作日也非休息日
workRestOffList: Array
},
data () {
return {
calendarWeek: ['一', '二', '三', '四', '五', '六', '日'],
today: dayjs().format('YYYYMMDD'),
isCtrl: false,
isShift: false,
monthCN
}
},
computed: {
calendar () {
const calendatArr = []
const { year, month } = getNewDate(getDate(this.year, this.month, 1)) const currentFirstDay = getDate(year, month, 1) // 获取当前月第一天星期几
const weekDay = currentFirstDay.getDay()
let startTime = null if (weekDay === 0) {
// 当月第一天是星期天
startTime = currentFirstDay - 6 * 24 * 60 * 60 * 1000
} else {
startTime = currentFirstDay - (weekDay - 1) * 24 * 60 * 60 * 1000
} // 为了页面整齐排列 一并绘制42天
for (let i = 0; i < 42; i++) {
calendatArr.push({
date: new Date(startTime + i * 24 * 60 * 60 * 1000),
year: new Date(startTime + i * 24 * 60 * 60 * 1000).getFullYear(),
month: new Date(startTime + i * 24 * 60 * 60 * 1000).getMonth() + 1,
day: new Date(startTime + i * 24 * 60 * 60 * 1000).getDate()
})
} return calendatArr
},
...mapState('d2admin', { selectedList: s => s.multiMarketCalendar.selectList })
},
mounted () {
this.onKeyEvent()
},
methods: {
...mapMutations({ setSelectCalendar: 'd2admin/multiMarketCalendar/setSelectCalendar' }),
onKeyEvent () {
window.addEventListener('keydown', e => {
const _e = e || window.event
switch (_e.keyCode) {
case 16:
this.isShift = true
break
case 17:
this.isCtrl = true
break
}
}) window.addEventListener('keyup', e => {
const _e = e || window.event
switch (_e.keyCode) {
case 16:
this.isShift = false
break
case 17:
this.isCtrl = false
break
}
})
},
// 是否是当前月
isCurrentMonth (date) {
const { year: currentYear, month: currentMonth } = getNewDate(getDate(this.year, this.month, 1))
const { year, month } = getNewDate(date)
return currentYear === year && currentMonth === month
},
onDay (item) {
// 如果是同一年,当点击的月份小于当前系统日期的月份,则禁止点击。
if (item.year === new Date().getFullYear() && item.month < new Date().getMonth() + 1) {
return
} // 如果是同一年又是同一个月,当点击的日期小于当前的日期,则禁止点击
if (item.year === new Date().getFullYear() && item.month === new Date().getMonth() + 1 && item.day < new Date().getDate()) {
return
} // 如果点击的年份小月当前年份,则禁止点击。
if (item.year < new Date().getFullYear()) {
return
} // 如果点击的日期不是那个月应有的日期(每个月的日期里边也会包含上个月和下个月的某几天日期,只是这些日期的颜色被置灰了),则禁止点击。
if (!this.isCurrentMonth(item.date)) {
return
} const dateStr = dayjs(item.date).format('YYYYMMDD')
const { isCtrl, isShift } = this // 按住ctrl
if (isCtrl && !isShift) {
this.setSelectCalendar({ dateStr, item, isCtrl })
} else if (isCtrl && isShift) { // 同时按住ctrl和shift
this.setSelectCalendar({ dateStr, item, isCtrl, isShift })
} else if (isShift && !isCtrl) { // 按住shift
this.setSelectCalendar({ dateStr, item, isShift })
} else { // 不按ctrl和shift,一次只能选择一个
this.setSelectCalendar({ dateStr, item })
}
},
getWeekClass (year, month) {
if (year < new Date().getFullYear()) return 'is-disabled' if (year === new Date().getFullYear() && month < new Date().getMonth() + 1) return 'is-disabled'
},
getCellClass (item, idx) {
const { workList, weekendList, workRestOffList } = this
const date = dayjs(item.date).format('YYYYMMDD')
const today = `${item.year}${item.month < 10 ? '0' + item.month : item.month}${item.day < 10 ? '0' + item.day : item.day}` // 被选中的日期
const index = this.selectedList.indexOf(date) if (index !== -1) {
// 如果选中的日期是当年日期的周六周日,且这些日期不是工作日,则在选中时还是要保留它原来的红色字体,如果是工作日,则在选中时就不能再展示成红色字体了。
if ((idx % 7 === 5 || idx % 7 === 6) && this.isCurrentMonth(item.date) && item.year >= new Date().getFullYear() && !(workList.length && workList.includes(date))) {
return 'is-selected is-weekend'
} else if (weekendList.length && weekendList.includes(date)) { // 休息日的选中(非周六周日的休息日)
return 'is-selected is-weekend'
} else if (workRestOffList.length && workRestOffList.includes(date)) { // 异常日期的选中
return 'is-selected is-workRestOff'
} else {
return 'is-selected'
}
} // 默认显示当前日期
if (today === this.today) {
// 如果把当前日期也置为休息日,则也是要将当前日期标红
if (weekendList.length && weekendList.includes(date)) {
return 'is-today is-weekend'
} // 异常日期
if (workRestOffList.length && workRestOffList.includes(date)) {
return 'is-today is-workRestOff'
} return 'is-today'
} // 如果日期不是那个月应有的日期(每个月的日期里边也会包含上个月和下个月的某几天日期,只是这些日期的颜色被置灰了),则禁止点击。
// if (!this.isCurrentMonth(item.date)) {
// return 'is-disabled'
// } // 如果是同一年,当月份小于当前系统日期的月份,则禁止点击。
if (item.year === new Date().getFullYear() && item.month < new Date().getMonth() + 1) {
// 周六周日被设置成了工作日
if ((idx % 7 === 5 || idx % 7 === 6) && (workList.length && workList.includes(date))) {
return 'is-disabled is-work'
} // 异常日期
if (workRestOffList.length && workRestOffList.includes(date)) {
return 'is-disabled is-workRestOff'
} // 周六周日标红
if (idx % 7 === 5 || idx % 7 === 6 || (weekendList.length && weekendList.includes(date))) {
return 'is-disabled is-weekend'
} return 'is-disabled'
} // 如果是同一年又是同一个月,当日期小于当前的日期,则禁止点击
if (item.year === new Date().getFullYear() && item.month === new Date().getMonth() + 1 && item.day < new Date().getDate()) {
// 周六周日被设置成了工作日
if ((idx % 7 === 5 || idx % 7 === 6) && (workList.length && workList.includes(date))) {
return 'is-disabled is-work'
} // 异常日期
if (workRestOffList.length && workRestOffList.includes(date)) {
return 'is-disabled is-workRestOff'
} // 周六周日标红
if (idx % 7 === 5 || idx % 7 === 6 || (weekendList.length && weekendList.includes(date))) {
return 'is-disabled is-weekend'
} return 'is-disabled'
} // 如果年份小月当前年份,则禁止点击。
if (item.year < new Date().getFullYear()) {
// 周六周日被设置成了工作日
if ((idx % 7 === 5 || idx % 7 === 6) && (workList.length && workList.includes(date))) {
return 'is-disabled is-work'
} // 异常日期
if (workRestOffList.length && workRestOffList.includes(date)) {
return 'is-disabled is-workRestOff'
} // 周六周日标红
if (idx % 7 === 5 || idx % 7 === 6 || (weekendList.length && weekendList.includes(date))) {
return 'is-disabled is-weekend'
} return 'is-disabled'
} // 周六周日标红
if ((idx % 7 === 5 || idx % 7 === 6) && this.isCurrentMonth(item.date) && item.year >= new Date().getFullYear()) {
// 周六周日被设置成了工作日,则不标红
if (workList.length && workList.includes(date)) {
return 'is-work'
} if (workRestOffList.length && workRestOffList.includes(date)) {
return 'is-workRestOff'
} return 'is-weekend'
} // 休息日标红
if (weekendList.length && weekendList.includes(date)) {
return 'is-weekend'
} // 既非工作日也非休息日
if (workRestOffList.length && workRestOffList.includes(date)) {
return 'is-workRestOff'
}
}
},
beforeDestroy() {
window.removeEventListener('keydown')
window.removeEventListener('keyup')
}
}
</script> <style lang="scss" scoped>
.calendar-header, .calendar-week{
user-select: none;
.is-disabled{
cursor: not-allowed;
opacity: 0.5;
}
}
.tiled-calendar {
height: 100%;
box-sizing: border-box;
margin: 0 20px 10px;
font-size: 14px;
ul, li{
margin: 0;
padding: 0;
}
.calendar-header {
border-bottom: 1px solid #EDEEF1;
padding: 8px 0;
color: #1B1F26;
text-align: center;
margin-bottom: 8px;
}
.calendar-week {
display: flex;
height: 28px;
line-height: 28px;
border-right: none;
border-left: none;
margin-bottom: 2px;
.calendar-week-item {
list-style-type: none;
width: 14.28%;
text-align: center;
color: #737985;
}
}
.calendar-body {
display: flex;
flex-wrap: wrap;
margin-top: 5px;
.calendar-day {
width: 14.28%;
text-align: center;
height: 34px;
line-height: 34px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
.calendar-day-item {
display: block;
width: 26px;
height: 26px;
line-height: 26px;
border-radius: 50%;
cursor: pointer;
user-select: none;
color: #1B1F26;
&:hover{
background: rgba(69, 115, 243, .12);
}
}
.is{
&-today, &-selected{
border: 1px solid #E60012;
&:hover{
background: rgba(69, 115, 243, .12);
}
}
&-selected {
border: 1px solid #4573F3;
&:hover{
background: none;
}
}
&-disabled{
cursor: not-allowed;
color:#9E9E9E;
&:hover{
background: none;
}
}
&-weekend{
color: #E60012;
}
&-workRestOff{
color: #FF7D00;
}
&-disabled{
&.is-workRestOff{
color: rgba(255, 125, 0, .4)
}
}
&-disabled{
&.is-weekend{
color: rgba(230, 0, 18, .4)
}
}
&-work{
color: #1B1F26;
}
&-disabled.is-work{
color: #9E9E9E
}
}
}
}
}
</style>

从以上也能看出,我们的需求还是很复杂的,比如,历史日期也就是所谓的过期的日期不能被选中,周六周日的日期要标红,还可以把工作日置为休息日,休息日置为工作日等等。这些功能说起来就一句话,可要实现出来,那可就属实太复杂了。

对了,在实现的过程中,我还使用到了vuex来保存点击选中的数据calendar.js:

let selectList = []
let shiftData = null
let shiftDate = ''
let currentSelect = []
let lastSelect = [] // 获取两个日期中间的所有日期
const getBetweenDay = (starDay, endDay) => {
var arr = []
var dates = [] // 设置两个日期UTC时间
var db = new Date(starDay)
var de = new Date(endDay) // 获取两个日期GTM时间
var s = db.getTime() - 24 * 60 * 60 * 1000
var d = de.getTime() - 24 * 60 * 60 * 1000 // 获取到两个日期之间的每一天的毫秒数
for (var i = s; i <= d;) {
i = i + 24 * 60 * 60 * 1000
arr.push(parseInt(i))
} // 获取每一天的时间 YY-MM-DD
for (var j in arr) {
var time = new Date(arr[j])
var year = time.getFullYear(time)
var mouth = (time.getMonth() + 1) >= 10 ? (time.getMonth() + 1) : ('0' + (time.getMonth() + 1))
var day = time.getDate() >= 10 ? time.getDate() : ('0' + time.getDate())
var YYMMDD = year + '' + mouth + '' + day
dates.push(YYMMDD)
} return dates
} const shiftSelect = (dateStr, item) => {
if (!shiftData) {
shiftData = item.date
shiftDate = `${item.year}-${item.month}-${item.day}`
// 如果当前日期已选中,再次点击当前日期就取消选中。
if (selectList.includes(dateStr)) {
selectList.splice(selectList.indexOf(dateStr), 1)
} else {
selectList.push(dateStr)
}
} else {
if (shiftData < item.date) {
currentSelect = getBetweenDay(shiftDate, `${item.year}-${item.month}-${item.day}`)
} else if (shiftData > item.date) {
currentSelect = getBetweenDay(`${item.year}-${item.month}-${item.day}`, shiftDate)
} else {
currentSelect = [dateStr]
} selectList = selectList.filter(item => !lastSelect.includes(item)) // 移除上次按shift复制的
selectList = selectList.concat(currentSelect) // 添加本次按shift复制的
lastSelect = currentSelect
}
} export default {
namespaced: true,
state: {
selectList: []
},
mutations: {
setSelectCalendar (s, { dateStr, item, isCtrl, isShift }) {
// 按住ctrl
if (isCtrl && !isShift) {
// 如果当前日期已选中,再次点击当前日期就取消选中。
if (selectList.includes(dateStr)) {
selectList.splice(selectList.indexOf(dateStr), 1)
} else {
selectList.push(dateStr)
} // 加上lastSelect = []这个就会在shift连续选了多个后,再按住ctrl键取消已选中的日期的中间的几个后,下次再按住
// shift多选时,就会从按住ctrl取消选中的最后一个日期开始连续选择,但刚才取消已选中的那些日期
// 之前的已经选中的日期依旧处于选中状态,与电脑自身的按住shift键多选的效果略有不同,所以要注释掉这串代码。
// lastSelect = [] // 最开始这串代码是放开的
} else if (isShift && !isCtrl) {
// 加上selectList = []可以实现按住ctrl单选几个不连续的日期后,再按住shift键连选其他日期,就可以把之前
// 按住ctrl键单选的其他日期都取消选中。不加selectList = []是可以在按住shift键连选其他日期时同时保留之前
// 按住ctrl键单选的其他日期。
selectList = [] // 最开始这串代码是没有的 shiftSelect(dateStr, item)
} else if (isCtrl && isShift) { // 同时按住ctrl和shift
lastSelect = []
shiftSelect(dateStr, item)
} else { // 不按ctrl和shift,一次只能选择一个
selectList = [dateStr]
} if (!isShift) {
shiftData = item.date
shiftDate = `${item.year}-${item.month}-${item.day}`
} selectList = [...new Set(selectList)].sort() // 去重、排序 s.selectList = selectList
},
setSelectEmpty (s) {
selectList = []
shiftData = null
shiftDate = ''
currentSelect = []
lastSelect = []
s.selectList = []
}
}
}

再来看看在以上单个日期组件的基础上实现1年12个月日历平铺的代码吧:

<template>
<div class="material-calendar-parent">
<el-date-picker v-model="year" type="year" placeholder="选择年" @change="onChange" />
<el-row :gutter="10">
<el-col :span="6" v-for="(n, i) in 12" :key="i">
<Calendar :year="year.getFullYear()" :month="i" :key="n" :workList="workList" :weekendList="weekendList" :workRestOffList="workRestOffList" />
</el-col>
</el-row>
</div>
</template> <script>
import Calendar from './calendar' export default {
components: { Calendar },
data(){
return {
year: new Date(),
workList: ['20220206', '20230107', '20230311'],
weekendList: [{date: '20220118', market: '马来西亚,泰国'}, {date: '20230123', market: '中国澳门,韩国'}, {date: '20230213', market: '中国香港,美国'}, {date: '20230313', market: '中国台湾,法国'}],
workRestOffList: ['20220101', '20220105', '20230101', '20230105', '20230218', '20230322', '20230330'],
}
},
methods: {
onChange(v){
this.year = v
}
}
}
</script> <style lang="scss">
.material-calendar-parent{
.el-col{
border-right: 1px solid #eee;
border-bottom: 1px solid #eee;
}
}
</style>

写到这里就不准备再往下写了,所有的代码都贴出来了。如果你想要的效果跟我的这个不太一样,你自己在这个基础上改吧改吧应该就可以用了。其实最关键的部分也就是那个按住ctrl、shift键实现跨月、跨年多选日期,理解了这个原理,其余的实现部分尽管复杂但并不难。

vue平铺日历组件之按住ctrl、shift键实现跨月、跨年多选日期的功能的更多相关文章

  1. Vue文件封装日历组件

    封装就是要具有灵活性,样式自适应,调用的时候传入props就可以变成自己想要的样式. 效果展示网址:https://1963331542.github.io/ 源代码: <template> ...

  2. 基于Vue的简单日历组件

    日历组件 由于移动端项目中需要用到日历组件,网上找了下,没看到几个合适的,就尝试着自己写一个.然后发现也不是很复杂,目前只做了最基本的功能,大家也可以拿去做做二次开发. 如何写一个日历组件 基础效果如 ...

  3. IntelliJ IDEA 2017版 快捷键CTRL + SHIFT + A无效如何调试(详细的开启idea自动make功能 )

    1.前景描述 因为我把编译器的快捷键都设置成eclipse模式了,所以要做热部署的时候,需要CTRL + SHIFT + A --> 查找Registry --> 找到并勾选compile ...

  4. Flutter——ListView组件(平铺列表组件)

    ListView的常见参数: 名称 类型 说明 scrollDirection Axis Axis.horizontal 水平列表 Axis.vertical 垂直列表 padding EdgeIns ...

  5. 如何用vue封装一个防用户删除的平铺页面的水印组件

    需求 为了防止截图等安全问题,在web项目页面中生成一个平铺全屏的水印 要求水印内容为用户名,水印节点用户不能通过开发者工具等删除 效果 如上图 在body节点下插入水印DOM节点,水印节点覆盖在页面 ...

  6. vue初学实践之路——vue简单日历组件(1)

    ---恢复内容开始--- 最近做的项目有一个需求,需要有一个日历组件供预定功能使用,之前的代码过于繁琐复杂,所以我采用vue重写了这个组件. npm.vue等等安装. 只是一个简单的日历组件,所以并不 ...

  7. vue-calendar 基于 vue 2.0 开发的轻量,高性能日历组件

    vue-calendar-component 基于 vue 2.0 开发的轻量,高性能日历组件 占用内存小,性能好,样式好看,可扩展性强 原生 js 开发,没引入第三方库 Why Github 上很多 ...

  8. vue初学实践之路——vue简单日历组件(3)

    这一篇我们来实现管理员修改每一天剩余数量的功能. <div id="calendar"> <div id="left"> <spa ...

  9. Vue自定义日历组件

    今天给大家介绍Vue的日历组件,可自定义样式.日历类型及支持扩展,可自定义事件回调.Props数据传输. 线上demo效果 示例 Template: <Calendar :sundayStart ...

  10. 一个vue的日历组件

    说明: 1.基于element-ui开发的vue日历组件. 地址 更新: 1.增加value-format指定返回值的格式2.增加头部插槽自定义头部 <ele-calendar > < ...

随机推荐

  1. Javaweb学习笔记第十五弹--Listente概述、AJAX、Axiox、JSON

    Listener(监听器) 可以在application.session和request三个对象创建 Javaweb提供了8个监听器,其中较为典型的是ServletContextListener监听器 ...

  2. 10.4 提高叠加处理速度(2) (harib07d)

    ps:能力有限,若有错误及纰漏欢迎指正.交流 sheet_refreshsub void sheet_refreshsub(struct SHTCTL *ctl, int vx0, int vy0, ...

  3. 开发者实践丨Agora Home AI 音视频的未来

    本文作者是本届 RTE 2021 创新编程挑战赛获奖者,来自上海交通大学的李新春.他分享了本次参赛作品的构思.系统设计和开发的心得. 01 不得忽略的背景 从国家层面上讲,十四五期间我国人工智能发展的 ...

  4. CF859E题解

    题意简述 翻译很清楚了 题目解法 如果一个人想去的位置上原来坐着人,那么他要坐到这个位置上,就要把原来的人赶走. 原来的人被赶走了,就只能去想去的位置.如果那个位置上有人,又要把那个人赶走. 我们发现 ...

  5. openfoam并行通信探索(一)

    前言 最近在忙,快一两周没更新了,今天说下如何实现openfoam内的并行通信 为什么要并行通信 说到并行通信大家不要害怕啊,只是不同核之间数据传递,比如说咱们仿真开16个核,3号计算单元对4号计算单 ...

  6. Java面试——Netty

    一.BIO.NIO 和 AIO [1]阻塞 IO(Blocking I/O):同步阻塞I/O模式,当一条线程执行 read() 或者 write() 方法时,这条线程会一直阻塞直到读取一些数据或者写出 ...

  7. LockSupport 详解

    更多内容,前往IT-BLOG LockSupport 用来创建锁和其他同步类的基本线程阻塞原语.简而言之,当调用 LockSupport.park时,表示当前线程将会等待,直至获得许可,当调用 Loc ...

  8. Windows10 穿越火线手感和Windows7不一样

    如果是穿越火线或者其他FPS玩家,应该会感觉Win10和WIin7两者手感会有一定的区别.为什么升级了系统变菜了?心理作用?其实确实和系统有关系哦.我从Windows7升级到Windows10玩穿越火 ...

  9. 设置Mysql sort_buffer_size参数

    按照官网的解释:Each session that must perform a sort allocates a buffer of this size. sort_buffer_size is n ...

  10. 字符串算法--$\mathcal{KMP,Trie}$树

    \(\mathcal{KMP算法}\) 实际上,完全没必要从\(S\)的每一个字符开始,暴力穷举每一种情况,\(Knuth.Morris\)和\(Pratt\)对该算法进行了改进,称为KMP算法. 而 ...