Files
iBook/readme.md
2025-10-21 10:46:03 +08:00

578 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# iBooks 笔记专家 详细设计文档
> 版本: 1.2 (2025-10 CFI排序与优化)
> 维护者: 项目开发组
> 说明: 本文档统一重新编排章节,增加架构与 UML 部分,便于后续扩展与维护。
| 版本 | 日期 | 说明 |
|------|------|------|
| 1.0 | 2025-08 | 初版文档 |
| 1.1 | 2025-09 | 重组目录新增模块拆分、UML、AI 简评与可视化章节整理 |
| 1.2 | 2025-10 | **重大更新**: EPUB CFI排序系统界面优化性能提升 |
## 1. 概述
本工具用于从 macOS iBooksApple Books应用的数据文件中提取用户的书籍笔记并以 Markdown 格式导出。**v1.2版本重大改进**实现完整的EPUB CFICanonical Fragment Identifier排序系统确保笔记按真实阅读位置排序支持交互式选择书籍导出导出内容结构清晰便于后续整理和阅读。
### 🆕 v1.2 新特性
- **🎯 EPUB CFI 排序**: 完整实现IDPF规范笔记按文档真实位置排序
- **📊 阅读统计修复**: 7天/30天/年度阅读时长统计准确性提升
- **🎨 界面优化**: 清理控制台警告,改进按钮交互效果
- **⚡ 性能提升**: 优化数据处理流程,减少冗余计算
- **🧪 测试覆盖**: 全面的CFI解析、排序、导出验证测试
---
## 2. 功能
## 2.1 主要功能
- 自动同步 iBooks 数据库和书籍信息文件到本地 `./data` 目录。
- 解析 iBooks 笔记数据库,构建结构化的 `booksnote` 数据。
- **🆕 EPUB CFI 排序**: 按 EPUB 规范解析 CFI 位置信息,确保笔记按真实阅读顺序排列。
- 解析书籍元数据(如书名、路径等)。
- 支持交互式模糊搜索选择要导出的书籍。
- 按章节导出所选书籍的所有笔记,格式为 Markdown。
- 书名中如含有"-xxxx"后缀,仅保留"-"前的主书名。
- 书籍选择菜单按最近打开时间last_open降序排序显示格式为"书名 [时间戳]"。
- **🆕 阅读统计**: 精确的7天/30天/年度阅读时长统计和可视化图表。
### 2.1.1 CFI 排序技术
EPUB CFI (Canonical Fragment Identifier) 是 IDPF 制定的标准,用于精确定位 EPUB 文档中的位置:
```
epubcfi(/6/22[id19]!/4[section]/40/1,:96,:214)
```
- **Spine 路径** (`/6/22`): 文档结构中的章节位置
- **Local 路径** (`/4[section]/40/1`): 章节内的元素位置
- **字符偏移** (`:96,:214`): 文本内的精确字符范围
v1.2 实现完整的 CFI 解析和排序算法,确保笔记按真实阅读顺序而非字符串顺序排列。
## 2.1 GUI
![img](uml/iShot_2025-09-07_12.22.26.png)
![img](uml/iShot_2025-09-07_12.22.05.png)
![img](uml/iShot_2025-09-07_12.22.16.png)
笔记导出markdown格式
![img](uml/iShot_2025-09-07_12.31.49.png)
![img](uml/iShot_2025-09-07_12.32.41.png)
---
## 3. UML 图Mermaid
> 注:使用 Mermaid 语法,支持在支持渲染的 Markdown 查看。类之间仅展示主要依赖/调用,非完整字段集合。
### 3.1 类图(核心模块)
![img](uml/iShot_2025-09-07_12.55.41.png)
### 3.2 时序图:应用启动
![img](uml/iShot_2025-09-07_12.56.27.png)
### 3.3 时序图:选择书籍 + AI 简评
![img](uml/iShot_2025-09-07_12.56.00.png)
### 3.4 时序图:导出 Markdown
![img](uml/iShot_2025-09-07_12.57.06.png)
### 3.5 时序图:已读书籍网格刷新
![img](uml/iShot_2025-09-07_12.57.19.png)
### 3.6 时序图:点击已读封面跳转
![img](uml/iShot_2025-09-07_12.57.28.png)
---
## 4. 主要数据结构
### 4.1 booksnote (v1.2 CFI 排序优化)
**v1.2 数据结构变更**:为支持 CFI 排序,`annotations` 数据结构从嵌套字典改为列表格式:
```python
# v1.1 及之前版本
booksnote = {
assetid: { label_path: { uuid: {
'creationdate': '2023/7/12',
'filepos': None,
'idref': '008.xhtml',
'note': None,
'selectedtext': '這就是宣傳的恐怖之處'
}}}
}
# v1.2 CFI 排序版本
annotations = {
assetid: [ # 按 CFI 位置排序的列表
{
'uuid': 'annotation_id',
'creationdate': '2023-07-12 14:30:00',
'location': 'epubcfi(/6/22[id19]!/4[section]/40/1,:96,:214)',
'chapter_info': '章节_id19',
'note': '用户笔记内容',
'selectedtext': '選中的文本',
'physical_location': 1250 # 备用排序
}
]
}
```
**关键改进**
- **CFI 位置字段**: `location` 存储完整的 EPUB CFI 字符串
- **章节信息**: `chapter_info` 自动解析的可读章节名称
- **排序保证**: 列表已按 CFI 位置预排序,确保阅读顺序正确
- **兼容性**: 保持向后兼容,自动适配新数据结构
---
## 5. 主要流程
### 5.1 数据同步
- 自动将 iBooks 的数据库和 plist 文件复制到本地 `data/` 目录,便于后续处理。
### 5.2 构建 booksnote (v1.2 CFI 排序优化)
- 通过 `AnnotationManager.get_annotations()` 解析 SQLite 笔记数据库,获取所有笔记。
- **🆕 CFI 排序处理**
- 使用 `EpubCFIParser` 解析每条笔记的 `ZANNOTATIONLOCATION` 字段
- 提取 spine 路径、local 路径和字符偏移信息
- 按 CFI 语义顺序排序,确保笔记按真实阅读位置排列
- 降级处理CFI 解析失败时使用物理位置和创建时间排序
- 通过 `parse_books_plist` 解析书籍元数据,获取书名、路径等信息。
- 遍历每本书的所有笔记结合OPF、NCX文件和HTML 文件,定位章节名。
- 若无法通过目录文件定位章节,则尝试通过笔记选中文本在 HTML 文件中查找章节,否则标记为"未找到章节"。
#### CFI 排序技术流程
```mermaid
graph LR
A[笔记数据] --> B[CFI解析]
B --> C{解析成功?}
C -->|是| D[CFI排序键]
C -->|否| E[物理位置+时间]
D --> F[按位置排序]
E --> F
F --> G[有序笔记列表]
```
### 5.3 交互式选择书籍
- 读取 Books.plist 获取所有书籍元数据。
- 读取 BKLibrary.sqlite获取每本书的最近打开时间last_open苹果时间戳基准2001-01-01
- 生成书名列表(优先 `displayname`,其次 `itemname`,否则用 `assetid`),并去除“-xxxx”后缀。
- 按 last_open 时间戳降序排列,菜单显示“书名 [时间戳]”,时间戳为 last_open 字段。
- 使用 InquirerPy 提供模糊搜索交互界面,供用户选择要导出的书籍。
### 5.4 导出 Markdown
- 仅导出用户选择的书籍。
- Markdown 格式如下:
```
# 笔记导出 2025-08-06 12:00
## 书名
### 章节名
选中文本
> 笔记内容
```
- 每条笔记独立分行,章节分组。
---
## 6. 核心模块架构 (v1.2)
### 6.1 主要模块
| 模块 | 功能 | v1.2 更新 |
|------|------|----------|
| `annotationdata.py` | 笔记数据库接口 | **重构** CFI 排序集成 |
| `epub_cfi_parser.py` | **🆕 新增** CFI 解析引擎 | IDPF 规范完整实现 |
| `booklist_parse.py` | 书籍元数据与统计 | 适配新数据结构 |
| `exportbooknotes.py` | 笔记导出功能 | 支持CFI排序列表 |
| `ibook_export_app.py` | 主GUI应用 | 界面优化,统计修复 |
| `charts.py` | 统计图表组件 | 性能优化 |
### 6.2 关键函数说明
#### 6.2.1 AnnotationManager.get_annotations (CFI 排序)
```python
def get_annotations(self, bookid=None) -> dict:
"""
返回按 CFI 位置排序的笔记数据
Returns:
{assetid: [annotations_list]} # v1.2 列表格式
"""
```
- **输入**书籍ID可选
- **输出**:按 CFI 排序的笔记列表字典
- **v1.2 改进**
- 集成 `EpubCFIParser` 进行位置解析
- 自动章节信息提取
- 多级排序键CFI → 物理位置 → 创建时间
#### 6.2.2 EpubCFIParser (新增模块)
```python
class EpubCFIParser:
@staticmethod
def parse_cfi(cfi_string: str) -> tuple:
"""解析 CFI 字符串为结构化数据"""
@staticmethod
def create_sort_key(cfi_string: str) -> tuple:
"""创建 CFI 排序键"""
@staticmethod
def extract_chapter_info(cfi_string: str) -> str:
"""提取可读的章节信息"""
```
#### 6.2.3 export_booksnote_to_md (适配更新)
- **输入**CFI 排序的 annotations、booksinfo、导出路径
- **输出**Markdown 字符串,并写入文件
- **v1.2 改进**:适配新的列表数据结构,保持导出格式兼容
---
## 7. 交互与用户体验
- 通过命令行交互,用户可模糊搜索并选择要导出的书籍。
- 若无可导出的笔记,程序自动退出并提示。
- 导出后,显示导出文件路径和书名。
---
## 8. 代码片段示例
### 8.1 书名处理逻辑
```python
name = info.get('displayname') or info.get('itemname') or assetid
# 如果书名中包含“-”,只取“-”前面的部分
if '-' in name: name = name.split('-', 1)[0].strip()
```
### 8.2 交互式选择与排序
```python
from booklist_parse import get_books_last_open
last_open_times = get_books_last_open('data/BKLibrary.sqlite')
for assetid, info in booksinfo.items():
...
ts = last_open_times.get(assetid, {}).get('last_open', 0)
assetid2lastopen[assetid] = ts
sorted_assetids = sorted(assetid2name.keys(), key=lambda aid: assetid2lastopen[aid], reverse=True)
choices = [f"{assetid2name[aid]} [{assetid2lastopen[aid]}]" for aid in sorted_assetids]
answer = inquirer.fuzzy(
message="请选择要导出的书名(支持模糊搜索):",
choices=choices,
multiselect=False,
instruction="上下键选择,输入可模糊筛选,回车确定"
).execute()
```
---
## 9. 依赖说明
- Python 3
- 主要依赖库:`InquirerPy`, `bs4`, `shutil`, `os`, `datetime`, `sqlite3`
- 需有 iBooks 数据库、plist 文件和 BKLibrary.sqlite 的本地访问权限
---
## 10. 目录结构
- `data/`:存放同步下来的数据库和 plist 文件(含 AEAnnotation.sqlite、Books.plist、BKLibrary.sqlite 等)
- `notes/`:导出的 Markdown 文件
- `examples/`epub 示例文件夹
---
## 11. 主要代码文件说明(细化)
### 11.1 exportbooknotes.py
- 采用 OOP 设计,核心类为 `BookNotesExporter`
- `build_booksnote(bookid=None)`:构建结构化笔记数据。
- `export_booksnote_to_md(booksnote, booksinfo, out_path=None)`:导出为 Markdown。
- `find_file_by_ext`、`get_toc_tree` 等辅助方法。
- 数据同步:自动复制 iBooks 数据库和元数据到本地。
- 菜单交互:按最近打开时间戳排序,显示“书名 [时间戳]”,支持模糊搜索。
- 只处理用户选中书籍的笔记,按章节分组导出 Markdown。
- 依赖核心解析模块,负责主流程调度。
### 11.2 annotationdata.py
- OOP 设计,核心类为 `AnnotationManager`
- `get_annotations(bookid=None)`:返回所有或指定 assetid 的笔记。
- `parse_location(location)`:静态方法,解析定位信息。
- 解析 AEAnnotation.sqlite提取所有或指定 assetid 的笔记。
- 支持苹果时间戳转换,结构化输出。
### 11.3 booklist_parse.py
- OOP 设计,核心类为 `BookListManager`
- `get_books_info()`:获取书籍元数据。
- `get_books_last_open()`:获取每本书的最近打开时间。
- 解析 Books.plist获取书籍元数据书名、作者、路径、时间等
- 解析 BKLibrary.sqlite获取每本书的最近打开时间。
### 11.4 opf_parse.py
- OOP 设计,核心类为 `OPFParser`
- `parse_opf(filepath)`:静态方法,返回 id->href 映射。
- 解析 epub 的 OPF 文件获取章节与文件映射关系idref -> href
### 11.5 toc_parse.py
- OOP 设计,核心类为 `TOCParser`
- `parse_navpoints(navpoints)`:递归解析 navPoint 节点。
- `find_label_path(node, ref, filepos, path)`:查找章节路径。
- `find_section_by_selectedtext(html_path, selectedtext)`:通过选中文本定位章节标题。
- `parse_html_title(html_path)`:解析 html 文件标题。
- 解析 NCX 目录文件,递归构建章节树结构。
### 11.6 backup/booksnote.py
- 历史/备份脚本,辅助数据迁移或格式转换。
---
## 12. 扩展与维护建议
- 可扩展支持多本书批量导出
- 可增加导出格式(如 HTML、PDF
- 可优化章节定位算法,提升准确率
- 可增加 GUI 交互界面
---
如需进一步细化某一部分设计,请告知!
---
## 13. GUI 架构与模块调用关系2025 拆分后更新)
### 13.1 模块职责概览
| 模块 | 主要类/函数 | 职责 | 关键依赖 |
|------|-------------|------|----------|
| `ibook_export_app.py` | `IBookExportApp` | GUI 入口,组合各 Mixin组织信号/槽,生命周期管理 | `CoverMixin`, `FinishedBooksMixin`, `BookReviewWorker`, `BookListManager`, `BookNotesExporter` |
| `cover_mixin.py` | `CoverMixin` | 解析、查找、缩放显示封面 | `config` (目录), Qt Widgets |
| `finished_books_mixin.py` | `FinishedBooksMixin` | 已读书籍网格数据装载与自适应布局、点击跳转 | `BookListManager.get_finished_books_this_year`, `CoverMixin.find_book_cover` |
| `review_worker.py` | `BookReviewWorker` | 后台线程获取书籍 AI 简评并写入 `bookintro.json` | `ai_interface.DashScopeChatClient` |
| `booklist_parse.py` | `BookListManager` | 书籍基础元数据、阅读统计、已读书籍列表 | `annotationdata.AnnotationManager`, `config` |
| `annotationdata.py` | `AnnotationManager` | 解析笔记 SQLite返回结构化笔记 | SQLite DB |
| `exportbooknotes.py` | `BookNotesExporter` | 构建/导出 Markdown 笔记 | `annotationdata`, `toc_parse`, `opf_parse` |
| `toc_parse.py` | `TOCParser` | 解析 NCX/HTML 标题,定位章节路径 | 文件系统/HTML |
| `opf_parse.py` | `OPFParser` | 解析 OPF 获取 manifest 映射 | OPF XML |
| `charts.py` | 图表组件 | 周 / 月 / 年 / 气泡指标可视化 | `BookListManager` 汇总数据 |
### 13.2 运行时对象关系(简化 UML 文本表示)
```
IBookExportApp
├── BookListManager (数据/统计)
├── BookNotesExporter (导出)
├── [Composition via Mixin] CoverMixin
├── [Composition via Mixin] FinishedBooksMixin
├── 0..n BookReviewWorker (异步 AI 简评线程池 _active_workers)
└── charts.* Widgets (按需创建)
```
`IBookExportApp` 通过多继承获得封面与已读网格功能:
1. 封面查找 -> `CoverMixin.find_book_cover`
2. 网格构建 -> `FinishedBooksMixin._populate_finished_books_grid`
3. AI 简评 -> `BookReviewWorker` 发起,完成后回调 `_on_review_finished`
### 13.3 启动序列Startup Sequence
1. 用户运行 `ibook_export_app.py` → 创建 `QApplication`
2. `IBookExportApp.__init__`
- 加载 `.ui`
- `sync_source_files(config)`(复制原始库到 `data/`
- 构建 `BookListManager` → 加载 plist / 统计数据
- 构建书籍列表(按 last_open 排序)填充 `QListWidget`
- 初始化封面标签 & `_load_initial()`(前三本封面 + 首本 AI 简评启动)
- `_populate_finished_books_grid()`(已读网格初填)
- 安装事件过滤器(视口 Resize + Tab 切换策略 C + A
- 安排延迟 `_relayout_finished_grid()` 确保初次布局正确
3. 主窗口显示;用户可交互。
### 13.4 书籍切换流程Selecting a Book
1. 用户在列表中选中条目 → `currentRowChanged` → `update_book_info(row)`
2. 刷新右侧三张封面(当前 + 后两本轮播预览)
3. 构建基础信息 `_base_info_cache`
4. 若 `bookintro.json` 已有简评 → 直接渲染;否则启动新 `BookReviewWorker` → 占位“简评获取中...”
5. 线程完成 → 通过信号调用 `_on_review_finished` → 更新 HTML。
### 13.5 AI 简评线程生命周期
1. 实例化 `BookReviewWorker(bookname, prompt, json_path)`
2. 连接 `finished` 信号到:
- `_on_review_finished`
- `_remove_worker`(清理活动线程列表)
3. `worker.start()` → 线程内部:调用 `DashScopeChatClient.ask()`;写入/更新 `bookintro.json`;发送 `finished` 信号。
4. 主线程根据 `_current_bookname` 校验是否仍是当前书,防止串写。
### 13.6 已读书籍网格刷新逻辑
事件触发:
1. 程序启动初次调用 `_populate_finished_books_grid()`
2. Tab 切换到“已读书籍”标签 → `_on_main_tab_changed()` → 若命中,立即 `_relayout_finished_grid()` 并延迟一次;(后续可扩展为定期重新查询)
3. (可选待扩展)外部刷新按钮调用 `_populate_finished_books_grid()`
数据来源:`BookListManager.get_finished_books_this_year()`
- 查询本地 `BKLibrary.sqlite` 中 `ZISFINISHED=1 AND ZDATEFINISHED NOT NULL`
- 将 Apple epoch(2001) 秒转为 `datetime`,过滤 `year==当前年`
- 返回列表后排序(时间倒序)
### 13.7 封面加载与缩放流程
1. `_load_initial()` / `update_book_info()` 内调用 `find_book_cover()` 获取路径
2. 原图 QPixmap 存入 `_cover_pixmaps_original`
3. `_apply_cover_scale()` 使用当前 `cover_ratio`(默认 1.2)和弹性策略计算目标高度
4. 固定宽 180px受硬上限 400px 与(可选)文本区 45% 限制
5. 非弹性模式忽略文本高度,仅 ratio + 硬上限。
### 13.8 导出流程Export Notes
1. 用户点击“导出”按钮 → `export_notes()`
2. 取当前行 assetid → `BookNotesExporter.build_booksnote(bookid)`
3. 组装单书字典 → `export_booksnote_to_md()` 输出 Markdown 到 `notes/` 目录
4. 弹窗提示路径。
### 13.9 统计图表流程
1. 启动后调用 `_init_charts()`(懒加载)
2. 获取周 / 月 / 年聚合数据及总指标
3. 构造原生自绘组件 `BarChartWidget` / `ScatterChartWidget` / `BubbleMetricsWidget`
4. 添加到对应 Layout。
### 13.10 关键调用关系(摘要)
```
update_book_info -> find_book_cover (CoverMixin)
update_book_info -> BookReviewWorker.start -> _on_review_finished
_populate_finished_books_grid (FinishedBooksMixin) -> manager.get_finished_books_this_year
_on_finished_cover_clicked -> _switch_to_export_tab -> listwidget.setCurrentRow -> update_book_info
export_notes -> BookNotesExporter.build_booksnote -> export_booksnote_to_md
_init_charts -> manager.get_total_readtime* 系列函数
```
### 13.11 数据流摘要
```
iBooks 原始文件 -> sync_source_files -> data/*.sqlite / Books.plist
└─ BookListManager 载入 -> booksinfo / open_times / 阅读统计
├─ IBookExportApp 构建主列表
├─ FinishedBooksMixin 查询已读 -> 网格
└─ charts.py 生成可视化
注释数据库 -> AnnotationManager -> 笔记结构 -> BookNotesExporter.build_booksnote -> Markdown
AI 请求 -> BookReviewWorker -> DashScopeChatClient.ask -> bookintro.json -> _on_review_finished 渲染
```
### 13.12 扩展点与建议
1. 已读书籍:增加“显示全部年份 / 仅今年”开关;提供手动“刷新”按钮。
2. 封面缓存:引入 LRU (assetid -> QPixmap) 降低重复磁盘扫描。
3. AI 简评:加速策略(本地缓存 TTL、批量预取前 N 本)。
4. 异步任务:统一线程池/任务队列,避免过多 QThread 分散管理。
5. 测试建议:
- 单元:`BookListManager.get_finished_books_this_year` 年份过滤、无行时返回。
- 单元:封面查找:构造临时目录含多个候选文件。
- 集成:启动后模拟选择书籍 → 断言 `_current_bookname` 及 HTML 含字段。
6. 性能:大书量时(>1000列表初始化可用分页或懒加载。
7. 打包:后续可用 `PyInstaller`将可执行与资源icons、ui整合。
### 13.13 风险与缓解
| 风险 | 描述 | 缓解 |
|------|------|------|
| 数据不同步 | data/ 下 sqlite 未更新导致已读缺失 | 提供“重新同步”按钮;比对文件修改时间 |
| AI 接口失败 | 网络/配额问题 | 回退本地提示文本;重试按钮 |
| UI 首次网格错排 | 视口宽度未稳定 | 采用双阶段(立即+延迟)重排 & Resize 监听 |
| 线程未回收 | 多次切换书籍产生积压 | 维护 `_active_workers` 列表并在完成回收 |
---
(本节为 2025-09 结构化重构新增)
## 14. 书籍阅读时长统计与可视化
### 14.1 阅读时长统计逻辑
1. `readtime30d`每本书最近30天每天的阅读时长分钟索引0为今天索引29为30天前。
2. `readtime12m`每本书今年每月的累计阅读时长分钟索引0为1月索引11为12月。统计逻辑为遍历今年每一天按月累计。
3. `readtime_year`:每本书今年总阅读时长(分钟),为`readtime12m`各月之和。
4. 支持无笔记但当天有打开书籍时,阅读时长设为`READ_TIME_OPEN_DAY`config.py配置默认30分钟
5. 多条笔记时统计相邻笔记时间差仅累加小于3小时的部分更真实反映实际阅读行为。
### 14.2 全局统计函数
- `get_total_readtime_year()`:返回全年所有书的累计阅读时间(分钟)。
- `get_total_readtime12m()`返回全年所有书的月度累计阅读时间长度12的列表单位分钟
- `get_total_readtime(days=30)`返回最近days天每天所有书籍的总阅读时间分钟索引0为今天。
#### 设计说明
- 所有统计均以“分钟”为单位,便于可视化和分析。
- 年度统计遍历今年每一天,保证月度和年度数据完整。
- 统计逻辑与实际阅读行为高度贴合,支持无笔记但有打开书籍的场景。
---
### 14.3 可视化设计(统计标签页)
**布局**:统计页使用 2x2 宫格:
- 左上frame_bubble综合指标气泡图。
- 右上frame_year全年 12 个月阅读时长柱状图。
- 左下 (frame_week):最近 7 天阅读时长柱状图索引0=今天)。
- 右下 (frame_month):最近 30 天阅读时长柱状图索引0=今天)。
**数据来源**
- 周图:`get_total_readtime(days=7)` 结果列表(单位:分钟)。
- 月图:`get_total_readtime(days=30)` 结果列表(单位:分钟)。
- 年图:`get_total_readtime12m()` 返回长度 12 列表(分钟)。
- 综合:
* 全年阅读小时数 = `get_total_readtime_year() / 60`向下取整或保留1位小数
* 月均阅读小时数 = `(sum(month_list) / 12) / 60`。
* 近7天阅读小时数 = `sum(week_list) / 60`。
* 日均阅读分钟数 = `sum(month_list[:30 或 recent30]) / 30`使用最近30天合计除以30
**气泡图**
- 使用 5 个气泡新增“已读”分别表示全年累计、月均、近7天、日均、今年已读完书籍数量。
- 半径 r ~ sqrt(value_normalized) 以减弱大值差异;先将不同单位映射到统一“分钟等价”尺度:`h -> *60``m -> 原值``book -> *60`(约定 1 本折算 1 小时,确保视觉上不至过小)。
- 默认布局4 指标布局在十字形当包含“已读”且数量≥5 时采用菱形 + 中心分布(中间放“已读”)。
- 颜色:全年(#5b6ee1)、月均(#c9b2d9)、近7天(#f4b2c2)、日均(#b9b542)、已读(#6aa84f)。
- 文本格式:`值 + 单位\n标签`;单位规则:`h` 显示“x.x 小时 / x 小时”,`m` 显示“x 分钟”,`book` 显示“x 本书”。
- 归一化约束:自动检测重叠,通过求最小非重叠缩放系数 S使圆之间保留最小 6px 间距。
**渲染技术**
- 使用原生 Qt 自绘组件QWidget + QPainter实现柱状图与气泡图文件 `charts.py`。
- 优势:减少第三方依赖(移除 matplotlib启动更快、打包体积更小自绘可精细掌控布局与样式。
- 结构:
* `BarChartWidget`:通用柱状图组件,支持数值标签、自适应缩放、单位显示。
* `BubbleMetricsWidget`:四指标气泡图,按归一化后的平方根缩放半径,支持动态指标扩展。
- 刷新策略:当前初始化时构建;若后续增加刷新按钮,可对组件调用 setData/setMetrics 后 update()。
**更新策略**
1. 启动时已调用 `sync_source_files`,再构建 `BookListManager`。
2. 通过管理器获取三类聚合数据。
3. 生成 numpy 数组(可选)并绘制。
4. 若无数据(全 0显示占位提示“暂无阅读数据”。
**异常处理**
- 捕获绘图异常ImportError/RuntimeError在 frame 中放置 QLabel 显示错误信息而不是抛出。
**后续扩展**
- 柱状图支持堆叠 / 渐变填充、鼠标 hover tooltip。
- 气泡图支持动画过渡或改为雷达/仪表盘形式;“已读”气泡可按年份切换(未来提供年份选择器)。
- 增加刷新按钮与 Esc 退出全屏逻辑。
---