在前端开发领域,实现一个功能完备的日历任务管理系统是一个常见且具有挑战性的任务。本文将深入剖析一段用于构建此类系统的 JavaScript 代码,带你一步步了解其核心逻辑和实现细节。
一、整体功能概述
这段代码实现了一个基于日历的任务管理系统,用户可以查看日历、切换月份、添加和编辑任务。系统会从 js/student.json
文件中获取任务数据,并将其存储在本地存储中。用户可以通过点击日历上的日期来添加新任务,也可以点击已有的任务进行编辑。同时,系统会对任务的时间冲突进行检查,确保同一时间内的任务数量不超过 4 个。
二、代码详细解析
1. 数据获取与初始化
let data;
let checkDate = 0;
let now = new Date();
$.ajax({
type: "get",
url: "js/student.json",
async: false,
success: function (res) {
classroomData = res.classroom_list;
if (!localStorage.getItem("clock")) {
localStorage.setItem("clock", JSON.stringify(res.date_clock));
data = JSON.parse(localStorage.getItem("clock"));
render(data);
globalData = data;
} else {
data = JSON.parse(localStorage.getItem("clock"));
render(data);
globalData = data;
}
},
error: function (e) {
console.log(e);
console.log(e.responseText);
},
});
- 首先,定义了全局变量
data
用于存储任务数据,checkDate
用于记录时间冲突的任务数量,now
用于获取当前日期时间。 - 使用
$.ajax
发起一个GET
请求,从js/student.json
文件中获取数据。请求设置为同步(async: false
),确保在数据获取完成后再继续执行后续代码。 - 请求成功后,检查本地存储中是否存在
clock
数据。如果不存在,则将从服务器获取的date_clock
数据存储到本地存储中,并解析为data
变量,然后调用render
函数进行日历渲染。如果本地存储中已经存在clock
数据,则直接解析并渲染。
2. 日历渲染函数 render
function render(data) {
let year = now.getFullYear();
let month = now.getMonth() + 1;
let self_dates = new Date(year, month, 0).getDate();
let firstDay = new Date(`${year}-${month}-1`).getDay();
let date_str = `${month}月${year}`;
document.getElementsByClassName("font")[0].innerHTML = date_str;
let str = ``;
// 上月日期填充
if (firstDay != 1) {
let last_month = now.getMonth() - 1 < 0 ? 12 : now.getMonth();
let last_year = month == 1 ? year - 1 : year;
let last_dates = new Date(last_year, last_month, 0).getDate();
for (let i = 0; i < firstDay - 1; i++) {
str =
`
<div class="items" style="color:gray;" data-time="${last_year}-${last_month < 10 ? "0" + last_month : last_month
}-${last_dates}">
<div class="date">${last_dates}</div>
</div>` + str;
last_dates--;
}
}
// 当前月份日期填充
for (let i = 0; i < self_dates; i++) {
str += `
<div class="items ${(() => {
let now = new Date();
if (
now.getFullYear() == year &&
now.getMonth() + 1 == month &&
now.getDate() == i + 1
) {
return "light";
} else {
return "";
}
})()}"
data-time="${year}-${month < 10 ? "0" + month : month}-${i + 1 < 10 ? "0" + (i + 1) : i + 1
}" >
<div class="date">${i + 1}</div>`;
for (let j in data) {
let start = new Date(data[j].start);
let end = new Date(data[j].end);
let nows = new Date(
`${year}-${month < 10 ? "0" + month : month}-${i + 1}`
);
// 清除时间部分,确保只比较日期
start.setHours(0, 0, 0, 0);
end.setHours(0, 0, 0, 0);
nows.setHours(0, 0, 0, 0);
// 日期比较
if (nows <= end && nows >= start) {
if (nows.getTime() === start.getTime()) {
str += `<div class="plan" dataId="${data[j].id}" dataTitle="${data[j].title}" dataEnd="${data[j].end}" dataScore="${data[j].score}" dataStart=${data[j].start} onclick="edit(${data[j].id},${data[j].start})">${data[j].title}</div>`;
} else if (nows.getTime() === end.getTime()) {
str += `<div dataTitle="${data[j].title}" dataEnd="${data[j].end}" dataStart=${data[j].start} dataScore="${data[j].score}" class="plan" onclick="edit(${data[j].id},${data[j].start})">>>></div>`;
} else {
str += `<div dataTitle="${data[j].title}" dataEnd="${data[j].end}" dataStart=${data[j].start} dataScore="${data[j].score}" class="plan" onclick="edit(${data[j].id},${data[j].start})"></div>`;
}
}
}
str += `</div>`;
}
// 填充下个月的日期
let next_month_first_day = new Date(year, month, 1).getDay();
let next_month_days = new Date(year, month + 1, 0).getDate();
for (
let i = 0;
i < 8 - (next_month_first_day % 8) && i < next_month_days;
i++
) {
str += `
<div class="items" style="color:gray;" data-time="${year}-${month + 1 < 10 ? "0" + (month + 1) : month + 1
}-${i + 1 < 10 ? "0" + (i + 1) : i + 1}">
<div class="date">${i + 1}</div>
</div>`;
}
document.getElementById("content").innerHTML = str;
}
- 该函数用于渲染日历界面。首先获取当前年份、月份、当月天数和当月第一天是星期几,并将日期信息显示在页面上。
- 接着进行上月日期填充,如果当月第一天不是星期一,则填充上月的部分日期,日期颜色设为灰色。
- 然后填充当前月份的日期,对于当天的日期添加
light
类进行高亮显示。同时,遍历任务数据,检查每个日期是否有任务,如果有则根据任务的开始和结束日期添加相应的任务提示元素。 - 最后填充下个月的部分日期,同样将日期颜色设为灰色。将生成的 HTML 字符串插入到
id
为content
的元素中。
3. 日期切换功能
// 显示下一月的点击事件
function next() {
now.setMonth(now.getMonth() + 1);
render(data);
}
// 显示上一月的点击事件
function last() {
now.setMonth(now.getMonth() - 1);
render(data);
}
// 返回今天的日期并刷新页面
function toady() {
now = new Date();
render(data);
}
next
函数将当前日期的月份加 1,然后调用render
函数重新渲染日历,显示下一个月的内容。last
函数将当前日期的月份减 1,同样调用render
函数显示上一个月的日历。toady
函数将now
重置为当前日期,再调用render
函数回到当天的日历显示
4. 任务交互功能
点击日期添加任务
$(document).on("click", ".items", function (e) {
selectedDate = e.target.closest(".items").getAttribute("data-time");
console.log(selectedDate);
$(".curtain").eq(0).fadeIn(200);
$(".append_title_font").eq(0).html("打卡添加");
$("#dates").val(selectedDate);
});
点击任务编辑
let editId;
function edit(id, start) {
event.stopPropagation();
selectedDate = start;
console.log(start);
editId = id;
for (let i = 0; i < data.length; i++) {
if (data[i].id == id) {
console.log(data[i]);
$("#date").val(data[i].end);
$("#dates").val(data[i].start);
$("#integral").val(data[i].score);
$("#task").val(data[i].title);
$(".curtain").eq(0).fadeIn(200);
$(".append_title_font").eq(0).html("打卡编辑");
}
}
}
当用户点击任务元素时,阻止事件冒泡,记录任务的 id
到 editId
,并将任务的起始日期、结束日期、积分和标题填充到输入框中,显示弹出层并切换到编辑模式。
确认添加 / 编辑任务
function append() {
maxId = Math.max(...data.map((item) => parseInt(item.id)));
if ($("#date").val() == "") {
$("#start_border").css("border", "1px solid #FF4D00");
$("#date_error").css("display", "block");
}
let inputDate = new Date($("#date").val());
if (new Date(selectedDate) > inputDate) {
$("#start_border").css("border", "1px solid #FF4D00");
$("#date_error").css("display", "block");
$("#date_error").html("结束日期不能小于选择的时间");
return;
}
if ($("#integral").val() == "") {
$("#integral").css("border", "1px solid #FF4D00");
$("#integral_error").css("display", "block");
}
if ($("#task").val() == "") {
$("#task").css("border", "1px solid #FF4D00");
$("#task_error").css("display", "block");
}
let changeFirst = changeDate(new Date(selectedDate));
let changeThi = changeDate(inputDate);
for (let i = 0; i < data.length; i++) {
if (
changeFirst <= data[i].start &&
changeThi >= data[i].end &&
$("#date").val() != `` &&
$("#integral").val() != `` &&
$("#task").val() != ``
) {
checkDate++;
}
}
if (checkDate >= 4) {
showAlerts(`.ErrorModal`, `10%`, `同一时间只能添加四个任务`);
checkDate = 0;
return;
}
if (
$("#date").val() == "" ||
$("#integral").val() == "" ||
$("#task").val() == ""
) {
return;
}
if (editId == undefined) {
let task_obj = {
id: maxId + 1,
title: $("#task").val(),
start: selectedDate,
end: $("#date").val(),
score: $("#integral").val(),
};
data.push(task_obj);
localStorage.setItem("clock", JSON.stringify(data));
let overlappingTasksCount = 0;
for (let i = 0; i < data.length; i++) {
let existingTaskStart = new Date(data[i].start);
let existingTaskEnd = new Date(data[i].end);
let newTaskStart = new Date(selectedDate);
let newTaskEnd = new Date($("#inputdate").val());
if (
(newTaskStart >= existingTaskStart &&
newTaskStart <= existingTaskEnd) ||
(newTaskEnd >= existingTaskStart && newTaskEnd <= existingTaskEnd) ||
(existingTaskStart >= newTaskStart &&
existingTaskStart <= newTaskEnd) ||
(existingTaskEnd >= newTaskStart && existingTaskEnd <= newTaskEnd)
) {
overlappingTasksCount++;
}
}
if (overlappingTasksCount > 4) {
data.pop();
localStorage.setItem("clock", JSON.stringify(data));
showAlerts(`.ErrorModal`, `10%`, `同一时间只能添加四个任务`);
return;
}
render(data);
showAlerts(`.correctModal`, `10%`, `添加成功`);
hide();
} else {
$(".curtain").eq(0).fadeIn(200);
$(".append_title_font").eq(0).html("打卡编辑");
for (let i = 0; i < data.length; i++) {
if (data[i].id == editId) {
data[i].end = $("#date").val();
data[i].score = $("#integral").val();
data[i].title = $("#task").val();
}
}
localStorage.setItem("clock", JSON.stringify(data));
showAlerts(`.correctModal`, `10%`, `编辑成功`);
render(data);
hide();
editId = undefined;
}
}
- 该函数首先检查输入框是否为空,以及结束日期是否小于起始日期,如果有问题则显示相应的错误提示。
- 然后进行时间冲突检查,通过
changeDate
函数格式化日期,遍历任务数据,统计时间冲突的任务数量。如果冲突任务数量超过 4 个,则显示错误提示并停止操作。 - 如果是添加新任务(
editId
未定义),生成新的任务对象并添加到data
数组中,更新本地存储。再次检查时间冲突,如果超过 4 个则撤销添加操作。添加成功后,重新渲染日历并显示成功提示。 - 如果是编辑任务(
editId
已定义),找到对应任务并更新其信息,更新本地存储,重新渲染日历并显示成功提示。
5. 输入框交互与辅助函数
$("#date").on(`blur`, function () {
setTimeout(function () {
if ($("#date").val() !== "") {
$("#start_border").css("border", "");
$("#date_error").css("display", "none");
}
}, 500);
$("#task").focus();
setTimeout(function () {
$("#task").blur();
}, 100);
});
$("input").change(function () {
if ($("#date").val() !== "") {
$("#start_border").css("border", "");
$("#date_error").css("display", "none");
}
if ($("#integral").val() !== "") {
$("#integral").css("border", "");
$("#integral_error").css("display", "none");
}
if ($("#task").val() !== "") {
$("#task").css("border", "");
$("#task_error").css("display", "none");
}
});
function hide() {
$(".curtain").eq(0).fadeOut(200);
$("#start_border").css("border", "");
$("#date_error").css("display", "none");
$("#integral").css("border", "");
$("#integral_error").css("display", "none");
$("#task").css("border", "");
$("#task_error").css("display", "none");
$("#date").val("");
$("#integral").val("");
$("#task").val("");
editId = undefined;
}
function changeDate(date) {
let year = date.getFullYear();
let month = (date.getMonth() + 1).toString().padStart(2, "0");
let day = date.getDate().toString().padStart(2, "0");
return `${year}-${month}-${day}`;
}
$("#date").on('blur')
事件处理函数在date
输入框失去焦点时,延迟 500 毫秒检查输入框的值,如果不为空则清除边框错误样式和错误提示信息。同时,将焦点切换到task
输入框,再延迟 100 毫秒让其失去焦点。$("input").change()
事件处理函数在输入框的值发生变化时,检查各个输入框的值,如果不为空则清除相应的边框错误样式和错误提示信息。hide
函数用于隐藏弹出层,清除输入框的错误样式和提示信息,清空输入框的值,并将editId
重置为undefined
。changeDate
函数将Date
对象格式化为YYYY-MM-DD
的字符串形式。
三、总结
通过对这段代码的详细解析,我们了解了如何构建一个交互式的日历任务管理系统。从数据获取、日历渲染到任务的添加、编辑和时间冲突检查,每个环节都有其独特的实现逻辑。在实际开发中,我们可以根据需求对代码进行进一步的优化和扩展,例如添加更多的任务信息字段、优化界面样式等。希望本文能为你在前端开发中实现类似功能提供一些有价值的参考