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 }