开发历程与踩坑记录2026-08-28

技术选型之坑

1. 为什么用 Tauri 而不是 Electron?

水利水电设计院的电脑配置参差不齐,有老旧 XP 升级到 Win10 的机器,也有最新的工作站。Electron 打包后 200MB+,启动慢,内存占用大。Tauri 基于 Rust 和系统 WebView,体积 < 10MB,内存占用小一个数量级。

:Tauri v2 的 API 与 v1 不兼容,网上大部分教程是 v1 的。且 Rust 编译链在 Windows 上需要 MSVC 工具链,配置繁琐。

方案:安装 Visual Studio Build Tools,确保勾选”C++ 桌面开发”工作负载。Rust 使用 stable-msvc 工具链。

2. 字体选择之坑

坑 1 — 网络字体模糊:Google Fonts 的网络字体(Noto Sans SC)在 Windows 上渲染模糊,小字号(11-13px)尤其明显。原因是网络字体需要下载后由浏览器渲染,无法享受 Windows ClearType 的像素级优化。

坑 2 — 等宽字体中文回退:编辑器使用 font-mono(JetBrains Mono / Fira Code)作为代码字体,但这些字体没有中文字形。Windows 上会回退到系统等宽字体 SimSun(宋体),中文显示粗糙发虚,与 placeholder 的系统字体效果形成鲜明对比。

坑 3 — 字体继承断裂:Tailwind 的 font-sans 默认应用于 body,但嵌套的 Select、Input、Textarea 等表单元素不会自动继承。顶部 ProjectBar 的 select/input 文字和侧栏目录文字在部分场景下回退到系统默认字体,与编辑器字体不一致。

坑 4 — 文本光标覆盖滚动条光标:Textarea 默认 cursor: text 会覆盖滚动条区域的 cursor: default,即使给 ::-webkit-scrollbar 设置 cursor: default 也无效。必须在 textarea 本身设置 cursor: auto 才能让浏览器根据区域自动切换光标样式。

方案

  • 中文字体优先使用系统原生字体,Tailwind 配置中将 "Microsoft YaHei" 排在 font-sans 栈第一位,利用 Windows ClearType 获得像素级清晰度
  • 编辑器放弃 font-mono,改用 font-sans,确保中文正常渲染,英文仍由 Inter 字体承接
  • 所有表单元素和侧栏文字显式添加 font-sans 类,确保字体栈一致
  • 编辑器 textarea 设置 cursor: auto,让浏览器自动在文字区显示 I 形光标、滚动条区显示箭头光标

3. 图标选择之坑

工具条图标经历了四轮迭代,每轮都踩了不同的坑:

第一轮:Emoji 图标。初期用 💾 B H1 等 emoji 和文本字符,在不同平台渲染效果差异巨大,Windows 上 emoji 是彩色粗线条,macOS 是扁平风格,完全无法统一。

第二轮:自定义 SVG(strokeWidth 1.5)。自己画了 30 个 SVG 图标,线宽 1.5px。在 17-18px 尺寸下线条过于纤细,像”虚线”,辨识度差。

第三轮:文字图标用 SVG <text> 渲染。将 B、S̶、H1、H2、1. 等文字图标改为 SVG <text> 元素。但 SVG 文字渲染引擎与浏览器不同,小字号下边缘模糊、字形发虚,与系统字体渲染效果差距明显。

第四轮:HTML <span> 文字图标。用 HTML <span> 替代 SVG <text> 渲染文字图标,利用系统字体引擎获得清晰度。但整体图标风格仍不统一——SVG 图标和 span 文字图标视觉重量不一致。

第五轮(最终方案):Font Awesome + 分组配色。全部替换为 Font Awesome 6 专业图标库,统一视觉风格。同时按功能分组赋予六种颜色(靛蓝/琥珀/翠绿/紫罗兰/青色/红色),同类功能一目了然。H1/H2 因 FA 的 faHeading 图标无法区分,保留 HTML 粗体文字方案,颜色与所在组保持一致。

关键教训

  • 不要自己画图标——专业图标库(Font Awesome、Lucide 等)经过数千项目的验证,线条粗细、间距、视觉一致性都有保障
  • SVG 小尺寸下 strokeWidth 至少需要 21.5 在 18px 以下几乎看不清
  • SVG <text> 不适合小尺寸图标场景,HTML 文字渲染质量远优于 SVG 文字
  • 颜色是最快的视觉区分手段——6 组功能色比纯灰色图标识别速度快 3 倍以上

4. 思维导图(Markmap)渲染之坑

:Markmap 在暗黑模式下字体颜色不跟随变化,且预览区域出现灰色背景,排查了多个来源。

