项目进展2026-09-09

MD 阅读 · 功能需求说明书

文档目的:项目经理/开发团队依据本文档,可以完整复刻出一款功能与交互一模一样的 MD 文件阅读器。 范围说明:本文档只描述功能与产品细节(行为、交互、参数、边界情况、验收标准),不限定实现框架与技术选型。 文档版本:v1.0 | 日期:2026-09-04 | 对应实现:已全部实现并通过真机验证


1. 产品概述

1.1 产品定位

一款 Markdown 文件阅读器。核心场景:用户在微信等聊天工具中收到 .md 文件,通过系统"用其他应用打开"选择本应用,即可获得排版精美的预览阅读体验;也可在应用内主动选择本地文件打开。

1.2 核心价值

  1. 打开即读:从微信选"用其他应用打开 → MD阅读",直接进入预览态,零操作成本。
  2. 渲染专业:标题、表格、代码块、流程图(Mermaid)完整渲染,尤其表格支持自适应列宽 + 横向滚动,不出现"异型表"。
  3. 完全离线:渲染过程不发起任何网络请求(含 Mermaid 图),无隐私风险,利于应用商店过审。
  4. 零权限打扰:不申请任何敏感权限,运行全程无权限弹窗。

1.3 应用基本信息

项值
应用名称MD阅读
包名com.bzm.mdreader
目标平台HarmonyOS NEXT 手机(API 12+)
安装包体积≈ 2.8MB(含离线 Mermaid 渲染引擎 2.45MB)

1.4 术语约定(全文统一)

  • 预览:Markdown 渲染后的排版视图(默认视图)。
  • 源码:Markdown 原始文本视图。
  • 全屏:隐藏系统状态栏(时间/电量)的沉浸模式,分两种——应用级全屏(整个页面)与 Mermaid 图全屏查看(单张图)。
  • 主菜单:右上角 ⋮ 按钮弹出的菜单。
  • 块(Block):Markdown 中独立成段的元素(标题/段落/表格/代码块等)。

2. 功能总览

编号功能模块说明优先级
F1文件打开(外部关联)微信"用其他应用打开"接收 .md 文件P0
F2文件打开(应用内选择)应用内主动选择本地 md 文件P0
F3预览渲染全类型 Markdown 块渲染P0
F4表格渲染列宽自适应、横向滚动、同行等高P0
F5Mermaid 图渲染离线渲染流程图/时序图/甘特图等P0
F6Mermaid 全屏查看手势缩放/拖动/双击,缩放上限按文字归一化P0
F7源码视图预览/源码一键切换P0
F8应用全屏隐藏状态栏,返回键先退出全屏P1
F9横竖屏跟随系统重力感应自动切换P1
F10中文文件名支持文件名正确解码显示P0
F11编码自动识别UTF-8 为主,GB18030 自动回退P0
F12大文件防崩溃数百 KB、含 20+ 表格的文档不卡死不闪退P0
F13文件切换正确性已开文件时打开新文件,标题与内容同步切换P0

3. 文件打开功能(F1 / F2)

3.1 外部打开(微信场景,核心链路)

用户旅程:微信收到 .md 文件 → 长按/点开 → "用其他应用打开" → 列表出现 MD阅读 → 选中 → 应用启动并直接显示该文件预览。

产品要求:

  1. 应用必须在系统"打开方式"列表中声明可打开 Markdown 类型的文件(.md / text/markdown / general.markdown)。
  2. 只能声明"打开文件"用途(打开后展示内容),不得声明编辑/保存用途,避免审核争议。
  3. 打开后不得出现任何中间页:直接进入预览态,顶栏标题显示文件名。
  4. 若微信把 .md 误标为纯文本(text/plain)传递,也应尽量兼容打开。

3.2 应用内选择文件

  1. 入口:右上角主菜单 → 打开文件。
  2. 弹出系统文件选择器,筛选 Markdown/文本类文件。
  3. 选中后立即渲染;打开失败(如文件被移动)时给出明确提示,不得白屏、不得崩溃。

3.3 文件名显示规则

  1. 顶栏标题显示文件名(含 .md 后缀)。
  2. 中文及特殊字符文件名必须正确解码显示(如 %E7%A8%8E%E5%8A%A1.md 要显示为 税务.md),不得显示百分号转义串。
  3. 文件名过长时单行省略号截断,不得换行或挤压按钮。

3.4 文件编码识别

按以下顺序读取文件:

  1. 优先按 UTF-8 解码;
  2. 解码失败时自动回退 GB18030(兼容旧 Windows/记事本文档);
  3. 全程不需要用户手动选择编码。

4. 内容解析规则(F3 前置,必须逐条实现)

