Task Forge 是一款基于 PyQt6 · SQLAlchemy · SQLite 的本地优先(Local-First)单体桌面任务管理系统。
数据主权、离线可用、历史可追溯三项特性收敛于一个零部署桌面单体内,
为个人知识工作者提供完整的任务生命周期管理能力。
[!NOTE] README 依赖的演示素材(GIF / PNG)统一放置于
docs/demo/。本地录制操作步骤见docs/GIF录制操作步骤.md。
docs/demo/ 中已补齐 README 当前使用的全部演示素材,来源于 screenshot/:
| README 引用文件 | 来源素材 |
|---|---|
01-today-view.png |
今日任务.png |
02-create-task.gif |
创建任务.gif |
03-parent-child-dag.gif |
子任务全部完成父任务自动完成.gif |
04-gantt-view.png |
甘特图时间轴.png |
05-calendar-view.gif |
日历使用.gif |
06-kanban-board.gif |
工作台任务查阅.gif |
08-reminder-dialog.gif |
任务提醒.gif |
10-export-import.gif |
数据持久化展示.gif |
12-theme-settings.gif |
主题切换.gif |
14-focus-timer.gif |
专注计时.gif |
其余补充素材(如 AI深度分析.gif、按日期筛选任务.gif、背景图片切换.gif 等)也已同步到 docs/demo/,便于后续继续扩展 README 演示内容。
|
① 安装依赖
pip install -r requirements.txt
|
→ |
② 启动应用
python src/main.py
|
→ |
③ 运行回归测试
python tests/editor_regression_suite.py
|
首次启动无需手动干预,系统自动完成:
| 步骤 | 函数 | 说明 |
|---|---|---|
| ① | _configure_qt_font_directory() |
Qt 字体路径注入 |
| ② | load_config() |
加载 app_config.json + 日志 |
| ③ | DB() |
ORM 初始化 + _ensure_schema() 自动迁移 |
| ④ | seed_database_if_empty(db) |
演示数据植入(幂等) |
| ⑤ | get_theme_profile() |
读取主题配置 |
| ⑥ | apply_theme(...) |
全局样式装配 |
| ⑦ | MainWindow().showMaximized() |
主窗口上屏(默认 today 视图) |
| ⑧ | app.exec() |
进入 Qt 事件循环 |
| 表现层 | src/ui/ · 25+ UI 模块 · PyQt6 组件树 · 主题装配 · 视觉资产 |
| 交互层 | MainWindow.py · 事件路由 · switch_view() · refresh_everything() · 提醒调度 |
| 领域层 | task_composer.py · 规则装配 · payload() 验证 · 时间约束联动 |
| 数据层 | DB.py · Task.py · Note.py · SQLAlchemy ORM · _ensure_schema · SSOT 唯一写入 |
| 资源层 | src/assets/ · 图标 / 主题 / 声效 · 三套配色 · 背景图管理 |
表现层 · PyQt6 · 25+ UI 模块TodayView | PlanView | TaskTree | HubView | GanttView | Calendar
|
|||
| ↓ UI 事件(点击 / 勾选 / 输入) | |||
交互层 · MainWindowswitch_view() refresh_everything() check_reminders()
|
|||
| ↓ 领域规则 · 编辑器装配 · 提醒调度 | |||
|
|||
| ↓ DB API Call | |||
数据层 · DB.py (SSOT · SQLAlchemy Session)create_task | update_task | toggle_task | _refresh_parent_chain | gantt_entries | personal_analytics_snapshot
|
|||
| ↓ → refresh_everything() 广播 | |||
SQLite · data/task_forge.db + .db.bak(冷启动备份)
|
Tip
refresh_everything() 是唯一重绘入口。所有视图均通过此函数获取最新状态,彻底消除「界面先变、落盘失败」的 State Drift 竞态问题。
|
上游触发
switch_view(mode) — 视图路由_refresh_tree() — 任务树重建check_reminders() — 提醒轮询
|
refresh_everything()
MainWindow.py · 唯一重绘入口
|
下游写入(DB.py)
create_task(payload)update_task(id, payload)_refresh_parent_chain() — DAG 同步
|
Task-Forge/
├─ README.md # 项目说明(课程要求)
├─ requirements.txt # 依赖列表(课程要求)
├─ src/ # 源代码(课程要求)
│ ├─ main.py
│ ├─ MainWindow.py
│ ├─ DB.py
│ ├─ Task.py
│ ├─ Note.py
│ ├─ runtime_support.py
│ ├─ assets/
│ └─ ui/
├─ data/ # 数据文件(课程要求)
│ ├─ app_config.json
│ ├─ categories.json
│ ├─ demo_data.json
│ └─ task_forge.db.bak
├─ docs/ # AI 使用说明文档(课程要求)
│ ├─ AI使用说明.tex
│ ├─ AI使用说明.pdf
│ ├─ AI使用说明.md
│ ├─ fonts/ # README/TeX 需要的本地字体
│ │ ├─ NotoSansSC-VF.ttf
│ │ └─ NotoSerifSC-VF.ttf
│ └─ demo/
├─ tests/ # 回归测试(扩展内容)
├─ scripts/ # 工具脚本(扩展内容)
└─ screenshot/ # 演示录屏素材(扩展内容)
Note
为保证 README 表格和 docs/AI使用说明.tex 在不同机器上的中文排版一致性,项目已将所需字体放入 docs/fonts/。若本机已安装同名字体,系统会优先使用本机字体。
定义于
src/Task.py,SQLAlchemy 2.0 DeclarativeBase 风格,自引用 DAG 外键。
| 字段 | SQLAlchemy 类型 | 约束 | 语义说明 |
|---|---|---|---|
id |
Integer |
PK · AutoIncrement | 全局唯一标识符 |
title |
String(120) |
NOT NULL | 任务标题 |
category |
String(60) |
default="学习" | 一级分类标签 |
tags |
Text |
default="" | 多标签文本(空格分隔) |
priority |
String(10) |
枚举:高 / 中 / 低 | 任务优先级 |
due_at |
DateTime |
Nullable · Index | 截止时间 |
remind_at |
DateTime |
Nullable | 提醒时间 |
progress |
Integer |
default=0 | 完成度 |
estimated_minutes |
Integer |
default=0 | 预估投入时长 |
tracked_minutes |
Integer |
default=0 | 实际累计投入 |
recurrence_rule |
String(20) |
default="不重复" | 循环规则(每天 / 每周 / 自定义) |
completed |
Boolean |
default=False | 任务完成态标志 |
completed_at |
DateTime |
Nullable | 完成时间戳,归档时写入 |
reminder_sent |
Boolean |
default=False | 防重复互斥锁,弹窗展示后置 True |
sort_order |
Integer |
default=0 | 同级任务排序权重 |
parent_id |
Integer |
FK(self) · Nullable · CASCADE | 父任务引用,构成 DAG |
created_at |
DateTime |
default=now | 创建时间戳 |
updated_at |
DateTime |
onupdate=now | 最后修改时间戳 |
description |
Text |
default="" | 任务详情说明 |
父节点进度聚合:
效率偏差(分析 Hub 核心指标):
截止与提醒时间约束:
由 _sync_reminder_constraints() 实时校验并自动修正。
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
id |
Integer |
PK | 便签唯一 ID |
title |
String |
default="未命名便签" | 便签标题 |
content |
Text |
Nullable | 便签正文 |
pinned |
Boolean |
default=False | 置顶标志 |
created_at |
DateTime |
default=now | 创建时间戳 |
updated_at |
DateTime |
onupdate=now | 最后修改时间戳 |
DB.py 内建两种零摩擦迁移机制:
_ensure_schema()—ALTER TABLE追加缺失列(tags、tracked_minutes、progress、recurrence_rule),无需手动脚本_resolve_db_path()— 自动升级历史命名task_studio.db → task_forge.db
# src/MainWindow.py — 每次 refresh_everything() 后重建
self.task_map: dict[int, Task] # O(1) 任务查找
self.children_map: dict[int, list[Task]] # 子树枚举与递归计算| 状态 | 条件 | 转换方向 |
|---|---|---|
| 未完成 | completed=False |
→ 部分完成(子节点进度 < 100%) |
| 部分完成 | progress < 100% |
→ 全部完成(全部子节点 completed=True) |
| 全部完成 | all children done | → 父节点归档(completed=True,写入 completed_at) |
| 回退 | 任意子节点取消完成 | → 强制回退 completed=False,向上递归至树根 |
隔离约束:reminder_sent 字段不受父节点刷新影响,保持原有锁状态
|
||
位于 src/DB.py:
- 全量完成 → 父节点自动
completed=True,写入completed_at - 任一子节点未完成 → 父节点强制回退
completed=False - 向上递归 直至
parent_id IS NULL(树根)为止
forced_visible_ids 机制解决过滤视图下的可见性断层:
| 场景 | 问题 | 解决方案 |
|---|---|---|
| today 视图创建子任务 | 子任务 due_at=None 被过滤隐藏 |
forced_visible_ids 强制显示 |
| plan 视图创建子任务 | 父任务在不同日期,子任务被过滤 | 扩展相关分支临时可见 |
| 批量完成后刷新 | 完成的父任务消失难以确认结果 | 保留 1 轮渲染周期可见 |
提醒定时器Single-shot |
MainWindow | DB.py | 声音引擎 | 提醒弹窗 |
|---|---|---|---|---|
① timeout() → check_reminders()
|
||||
② MW → DB: due_reminders(current_time) → 返回待提醒任务列表
|
||||
③ [存在待提醒任务] → 声音引擎: _play_reminder_sound() → DB: mark_reminders_sent(task_ids) → 弹窗: _show_reminder_dialog(tasks)
|
||||
④ [用户选择延期] → DB: postpone_reminder(task_id, minutes)(重置 reminder_sent=False)
|
||||
⑤ MW → DB: next_pending_reminder_at() → 定时器: setSingleShot(True); start(delta_ms)
|
||||
| 触发条件 | 变化方向 | 执行函数 |
|---|---|---|
| 提醒时间到达,弹窗已展示 | False → True | mark_reminders_sent() |
用户修改 remind_at 字段 |
True → False | update_task() 内部重置 |
| 任务被标记为完成 | False → True | _set_completion_state() |
| 用户点击「延期」 | True → False | postpone_reminder() |
父节点刷新(_refresh_parent_chain) |
无变化(隔离) | — |
Important
架构约束:严禁绕过 SQLAlchemy Session 直接写 task_forge.db。reminder_sent 的原子性完全依赖事务——绕过 ORM 将导致提醒重复触发。
核心类:
TaskEditorView—src/ui/task_composer.py
# 独立新建任务
set_context(task=None, preferred_parent_id=None, parent_title=None)
# 编辑已有任务(回显全部字段)
set_context(task=task, preferred_parent_id=None, parent_title=None)
# 创建子任务(fixed_parent_id 锁定,防止提交时解除父子关系)
set_context(task=None, preferred_parent_id=pid, parent_title=title)
|
|
today今日聚焦 · 默认启动 |
plan未完成计划树 |
tasktree全量任务树 |
completed已完成任务归档 |
hub看板 + 分析 Hub |
calendar月度任务分布 |
gantt时间轴进度图(自绘) |
settings设置中心 |
create新建任务编辑器 |
edit编辑已有任务 |
detail任务详情全屏 |
所有视图均通过 switch_view(mode) 统一路由MainWindow.py |
export_data()JSON — 全量任务 + 便签
含 ID 与父子关系保全
|
export_csv()CSV — 电子表格格式
Excel / Numbers 兼容
|
export_week_report()Markdown — 周报模板
可直接粘贴提交
|
Caution
import_data() 执行覆盖性导入。操作前请先确认 task_forge.db.bak 时间戳为最新,或手动执行 export_data() 备份当前数据。
|
|
|
| 约定项 | 说明 |
|---|---|
| 启动指令 | python src/main.py |
| 质量门禁 | python tests/editor_regression_suite.py + mainwindow_regression_suite.py |
| 代码风格 | PEP 8 · snake_case pyqtSignal · 类型注解优先 |
| 架构约束 | 禁止 UI 层直接实例化 Session · 所有写操作通过 DB 类接口 |
| 依赖管理 | 新增 PyPI 依赖须同步更新 requirements.txt 并版本锁定 |
| 禁止绕过 ORM 直接写 task_forge.db · DAG 一致性和 reminder_sent 原子性依赖 Session | |
验证数据库迁移健壮性
在缺少 progress 列的旧版数据库上运行 python src/main.py,
_ensure_schema() 会自动执行 ALTER TABLE tasks ADD COLUMN progress INTEGER DEFAULT 0,应用正常启动。
执行完整回归并解析结果
python tests/editor_regression_suite.py
python tests/mainwindow_regression_suite.py成功判定:输出包含 editor-regression-suite-passed:N 与 mainwindow-regression-suite-passed:M
|
已完成
|
近期规划
|
中长期规划
|
首次启动为什么会有短暂延迟?
启动时 _ensure_schema() 执行 schema 检查,seed_database_if_empty() 在空库情况下植入演示数据。两个操作均为幂等,通常在 1 秒内完成。后续启动仅执行备份操作,速度更快。
提醒没有弹出,可能是什么原因?
- 检查任务的
remind_at是否正确设置(须早于或等于due_at) - 检查
reminder_sent是否已被设置为True(通过重新修改remind_at来重置) - Windows 平台:系统通知不会触发(设计行为),提醒通过应用内弹窗展示
- 确认
settings视图中提醒声音未被禁用
父任务完成状态为什么没有自动更新?
_refresh_parent_chain 仅在子任务完成状态通过 toggle_task() 或 batch_toggle_tasks() 变化时触发。若通过非标准方式(如直接写数据库)完成子任务,父节点不会自动刷新。请始终通过 DB 类接口操作数据。
如何在演示场景下快速重置到初始状态?
# 关闭应用后执行
Remove-Item data/task_forge.db -Force
# 下次启动 seed_database_if_empty() 将自动植入完整演示数据
python src/main.py| 类别 | 路径 | 说明 |
|---|---|---|
| 应用启动 | src/main.py |
8 步启动序列 + 主题装配 |
| 主窗口路由 | src/MainWindow.py |
视图切换 + 提醒调度 + 内存索引重建 |
| 数据访问层 | src/DB.py |
所有数据操作的 SSOT,唯一写入通道 |
| 任务 ORM | src/Task.py |
19 字段 + 自引用 DAG 关系定义 |
| 便签 ORM | src/Note.py |
6 字段便签实体 |
| 任务编辑器 | src/ui/task_composer.py |
TaskEditorView + set_context() + payload() |
| 看板与分析 | src/ui/hub_view.py |
HubView · KanbanBoard · AdvancedAnalyticsView |
| 甘特图 | src/ui/gantt_view.py |
GanttView · GanttTimeline.paintEvent() |
| 编辑器回归 | tests/editor_regression_suite.py |
7 测试域 |
| 主窗口回归 | tests/mainwindow_regression_suite.py |
8 测试域 |
| 术语 | 含义 |
|---|---|
| Local-First | 数据优先驻留本地,操作优先本地生效,离线环境完整可用 |
| SSOT | Single Source of Truth,单一事实来源(此处为 DB.py) |
| DAG | Directed Acyclic Graph,有向无环图,保证父子任务引用无循环 |
| State Drift | 视图状态与持久化状态不一致的竞态现象 |
| Debounce Lock |
reminder_sent 字段实现的防重复提醒互斥锁 |
| Payload |
TaskEditorView.payload() 装配的任务数据字典,为 DB API 统一入参 |
forced_visible_ids |
强制可见集合,防止新建子任务在受限过滤视图中消失 |
| Single-shot Timer |
reminder_timer.setSingleShot(True),到期后不自动重启,精准调度 |
estimated_minutes,任务预估投入时长 |
|
tracked_minutes,实际累计专注投入时长 |