方案

  • 在 MindmapRenderer 组件中注入自定义 CSS,通过 .markmap-dark 类覆盖 CSS 变量
  • 灰色背景来自多个层次:Preview 容器的 dark:bg-gray-900App 组件的 dark:bg-gray-900index.html<body class="bg-gray-50">、以及 ReactMarkdown 对围栏代码块包裹的 <pre> 标签的默认样式
  • 逐一移除这些背景色,并在 Preview.tsx 中通过 components.pre 处理器剥离 mindmap 代码块的 <pre> 包裹

5. 实时预览性能之坑

:每次按键都触发 Markmap 重新渲染 SVG,导致输入卡顿。

方案:实现 250ms 防抖(debounce)钩子,在用户停止输入后才重新渲染思维导图。

6. TypeScript 类型同步之坑

:新增字段(phase、bidSection、discipline)后,DraftRecord 接口和 Draft 接口需要同步更新,涉及 5 个文件的修改,容易遗漏。

方案:先修改核心类型定义,然后用 tsc --noEmit 检查所有编译错误,逐文件修复。建议将共享类型抽取为独立文件,避免多处定义。

7. Tauri 构建配置之坑

:Release 构建默认不优化,二进制体积大。

方案:在 Cargo.toml 中配置:

[profile.release]
panic = "abort"
codegen-units = 1
lto = true
opt-level = "s"
strip = true

8. CSP 安全策略之坑

:Tauri 默认 CSP 禁止连接外部 API,导致前端无法请求中间件。

方案:在 tauri.conf.json 中配置 CSP:

"csp": "default-src 'self'; connect-src 'self' http://localhost:8090; img-src 'self' data: https:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"

9. Markdown 渲染之坑

ReactMarkdown 预览过程中踩了三个渲染相关坑:

