├── .eslintrc.json ├── .gitignore ├── README.md ├── app ├── favicon.ico ├── fonts │ ├── GeistMonoVF.woff │ └── GeistVF.woff ├── globals.css ├── layout.tsx └── page.tsx ├── article.md ├── components ├── ContactModal.tsx ├── EditorPanel.tsx ├── Preview.tsx ├── PreviewPanel.tsx └── ThemeSwitcher.tsx ├── contexts └── ThemeContext.tsx ├── docs ├── promotion.md └── theme-optimization.md ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── postcss.config.mjs ├── public ├── file.svg ├── globe.svg ├── images │ ├── product.png │ ├── wx-qr-placeholder.svg │ └── wx-qr.jpg ├── next.svg ├── vercel.svg └── window.svg ├── tailwind.config.js ├── themes ├── dark.ts ├── default.ts ├── elegant.ts ├── index.ts ├── minimalist.ts ├── nature.ts ├── soft.ts └── warm.ts ├── tsconfig.json └── utils ├── defaultContent.ts ├── markdownConverter.ts └── themeUtils.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # next.js 10 | /.next/ 11 | /out/ 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # local env files 26 | .env*.local 27 | 28 | # vercel 29 | .vercel 30 | 31 | # typescript 32 | *.tsbuildinfo 33 | next-env.d.ts 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ✨ 飞书文档转公众号排版工具 2 | 3 |
4 | 飞书文档转公众号排版工具 5 | 6 |

一个强大的在线工具,助您快速将飞书文档转换为美观的微信公众号文章

7 | 8 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 9 | [![Next.js](https://img.shields.io/badge/Next.js-14-black?logo=next.js)](https://nextjs.org/) 10 | [![React](https://img.shields.io/badge/React-18-blue?logo=react)](https://reactjs.org/) 11 | [![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue?logo=typescript)](https://www.typescriptlang.org/) 12 |
13 | 14 | ## 🚀 核心功能 15 | 16 | ### 📝 一键转换 17 | - **智能解析**:从飞书文档直接复制,自动转换为标准 Markdown 格式 18 | - **格式保持**:完美保留原文档的排版结构和样式 19 | 20 | ### 🎨 多样主题 21 | - **科技蓝**:现代科技风格,适合技术类文章 22 | - **商务橙**:商务专业风格,适合商业内容 23 | - **极简黑**:简约黑白风格,专注内容本身 24 | - **清新绿**:清新自然风格,适合生活类文章 25 | - **典雅紫**:优雅紫色风格,适合文艺内容 26 | 27 | ### 📱 双屏预览 28 | - **手机预览**:模拟微信公众号手机端阅读效果 29 | - **电脑预览**:适配桌面端显示效果 30 | - **实时同步**:编辑内容实时预览,所见即所得 31 | 32 | ### 🎯 完整支持 33 | - ✅ **图片处理**:支持图片上传和链接引用 34 | - ✅ **表格渲染**:完美显示复杂表格结构 35 | - ✅ **代码高亮**:多语言代码块语法高亮 36 | - ✅ **列表格式**:有序、无序列表完美支持 37 | - ✅ **引用块**:支持多级引用和特殊标注 38 | 39 | ### 🚧 计划功能 40 | - 🔄 **数学公式**:LaTeX 数学公式渲染(开发中) 41 | 42 | ## 📖 使用指南 43 | 44 | ### 第一步:准备内容 45 | 1. 打开您的飞书文档 46 | 2. 选择需要转换的内容 47 | 3. 使用 `Ctrl+C` (Windows) 或 `Command+C` (Mac) 复制 48 | 49 | ### 第二步:粘贴转换 50 | 1. 在工具的左侧编辑器中粘贴内容 51 | 2. 系统自动识别并转换格式 52 | 3. 查看右侧预览区域的效果 53 | 54 | ### 第三步:选择主题 55 | 1. 在工具右侧选择合适的主题样式 56 | 2. 预览不同主题的显示效果 57 | 3. 根据文章类型选择最佳主题 58 | 59 | ### 第四步:导出使用 60 | 1. 点击"复制到公众号"按钮 61 | 2. 粘贴到微信公众号编辑器 62 | 3. 发布您的美观文章 63 | 64 | ## 🛠️ 本地开发 65 | 66 | ### 环境要求 67 | - Node.js 18+ 68 | - npm 或 yarn 69 | 70 | ### 快速开始 71 | 72 | ```bash 73 | # 克隆项目 74 | git clone https://github.com/mengjian-github/lark-to-markdown.git 75 | cd lark-to-markdown 76 | 77 | # 安装依赖 78 | npm install 79 | 80 | # 启动开发服务器 81 | npm run dev 82 | 83 | # 在浏览器中打开 http://localhost:3000 84 | ``` 85 | 86 | ### 构建部署 87 | 88 | ```bash 89 | # 构建生产版本 90 | npm run build 91 | 92 | # 启动生产服务器 93 | npm start 94 | 95 | # 静态导出(可选) 96 | npm run export 97 | ``` 98 | 99 | ## 🔧 技术栈 100 | 101 | | 技术 | 版本 | 用途 | 102 | |------|------|------| 103 | | **Next.js** | 14.0.4 | React 全栈框架 | 104 | | **React** | 18.2.0 | 用户界面库 | 105 | | **TypeScript** | 5.3.3 | 类型安全的 JavaScript | 106 | | **TailwindCSS** | 3.3.6 | 原子化 CSS 框架 | 107 | | **React Markdown** | 9.0.1 | Markdown 渲染组件 | 108 | | **Turndown** | 7.1.2 | HTML 到 Markdown 转换 | 109 | 110 | ## 🌐 在线使用 111 | 112 | 🔗 **访问地址**: [https://www.larkmd.online/](https://www.larkmd.online/) 113 | 114 | ## 📱 联系作者 115 | 116 |
117 |

遇到问题或有建议?随时联系我!

118 | 119 | **点击工具右上角"联系作者"按钮可以:** 120 | - 📱 扫码添加作者微信 121 | - 🐱 访问 GitHub 项目 122 | - 💬 反馈问题和建议 123 | - 📚 获取使用帮助 124 |
125 | 126 | ## 🤝 贡献指南 127 | 128 | 欢迎提交 Issue 和 Pull Request! 129 | 130 | 1. Fork 本项目 131 | 2. 创建特性分支 (`git checkout -b feature/AmazingFeature`) 132 | 3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) 133 | 4. 推送到分支 (`git push origin feature/AmazingFeature`) 134 | 5. 开启 Pull Request 135 | 136 | ## 📄 开源协议 137 | 138 | 本项目采用 MIT 协议 - 查看 [LICENSE](LICENSE) 文件了解详情 139 | 140 | --- 141 | 142 |
143 |

⭐ 如果这个项目对您有帮助,请给它一个 Star!

144 |

💡 提示: 支持飞书文档的所有常用格式,让您的公众号文章更加专业美观

145 |
146 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjian-github/lark-to-markdown/b68c0b450dd8855bd1a74a5694e846b945c72bfc/app/favicon.ico -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjian-github/lark-to-markdown/b68c0b450dd8855bd1a74a5694e846b945c72bfc/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjian-github/lark-to-markdown/b68c0b450dd8855bd1a74a5694e846b945c72bfc/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* 编辑器样式 */ 6 | .w-md-editor { 7 | --md-editor-bg-color: #ffffff !important; 8 | --md-editor-text-color: #1f2937 !important; 9 | } 10 | 11 | /* 手机预览模式样式 */ 12 | .mobile-preview { 13 | width: 375px; 14 | background: white; 15 | border-radius: 40px; 16 | padding: 20px 0; 17 | box-shadow: 0 0 0 10px #f3f4f6, 0 0 0 11px #e5e7eb; 18 | position: relative; 19 | } 20 | 21 | .phone-notch { 22 | position: absolute; 23 | top: 0; 24 | left: 50%; 25 | transform: translateX(-50%); 26 | width: 200px; 27 | height: 20px; 28 | background: #000; 29 | border-bottom-left-radius: 20px; 30 | border-bottom-right-radius: 20px; 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | gap: 8px; 35 | } 36 | 37 | .phone-speaker { 38 | width: 60px; 39 | height: 6px; 40 | background: #222; 41 | border-radius: 3px; 42 | } 43 | 44 | .phone-camera { 45 | width: 10px; 46 | height: 10px; 47 | background: #222; 48 | border-radius: 50%; 49 | } 50 | 51 | /* Tooltip 样式 */ 52 | [title] { 53 | position: relative; 54 | } 55 | 56 | [title]:hover::before { 57 | content: attr(title); 58 | position: absolute; 59 | right: calc(100% + 8px); 60 | top: 50%; 61 | transform: translateY(-50%); 62 | padding: 4px 8px; 63 | background: rgba(0, 0, 0, 0.8); 64 | color: white; 65 | font-size: 12px; 66 | border-radius: 4px; 67 | white-space: nowrap; 68 | pointer-events: none; 69 | opacity: 0; 70 | animation: tooltipFadeIn 0.1s ease-out forwards; 71 | } 72 | 73 | @keyframes tooltipFadeIn { 74 | to { 75 | opacity: 1; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Metadata } from 'next'; 3 | import './globals.css'; 4 | 5 | export const metadata: Metadata = { 6 | title: '飞书文档转公众号 | 在线排版工具', 7 | description: '专业的飞书文档转微信公众号编辑器,支持一键排版、Markdown 编辑、实时预览。完美支持飞书文档图片、表格、代码块等格式转换,让公众号排版更轻松。', 8 | keywords: '飞书文档,微信公众号,排版工具,markdown编辑器,在线排版,文档转换,飞书转公众号,公众号编辑器', 9 | authors: [{ name: '飞书文档转换工具' }], 10 | openGraph: { 11 | title: '飞书文档转公众号 | 在线排版工具', 12 | description: '一键将飞书文档转换为美观的公众号文章,支持图片、表格、代码等完整格式', 13 | type: 'website', 14 | }, 15 | }; 16 | 17 | export default function RootLayout({ 18 | children, 19 | }: { 20 | children: React.ReactNode; 21 | }): React.JSX.Element { 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {children} 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useEffect, useState } from 'react'; 4 | import '@uiw/react-md-editor/markdown-editor.css'; 5 | import { ThemeProvider, useTheme } from '../contexts/ThemeContext'; 6 | import EditorPanel from '../components/EditorPanel'; 7 | import PreviewPanel from '../components/PreviewPanel'; 8 | import { defaultContent } from '../utils/defaultContent'; 9 | import { convertHtmlToMarkdown, processHtmlForWeixin } from '../utils/markdownConverter'; 10 | 11 | const Home: React.FC = () => { 12 | const [markdown, setMarkdown] = useState(defaultContent); 13 | const [mounted, setMounted] = useState(false); 14 | const [copied, setCopied] = useState(false); 15 | const [copyError, setCopyError] = useState(null); 16 | const [isCopying, setIsCopying] = useState(false); 17 | const [isMobilePreview, setIsMobilePreview] = useState(true); 18 | const { themeName, setTheme } = useTheme(); 19 | 20 | useEffect(() => { 21 | setMounted(true); 22 | document.documentElement.setAttribute('data-color-mode', 'light'); 23 | }, []); 24 | 25 | const handleCopyToWeixin = async () => { 26 | // 重置状态 27 | setCopied(false); 28 | setCopyError(null); 29 | setIsCopying(true); 30 | 31 | try { 32 | document.body.style.cursor = 'wait'; 33 | 34 | // 获取预览内容 35 | const contentElement = isMobilePreview 36 | ? document.querySelector('.mobile-preview .markdown-body') || document.querySelector('.mobile-preview .px-2 > div') 37 | : document.querySelector('.max-w-\\[780px\\] .markdown-body') || document.querySelector('.flex-1.overflow-auto.px-4 > div'); 38 | 39 | if (!contentElement) { 40 | throw new Error('无法获取预览内容,请尝试刷新页面或切换预览模式'); 41 | } 42 | 43 | const previewContent = contentElement.outerHTML; 44 | 45 | // 处理HTML内容以便复制到微信公众号 46 | const processedHtml = processHtmlForWeixin(previewContent); 47 | 48 | // 创建包含处理后样式的HTML blob 49 | const blob = new Blob([processedHtml], { type: 'text/html' }); 50 | 51 | // 检查 navigator.clipboard 是否可用 52 | if (!navigator.clipboard || !navigator.clipboard.write) { 53 | throw new Error('您的浏览器不支持高级剪贴板功能,请尝试使用Chrome或Edge浏览器'); 54 | } 55 | 56 | // 复制到剪贴板 57 | await navigator.clipboard.write([ 58 | new ClipboardItem({ 59 | 'text/html': blob 60 | }) 61 | ]); 62 | 63 | // 显示成功提示 64 | setCopied(true); 65 | 66 | // 添加成功动画效果 67 | const copyBtn = document.querySelector('.copy-btn') as HTMLElement; 68 | if (copyBtn) { 69 | copyBtn.classList.add('copy-success'); 70 | setTimeout(() => copyBtn.classList.remove('copy-success'), 1000); 71 | } 72 | 73 | setTimeout(() => setCopied(false), 3000); 74 | } catch (error) { 75 | console.error('复制失败:', error); 76 | setCopyError(error instanceof Error ? error.message : '未知错误'); 77 | setTimeout(() => setCopyError(null), 5000); 78 | } finally { 79 | document.body.style.cursor = ''; 80 | setIsCopying(false); 81 | } 82 | }; 83 | 84 | const handlePaste = (e: React.ClipboardEvent) => { 85 | const html = e.clipboardData.getData('text/html'); 86 | if (html) { 87 | e.preventDefault(); 88 | const markdownContent = convertHtmlToMarkdown(html); 89 | setMarkdown(markdownContent); 90 | } 91 | }; 92 | 93 | if (!mounted) { 94 | return ( 95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | ); 106 | } 107 | 108 | return ( 109 |
110 | {/* 左侧编辑器 */} 111 | setMarkdown(value || '')} 114 | onPaste={handlePaste} 115 | /> 116 | 117 | {/* 右侧预览 */} 118 | 129 | 130 | 165 |
166 | ); 167 | }; 168 | 169 | // 包装组件 170 | export default function App() { 171 | return ( 172 | 173 | 174 | 175 | ); 176 | } 177 | -------------------------------------------------------------------------------- /article.md: -------------------------------------------------------------------------------- 1 | # 🔥爆肝体验:Claude 3.7 Sonnet MAX值这个"智商税"吗?一晚8美元,肠子都悔青了! 2 | 3 | ![Claude 3.7 Sonnet MAX发布图] 4 | 5 | 深夜里,钱包又瘦了8美元,我却舍不得关掉Cursor... 6 | 7 | 各位读者好,**不要学我这个AI败家玩家**!今天带大家看看我如何在一个小时内烧掉一顿海底捞的钱,只为体验号称"智力天花板"的Claude 3.7 Sonnet MAX! 8 | 9 | Cursor偷偷上线了对"MAX"版本的支持,这是在Claude 3.7 Sonnet基础上的增强版,他们称之为"Maximum intelligence, context and thinking",还美其名曰"智能的极限体验"。然而这个所谓的"极限体验"背后,藏着一个残酷真相:**每次高级调用加收$0.05**!天啊,谁能想到玩AI也能"氪金"?这在AI界简直是"尊享VIP服务"了! 10 | 11 | 这不,**收到Cursor更新支持新模型的消息,手都在颤抖**,毫不犹豫地点击了升级,内心有个声音说:"应该不会花太多吧..." 12 | 13 | ## 🚀 初体验:献祭我的主页,换取"MAX"快感 14 | 15 | 怀着对未来科技的无限期待,我把辛苦开发的个人主页扔给了Claude 3.7 MAX: 16 | 17 | ![个人主页原版截图] 18 | 19 | 它立刻给我返回了一堆专业到让我头晕的UI/UX分析。这分析看得我头大如斗,于是我直接挥手:"别BB,直接上手改!" 20 | 21 | ![Claude的分析建议] 22 | 23 | ## 💻 改造过程:当"超长上下文"撞上我的超短耐心 24 | 25 | 天啊,Claude开始了疯狂操作!那一刻我才真正理解什么叫"Maximum context" - **它同时索引和理解了十多个项目文件,简直像看透了我代码的一切**。而我只是目瞪口呆地看着屏幕上飞速滚动的代码和分析,心里默默感叹:"这...就是贵的力量吗?" 26 | 27 | 然而,第一版代码出现bug时,我内心是崩溃的:"我花了这么多钱,竟然还有bug???" 28 | 29 | ![代码报错截图] 30 | 31 | 修复后的第一版设计让我陷入了沉思: 32 | 33 | ![优化后的个人主页第一版] 34 | 35 | **等等,我付了高价就为了这个?** 这渐变色简直像是90年代网页设计的回魂!那一刻,我感受到了什么叫做"期望越高,失望越大"。作为一个完美主义者,我咬着牙继续要求优化,心想:"钱都花了,总得整出个像样的东西吧!" 36 | 37 | ## ✨ 二次优化:深夜惊喜,让我舍不得睡觉 38 | 39 | 第二次优化后,我的表情从嫌弃变成了惊喜: 40 | 41 | ![优化后的个人主页最终版] 42 | 43 | 整个网站焕然一新,Claude 3.7的标志性蓝紫色调,有那么一瞬间,我感觉自己像是拥有了整个银河系。那一刻我心想:"这才是我要的感觉啊!"同时忍不住在深夜发出满足的微笑。 44 | 45 | ## 💰 账单来袭:心脏骤停的瞬间 46 | 47 | 然而,**快乐总是短暂的**。当我回到Cursor查看账单时,只感觉心脏骤停: 48 | 49 | ![Cursor账单截图] 50 | 51 | **$8.10!!!** 其中144个premium tool call占了$7.20! 52 | 53 | 那一刻我的表情大概是:😱 54 | 55 | 手忙脚乱地切回页面,想叫停,**已经来不及了**!心痛如刀绞,脑海里全是"这钱本可以...": 56 | 57 | - 这钱本可以买两杯星巴克... 58 | - 这钱本可以点一顿还不错的外卖... 59 | - 这钱本可以...好吧,在上海也就半个烤冷面吧... 60 | 61 | ## 🤔 值不值?凌晨三点的灵魂拷问 62 | 63 | 凌晨三点,我抱着空空的钱包,陷入了灵魂拷问: 64 | 65 | 1. **能力确实强得离谱**:它读懂我的代码比我的前任更了解我,它理解我的需求比我的产品经理更专业 66 | 2. **效率简直令人发指**:它写代码的速度,让我怀疑我这么多年的加班都是为了什么? 67 | 3. **价格...太痛了**:每一次点击都像是在刷一次外卖,压力山大 68 | 4. **使用场景有限**:除非老板报销,否则真的会"肉疼" 69 | 70 | **最悲哀的是** - 它给我的结果确实比我自己折腾几天可能还要好...这是我不愿承认但不得不面对的事实。 71 | 72 | ## 互动话题:你会为了"更聪明"的AI买单吗? 73 | 74 | 你是不是也曾在深夜里,为了一个问题或创意,掏出信用卡"氪金"体验各种新工具?面对越来越智能但也越来越贵的AI服务,你的钱包准备好了吗? 75 | 76 | **我们到底该在什么时候刷卡,什么时候止损?** 77 | 78 | **什么场景值得我们"挥金如土"?**是不是当你深夜加班,死线在即,老板还在催,这时花$8解决问题,好像也不是完全不能接受? 79 | 80 | 欢迎在评论区和我一起吐槽你的"AI氪金"心路历程!顺便说一下,你希望我用这个"烧钱神器"完成什么任务?(友情提示:你出主意,我出钱😂) 81 | 82 | 关注我,一起见证科技进步(和钱包消瘦)的每一天!下期预告:我将用Claude 3.7 MAX重写公司核心代码,看看能否一步到位把整个团队干掉...(如果账户余额允许的话) 83 | 84 | #AI智商税 #Claude3.7 #人工智能 #深夜剁手 #Anthropic新品 #程序员的崩溃 -------------------------------------------------------------------------------- /components/ContactModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FiX, FiGithub, FiMail, FiMessageCircle, FiHeart, FiStar } from 'react-icons/fi'; 3 | 4 | interface ContactModalProps { 5 | visible: boolean; 6 | onCancel: () => void; 7 | } 8 | 9 | const ContactModal: React.FC = ({ 10 | visible, 11 | onCancel 12 | }) => { 13 | if (!visible) return null; 14 | 15 | return ( 16 |
17 | {/* 背景遮罩 */} 18 |
22 | 23 | {/* 模态框内容 */} 24 |
25 | {/* 关闭按钮 */} 26 | 32 | 33 | {/* 头部区域 - 简化 */} 34 |
35 |
36 | 37 |
38 |

