# 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` | 任务管理页面构建、批量操作、手动归档/清除、批量导出 |

控制器通过构造函数注入依赖，不直接访问数据库。

### 1.3.1 导航骨架（2.0）

- **NavShell**（`src/ui/nav_shell.py`）：常驻左侧图标栏，按分组「工作 / 洞察 / 管理」渲染
  `VIEW_REGISTRY` 中的页面入口，底部为设置齿轮；点击或 <kbd>Ctrl</kbd>+<kbd>1</kbd>..<kbd>5</kbd> 切换页面。
- **视图注册表**（`src/ui/views/__init__.py`）：新增页面只需 `register(ViewSpec(...))`，
  NavShell 自动渲染入口，`QStackedWidget` 懒构建页面。旧视图名经 `VIEW_ALIASES` 别名兼容
  （`edit → tasks`、`dashboard → analysis`、`batch → manage`）。
- **SelectionContext**（`src/ui/selection_context.py`）：跨视图共享的分区 / 选中任务 / 时段粒度，
  视图之间不再互相触碰私有属性，读取状态并订阅 `changed` 即可。
- 当前页面入口：总览、任务、任务图谱（占位）、活动分析、任务管理。

数据刷新职责已收敛：SignalBus 事件由 `TimelineController` 独家订阅（含 50ms 去抖），
MainWindow 不再持有重复的刷新管线；任务页的过滤全部由时间轴工具行承载
（粒度 / 搜索 / 状态 / 优先级 / 排序），旧 FilterCoordinator 与分页机制已退役。

### 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` 的语义角色引用，亮/暗主题全局切换。
   视觉规范（暖灰精修）：浅色暖灰阶（正文 `#3a3832`/次要 `#6f6a5f` 对比度 ≥4.5:1）、
   暖调靛青强调 `#4d57c3`；深色 Tokyo-night 基底 + 暖灰文字 + `#7c83ea` 强调；
   圆角标度 4/6/8、卡片 `surface_raised` 分层 + 软阴影（`apply_card_shadow`）、
   状态色统一走 `status_color()` 令牌辅助函数。

### 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` | — | SettingsDrawer, MainWindow | MainWindow |
| `config_changed` | — | AppConfig, SettingsDrawer | 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 输出
```

**命令集**（14 个）：`list`（筛选/排序/分页）、`today`（今日摘要分组，分区可过滤）、
`activity`（指定日期活动时间线）、`add`（Markdown 行为主 + flags 覆盖）、`edit`
（字段修改 + `--dry-run` diff）、`done`（状态变更，触发周期克隆与即时归档）、`log`
（追加活动进展）、`rm`、`tags`、`partitions`（增删改）、`archive`（`--all` 归档全部
已完成）、`recurrence`（`+1d/+1w/+1m/+1y`）、`reminder`（全局提醒配置）、`export`
（md/xlsx，复用 `MarkdownExporter`/`task_exporter`）、`report`（周报/月报摘要）。

**关键设计**：

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 优先）；自动续接上次会话（会话 ID 捕获 + `--resume`）；专用工作区
   `ai_workspace/` + 首条指令 `/tadado` 保证 skill 唯一加载；未安装助手时菜单置灰；
   启动时注入 `TADADO_EXE`/`TADADO_PARTITION`；会话 jsonl usage 超 80% 托盘提醒 /compact。
9. **Skill 管理**（设置 → AI 助手）— `resources/skill/tadado/SKILL.md`（随程序分发、
   带 version 字段）为唯一权威源：「编辑 Skill」打开微调，「同步到 Claude / Codex」
   分发到 `~/.claude`/`~/.agents` 宿主目录后生效；状态行实时展示版本与同步状态。
10. **字体平台适配** — Windows 用系统字体（安装包剔除内置字体 -27MB）；Linux 启动时
   探测系统 CJK/Emoji 字体族，缺失才加载内置 Noto 字体。
11. **设置对话框页签化** — 常规 / AI 助手 / 归档与分区 / 关于；帮助文档并入关于页，
   与版本更新记录一样以浏览器打开（左侧可收起目录 + 锚点滚动高亮）。

**关联修复**：`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**~~ — 已删除（2.0 阶段 3：任务页改由时间轴承载，该面板为孤儿）

#### 任务凸显方案

**术语定义**："任务凸显"（Task Highlighting）指当前活动任务（即编辑面板中正在编辑的任务）在任务列表中的视觉强调。实现方式为 **仅 COL_CONTENT 列红色加粗**，无整行高亮，不干扰紧急程度背景渲染。

**触发场景**（统一入口 `MainWindow._on_task_selected(task)`）：
- 用户在表格中点击某行
- 单任务/多任务创建完成后自动选中首项
- 速览栏预设切换后自动选中首个任务
- 进度栏筛选后自动选中首个任务
- 筛选栏变更后自动选中首个任务
- 分区激活后自动选中首个任务
- "返回首页"操作后自动选中首个任务
- 轮播点击（经 `_select_and_load_task` 委托）
- 分页翻页后自动选中首个任务
- 数据刷新后恢复上次凸显任务（经 `_select_and_load_task` 委托）

