From 8e8a0bfba6057c8556699a5357a308dde6391641 Mon Sep 17 00:00:00 2001 From: kinghwj Date: Fri, 11 Sep 2026 17:30:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E7=A7=BB=E6=A4=8D=202.0=20?= =?UTF-8?q?=E9=98=85=E8=AF=BB=E7=9C=8B=E6=9D=BF=E6=8E=A2=E7=B4=A2=20Demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/code-design.md | 50 +++++++ docs/feature-checklist.md | 127 ++++++++++++++++++ docs/technical-guide.md | 57 ++++++++ web-demo/app.js | 273 ++++++++++++++++++++++++++++++++++++++ web-demo/demo-data.js | 25 ++++ web-demo/index.html | 16 +++ web-demo/styles.css | 11 ++ 7 files changed, 559 insertions(+) create mode 100644 docs/code-design.md create mode 100644 docs/feature-checklist.md create mode 100644 docs/technical-guide.md create mode 100644 web-demo/app.js create mode 100644 web-demo/demo-data.js create mode 100644 web-demo/index.html create mode 100644 web-demo/styles.css diff --git a/docs/code-design.md b/docs/code-design.md new file mode 100644 index 0000000..0223520 --- /dev/null +++ b/docs/code-design.md @@ -0,0 +1,50 @@ +# 阅间 Demo 设计与数据流 + +## 页面分工 + +统计并入阅读概览;导航保留阅读概览、我的书架、阅读目标、笔记与划线、发现好书。概览展示阅读统计和目标摘要,目标页管理多份具体阅读清单。 + +完整功能、业务规则、接口落点与状态见 [功能目标清单](feature-checklist.md)。该清单先于本轮实现建立,后续代码开发继续追加和更新。 + +此版本属于 2.0 设计探索。正式版本需要接入真实接口、完成验收并合并到正式 Web 项目的 `master`,随后再创建 `v2.0` 标签。 + +## 数据流 + +`demo-data.js` 提供样例书籍、书单及初始目标。`app.js` 根据当前页面和筛选条件渲染内容;用户输入写入目标草稿,验证通过后保存到浏览器,再更新目标列表。现有 Flask 服务和数据采集代码不参与本轮 Demo。 + +书籍卡片打开详情;详情页可选目录、个人笔记、热门划线、点评、相似书。个人笔记与公开点评使用不同数据集合。热门划线中的想法继续打开单条详情。 + +书籍、书单详情均可加入已有目标,也可带着已选内容创建新目标。 + +## 阅读目标口径与例子 + +目标字段:`id`、`name`、`start`、`end`、`ids`、`sources`。`ids` 存放去重后的书籍 ID;`sources` 记录曾加入的书单名称,作为来源说明,不表示持续关联。 + +例:单独选择已读完的《活着》,再加入包含《活着》《人生海海》《悉达多》的书单。保存后总数为 3 本、已读完为 1 本,进度为 33%。从目标中移除《人生海海》后,进度为 1 / 2,即 50%。 + +完成状态取书籍当前已读状态,不受目标起止日期约束。同一本书可计入多个目标。同一目标内不重复计数。 + +状态判断先检查是否全部读完,再检查日期。例如目标时间为未来一周,但其中两本书都已读完,直接显示已完成;尚未全部完成时,按日期显示未开始、进行中、已到期。 + +书单采用加入时的书目,不自动跟随外部增删。移除目标内一本书,只影响这个目标,不改变书架或其他目标。 + +## 实现位置 + +| 职责 | 位置 | +|---|---| +| 去重与完成率、状态 | `app.js`:`uniqueIds`、`goalMetrics` | +| 新建、编辑、书单混选 | `app.js`:`openGoalEditor`、`renderPicker`、`saveGoal` | +| 加入已有目标 | `app.js`:`chooseTarget`、`appendGoal` | +| 本地持久化与异常恢复 | `app.js`:初始加载、`persistGoals` | +| 页面与详情导航 | `app.js`:`navigate`、`renderPage`、`handleAction` | +| 笔记导出 | `app.js`:`exportNotes` | + +用户输入的目标名称和来源在 HTML 渲染前转义。日期和内容在保存前校验;原生对话框提供焦点管理和 Escape 关闭。 + +## 示例与真实接口的边界 + +概览图表使用固定样例;目标日期状态使用浏览器当前本地日期。笔记导出包含当前选中书目的划线和个人想法全集,不受内容类型筛选影响,不包含书签内容及公开点评。 + +书架的有声内容完结不等于用户读完,所以未用其完结状态计算阅读目标。文章收藏是入口,不假装能直接列出内部文章。缺失资料显示暂无,不补造 ISBN 等字段。 + +正式接口接入单独推进,必须核对实时可用接口、鉴权、分页、错误、升级提示及字段口径;本轮 Demo 的完成状态不得用于宣称这些能力已真实接入。 diff --git a/docs/feature-checklist.md b/docs/feature-checklist.md new file mode 100644 index 0000000..03597b2 --- /dev/null +++ b/docs/feature-checklist.md @@ -0,0 +1,127 @@ +# 阅间功能目标清单 + +> 创建日期:2026-09-11。本文先于本轮 Demo 开发建立,是后续实现、验收和追加需求的统一清单。 + +当前版本定位:本目录中的静态 Demo 是 2.0 设计探索,不是正式 2.0 发布。只有后续真实功能合并到正式 Web 项目的 `master` 并完成验收后,才创建 `v2.0` 标签。 + +## 使用方式 + +- 功能编号固定,后续需求追加新编号,不复用或删除旧编号。 +- 分别维护「Demo 状态」与「真实接入状态」。假数据交互完成,不等于接口或正式功能完成。 +- 状态使用:待实现、进行中、已验证、待接口验证、暂不支持。 +- 实现后记录文件和函数或元素位置、验收结果;业务口径变化时同步更新规则与变更记录。 +- 当前范围为优化本地假数据 Demo,不接入账号,不修改原 Python 业务逻辑,不推送。 + +## 已确认业务口径 + +| 编号 | 规则 | +|---|---| +| R01 | 合并阅读概览与阅读统计,统计在概览内展示,不设置重复导航。 | +| R02 | 支持多个阅读目标,每个目标有名称、开始日期、结束日期。结束日期不得早于开始日期。 | +| R03 | 目标可添加具体书籍、一个或多个书单,允许混合选择。 | +| R04 | 同一目标内按书籍 ID 去重;同一本书可属于多个目标。 | +| R05 | 已读完的书直接计为完成,不要求在目标时间范围内读完,不做重读。 | +| R06 | 完成率 = 已读完书籍数 / 去重后目标书籍数。创建目标至少选一本;空目标展示未开始,不除零。 | +| R07 | 时间范围仅安排计划和提示状态,不改变书籍是否读完。全部读完优先显示已完成;否则按未开始、进行中、已到期展示。 | +| R08 | 本版把书单当前成员加入目标并展示来源,保存时提示「加入当前书目」。书单后续变动不自动增删目标书籍;自动跟随属于后续待讨论项。 | +| R09 | Demo 目标保存在当前浏览器,刷新保留;不写回微信读书。现有文档没有目标写回接口。 | +| R10 | 接入时按接口已读完状态判断,不把进度 1 当作完成:书架 finishReading=1;独立进度需要 progress=100 且存在 finishTime。冲突、缺失数据需处理,不伪造已读状态。 | +| R11 | 书架条目数 = books.length + albums.length + (mp 非空 ? 1 : 0)。有声专辑「已完结」不是用户「已读完」。 | +| R12 | 笔记总数 = 划线数 + 想法/点评数 + 书签数。公开点评不计入个人笔记;书签目前仅有数量,无内容。 | +| R13 | 时间戳显示 YYYY-MM-DD;时长按秒转换为小时/分钟;评分按对应接口说明转换,不能混用百分制、五分制。 | +| R14 | 演示数据与真实数据明确区分。无 deepLink 不拼接阅读链接。可选字段缺失不当作 0,也不补造内容。 | + +## 功能目标与追踪 + +以下位置已按本轮实现回填。函数名作为稳定定位依据,行号仅对应本次版本。 + +| ID | 功能与验收标准 | Demo 状态 | 真实接入状态 | 代码位置 | +|---|---|---|---|---| +| F01 | 五个入口:概览、书架、目标、笔记、发现;暗亮切换及移动适配 | 已验证(示例) | 不涉及接口 | `web-demo/app.js:54` `navigate`; `web-demo/app.js:30` `setTheme` | +| F02 | 概览内切换周/月/年/总计,展示时长、天数、日均、排行、偏好 | 已验证(示例) | 待实现 | `web-demo/app.js:85` `renderOverview` | +| F03 | 多目标列表、名称、时间范围、封面组合、完成率、到期状态 | 已验证(示例) | 本地功能,待正式持久化 | `web-demo/app.js:76` `renderGoals`; `web-demo/app.js:67` `goalMetrics` | +| F04 | 创建与编辑目标,校验日期和非空内容;刷新保留 | 已验证(示例) | 本地功能,待正式持久化 | `web-demo/app.js:122` `openGoalEditor`; `web-demo/app.js:138` `saveGoal` | +| F05 | 目标混合选择单书与书单,去重,已读直接计完成 | 已验证(示例) | 待接真实书籍状态 | `web-demo/app.js:131` `selectedGoalBooks`; `web-demo/app.js:67` `goalMetrics` | +| F06 | 目标详情查看成员、来源、单本进度;编辑移除成员 | 已验证(示例) | 待接真实书籍状态 | `web-demo/app.js:118` `openGoalDetail`; `web-demo/app.js:122` `openGoalEditor` | +| F07 | 我的书架:在读/已读/想读、书单、有声书和文章收藏入口 | 已验证(示例) | 待实现 | `web-demo/app.js:105` `renderShelf` | +| F08 | 我的书单列表与详情;从书单添加目标 | 已验证(示例) | 待实现 | `web-demo/app.js:104` `renderLists`; `web-demo/app.js:113` `openList` | +| F09 | 发现页:书籍/书单搜索、公开书单详情示意、空结果 | 已验证(示例) | 搜索待实现;公开书单成员待接口验证 | `web-demo/app.js:158` `renderDiscover`; `web-demo/app.js:113` `openList` | +| F10 | 书籍详情:信息、阅读进度、目录;加入目标 | 已验证(示例) | 待实现 | `web-demo/app.js:192` `openBook`; `web-demo/app.js:197` `renderBookTab` | +| F11 | 笔记本、划线与个人想法、书签数量、按书过滤、内容导出 | 内容/导出预览已验证;文件下载待验证 | 待实现;书签内容暂不支持 | `web-demo/app.js:177` `renderNotes`; `web-demo/app.js:184` `exportNotes` | +| F12 | 热门划线、章节热度、划线下想法及想法详情 | 已验证(示例) | 待实现 | `web-demo/app.js:197` `renderBookTab`; `web-demo/app.js:208` `openThought` | +| F13 | 公开点评筛选、点评详情,与个人笔记分离 | 已验证(示例) | 待实现 | `web-demo/app.js:197` `renderBookTab`; `web-demo/app.js:208` `openThought` | +| F14 | 个性化推荐和相似书推荐,可查详情和加入目标 | 已验证(示例) | 待实现 | `web-demo/app.js:158` `renderDiscover`; `web-demo/app.js:197` `renderBookTab` | +| F15 | 搜索其余类型:作者、网文、听书、全文、公众号、文章、综合 | 已验证(示例) | 待实现;非书籍回包结构需实测 | `web-demo/app.js:158` `renderDiscover` | +| F16 | 数据状态:加载、空、失败及重试;演示来源常驻 | 已验证(示例) | 待实现 | `web-demo/app.js:158` `renderDiscover` | +| F17 | API Key 仅服务端使用;版本上报、升级中断、错误转换、分页 | 不属于本轮 Demo | 待实现 | 后续核对 `app.py` 和 `fetch_data.py`,复用现有服务端能力 | +| F18 | 可选统计:阅读时段、作者/出版社/版权方、阅读听书比例、好友排行、勋章、年报 | 已验证(示例) | 待实现;按实际返回字段显示 | `web-demo/app.js:85` `renderOverview` | + +## 全部文档接口的落点 + +依据本地 weread-skills 1.0.4 能力文档。未调用实时 `/_list`,因此此表是文档已知能力,不宣称已发现网关所有实际接口。 + +| 接口 | 使用场景 | 约束与后续接入要求 | +|---|---|---| +| /_list | 接入前核对网关可用接口 | 获取后补齐本清单;不可将未验证能力标完成 | +| /shelf/sync | 书架、我的书单、专辑、文章收藏 | archive 包含名称与 bookIds;mp 仅入口,不是文章内容 | +| /store/search | 综合、电子书、网文、听书、作者、全文、书单、公众号、文章 | 显式 scope;分页看 hasMore/searchIdx;书单成员结构待验证 | +| /book/info | 详情资料与评分 | 按返回字段展示,不补造 ISBN 等信息 | +| /book/chapterinfo | 目录与章节选择 | 使用 chapterUid,保留层级和付费状态 | +| /book/getprogress | 书籍进度、已读状态 | 进度 0–100;时长秒;最近阅读优先加载,避免全书架逐本请求 | +| /readdata/detail | 概览全部阅读统计 | 自然周期;总量用 totalReadTime;日均按自然日;preferTime 从 6 点开始;可选数据缺失时隐藏 | +| /user/notebooks | 笔记本与数量 | count + lastSort 分页;总数含书签 | +| /book/bookmarklist | 划线内容 | 不含书签内容 | +| /review/list/mine | 个人想法与点评 | 与划线合并导出;使用 bookid 参数及 synckey 分页 | +| /book/underlines | 章节划线热度 | 只有人数/位置等统计,不含文本 | +| /book/bestbookmarks | 热门划线 | 固定前 20 条,不支持分页 | +| /book/readreviews | 划线下想法 | 使用返回 range、chapterUid;每个 range 独立分页 | +| /review/single | 想法、点评详情 | 富文本正式接入时需净化后渲染 | +| /review/list | 公开点评及筛选 | 双层 review 包装;20/40/60/80/100 对应 1–5 星 | +| /book/recommend | 为你推荐 | 使用 searchIdx 分页 | +| /book/similar | 相似推荐 | 显式 count/maxIdx;后续携带 sessionId | + +## 验收记录 + +2026-09-11 在本地浏览器实测,结果如下。均为假数据 Demo 验证,不代表真实接口验收。 + +| 场景 | 输入或操作 | 实际结果 | +|---|---|---| +| 导航合并 | 查看侧边栏 | 共 5 个入口,无独立阅读统计 | +| 日期校验 | 开始 2026-09-12,结束 2026-09-01 | 原生表单阻止保存,提示结束日期过早 | +| 非空校验 | 只填写名称与日期,未选择书籍 | 提示至少选择一本书或一个书单 | +| 混合去重 | 单独选《活着》,再加入含它的三本书单 | 已选 3 本,1 本已读完,保存显示 33% | +| 保存刷新 | 保存“周末慢读清单”后刷新 | 名称、日期、3 本书与进度保留 | +| 编辑移除 | 从上述目标移除《人生海海》 | 剩余 2 本,1 本完成,显示 50% | +| 已有目标去重 | 重复加入“心理学,向内走一程”书单 | 原目标仍为 3 本,1 本完成 | +| 书单查询 | 发现页选择书单,输入“心理学” | 仅显示匹配书单,详情可加入已有目标 | +| 书籍内容 | 三体 → 目录 → 示例章节 2 → 划线想法 → 详情 | 展示对应入口及示例内容 | +| 点评筛选 | 读者点评选择“一般” | 仅显示三星的一般点评示例 | +| 相似推荐 | 在书籍详情选择相似推荐 | 展示其他 4 本书,可进入详情 | +| 笔记边界 | 全部笔记 → 书签数量 | 总数 10,划线 2、想法 2、书签 6;不显示书签内容 | +| 导出预览 | 导出全部笔记 | 生成 4 条内容:2 条划线、2 条想法,包含演示声明 | +| 文件下载 | 等待浏览器下载事件 | 事件超时,未确认落盘;保留可复制预览和下载链接,后续实测下载 | +| 搜索类型 | 推荐、作者、网文、听书、全文、公众号、文章、综合 | 均切换为对应类型示意 | +| 失败重试 | 预览状态选失败,再点击重试 | 显示失败状态,重试恢复结果 | +| 统计周期 | 切换月、年、总计、周 | 图表与总量切换;年度最终核对为 128小时42分钟 | +| 书架边界 | 我的书单、有声书、文章收藏 | 展示书单、有声信息和收藏入口边界说明 | +| 主题与响应式 | 1440px 桌面、390px 手机,暗亮切换 | 桌面双列目标;手机单列且编辑弹窗可滚动,无横向溢出 | +| 静态检查 | Node 语法检查 app.js 和 demo-data.js | 均通过 | + +可重复手工验收:新建空内容目标检查提示;选择《活着》和文学书单验证 1/3;刷新验证保存;移除一本验证 1/2。不要把验收用目标写入未来真实账号。 + +## 变更记录 + +| 日期 | 变更 | 依据 | +|---|---|---| +| 2026-09-11 | 建立清单;确认多目标、日期范围、单书与书单混合、已读直接完成 | 本任务用户需求 | +| 2026-09-11 | 完成第二版 Demo,回填代码位置与验收;真实接口接入保留待实现 | 本轮浏览器实测 | + +## 后续追加模板 + +复制下面一行到功能目标表,分配下一个编号,并在完成时同步验收记录。 + +| ID | 功能与验收标准 | Demo 状态 | 真实接入状态 | 代码位置 | +|---|---|---|---|---| +| F19 | 待填写;涉及新规则时追加 R 编号 | 待实现 | 待实现 | 实现后填写文件、函数和行号 | + +后续开发顺序:核对实时 `/_list` 与公开书单成员能力 → 服务端鉴权及只读接口 → 替换 Demo 数据 → 校验分页与字段口径 → 正式目标持久化。每次只将已实际验收的范围标为完成。 diff --git a/docs/technical-guide.md b/docs/technical-guide.md new file mode 100644 index 0000000..36b457f --- /dev/null +++ b/docs/technical-guide.md @@ -0,0 +1,57 @@ +# 阅间设计 Demo 使用说明 + +## 范围与启动 + +这是独立的静态设计 Demo,不调用微信读书接口,不需要 API Key,也不修改现有 Flask 服务、数据采集脚本和 8080 页面。功能与接口的后续实现进度统一维护在 [功能目标清单](feature-checklist.md)。 + +该 Demo 定位为 2.0 设计探索。当前提交用于保存探索成果,不代表 2.0 正式发布,也不创建 `v2.0` 标签。 + +在项目根目录执行: + +```sh +python3 -m http.server 8765 --bind 127.0.0.1 --directory web-demo +``` + +访问 `http://127.0.0.1:8765`。也可以直接打开 `web-demo/index.html`;推荐通过本地服务器使用,以便浏览器稳定保存目标数据。 + +无需安装前端依赖。页面由以下文件构成: + +| 文件 | 内容 | +|---|---| +| `web-demo/index.html` | 页面外壳、导航、详情与目标编辑弹窗 | +| `web-demo/styles.css` | 暗亮主题、书封示意、响应式排版 | +| `web-demo/demo-data.js` | 书籍、书单、初始目标、笔记与点评样例 | +| `web-demo/app.js` | 页面渲染、目标操作、搜索筛选、导出 | + +## 保存与数据边界 + +目标使用当前来源的 `localStorage` 键 `weread-demo-goals-v2`,主题使用 `weread-demo-theme`。刷新后保留;更换浏览器、地址或端口不会自动迁移;清除网站数据会清除本地目标。 + +书单加入目标时保存当前书籍集合,后续不自动跟随书单变化。同一目标内书籍去重,已读完直接计为完成。新增目标必须有名称、有效时间范围和至少一本书。 + +本地存储读取失败时显示示例目标并提示,不自动覆盖原存储;保存失败时保留编辑表单并提示重试。 + +所有阅读数据、书单成员、封面、目录、笔记和点评都是示例。用户编辑的目标只保存在本机,不会写回微信读书。公开书单完整成员获取方式仍需接口验证。 + +## 使用入口 + +- 阅读目标:新建目标,填写日期,勾选书籍或点击书单快捷加入,保存后可再编辑。 +- 我的书架:切换我的书单、有声书、文章收藏与阅读状态。 +- 发现好书:选择搜索类型;右侧预览状态可查看加载、空结果、失败及重试效果。 +- 书籍详情:从卡片进入,切换目录、个人笔记、热门划线、点评、相似推荐。 +- 笔记:按书目和类型筛选,导出当前书目的划线与想法,书签仅显示数量。 + +## 验证 + +语法检查: + +```sh +node --check web-demo/app.js +node --check web-demo/demo-data.js +``` + +交互验收记录见 [功能目标清单](feature-checklist.md)。真实 API、鉴权、分页和服务器持久化尚未验证。 + +## 与 1.0 的关系 + +1.0 继续由根目录的 `app.py`、`fetch_data.py`、`templates/` 和 `static/` 提供,运行在 8080 端口。2.0 探索 Demo 位于 `web-demo/`,两者可以独立预览。 diff --git a/web-demo/app.js b/web-demo/app.js new file mode 100644 index 0000000..f720af6 --- /dev/null +++ b/web-demo/app.js @@ -0,0 +1,273 @@ +const $ = selector => document.querySelector(selector); +const esc = value => String(value).replace(/[&<>"']/g, char => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[char])); +const findBook = id => books.find(book => book.id === id); +const uniqueIds = ids => [...new Set(ids)].filter(id => findBook(id)); +const today = () => new Date().toLocaleDateString('sv-SE'); +const storageKey = 'weread-demo-goals-v2'; +let page = 'overview', shelfTab = 'reading', discoverTab = 'books', searchQuery = '', period = 'week'; +let noteBook = 'all', noteType = 'all', goalFilter = 'all', dataState = 'ready'; +let detailBook = null, bookTab = 'info', reviewFilter = '全部', selectedChapter = 'all'; +let editingGoalId = null, draftIds = new Set(), draftSources = [], goals = initialGoals.map(goal => ({...goal, ids:[...goal.ids], sources:[...goal.sources]})); +let toastTimer, storageWarning = ''; +// 本地数据损坏时保留原存储并恢复示例;保存前仍要求用户通过有效表单提交。 +try { + const saved = localStorage.getItem(storageKey); + if (saved) { + const parsed = JSON.parse(saved); + if (!Array.isArray(parsed) || !parsed.every(goal => typeof goal.id === 'string' && typeof goal.name === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(goal.start) && /^\d{4}-\d{2}-\d{2}$/.test(goal.end) && goal.start <= goal.end && Array.isArray(goal.ids) && Array.isArray(goal.sources))) throw new Error('invalid'); + goals = parsed.map(goal => ({...goal,ids:uniqueIds(goal.ids)})); + } +} catch { storageWarning = '本地目标暂时无法读取,已显示示例目标;原存储未修改。'; } +function toast(message) { + clearTimeout(toastTimer); $('#toast').textContent = message; $('#toast').classList.remove('hidden'); + toastTimer = setTimeout(() => $('#toast').classList.add('hidden'), 3500); +} +function persistGoals(next) { + try { localStorage.setItem(storageKey, JSON.stringify(next)); } + catch { $('#goal-error').textContent = '浏览器无法保存数据,请允许本地存储后重试。'; return false; } + goals = next; return true; +} +function setTheme(theme) { + document.documentElement.dataset.theme = theme; + $('#theme').innerHTML = ``; + $('#theme').setAttribute('aria-label', `切换至${theme === 'dark' ? '亮' : '暗'}色主题`); +} +try { setTheme(localStorage.getItem('weread-demo-theme') === 'dark' ? 'dark' : 'light'); } catch { setTheme('light'); } +$('#theme').onclick = () => { + const theme = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'; setTheme(theme); + try { localStorage.setItem('weread-demo-theme', theme); } catch { toast('主题已切换,本次浏览有效。'); } +}; +function cover(book, mini = false) { + if (mini) return ``; + return `
${book.en}${book.short.replaceAll('\n','
')}
${book.author}READING SPACE · 示意封面
`; +} +function bookCard(book) { + return ``; +} +function bookRow(book, source = '') { + return `
${cover(book,true)}
${book.title}

${book.author}${source ? ' · '+esc(source) : ''}

${book.status === 'done' ? '已读完' : `${book.progress}%`}
`; +} +function welcome(title, subtitle, action = '') { + return `
A LITTLE READING, EVERY DAY

${title}

${subtitle}

${action}
`; +} +const pageNames = {overview:'阅读概览',shelf:'我的书架',goals:'阅读目标',notes:'笔记与划线',discover:'发现好书'}; +function navigate(next) { + page = next; $('#crumb').textContent = pageNames[page]; + document.querySelectorAll('[data-page]').forEach(button => {button.classList.toggle('active',button.dataset.page === page);button.setAttribute('aria-current',button.dataset.page === page ? 'page' : 'false');}); + renderPage(); +} +function renderPage() { + const views = {overview:renderOverview,shelf:renderShelf,goals:renderGoals,notes:renderNotes,discover:renderDiscover}; + $('#page-content').innerHTML = views[page](); +} +function tabs(items, selected, action) { + return `
${items.map(([id,label]) => ``).join('')}
`; +} +// 已读状态与目标日期无关;先按书籍 ID 去重,再计算完成率。 +function goalMetrics(goal, date = today()) { + const ids = uniqueIds(goal.ids), done = ids.filter(id => findBook(id).status === 'done').length; + const status = ids.length && done === ids.length ? '已完成' : date < goal.start ? '未开始' : date > goal.end ? '已到期' : '进行中'; + return {total:ids.length,done,percent:ids.length ? Math.round(done/ids.length*100):0,status}; +} +function goalCard(goal) { + const result = goalMetrics(goal); + return `
${result.status}

${esc(goal.name)}

${goal.start} — ${goal.end}
${goal.ids.slice(0,4).map(id=>cover(findBook(id),true)).join('')}${result.total>4?`+ ${result.total-4} 本`:''}
${result.done} / ${result.total} 本已读完${result.percent}%
`; +} +function renderGoals() { + const list = goals.filter(goal => goalFilter === 'all' || (goalFilter === 'complete' ? goalMetrics(goal).status === '已完成' : goalMetrics(goal).status !== '已完成')); + return welcome('把想读,变成读过','几本书、一份书单,或一段想留给阅读的时间。', '')+ + `
YOUR NEXT CHAPTER

目标不必宏大,开始就很好。

每份清单都有自己的节奏。已经读完的书,也会成为你完成目标的一部分。

${goals.length}个目标${goals.filter(g=>goalMetrics(g).status==='已完成').length}个已完成${uniqueIds(goals.flatMap(g=>g.ids)).length}本不同的书
❧
`+ + tabs([['all','全部目标'],['active','未完成'],['complete','已完成']],goalFilter,'goal-filter')+`
${list.map(goalCard).join('') || '
这里还没有目标,创建一份属于你的阅读清单。
'}
`; +} +function summaryGoals() { + return `

我的阅读目标

${goals.slice(0,3).map(goal=>{const m=goalMetrics(goal);return ``;}).join('')}
`; +} +function renderOverview() { + const values = {week:[42,65,33,57,86,71,48],month:[48,54,65,38,70,52,64,75,68,42,46],year:[8,11,14,9,18,15,12,14,27.7],overall:[75,96,128.7]}[period]; + const labels = {week:['一','二','三','四','五','六','日'],month:['1日','2日','3日','4日','5日','6日','7日','8日','9日','10日','11日'],year:['1月','2月','3月','4月','5月','6月','7月','8月','9月'],overall:['2024','2025','2026']}[period]; + const max = Math.ceil(Math.max(...values)/10)*10; + const totalMinutes = values.reduce((a,b)=>a+b,0)*(period==='year'||period==='overall'?60:1); + const duration = `${Math.floor(totalMinutes/60)}小时${Math.round(totalMinutes%60)}分钟`; + const days = {week:7,month:11,year:132,overall:412}[period]; + const naturalDays = {week:7,month:11,year:254,overall:985}[period]; + const units = period==='year'||period==='overall'?'时':'分'; + const stats = [['阅读 / 收听时长',duration,'当前所选周期','clock'],['有效阅读天数',`${days}天`,'单日满 1 分钟计一天','flag'],['自然日均阅读',`${Math.round(totalMinutes/naturalDays)}分钟`,'按周期自然日计算','chart'],['笔记总数','10条','划线 2 · 想法 2 · 书签 6','note']]; + return welcome('在字里行间,遇见自己','阅读、目标与灵感,在这里慢慢积累。','')+ + `
${tabs([['week','本周'],['month','本月'],['year','本年'],['overall','总计']],period,'period')}统计示例 · ${period==='overall'?'2024—2026':'2026 年'}
${stats.map(([label,value,hint,icon])=>`
${label}
${value}
${hint}
`).join('')}
+

阅读的日常 每一天,都算数

${period==='year'?'按月':period==='overall'?'按年':'按日'}
${duration}阅读与收听合计
${[max,Math.round(max*2/3),Math.round(max/3),0].map(n=>``).join('')}
${values.map((v,i)=>``).join('')}

继续阅读

${books.filter(b=>b.status==='reading').map(bookCard).join('')}
+

读得最多

${books.slice(0,3).map((b,i)=>`
${i+1}. ${b.title}${[2,1,1][i]}小时${[12,35,6][i]}分钟
`).join('')}

偏爱的阅读方向

${[['文学',80],['心理学',60],['科幻',40]].map(([label,val])=>`
${label}相对偏好 ${val}
`).join('')}

相对偏好权重,不是阅读占比

留给自己的时段

${Array.from({length:24},(_,i)=>``).join('')}
06:0018:00次日 05:00

偏好夜间阅读 · 示例时段分布

+
更多阅读画像 · 作者、听书、勋章与年度回顾

偏好作者与出版方

刘慈欣3 本 · 5小时30分钟
示例出版社4 本
示例版权方3 本

阅读与收听

文字阅读80%

示例分布;正式接入只在接口返回时展示。

阅读足迹

好友排行本周第 3 · 示例
勋章初读 · 笔记 · 坚持
`; +} +function listCard(list, index) { + return ``; +} +function renderLists(lists) { return `
${lists.map(listCard).join('') || '
没有匹配的书单,换个关键词试试。
'}
`; } +function renderShelf() { + let content; + if (shelfTab==='lists') content=renderLists(booklists.filter(l=>l.source==='我的书单')); + else if (shelfTab==='audio') content=`
♫

晚间听书 · 文学选读

示例演播者 · 24 集 · 内容已完结

内容完结不代表已听完;当前不纳入按本阅读目标。

`; + else if(shelfTab==='articles') content=`
▤

文章收藏

这是书架中的收藏入口。

当前书架接口不提供入口内文章列表;可在发现页搜索文章。

`; + else content=`
${books.filter(b=>shelfTab==='all'||b.status===shelfTab).map(bookCard).join('')}
`; + return welcome('我的书架','8 本电子书 · 1 个有声专辑 · 1 个文章收藏入口,共 10 个书架条目。')+tabs([['all','全部电子书 8'],['reading','在读 4'],['done','已读完 2'],['wish','想读 2'],['lists','我的书单 2'],['audio','有声书 1'],['articles','文章收藏']],shelfTab,'shelf-tab')+content; +} +function openList(id) { + const list=booklists.find(l=>l.id===id); + showDetail('书单详情',`
A COLLECTION OF GOOD BOOKS

${list.name}

${list.description}

${list.source} · ${list.ids.length} 本 · 示例成员

${list.ids.map(id=>bookRow(findBook(id))).join('')}
${list.source.startsWith('公开')?'

公开书单成员为设计示意,完整成员获取方式待真实接口验证。

':''}`); +} +function showDetail(title, html) { $('#detail-title').textContent=title;$('#detail-content').innerHTML=html;if(!$('#detail').open)$('#detail').showModal(); } +function openGoalDetail(id) { + const goal=goals.find(g=>g.id===id), m=goalMetrics(goal); + showDetail('阅读目标',`
${m.status}${goal.start} — ${goal.end}

${esc(goal.name)}

${m.done} / ${m.total}本已读完${m.percent}%已完成

已读完直接计入完成,不受目标日期影响。

${goal.sources.length?`

加入过的书单:${goal.sources.map(esc).join('、')} · 保存时的书目

`:''}
${goal.ids.map(id=>bookRow(findBook(id))).join('')}
`); +} +function openGoalEditor(id=null, prefill=[], sources=[]) { + $('#detail').close(); editingGoalId=id; + const goal=id?goals.find(g=>g.id===id):null; + draftIds=new Set(goal?goal.ids:prefill);draftSources=[...(goal?goal.sources:sources)]; + $('#editor-title').textContent=goal?'编辑阅读目标':'新建阅读目标';$('#goal-name').value=goal?goal.name:''; + $('#goal-start').value=goal?goal.start:today();$('#goal-end').value=goal?goal.end:`${today().slice(0,4)}-12-31`; + $('#goal-end').min=$('#goal-start').value;$('#goal-end').setCustomValidity('');$('#goal-name').setCustomValidity(''); + $('#picker-search').value='';$('#goal-error').textContent='';renderPicker();$('#goal-editor').showModal(); +} +function selectedGoalBooks() { return uniqueIds([...draftIds]); } +function renderPicker() { + const query=$('#picker-search').value.trim().toLowerCase(); + const list=books.filter(b=>`${b.title}${b.author}`.toLowerCase().includes(query)); + $('#selection-count').textContent=`已选 ${draftIds.size} 本 · ${[...draftIds].filter(id=>findBook(id).status==='done').length} 本已读完`; + $('#goal-picker').innerHTML=`

快捷加入书单当前书目

${booklists.filter(l=>!query||l.name.includes(query)||l.ids.some(id=>list.some(b=>b.id===id))).map(l=>``).join('')}
${list.map(b=>``).join('')||'
没有匹配的书籍
'}
`; +} +function saveGoal(event) { + event.preventDefault();const name=$('#goal-name').value.trim(),start=$('#goal-start').value,end=$('#goal-end').value,ids=selectedGoalBooks(); + if(!name){$('#goal-name').setCustomValidity('请输入目标名称');$('#goal-name').reportValidity();return;} + if(endg.id===editingGoalId?goal:g):[goal,...goals]; + if(!persistGoals(next))return; + $('#goal-editor').close();navigate('goals');toast('阅读目标已保存,已读完的书已计入完成。'); +} +function chooseTarget(ids,sources=[]) { + showDetail('加入阅读目标',`

选择已有目标,或为这些书新建目标。重复书籍只加入一次。

${goals.map(g=>`
${esc(g.name)}

${g.start} — ${g.end} · ${g.ids.length} 本

`).join('')}
`); + pendingBooks=ids;pendingSources=sources; +} +let pendingBooks=[],pendingSources=[]; +function appendGoal(id) { + const next=goals.map(g=>g.id===id?{...g,ids:uniqueIds([...g.ids,...pendingBooks]),sources:[...new Set([...g.sources,...pendingSources])]}:g); + try { localStorage.setItem(storageKey,JSON.stringify(next)); } catch { toast('无法保存到浏览器,请允许本地存储后重试。');return; } + goals=next;renderPage();openGoalDetail(id);toast('已加入目标,重复书籍已自动去重。'); +} +function renderDiscover() { + const options=[['books','书籍'],['lists','书单'],['recommend','为你推荐'],['all','综合'],['author','作者'],['novel','网文'],['audio','听书'],['fulltext','全文'],['account','公众号'],['article','文章']]; + const matched=books.filter(b=>`${b.title}${b.author}`.toLowerCase().includes(searchQuery.toLowerCase())); + let content=''; + if(dataState!=='ready') { + const copy={loading:['正在寻找好书','加载状态示意,点击下方按钮查看结果。'],empty:['暂时没有找到','换个关键词,或看看为你推荐。'],error:['暂时无法获取数据','连接失败示意,你的阅读目标仍保存在本地。']}[dataState]; + content=`
${dataState==='loading'?'LOADING':dataState==='error'?'TRY AGAIN':'NOT FOUND'}

${copy[0]}

${copy[1]}

`; + } else if(discoverTab==='lists') content=renderLists(booklists.filter(l=>`${l.name}${l.description}`.includes(searchQuery))); + else if(discoverTab==='books'||discoverTab==='recommend') content=`${discoverTab==='recommend'?'
PICKED FOR YOUR NEXT CHAPTER

读过的书,会带你遇见下一本。

根据你对文学与心理学的兴趣推荐 · 推荐理由为示例

':''}
${(discoverTab==='recommend'?matched.filter(b=>b.status==='wish'||b.id==='b0'):matched).map(bookCard).join('')||'
没有找到相关书籍,试试其他书名或作者。
'}
`; + else if(discoverTab==='author') content=`
${[...new Set(matched.map(b=>b.author))].map(name=>`
${name.slice(0,1)}
${name}

作者结果 · 示例

`).join('')||'
没有匹配的作者。
'}
`; + else if(discoverTab==='all') content=`

书籍

${matched.slice(0,4).map(bookCard).join('')||'
没有匹配的书籍。
'}

书单

${renderLists(booklists.filter(l=>l.name.includes(searchQuery)))}`; + else { + const examples={novel:['星河来信','网文小说 · 示例作品','连载中 · 128 章'],audio:['夜晚的文学课','有声专辑 · 示例演播者','24 集 · 内容已完结'],fulltext:['关于阅读的一段文字','全文搜索 · 原创示例文本','阅读让日常拥有了更宽阔的边界。'],account:['一页书房','公众号 · 示例账号','记录阅读、日常与思考'],article:['如何为自己留一点阅读时间','文章 · 原创示例','2026-09-11 · 一页书房']}[discoverTab]; + const matches=!searchQuery||examples.join('').includes(searchQuery); + content=matches?`
${discoverTab==='audio'?'♫':'▤'}

${examples[0]}

${examples[1]}

${examples[2]}

`:'
没有匹配的演示内容,可清空关键词查看该类型示意。
'; + } + return welcome('下一本,遇见什么?','寻找一本书,也发现一份恰好适合你的书单。')+ + `
${tabs(options,discoverTab,'discover-tab')}
${content}

搜索、推荐、书单均为设计样例,未请求微信读书服务。

`; +} +function renderNotes() { + const list=personalNotes.filter(note=>(noteBook==='all'||note.bookId===noteBook)&&(noteType==='all'||note.type===noteType)); + const counts=personalNotes.filter(note=>noteBook==='all'||note.bookId===noteBook); + const bookmarks=noteBook==='all'?6:noteBook==='b0'?3:noteBook==='b1'?2:noteBook==='b2'?1:0; + return welcome('把读过的,留在心里','划线、想法与每一次停顿,都有自己的意义。','')+ + `
${counts.length+bookmarks}条笔记划线 ${counts.filter(n=>n.type==='highlight').length}想法 ${counts.filter(n=>n.type==='thought').length}书签 ${bookmarks}
${tabs([['all','全部内容'],['highlight','划线'],['thought','想法 / 个人点评'],['bookmark','书签数量']],noteType,'note-type')}${noteType==='bookmark'?`
${bookmarks} 个书签 · 当前接口只提供数量,不提供书签内容。
`:list.map(note=>`
${note.type==='thought'?'我的想法':'划线示意'}
${note.text}
《${findBook(note.bookId).title}》 · ${note.date}

原创示例文本,非书中原文

`).join('')||'
当前筛选没有笔记。
'}

总笔记数包含书签;导出包含划线和个人想法,不含公开点评及书签内容。

`; +} +function exportNotes() { + // 导出按当前选中书目取划线和想法全集,内容类型筛选不影响导出完整性。 + const notes=personalNotes.filter(note=>noteBook==='all'||note.bookId===noteBook); + const text='# 阅间笔记 · 演示数据\n\n原创示例,非书中原文。书签仅提供数量,不导出内容。\n\n'+notes.map(n=>`## ${findBook(n.bookId).title}\n\n${n.type==='thought'?'想法':'划线示意'} · ${n.date}\n\n${n.text}`).join('\n\n'); + const url=URL.createObjectURL(new Blob([text],{type:'text/markdown;charset=utf-8'})); + showDetail('导出笔记', `

已生成当前书目的划线与想法。可下载 Markdown,或直接选中下方内容复制;不含书签内容。

`); + $('#detail').addEventListener('close',()=>URL.revokeObjectURL(url),{once:true}); +} +function openBook(id) { detailBook=id;bookTab='info';selectedChapter='all';renderBookDetail(); } +function renderBookDetail() { + const book=findBook(detailBook); + showDetail('书籍详情',`
${cover(book)}
READING SPACE / BOOK

${book.title}

${book.author}

评分 ${Math.round(Number(book.score)*10)}/100 · 示例评分

${book.status==='done'?'已读完':`已读 ${book.progress}%`} · 累计 2小时18分钟

${[['info','简介'],['chapters','目录'],['notes','我的笔记'],['popular','热门划线'],['reviews','读者点评'],['similar','相似推荐']].map(([id,label])=>``).join('')}
${renderBookTab(book)}

全部为设计示例 · 目录、文字和读者资料非真实接口内容。

`); +} +function renderBookTab(book) { + if(bookTab==='info')return `

${book.desc}

分类:${book.id==='b1'?'科幻':'文学 / 人文'} · 示例
最近阅读:2026-09-11
出版社:示例出版社
译者 / ISBN:暂无演示数据
字数:约 20 万字 · 示例
出版日期:暂无演示数据
`; + if(bookTab==='chapters')return `

目录结构示意;选择章节可查看该章热门划线与热度。

${['序章 · 故事的开始','第一章 · 在旅途中','第二章 · 看见新的世界','第三章 · 留给自己的答案'].map((name,i)=>`
${String(i+1).padStart(2,'0')}   ${name} · 示例章节
`).join('')}`; + if(bookTab==='notes') { + const notes=personalNotes.filter(n=>n.bookId===book.id); + return notes.map(n=>`
${n.type==='thought'?'个人想法':'划线示意'}
${n.text}
${n.date} · 原创示例文本
`).join('')||'
这本书暂无个人笔记示例。
'; + } + if(bookTab==='popular')return `
${selectedChapter==='all'?'全书':`示例章节 ${Number(selectedChapter)+1}`} · 热门划线最多展示 20 条
${['阅读让我们有机会,在自己的生活之外再生活一次。','那些认真思考过的问题,会在未来的某一天回应你。'].map((text,i)=>`
${[1286,862][i]} 人划线 · 示例热度
${text}
原创示例文本,非书中原文
`).join('')}

章节热度提供人数与位置,原文来自热门划线;这里用示例文本说明展示方式。

`; + if(bookTab==='reviews')return tabs([['全部','全部'],['推荐','推荐'],['一般','一般'],['不行','不行'],['最新','最新']],reviewFilter,'review-filter')+publicReviews.filter(r=>reviewFilter==='全部'||reviewFilter==='最新'||r.type===reviewFilter).map(review=>`
${review.name} · 示例读者${'★'.repeat(review.rating)}${'☆'.repeat(5-review.rating)}

${review.text}

${review.date} · 公开点评示例
`).join(''); + return `

相似推荐示例 · 根据主题与阅读兴趣推荐

${books.filter(b=>b.id!==book.id).slice(0,4).map(bookCard).join('')}
`; +} +function openThought(text, subtitle) { showDetail('想法详情',`
示例内容
${esc(text)}
${esc(subtitle)} · 2026-09-11

这里展示单条想法的完整内容;不生成未经接口提供的评论或点赞列表。

`); } +function handleAction(button) { + const action=button.dataset.action,id=button.dataset.id; + switch(action) { + case 'page':navigate(id);break; + case 'new-goal':openGoalEditor();break; + case 'edit-goal':openGoalEditor(id);break; + case 'goal-detail':openGoalDetail(id);break; + case 'goal-filter':goalFilter=id;renderPage();break; + case 'close-detail':$('#detail').close();break; + case 'close-editor':$('#goal-editor').close();break; + case 'pick-list':{ + const list=booklists.find(l=>l.id===id);list.ids.forEach(bookId=>draftIds.add(bookId)); + if(!draftSources.includes(list.name))draftSources.push(list.name);renderPicker();break; + } + case 'book':openBook(id);break; + case 'list':openList(id);break; + case 'add-book':chooseTarget([id]);break; + case 'add-list':{const list=booklists.find(l=>l.id===id);chooseTarget(list.ids,[list.name]);break;} + case 'append-goal':appendGoal(id);break; + case 'new-with-selection':openGoalEditor(null,pendingBooks,pendingSources);break; + case 'shelf-tab':shelfTab=id;renderPage();break; + case 'period':period=id;renderPage();break; + case 'discover-tab':discoverTab=id;renderPage();break; + case 'retry':dataState='ready';renderPage();break; + case 'notebook':noteBook=id;renderPage();break; + case 'note-type':noteType=id;renderPage();break; + case 'export-notes':exportNotes();break; + case 'personal-note':{const note=personalNotes.find(n=>n.id===id);openThought(note.text,findBook(note.bookId).title+' · 个人笔记示例');break;} + case 'book-tab':bookTab=id;renderBookDetail();break; + case 'chapter':selectedChapter=id;bookTab='popular';renderBookDetail();break; + case 'review-filter':reviewFilter=id;renderBookDetail();break; + case 'review-detail':{const r=publicReviews.find(r=>r.id===id);openThought(r.text,r.name+' · 公开点评示例');break;} + case 'highlight-thoughts':showDetail('划线下的想法',`
${id==='0'?'阅读让我们有机会,在自己的生活之外再生活一次。':'那些认真思考过的问题,会在未来的某一天回应你。'}

原创划线示意

山间来信 · 示例读者2026-09-11

这句话让我想起,每一段阅读经历都有自己的时间。

仅展示一条想法示例,真实内容需分页获取。

`);break; + case 'thought-detail':openThought('这句话让我想起,每一段阅读经历都有自己的时间。','山间来信 · 公开想法示例');break; + case 'author-books':searchQuery=button.dataset.name;discoverTab='books';$('#global-search').value=searchQuery;renderPage();break; + case 'article-search':searchQuery='';discoverTab='article';navigate('discover');break; + case 'search-example':openThought('这是此类搜索结果的详情布局示意。实际内容、可跳转地址及字段以未来接口返回为准。','搜索结果 · '+id);break; + case 'year-report':showDetail('年度回顾',`
2026 / A YEAR IN BOOKS

翻过的每一页,都算数。

128小时42分钟 · 132 个阅读日 · 示例年度数据

偏爱文学,习惯在夜晚阅读。每一次阅读,都是给自己的时间。

年报布局示意,真实年度报告入口只使用服务端返回的链接。

`);break; + } +} +document.addEventListener('click',event=>{ + const action=event.target.closest('[data-action]');if(action){handleAction(action);return;} + const nav=event.target.closest('[data-page]');if(nav){searchQuery='';$('#global-search').value='';navigate(nav.dataset.page);} +}); +document.addEventListener('change',event=>{ + if(event.target.matches('[data-pick-book]')){const id=event.target.dataset.pickBook;event.target.checked?draftIds.add(id):draftIds.delete(id);renderPicker();} + if(event.target.id==='data-state'){dataState=event.target.value;renderPage();} + if(event.target.id==='chapter-filter'){selectedChapter=event.target.value;renderBookDetail();} +}); +$('#goal-start').onchange=()=>{$('#goal-end').min=$('#goal-start').value;}; +$('#goal-name').oninput=()=>$('#goal-name').setCustomValidity(''); +$('#goal-form').onsubmit=saveGoal;$('#picker-search').oninput=renderPicker; +$('#global-search').oninput=event=>{searchQuery=event.target.value.trim();dataState='ready';if(page!=='discover')discoverTab='books';navigate('discover');}; +document.addEventListener('input',event=>{ + if(event.target.id==='discover-search'){ + const position=event.target.selectionStart;searchQuery=event.target.value;$('#global-search').value=searchQuery;renderPage(); + $('#discover-search').focus();$('#discover-search').setSelectionRange(position,position); + } +}); +document.addEventListener('keydown',event=>{if((event.metaKey||event.ctrlKey)&&event.key==='k'){event.preventDefault();$('#global-search').focus();}}); +// 原生 dialog 提供焦点管理与 Escape 关闭;只有点击实际遮罩时才关闭。 +document.querySelectorAll('dialog').forEach(dialog=>dialog.addEventListener('click',event=>{ + const rect=dialog.getBoundingClientRect();if(event.target===dialog&&(event.clientXrect.right||event.clientYrect.bottom))dialog.close(); +})); +navigate('overview');if(storageWarning)toast(storageWarning); diff --git a/web-demo/demo-data.js b/web-demo/demo-data.js new file mode 100644 index 0000000..ea9740a --- /dev/null +++ b/web-demo/demo-data.js @@ -0,0 +1,25 @@ +// 所有数据均为设计示例,不来自真实账号。 +const books = [{title:'也许你该找个人聊聊',short:'也许你该\n找个人\n聊聊',author:'洛莉·戈特利布',en:'MAYBE YOU SHOULD TALK TO SOMEONE',cover:'c1',progress:68,status:'reading',score:'9.1',desc:'一位心理治疗师的回忆录。跟随几个普通人的故事,理解关系、失去与成长,也重新认识自己。'},{title:'三体',short:'三体',author:'刘慈欣',en:'THE THREE-BODY PROBLEM',cover:'c2',progress:42,status:'reading',score:'9.4',desc:'从一段隐秘的往事出发,走进人类与宇宙文明的宏大故事。科学、想象与命运交织,打开另一种观看世界的尺度。'},{title:'蛤蟆先生去看心理医生',short:'蛤蟆先生\n去看\n心理医生',author:'罗伯特·戴博德',en:'COUNSELLING FOR TOADS',cover:'c3',progress:85,status:'reading',score:'8.8',desc:'借由蛤蟆先生与心理咨询师的对话,探索情绪与行为背后的原因。这是一次关于理解自己、为自己负责的旅程。'},{title:'人生海海',short:'人生\n海海',author:'麦家',en:'LIFE IS A VAST SEA',cover:'c4',progress:26,status:'reading',score:'9.0',desc:'在复杂的时代与人情之中,讲述一个人的秘密与命运。人生如海,有暗流,也有让人继续前行的力量。'},{title:'活着',short:'活着',author:'余华',en:'TO LIVE',cover:'c3',progress:100,status:'done',score:'9.5',desc:'一个人与时代相遇的故事,关于生命的韧性与日常的重量。'},{title:'被讨厌的勇气',short:'被讨厌\n的勇气',author:'岸见一郎 / 古贺史健',en:'THE COURAGE TO BE DISLIKED',cover:'c1',progress:100,status:'done',score:'8.6',desc:'以对话的方式讨论人际关系、自我接纳与选择的自由。'},{title:'悉达多',short:'悉达多',author:'赫尔曼·黑塞',en:'SIDDHARTHA',cover:'c4',progress:0,status:'wish',score:'9.2',desc:'一个寻找自我的人,穿过不同的人生经验,在河流边学会倾听。'},{title:'置身事内',short:'置身\n事内',author:'兰小欢',en:'INSIDE THE SYSTEM',cover:'c2',progress:0,status:'wish',score:'9.3',desc:'从具体的经济生活出发,理解政府与经济发展的关系。'}]; +books.forEach((book, index) => { book.id = `b${index}`; }); +const booklists = [ + {id:'l1',name:'在故事里,读懂人生',source:'我的书单',description:'从别人的故事里,找到自己生活的回声。',ids:['b3','b4','b6']}, + {id:'l2',name:'心理学,向内走一程',source:'我的书单',description:'理解情绪与关系,也练习温柔地对待自己。',ids:['b0','b2','b5']}, + {id:'l3',name:'重新认识这个世界',source:'公开书单 · 示例',description:'从宇宙到日常,拓宽理解世界的尺度。',ids:['b1','b7','b6']}, + {id:'l4',name:'适合慢慢读的夜晚',source:'公开书单 · 示例',description:'把时间留给文学,让心绪落在书页之间。',ids:['b4','b6','b0']} +]; +const initialGoals = [ + {id:'g1',name:'秋季文学计划',start:'2026-09-01',end:'2026-11-30',ids:['b3','b4','b6','b1'],sources:['在故事里,读懂人生']}, + {id:'g2',name:'给内心一点生长的空间',start:'2026-09-01',end:'2026-10-31',ids:['b0','b2','b5'],sources:['心理学,向内走一程']}, + {id:'g3',name:'读完的书,成为自己的底气',start:'2026-08-01',end:'2026-08-31',ids:['b4','b5'],sources:[]} +]; +const personalNotes = [ + {id:'n1',bookId:'b0',type:'thought',text:'把注意力还给生活本身,让平凡的日子有迹可循。',date:'2026-09-11'}, + {id:'n2',bookId:'b1',type:'highlight',text:'在辽阔的未知面前,每一个问题都值得被认真对待。',date:'2026-09-10'}, + {id:'n3',bookId:'b2',type:'thought',text:'允许情绪出现,也给自己一些理解它的时间。',date:'2026-09-08'}, + {id:'n4',bookId:'b3',type:'highlight',text:'人生的答案,或许藏在继续向前的日子里。',date:'2026-09-06'} +]; +const publicReviews = [ + {id:'r1',name:'山间来信',rating:5,type:'推荐',text:'适合放慢速度去读。最打动我的,是读完之后还会不断回想的那些问题。',date:'2026-09-10'}, + {id:'r2',name:'晚风书屋',rating:3,type:'一般',text:'有一些有趣的视角,也有需要耐心消化的地方。每个人会读出不同的感受。',date:'2026-09-09'}, + {id:'r3',name:'一页一页',rating:1,type:'不行',text:'这次阅读没有找到共鸣,可能还不是适合我当下阅读的书。',date:'2026-09-08'} +]; diff --git a/web-demo/index.html b/web-demo/index.html new file mode 100644 index 0000000..f390a29 --- /dev/null +++ b/web-demo/index.html @@ -0,0 +1,16 @@ + + + + +阅间 · 微信读书看板 + + + + + +
L
设计预览 · 全部为示例数据目标保存在当前浏览器
阅间 · 与好书相伴,与自己相遇独立设计 Demo · 未连接微信读书账号
+

书籍详情

+
MAKE ROOM FOR READING

新建阅读目标

想读些什么?

选择书籍或加入书单当前书目,重复书籍只算一本,已读完直接计入完成。

+ \ No newline at end of file diff --git a/web-demo/styles.css b/web-demo/styles.css new file mode 100644 index 0000000..c40ae2d --- /dev/null +++ b/web-demo/styles.css @@ -0,0 +1,11 @@ + +:root{--bg:#f7f8fa;--panel:#fff;--text:#242a34;--muted:#9298a4;--line:#edf0f3;--accent:#356ce5;--soft:#edf3ff;--sidebar:#fff;--shadow:0 8px 32px #24355005;--quote:#f8f6f1;--green:#449682} +[data-theme=dark]{--bg:#14171e;--panel:#1d222c;--text:#e7eaf0;--muted:#8892a4;--line:#2c323f;--accent:#80a5ff;--soft:#26344e;--sidebar:#191d26;--shadow:0 8px 32px #0001;--quote:#292820;--green:#75c6ac;color-scheme:dark} +*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font-family:"PingFang SC","Microsoft YaHei",sans-serif;font-size:14px;transition:background .25s,color .25s}button,input{font:inherit}button{cursor:pointer;color:inherit}button:focus-visible,a:focus-visible,input:focus-visible{outline:3px solid var(--accent);outline-offset:4px}button{border:0;background:none}svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}.sidebar{position:fixed;inset:0 auto 0 0;width:218px;padding:39px 23px 24px;background:var(--sidebar);border-right:1px solid var(--line);display:flex;flex-direction:column;z-index:5}.logo{display:flex;align-items:center;gap:11px;font-size:24px;font-weight:650;letter-spacing:2px}.logo-mark{background:#356ce5;color:white;width:37px;height:37px;display:grid;place-items:center;border-radius:12px}.logo-mark svg{width:25px;height:25px}.logo small{display:block;letter-spacing:2px;font-size:9px;color:var(--muted);margin-top:3px;font-weight:500}.navlabel{font-size:10px;color:var(--muted);letter-spacing:1.7px;margin:53px 14px 15px}.nav{display:flex;flex-direction:column;gap:9px}.nav button{text-align:left;padding:14px 17px;border-radius:9px;display:flex;align-items:center;gap:13px;color:#818997}.nav button.active{color:var(--accent);background:var(--soft);font-weight:600}.nav button:hover{background:var(--soft)}.nav em{margin-left:auto;font-size:10px;font-style:normal;background:var(--line);padding:2px 6px;border-radius:4px}.sidebar-bottom{margin-top:auto}.connection{padding:16px 12px;border:1px solid var(--line);border-radius:10px;font-size:12px;line-height:1.9}.dot{display:inline-block;background:var(--green);width:6px;height:6px;border-radius:50%;margin-right:7px}.connection small{display:block;font-size:10px;color:var(--muted)}.profile{display:flex;align-items:center;gap:10px;margin-top:24px;padding:0 8px}.avatar{width:33px;height:33px;border-radius:50%;display:grid;place-items:center;background:#e4eadf;color:#5e7252;font-family:Georgia;font-size:18px}.profile small{display:block;font-size:10px;color:var(--muted);margin-top:4px}.main{margin-left:218px;padding:0 40px 32px;max-width:1800px}.topbar{height:91px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line)}.breadcrumb{font-size:12px;color:var(--muted)}.breadcrumb span{color:var(--text);margin-left:12px}.top-actions{display:flex;align-items:center;gap:20px}.searchbox{display:flex;align-items:center;gap:10px;color:var(--muted);background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:9px 12px}.searchbox input{background:none;border:0;outline:none;width:180px;color:var(--text);font-size:11px}.key{font-size:10px;border:1px solid var(--line);border-radius:4px;padding:2px 4px}.iconbtn{padding:7px;border:1px solid var(--line);border-radius:8px;background:var(--panel)}.welcome{display:flex;justify-content:space-between;align-items:center;margin:33px 0 27px}.eyebrow{font-size:10px;letter-spacing:2px;color:var(--muted);margin-bottom:10px}h1{font-size:27px;letter-spacing:.5px;margin:0 0 10px;font-weight:600}h1 span{font-family:Georgia;font-size:25px;color:#c3a977}p{margin:0;color:var(--muted);font-size:12px;line-height:1.8}.welcome-right{display:flex;gap:12px;align-items:center}.demo-tag{font-size:10px;padding:6px 9px;background:var(--quote);border:1px solid var(--line);border-radius:5px;color:var(--muted)}.primary{background:#356ce5;color:white;padding:11px 16px;border-radius:7px;font-size:12px}.primary svg{width:15px;height:15px;margin-right:6px}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:18px;margin-bottom:23px}.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow)}.stat{padding:21px 23px}.stat-label{display:flex;justify-content:space-between;align-items:center;color:var(--muted);font-size:12px}.stat-label svg{width:17px;height:17px;color:#9aa8bf}.stat-value{font-family:Georgia,serif;font-size:34px;letter-spacing:-1px;margin:14px 0 12px}.stat-value small{font-family:"PingFang SC",sans-serif;font-size:11px;color:var(--muted);letter-spacing:0;margin-left:5px}.stat-foot{font-size:10px;color:var(--muted)}.stat-foot b{color:var(--green);font-weight:500;margin-right:7px}.columns{display:grid;grid-template-columns:minmax(0,1fr) 296px;gap:23px}.section-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px}.section-head h2{font-size:15px;font-weight:600;margin:0}.section-head h2 small{font-weight:400;color:var(--muted);font-size:10px;margin-left:10px}.text-button{font-size:11px;color:var(--muted);padding:3px}.text-button:hover{color:var(--accent)}.trend{padding:24px 25px;margin-bottom:23px}.segmented{background:var(--bg);padding:3px;display:flex;border-radius:6px}.segmented button{font-size:10px;padding:5px 12px;color:var(--muted);border-radius:4px}.segmented button.active{background:var(--panel);color:var(--accent);box-shadow:0 1px 5px #0001}.trend-total{font-family:Georgia;font-size:25px;margin-bottom:10px}.trend-total small{font-family:"PingFang SC";font-size:10px;color:var(--muted);margin-left:9px}.chart{height:130px;position:relative;padding-left:30px}.gridlines{position:absolute;inset:0 0 15px 30px;display:flex;flex-direction:column;justify-content:space-between}.gridlines i{border-top:1px dashed var(--line);position:relative}.gridlines i:before{content:attr(data-label);position:absolute;left:-29px;top:-6px;color:var(--muted);font-size:9px;font-style:normal}.bars{display:flex;align-items:end;justify-content:space-around;height:100%;gap:14px;position:relative;padding-bottom:15px}.bar{height:var(--h);max-width:33px;flex:1;background:#dbe6fc;border-radius:4px 4px 0 0;position:relative;transition:height .35s}.bar:nth-child(5){background:#4d7ce7}.bar span{position:absolute;top:calc(100% + 9px);left:50%;transform:translateX(-50%);font-size:9px;color:var(--muted);white-space:nowrap}.bar:hover{background:#7199ef}.bar:hover:before{content:attr(data-tip);position:absolute;bottom:calc(100% + 6px);left:50%;transform:translateX(-50%);white-space:nowrap;background:var(--text);color:var(--panel);padding:5px 7px;font-size:10px;border-radius:4px}.shelf{padding:24px 25px}.tabs{display:flex;gap:25px;border-bottom:1px solid var(--line);margin-top:-4px;margin-bottom:24px}.tabs button{padding:0 0 12px;font-size:11px;color:var(--muted);border-bottom:2px solid transparent}.tabs button.active{color:var(--accent);border-bottom-color:var(--accent)}.books{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:23px}.book{text-align:left;padding:0;min-width:0;transition:transform .2s}.book:hover{transform:translateY(-5px)}.cover{height:160px;width:112px;max-width:100%;border-radius:2px 5px 5px 2px;position:relative;padding:15px 12px;overflow:hidden;box-shadow:5px 8px 13px #17212c20,inset 4px 0 3px #ffffff25;display:flex;flex-direction:column;color:#fff}.cover:after{content:"";position:absolute;inset:0;border-left:4px solid #0002;pointer-events:none}.cover .tiny{font-size:6px;letter-spacing:1px;opacity:.8}.cover .title{font-family:"Songti SC","STSong",serif;line-height:1.3;font-size:22px;margin-top:15px;font-weight:700}.cover .author{font-size:7px;margin-top:10px}.cover .publisher{font-size:6px;margin-top:auto;letter-spacing:1px}.c1{background:#efe9d8;color:#374432}.c1 .title{font-size:19px;letter-spacing:1px;margin-top:15px;z-index:1}.c1:before{content:"";position:absolute;width:130px;height:110px;border:1px solid #73876a66;border-radius:50%;left:-29px;bottom:-24px;box-shadow:0 0 0 8px #72826313,0 0 0 18px #72826313,0 0 0 30px #72826313}.c2{background:#182b3b}.c2 .title{font-size:37px;letter-spacing:6px}.c2:before{content:"";position:absolute;left:20px;bottom:-42px;width:115px;height:115px;border:1px solid #b8855144;border-radius:50%;box-shadow:0 0 0 8px #b8855122,0 0 0 17px #b8855111}.c3{background:#db502e}.c3 .title{font-size:19px;color:#fff3d7}.c3:before{content:"";position:absolute;width:60px;height:60px;background:#f4c66a;bottom:13px;right:-8px;border-radius:50%}.c4{background:#f0eee2;color:#282722}.c4 .title{font-size:24px}.c4:before{content:"";position:absolute;width:90px;height:90px;bottom:-24px;right:-25px;background:repeating-linear-gradient(45deg,#3d6274 0 5px,transparent 5px 10px);transform:rotate(-15deg)}.book h3{font-size:12px;font-weight:500;margin:14px 0 5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.book p{font-size:10px}.progress{height:3px;background:var(--line);border-radius:3px;margin-top:13px}.progress i{display:block;height:100%;background:#8aa7d8;width:var(--progress)}.book-meta{font-size:9px;color:var(--muted);display:flex;justify-content:space-between;margin-top:7px}.rightcol{display:flex;flex-direction:column;gap:23px}.goal{padding:24px}.goal-ring{margin:12px auto 16px;width:142px;height:142px;border-radius:50%;background:conic-gradient(#679483 0 216deg,var(--line) 216deg);padding:9px;transform:rotate(-90deg)}.ring-inner{width:100%;height:100%;border-radius:50%;background:var(--panel);display:flex;flex-direction:column;align-items:center;justify-content:center;transform:rotate(90deg)}.ring-inner strong{font-family:Georgia;font-size:36px;font-weight:400}.ring-inner small{color:var(--muted);font-size:10px;margin-top:5px}.goal-caption{text-align:center;font-size:11px;margin-bottom:18px}.goal-caption span{color:var(--green)}.goal-footer{padding-top:15px;border-top:1px solid var(--line);display:flex;justify-content:space-between;font-size:10px;color:var(--muted)}.goal-footer b{color:var(--text);font-weight:500}.notes{padding:24px}.note{border-left:2px solid #d7bd7e;padding-left:13px;margin:0 0 19px}.note blockquote{font-family:"Songti SC","STSong",serif;font-size:13px;line-height:1.9;margin:0 0 11px}.note small{font-size:9px;color:var(--muted)}.note:last-child{margin-bottom:0}.quote{padding:20px 24px;background:var(--quote);border-radius:12px;position:relative}.quote .eyebrow{font-size:9px;letter-spacing:1.5px}.quote p{font-family:"Songti SC",serif;color:var(--text);font-size:13px;line-height:1.9}.quote small{display:block;text-align:right;margin-top:12px;color:var(--muted);font-size:9px}.footer{display:flex;justify-content:space-between;margin-top:24px;font-size:9px;color:var(--muted)}.empty{padding:40px;text-align:center;color:var(--muted);grid-column:1/-1}.notes-page{display:grid;gap:20px}.notes-page .note{padding:20px;margin:0}.hidden{display:none!important}dialog{border:1px solid var(--line);background:var(--panel);color:var(--text);border-radius:18px;padding:32px;width:520px;max-width:90vw;box-shadow:0 30px 100px #0003}dialog::backdrop{background:#10182877;backdrop-filter:blur(5px)}.dialog-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}.dialog-top h2{margin:0;font-size:18px}.detail-body{display:flex;gap:26px;margin-bottom:22px}.detail-info{flex:1}.detail-info h3{font-size:22px;margin:8px 0}.detail-info p{margin:8px 0}.detail-desc{font-size:13px;line-height:2;color:var(--muted)}#toast{position:fixed;bottom:30px;left:50%;transform:translateX(-50%);background:var(--text);color:var(--panel);padding:13px 23px;border-radius:10px;z-index:30;font-size:12px}.fade{animation:fade .5s ease both}@keyframes fade{from{opacity:0;transform:translateY(7px)}to{opacity:1;transform:translateY(0)}}@media(min-width:1500px){.main{padding-left:55px;padding-right:55px}.columns{grid-template-columns:minmax(0,1fr) 330px}.cover{height:185px;width:130px}.chart{height:160px}}@media(max-width:1150px){.sidebar{width:180px;padding-left:16px;padding-right:16px}.main{margin-left:180px;padding:0 24px 24px}.columns{grid-template-columns:minmax(0,1fr) 250px;gap:16px}.books{gap:12px}.cover{height:140px}.stats{gap:12px}.stat{padding:18px 15px}.shelf,.trend{padding:20px}.searchbox input{width:130px}.welcome-right .demo-tag{display:none}}@media(max-width:900px){.stats{grid-template-columns:repeat(2,1fr)}.sidebar{width:76px;padding:28px 12px}.logo{justify-content:center}.logo>div:last-child,.navlabel,.nav button span,.nav em,.sidebar-bottom{display:none}.nav{margin-top:45px}.nav button{padding:14px;justify-content:center}.main{margin-left:76px;padding:0 22px 24px}.columns{grid-template-columns:1fr}.rightcol{display:grid;grid-template-columns:1fr 1fr}.quote{grid-column:1/-1}.cover{height:165px}.welcome-right .primary{font-size:0}.welcome-right .primary svg{margin:0}.stat-value{font-size:28px}.topbar{height:75px}}@media(max-width:600px){.main{padding:0 16px 20px;margin-left:60px}.sidebar{width:60px;padding:25px 6px}.logo-mark{width:33px;height:33px}.nav button{padding:12px}.searchbox{padding:8px}.searchbox input{width:112px}.key,.breadcrumb,.top-actions>.avatar{display:none}.top-actions{width:100%;justify-content:space-between;gap:8px}.welcome{margin-top:25px}h1{font-size:21px}.welcome p{font-size:10px}.eyebrow{font-size:8px}.stats{grid-template-columns:repeat(2,1fr)}.stat{padding:16px}.stat-value{margin:10px 0}.rightcol{display:flex}.books{grid-template-columns:repeat(2,1fr);gap:24px 15px}.cover{height:160px;width:112px}.section-head h2 small{display:none}.tabs{gap:18px}.footer{font-size:8px}.footer span:last-child{display:none}.detail-body{gap:16px}.detail-info h3{font-size:18px}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation:none!important;transition:none!important}} +/* 阅读目标与内容页:与既有主题变量共用色彩。 */ +:root{--muted:#717b8b}[data-theme=dark]{--muted:#a2adbf}h3{font-size:14px;margin:0}button:disabled{opacity:.45;cursor:not-allowed}.demo-banner{display:flex;justify-content:space-between;color:var(--muted);font-size:10px;margin-top:18px}.welcome{margin-top:26px}.page-actions{display:flex;align-items:center;gap:10px}.secondary{padding:10px 15px;border:1px solid var(--line);background:var(--panel);border-radius:7px;font-size:12px}.pill{display:inline-block;padding:5px 8px;font-size:10px;border-radius:5px;background:var(--soft);color:var(--accent);white-space:nowrap}.pill.green{background:#6a9a8219;color:var(--green)}.pill.amber{background:#c3923919;color:#b78a3f}.hint{font-size:11px;color:var(--muted);line-height:1.8}.panel{padding:24px}.panel+.panel{margin-top:20px}.toolbar{display:flex;gap:10px;align-items:center;justify-content:space-between;flex-wrap:wrap;margin:0 0 22px}.toolbar .tabs{margin:0;flex-wrap:wrap}.toolbar input,.toolbar select,.field input,select{border:1px solid var(--line);border-radius:7px;padding:10px 12px;background:var(--panel);color:var(--text);font:inherit;font-size:12px}.toolbar select{max-width:100%}.field{display:flex;flex-direction:column;gap:8px;font-size:12px;margin-bottom:18px}.field input{width:100%;background:var(--bg)}.date-fields{display:grid;grid-template-columns:1fr 1fr;gap:16px}.metrics-row{display:flex;gap:24px;padding:18px 0;margin-bottom:10px;color:var(--muted);font-size:11px}.metrics-row strong{font-family:Georgia;font-size:28px;color:var(--text);margin-right:6px;font-weight:400}.goals-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px}.goal-card{padding:25px;position:relative;overflow:hidden}.goal-card:before{content:'';position:absolute;inset:0 auto 0 0;width:3px;background:var(--green)}.goal-card h3{font-size:19px;margin:17px 0 8px;word-break:break-word}.goal-card .section-head{margin-bottom:0}.goal-date{font-size:11px;color:var(--muted)}.cover-stack{display:flex;gap:0;margin:24px 0 20px;height:92px;padding-left:4px}.mini-cover{width:66px;height:92px;padding:10px 8px;border-radius:2px 4px 4px 2px;box-shadow:4px 5px 10px #0002;display:flex;flex-direction:column;flex-shrink:0;position:relative;overflow:hidden;border-left:3px solid #0002}.cover-stack .mini-cover{margin-left:-4px;transform:rotate(-3deg);transform-origin:bottom}.cover-stack .mini-cover:nth-child(2n){transform:rotate(4deg)}.mini-cover b{font-family:'Songti SC',serif;font-size:12px;position:relative;z-index:1;line-height:1.5}.mini-cover small{font-size:6px;position:relative;margin-top:auto}.stack-more{align-self:center;margin-left:18px;color:var(--muted);font-size:12px}.goal-progress{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--muted);margin:12px 0 9px}.goal-progress strong{font-size:22px;color:var(--text);font-family:Georgia;font-weight:400}.goal-card .progress{height:5px;margin:0 0 19px}.goal-card .progress i{background:var(--green)}.goal-card-footer{display:flex;align-items:center;justify-content:space-between;border-top:1px solid var(--line);padding-top:16px}.goal-card-footer small{font-size:10px;color:var(--muted)}.goal-card-footer button{font-size:11px;color:var(--accent)}.goal-intro{display:grid;grid-template-columns:1fr auto;gap:28px;background:var(--quote);padding:26px 30px;margin-bottom:24px;border:1px solid var(--line);border-radius:12px}.goal-intro h2{font-family:'Songti SC',serif;font-size:23px;font-weight:500;margin:0 0 10px}.goal-intro p{max-width:500px}.goal-intro .symbol{font-family:Georgia;font-size:65px;align-self:center;color:#819583}.goal-summary-row{display:block;text-align:left;width:100%;padding:16px 0;border-top:1px solid var(--line)}.goal-summary-row strong{font-size:12px;font-weight:500}.goal-summary-row small{display:block;margin-top:7px;color:var(--muted);font-size:10px}.goal-summary-row .progress{margin-top:10px}.list-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px}.list-card{padding:21px;display:block;text-align:left;transition:transform .2s}.list-card:hover{transform:translateY(-3px)}.list-art{height:117px;display:flex;align-items:center;justify-content:center;background:var(--quote);border-radius:7px;margin-bottom:18px}.list-art .mini-cover:nth-child(2){transform:translateY(-5px);margin:0 -4px;z-index:1}.list-card h3{font-size:15px;margin-bottom:8px}.list-card p{font-size:11px}.list-card>small{display:block;color:var(--muted);font-size:10px;margin-top:15px}.full-books{grid-template-columns:repeat(5,minmax(0,1fr));gap:26px}.full-books .cover{height:168px;width:118px}.row-list{display:flex;flex-direction:column}.book-row{display:flex;align-items:center;gap:15px;padding:14px 0;border-bottom:1px solid var(--line)}.book-row:last-child{border:0}.book-row .mini-cover{width:42px;height:58px;padding:5px}.book-row .mini-cover b{font-size:8px}.book-row .mini-cover small{display:none}.book-row-text{flex:1;min-width:0}.book-row-text b{font-size:12px;font-weight:500}.book-row-text p{font-size:10px;margin-top:4px}.book-row .text-button{color:var(--accent)}.book-row-status{font-size:11px;color:var(--green)}.book-row .progress{max-width:200px}.notebook-grid{display:grid;grid-template-columns:230px minmax(0,1fr);gap:24px}.notebook-menu button{display:block;width:100%;text-align:left;border-radius:8px;padding:16px;margin-bottom:7px;border:1px solid transparent}.notebook-menu button.active{background:var(--soft);border-color:var(--line)}.notebook-menu button b{font-size:12px;font-weight:500}.notebook-menu button small{display:block;font-size:10px;margin-top:6px;color:var(--muted)}.note-content{padding:22px;margin:0 0 15px;background:var(--panel);border:1px solid var(--line);border-radius:10px}.note-content blockquote{margin:15px 0;font-family:'Songti SC',serif;font-size:16px;line-height:1.9}.note-content small{color:var(--muted);font-size:10px}.note-original{background:var(--quote);border-left:2px solid #b6a475;padding:12px;font-size:12px;line-height:1.9;margin-top:12px}.detail-tabs{display:flex;gap:19px;border-bottom:1px solid var(--line);margin:25px 0 18px;overflow:auto}.detail-tabs button{white-space:nowrap;font-size:12px;color:var(--muted);padding:0 0 12px;border-bottom:2px solid transparent}.detail-tabs button.active{color:var(--accent);border-color:var(--accent)}#detail{width:760px;max-height:90vh}#goal-editor{width:700px;max-height:90vh}.picker-head{margin:22px 0 10px}.picker-list{max-height:280px;overflow:auto;border:1px solid var(--line);border-radius:8px;margin-top:12px}.pick-row{display:flex;align-items:center;gap:12px;padding:12px 15px;border-bottom:1px solid var(--line);cursor:pointer}.pick-row:last-child{border:0}.pick-row input{accent-color:#356ce5;width:16px;height:16px}.pick-row b{font-size:12px;font-weight:500}.pick-row small{display:block;color:var(--muted);font-size:10px;margin-top:4px}.pick-row .pill{margin-left:auto}.picker-lists{display:flex;gap:7px;flex-wrap:wrap;margin:12px 0}.picker-lists button{border:1px solid var(--line);border-radius:6px;font-size:11px;padding:8px;background:var(--bg)}.picker-lists button.active{background:var(--soft);color:var(--accent)}.editor-footer{display:flex;justify-content:space-between;align-items:center;border-top:1px solid var(--line);padding-top:20px;margin-top:12px}.editor-footer>span{font-size:10px;color:var(--muted)}.form-error{color:#bd5151;font-size:12px;margin:12px 0}.insight-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px;margin-top:22px}.insight h3{margin-bottom:20px}.insight-line{display:flex;justify-content:space-between;gap:12px;padding:10px 0;color:var(--muted);font-size:11px}.insight-line b{color:var(--text);font-weight:500}.insight .progress{margin-top:5px}.section-space{margin-top:24px}.discovery-hero{padding:27px;background:var(--quote);border-radius:10px;margin-bottom:24px}.discovery-hero h2{font-family:'Songti SC',serif;font-size:22px;font-weight:500;margin:0 0 8px}.status-box{text-align:center;padding:65px 20px}.status-box h3{margin:12px}.status-box button{margin-top:18px}.type-chips{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:20px}.type-chips button{padding:8px 11px;font-size:11px;border-radius:7px;border:1px solid var(--line)}.type-chips button.active{background:var(--soft);color:var(--accent);border-color:transparent}.review-row{border-bottom:1px solid var(--line);padding:18px 0}.review-row p{font-size:13px;color:var(--text);margin-top:12px}.review-row .meta{display:flex;align-items:center;justify-content:space-between;font-size:11px;color:var(--muted)}.detail-subtitle{margin:15px 0}.book-details-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;font-size:12px}.book-details-grid span{color:var(--muted)}.chapter-row{padding:14px 0;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;font-size:12px}.media-card{display:flex;gap:18px;align-items:center;padding:24px}.media-icon{width:65px;height:65px;flex-shrink:0;background:var(--soft);color:var(--accent);border-radius:14px;display:grid;place-items:center;font-size:27px}.hour-bars{height:58px;display:flex;align-items:end;gap:3px}.hour-bars i{height:var(--height);flex:1;background:#84a4d8;border-radius:2px 2px 0 0}.insight-detail{margin-top:20px}.insight-detail summary{cursor:pointer;font-size:12px;color:var(--accent);padding:15px 0}.source-tag{font-size:10px;color:var(--muted);margin:8px 0}.goal-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:22px}.detail-empty{padding:35px;color:var(--muted);font-size:12px;text-align:center}.cover .title{flex-shrink:0}.cover .tiny{font-size:5px}.c1 .title,.c3 .title{font-size:17px}.cover .author{font-size:6px}.cover .publisher{font-size:5px}.trend-total small{display:inline-block;font-size:11px}.trend{margin-bottom:0}.shelf{margin-top:22px}.list-index{color:var(--muted);font-size:10px;margin-right:5px}.summary-goals{margin-bottom:0} +@media(max-width:1200px){.full-books{grid-template-columns:repeat(4,minmax(0,1fr))}.list-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.insight-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:900px){.goals-grid{grid-template-columns:1fr}.columns>.rightcol{display:flex}.notebook-grid{grid-template-columns:1fr}.notebook-menu{display:flex;overflow:auto}.notebook-menu button{min-width:160px}.full-books{grid-template-columns:repeat(3,minmax(0,1fr))}.insight-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.sidebar .nav{gap:9px}.welcome{align-items:flex-start;gap:15px}.welcome h1{line-height:1.4}.welcome .primary{white-space:nowrap}.goal-intro{padding:22px}.demo-banner span:last-child{display:none}}@media(max-width:600px){.full-books{grid-template-columns:repeat(2,minmax(0,1fr))}.list-grid,.insight-grid{grid-template-columns:1fr}.goal-intro{grid-template-columns:1fr}.goal-intro .symbol{display:none}.goal-card{padding:20px}.welcome{flex-direction:column}.welcome .page-actions{align-self:flex-end}.panel{padding:18px}.date-fields{grid-template-columns:1fr;gap:0}.detail-body>.cover{width:95px;flex-shrink:0}.detail-body{gap:15px}.detail-info h3{font-size:18px}.book-details-grid{grid-template-columns:1fr}.toolbar input{width:100%}.metrics-row{gap:16px;flex-wrap:wrap}.metrics-row strong{font-size:23px}.goal-intro h2{font-size:20px}.cover-stack .mini-cover{width:57px}.stack-more{font-size:10px;margin-left:10px}.demo-banner{font-size:9px}.goal-card h3{font-size:17px}dialog{padding:20px}.editor-footer{gap:10px}.book-row{gap:9px}.book-row-text b{font-size:11px}} +.compact-value{font-family:Georgia,'PingFang SC',sans-serif;font-size:25px;line-height:1.5;letter-spacing:-.5px}.stat-foot{line-height:1.7}.insight-detail .insight-grid{margin-top:0}.sidebar .nav button[aria-current=page]{color:var(--accent)} +@media(max-width:600px){.compact-value{font-size:20px}.list-art .mini-cover{width:56px}.full-books .cover{width:105px;height:159px}.goal-card .section-head{gap:10px}.book-row .mini-cover{display:none}.detail-tabs{gap:16px}.date-fields input{min-height:40px}.detail-info .primary{font-size:11px;padding:9px}} +.mini-cover.c2,.mini-cover.c3{color:#fff5df} +.export-preview{width:100%;height:320px;margin-top:20px;border:1px solid var(--line);border-radius:8px;padding:18px;background:var(--bg);color:var(--text);font:12px/1.9 'PingFang SC',sans-serif;resize:vertical}