解析器必须把 Markdown 文本切分为块,并满足以下容错规则(真实用户文档大量不规范写法,每条都来自实测踩坑):

4.1 全局预处理(进入解析前必须完成)

#规则原因(不加会出的问题)
1剔除文件开头的 BOM 字符(U+FEFF)Windows 记事本保存的文件首行标题会失效
2统一换行符:CRLF / CR 全部转 LF微信转发的 Windows 文档行尾残留 \r,会同时破坏"行尾闭合符标题""独立图片行""表格分隔行"的识别
3全角井号 #(U+FF03)转半角 #中文输入法打出的标题无法识别
4全角空格(U+3000)转半角空格全角空格缩进的标题/列表无法识别

4.2 块级元素识别规则(顶层分发)

块类型识别规则容错要求
标题行首 #×1~6允许行首 ≤3 个半角空格缩进;允许井号后无空格(##文字);允许行尾闭合符(## 标题 ##,闭合符需剥离);# 超过 6 个按 6 级处理
分割线行内容为 --- / *** / ___(≥3 个)允许 ≤3 空格缩进;允许行尾空白;不得被并入上一段
引用行首 >允许缩进与 > 后无空格;连续引用行合并为一块
无序列表行首 - / * / + + 空格允许缩进;连续项合并为一块
有序列表行首 数字. + 空格允许缩进;连续项合并为一块
独立图片行整行只有 ![alt](url)允许缩进与行尾空白;不得被并入段落
代码块成对的 ``` 围栏围栏内内容原样保留,不做任何 Markdown 解析
Mermaid 块```mermaid 围栏单独识别为图表块(见 §6),不得当作普通代码块
表格以 `\ 开头的连续行(含分隔行 \
段落其余连续非空行相邻非空行合并为一段

兜底硬性要求:逐行扫描循环必须保证"任何一行至少被消费一次"(即使某行匹配不到任何块规则也强制前进),绝不允许死循环。

4.3 行内(Inline)元素

段落/标题/列表项/引用内需支持:**粗体**、*斜体*、` 行内代码 、链接、~~删除线~~`。行内代码内的 Markdown 符号不解析。


5. 预览渲染规范(F3 / F4)

5.1 基准排版

正文基础字号 16px,基准色 #333。

5.2 标题渲染参数(逐级定义,必须精确一致)

级别字号颜色上间距特殊处理
H126px#00000020px标题下方加分割线
H222px#00000016px标题下方加分割线
H319px#2b2b2b12px—
H417.5px#2b2b2b12px—
H516.5px#2b2b2b12px—
H615.5px#2b2b2b12px—

设计要点:H5/H6 必须与正文 16px 有可辨识差异(曾因 H5=16/H6=15 与正文无差别被误认为"标题没渲染")。

5.3 表格渲染(重点,实测问题最多)

  1. 列宽自适应:列宽按该列最长单元格的"显示宽度"计算;中日韩字符与全角字符按 2 个字符位计,其余按 1。
  2. 列宽公式参数(单一数据源,改一处全局生效):
  • 单字符位宽 7.2px;单元格内边距 16px;防贴边余量系数 1.08;
  • 列最小宽 76px;列最大宽 800px(超宽列靠横向滚动查看,不换行压缩)。
  1. 单元格不换行截断策略:超过 100 字符的内容在计算列宽与行高时同样按 100 截断,保证"高度估算"与"实际渲染"完全一致,表格高度不跳动。
  2. 同行等高:同一行所有单元格必须顶端对齐且背景等高——即使某单元格换行多行,同行其他单元格的浅灰底色也必须拉到同高,不得出现"异型表"。
  3. 横向滚动:整表置于横向滚动容器内;行高在此前按内容预估好,避免滚动时高度二次抖动。
  4. 表头加粗、隔行浅灰底、单元格间 1px 分隔线。

5.4 其他块渲染

  • 代码块:深色底(#f6f8fa 类)、等宽字体、圆角卡片、保留空格缩进、可横向滚动。
  • 引用:左侧 4px 竖线(灰蓝色)+ 浅灰底 + 左内边距。
  • 分割线:细灰线,上下留白。
  • 图片:宽度自适应内容区,保留 alt 占位;加载失败显示占位不崩。
  • 列表:无序用 •,有序用 1. 2. 3.,层级缩进清晰。
  • 段落:行距舒适(建议 1.5 倍),段间距 12px 左右。

5.5 大文件渲染硬性要求(F12)

  1. 必须懒加载渲染:只构建屏幕可视区域的块,滚动时增量创建/回收。实测数据:21KB、含 22 个表格(531 单元格)的文档,若一次性构建全部组件,内存冲到 857MB 并触发 6 秒主线程卡死闪退——懒加载是红线,不是优化项。
  2. 同一文档只解析一次:同一份原文重复进入预览时命中缓存直接复用,严禁"渲染 → 重解析 → 再渲染"的正反馈循环(会导致反复重建直到崩溃)。
  3. 切换新文件时必须整体重建渲染(见 §10),不受缓存影响。
  4. 极端场景已知限制:单个表格内部若有数百行,暂为一次性构建(可接受);单表数千行需另行拆分。

6. Mermaid 图渲染(F5)

6.1 渲染要求

  1. 支持 Mermaid 语法图:流程图、时序图、类图、状态图、甘特图、饼图等。
  2. 必须完全离线:渲染引擎(mermaid.min.js)随安装包内置,任何情况下不得联网加载脚本或字体。
  3. 图渲染后宽度收敛至页面宽度(超宽图等比缩放,不出横向滚动条)。
  4. 渲染完成后自动探测图形实际高度并撑开容器,不得截断图形底部(探测可在渲染后约 700ms 二次确认)。
  5. 渲染失败(语法错误等)降级为代码块展示原文,不得白屏。

6.2 进入全屏的入口

  1. 图区域叠加一个透明点击层,点击整图任意位置进入全屏查看。
  2. 图上叠一个小角标提示(如"全屏查看"),让用户知道可点。

7. Mermaid 全屏查看器(F6,手势细节必须逐条实现)

7.1 布局

  1. 顶部蓝色工具条(#2a6fdb):左侧标题"渲染图",右侧"关闭"按钮。
  2. 图区背景深灰,图居中显示。
  3. 进入全屏时禁止 WebView 自身缩放(zoomAccess=false 一类的设置),缩放完全交给页面手势逻辑,避免双重缩放打架。

7.2 手势规格

手势行为
双指捏合以双指中心缩放
双指/单指拖动平移视图
双击在"适配屏幕"与"适合阅读"两档间切换
松手后缩放值保持不变——严禁松手后回弹/复位(详见 7.3 防误判)

7.3 关键规则:松手不得触发缩放复位(实测踩坑重点)

问题现象:双指缩放松手瞬间,视图被误判为"双击",自动跳到默认档——表现为"放大后又缩回""缩小后又弹回"。

根因:双指松开时两个手指先后抬起,会触发两次"手指抬起"事件;第二次抬起距第一次 <300ms 且位移很小,恰好符合双击判定条件。

必须实现的防误判规则(逐条):

  1. 手势状态机维护 mode:none / pinch(双指)/ pan(拖动)。
  2. 只要抬手指后仍有手指在屏幕上(即双指松开的第一下),直接忽略该事件,不进入任何点击判定。
  3. 只有"屏幕完全空手"时的抬起,才参与双击判定;双击判定窗口 300ms 内、移动距离阈值 12px。
  4. 双击切换目标:若当前缩放 < 阅读档则放大到阅读档,否则回到适配屏幕档。

7.4 缩放上限:按文字大小归一化(产品核心规格)

  1. 每张图渲染后,探测图中主体文字的实际渲染字号(取全部文字节点字号的 60% 分位数——不用中位数,避免边缘小字如连线标签拉低基准;兼容以 DOM 方式渲染的文字标签)。
  2. 缩放上限 = 30px ÷ 该图主体文字字号。
  3. 产品效果(验收标准):任何一张图,无论初始字号多大,放大到极限时主体文字都恰好约 30px,清晰可读,且所有图"放大到最大"的手感一致。
  4. 缩放下限:0.5 倍(可缩到比适配更小)。
  5. 文字为矢量渲染,任何倍率下不糊。

8. 顶部导航与菜单(F7 / F2 入口)

8.1 顶栏布局(预览/源码态)

┌──────────────────────────────────────────┐
│ 文件名.md(左对齐,加粗 18px)      [⋮] │  ← 56px 高
├──────────────────────────────────────────┤
  1. 左侧:标题。未打开文件时显示应用名"MD阅读"。
  2. 右侧:主菜单按钮 ⋮。
  3. 左上角不放置任何菜单按钮(历史上曾有"竖屏/横屏"入口,已废弃,见 §9)。
  4. 按钮热区 48×48px 以上(竖屏、横屏下都易点中),图标字号 30。

8.2 主菜单(右上 ⋮)菜单项与行为

菜单项行为
预览切到渲染视图(当前已在预览态时显示勾选态或不可点,按实现自定,两态互斥)
源码切到纯文本视图,等宽字体,保留原文全部字符
全屏进入应用级全屏(§9.2),菜单态保持当前预览/源码不变
打开文件弹出系统文件选择器(§3.2)

8.3 视图切换要求

预览 ↔ 源码切换必须瞬时完成(同文档已解析过,直接复用),不得出现白屏或重复解析。


9. 全屏与屏幕方向(F8 / F9)

9.1 屏幕方向

  1. 完全跟随系统重力感应自动旋转,应用不锁定方向。
  2. 应用内不提供任何手动"竖屏/横屏"切换按钮(曾在左上角 ⋮ 菜单内提供,已按需求移除)。
  3. 横屏下顶栏、菜单、按钮布局必须正常可用(按钮 48×48 热区即为此设计)。

9.2 应用级全屏

  1. 进入:主菜单 → 全屏。
  2. 效果:隐藏系统状态栏(时间/电量/信号),内容区域扩展至屏幕顶部。
  3. 退出:按系统返回键退出全屏而不是退出应用(返回键必须先消耗在退出全屏上);也可从菜单再次操作退出。
  4. 全屏态下主菜单按钮仍然可用(悬浮于顶栏)。

9.3 Mermaid 全屏的返回键

Mermaid 全屏查看态下按返回键:退出图全屏,而非退出应用或退出应用全屏。


10. 文件切换与多文件场景(F13,实测踩坑重点)

场景:应用已打开文件 A → 用户切到微信 → 再用"其他应用打开"文件 B。

必须实现的产品行为:

  1. 不新开应用实例,复用已运行的应用。
  2. 顶栏标题立即变为 B 的文件名。
  3. 内容区立即且完整切换为 B 的内容——严禁出现"标题是 B、内容还是 A"的错位(这是已修复的高危 bug:视图复用机制在两文档块数相近时会错误复用旧内容,实现时必须保证换文件 = 视图整体重建,例如以"文档版本号"参与每个渲染单元的唯一标识)。
  4. 切换后滚动位置回到顶部。
  5. 若 A/B 内容恰好完全相同,也必须正常显示(不因内容相同跳过刷新而白屏)。

11. 权限与隐私合规

  1. 不申请任何敏感权限:无网络、无存储批量扫描、无位置、无通讯录;用户全程看不到权限弹窗。
  2. 文件访问仅限"用户主动选择打开的文件"与系统分享传入的文件,属免权限场景。
  3. 渲染全程离线,无任何埋点/上报/网络请求。
  4. 上述是应用商店审核友好性的核心卖点,任何功能增强不得引入联网需求。

12. 边界情况与容错清单(验收必测)

#场景预期
E1带中文文件名的 md 文件标题正确显示中文名
E2GB18030 编码的旧文档正常显示,无乱码
E3带 BOM 的文件,首行是 # 标题首行标题正常渲染
E4CRLF 换行的 Windows 文档全部块级语法正常识别
E5全角 #标题、全角空格缩进标题正常渲染为标题
E6##文字(井号后无空格)正常渲染
E7## 标题 ##(闭合井号)渲染为 H2,闭合符不显示
E8行首有 1~3 空格的标题/列表/引用/分割线正常识别
E921KB / 22 表格 / 531 单元格大文档秒开、滚动流畅、内存正常(<200MB)、不闪退
E10语法错误的 mermaid 块降级显示源码,不崩溃
E11双指缩放松手缩放值保持,不回弹
E12已开 A 文件再打开 B 文件标题与内容同步切换(§10)
E13全屏态按返回键先退全屏,不退出应用
E14超宽表格(某列 800px 上限)横向滚动可看全,不换行压扁
E15打开被删除/无法读取的文件明确提示,不崩溃
E16横屏使用所有功能布局正常、按钮可点

13. 验收标准汇总

  1. F1~F13 全部功能按本文档参数实现,§12 的 16 条边界用例全部通过。
  2. 性能红线:含 20+ 表格的真实大文档(如 21KB/22 表)打开 ≤3 秒、滚动无卡顿、无内存异常增长。
  3. 交互红线:① 缩放松手不回弹;② 换文件内容同步刷新;③ 返回键先退全屏。
  4. 合规红线:全程离线、零敏感权限、零权限弹窗。

14. 附:与历史需求的差异记录(避免复刻时走弯路)

曾经的设计现行设计变更原因
左上角 ⟳ 图标切横竖屏删除,改系统重力感应简化交互
左上 ⋮ 菜单放竖屏/横屏左上角不放任何按钮同上
右上 ⋮ 含横竖屏项右上 ⋮ 仅:预览/源码/全屏/打开文件职责单一
缩放上限固定倍率按文字 30px 归一化不同图手感统一
「富文本/查看原文」统一叫「预览/源码」术语规范
Mermaid 全屏用 WebView 原生缩放页面手势接管(禁用原生缩放)避免双重缩放打架
← 返回文章列表