**呈现规则**：
- 仅 COL_CONTENT（任务内容列）文字红色 + 粗体
- 红色值走 `design_tokens.danger`（浅色 `#c4453c` / 深色 `#e06c63`），主题切换自动适配
- 紧急程度整行背景色不受影响，所有行统一渲染
- 其余列（行号、创建时间、截止时间、进度、状态、标签、归档）无额外视觉变化
- 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` 即锁定分区

#### 实现方案

- 状态提示改由 `Toast` 浮层承载（`src/ui/toast.py`）：瞬时消息不再常驻状态栏
- 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 筛选栏（2.0 阶段 3 起已退役）

> **已退役**：FilterBar 随任务页改由时间轴承载而删除，其搜索 / 状态 / 优先级 / 排序
> 能力已迁入时间轴工具行。以下内容保留作历史记录。

**文件**（已删除）：`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 统计组件（2.0 阶段 3 起已退役）

> **已退役**：StatusBadgeStrip / ProgressDynamicsBar / QuickOverviewBar 随任务页改由
> 时间轴承载而删除（状态计数保留在状态栏）。以下内容保留作历史记录。

**文件**（已删除）：`status_badge_strip.py`、`progress_dynamics_bar.py`、`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("analysis")` / 侧栏「活动分析」/ <kbd>Ctrl</kbd>+<kbd>4</kbd>）

#### 需求

- Ctrl+4 切换，上下两区布局：「活动热力图」+「活动报告」
- 紧凑热力图（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("manage")` / 侧栏「任务管理」/ <kbd>Ctrl</kbd>+<kbd>5</kbd>）

#### 定位

任务管理控制台 = 审视全局 → 定位问题 → 批量处置。区别于任务页(Ctrl+2)的「浏览编辑单任务」，管理页侧重「批量审视、处置多任务」。

与任务页的差异：

| | 任务页 (Ctrl+2) | 任务管理 (Ctrl+5) |
|---|---|---|
| 核心任务 | 浏览、编辑单个任务 | 批量审视、处置多任务 |
| 表格 | 半栏 + 编辑面板 | 全宽 9 列（含归档列） |
| 筛选 | 时间轴工具行 (粒度+搜索+状态+优先级+排序) | 仅关键词搜索 |
| 批量操作 | BatchToolbar（辅助） | BatchToolbar + 导出下拉（核心） |
| 导入导出 | 无 | 导出 MD / 导出 Excel |
| 清理 | 无 | 手动归档 + 清除已归档 |

#### 需求

- Ctrl+5 切换，左右分栏：左侧管理面板(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` → `TimelineController`（主视图）+ `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` 解析云盘文件版本
- `AboutPage`（设置抽屉「关于」页签）接收 `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) 做每日摘要。（2.0 阶段 3 起任务页的轮播栏 / 进度动态栏已退役，提醒能力改由状态栏统计与时间轴色条表达。）

#### 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 图标（返回主界面）+ 全局热键提示 + 右侧常驻置顶按钮 + 4 个窗口按钮（缩小到托盘/最小化/切换全屏/关闭）
- 页面导航集中到左侧 NavShell 图标栏（工作 / 洞察 / 管理三组 + 设置齿轮），标题栏不再承载页面切换按钮
- 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 → 热键提示 → stretch → 置顶按钮 → 4×窗口按钮
- `_setup_central_widget()` — `NavShell`（左）+ `QStackedWidget`（右）；页面经 `VIEW_REGISTRY` 注册表懒构建，`_switch_view()` 处理别名与侧栏高亮
- `_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()` 隐藏到托盘
- 标题栏命中测试：靠 `menuWidget().childAt()` 判定光标是否在按钮上（按钮→`HTCLIENT`，空白→`HTCAPTION`），不依赖固定宽度
- 状态栏 `_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 | ...              时钟      │ ← 底部工具条（Toast 浮层承载瞬时提示，已无 QStatusBar）
└──────────────────────────────────────────────────────────────┘
```

---

### 2.13 主题系统

**文件**：[src/utils/design_tokens.py](src/utils/design_tokens.py)、[resources/themes/base.qss](resources/themes/base.qss)

> 亮/暗两套颜色都在 `design_tokens.py` 里（`LIGHT_TOKENS` / `DARK_TOKENS`），
> `base.qss` 是**唯一**的 QSS 文件，颜色一律写成 `{{token}}` 占位符，加载时由
> `expand_qss()` 按当前主题展开。早期按主题拆分的 `light.qss` / `dark.qss`
> 已删除——两份文件 95% 内容重复，改一处要同步改两处。

#### 需求

- 浅色/深色双主题，默认浅色
- 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 级色阶
- `base.qss` 覆盖：全局字体栈/菜单/自定义标题栏/工具栏/按钮/复选框/选项卡/输入框/下拉框/文本编辑/表格/标签/热力图/对话框/日期时间/分区蒙版/卡片/弹窗
- 圆角与控件几何直接写 px（对齐 `resources/ui-mockup/tadado-2.0.html`）：卡片 10px、按钮与输入 8px、内部元素 6–7px、胶囊 999px；颜色则**禁止**写死，必须走 `{{token}}`

#### 主题切换流程

```
AppConfig 主题变更
  → config_changed 信号
  → TadadoApp._load_theme()
  → DesignTokens.refresh_tokens()
  → build_palette() → QApplication.setPalette()
  → 加载 base.qss（expand_qss 展开 {{token}} 占位符）
  → 所有 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 | — | 无操作，标题栏保持系统默认 |

适用窗口：设置抽屉（`SettingsDrawer`）与关于页（`AboutPage`）——二者带原生窗口边框。在 `showEvent` 中根据当前主题自动调用，非 Windows 平台零副作用。

> 原先的模态 `SettingsDialog` / `AboutDialog` 已移除：设置改为右侧抽屉，
> 关于并入抽屉的第四个页签。

---
### 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`~~ — 已随旧列表退役（2.0 阶段 3）；批量操作统一由管理页 BatchController 承担
- 延后处理（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)。
