Files
timeline/public/js/calendar.js
2025-11-11 14:42:42 +08:00

259 lines
7.1 KiB
JavaScript

import state from './state.js';
import { fetchCalendar, saveCalendar, ApiError } from './api.js';
import { showToast } from './toast.js';
export default class CalendarView {
constructor({ onUnauthorized } = {}) {
this.calendarEl = document.getElementById('calendar');
this.monthDisplayEl = document.getElementById('currentMonth');
this.prevBtn = document.getElementById('prevMonth');
this.nextBtn = document.getElementById('nextMonth');
this.todayBtn = document.getElementById('todayBtn');
this.weekdays = ['日', '一', '二', '三', '四', '五', '六'];
this.monthNames = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'];
this.onUnauthorized = onUnauthorized;
this.saveDelay = 500;
}
init() {
this.bindNavigation();
this.renderCalendar();
}
bindNavigation() {
if (this.prevBtn) {
this.prevBtn.addEventListener('click', () => {
state.currentDate.setMonth(state.currentDate.getMonth() - 1);
this.renderCalendar();
});
}
if (this.nextBtn) {
this.nextBtn.addEventListener('click', () => {
state.currentDate.setMonth(state.currentDate.getMonth() + 1);
this.renderCalendar();
});
}
if (this.todayBtn) {
this.todayBtn.addEventListener('click', () => {
state.currentDate = new Date();
this.renderCalendar();
});
}
}
async loadData() {
if (!state.currentServer || !state.token) {
return;
}
try {
showToast('正在加载...', 'info');
const result = await fetchCalendar(state.currentServer, state.token);
state.markedDates = result.markedDates || {};
this.renderCalendar();
showToast('数据加载成功', 'success');
} catch (error) {
if (error instanceof ApiError && (error.status === 401 || error.status === 403)) {
showToast('认证已过期,请重新登录', 'error');
if (this.onUnauthorized) {
this.onUnauthorized();
}
return;
}
showToast(error.message || '加载失败', 'error');
}
}
clearData() {
state.markedDates = {};
this.renderCalendar();
}
renderCalendar() {
if (!this.calendarEl || !this.monthDisplayEl) return;
const currentDate = state.currentDate;
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
this.calendarEl.innerHTML = '';
this.weekdays.forEach(day => {
const weekdayEl = document.createElement('div');
weekdayEl.className = 'weekday';
weekdayEl.textContent = day;
this.calendarEl.appendChild(weekdayEl);
});
this.monthDisplayEl.textContent = `${year}${this.monthNames[month]}`;
const firstDay = this.getFirstDayOfMonth(currentDate);
const daysInMonth = this.getDaysInMonth(currentDate);
const prevMonthDate = new Date(year, month, 0);
const daysInPrevMonth = prevMonthDate.getDate();
for (let i = firstDay - 1; i >= 0; i--) {
const day = daysInPrevMonth - i;
const date = new Date(year, month - 1, day);
this.calendarEl.appendChild(this.createDayElement(date, day, true));
}
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(year, month, day);
this.calendarEl.appendChild(this.createDayElement(date, day, false));
}
const totalCells = this.calendarEl.children.length - this.weekdays.length;
const remainingCells = 42 - totalCells;
for (let day = 1; day <= remainingCells; day++) {
const date = new Date(year, month + 1, day);
this.calendarEl.appendChild(this.createDayElement(date, day, true));
}
}
createDayElement(date, dayNumber, isOtherMonth) {
const dayEl = document.createElement('div');
dayEl.className = 'day';
if (isOtherMonth) {
dayEl.classList.add('other-month');
}
// Check if the day is in the future
if (this.isFutureDate(date)) {
dayEl.classList.add('future-day');
}
if (this.isToday(date)) {
dayEl.classList.add('today');
}
const status = this.getDateStatus(date);
if (status !== 'none') {
dayEl.classList.add(status);
}
const dayNumberEl = document.createElement('div');
dayNumberEl.className = 'day-number';
dayNumberEl.textContent = dayNumber;
dayEl.appendChild(dayNumberEl);
if (status !== 'none') {
const markEl = document.createElement('div');
markEl.className = 'day-mark';
markEl.textContent = status === 'completed' ? '✓' : '○';
dayEl.appendChild(markEl);
}
dayEl.addEventListener('click', () => {
if (!isOtherMonth) {
this.toggleDateStatus(date);
}
});
return dayEl;
}
toggleDateStatus(date) {
if (!state.currentServer || !state.token) {
showToast('请先选择服务器并登录', 'error');
return;
}
const dateKey = this.formatDate(date);
const currentStatus = state.markedDates[dateKey] || 'none';
let nextStatus;
switch (currentStatus) {
case 'none':
nextStatus = 'completed';
break;
case 'completed':
nextStatus = 'partial';
break;
case 'partial':
default:
nextStatus = 'none';
break;
}
if (nextStatus === 'none') {
delete state.markedDates[dateKey];
} else {
state.markedDates[dateKey] = nextStatus;
}
this.renderCalendar();
this.scheduleSave();
}
scheduleSave() {
if (!state.currentServer || !state.token) return;
if (state.saveTimeoutId) {
clearTimeout(state.saveTimeoutId);
}
state.saveTimeoutId = setTimeout(() => {
this.persistMarkedDates();
}, this.saveDelay);
}
async persistMarkedDates() {
state.saveTimeoutId = null;
try {
await saveCalendar(state.currentServer, state.token, state.markedDates);
showToast('数据已保存', 'success');
} catch (error) {
if (error instanceof ApiError && (error.status === 401 || error.status === 403)) {
showToast('认证已过期,请重新登录', 'error');
if (this.onUnauthorized) {
this.onUnauthorized();
}
return;
}
showToast(error.message || '保存失败', 'error');
}
}
getFirstDayOfMonth(date) {
return new Date(date.getFullYear(), date.getMonth(), 1).getDay();
}
getDaysInMonth(date) {
return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
}
getDateStatus(date) {
const dateKey = this.formatDate(date);
return state.markedDates[dateKey] || 'none';
}
isToday(date) {
const today = new Date();
return date.getFullYear() === today.getFullYear() &&
date.getMonth() === today.getMonth() &&
date.getDate() === today.getDate();
}
// Check if a date is in the future
isFutureDate(date) {
const today = new Date();
today.setHours(0, 0, 0, 0);
const checkDate = new Date(date);
checkDate.setHours(0, 0, 0, 0);
return checkDate > today;
}
formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
}