diff --git a/public/js/api.js b/public/js/api.js index 194a1d5..b8bbaba 100644 --- a/public/js/api.js +++ b/public/js/api.js @@ -53,14 +53,22 @@ export async function fetchCurrentUser(server, token) { return request(server, '/api/auth/me', { token }); } -export async function fetchCalendar(server, token) { - return request(server, '/api/calendar', { token }); +export async function fetchCalendar(server, token, year, month) { + const params = new URLSearchParams(); + if (year !== undefined && month !== undefined) { + params.append('year', year); + params.append('month', month); + } + const queryString = params.toString(); + const path = queryString ? `/api/calendar?${queryString}` : '/api/calendar'; + return request(server, path, { token }); } -export async function saveCalendar(server, token, markedDates) { + +export async function saveCalendar(server, token, markedDates, deletedDates) { return request(server, '/api/calendar', { method: 'POST', token, - body: { markedDates } + body: { markedDates, deletedDates } }); } diff --git a/public/js/calendar.js b/public/js/calendar.js index b472c89..817eeef 100644 --- a/public/js/calendar.js +++ b/public/js/calendar.js @@ -26,6 +26,8 @@ export default class CalendarView { this.prevBtn.addEventListener('click', () => { state.currentDate.setMonth(state.currentDate.getMonth() - 1); this.renderCalendar(); + // Load data for the new month + this.loadData(); }); } @@ -33,6 +35,8 @@ export default class CalendarView { this.nextBtn.addEventListener('click', () => { state.currentDate.setMonth(state.currentDate.getMonth() + 1); this.renderCalendar(); + // Load data for the new month + this.loadData(); }); } @@ -40,6 +44,8 @@ export default class CalendarView { this.todayBtn.addEventListener('click', () => { state.currentDate = new Date(); this.renderCalendar(); + // Load data for the current month + this.loadData(); }); } } @@ -51,21 +57,12 @@ export default class CalendarView { try { showToast('正在加载...', 'info'); - const result = await fetchCalendar(state.currentServer, state.token); - // Convert the new data structure to the old one for backward compatibility - const markedDates = {}; - for (const [date, data] of Object.entries(result.markedDates || {})) { - if (typeof data === 'string') { - // Old format - just status - markedDates[date] = data; - } else if (data && typeof data === 'object') { - // New format - object with status and updatedAt - markedDates[date] = data.status; - } - } - state.markedDates = markedDates; - // Store the full data including timestamps - state.markedDatesWithTimestamps = result.markedDates || {}; + // Pass current year and month to only fetch data for the displayed month + const year = state.currentDate.getFullYear(); + const month = state.currentDate.getMonth() + 1; // getMonth() returns 0-11, need 1-12 + const result = await fetchCalendar(state.currentServer, state.token, year, month); + // Merge with existing data + Object.assign(state.markedDates, result.markedDates || {}); this.renderCalendar(); showToast('数据加载成功', 'success'); } catch (error) { @@ -82,7 +79,7 @@ export default class CalendarView { clearData() { state.markedDates = {}; - state.markedDatesWithTimestamps = {}; + state.deletedDates = []; this.renderCalendar(); } @@ -169,7 +166,7 @@ export default class CalendarView { if (timestamp) { // Store the ISO timestamp dayEl.dataset.modificationTime = timestamp; - + // Add hover events for custom tooltip dayEl.addEventListener('mouseenter', (e) => { const isoTime = e.currentTarget.dataset.modificationTime; @@ -185,7 +182,7 @@ export default class CalendarView { }); this.showTooltip(e, `修改时间: ${localTime}`); }); - + dayEl.addEventListener('mouseleave', () => { this.hideTooltip(); }); @@ -209,7 +206,7 @@ export default class CalendarView { tip.parentNode.removeChild(tip); } }); - + // Create new tooltip element with unique ID const tooltip = document.createElement('div'); const uniqueId = 'tooltip-' + Date.now(); @@ -217,19 +214,19 @@ export default class CalendarView { tooltip.className = 'tooltip'; tooltip.textContent = content; document.body.appendChild(tooltip); - + // Position tooltip relative to the target element const rect = event.currentTarget.getBoundingClientRect(); tooltip.style.left = (rect.left + rect.width / 2) + 'px'; tooltip.style.top = (rect.top - 10) + 'px'; tooltip.style.transform = 'translate(-50%, -100%)'; - + // Show tooltip with a slight delay to ensure position is set requestAnimationFrame(() => { tooltip.classList.add('show'); }); } - + hideTooltip() { const tooltips = document.querySelectorAll('.tooltip'); tooltips.forEach(tooltip => { @@ -307,7 +304,8 @@ export default class CalendarView { } const dateKey = this.formatDate(date); - const currentStatus = state.markedDates[dateKey] || 'none'; + const dateData = state.markedDates[dateKey]; + const currentStatus = (dateData && dateData.status) || 'none'; let nextStatus; switch (currentStatus) { @@ -325,11 +323,21 @@ export default class CalendarView { if (nextStatus === 'none') { delete state.markedDates[dateKey]; + // Track this deletion + if (!state.deletedDates.includes(dateKey)) { + state.deletedDates.push(dateKey); + } // Clear pending tooltip for removed marks this.pendingTooltipDate = null; this.hideTooltip(); } else { - state.markedDates[dateKey] = nextStatus; + // Store status, timestamp will be updated by server on save + state.markedDates[dateKey] = { status: nextStatus }; + // Remove from deleted dates if it was there + const deleteIndex = state.deletedDates.indexOf(dateKey); + if (deleteIndex !== -1) { + state.deletedDates.splice(deleteIndex, 1); + } // Store the date for tooltip after save completes this.pendingTooltipDate = dateKey; } @@ -353,7 +361,17 @@ export default class CalendarView { async persistMarkedDates() { state.saveTimeoutId = null; try { - await saveCalendar(state.currentServer, state.token, state.markedDates); + // Convert state format { date: { status, updatedAt } } to API format { date: status } + const markedDatesForAPI = {}; + for (const [date, data] of Object.entries(state.markedDates)) { + if (data && typeof data === 'object' && data.status) { + markedDatesForAPI[date] = data.status; + } + } + + await saveCalendar(state.currentServer, state.token, markedDatesForAPI, state.deletedDates); + // Clear the deleted dates array after successful save + state.deletedDates = []; showToast('数据已保存', 'success'); // Reload data to get updated timestamps await this.loadData(); @@ -384,12 +402,16 @@ export default class CalendarView { getDateStatus(date) { const dateKey = this.formatDate(date); - return state.markedDates[dateKey] || 'none'; + const dateData = state.markedDates[dateKey]; + if (dateData && typeof dateData === 'object') { + return dateData.status || 'none'; + } + return 'none'; } getDateTimestamp(date) { const dateKey = this.formatDate(date); - const dateData = state.markedDatesWithTimestamps[dateKey]; + const dateData = state.markedDates[dateKey]; if (dateData && typeof dateData === 'object' && dateData.updatedAt) { return dateData.updatedAt; } diff --git a/public/js/state.js b/public/js/state.js index b734708..2b035d0 100644 --- a/public/js/state.js +++ b/public/js/state.js @@ -1,7 +1,7 @@ const state = { currentDate: new Date(), - markedDates: {}, - markedDatesWithTimestamps: {}, + markedDates: {}, // Now stores objects with { status, updatedAt } + deletedDates: [], // Track dates that have been explicitly deleted currentServer: '', token: '', user: null, diff --git a/server/routes/calendar.js b/server/routes/calendar.js index cb607b4..520320e 100644 --- a/server/routes/calendar.js +++ b/server/routes/calendar.js @@ -5,31 +5,58 @@ const router = express.Router(); router.get('/', (req, res) => { const userId = req.user.userId; + const { year, month } = req.query; - db.all( - 'SELECT date, status, updated_at FROM calendar_marks WHERE user_id = ?', - [userId], - (err, rows) => { - if (err) { - return res.status(500).json({ success: false, error: '获取数据失败' }); - } + // Build query based on whether year and month are provided + let query = 'SELECT date, status, updated_at FROM calendar_marks WHERE user_id = ?'; + const params = [userId]; - const markedDates = {}; - rows.forEach((row) => { - markedDates[row.date] = { - status: row.status, - updatedAt: row.updated_at - }; - }); + // If year and month are provided, filter by that month plus adjacent months + if (year && month) { + const yearNum = parseInt(year, 10); + const monthNum = parseInt(month, 10); - res.json({ success: true, markedDates }); + // Validate year and month + if (isNaN(yearNum) || isNaN(monthNum) || monthNum < 1 || monthNum > 12) { + return res.status(400).json({ success: false, error: '无效的年份或月份' }); } - ); + + // Calculate the date range: previous month to next month + // This covers all dates that might be visible in the calendar view + const prevMonth = monthNum === 1 ? 12 : monthNum - 1; + const prevYear = monthNum === 1 ? yearNum - 1 : yearNum; + const nextMonth = monthNum === 12 ? 1 : monthNum + 1; + const nextYear = monthNum === 12 ? yearNum + 1 : yearNum; + + const startDate = `${prevYear}-${String(prevMonth).padStart(2, '0')}-01`; + // Get the last day of next month + const lastDayOfNextMonth = new Date(nextYear, nextMonth, 0).getDate(); + const endDate = `${nextYear}-${String(nextMonth).padStart(2, '0')}-${String(lastDayOfNextMonth).padStart(2, '0')}`; + + query += ' AND date >= ? AND date <= ?'; + params.push(startDate, endDate); + } + + db.all(query, params, (err, rows) => { + if (err) { + return res.status(500).json({ success: false, error: '获取数据失败' }); + } + + const markedDates = {}; + rows.forEach((row) => { + markedDates[row.date] = { + status: row.status, + updatedAt: row.updated_at + }; + }); + + res.json({ success: true, markedDates }); + }); }); router.post('/', (req, res) => { const userId = req.user.userId; - const { markedDates } = req.body || {}; + const { markedDates, deletedDates } = req.body || {}; if (!markedDates || typeof markedDates !== 'object') { return res.status(400).json({ success: false, error: '无效的数据格式' }); @@ -40,75 +67,67 @@ router.post('/', (req, res) => { return new Date().toISOString(); }; - const dbOperations = Object.entries(markedDates).map(([date, status]) => { - return new Promise((resolve, reject) => { - // First check if this date exists and if status has changed - db.get( - 'SELECT status FROM calendar_marks WHERE user_id = ? AND date = ?', - [userId, date], - (err, row) => { - if (err) { - return reject(err); - } - - // Only update timestamp if status changed or it's a new entry - const statusChanged = !row || row.status !== status; - - if (statusChanged) { - // Status changed - update with new timestamp - const currentTimestamp = getLocalTimestamp(); - db.run( - `INSERT INTO calendar_marks (user_id, date, status, updated_at) - VALUES (?, ?, ?, ?) - ON CONFLICT(user_id, date) DO UPDATE SET - status = excluded.status, - updated_at = excluded.updated_at`, - [userId, date, status, currentTimestamp], - (err) => { - if (err) reject(err); - else resolve(); - } - ); - } else { - // Status unchanged - keep existing timestamp - db.run( - `INSERT INTO calendar_marks (user_id, date, status, updated_at) - VALUES (?, ?, ?, (SELECT updated_at FROM calendar_marks WHERE user_id = ? AND date = ?)) - ON CONFLICT(user_id, date) DO UPDATE SET - status = excluded.status`, - [userId, date, status, userId, date], - (err) => { - if (err) reject(err); - else resolve(); - } - ); - } - } - ); - }); - }); + const dbOperations = []; - const dates = Object.keys(markedDates); - if (dates.length > 0) { - const placeholders = dates.map(() => '?').join(','); + // Update or insert marked dates + Object.entries(markedDates).forEach(([date, status]) => { dbOperations.push( new Promise((resolve, reject) => { - db.run( - `DELETE FROM calendar_marks WHERE user_id = ? AND date NOT IN (${placeholders})`, - [userId, ...dates], - (err) => { - if (err) reject(err); - else resolve(); + // First check if this date exists and if status has changed + db.get( + 'SELECT status FROM calendar_marks WHERE user_id = ? AND date = ?', + [userId, date], + (err, row) => { + if (err) { + return reject(err); + } + + // Only update timestamp if status changed or it's a new entry + const statusChanged = !row || row.status !== status; + + if (statusChanged) { + // Status changed - update with new timestamp + const currentTimestamp = getLocalTimestamp(); + db.run( + `INSERT INTO calendar_marks (user_id, date, status, updated_at) + VALUES (?, ?, ?, ?) + ON CONFLICT(user_id, date) DO UPDATE SET + status = excluded.status, + updated_at = excluded.updated_at`, + [userId, date, status, currentTimestamp], + (err) => { + if (err) reject(err); + else resolve(); + } + ); + } else { + // Status unchanged - keep existing timestamp + db.run( + `INSERT INTO calendar_marks (user_id, date, status, updated_at) + VALUES (?, ?, ?, (SELECT updated_at FROM calendar_marks WHERE user_id = ? AND date = ?)) + ON CONFLICT(user_id, date) DO UPDATE SET + status = excluded.status`, + [userId, date, status, userId, date], + (err) => { + if (err) reject(err); + else resolve(); + } + ); + } } ); }) ); - } else { + }); + + // Delete explicitly removed dates + if (deletedDates && Array.isArray(deletedDates) && deletedDates.length > 0) { + const placeholders = deletedDates.map(() => '?').join(','); dbOperations.push( new Promise((resolve, reject) => { db.run( - 'DELETE FROM calendar_marks WHERE user_id = ?', - [userId], + `DELETE FROM calendar_marks WHERE user_id = ? AND date IN (${placeholders})`, + [userId, ...deletedDates], (err) => { if (err) reject(err); else resolve();