DESIGN.md · diff

git:20260814.e4d419e to git:20260814.8dc0f2c

5 added, 0 removed. Audit A to A.

# Tadado 详细设计说明
> 本文档按功能模块记录需求、实现方案和界面布局标注,用于快速项目重建和迭代参考。
> 运行时 AI 指令参见 [CLAUDE.md](CLAUDE.md),更新日志参见 [CHANGELOG.md](CHANGELOG.md)。
---
## 1. 项目总览
### 1.1 技术栈
| 层面 | 技术 |
|------|------|
| 语言 | Python 3.10+ |
| GUI | PySide6 ≥ 6.5.0 |
| 数据库 | SQLite 3 + FTS5 全文索引 |
| 定时任务 | APScheduler ≥ 3.10 (QtScheduler) |
| 日期计算 | python-dateutil ≥ 2.8 |
| Excel 导出 | openpyxl ≥ 3.1 |
| 打包 | PyInstaller + Inno Setup |
| 开发工具 | pytest, black, ruff |
### 1.2 四层架构
```
┌──────────────────────────────────────────┐
│ UI 层 (src/ui/) │
│ MainWindow, TaskListView, Heatmap, ... │
│ controllers/ (Partition, Batch, Filter) │
├──────────────────────────────────────────┤
│ 服务层 (src/services/) │
│ TaskService, Parser, Formatter, │
│ Scheduler, Notifier, Archiver, │
│ Recurrence, UpdateChecker │
├──────────────────────────────────────────┤
│ 模型层 (src/models/) │
│ Task, TaskStatus, TaskFilter, │
│ TaskRepository (SQLite CRUD + FTS5) │
├──────────────────────────────────────────┤
│ SQLite 持久化 │
│ tasks / tasks_fts / partitions / │
│ notification_log │
└──────────────────────────────────────────┘
模块间通信:SignalBus (Qt 信号,单例)
数据门面:TaskService(UI 层与数据层唯一接缝)
配置中心:AppConfig (JSON 持久化,热加载)
主题系统:DesignTokens (20+ 语义色角色)
```
### 1.3 UI 控制器
MainWindow 拆分为 3 个可独立测试的控制器(`src/ui/controllers/`):
| 控制器 | 文件 | 职责 |
|--------|------|------|
| PartitionController | `partition_controller.py` | 分区生命周期、密码缓存、空闲锁定、状态栏分区按钮和菜单 |
| BatchController | `batch_controller.py` | 任务管理页面构建、批量操作、手动归档/清除、批量导出 |
| FilterCoordinator | `filter_coordinator.py` | 编辑视图数据刷新、过滤器合并、分页状态、任务选择和高亮 |
控制器通过构造函数注入依赖,不直接访问数据库。
### 1.4 核心设计决策
1. **TaskService 单门面** — UI 层所有数据操作通过 `TaskService`,不再直接调用 `TaskRepository`。TaskService 持有 Parser/Formatter/SignalBus,在写操作后统一发射信号、重建 raw_md。
2. **raw_md 是规范数据源** — 结构化字段从 Markdown 解析派生,始终可重新生成。`MarkdownTaskFormatter` 保证往返稳定。
3. **SignalBus 解耦** — 所有模块通过 Qt 信号通信,无直接跨层调用。
3. **生产/开发环境隔离** — `sys.frozen` 判断,打包版使用预制 package DB(4 分区 + 演示空间预设数据),开发版使用 dev DB(含测试分区 + 功能演示分区);构建时通过 `scripts/create_package_db.py` 生成 package DB,frozen 模式跳过自动 seeding。
4. **Design Tokens 语义化配色** — 所有颜色通过 `design_tokens.py` 的语义角色引用,亮/暗主题全局切换。
### 1.4 全局信号总线
| 信号 | 参数 | 发射方 | 监听方 |
|------|------|--------|--------|
| `task_created` | Task | TaskEditPanel, TaskInputWidget, Recurrence | MainWindow, HeatmapWidget |
| `task_updated` | Task | TaskEditPanel | MainWindow, HeatmapWidget |
| `task_deleted` | task_id (str) | TaskListView, BatchToolbar | MainWindow |
| `task_status_changed` | Task, old_status | TaskEditPanel, BatchToolbar, Recurrence | MainWindow, HeatmapWidget |
| `reminders_fired` | list[tuple[Task, int]] | TaskScheduler (已废弃) | — |
| `daily_digest` | — | TaskScheduler | TaskNotifier |
| `archive_completed` | count (int) | TaskArchiver | MainWindow |
| `date_selected` | date | CalendarHeatmapWidget | MainWindow |
| `date_range_selected` | date, date | CalendarHeatmapWidget | MainWindow, ActivityReportPanel |
| `heatmap_create_task` | date | CalendarHeatmapWidget | MainWindow |
| `partitions_changed` | — | SettingsDialog, MainWindow | MainWindow |
| `config_changed` | — | AppConfig, SettingsDialog | MainWindow, 各组件主题刷新 |
| `batch_operation_completed` | summary (dict) | TaskRepository | MainWindow |
| `tasks_bulk_created` | count, task_ids | MultiTaskDialog, TaskEditPanel | MainWindow |
| `application_quit` | — | app.py | SystemTrayManager |
| `scan_completed` | task_count | MarkdownImporter | MainWindow |
| `scan_error` | error_msg | MarkdownImporter | MainWindow |
**文件**:[src/utils/signal_bus.py](src/utils/signal_bus.py)
### 1.5 日志系统
**文件**:[src/utils/log_manager.py](src/utils/log_manager.py)
Tadado 使用 Python 标准库 `logging` 模块实现日志记录。
| 属性 | 值 |
|------|-----|
| Logger 名称 | `runlog` |
| 日志文件 | `resources/loginfo/tadado.log` |
| 轮转策略 | `TimedRotatingFileHandler`,每日午夜切割,保留 3 天 |
| 格式 | `%(asctime)s [%(levelname)s] %(name)s - %(message)s` |
| 初始化时机 | `TadadoApp.__init__()` 首行,早于任何业务初始化 |
**日志级别规范**:
| 级别 | 使用场景 |
|------|---------|
| `ERROR` | 数据库异常、文件 I/O 错误、网络 API 错误、配置加载失败 |
| `WARNING` | JSON 解析失败、版本解析失败、更新检测超时/回退、定时任务配置异常 |
| `INFO` | 任务 CRUD、批量操作、分区操作、标签操作、应用启动/关闭阶段、配置变更 |
| `DEBUG` | 查询语句构造(当前未使用,预留给未来诊断) |
**性能约束**:
- 不在 `search()`、`search_with_total()`、`count()`、热力图聚合等高频路径中记录日志
- 不在 1s 时钟、5s 轮播、30s 空闲检测等定时回调中记录日志
- 不在循环内部逐条记录,仅在操作边界记录一次汇总结果
- `logging` 模块内置线程锁(`threading.RLock`),与 APScheduler 后台线程兼容
### 1.6 CLI 与 Claude Code Skill 通道
Tadado 提供命令行通道,供 Claude Code skill(`.claude/skills/tadado/SKILL.md`)
与终端用户操作任务,与 GUI 共享同一份 SQLite 数据与同一套 TaskService 逻辑。
**模块结构**(`src/cli/`):
| 文件 | 职责 |
|------|------|
| `parser.py` | argparse 子命令定义(12 命令) |
| `commands.py` | 命令执行核心:接收 TaskService + AppConfig,返回 JSON 结果;headless 与 GUI 转发共用;错误抛 `CliError` |
| `output.py` | 稳定 JSON schema(任务对象字段与 Task 模型对齐)+ `--format human` 渲染 |
| `headless.py` | `run_cli()` 入口:UTF-8 stdio、`--format` 提取、转发或 headless 执行、`TADADO_DATA_DIR` 数据目录覆盖 |
| `forward.py` | 转发客户端:QLocalSocket 连接运行中 GUI(单写者原则) |
| `protocol.py` | 管道协议:请求帧 `TADADO_CLI/1\n` + JSON,响应为裸 JSON;旧版 GUI 无响应 → 退出码 10 |
**执行流程**:
```
tadado-cli <command> [args]
├─ 连接 QLocalServer("Tadado_Instance") 成功 → 请求帧转发给 GUI
│ GUI 线程内执行 commands.execute()(用其 TaskService/AppConfig)
│ → 裸 JSON 响应回传,CLI 渲染输出;UI 信号触发实时刷新
│ └─ 旧版 GUI(无协议处理)→ 报错退出码 10,绝不回退直写 DB
└─ 连接失败(GUI 未运行)→ headless 模式
QCoreApplication + AppConfig + TaskRepository + TaskService
→ 同栈执行 → JSON 输出
```
**命令集**(12 个):`list`(筛选/排序/分页)、`today`(今日摘要分组)、`add`
(Markdown 行为主 + flags 覆盖)、`edit`(字段修改 + `--dry-run` diff)、`done`
(状态变更,触发周期克隆与即时归档)、`rm`、`tags`、`partitions`(增删改)、
`archive`(`--all` 归档全部已完成)、`recurrence`(`+1d/+1w/+1m/+1y`)、
`reminder`(全局提醒配置)、`export`(md/xlsx,复用 `MarkdownExporter`/`task_exporter`)。
**关键设计**:
1. **单一写者 + 实例身份校验** — GUI 运行时 CLI 一律转发,绝不双进程写库;GUI 未运行时才
headless 直写。请求帧携带调用方 app 版本与数据目录,GUI 校验不一致即拒绝(退出码 11),
防止旧版/异库实例占用管道名导致写入错误实例。
2. **raw_md 不被绕过** — `add` 用 formatter 构造规范 Markdown 行再走 `TaskService.create_task`;
`edit` 走 `update_task` 重建 raw_md。
3. **LLM 输入防御** — `add` 归一化 `TODO<日期>`(缺空格)→ `TODO <日期>`,与 GUI 语法一致;
任务/分区 ID 支持 ≥8 位唯一前缀解析(人类输出截断的 8 位 ID 可直接复用)。
4. **管道可靠性**(Windows 命名管道经验)— 服务端写完响应后 `waitForDisconnected` 再关闭
(立即 close 会丢弃未读数据);客户端写完请求立即读响应(`waitForBytesWritten` 会空转超时);
`read_raw` 收到首个 chunk 即返回(防双方互相等待死锁)。
5. **打包** — `Tadado.spec` 单 Analysis 单脚本(`main.py`)双 EXE:`Tadado.exe`
(windowed GUI)+ `tadado-cli.exe`(console CLI)共用同一 `_internal` 包;
入口分流按 `argv[0]` 文件名(`tadado-cli.exe`)或 `--cli` 参数判定
(PyInstaller 对多脚本切片的入口分配不可靠,故不切片);
`build.bat` 改由 spec 驱动。
6. **安全** — `rm`/`archive`/`edit` 提供 `--dry-run`;SKILL.md 规定破坏性操作先展示确认。
7. **环境隔离** — dev 用 `uv run python main.py --cli`(dev DB),发布版用安装的
`tadado-cli.exe`(用户 DB);`TADADO_DATA_DIR` 可覆盖数据目录。
+ 8. **AI 助手托盘入口**(`src/services/ai_assistant.py`)— 托盘「AI 助手」一键启动专属
+ Claude Code / Codex 会话:单一 provider(配置 `ai_assistant.provider` 指定,未配置
+ 自动检测 claude 优先);专用工作区 `ai_workspace/` + 首条指令 `/tadado` 保证
+ skill 唯一加载;未安装助手时菜单置灰。启动时注入 `TADADO_EXE` 指向同版本
+ `tadado-cli.exe`。
**关联修复**:`repository.count()` 的 FTS 分支修正为 `rowid IN` + LIKE 兜底
(原实现 `id IN` 恒假且缺中文 LIKE,导致关键词搜索时 total 恒为 0);
补齐缺失的 `md_exporter.py`/`md_importer.py` 服务模块(修复 GUI 导入/导出
点击即 ImportError 的潜伏 bug);批量导出 Excel 逻辑下沉到 `task_exporter.py`。
---
## 2. 功能模块设计
### 2.1 任务列表
**文件**:[src/ui/task_list/](src/ui/task_list/)
#### 需求
- 9 列 QTableView:复选框(30px)、序号(30px)、创建时间(80px)、任务内容(Stretch)、截止时间(95px)、进度(45px)、状态(55px)、标签(80px)、归档(55px)
- 优先级渲染:delegate 整行 `fillRect` 背景色,4 级颜色走 design_tokens(紧急红/重要橙/关注绿/普通淡蓝),alpha 浅色 35/深色 50。凸显任务红色加粗叠加在背景之上,不跳过
- 优先级定义:`Task.urgency: int`(0=紧急, 1=重要, 2=关注, 3=普通,默认 3)
- Markdown 语法 `- [***]`:`[]` 中仅统计 `*` 数量决定优先级(clamp 0-3),其他字符忽略。状态关键字不在 Markdown 中体现。旧格式 `[ ]`/`[x]` 兼容映射为普通优先级
- Markdown 格式:`- [优先级] <截止日期> <截止时间> 任务内容 #标签`(无状态关键字)。标签 `#` 前须有空格或行首才识别为标签,紧跟内容则视为内容的一部分
- `load_task` 直接使用 `task.raw_md` 显示,不再手动重建
- 编辑面板活动时间线进度行新增"优先级"下拉(DropdownWidget 90px,与状态下拉同样式)
- 批操作栏新增"更改优先级"按钮(QPushButton+QMenu,4 级子菜单)
- 右键菜单新增"更改优先级"子菜单
- 排序:`CASE WHEN status='DONE' THEN 1 ELSE 0 END ASC → urgency ASC → deadline_date ASC(NULL 最后)→ created_at ASC → completed_at DESC`。已完成任务始终排在最底部,组内按完成时间倒序(最近完成的排前面),通过设置 → 任务列表 → "已完成置底"复选框可关闭
- 排序刷新:`_build_filter_with_sort()` 以筛选栏为基底,叠加分区/速览范围
- 自定义绘制:圆形复选框(绿色实心勾)、状态圆角徽章、整行优先级背景
- 暂停任务渲染透明度 45%
- 凸显任务红色加粗(仅 COL_CONTENT 列,不干扰紧急程度整行背景渲染)
- ExtendedSelection 多选 + 右键菜单(编辑/详情/删除/更改状态/更改优先级/复制 MD)
- 分页:上/下页 + 每页条数(20/50/100) + 页码显示
- 交替行颜色、无网格线
#### 实现方案
- **TaskListModel** (`task_list_model.py`) — QAbstractTableModel,`_tasks: list[Task]` 数据源,`_checked_ids: set` 管理复选框。9 列常量定义。`highlighted_task_id()` public getter。支持行前插、行移动、选中状态追踪
- **TaskListDelegate** (`task_list_delegate.py`) — QStyledItemDelegate,`paint()` 中按列分支:0 列画复选框圆,6 列画 `_paint_status_badge()`(圆角矩形 + display_color),8 列画归档文字,3 列凸显任务红色加粗手绘。所有非复选框列调用 `_draw_urgency_bg()` 绘制整行优先级背景色(红/橙/绿/淡蓝,所有行统一绘制不跳过凸显任务)
- **TaskListView** (`task_list_view.py`) — QTableView,`selected_task_ids()` 获取多选。右键菜单新增"更改优先级"子菜单(`_on_change_urgency`),通过 `task_updated` 信号触发列表刷新
- **TaskListPanel** (`task_list_panel.py`) — 独立组合面板(含 TaskInputWidget+FilterBar+TaskListView),部分场景使用
#### 任务凸显方案
**术语定义**:"任务凸显"(Task Highlighting)指当前活动任务(即编辑面板中正在编辑的任务)在任务列表中的视觉强调。实现方式为 **仅 COL_CONTENT 列红色加粗**,无整行高亮,不干扰紧急程度背景渲染。
**触发场景**(统一入口 `MainWindow._on_task_selected(task)`):
- 用户在表格中点击某行
- 单任务/多任务创建完成后自动选中首项
- 速览栏预设切换后自动选中首个任务
- 进度栏筛选后自动选中首个任务
- 筛选栏变更后自动选中首个任务
- 分区激活后自动选中首个任务
- "返回首页"操作后自动选中首个任务
- 轮播点击(经 `_select_and_load_task` 委托)
- 分页翻页后自动选中首个任务
- 数据刷新后恢复上次凸显任务(经 `_select_and_load_task` 委托)
**呈现规则**:
- 仅 COL_CONTENT(任务内容列)文字红色 + 粗体
- 红色值走 `design_tokens.danger`(浅色 `#c0392b` / 深色 `#e07070`),主题切换自动适配
- 紧急程度整行背景色不受影响,所有行统一渲染
- 其余列(行号、创建时间、截止时间、进度、状态、标签、归档)无额外视觉变化
- Qt 原生选中蓝色高亮已被移除(Col 0 复选框列、Col 6 状态徽章列均不再响应 `State_Selected`),选中状态仅通过复选框勾选状态体现
**与其他视觉系统的关系**:
- 紧急程度背景(`_draw_urgency_bg`)是独立的业务配色方案,所有行始终绘制
- 暂停任务透明度 45% 先于凸显渲染
- 多任务批量创建时不再使用 `_bold_task_ids`(排序已确保新任务在顶部,首条红色加粗即可)
**代码入口**(`main_window.py`):
- `_on_task_selected(task)` — 统一凸显入口,执行:`set_highlighted_task` + `load_task` + `selectRow` + `scrollTo`
- `_on_view_task_selected(task)` — 信号守卫,阻止 `selectRow()` 导致的递归重入
- `_select_and_load_task(task_id)` — 按 ID 查找后委托给 `_on_task_selected`
- 模型内部:`TaskListModel._highlighted_task_id`(历史命名,实际效果为红色加粗)
**凸显渲染实现**(`task_list_delegate.py`):
- Col 3(任务内容)凸显时直接手绘红色加粗文字,紧急背景始终绘制于底层,形成叠加效果
**新建任务自动定位**("新建任务状态"机制):
- 单/多任务保存后,`_new_task_sort_active=True`,排序临时切换为"创建时间"倒序,新任务自然排在最前,首条红色加粗
- 同时自动清除搜索、优先级、状态过滤(`reset()` 在 `_setting_sort_internally` 守卫内,避免信号消耗标志位),确保新任务不受当前过滤条件影响
- `activate_preset("today")` 前临时清除 `_new_task_sort_active`,避免 `_on_quick_preset` 恢复默认排序
- 破坏条件(任一触发即恢复默认排序):① 手动调整筛选栏 ② 点击速览栏预设按钮 ③ 切换视图
- 内部守卫 `_setting_sort_internally` 防止代码自身 `set_sort`/`reset` 误触发状态破坏
- 切换视图时通过 `_switch_view` 恢复设置默认排序
- 多任务创建时间戳统一(同一 `now`),`rowid ASC` tiebreaker 保证插入顺序
#### 预览效果
```
┌──────────────────────────────────────────────────────────────┐
│ [☐全选] 更改状态 ▾ │更改优先级▾│ [删除] [中止] [重启] │ ← BatchToolbar
├────┬───┬──────────┬──────────────────┬────────┬────┬──────┬──┤
│ ☐ │ # │ 创建时间 │ 任务内容 │ 截止时间│进度│ 状态 │标签│
├────┼───┼──────────┼──────────────────┼────────┼────┼──────┼──┤
│ ☐ │ 1 │ 2026-05-20│ 重构认证模块 #后端│ 05-30 │80% │ 进行 │后端│ ← 红色背景(紧急)
│ ☐ │ 2 │ 2026-05-22│ 阅读系统设计 #学习│ 06-05 │ 0% │ 待办 │学习│ ← 橙色背景(重要)
│ ☐ │ 3 │ 2026-04-10│ 整理本月开支 #生活│ 04-25 │ 0% │ 逾期 │生活│ ← 绿色背景(关注)
│ ☐ │ 4 │ 2026-06-01│ 随便看看 #杂项 │ -- │ 0% │ 待办 │杂项│ ← 淡蓝背景(普通)
└────┴───┴──────────┴──────────────────┴────────┴────┴──────┴──┘
│ ✓ │ 1 │ 2026-05… │ 重构认证模块 │ 05-30 │80% │ 进行中│#后端│ ← urgency 暖色
│ ☐ │ 2 │ 2026-05… │ 阅读系统设计 │ 06-05 │ 0% │ 待办 │#学习│ ← urgency 中色
│ ☐ │ 3 │ 2026-04… │ 整理本月开支 │ 04-25 │ 0% │ 逾期 │#生活│ ← urgency 红色
├────┴───┴──────────┴──────────────────┴────────┴────┴──────┴──┤
│ ‹ 1 / 3 页 › [20 条/页 ▾] │ ← Pagination
└──────────────────────────────────────────────────────────────┘
```
---
### 2.2 任务编辑器
**文件**:[src/ui/task_list/task_edit_panel.py](src/ui/task_list/task_edit_panel.py)
#### 需求
- 双模式:首页(无选中任务时显示欢迎横幅) / 编辑模式(选中任务后显示详情)
- Markdown 源编辑区(QTextEdit) + 实时 HTML 预览(QLabel rich text)
- 折叠/展开切换:折叠时显示任务摘要,展开时直接进入编辑模式(Markdown 编辑器同步可见)
- 截止日期选择(CalendarPopup) + 时间选择(TimePopup) + 快速计算器(DeadlineIntervalCalculator, 6 选项平铺: 今天/明天/本周日/一周后/本月末/下月今天)
- 草稿模式:新建任务时预填当天日期和标签模板,未保存提示横幅
- 多任务创建:`create_draft_multi()` 生成 3 行模板
- 活动时间线:_TimelineBrowser + 状态下拉 + 优先级下拉 + 进度输入 + 追加进展按钮
- 操作按钮:编辑切换 / 保存 / 删除
- 分区选择器
#### 实现方案
- **TaskEditPanel** (QWidget) — 外层 QVBoxLayout:`_editor_header_widget`(固定顶部标签+折叠按钮) → QScrollArea(内部含 `_draft_banner`、`_editor_collapsible`、`_task_summary`、`_timeline_card`)
- `_editor_collapsible` 包含:`_source_edit`(QTextEdit, Markdown 源) → `_preview_label`(QLabel, 实时 HTML 渲染) → 时间行(`_deadline_date_edit`+`_deadline_time_edit`+快速计算按钮) → 操作按钮(编辑/保存/删除)
- **DeadlineIntervalCalculator** (QDialog) — 快速计算弹窗,6 个截止时间选项平铺展示:今天 / 明天(+1天) / 本周日 / 一周后(+7天) / 本月末 / 下月今天(+1个月),选中后预览"标签 (日期 时间)",点击应用填入日期时间选择器
- `_timeline_card` 包含:状态+进度输入 → `_timeline_browser`(QTextBrowser 检测锚点点击) → `_new_log_input`(QTextEdit)
- 内部类:`_TimelineEntryWidget`(时间线卡片, 图标+时间戳+内容, 48px)、`_TimelineBrowser`(QTextBrowser 子类)、`_BannerWidget`(背景图→半透明遮罩→HTML 文本三层渲染,遮罩 150/160 alpha 确保文字可读)
- Banner 动态切换:`_partition_has_tasks()` 查询当前分区是否有活跃任务,有任务时显示日历日期(`_build_date_html`,📅 + 日期 + 忌拖延·宜行动),无任务时显示欢迎语(`_build_welcome_html`/`_build_draft_html`,🎉/✍️ + 今日无事)
- 信号:`textChanged` → `_on_raw_md_changed()` 实时解析预览
#### 预览效果
```
┌───────────────────────────────────────────────┐
│ ▼ 编辑任务 [折叠 ▲] │ ← _editor_header_widget
├───────────────────────────────────────────────┤
│ ┌─ 草稿未保存 ────────────────────────────┐ │ ← _draft_banner (半透明遮罩 + 动态HTML)
│ │ 📅 2026年6月11日 星期三 │ │ 有任务→日历 / 空分区→欢迎
│ └──────────────────────────────────────────┘ │
│ ── Markdown ───────────────────────────────── │
│ ┌─────────────────────────────────────────┐ │
│ │ - [ ] TODO <2026-05-30> 任务标题 #标签 │ │ ← _source_edit (QTextEdit)
│ └─────────────────────────────────────────┘ │
│ ┌─ 预览 ──────────────────────────────────┐ │
│ │ □ 待办 · 05-30 · 任务标题 `#标签` │ │ ← _preview_label (QLabel)
│ └─────────────────────────────────────────┘ │
│ 截止日: [2026-05-30 ▾] 时间: [14:30 ▾] [快速计算]│ ← CalendarPopup/TimePopup
│ [编辑] [保存] [删除] │
│ ── 活动时间线 ──────────────────────────────── │
│ 状态: [进行中 ▾] 优先级: [紧急 ▾] 进度: [__80__%] [追加进展] │
│ ┌─────────────────────────────────────────┐ │
│ │ ● 05-30 10:30 [进行中|80%|紧急] 收集各团队Q3数据报表 │ │ ← _TimelineEntryWidget
│ │ ○ 05-30 14:00 完成初稿15页 │ │ (48px 卡片)
│ │ + 输入新进展... │ │
│ └─────────────────────────────────────────┘ │
└───────────────────────────────────────────────┘
```
---
### 2.3 底部状态栏
**文件**:[src/ui/main_window.py](src/ui/main_window.py) (`_setup_status_bar` 方法)
#### 需求
- 左:分区图标 + 分区名称 + 按状态分列统计(逾期/进行中/待办/已完成) + 共计总数 + 每日名言(来自 config `motd` 字段)
- 右:实时时钟 年月日 时分秒 AM/PM(每秒更新)
- 空闲锁定:30 秒定时器检测 `_last_activity`,超 `auto_lock_minutes/2` 即锁定分区
#### 实现方案
- QStatusBar + `addWidget(_status_msg, stretch=1)` 左对齐 + `addPermanentWidget(_status_clock)` 右对齐
- QTimer(1000ms) → `_update_status_clock()`
- `_update_status_bar()` 调用 `TaskRepository.get_status_counts(partition_id=...)` 获取当前分区各状态计数,格式化拼接后写入 `_status_msg`
- 统计格式:`逾期 X | 进行中 X | 待办 X | 已完成 X | 共X项`
- `_setup_idle_lock()` QTimer(30s) → `_check_idle_lock()` 比对 `_last_activity`
#### 预览效果
```
┌──────────────────────────────────────────────────────────────────────────────┐
│ 📁 工作 :: 逾期 3 | 进行中 2 | 待办 8 | 已完成 5 | 共18项 | 今日无事 🌿 │ 2026年05月30日 02:30:25 PM │
└──────────────────────────────────────────────────────────────────────────────┘
addWidget (stretch=1) addPermanentWidget
```
---
### 2.4 筛选栏
**文件**:[src/ui/widgets/filter_bar.py](src/ui/widgets/filter_bar.py)
#### 需求
- 搜索框(QLineEdit + 300ms 防抖 QTimer)
- 优先级下拉(DropdownWidget):全部优先级 / ● 紧急(0) / ● 重要(1) / ● 关注(2) / ● 普通(3)
- 状态下拉(DropdownWidget):全部 / 待办 / 进行中 / 已完成 / 逾期
- 排序下拉(DropdownWidget):优先级 / 截止日 / 创建时间 / 状态 / 标题,默认"优先级"
- 所有下拉项选中后显示 ✓ 标记(_CheckmarkDelegate)
#### 实现方案
- FilterBar(QWidget):水平布局 `_search(2x stretch)` + `_priority_combo` + `_status_combo` + `sort_label` + `_sort_combo`
- `_SORT_MAP` 字典映射显示名 → SortCriterion.field
- `build_filter()` 构建 TaskFilter 实例(含 `urgencies` 过滤),通过 `filter_changed` 信号发射
- `reset()` 同时清除搜索、优先级、状态下拉
- 下拉框宽度通过 `combo_width()` 计算:中文 12px/字 + 36px 控件填充
#### 预览效果
```
┌──────────────────────────────────────────────────────────────────────────┐
│ [搜索任务关键词..._________] 优先级 ▾ ▼ 状态 ▾ ▼ 排序 ▾ ▼ │
└──────────────────────────────────────────────────────────────────────────┘
stretch=2 combo(4字) combo(3字) combo(4字)
```
---
### 2.5 统计组件
**文件**:[src/ui/widgets/status_badge_strip.py](src/ui/widgets/status_badge_strip.py)、[progress_dynamics_bar.py](src/ui/widgets/progress_dynamics_bar.py)、[quick_overview_bar.py](src/ui/widgets/quick_overview_bar.py)
#### 2.5.1 StatusBadgeStrip — 状态徽章
**需求**:4 个可点击状态计数徽章(逾期/待办/进行中/已完成),点击切换激活(背景填充+文字加粗),再点取消。
**实现方案**:`_StatBadge(QPushButton)` 内部类,`_active` 控制填充/轮廓样式,clicked → `filter_changed(TaskFilter)`。
**预览**:
```
┌────────┐ ┌────────┐ ┌──────────┐ ┌──────────┐
│ 逾期 3 │ │ 待办 8 │ │ 进行中 2 │ │ 已完成 5 │
└────────┘ └────────┘ └──────────┘ └──────────┘
红色药丸 蓝色 橙色 绿色
点击=激活 点击=激活 点击=激活 点击=激活
```
#### 2.5.2 ProgressDynamicsBar — 进度动态栏
**需求**:6 个时段按钮(始终可点击,不再联动速览栏)。双重模式:未点击时 1 列显示最近活跃任务的最新进展(包含截止紧迫度);点击后 1 列轮播按活动日志数量降序的 Top 6 任务。
**实现方案**:`_show_latest_activity()` 扫描 activity_log 找最新记录,追加 `deadline_suffix()`(如 `⏰14:30截止`、`⚠逾期2天`)。`_rank_tasks()` 按 `activity_count × deadline_weight` 降序排列(今日/逾期任务加权 2×,3 天内加权 1.5×)。`reset_to_unclicked()` 取消选中并回到 hint 模式。点击发射 `progress_filter_activated(TaskFilter)`。`filter_tasks_by_activity()` 做 Python 层 activity_log timestamp 精准扫描。
**预览**:
```
未点击: [昨天] [今天] [上周] [本周] [上月] [本月] │ 最新: 重构认证模块 — 完成了接口联调 ⏰14:30截止
点击后: [昨天] [■今天] [上周] [本周] [上月] [本月] │ +3条 重构认证模块 ⚠逾期2天
```
按钮始终可点击 (不与速览栏锁定) 轮播区 (单列, 点击后每5s切换)
#### 2.5.3 QuickOverviewBar — 速览栏
**需求**:6 个预设按钮(昨天/今天/上周/本周/上月/本月) + 自动轮播(按紧急度排序,每 5 秒切换展示组,每组 3 个任务)。标签支持时间粒度:当 `deadline_time` 存在且 deadline 为今天时,显示"X 分钟后"/"X 小时后"/"HH:MM截止"/"已超时";颜色按剩余时间分 4 档(>3h 绿、1-3h 橙、<1h 红、已超时 红)。tooltip 显示精确截止日期时间。
**过滤逻辑**(2026-06-07 重构,2026-06-11 修订):按 `created_at ≤ 时间上限` + `archived = 0`(排除已完成已归档),不再按 `deadline_date` 过滤:
| 按钮 | 过滤 |
|------|------|
| 昨天 | 昨天及之前创建 + 排除已归档 |
| 今天 | 今天及之前创建 + 排除已归档 |
| 上周 | 上周日及之前创建 + 排除已归档 |
| 本周 | 本周日及之前创建 + 排除已归档 |
| 上月 | 上月最后一天及之前创建 + 排除已归档 |
| 本月 | 本月最后一天及之前创建 + 排除已归档 |
**实现方案**:`_build_ui()` 创建预设按钮 + 轮播区 QLabel,QTimer(5s) → `_scroll()`。`preset_activated(str)` 和 `task_clicked(task_id)` 信号。`build_filter()` 设置 `created_to`,仓库层 `archived=0` 默认排除已归档。
**预览**:
```
┌────────────────────────────────────────────────────────────┐
│ [昨天] [■今天] [上周] [本周] [上月] [本月] │ 重构认证模块 阅读系统… │
└────────────────────────────────────────────────────────────┘
预设按钮 (点击高亮蓝色) 轮播区 (每5s切换)
```
---
### 2.6 活动分析(Activity Analysis)
**文件**:[src/ui/calendar_heatmap/](src/ui/calendar_heatmap/)、[src/ui/main_window.py](src/ui/main_window.py)(`_switch_view("dashboard")`)
#### 需求
- Ctrl+2 切换,上下两区布局:「活动热力图」+「活动报告」
- 紧凑热力图(12px 单元格,4 组配色方案可选:☀️ 暖阳 / 🌱 新绿 / 🌊 海洋 / 🌸 樱花)+ 悬浮 Tooltip + 点击日期选中
- 统计卡片同行右侧显示
- PeriodSelectorBar:昨天/今天/上周/本周/上月/本月 + 自定义日期范围(CalendarPopup)
- 左侧 TaskTreePanel:FlowLayout 胶囊标签云(可勾选,默认仅勾选活动数>0的标签,活动数为0的标签不预选),顶部搜索框(实时筛选标签)+ 紧凑全选切换按钮(28×22),标签名超 8 字截断;与右侧导航栏同行水平对齐
- 右侧 ActivityContentView:顶部导航栏(◀ ▶ │ #标签名 (序号/总数),靠左,28×22 箭头按钮)+ 1px 分隔线 + 有序列表活动内容;点击箭头在勾选标签队列中循环切换
- 搜索框实时过滤内容 + 导出(MD/Excel/TXT,按勾选标签全量导出,文件名含分区+日期范围+标签数,Excel 分列)
- 时段选择 + 标签点击/勾选 + 搜索 + 导航按钮联动
- 热力图日期点击 → 设置自定义日期范围
#### 实现方案
- `CalendarHeatmapWidget`(紧凑常量 + 配色方案 `heatmap_gradient()`,支持 4 组方案切换)
- `HeatmapStatsPanel`、`PeriodSelectorBar`(`_CalendarDateEdit` 子类 + CalendarPopup)
- `TaskTreePanel`:FlowLayout 胶囊标签云 + 搜索框 `_apply_tag_filter()` 实时过滤 + 紧凑全选按钮(`toggle_all_checked()`) + `tag_selected`/`checked_tags_changed` 信号 + `select_prev()`/`select_next()` 循环切换
- `ActivityContentView`:顶部导航栏(`prev_requested`/`next_requested` 信号)+ QTextBrowser HTML 有序列表 + `set_current_tag(tag, pos, total)` 显示序号 + `set_search_text`/`get_plain_text`
- 导出默认文件名 `{分区}_{日期范围}_{N}个标签.{ext}`,按勾选标签全量导出,Excel 分列(序号/任务/状态变更/进度变更/活动信息)
### 2.7 任务管理控制台
**文件**:[src/ui/main_window.py](src/ui/main_window.py)(`_switch_view("batch")`)
#### 定位
任务管理控制台 = 审视全局 → 定位问题 → 批量处置。区别于编辑视图(Ctrl+1)的「浏览编辑单任务」,管理视图侧重「批量审视、处置多任务」。
与编辑视图的差异:
| | 编辑视图 (Ctrl+1) | 管理控制台 (Ctrl+3) |
|---|---|---|
| 核心任务 | 浏览、编辑单个任务 | 批量审视、处置多任务 |
| 表格 | 半栏 + 编辑面板 | 全宽 9 列(含归档列) |
| 筛选 | 完整 FilterBar (搜索+状态+排序) | 仅关键词搜索 |
| 批量操作 | BatchToolbar(辅助) | BatchToolbar + 导出下拉(核心) |
| 导入导出 | 无 | 导出 MD / 导出 Excel |
| 清理 | 无 | 手动归档 + 清除已归档 |
#### 需求
- Ctrl+3 切换,左右分栏:左侧管理面板(180px) + 中间任务表格 + 右侧标签管理面板(30%)
- 左侧面板:关键词搜索框 + 归档/清理操作按钮 + 筛选条件(状态/时间/进度/标签/归档状态)
- 右侧面板:标签管理(重命名/合并),帮助快速规范化统一标签
- 表格 9 列:复选框(36px)、序号(36px)、创建时间(100px)、任务内容(Stretch)、截止时间(105px)、进度(55px)、状态(65px)、标签(90px)、归档(55px)
- 归档列:已完成(archived=1)显示「已归档」、已完成(archived=0)显示「未归档」、未完成显示「/」
- BatchToolbar 新增「导出▾」下拉按钮(导出 MD / 导出 Excel)
- 分页(20条/页)+ ConfirmBar 确认机制
- 手动归档:立即归档当前分区所有已完成任务(不受 `archive_days` 阈值限制)
- 清除已归档:永久删除当前分区所有 `archived=1` 任务(需二次确认)
#### 手动归档 vs 自动归档
| | 自动归档 (TaskArchiver) | 手动归档 (管理控制台) |
|---|---|---|
| 触发 | 每日 02:07 Cron | 用户点击按钮 |
| 范围 | 受分区 `archive_days` 阈值限制 | 当前分区**全部**已完成任务 |
| 依赖 | 仅 `archive_days` 控制(0=即时,9999=永不) | 不依赖配置,随时可用 |
| 定位 | 日常自动维护 | 用户主动即时清理 |
两者互补:自动归档负责日常按规则静默维护,手动归档给用户随时清理的自由。
#### 实现方案
- 左侧面板 `_manage_sidebar`(QWidget, fixedWidth=180):QVBoxLayout 排列筛选区 + 操作区
- 搜索框 `_batch_search`(QLineEdit + 300ms 防抖)+ 状态下拉 `_batch_status_combo` + 优先级下拉 `_batch_priority_combo`
- 标签搜索框 `_batch_tag_input`(QLineEdit),placeholder 为 `#标签1 #标签2`,输入自动剥离 `#` 前缀后转为 filter.tags 集合
- 归档按钮 → `_on_manual_archive()`:获取当前分区所有 DONE 任务 → `repository.archive_batch(ids)` → 刷新表格
- 清除按钮 → `_on_clear_archived()`:QMessageBox 二次确认 → `repository.batch_delete(archived_ids)` → 刷新表格
- 独立 `TaskListModel`(9 列)+ `TaskListView` 实例
- BatchToolbar 新增导出下拉 `_export_menu`(QMenu + `export_requested(str)` 信号),复用 MarkdownExporter / ReportExporter
- 确认浮层 `_confirm_bar`:QWidget 显隐控制 + 操作委派
- `_batch_pending_action` dict 存储待执行操作
- 右侧标签管理面板 `_batch_tag_panel`(TagManagementPanel):QSplitter 70:30 布局,标签列表 + 搜索 + 重命名/合并按钮
#### 预览效果
```
┌──────────────────────────────────────────────────────────────────┐
│ 任务管理控制台 [← 返回] │
├────────────┬─────────────────────────────────────────────────────┤
│ 搜索 │ [全选][更改状态▾][删除][中止][重启][导出▾] 已选3项 │
│ [________] │ │
│ ├─────────────────────────────────────────────────────┤
│ 清理 │ ┌──┬──┬──────────┬──────────┬──┬────┬──────┬──┬──┐ │
│ [归档已完成]│ │☐ │ #│ 任务内容 │ 截止时间 │进度│状态│ 标签 │归档│ │
│ [清除已归档]│ ├──┼──┼──────────┼──────────┼──┼────┼──────┼──┼──┤ │
│ │ │ │ │重构认证模块│ 05-30 │80%│进行中│ #后端│ / │ │
│ │ │ │ │阅读系统设计│ 06-05 │ 0%│ 待办 │ #学习│ / │ │
│ │ │ │ │整理本月开支│ 04-25 │ 0%│ 逾期 │ #生活│ / │ │
│ │ │✓ │ │已完成项目 │ 05-20 │100%│已完成│ #工作│未归档│ │
│ │ │ │ │旧版重构 │ 03-15 │100%│已完成│ #归档│已归档│ │
│ │ └──┴──┴──────────┴──────────┴──┴────┴──────┴──┴──┘ │
│ ├─────────────────────────────────────────────────────┤
│ │ ‹ 上一页 1 / 3 页 下一页 › 共 18 项 │
│ │ [确认栏: 确认删除 3 项任务? 是 / 否] │
└────────────┴─────────────────────────────────────────────────────┘
180px stretch
```
#### 与设置中归档配置的关系
- 设置 → 自动化 → 启用自动归档:仅控制 TaskArchiver 定时任务,不影响手动归档按钮
- 设置 → 分区管理 → 归档天数:仅影响自动归档的筛选阈值,手动归档不受此限制
- 手动归档和清除已归档始终作用于当前分区,与配置开关无关
#### 标签管理面板
**文件**:[src/ui/widgets/tag_management_panel.py](src/ui/widgets/tag_management_panel.py)
**定位**:帮助用户快速规范化统一标签,提供标签重命名和合并功能,操作自动同步更新所有关联任务。
**需求**:
- 右侧 30% 面板,与左侧内容通过 QSplitter(70:30) 分隔
- 标签列表按使用次数降序排列,显示格式 `#标签名 (N)`
- 搜索框实时过滤标签(300ms 防抖)
- 重命名:选中标签 → 弹窗输入新名 → 自动更新所有含该标签的任务(raw_md + tags JSON + FTS5)
- 合并:多选标签(Ctrl+click) → 弹窗选择合并目标 → 批量替换 + 去重
- 右键菜单:快捷重命名 / 合并选中到此
- 重命名/合并操作涵盖所有任务(含已归档),标签列表同步显示全部任务的标签计数
- 操作后发射 `tag_changed` 信号 → 桥接 `SignalBus.tag_changed` → `FilterCoordinator.refresh()`(主视图)+ `BatchController.refresh_page()`(批量页面任务表格)
**实现方案**:
- `TagManagementPanel(QWidget)`:外层容器(bg_secondary + border-left) + QVBoxLayout
- 标题栏 "🏷 标签管理" + 搜索框 + QListWidget(ExtendedSelection) + 按钮行(重命名/合并/刷新)
- 空态显示 "暂无标签"
- 重命名流程:`QInputDialog.getText()` → 校验(# 字符/冲突检测) → `_execute_rename()` 逐任务替换并 `MarkdownTaskFormatter.format()` 再生 raw_md → 发射信号
- 合并流程:自定义 QDialog(QComboBox 选目标) → `_execute_merge()` 逐任务替换源标签 → 去重 → 再生 raw_md → 发射信号
- 分区感知:`set_partition_id()` 限定标签范围,`refresh()` 调用 `repository.get_all_tags_with_counts(partition_id)`;分区激活时 `BatchController.set_active_partition()` 传播,视图切换时同步
- 仓库新增方法:`get_all_tags_with_counts()`、`get_tasks_by_tag()`、`get_tasks_by_tags()` — 均不做 `archived` 过滤,确保标签操作全局生效
- 任务-标签双向联动:点击任务行 → 标签面板中该任务关联的标签加粗+accent 色高亮;点击标签项 → 任务列表中含该标签的任务前置(再次点击同一标签取消前置);鼠标悬停标签列显示全部标签信息
- 信号:`tag_clicked(str)` (QListWidget itemClicked → emit tag_clicked) + `highlight_tags(set[str])` (字体加粗+accent色);`TaskListView.selection_cleared()` 清空选中时取消高亮
**最终更新预览**:
```
┌──────────────────────────────────────────────────────────────────────────────┐
│ 任务管理控制台 [← 返回] │
├────────────┬──────────────────────────────────────┬──────────────────────────┤
│ 筛选 │ [全选][更改状态▾][删除][中止]... │ 🏷 标签管理 │
│ [关键词] │ │ │
│ [状态] │ ┌──┬──┬──────────┬──────────┬──┬──┐ │ 🔍 搜索标签... │
│ [创建时间] │ │☐ │ #│ 任务内容 │ 截止时间 │..│..│ │ │
│ [截止时间] │ ├──┼──┼──────────┼──────────┼──┼──┤ │ #work (12) │
│ [进度] │ │ │ │重构认证模块│ 05-30 │..│..│ │ #personal (8) │
│ #标签1 #标签2│ │ │ │阅读系统设计│ 06-05 │..│..│ │ #health (5) │
│ [归档状态] │ │ │ │整理本月开支│ 04-25 │..│..│ │ │
│ │ └──┴──┴──────────┴──────────┴──┴──┘ │ [✏ 重命名] [🔗 合并] │
│ 操作 │ │ [🔄 刷新] │
│ [归档已完成]│ ‹ 上一页 1 / 3 页 下一页 › 共18项 │ │
│ [清除已归档]│ │ │
└────────────┴──────────────────────────────────────┴──────────────────────────┘
180px ~50% ~30%
```
### 2.8 日历热图(基础组件)
#### 需求
- 12 月 × 7 天(行) × 5 周(列) 矩阵布局
- 配色方案渐变(4 组可选,`heatmap_gradient()` 8 级,空单元格→高活跃)
- 月份标题横轴、星期标签纵轴
- 日期悬浮 Tooltip:显示日期 + 条目数 + 任务数 + 标签分解
- 标签筛选下拉 + 年份切换(< 2026 >)
- 点击日期可创建任务
- 活动报告面板:按标签分组进度摘要,支持 Markdown/Excel 导出
- 高亮范围:来自速览栏预设的列背景色调
#### 实现方案
- **CalendarHeatmapWidget** (QWidget) — `nav_bar`(标签筛选+年份切换+返回按钮) + `_HeatmapGrid`(自定义 paintEvent)
- **_HeatmapGrid** — `paintEvent()` 中逐单元格绘制:12 列(月)×5 列(周)=60 列矩阵。`_cell_step` / `_cell_size` 在 `resizeEvent()` 中自适应。`_date_at_pos()` 像素坐标→日期
- **HeatmapModel** (非 QT) — 加载 `get_heatmap_activity_data()` 返回 `(entry_counts, task_counts)` 字典,8 级对数刻度分桶。统计:总计/活跃天/最长连续/当前连续/日均/月均/周均
- **HeatmapTooltip** (QDialog) — 无边框置顶浮动卡片,`tag_breakdown_for_date()` 显示明细
- **ActivityReportPanel** — QTreeWidget(标签→任务树)+QTextBrowser(HTML 详情),Export 按钮调用 `export_markdown()` / `export_excel()`
- **HeatmapCollapsePanel** — 可折叠封装器,零边距 QVBoxLayout
#### 预览效果
```
┌──────────────────────────────────────────────────────────────┐
│ [全部标签 ▾ ▼] ‹ 2026 › [返回任务] │ ← NavBar
├──────────────────────────────────────────────────────────────┤
│ 1月 2月 3月 4月 5月 6月 ... 12月 │ ← 月份标签
│ 一 □□□□□ □□□□□ □□□□□ □□□□□ □□□□□ □□□□□ ... □□□□□ │ ← Mon
│ 二 □□□□□ □□□□□ □□□□□ □□□□□ □□□□□ □□□□□ ... □□□□□ │
│ 三 □■■□□ □■■□□ □■■□□ □■■□□ □■■□□ □■■□□ ... □■■□□ │
│ 四 □■■□□ □■■□□ □■■□□ □■■□□ □■■□□ □■■□□ ... □■■□□ │
│ 五 □□□□□ □□□□□ □□□□□ □□□□□ □□□□□ □□□□□ ... □□□□□ │
│ 六 □□□□□ □□□□□ □□□□□ □□□□□ □□□□□ □□□□□ ... □□□□□ │
│ 日 □□□□□ □□□□□ □□□□□ □□□□□ □□□□□ □□□□□ ... □□□□□ │
│ │
│ 悬浮 Tooltip: │
│ ┌──────────────────────┐ │
│ │ 2026年5月30日 星期六 │ │
│ │ 条目: 5 任务: 3 │ │
│ │ #后端: 1 #学习: 2 │ │
│ └──────────────────────┘ │
│ │
│ 图例: □ □ ■ ■ ■ ■ ■ ■ │
│ 0 1 2 3 4 5 6 7+ → accent 渐变 │
├──────────────────────────────────────────────────────────────┤
│ ┌─ 活动报告 ───────────────────────────────────────────┐ │
│ │ 1. #后端 重构认证模块 (30%→80%):完成JWT验证… │ │
│ │ 2. #学习 阅读系统设计 (50%→90%):优化查询… │ │
│ │ [导出MD] [导出Excel] │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
```
---
### 2.7 分区管理
**文件**:[src/models/partition.py](src/models/partition.py)、[src/models/repository.py](src/models/repository.py)(分区 CRUD 方法)、[src/ui/main_window.py](src/ui/main_window.py)(密码锁定 UI)
#### 需求
- 分区 CRUD:增删改查,**禁止删除最后一个分区**(至少保留一个)
- 默认分区:设置中设定,运行时启动激活链 `last_partition_id → default_partition → first_unlocked`,确保状态栏始终有活动分区
- 首次启动:`ensure_default_partition()` 自动创建名为"功能演示"的默认分区
- 密码保护:设置/清除密码,解锁后内存中保留;**切换分区立即恢复锁定**(2026-06-04 修复:此前解锁后切换分区不会重新锁定,为严重安全漏洞);**状态栏分区按钮/菜单显示 🔒(锁定)/🔓(已解锁)双态**,与 ✓ 选中标记互不冲突
- 自动锁定:按分区独立设置(默认 3 分钟),空闲超过 `auto_lock_minutes/2` 时自动锁定;仅对有密码的分区生效;**使用 DB 直接查询密码状态,解锁后仍可正常触发重新锁定**(2026-06-04 修复:此前解锁后 `_partition_passwords[pid]=""` 导致锁定条件永远为 False)
- 分区切换:QToolButton 下拉菜单,切换后刷新任务列表
- 归档天数:每分区独立配置,默认值 0。0=完成任务后即时归档,1~9998=完成后N天午夜归档,9999=永不归档
#### 实现方案
- SQLite `partitions` 表:id, name, sort_order, password, archive_days, auto_lock_minutes, created_at
- `_partition_passwords: dict[str, str]` — 主窗口内存管理(空字符串=已解锁),`_load_partitions()` 从 DB 同步
- `_partition_auto_lock: dict[str, int]` — 按分区独立自动锁定分钟数(默认 3),`_check_idle_lock()` 读取
- `_partition_mask` (QStackedLayout index 1) — 密码蒙版覆盖 QSplitter,含提示标签+解锁按钮
- 空闲检测:`_check_idle_lock()` 每 30s 运行,`now - _last_activity > auto_lock_minutes/2` → `_lock_partition()`
- **默认分区激活链**:`_load_partitions()` 中按 `last_partition_id → default_partition → first_unlocked` 优先级激活;若当前激活分区被删除(不存在于 DB),自动重置为 None 落入激活链
- **分区过滤一致性**:`_refresh_all_views` 和 `_build_filter_with_sort` 中 `partition_id` 空字符串统一转 `None`,避免 SQL `WHERE partition_id=''` 不匹配 `NULL`
#### 预览效果
```
分区切换按钮: [📁 工作 ▾ ▼]
├───────────
│ 📁 工作 ← 当前
│ 📁 个人
│ 📁 学习
│ ──────────
│ ⊕ 新建分区
└───────────
未解锁分区蒙版:
┌──────────────────────────────────────────────┐
│ │
│ 🔒 此分区已锁定 │
│ 请输入密码解锁 │
│ [________] [解锁] │
│ │
└──────────────────────────────────────────────┘
```
---
### 2.8 设置对话框
**文件**:[src/ui/dialogs/settings_dialog.py](src/ui/dialogs/settings_dialog.py)
#### 需求
单页布局,4 个功能区块,QGridLayout 双列统一对齐:
| 区块 | 配置项 | 控件 |
|------|--------|------|
| 外观 | 主题、最小化到托盘、开机自动启动 | 下拉(120px) / 复选框 / 复选框 |
| 任务列表 | 每页条数、默认排序、已完成置底 | 下拉(120px) / 下拉(120px) / 复选框,同列显示 |
| 活动热力图 | 起始年份、配色方案 | 下拉(当前年份±5) / 下拉(4组方案),同行显示 |
| 归档 / 分区管理 | 分区设定表格 | 5列:名称、默认分区、归档阈值(天)、自动锁定(分)、密码;工具栏"+ 新增""− 删除";表格最大高度 300px,表头固定 |
归档阈值 >= 0,默认 0。0=完成任务后即时归档,1~9998=完成后N天午夜归档,9999=永不归档。双击单元格编辑数值。复选框改用标准 QCheckBox(无动画)。
#### 实现方案
- QGridLayout 双列布局:列 0 标签(100px 右对齐)、列 1 字段(自适应),section header 跨两列
- 归档分区表格使用 `_CenterHost`(stretch-sandwich 布局:VBox+stretch+HBox+stretch+widget+stretch+stretch)包裹 QCheckBox/QPushButton 实现水平垂直居中,避免 `sizeHint()` 受全局 QSS 污染
- QCheckBox 设置 `spacing: 0px;` 消除全局 QSS 的 8px 文本间距对无文字复选框的偏移
- QPushButton(密码按钮)设置 `padding: 0px;` 覆盖全局 QSS 内边距
- `_update_table_height()` 设置 `setMaximumHeight(300)` + `setMinimumHeight(min(content, 300))`,表头固定、表体内部滚动
- 工具栏(+ 新增 / − 删除)位于表格上方、ScrollArea 内,始终可见
- 默认分区 QCheckBox 单选互斥(`_on_default_toggled`),**禁止取消最后一个默认分区**
- 删除分区校验:`count_tasks_in_partition()` > 0 时阻止;**仅剩一个分区时禁止删除**
- **保存前校验**:`_on_accept` 验证有且仅有一个默认分区,不通过则拒绝关闭
- **兜底修复**:`_populate_partition_table` 若无默认分区被勾选(default_id 失效),自动勾选首个
- 确认时调用 `_config.set()` → `_config.save()` → 发射 `config_changed`
---
### 2.9 系统托盘
**文件**:[src/ui/system_tray.py](src/ui/system_tray.py)
#### 需求
- 系统托盘图标(QSystemTrayIcon)
- 右键菜单:显示/隐藏窗口、新建任务(打开+聚焦输入)、退出
- 双击托盘图标切换窗口可见性
- `show_message()` 用于每日摘要推送(TaskNotifier 调用)
#### 实现方案
- `SystemTrayManager`(非 QWidget) — `_build_menu()` 构建 QMenu,`activated` 信号→`_on_activated()`(DoubleClick→`_toggle_window()`)
- 图标通过 `IconLoader.app_icon()` 加载多分辨率 .ico
#### 预览效果
```
任务栏托盘:
[📋] ← 右键 ┌──────────────┐
│ 显示/隐藏窗口 │
│ 新建任务... │
│ ──────────── │
│ 退出 │
└──────────────┘
每日摘要气泡:
┌──────────────────────────┐
│ Tadado 每日摘要 │
│ 逾期 2 项,今日到期 5 项 │
│ 写报告、修Bug、开会 │
│ …等 7 项 │
└──────────────────────────┘
```
---
### 2.10 版本与更新
**文件**:[src/services/update_checker.py](src/services/update_checker.py)、[src/ui/dialogs/about_dialog.py](src/ui/dialogs/about_dialog.py)
#### 需求
帮助 → 关于对话框提供版本追溯和更新检测:
- 显示当前版本号(统一来源 `src/_version_data.py`,通过 `src/version.py` 公开 API 访问)
- [检查更新] 按钮:先查 GitHub Release API,不可达则自动回退到阿里云盘(通过本地 `aliyunpan` CLI 查询文件夹内安装包文件名解析最新版本)
- 20 秒超时,超时按"无更新"处理
- 检测到新版本时,下载渠道区标注 ⭐ 推荐
- 提供 GitHub Releases + 阿里云盘双下载渠道
- 交流方式:邮箱 + 微信公众号 + GitHub 项目地址
#### 实现方案
- `UpdateChecker(QObject)`:`QNetworkAccessManager` 异步查询 GitHub API,失败时通过 `QProcess` 调 `aliyunpan ls` 解析云盘文件版本
- `AboutDialog` 新增 `update_checker` 参数,[检查更新] 按钮禁用态、结果文字、下载渠道动态 ⭐ 标注
- 版本比较:`tuple(int,int,int)` 去 `v` 前缀
- 阿里云盘上传:`release.ps1` + `upload_aliyun.ps1`(本地脚本,不入库)通过 `aliyunpan` CLI 上传至资源库 `/Tadado/`
---
### 2.11 后台服务
**文件**:[src/services/scheduler.py](src/services/scheduler.py)、[notifier.py](src/services/notifier.py)、[archiver.py](src/services/archiver.py)、[recurrence.py](src/services/recurrence.py)
#### 2.10.1 TaskScheduler — 定时调度
**需求**:每分钟 `refresh_overdue_status()` 自动设置/恢复 OVERDUE 状态。每天在配置时间(`daily_digest_time`,默认 09:00)发射 `daily_digest` 信号供 Notifier 发送每日摘要。
**实现方案**:APScheduler `QtScheduler` + 双 job:(1) IntervalTrigger(1min) 做 overdue 刷新,(2) CronTrigger(hour, minute) 做每日摘要。提醒的主要能力已迁移到轮播栏(QuickOverviewBar / ProgressDynamicsBar)的被动信息展示。
#### 2.10.2 TaskNotifier — 每日摘要
**需求**:监听 `daily_digest`,遵守安静时段与 `reminders.enabled` 开关,查询今日到期+逾期任务,合并为单条托盘摘要通知。
**实现方案**:`_on_daily_digest()` → 查 `get_due_today()` + `get_overdue()` → 拼装 "逾期 N 项,今日到期 M 项\nA、B、C…等 X 项" → `tray.show_message()`。安静时段逻辑不变。
#### 2.10.3 TaskArchiver — 自动归档
**需求**:每日 02:07(Cron) 运行,按分区 `archive_days` 阈值归档已完成任务。
**归档规则**:
- `archive_days = 0`:任务标记 DONE 时即时归档(通过监听 `task_status_changed` 信号触发,覆盖单任务、批量操作、新建即 DONE 三条路径)
- `archive_days = 1~9998`:完成后 N 天,午夜自动归档。筛选条件:`completed_at ≤ today - archive_days`
- `archive_days ≥ 9999`:永不归档
- 切换分区或设置中改阈值为 0 时,追溯归档该分区所有已有 DONE 任务
- 归档任务从 DONE 改为其他状态时,自动取消归档(`archived=1 → archived=0`)
**实现方案**:APScheduler CronTrigger(hour=2, minute=7)。遍历分区 → `get_tasks_for_archive(cutoff)` → `archive_batch(task_ids)` → 发射 `archive_completed(count)`。即时归档和取消归档由 TaskService 监听 SignalBus 信号处理。
#### 2.10.4 TaskRecurrence — 循环任务
**需求**:任务完成(DONE)时,根据 `recurrence_rule`(+1d/+1w/+1m/+1y)自动创建下一实例。
**实现方案**:监听 `task_status_changed`(仅 DONE 触发)。`_parse_rule()`:`timedelta(d/w)` 或 `dateutil.relativedelta(m/y)`。新任务:TODO 状态 + 偏移日期 + 继承标题/标签/规则。
#### 服务间交互
```
每分钟 ──→ TaskScheduler._check_due_tasks()
└─ refresh_overdue_status()
每日 09:00 ──→ TaskScheduler._emit_daily_digest()
└─ emit(daily_digest) ──→ TaskNotifier
└─ 检查 enabled + quiet_hours → 查询到期/逾期 → 合并为单条托盘摘要
每日 02:07 ──→ TaskArchiver._run_archive()
└─ 按分区 archive_days 归档 → emit(archive_completed)
任务 DONE ──→ TaskRecurrence._on_status_changed()
└─ 解析 recurrence_rule → 创建新任务实例 → emit(task_created)
```
---
### 2.11 导入/导出
**文件**:[src/services/](src/services/)(md_importer / md_exporter)、[src/ui/calendar_heatmap/report_exporter.py](src/ui/calendar_heatmap/report_exporter.py)
#### 需求
- 导入 Markdown:文件对话框 → 逐行解析 → 批量入库
- 导出 Markdown:遍历任务 → 每行 raw_md 写入文件
- 活动报告导出:Markdown(紧凑文本) / Excel(openpyxl 格式化工作簿)
#### 实现方案
- 导入:`QFileDialog.getOpenFileName()` → 逐行 `MarkdownTaskParser.parse()` → `TaskRepository.insert()` 批量
- 导出:`QFileDialog.getSaveFileName()` → 遍历 `Task.raw_md` → 写入文件
- 活动报告:`export_markdown(report_data)` 字符串拼接 / `export_excel(report_data, filepath)` openpyxl Workbook
---
### 2.12 窗口管理
**文件**:[src/ui/main_window.py](src/ui/main_window.py)、[src/ui/icon_draw.py](src/ui/icon_draw.py)、[src/utils/win32_theme.py](src/utils/win32_theme.py)
#### 需求
- 无边框窗口(`FramelessWindowHint`)
- 自定义标题栏(36px):App 图标 + 6 个图标文字按钮(新建单任务/多任务/活动分析/任务管理/设置/帮助▾)+ 右侧图标按钮(缩小到托盘/最小化/切换全屏/关闭)
- Windows Aero Snap 支持(左右停靠、四分之一分屏、拖拽到顶部最大化、Win+方向键快捷键)
- Win32 原生拖拽 + 边缘缩放(8px 热区边框,与 Win10/11 标准一致)
- 固定默认尺寸:1050×680,用户可通过全屏按钮调整
- 图标运行时绘制(Phosphor 风格填充 PRIMARY 蓝),主题色适配(`design_tokens.text_primary`)
- 分区选择器位于状态栏左侧(accent 色加粗按钮)
- 最小化行为受 `minimize_to_tray` 配置控制:开启时最小化→隐藏到托盘,关闭时正常最小化到任务栏
#### 实现方案
- `_setup_custom_title_bar()` — 固定 36px QWidget,QHBoxLayout:AppIcon → 6×图标按钮 → stretch → 4×窗口按钮
- `_ThemedIconEngine` (icon_loader.py):运行时 QPainter 绘制,颜色自 `design_tokens`
- `enable_window_snap()` ([src/utils/win32_theme.py](src/utils/win32_theme.py)) — 通过 `SetWindowLongW` 恢复 `WS_THICKFRAME | WS_CAPTION` 窗口样式,启用 Aero Snap;`DWMWA_NCRENDERING_DISABLED` 阻止 DWM 实际绘制原生标题栏
- `nativeEvent()` 处理三种消息:
- `WM_NCHITTEST`:用 `childAt()` 精确识别光标下方是否有按钮控件,有按钮→`HTCLIENT`(可点击),无按钮→`HTCAPTION`(整条标题栏空白区域均可拖拽触发 Snap)
- `WM_NCCALCSIZE` (wParam 0 和 1 均处理):扩展客户区覆盖整个窗口,防止隐形边框压缩内容
- `WM_GETMINMAXINFO`:交给 DefWindowProc 默认处理,最大化时适配显示器工作区
- `changeEvent()` 拦截 `WindowStateChange`:`minimize_to_tray=True` 时最小化→`hide()` 隐藏到托盘
- 标题栏命中测试区域:右 144px(4×36px)为窗口按钮区
- 状态栏 `_status_partition_btn` + `_status_partition_menu` 替代原菜单栏分区项
#### 启动残影防护
**问题**:Win11 上 `FramelessWindowHint` 无边框窗口启动时,DWM 在 Qt 自定义渲染就绪前短暂绘制原生标题栏按钮(`_ □ X`)。经 8 轮排查,根因在 DWM 首次合成时机 — 之前所有 DWM 层方案均在 `show()` 之后设置,为时已晚。
**解决方案** — 三层纵深防御:
| 层级 | 机制 | 文件 | 说明 |
|------|------|------|------|
| 1 | **启动遮罩** `StartupShield` | [src/ui/splash_screen.py](src/ui/splash_screen.py) | 主题色 QWidget(`Tool`+`WindowStaysOnTopHint`),在任何耗时初始化前显示,覆盖整个启动过程 |
| 2 | **DWM NC 渲染禁用** | [src/ui/main_window.py](src/ui/main_window.py) | `DWMWA_NCRENDERING_POLICY = DWMNCRP_DISABLED`,在 `show()` 前通过 `winId()` 强制创建 HWND 后立即设置,从 DWM 层禁止绘制原生 NC 按钮 |
| 3 | **DWM CLOAK** | [src/ui/main_window.py](src/ui/main_window.py) | `DWMWA_CLOAK`,在 `show()` 前设置,窗口对 DWM 完全不可见;`_finish_startup()` 中解除 + 50ms 延迟后关闭遮罩,确保首帧即为完整自定义界面 |
**启动时序**:
```
QApplication → AppConfig + init_tokens() + _load_theme() → StartupShield.show()
→ TaskRepository / MainWindow (含 DWM 预配置) / 后台服务
→ WA_DontShowOnScreen=False → show() → QTimer.singleShot(0, _finish_startup)
→ uncloak → 50ms → shield.dismiss() → tray.show()
```
#### 预览效果
```
┌──────────────────────────────────────────────────────────────┐
│ [I] [📝单] [📋多] [📊分析] [📋管理] [⚙设置] [❓帮助▾] ... [⤓] [—] [⛶] [✕] │ ← 36px
├──────────────────────────────────────────────────────────────┤
│ 中央区域 │
├──────────────────────────────────────────────────────────────┤
│ 📁 工作 ▾ │ 逾期 X | 进行中 X | ... 时钟 │ ← QStatusBar
└──────────────────────────────────────────────────────────────┘
```
---
### 2.13 主题系统
**文件**:[src/utils/design_tokens.py](src/utils/design_tokens.py)、[resources/themes/light.qss](resources/themes/light.qss)、[resources/themes/dark.qss](resources/themes/dark.qss)
#### 需求
- 浅色/深色双主题,默认浅色
- DesignTokens 20+ 语义颜色角色
- QPalette 全局应用 + QSS 层叠
- 热切换:配置变更 → `config_changed` → 重设 QPalette 和 QSS
- QSS 中 `__ICONS__` 占位符,加载时替换为实际路径
#### 实现方案
- **DesignTokens** (冻结 dataclass) — 20+ 语义角色:
| 类别 | 令牌 | 用途 |
|------|------|------|
| 背景 | `bg_primary`, `bg_secondary`, `bg_tertiary`, `bg_welcome_fallback` | 窗口/面板/输入框背景 |
| 文本 | `text_primary`, `text_secondary`, `text_disabled`, `text_welcome_accent`, `text_welcome_sub`, `text_on_accent` | 各级文字/强调/禁用 |
| 边框 | `border_primary`, `border_focus` | 默认边框/聚焦边框 |
| 语义 | `accent`, `accent_hover`, `danger`, `danger_hover`, `danger_bg`, `success` | 强调/危险/成功 |
| 热力图 | `heatmap_empty` | 热力图空白单元格 |
| 其他 | `separator`, `timeline_dot`, `timeline_done` | 分隔线/时间线 |
- **LIGHT_TOKENS**: 暖纸白 `#f5f4f0` 主背景, 柔和蓝 `#5b8def` 强调, `#2c2c2c` 主文字
- **DARK_TOKENS**: 深炭黑 `#1a1b26` 主背景, 柔和蓝 `#7aa2f7` 强调, `#c9d1d9` 主文字
- `get_tokens()` 单例, `init_tokens(config)` 绑定, `refresh_tokens()` 重新解析
- `build_palette()` — 构造 QPalette(Window/Base/Button/Highlight/Link/ToolTip/BrightText/Disabled 等色组)
- `heatmap_gradient(levels)` — 基于当前配色方案(`HEATMAP_SCHEMES` 注册表,4 组预设)插值生成渐变,亮/暗双主题各 8 级色阶
- QSS 文件覆盖:全局/菜单/标题栏/工具栏/状态栏/按钮/复选框/选项卡/输入框/下拉框/文本编辑/表格/标签/热力图/报告/对话框/日期时间/分区蒙版/卡片/弹窗
#### 主题切换流程
```
AppConfig 主题变更
→ config_changed 信号
→ MainWindow._load_theme()
→ DesignTokens.refresh_tokens()
→ build_palette() → QApplication.setPalette()
→ 加载 QSS (替换 __ICONS__ 占位符)
→ 所有 UI 组件自动重绘
```
#### 原生标题栏暗色适配
**文件**:[src/utils/win32_theme.py](src/utils/win32_theme.py)
通过 Windows DWM API 为非无框对话框设置暗色原生标题栏,弥补 QPalette/QSS 无法控制原生窗口装饰的局限:
| 条件 | 机制 | 效果 |
|------|------|------|
| Win11 (build ≥ 22000) | `DWMWA_USE_IMMERSIVE_DARK_MODE` + `DWMWA_CAPTION_COLOR` | 标题栏精确匹配 `surface_raised`(与主窗口自定义标题栏一致) |
| Win10 1809+ (17763–22000) | `DWMWA_USE_IMMERSIVE_DARK_MODE` | 标题栏为系统暗灰色(接近但不完全一致) |
| Win10 < 1809 / 非 Windows | — | 无操作,标题栏保持系统默认 |
适用对话框:设置(`SettingsDialog`)、关于(`AboutDialog`)。在 `showEvent` 中根据当前主题自动调用,非 Windows 平台零副作用。
---
### 2.14 批量操作
**文件**:[src/ui/task_list/batch_toolbar.py](src/ui/task_list/batch_toolbar.py)、[src/models/repository.py](src/models/repository.py)(批量方法)
#### 需求
- 全选/取消全选按钮
- 5 个操作按钮:更改状态(进行中/已完成)、删除、中止、重启、延后处理(+1/+5/+7/+10/+20/+30天)
- 延后处理:调整选中任务的 deadline_date,无截止时间的任务以今天为基准;执行后自动刷新逾期状态;活动日志格式 `[批量操作] 延后处理: 截止时间 {旧} -> {新}(+{N}天)`
- 选中计数标签:"已选 N 项"(纯显示,无交互,位于操作按钮之后)
- 无选中时 hover 显示 Toast 提示
- 操作后刷新所有关联视图
- 编辑视图批处理操作通过 QMessageBox 确认,批量视图通过 QMessageBox 确认(批量管理页面额外调用 `_refresh_batch_page()` 刷新表格)
#### 实现方案
- BatchToolbar(QWidget) — 水平布局:全选按钮 + 更改状态下拉 + 删除/中止/重启/延后处理按钮 + 导出下拉 + 计数标签
- 信号:`select_all_requested` / `deselect_all_requested` / `batch_status_change` / `batch_urgency_change`(list, int) / `batch_delete` / `batch_suspend` / `batch_restart` / `batch_postpone`(list, int) / `export_requested`(str)
- 编辑视图 `_batch_toolbar` 信号连接至对应 handler(2026-06-01 修复,此前均未连接)
- 延后处理(2026-06-02 新增):Repository `batch_postpone(ids, days)` 逐任务更新 deadline_date + 记录 activity_log + refresh_overdue_status()
- 调整分区(2026-06-04 新增):右键菜单"调整分区",将选中任务迁移至其他分区;FROM 和 TO 分区若设有密码需依次验证;密码验证通过 + 确认弹窗后执行 `batch_move_partition(ids, to_partition_id)`;迁移后视图受底部状态栏当前分区控制(已迁移任务从当前分区消失)
- 编辑视图和批量视图均使用 QMessageBox 确认
- Repository 批量方法使用事务 + 逐条记录 activity_log(显示中文状态名)
- `_warn_if_empty()` — 空选中时 2 秒 QToolTip 提示
#### 预览效果
```
┌──────────────────────────────────────────────────────────────┐
│ [☐ 全选] [更改状态 ▾] [删除] [中止] [重启] [延后处理 ▾] [导出 ▾] 已选 3 项 │
└──────────────────────────────────────────────────────────────┘
```
---
### 2.15 任务输入
**文件**:[src/ui/widgets/task_input.py](src/ui/widgets/task_input.py)
#### 需求
- 单行 QLineEdit,Enter 创建任务
- Markdown 语法输入:`- [ ] <YYYY-MM-DD HH:MM> 标题 #标签`(规范格式,无状态关键字)
- 解析失败时红色边框闪烁(400ms QTimer)
- 创建后清空并发射 `task_created`
- Ctrl+N 全局快捷键聚焦
#### 实现方案
- TaskInputWidget(QWidget):`_input`(QLineEdit) + `returnPressed`→`_on_text_entered()`
- `MarkdownTaskParser.parse()` → 构造 Task → `repository.insert()` → emit(task_created)
- 闪烁:`_flash_error()` 设置红色 border → 400ms QTimer 恢复
#### 预览效果
```
┌──────────────────────────────────────────────────────────────┐
│ - [ ] TODO <2026-05-30> 输入Markdown任务,Enter创建 │ Ctrl+N │
└──────────────────────────────────────────────────────────────┘
QLineEdit (#taskInput) 快捷键提示
```
---
## 3. 附录
### 3.1 关键数据流
#### Markdown 创建流
```
用户输入 raw_md
→ MarkdownTaskParser.parse()
→ ParsedTask(checkbox, status, dates, title, tags)
→ Task 数据类构造
→ TaskRepository.insert()
→ SQLite tasks 表 + FTS5 索引更新
→ SignalBus.task_created.emit(Task)
→ MainWindow._on_task_created()
→ 若速览按钮非"今天"则自动切换 activate_preset("today")
→ 筛选栏 reset() → 刷新列表 → 新任务置顶 → 自动选中定位
```
#### 编辑保存流
```
TaskEditPanel._source_edit 文本变更
→ _on_raw_md_changed() (300ms debounce)
→ MarkdownTaskParser.parse()
→ MarkdownTaskFormatter.format() 规范化
→ TaskRepository.update()
→ SignalBus.task_updated.emit(Task)
→ 所有监听组件刷新
```
#### 状态循环流
```
用户点击状态标签
→ TaskStatus.next_status
TODO → DOING → DONE → DOING (循环)
OVERDUE 锁定 (仅系统可改)
→ formatter.format() 重新生成 raw_md
→ TaskRepository.update()
→ SignalBus.task_status_changed.emit(Task, old_status)
→ 回调: TaskRecurrence (DONE→新实例), MainWindow 刷新
```
### 3.2 数据库 Schema
```sql
-- tasks (主表, 28 列)
CREATE TABLE tasks (
id TEXT PRIMARY KEY,
raw_md TEXT NOT NULL, -- 规范 Markdown 行
title TEXT NOT NULL,
status TEXT NOT NULL,
priority INTEGER DEFAULT 0, -- 已弃用
tags TEXT DEFAULT '[]', -- JSON 数组
scheduled_date TEXT,
deadline_date TEXT,
deadline_time TEXT, -- HH:MM
created_at TEXT,
updated_at TEXT,
completed_at TEXT,
archived INTEGER DEFAULT 0,
archived_at TEXT,
recurrence_rule TEXT, -- +1d, +1w, +1m, +1y
parent_id TEXT REFERENCES tasks(id),
partition_id TEXT,
notes TEXT,
activity_log TEXT DEFAULT '[]', -- JSON 数组 [{ts, status, progress, content, urgency}]
progress INTEGER DEFAULT 0, -- 0-100
activity_yesterday INTEGER DEFAULT 0,
activity_today INTEGER DEFAULT 0,
activity_week INTEGER DEFAULT 0,
activity_last_week INTEGER DEFAULT 0,
activity_month INTEGER DEFAULT 0,
activity_last_month INTEGER DEFAULT 0,
suspended INTEGER DEFAULT 0,
urgency INTEGER DEFAULT 3 -- 0=紧急, 1=重要, 2=关注, 3=普通
);
-- FTS5 全文索引
CREATE VIRTUAL TABLE tasks_fts USING fts5(
raw_md, title, notes, tags,
content='tasks', tokenize='unicode61'
);
-- 分区
CREATE TABLE partitions (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
sort_order INTEGER DEFAULT 0,
password TEXT DEFAULT '',
archive_days INTEGER NOT NULL DEFAULT 0,
auto_lock_minutes INTEGER NOT NULL DEFAULT 3,
created_at TEXT DEFAULT (datetime('now'))
);
-- 通知去重
CREATE TABLE notification_log (
task_id TEXT NOT NULL,
interval_minutes INTEGER NOT NULL,
sent_at TEXT NOT NULL,
PRIMARY KEY (task_id, interval_minutes)
);
-- 索引
CREATE INDEX idx_tasks_status ON tasks(status);
CREATE INDEX idx_tasks_deadline ON tasks(deadline_date);
CREATE INDEX idx_tasks_archived ON tasks(archived);
CREATE INDEX idx_tasks_partition ON tasks(partition_id);
CREATE INDEX idx_tasks_suspended ON tasks(suspended);
```
### 3.2.1 打包数据库
**设计目的**:打包版使用预制数据库替代开发版自动 seeding,确保最终用户获得干净的初始体验。
**生成脚本**:[scripts/create_package_db.py](scripts/create_package_db.py)
**分区设计**:
| 排序 | 名称 | 密码 | 内容 |
|------|------|------|------|
| 0 | 工作 | 空 | 无 |
| 1 | 学习 | 空 | 无 |
| 2 | 个人 | 空 | 无 |
| 3 | 演示空间 | 空 | 15 个生活场景演示任务 |
**演示空间任务覆盖**:TODO(7) / DOING(4) / DONE(2) / OVERDUE(2) / SUSPENDED(1),4 级紧急度全覆盖,含循环规则 +1w、活动时间线、多标签。
**构建流程**:[pack_scripts/build.bat](pack_scripts/build.bat) 在 PyInstaller 编译前:
1. 备份 dev DB → 删除 dev DB
2. 运行 `uv run python scripts/create_package_db.py` 生成 package DB
3. PyInstaller 编译(`--add-data="resources;resources"` 自动包含 package DB)
4. 删除 package DB → 恢复 dev DB
**运行时隔离**:
- Dev 模式(`python main.py`):`ensure_default_partition()` 若无分区则自动创建"功能演示"分区
- Frozen 模式(exe):`ensure_default_partition()` 发现已有 package DB 中的 4 个分区,返回首个现有分区 ID
- 安全保护:`create_package_db.py` 检测到"测试分区"标记时拒绝覆盖,防止误删 dev DB
### 3.3 关键常量
| 常量 | 值 | 位置 |
|------|------|------|
| 标题栏高度 | 36px | main_window.py `_TITLE_BAR_HEIGHT` |
| 窗口热区边框 | 6px | main_window.py `nativeEvent` |
| 窗口默认尺寸 | 1050×680 | app.py `__init__` |
| 窗口最小尺寸 | 900×600 | main_window.py `apply_screen_size()` |
| 窗口最大尺寸 | 1400×900 | main_window.py `apply_screen_size()` |
| 编辑器分栏比例 | 50/50 | main_window.py `_splitter.setStretchFactor` |
| 热力图目标单元格 | 14px | calendar_heatmap_widget.py `_TARGET_CELL` |
| 热力图月间隙 | 4px | calendar_heatmap_widget.py `_MONTH_GAP` |
| 热力图左边距 | 42px | calendar_heatmap_widget.py |
| 分页选项 | [20, 50, 100] | DropdownWidget |
| 默认分页 | 20 | config.py |
| 空闲锁定检查间隔 | 30s | main_window.py `_setup_idle_lock` |
| 搜索防抖 | 300ms | filter_bar.py |
| 轮播间隔 | 5s | quick_overview_bar.py |
| 自动归档时间 | 02:07 | archiver.py |
| 状态徽章圆角 | 12px | task_list_delegate.py |
| 下拉框宽度 | `max_chars*12+36` | widget_utils.py `combo_width()` |
| 每日摘要推送时间 | 09:00 | config.py |
| 默认安静时段 | 22:00-08:00 | config.py |
### 3.4 依赖清单
| 包 | 版本 | 用途 |
|------|------|------|
| PySide6 | ≥ 6.5.0 | Qt GUI 框架 |
| APScheduler | ≥ 3.10.0 | 后台定时任务(QtScheduler) |
| pynput | ≥ 1.7.0 | 全局键盘监听 |
| python-dateutil | ≥ 2.8.0 | 循环任务月份偏移(relativedelta) |
| openpyxl | ≥ 3.1.0 | Excel 报告导出 |
| pytest | ≥ 7.4.0 | 测试框架(开发) |
| pytest-qt | ≥ 4.2.0 | Qt 测试支持(开发) |
| pytest-mock | ≥ 3.11.0 | Mock 支持(开发) |
| PyInstaller | ≥ 6.0.0 | standalone 打包(开发) |
| black | — | 代码格式化(开发) |
| ruff | — | Linting(开发) |
### 3.5 测试策略
| 测试文件 | 用例数 | 覆盖范围 |
|----------|--------|----------|
| `test_md_parser.py` | 20 | 标准格式、最小格式、回退解析、批量解析、截止时间、错误处理、OVERDUE |
| `test_md_formatter.py` | 7 | 完整/最小/无标签格式化、往返稳定性 |
| `test_repository.py` | 21 | CRUD、搜索过滤、排序、分页、聚合(热力图/统计)、OVERDUE 自动检测/恢复、优先级排序 |
| `test_task.py` | 7 | Task.urgency_score: 逾期/今天/未来/DONE/无截止日/OVERDUE |
**往返测试关键**:`test_md_formatter.py::TestRoundTrip` — `Task → format() → parse() → Task` 字段必须等价,保证 raw_md 是可靠的规范数据源。
---
> 本文档关联 [CLAUDE.md](CLAUDE.md) 和 [CHANGELOG.md](CHANGELOG.md)。