从 Markdown 到图片卡片:MarkCard Studio 的功能设计与官网实现记录
说明:本文记录一个开源项目的功能设计与前端实现,不构成商业推荐。文中的功能和版本信息以项目当前代码为准。所有
IMAGE_URL_xx均为图片地址占位符,发布前需要替换为实际图床链接。
一、为什么要做 Markdown 卡片排版
我平时习惯用 Markdown 记录技术笔记。它适合组织标题、列表、代码块和图片,但当内容需要以图片形式发布时,通常还要经历一次重复排版:调整画布比例、拆分段落、处理代码高亮,再逐页导出。
MarkCard Studio 就是围绕这个过程做的一次桌面工具实践。它的基本思路并不复杂:保留 Markdown 作为内容源,根据预设规则把长文拆成多页,再将每一页渲染为固定尺寸的图片卡片。这样可以把"内容编写"和"视觉样式"分开处理,修改文字时不必重新搭建整套版式。

这个项目包含两个相关部分:
- MarkCard Studio 桌面端:负责 Markdown 编辑、分页、渲染与文件导出;
markcard-web官网前端:负责展示产品功能、主题效果、导出示例和使用说明。
本文先介绍桌面端的使用逻辑,再结合 markcard-web 仓库说明官网是如何组织的。
二、项目解决的主要问题
1. 长内容如何拆成多张卡片
将一篇长文直接截图,容易得到一张很长、字号又很小的图片。项目提供了几种拆分思路,例如按照二级标题、三级标题、自定义分隔符或内容长度分页。
使用标题分页时,一篇文章可以按照下面的方式组织:
markdown
# Rust 中的所有权
## 所有权规则
- 每个值都有一个所有者
- 同一时刻只能有一个所有者
- 所有者离开作用域后,值会被释放
## 借用与引用
借用允许在不转移所有权的前提下访问数据。
## 生命周期
生命周期用于描述引用保持有效的范围。
这种写法的好处是:Markdown 本身仍是一份结构完整的笔记,而标题同时也能成为自然的分页边界。