飞书转公众号工具

39 |

让文档转换更简单

40 |
41 | 42 | {/* 二维码区域 - 紧凑布局 */} 43 |
44 |
45 |
46 | 47 | 扫码添加作者微信 48 |
49 |
50 |
51 |
52 | 作者微信二维码 { 57 | const target = e.target as HTMLImageElement; 58 | if (target.src.includes('wx-qr.jpg')) { 59 | target.src = '/images/wx-qr-placeholder.svg'; 60 | } else { 61 | target.style.display = 'none'; 62 | const placeholder = target.nextElementSibling as HTMLElement; 63 | if (placeholder) { 64 | placeholder.style.display = 'flex'; 65 | } 66 | } 67 | }} 68 | /> 69 |
70 | 二维码 71 |
72 |
73 |
74 |
75 |

长按识别或扫描二维码

76 |
77 |
78 | 79 | 问题咨询 80 |
81 |
82 | 83 | 功能建议 84 |
85 |
86 |
87 |
88 |
89 |
90 | 91 | {/* 联系方式 - 简化 */} 92 |
93 |
94 |
95 |
96 | 97 |
98 |
微信
99 |
100 | 101 |
{ 104 | window.open('https://github.com/mengjian-github/lark-to-markdown', '_blank'); 105 | }} 106 | > 107 |
108 | 109 |
110 |
GitHub
111 |
112 | 113 |
114 |
115 | 116 |
117 |
邮箱
118 |
119 |
120 |
121 | 122 | {/* 反馈区域 - 简化 */} 123 |
124 |
125 |
126 | 127 | 支持项目 128 |
129 |
130 |
131 | 132 | 觉得有用?给个 ⭐ 支持 133 |
134 |
135 | 136 | 分享给更多朋友 137 |
138 |
139 |
140 |
141 | 142 | {/* 底部按钮 */} 143 |
144 | 150 |
151 |
152 | 153 | 169 |
170 | ); 171 | }; 172 | 173 | export default ContactModal; -------------------------------------------------------------------------------- /components/EditorPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import dynamic from 'next/dynamic'; 3 | 4 | // 动态导入MDEditor以避免SSR问题 5 | const MDEditor = dynamic( 6 | () => import('@uiw/react-md-editor').then((mod) => mod.default), 7 | { ssr: false } 8 | ); 9 | 10 | interface EditorPanelProps { 11 | markdown: string; 12 | onChange: (value: string | undefined) => void; 13 | onPaste: (e: React.ClipboardEvent) => void; 14 | } 15 | 16 | const EditorPanel: React.FC = ({ markdown, onChange, onPaste }) => { 17 | return ( 18 |
19 |

20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 飞书文档转公众号 28 | 排版工具 29 |

