UI 变更: - 移除重做按钮及 redo/canRedo/redoStack 相关逻辑 (useCalendar, HeaderBar) - 移除应用更改按钮, EventDetail 改为 watch form 自动应用 (deep watch) - 用 syncing 标志防止 syncForm 回写时触发自动应用的循环 死代码清理: - weekday.js: 移除未使用的 BYDAY_TO_INDEX 和 dowLabel - date.js: 移除未使用的 toHHMM - App.vue: 移除未使用的 .empty CSS - DropZone.vue: 移除空的 .icon 容器及 CSS - test/weekday.test.js: 移除对应死代码的 2 个测试用例 性能修复: - WeekPreview: layoutEvents 从模板内联调用改为 computed (laidOutByDate), 避免每次渲染重新计算布局 - MonthPreview: occByDate 过滤为仅网格内 42 天, 避免展开全部 occurrence 代码质量: - useCalendar: prevMonth/nextMonth 合并为 shiftMonth(delta), 消除重复 - WeekPreview/MonthPreview: 移除未使用导入 (toISODate, toDate)
216 lines
5.4 KiB
JavaScript
216 lines
5.4 KiB
JavaScript
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'
|
|
import { todayISO, addDays, startOfMonth, parseISODate, pad2 } from '../lib/date.js'
|
|
|
|
const MAX_HISTORY = 50
|
|
|
|
// 单例响应式状态
|
|
const state = reactive({
|
|
meta: null,
|
|
events: [],
|
|
selectedUid: null,
|
|
history: [],
|
|
fileName: null,
|
|
dirty: false,
|
|
viewMode: 'edit', // 'edit' | 'month' | 'week'
|
|
previewDate: todayISO(), // 预览锚定日期 (月预览取当月, 周预览取当周)
|
|
})
|
|
|
|
// ------------------------------------------------------------------ //
|
|
// 快照 / 撤销
|
|
// ------------------------------------------------------------------ //
|
|
function snapshot() {
|
|
return {
|
|
meta: state.meta ? cloneEvent(state.meta) : null,
|
|
events: state.events.map(cloneEvent),
|
|
selectedUid: state.selectedUid,
|
|
}
|
|
}
|
|
|
|
function pushHistory() {
|
|
state.history.push(snapshot())
|
|
if (state.history.length > MAX_HISTORY) state.history.shift()
|
|
}
|
|
|
|
function restore(snap) {
|
|
state.meta = snap.meta ? cloneEvent(snap.meta) : null
|
|
state.events = snap.events.map(cloneEvent)
|
|
state.selectedUid = snap.selectedUid
|
|
}
|
|
|
|
// ------------------------------------------------------------------ //
|
|
// 文件加载
|
|
// ------------------------------------------------------------------ //
|
|
function loadFile(file) {
|
|
return file.text().then((text) => {
|
|
const parsed = parseICS(text)
|
|
state.meta = parsed.meta
|
|
state.events = parsed.events
|
|
state.selectedUid = null
|
|
state.history = []
|
|
state.fileName = file.name
|
|
state.dirty = false
|
|
})
|
|
}
|
|
|
|
// ------------------------------------------------------------------ //
|
|
// 事件操作
|
|
// ------------------------------------------------------------------ //
|
|
function selectEvent(uid) {
|
|
state.selectedUid = uid
|
|
}
|
|
|
|
function organize() {
|
|
if (state.events.length === 0) return
|
|
pushHistory()
|
|
const result = organizeEvents(state.events)
|
|
state.events = result.events
|
|
state.dirty = true
|
|
return result.stats
|
|
}
|
|
|
|
/**
|
|
* 更新指定事件的字段 (自动记录历史)。
|
|
* 供 EventDetail 的自动应用 (watch) 调用。
|
|
*/
|
|
function updateEvent(uid, patch) {
|
|
pushHistory()
|
|
const ev = state.events.find((e) => e.uid === uid)
|
|
if (ev) {
|
|
Object.assign(ev, patch)
|
|
state.dirty = true
|
|
}
|
|
}
|
|
|
|
function deleteEvent(uid) {
|
|
pushHistory()
|
|
state.events = state.events.filter((e) => e.uid !== uid)
|
|
if (state.selectedUid === uid) state.selectedUid = null
|
|
state.dirty = true
|
|
}
|
|
|
|
function addEvent() {
|
|
pushHistory()
|
|
const ev = createEvent()
|
|
state.events.push(ev)
|
|
state.selectedUid = ev.uid
|
|
state.dirty = true
|
|
}
|
|
|
|
function splitEvent(uid, splitDate) {
|
|
const ev = state.events.find((e) => e.uid === uid)
|
|
if (!ev || !ev.rrule) return
|
|
|
|
const parts = splitRecurringEvent(ev, splitDate)
|
|
if (!parts) return
|
|
|
|
pushHistory()
|
|
const [partA, partB] = parts
|
|
const idx = state.events.findIndex((e) => e.uid === uid)
|
|
state.events.splice(idx, 1, partA, partB)
|
|
state.selectedUid = partB.uid
|
|
state.dirty = true
|
|
}
|
|
|
|
function toggleOccurrence(uid, date) {
|
|
pushHistory()
|
|
const ev = state.events.find((e) => e.uid === uid)
|
|
if (!ev) return
|
|
const i = ev.exdates.indexOf(date)
|
|
if (i >= 0) {
|
|
ev.exdates.splice(i, 1)
|
|
} else {
|
|
ev.exdates.push(date)
|
|
ev.exdates.sort()
|
|
}
|
|
state.dirty = true
|
|
}
|
|
|
|
function undo() {
|
|
if (state.history.length === 0) return
|
|
restore(state.history.pop())
|
|
}
|
|
|
|
// ------------------------------------------------------------------ //
|
|
// 序列化
|
|
// ------------------------------------------------------------------ //
|
|
function serialize() {
|
|
return serializeICS({ meta: state.meta, events: state.events })
|
|
}
|
|
|
|
function markSaved() {
|
|
state.dirty = false
|
|
}
|
|
|
|
// ------------------------------------------------------------------ //
|
|
// 视图切换 / 预览导航
|
|
// ------------------------------------------------------------------ //
|
|
function setViewMode(mode) {
|
|
state.viewMode = mode
|
|
}
|
|
|
|
function shiftMonth(delta) {
|
|
const { year, month } = parseISODate(startOfMonth(state.previewDate))
|
|
const d = new Date(year, month - 1 + delta, 1)
|
|
state.previewDate = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-01`
|
|
}
|
|
|
|
function prevMonth() {
|
|
shiftMonth(-1)
|
|
}
|
|
|
|
function nextMonth() {
|
|
shiftMonth(1)
|
|
}
|
|
|
|
function prevWeek() {
|
|
state.previewDate = addDays(state.previewDate, -7)
|
|
}
|
|
|
|
function nextWeek() {
|
|
state.previewDate = addDays(state.previewDate, 7)
|
|
}
|
|
|
|
// ------------------------------------------------------------------ //
|
|
// 计算属性
|
|
// ------------------------------------------------------------------ //
|
|
const selectedEvent = computed(() =>
|
|
state.events.find((e) => e.uid === state.selectedUid) || null,
|
|
)
|
|
|
|
const canUndo = computed(() => state.history.length > 0)
|
|
const isLoaded = computed(() => state.events.length > 0)
|
|
const stats = computed(() => {
|
|
const nRecur = state.events.filter((e) => e.rrule !== null).length
|
|
return { total: state.events.length, recurring: nRecur, single: state.events.length - nRecur }
|
|
})
|
|
|
|
export function useCalendar() {
|
|
return {
|
|
state,
|
|
selectedEvent,
|
|
canUndo,
|
|
isLoaded,
|
|
stats,
|
|
loadFile,
|
|
selectEvent,
|
|
organize,
|
|
updateEvent,
|
|
deleteEvent,
|
|
addEvent,
|
|
splitEvent,
|
|
toggleOccurrence,
|
|
undo,
|
|
serialize,
|
|
markSaved,
|
|
setViewMode,
|
|
prevMonth,
|
|
nextMonth,
|
|
prevWeek,
|
|
nextWeek,
|
|
}
|
|
}
|