Compare commits
1 Commits
master
...
refactor/c
| Author | SHA1 | Date | |
|---|---|---|---|
| d32f8be018 |
@@ -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)
|
||||
|
||||
@@ -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
25
docs/error-handling.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# 错误处理与日志约定
|
||||
|
||||
timeTableFix 的错误处理分两层:纯库函数 fail-fast,UI 层捕获后既写控制台又给用户反馈。
|
||||
|
||||
## 纯库层(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]` 前缀的日志均来自本子项目,便于在嵌入宿主时与宿主日志区分。
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 在日期无法对齐到网格时抛错 (fail-fast); 捕获后提示用户。
|
||||
try {
|
||||
splitEvent(ev.uid, splitDate.value)
|
||||
} catch (err) {
|
||||
console.error('[timeTableFix] 拆分失败', err)
|
||||
alert(t('eventDetail.splitDateInvalid'))
|
||||
return
|
||||
}
|
||||
showSplit.value = false
|
||||
splitDate.value = ''
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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 并做并排布局, 按日期分组
|
||||
|
||||
@@ -49,13 +49,13 @@ function organize() {
|
||||
/**
|
||||
* 更新指定事件的字段。
|
||||
* 供 EventDetail 的自动应用 (watch) 调用 -- 表单变化即写回, 无需显式保存。
|
||||
* uid 不存在视为编程错误, 直接抛出 (调用方应先校验 selectedEvent)。
|
||||
*/
|
||||
function updateEvent(uid, patch) {
|
||||
const ev = state.events.find((e) => e.uid === uid)
|
||||
if (ev) {
|
||||
if (!ev) throw new Error(`updateEvent: 事件不存在 uid=${uid}`)
|
||||
Object.assign(ev, patch)
|
||||
}
|
||||
}
|
||||
|
||||
function deleteEvent(uid) {
|
||||
state.events = state.events.filter((e) => e.uid !== 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)
|
||||
|
||||
@@ -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}'
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ export default {
|
||||
collapseNight: '折叠夜间',
|
||||
noEvents: '本周无日程',
|
||||
untitled: '(无标题)',
|
||||
rangeFmt: '{s} - {e}',
|
||||
tooltipLocation: '地点: {location}',
|
||||
tooltipDesc: '描述: {description}'
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user