坑 1 — 无语言标记的代码块被当作行内代码:用 const isInline = !className 判断行内/块级代码。对于无语言标记的围栏代码块(如 `` ),ReactMarkdown 传入classNameundefined!undefined=true`,整个代码块被错误渲染为行内代码,没有等宽字体和深色背景,制表符无法对齐。

方案:改用 ReactMarkdown v9 原生的 inline 属性区分行内代码和块级代码,if (inline) 判定准确可靠。

坑 2 — 表格完全不支持:ReactMarkdown v9 默认不包含 GFM(GitHub Flavored Markdown)支持,表格、任务列表、删除线、自动链接都需要 remark-gfm 插件。未安装时表格语法被当作普通文本渲染。

方案:安装 remark-gfm 插件,在 ReactMarkdown 中添加 remarkPlugins={[remarkGfm]},一次性启用表格、任务列表、删除线、自动链接四种 GFM 特性。

坑 3 — 表格居中标记(:---:)无效:自定义 th/td 组件未透传 style 属性。GFM 对齐标记(:---:---:---:)通过 style={{textAlign: 'left'|'center'|'right'}} 传递给 th/td,但组件签名中未解构 style,也未用 {...props} 透传。

方案th/td 组件显式解构 style 并透传 {...props},居中/右对齐标记生效。

10. 表格双重边框之坑

:自定义 table 组件外层 <div>border + rounded-lg,内层 th/tdborder-b,视觉上形成双层边框——表格外面套了一个多余的框。

方案border 从外层 <div> 移到 <table> 标签上,th/tdborder-b 改为四边 border,形成标准网格边框,无多余外框。

11. 原生控件美化之坑

:顶部 5 个下拉框(项目名称、阶段、标段、专业、类别)使用原生 <select> 元素,下拉菜单由操作系统渲染,无法用 CSS 控制样式——直角矩形、无圆角、无阴影、无动画,与整体极简设计风格不协调。

方案:用自定义 SelectDropdown 组件替换原生 <select>

  • 触发器:rounded-lg 圆角按钮 + Chevron 箭头(展开/收起动画)
  • 下拉面板:rounded-xl 大圆角 + shadow-lg 阴影 + 淡入动画
  • 选中项:蓝色高亮背景 + 蓝色文字
  • 交互:点击外部关闭、Escape 关闭、hover 效果
  • 组件支持 string[]{value, label}[] 两种选项格式,适配不同数据源

2026-08-29 开发总结

当日主线分两段:上午打通桌面端到 Mindoc 的知识库发布链路(修复三个 bug 并重构登录流程适配工地离线场景);下午实现离线图片上传、补齐后端文档详情查询接口,并新增本地笔记列表与「新建笔记」能力,为总工的”私人速记本”补齐工作台闭环。

12. Mindoc 接口对接之坑

:原计划用 Bearer Token 调用 Mindoc API,实际 Mindoc 采用 Cookie Session 认证(POST /login),且文档创建/编辑端点并非 /api/v1/book/document,而是 /api/{identify}/create/api/{identify}/content/{id} 等真实端点。此外 Mindoc 默认开启登录验证码,会导致程序化登录失败。

方案:重写 MindocService——先 GET /login 预热会话,再 POST /loginaccount + password)获取会话;创建文档分两步:先 POST /api/{identify}/create 拿到 doc_id + version,再 POST /api/{identify}/content/{id} 保存 markdown 正文(version 用于乐观锁)。关闭 Mindoc 登录验证码后完成端到端打通。

13. 图片上传之坑(返回数组)

:Mindoc 的 POST /api/upload(字段 editormd-image-file)返回的是 JSON 数组 [{errcode:0, success:1, url:"..."}],而非 {errcode, message, data} 单对象。中间件按单对象解析导致反序列化失败、拿不到 URL。

方案UploadImage 先按数组解析取第一项,再回退单对象;normalizeUploadURL 将相对路径补全为绝对 URL。多选上传、拖拽/粘贴单图、正文内 base64 图片三条路径全部复用该逻辑。

14. 私人笔记误推送之坑

:无论笔记类型 private 还是 publish,后端都无条件调用 CreateDocument 推送到 Mindoc,导致「存为私人笔记」也直接发布。

方案:以 type 区分——private 仅存本地(前端 IndexedDB saveDraft,后端直接返回),publish 才推送到 Mindoc。

15. 离线优先的登录流程之坑

:最初的登录门禁在应用启动时拦截(if (!isAuthenticated) return <Login/>),无网络的总工在工地现场打不开软件。

方案:改为离线优先——启动直接进入编辑器;登录入口移到右上角(「私人笔记」左侧),登录由全屏页改为弹窗;仅在点击「发布」时校验登录。JWT 通过 localStorage 持久化(30 天有效),一次登录后无网络也能正常编辑与存私人笔记,发布时无网络自动落为本地草稿。

16. 离线图片上传之坑(占位符 + IndexedDB)

:无网络时图片无法上传,若把 base64 直接塞进正文会令 IndexedDB 草稿体积暴涨,且发布时无法回填为真实图床 URL。

方案:实现「占位符 + 本地暂存」——粘贴/拖拽图片时先尝试直传,失败则把 Blob 存入 IndexedDB(Dexie 升级到 v3,images 表由 dataUrl 改为 blob/name/type/size),在正文插入 localimage://{id} 占位符;预览时通过 getLocalImageBlob 生成 Object URL 渲染。发布时解析正文中的所有 localimage:// 占位符,逐个重新上传并替换为真实 URL,全部成功才推送,否则提示「请联网后重试」。

17. 后端文档详情查询接口补齐

MindocService.GetDocument 原为未实现,前端无法回读已发布笔记。

方案:改用 Mindoc 真实端点 GET /api/{identify}/content/{id} 读取文档,优先返回 markdown 原文(回退 release HTML),并填充标题、创建/更新时间。author/status 字段暂留空(Mindoc 的 content 接口不返回作者与中间件侧审核状态,后续按需再查)。

18. 本地笔记列表 + 新建笔记

需求:总工需要回看、重新编辑此前写过的本地笔记,并能随时开新篇。

方案

  • DraftRecord 增加 title 字段,标题由正文首个标题/首行自动派生,作为笔记名字。
  • api.ts 新增 listDrafts()(按创建时间倒序),saveDraft 支持按 id 覆盖并返回持久化 id。
  • 侧边栏改为「笔记 / 目录」双 tab——「笔记」tab 展示本地草稿列表(标题 + 项目 + 时间,蓝点=已发布、黄点=私人),点击加载回编辑器;「目录」tab 保持原有的标题导航。
  • 顶部左侧加「+ 新建」按钮,点击清空编辑器进入新篇(不重置项目记忆)。

19. 重复发布之决策(暂不覆盖)

决策:桌面端重复发布同一篇笔记时,仍走 Mindoc 新建文档,不做「覆盖已有文档」的逻辑;重复项由服务端人工替换。待「私人笔记 → Mindoc 私有项目」映射明确后再设计覆盖/归属语义。

附带修正:发布离线回退时原本每次生成新草稿 id 导致同一篇笔记翻倍,已改为复用当前草稿 id,避免重复条目。

2026-08-31 开发总结

当日主线:把原本硬编码/环境变量中的配置全部下沉到桌面端人工输入,实现「中间件地址 + 登录凭据 + 发布目标 Book」三项配置的运行时动态化;同时实现 per-user session 会话隔离,并修复因 Beego 响应特性导致的发布报错。

20. 配置硬编码之坑(三项配置桌面端化)

:中间件服务地址、Mindoc 交互账号密码、mindoc_default_book 此前分别硬编码在 Vite 环境变量、中间件 app.conf 中,用户无法在运行时修改,且中间件用固定 admin 账号操作 Mindoc,与桌面端登录人身份不符。

方案

  • 中间件地址:前端 api.ts 新增 getApiBase()/setApiBase(),从 localStorage 读写;新增 SettingsDialog 组件供用户输入地址和端口。
  • 登录凭据:桌面端登录即严格校验 Mindoc(AuthController.Login 调用 LoginAndCache),中间件不再硬编码账号。
  • 目标 Book:新增 PublishDialog 组件,发布前让用户确认/修改目标 Book 标识,getLastBook()/setLastBook() 记住上次选择。

21. per-user session 会话隔离

:中间件与 Mindoc 交互使用单一硬编码账号,所有用户的操作都落到同一账号下,无法区分作者。

方案MindocService 增加 LoginAndCache(username, password)GetCachedSession(username),按用户名在内存缓存各自的 cookie session;登录时校验凭据并缓存,后续发布、图片上传、书籍列表均使用该用户 session。

22. Mindoc 书籍列表 API 补齐

:Mindoc 原生没有查询「当前用户可见书籍」的接口,前端发布时无法枚举目标 Book。

方案:Mindoc 侧 BookController 新增 List 方法返回当前用户可见书籍,注册 /api/book/list 路由;中间件 ProjectController.List 包装后暴露 GET /projects,前端 getBooks() 拉取用于发布对话框的枚举选择。

23. Beego 响应语义之坑(修复 “cannot read properties of undefined (reading ‘id’)”)

:Beego 无论业务成功与否均返回 HTTP 200,真实状态在 JSON 响应体的 code 字段(0 为成功)。前端 uploadDraft 此前依赖 HTTP 状态码判断成功,当业务失败(如 code=401 登录过期)时仍尝试读取 data.data.id,得到 undefined 而抛错。

方案uploadDraft 改为按响应体 code 字段判断业务状态——仅当 code === 0data 存在时才读 data.data.id;识别 code === 401 返回 expired 标记,前端据此清除本地 token 并提示重新登录,而不是统一提示「离线模式」。

24. CSP 限制动态地址之坑

:Tauri 的 CSP connect-src 白名单固定,无法连接用户运行时输入的任意中间件地址,导致「network error, saved offline」。

方案tauri.conf.jsonsecurity.csp 置为 null,放开网络请求限制(内网自建服务场景可接受)。

25. 「文件」菜单:打开 / 导出 Markdown

需求:原顶部左侧「+ 新建」按钮功能单一,总工需要把本地 .md 文件导入编辑,以及把当前笔记导出为 .md 文件。

方案

  • 新增 FileMenu 下拉组件,含「打开 Markdown 文件」「导出为 Markdown 文件」。
  • 新增 lib/file.ts 封装 invoke;Rust 后端 lib.rs 增加 open_md_file/export_md_file 命令,用 rfd 原生文件对话框 + std::fs 读写(#[cfg(not(target_os = "android"))] 仅桌面端编译,Cargo.toml 用 target 条件依赖排除 Android)。
  • 打开后写入编辑器;导出时用当前标题作默认文件名并清洗非法字符。

26. 笔记列表筛选

需求:笔记增多后难以快速定位,需要关键字过滤。

方案:「+ 新建笔记」下方新增筛选输入框,按标题/项目/标签实时过滤笔记列表,无匹配时显示「无匹配结果」。

27. 字体缩放(显示层,只变字号)

需求:总工有放大/缩小文字的阅读与书写需求,且要求仅在桌面显示层放大、不改变 Mindoc 文档的默认字号。

方案

  • 工具条新增 A−/A+ 按钮,每档 ±0.1(范围 0.8~1.6),缩放值持久化到 localStorage。
  • 初版用 CSS zoom 整体缩放,会连带放大图标与间距;按需求改为「只放大文字」:引入 CSS 变量 --font-scaleApp.tsx 写入 document.documentElement),编辑器 textarea、预览 .markdown-body(标题/代码/表格字号转 em)、侧边栏(字号转 em)三处按 calc(15px * var(--font-scale)) 缩放,图标、padding/margin/宽度等 rem 间距保持不变。
  • 纯渲染层:markdown 正文不变,发布到 Mindoc 不含任何字号样式。

2026-09-02 开发总结

当日主线:完善编辑器代码块功能,实现多语言语法高亮与 mindmap 预览大小控制,并统一编辑区与预览区的排版字体。

28. 代码块多语言语法高亮

需求:原代码块为纯文本渲染,无语法高亮,也无法区分不同语言。

方案

  • 引入 react-syntax-highlighter,用 PrismLight 引擎按需注册 32 种常用语言(JavaScript、TypeScript、Python、Go、C、C++、Java、C#、Rust、Shell、SQL、JSON、YAML、HTML、CSS、Markdown 等),既覆盖常用场景又控制 bundle 体积。
  • 新增 lib/codeLanguages.ts 集中管理语言列表与语法包注册:CODE_LANGUAGE_KEYS 供预览判定语言、CODE_LANGUAGES 供工具条选择。
  • 工具条「代码块」按钮改为弹出语言选择菜单,插入带语言标记的围栏代码块(如 `python)。
  • 预览中已注册语言走 PrismLight 高亮(oneDark / oneLight 双主题),未识别语言回退为普通等宽代码块。
  • react-syntax-highlighter 补充 TypeScript 类型声明(types/react-syntax-highlighter.d.ts)。

