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
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { useCalendar } from '../composables/useCalendar.js'
|
import { useCalendar } from '../composables/useCalendar.js'
|
||||||
import { DOW_EN } from '../lib/weekday.js'
|
import { DOW, DOW_EN } from '../lib/weekday.js'
|
||||||
import OccurrenceGrid from './OccurrenceGrid.vue'
|
import OccurrenceGrid from './OccurrenceGrid.vue'
|
||||||
|
|
||||||
const { selectedEvent, updateEvent, deleteEvent, splitEvent, toggleOccurrence } = useCalendar()
|
const { selectedEvent, updateEvent, deleteEvent, splitEvent, toggleOccurrence } = useCalendar()
|
||||||
@@ -142,7 +142,7 @@ function onToggleOcc(date) {
|
|||||||
<label>星期 (BYDAY)</label>
|
<label>星期 (BYDAY)</label>
|
||||||
<select v-model="form.byday">
|
<select v-model="form.byday">
|
||||||
<option v-for="(d, i) in DOW_EN" :key="d" :value="d">
|
<option v-for="(d, i) in DOW_EN" :key="d" :value="d">
|
||||||
{{ ['周日','周一','周二','周三','周四','周五','周六'][i] }}
|
{{ DOW[i] }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useCalendar } from '../composables/useCalendar.js'
|
import { useCalendar } from '../composables/useCalendar.js'
|
||||||
import { DOW } from '../lib/weekday.js'
|
import { DOW } from '../lib/weekday.js'
|
||||||
|
import { toDate } from '../lib/date.js'
|
||||||
|
|
||||||
const { state, stats, addEvent } = useCalendar()
|
const { state, stats, addEvent, selectEvent } = useCalendar()
|
||||||
|
|
||||||
function selectEvent(uid) {
|
|
||||||
state.selectedUid = uid
|
|
||||||
}
|
|
||||||
|
|
||||||
function dowFor(ev) {
|
function dowFor(ev) {
|
||||||
return DOW[new Date(ev.dtstartDate + 'T00:00:00').getDay()]
|
return DOW[toDate(ev.dtstartDate).getDay()]
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useCalendar } from '../composables/useCalendar.js'
|
import { useCalendar } from '../composables/useCalendar.js'
|
||||||
import { ref } from 'vue'
|
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)
|
const fileInput = ref(null)
|
||||||
|
|
||||||
function onOrganize() {
|
function onOrganize() {
|
||||||
@@ -19,7 +19,6 @@ function onImport() {
|
|||||||
function onFilePicked(e) {
|
function onFilePicked(e) {
|
||||||
const file = e.target.files[0]
|
const file = e.target.files[0]
|
||||||
if (!file) return
|
if (!file) return
|
||||||
const { loadFile } = useCalendar()
|
|
||||||
loadFile(file).catch((err) => alert('解析失败:' + (err.message || err)))
|
loadFile(file).catch((err) => alert('解析失败:' + (err.message || err)))
|
||||||
e.target.value = ''
|
e.target.value = ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { DOW } from '../lib/weekday.js'
|
import { DOW } from '../lib/weekday.js'
|
||||||
|
import { toDate } from '../lib/date.js'
|
||||||
import { expandOccurrences } from '../lib/recur.js'
|
import { expandOccurrences } from '../lib/recur.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -11,7 +12,7 @@ const emit = defineEmits(['toggle'])
|
|||||||
const occurrences = computed(() => expandOccurrences(props.event))
|
const occurrences = computed(() => expandOccurrences(props.event))
|
||||||
|
|
||||||
function dowFor(dateStr) {
|
function dowFor(dateStr) {
|
||||||
return DOW[new Date(dateStr + 'T00:00:00').getDay()]
|
return DOW[toDate(dateStr).getDay()]
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { reactive, computed } from 'vue'
|
import { reactive, computed } from 'vue'
|
||||||
import { parseICS, serializeICS } from '../lib/ical-io.js'
|
import { parseICS, serializeICS } from '../lib/ical-io.js'
|
||||||
import { organize as organizeEvents } from '../lib/organize.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
|
const MAX_HISTORY = 50
|
||||||
|
|
||||||
@@ -20,8 +22,8 @@ const state = reactive({
|
|||||||
// ------------------------------------------------------------------ //
|
// ------------------------------------------------------------------ //
|
||||||
function snapshot() {
|
function snapshot() {
|
||||||
return {
|
return {
|
||||||
meta: state.meta ? JSON.parse(JSON.stringify(state.meta)) : null,
|
meta: state.meta ? cloneEvent(state.meta) : null,
|
||||||
events: JSON.parse(JSON.stringify(state.events)),
|
events: state.events.map(cloneEvent),
|
||||||
selectedUid: state.selectedUid,
|
selectedUid: state.selectedUid,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,8 +35,8 @@ function pushHistory() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function restore(snap) {
|
function restore(snap) {
|
||||||
state.meta = snap.meta ? JSON.parse(JSON.stringify(snap.meta)) : null
|
state.meta = snap.meta ? cloneEvent(snap.meta) : null
|
||||||
state.events = JSON.parse(JSON.stringify(snap.events))
|
state.events = snap.events.map(cloneEvent)
|
||||||
state.selectedUid = snap.selectedUid
|
state.selectedUid = snap.selectedUid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +56,10 @@ function loadFile(file) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectEvent(uid) {
|
||||||
|
state.selectedUid = uid
|
||||||
|
}
|
||||||
|
|
||||||
function organize() {
|
function organize() {
|
||||||
if (state.events.length === 0) return
|
if (state.events.length === 0) return
|
||||||
pushHistory()
|
pushHistory()
|
||||||
@@ -81,23 +87,7 @@ function deleteEvent(uid) {
|
|||||||
|
|
||||||
function addEvent() {
|
function addEvent() {
|
||||||
pushHistory()
|
pushHistory()
|
||||||
const today = new Date()
|
const ev = createEvent()
|
||||||
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' },
|
|
||||||
}
|
|
||||||
state.events.push(ev)
|
state.events.push(ev)
|
||||||
state.selectedUid = ev.uid
|
state.selectedUid = ev.uid
|
||||||
state.dirty = true
|
state.dirty = true
|
||||||
@@ -107,34 +97,11 @@ function splitEvent(uid, splitDate) {
|
|||||||
const ev = state.events.find((e) => e.uid === uid)
|
const ev = state.events.find((e) => e.uid === uid)
|
||||||
if (!ev || !ev.rrule) return
|
if (!ev || !ev.rrule) return
|
||||||
|
|
||||||
// Snap splitDate to the next grid point >= splitDate
|
const parts = splitRecurringEvent(ev, splitDate)
|
||||||
const step = ev.rrule.freq === 'WEEKLY' ? 7 * (ev.rrule.interval || 1) : (ev.rrule.interval || 1)
|
if (!parts) return
|
||||||
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')}`
|
|
||||||
|
|
||||||
pushHistory()
|
pushHistory()
|
||||||
|
const [partA, partB] = parts
|
||||||
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 idx = state.events.findIndex((e) => e.uid === uid)
|
const idx = state.events.findIndex((e) => e.uid === uid)
|
||||||
state.events.splice(idx, 1, partA, partB)
|
state.events.splice(idx, 1, partA, partB)
|
||||||
state.selectedUid = partB.uid
|
state.selectedUid = partB.uid
|
||||||
@@ -199,6 +166,7 @@ export function useCalendar() {
|
|||||||
isLoaded,
|
isLoaded,
|
||||||
stats,
|
stats,
|
||||||
loadFile,
|
loadFile,
|
||||||
|
selectEvent,
|
||||||
organize,
|
organize,
|
||||||
updateEvent,
|
updateEvent,
|
||||||
deleteEvent,
|
deleteEvent,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import ICAL from 'ical.js'
|
import ICAL from 'ical.js'
|
||||||
|
import { pad2, parseISODate } from './date.js'
|
||||||
|
|
||||||
// ------------------------------------------------------------------ //
|
// ------------------------------------------------------------------ //
|
||||||
// Timezone registration (ical.js does not bundle IANA zones)
|
// 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) {
|
function icalTimeToISODate(t) {
|
||||||
return String(n).padStart(2, '0')
|
|
||||||
}
|
|
||||||
|
|
||||||
function timeToISODate(t) {
|
|
||||||
return `${t.year}-${pad2(t.month)}-${pad2(t.day)}`
|
return `${t.year}-${pad2(t.month)}-${pad2(t.day)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeToHHMM(t) {
|
function icalTimeToHHMM(t) {
|
||||||
return `${pad2(t.hour)}:${pad2(t.minute)}`
|
return `${pad2(t.hour)}:${pad2(t.minute)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +65,7 @@ function parseEvent(veventComp) {
|
|||||||
const exdates = []
|
const exdates = []
|
||||||
for (const prop of veventComp.getAllProperties('exdate')) {
|
for (const prop of veventComp.getAllProperties('exdate')) {
|
||||||
for (const t of prop.getValues()) {
|
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') || '',
|
summary: veventComp.getFirstPropertyValue('summary') || '',
|
||||||
location: veventComp.getFirstPropertyValue('location') || '',
|
location: veventComp.getFirstPropertyValue('location') || '',
|
||||||
description: veventComp.getFirstPropertyValue('description') || '',
|
description: veventComp.getFirstPropertyValue('description') || '',
|
||||||
dtstartDate: timeToISODate(start),
|
dtstartDate: icalTimeToISODate(start),
|
||||||
dtstartTime: timeToHHMM(start),
|
dtstartTime: icalTimeToHHMM(start),
|
||||||
dtendTime: timeToHHMM(end),
|
dtendTime: icalTimeToHHMM(end),
|
||||||
tzid,
|
tzid,
|
||||||
rrule,
|
rrule,
|
||||||
exdates,
|
exdates,
|
||||||
@@ -125,11 +123,6 @@ export function parseICS(text) {
|
|||||||
// ------------------------------------------------------------------ //
|
// ------------------------------------------------------------------ //
|
||||||
// Helpers for serialization
|
// Helpers for serialization
|
||||||
// ------------------------------------------------------------------ //
|
// ------------------------------------------------------------------ //
|
||||||
function parseISODate(s) {
|
|
||||||
const [y, m, d] = s.split('-').map(Number)
|
|
||||||
return { year: y, month: m, day: d }
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseHHMM(s) {
|
function parseHHMM(s) {
|
||||||
const [h, m] = s.split(':').map(Number)
|
const [h, m] = s.split(':').map(Number)
|
||||||
return { hour: h, minute: m }
|
return { hour: h, minute: m }
|
||||||
|
|||||||
Reference in New Issue
Block a user