2. 同一份内容如何适配不同尺寸
图片平台常见的画布比例并不统一。项目内置了 3:4、9:16、6:7 和 1:1 等画布预设,也允许自定义宽高。切换尺寸时,内容源不变,分页结果和卡片布局会根据新画布重新计算。
这里有一个实际需要注意的问题:比例变化不是简单缩放。同一段文字在 1:1 画布中可能需要两页,在 9:16 画布中则可能只占一页。因此预览区既要展示单页效果,也要提供全部页面的总览,方便检查断页位置。
3. 技术内容中的代码、公式和图表
技术文章往往不只有普通段落,还包含代码块、数学公式、流程图、表格和任务列表。MarkCard Studio 的功能说明中包含以下内容类型:
- 使用 Highlight.js 处理代码语法高亮;
- 使用 KaTeX 渲染行内公式和块级公式;
- 使用 Mermaid 渲染流程图、时序图等图表;
- 支持表格、任务列表和 Callout 提示块。
例如,下面是一段可以放进技术卡片的 Mermaid 内容:
#mermaid-svg-ZjSBoEy6ml4lrRrH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZjSBoEy6ml4lrRrH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZjSBoEy6ml4lrRrH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZjSBoEy6ml4lrRrH .error-icon{fill:#552222;}#mermaid-svg-ZjSBoEy6ml4lrRrH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZjSBoEy6ml4lrRrH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZjSBoEy6ml4lrRrH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZjSBoEy6ml4lrRrH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZjSBoEy6ml4lrRrH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZjSBoEy6ml4lrRrH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZjSBoEy6ml4lrRrH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZjSBoEy6ml4lrRrH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZjSBoEy6ml4lrRrH .marker.cross{stroke:#333333;}#mermaid-svg-ZjSBoEy6ml4lrRrH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZjSBoEy6ml4lrRrH p{margin:0;}#mermaid-svg-ZjSBoEy6ml4lrRrH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZjSBoEy6ml4lrRrH .cluster-label text{fill:#333;}#mermaid-svg-ZjSBoEy6ml4lrRrH .cluster-label span{color:#333;}#mermaid-svg-ZjSBoEy6ml4lrRrH .cluster-label span p{background-color:transparent;}#mermaid-svg-ZjSBoEy6ml4lrRrH .label text,#mermaid-svg-ZjSBoEy6ml4lrRrH span{fill:#333;color:#333;}#mermaid-svg-ZjSBoEy6ml4lrRrH .node rect,#mermaid-svg-ZjSBoEy6ml4lrRrH .node circle,#mermaid-svg-ZjSBoEy6ml4lrRrH .node ellipse,#mermaid-svg-ZjSBoEy6ml4lrRrH .node polygon,#mermaid-svg-ZjSBoEy6ml4lrRrH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZjSBoEy6ml4lrRrH .rough-node .label text,#mermaid-svg-ZjSBoEy6ml4lrRrH .node .label text,#mermaid-svg-ZjSBoEy6ml4lrRrH .image-shape .label,#mermaid-svg-ZjSBoEy6ml4lrRrH .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZjSBoEy6ml4lrRrH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZjSBoEy6ml4lrRrH .rough-node .label,#mermaid-svg-ZjSBoEy6ml4lrRrH .node .label,#mermaid-svg-ZjSBoEy6ml4lrRrH .image-shape .label,#mermaid-svg-ZjSBoEy6ml4lrRrH .icon-shape .label{text-align:center;}#mermaid-svg-ZjSBoEy6ml4lrRrH .node.clickable{cursor:pointer;}#mermaid-svg-ZjSBoEy6ml4lrRrH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZjSBoEy6ml4lrRrH .arrowheadPath{fill:#333333;}#mermaid-svg-ZjSBoEy6ml4lrRrH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZjSBoEy6ml4lrRrH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZjSBoEy6ml4lrRrH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZjSBoEy6ml4lrRrH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZjSBoEy6ml4lrRrH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZjSBoEy6ml4lrRrH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZjSBoEy6ml4lrRrH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZjSBoEy6ml4lrRrH .cluster text{fill:#333;}#mermaid-svg-ZjSBoEy6ml4lrRrH .cluster span{color:#333;}#mermaid-svg-ZjSBoEy6ml4lrRrH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZjSBoEy6ml4lrRrH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZjSBoEy6ml4lrRrH rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZjSBoEy6ml4lrRrH .icon-shape,#mermaid-svg-ZjSBoEy6ml4lrRrH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZjSBoEy6ml4lrRrH .icon-shape p,#mermaid-svg-ZjSBoEy6ml4lrRrH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZjSBoEy6ml4lrRrH .icon-shape .label rect,#mermaid-svg-ZjSBoEy6ml4lrRrH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZjSBoEy6ml4lrRrH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZjSBoEy6ml4lrRrH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZjSBoEy6ml4lrRrH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Markdown 源文档
解析内容结构
按规则分页
应用画布与主题
导出图片或 PDF


