feat: 自动保存/移除撤销与红点, 间隔合并改进(DAILY/稀疏/交错拆分), 删死代码, 更新测试与文档

- 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 适配
This commit is contained in:
zikai
2026-07-22 06:32:47 +00:00
parent b49dbd46eb
commit 037b70deec
13 changed files with 617 additions and 137 deletions

View File

@@ -2,7 +2,7 @@
import { useCalendar } from '../composables/useCalendar.js'
import { ref } from 'vue'
const { state, canUndo, organize, undo, serialize, markSaved, loadFile } = useCalendar()
const { state, organize, serialize, loadFile } = useCalendar()
const fileInput = ref(null)
function onOrganize() {
@@ -24,7 +24,7 @@ function onFilePicked(e) {
}
function onDownload() {
if (state.dirty && !confirm('有未保存的更改,仍要下载?')) return
// 兜底自动保存: 始终序列化当前最新状态 (所有编辑已即时生效)
const text = serialize()
const blob = new Blob([text], { type: 'text/calendar' })
const url = URL.createObjectURL(blob)
@@ -34,7 +34,6 @@ function onDownload() {
a.download = base + '.edited.ics'
a.click()
URL.revokeObjectURL(url)
markSaved()
}
</script>
@@ -43,18 +42,16 @@ function onDownload() {
<button class="btn btn-primary" @click="onOrganize" :disabled="state.events.length === 0">
整理去重
</button>
<button class="btn btn-secondary" @click="undo" :disabled="!canUndo">撤销</button>
<button class="btn btn-secondary" @click="onImport">导入文件</button>
<button class="btn btn-success" @click="onDownload" :disabled="state.events.length === 0">
下载 ICS
</button>
<span v-if="state.dirty" class="dirty-dot" title="有未保存更改"></span>
<input ref="fileInput" type="file" accept=".ics" @change="onFilePicked" hidden />
</div>
</template>
<style scoped>
.header-actions { display: flex; gap: 10px; align-items: center; }
.header-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; justify-content: center; }
.btn {
padding: 7px 16px;
border: none;
@@ -63,6 +60,7 @@ function onDownload() {
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; }
@@ -71,5 +69,9 @@ function onDownload() {
.btn-success:hover:not(:disabled) { background: #229954; }
.btn-secondary { background: #ecf0f1; color: #2c3e50; }
.btn-secondary:hover:not(:disabled) { background: #d5dbdb; }
.dirty-dot { color: #e74c3c; font-size: 16px; }
@media (max-width: 768px) {
.header-actions { gap: 6px; }
.btn { padding: 5px 10px; font-size: 12px; }
}
</style>