Compare commits

...

1 Commits

Author SHA1 Message Date
d32f8be018 refactor: 清理死代码 + 提前失败 + 错误记录控制台
- 移除 EventDetail.vue 未用的 computed 导入
- 移除 HeaderBar.vue 未用的 .btn-secondary CSS
- store (updateEvent/splitEvent/toggleOccurrence): uid 缺失改抛错 (fail-fast), 不再静默 return
- EventDetail onSplit: 捕获 splitEvent 抛错并提示用户
- DropZone/HeaderBar 解析失败: catch 内补 console.error
- ical-io getZone: 时区未注册回退 UTC 时 console.warn
- WeekPreview 周标签改用 i18n (weekPreview.rangeFmt), 与 MonthPreview 一致
- docs: 新增 error-handling.md; 修正 auto-save-design state 快照补 timezone
- README: 补充部署说明 (经 zMainPage apache2 托管)
2026-07-28 11:08:06 +08:00
11 changed files with 61 additions and 14 deletions

View File

@@ -49,7 +49,7 @@ timeTableFix/
| Node.js | ≥ 18 |
| ical.js | ICS 解析库npm 依赖) |
> 纯前端应用,无运行期后端依赖。
> 纯前端应用,无运行期后端依赖。生产部署经 [zMainPage](https://git.zikai.wang/zikai/zMainPage) 作为子模块集成,由其 apache2 静态托管(无独立 apache2 配置);详见 zMainPage 的部署文档。
## 如何使用
@@ -84,4 +84,5 @@ npm test # 单元测试57 项)
- [时区选择(自动检测 / 回退墨尔本)](./docs/timezone.md)
- [间隔合并与链拆分算法](./docs/organize-algorithm.md)
- [自动保存设计](./docs/auto-save-design.md)
- [错误处理与日志约定](./docs/error-handling.md)
- [初始设计文档](./docs/superpowers/specs/2026-07-13-timetable2-design.md)

View File

@@ -61,6 +61,7 @@ const state = reactive({
fileName: null,
viewMode: 'edit', // 'edit' | 'month' | 'week'
previewDate: todayISO(),
timezone: detectTimezone(), // 日历级默认时区 (新建事件 tzid / X-WR-TIMEZONE)
})
```

25
docs/error-handling.md Normal file
View File

@@ -0,0 +1,25 @@
# 错误处理与日志约定
timeTableFix 的错误处理分两层:纯库函数 fail-fastUI 层捕获后既写控制台又给用户反馈。
## 纯库层src/lib/
- 解析/算法错误直接抛出或返回 `null`,由调用方决定如何反馈。
- `parseICS``ICAL.parse` 对非法文本抛错,向上传播。
- `splitRecurringEvent` / `fitSeries`:不可处理时返回 `null`(合法的「拒绝」语义)。
- `getZone`时区未注册VTIMEZONE 缺失)时回退 UTC 并 `console.warn`,避免静默时间偏移。
## store 层src/composables/useCalendar.js
- `loadFile`:不吞错,`parseICS` 的异常直接 reject 给调用方。
- `updateEvent` / `splitEvent` / `toggleOccurrence`uid 不存在视为编程错误,**抛错**而非静默 return。调用方EventDetail在调前已校验 `selectedEvent`,正常流程不会触发;触发即暴露 bug。
## UI 层src/components/
- 文件解析失败DropZone / HeaderBar`.catch``console.error` 记录原始错误,再用 `alert` 告知用户。
- 拆分失败EventDetail `onSplit``splitEvent` 抛错被 `try/catch` 捕获,`console.error` + 提示日期无效。
- 时区检测/列表降级timezone.js旧环境 `Intl` 不可用时静默回退墨尔本(有测试覆盖),属可接受的降级。
## 控制台前缀
所有 `[timeTableFix]` 前缀的日志均来自本子项目,便于在嵌入宿主时与宿主日志区分。

View File

@@ -18,6 +18,7 @@ function onDrop(e) {
return
}
loadFile(file).catch((err) => {
console.error('[timeTableFix] 解析 ICS 失败', err)
error.value = t('dropzone.parseError') + (err.message || err)
})
}
@@ -27,6 +28,7 @@ function onPick(e) {
const file = e.target.files[0]
if (!file) return
loadFile(file).catch((err) => {
console.error('[timeTableFix] 解析 ICS 失败', err)
error.value = t('dropzone.parseError') + (err.message || err)
})
}

View File

@@ -1,5 +1,5 @@
<script setup>
import { ref, watch, computed } from 'vue'
import { ref, watch } from 'vue'
import { useCalendar } from '../composables/useCalendar.js'
import { useLocale } from '../composables/useLocale.js'
import { DOW, DOW_EN, bydayFromDate } from '../lib/weekday.js'
@@ -88,7 +88,14 @@ function onSplit() {
alert(t('eventDetail.splitDateInvalid'))
return
}
splitEvent(ev.uid, splitDate.value)
// splitEvent 在日期无法对齐到网格时抛错 (fail-fast); 捕获后提示用户。
try {
splitEvent(ev.uid, splitDate.value)
} catch (err) {
console.error('[timeTableFix] 拆分失败', err)
alert(t('eventDetail.splitDateInvalid'))
return
}
showSplit.value = false
splitDate.value = ''
}

View File

@@ -23,7 +23,10 @@ function onImport() {
function onFilePicked(e) {
const file = e.target.files[0]
if (!file) return
loadFile(file).catch((err) => alert(t('header.parseError') + (err.message || err)))
loadFile(file).catch((err) => {
console.error('[timeTableFix] 解析 ICS 失败', err)
alert(t('header.parseError') + (err.message || err))
})
e.target.value = ''
}
@@ -79,8 +82,6 @@ function onDownload() {
.btn-primary:hover:not(:disabled) { background: var(--accent-hover); border-color: var(--accent-hover); }
.btn-success { background: var(--success); border-color: var(--success); color: #fff; }
.btn-success:hover:not(:disabled) { filter: brightness(0.92); }
.btn-secondary { background: var(--bg); color: var(--text-secondary); }
.btn-secondary:hover:not(:disabled) { background: var(--surface); color: var(--text-primary); }
/* 时区选择器:与按钮同行,紧凑 */
.tz-picker { display: flex; align-items: center; gap: 4px; }

View File

@@ -34,7 +34,8 @@ const weekLabel = computed(function () {
const days = weekDays.value
const f = parseISODate(days[0])
const l = parseISODate(days[6])
return f.year + '/' + f.month + '/' + f.day + ' - ' + l.year + '/' + l.month + '/' + l.day
const fmt = (p) => `${p.year}/${p.month}/${p.day}`
return t('weekPreview.rangeFmt', { s: fmt(f), e: fmt(l) })
})
// 收集本周所有 occurrence 并做并排布局, 按日期分组

View File

@@ -49,12 +49,12 @@ function organize() {
/**
* 更新指定事件的字段。
* 供 EventDetail 的自动应用 (watch) 调用 -- 表单变化即写回, 无需显式保存。
* uid 不存在视为编程错误, 直接抛出 (调用方应先校验 selectedEvent)。
*/
function updateEvent(uid, patch) {
const ev = state.events.find((e) => e.uid === uid)
if (ev) {
Object.assign(ev, patch)
}
if (!ev) throw new Error(`updateEvent: 事件不存在 uid=${uid}`)
Object.assign(ev, patch)
}
function deleteEvent(uid) {
@@ -70,10 +70,11 @@ function addEvent() {
function splitEvent(uid, splitDate) {
const ev = state.events.find((e) => e.uid === uid)
if (!ev || !ev.rrule) return
if (!ev) throw new Error(`splitEvent: 事件不存在 uid=${uid}`)
if (!ev.rrule) throw new Error('splitEvent: 非重复事件无法拆分')
const parts = splitRecurringEvent(ev, splitDate)
if (!parts) return
if (!parts) throw new Error('splitEvent: 拆分条件不满足 (日期越界或不在网格)')
const [partA, partB] = parts
const idx = state.events.findIndex((e) => e.uid === uid)
@@ -83,7 +84,7 @@ function splitEvent(uid, splitDate) {
function toggleOccurrence(uid, date) {
const ev = state.events.find((e) => e.uid === uid)
if (!ev) return
if (!ev) throw new Error(`toggleOccurrence: 事件不存在 uid=${uid}`)
const i = ev.exdates.indexOf(date)
if (i >= 0) {
ev.exdates.splice(i, 1)

View File

@@ -73,6 +73,7 @@ export default {
collapseNight: 'Hide Night',
noEvents: 'No events this week',
untitled: '(Untitled)',
rangeFmt: '{s} - {e}',
tooltipLocation: 'Location: {location}',
tooltipDesc: 'Description: {description}'
}

View File

@@ -73,6 +73,7 @@ export default {
collapseNight: '折叠夜间',
noEvents: '本周无日程',
untitled: '(无标题)',
rangeFmt: '{s} - {e}',
tooltipLocation: '地点: {location}',
tooltipDesc: '描述: {description}'
}

View File

@@ -132,7 +132,13 @@ function parseHHMM(s) {
function getZone(tzid) {
if (tzid === 'UTC' || tzid === 'Z') return ICAL.Timezone.utcTimezone
const z = ICAL.TimezoneService.get(tzid)
return z || ICAL.Timezone.utcTimezone
if (!z) {
// 时区未注册 (VTIMEZONE 缺失): ical.js 回退 UTC 会导致时间偏移。
// 记录到控制台便于定位 (典型成因: 手建事件 tzid 未在源 ICS 中定义)。
console.warn(`[timeTableFix] 时区 ${tzid} 未注册, 回退 UTC`)
return ICAL.Timezone.utcTimezone
}
return z
}
function makeTime(dateStr, timeStr, tzid) {