4. 从预览到文件导出
项目目前展示了四类导出结果:
| 导出形式 | 适用情况 |
|---|---|
| PNG 图片包 | 保留清晰文字和透明信息,适合逐页使用 |
| JPG 图片包 | 希望适当减小文件体积时使用 |
| 拼接长图 | 需要连续阅读或保存为单张图片时使用 |
| 多页 PDF | 用于归档、阅读或打印 |
导出前最好检查三件事:文字是否溢出、分页位置是否自然、本地图片是否正确解析。尤其是代码块和表格,它们的宽度往往比普通段落更容易超出卡片边界。
三、主题系统不是简单换一个背景色
当前项目展示了 16 套主题,包括瑞士网格、暖阳日记、清新绿洲、墨色简报、苹果备忘录、复古报刊、暗黑极客、包豪斯拼贴、Riso 果酱、Y2K 银翼和蓝图工坊等风格。
从实现目标看,一套主题至少需要协调以下元素:
- 页面背景、内容区域和边框;
- 标题、正文、引用和链接的字号与颜色;
- 代码块、表格和 Callout 的独立样式;
- 页眉、页脚与页码;
- 装饰图形、纹理或贴纸;
- 深浅色之间的对比度。
因此,主题更接近一组完整的排版规则,而不是单独的颜色变量。同一份 Markdown 在不同主题下仍要保持标题层级清晰、正文可读、代码块不溢出,这比追求单张预览图的视觉效果更重要。
项目还使用了部分 OpenMoji 贴纸素材。此类第三方资源不能只把文件放进项目,还要保留来源和许可说明。OpenMoji 使用 CC BY-SA 4.0 许可,仓库的 public/stickers/openmoji/README.md 中记录了对应署名。
四、本地优先在这个项目中意味着什么
桌面端基于 Tauri 2 和 Rust 构建。这里所说的"本地优先",主要指文档读取、Markdown 解析、图片引用、页面渲染和文件导出在用户设备上完成,日常编辑不依赖一个远程排版后端。
可以把数据流简化为:
text
本地 Markdown / 本地图片
↓
桌面端解析与分页
↓
主题渲染与预览检查
↓
PNG / JPG / 长图 / PDF
本地处理有两个直接影响:一是断网时仍可以完成主要编辑和导出流程;二是引用相对路径图片时,不必先把图片上传到图床。
不过,"桌面端本地处理"和"项目官网"应当区分开。当前官网代码中接入了访问统计脚本,因此不能把桌面应用的数据处理方式泛化为整个网站的网络行为。写隐私说明时,应该分别描述应用和网站,并以实际代码及隐私政策为准。
五、一次完整的使用流程
下面用一篇技术笔记举例。
第一步:整理 Markdown 结构
先使用标题划分章节,尽量让每个章节只表达一个主题。超长代码块可以提前拆分,图片则使用 Markdown 标准语法引用:
markdown
如果打开的是本地 .md 文件,相对路径应当相对于文档所在目录解析。移动文档时,需要连同图片目录一起移动,否则预览可能找不到资源。
第二步:选择画布和主题
根据最终用途选择比例,然后切换主题。这个阶段应重点检查正文可读性,不必为了填满卡片而堆叠内容。若页面底部只剩一两行,可以调整分页位置或略微修改段落结构。
第三步:检查并导出
在总览中依次检查每一页,确认页码、标题、图片和代码块没有异常,再选择导出格式。需要逐张使用时可以导出 PNG 或 JPG;需要连续阅读时可以选择长图或 PDF。
六、官网前端如何组织
markcard-web 是一个独立的静态前端项目,技术栈比较精简:
| 类别 | 使用方案 |
|---|---|
| 前端框架 | Vue 3.5、Composition API、<script setup> |
| 构建工具 | Vite 8 |
| 样式 | Tailwind CSS 4 |
| 图标 | Lucide Vue |
| 多语言 | 基于 Vue 响应式 API 的轻量字典 |
| 包管理器 | pnpm |
页面入口 src/App.vue 没有承担过多业务逻辑,而是按页面区块组合组件:
vue
<template>
<div class="min-h-screen bg-slate-900 text-slate-100">
<Navbar />
<HeroSection />
<FeaturesSection />
<ThemeGallery />
<WorkflowSection />
<RichContentSection />
<PrivacySection />
<ExportSection />
<DownloadSection />
<FaqSection />
<FooterSection />
</div>
</template>
这种结构适合内容型单页站点:组件边界与页面章节一致,某一部分的内容或布局变化时,不需要修改整个页面。
1. 用组合式函数管理中英文内容
项目没有引入完整的国际化框架,而是在 src/composables/useI18n.js 中维护响应式语言状态和字典。核心逻辑可以概括为:
javascript
import { ref, computed } from 'vue'
const currentLang = ref('zh-CN')
const t = computed(() => dictionary[currentLang.value])
function toggleLanguage() {
currentLang.value = currentLang.value === 'zh-CN' ? 'en' : 'zh-CN'
}
这一方案的优点是依赖少、理解成本低,适合当前只有两种语言且页面结构固定的场景。需要注意的是,新增字段时必须同步维护两套字典,否则组件读取到的内容可能为空。
项目还把截图和 PDF 地址放进语言字典中。切换语言时,不仅文字变化,示例素材也会一起切换:
javascript
hero: {
screenshotUrl: '/imgs/ZH/首页_ZH.webp'
},
export: {
pdfUrl: '/imgs/ZH/markcard-document-11pages.pdf'
}
这比在组件内部到处判断语言更容易维护。
2. 用数据驱动主题画廊
主题画廊没有为 16 个主题分别编写组件,而是用数组保存主题元数据,再通过计算属性完成分类筛选:
javascript
const activeCategory = ref('all')
const filteredThemes = computed(() => {
if (activeCategory.value === 'all') return allThemes
return allThemes.filter(theme => theme.category === activeCategory.value)
})
模板只负责遍历 filteredThemes。以后增加主题时,主要工作是补充数据和预览资源,不需要复制卡片结构。
3. 静态资源与缓存
中文和英文的产品截图分别放在 public/imgs/ZH/ 与 public/imgs/EN/ 下。Vercel 配置为图片、壁纸和贴纸设置了长期缓存,而页面入口每次访问都会向服务器确认是否存在新版本。
这里有一个容易忽略的细节:Vite 构建出来的 JS 和 CSS 文件名带有内容哈希,文件变化后 URL 也会变化;但 public 目录中的文件不会自动增加哈希。因此替换公开目录中的图片时,最好使用新文件名并同步更新引用,避免浏览器继续读取旧缓存。
七、本地运行官网项目
官网项目不需要后端服务,也不依赖环境变量。根据仓库当前说明,开发环境需要 Node.js 20.19+ 或 22.12+,以及 pnpm 9+。
bash
git clone https://github.com/pangxiaobin/markcard-web.git
cd markcard-web
pnpm install
pnpm dev
完成修改后,可以执行生产构建进行检查:
bash
pnpm build
pnpm preview
生产文件会生成到 dist/ 目录,可部署到支持静态资源托管的环境。如果部署在域名子路径下,还需要配置 Vite 的 base,并检查以 /imgs/... 开头的资源路径。
八、开发过程中值得记录的几点
1. 内容结构应先于视觉样式
Markdown 卡片的核心仍然是内容。先确定标题、段落和分页边界,再选择主题,通常比先套样式再填文字更稳定。
2. 示例素材也是功能测试
仓库里保留了中英文多页卡片和 PDF 示例。它们不仅用于展示,也可以作为回归检查样本:主题、字体或分页逻辑发生变化后,重新导出同一份文档,就能较直观地发现布局差异。
3. 对外描述要与代码边界一致
桌面应用、官网和第三方托管服务承担的职责不同。诸如"本地处理""离线可用""访问统计"等描述都应该说明适用范围,避免一句话覆盖所有模块。
4. 第三方素材需要保留许可信息
图标、字体、贴纸和示例图片都可能有各自的许可条件。开源项目除了公开源代码,也应把素材来源、修改情况和署名方式记录清楚。
九、目前的边界与后续方向
MarkCard Studio 当前版本为 v0.1.0,仍处在早期阶段。现有功能覆盖 Markdown 分页、主题切换、画布预设、富文本渲染和多格式导出,但不同操作系统、字体环境以及复杂 Markdown 文档仍可能带来兼容性问题。
后续可以继续关注这些方向:
- 为分页算法补充更复杂的文档样本和回归测试;
- 改善超长代码行、宽表格和大尺寸公式的适配;
- 统一主题设计令牌,降低新增主题的维护成本;
- 为官网补充更明确的应用与网站隐私边界说明;
- 持续检查键盘操作、对比度和减少动态效果等可访问性细节。
十、总结
MarkCard Studio 的重点并不是替代完整的设计软件,而是尝试建立一条更直接的内容转换路径:以 Markdown 保存结构化内容,通过规则完成分页,再应用画布和主题,最后导出为图片或 PDF。
对我来说,这个项目比较有价值的实践包括三个方面:桌面端如何处理本地文档与图片、卡片主题如何保持内容可读性,以及 Vue 单页官网如何用组件化和数据驱动的方式展示较多示例。即使不使用这套工具,其中关于内容与样式分离、静态资源缓存、第三方素材许可的处理方式,也可以复用到其他内容型项目中。
项目资料:
- 官网前端仓库:https://markcard.woollypix.cn/
- 桌面端仓库:
https://github.com/pangxiaobin/MarkCardStudio - 开源许可:GNU General Public License v3.0
- OpenMoji 素材许可:CC BY-SA 4.0