From b352c4e1169e3cb9dcec44c7dfc7692d79b3e523 Mon Sep 17 00:00:00 2001 From: timeTable2 dev Date: Mon, 13 Jul 2026 20:06:00 +0800 Subject: [PATCH] refactor: high-cohesion low-coupling cleanup across store and components ical-io.js: - Import pad2/parseISODate from date.js (remove local duplicates) - Rename ICAL.Time helpers to icalTimeTo* for clarity useCalendar.js: - Delegate addEvent to createEvent() factory - Delegate splitEvent to splitRecurringEvent() pure function - Use cloneEvent() instead of JSON.parse(JSON.stringify()) - Add selectEvent() action (components no longer mutate state.selectedUid directly) Components: - EventList: use selectEvent action + toDate() from date.js - HeaderBar: destructure loadFile at top level (was re-calling useCalendar in handler) - EventDetail: import DOW from weekday.js (was inlining the array) - OccurrenceGrid: use toDate() from date.js --- src/components/EventDetail.vue | 4 +- src/components/EventList.vue | 9 ++--- src/components/HeaderBar.vue | 3 +- src/components/OccurrenceGrid.vue | 3 +- src/composables/useCalendar.js | 62 ++++++++----------------------- src/lib/ical-io.js | 25 +++++-------- 6 files changed, 32 insertions(+), 74 deletions(-) diff --git a/src/components/EventDetail.vue b/src/components/EventDetail.vue index 1cca6a2..9ce48df 100644 --- a/src/components/EventDetail.vue +++ b/src/components/EventDetail.vue @@ -1,7 +1,7 @@ diff --git a/src/components/HeaderBar.vue b/src/components/HeaderBar.vue index bd29e5e..46e844a 100644 --- a/src/components/HeaderBar.vue +++ b/src/components/HeaderBar.vue @@ -2,7 +2,7 @@ import { useCalendar } from '../composables/useCalendar.js' import { ref } from 'vue' -const { state, canUndo, canRedo, organize, undo, redo, serialize, markSaved } = useCalendar() +const { state, canUndo, canRedo, organize, undo, redo, serialize, markSaved, loadFile } = useCalendar() const fileInput = ref(null) function onOrganize() { @@ -19,7 +19,6 @@ function onImport() { function onFilePicked(e) { const file = e.target.files[0] if (!file) return - const { loadFile } = useCalendar() loadFile(file).catch((err) => alert('解析失败:' + (err.message || err))) e.target.value = '' } diff --git a/src/components/OccurrenceGrid.vue b/src/components/OccurrenceGrid.vue index 34d372b..f09b2d9 100644 --- a/src/components/OccurrenceGrid.vue +++ b/src/components/OccurrenceGrid.vue @@ -1,6 +1,7 @@ diff --git a/src/composables/useCalendar.js b/src/composables/useCalendar.js index 0f8d841..14cf58c 100644 --- a/src/composables/useCalendar.js +++ b/src/composables/useCalendar.js @@ -1,6 +1,8 @@ import { reactive, computed } from 'vue' import { parseICS, serializeICS } from '../lib/ical-io.js' import { organize as organizeEvents } from '../lib/organize.js' +import { createEvent, cloneEvent } from '../lib/event-factory.js' +import { splitRecurringEvent } from '../lib/split.js' const MAX_HISTORY = 50 @@ -20,8 +22,8 @@ const state = reactive({ // ------------------------------------------------------------------ // function snapshot() { return { - meta: state.meta ? JSON.parse(JSON.stringify(state.meta)) : null, - events: JSON.parse(JSON.stringify(state.events)), + meta: state.meta ? cloneEvent(state.meta) : null, + events: state.events.map(cloneEvent), selectedUid: state.selectedUid, } } @@ -33,8 +35,8 @@ function pushHistory() { } function restore(snap) { - state.meta = snap.meta ? JSON.parse(JSON.stringify(snap.meta)) : null - state.events = JSON.parse(JSON.stringify(snap.events)) + state.meta = snap.meta ? cloneEvent(snap.meta) : null + state.events = snap.events.map(cloneEvent) state.selectedUid = snap.selectedUid } @@ -54,6 +56,10 @@ function loadFile(file) { }) } +function selectEvent(uid) { + state.selectedUid = uid +} + function organize() { if (state.events.length === 0) return pushHistory() @@ -81,23 +87,7 @@ function deleteEvent(uid) { function addEvent() { pushHistory() - const today = new Date() - const y = today.getFullYear() - const m = String(today.getMonth() + 1).padStart(2, '0') - const d = String(today.getDate()).padStart(2, '0') - const ev = { - uid: crypto.randomUUID(), - summary: '新日程', - location: '', - description: '', - dtstartDate: `${y}-${m}-${d}`, - dtstartTime: '09:00', - dtendTime: '10:00', - tzid: 'UTC', - rrule: null, - exdates: [], - _raw: { dtstamp: new Date().toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z' }, - } + const ev = createEvent() state.events.push(ev) state.selectedUid = ev.uid state.dirty = true @@ -107,34 +97,11 @@ function splitEvent(uid, splitDate) { const ev = state.events.find((e) => e.uid === uid) if (!ev || !ev.rrule) return - // Snap splitDate to the next grid point >= splitDate - const step = ev.rrule.freq === 'WEEKLY' ? 7 * (ev.rrule.interval || 1) : (ev.rrule.interval || 1) - const start = new Date(ev.dtstartDate + 'T00:00:00') - let snap = new Date(splitDate + 'T00:00:00') - // Advance snap to the next grid point on or after splitDate - while (snap < start || ((snap - start) / (24 * 3600 * 1000)) % step !== 0) { - if (snap < start) { snap = new Date(start); break } - snap.setDate(snap.getDate() + 1) - if (snap > new Date(ev.rrule.untilDate + 'T00:00:00')) return // out of range - } - const snapStr = `${snap.getFullYear()}-${String(snap.getMonth() + 1).padStart(2, '0')}-${String(snap.getDate()).padStart(2, '0')}` - - // Part A: rrule.untilDate = day before snap (previous grid point) - const prevGrid = new Date(snap) - prevGrid.setDate(prevGrid.getDate() - step) - const prevStr = `${prevGrid.getFullYear()}-${String(prevGrid.getMonth() + 1).padStart(2, '0')}-${String(prevGrid.getDate()).padStart(2, '0')}` + const parts = splitRecurringEvent(ev, splitDate) + if (!parts) return pushHistory() - - const partA = JSON.parse(JSON.stringify(ev)) - partA.rrule = { ...ev.rrule, untilDate: prevStr } - partA.exdates = ev.exdates.filter((d) => d < splitDate) - - const partB = JSON.parse(JSON.stringify(ev)) - partB.uid = crypto.randomUUID() - partB.dtstartDate = snapStr - partB.exdates = ev.exdates.filter((d) => d >= splitDate) - + const [partA, partB] = parts const idx = state.events.findIndex((e) => e.uid === uid) state.events.splice(idx, 1, partA, partB) state.selectedUid = partB.uid @@ -199,6 +166,7 @@ export function useCalendar() { isLoaded, stats, loadFile, + selectEvent, organize, updateEvent, deleteEvent, diff --git a/src/lib/ical-io.js b/src/lib/ical-io.js index ac296e0..70d1e21 100644 --- a/src/lib/ical-io.js +++ b/src/lib/ical-io.js @@ -1,4 +1,5 @@ import ICAL from 'ical.js' +import { pad2, parseISODate } from './date.js' // ------------------------------------------------------------------ // // Timezone registration (ical.js does not bundle IANA zones) @@ -13,17 +14,14 @@ function registerTimezones(vcalendar) { } // ------------------------------------------------------------------ // -// Helpers +// ICAL.Time <-> plain string helpers +// (ICAL.Time exposes .year/.month/.day/.hour/.minute directly) // ------------------------------------------------------------------ // -function pad2(n) { - return String(n).padStart(2, '0') -} - -function timeToISODate(t) { +function icalTimeToISODate(t) { return `${t.year}-${pad2(t.month)}-${pad2(t.day)}` } -function timeToHHMM(t) { +function icalTimeToHHMM(t) { return `${pad2(t.hour)}:${pad2(t.minute)}` } @@ -67,7 +65,7 @@ function parseEvent(veventComp) { const exdates = [] for (const prop of veventComp.getAllProperties('exdate')) { for (const t of prop.getValues()) { - exdates.push(timeToISODate(t)) + exdates.push(icalTimeToISODate(t)) } } @@ -83,9 +81,9 @@ function parseEvent(veventComp) { summary: veventComp.getFirstPropertyValue('summary') || '', location: veventComp.getFirstPropertyValue('location') || '', description: veventComp.getFirstPropertyValue('description') || '', - dtstartDate: timeToISODate(start), - dtstartTime: timeToHHMM(start), - dtendTime: timeToHHMM(end), + dtstartDate: icalTimeToISODate(start), + dtstartTime: icalTimeToHHMM(start), + dtendTime: icalTimeToHHMM(end), tzid, rrule, exdates, @@ -125,11 +123,6 @@ export function parseICS(text) { // ------------------------------------------------------------------ // // Helpers for serialization // ------------------------------------------------------------------ // -function parseISODate(s) { - const [y, m, d] = s.split('-').map(Number) - return { year: y, month: m, day: d } -} - function parseHHMM(s) { const [h, m] = s.split(':').map(Number) return { hour: h, minute: m }