- organize: seriesKey 移除 weekday, 解锁每3天等 DAILY 序列合并 - organize: fitSeries 放宽阈值(稀疏序列如 BUS LAW Workshop 合并) - organize: 新增 splitIntoChains, GCD=1 的交错多班次拆为多个系列 - useCalendar: 移除 dirty/history/undo/snapshot/markSaved 死代码 - HeaderBar: 移除撤销按钮与红点●, 下载兜底序列化当前状态 - 测试: 42 项(原32), 覆盖 DAILY/稀疏/交错拆分/两两和候选等场景 - 文档: README 精简, 新增 docs/organize-algorithm.md 与 auto-save-design.md - 保留移动端 CSS 适配
78 lines
2.3 KiB
Vue
78 lines
2.3 KiB
Vue
<script setup>
|
|
import { useCalendar } from '../composables/useCalendar.js'
|
|
import { ref } from 'vue'
|
|
|
|
const { state, organize, serialize, loadFile } = useCalendar()
|
|
const fileInput = ref(null)
|
|
|
|
function onOrganize() {
|
|
const stats = organize()
|
|
if (stats && stats.series === 0) {
|
|
alert('未发现可合并的重复模式')
|
|
}
|
|
}
|
|
|
|
function onImport() {
|
|
fileInput.value?.click()
|
|
}
|
|
|
|
function onFilePicked(e) {
|
|
const file = e.target.files[0]
|
|
if (!file) return
|
|
loadFile(file).catch((err) => alert('解析失败:' + (err.message || err)))
|
|
e.target.value = ''
|
|
}
|
|
|
|
function onDownload() {
|
|
// 兜底自动保存: 始终序列化当前最新状态 (所有编辑已即时生效)
|
|
const text = serialize()
|
|
const blob = new Blob([text], { type: 'text/calendar' })
|
|
const url = URL.createObjectURL(blob)
|
|
const a = document.createElement('a')
|
|
const base = state.fileName ? state.fileName.replace(/\.ics$/i, '') : 'calendar'
|
|
a.href = url
|
|
a.download = base + '.edited.ics'
|
|
a.click()
|
|
URL.revokeObjectURL(url)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="header-actions">
|
|
<button class="btn btn-primary" @click="onOrganize" :disabled="state.events.length === 0">
|
|
整理去重
|
|
</button>
|
|
<button class="btn btn-secondary" @click="onImport">导入文件</button>
|
|
<button class="btn btn-success" @click="onDownload" :disabled="state.events.length === 0">
|
|
下载 ICS
|
|
</button>
|
|
<input ref="fileInput" type="file" accept=".ics" @change="onFilePicked" hidden />
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.header-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; justify-content: center; }
|
|
.btn {
|
|
padding: 7px 16px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
transition: background 0.15s;
|
|
white-space: nowrap;
|
|
}
|
|
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
.btn-primary { background: #3498db; color: #fff; }
|
|
.btn-primary:hover:not(:disabled) { background: #2980b9; }
|
|
.btn-success { background: #27ae60; color: #fff; }
|
|
.btn-success:hover:not(:disabled) { background: #229954; }
|
|
.btn-secondary { background: #ecf0f1; color: #2c3e50; }
|
|
.btn-secondary:hover:not(:disabled) { background: #d5dbdb; }
|
|
|
|
@media (max-width: 768px) {
|
|
.header-actions { gap: 6px; }
|
|
.btn { padding: 5px 10px; font-size: 12px; }
|
|
}
|
|
</style>
|