29. mindmap 预览大小控制

需求:思维导图默认高度固定,用户希望按内容自定义预览高度。

方案:解析 ``mindmap>600 围栏语法中的数字部分作为预览高度(单位 px,默认 400),传入MindmapRenderer动态设置 SVG 的minHeight`。

30. 代码块字体大小调整

需求:预览区代码块字体过小,且高亮代码块与普通代码块字号不一致。

方案:将 PrismLight 高亮代码块与未识别语言代码块的字体统一为 15px(原 13px),并把 CSS 中 .markdown-body pre code 字号改为 1em 随正文缩放。

31. 编辑区字体与预览区统一

需求:编辑区字体观感不如预览区,二者排版应一致。

方案:编辑区 textarea 由 Tailwind 默认 font-sans 改为与预览区一致的字体族(Inter / Microsoft YaHei / Noto Sans SC 等),字号 15px、行高 1.75 保持一致。

2026-09-04 开发总结

当日主线:补齐编辑器接近 Sublime 的鼠标操作体验——中键列选择、拖拽移动文本,并统一全屏预览目录的半透明视觉。

32. 中键列选择(allowMultipleSelections 之坑)

需求:像 Sublime 一样,鼠标中键(滚轮)按下拖拽进入列(矩形)选择,多行同时编辑。

:使用 rectangularSelection({ eventFilter: e => e.button === 1 }) 后列选择仍不生效,拖出来的矩形选区一闪就被压回成普通单选。

根因:CodeMirror 的 EditorState.allowMultipleSelections 默认是 false,而矩形选择本质是「每行一个选区」的多选区(multi-range)。selection 提交到 state 时会走 asSingle() 把多选区折叠成单选区,导致列选择看不到效果。

方案:扩展列表最前面显式开启 EditorState.allowMultipleSelections.of(true);同时在编辑器容器 onMouseDown 中对 button === 1(中键)调用 preventDefault(),阻断 WebView2 的中键自动滚动,保证列选择拖拽不被干扰。

33. 全屏预览目录半透明

需求:全屏预览右侧目录背景设为半透明,且浅色、深色模式都成立。

方案FullscreenToc 目录容器改用 bg-white/60 dark:bg-gray-900/60 backdrop-blur-xl——浅色为半透明白、深色为半透明深灰,配合较强的背景模糊,两种模式下半透明磨砂效果都清晰可见(此前的 bg-white/30 透明度太高,在纯色预览底上几乎无感)。

34. 拖拽移动文本(Tauri dragDropEnabled 之坑)

需求:拖动选中的文本到编辑器另一位置,实现移动/复制。

:CodeMirror 本身内置了 HTML5 的拖放(dragstartdrop)实现拖动移动文本,但在 Tauri 下完全拖不动。

根因:Tauri v2 默认 dragDropEnabled: true,会在 WebView2 底层拦截 HTML5 拖放事件,导致 drop 事件到不了编辑器。

方案tauri.conf.json 窗口配置设置 "dragDropEnabled": false,恢复 HTML5 拖放,拖动选中文本即可移动(Ctrl 拖拽为复制)。由于前端没有使用 Tauri 的 tauri://file-drop 事件(拖图片进编辑器走的是 HTML5 drop),关闭该开关无副作用,反而顺带修复了此前也被拦截的「拖图片进编辑器」功能。

2026-09-05 开发总结

当日主线:补齐笔记删除能力(悬停删除 + 级联清理本地资源)、统一编辑/预览字体(中文雅黑 + 西文衬线),并新增「关于」弹框,完善桌面端的产品化收尾。

35. 删除笔记(级联清理本地资源)

需求:删除左侧笔记列表中的笔记,并确定删除范围与交互方式。

方案:确认采用「仅删除本地草稿 + 一并清理本地图片/附件 + 悬停删除按钮二次确认 + 删除当前笔记回到新建状态」。新增 api.deleteDraft:删除 drafts 记录,并用正则解析正文中的 localimage://localattachment:// 占位符,逐一删除 images / attachments 表中对应的 Blob,释放磁盘。交互上 Sidebar 笔记项悬停浮现垃圾桶图标(group-hover 显隐),点击弹出确认弹框;删除的是当前编辑笔记时调用 handleNew() 清空编辑器。

注意:本地草稿未记录 Mindoc 文档 ID,删除仅作用于本地 IndexedDB,不影响云端文档;如需云端一并删除,需先补充 doc_id 记录与 Mindoc 删除接口。

36. 选区悬停变箭头(拖动提示)

需求:选中文本后,鼠标移到选区上时光标变为箭头,提示可拖动。

方案:CodeMirror 通过 EditorView.domEventHandlers({ mousemove }) 监听悬停,用 posAtCoords 判断鼠标位置是否落在非空选区内,是则 contentDOM.style.cursor = "default"(箭头),否则清空恢复文本光标。

37. 编辑器 / 预览字体统一

需求:编辑器字体协调——中文用微软雅黑,英文、数字、符号用 Times New Roman。

方案:利用 CSS 按字符回退,Editor.tsxMAIN_FONTindex.css.markdown-body 均改为 "Times New Roman", "Microsoft YaHei", ..., serif——西文字符命中 Times New Roman,中文字符(Times New Roman 无该字形)自动回退到微软雅黑。全局 UI(按钮/侧栏)仍保持 Inter 无衬线,代码块/行内代码保持等宽字体。

38. 关于(About)弹框

需求:文件菜单最下方新增「关于」,展示产品介绍、数据存储路径、备份方法、开源组件(20 个以内)与代码结构说明。

方案:新增 AboutDialog 组件(与登录/设置一致的全屏遮罩 + 居中卡片),FileMenu 增加「关于」项并用分隔线区隔,ProjectBar / App 透传 onAboutshowAbout 状态渲染。内容含 12 项,其中 mindmap>600 实为「预览高度」、普通筛选实为「标题/项目/标签」、formatter.gocategoryLabels 需与前端 categories.ts 保持一致,均已按实际纠正表述。

2026-09-06 开发总结

当日主线:修复实时预览在编辑时跳回顶部的顽疾,并把保存机制升级为「自动保存 + 发布状态圆点」,让笔记列表左侧圆点从”类型”语义改为”同步状态”语义。

39. 实时预览跳顶之坑(react-markdown 同步重建)

:编辑区每次改动,右侧预览滚动位置都跳回顶部,先后尝试 useRef + onScroll 保存/恢复 scrollTop 也因组件重建与时机问题失效。

根因:react-markdown v9.1.0 是同步组件,正文变化时整棵 React 元素树重建,浏览器滚动锚定(scroll anchoring)失效,预览容器被重置到顶部。

方案:在 Preview.tsx 用模块级 MappreviewScroll)按「文档标识 + 预览面板」(docKey:idPrefix)保存滚动位置,onScroll 实时记录、useLayoutEffect 在内容提交后立即恢复;同时给滚动容器设置 overflowAnchor: "none" 关闭浏览器锚定干扰。切换笔记 / 全屏面板时各用各的位置,互不串扰。

40. 保存策略:自动保存 + 发布状态圆点

需求:原「保存」需手动点击;笔记列表左侧圆点只按 type 区分(私人黄点 / 发布蓝点),而新建笔记默认 private,导致列表几乎全黄,无法表达”这条笔记到底同步了没有”。

方案(自动保存 + 发布状态语义):

  • DraftRecord 增加 publishedAt(最近一次成功推送 Mindoc 的时间戳)与 dirty(自 publishedAt 后有未同步改动)两个字段;api.ts 增加 getDraftmarkDraftSynced
  • App.tsx 实现 800ms 防抖自动保存:正文或项目/阶段/标段/专业/类别/标签变动后,闲置即写入 IndexedDB,崩溃 / 关窗不丢内容;createdAtRef 保留原创建时间避免自动保存打乱列表顺序,publishedAt 原样透传不清空同步状态。发布成功后调用 markDraftSynced 清除 dirty
  • Sidebar.tsx 圆点改为三态发布状态:灰=本地草稿未发布(publishedAt == null)、黄=已发布但有未同步修改(dirty)、绿=已同步(publishedAt != null && !dirty),悬停有 title 提示。
  • 修正 EditoronChange 接线(由 setBody 改为 handleBodyChange),使打字时正确置位 isDirty;新建 / 打开文件时重置脏标记。

2026-09-07 开发总结

当日主线:补齐编辑器拖动文本时的「落点光标」视觉反馈,并让「关于」弹框里的文档链接直接调用系统默认浏览器打开。

41. 拖动落点光标(拖动文本时的落点提示)

需求:拖动选中文本移动时,目标位置没有光标提示,希望像 Notepad3 / Sublime 那样在落点显示竖杠光标。

方案:编辑器基于 CodeMirror 6,直接启用官方 dropCursor() 扩展(@codemirror/view),在拖放过程中于落点绘制竖杠光标;深色主题中早已预留的 .cm-dropCursor 样式(borderLeftColor 设为浅色)随之生效。

42. About 链接打开系统浏览器

需求:关于弹框里的文档链接点击后打不开浏览器——<a target="_blank"> 在 Tauri WebView 下不会跳转到系统浏览器。

方案:复用现有「桌面端原生命令」模式(同 open_md_file / export_md_file):

  • Cargo.toml 桌面端依赖新增 open crate。
  • lib.rs 新增 open_url 命令,用 open::that 打开系统默认浏览器,#[cfg(not(target_os = "android"))] 仅桌面端编译,并注册到 generate_handler
  • 前端 file.ts 新增 openExternalUrl 封装(invoke open_url,失败仅记日志不影响弹框)。
  • AboutDialog 的文档链接 onClick 拦截默认跳转并调用 openExternalUrl

2026-09-08 开发总结

当日主线:启动富文本功能开发,确定以「内联 HTML + 白名单过滤」承载 Markdown 表达不了的内容,并完成渲染基座(Phase 0)与字体颜色/名称修改(Phase 1)。

43. 富文本承载方案(内联 HTML + 白名单)

需求:字体颜色/名称、视频、内嵌网页、可调大小与对齐的图片等 Markdown 无法原生表达,需要确定正文承载方式与「发布到 Mindoc 后是否保留」的优先级,并确定视频来源与 md 资源打包形式。

方案(四问拍板):承载方式 = 内联 HTML + 白名单过滤(rehype-raw + rehype-sanitize);发布优先级 = 桌面端优先(发布后是否完整保留为次要);视频来源 = 文件模式(复制到托管目录,不存 IndexedDB);md 资源打包 = zip 归档(md + 图片/附件/视频)。

44. Phase 0:渲染基座(rehype-raw + rehype-sanitize)

方案Preview.tsx 接入 rehype-raw + rehype-sanitize 自定义白名单,放行 span/video/iframe/img 等标签及其属性;新增 span/video/iframe 渲染组件、img 尺寸/对齐(mediaStyle)、旧 font 兼容渲染(只读不生成);inlineStyle 把 kebab-case 的 style 属性名 camelize;放行 localimage:///localattachment:///videofile:// URL。

45. Phase 1:字体颜色 / 字体名称(span style)

需求:给选中的文字改颜色、字体名称、字号、背景色。

:最初拟用 <font color size face> 标签,但 Mindoc 实际支持的是 <span style="...">(color / font-family / font-size / background / rgba 半透明),且 <font> 属废弃旧式标签。

方案:彻底抛弃 <font>,改为生成 <span style="...">Editor.tsx 新增 applyFontStyle(styles),用正则检测已包裹的 span 并与旧 style 合并(cssDecls/cssString)避免嵌套;MarkdownToolbar.tsx 新增「文字颜色」「背景色」「字体」「字号」四个按钮 + 弹出面板(16 色 + rgba 半透明预设 + 原生取色器 + 字体/字号下拉);Icons.tsx 新增对应图标。

2026-09-09 开发总结

当日主线:完成富文本 Phase 2(视频/内嵌网页/图片样式)、Phase 3(zip 打包导出/导入),修复本地视频无法播放,并把「批量插入图片」「图片样式」合并为单一「图片」入口。

46. Phase 2:插入视频(文件模式 + asset 协议之坑)

方案:视频走文件模式——Rust 新增 import_video_file 命令(rfd 选择视频 → 复制到 app 数据目录 videos/);前端 localVideos.ts 把视频登记进 IndexedDB 的 videos 表(只存 path/name,不存二进制),正文插入 <video src="videofile://{id}" controls>Preview.LocalVideoconvertFileSrc 把磁盘路径转成可加载的 asset URL。

convertFileSrc 产生的 asset URL 无法加载,cargo build 直接报「tauri dependency features does not match the allowlist, add the protocol-asset feature」。根因是 asset 协议需要两处同时开启。

方案Cargo.tomltauri 依赖加 features = ["protocol-asset"]tauri.conf.jsonapp.security.assetProtocol 设为 {"enable": true, "scope": ["$APPDATA/**"]}csp 保持 null 即可)。

47. Phase 2:内嵌网页 + 图片尺寸/对齐(合并图片入口)

方案

  • 内嵌网页:新增 IframeDialog(URL + 宽 + 高),插入 <iframe src width height>Previewiframe 组件白名单放行。
  • 图片尺寸/对齐:新增可调宽度与对齐方式的图片插入。随后按用户要求,把「批量插入图片」与「图片样式」两个按钮合并为单一「图片」按钮——ImageUploadDialog 同时支持多选/拖拽本地图片、URL 输入、可选宽度/对齐;留空样式插入标准 ![图片](src),填了宽度或对齐则插入 <img src width align>。删除 ImageStyleDialog 及无用的 IconImageStyle

48. Phase 3:md + 图片/附件/视频 的 zip 打包导出与导入

需求:原导出只是单个 .md,图片/附件/视频无法一起带走,无法完整归档或在另一台机器恢复。

方案:zip 归档,内含 note.md(正文)+ manifest.json[{id,kind,name,mime}])+ media/{id}(媒体二进制)。Rust 新增 export_md_zip/import_md_zipzip+base64 依赖,仅桌面端);图片/附件(IndexedDB Blob)以 base64 传输,视频走磁盘路径由 Rust 直接读写(避免大文件 base64);导入时 id 原样保留、正文占位符无需改写;FileMenu 新增「从压缩包导入」「导出压缩包(含图片/附件/视频)」。

49. 本地视频无法播放(urlTransform 白名单)

:插入的视频在实时预览中无法播放,src 被清空。

根因:react-markdown 渲染 raw HTML 时对 video[src] 走自定义 urlTransform,而它只放行了 localimage:///localattachment://videofile:// 落到 defaultUrlTransform 被当作未知协议清空。

方案Preview.tsxurlTransform 增加 isLocalVideo(url) 放行 videofile://

2026-09-10 开发总结

当日主线:补齐文字级排版工具(上标/下标/下划线)、微调颜色组图标,并为全屏预览新增「演示(PPT 翻页)」展示模式。

50. 上标 / 下标 / 下划线

需求:缺少上标、下标(单位、角标、脚注)与文字下划线三种常用文字格式。

方案:沿用「内联 HTML」承载——工具条新增「下划线」「上标」「下标」三按钮(图标 fa-underline / fa-superscript / fa-subscript),分别用 insertAtCursor 包裹 <u>…</u><sup>…</sup><sub>…</sub><u>/<sup>/<sub> 均在 rehype-sanitize 默认白名单内,预览无需额外放行即可正常渲染。

51. 文字颜色 / 背景色图标调整

需求:更换「文字颜色」「背景色」按钮的字体图标。

方案:文字颜色由 fa-palette 换为 fa-droplet(墨滴),背景色由 fa-highlighter 换为 fa-fill-drip(填充滴管),Icons.tsx 同步替换导入与导出。

52. 全屏演示模式(PPT 翻页)

需求:实时预览全屏下新增一种类似 PPT 的按页滑动展示模式。

方案

  • 新增 lib/slides.tssplitSlides(body):按 H1/H2 标题切分正文为「页」,标题前内容作首页,H3 及以下归属当前页,无标题则整篇一页。
  • App.tsx 给全屏预览加 fullscreenSlideMode 状态,在「滚动」与「演示」两模式间切换(右下角控制条按钮);演示模式下以横向 scroll-snap 容器每页渲染一个复用现有 Preview 的独立分页。
  • 翻页交互:←/→/↑/↓/空格/PageUp/PageDown 翻页,底部居中显示 ‹ n / m › 页码 + 左右按钮;goToSlidescrollTo(left = idx × clientWidth) 定位,onScroll 回算当前页;Esc 仍退出全屏(退出自动回到滚动模式)。
  • 每页继承字号缩放(A+/A−)与全部富文本能力;单页内容超高时该页内部可竖向滚动。

作者:秦晓川  创建时间:2026-09-13 12:06
最后编辑:秦晓川  更新时间:2026-09-22 22:19