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:
@@ -91,4 +91,10 @@ function onPick(e) {
|
||||
font-size: 13px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dropzone { margin: 16px; border-radius: 8px; }
|
||||
.hint { font-size: 13px; }
|
||||
.pick-btn { padding: 8px 20px; font-size: 13px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -263,4 +263,11 @@ function onToggleOcc(date) {
|
||||
}
|
||||
.split-panel p { width: 100%; font-size: 13px; color: #7f8c8d; margin-bottom: 8px; }
|
||||
.split-panel input { padding: 6px 10px; border: 1px solid #d5dbdb; border-radius: 6px; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.detail { padding: 16px; }
|
||||
.form-row { flex-direction: column; gap: 0; }
|
||||
.toolbar { flex-wrap: wrap; }
|
||||
.btn { padding: 6px 12px; font-size: 12px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -84,4 +84,13 @@ function dowFor(ev) {
|
||||
}
|
||||
.badge-recur { background: #e8f5e9; color: #2e7d32; }
|
||||
.badge-single { background: #fff3e0; color: #e65100; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.event-list {
|
||||
width: 100%;
|
||||
max-height: 40vh;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #e0e3e6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -244,4 +244,16 @@ function hideTooltip() {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.month-preview { padding: 10px 8px; }
|
||||
.preview-toolbar { gap: 8px; flex-wrap: wrap; }
|
||||
.nav-btn { padding: 4px 10px; font-size: 12px; }
|
||||
.month-label { font-size: 14px; min-width: 100px; }
|
||||
.grid-header { font-size: 11px; padding: 5px 0; }
|
||||
.grid-cell { padding: 2px 3px; }
|
||||
.day-num { font-size: 11px; }
|
||||
.event-chip { font-size: 9px; padding: 0 3px; }
|
||||
.event-tooltip { max-width: calc(100vw - 32px); bottom: 8px; right: 8px; left: 8px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -268,4 +268,20 @@ function tooltip(ev) {
|
||||
font-size: 10px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.week-preview { padding: 10px 4px; }
|
||||
.preview-toolbar { gap: 8px; flex-wrap: wrap; }
|
||||
.nav-btn { padding: 4px 10px; font-size: 12px; }
|
||||
.week-label { font-size: 13px; min-width: 140px; }
|
||||
.week-grid { min-width: 600px; }
|
||||
.time-col { width: 40px; }
|
||||
.time-slot { font-size: 10px; padding-right: 3px; }
|
||||
.day-header { font-size: 10px; height: 34px; }
|
||||
.day-date { font-size: 12px; }
|
||||
.time-spacer { height: 34px; }
|
||||
.event-block { font-size: 10px; padding: 1px 2px; }
|
||||
.event-title { font-size: 10px; }
|
||||
.event-time { font-size: 9px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user