diff --git a/.workbuddy/memory/2026-05-06.md b/.workbuddy/memory/2026-05-06.md index f09fce0..d7df543 100644 --- a/.workbuddy/memory/2026-05-06.md +++ b/.workbuddy/memory/2026-05-06.md @@ -113,3 +113,22 @@ - `eventDidMount` 中为每个事件绑定 `contextmenu` 事件 - `onMounted` 注册全局 click 关闭菜单,`onBeforeUnmount` 移除 - i18n 新增 `calendar.copy_event/paste_event/copy_success/paste_success/no_event_to_paste` + +## 修复:粘贴多天日程时结束日期少1天 + +**问题**:Ctrl+V 粘贴大于1天的日程时,目标结束日期少1天。 + +**原因**:`pasteToDate` 中用 `new Date(targetEndMs).toISOString().split('T')[0]` 计算目标结束日期,`toISOString()` 输出 UTC 时间,在 UTC+8 时区下日期会往前偏移1天。 + +**修复**(`CalendarDetail.vue` `pasteToDate` 函数): +- 改用天数差 `Math.round((origEndDate - origStartDate) / 86400000)` 替代毫秒差 +- 用本地日期方法 `targetEndDate.setDate(targetEndDate.getDate() + diffDays)` + 手动拼接 `YYYY-MM-DD`,替代 `toISOString().split('T')[0]` + +## 修复:Ctrl+C 无法复制大于1天的日程 + +**问题**:Ctrl+C 复制多天日程后粘贴无效果。 + +**根因**:`calendarOptions.value.events` 中多天事件的 `end` 是 `DateUtils.toCalendarEnd()` 返回的 **Date 对象**(非字符串)。Ctrl+C 直接 `selectedEvent.end || selectedEvent.start` 存入 clipboard,导致 `pasteToDate` 调用 `clipboard.value.end.split('T')[0]` 时 Date 对象没有 `split` 方法而报错。单天事件因走 `isSameDay` 分支不解析 `end`,所以不受影响。 + +**修复**(`CalendarDetail.vue` `handleKeyDown` Ctrl+C 分支): +- 检测 `end` 是否为 Date 实例,如果是则用本地日期方法转为 `YYYY-MM-DD` 字符串再存入 clipboard diff --git a/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue b/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue index be81448..07612aa 100644 --- a/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue +++ b/frontend/ops_vue_js/src/views/calendar/CalendarDetail.vue @@ -55,6 +55,7 @@ const colorOptions = ref([ const pageData = ref({ seleEventID: 0, + selectedDate: '', // 选中的格子日期,用于 Ctrl+V 粘贴 lastClickTime: 0, lastClickTimeStr: "", lastEventClickTime: 0, @@ -126,50 +127,13 @@ async function pasteEvent() { targetStart = selectedEvent.start.split('T')[0] } } + closeContextMenu() if (!targetStart) { toast.warning(t('calendar.no_event_to_paste')) - closeContextMenu() return } - // 计算原始日程的时长(天数) - const origStart = clipboard.value.start.split('T')[0] - const origEnd = clipboard.value.end.split('T')[0] - const origStartMs = new Date(origStart).getTime() - const origEndMs = new Date(origEnd).getTime() - const durationMs = origEndMs - origStartMs - const isSameDay = durationMs === 0 || origStart === origEnd - - // 粘贴的结束日期 = 目标起始日期 + 原始时长 - let targetEnd = targetStart - if (!isSameDay) { - const targetEndMs = new Date(targetStart).getTime() + durationMs - targetEnd = new Date(targetEndMs).toISOString().split('T')[0] - } - - try { - const result = await calendarApi.addEvent({ - calendar_id: calendarId.value, - title: clipboard.value.title, - start: toDatetime(targetStart), - end: toDatetime( - isSameDay - ? targetEnd - : DateUtils.toRealEnd(targetEnd), - ), - schedule_type: clipboard.value.scheduleType, - is_public: clipboard.value.isPublic, - }) - if (result.errCode === 0) { - toast.success(t('calendar.paste_success')) - getEvents() - } else { - toast.error(t('message.server_error')) - } - } catch { - // 拦截器已处理 - } - closeContextMenu() + await pasteToDate(targetStart) } // 选中/取消选中事件 @@ -511,6 +475,7 @@ const calendarOptions = ref({ const nowTime = new Date().getTime() const timeDifference = nowTime - pageData.value.lastClickTime unseleEventAll() + pageData.value.selectedDate = info.dateStr if (info.dateStr === pageData.value.lastClickTimeStr) { if (timeDifference < 400 && timeDifference > 0) { @@ -542,6 +507,7 @@ const calendarOptions = ref({ const eventid = parseInt(info.event.id) unseleEventAll() + pageData.value.selectedDate = '' const target = calendarOptions.value.events.find(item => String(item.id) === String(info.event.id)) if (target) { target.borderColor = "#000000" @@ -606,6 +572,97 @@ watch(locale, () => { ] }) +// 键盘快捷键:Ctrl+C 复制选中日程,Ctrl+V 粘贴到选中格子 +function handleKeyDown(e) { + // 忽略在输入框中的快捷键 + if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return + + if (e.ctrlKey || e.metaKey) { + if (e.key === 'c') { + // Ctrl+C:复制选中日程 + if (pageData.value.seleEventID) { + const selectedEvent = calendarOptions.value.events.find( + item => item.id === pageData.value.seleEventID + ) + if (selectedEvent) { + // 将 end 统一转为 YYYY-MM-DD 字符串(多天事件的 end 是 Date 对象) + let endStr = selectedEvent.end || selectedEvent.start + if (endStr instanceof Date) { + const y = endStr.getFullYear() + const m = String(endStr.getMonth() + 1).padStart(2, '0') + const d = String(endStr.getDate()).padStart(2, '0') + endStr = `${y}-${m}-${d}` + } + clipboard.value = { + id: selectedEvent.id, + title: selectedEvent.title, + start: selectedEvent.start, + end: endStr, + color: selectedEvent.backgroundColor, + scheduleType: selectedEvent.extendedProps?.scheduleType || 'work', + isPublic: selectedEvent.extendedProps?.isPublic || false, + } + toast.success(t('calendar.copy_success')) + } + } + } else if (e.key === 'v') { + // Ctrl+V:粘贴到选中格子 + if (clipboard.value && pageData.value.selectedDate) { + e.preventDefault() + pasteToDate(pageData.value.selectedDate) + } + } + } +} + +// 粘贴到指定日期 +async function pasteToDate(targetStart) { + if (!clipboard.value) return + + const origStart = clipboard.value.start.split('T')[0] + const origEnd = clipboard.value.end.split('T')[0] + + // 计算原始事件天数(使用本地日期避免时区偏移) + const origStartDate = new Date(origStart) + const origEndDate = new Date(origEnd) + const diffDays = Math.round((origEndDate - origStartDate) / 86400000) + const isSameDay = diffDays === 0 + + let targetEnd = targetStart + if (!isSameDay) { + // 用本地日期方法计算目标结束日期,避免 toISOString() 的 UTC 时区偏移 + const targetEndDate = new Date(targetStart) + targetEndDate.setDate(targetEndDate.getDate() + diffDays) + const y = targetEndDate.getFullYear() + const m = String(targetEndDate.getMonth() + 1).padStart(2, '0') + const d = String(targetEndDate.getDate()).padStart(2, '0') + targetEnd = `${y}-${m}-${d}` + } + + try { + const result = await calendarApi.addEvent({ + calendar_id: calendarId.value, + title: clipboard.value.title, + start: toDatetime(targetStart), + end: toDatetime( + isSameDay + ? targetEnd + : DateUtils.toRealEnd(targetEnd), + ), + schedule_type: clipboard.value.scheduleType, + is_public: clipboard.value.isPublic, + }) + if (result.errCode === 0) { + toast.success(t('calendar.paste_success')) + getEvents() + } else { + toast.error(t('message.server_error')) + } + } catch { + // 拦截器已处理 + } +} + let resizeObserver = null let refreshTimer = null @@ -613,6 +670,8 @@ onMounted(() => { fetchCalendarInfo() // 点击任意位置关闭右键菜单 document.addEventListener('click', closeContextMenu) + // 键盘快捷键 + document.addEventListener('keydown', handleKeyDown) // 每 5 秒刷新一次数据 refreshTimer = setInterval(() => { getEvents() @@ -636,6 +695,7 @@ onMounted(() => { resizeObserver = null } document.removeEventListener('click', closeContextMenu) + document.removeEventListener('keydown', handleKeyDown) clearTimeout(resizeTimer) }) })