日历日程
页面布局
页面布局(从上到下):
- 日期切换,周/月类型切换
- 日历日程(周):日历(左右滚动切换周)+日程(上下滚动切换周)
- 日历日程(月):日历(内部混合日程)(左右滚动切换月),月日历下方展示当天日程(3条),点击则悬浮窗展示所有日程,跨天日程展示
主要功能难点分析
左右滑动切换周/月
: 使用轮播图组件切换,表现更真实。为了实现左右滑动时,看到上/下周日期,需要提前存储上/下周的日期。
上下滑动切换周/月
: 配合下拉加载和上拉刷新,实现上下滑动切换周/月。或者通过当前滚动位置(距离顶部30px,距离底部30px)进行上下滑动切换周/月。
当前日期切换
: 在切换当前日期时,需要滚动到当前日期位置事件,或者加载当前日期所在周事件然后滚动。因为很多地方都会触发日期更新,所有应该使用日期对象存储当前日期,同时添加触发日期更新的类型,便于后续判断。
跨天日程的展示
:
为每个日程定义一个间隔天数diff,该属性表示日程的开始日期和结束日期(或者是周六,一周的结束日,以最小的为准)相差的天数。
定义一个日程等级属性level,该属性表示日程在日历中展示的位置(从上到下)。在同一天中,diff越大的日程,level越小,就会越排在上面。
定义一个日程等级数组levelArr,该数组在一周的开始日初始化,每添加一个日程属性level,就会将该日程的id添加到levelArr中。当日程的结束日期等于当前日期时,需要释放该日程在levelArr中的位置,置空。以让后续的日程可以占据该位置。
定义一个释放日程数组needReleaseIdArr,用于存放需要释放的levelArr对应的索引,以便在循环修改某日日程结束之后,将该日程的id对应的索引从levelArr中移除。
html结构展示溢出内容
:只展示日程的第一天所占位置,后续相同日程需要隐藏。这时为了防止出现布局混乱,需要固定父级容器高宽,同时给每个日程项div设置width(通过diff属性),让其溢出展示内容。
固定列布局
: 使用grid布局,为了使每列宽度相等且固定,需要设置grid-template-columns: repeat(7, minmax(0, 1fr));
,
事件数据便捷携带的处理
: 为了便于在事件当中便于处于点击元素的相关信息,可以给每个日程项添加相应的自定义属性data-*
。类似微信小程序用法。
周日程点击日期滑倒指定日程列表位置
: 给日期定义 一个 自定义属性data-date="2024-06-01"
,以及一个listRef,在点击日期时,当点击日期时,获取到自定义属性data-date="2024-06-01"
,然后通过该日期去匹配日程列表listRef中的元素,找到则滚动到该位置。
上下滑动日程列表时,周日期对应高亮显示
: 在日程列表滚动时,获取到最后一个在视口中的日期元素,然后高亮该日期。计算方式通过:子元素顶部到上一个relative元素的距离 - 上一个relative元素的滑动距离 < 上一个relative元素的视口高度 - 100计算,即child.offsetTop - eventBoxRef.value.scrollTop < eventBoxRef.value.clientHeight - 100
权限区分
: 为了让用户看到不一样的内容,可通过点击链接的内容进行鉴权,不同的内容赋予不一样的权限。
核心函数
// 跨天事件的处理,给items的每个项,添加等级level,便于html展示跨天日程
// listMap的结构如下:
/*
{
"2024-06-01": {
"items": [
{
"id": 1,
"startTime": "2024-06-01 00:00:00",
"endTime": "2024-06-02 00:00:00",
"title": "跨天日程1",
"content": "跨天日程1的内容",
// 日程在一天中的level,从上到下排列
"level": 1,
// 日程跨天(最多到一周的结束)的天数,便于结合level排序
// 同时为middle为false的div添加width: (diff + 1) * 100%
"diff": 1,
// 该日程是否是非开始日期,如果是,则给定一个class,隐藏它
"middle": true,
},
]
}
}
*/
export function handleCrossDayEvent(listMap) {
// 存放已经存储等级的item id,当到结束时间后,对应index的id会置为空串
let levelArr = []
for (const key in listMap) {
const curT = getCurrentDate(new Date(key))
// 如果是周日(一周开头),则重置等级
if (curT.weekIndex == 0) {
levelArr = []
}
// 需要释放的id数组
const newItems = []
const needReleaseIdArr = []
const levelKeyArr = []
let items = listMap[key].items || [];
// 根据当前日期到endTime的天数排序,如果天数大于当前日期到周六的天数,则根据当前日期到周六的天数排序
items = items.map(item => {
const startT = getCurrentDate(new Date(item.startTime))
const endT = getCurrentDate(new Date(item.endTime))
const diff = endT.day - curT.day
const diff2 = 6 - curT.weekIndex
item.diff = diff2 > diff ? diff : diff2
const isIn = curT.string >= startT.string && curT.string <= endT.string ? true : false;
const isInStart = curT.string == startT.string;
const isWeekSun = curT.weekIndex == 0
if (!isInStart && !isWeekSun) {
item.middle = true;
}
// 判断是否在起始时间范围内,如果在
if (isIn) {
// 查找levelArr中是否含有item.id
const findIndex = levelArr.findIndex(i => i == item.id)
if (findIndex == -1) {
// 查找第一个元素为空的
const findIndex1 = levelArr.findIndex(i => {
return i == ''
})
// 如果找到了,将id放到第一个元素为空的index中
if (findIndex1 != -1) {
levelArr[findIndex1] = item.id;
levelKeyArr[findIndex1] = item.id;
newItems[findIndex1] = item
item.level = findIndex1 + 1
}
// 如果没找到,则将id放到levelArr的最后面
else {
levelArr.push(item.id)
levelKeyArr.push(item.id)
newItems.push(item)
item.level = levelArr.length;
}
} else {
item.level = findIndex + 1
newItems.push(item)
}
// 如果是最后一个,则将levelArr对应的元素置为空
const isInEnd = curT.string == endT.string;
if (isInEnd) {
const findIndex33 = levelArr.findIndex(i => i == item.id)
needReleaseIdArr.push(findIndex33)
}
} else {
item.level = ''
}
return item
})
items = items.sort((a, b) => {
return b.diff - a.diff
})
// items循环结束后,如果需要释放(当前日期是结束日期),则将levelArr中对应的元素置为空串
needReleaseIdArr.forEach(index => {
levelArr[index] = ''
})
// 对items进行去重,根据id
items = items.filter((item, index, arr) => {
return arr.findIndex(i => i.id == item.id) === index
})
// 将items放在对象中,level为key
let mapNewestItems = {}
let newestItems = []
items.forEach(item => {
mapNewestItems[item.level] = item
})
// 获取到对象中最大的level的数值
const keys = Object.keys(mapNewestItems)
const max = Math.max(...keys)
// 根据最大level数,将中间level数为空的,加上一个空的item对象,用于占位
for (let i = 1; i <= max; i++) {
if (!mapNewestItems[i]) {
newestItems[i - 1] = {
level: i,
id: i + 1000,
zhanwei: true,
}
} else {
newestItems[i - 1] = mapNewestItems[i]
}
}
listMap[key].itemLength = (items || []).length
listMap[key].items = newestItems
}
return listMap;
}
// 获取当天日期
export function getCurrentDate(datea = new Date()) {
const date = datea;
return {
date: date,
year: date.getFullYear(),
month: date.getMonth() + 1,
day: date.getDate(),
week: weekDays[date.getDay()],
weekIndex: date.getDay(),
weekOfYear: getWeekOfYear(date),
string: formatDate(date, 'yyyy-MM-dd'),
stringFull: formatDate(date, 'yyyy-MM-dd HH:mm'),
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// 滚动到特定位置
function scrollToPosition() {
const parent = eventBoxRef.value;
// 获取parent html子元素的数量
if (!parent || parent.childElementCount == 0 || !eventListRef.value || eventListRef.value.length == 0) {
return;
}
const child = eventListRef.value.find(item => {
return item.classList.contains('d-' + currentDateObj.value.string)
})
if (!child) {
return;
}
parent.scrollTo({
top: child.offsetTop - 12,//需要父元素设置postion(relative、absolute、fixed)
behavior: "smooth"
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
其他便捷性操作
- 定义全局可用的scss变量,比如日程的类型颜色。