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}`; } }