Imported from zhouyilong/taskReminder (
AGENTS.md). Install upstream withnpx skills add zhouyilong/taskReminder. Copyright stays with the author.
仓库指南
项目结构与模块组织
src/存放 Vue 3 + TypeScript 前端,共三个窗口入口:- 主窗口:
index.html→src/main.ts→src/App.vue(只负责外壳:标题栏、侧边栏、按 Tab 切换视图、挂载共享弹窗与全局事件监听)。src/views/:每个 Tab 一个视图——TodayView(今天时间线)、CalendarView(日历月视图 / 周视图,可拖动待办改期到某天或某个时段)、TasksView(含搜索、标签/优先级筛选与排序)、CompletedView、RecurringView、StickiesView(便签管理列表:显示中 / 已隐藏 / 已关闭,打开、置前、关闭)、RecordsView、StatsView(统计)、TrashView(回收站)。Tab 键定义在src/navigation.ts,上次打开的 Tab 记在localStorage.activeTab。src/composables/:模块级单例的共享状态——useAppData(四个列表、refreshAll、dataVersion)、useSettings(设置草稿、同步状态、设置/云同步弹窗开关)、useUpdater、useUiPrefs(主题、缩放、透明度、侧边栏)、useDialogs(确认框、详情、编辑待办/循环提醒)、useItemActions(完成、删除、右键菜单等通用操作)、useSmartAdd(带自然语言识别的新建待办/循环提醒)、useContextMenu、usePagination、useNow、useSearchPalette(全局搜索面板开关)。
- 提醒弹窗:
notification.html→src/notification.ts→src/NotificationApp.vue。 - 桌面便签:
sticky-note-item.html→src/stickyNoteItem.ts→src/StickyNoteItemApp.vue(每张便签一个独立窗口,窗口标签为sticky-note-item-<编码后的 id>)。 - 快速添加:
quick-add.html→src/quickAdd.ts→src/QuickAddApp.vue(窗口标签quick-add,由全局快捷键或托盘菜单打开)。
- 主窗口:
src/components/存放可复用 UI 组件:MarkdownNoteEditor.vue:基于 Milkdown Crepe 的 Markdown 所见即所得编辑器,variant支持card(带边框)与ghost(无边框,嵌入卡片或便签)。内容变更有 200ms 防抖,提交表单前先调用组件的flush()(ref取得),否则会丢掉最后的输入。Modal.vue:通用弹窗(带进出过渡动画)。AppTitlebar.vue/AppSidebar.vue:主窗口标题栏与侧边栏。SettingsModal.vue/WebdavModal.vue:应用设置与云同步设置。SharedDialogs.vue:挂在 App.vue 的共享弹窗(TaskEditModal、RecurringEditModal、详情、确认框、右键菜单),由useDialogs驱动,任何视图都可打开。RecurringFields.vue:循环提醒各模式的规则字段,新建表单与编辑弹窗共用;RecurringEndFields.vue:结束条件(结束日期、次数,v2.2),同样两处共用。Pagination.vue:表格分页条,配合usePagination。WeekdayPicker.vue:每周多天选择(位掩码v-model,含工作日/周末/每天预设)。TaskBadges.vue(项目、优先级与标签徽标)、TagInput.vue(标签输入)、PriorityPicker.vue(优先级分段选择)、SmartParseHint.vue(自然语言识别结果提示)。DataModal.vue:导入导出与本地备份(从设置打开)。TaskBatchBar.vue:待办多选后的批量操作栏(完成、删除、加标签、移到项目、优先级、改提醒时间)。SearchPalette.vue:全局搜索面板(Ctrl+K/ 标题栏按钮),挂在 App.vue,回车跳到对应页面并打开编辑或详情。StatsBarChart.vue:统计面板的堆叠柱状图(提醒与完成趋势共用)。ChecklistProgress.vue:正文中勾选清单的进度徽标(如 2/5)。
src/api.ts封装所有 Tauriinvoke命令;src/types.ts为前后端共享的数据类型。src/markdown.tsMarkdown 转纯文本/预览文本工具(列表描述、提醒记录去掉前导列表标记)与勾选清单进度(checklistProgress,Crepe 保存为* [ ])。src/format.ts主窗口共用的时间与文案格式化;src/recurring.ts循环规则展示、表单草稿、校验与提交载荷。src/timeline.ts(“今天”时间线)、src/stats.ts(统计面板)、src/calendar.ts(日历网格与按天归类)、src/tasks.ts(标签、优先级、项目、筛选排序)、src/stickies.ts(便签列表状态、筛选排序)、src/nlp.ts(自然语言时间/标签/项目/优先级/循环规则与结束条件识别)、src/search.ts(全局搜索与各列表关键词搜索的统一规则:多词为“且”,#标签只与标签前缀匹配,@项目只与项目前缀匹配)、src/retention.ts(已完成待办保留期选项与说明)为纯函数,测试在同名*.spec.ts。src/syncStatus.ts云同步状态码到文案与色调的映射(兼容旧版中文状态);src/quietHours.ts勿扰时段说明文案。src/safeStorage.ts带异常保护的localStorage封装;src/startupError.ts启动失败时渲染错误页。src/styles.css为三个窗口共用的全局样式表(设计令牌、主窗口、提醒弹窗、便签);组件私有样式放在.vue文件内。src/update.ts自动更新逻辑模块(检查更新、安装更新、偏好管理)。src/due.ts(截止时间与提前提醒、taskAnchorTime、拖动改期时平移提醒)、src/reorder.ts(拖拽排序的位置计算)、src/selection.ts(列表多选)、src/keyboard.ts(主窗口快捷键映射)、src/modalStack.ts(Esc 关闭最上层弹窗)、src/notificationSound.ts(提醒弹窗提示音)为纯函数或小工具,测试在同名*.spec.ts。src/weekdays.ts每周位掩码工具(与后端一致:周一 = bit0 … 周日 = bit6);src/shortcut.ts全局快捷键录制与展示。src-tauri/存放 Tauri 应用的 Rust 后端。src-tauri/src/为 Rust 应用代码:main.rs(插件、命令注册与启动流程)、commands/(前端invoke的命令,按业务分为tasks、recurring、notification、trash、settings、sticky、system、data,公共的ApiResult/into_api在commands/mod.rs)、windows/(窗口事件;windows/sticky.rs为便签窗口的标签编码、创建显示、层级与 UI 状态注入,以及 Windows 上的贴边吸附;windows/placement.rs为便签位置的纯几何计算:越界校正、吸附)、db/(SQLite 读写:mod.rs为连接池、初始化与凭据库,业务表按tasks、recurring、records、sticky、settings、tombstones、import、migrations拆分,测试在db/tests.rs)、scheduler.rs(提醒调度与弹窗)、recurrence.rs(循环规则计算)、sync/(WebDAV 同步:service.rs调度与入口、remote.rs同步主流程与加密、merge.rs快照导出与按行合并、webdav.rs客户端与同步锁,测试在sync/tests.rs)、sync_schema.rs(参与同步的表与列的唯一定义、合并时保留不认识的列)、sync_crypto.rs(同步端到端加密:Argon2id + AES-256-GCM)、notification_queue.rs(提醒弹窗队列)、time.rs(时间的唯一入口:now()、数据库时间格式与各固定格式的解析与格式化)、holidays.rs(中国法定节假日与调休:内置数据、在线更新缓存与用户覆盖文件的合并与校验)、holiday_update.rs(每天从仓库拉取节假日数据)、quick_add.rs(快速添加窗口)、shortcuts.rs(全局快捷键统一注册:快速添加、显示/隐藏全部便签)、quiet_hours.rs(勿扰时段判断)、backup.rs(JSON/Markdown/ICS 导出、JSON 导入合并、每日本地备份)、tray.rs(托盘菜单与提示:下一条提醒、完成/推迟、显示/隐藏全部便签)、autostart.rs、single_instance.rs、paths.rs(数据目录)、models.rs、kinds.rs(状态、类型、处理结果、循环模式的强类型枚举,未知值原样保留)、state.rs、errors.rs、maintenance.rs(定期清理与优化)。src-tauri/migrations/存放数据库迁移文件;新增迁移后需在db/migrations.rs的migration_scripts()中登记;新增同步列还要登记到sync_schema.rs的SYNC_TABLES(测试会检查迁移结果与登记一致)。src-tauri/data/holidays-cn.json为内置法定节假日数据(off放假日、work调休上班日,支持[开始, 结束]区间),每年国务院发布次年安排后追加,并补充holidays.rs中的测试。- 该文件同时是在线更新的数据源:客户端每天从
main分支拉取(jsDelivr / GitHub raw),推送到main后已发布的应用即可获得新年份。客户端只采用内置数据没有的年份,且拒绝删减已下载年份的文件,因此只能追加年份;已发布年份的更正需随应用版本发布(内置数据优先)。CI 的holidays任务用scripts/check-holidays.mjs与基准版本比较:删除年份或日期一律失败;已有年份新增日期视为更正,需在提交信息或 PR 标题中加入holidays-correction。 src-tauri/icons/存放应用图标;源文件在src-tauri/icons/source/(icon.svg为主图标,icon-small.svg为 16–32px 简化版),修改后执行python3 src-tauri/icons/source/render.py重新生成 PNG 与icon.ico(需pip install cairosvg pillow)。src-tauri/capabilities/default.json定义各窗口的权限。src-tauri/tauri.conf.json定义窗口、打包、更新器与应用元数据;src-tauri/tauri.updater.conf.json为签名构建时的覆盖配置。
docs/ROADMAP.md为功能扩展与重构路线图,完成条目后同步勾选并补充变更记录。.github/workflows/release.yml推送v*tag 时签名构建 MSI 并上传到草稿 Release(发布说明取docs/release-notes/v{version}.md)。.github/workflows/ci.yml为 CI(Ubuntu + Windows:pnpm build、pnpm test、cargo fmt --check、cargo clippy --all-targets -- -D warnings、cargo test;另有holidays任务检查节假日数据、e2e任务运行 Playwright)。scripts/存放构建与发布脚本。scripts/build-updater.ps1签名构建 MSI + 生成更新清单。scripts/write-updater-manifest.mjs生成latest.json更新清单。scripts/check-release-version.mjs检查三处版本号一致且与发布 tag 相符(release.yml使用)。scripts/check-holidays.mjs检查节假日数据格式且只追加年份(规则在holidays-check.mjs,测试在holidays-check.spec.mjs,ci.yml使用)。scripts/tauri.mjsTauri CLI 包装器(处理 VS Dev Shell 环境)。
构建、测试与开发命令
pnpm dev:启动 Web UI 的 Vite 开发服务器。pnpm build:先执行vue-tsc --noEmit类型检查,再将前端打包到dist/。pnpm typecheck:仅做类型检查。pnpm test:运行 Vitest 前端单元测试(src/**/*.spec.ts与scripts/**/*.spec.mjs)。pnpm e2e:运行 Playwright 端到端测试(e2e/*.spec.ts,自动启动 Vite 开发服务器,Tauri 运行时由e2e/tauri-mock.js模拟)。pnpm preview:本地预览生产构建。pnpm tauri dev:以开发模式运行完整的 Tauri 桌面应用。pnpm tauri build:生成生产环境桌面应用包,不生成 updater 签名产物。- 若
pnpm tauri dev仍显示旧界面,可先执行pnpm build再运行pnpm tauri dev,避免回退到过期的dist/。
- 若
pnpm release:updater:执行完整的更新器构建流程(签名 + MSI + latest.json)。
版本管理
- 版本号需在三处同步修改:
package.json、src-tauri/Cargo.toml、src-tauri/tauri.conf.json。 - 发布更新时,GitHub Release 需上传三个文件:
.msi、.msi.sig、latest.json。
自动更新发布流程
- 修改三处版本号
- 构建:推荐直接执行
pnpm release:updater;若手动构建,则需设置TAURI_SIGNING_PRIVATE_KEY和TAURI_SIGNING_PRIVATE_KEY_PASSWORD后运行pnpm tauri build --bundles msi --config src-tauri/tauri.updater.conf.json - 生成清单:
pnpm release:updater会自动生成;若手动构建,则执行node scripts/write-updater-manifest.mjs - 发布:
gh release create v{version}上传 MSI + .sig + latest.json - 签名私钥位于
~/.tauri/taskReminder-updater.key
也可以交给 CI(.github/workflows/release.yml):改好三处版本号、写好 docs/release-notes/v{version}.md 后推送 v{version} tag,工作流会检查版本号(scripts/check-release-version.mjs)、签名构建 MSI、生成 latest.json 并上传到草稿 Release,检查无误后在 GitHub 上点“Publish release”。需要先在仓库 Secrets 中配置 TAURI_SIGNING_PRIVATE_KEY(私钥文件内容)与 TAURI_SIGNING_PRIVATE_KEY_PASSWORD。
编码风格与命名约定
- 缩进:
.vue、.ts、.css使用 2 个空格(保持现有格式)。 - Vue 组件文件名使用
PascalCase(如NotificationApp.vue)。 - TypeScript 标识符:函数/变量用
camelCase,类型用PascalCase。 - CSS 类名使用
kebab-case(如.titlebar-actions)。
UI 样式约定(v1.5.6 起)
- 颜色一律使用
src/styles.css中:root定义的设计令牌(如--bg-surface、--text-muted、--primary、--primary-soft、--primary-text、--success-*、--warning-*、--danger-*、--violet-*),不要在组件里写死颜色。 :root为深色主题;浅色主题在文件末尾的.light-theme { ... }中覆盖同名令牌。新增令牌时两处都要补齐。- 文字层级:
--text-title(标题/强调)>--text-base(正文)>--text-muted(次要)>--text-faint(占位、空值)。 - 字体统一使用
var(--font-sans),不要引用未随应用打包的 Web 字体(如 Fraunces、Inter、DM Sans),否则 Windows 上会回退为衬线字体。 - 常用组件类:
- 表格单元格:
.cell-title(加粗标题)、.cell-muted(次要描述)、.cell-time(等宽数字时间)、.cell-empty(空值)。 - 标签:
.chip(类型/模式,.is-task/.is-recurring)、.status-pill(带圆点状态,.is-running/.is-paused/.action-*)、.time-chip(提醒时间,.is-upcoming/.is-overdue)。 - 表格空状态:在
tbody末尾加tr.table-empty-row > td[colspan] > .table-empty(含.table-empty-title与.table-empty-hint)。 - 容器:
.table-card(表格卡片)、.composer-card(新建任务卡片)、.form-card(表单卡片)、.search-field(带图标搜索框)。 - 复选框已全局自定义样式;圆形“完成”勾选框使用
.check-round。
- 表格单元格:
select已全局去掉原生外观并使用内联 SVG 箭头;浅色主题的箭头颜色在.light-theme .select中单独覆盖。- 修改样式后需同时检查深色与浅色主题,以及侧边栏展开/收起、窗口宽度小于 980px(侧边栏变为横向标签栏)三种状态。
- 统计卡片:
.stat-row > .stat-tile(.stat-label、.stat-value、.stat-hint);卡片容器.stats-card;分段选择.segmented > .segmented-item.active。 - 统计图表颜色使用
--chart-completed/--chart-dismissed/--chart-snoozed/--chart-pending(深浅主题分别校验过色觉辨识度),图表必须带图例,并提供数据表视图。 - Rust 代码使用标准
snake_case的模块与函数命名;用cargo fmt(默认 rustfmt)格式化。
踩坑记录与注意事项
Vue Proxy 与 Tauri 插件私有字段冲突
- 问题:
@tauri-apps/plugin-updater的Update类使用了 JS 私有字段(#field),存入 Vueref()后被 Proxy 深度包装,调用downloadAndInstall()时报Cannot read private member from an object whose class did not declare it。 - 解决:使用
shallowRef代替ref存储 Tauri 插件返回的类实例对象,避免 Vue 深度代理。 - 规则:凡是 Tauri 插件返回的类实例(如
Update、Channel等),一律使用shallowRef而非ref存储。
github release / GitHub Release 更新发布与签名构建
- 关键词:github release、GitHub Release、Tauri updater、MSI、latest.json、TAURI_SIGNING_PRIVATE_KEY。
- 发布更新时,先同步三处版本号:
package.json、src-tauri/Cargo.toml、src-tauri/tauri.conf.json。 - 普通
pnpm tauri build不会生成 updater 签名产物,因此不要求设置TAURI_SIGNING_PRIVATE_KEY。 - PowerShell 推荐直接执行
pnpm release:updater。该脚本会读取~/.tauri/taskReminder-updater.key,用.Trim()清除签名私钥尾部换行,设置TAURI_SIGNING_PRIVATE_KEY_PASSWORD=""避免交互等待,并通过cmd /c "pnpm.cmd tauri build --bundles msi --config src-tauri/tauri.updater.conf.json"确保子进程继承签名环境变量。 - 构建产物位于
src-tauri/target/release/bundle/msi/,GitHub Release 必须上传三个文件:TaskReminderApp_{version}_x64_zh-CN.msi、TaskReminderApp_{version}_x64_zh-CN.msi.sig、latest.json。 - 创建 GitHub Release 时直接使用当前版本号执行:
gh release create v{version} src-tauri/target/release/bundle/msi/TaskReminderApp_{version}_x64_zh-CN.msi src-tauri/target/release/bundle/msi/TaskReminderApp_{version}_x64_zh-CN.msi.sig src-tauri/target/release/bundle/msi/latest.json --title "v{version}" --notes "{release notes}"。 - 在 bash 中构建时,直接
export TAURI_SIGNING_PRIVATE_KEY和TAURI_SIGNING_PRIVATE_KEY_PASSWORD后调用pnpm tauri build --bundles msi --config src-tauri/tauri.updater.conf.json,再运行node scripts/write-updater-manifest.mjs生成latest.json。
Linux 下便签窗口必须使用 App URL
- 问题:开发模式下便签窗口曾使用
WebviewUrl::External(localhost),Linux WebKitGTK 会将其视为远程源并拒绝invoke,导致便签无法保存。 - 解决:
sticky_note_item_url()在所有平台都返回WebviewUrl::App("sticky-note-item.html"),tauri dev会自动改写为build.devUrl,与主窗口同源;windows/sticky.rs中有对应单元测试守护。 - 规则:新增的 Tauri 窗口一律使用
WebviewUrl::App(...),不要为开发模式单独改用 External URL。
Linux 下透明便签窗口不重绘
- 问题:Linux WebKitGTK 的透明窗口在首帧后经常不重绘,便签一直停在“载入便签...”。
- 解决:仅在 Linux 关闭便签窗口透明,并通过初始化脚本直接注入便签数据;Windows 仍保持透明浮动外观。Linux 下相关样式挂在
html.sticky-note-mode.is-linux上。
提醒弹窗队列与巡检
- 提醒由后端
NotificationQueue维护,弹窗通过get_notification_queue读取、监听notification-queue事件更新,始终展示队首;ack_notification/snooze_notification返回剩余队列,队列为空时后端隐藏弹窗。 - 规则:新增触发提醒的路径一律走
ReminderScheduler::handle_task/handle_recurring,它们持有fire_lock并按提醒记录判重,保证计时器、30 秒巡检(fire_due)重复命中时不会重复弹出。不要绕过它直接写提醒记录再弹窗。 - 一次性提醒只补发 7 天内错过的(
MISSED_REMINDER_LOOKBACK_DAYS)。
删除与清理必须走墓碑
- 问题:直接
DELETE行后,远端库仍有该行,同步合并会把它重新插回本地(“复活”)。 - 规则:业务删除与定期清理一律写
deleted_at+updated_at(墓碑);只有purge_expired_tombstones按保留期(本地 7 天,开启同步 60 天)物理删除,同步在合并后、上传前也会调用它。 - 已完成待办的清理按本机设置
completed_retention_days(30 默认并限最近 100 条 / 90 / 365 / 0 永久,迁移V2.1.1;定期清理用DbManager::completed_retention_days只读这一列)。清理写的是墓碑并会同步,所以多设备时以最短的为准,界面说明中写明,不要改成只在本机隐藏。 - 回收站(
list_trash)只列出保留期内的墓碑。恢复即清除deleted_at;恢复循环提醒时从当前时间重新计算下次触发。 - 回收站的“永久删除”(
purge_trash→expire_tombstones)不能直接DELETE:把deleted_at改为EXPIRED_TOMBSTONE_TIME并刷新updated_at,让本地行在合并中胜出,再由清理物理删除(未开启同步时立即清理)。
同步列的前向兼容(2.0.3 起)
- 合并前只给本机库补齐登记的同步列(
ensure_sync_columns),不要给远端快照补列:补上的默认值会在远端行胜出时清空本机较新版本才有的字段。 - 合并时参与同步的列 =
SYNC_TABLES中登记的列 ∪ 远端库中本版本不认识的列:sync_schema::adopt_remote_columns把远端新增的列按其定义ALTER TABLE ADD COLUMN补到本机,值随合并保存、随上传带回。胜出的行只写它所在库实际拥有的列(INSERT ... ON CONFLICT DO UPDATE),远端来自不认识某列的旧版本时,该列保留本机的值。 - 规则:新增同步列必须可空或带字面量
DEFAULT(NOT NULL无默认值的列旧版本补不上,会被跳过);同步表中不要再加只在本机使用的列(放到settings或单独的表),已有的tasks.sticky_is_pinned登记在local_only。 - 迁移中的
ALTER TABLE ... ADD COLUMN遇到已存在的列会跳过(db/migrations.rs的adds_existing_column),因为同步可能已经提前补上了这一列。 - 本机写同步表时不要用
INSERT OR REPLACE/REPLACE(会把不认识的列清成默认值),用UPDATE或ON CONFLICT DO UPDATE只改自己认识的列。
同步水位(2.0.4 起)
- 每次成功上传后,
sync/watermark.rs在本机表sync_watermark(上传前取的时间、远端身份 = 规范化的地址 + 远端目录)与sync_watermark_rows(上传快照中每一行的 id)记下水位;这两张表是本机专用的,export_local_snapshot会在上传的快照中清空。 - 合并时只有同一远端且距水位超过墓碑保留期(60 天),才把“本机有、远端没有、id 在上次上传集合中、
updated_at不晚于水位”的行视为已在其他设备删除并清理,在本机物理删除。保留期内远端缺行(远端被重置或用旧备份覆盖)、更换远端、水位之后新建 / 导入 / 修改的行都不受影响。 - 规则:不要放宽这些条件;新增同步表时它会自动纳入(按
SYNC_TABLES遍历)。
截止时间、便签颜色与手动排序(v2.1)
- 待办的提醒时间仍是
reminder_time(调度、托盘、稍后提醒都只看它);due_at是截止时间,提醒时间 = 截止时间 - 提前量,提前量不单独存储(src/due.ts的leadOf反推)。只改时间的调用(稍后提醒、托盘推迟)不要传dueAt,update_task省略时保留、null清除。 - 列表、日历、今天时间线、按时间排序一律用
taskAnchorTime(截止时间优先,没有时用提醒时间);日历拖动改期用moveTaskTimes,保持提前量。 - 便签颜色只接受
models::STICKY_COLORS(与src/stickies.ts的STICKY_COLOR_OPTIONS一致),不认识的值按默认显示、原样保存。颜色令牌--sticky-<颜色>-top/bottom/border/accent在:root与.light-theme中各一套,.sticky-color-<颜色>映射到纸面令牌,放在样式表末尾。 sort_order越小越靠前,空值排在后面;只在“手动顺序”下可拖动。
循环提醒结束条件与待办项目(v2.2)
- 结束条件存于同步列
recurring_tasks.ends_on(YYYY-MM-DD,含当天)与remaining_count(剩余次数,空为不限)。recurrence::has_ended:剩余次数为 0,或next_trigger的日期晚于结束日期;前端isRecurringEnded与之一致。 - 规则:到达结束条件时一律经
apply_end_condition写入is_paused = 1(旧版本不认识结束条件,靠暂停停止提醒)。凡是改了next_trigger的路径(调度器触发、区间外顺延、跳过本次、编辑、回收站恢复、节假日重算)都要调用;剩余次数只在advance_after_trigger(真正触发一次)中扣减,跳过本次不扣。 handle_recurring触发前先检查has_ended:旧设备“恢复”了已结束的提醒时改回暂停,不弹出。resume_recurring_task对已结束的提醒报错;update_recurring_task在编辑前已结束、编辑后不再结束时自动恢复,其他情况保留原暂停状态。界面上“已结束”= 暂停且isRecurringEnded(recurringStatus)。- 待办项目存于同步列
tasks.project(空 = 未分组),读写统一经过models::normalize_project(去掉前导@、控制字符,最多 32 字),前端normalizeProject与之一致。update_task的project省略时保留、空字符串移出项目:只改时间的调用不要传。项目列表由已有待办汇总(collectProjects),没有单独的项目表(不认识的表会在旧设备上传时丢失)。
循环模式的兼容性
- 每周多天存于
schedule_weekdays位掩码,schedule_weekday始终写入掩码中最早的一天,供旧版本读取;读取时掩码为空则回退到schedule_weekday。 - 状态与模式字段用
kinds.rs的枚举(TaskStatus、TaskType、ReminderKind、ReminderAction、RepeatMode);库中文本与 JSON 不变,不认识的值原样保留(Unknown),不要回退成已知值再写回。 - v2.1 新增
MONTHLY_LAST_DAY、MONTHLY_LAST_WORKDAY;依赖节假日数据的模式(WORKDAY、MONTHLY_LAST_WORKDAY)在节假日数据更新后都会重新计算(refreshed_workday_trigger),前端用dependsOnHolidays判断是否提示“节假日待更新”。 - 2.0.2 起不认识的循环模式本机不调度、不触发、不改写(
sanitize_recurring_task报错,调度器、巡检、托盘、预估都跳过),界面显示“需升级”且不能编辑。2.0.1 及更早的版本仍会把未知模式改成区间间隔并同步回来,新增循环模式时需在发布说明中提示多设备同时升级。
标签与优先级
- 标签存于
tasks.tags(逗号分隔),读写统一经过models::normalize_tags(去掉#、逗号,不区分大小写去重,最多 10 个、每个 24 字);前端src/tasks.ts的normalizeTags与之保持一致。 update_task的tags/priority省略时保留原值:稍后提醒、日历拖动改期等只改时间的调用不要传这两个字段。
导入导出与备份
- 文件对话框由后端
tauri-plugin-dialog弹出(export_data/import_data为 async 命令,阻塞对话框不能在主线程等待),前端不传文件路径;因此不需要为对话框添加前端权限。 - 导入、备份恢复都按“较新的
updated_at胜出”合并(导入走DbManager::import_rows,恢复复用sync::merge_databases),之后调用schedule_existing并notify_local_change。 - 备份文件名固定为
taskreminder-YYYYMMDD-HHMMSS.db,backup::resolve_backup只接受这种名字,防止路径穿越。
同步状态码
settings.webdav_last_sync_status存状态码(sync::SyncState:never/syncing/success/first_sync/lock_busy/failed/passphrase_mismatch),get_status与sync-status事件也返回状态码;文案只在前端src/syncStatus.ts映射。- 规则:不要再按中文文案判断同步状态;新增状态时同时更新
SyncState::parse与syncStatus.ts。读取时兼容 2.0.1 之前存的中文文案。 passphrase_mismatch:远端加密数据无法用本机同步密码解密(CryptoError::Decrypt→AppError::SyncPassphrase),通常是其他设备更换了密码。此状态下request_sync_if_needed暂停自动同步,手动同步照常;save_settings发现同步密码改动后立即同步一次。- 规则:
perform_sync、change_passphrase用reqwest::blocking并做 Argon2 派生,只能在spawn_blocking或普通线程中调用,不要放进async任务(会占住运行时线程,debug 构建中 reqwest 直接 panic)。
Cron 表达式
recurrence::cron_schedule_expr把 5 段表达式转换为croncrate 的 6 段格式:周字段按标准 Unix 含义(0/7 = 周日),数字周几会换成英文缩写,因为croncrate 的数字周几是 1 = 周日。6、7 段原样透传。- 库里保存用户输入的原始表达式,转换只在计算时进行。
密码存放(凭据库)
- WebDAV 密码与同步密码在 Windows 上存凭据管理器(
secrets.rs,服务名TaskReminderApp,开发构建为TaskReminderApp-dev,条目名{设备 ID}:{字段}),数据库中留空,settings.secret_storage记为keyring;其他平台或写入失败时存数据库(db)。 - 读写都经
DbManager::load_settings/save_settings,调用方拿到的AppSettings里总是明文密码。密码不变时save_settings不会写凭据库;凭据库读取失败时不会用空值覆盖。 - 测试中的
DbManager::new不访问系统凭据库;需要时用DbManager::with_secret_store注入secrets::MemoryStore。 - 新增敏感设置时同样经
resolve_secret_storage处理,并在export_local_snapshot_bytes中清空。
同步端到端加密
- 远端文件:明文为
taskreminder.db;开启加密后为taskreminder.db.enc,同时把taskreminder.db换成以TaskReminder-Encrypted-Placeholder开头的占位说明(旧版本把它当数据库合并会失败,从而不会上传明文)。 sync_with_remote的顺序不能随意调整:先判断远端是否加密(未开启加密却遇到加密文件、或解密失败都直接报错,绝不上传),合并后先传.enc再替换明文文件。- 远端存储经
RemoteStore接口访问,测试用内存实现(sync/tests.rs中的MemoryStore)覆盖首次同步、明文转加密、密码错误等流程;测试使用sync_crypto::TEST_PARAMS的小 KDF 参数。 - 上传的快照由
export_local_snapshot_bytes生成并清空webdav_password与sync_passphrase;新增敏感设置时要一并清空。 - 同步密码存于本机
settings.sync_passphrase(settings 表不参与合并);save_settings广播给其他窗口的设置会清空两个密码。 - 更换同步密码走
CloudSyncService::change_passphrase:用当前密码合并远端后以新密码上传(sync_with_remote_as),上传成功后才保存新密码;前端成功后只刷新同步相关字段,不要整体loadSettings覆盖草稿,也不要让旧草稿再次保存旧密码。 - 开发构建对
argon2/blake2单独开启优化(Cargo.toml的profile.dev.package),否则每次同步派生密钥要数秒。
全局快捷键
- 快捷键由
shortcuts::apply_shortcuts统一注册(先unregister_all再逐个注册:快速添加、显示/隐藏全部便签),启动时与保存设置后调用;某个失败不影响其他,原因合并后通过apply_quick_add_shortcut命令返回给设置界面。失败(格式无效、被其他程序占用、Wayland 不支持等)不能阻止应用启动。 - 新增全局快捷键时加到
apply_shortcuts中,不要在别处单独注册,否则会被unregister_all清掉。
勿扰时段、系统通知与托盘
- 提醒入队后一律经
ReminderScheduler::present展示:勿扰时段内只更新已打开的弹窗、不主动弹出,并记下quiet_held;巡检中的release_quiet_hold在勿扰结束后弹出积压的提醒。系统原生通知(tauri-plugin-notification)也在这里按设置发送,勿扰期间不发。 - 勿扰、系统通知、便签快捷键都是本机设置(
settings表,迁移V2.0.2),不参与同步。 - 托盘通过
listen_any("data-updated")与每 30 秒的定时器刷新,刷新在后台线程执行(tray::request_refresh),不要在持有锁时同步调用菜单 API。托盘中的“完成 / 推迟”复用commands::tasks::complete_task_by_id/reschedule_task_reminder。 - “隐藏全部便签”只隐藏窗口,不写
sticky_is_open,避免产生同步改动。
onMounted 中异步操作的异常隔离
- 问题:多个异步操作放在同一个
try块中,前面的操作抛异常会导致后面的操作被跳过(如自动更新检查被数据初始化异常阻断)。 - 解决:将相互独立的异步操作放在各自独立的
try/catch块中。 - 规则:
onMounted中多个独立的异步初始化操作应分别用try/catch包裹,互不影响。
时间处理
- 当前时间一律用
time::now()/time::now_string(),格式一律用time.rs的函数(数据库时间、钟点、日期、备份时间戳、ICS 等);不要在其他模块直接调用Local::now()、parse_from_str或写格式字符串。 - 与当前时间有关的逻辑在测试中用
let _now = time::fix_now("2026-09-30T10:00");固定时间(只影响当前线程,守卫离开作用域时恢复)。
测试指南
- 前端使用 Vitest:测试与被测模块同目录,命名为
*.spec.ts(scripts/下的脚本为*.spec.mjs),运行pnpm test。前端改动至少执行pnpm build(含vue-tsc类型检查)与pnpm test。 - 视图里的计算逻辑(如时间线、统计)优先抽成
src/下的纯函数再写测试,组件只做展示。 - Rust 测试位于
src-tauri/src/各模块的#[cfg(test)]中(便签窗口标签/URL、提醒队列、墓碑清理、同步合并、时间解析、节假日、循环规则);跨版本同步测试在sync/compat_tests.rs,按各发布版本的迁移建库(db::create_schema_up_to,仅测试)并写入src-tauri/tests/fixtures/中的示例数据(sync-sample-2.0.sql;库结构 2.1.0 起再加sync-sample-2.1.sql,2.2.0 起再加sync-sample-2.2.sql),发布新版本时把它的库结构版本加到RELEASES,通过cargo test运行;需要数据库的测试用临时目录创建DbManager,会自动执行迁移。 - 提交前运行
cargo fmt与cargo clippy --all-targets -- -D warnings,CI 会执行cargo fmt --check并在 clippy 有告警时失败。只在 Windows 编译的代码(#[cfg(target_os = "windows")])在 Linux 上检查不到,可用rustup target add x86_64-pc-windows-gnu(需mingw-w64)后执行cargo clippy --target x86_64-pc-windows-gnu --all-targets预检。 - 在 Linux 上构建会改写
src-tauri/gen/schemas/,这些生成文件的无关变动不要提交。 - 仅调整前端 UI 时,可用
pnpm dev在浏览器中预览;浏览器中没有 Tauri 运行时,需要在页面加载前注入window.__TAURI_INTERNALS__,直接复用e2e/tauri-mock.js(Playwright 的page.addInitScript({ path }),内存示例数据、调用记录在window.__TAURI_MOCK__.calls;可先设window.__TAURI_MOCK_SEED__覆盖数据)。新增后端命令时若界面依赖它,同步在模拟中补上。 - 端到端测试(
e2e/)只断言 DOM 与invoke参数,不做截图比对;用e2e/fixtures.ts的test/invokeCalls。Milkdown 编辑器的内容变更事件有 200ms 防抖,界面提交前会调用flush(),用例中输入后可以直接提交(“便签清单进度”用例守护这一点)。本地环境若已预装 Chromium,@playwright/test的版本需与之匹配(当前 1.56)。
提交与合并请求指南
- 提交信息使用简短祈使句,例如:
feat: 新增托盘开关、fix: 修复更新安装失败。 - PR 需包含:简要摘要、测试步骤,以及 UI 变更截图。
配置与资源
- 在
src-tauri/tauri.conf.json中更新窗口行为、打包标识符与应用元数据。 - 保持静态 HTML 入口文件(
index.html、notification.html、quick-add.html、sticky-note-item.html)简洁,并与 Vue 入口保持同步;新增入口时同时更新vite.config.ts的多页面配置。 - 更新器配置在
src-tauri/tauri.conf.json的plugins.updater节点,包含公钥和 GitHub Releases 端点。