30 |
31 | 46 |
47 |
48 | ); 49 | }; 50 | 51 | export default EditorPanel; -------------------------------------------------------------------------------- /components/Preview.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import ReactMarkdown from 'react-markdown'; 3 | import remarkGfm from 'remark-gfm'; 4 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; 5 | import { oneLight } from 'react-syntax-highlighter/dist/cjs/styles/prism'; 6 | import type { Components } from 'react-markdown'; 7 | import Image from 'next/image'; 8 | import { generateInlineStyles } from '../utils/themeUtils'; 9 | import { useTheme } from '../contexts/ThemeContext'; 10 | 11 | interface PreviewProps { 12 | content: string; 13 | } 14 | 15 | interface ComponentProps { 16 | children?: ReactNode; 17 | [key: string]: any; 18 | } 19 | 20 | const Preview: React.FC = ({ content }) => { 21 | const { currentTheme } = useTheme(); 22 | const styles = generateInlineStyles(currentTheme); 23 | 24 | const components: Partial = { 25 | h1: ({ children }: ComponentProps) =>

{children}

, 26 | h2: ({ children }: ComponentProps) =>

{children}

, 27 | h3: ({ children }: ComponentProps) =>

{children}

, 28 | h4: ({ children }: ComponentProps) =>

{children}

, 29 | h5: ({ children }: ComponentProps) =>
{children}
, 30 | h6: ({ children }: ComponentProps) =>
{children}
, 31 | p: ({ children }: ComponentProps) =>

{children}

, 32 | img: ({ src, alt }: ComponentProps) => ( 33 | src ? ( 34 |
35 | {alt 45 |
46 | ) : null 47 | ), 48 | pre: ({ children }: ComponentProps) =>
{children}
, 49 | code: ({ inline, className, children }: ComponentProps & { inline?: boolean; className?: string }) => { 50 | const match = /language-(\w+)/.exec(className || ''); 51 | const content = String(children).replace(/\n$/, ''); 52 | 53 | if (!inline && match) { 54 | // 代码块 55 | return ( 56 | 72 | {content} 73 | 74 | ); 75 | } else { 76 | // 行内代码 77 | return {children}; 78 | } 79 | }, 80 | table: ({ children }: ComponentProps) => {children}
, 81 | th: ({ children, style: cellStyle }: ComponentProps) => ( 82 | {children} 83 | ), 84 | td: ({ children, style: cellStyle }: ComponentProps) => ( 85 | {children} 86 | ), 87 | blockquote: ({ children }: ComponentProps) =>
{children}
, 88 | ul: ({ children, depth = 0 }: ComponentProps & { depth?: number }) => ( 89 |
    90 | {children} 91 |
92 | ), 93 | ol: ({ children, depth = 0 }: ComponentProps & { depth?: number }) => ( 94 |
    95 | {children} 96 |
97 | ), 98 | li: ({ children }: ComponentProps) =>
  • {children}
  • , 99 | a: ({ children, href }: ComponentProps) => ( 100 | 101 | {children} 102 | 103 | ), 104 | hr: () =>
    , 105 | strong: ({ children }: ComponentProps) => {children}, 106 | em: ({ children }: ComponentProps) => {children}, 107 | }; 108 | 109 | return ( 110 |
    111 | 115 | {content} 116 | 117 |
    118 | ); 119 | }; 120 | 121 | export default Preview; 122 | -------------------------------------------------------------------------------- /components/PreviewPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { FiCopy, FiCheck, FiSmartphone, FiMonitor, FiAlertCircle, FiMessageCircle } from 'react-icons/fi'; 3 | import Preview from './Preview'; 4 | import { ThemeName } from '../contexts/ThemeContext'; 5 | import ThemeSwitcher from './ThemeSwitcher'; 6 | import ContactModal from './ContactModal'; 7 | 8 | interface PreviewPanelProps { 9 | markdown: string; 10 | isMobilePreview: boolean; 11 | setIsMobilePreview: (value: boolean) => void; 12 | themeName: ThemeName; 13 | setTheme: (name: ThemeName) => void; 14 | copied: boolean; 15 | copyError: string | null; 16 | isCopying: boolean; 17 | handleCopyToWeixin: () => void; 18 | } 19 | 20 | const PreviewPanel: React.FC = ({ 21 | markdown, 22 | isMobilePreview, 23 | setIsMobilePreview, 24 | themeName, 25 | setTheme, 26 | copied, 27 | copyError, 28 | isCopying, 29 | handleCopyToWeixin 30 | }) => { 31 | const mobilePreviewRef = useRef(null); 32 | const [showViewTooltip, setShowViewTooltip] = useState(false); 33 | const [showCopyTooltip, setShowCopyTooltip] = useState(false); 34 | const [showContactTooltip, setShowContactTooltip] = useState(false); 35 | const [contactVisible, setContactVisible] = useState(false); 36 | 37 | return ( 38 |
    39 | {/* 顶部工具栏 */} 40 |
    41 |
    42 | 60 | {showViewTooltip && ( 61 |
    62 | {isMobilePreview ? '切换到电脑预览' : '切换到手机预览'} 63 |
    64 | )} 65 |
    66 | 67 |
    68 | {/* 联系作者按钮 */} 69 |
    70 | 79 | {showContactTooltip && ( 80 |
    81 | 联系作者反馈问题或建议 82 |
    83 | )} 84 |
    85 | 86 | {/* 复制按钮 */} 87 |
    88 | 118 | {showCopyTooltip && !copied && !copyError && !isCopying && ( 119 |
    120 | 复制到公众号 121 |
    122 | )} 123 | {copyError && !isCopying && ( 124 |
    125 | {copyError} 126 |
    127 | )} 128 |
    129 |
    130 |
    131 | 132 | {/* 主题切换区 */} 133 |
    134 |
    选择主题样式:
    135 | 136 |
    137 | 138 | {/* 预览内容 */} 139 |
    144 | {isMobilePreview && ( 145 | <> 146 |
    147 |
    148 |
    10:30
    149 |
    150 |
    151 |
    152 |
    153 |
    154 |
    155 | 156 |
    157 |
    158 |
    159 | 160 | )} 161 | 162 | {!isMobilePreview && ( 163 |
    164 | 165 |
    166 | )} 167 |
    168 | 169 | {/* 联系作者模态框 */} 170 | setContactVisible(false)} 173 | /> 174 | 175 | 218 |
    219 | ); 220 | }; 221 | 222 | export default PreviewPanel; -------------------------------------------------------------------------------- /components/ThemeSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTheme, ThemeName } from '../contexts/ThemeContext'; 3 | 4 | const themes: { name: ThemeName; label: string; color: string; textColor: string }[] = [ 5 | { name: 'default', label: '科技蓝', color: '#1a73e8', textColor: '#ffffff' }, 6 | { name: 'warm', label: '暖阳橙', color: '#e67e22', textColor: '#ffffff' }, 7 | { name: 'minimalist', label: '极简黑', color: '#34495e', textColor: '#ffffff' }, 8 | { name: 'nature', label: '清新绿', color: '#2e7d32', textColor: '#ffffff' }, 9 | { name: 'elegant', label: '典雅紫', color: '#7b2cbf', textColor: '#ffffff' }, 10 | { name: 'soft', label: '柔和粉', color: '#e66d85', textColor: '#ffffff' }, 11 | ]; 12 | 13 | const ThemeSwitcher: React.FC = () => { 14 | const { themeName, setTheme } = useTheme(); 15 | 16 | return ( 17 |
    18 | {themes.map((theme) => ( 19 | 33 | ))} 34 |
    35 | ); 36 | }; 37 | 38 | export default ThemeSwitcher; -------------------------------------------------------------------------------- /contexts/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useState } from 'react'; 2 | import { Theme, defaultTheme } from '../themes/default'; 3 | import { warmTheme, minimalistTheme, natureTheme, elegantTheme, softTheme } from '../themes'; 4 | 5 | export type ThemeName = 'default' | 'warm' | 'minimalist' | 'nature' | 'elegant' | 'soft'; 6 | 7 | interface ThemeContextType { 8 | currentTheme: Theme; 9 | themeName: ThemeName; 10 | setTheme: (name: ThemeName) => void; 11 | } 12 | 13 | const themes: Record = { 14 | default: defaultTheme, 15 | warm: warmTheme, 16 | minimalist: minimalistTheme, 17 | nature: natureTheme, 18 | elegant: elegantTheme, 19 | soft: softTheme, 20 | }; 21 | 22 | const ThemeContext = createContext({ 23 | currentTheme: defaultTheme, 24 | themeName: 'default', 25 | setTheme: () => {}, 26 | }); 27 | 28 | export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { 29 | const [themeName, setThemeName] = useState('default'); 30 | 31 | const value = { 32 | currentTheme: themes[themeName], 33 | themeName, 34 | setTheme: setThemeName, 35 | }; 36 | 37 | return ( 38 | 39 | {children} 40 | 41 | ); 42 | }; 43 | 44 | export const useTheme = () => { 45 | const context = useContext(ThemeContext); 46 | if (!context) { 47 | throw new Error('useTheme must be used within a ThemeProvider'); 48 | } 49 | return context; 50 | }; -------------------------------------------------------------------------------- /docs/promotion.md: -------------------------------------------------------------------------------- 1 | # 飞书文档转公众号,这款小工具让排版效率提升 10 倍! 2 | 3 | 你是否遇到过这些烦恼? 4 | 5 | - 📝 在飞书写好文章,复制到公众号就变得格式凌乱 6 | - 🎨 手动调整样式费时费力,还不一定好看 7 | - 📱 预览效果和手机端显示差异大 8 | - 🔧 代码、表格等特殊格式需要重新排版 9 | 10 | 如果有,那这款免费的在线工具一定能帮到你! 11 | 12 | ## 🚀 一键解决排版难题 13 | 14 | 「飞书文档转公众号」是一款专门解决飞书文档转公众号排版问题的在线工具。它能让你: 15 | 16 | 1. ✨ **一键转换**:从飞书直接复制,粘贴即可自动转换格式 17 | 2. 📱 **实时预览**:支持手机/电脑两种预览模式,所见即所得 18 | 3. 🎨 **精美主题**:内置多套精心设计的主题,一键切换 19 | 4. 💪 **完整功能**:完美支持图片、表格、代码块等各种格式 20 | 21 | ## 💡 特色功能 22 | 23 | ### 1. 智能格式转换 24 | - 完整保留文章结构和样式 25 | - 自动处理图片尺寸和链接 26 | - 保持表格对齐方式 27 | - 代码块自动高亮 28 | 29 | ### 2. 多端预览模式 30 | - 手机预览模式,还原真实阅读体验 31 | - 电脑预览模式,方便编辑调整 32 | - 实时切换,即时查看效果 33 | 34 | ### 3. 主题切换系统 35 | - 默认主题:清新专业,适合技术文章 36 | - 暖色主题:活力温暖,适合轻松内容 37 | - 极简主题:简约优雅,突出内容本身 38 | 39 | ## 📝 使用方法 40 | 41 | 使用起来超级简单: 42 | 43 | 1. 打开飞书文档,选中需要转换的内容 44 | 2. 复制并粘贴到工具的左侧编辑区 45 | 3. 在右侧实时预览效果,选择喜欢的主题 46 | 4. 点击复制按钮,直接粘贴到公众号后台 47 | 48 | 就这么简单,一篇文章的排版工作从原来的 10-15 分钟缩短到 1 分钟以内! 49 | 50 | ## 🌟 核心优势 51 | 52 | - 🚀 **效率提升**:告别手动排版,节省 90% 时间 53 | - 🎯 **专业设计**:多套精美主题,打造专业形象 54 | - 💻 **简单易用**:复制粘贴即可,无需特殊技能 55 | - 🆓 **完全免费**:开源免费,无需付费使用 56 | 57 | ## 🎁 马上开始体验 58 | 59 | 立即访问工具:[飞书文档转公众号工具](https://your-domain.com) 60 | 61 | 开源地址:[GitHub 仓库](https://github.com/your-username/your-repo) 62 | 63 | 如果这个工具对你有帮助,欢迎分享给更多需要的人! 64 | 65 | ## 🤝 技术支持 66 | 67 | - 遇到问题?欢迎在 GitHub 提交 Issue 68 | - 有建议?欢迎在评论区留言 69 | - 想参与开发?欢迎提交 Pull Request 70 | 71 | 让我们一起把内容创作的效率提升到新高度! -------------------------------------------------------------------------------- /docs/theme-optimization.md: -------------------------------------------------------------------------------- 1 | # 🎨 主题优化报告:符合微信公众号最佳实践 2 | 3 | ## 📋 优化概述 4 | 5 | 根据微信公众号排版最佳实践和2024年最新标准,对所有主题进行了全面优化,确保美观整洁且适合微信公众号阅读体验。 6 | 7 | ## 🎯 优化标准 8 | 9 | ### 📱 微信公众号最佳实践 10 | - **字体大小**:15px(最适合手机阅读) 11 | - **行间距**:1.75倍(符合微信阅读习惯) 12 | - **正文颜色**:#343434(柔和不刺眼的深灰色) 13 | - **段间距**:1.2em(增加文章呼吸感) 14 | - **边距优化**:增加图片、代码块等元素的间距 15 | - **统一主色**:每个主题使用一个主色调,避免颜色混乱 16 | 17 | ### 🔧 具体优化项目 18 | 19 | #### 1. **字体与间距优化** 20 | - ✅ 正文字体:统一15px 21 | - ✅ 代码字体:优化为14px(原来部分主题为13px或0.9em) 22 | - ✅ 表格字体:优化为15px(原来14px) 23 | - ✅ 行间距:统一1.75倍 24 | - ✅ 段落间距:优化为1.2em 25 | - ✅ 列表项间距:优化为0.5em 26 | 27 | #### 2. **标题层次优化** 28 | - ✅ H1:28px,居中,下边框,增加底部间距 29 | - ✅ H2:22px,左边框,增加上下间距 30 | - ✅ H3:18px,保持主色,增加间距 31 | - ✅ H4-H6:渐进式大小,合理间距 32 | 33 | #### 3. **色彩统一化** 34 | - ✅ 正文颜色:统一为#343434(符合微信最佳实践) 35 | - ✅ 主色调:每个主题保持单一主色 36 | - ✅ 强调文字:使用主题色突出重点 37 | 38 | #### 4. **元素间距优化** 39 | - ✅ 图片间距:1.5em上下,增加视觉呼吸感 40 | - ✅ 代码块间距:1.8em上下,1.2em内边距 41 | - ✅ 表格间距:1.5em上下,10px单元格内边距 42 | - ✅ 引用块间距:1.8em上下,1em内边距 43 | - ✅ 分隔线间距:2em上下 44 | 45 | #### 5. **圆角和视觉效果** 46 | - ✅ 图片圆角:6px(部分主题为4px) 47 | - ✅ 代码块圆角:6px 48 | - ✅ 引用块圆角:优化为0 6px 6px 0 49 | 50 | ## 🌈 主题详情 51 | 52 | ### 1. 🔵 **Default主题(科技蓝)** 53 | - **主色**:#1a73e8 54 | - **特点**:现代简洁,适合科技类、商务类文章 55 | - **应用场景**:企业公众号、技术分享、产品介绍 56 | 57 | ### 2. 🧡 **Warm主题(温暖橙)** 58 | - **主色**:#e67e22 59 | - **特点**:温暖亲和,阅读舒适 60 | - **应用场景**:生活分享、教育内容、情感文章 61 | 62 | ### 3. 🔘 **Minimalist主题(极简灰)** 63 | - **主色**:#34495e 64 | - **特点**:极简风格,突出内容本身 65 | - **应用场景**:文学创作、深度思考、专业内容 66 | 67 | ### 4. 🌿 **Nature主题(自然绿)** 68 | - **主色**:#2e7d32 69 | - **特点**:自然清新,护眼舒适 70 | - **应用场景**:健康养生、环保主题、户外活动 71 | 72 | ### 5. 💜 **Elegant主题(优雅紫)** 73 | - **主色**:#7b2cbf 74 | - **特点**:高雅大气,质感突出 75 | - **应用场景**:艺术设计、时尚美妆、品牌推广 76 | 77 | ### 6. 🌸 **Soft主题(柔和粉)** 78 | - **主色**:#e66d85 79 | - **特点**:温柔可爱,亲切友好 80 | - **应用场景**:女性内容、亲子教育、生活美学 81 | 82 | ### 7. 🔷 **Blue主题(专业蓝)** 83 | - **主色**:#1e88e5 84 | - **特点**:专业稳重,信任感强 85 | - **应用场景**:金融服务、医疗健康、政务公开 86 | 87 | ### 8. 🌙 **Dark主题(深色模式)** 88 | - **主色**:#bb86fc 89 | - **特点**:护眼模式,夜间阅读友好 90 | - **应用场景**:技术文档、夜间推送、特殊场景 91 | 92 | ## 📊 优化效果对比 93 | 94 | ### 优化前问题: 95 | ❌ 字体大小不统一(12px-18px混用) 96 | ❌ 颜色搭配不协调(多个颜色混用) 97 | ❌ 间距设置不合理(阅读体验差) 98 | ❌ 元素层次不清晰(视觉混乱) 99 | 100 | ### 优化后效果: 101 | ✅ **阅读体验提升**:符合微信用户阅读习惯 102 | ✅ **视觉统一性**:每个主题色彩协调一致 103 | ✅ **手机适配优化**:字体大小适合移动端阅读 104 | ✅ **专业美观**:符合现代设计美学标准 105 | 106 | ## 🎉 使用建议 107 | 108 | 1. **选择合适主题**:根据公众号定位和内容风格选择 109 | 2. **保持一致性**:建议长期使用同一主题保持品牌一致性 110 | 3. **测试效果**:发布前在不同设备上预览效果 111 | 4. **用户反馈**:关注读者反馈,适时调整 112 | 113 | ## 📱 微信公众号最佳实践总结 114 | 115 | - ✅ 字号15px,行距1.75倍 116 | - ✅ 正文颜色#343434,柔和不刺眼 117 | - ✅ 段落间适当留白,增加呼吸感 118 | - ✅ 标题层次清晰,引导阅读 119 | - ✅ 配色不超过3种,保持简洁 120 | - ✅ 适配手机屏幕,优化阅读体验 121 | 122 | --- 123 | 124 | 💡 **提示**:所有主题已经过优化,可直接用于微信公众号文章排版,无需额外调整即可获得专业的阅读体验。 -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | // 由于使用了 @uiw/react-md-editor,需要配置 webpack 6 | webpack: (config) => { 7 | config.resolve.fallback = { fs: false }; 8 | return config; 9 | }, 10 | images: { 11 | domains: [ 12 | 'bytedance.larkoffice.com', 13 | 'bytedance.feishu.cn', 14 | 'feishu.cn', 15 | 'sf3-cn.feishucdn.com', 16 | 'lf3-static.bytednsdoc.com', 17 | 'p3-hera.byteimg.com', 18 | 'p3-juejin.byteimg.com', 19 | 'p1-juejin.byteimg.com', 20 | 'p2-juejin.byteimg.com', 21 | 'p4-juejin.byteimg.com', 22 | 'p5-juejin.byteimg.com', 23 | 'p6-juejin.byteimg.com', 24 | ], 25 | unoptimized: true, 26 | }, 27 | } 28 | 29 | module.exports = nextConfig -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lark-to-markdown", 3 | "version": "1.0.0", 4 | "description": "飞书文档转公众号排版工具 - 一键将飞书文档转换为美观的微信公众号文章", 5 | "author": "孟健 ", 6 | "license": "MIT", 7 | "keywords": [ 8 | "lark", 9 | "feishu", 10 | "wechat", 11 | "weixin", 12 | "markdown", 13 | "converter", 14 | "公众号", 15 | "飞书", 16 | "微信" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/mengjian-github/lark-to-markdown.git" 21 | }, 22 | "homepage": "https://lark-to-markdown.vercel.app", 23 | "bugs": { 24 | "url": "https://github.com/mengjian-github/lark-to-markdown/issues" 25 | }, 26 | "private": false, 27 | "scripts": { 28 | "dev": "next dev", 29 | "build": "next build", 30 | "start": "next start", 31 | "lint": "next lint", 32 | "export": "next build && next export" 33 | }, 34 | "dependencies": { 35 | "@types/next": "^9.0.0", 36 | "@types/react-syntax-highlighter": "^15.5.13", 37 | "@uiw/react-markdown-preview": "^4.1.16", 38 | "@uiw/react-md-editor": "^3.25.6", 39 | "next": "14.0.4", 40 | "react": "^18.2.0", 41 | "react-dom": "^18.2.0", 42 | "react-icons": "^4.12.0", 43 | "react-markdown": "^9.0.1", 44 | "react-syntax-highlighter": "^15.6.1", 45 | "rehype-raw": "^7.0.0", 46 | "rehype-sanitize": "^6.0.0", 47 | "remark-gfm": "^4.0.0", 48 | "turndown": "^7.1.2" 49 | }, 50 | "devDependencies": { 51 | "@tailwindcss/typography": "^0.5.10", 52 | "@types/node": "^20.10.4", 53 | "@types/react": "^18.2.42", 54 | "@types/react-dom": "^18.2.17", 55 | "@types/turndown": "^5.0.4", 56 | "autoprefixer": "^10.4.16", 57 | "eslint": "^8.55.0", 58 | "eslint-config-next": "14.0.4", 59 | "postcss": "^8.4.32", 60 | "tailwindcss": "^3.3.6", 61 | "typescript": "^5.3.3" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/product.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjian-github/lark-to-markdown/b68c0b450dd8855bd1a74a5694e846b945c72bfc/public/images/product.png -------------------------------------------------------------------------------- /public/images/wx-qr-placeholder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 微信二维码占位符 70 | 71 | 72 | 请替换为实际的二维码图片 73 | 74 | -------------------------------------------------------------------------------- /public/images/wx-qr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjian-github/lark-to-markdown/b68c0b450dd8855bd1a74a5694e846b945c72bfc/public/images/wx-qr.jpg -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './app/**/*.{js,ts,jsx,tsx,mdx}', 5 | './components/**/*.{js,ts,jsx,tsx,mdx}', 6 | ], 7 | darkMode: 'class', 8 | theme: { 9 | extend: { 10 | typography: { 11 | DEFAULT: { 12 | css: { 13 | maxWidth: '100%', 14 | }, 15 | }, 16 | }, 17 | }, 18 | }, 19 | plugins: [ 20 | require('@tailwindcss/typography'), 21 | ], 22 | } -------------------------------------------------------------------------------- /themes/dark.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from './default'; 2 | 3 | export const darkTheme: Theme = { 4 | base: { 5 | fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', 6 | fontSize: '15px', 7 | lineHeight: '1.75', 8 | color: '#e0e0e0', 9 | letterSpacing: '0.03em', 10 | textAlign: 'left', 11 | }, 12 | headings: { 13 | color: '#bb86fc', 14 | fontWeight: '600', 15 | letterSpacing: '0.02em', 16 | h1: { 17 | fontSize: '28px', 18 | margin: '1.5em 0 1em', 19 | textAlign: 'center', 20 | borderBottom: '2px solid #bb86fc', 21 | paddingBottom: '0.5em', 22 | position: 'relative', 23 | }, 24 | h2: { 25 | fontSize: '22px', 26 | margin: '2em 0 1em', 27 | borderLeft: '4px solid #bb86fc', 28 | paddingLeft: '12px', 29 | color: '#bb86fc', 30 | }, 31 | h3: { 32 | fontSize: '18px', 33 | margin: '1.8em 0 1em', 34 | color: '#bb86fc', 35 | }, 36 | h4: { 37 | fontSize: '16px', 38 | margin: '1.5em 0 0.8em', 39 | }, 40 | h5: { 41 | fontSize: '15px', 42 | margin: '1.2em 0 0.8em', 43 | }, 44 | h6: { 45 | fontSize: '14px', 46 | margin: '1.2em 0 0.8em', 47 | color: '#b0b0b0', 48 | }, 49 | }, 50 | paragraph: { 51 | margin: '1.2em 0', 52 | lineHeight: '1.75', 53 | }, 54 | image: { 55 | maxWidth: '100%', 56 | margin: '1.5em auto', 57 | borderRadius: '6px', 58 | }, 59 | code: { 60 | fontFamily: 'Menlo, Monaco, Consolas, "Courier New", monospace', 61 | fontSize: '14px', 62 | lineHeight: '1.6', 63 | block: { 64 | background: '#1e1e1e', 65 | padding: '1.2em', 66 | margin: '1.8em 0', 67 | borderRadius: '6px', 68 | color: '#e0e0e0', 69 | overflow: 'auto', 70 | }, 71 | inline: { 72 | background: '#2d2d2d', 73 | padding: '0.2em 0.4em', 74 | borderRadius: '3px', 75 | color: '#bb86fc', 76 | fontFamily: 'Menlo, Monaco, Consolas, "Courier New", monospace', 77 | }, 78 | }, 79 | table: { 80 | width: '100%', 81 | margin: '1.5em 0', 82 | fontSize: '15px', 83 | borderCollapse: 'collapse', 84 | borderSpacing: '0', 85 | border: '1px solid #444', 86 | cell: { 87 | padding: '10px 16px', 88 | border: '1px solid #444', 89 | }, 90 | header: { 91 | background: '#2d2d2d', 92 | fontWeight: '600', 93 | border: '1px solid #444', 94 | }, 95 | }, 96 | blockquote: { 97 | margin: '1.8em 0', 98 | padding: '1em 1.5em', 99 | background: '#2d2d2d', 100 | borderRadius: '0 6px 6px 0', 101 | color: '#e0e0e0', 102 | borderLeft: '4px solid #bb86fc', 103 | fontStyle: 'normal', 104 | }, 105 | list: { 106 | margin: '1.2em 0', 107 | padding: '0 0 0 2em', 108 | item: { 109 | margin: '0.5em 0', 110 | lineHeight: '1.6', 111 | }, 112 | unordered: { 113 | listStyleType: 'disc', 114 | nestedLevel1: { 115 | listStyleType: 'circle', 116 | }, 117 | nestedLevel2: { 118 | listStyleType: 'square', 119 | }, 120 | }, 121 | ordered: { 122 | listStyleType: 'decimal', 123 | nestedLevel1: { 124 | listStyleType: 'lower-alpha', 125 | }, 126 | nestedLevel2: { 127 | listStyleType: 'lower-roman', 128 | }, 129 | }, 130 | }, 131 | link: { 132 | color: '#bb86fc', 133 | textDecoration: 'none', 134 | borderBottom: '1px dotted #bb86fc', 135 | }, 136 | hr: { 137 | margin: '2em 0', 138 | border: '1px solid #444', 139 | }, 140 | emphasis: { 141 | strong: { 142 | color: '#bb86fc', 143 | fontWeight: 'bold', 144 | }, 145 | em: { 146 | color: '#bb86fc', 147 | fontStyle: 'italic', 148 | }, 149 | }, 150 | }; -------------------------------------------------------------------------------- /themes/default.ts: -------------------------------------------------------------------------------- 1 | export interface Theme { 2 | base: { 3 | fontFamily: string; 4 | fontSize: string; 5 | lineHeight: string; 6 | color: string; 7 | letterSpacing: string; 8 | textAlign: string; 9 | }; 10 | headings: { 11 | color: string; 12 | fontWeight: string; 13 | letterSpacing: string; 14 | h1: { 15 | fontSize: string; 16 | margin: string; 17 | textAlign: string; 18 | borderBottom: string; 19 | paddingBottom: string; 20 | position: 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky'; 21 | }; 22 | h2: { 23 | fontSize: string; 24 | margin: string; 25 | borderLeft: string; 26 | paddingLeft: string; 27 | color: string; 28 | }; 29 | h3: { 30 | fontSize: string; 31 | margin: string; 32 | color: string; 33 | }; 34 | h4: { 35 | fontSize: string; 36 | margin: string; 37 | }; 38 | h5: { 39 | fontSize: string; 40 | margin: string; 41 | }; 42 | h6: { 43 | fontSize: string; 44 | margin: string; 45 | color: string; 46 | }; 47 | }; 48 | paragraph: { 49 | margin: string; 50 | lineHeight: string; 51 | }; 52 | image: { 53 | maxWidth: string; 54 | margin: string; 55 | borderRadius: string; 56 | }; 57 | code: { 58 | fontFamily: string; 59 | fontSize: string; 60 | lineHeight: string; 61 | block: { 62 | background: string; 63 | padding: string; 64 | margin: string; 65 | borderRadius: string; 66 | color: string; 67 | overflow?: string; 68 | fontSize?: string; 69 | lineHeight?: string; 70 | }; 71 | inline: { 72 | background: string; 73 | padding: string; 74 | borderRadius: string; 75 | color: string; 76 | fontFamily: string; 77 | fontSize?: string; 78 | }; 79 | }; 80 | table: { 81 | width: string; 82 | margin: string; 83 | fontSize: string; 84 | borderCollapse: string; 85 | borderSpacing: string; 86 | border: string; 87 | cell: { 88 | padding: string; 89 | border: string; 90 | }; 91 | header: { 92 | background: string; 93 | fontWeight: string; 94 | border: string; 95 | }; 96 | }; 97 | blockquote: { 98 | margin: string; 99 | padding: string; 100 | background: string; 101 | borderRadius: string; 102 | color: string; 103 | borderLeft: string; 104 | fontStyle?: string; 105 | }; 106 | list: { 107 | margin: string; 108 | padding: string; 109 | item: { 110 | margin: string; 111 | lineHeight: string; 112 | }; 113 | unordered: { 114 | listStyleType: string; 115 | nestedLevel1: { 116 | listStyleType: string; 117 | }; 118 | nestedLevel2: { 119 | listStyleType: string; 120 | }; 121 | }; 122 | ordered: { 123 | listStyleType: string; 124 | nestedLevel1: { 125 | listStyleType: string; 126 | }; 127 | nestedLevel2: { 128 | listStyleType: string; 129 | }; 130 | }; 131 | }; 132 | link: { 133 | color: string; 134 | textDecoration: string; 135 | borderBottom: string; 136 | }; 137 | hr: { 138 | margin: string; 139 | border: string; 140 | }; 141 | emphasis: { 142 | strong: { 143 | color: string; 144 | fontWeight: string; 145 | }; 146 | em: { 147 | color: string; 148 | fontStyle: string; 149 | }; 150 | }; 151 | } 152 | 153 | export const defaultTheme: Theme = { 154 | base: { 155 | fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', 156 | fontSize: '15px', 157 | lineHeight: '1.75', 158 | color: '#343434', 159 | letterSpacing: '0.03em', 160 | textAlign: 'left', 161 | }, 162 | headings: { 163 | color: '#1a73e8', 164 | fontWeight: '600', 165 | letterSpacing: '0.02em', 166 | h1: { 167 | fontSize: '28px', 168 | margin: '1.5em 0 1em', 169 | textAlign: 'center', 170 | borderBottom: '2px solid #1a73e8', 171 | paddingBottom: '0.5em', 172 | position: 'relative', 173 | }, 174 | h2: { 175 | fontSize: '22px', 176 | margin: '2em 0 1em', 177 | borderLeft: '4px solid #1a73e8', 178 | paddingLeft: '12px', 179 | color: '#1a73e8', 180 | }, 181 | h3: { 182 | fontSize: '18px', 183 | margin: '1.8em 0 1em', 184 | color: '#1a73e8', 185 | }, 186 | h4: { 187 | fontSize: '16px', 188 | margin: '1.5em 0 0.8em', 189 | }, 190 | h5: { 191 | fontSize: '15px', 192 | margin: '1.2em 0 0.8em', 193 | }, 194 | h6: { 195 | fontSize: '14px', 196 | margin: '1.2em 0 0.8em', 197 | color: '#4a5568', 198 | }, 199 | }, 200 | paragraph: { 201 | margin: '1.2em 0', 202 | lineHeight: '1.75', 203 | }, 204 | image: { 205 | maxWidth: '100%', 206 | margin: '1.5em auto', 207 | borderRadius: '6px', 208 | }, 209 | code: { 210 | fontFamily: 'Consolas, Monaco, "Courier New", monospace', 211 | fontSize: '14px', 212 | lineHeight: '1.6', 213 | block: { 214 | margin: '1.8em 0', 215 | padding: '1.2em', 216 | borderRadius: '6px', 217 | background: '#f6f8fa', 218 | color: '#333333', 219 | overflow: 'auto', 220 | fontSize: '14px', 221 | lineHeight: '1.6', 222 | }, 223 | inline: { 224 | padding: '0.2em 0.4em', 225 | borderRadius: '3px', 226 | background: '#f3f3f3', 227 | color: '#1a73e8', 228 | fontFamily: 'Consolas, Monaco, "Courier New", monospace', 229 | fontSize: '85%', 230 | }, 231 | }, 232 | table: { 233 | width: '100%', 234 | margin: '1.5em 0', 235 | fontSize: '15px', 236 | borderCollapse: 'collapse', 237 | borderSpacing: '0', 238 | border: '1px solid #e0e9fa', 239 | cell: { 240 | padding: '10px 16px', 241 | border: '1px solid #e0e9fa', 242 | }, 243 | header: { 244 | background: '#eef3fd', 245 | fontWeight: '600', 246 | border: '1px solid #d0e1fa', 247 | }, 248 | }, 249 | blockquote: { 250 | margin: '1.8em 0', 251 | padding: '1em 1.5em', 252 | borderLeft: '4px solid #1a73e8', 253 | fontStyle: 'normal', 254 | color: '#4a5568', 255 | background: '#f5f9ff', 256 | borderRadius: '0 6px 6px 0', 257 | }, 258 | list: { 259 | margin: '1.2em 0', 260 | padding: '0 0 0 1.5em', 261 | item: { 262 | margin: '0.5em 0', 263 | lineHeight: '1.75', 264 | }, 265 | unordered: { 266 | listStyleType: 'disc', 267 | nestedLevel1: { 268 | listStyleType: 'circle', 269 | }, 270 | nestedLevel2: { 271 | listStyleType: 'square', 272 | }, 273 | }, 274 | ordered: { 275 | listStyleType: 'decimal', 276 | nestedLevel1: { 277 | listStyleType: 'lower-alpha', 278 | }, 279 | nestedLevel2: { 280 | listStyleType: 'lower-roman', 281 | }, 282 | }, 283 | }, 284 | link: { 285 | color: '#1a73e8', 286 | textDecoration: 'none', 287 | borderBottom: '1px solid #1a73e8', 288 | }, 289 | hr: { 290 | margin: '2em 0', 291 | border: '1px solid #d0e1fa', 292 | }, 293 | emphasis: { 294 | strong: { 295 | color: '#1a73e8', 296 | fontWeight: '600', 297 | }, 298 | em: { 299 | color: '#1a73e8', 300 | fontStyle: 'italic', 301 | }, 302 | }, 303 | }; -------------------------------------------------------------------------------- /themes/elegant.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from './default'; 2 | 3 | export const elegantTheme: Theme = { 4 | base: { 5 | fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', 6 | fontSize: '15px', 7 | lineHeight: '1.75', 8 | color: '#343434', 9 | letterSpacing: '0.03em', 10 | textAlign: 'left', 11 | }, 12 | headings: { 13 | color: '#7b2cbf', 14 | fontWeight: '600', 15 | letterSpacing: '0.02em', 16 | h1: { 17 | fontSize: '28px', 18 | margin: '1.5em 0 1em', 19 | textAlign: 'center', 20 | borderBottom: '2px solid #7b2cbf', 21 | paddingBottom: '0.5em', 22 | position: 'relative', 23 | }, 24 | h2: { 25 | fontSize: '22px', 26 | margin: '2em 0 1em', 27 | borderLeft: '4px solid #7b2cbf', 28 | paddingLeft: '12px', 29 | color: '#7b2cbf', 30 | }, 31 | h3: { 32 | fontSize: '18px', 33 | margin: '1.8em 0 1em', 34 | color: '#7b2cbf', 35 | }, 36 | h4: { 37 | fontSize: '16px', 38 | margin: '1.5em 0 0.8em', 39 | }, 40 | h5: { 41 | fontSize: '15px', 42 | margin: '1.2em 0 0.8em', 43 | }, 44 | h6: { 45 | fontSize: '14px', 46 | margin: '1.2em 0 0.8em', 47 | color: '#4a5568', 48 | }, 49 | }, 50 | paragraph: { 51 | margin: '1.2em 0', 52 | lineHeight: '1.75', 53 | }, 54 | image: { 55 | maxWidth: '100%', 56 | margin: '1.5em auto', 57 | borderRadius: '4px', 58 | }, 59 | code: { 60 | fontFamily: 'Menlo, Monaco, Consolas, "Courier New", monospace', 61 | fontSize: '14px', 62 | lineHeight: '1.6', 63 | block: { 64 | background: '#f8f7fa', 65 | padding: '1.2em', 66 | margin: '1.8em 0', 67 | borderRadius: '4px', 68 | color: '#3a2f45', 69 | overflow: 'auto', 70 | }, 71 | inline: { 72 | background: '#f5f0fa', 73 | padding: '0.2em 0.4em', 74 | borderRadius: '3px', 75 | color: '#7b2cbf', 76 | fontFamily: 'Menlo, Monaco, Consolas, "Courier New", monospace', 77 | }, 78 | }, 79 | table: { 80 | width: '100%', 81 | margin: '1.5em 0', 82 | fontSize: '15px', 83 | borderCollapse: 'collapse', 84 | borderSpacing: '0', 85 | border: '1px solid #e7dcf5', 86 | cell: { 87 | padding: '10px 16px', 88 | border: '1px solid #e7dcf5', 89 | }, 90 | header: { 91 | background: '#f2ebfa', 92 | fontWeight: '600', 93 | border: '1px solid #d7c5eb', 94 | }, 95 | }, 96 | blockquote: { 97 | margin: '1.8em 0', 98 | padding: '1em 1.5em', 99 | borderLeft: '4px solid #7b2cbf', 100 | fontStyle: 'normal', 101 | color: '#4a5568', 102 | background: '#f7f4fb', 103 | borderRadius: '0 4px 4px 0', 104 | }, 105 | list: { 106 | margin: '1.2em 0', 107 | padding: '0 0 0 2em', 108 | item: { 109 | margin: '0.5em 0', 110 | lineHeight: '1.6', 111 | }, 112 | unordered: { 113 | listStyleType: 'disc', 114 | nestedLevel1: { 115 | listStyleType: 'circle', 116 | }, 117 | nestedLevel2: { 118 | listStyleType: 'square', 119 | }, 120 | }, 121 | ordered: { 122 | listStyleType: 'decimal', 123 | nestedLevel1: { 124 | listStyleType: 'lower-alpha', 125 | }, 126 | nestedLevel2: { 127 | listStyleType: 'lower-roman', 128 | }, 129 | }, 130 | }, 131 | link: { 132 | color: '#7b2cbf', 133 | textDecoration: 'none', 134 | borderBottom: '1px dotted #c8b6e2', 135 | }, 136 | hr: { 137 | margin: '2em 0', 138 | border: '1px solid #e7dcf5', 139 | }, 140 | emphasis: { 141 | strong: { 142 | color: '#7b2cbf', 143 | fontWeight: 'bold', 144 | }, 145 | em: { 146 | color: '#7b2cbf', 147 | fontStyle: 'italic', 148 | }, 149 | }, 150 | }; -------------------------------------------------------------------------------- /themes/index.ts: -------------------------------------------------------------------------------- 1 | export type { Theme } from './default'; 2 | export { defaultTheme } from './default'; 3 | export { warmTheme } from './warm'; 4 | export { minimalistTheme } from './minimalist'; 5 | export { natureTheme } from './nature'; 6 | export { elegantTheme } from './elegant'; 7 | export { softTheme } from './soft'; -------------------------------------------------------------------------------- /themes/minimalist.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from './default'; 2 | 3 | export const minimalistTheme: Theme = { 4 | base: { 5 | fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', 6 | fontSize: '15px', 7 | lineHeight: '1.75', 8 | color: '#343434', 9 | letterSpacing: '0.03em', 10 | textAlign: 'left', 11 | }, 12 | headings: { 13 | color: '#34495e', 14 | fontWeight: '600', 15 | letterSpacing: '0.02em', 16 | h1: { 17 | fontSize: '28px', 18 | margin: '1.5em 0 1em', 19 | textAlign: 'center', 20 | borderBottom: '2px solid #34495e', 21 | paddingBottom: '0.5em', 22 | position: 'relative', 23 | }, 24 | h2: { 25 | fontSize: '22px', 26 | margin: '2em 0 1em', 27 | borderLeft: '4px solid #34495e', 28 | paddingLeft: '12px', 29 | color: '#34495e', 30 | }, 31 | h3: { 32 | fontSize: '18px', 33 | margin: '1.8em 0 1em', 34 | color: '#34495e', 35 | }, 36 | h4: { 37 | fontSize: '16px', 38 | margin: '1.5em 0 0.8em', 39 | }, 40 | h5: { 41 | fontSize: '15px', 42 | margin: '1.2em 0 0.8em', 43 | }, 44 | h6: { 45 | fontSize: '14px', 46 | margin: '1.2em 0 0.8em', 47 | color: '#4a5568', 48 | }, 49 | }, 50 | paragraph: { 51 | margin: '1.2em 0', 52 | lineHeight: '1.75', 53 | }, 54 | image: { 55 | maxWidth: '100%', 56 | margin: '1.5em auto', 57 | borderRadius: '4px', 58 | }, 59 | code: { 60 | fontFamily: 'Menlo, Monaco, Consolas, "Courier New", monospace', 61 | fontSize: '14px', 62 | lineHeight: '1.6', 63 | block: { 64 | background: '#f8f9fa', 65 | padding: '1.2em', 66 | margin: '1.8em 0', 67 | borderRadius: '4px', 68 | color: '#2c3e50', 69 | overflow: 'auto', 70 | }, 71 | inline: { 72 | background: '#f5f7fa', 73 | padding: '0.2em 0.4em', 74 | borderRadius: '3px', 75 | color: '#476582', 76 | fontFamily: 'Menlo, Monaco, Consolas, "Courier New", monospace', 77 | }, 78 | }, 79 | table: { 80 | width: '100%', 81 | margin: '1.5em 0', 82 | fontSize: '15px', 83 | borderCollapse: 'collapse', 84 | borderSpacing: '0', 85 | border: '1px solid #e2e8f0', 86 | cell: { 87 | padding: '10px 16px', 88 | border: '1px solid #e2e8f0', 89 | }, 90 | header: { 91 | background: '#f7fafc', 92 | fontWeight: '600', 93 | border: '1px solid #dae1e7', 94 | }, 95 | }, 96 | blockquote: { 97 | margin: '1.8em 0', 98 | padding: '1em 1.5em', 99 | borderLeft: '4px solid #34495e', 100 | fontStyle: 'normal', 101 | color: '#4a5568', 102 | background: '#f5f7fa', 103 | borderRadius: '0 4px 4px 0', 104 | }, 105 | list: { 106 | margin: '1.2em 0', 107 | padding: '0 0 0 2em', 108 | item: { 109 | margin: '0.5em 0', 110 | lineHeight: '1.6', 111 | }, 112 | unordered: { 113 | listStyleType: 'disc', 114 | nestedLevel1: { 115 | listStyleType: 'circle', 116 | }, 117 | nestedLevel2: { 118 | listStyleType: 'square', 119 | }, 120 | }, 121 | ordered: { 122 | listStyleType: 'decimal', 123 | nestedLevel1: { 124 | listStyleType: 'lower-alpha', 125 | }, 126 | nestedLevel2: { 127 | listStyleType: 'lower-roman', 128 | }, 129 | }, 130 | }, 131 | link: { 132 | color: '#34495e', 133 | textDecoration: 'none', 134 | borderBottom: '1px dotted #eaecef', 135 | }, 136 | hr: { 137 | margin: '2em 0', 138 | border: '1px solid #e2e8f0', 139 | }, 140 | emphasis: { 141 | strong: { 142 | color: '#34495e', 143 | fontWeight: 'bold', 144 | }, 145 | em: { 146 | color: '#34495e', 147 | fontStyle: 'italic', 148 | }, 149 | }, 150 | }; -------------------------------------------------------------------------------- /themes/nature.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from './default'; 2 | 3 | export const natureTheme: Theme = { 4 | base: { 5 | fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', 6 | fontSize: '15px', 7 | lineHeight: '1.75', 8 | color: '#343434', 9 | letterSpacing: '0.03em', 10 | textAlign: 'left', 11 | }, 12 | headings: { 13 | color: '#2e7d32', 14 | fontWeight: '600', 15 | letterSpacing: '0.02em', 16 | h1: { 17 | fontSize: '28px', 18 | margin: '1.5em 0 1em', 19 | textAlign: 'center', 20 | borderBottom: '2px solid #2e7d32', 21 | paddingBottom: '0.5em', 22 | position: 'relative', 23 | }, 24 | h2: { 25 | fontSize: '22px', 26 | margin: '2em 0 1em', 27 | borderLeft: '4px solid #2e7d32', 28 | paddingLeft: '12px', 29 | color: '#2e7d32', 30 | }, 31 | h3: { 32 | fontSize: '18px', 33 | margin: '1.8em 0 1em', 34 | color: '#2e7d32', 35 | }, 36 | h4: { 37 | fontSize: '16px', 38 | margin: '1.5em 0 0.8em', 39 | }, 40 | h5: { 41 | fontSize: '15px', 42 | margin: '1.2em 0 0.8em', 43 | }, 44 | h6: { 45 | fontSize: '14px', 46 | margin: '1.2em 0 0.8em', 47 | color: '#4a5568', 48 | }, 49 | }, 50 | paragraph: { 51 | margin: '1.2em 0', 52 | lineHeight: '1.75', 53 | }, 54 | image: { 55 | maxWidth: '100%', 56 | margin: '1.5em auto', 57 | borderRadius: '4px', 58 | }, 59 | code: { 60 | fontFamily: 'Menlo, Monaco, Consolas, "Courier New", monospace', 61 | fontSize: '14px', 62 | lineHeight: '1.6', 63 | block: { 64 | background: '#f1f8e9', 65 | padding: '1.2em', 66 | margin: '1.8em 0', 67 | borderRadius: '4px', 68 | color: '#3c4a3c', 69 | overflow: 'auto', 70 | }, 71 | inline: { 72 | background: '#e8f5e9', 73 | padding: '0.2em 0.4em', 74 | borderRadius: '3px', 75 | color: '#2e7d32', 76 | fontFamily: 'Menlo, Monaco, Consolas, "Courier New", monospace', 77 | }, 78 | }, 79 | table: { 80 | width: '100%', 81 | margin: '1.5em 0', 82 | fontSize: '15px', 83 | borderCollapse: 'collapse', 84 | borderSpacing: '0', 85 | border: '1px solid #ddeedf', 86 | cell: { 87 | padding: '10px 16px', 88 | border: '1px solid #ddeedf', 89 | }, 90 | header: { 91 | background: '#eef7ef', 92 | fontWeight: '600', 93 | border: '1px solid #cce5cf', 94 | }, 95 | }, 96 | blockquote: { 97 | margin: '1.8em 0', 98 | padding: '1em 1.5em', 99 | borderLeft: '4px solid #2e7d32', 100 | fontStyle: 'normal', 101 | color: '#4a5568', 102 | background: '#f2f8f3', 103 | borderRadius: '0 4px 4px 0', 104 | }, 105 | list: { 106 | margin: '1.2em 0', 107 | padding: '0 0 0 2em', 108 | item: { 109 | margin: '0.5em 0', 110 | lineHeight: '1.6', 111 | }, 112 | unordered: { 113 | listStyleType: 'disc', 114 | nestedLevel1: { 115 | listStyleType: 'circle', 116 | }, 117 | nestedLevel2: { 118 | listStyleType: 'square', 119 | }, 120 | }, 121 | ordered: { 122 | listStyleType: 'decimal', 123 | nestedLevel1: { 124 | listStyleType: 'lower-alpha', 125 | }, 126 | nestedLevel2: { 127 | listStyleType: 'lower-roman', 128 | }, 129 | }, 130 | }, 131 | link: { 132 | color: '#2e7d32', 133 | textDecoration: 'none', 134 | borderBottom: '1px dotted #2e7d32', 135 | }, 136 | hr: { 137 | margin: '2em 0', 138 | border: '1px solid #ddeedf', 139 | }, 140 | emphasis: { 141 | strong: { 142 | color: '#2e7d32', 143 | fontWeight: 'bold', 144 | }, 145 | em: { 146 | color: '#2e7d32', 147 | fontStyle: 'italic', 148 | }, 149 | }, 150 | }; -------------------------------------------------------------------------------- /themes/soft.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from './default'; 2 | 3 | export const softTheme: Theme = { 4 | base: { 5 | fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', 6 | fontSize: '15px', 7 | lineHeight: '1.75', 8 | color: '#343434', 9 | letterSpacing: '0.03em', 10 | textAlign: 'left', 11 | }, 12 | headings: { 13 | color: '#e66d85', 14 | fontWeight: '600', 15 | letterSpacing: '0.02em', 16 | h1: { 17 | fontSize: '28px', 18 | margin: '1.5em 0 1em', 19 | textAlign: 'center', 20 | borderBottom: '2px solid #e66d85', 21 | paddingBottom: '0.5em', 22 | position: 'relative', 23 | }, 24 | h2: { 25 | fontSize: '22px', 26 | margin: '2em 0 1em', 27 | borderLeft: '4px solid #e66d85', 28 | paddingLeft: '12px', 29 | color: '#e66d85', 30 | }, 31 | h3: { 32 | fontSize: '18px', 33 | margin: '1.8em 0 1em', 34 | color: '#e66d85', 35 | }, 36 | h4: { 37 | fontSize: '16px', 38 | margin: '1.5em 0 0.8em', 39 | }, 40 | h5: { 41 | fontSize: '15px', 42 | margin: '1.2em 0 0.8em', 43 | }, 44 | h6: { 45 | fontSize: '14px', 46 | margin: '1.2em 0 0.8em', 47 | color: '#4a5568', 48 | }, 49 | }, 50 | paragraph: { 51 | margin: '1.2em 0', 52 | lineHeight: '1.75', 53 | }, 54 | image: { 55 | maxWidth: '100%', 56 | margin: '1.5em auto', 57 | borderRadius: '4px', 58 | }, 59 | code: { 60 | fontFamily: 'Menlo, Monaco, Consolas, "Courier New", monospace', 61 | fontSize: '14px', 62 | lineHeight: '1.6', 63 | block: { 64 | background: '#fef8fa', 65 | padding: '1.2em', 66 | margin: '1.8em 0', 67 | borderRadius: '4px', 68 | color: '#4a3a42', 69 | overflow: 'auto', 70 | }, 71 | inline: { 72 | background: '#fef0f3', 73 | padding: '0.2em 0.4em', 74 | borderRadius: '3px', 75 | color: '#e66d85', 76 | fontFamily: 'Menlo, Monaco, Consolas, "Courier New", monospace', 77 | }, 78 | }, 79 | table: { 80 | width: '100%', 81 | margin: '1.5em 0', 82 | fontSize: '15px', 83 | borderCollapse: 'collapse', 84 | borderSpacing: '0', 85 | border: '1px solid #f7dde3', 86 | cell: { 87 | padding: '10px 16px', 88 | border: '1px solid #f7dde3', 89 | }, 90 | header: { 91 | background: '#fdf0f3', 92 | fontWeight: '600', 93 | border: '1px solid #f4c4ce', 94 | }, 95 | }, 96 | blockquote: { 97 | margin: '1.8em 0', 98 | padding: '1em 1.5em', 99 | borderLeft: '4px solid #e66d85', 100 | fontStyle: 'normal', 101 | color: '#4a5568', 102 | background: '#fdf5f7', 103 | borderRadius: '0 4px 4px 0', 104 | }, 105 | list: { 106 | margin: '1.2em 0', 107 | padding: '0 0 0 2em', 108 | item: { 109 | margin: '0.5em 0', 110 | lineHeight: '1.6', 111 | }, 112 | unordered: { 113 | listStyleType: 'disc', 114 | nestedLevel1: { 115 | listStyleType: 'circle', 116 | }, 117 | nestedLevel2: { 118 | listStyleType: 'square', 119 | }, 120 | }, 121 | ordered: { 122 | listStyleType: 'decimal', 123 | nestedLevel1: { 124 | listStyleType: 'lower-alpha', 125 | }, 126 | nestedLevel2: { 127 | listStyleType: 'lower-roman', 128 | }, 129 | }, 130 | }, 131 | link: { 132 | color: '#e66d85', 133 | textDecoration: 'none', 134 | borderBottom: '1px dotted #f5c3ce', 135 | }, 136 | hr: { 137 | margin: '2em 0', 138 | border: '1px solid #f7dde3', 139 | }, 140 | emphasis: { 141 | strong: { 142 | color: '#e66d85', 143 | fontWeight: 'bold', 144 | }, 145 | em: { 146 | color: '#e66d85', 147 | fontStyle: 'italic', 148 | }, 149 | }, 150 | }; -------------------------------------------------------------------------------- /themes/warm.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from './default'; 2 | 3 | export const warmTheme: Theme = { 4 | base: { 5 | fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', 6 | fontSize: '15px', 7 | lineHeight: '1.75', 8 | color: '#343434', 9 | letterSpacing: '0.03em', 10 | textAlign: 'left', 11 | }, 12 | headings: { 13 | color: '#e67e22', 14 | fontWeight: '600', 15 | letterSpacing: '0.02em', 16 | h1: { 17 | fontSize: '28px', 18 | margin: '1.5em 0 1em', 19 | textAlign: 'center', 20 | borderBottom: '2px solid #e67e22', 21 | paddingBottom: '0.5em', 22 | position: 'relative', 23 | }, 24 | h2: { 25 | fontSize: '22px', 26 | margin: '2em 0 1em', 27 | borderLeft: '4px solid #e67e22', 28 | paddingLeft: '12px', 29 | color: '#e67e22', 30 | }, 31 | h3: { 32 | fontSize: '18px', 33 | margin: '1.8em 0 1em', 34 | color: '#e67e22', 35 | }, 36 | h4: { 37 | fontSize: '16px', 38 | margin: '1.5em 0 0.8em', 39 | }, 40 | h5: { 41 | fontSize: '15px', 42 | margin: '1.2em 0 0.8em', 43 | }, 44 | h6: { 45 | fontSize: '14px', 46 | margin: '1.2em 0 0.8em', 47 | color: '#4a5568', 48 | }, 49 | }, 50 | paragraph: { 51 | margin: '1.2em 0', 52 | lineHeight: '1.75', 53 | }, 54 | image: { 55 | maxWidth: '100%', 56 | margin: '1.5em auto', 57 | borderRadius: '6px', 58 | }, 59 | code: { 60 | fontFamily: 'Consolas, Monaco, "Courier New", monospace', 61 | fontSize: '14px', 62 | lineHeight: '1.6', 63 | block: { 64 | margin: '1.8em 0', 65 | padding: '1.2em', 66 | borderRadius: '6px', 67 | background: '#fffaf5', 68 | color: '#d35400', 69 | overflow: 'auto', 70 | fontSize: '14px', 71 | lineHeight: '1.6', 72 | }, 73 | inline: { 74 | padding: '0.2em 0.4em', 75 | borderRadius: '3px', 76 | background: 'transparent', 77 | color: '#e67e22', 78 | fontFamily: 'Consolas, Monaco, "Courier New", monospace', 79 | fontSize: '85%', 80 | }, 81 | }, 82 | table: { 83 | width: '100%', 84 | margin: '1.5em 0', 85 | fontSize: '15px', 86 | borderCollapse: 'collapse', 87 | borderSpacing: '0', 88 | border: '1px solid #fae1cd', 89 | cell: { 90 | padding: '10px 16px', 91 | border: '1px solid #fae1cd', 92 | }, 93 | header: { 94 | background: '#fcf4ed', 95 | fontWeight: '600', 96 | border: '1px solid #f8d3b0', 97 | }, 98 | }, 99 | blockquote: { 100 | margin: '1.8em 0', 101 | padding: '1em 1.5em', 102 | borderLeft: '4px solid #e67e22', 103 | fontStyle: 'normal', 104 | color: '#4a5568', 105 | background: '#fff9f2', 106 | borderRadius: '0 6px 6px 0', 107 | }, 108 | list: { 109 | margin: '1.2em 0', 110 | padding: '0 0 0 1.5em', 111 | item: { 112 | margin: '0.5em 0', 113 | lineHeight: '1.75', 114 | }, 115 | unordered: { 116 | listStyleType: 'disc', 117 | nestedLevel1: { 118 | listStyleType: 'circle', 119 | }, 120 | nestedLevel2: { 121 | listStyleType: 'square', 122 | }, 123 | }, 124 | ordered: { 125 | listStyleType: 'decimal', 126 | nestedLevel1: { 127 | listStyleType: 'lower-alpha', 128 | }, 129 | nestedLevel2: { 130 | listStyleType: 'lower-roman', 131 | }, 132 | }, 133 | }, 134 | link: { 135 | color: '#e67e22', 136 | textDecoration: 'none', 137 | borderBottom: '1px solid #e67e22', 138 | }, 139 | hr: { 140 | margin: '2em 0', 141 | border: '1px solid #fae1cd', 142 | }, 143 | emphasis: { 144 | strong: { 145 | color: '#e67e22', 146 | fontWeight: '600', 147 | }, 148 | em: { 149 | color: '#e67e22', 150 | fontStyle: 'italic', 151 | }, 152 | }, 153 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /utils/defaultContent.ts: -------------------------------------------------------------------------------- 1 | export const defaultContent = `# 飞书文档转公众号排版工具 2 | 3 | 这是一个强大的**飞书文档转换工具**,可以帮助你快速将飞书文档转换为美观的公众号文章。 4 | 5 | ## 🚀 核心功能 6 | 7 | | 功能 | 描述 | 支持情况 | 8 | |:--|:--|:--:| 9 | | 一键转换 | 从飞书文档直接复制,自动转换为 Markdown | ✓ | 10 | | 实时预览 | 支持手机和电脑两种预览模式 | ✓ | 11 | | 多种主题 | 提供默认、暖色、极简三种主题 | ✓ | 12 | | 完整样式 | 支持图片、表格、代码等富文本格式 | ✓ | 13 | 14 | ## 📝 使用方法 15 | 16 | 1. 打开飞书文档,复制需要转换的内容 17 | 2. 粘贴到左侧编辑区 18 | 3. 右侧实时预览效果 19 | 4. 点击复制按钮,粘贴到公众号 20 | 21 | ## 🎨 支持的样式 22 | 23 | ### 文本格式化 24 | - **重点内容**使用粗体 25 | - *需要强调*使用斜体 26 | - \`重要代码\`使用行内代码 27 | - ~~已废弃内容~~使用删除线 28 | 29 | ### 代码展示 30 | 31 | \`\`\`javascript 32 | // 支持代码高亮 33 | function formatDocument(content) { 34 | return markdown.format(content); 35 | } 36 | \`\`\` 37 | 38 | ### 引用示例 39 | 40 | > 💡 小贴士:可以使用快捷键 \`Ctrl+V\` (Windows) 或 \`Command+V\` (Mac) 直接粘贴飞书文档内容。 41 | 42 | ### 列表功能 43 | 44 | #### 无序列表 45 | - 支持基础列表 46 | - 支持多级列表 47 | - 自动缩进对齐 48 | - 保持飞书格式 49 | 50 | #### 有序列表 51 | 1. 第一步:复制飞书内容 52 | 2. 第二步:粘贴到编辑器 53 | 1. 自动转换格式 54 | 2. 实时预览效果 55 | 3. 第三步:复制到公众号 56 | 57 | ## 🌈 主题切换 58 | 59 | 右下角提供了三种主题: 60 | 1. ☀️ 默认主题 - 清新简约 61 | 2. 💧 暖色主题 - 柔和舒适 62 | 3. 🪶 极简主题 - 简约大方 63 | 64 | ## 📱 预览模式 65 | 66 | 支持两种预览模式: 67 | - 手机预览 - 直观感受公众号效果 68 | - 电脑预览 - 宽屏编辑更方便 69 | 70 | ## 🔍 更多信息 71 | 72 | 访问[项目介绍](https://mp.weixin.qq.com/s/apH33XoZFtNwtRlkoqTX_Q)了解更多信息。 73 | 74 | --- 75 | 76 | ## 🎯 后续规划 77 | 78 | - [x] 基础功能完善 79 | - [x] 多主题支持 80 | - [ ] 自定义主题 81 | - [ ] 更多预设模板 82 | 83 | > 📢 如果你觉得这个工具有帮助,欢迎分享给更多人! 84 | `; -------------------------------------------------------------------------------- /utils/markdownConverter.ts: -------------------------------------------------------------------------------- 1 | import TurndownService from 'turndown'; 2 | 3 | // 创建Turndown服务实例 4 | const turndownService = new TurndownService({ 5 | headingStyle: 'atx', 6 | codeBlockStyle: 'fenced', 7 | emDelimiter: '*', 8 | }); 9 | 10 | // 自定义加粗文本的转换规则 11 | turndownService.addRule('strong', { 12 | filter: ['strong', 'b'], 13 | replacement: function (content, node, options) { 14 | const prevIsStrong = node.previousSibling?.nodeName?.toLowerCase() === 'strong' || 15 | node.previousSibling?.nodeName?.toLowerCase() === 'b'; 16 | const nextIsStrong = node.nextSibling?.nodeName?.toLowerCase() === 'strong' || 17 | node.nextSibling?.nodeName?.toLowerCase() === 'b'; 18 | 19 | const prefix = prevIsStrong ? '' : '**'; 20 | const suffix = nextIsStrong ? '' : '**'; 21 | 22 | const needSpaceBefore = !prevIsStrong && node.previousSibling && 23 | node.previousSibling.nodeType === 3 && 24 | !node.previousSibling.nodeValue?.endsWith(' '); 25 | 26 | const needSpaceAfter = !nextIsStrong && node.nextSibling && 27 | node.nextSibling.nodeType === 3 && 28 | !node.nextSibling.nodeValue?.startsWith(' '); 29 | 30 | return (needSpaceBefore ? ' ' : '') + 31 | prefix + content.trim() + suffix + 32 | (needSpaceAfter ? ' ' : ''); 33 | } 34 | }); 35 | 36 | // 获取单元格对齐方式 37 | function getCellAlignment(cell: HTMLElement): string { 38 | const style = cell.getAttribute('style') || ''; 39 | const className = cell.getAttribute('class') || ''; 40 | 41 | if (style.includes('text-align: center') || className.includes('align-center')) { 42 | return ':---:'; 43 | } 44 | if (style.includes('text-align: right') || className.includes('align-right')) { 45 | return '---:'; 46 | } 47 | return ':---'; // 默认左对齐 48 | } 49 | 50 | // 处理图片 51 | function processImage(img: HTMLImageElement): string { 52 | let src = img.getAttribute('src') || ''; 53 | const alt = img.alt || ''; 54 | const originSrc = img.getAttribute('data-origin-src'); 55 | const tokenSrc = img.getAttribute('data-token-src'); 56 | 57 | // 获取图片尺寸 58 | const width = img.getAttribute('width') || img.style.width || ''; 59 | const height = img.getAttribute('height') || img.style.height || ''; 60 | const style = img.getAttribute('style') || ''; 61 | 62 | // 从style中提取宽高 63 | const widthMatch = style.match(/width:\s*(\d+)px/); 64 | const heightMatch = style.match(/height:\s*(\d+)px/); 65 | const styleWidth = widthMatch ? widthMatch[1] : ''; 66 | const styleHeight = heightMatch ? heightMatch[1] : ''; 67 | 68 | // 优先使用飞书的原始图片链接 69 | if (originSrc) { 70 | src = originSrc; 71 | } else if (tokenSrc) { 72 | src = tokenSrc; 73 | } 74 | 75 | // 如果是base64图片,直接使用 76 | if (src.startsWith('data:image')) { 77 | return `![${alt}](${src})`; 78 | } 79 | 80 | // 处理飞书域名的图片 81 | if (src.includes('feishu.cn') || src.includes('larksuite.com')) { 82 | if (!src.startsWith('http')) { 83 | src = 'https:' + src; 84 | } 85 | } 86 | 87 | // 构建HTML格式的图片标签以保持尺寸 88 | const actualWidth = width || styleWidth; 89 | const actualHeight = height || styleHeight; 90 | 91 | if (actualWidth || actualHeight) { 92 | const sizeStyle = []; 93 | if (actualWidth) sizeStyle.push(`width: ${actualWidth}px`); 94 | if (actualHeight) sizeStyle.push(`height: ${actualHeight}px`); 95 | return `${alt}`; 96 | } 97 | 98 | return `![${alt}](${src})`; 99 | } 100 | 101 | // 处理单元格内容 102 | function processCellContent(cell: HTMLElement): string { 103 | // 处理图片 104 | const img = cell.querySelector('img'); 105 | if (img) { 106 | return processImage(img); 107 | } 108 | 109 | // 处理加粗 110 | const boldText = Array.from(cell.querySelectorAll('strong, b')).map(b => b.textContent).join(' '); 111 | if (boldText) { 112 | return `**${boldText}**`; 113 | } 114 | 115 | // 处理普通文本 116 | const text = cell.textContent?.trim() || ''; 117 | return text.replace(/\|/g, '\\|'); // 转义表格中的竖线 118 | } 119 | 120 | // 配置图片转换规则 121 | turndownService.addRule('image', { 122 | filter: 'img', 123 | replacement: function(content, node) { 124 | const img = node as HTMLImageElement; 125 | return processImage(img); 126 | } 127 | }); 128 | 129 | // 配置代码块规则 130 | turndownService.addRule('codeBlock', { 131 | filter: function(node: HTMLElement): boolean { 132 | if (node.nodeName !== 'PRE') return false; 133 | const code = node.firstChild as HTMLElement | null; 134 | return !!(code && code.nodeName === 'CODE'); 135 | }, 136 | replacement: function(content: string, node: Node) { 137 | const code = node.firstChild as HTMLElement; 138 | const className = code?.getAttribute('class') || ''; 139 | const lang = className.replace('language-', '') || ''; 140 | const text = code?.textContent || ''; 141 | return '\n```' + lang + '\n' + text + '\n```\n'; 142 | } 143 | }); 144 | 145 | // 配置表格转换规则 146 | turndownService.addRule('table', { 147 | filter: 'table', 148 | replacement: function(content: string, node: Node) { 149 | const table = node as HTMLTableElement; 150 | let markdown = '\n'; 151 | 152 | // 处理表头 153 | const headerRow = table.querySelector('tr'); 154 | if (headerRow) { 155 | const headers = Array.from(headerRow.querySelectorAll('th,td')).map(cell => { 156 | return processCellContent(cell as HTMLElement); 157 | }); 158 | 159 | // 获取每列的对齐方式 160 | const alignments = Array.from(headerRow.querySelectorAll('th,td')).map(cell => { 161 | return getCellAlignment(cell as HTMLElement); 162 | }); 163 | 164 | markdown += '| ' + headers.join(' | ') + ' |\n'; 165 | markdown += '|' + alignments.join('|') + '|\n'; 166 | } 167 | 168 | // 处理表格内容 169 | const rows = Array.from(table.querySelectorAll('tr')).slice(1); 170 | rows.forEach(row => { 171 | const cells = Array.from(row.querySelectorAll('td')).map(cell => { 172 | return processCellContent(cell as HTMLElement); 173 | }); 174 | markdown += '| ' + cells.join(' | ') + ' |\n'; 175 | }); 176 | 177 | return markdown + '\n'; 178 | } 179 | }); 180 | 181 | /** 182 | * 将HTML内容转换为Markdown 183 | */ 184 | export function convertHtmlToMarkdown(html: string): string { 185 | return turndownService.turndown(html); 186 | } 187 | 188 | /** 189 | * 处理HTML内容以便复制到微信公众号 190 | */ 191 | export function processHtmlForWeixin(htmlContent: string): string { 192 | // 创建临时DOM元素用于处理HTML 193 | const tempDiv = document.createElement('div'); 194 | tempDiv.innerHTML = htmlContent; 195 | 196 | // 尝试智能识别主要内容区 197 | const contentRoot = tempDiv.querySelector('.markdown-body') || 198 | tempDiv.querySelector('div[style] > div') || 199 | tempDiv.querySelector('div > div > div') || 200 | tempDiv; 201 | 202 | // 移除可能干扰内容的UI元素 203 | const uiSelectors = ['.toolbar', '.status-bar-content', '.phone-status-bar', '.phone-home-indicator']; 204 | uiSelectors.forEach(selector => { 205 | contentRoot.querySelectorAll(selector).forEach(el => el.parentNode?.removeChild(el)); 206 | }); 207 | 208 | // 处理列表项中的文本节点 209 | contentRoot.querySelectorAll('li').forEach(li => { 210 | Array.from(li.childNodes).forEach(node => { 211 | if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim()) { 212 | const span = document.createElement('span'); 213 | span.style.display = 'inline'; 214 | span.textContent = node.textContent; 215 | node.parentNode?.replaceChild(span, node); 216 | } 217 | }); 218 | }); 219 | 220 | // 处理图片元素 221 | contentRoot.querySelectorAll('img').forEach(img => { 222 | img.style.maxWidth = img.style.maxWidth || '100%'; 223 | img.style.display = img.style.display || 'block'; 224 | img.style.margin = img.style.margin || '10px auto'; 225 | }); 226 | 227 | // 处理代码块 228 | contentRoot.querySelectorAll('pre').forEach(pre => { 229 | if (pre instanceof HTMLElement) { 230 | pre.style.backgroundColor = '#f6f8fa'; 231 | pre.style.borderRadius = '3px'; 232 | pre.style.padding = '16px'; 233 | pre.style.overflow = 'auto'; 234 | pre.style.fontSize = '14px'; 235 | pre.style.lineHeight = '1.45'; 236 | pre.style.fontFamily = 'SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace'; 237 | } 238 | }); 239 | 240 | // 处理表格 241 | contentRoot.querySelectorAll('table').forEach(table => { 242 | if (table instanceof HTMLElement) { 243 | table.style.borderCollapse = 'collapse'; 244 | table.style.width = '100%'; 245 | table.style.margin = '16px 0'; 246 | 247 | table.querySelectorAll('th, td').forEach(cell => { 248 | if (cell instanceof HTMLElement) { 249 | cell.style.border = cell.style.border || '1px solid #dfe2e5'; 250 | cell.style.padding = cell.style.padding || '8px 12px'; 251 | } 252 | }); 253 | 254 | table.querySelectorAll('th').forEach(th => { 255 | if (th instanceof HTMLElement) { 256 | th.style.backgroundColor = '#f6f8fa'; 257 | th.style.fontWeight = 'bold'; 258 | } 259 | }); 260 | } 261 | }); 262 | 263 | // 添加内联样式表 264 | const style = document.createElement('style'); 265 | style.textContent = ` 266 | body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; } 267 | p { margin: 10px 0; } 268 | img { max-width: 100%; display: block; margin: 10px auto; } 269 | code { background-color: rgba(0, 0, 0, 0.05); padding: 0.2em 0.4em; border-radius: 3px; font-family: monospace; } 270 | pre { background-color: #f6f8fa; padding: 16px; overflow: auto; border-radius: 3px; margin: 16px 0; } 271 | blockquote { padding-left: 1em; border-left: 4px solid #ddd; color: #666; margin: 16px 0; } 272 | table { border-collapse: collapse; width: 100%; margin: 16px 0; } 273 | th, td { border: 1px solid #dfe2e5; padding: 8px 12px; } 274 | th { background-color: #f6f8fa; } 275 | `; 276 | 277 | // 返回带样式的HTML 278 | return style.outerHTML + (contentRoot === tempDiv ? contentRoot.innerHTML : contentRoot.outerHTML); 279 | } 280 | 281 | export default turndownService; -------------------------------------------------------------------------------- /utils/themeUtils.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from '../themes/default'; 2 | 3 | export function generateInlineStyles(theme: Theme) { 4 | return { 5 | div: { 6 | fontFamily: theme.base.fontFamily, 7 | fontSize: theme.base.fontSize, 8 | lineHeight: theme.base.lineHeight, 9 | color: theme.base.color, 10 | letterSpacing: theme.base.letterSpacing, 11 | textAlign: theme.base.textAlign as 'justify', 12 | }, 13 | h1: { 14 | fontWeight: theme.headings.fontWeight, 15 | color: theme.headings.color, 16 | fontSize: theme.headings.h1.fontSize, 17 | margin: theme.headings.h1.margin, 18 | textAlign: theme.headings.h1.textAlign as 'center', 19 | lineHeight: '1.5', 20 | letterSpacing: theme.headings.letterSpacing, 21 | borderBottom: theme.headings.h1.borderBottom, 22 | paddingBottom: theme.headings.h1.paddingBottom, 23 | position: theme.headings.h1.position as 'relative', 24 | }, 25 | h2: { 26 | fontWeight: theme.headings.fontWeight, 27 | color: theme.headings.color, 28 | fontSize: theme.headings.h2.fontSize, 29 | margin: theme.headings.h2.margin, 30 | lineHeight: '1.5', 31 | letterSpacing: theme.headings.letterSpacing, 32 | borderLeft: theme.headings.h2.borderLeft, 33 | paddingLeft: theme.headings.h2.paddingLeft, 34 | }, 35 | h3: { 36 | fontWeight: theme.headings.fontWeight, 37 | color: theme.headings.color, 38 | fontSize: theme.headings.h3.fontSize, 39 | margin: theme.headings.h3.margin, 40 | lineHeight: '1.5', 41 | letterSpacing: theme.headings.letterSpacing, 42 | }, 43 | h4: { 44 | fontWeight: theme.headings.fontWeight, 45 | color: theme.headings.color, 46 | fontSize: theme.headings.h4.fontSize, 47 | margin: theme.headings.h4.margin, 48 | lineHeight: '1.5', 49 | letterSpacing: theme.headings.letterSpacing, 50 | }, 51 | h5: { 52 | fontWeight: theme.headings.fontWeight, 53 | color: theme.headings.color, 54 | fontSize: theme.headings.h5.fontSize, 55 | margin: theme.headings.h5.margin, 56 | lineHeight: '1.5', 57 | letterSpacing: theme.headings.letterSpacing, 58 | }, 59 | h6: { 60 | fontWeight: theme.headings.fontWeight, 61 | color: theme.headings.h6.color, 62 | fontSize: theme.headings.h6.fontSize, 63 | margin: theme.headings.h6.margin, 64 | lineHeight: '1.5', 65 | letterSpacing: theme.headings.letterSpacing, 66 | }, 67 | p: { 68 | margin: theme.paragraph.margin, 69 | lineHeight: theme.paragraph.lineHeight, 70 | }, 71 | img: { 72 | maxWidth: theme.image.maxWidth, 73 | height: 'auto', 74 | margin: theme.image.margin, 75 | display: 'block', 76 | borderRadius: theme.image.borderRadius, 77 | }, 78 | pre: { 79 | fontFamily: theme.code.fontFamily, 80 | fontSize: theme.code.fontSize, 81 | lineHeight: theme.code.lineHeight, 82 | background: theme.code.block.background, 83 | padding: theme.code.block.padding, 84 | margin: theme.code.block.margin, 85 | borderRadius: theme.code.block.borderRadius, 86 | color: theme.code.block.color, 87 | overflow: 'auto', 88 | }, 89 | codeInline: { 90 | fontFamily: theme.code.fontFamily, 91 | background: theme.code.inline.background, 92 | padding: theme.code.inline.padding, 93 | borderRadius: theme.code.inline.borderRadius, 94 | fontSize: '0.9em', 95 | color: theme.code.inline.color, 96 | }, 97 | codeBlock: { 98 | fontFamily: theme.code.fontFamily, 99 | }, 100 | table: { 101 | borderCollapse: 'collapse' as const, 102 | width: theme.table.width, 103 | margin: theme.table.margin, 104 | fontSize: theme.table.fontSize, 105 | borderSpacing: theme.table.borderSpacing, 106 | border: theme.table.border, 107 | }, 108 | th: { 109 | border: theme.table.header.border, 110 | padding: theme.table.cell.padding, 111 | background: theme.table.header.background, 112 | fontWeight: theme.table.header.fontWeight, 113 | }, 114 | td: { 115 | border: theme.table.cell.border, 116 | padding: theme.table.cell.padding, 117 | }, 118 | blockquote: { 119 | margin: theme.blockquote.margin, 120 | padding: theme.blockquote.padding, 121 | background: theme.blockquote.background, 122 | borderRadius: theme.blockquote.borderRadius, 123 | color: theme.blockquote.color, 124 | borderLeft: theme.blockquote.borderLeft, 125 | }, 126 | ul: { 127 | margin: theme.list.margin, 128 | padding: theme.list.padding, 129 | listStyleType: theme.list.unordered.listStyleType, 130 | }, 131 | ol: { 132 | margin: theme.list.margin, 133 | padding: theme.list.padding, 134 | listStyleType: theme.list.ordered.listStyleType, 135 | }, 136 | ulNested1: { 137 | margin: theme.list.margin, 138 | padding: theme.list.padding, 139 | listStyleType: theme.list.unordered.nestedLevel1.listStyleType, 140 | }, 141 | ulNested2: { 142 | margin: theme.list.margin, 143 | padding: theme.list.padding, 144 | listStyleType: theme.list.unordered.nestedLevel2.listStyleType, 145 | }, 146 | olNested1: { 147 | margin: theme.list.margin, 148 | padding: theme.list.padding, 149 | listStyleType: theme.list.ordered.nestedLevel1.listStyleType, 150 | }, 151 | olNested2: { 152 | margin: theme.list.margin, 153 | padding: theme.list.padding, 154 | listStyleType: theme.list.ordered.nestedLevel2.listStyleType, 155 | }, 156 | li: { 157 | margin: theme.list.item.margin, 158 | lineHeight: theme.list.item.lineHeight, 159 | }, 160 | a: { 161 | color: theme.link.color, 162 | textDecoration: theme.link.textDecoration, 163 | borderBottom: theme.link.borderBottom, 164 | }, 165 | hr: { 166 | margin: theme.hr.margin, 167 | border: 'none', 168 | borderTop: theme.hr.border, 169 | }, 170 | strong: { 171 | color: theme.emphasis.strong.color, 172 | fontWeight: theme.emphasis.strong.fontWeight, 173 | }, 174 | em: { 175 | color: theme.emphasis.em.color, 176 | fontStyle: theme.emphasis.em.fontStyle, 177 | }, 178 | code: { 179 | fontFamily: theme.code.fontFamily, 180 | fontSize: theme.code.fontSize, 181 | lineHeight: theme.code.lineHeight, 182 | background: theme.code.inline.background, 183 | padding: theme.code.inline.padding, 184 | borderRadius: theme.code.inline.borderRadius, 185 | color: theme.code.inline.color, 186 | }, 187 | }; 188 | } 189 | 190 | --------------------------------------------------------------------------------