update at 2025-10-08 22:26:26

This commit is contained in:
douboer
2025-10-08 22:26:26 +08:00
parent 7b36394427
commit e25dca5fdd
24 changed files with 180 additions and 1485 deletions

View File

@@ -1,36 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
## [Unreleased]
### Added
- EXIF 图片方向自动处理:自动检测 JPEG EXIF Orientation (1/3/6/8),按需旋转并转换为 PNG保证公众号显示方向正确。
- Gallery 短代码 `mppickall` 参数:`mppickall=1` 选取目录全部图片,`0` 或缺省按 `galleryNumPic` 限制。
- 批量发布功能:新增“批量发布文章”模态,支持按标签/文件名/文件夹/frontmatter 条件筛选、结果列表多选(复选框/鼠标框选)、全选/取消全选,并可将选中文章依次发布到公众号草稿箱,发布过程显示进度与成功/失败统计(每篇间有短延迟以降低请求频率)。
### Changed
- README新增图片方向处理说明、Gallery 参数使用示例。
### Notes
- 若遇到其他 EXIF 方向值(除 1/3/6/8当前保持原样可后续扩展。
## [1.3.0] - 2025-09-25
### Optimized
- 主题资源加载与提示逻辑优化:升级提示清理旧主题再下载。
### Added
- 多主题/代码高亮资源增量更新支持。
### Fixed
- 若干边缘情况下的 frontmatter 解析回退稳定性。
## [1.2.x]
- 历史版本条目待补充(如需补录,请提供对应版本变更点)。
---
## 维护指引
- 发布新版本:更新 `package.json` / `manifest.json` 的版本号;追加 `versions.json`;将当前 Unreleased 条目移动为新的版本号,并添加日期;再创建新的 Unreleased 模板。
- 提交信息建议:`feat: ...`, `fix: ...`, `docs: ...`, `refactor: ...` 等 Conventional Commits 风格。

View File

@@ -1,563 +0,0 @@
## 更新说明
> [!IMPORTANT]
> NoteToMP 1.3.0版本对主题进行了优化,升级后请先清理旧版本主题文件,再重新下载新版主题。
>
> 操作步骤在NoteToMP插件设置中先点击『清空主题-清空』,然后点击『获取更多主题-下载』
>
> 注意:如果修改过主题文件请做备份后再操作。
完整历史变更请查看: [CHANGELOG](./CHANGELOG.md)
## 1、简介
这是一个Obsidian插件针对微信公众号编缉器进行了优化通过本插件复制笔记可以把笔记样式同步到公众号编缉器轻轻松松搞定文章格式一劳永逸而且支持代码高亮、代码行数显示、主题背景颜色等。针对微信公众号不能放链接也专门处理了提供直接展示链接地址和文末脚注展示两种方式。本项目初衷仅是为了能够将Obsidian中笔记的样式完美同步到微信公众号的编辑器中因此项目重点在于保证文章格式的一致性而不是成为一个微信公众号编辑器。
### 图片方向自动处理
为了优化微信公众号图片上传体验,插件新增了 EXIF 方向自动处理功能:
**功能说明:**
- 自动检测 JPEG 图片的 EXIF Orientation 信息
- 对存在方向问题的图片自动旋转并转换为 PNG 格式
- 确保上传到微信公众号的图片显示方向正确
**支持的方向类型:**
- `Orientation=1`:正常方向(无需处理)
- `Orientation=3`:需旋转 180°
- `Orientation=6`:需顺时针旋转 90°右旋 90°
- `Orientation=8`:需逆时针旋转 90°左旋 90°
**处理流程:**
1. 检测图片文件类型(仅处理 JPEG/JPG 格式)
2. 读取 EXIF 方向信息
3. 如有方向问题,使用 Canvas 进行旋转处理
4. 将处理后的图片转换为 PNG 格式上传
**用户体验:**
- 本地 Obsidian 中显示正常的图片,上传到公众号后也会保持正确方向
- 自动处理,无需用户手动调整
- 转换为 PNG 格式可避免 EXIF 信息导致的显示问题
### 调试日志
在控制台(开发者工具)可看到:
```
[note2mp] active file path: your/file/path.md
[note2mp] use default cover: cover.png -> ![[cover.png]]
[note2mp] EXIF orientation detected: 6
[note2mp] Image converted to PNG with rotation
```
路径日志做了节流:同一文件 3 秒内不重复打印。后续可加"调试开关"以完全关闭。
### 摘要、封面裁剪、原文链接等ges/screenshot.png)
## 2、安装
首先,**请确认已关闭了Obsidian的安全模式**。如未关闭,请通过**设置——第三方插件——关闭安全模式**关闭。
### 2.1 插件安装
#### 从官方**社区插件市场**安装
通过Obsidian**设置——第三方插件——社区插件市场**,输入**NoteToMP**搜索安装。
### 2.2 主题资源安装
如果采用的是用从插件市场或者Github下载安装的方式在插件安装完成后还需要再下载主题资源。网盘里的安装包已经集成了主题样式无需下载。
**1通过设置下载**
为了尽可能保证插件符合官方规范主题和代码高亮需要打开Obsidian的**设置**界面,在底部的**第三方插件**——**Note to MP**——**获取更多主题**手动下载。
**2手动下载**
也可以直接在[Release](https://github.com/sunbooshi/note-to-mp/releases)页面下载`assets.zip`文件,解压后放到`.obsidian/plugins/note-to-mp/assets`目录下。
### 2.3 常见安装问题
**只有默认主题**
确认根据**2.2 主题资源安装**里的步骤操作了,然后检查一下插件目录内容,应如下所示:
```
.obsidian/plugins/note-to-mp/
├── assets
│ ├── themes.json
│ ├── highlights.json
│ ├── themes
│ │ ├── maple.css
│ │ ├── mweb-ayu.css
│ │ └── ...
│ └── highlights
│ ├── a11y-dark.css
│ ├── a11y-light.css
│ └── ...
├── main.js
├── manifest.json
└── styles.css
```
## 3、使用
点击Obsidian左侧工具栏中的图标
![](images/clipboard-paste.png)或者按`Ctrl+P`打开命令,搜索**复制到公众号**。
检查样式无误后,点击**复制**按钮,然后到公众号粘贴即可。
![](images/20240630221748.jpg)
**★ 公众号**
插件支持多公众号,在下拉菜单中进行不同公众号的切换。该功能需要订阅才能使用。
**★ 复制**
检查样式无误后,点击**复制**按钮,然后到公众号编辑器粘贴即可。
**★ 上传图片**
点击上传图片会将文章中的本地图片上传到微信公众号,同时会替换预览中的图片地址,而您原始文章中的图片地址不会替换。上传图片完成之后,此时点击“复制”,然后到微信公众号编缉器中粘贴就可以把图片带过去了。该功能需要订阅才能使用。
**★ 发草稿**
点击发草稿会上传文章中的本地图片,并且将文章发送到公众号的草稿箱,省去粘贴步骤。在文章正式发布之前还有一些选项需要您设置,比如文章摘要等。考虑到安全性,插件暂不提供直接发布功能。该功能需要订阅才能使用。
**★ 刷新**
如果笔记内容更新了,但是预览没有更新,可以点击一下刷新按钮。
**★ 封面**
发草稿必须设置文章封面,使用默认封面,是从您的永久素材中选取最近使用的作为封面,您需要在发布文章之前重新设置一下。本地上传则需要你选取一张本地图片作为封面。
**★ 样式**
可以选取笔记的样式目前有30多款还在持续增加中。如果有钟意的样式可以在插件设置中设置为默认样式这样就不用每次都点一下了。
**★ 代码高亮**
设置代码高亮的样式。
### 数学公式使用指南
- [LaTeX使用指南从入门到精通 - 少数派](https://sspai.com/post/77684)
- [通用 LaTeX 数学公式语法手册 - UinIO.com 电子技术实验室](http://www.uinio.com/Math/LaTex/)
- [AsciiMath Parser 🚀 Asciimath Parser](https://asciimath.widcard.win/zh/introduction/)
- [AsciiMath](https://asciimath.org/)
目前插件支持LaTeX和AsciiMath两种数学公式语法对于公式输入不是特别频繁而且不怎么熟悉LaTeX的用户来说可以尝试使用AsciiMathAsciiMath相对简单一些可以现学现用直接在官网查找手册编写就可以了。因为在正常的Markdown语法中无法区分采用的是哪种数学公式语法所以需要在插件中设置默认的数学公式语法默认是LaTeX语法。对于有混写需求的朋友来说可以采用代码块的语法来写数学公式然后指定latex或者asciimath来明确当前语法。但使用代码块语法的时候在Obsidian中并不能实时预览公式。
如果需要使用AsciiMath还需要安装asciimath插件才能在Obsidian中实时预览不过asciimath插件的解析器和官方的语法有一些差异主要体现在矩阵写法上所以使用时也需注意。另外需要特别提醒的是AsciiMath不支持在一个语法块中写多行公式所以如果要写多行公式只能每行公式单独写一个语法块。LaTeX是支持多行公式的。
数学公式的专业性很强,我也无法全面测试,如果遇到无法正常渲染的情况,欢迎反馈。
````markdown
行内公式:$c=\pm\sqrt{a^2+b^2}$
行间公式:
$$
c=\pm\sqrt{a^2+b^2}
$$
使用代码块方式可以指定公式语法,该方法仅适用行间公式。
采用latex语法的数学公式
``` latex
c=\pm\sqrt{a^2+b^2}
```
采用asciimath的数学公式
``` am
c=+-sqrt(a^2+b^2)
```
````
数学公式的渲染效果可以看这篇文章:[公众号文章里的数学公式排版秘籍](https://mp.weixin.qq.com/s/-kpT2U1gT_5W3TsDCAVgsw)👈️
### 自定义CSS使用指南
新建一篇笔记,例如**自定义样式**,直接将如下内容粘贴进笔记:
````CSS
```CSS
.note-to-mp {
font-family: Optima, Optima-regular, "Microsoft YaHei", PingFangSC-regular, serif;
padding: 0;
background-color: #FFFFFF;
}
```
````
然后打开NoteToMP插件设置将**自定义样式**即包含自定义CSS内容的笔记名称粘贴到**自定义CSS笔记**中即可。如果不使用自定义CSS留空即可。
关于自定义CSS的写法可以参考下面的代码
```css
/* 全局属性
* 这里可以设置字体,字体大小,边距,背景颜色等
*/
.note-to-mp {
/* 注:请在大括号内改写!!! */
}
/* 段落 */
.note-to-mp p {
/* 注:请在大括号内改写!!! */
}
/* 一级标题 */
.note-to-mp h1 {
/* 注:请在大括号内改写!!! */
}
/* 二级标题 */
.note-to-mp h2 {
/* 注:请在大括号内改写!!! */
}
/* 三级标题 */
.note-to-mp h3 {
/* 注:请在大括号内改写!!! */
}
/* 无序列表整体样式
* list-style-type: square|circle|disc;
*/
.note-to-mp ul {
/* 注:请在大括号内改写!!! */
}
/* 加粗 */
.note-to-mp strong {
/* 注:请在大括号内改写!!! */
}
/* 斜体 */
.note-to-mp em {
/* 注:请在大括号内改写!!! */
}
/* 加粗斜体 */
.note-to-mp em strong {
/* 注:请在大括号内改写!!! */
}
/* 删除线 */
.note-to-mp del {
/* 注:请在大括号内改写!!! */
}
/* 分隔线
*/
.note-to-mp hr {
/* 注:请在大括号内改写!!! */
}
/* 图片
*/
.note-to-mp img {
/* 注:请在大括号内改写!!! */
}
/*
* 文件嵌入引用
*/
.note-embed-file {
/* 注:请在大括号内改写!!! */
}
/*
* 高亮颜色
*/
.note-highlight {
/* background-color: rgba(255,208,0, 0.4); */
}
/*
* Callout
* 可以调整各种类型Callout的文字颜色和背景颜色
* color: rgb(158, 158, 158);
* background-color: rgba(158, 158, 158, 0.1);
*/
.note-callout-note {
}
/* abstract tip hint */
.note-callout-abstract {
}
.note-callout-success {
}
/* question help, faq, warning, caution, attention */
.note-callout-question {
}
/* failure, fail, missing, danger, error, bug */
.note-callout-failure {
}
.note-callout-example {
}
.note-callout-quote {
}
```
例如这篇文章[几个让公众号排版更精致的小技巧,手机上也可以!](https://mp.weixin.qq.com/s/Q4_pV9TW8un3qZ0vrUvD1A)👈️使用的自定义样式如下:
```css
.note-to-mp {
font-family: Optima-regular, Optima, "Microsoft YaHei", PingFangSC-regular, serif;
}
h2 strong {
display: inline-block;
background: rgb(90, 185, 131);
color: rgb(255, 255, 255);
padding: 2px 16px;
border-top-right-radius: 3px;
border-top-left-radius: 3px;
margin-right: 10px;
visibility: visible;
}
h2 {
border-bottom: rgb(90, 185, 131) 2px solid;
color: rgb(90, 185, 131);
}
section .note-callout-example {
color: rgb(90, 185, 131);
background-color: rgba(90, 185, 131, 0.1);
}
```
上面的例子,通过`.note-to-mp`指定了文章的字体,通过`h2 strong`单独定义了h2标题下strong的样式这样可以在标题中通过使用粗体增加了一个边框样式。通过`h2`定义了h2标题的底部线条的宽度和文本颜色。这样配合**Olive Dunk**主题就形成了自己的风格。
### 公众号名片
请参考 https://mp.weixin.qq.com/s/1wYd15Irmv9BPabgp5XMCA
### 设置图片大小
在Obsidian中可以设置图片的大小语法如下
```markdown
![[1.jpg|120x80]] 设置图片的宽度和高度
![[1.jpg|120]] 设置图片的宽度,高度按比例缩放
```
NoteToMP插件支持该语法。
### 文件嵌入
文件嵌入是Obsidian一个很有用的功能可以直接展示其它文件中的段落、章节。在写公众号的时候可以将以前文章的内容引用展示也可以将该功能当作模板使用。
文件嵌入的语法如下:
```markdown
![[文件名称#章节标题]]
![[文件名称#^段落标记]]
```
在NoteToMP插件中有两种展示文件嵌入内容的样式一种是引用也就是Obsidian默认的方式一种是正文相当于模板的方式。与模板不同的是采用嵌入方式内容会跟随被嵌入文件的内容更改。
## 批量发布Batch Publish
从 v1.3 起,插件新增“批量发布文章”功能,方便把满足条件的一批文章批量发送到公众号草稿箱以便后续编辑与发布。
如何打开在命令面板Ctrl/Cmd+P中搜索“批量发布文章”或在插件命令中找到“批量发布文章”。
主要功能:
- 条件筛选按标签tag、文件名关键字、文件夹路径、及 frontmatter 字段进行筛选,支持 AND/OR 逻辑组合(当前为 AND 默认行为)。
- 预览与选择:筛选结果以列表展示,支持单个复选、全选、取消全选,以及鼠标拖拽框选(常规拖拽为添加选择,按 Ctrl/Cmd 拖拽为取消选择)。
- 批量发布:点击“发布选中文章”后会依次调用渲染与上传流程(与单篇发布同一实现),每篇之间有 2s 延迟以降低并发请求风险。发布过程中会显示进度通知并在结束汇总成功/失败数量。
注意事项:
- 批量发布会激活 NotePreview 视图并复用其渲染/上传逻辑,若无法取得 NotePreview将无法完成发布。
- 单篇发布失败不会中断整体流程,失败项会在结束时统计并提示。
- 为避免误操作,建议先在小范围内测试筛选条件与发布流程再对大量文件执行批量发布。
示例使用场景:
- 你想要把所有标记为 `篆刻` 的文章筛选出来,批量上传到公众号草稿箱并逐条完善后发布。
- 按文件夹 `content/post` 筛选并批量发布该文件夹下的近期文章。
### 详细使用指南(一步步)
1. 打开模态
- 命令面板Ctrl/Cmd+P→ 输入“批量发布文章”,回车打开模态窗口。
2. 设置筛选条件
- 按标签:在“按标签筛选”中输入标签名(例如 `篆刻`)。
- 按文件名:输入关键词(例如 `教程`)。
- 按文件夹:输入路径(例如 `content/post`)。默认值已预填为 `content/post`。
- 按 frontmatter目前可通过自定义筛选扩展未来计划支持更复杂的 frontmatter 表达式)。
3. 回车快速应用
- 在任一输入框中按回车将立即执行“应用筛选”。
4. 选择文章
- 使用复选框逐条选择;点击行的任意位置也会切换对应复选框。
- 使用鼠标拖拽进行框选:不按修饰键时为“添加选择”,按住 Ctrl/Cmd 时为“取消选择”。
- 支持“全选/取消全选”复选框。
5. 批量发布
- 点击“发布选中文章”开始发布。发布会按顺序执行并在每篇之间等待 2 秒。
- 发布过程中会显示进度提示Notice发布结束会弹出成功/失败汇总。
### 筛选示例(可参考)
- 筛选有 `篆刻` 标签的文章:在标签输入框输入 `篆刻`,按回车。
- 筛选文件名包含 `教程` 的文章:在文件名输入框输入 `教程`。
- 同时按标签和目录筛选:标签输入 `篆刻`,文件夹输入 `content/post`,按回车。
### 常见问题与故障排查
- 无法发布或没有响应:检查是否已激活 `NotePreview` 视图(插件会在发布前尝试激活);如果视图打开失败,尝试手动打开插件右侧的预览窗格再重试。
- 部分文章发布失败:失败不会中断整体流程,发布结束时会通知失败数量。点击失败项单独重试发布。
- 图片上传失败或方向错误:插件会自动处理 JPEG 的 EXIF 方向并转换为 PNG若仍有问题请检查图片是否受损或在 `开发者工具` 查看日志(节流为 3 秒同一路径)。
- 筛选结果为空:确认筛选条件是否正确(区分目录路径、标签是否存在、关键词是否拼写正确)。可以先留空所有条件查看全部可用文章,然后逐项缩小范围。
### 配置说明(相关设置)
- `defaultCoverPic`:默认封面文件名(默认 `cover.png`),当文章没有 frontmatter 封面与正文首图时使用。
- `galleryNumPic`Gallery 展开时默认选取的图片数量(可在设置中调整)。
- `batchPublishPresets`:预设筛选模板(可在插件设置中新增常用筛选项)。
### 使用建议与最佳实践
- 先在少量文章上试运行一次批量发布,确认模板、封面与图片上传逻辑满足需求,再对大量文件执行批量发布。
- 如果担心频率限制或网络不稳定,可在代码中调整发布间隔(当前为 2s或分批次发布以降低失败率。
- 建议为常用筛选条件创建 Preset设置中节省重复输入时间。
### 示例:把 database 视图筛选规则映射到模态
如果你使用 Obsidian Dataview 或内置视图创建了如下视图:
```yaml
views:
- type: table
name: 表格
filters:
and:
- file.tags.contains("篆刻")
order:
- file.name
```
在模态中相当于:标签输入 `篆刻`,排序选择按 `文件名name`。
---
如果你还需要我把一张示例截图(标注关键按钮)加入 README我可以把占位图片生成并放到 `images/` 目录(需要你允许我在本地生成渲染的图片),或者我可以为你准备好截图模板与标注位置说明,方便你手动截屏粘贴。
### 插入SVG图标
https://www.bilibili.com/video/BV15XWVeEEJa/
### Gallery 短代码支持
自 1.x 版本起,插件支持将形如 Hugo/Hexo 风格的短代码:
```
{{<gallery dir="/img/guanzhan/1" figcaption="毕业展"/>}}{{<load-photoswipe>}}
```
在渲染阶段自动展开为若干行图片 WikiLink
```
![[001.jpg]]
![[002.jpg]]
```
可选参数新增:
`mppickall=1` 选取目录中所有图片忽略“Gallery 选取图片数”限制);`mppickall=0` 或缺省时按配置的数量限制。支持写法:`mppickall=1`、`mppickall='1'`、`mppickall="1"`0 同理)。
示例:
```
{{<gallery dir="/img/guanzhan/1" mppickall=1/>}}{{<load-photoswipe>}}
```
或属性顺序不同、带 figcaption
```
{{<gallery dir="/img/guanzhan/1" figcaption="毕业展" mppickall=1/>}}{{<load-photoswipe>}}
```
在 `mppickall=1` 情况下,仍保持文件名排序(同原逻辑)。
配置项:
- Gallery 根路径galleryPrePath指向本地实际图片根目录用于拼接短代码中的 dir 得到真实磁盘路径。
- Gallery 选取图片数galleryNumPic每个 gallery 最多展开前 N 张图片(按文件名排序)。
可在插件设置界面直接修改,无需重启。若希望随机选取或按时间排序,可后续在 issue 中反馈需求。若需要永久“全部图片”效果,可同时将“选取图片数”设为一个足够大的值,或在需要的单个 gallery 上使用 `mppickall=1` 精确控制。
### Gallery 块与 figure 支持
除了带 dir 的短代码,还支持块级:
```
{{<gallery>}}
{{<figure src="/img/a.jpg" caption="说明" >}}
{{<figure link="/img/b.png" caption="说明" >}}
{{</gallery>}}
```
渲染为:
```
![[a.jpg]]
![[b.png]]
```
说明:
- 支持 `src` 或 `link` 属性任选其一。
- `caption` 当前忽略(可后续增强:写入 `![[file|caption]]` 或紧随段落)。
- 去重/排序策略:按出现顺序,文件名原样。
### 自定义行级语法扩展
为提升公众号排版效率,插件内置以下“轻语法”转换(发生在 Markdown 解析前):
1. 斜体标注:`[fig 一段说明 /]` → `<span style="font-style:italic;...">一段说明</span>`
2. 彩色提示块(只作用当前这一行,不跨行):
- `|| 内容` 默认灰底
- `||r 内容` 棕底白字
- `||g 内容` 黄绿色背景
- `||b 内容` 浅灰背景
- `||y 内容` 浅黄背景
这些语法不会写回原笔记,只影响发布预览。后续可加入:类名替换 + 主题化配置 + caption 支持,欢迎反馈需求。
### 无图片时的默认封面
自动封面选择优先级:
1. frontmatter: cover / image非空
2. 正文首图Markdown 或 WikiLink
3. Gallery 短代码 / 块展开得到的首图
4. 默认封面 `defaultCoverPic`(设置面板可配置,默认 `cover.png`
配置说明:
- 若填写文件名(如 `cover.png`),会按当前笔记目录解析并包装为 `![[cover.png]]`。
- 若填写完整 `![[xxx]]` 语法或 `http(s)://` URL将原样使用。
- 若文件不存在,不会报错(可后续增加存在性提示)。
### Frontmatter 解析回退
如果 Obsidian `metadataCache` 暂未命中(例如首次载入或缓存延迟),插件会手动对首段 `---` YAML 进行轻量行级解析,提取:
- title / author / cover(image)
避免因为缓存未就绪导致标题/作者缺失。若需复杂 YAML数组、多行字符串建议等待官方缓存或后续考虑引入完整 YAML 解析库。
### 摘要、封面裁剪、原文链接等
```yaml
---
标题:
作者: 孙博士
封面: "![[封面模板.jpeg]]"
摘要:
封面裁剪:
原文地址:
打开评论: true
仅粉丝可评论: true
公众号: 孙博士研究所
样式: MWeb Panic
代码高亮: docco
---
```
视频教程https://www.bilibili.com/video/BV15XWVeEEmA/
## 4、反馈交流群
**微信群:**
加微信:**Genius35Plus**,备注:**NoteToMP**
![](images/20240702203745.jpg)
## 附:批量发布 - 快速交互速览与截图占位
如果你想把功能教学放到 README 中,这里是推荐的简短速览(已在模态中实现):
- 回车快速应用:在任一筛选输入框中按回车即可触发“应用筛选”,无需额外点击按钮。
- 鼠标框选:在结果列表中按住鼠标左键并拖拽可以创建选择框,松开后会添加范围内的文章为选中状态。
- Ctrl/Cmd + 拖拽:按住 Ctrl或 macOS 上的 Cmd/Meta再拖拽会把框内的文章从当前选择中取消方便进行批量取消选中
- 全选/取消全选:列表顶部提供全选复选框,一键切换所有结果的选择状态。
截图占位:如果你希望我把一张带标注的示例截图放到 `images/`,请回复“可以生成截图”,我会:
1. 在 `images/` 中放置占位文件 `batch-publish-example.png`(示例标注),
2. 在 README 中替换占位为图片预览并附带关键交互标注说明。
如果你更愿意手动截屏我也可以把一个标注模板SVG 或说明)发给你,方便手动粘贴到 `images/` 目录。

View File

@@ -1,74 +0,0 @@
# Note2MP 里程碑版本 v1.3.0
## 版本信息
- **版本号**: v1.3.0
- **发布日期**: 2024年9月27日
- **Git Tag**: v1.3.0
- **Git Branch**: release/v1.3.0
- **Git Commit**: 50e8d61
## 主要功能特性
### 批量发布系统
- 完整的数据库式文章筛选功能
- 支持标签、文件名、文件夹多维度筛选
- 日期范围筛选和排序选项
- 批量选择和发布进度追踪
### 高级UI交互
- 鼠标拖拽多选功能
- Ctrl键修饰符支持
- 滚动容器偏移处理
- 响应式界面设计
### 图库功能增强
- mppickall参数支持 (mppickall=1)
- 支持多种引号格式: `mppickall=1`, `mppickall='1'`, `mppickall="1"`
- EXIF图片方向自动处理
- JPEG转PNG转换优化
### 文档和代码质量
- 全面的中文代码注释
- 详细设计文档 (detaildesign.md)
- 架构图表文档 (diagrams.md with Mermaid)
- 完整的变更日志和README
## 归档内容
- `main.js` - 构建后的插件主文件
- `manifest.json` - Obsidian插件清单
- `styles.css` - 样式文件
- `package.json` - 项目依赖信息
- `README.md` - 项目说明文档
- `CHANGELOG.md` - 变更日志
- `detaildesign.md` - 详细设计文档
- `diagrams.md` - 架构图表文档
- `source-snapshot-v1.3.0.tar.gz` - 完整源代码快照
## 回滚说明
如需回滚到此版本:
1. **使用Git Tag回滚**:
```bash
git checkout v1.3.0
git checkout -b rollback-to-v1.3.0
```
2. **使用发布分支**:
```bash
git checkout release/v1.3.0
```
3. **使用源代码快照**:
```bash
tar -xzf archives/v1.3.0/source-snapshot-v1.3.0.tar.gz
```
## 版本对比
此版本可作为后续重大修改的对比基准。主要用于:
- 功能回归测试
- 性能对比分析
- 代码架构变更评估
- 稳定性基准对比
---
*此版本为稳定的里程碑版本,建议在进行大规模代码修改前保存此状态。*

View File

@@ -1,365 +0,0 @@
# note-to-mp 设计文档 (Detail Design)
> 拆分文档索引:
> - 架构总览:`architecture.md`
> - 图片管线:`image-pipeline.md`
> - 渲染服务蓝图:`render-service-blueprint.md`
> - 图示 (Mermaid)`diagrams.md`
> 本文件保留全量细节,增量演进请同步上述子文档。
## 1. 背景
为满足从 Obsidian 笔记到微信公众号文章的高质量发布需求,插件需要:
- 支持多种图片书写形式Wikilink 与标准 Markdown
- 统一图片处理与上传(包括 WebP 转换、水印、封面选择)。
- 自动抽取文章元数据(标题、作者、封面图)。
- 支持自定义短代码(`gallery`)与行级语法扩展(`||` 样式块、`[fig .../]` 等)。
- 提供灵活的封面回退逻辑frontmatter 指定优先,缺省取正文第一图)。
## 2. 目标
| 目标 | 说明 |
|------|------|
| 图片语法统一 | `![[file.png]]``![alt](path/file.png)` 最终统一进入 LocalImage 管线 |
| 元数据抽取 | 自动获取标题、作者、封面图(可回退)供后续上传逻辑使用 |
| 封面回退 | 未显式指定封面时,自动决策第一张图片 |
| Gallery 支持 | 将 `{{<gallery .../>}}{{<load-photoswipe>}}` 转成图片 wikilinks 列表 |
| 预处理 | 在 Markdown 渲染前执行自定义语法转 HTML |
| 易扩展 | 提供独立函数/接口减少耦合,如 `selectGalleryImages``extractWeChatMeta` |
| 默认封面配置 | 无任何图片候选时使用 `defaultCoverPic` (可配置) |
| 前置回退解析 | 若 metadataCache 缺失 frontmatter启用手动行级解析回退 |
| Gallery 块扩展 | 支持 `{{<gallery>}}` 块 + 内部 `figure src|link=` 解析 |
| 行级语法扩展 | `[fig .../]``||r`/`||g`/`||b`/`||y`/`||` 统一由 `applyCustomInlineBlocks` 处理 |
| 调试日志节流 | 输出当前文件路径与默认封面选用日志3 秒内同路径不重复 |
## 3. 术语与定义
- **Wikilink 图片语法**`![[xxx.png]]`
- **标准 Markdown 图片**`![描述](path/to/xxx.png)`
- **Frontmatter**:位于首部 `---` 包裹的元数据区域。
- **Cover封面**:用于公众号首图上传的图片。
- **Gallery Shortcode**`{{<gallery dir="/img/foo" figcaption="说明"/>}}{{<load-photoswipe>}}`
## 4. 系统现状概览
主要处理链路:
```
Raw Markdown
↓ extractWeChatMeta (保留 frontmatter 内容供分析)
↓ 去 frontmatter
↓ transformGalleryShortcodes (gallery → ![[...]] 列表)
↓ transformGalleryBlock (gallery 块/figure → ![[...]] 列表)
↓ marked.parse() (图片扩展 -> LocalImage token
↓ applyCustomInlineBlocks (fig/彩色段落 轻语法 HTML 化)
↓ 生成 HTML + 样式注入
↓ setArticle()
↓ getArticleContent() -> preprocessContent(line regex 替换) -> 最终 HTML
```
## 5. 架构模块划分
| 模块 | 关键函数/变量 | 作用 |
|------|---------------|------|
| 内容预处理 | `preprocessContent()` | 行级 Regex 转 HTML图片路径修正、`||` 块、`[fig .../]` |
| 图片统一解析 | `LocalFileRegex`、MarkdownImage tokenizer | 标准化所有图片为 LocalImage token |
| 图片资源管理 | `LocalImageManager` | 记录本地图片、上传、替换 URL、Base64 嵌入 |
| Gallery | `_listGalleryImages` / `selectGalleryImages` / `transformGalleryShortcodes` | 短代码 → wikilink 列表(可扩展 figcaption |
| 元数据抽取 | `extractWeChatMeta` / `getWeChatArticleMeta` | 标题 / 作者 / 封面图计算 |
| 封面自动补全 | `getMetadata()` 尾部逻辑 | 无 frontmatter cover 时回填 |
| 图片上传 | `uploadLocalImage` / `uploadCover` | WebP→JPG、加水印、水印依赖 wasm |
| WebP 支持 | `PrepareImageLib` + wasm | 转换后再上传 |
| 渲染管线 | `renderMarkdown` | 串联以上逻辑 |
## 6. 数据流示意
参见第 4 节图。每个阶段保证产物单向流入下一层,避免循环依赖。
## 7. 关键算法与实现细节
### 7.1 图片统一转换
- Regex`LocalFileRegex = /^(?:!\[\[(.*?)\]\]|!\[[^\]]*\]\(([^\n\r\)]+)\))/`
- Markdown 标准图片 tokenizer
1. 匹配 `![alt](path)``matches[0]`
2. 取 basename → 构造 `![[basename]]` 语义(内部直接建 LocalImage token不再二次正则回匹配
3. 避免原先多余 `-2.png)` 残留问题。
### 7.2 元数据抽取(`extractWeChatMeta`
- 捕获 frontmatter 简易块(首个 `---` 区间)。
- 解析 `title / author / image` 单行 KV。
- `image` → 取 basename → `![[basename]]`
- 回退封面:同时匹配 wikilink + markdown 图片,比较 index 取出现最早的一种。
- 返回:`{ title, author, coverLink, rawImage }`
-`getMetadata()` 融合以补齐空缺字段。
- 若 Obsidian `metadataCache` 返回为空或缺失字段,触发手动 fallback扫描首段 frontmatter 行(不依赖外部 YAML 包),支持 `key: value` 单行形式;空字符串的 cover/image 会被视为未提供。
- 追加默认封面逻辑封面候选链frontmatter cover > 正文首本地图/本地 wikilink/markdown > gallery 生成图 > defaultCoverPic
### 7.3 前置处理(`preprocessContent`
- `[fig .../]``<span>`(题注样式)。
- 行级命令:`||r / ||g / ||b / ||y / ||` → 不同背景色 `<p>`
- `<img src="img/...">` → 前缀补全 `/img/`
### 7.4 Gallery 功能
- 短代码 Regex`{{<gallery dir="..."( figcaption="...")?/ >}}{{<load-photoswipe>}}`
- `_listGalleryImages`:读目录 + 过滤扩展 + 排序 + 截断。
- `selectGalleryImages`:对外通用(支持未来 random / prefix / includeDirInLink
- 输出:多行 `![[file]]`,并追加可选 `figcaption` div。
#### 7.4.1 块级 Gallery 语法(新增)
支持:
```
{{<gallery>}}
{{<figure src="/img/foo-1.png" caption="说明" >}}
{{<figure src="/img/foo-2.jpeg" caption="说明2" >}}
{{</gallery>}}
```
转换:
```
![[foo-1.png]]
![[foo-2.jpeg]]
```
规则:
- 仅取 src 的 basename忽略 caption后续可扩展为题注输出
- 若块内未匹配到任何 figure保留原文本。
- 正则:`/{{<gallery>}}([\s\S]*?){{<\/gallery>}}/g` 与内部 `figureRegex = /{{<figure\s+src="([^"]+)"[^>]*>}}/g`
- 输出顺序按出现顺序。
- `figure` 标签支持 `src="..."` 与可选 `link="..."`,当存在 link 时仍按 `src` 的 basename 作为图片候选;后续可利用 link 生成超链接包装。
#### 7.4.2 link 属性与未来 caption 计划
- 当前:`link` 仅被解析但未输出额外结构,保留在后续渲染扩展阶段使用(例如生成 `<a>` 包裹 `<img>`)。
- 规划:`caption` 字段可映射为 wikilink alias 或 `<figcaption>`
### 7.5 行级轻语法扩展 (`applyCustomInlineBlocks`)
- 输入:渲染后 HTML / 或预处理文本段落。
- 规则:
- `[fig 内容 /]``<span class="n2m-fig">内容</span>`(当前实现可能用内联 style后续计划换 class
- `||r 文本` / `||g` / `||b` / `||y` / `|| 文本` → 彩色背景段落 `<p style>...</p>`
- 节点安全:通过转义内部 HTML 以防注入(若未实现需列入风险)。
- 后续:提取公共 class + 主题 CSS。
### 7.6 调试日志与节流
- 目的:调试封面选取与路径解析;避免刷屏。
- 机制记录最近一次输出路径时间戳3 秒内同路径日志抑制。
- 日志包括:当前 markdown 文件绝对路径;默认封面 fallback 触发说明gallery 转换统计(可选)。
### 7.7 配置项外化 (Settings 更新)
- 新增:`galleryPrePath`, `galleryNumPic`, `defaultCoverPic`
- 位置:`NMPSettings` + `SettingTab` UI 输入框。
- 迁移:移除硬编码常量 `GALLERY_PRE_PATH` / `GALLERY_NUM_PIC`
- 默认值:`defaultCoverPic = 'cover.png'`(可为相对/绝对/网络 URL 或 wikilink 形式)。
- 风险:用户提供的默认封面不存在 → 目前不校验,可后续增加存在性检查与 Notice。
### 7.8 封面候选决策链(更新版)
1. 若已有 `thumb_media_id`(外部指定)→ 不再上传本地封面,保持 null。
2. frontmatter cover/image非空字符串→ 使用其 basename 生成 wikilink。
3. 正文扫描首个本地图片markdown / wikilink忽略 http/https
4. 若正文无 → 使用 gallery 自动展开生成的第一张候选。
5. 若仍无 → 使用 `defaultCoverPic`(若配置)。
6.`defaultCoverPic` 也无 → cover 为空。
Edge Cases
- frontmatter cover: "" → 视为未提供。
- defaultCoverPic 若为绝对 URL → 在上传阶段需区分远程/本地策略。
- gallery 展开后若所有图片为远程 URL未来支持→ 不作为本地候选,跳到 defaultCoverPic。
## 8. 正则清单
| 场景 | 正则 | 说明 |
|------|------|------|
| frontmatter | `^---[\s\S]*?\n---` | 仅首段 |
| Wikilink 图片 | `!\[\[(.+?)\]\]` | 非贪婪 |
| Markdown 图片 | `!\[[^\]]*\]\(([^\n\r\)]+)\)` | 不跨行 |
| Gallery | `{{<gallery\s+dir=\"([^\"]+)\"(?:\s+figcaption=\"([^\"]*)\")?\s*\/>}}{{<load-photoswipe>}}` | 捕获 dir/caption |
| Gallery 块 | `{{<gallery>}}([\s\S]*?){{<\/gallery>}}` | 块包裹内容 |
| Gallery figure | `{{<figure\s+src=\"([^\"]+)\"[^>]*>}}` | 提取图片 src |
| Figure link 属性 | `link=\"([^\"]+)\"` | 可选外链(当前仅解析) |
| fig | `\[fig([^>]*?)\/]` | 题注 |
| 行块 | `\|\|r (.*)` 等 | 行级匹配 |
## 9. 错误与边界
| 情况 | 行为 |
|------|------|
| frontmatter 缺尾部 | 不解析,当普通正文 |
| 无 image 且正文无图 | `coverLink` 为空 |
| Gallery 目录缺失 | 原样保留短代码 |
| WebP 转换失败 | 记录日志,使用原文件 |
| 非支持图片扩展 | 忽略该文件 |
## 10. 性能
- 正则线性扫描 O(n)。
- Gallery 目录排序 O(m log m)。
- 可后续对 `_listGalleryImages` 结果加缓存。
## 11. 配置 & 常量
| 常量 | 说明 | 后续计划 |
|------|------|----------|
| `galleryPrePath` | 画廊根目录(配置项) | 未来参数化 per-block 覆盖 |
| `galleryNumPic` | 默认选图数量(配置项) | 支持块/短代码 count 覆盖 |
| `defaultCoverPic` | 默认封面备用 | 校验存在 / 多备选随机 |
| 移除GALLERY_PRE_PATH | (已外化) | - |
| 移除GALLERY_NUM_PIC | (已外化) | - |
| 行级样式内联 | 直接 embed style | 可改 class + CSS |
## 12. 对外接口
| 方法 | 描述 |
|------|------|
| `getWeChatArticleMeta()` | 获取最近一次渲染抽取的 meta |
| `getMetadata()` | 微信上传所需聚合元数据,含封面补回 |
| `uploadCover()` | 上传封面,含 webp 处理 |
| `uploadLocalImage()` | 上传正文图片 |
| `renderMarkdown()` | 触发整个渲染链路 |
## 13. 测试建议
| 测试项 | 用例 |
|--------|------|
| frontmatter | 正常/缺尾部/缺字段/中文标题 |
| 首图回退 | wikilink 与 markdown 顺序交错 |
| Gallery | 有/无目录;含 caption空目录 |
| 图片文件名 | 中文/空格/连字符/数字/大小写扩展 |
| 行级语法 | 多种颜色并存/与普通段落混排 |
| WebP | 可转换/未准备 wasm |
| 覆盖逻辑 | frontmatter 不同组合(仅 author、仅 title 等) |
## 14. 可扩展点
| 方向 | 说明 |
|------|------|
| 更完整 YAML | 使用 `js-yaml` 支持多行、列表、复杂类型 |
| tags/categories | 抽取为数组并暴露接口 |
| Gallery 参数 | 支持 `count=``random=``includeDir=` 等 |
| 封面策略 | 配置“frontmatter 优先 / 正文优先 / 首图随机” |
| 图廊 HTML 模式 | 直接生成 `<figure>` 集合而非 wikilink 列表 |
| 样式外置 | 行级块样式改为统一 CSS class |
| 默认封面池 | 支持数组随机选择 default cover |
| 默认封面校验 | 选择时校验存在性 + Notice 提示 |
| caption alias | gallery figure caption -> wikilink alias/figcaption |
| link wrap | figure link 生成 `<a>` 包裹图片 |
| debug 开关 | 设置中关闭全部调试日志 |
| 目录缓存 | 减少频繁 IO |
## 15. 风险与规避
| 风险 | 缓解 |
|------|------|
| 简化 frontmatter 误判 | 提示限制 + 计划引入 YAML 解析 |
| 正则误伤 | 增加单元测试覆盖边界字符 |
| Gallery IO 阻塞 | 后续异步 + loading 占位 |
| 移动端缺 fs | try/catch + 环境判断 |
| 样式散落行内 | 后续集中到主题 CSS |
## 16. 示例复盘
示例:
```
---
title: 6月特种兵式观展
author: 大童
image: "/img/shufa/a.jpg"
---
前言
![首图](img/b-first.png)
![[c-second.png]]
```
结果:
- 封面:`![[a.jpg]]`frontmatter 优先)
- 若删去 image 行 → 封面:`![[b-first.png]]`(首图)
## 17. 迭代优先级建议
| 优先级 | 项目 |
|--------|------|
| 高 | 封面 UI 选择确认 |
| 中 | YAML 解析器集成 |
| 中 | Gallery 参数化count/random |
| 中 | tags/categories 抽取 |
| 低 | 图廊 HTML figure 模式 |
## 18. 关键函数索引
| 函数 | 作用 |
|------|------|
| `extractWeChatMeta` | 抽取标题/作者/封面回退 |
| `transformGalleryShortcodes` | gallery 短代码 → wikilinks |
| `selectGalleryImages` | 画廊图片选择封装 |
| `preprocessContent` | 行级语法 HTML 化 |
| `getWeChatArticleMeta` | 获取抽取的 meta |
| `getMetadata` | 最终上传元数据(含封面回填) |
| `MarkdownImage.tokenizer` | 标准图片转 LocalImage token |
| `LocalFileRegex` | 统一匹配图片语法 |
## 19. 总结
通过“标准化 → 抽取 → 预处理 → 渲染 → 上传”分层设计,确保各功能模块低耦合并可独立演进。当前设计已满足基础运营发布需求,后续可按优先级增强 YAML 解析、封面配置、多图策略与 gallery 表现力。
---
*若需我继续实现 tags/categories 抽取或 gallery 参数扩展,请直接提出。*
## 附录 A. 草稿箱清空功能
### A.1 背景
运营过程中测试/多次上传会堆积大量“草稿”,需要一键清理能力,并具备安全保护与预览模式。
### A.2 接口
| 方法 | 说明 |
|------|------|
| `clearAllDrafts(appid, { confirm, batchSize=20, retainLatest=0, dryRun=false })` | 批量列出并删除草稿;需 `confirm:true` 才执行实际删除 |
### A.3 选项说明
| 选项 | 类型 | 说明 |
|------|------|------|
| confirm | boolean | 必须显式 true否则抛错中止 |
| batchSize | number | 分页拉取条数(默认 20受微信接口限制 |
| retainLatest | number | 保留最新 N 条(按接口返回顺序) |
| dryRun | boolean | 仅统计将删除的数量,不执行删除 |
### A.4 返回结构
```
{
total: number, // 收集到的全部 media_id 数
skip: number, // 被保留的数量(= retainLatest 实际保留)
success: number, // 实际删除成功数dryRun= true 时恒 0
fail: number, // 删除失败数
fails: Array<{ media_id, status? , errcode?, errmsg?, text? }>,
dryRun: boolean
}
```
### A.5 安全措施
1. `confirm` 必须为 true。
2. 可设置 `retainLatest` 防止误删全部。
3. `dryRun` 先预览再正式执行。
4. 删除逐条执行,可在失败时保留失败列表审计。
### A.6 未来增强
| 方向 | 说明 |
|------|------|
| 并发删除 | Promise pool 控制并发提升速度 |
| 过滤条件 | 按标题关键词/日期范围选择性删除 |
| 进度通知 | 分批实时进度 Notice / 状态栏 |
| UI 集成 | 命令面板 + 二次确认弹窗 |
| 时间排序校验 | 根据返回 `update_time` 明确排序而非假设 |
### A.7 命令面板入口
已添加命令:`清空微信草稿箱 (危险)` (id: `note-to-mp-clear-drafts`)
流程:
1. 首次 confirm提示风险。
2. 询问是否 dryRun输入 y 仅预览)。
3. 若非 dryRun再询问保留最近 N 条。
4. 二次 confirm 再次确认删除范围。
5. 调用 `clearAllDrafts(null, { confirm:true, dryRun, retainLatest })`
失败处理:捕获异常并 Notice 显示;控制台输出详细错误。
### A.8 可视化操作面板 (Modal)
新增 `ClearDraftsModal`:提供表单而非多级 confirm/prompt。
表单字段:
- appid (可留空自动从当前文章 frontmatter 获取)
- 保留最近 N 条number默认 0
- DryRun 复选框(默认勾选)
交互流程:
1. 打开命令 → 弹出 Modal。
2. 用户填写/确认参数,首次点“执行”→ 若为真实删除且非 dryRun会再弹出 confirm。
3. 结果以 JSON 形式写入下方 <pre> 区域,便于复制。
4. Notice 简要提示DryRun 或 完成)。
错误处理:
- try/catch 包裹,失败写入 resultPre 文本 + Notice。
- run 按钮在执行期间 disabled防止重复触发。
后续增强设想:
| 项目 | 说明 |
|------|------|
| 进度条 | 删除大批量时显示当前进度/总数 |
| 失败重试 | 针对 fails 列表单独重试按钮 |
| 过滤条件 | 增加标题关键词 / 日期起止输入 |
| 多账号选择 | 下拉列出已配置的 appid 列表 |
| 日志导出 | 一键复制 JSON 结果 |

View File

@@ -1,178 +0,0 @@
# Diagrams (Mermaid)
> 动态架构与主要交互可视化。与文字说明对应:`architecture.md` / `image-pipeline.md` / `render-service-blueprint.md`。
## 1. 模块类图 (当前实现概览)
```mermaid
classDiagram
class NotePreview {
+renderMarkdown()
+uploadImages()
+postArticle()
+postImages()
+exportHTML()
}
class ArticleRender {
+renderMarkdown(file)
+getMetadata()
+uploadImages(appid)
+postArticle(appid,cover?)
+postImages(appid)
+exportHTML()
+transformGalleryBlock()
+applyCustomInlineBlocks()
}
class LocalImageManager {
+setImage(path,info)
+uploadLocalImage(token,vault)
+uploadRemoteImage(root,token)
+replaceImages(root)
}
class LocalFile {
+markedExtension()
}
class AssetsManager {
+loadAssets()
+getTheme(name)
+getHighlight(name)
}
class NMPSettings {
+wxInfo
+authKey
+enableMarkdownImageToWikilink
+galleryPrePath
+galleryNumPic
+defaultCoverPic
}
class WeChatAPI {
+wxGetToken()
+wxAddDraft()
+wxUploadImage()
}
NotePreview --> ArticleRender
ArticleRender --> LocalImageManager
ArticleRender --> AssetsManager
ArticleRender --> NMPSettings
ArticleRender --> WeChatAPI
ArticleRender --> LocalFile
LocalFile --> LocalImageManager
NotePreview --> NMPSettings
NotePreview --> AssetsManager
```
## 2. 发布草稿时序图
```mermaid
sequenceDiagram
participant U as User
participant NP as NotePreview
participant AR as ArticleRender
participant WX as WeChatAPI
participant LIM as LocalImageManager
U->>NP: 点击 发草稿
NP->>AR: postArticle(appid)
AR->>WX: wxGetToken(authKey, appid)
WX-->>AR: token
AR->>AR: cachedElementsToImages()
AR->>LIM: uploadLocalImage(token)
LIM-->>AR: local media_id(s)
AR->>LIM: uploadRemoteImage(token)
LIM-->>AR: remote media_id(s)
AR->>LIM: replaceImages()
AR->>AR: resolve cover (frontmatter / first image / default)
AR->>WX: wxAddDraft(draft JSON)
WX-->>AR: media_id | err
AR-->>NP: 结果
NP-->>U: 成功 / 失败提示
```
## 3. 图片上传流程图
```mermaid
graph TD
A[Start UploadImages] --> B{AuthKey/AppId?}
B -- No --> Z[Throw Error]
B -- Yes --> C[Get Token]
C --> D[cachedElementsToImages]
D --> E[uploadLocalImage]
E --> F[uploadRemoteImage]
F --> G[replaceImages]
G --> H[Copy HTML to Clipboard]
H --> I[End]
```
## 4. 自动封面推断逻辑
```mermaid
graph TD
A[Need Cover?] -->|No| Z[Skip]
A -->|Yes| B[Frontmatter cover?]
B -- Yes --> H[Use frontmatter]
B -- No --> C[Strip Frontmatter]
C --> D[Scan Markdown Images]
C --> E[Scan Wikilink Images]
D --> F[Collect Candidates]
E --> F[Collect Candidates]
F --> G{Any Body Image?}
G -- Yes --> H[Use first body image]
G -- No --> I[Gallery Expanded?]
I -- Yes --> H[Use first gallery image]
I -- No --> J[defaultCoverPic Config?]
J -- Yes --> H[Use defaultCoverPic]
J -- No --> Z[Cover stays empty]
```
## 4.1 行级轻语法与日志节流 (补充)
```mermaid
graph TD
M[Markdown Raw] --> P[Preprocess Gallery Shortcode]
P --> GB[Gallery Block Parse]
GB --> MD[Marked Parse]
MD --> IB[applyCustomInlineBlocks]
IB --> R[Render HTML]
R --> L{Log Throttle}
L --> R1[Path Log]
L --> R2[Cover Fallback Log]
```
## 5. 未来 RenderService Pipeline 图
```mermaid
graph TD
L[Loader] --> FM[Frontmatter]
FM --> PP[Preprocessors]
PP --> P[Parser]
P --> TR[Transformers]
TR --> RI[ResourceIndex]
RI --> R[Renderer]
R --> PO[Postprocessors]
PO --> EX[Exporters]
```
## 6. 并发上传示意 (未来优化)
```mermaid
graph TD
A[Images] --> B[Partition]
B --> C[Pool]
C --> D[Upload]
D --> E{Success?}
E -->|No| R[Retry]
E -->|Yes| F[Collect ids]
R --> C
F --> G[Done]
```
## 7. 状态机概览 (发布按钮)
```mermaid
stateDiagram-v2
[*] --> Idle
Idle --> Uploading : 点击 上传/发布
Uploading --> Publishing : 草稿模式
Uploading --> Completed : 仅上传
Publishing --> Completed : 响应成功
Publishing --> Failed : 接口错误
Uploading --> Failed : 资源错误
Failed --> Idle : 用户重试
Completed --> Idle : 新文件切换
```
---
需要我将这些图嵌入到 README 的一个“开发者”章节吗?可以继续提出。

View File

@@ -1,10 +0,0 @@
{
"id": "note-to-mp",
"name": "NoteToMP",
"version": "1.3.0",
"minAppVersion": "1.4.5",
"description": "Send notes to WeChat MP drafts, or copy notes to WeChat MP editor, perfect preservation of note styles, support code highlighting, line numbers in code, and support local image uploads.",
"author": "Sun Booshi",
"authorUrl": "https://sunboshi.tech",
"isDesktopOnly": false
}

View File

@@ -1,32 +0,0 @@
{
"name": "note-to-mp",
"version": "1.3.0",
"description": "This is a plugin for Obsidian (https://obsidian.md)",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"download": "node tools/download.mjs",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
},
"dependencies": {
"@zip.js/zip.js": "^2.7.43",
"highlight.js": "^11.9.0",
"html-to-image": "^1.11.11",
"marked": "^12.0.1",
"marked-highlight": "^2.1.3"
}
}

View File

@@ -1,142 +0,0 @@
/* archives/v1.3.0/styles.css — 归档版本的样式文件。 */
/* =========================================================== */
/* UI 样式 */
/* =========================================================== */
.note-preview {
min-height: 100%;
width: 100%;
height: 100%;
background-color: #fff;
display: flex;
flex-direction: column;
}
.render-div {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.preview-toolbar {
position: relative;
min-height: 100px;
border-bottom: #e4e4e4 1px solid;
background-color: var(--background-primary);
}
.toolbar-line {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
margin: 10px 10px;
}
.copy-button {
margin-right: 10px;
}
.refresh-button {
margin-right: 10px;
}
.upload-input {
margin-left: 10px;
visibility: hidden;
width: 0px;
}
.style-label {
margin-right: 10px;
}
.style-select {
margin-right: 10px;
width: 120px;
}
.msg-view {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--background-primary);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 18px;
z-index: 9999;
display: none;
}
.msg-title {
margin-bottom: 20px;
max-width: 90%;
}
.note-mpcard-wrapper {
margin: 20px 20px;
background-color: rgb(250, 250, 250);
padding: 10px 20px;
border-radius: 10px;
}
.note-mpcard-content {
display: flex;
}
.note-mpcard-headimg {
border: none !important;
border-radius: 27px !important;
box-shadow: none !important;
width: 54px !important;
height: 54px !important;
margin: 0 !important;
}
.note-mpcard-info {
margin-left: 10px;
}
.note-mpcard-nickname {
font-size: 17px;
font-weight: 500;
color: rgba(0, 0, 0, 0.9);
}
.note-mpcard-signature {
font-size: 14px;
color: rgba(0, 0, 0, 0.55);
}
.note-mpcard-foot {
margin-top: 20px;
padding-top: 10px;
border-top: 1px solid #ececec;
font-size: 14px;
color: rgba(0, 0, 0, 0.3);
}
.loading-wrapper {
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
}
.loading-spinner {
width: 50px; /* 可调整大小 */
height: 50px;
border: 4px solid #fcd6ff; /* 底色,浅灰 */
border-top: 4px solid #bb0cdf; /* 主色,蓝色顶部产生旋转感 */
border-radius: 50%; /* 圆形 */
animation: spin 1s linear infinite; /* 旋转动画 */
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@@ -14,6 +14,7 @@ for FILE in "${FILES[@]}"; do
BACKUP="$PLUGIN_DIR/backup/$FILE.bk" BACKUP="$PLUGIN_DIR/backup/$FILE.bk"
if [ -f "$TARGET" ]; then if [ -f "$TARGET" ]; then
mkdir -p "$(dirname "$BACKUP")"
cp -f "$TARGET" "$BACKUP" cp -f "$TARGET" "$BACKUP"
echo "已备份 $TARGET -> $BACKUP" echo "已备份 $TARGET -> $BACKUP"
fi fi
@@ -25,3 +26,12 @@ for FILE in "${FILES[@]}"; do
echo "⚠️ 源文件 $FILE 不存在,跳过" echo "⚠️ 源文件 $FILE 不存在,跳过"
fi fi
done done
# 4. 覆盖复制 assets 目录(静默)
if [ -d "assets" ]; then
mkdir -p "$PLUGIN_DIR/assets"
rsync -a --delete assets/ "$PLUGIN_DIR/assets/" >/dev/null
echo "已同步 assets -> $PLUGIN_DIR/assets/"
else
echo "⚠️ 源目录 assets 不存在,跳过"
fi

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -27,7 +27,9 @@ function parseAspectRatio(ratio: string): { width: number; height: number } {
*/ */
function getTargetPageHeight(settings: NMPSettings): number { function getTargetPageHeight(settings: NMPSettings): number {
const ratio = parseAspectRatio(settings.sliceImageAspectRatio); const ratio = parseAspectRatio(settings.sliceImageAspectRatio);
return Math.round((settings.sliceImageWidth * ratio.height) / ratio.width); const height = Math.round((settings.sliceImageWidth * ratio.height) / ratio.width);
console.log(`[paginator] 计算页面高度: 宽度=${settings.sliceImageWidth}, 比例=${settings.sliceImageAspectRatio} (${ratio.width}:${ratio.height}), 高度=${height}`);
return height;
} }
/** /**
@@ -150,20 +152,26 @@ function wrapPageContent(elements: Element[]): string {
/** /**
* 渲染单个页面到容器 * 渲染单个页面到容器
* 预览时缩放显示,切图时使用实际尺寸
*/ */
export function renderPage( export function renderPage(
container: HTMLElement, container: HTMLElement,
pageContent: string, pageContent: string,
settings: NMPSettings settings: NMPSettings
): void { ): void {
const pageHeight = getTargetPageHeight(settings); // 实际内容尺寸(切图使用)
const pageWidth = settings.sliceImageWidth; const actualPageWidth = settings.sliceImageWidth;
const actualPageHeight = getTargetPageHeight(settings);
console.log(`[renderPage] 渲染页面: 宽=${actualPageWidth}, 高=${actualPageHeight}`);
container.innerHTML = ''; container.innerHTML = '';
// 直接设置为实际尺寸,用于切图
// 预览时通过外层 CSS 的 max-width 限制显示宽度,浏览器自动缩放
container.style.cssText = ` container.style.cssText = `
width: ${pageWidth}px; width: ${actualPageWidth}px;
min-height: ${pageHeight}px; height: ${actualPageHeight}px;
max-height: ${pageHeight}px;
overflow: hidden; overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
padding: 40px; padding: 40px;

View File

@@ -52,9 +52,11 @@ export async function sliceCurrentPage(
const originalWidth = pageElement.style.width; const originalWidth = pageElement.style.width;
const originalMaxWidth = pageElement.style.maxWidth; const originalMaxWidth = pageElement.style.maxWidth;
const originalMinWidth = pageElement.style.minWidth; const originalMinWidth = pageElement.style.minWidth;
const originalTransform = pageElement.style.transform;
try { try {
// 临时设置为目标宽度 // 临时移除 transform 缩放,恢复实际尺寸用于切图
pageElement.style.transform = 'none';
pageElement.style.width = `${sliceImageWidth}px`; pageElement.style.width = `${sliceImageWidth}px`;
pageElement.style.maxWidth = `${sliceImageWidth}px`; pageElement.style.maxWidth = `${sliceImageWidth}px`;
pageElement.style.minWidth = `${sliceImageWidth}px`; pageElement.style.minWidth = `${sliceImageWidth}px`;
@@ -78,6 +80,7 @@ export async function sliceCurrentPage(
} finally { } finally {
// 恢复样式 // 恢复样式
pageElement.style.transform = originalTransform;
pageElement.style.width = originalWidth; pageElement.style.width = originalWidth;
pageElement.style.maxWidth = originalMaxWidth; pageElement.style.maxWidth = originalMaxWidth;
pageElement.style.minWidth = originalMinWidth; pageElement.style.minWidth = originalMinWidth;

View File

@@ -28,14 +28,14 @@ export class XiaohongshuPreview {
// UI 元素 // UI 元素
topToolbar!: HTMLDivElement; topToolbar!: HTMLDivElement;
templateSelect!: HTMLSelectElement; templateSelect!: HTMLSelectElement;
themeSelect!: HTMLSelectElement; fontSizeInput!: HTMLInputElement;
fontSelect!: HTMLSelectElement;
fontSizeDisplay!: HTMLSpanElement;
pageContainer!: HTMLDivElement; pageContainer!: HTMLDivElement;
bottomToolbar!: HTMLDivElement; bottomToolbar!: HTMLDivElement;
pageNavigation!: HTMLDivElement; pageNavigation!: HTMLDivElement;
pageNumberDisplay!: HTMLSpanElement; pageNumberDisplay!: HTMLSpanElement;
styleEl: HTMLStyleElement | null = null; // 主题样式注入节点
currentThemeClass: string = '';
// 分页数据 // 分页数据
pages: PageInfo[] = []; pages: PageInfo[] = [];
@@ -61,6 +61,14 @@ export class XiaohongshuPreview {
build(): void { build(): void {
this.container.empty(); this.container.empty();
this.container.addClass('xhs-preview-container'); this.container.addClass('xhs-preview-container');
// 准备样式挂载节点
if (!this.styleEl) {
this.styleEl = document.createElement('style');
this.styleEl.setAttr('data-xhs-style', '');
}
if (!this.container.contains(this.styleEl)) {
this.container.appendChild(this.styleEl);
}
// 顶部工具栏 // 顶部工具栏
this.buildTopToolbar(); this.buildTopToolbar();
@@ -102,31 +110,7 @@ export class XiaohongshuPreview {
option.text = name; option.text = name;
}); });
// 主题选择 // 字号控制(可直接编辑)
const themeLabel = this.topToolbar.createDiv({ cls: 'toolbar-label' });
themeLabel.innerText = '主题';
this.themeSelect = this.topToolbar.createEl('select', { cls: 'xhs-select' });
const themes = this.assetsManager.themes;
themes.forEach(theme => {
const option = this.themeSelect.createEl('option');
option.value = theme.className;
option.text = theme.name;
});
this.themeSelect.value = this.settings.defaultStyle;
this.themeSelect.onchange = () => this.onThemeChanged();
// 字体选择
const fontLabel = this.topToolbar.createDiv({ cls: 'toolbar-label' });
fontLabel.innerText = '字体';
this.fontSelect = this.topToolbar.createEl('select', { cls: 'xhs-select' });
['系统默认', '宋体', '黑体', '楷体', '仿宋'].forEach(name => {
const option = this.fontSelect.createEl('option');
option.value = name;
option.text = name;
});
this.fontSelect.onchange = () => this.onFontChanged();
// 字号控制
const fontSizeLabel = this.topToolbar.createDiv({ cls: 'toolbar-label' }); const fontSizeLabel = this.topToolbar.createDiv({ cls: 'toolbar-label' });
fontSizeLabel.innerText = '字号'; fontSizeLabel.innerText = '字号';
const fontSizeGroup = this.topToolbar.createDiv({ cls: 'font-size-group' }); const fontSizeGroup = this.topToolbar.createDiv({ cls: 'font-size-group' });
@@ -134,7 +118,13 @@ export class XiaohongshuPreview {
const decreaseBtn = fontSizeGroup.createEl('button', { text: '', cls: 'font-size-btn' }); const decreaseBtn = fontSizeGroup.createEl('button', { text: '', cls: 'font-size-btn' });
decreaseBtn.onclick = () => this.changeFontSize(-1); decreaseBtn.onclick = () => this.changeFontSize(-1);
this.fontSizeDisplay = fontSizeGroup.createEl('span', { text: '16', cls: 'font-size-display' }); this.fontSizeInput = fontSizeGroup.createEl('input', {
cls: 'font-size-input',
attr: { type: 'number', min: '12', max: '36', value: '16' }
});
this.fontSizeInput.style.width = '50px';
this.fontSizeInput.style.textAlign = 'center';
this.fontSizeInput.onchange = () => this.onFontSizeInputChanged();
const increaseBtn = fontSizeGroup.createEl('button', { text: '', cls: 'font-size-btn' }); const increaseBtn = fontSizeGroup.createEl('button', { text: '', cls: 'font-size-btn' });
increaseBtn.onclick = () => this.changeFontSize(1); increaseBtn.onclick = () => this.changeFontSize(1);
@@ -188,6 +178,8 @@ export class XiaohongshuPreview {
new Notice(`分页完成:共 ${this.pages.length}`); new Notice(`分页完成:共 ${this.pages.length}`);
this.currentPageIndex = 0; this.currentPageIndex = 0;
// 初次渲染时应用当前主题
this.applyThemeCSS();
this.renderCurrentPage(); this.renderCurrentPage();
} finally { } finally {
document.body.removeChild(tempContainer); document.body.removeChild(tempContainer);
@@ -203,7 +195,15 @@ export class XiaohongshuPreview {
const page = this.pages[this.currentPageIndex]; const page = this.pages[this.currentPageIndex];
this.pageContainer.empty(); this.pageContainer.empty();
const pageElement = this.pageContainer.createDiv({ cls: 'xhs-page' }); // 重置滚动位置到顶部
this.pageContainer.scrollTop = 0;
// 创建包裹器,为缩放后的页面预留正确的布局空间
const wrapper = this.pageContainer.createDiv({ cls: 'xhs-page-wrapper' });
const classes = ['xhs-page'];
if (this.currentThemeClass) classes.push('note-to-mp');
const pageElement = wrapper.createDiv({ cls: classes.join(' ') });
renderPage(pageElement, page.content, this.settings); renderPage(pageElement, page.content, this.settings);
// 应用字体设置 // 应用字体设置
@@ -214,47 +214,33 @@ export class XiaohongshuPreview {
} }
/** /**
* 应用字体设置 * 应用字体设置(仅字号,字体从主题读取)
*/ */
private applyFontSettings(element: HTMLElement): void { private applyFontSettings(element: HTMLElement): void {
const fontFamily = this.fontSelect.value; element.style.fontSize = `${this.currentFontSize}px`;
const fontSize = this.currentFontSize;
let fontFamilyCSS = '';
switch (fontFamily) {
case '宋体': fontFamilyCSS = 'SimSun, serif'; break;
case '黑体': fontFamilyCSS = 'SimHei, sans-serif'; break;
case '楷体': fontFamilyCSS = 'KaiTi, serif'; break;
case '仿宋': fontFamilyCSS = 'FangSong, serif'; break;
default: fontFamilyCSS = 'system-ui, -apple-system, sans-serif';
}
element.style.fontFamily = fontFamilyCSS;
element.style.fontSize = `${fontSize}px`;
} }
/** /**
* 切换字号 * 切换字号(± 按钮)
*/ */
private changeFontSize(delta: number): void { private async changeFontSize(delta: number): Promise<void> {
this.currentFontSize = Math.max(12, Math.min(24, this.currentFontSize + delta)); this.currentFontSize = Math.max(12, Math.min(36, this.currentFontSize + delta));
this.fontSizeDisplay.innerText = String(this.currentFontSize); this.fontSizeInput.value = String(this.currentFontSize);
this.renderCurrentPage(); await this.repaginateAndRender();
} }
/** /**
* 主题改变 * 字号输入框改变事件
*/ */
private onThemeChanged(): void { private async onFontSizeInputChanged(): Promise<void> {
new Notice('主题已切换,请刷新预览'); const val = parseInt(this.fontSizeInput.value, 10);
// TODO: 重新渲染文章 if (isNaN(val) || val < 12 || val > 36) {
this.fontSizeInput.value = String(this.currentFontSize);
new Notice('字号范围: 12-36');
return;
} }
this.currentFontSize = val;
/** await this.repaginateAndRender();
* 字体改变
*/
private onFontChanged(): void {
this.renderCurrentPage();
} }
/** /**
@@ -379,14 +365,59 @@ export class XiaohongshuPreview {
destroy(): void { destroy(): void {
this.topToolbar = null as any; this.topToolbar = null as any;
this.templateSelect = null as any; this.templateSelect = null as any;
this.themeSelect = null as any; this.fontSizeInput = null as any;
this.fontSelect = null as any;
this.fontSizeDisplay = null as any;
this.pageContainer = null as any; this.pageContainer = null as any;
this.bottomToolbar = null as any; this.bottomToolbar = null as any;
this.pageNavigation = null as any; this.pageNavigation = null as any;
this.pageNumberDisplay = null as any; this.pageNumberDisplay = null as any;
this.pages = []; this.pages = [];
this.currentFile = null; this.currentFile = null;
this.styleEl = null;
}
/** 组合并注入主题 + 高亮 + 自定义 CSS使用全局默认主题 */
private applyThemeCSS() {
if (!this.styleEl) return;
const themeName = this.settings.defaultStyle;
const highlightName = this.settings.defaultHighlight;
const theme = this.assetsManager.getTheme(themeName);
const highlight = this.assetsManager.getHighlight(highlightName);
const customCSS = (this.settings.useCustomCss || this.settings.customCSSNote.length>0) ? this.assetsManager.customCSS : '';
const baseCSS = this.settings.baseCSS ? `.note-to-mp {${this.settings.baseCSS}}` : '';
const css = `${highlight?.css || ''}\n\n${theme?.css || ''}\n\n${baseCSS}\n\n${customCSS}`;
this.styleEl.textContent = css;
this.currentThemeClass = theme?.className || '';
}
private async repaginateAndRender(): Promise<void> {
if (!this.articleHTML) return;
const totalBefore = this.pages.length || 1;
const posRatio = (this.currentPageIndex + 0.5) / totalBefore; // 以当前页中心作为相对位置
new Notice('重新分页中...');
const tempContainer = document.createElement('div');
tempContainer.innerHTML = this.articleHTML;
tempContainer.style.width = `${this.settings.sliceImageWidth}px`;
tempContainer.style.fontSize = `${this.currentFontSize}px`;
// 字体从全局主题中继承,无需手动指定
tempContainer.classList.add('note-to-mp');
tempContainer.className = this.currentThemeClass ? `note-to-mp ${this.currentThemeClass}` : 'note-to-mp';
document.body.appendChild(tempContainer);
try {
this.pages = await paginateArticle(tempContainer, this.settings);
if (this.pages.length > 0) {
const newIndex = Math.floor(posRatio * this.pages.length - 0.5);
this.currentPageIndex = Math.min(this.pages.length - 1, Math.max(0, newIndex));
} else {
this.currentPageIndex = 0;
}
this.renderCurrentPage();
new Notice(`重新分页完成:共 ${this.pages.length}`);
} catch (e) {
console.error('重新分页失败', e);
new Notice('重新分页失败');
} finally {
document.body.removeChild(tempContainer);
}
} }
} }

View File

@@ -458,12 +458,39 @@ label:hover {
.xhs-page-container { .xhs-page-container {
flex: 1; flex: 1;
overflow: auto; overflow-y: auto;
overflow-x: hidden;
display: flex; display: flex;
justify-content: center; flex-direction: column;
align-items: center; align-items: center;
padding: 20px; padding: 0px;
background: radial-gradient(ellipse at top, rgba(255,255,255,0.1) 0%, transparent 70%); background: radial-gradient(ellipse at top, rgba(255,255,255,0.1) 0%, transparent 70%);
min-height: 0; /* 允许 flex 子项正确收缩和滚动 */
}
/* 小红书单页包裹器:为缩放后的页面预留正确的布局空间 */
.xhs-page-wrapper {
/* 显示尺寸缩放后540 × 720 */
width: 540px;
height: 720px;
margin: 0px auto;
position: relative;
overflow: visible;
}
/* 小红书单页样式:实际尺寸 1080×1440通过 scale 缩放到 540×720 */
.xhs-page {
/* 实际尺寸由 renderPage 设置1080×1440 */
transform-origin: top left;
transform: scale(0.5); /* 540/1080 = 0.5 */
background: white;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border-radius: 8px;
}
.xhs-page img {
max-width: 100%;
height: auto;
} }
.xhs-top-toolbar { .xhs-top-toolbar {

View File

@@ -61,7 +61,9 @@
2. 顶部按钮适应窗口宽度,超出窗口,折行显示。 2. 顶部按钮适应窗口宽度,超出窗口,折行显示。
3. 小红书模式,页预览不完整 3. 小红书模式,html分页预览不是从顶部开始显示显示不完整
小红书模式,预览窗口似乎只显示了一部分?上面部分被挡住了吗?
参考微信公众号模式下的预览窗口,不同点在于小红书模式下,每页的宽高比按配置要求。
4. 修改: 4. 修改:
- 公共部分独立出来如“发布平台”放在新建platform-choose.ts中“发布平台”选择切换平台逻辑放在该模块中便于以后其他平台扩展。 - 公共部分独立出来如“发布平台”放在新建platform-choose.ts中“发布平台”选择切换平台逻辑放在该模块中便于以后其他平台扩展。
@@ -87,7 +89,21 @@
SOLVEobsidian控制台打印信息定位在哪里阻塞AI修复。 SOLVEobsidian控制台打印信息定位在哪里阻塞AI修复。
5. 把代码逻辑中的所有css移到styles.css中。 6. 把代码逻辑中的所有css移到styles.css中。
7. 小红书模式,页面渲染使用选择的主题。参考微信公众号模式进行渲染。
8. 需求:主题、字体、字大小变化时,需要重新分页。
去掉主题设置,使用全局主题设置。(❗️先简化,后续小红书和微信公众号应主题应该需要独立开。)
去掉字体设置,使用主题字体。
字体大小支持直接编辑。
问题:
- 字变大时,一页的内容放不下,重新分页应该会增加页数。但现在重新分页当前页放不下的内容只是被剪掉了。
- 表格显示不完整。
9. styles.css中有很多冗余。
问题:小红书预览布局有问题❓