├── .gitignore
├── LICENSE
├── README.md
├── build.sh
├── esbuild.config.mjs
├── manifest.json
├── package-lock.json
├── package.json
├── src
├── assets
│ ├── donate.ts
│ ├── donate
│ │ ├── alipay.png
│ │ └── wechat_pay.png
│ └── qrcode.ts
├── backgroundManager.ts
├── backgrounds
│ └── index.ts
├── converter.ts
├── copyManager.ts
├── donateManager.ts
├── main.ts
├── settings
│ ├── ConfirmModal.ts
│ ├── CreateBackgroundModal.ts
│ ├── CreateFontModal.ts
│ ├── CreateTemplateModal.ts
│ ├── MPSettingTab.ts
│ ├── settings.ts
│ └── templatePreviewModal.ts
├── styles
│ ├── index.css
│ ├── settings
│ │ ├── background-modal.css
│ │ ├── font-modal.css
│ │ ├── settings.css
│ │ ├── template-modal.css
│ │ └── template-preview-modal.css
│ └── view
│ │ ├── layout.css
│ │ └── preview.css
├── templateManager.ts
├── templates
│ ├── academic.json
│ ├── brown.json
│ ├── dark.json
│ ├── darkgreen.json
│ ├── default.json
│ ├── elegant.json
│ ├── index.ts
│ ├── minimal.json
│ ├── orange.json
│ ├── scarlet.json
│ ├── yeban-orange.json
│ └── yeban.json
├── utils
│ └── nanoid.ts
└── view.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # 依赖目录
2 | node_modules/
3 |
4 | # 构建输出
5 | dist/
6 | main.js
7 | data.json
8 | styles.css
9 | # 操作系统文件
10 | .DS_Store
11 | Thumbs.db
12 |
13 | # IDE 配置
14 | .vscode/
15 | .idea/
16 |
17 | # 日志文件
18 | *.log
19 |
20 | # 环境变量
21 | .env
22 | .env.local
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 夜半Yeban
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MP Preview
2 | 一键将 Obsidian 笔记转换为微信公众号格式的插件。
3 |
4 |    [](#支持作者)
5 |
6 | ## 功能特点
7 | - 📝 一键将 Markdown 文档转换为微信公众号格式
8 | - 🎨 提供多种精美模板,支持自定义字体和字号
9 | - 🖼️ 支持自定义背景和样式
10 | - 🔄 实时预览编辑效果
11 | - 📋 一键复制到剪贴板,直接粘贴到公众号
12 | - 🔒 锁定功能避免预览刷新打断书写
13 |
14 | ## 使用方法
15 | 1. 打开任意 Markdown 文档
16 | 2. 点击侧边栏的公众号图标打开预览面板
17 | 3. 选择喜欢的模板和字体
18 | 4. 调整字号大小以适应内容
19 | 5. 实时预览编辑效果
20 | 6. 点击【复制到公众号】按钮
21 | 7. 直接粘贴到微信公众号编辑器中
22 |
23 | ## 安装方法
24 | ### 从 Obsidian 社区插件安装(推荐)
25 | 1. 打开 Obsidian 设置
26 | 2. 转到第三方插件设置
27 | 3. 关闭安全模式
28 | 4. 点击浏览社区插件
29 | 5. 搜索 "MP Preview"
30 | 6. 点击安装并启用插件
31 |
32 | ### 手动安装
33 | 1. 下载最新版本的 release 文件:https://github.com/Yeban8090/mp-preview/releases
34 | 2. 解压后将文件夹复制到 Obsidian 插件目录:`{vault}/.obsidian/plugins/`
35 | 3. 重启 Obsidian
36 | 4. 在设置中启用插件
37 |
38 | ## 使用技巧
39 | - 使用锁定按钮(🔒)可以暂停实时预览,避免编辑大文档时频繁刷新
40 | - 调整字号大小以获得最佳阅读体验
41 | - 不同模板适合不同类型的文章,可以根据内容选择合适的模板
42 | - 复制前可以预览最终效果,确保格式正确
43 | - 支持代码块、表格、图片等 Markdown 元素
44 |
45 | ## 支持的语言
46 | 插件界面目前支持:
47 | - 简体中文
48 |
49 | ## 支持作者
50 | 如果这个插件对你有所帮助,可以考虑请作者喝杯咖啡 ☕:
51 |
52 |
53 |
54 |
55 |
微信支付
56 |

57 |
58 |
59 |
支付宝
60 |

61 |
62 |
68 |
69 |
70 |
71 | 您的支持是我持续改进这个插件的动力!
72 |
73 | ## 许可证
74 | MIT License。查看 [LICENSE](LICENSE) 获取更多信息。
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # 获取版本号
4 | version=$(grep '"version"' manifest.json | cut -d '"' -f 4)
5 | zip_name="mp-preview-${version}.zip"
6 |
7 | # 检查目标文件是否存在
8 | if [ -f "../../${zip_name}" ]; then # 修改路径检查到上两层
9 | read -p "文件 ${zip_name} 已存在,是否覆盖?(y/n) " answer
10 | if [ "$answer" != "y" ]; then
11 | echo "打包已取消"
12 | exit 1
13 | fi
14 | fi
15 |
16 | # 创建临时目录
17 | mkdir -p ../temp/mp-preview
18 |
19 | # 复制必要文件
20 | cp main.js manifest.json styles.css ../temp/mp-preview/
21 | cp -r assets ../temp/mp-preview/
22 |
23 | # 切换到临时目录的上级目录
24 | cd ../temp
25 |
26 | # 创建 zip 文件
27 | zip -r "${zip_name}" mp-preview
28 |
29 | # 移动 zip 文件到上两层目录
30 | mv "${zip_name}" ../../
31 |
32 | echo "打包完成:${zip_name}"
--------------------------------------------------------------------------------
/esbuild.config.mjs:
--------------------------------------------------------------------------------
1 | import esbuild from "esbuild";
2 | import process from "process";
3 | import builtins from "builtin-modules";
4 |
5 | const banner =
6 | `/*
7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
8 | if you want to view the source, please visit the github repository of this plugin
9 | */
10 | `;
11 |
12 | const prod = process.argv[2] === "production";
13 |
14 | const config = {
15 | banner: {
16 | js: banner,
17 | },
18 | entryPoints: ["src/main.ts"],
19 | bundle: true,
20 | external: [
21 | "obsidian",
22 | "electron",
23 | "@codemirror/autocomplete",
24 | ...builtins
25 | ],
26 | format: "cjs",
27 | target: "es2018",
28 | logLevel: "info",
29 | sourcemap: prod ? false : "inline",
30 | treeShaking: true,
31 | outfile: "main.js",
32 | };
33 |
34 | // 修改 CSS 配置部分
35 | const cssConfig = {
36 | entryPoints: ['src/styles/index.css'],
37 | bundle: true,
38 | outfile: 'styles.css', // 直接输出到项目根目录
39 | allowOverwrite: true
40 | };
41 |
42 | if (prod) {
43 | await Promise.all([
44 | esbuild.build(config),
45 | esbuild.build(cssConfig)
46 | ]).catch(() => process.exit(1));
47 | } else {
48 | const [jsContext, cssContext] = await Promise.all([
49 | esbuild.context(config),
50 | esbuild.context(cssConfig)
51 | ]);
52 |
53 | await Promise.all([
54 | jsContext.watch(),
55 | cssContext.watch()
56 | ]);
57 |
58 | console.log("⚡ 正在监听 JavaScript 和 CSS 变更...");
59 | }
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "mp-preview",
3 | "name": "MP Preview",
4 | "version": "1.0.4",
5 | "minAppVersion": "0.15.0",
6 | "description": "Preview and convert Markdown files to MP format",
7 | "author": "Yeban",
8 | "isDesktopOnly": true
9 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mp-preview",
3 | "version": "1.0.2",
4 | "description": "一键将 Markdown 文档转换为微信公众号格式",
5 | "main": "main.js",
6 | "scripts": {
7 | "dev": "node esbuild.config.mjs",
8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
9 | "version": "node version-bump.mjs && git add manifest.json versions.json"
10 | },
11 | "keywords": [
12 | "obsidian",
13 | "plugin"
14 | ],
15 | "author": "",
16 | "license": "MIT",
17 | "devDependencies": {
18 | "@types/node": "^16.11.6",
19 | "@typescript-eslint/eslint-plugin": "5.29.0",
20 | "@typescript-eslint/parser": "5.29.0",
21 | "builtin-modules": "3.3.0",
22 | "esbuild": "0.17.3",
23 | "obsidian": "latest",
24 | "tslib": "2.4.0",
25 | "typescript": "4.7.4"
26 | },
27 | "dependencies": {
28 | "nanoid": "^5.1.5"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/assets/donate/alipay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yeban8090/mp-preview/cb2d236f946abd10b4a6a07fb56342f61b11e3f3/src/assets/donate/alipay.png
--------------------------------------------------------------------------------
/src/assets/donate/wechat_pay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yeban8090/mp-preview/cb2d236f946abd10b4a6a07fb56342f61b11e3f3/src/assets/donate/wechat_pay.png
--------------------------------------------------------------------------------
/src/backgroundManager.ts:
--------------------------------------------------------------------------------
1 | import { SettingsManager } from "./settings/settings";
2 |
3 | export interface Background {
4 | id: string;
5 | name: string;
6 | style: string;
7 | isPreset?: boolean;
8 | isVisible?: boolean;
9 | }
10 |
11 | export class BackgroundManager {
12 | private currentBackground: Background | null = null;
13 | private settingsManager: SettingsManager;
14 |
15 | constructor(settingsManager: SettingsManager) {
16 | this.settingsManager = settingsManager;
17 | }
18 |
19 | public setBackground(id: string | null): boolean {
20 | if (!id) {
21 | this.currentBackground = null;
22 | return true;
23 | }
24 |
25 | const background = this.settingsManager.getBackground(id);
26 | if (background) {
27 | // 检查背景是否可见
28 | if (background.isVisible === false) {
29 | console.warn(`尝试设置不可见的背景: ${id}`);
30 | return false;
31 | }
32 |
33 | this.currentBackground = background;
34 | return true;
35 | }
36 |
37 | console.warn(`未找到背景: ${id}`);
38 | return false;
39 | }
40 |
41 | public applyBackground(element: HTMLElement) {
42 | const section = element.querySelector('.mp-content-section');
43 | if (section) {
44 | if (!this.currentBackground) {
45 | section.setAttribute('style', ''); // 当没有背景时,清除样式
46 | return;
47 | }
48 | section.setAttribute('style', this.currentBackground.style);
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/src/backgrounds/index.ts:
--------------------------------------------------------------------------------
1 | export const backgrounds = {
2 | backgrounds: [
3 | {
4 | id: "grid",
5 | name: "网格",
6 | style: "box-sizing: border-box; margin: 0; padding: 0; background-image: linear-gradient(90deg, rgba(50, 0, 0, 0.03) 2%, rgba(0, 0, 0, 0) 2%), linear-gradient(360deg, rgba(50, 0, 0, 0.03) 2%, rgba(0, 0, 0, 0) 2%); background-size: 20px 20px; background-position: center center;"
7 | },
8 | {
9 | id: "crosshatch",
10 | name: "交叉",
11 | style: "box-sizing: border-box; margin: 0; padding: 0; background-image: repeating-linear-gradient(45deg, rgba(50, 0, 0, 0.02) 0, rgba(50, 0, 0, 0.02) 1px, transparent 1px, transparent 50%), repeating-linear-gradient(-45deg, rgba(50, 0, 0, 0.02) 0, rgba(50, 0, 0, 0.02) 1px, transparent 1px, transparent 50%); background-size: 20px 20px;"
12 | },
13 | {
14 | id: "dots",
15 | name: "圆点",
16 | style: "box-sizing: border-box; margin: 0; padding: 0; background-image: radial-gradient(rgba(50, 0, 0, 0.03) 1px, transparent 1px); background-size: 20px 20px; background-position: center center;"
17 | },
18 | {
19 | id: "dash",
20 | name: "虚线",
21 | style: "box-sizing: border-box; margin: 0; padding: 0; background-image: linear-gradient(90deg, rgba(50, 0, 0, 0.03) 50%, transparent 50%); background-size: 8px 1px; background-position: center center;"
22 | },
23 | {
24 | id: "wave",
25 | name: "波浪",
26 | style: "box-sizing: border-box; margin: 0; padding: 0; background-image: linear-gradient(45deg, rgba(50, 0, 0, 0.04) 12%, transparent 12%, transparent 88%, rgba(50, 0, 0, 0.04) 88%), linear-gradient(135deg, rgba(50, 0, 0, 0.04) 12%, transparent 12%, transparent 88%, rgba(50, 0, 0, 0.04) 88%), linear-gradient(45deg, rgba(50, 0, 0, 0.04) 12%, transparent 12%, transparent 88%, rgba(50, 0, 0, 0.04) 88%), linear-gradient(135deg, rgba(50, 0, 0, 0.04) 12%, transparent 12%, transparent 88%, rgba(50, 0, 0, 0.04) 88%); background-size: 30px 30px; background-position: 0 0, 0 0, 15px 15px, 15px 15px;"
27 | },
28 | {
29 | id: "checkerboard",
30 | name: "棋盘",
31 | style: "box-sizing: border-box; margin: 0; padding: 0; background-image: linear-gradient(45deg, rgba(50, 0, 0, 0.04) 25%, transparent 25%), linear-gradient(-45deg, rgba(50, 0, 0, 0.04) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, rgba(50, 0, 0, 0.04) 75%), linear-gradient(-45deg, transparent 75%, rgba(50, 0, 0, 0.04) 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px;"
32 | }
33 | ]
34 | };
--------------------------------------------------------------------------------
/src/converter.ts:
--------------------------------------------------------------------------------
1 | import { App } from 'obsidian';
2 |
3 | export class MPConverter {
4 | private static app: App;
5 |
6 | static initialize(app: App) {
7 | this.app = app;
8 | }
9 |
10 | static formatContent(element: HTMLElement): void {
11 | // 创建 section 容器
12 | const section = document.createElement('section');
13 | section.className = 'mp-content-section';
14 | // 移动原有内容到 section 中
15 | while (element.firstChild) {
16 | section.appendChild(element.firstChild);
17 | }
18 | element.appendChild(section);
19 |
20 | // 处理元素
21 | this.processElements(section);
22 | }
23 |
24 | private static processElements(container: HTMLElement | null): void {
25 | if (!container) return;
26 | // 处理列表项内部元素,用section包裹
27 | container.querySelectorAll('li').forEach(li => {
28 | // 创建section元素
29 | const section = document.createElement('section');
30 | // 将li的所有子元素移动到section中
31 | while (li.firstChild) {
32 | section.appendChild(li.firstChild);
33 | }
34 | // 将section添加到li中
35 | li.appendChild(section);
36 | });
37 |
38 | // 处理代码块
39 | container.querySelectorAll('pre code').forEach(el => {
40 | const pre = el.parentElement;
41 | if (pre) {
42 | // 添加 macOS 风格的窗口按钮
43 | const header = document.createElement('div');
44 | header.className = 'mp-code-header';
45 |
46 | // 添加三个窗口按钮
47 | for (let i = 0; i < 3; i++) {
48 | const dot = document.createElement('span');
49 | dot.className = 'mp-code-dot';
50 | header.appendChild(dot);
51 | }
52 |
53 | pre.insertBefore(header, pre.firstChild);
54 |
55 | // 移除原有的复制按钮
56 | const copyButton = pre.querySelector('.copy-code-button');
57 | if (copyButton) {
58 | copyButton.remove();
59 | }
60 | }
61 | });
62 |
63 | // 处理图片
64 | container.querySelectorAll('span.internal-embed[alt][src]').forEach(async el => {
65 | const originalSpan = el as HTMLElement;
66 | const src = originalSpan.getAttribute('src');
67 | const alt = originalSpan.getAttribute('alt');
68 |
69 | if (!src) return;
70 |
71 | try {
72 | const linktext = src.split('|')[0];
73 | const file = this.app.metadataCache.getFirstLinkpathDest(linktext, '');
74 | if (file) {
75 | const absolutePath = this.app.vault.adapter.getResourcePath(file.path);
76 | const newImg = document.createElement('img');
77 | newImg.src = absolutePath;
78 | if (alt) newImg.alt = alt;
79 | originalSpan.parentNode?.replaceChild(newImg, originalSpan);
80 | }
81 | } catch (error) {
82 | console.error('图片处理失败:', error);
83 | }
84 | });
85 | }
86 | }
--------------------------------------------------------------------------------
/src/copyManager.ts:
--------------------------------------------------------------------------------
1 | import { Notice } from 'obsidian';
2 |
3 | export class CopyManager {
4 | private static cleanupHtml(element: HTMLElement): string {
5 | // 创建克隆以避免修改原始元素
6 | const clone = element.cloneNode(true) as HTMLElement;
7 |
8 | // 移除所有的 data-* 属性
9 | clone.querySelectorAll('*').forEach(el => {
10 | Array.from(el.attributes).forEach(attr => {
11 | if (attr.name.startsWith('data-')) {
12 | el.removeAttribute(attr.name);
13 | }
14 | });
15 | });
16 |
17 | // 移除所有的 class 属性
18 | clone.querySelectorAll('*').forEach(el => {
19 | el.removeAttribute('class');
20 | });
21 |
22 | // 移除所有的 id 属性
23 | clone.querySelectorAll('*').forEach(el => {
24 | el.removeAttribute('id');
25 | });
26 |
27 | // 使用 XMLSerializer 安全地转换为字符串
28 | const serializer = new XMLSerializer();
29 | return serializer.serializeToString(clone);
30 | }
31 |
32 | private static async processImages(container: HTMLElement): Promise {
33 | const images = container.querySelectorAll('img');
34 | const imageArray = Array.from(images);
35 |
36 | for (const img of imageArray) {
37 | try {
38 | const response = await fetch(img.src);
39 | const blob = await response.blob();
40 | const reader = new FileReader();
41 | await new Promise((resolve, reject) => {
42 | reader.onload = () => {
43 | img.src = reader.result as string;
44 | resolve(null);
45 | };
46 | reader.onerror = reject;
47 | reader.readAsDataURL(blob);
48 | });
49 | } catch (error) {
50 | console.error('图片转换失败:', error);
51 | }
52 | }
53 | }
54 |
55 | public static async copyToClipboard(element: HTMLElement): Promise {
56 | try {
57 | const clone = element.cloneNode(true) as HTMLElement;
58 | await this.processImages(clone);
59 |
60 | const contentSection = clone.querySelector('.mp-content-section');
61 | if (!contentSection) {
62 | throw new Error('找不到内容区域');
63 | }
64 | // 使用新的 cleanupHtml 方法
65 | const cleanHtml = this.cleanupHtml(contentSection as HTMLElement);
66 |
67 | const clipData = new ClipboardItem({
68 | 'text/html': new Blob([cleanHtml], { type: 'text/html' }),
69 | 'text/plain': new Blob([clone.textContent || ''], { type: 'text/plain' })
70 | });
71 |
72 | await navigator.clipboard.write([clipData]);
73 | new Notice('已复制到剪贴板');
74 | } catch (error) {
75 | new Notice('复制失败');
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/src/donateManager.ts:
--------------------------------------------------------------------------------
1 |
2 | import { App, Plugin } from 'obsidian';
3 | import { DONATE_QR } from './assets/donate';
4 | import { QRCODE_QR } from './assets/qrcode';
5 |
6 | export class DonateManager {
7 | private static overlay: HTMLElement;
8 | private static modal: HTMLElement;
9 | private static app: App;
10 | private static plugin: Plugin;
11 |
12 | public static initialize(app: App, plugin: Plugin) {
13 | this.app = app;
14 | this.plugin = plugin;
15 | }
16 |
17 | public static showDonateModal(container: HTMLElement) {
18 | this.overlay = container.createEl('div', {
19 | cls: 'mp-donate-overlay'
20 | });
21 |
22 | this.modal = this.overlay.createEl('div', {
23 | cls: 'mp-about-modal'
24 | });
25 |
26 | // 添加关闭按钮
27 | const closeButton = this.modal.createEl('button', {
28 | cls: 'mp-donate-close',
29 | text: '×'
30 | });
31 |
32 | // 添加作者信息区域
33 | const authorSection = this.modal.createEl('div', {
34 | cls: 'mp-about-section mp-about-intro-section'
35 | });
36 |
37 | authorSection.createEl('h4', {
38 | text: '关于作者',
39 | cls: 'mp-about-title'
40 | });
41 |
42 | const introEl = authorSection.createEl('p', {
43 | cls: 'mp-about-intro'
44 | });
45 |
46 | // 使用 createEl 替代 innerHTML
47 | introEl.createSpan({ text: '你好,我是' });
48 | introEl.createSpan({ text: '【夜半】', cls: 'mp-about-name' });
49 | introEl.createSpan({ text: ',一名' });
50 | introEl.createSpan({ text: '全职写作与独立开发者', cls: 'mp-about-identity' });
51 | introEl.createSpan({ text: '。' });
52 |
53 | const roleList = authorSection.createEl('div', {
54 | cls: 'mp-about-roles'
55 | });
56 |
57 | const roleEl = roleList.createEl('p', {
58 | cls: 'mp-about-role'
59 | });
60 |
61 | // 使用 createEl 和 createSpan 替代 innerHTML
62 | roleEl.createSpan({ text: '这款插件是我为了在 Obsidian 写作后,' });
63 | roleEl.createEl('br');
64 | roleEl.createSpan({ text: '无需繁琐排版一键即可发布到公众号而开发的工具,' });
65 | roleEl.createEl('br');
66 | roleEl.createSpan({ text: '希望能让你的' });
67 | roleEl.createSpan({ text: '排版更轻松', cls: 'mp-about-highlight' });
68 | roleEl.createSpan({ text: ',让你的' });
69 | roleEl.createSpan({ text: '创作更高效', cls: 'mp-about-value' });
70 | roleEl.createSpan({ text: '。' });
71 |
72 | // 添加插件介绍
73 | const descEl = authorSection.createEl('p', {
74 | cls: 'mp-about-desc'
75 | });
76 |
77 | // 使用 createEl 替代 innerHTML
78 | descEl.createSpan({ text: '如果这款插件对你有帮助,' });
79 | descEl.createEl('br');
80 | descEl.createSpan({ text: '或者你愿意支持我的独立开发与写作,欢迎请我喝咖啡☕️。' });
81 | descEl.createEl('br');
82 | descEl.createSpan({ text: '你的支持来说意义重大,它能让我更专注地开发、写作。' });
83 |
84 | // 添加打赏区域
85 | const donateSection = this.modal.createEl('div', {
86 | cls: 'mp-about-section mp-about-donate-section'
87 | });
88 |
89 | donateSection.createEl('h4', {
90 | text: '请我喝咖啡',
91 | cls: 'mp-about-subtitle'
92 | });
93 |
94 | const donateQR = donateSection.createEl('div', {
95 | cls: 'mp-about-qr'
96 | });
97 | donateQR.createEl('img', {
98 | attr: {
99 | src: DONATE_QR,
100 | alt: '打赏二维码'
101 | }
102 | });
103 |
104 | // 添加公众号区域
105 | const mpSection = this.modal.createEl('div', {
106 | cls: 'mp-about-section mp-about-mp-section'
107 | });
108 |
109 | const mpDescEl = mpSection.createEl('p', {
110 | cls: 'mp-about-desc'
111 | });
112 |
113 | // 使用 createEl 替代 innerHTML
114 | mpDescEl.createSpan({ text: '如果你想了解更多关于创作、效率工具的小技巧,' });
115 | mpDescEl.createEl('br');
116 | mpDescEl.createSpan({ text: '或者关注我未来的写作动态,欢迎关注我的微信公众号。' });
117 |
118 | mpSection.createEl('h4', {
119 | text: '微信公众号',
120 | cls: 'mp-about-subtitle'
121 | });
122 |
123 | const mpQR = mpSection.createEl('div', {
124 | cls: 'mp-about-qr'
125 | });
126 | mpQR.createEl('img', {
127 | attr: {
128 | src: QRCODE_QR,
129 | alt: '公众号二维码'
130 | }
131 | });
132 |
133 | const footerEl = mpSection.createEl('p', {
134 | cls: 'mp-about-footer'
135 | });
136 |
137 | // 使用 createEl 替代 innerHTML
138 | footerEl.createSpan({ text: '期待与你一起,在创作的世界里' });
139 | const strongText = footerEl.createEl('strong');
140 | strongText.createSpan({ text: '找到属于自己的意义' });
141 | footerEl.createSpan({ text: '。' });
142 |
143 | // 添加关闭事件
144 | closeButton.addEventListener('click', () => this.closeDonateModal());
145 | this.overlay.addEventListener('click', (e) => {
146 | if (e.target === this.overlay) {
147 | this.closeDonateModal();
148 | }
149 | });
150 | }
151 |
152 | private static closeDonateModal() {
153 | if (this.overlay) {
154 | this.overlay.remove();
155 | }
156 | }
157 | }
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { Plugin, Notice } from 'obsidian';
2 | import { MPView, VIEW_TYPE_MP } from './view';
3 | import { TemplateManager } from './templateManager';
4 | import { SettingsManager } from './settings/settings';
5 | import { MPConverter } from './converter';
6 | import { DonateManager } from './donateManager';
7 | import { MPSettingTab } from './settings/MPSettingTab';
8 | export default class MPPlugin extends Plugin {
9 | settingsManager: SettingsManager;
10 | templateManager: TemplateManager;
11 | async onload() {
12 | // 初始化设置管理器
13 | this.settingsManager = new SettingsManager(this);
14 | await this.settingsManager.loadSettings();
15 |
16 | // 初始化模板管理器
17 | this.templateManager = new TemplateManager(this.app, this.settingsManager);
18 |
19 | // 初始化转换器
20 | MPConverter.initialize(this.app);
21 |
22 | DonateManager.initialize(this.app, this);
23 |
24 | // 注册视图
25 | this.registerView(
26 | VIEW_TYPE_MP,
27 | (leaf) => new MPView(leaf, this.templateManager, this.settingsManager)
28 | );
29 |
30 | // 自动打开视图但不聚焦
31 | this.app.workspace.onLayoutReady(() => {
32 | const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_MP);
33 | if (leaves.length === 0) {
34 | const rightLeaf = this.app.workspace.getRightLeaf(false);
35 | if (rightLeaf) {
36 | rightLeaf.setViewState({
37 | type: VIEW_TYPE_MP,
38 | active: false,
39 | });
40 | }
41 | }
42 | });
43 |
44 | // 添加命令到命令面板
45 | this.addCommand({
46 | id: 'open-mp-preview',
47 | name: '打开公众号预览插件',
48 | callback: async () => {
49 | await this.activateView();
50 | }
51 | });
52 |
53 | // 在插件的 onload 方法中添加:
54 | this.addSettingTab(new MPSettingTab(this.app, this));
55 | }
56 |
57 | async activateView() {
58 | // 如果视图已经存在,激活它
59 | const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_MP);
60 | if (leaves.length > 0) {
61 | this.app.workspace.revealLeaf(leaves[0]);
62 | return;
63 | }
64 |
65 | // 创建新视图
66 | const rightLeaf = this.app.workspace.getRightLeaf(false);
67 | if (rightLeaf) {
68 | await rightLeaf.setViewState({
69 | type: VIEW_TYPE_MP,
70 | active: true,
71 | });
72 | } else {
73 | // 如果无法获取右侧面板,显示错误提示
74 | new Notice('无法创建视图面板');
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/src/settings/ConfirmModal.ts:
--------------------------------------------------------------------------------
1 | import { App, Modal, Setting } from 'obsidian';
2 |
3 | export class ConfirmModal extends Modal {
4 | private message: string;
5 | private onConfirm: () => void;
6 |
7 | constructor(app: App, title: string, message: string, onConfirm: () => void) {
8 | super(app);
9 | this.titleEl.setText(title);
10 | this.message = message;
11 | this.onConfirm = onConfirm;
12 | }
13 |
14 | onOpen() {
15 | const { contentEl } = this;
16 | contentEl.createEl('p', { text: this.message });
17 |
18 | new Setting(contentEl)
19 | .addButton(btn => btn
20 | .setButtonText('确认')
21 | .setCta()
22 | .onClick(() => {
23 | this.onConfirm();
24 | this.close();
25 | }))
26 | .addButton(btn => btn
27 | .setButtonText('取消')
28 | .onClick(() => this.close()));
29 | }
30 |
31 | onClose() {
32 | const { contentEl } = this;
33 | contentEl.empty();
34 | }
35 | }
--------------------------------------------------------------------------------
/src/settings/CreateBackgroundModal.ts:
--------------------------------------------------------------------------------
1 | import { App, Modal, Setting, Notice } from 'obsidian';
2 | import { Background } from '../backgroundManager';
3 | import { nanoid } from '../utils/nanoid';
4 |
5 | interface CssTemplate {
6 | name: string;
7 | style: string;
8 | settings: string[];
9 | }
10 |
11 | export class CreateBackgroundModal extends Modal {
12 | private onSubmit: (background: Background) => void;
13 | private background: Background;
14 | private isEditing: boolean;
15 |
16 | // 背景属性
17 | private backgroundType: 'color' | 'css' = 'color';
18 | private backgroundColor: string = '#f5f5f5';
19 | private backgroundCssStyle: string = '';
20 | private cssTemplateType: string = 'custom';
21 | private patternColor: string = 'rgba(50, 0, 0, 0.03)';
22 | private patternSize: number = 20;
23 |
24 | // 预设的CSS模板
25 | private cssTemplates: Record = {
26 | custom: {
27 | name: '自定义',
28 | style: '',
29 | settings: ['custom']
30 | },
31 | grid: {
32 | name: '网格',
33 | style: 'background-image: linear-gradient(90deg, rgba(50, 0, 0, 0.03) 2%, transparent 2%), linear-gradient(360deg, rgba(50, 0, 0, 0.03) 2%, transparent 2%); background-size: 20px 20px;',
34 | settings: ['color', 'size']
35 | },
36 | diagonalStripes: {
37 | name: '对角条纹',
38 | style: 'background-image: linear-gradient(135deg, rgba(50, 0, 0, 0.05) 25%, transparent 25%, transparent 50%, rgba(50, 0, 0, 0.05) 50%, rgba(50, 0, 0, 0.05) 75%, transparent 75%, transparent); background-size: 20px 20px;',
39 | settings: ['color', 'size']
40 | },
41 | polkaDots: {
42 | name: '波尔卡圆点',
43 | style: 'background-image: radial-gradient(circle, rgba(50, 0, 0, 0.05) 10%, transparent 10%); background-size: 20px 20px;',
44 | settings: ['color', 'size']
45 | },
46 | zigzag: {
47 | name: '锯齿形',
48 | style: 'background-image: linear-gradient(135deg, rgba(50, 0, 0, 0.05) 25%, transparent 25%, transparent 50%, rgba(50, 0, 0, 0.05) 50%, rgba(50, 0, 0, 0.05) 75%, transparent 75%, transparent); background-size: 20px 20px; background-position: 0 0, 10px 10px;',
49 | settings: ['color', 'size']
50 | },
51 | honeycomb: {
52 | name: '蜂窝',
53 | style: 'background-image: linear-gradient(30deg, rgba(50, 0, 0, 0.05) 12%, transparent 12%, transparent 50%, rgba(50, 0, 0, 0.05) 50%, rgba(50, 0, 0, 0.05) 62%, transparent 62%, transparent); background-size: 20px 35px;',
54 | settings: ['color', 'size']
55 | },
56 | wave: {
57 | name: '波浪',
58 | style: 'background-image: linear-gradient(45deg, rgba(50, 0, 0, 0.04) 12%, transparent 12%, transparent 88%, rgba(50, 0, 0, 0.04) 88%), linear-gradient(135deg, rgba(50, 0, 0, 0.04) 12%, transparent 12%, transparent 88%, rgba(50, 0, 0, 0.04) 88%); background-size: 30px 30px; background-position: 0 0, 0 0, 15px 15px, 15px 15px;',
59 | settings: ['color', 'size']
60 | },
61 | checkerboard: {
62 | name: '棋盘',
63 | style: 'background-image: linear-gradient(45deg, rgba(50, 0, 0, 0.04) 25%, transparent 25%), linear-gradient(-45deg, rgba(50, 0, 0, 0.04) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, rgba(50, 0, 0, 0.04) 75%), linear-gradient(-45deg, transparent 75%, rgba(50, 0, 0, 0.04) 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px;',
64 | settings: ['color', 'size']
65 | }
66 | };
67 |
68 | constructor(
69 | app: App,
70 | onSubmit: (background: Background) => void,
71 | background?: Background
72 | ) {
73 | super(app);
74 | this.onSubmit = onSubmit;
75 | this.isEditing = !!background;
76 |
77 | if (background) {
78 | this.background = { ...background };
79 | // 从 style 中解析出类型和相关属性
80 | this.parseStyleToProperties(background.style);
81 | } else {
82 | this.background = {
83 | id: nanoid(),
84 | name: '',
85 | style: 'background-color: #f5f5f5;'
86 | };
87 | this.backgroundType = 'color';
88 | this.backgroundColor = '#f5f5f5';
89 | }
90 | }
91 |
92 | // 从 style 字符串中解析出背景类型和相关属性
93 | private parseStyleToProperties(style: string) {
94 | if (style.includes('background-color:')) {
95 | this.backgroundType = 'color';
96 | const colorMatch = style.match(/background-color: (#[a-fA-F0-9]+)/);
97 | if (colorMatch && colorMatch[1]) {
98 | this.backgroundColor = colorMatch[1];
99 | }
100 | } else {
101 | this.backgroundType = 'css';
102 | // 移除基本样式,保留自定义 CSS
103 | this.backgroundCssStyle = style.replace(/box-sizing: border-box; margin: 0; padding: 0;/, '').trim();
104 |
105 | // 尝试匹配预设模板
106 | let matched = false;
107 | for (const [key, template] of Object.entries(this.cssTemplates)) {
108 | if (key === 'custom') continue;
109 |
110 | if (this.backgroundCssStyle === template.style) {
111 | this.cssTemplateType = key;
112 | matched = true;
113 | break;
114 | }
115 | }
116 |
117 | if (!matched) {
118 | this.cssTemplateType = 'custom';
119 | }
120 | }
121 | }
122 |
123 | // 解析CSS样式到各个组件
124 | private parseCssToComponents() {
125 | // 提取颜色
126 | const colorMatch = this.backgroundCssStyle.match(/rgba?\([^)]+\)/);
127 | if (colorMatch) {
128 | this.patternColor = colorMatch[0];
129 | }
130 |
131 | // 提取大小
132 | const sizeMatch = this.backgroundCssStyle.match(/background-size:\s*(\d+)px/);
133 | if (sizeMatch && sizeMatch[1]) {
134 | this.patternSize = parseInt(sizeMatch[1]);
135 | }
136 | }
137 |
138 | // 更新图案颜色(保持透明度不变)
139 | private updatePatternColor(hexColor: string) {
140 | const r = parseInt(hexColor.slice(1, 3), 16);
141 | const g = parseInt(hexColor.slice(3, 5), 16);
142 | const b = parseInt(hexColor.slice(5, 7), 16);
143 |
144 | // 提取当前透明度
145 | let alpha = 0.03;
146 | const alphaMatch = this.patternColor.match(/rgba\([^,]+,[^,]+,[^,]+,([^)]+)\)/);
147 | if (alphaMatch && alphaMatch[1]) {
148 | alpha = parseFloat(alphaMatch[1]);
149 | }
150 |
151 | this.patternColor = `rgba(${r}, ${g}, ${b}, ${alpha})`;
152 | }
153 |
154 | // 更新图案透明度(保持颜色不变)
155 | private updatePatternOpacity(opacity: number) {
156 | const colorMatch = this.patternColor.match(/rgba\(([^,]+),([^,]+),([^,]+),[^)]+\)/);
157 | if (colorMatch) {
158 | const [_, r, g, b] = colorMatch;
159 | this.patternColor = `rgba(${r}, ${g}, ${b}, ${opacity})`;
160 | } else {
161 | // 如果之前不是rgba格式,转换为rgba
162 | const r = parseInt(this.patternColor.slice(1, 3), 16);
163 | const g = parseInt(this.patternColor.slice(3, 5), 16);
164 | const b = parseInt(this.patternColor.slice(5, 7), 16);
165 | this.patternColor = `rgba(${r}, ${g}, ${b}, ${opacity})`;
166 | }
167 | }
168 |
169 | // 根据当前组件设置更新CSS样式
170 | private updateCssStyle() {
171 | if (this.cssTemplateType === 'custom') {
172 | return; // 自定义模式下不自动更新CSS
173 | }
174 |
175 | const template = this.cssTemplates[this.cssTemplateType];
176 | if (!template) return;
177 |
178 | // 替换模板中的颜色和大小
179 | let style = template.style;
180 |
181 | // 替换所有颜色
182 | style = style.replace(/rgba\([^)]+\)/g, this.patternColor);
183 |
184 | // 替换所有大小
185 | style = style.replace(/(\d+)px/g, `${this.patternSize}px`);
186 |
187 | this.backgroundCssStyle = style;
188 | }
189 |
190 | // 根据模板类型更新设置项的可见性
191 | private updateSettingsVisibility(cssSection: HTMLElement, templateType: string) {
192 | const template = this.cssTemplates[templateType];
193 | if (!template) return;
194 |
195 | // 获取所有可能的设置项容器
196 | const colorSettingContainer = cssSection.querySelector('.pattern-color-setting');
197 | const sizeSettingContainer = cssSection.querySelector('.pattern-size-setting');
198 | const customCssContainer = cssSection.querySelector('.custom-css-container');
199 |
200 | // 根据模板需要的设置项显示或隐藏
201 | if (colorSettingContainer) {
202 | colorSettingContainer.toggleClass('is-hidden', !template.settings.includes('color'));
203 | }
204 |
205 | if (sizeSettingContainer) {
206 | sizeSettingContainer.toggleClass('is-hidden', !template.settings.includes('size'));
207 | }
208 |
209 | if (customCssContainer) {
210 | customCssContainer.toggleClass('is-hidden', !template.settings.includes('custom'));
211 |
212 | // 如果显示自定义CSS,同时显示文本区域
213 | const customCssTextArea = cssSection.querySelector('.custom-css-textarea');
214 | if (customCssTextArea && template.settings.includes('custom')) {
215 | customCssTextArea.toggleClass('is-hidden', false);
216 | }
217 | }
218 | }
219 |
220 | // 更新类型特定设置的可见性
221 | private updateTypeSpecificSettings() {
222 | const colorSection = this.contentEl.querySelector('.background-color-section');
223 | const cssSection = this.contentEl.querySelector('.background-css-section');
224 |
225 | if (colorSection && cssSection) {
226 | colorSection.toggleClass('is-hidden', this.backgroundType !== 'color');
227 | cssSection.toggleClass('is-hidden', this.backgroundType !== 'css');
228 |
229 | // 如果切换到CSS背景类型,根据当前模板更新设置项可见性
230 | if (this.backgroundType === 'css') {
231 | this.updateSettingsVisibility(cssSection as HTMLElement, this.cssTemplateType);
232 | }
233 | }
234 |
235 | const previewEl = this.contentEl.querySelector('.background-preview');
236 | if (previewEl) {
237 | this.updatePreview(previewEl as HTMLElement);
238 | }
239 | }
240 |
241 | // 更新预览区域
242 | private updatePreview(previewEl?: HTMLElement) {
243 | const el = previewEl || this.contentEl.querySelector('.background-preview');
244 | if (!el) return;
245 |
246 | let style = '';
247 | switch (this.backgroundType) {
248 | case 'color':
249 | style = `background-color: ${this.backgroundColor};`;
250 | break;
251 | case 'css':
252 | style = this.backgroundCssStyle;
253 | break;
254 | }
255 |
256 | el.setAttribute('style', style);
257 | }
258 |
259 | // 生成最终样式
260 | private generateStyle() {
261 | let style = 'box-sizing: border-box; margin: 0; padding: 0; ';
262 |
263 | switch (this.backgroundType) {
264 | case 'color':
265 | style += `background-color: ${this.backgroundColor};`;
266 | break;
267 | case 'css':
268 | style += this.backgroundCssStyle;
269 | break;
270 | }
271 |
272 | this.background.style = style;
273 | }
274 |
275 | // 表单验证
276 | private validateForm(): boolean {
277 | if (!this.background.name) {
278 | new Notice('请输入背景名称');
279 | return false;
280 | }
281 |
282 | switch (this.backgroundType) {
283 | case 'color':
284 | if (!this.backgroundColor) {
285 | new Notice('请选择背景颜色');
286 | return false;
287 | }
288 | break;
289 | case 'css':
290 | if (!this.backgroundCssStyle) {
291 | new Notice('请输入CSS样式');
292 | return false;
293 | }
294 | break;
295 | }
296 |
297 | return true;
298 | }
299 |
300 | // 打开模态框时的处理
301 | onOpen() {
302 | const { contentEl } = this;
303 | contentEl.empty();
304 | contentEl.addClass('mp-background-modal');
305 |
306 | // 标题
307 | contentEl.createEl('h2', { text: this.isEditing ? '编辑背景' : '创建新背景' });
308 |
309 | // 基本信息
310 | const basicSection = contentEl.createDiv('background-basic-section');
311 |
312 | // 背景名称
313 | new Setting(basicSection)
314 | .setName('背景名称')
315 | .setDesc('输入背景的名称')
316 | .addText(text => {
317 | text.setValue(this.background.name || '')
318 | .onChange(value => {
319 | this.background.name = value;
320 | });
321 | });
322 |
323 | // 背景类型
324 | new Setting(basicSection)
325 | .setName('背景类型')
326 | .setDesc('选择背景的类型')
327 | .addDropdown(dropdown => {
328 | dropdown
329 | .addOption('color', '纯色背景')
330 | .addOption('css', 'CSS背景图案')
331 | .setValue(this.backgroundType)
332 | .onChange(value => {
333 | this.backgroundType = value as 'color' | 'css';
334 | this.updateTypeSpecificSettings();
335 | });
336 | });
337 |
338 | // 类型特定设置容器
339 | const typeSpecificSection = contentEl.createDiv('background-type-specific-section');
340 |
341 | // 纯色背景设置
342 | const colorSection = typeSpecificSection.createDiv('background-color-section');
343 | new Setting(colorSection)
344 | .setName('背景颜色')
345 | .setDesc('选择背景的颜色')
346 | .addColorPicker(color => {
347 | color.setValue(this.backgroundColor)
348 | .onChange(value => {
349 | this.backgroundColor = value;
350 | this.updatePreview();
351 | });
352 | });
353 |
354 | // CSS背景图案设置
355 | const cssSection = typeSpecificSection.createDiv('background-css-section');
356 |
357 | // 添加模板选择下拉框
358 | new Setting(cssSection)
359 | .setName('背景模板')
360 | .setDesc('选择预设的背景模板')
361 | .addDropdown(dropdown => {
362 | for (const [key, template] of Object.entries(this.cssTemplates)) {
363 | dropdown.addOption(key, template.name);
364 | }
365 | dropdown.setValue(this.cssTemplateType)
366 | .onChange(value => {
367 | this.cssTemplateType = value;
368 | if (value !== 'custom') {
369 | this.backgroundCssStyle = this.cssTemplates[value].style;
370 |
371 | // 解析CSS样式到各个组件
372 | this.parseCssToComponents();
373 |
374 | // 更新UI控件以反映当前模板的设置
375 | const colorPicker = cssSection.querySelector('input[type="color"]');
376 | const opacitySlider = cssSection.querySelector('.slider');
377 | const sizeSlider = cssSection.querySelectorAll('.slider')[1];
378 |
379 | if (colorPicker) {
380 | // 从rgba提取十六进制颜色
381 | const rgbaMatch = this.patternColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
382 | if (rgbaMatch) {
383 | const [_, r, g, b] = rgbaMatch;
384 | const hexColor = '#' +
385 | parseInt(r).toString(16).padStart(2, '0') +
386 | parseInt(g).toString(16).padStart(2, '0') +
387 | parseInt(b).toString(16).padStart(2, '0');
388 | (colorPicker as HTMLInputElement).value = hexColor;
389 | }
390 | }
391 |
392 | if (opacitySlider) {
393 | const opacityMatch = this.patternColor.match(/rgba\([^,]+,[^,]+,[^,]+,([^)]+)\)/);
394 | if (opacityMatch && opacityMatch[1]) {
395 | const opacity = parseFloat(opacityMatch[1]) * 100;
396 | // 使用类型断言来访问__component__属性
397 | const sliderComponent = (opacitySlider.parentElement as any).__component__;
398 | if (sliderComponent && typeof sliderComponent.setValue === 'function') {
399 | sliderComponent.setValue(opacity);
400 | }
401 | }
402 | }
403 |
404 | if (sizeSlider) {
405 | // 使用类型断言来访问__component__属性
406 | const sliderComponent = (sizeSlider.parentElement as any).__component__;
407 | if (sliderComponent && typeof sliderComponent.setValue === 'function') {
408 | sliderComponent.setValue(this.patternSize);
409 | }
410 | }
411 | }
412 |
413 | // 更新自定义CSS区域的可见性
414 | const customCssContainer = cssSection.querySelector('.custom-css-container');
415 | const customCssTextArea = cssSection.querySelector('.custom-css-textarea');
416 | if (customCssContainer && customCssTextArea) {
417 | const isCustom = value === 'custom';
418 | customCssTextArea.toggleClass('is-hidden', !isCustom);
419 | }
420 |
421 | // 更新CSS设置项的可见性
422 | this.updateSettingsVisibility(cssSection, value);
423 |
424 | this.updatePreview();
425 | });
426 | });
427 |
428 | // 背景颜色设置
429 | const colorSettingContainer = cssSection.createDiv('pattern-color-setting');
430 | new Setting(colorSettingContainer)
431 | .setName('图案颜色')
432 | .setDesc('设置背景图案的颜色')
433 | .addColorPicker(color => {
434 | // 从rgba提取十六进制颜色
435 | const rgbaMatch = this.patternColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
436 | let hexColor = '#320000'; // 默认颜色
437 |
438 | if (rgbaMatch) {
439 | const [_, r, g, b] = rgbaMatch;
440 | hexColor = '#' +
441 | parseInt(r).toString(16).padStart(2, '0') +
442 | parseInt(g).toString(16).padStart(2, '0') +
443 | parseInt(b).toString(16).padStart(2, '0');
444 | }
445 |
446 | color.setValue(hexColor)
447 | .onChange(value => {
448 | // 更新颜色但保持透明度
449 | this.updatePatternColor(value);
450 | this.updateCssStyle();
451 | this.updatePreview();
452 | });
453 | })
454 | .addSlider(slider => {
455 | // 提取当前透明度
456 | let opacity = 3; // 默认透明度3%
457 | const alphaMatch = this.patternColor.match(/rgba\([^,]+,[^,]+,[^,]+,([^)]+)\)/);
458 | if (alphaMatch && alphaMatch[1]) {
459 | opacity = parseFloat(alphaMatch[1]) * 100;
460 | }
461 |
462 | slider.setLimits(0, 100, 1)
463 | .setValue(opacity)
464 | .setDynamicTooltip()
465 | .onChange(value => {
466 | // 更新透明度但保持颜色
467 | this.updatePatternOpacity(value / 100);
468 | this.updateCssStyle();
469 | this.updatePreview();
470 | });
471 | });
472 |
473 | // 图案大小设置
474 | const sizeSettingContainer = cssSection.createDiv('pattern-size-setting');
475 | new Setting(sizeSettingContainer)
476 | .setName('图案大小')
477 | .setDesc('设置背景图案的大小')
478 | .addSlider(slider => {
479 | slider.setLimits(5, 50, 1)
480 | .setValue(this.patternSize) // 使用当前模板的大小
481 | .setDynamicTooltip()
482 | .onChange(value => {
483 | this.patternSize = value;
484 | this.updateCssStyle();
485 | this.updatePreview();
486 | });
487 | });
488 |
489 | // 自定义CSS容器
490 | const customCssContainer = cssSection.createDiv('custom-css-container');
491 | const customCssTextArea = customCssContainer.createDiv('custom-css-textarea');
492 | customCssTextArea.toggleClass('is-hidden', this.cssTemplateType !== 'custom');
493 |
494 | new Setting(customCssTextArea)
495 | .setName('CSS代码')
496 | .setDesc('直接输入CSS样式代码')
497 | .addTextArea(text => {
498 | text.setValue(this.backgroundCssStyle)
499 | .onChange(value => {
500 | this.backgroundCssStyle = value;
501 | this.updatePreview();
502 | });
503 | text.inputEl.rows = 10;
504 | text.inputEl.cols = 50;
505 | });
506 |
507 | // 背景预览
508 | const previewSection = contentEl.createDiv('background-preview-section');
509 | previewSection.createEl('h3', { text: '预览' });
510 | const previewEl = previewSection.createDiv('background-preview');
511 | previewEl.createSpan({ text: '预览效果' });
512 | this.updatePreview(previewEl);
513 |
514 | // 按钮区域
515 | const buttonSection = contentEl.createDiv('background-button-section');
516 | new Setting(buttonSection)
517 | .addButton(btn => {
518 | btn.setButtonText('取消')
519 | .onClick(() => {
520 | this.close();
521 | });
522 | })
523 | .addButton(btn => {
524 | btn.setButtonText(this.isEditing ? '保存' : '创建')
525 | .setCta()
526 | .onClick(() => {
527 | if (!this.validateForm()) {
528 | return;
529 | }
530 | this.generateStyle();
531 | this.onSubmit(this.background);
532 | this.close();
533 | });
534 | });
535 |
536 | // 初始化类型特定设置
537 | this.updateTypeSpecificSettings();
538 | }
539 |
540 | // 关闭模态框时的处理
541 | onClose() {
542 | const { contentEl } = this;
543 | contentEl.empty();
544 | }
545 | }
--------------------------------------------------------------------------------
/src/settings/CreateFontModal.ts:
--------------------------------------------------------------------------------
1 | import { App, Modal, Setting, setIcon } from 'obsidian';
2 |
3 | export class CreateFontModal extends Modal {
4 | private font: { value: string; label: string; isPreset?: boolean };
5 | private onSubmit: (font: { value: string; label: string }) => void;
6 |
7 | constructor(
8 | app: App,
9 | onSubmit: (font: { value: string; label: string }) => void,
10 | existingFont?: { value: string; label: string; isPreset?: boolean }
11 | ) {
12 | super(app);
13 | this.onSubmit = onSubmit;
14 | this.font = existingFont ?? { value: '', label: '' };
15 | }
16 |
17 | onOpen() {
18 | const { contentEl } = this;
19 | contentEl.empty();
20 | contentEl.addClass('mp-font-modal');
21 |
22 | // 修改标题容器结构
23 | const headerContainer = contentEl.createDiv({ cls: 'mfd-header' });
24 | headerContainer.createEl('h3', { text: this.font.label ? '编辑字体' : '添加字体' });
25 |
26 | // 帮助按钮容器
27 | const helpBtnContainer = headerContainer.createDiv({ cls: 'mfd-help-trigger' });
28 | const helpBtn = helpBtnContainer.createEl('button', { cls: 'mfd-help-btn' });
29 | setIcon(helpBtn, 'help-circle');
30 |
31 | // 提示框
32 | const helpTooltip = helpBtnContainer.createDiv({ cls: 'mfd-help-tooltip' });
33 | helpTooltip.setText(`👋 字体值设置说明
34 | • 单个字体:Arial 或 "Microsoft YaHei"
35 | • 中文字体:需要同时设置中英文名称
36 | • 字体族:添加 serif/sans-serif
37 | • 多个字体用逗号分隔
38 | 示例
39 | • 宋体:SimSun, "宋体", serif
40 | • 微软雅黑:"Microsoft YaHei", "微软雅黑", sans-serif`);
41 |
42 | new Setting(contentEl)
43 | .setName('字体名称')
44 | .setDesc('显示在下拉菜单中的名称')
45 | .addText(text => text
46 | .setValue(this.font.label)
47 | .onChange(value => this.font.label = value));
48 |
49 | new Setting(contentEl)
50 | .setName('字体值')
51 | .setDesc('CSS font-family 的值')
52 | .addText(text => text
53 | .setValue(this.font.value)
54 | .onChange(value => this.font.value = value))
55 |
56 | new Setting(contentEl)
57 | .addButton(btn => btn
58 | .setButtonText('确定')
59 | .setCta()
60 | .onClick(() => {
61 | if (!this.font.label || !this.font.value) {
62 | return;
63 | }
64 | this.onSubmit(this.font);
65 | this.close();
66 | }))
67 | .addButton(btn => btn
68 | .setButtonText('取消')
69 | .onClick(() => this.close()));
70 | }
71 |
72 | onClose() {
73 | const { contentEl } = this;
74 | contentEl.empty();
75 | }
76 | }
--------------------------------------------------------------------------------
/src/settings/MPSettingTab.ts:
--------------------------------------------------------------------------------
1 | import { App, PluginSettingTab, Setting, setIcon, Notice } from 'obsidian';
2 | import MPPlugin from '../main'; // 修改插件名以匹配类名
3 | import { CreateTemplateModal } from './CreateTemplateModal';
4 | import { CreateFontModal } from './CreateFontModal';
5 | import { CreateBackgroundModal } from './CreateBackgroundModal'; // 添加导入
6 | import { ConfirmModal } from './ConfirmModal';
7 | import { TemplatePreviewModal } from './templatePreviewModal'; // 添加导入
8 | export class MPSettingTab extends PluginSettingTab {
9 | plugin: MPPlugin; // 修改插件类型以匹配类名
10 | private expandedSections: Set = new Set();
11 |
12 | constructor(app: App, plugin: MPPlugin) { // 修改插件类型以匹配类名
13 | super(app, plugin);
14 | this.plugin = plugin;
15 | }
16 |
17 | private createSection(containerEl: HTMLElement, title: string, renderContent: (contentEl: HTMLElement) => void) {
18 | const section = containerEl.createDiv('settings-section');
19 | const header = section.createDiv('settings-section-header');
20 |
21 | const toggle = header.createSpan('settings-section-toggle');
22 | setIcon(toggle, 'chevron-right');
23 |
24 | header.createEl('h4', { text: title });
25 |
26 | const content = section.createDiv('settings-section-content');
27 | renderContent(content);
28 |
29 | header.addEventListener('click', () => {
30 | const isExpanded = !section.hasClass('is-expanded');
31 | section.toggleClass('is-expanded', isExpanded);
32 | setIcon(toggle, isExpanded ? 'chevron-down' : 'chevron-right');
33 | if (isExpanded) {
34 | this.expandedSections.add(title);
35 | } else {
36 | this.expandedSections.delete(title);
37 | }
38 | });
39 |
40 | if (this.expandedSections.has(title) || (!containerEl.querySelector('.settings-section'))) {
41 | section.addClass('is-expanded');
42 | setIcon(toggle, 'chevron-down');
43 | this.expandedSections.add(title);
44 | }
45 |
46 | return section;
47 | }
48 |
49 | display(): void {
50 | const { containerEl } = this;
51 | containerEl.empty();
52 | containerEl.addClass('mp-settings');
53 |
54 | containerEl.createEl('h2', { text: 'MP Preview' });
55 |
56 | this.createSection(containerEl, '基本选项', el => this.renderBasicSettings(el));
57 | this.createSection(containerEl, '模板选项', el => this.renderTemplateSettings(el));
58 | this.createSection(containerEl, '背景选项', el => this.renderBackgroundSettings(el));
59 | }
60 |
61 | private renderBasicSettings(containerEl: HTMLElement): void {
62 | // 字体管理区域
63 | const fontSection = containerEl.createDiv('mp-settings-subsection');
64 | const fontHeader = fontSection.createDiv('mp-settings-subsection-header');
65 | const fontToggle = fontHeader.createSpan('mp-settings-subsection-toggle');
66 | setIcon(fontToggle, 'chevron-right');
67 |
68 | fontHeader.createEl('h3', { text: '字体管理' });
69 |
70 | const fontContent = fontSection.createDiv('mp-settings-subsection-content');
71 |
72 | // 折叠/展开逻辑
73 | fontHeader.addEventListener('click', () => {
74 | const isExpanded = !fontSection.hasClass('is-expanded');
75 | fontSection.toggleClass('is-expanded', isExpanded);
76 | setIcon(fontToggle, isExpanded ? 'chevron-down' : 'chevron-right');
77 | });
78 |
79 | // 字体列表
80 | const fontList = fontContent.createDiv('font-management');
81 | this.plugin.settingsManager.getFontOptions().forEach(font => {
82 | const fontItem = fontList.createDiv('font-item');
83 | const setting = new Setting(fontItem)
84 | .setName(font.label)
85 | .setDesc(font.value);
86 |
87 | // 只为非预设字体添加编辑和删除按钮
88 | if (!font.isPreset) {
89 | setting
90 | .addExtraButton(btn =>
91 | btn.setIcon('pencil')
92 | .setTooltip('编辑')
93 | .onClick(() => {
94 | new CreateFontModal(
95 | this.app,
96 | async (updatedFont) => {
97 | await this.plugin.settingsManager.updateFont(font.value, updatedFont);
98 | this.display();
99 | new Notice('请重启 Obsidian 或重新加载以使更改生效');
100 | },
101 | font
102 | ).open();
103 | }))
104 | .addExtraButton(btn =>
105 | btn.setIcon('trash')
106 | .setTooltip('删除')
107 | .onClick(() => {
108 | // 新增确认模态框
109 | new ConfirmModal(
110 | this.app,
111 | '确认删除字体',
112 | `确定要删除「${font.label}」字体配置吗?`,
113 | async () => {
114 | await this.plugin.settingsManager.removeFont(font.value);
115 | this.display();
116 | new Notice('请重启 Obsidian 或重新加载以使更改生效');
117 | }
118 | ).open();
119 | }));
120 | }
121 | });
122 |
123 | // 添加新字体按钮
124 | new Setting(fontContent)
125 | .addButton(btn => btn
126 | .setButtonText('+ 添加字体')
127 | .setCta()
128 | .onClick(() => {
129 | new CreateFontModal(
130 | this.app,
131 | async (newFont) => {
132 | await this.plugin.settingsManager.addCustomFont(newFont);
133 | this.display();
134 | new Notice('请重启 Obsidian 或重新加载以使更改生效');
135 | }
136 | ).open();
137 | }));
138 | }
139 |
140 | private renderTemplateSettings(containerEl: HTMLElement): void {
141 | // 模板显示设置部分 - 从基本设置移动到这里
142 | const templateVisibilitySection = containerEl.createDiv('mp-settings-subsection');
143 | const templateVisibilityHeader = templateVisibilitySection.createDiv('mp-settings-subsection-header');
144 |
145 | const templateVisibilityToggle = templateVisibilityHeader.createSpan('mp-settings-subsection-toggle');
146 | setIcon(templateVisibilityToggle, 'chevron-right');
147 |
148 | templateVisibilityHeader.createEl('h3', { text: '模板显示选项' });
149 |
150 | const templateVisibilityContent = templateVisibilitySection.createDiv('mp-settings-subsection-content');
151 |
152 | // 折叠/展开逻辑
153 | templateVisibilityHeader.addEventListener('click', () => {
154 | const isExpanded = !templateVisibilitySection.hasClass('is-expanded');
155 | templateVisibilitySection.toggleClass('is-expanded', isExpanded);
156 | setIcon(templateVisibilityToggle, isExpanded ? 'chevron-down' : 'chevron-right');
157 | });
158 |
159 | // 模板选择容器
160 | const templateSelectionContainer = templateVisibilityContent.createDiv('template-selection-container');
161 |
162 | // 左侧:所有模板列表
163 | const allTemplatesContainer = templateSelectionContainer.createDiv('all-templates-container');
164 | allTemplatesContainer.createEl('h4', { text: '隐藏模板' });
165 | const allTemplatesList = allTemplatesContainer.createDiv('templates-list');
166 |
167 | // 中间:控制按钮
168 | const controlButtonsContainer = templateSelectionContainer.createDiv('control-buttons-container');
169 | const addButton = controlButtonsContainer.createEl('button', { text: '>' });
170 | const removeButton = controlButtonsContainer.createEl('button', { text: '<' });
171 |
172 | // 右侧:显示的模板列表
173 | const visibleTemplatesContainer = templateSelectionContainer.createDiv('visible-templates-container');
174 | visibleTemplatesContainer.createEl('h4', { text: '显示模板' });
175 | const visibleTemplatesList = visibleTemplatesContainer.createDiv('templates-list');
176 |
177 | // 获取所有模板
178 | const allTemplates = this.plugin.settingsManager.getAllTemplates();
179 |
180 | // 渲染模板列表
181 | const renderTemplateLists = () => {
182 | // 清空列表
183 | allTemplatesList.empty();
184 | visibleTemplatesList.empty();
185 |
186 | // 填充左侧列表(所有未显示的模板)
187 | allTemplates
188 | .filter(template => template.isVisible === false)
189 | .forEach(template => {
190 | const templateItem = allTemplatesList.createDiv('template-list-item');
191 | templateItem.textContent = template.name;
192 | templateItem.dataset.templateId = template.id;
193 |
194 | // 点击选中/取消选中
195 | templateItem.addEventListener('click', () => {
196 | templateItem.toggleClass('selected', !templateItem.hasClass('selected'));
197 | });
198 | });
199 |
200 | // 填充右侧列表(所有显示的模板)
201 | allTemplates
202 | .filter(template => template.isVisible !== false) // 默认显示
203 | .forEach(template => {
204 | const templateItem = visibleTemplatesList.createDiv('template-list-item');
205 | templateItem.textContent = template.name;
206 | templateItem.dataset.templateId = template.id;
207 |
208 | // 点击选中/取消选中
209 | templateItem.addEventListener('click', () => {
210 | templateItem.toggleClass('selected', !templateItem.hasClass('selected'));
211 | });
212 | });
213 | };
214 |
215 | // 初始渲染
216 | renderTemplateLists();
217 |
218 | // 添加按钮事件
219 | addButton.addEventListener('click', async () => {
220 | const selectedItems = Array.from(allTemplatesList.querySelectorAll('.template-list-item.selected'));
221 | if (selectedItems.length === 0) return;
222 |
223 | for (const item of selectedItems) {
224 | const templateId = (item as HTMLElement).dataset.templateId;
225 | if (!templateId) continue;
226 |
227 | const template = allTemplates.find(t => t.id === templateId);
228 | if (template) {
229 | template.isVisible = true;
230 | await this.plugin.settingsManager.updateTemplate(templateId, template);
231 | }
232 | }
233 |
234 | renderTemplateLists();
235 | new Notice('请重启 Obsidian 或重新加载以使更改生效');
236 | });
237 |
238 | // 移除按钮事件
239 | removeButton.addEventListener('click', async () => {
240 | const selectedItems = Array.from(visibleTemplatesList.querySelectorAll('.template-list-item.selected'));
241 | if (selectedItems.length === 0) return;
242 |
243 | for (const item of selectedItems) {
244 | const templateId = (item as HTMLElement).dataset.templateId;
245 | if (!templateId) continue;
246 |
247 | const template = allTemplates.find(t => t.id === templateId);
248 | if (template) {
249 | template.isVisible = false;
250 | await this.plugin.settingsManager.updateTemplate(templateId, template);
251 | }
252 | }
253 |
254 | renderTemplateLists();
255 | new Notice('请重启 Obsidian 或重新加载以使更改生效');
256 | });
257 |
258 | // 模板管理区域
259 | const templateList = containerEl.createDiv('template-management');
260 | // 渲染自定义模板
261 | templateList.createEl('h4', { text: '自定义模板', cls: 'template-custom-header' });
262 | this.plugin.settingsManager.getAllTemplates()
263 | .filter(template => !template.isPreset)
264 | .forEach(template => {
265 | const templateItem = templateList.createDiv('template-item');
266 | new Setting(templateItem)
267 | .setName(template.name)
268 | .setDesc(template.description)
269 | .addExtraButton(btn =>
270 | btn.setIcon('eye')
271 | .setTooltip('预览')
272 | .onClick(() => {
273 | new TemplatePreviewModal(this.app, template, this.plugin.templateManager).open(); // 修改为使用预览模态框
274 | }))
275 | .addExtraButton(btn =>
276 | btn.setIcon('pencil')
277 | .setTooltip('编辑')
278 | .onClick(() => {
279 | new CreateTemplateModal(
280 | this.app,
281 | this.plugin,
282 | (updatedTemplate) => {
283 | this.plugin.settingsManager.updateTemplate(template.id, updatedTemplate);
284 | this.display();
285 | new Notice('请重启 Obsidian 或重新加载以使更改生效');
286 | },
287 | template
288 | ).open();
289 | }))
290 | .addExtraButton(btn =>
291 | btn.setIcon('trash')
292 | .setTooltip('删除')
293 | .onClick(() => {
294 | // 新增确认模态框
295 | new ConfirmModal(
296 | this.app,
297 | '确认删除模板',
298 | `确定要删除「${template.name}」模板吗?此操作不可恢复。`,
299 | async () => {
300 | await this.plugin.settingsManager.removeTemplate(template.id);
301 | this.display();
302 | new Notice('请重启 Obsidian 或重新加载以使更改生效');
303 | }
304 | ).open();
305 | }));
306 | });
307 |
308 | // 添加新模板按钮
309 | new Setting(containerEl)
310 | .addButton(btn => btn
311 | .setButtonText('+ 新建模板')
312 | .setCta()
313 | .onClick(() => {
314 | new CreateTemplateModal(
315 | this.app,
316 | this.plugin,
317 | async (newTemplate) => {
318 | await this.plugin.settingsManager.addCustomTemplate(newTemplate);
319 | this.display();
320 | new Notice('请重启 Obsidian 或重新加载以使更改生效');
321 | }
322 | ).open();
323 | }));
324 | }
325 |
326 | private renderBackgroundSettings(containerEl: HTMLElement): void {
327 | // 背景显示设置部分
328 | const backgroundVisibilitySection = containerEl.createDiv('mp-settings-subsection');
329 | const backgroundVisibilityHeader = backgroundVisibilitySection.createDiv('mp-settings-subsection-header');
330 |
331 | const backgroundVisibilityToggle = backgroundVisibilityHeader.createSpan('mp-settings-subsection-toggle');
332 | setIcon(backgroundVisibilityToggle, 'chevron-right');
333 |
334 | backgroundVisibilityHeader.createEl('h3', { text: '背景显示' });
335 |
336 | const backgroundVisibilityContent = backgroundVisibilitySection.createDiv('mp-settings-subsection-content');
337 |
338 | // 折叠/展开逻辑
339 | backgroundVisibilityHeader.addEventListener('click', () => {
340 | const isExpanded = !backgroundVisibilitySection.hasClass('is-expanded');
341 | backgroundVisibilitySection.toggleClass('is-expanded', isExpanded);
342 | setIcon(backgroundVisibilityToggle, isExpanded ? 'chevron-down' : 'chevron-right');
343 | });
344 |
345 | // 背景选择容器
346 | const backgroundSelectionContainer = backgroundVisibilityContent.createDiv('background-selection-container');
347 |
348 | // 左侧:所有背景列表
349 | const allBackgroundsContainer = backgroundSelectionContainer.createDiv('all-backgrounds-container');
350 | allBackgroundsContainer.createEl('h4', { text: '隐藏背景' });
351 | const allBackgroundsList = allBackgroundsContainer.createDiv('backgrounds-list');
352 |
353 | // 中间:控制按钮
354 | const controlButtonsContainer = backgroundSelectionContainer.createDiv('control-buttons-container');
355 | const addButton = controlButtonsContainer.createEl('button', { text: '>' });
356 | const removeButton = controlButtonsContainer.createEl('button', { text: '<' });
357 |
358 | // 右侧:显示的背景列表
359 | const visibleBackgroundsContainer = backgroundSelectionContainer.createDiv('visible-backgrounds-container');
360 | visibleBackgroundsContainer.createEl('h4', { text: '显示背景' });
361 | const visibleBackgroundsList = visibleBackgroundsContainer.createDiv('backgrounds-list');
362 |
363 | // 获取所有背景
364 | const allBackgrounds = this.plugin.settingsManager.getAllBackgrounds();
365 |
366 | // 渲染背景列表
367 | const renderBackgroundLists = () => {
368 | // 清空列表
369 | allBackgroundsList.empty();
370 | visibleBackgroundsList.empty();
371 |
372 | // 填充左侧列表(所有未显示的背景)
373 | allBackgrounds
374 | .filter(background => background.isVisible === false)
375 | .forEach(background => {
376 | const backgroundItem = allBackgroundsList.createDiv('background-list-item');
377 | backgroundItem.textContent = background.name;
378 | backgroundItem.dataset.backgroundId = background.id;
379 |
380 | // 点击选中/取消选中
381 | backgroundItem.addEventListener('click', () => {
382 | backgroundItem.toggleClass('selected', !backgroundItem.hasClass('selected'));
383 | });
384 | });
385 |
386 | // 填充右侧列表(所有显示的背景)
387 | allBackgrounds
388 | .filter(background => background.isVisible !== false) // 默认显示
389 | .forEach(background => {
390 | const backgroundItem = visibleBackgroundsList.createDiv('background-list-item');
391 | backgroundItem.textContent = background.name;
392 | backgroundItem.dataset.backgroundId = background.id;
393 |
394 | // 点击选中/取消选中
395 | backgroundItem.addEventListener('click', () => {
396 | backgroundItem.toggleClass('selected', !backgroundItem.hasClass('selected'));
397 | });
398 | });
399 | };
400 |
401 | // 初始渲染
402 | renderBackgroundLists();
403 |
404 | // 添加按钮事件
405 | addButton.addEventListener('click', async () => {
406 | const selectedItems = Array.from(allBackgroundsList.querySelectorAll('.background-list-item.selected'));
407 | if (selectedItems.length === 0) return;
408 |
409 | for (const item of selectedItems) {
410 | const backgroundId = (item as HTMLElement).dataset.backgroundId;
411 | if (!backgroundId) continue;
412 |
413 | const background = allBackgrounds.find(b => b.id === backgroundId);
414 | if (background) {
415 | background.isVisible = true;
416 | await this.plugin.settingsManager.updateBackground(backgroundId, background);
417 | }
418 | }
419 |
420 | renderBackgroundLists();
421 | new Notice('背景显示设置已更新');
422 | });
423 |
424 | // 移除按钮事件
425 | removeButton.addEventListener('click', async () => {
426 | const selectedItems = Array.from(visibleBackgroundsList.querySelectorAll('.background-list-item.selected'));
427 | if (selectedItems.length === 0) return;
428 |
429 | for (const item of selectedItems) {
430 | const backgroundId = (item as HTMLElement).dataset.backgroundId;
431 | if (!backgroundId) continue;
432 |
433 | const background = allBackgrounds.find(b => b.id === backgroundId);
434 | if (background) {
435 | background.isVisible = false;
436 | await this.plugin.settingsManager.updateBackground(backgroundId, background);
437 | }
438 | }
439 |
440 | renderBackgroundLists();
441 | new Notice('背景显示已更新');
442 | });
443 |
444 | // 背景管理区域
445 | const backgroundList = containerEl.createDiv('background-management');
446 |
447 | // 渲染自定义背景
448 | backgroundList.createEl('h4', { text: '自定义背景', cls: 'background-custom-header' });
449 | this.plugin.settingsManager.getAllBackgrounds()
450 | .filter(background => !background.isPreset)
451 | .forEach(background => {
452 | const backgroundItem = backgroundList.createDiv('background-item');
453 | new Setting(backgroundItem)
454 | .setName(background.name)
455 | .addExtraButton(btn =>
456 | btn.setIcon('pencil')
457 | .setTooltip('编辑')
458 | .onClick(() => {
459 | // 使用背景编辑模态框
460 | new CreateBackgroundModal(
461 | this.app,
462 | async (updatedBackground) => {
463 | await this.plugin.settingsManager.updateBackground(background.id, updatedBackground);
464 | this.display();
465 | new Notice('背景已更新');
466 | },
467 | background
468 | ).open();
469 | }))
470 | .addExtraButton(btn =>
471 | btn.setIcon('trash')
472 | .setTooltip('删除')
473 | .onClick(() => {
474 | new ConfirmModal(
475 | this.app,
476 | '确认删除背景',
477 | `确定要删除「${background.name}」背景吗?此操作不可恢复。`,
478 | async () => {
479 | await this.plugin.settingsManager.removeBackground(background.id);
480 | this.display();
481 | new Notice('背景已删除');
482 | }
483 | ).open();
484 | }));
485 |
486 | // 添加背景预览
487 | const previewEl = backgroundItem.createDiv('background-preview');
488 | previewEl.setAttribute('style', background.style);
489 | });
490 |
491 | // 添加新背景按钮
492 | new Setting(containerEl)
493 | .addButton(btn => btn
494 | .setButtonText('+ 新建背景')
495 | .setCta()
496 | .onClick(() => {
497 | // 使用新的背景创建模态框
498 | new CreateBackgroundModal(
499 | this.app,
500 | async (newBackground) => {
501 | await this.plugin.settingsManager.addCustomBackground(newBackground);
502 | this.display();
503 | new Notice('背景已创建');
504 | }
505 | ).open();
506 | }));
507 | }
508 | }
--------------------------------------------------------------------------------
/src/settings/settings.ts:
--------------------------------------------------------------------------------
1 | import { Template } from '../templateManager';
2 | import { Background } from '../backgroundManager';
3 |
4 | interface MPSettings {
5 | backgroundId: string;
6 | templateId: string;
7 | fontFamily: string;
8 | fontSize: number;
9 | templates: Template[];
10 | customTemplates: Template[];
11 | backgrounds: Background[];
12 | customBackgrounds: Background[];
13 | customFonts: { value: string; label: string; isPreset?: boolean }[];
14 | }
15 |
16 | const DEFAULT_SETTINGS: MPSettings = {
17 | backgroundId: 'default',
18 | templateId: 'default',
19 | fontFamily: '-apple-system',
20 | fontSize: 16,
21 | templates: [],
22 | customTemplates: [],
23 | backgrounds: [],
24 | customBackgrounds: [],
25 | customFonts: [
26 | {
27 | value: 'Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, "PingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif',
28 | label: '默认字体',
29 | isPreset: true
30 | },
31 | { value: 'SimSun, "宋体", serif', label: '宋体', isPreset: true },
32 | { value: 'SimHei, "黑体", sans-serif', label: '黑体', isPreset: true },
33 | { value: 'KaiTi, "楷体", serif', label: '楷体', isPreset: true },
34 | { value: '"Microsoft YaHei", "微软雅黑", sans-serif', label: '雅黑', isPreset: true }
35 | ],
36 | };
37 |
38 | export class SettingsManager {
39 | private plugin: any;
40 | private settings: MPSettings;
41 |
42 | constructor(plugin: any) {
43 | this.plugin = plugin;
44 | this.settings = DEFAULT_SETTINGS;
45 | }
46 |
47 | async loadSettings() {
48 | let savedData = await this.plugin.loadData();
49 | if (!savedData) {
50 | savedData = {};
51 | }
52 | if (!savedData.templates || savedData.templates.length === 0) {
53 | const { templates } = await import('../templates');
54 | savedData.templates = Object.values(templates).map(template => ({
55 | ...template,
56 | isPreset: true,
57 | isVisible: true // 默认可见
58 | }));
59 | }
60 | if (!savedData.customTemplates) {
61 | savedData.customTemplates = [];
62 | }
63 | if (!savedData.customFonts) {
64 | savedData.customFonts = DEFAULT_SETTINGS.customFonts;
65 | }
66 | // 加载背景设置
67 | if (!savedData.backgrounds || savedData.backgrounds.length === 0) {
68 | const { backgrounds } = await import('../backgrounds');
69 | savedData.backgrounds = backgrounds.backgrounds.map(background => ({
70 | ...background,
71 | isPreset: true,
72 | isVisible: true
73 | }));
74 | }
75 | if (!savedData.customBackgrounds) {
76 | savedData.customBackgrounds = [];
77 | }
78 | if (!savedData.customFonts) {
79 | savedData.customFonts = DEFAULT_SETTINGS.customFonts;
80 | }
81 | this.settings = Object.assign({}, DEFAULT_SETTINGS, savedData);
82 | }
83 |
84 | getAllTemplates(): Template[] {
85 | return [...this.settings.templates, ...this.settings.customTemplates];
86 | }
87 |
88 | getVisibleTemplates(): Template[] {
89 | return this.getAllTemplates().filter(template => template.isVisible !== false);
90 | }
91 |
92 | getTemplate(templateId: string): Template | undefined {
93 | return this.settings.templates.find(template => template.id === templateId)
94 | || this.settings.customTemplates.find(template => template.id === templateId);
95 | }
96 |
97 | async addCustomTemplate(template: Template) {
98 | template.isPreset = false;
99 | template.isVisible = true; // 默认可见
100 | this.settings.customTemplates.push(template);
101 | await this.saveSettings();
102 | }
103 |
104 | async updateTemplate(templateId: string, updatedTemplate: Partial) {
105 | const presetTemplateIndex = this.settings.templates.findIndex(t => t.id === templateId);
106 | if (presetTemplateIndex !== -1) {
107 | this.settings.templates[presetTemplateIndex] = {
108 | ...this.settings.templates[presetTemplateIndex],
109 | ...updatedTemplate
110 | };
111 | await this.saveSettings();
112 | return true;
113 | }
114 |
115 | const customTemplateIndex = this.settings.customTemplates.findIndex(t => t.id === templateId);
116 | if (customTemplateIndex !== -1) {
117 | this.settings.customTemplates[customTemplateIndex] = {
118 | ...this.settings.customTemplates[customTemplateIndex],
119 | ...updatedTemplate
120 | };
121 | await this.saveSettings();
122 | return true;
123 | }
124 |
125 | return false;
126 | }
127 |
128 | async removeTemplate(templateId: string): Promise {
129 | const template = this.getTemplate(templateId);
130 | if (template && !template.isPreset) {
131 | this.settings.customTemplates = this.settings.customTemplates.filter(t => t.id !== templateId);
132 | if (this.settings.templateId === templateId) {
133 | this.settings.templateId = 'default';
134 | }
135 | await this.saveSettings();
136 | return true;
137 | }
138 | return false;
139 | }
140 |
141 | async saveSettings() {
142 | await this.plugin.saveData(this.settings);
143 | }
144 |
145 | getSettings(): MPSettings {
146 | return this.settings;
147 | }
148 |
149 | async updateSettings(settings: Partial) {
150 | this.settings = { ...this.settings, ...settings };
151 | await this.saveSettings();
152 | }
153 |
154 | getFontOptions() {
155 | return this.settings.customFonts;
156 | }
157 |
158 | async addCustomFont(font: { value: string; label: string }) {
159 | this.settings.customFonts.push({ ...font, isPreset: false });
160 | await this.saveSettings();
161 | }
162 |
163 | async removeFont(value: string) {
164 | const font = this.settings.customFonts.find(f => f.value === value);
165 | if (font && !font.isPreset) {
166 | this.settings.customFonts = this.settings.customFonts.filter(f => f.value !== value);
167 | await this.saveSettings();
168 | }
169 | }
170 |
171 | async updateFont(oldValue: string, newFont: { value: string; label: string }) {
172 | const index = this.settings.customFonts.findIndex(f => f.value === oldValue);
173 | if (index !== -1 && !this.settings.customFonts[index].isPreset) {
174 | this.settings.customFonts[index] = { ...newFont, isPreset: false };
175 | await this.saveSettings();
176 | }
177 | }
178 |
179 | // 背景相关方法
180 | getAllBackgrounds(): Background[] {
181 | return [...this.settings.backgrounds, ...this.settings.customBackgrounds];
182 | }
183 |
184 | getVisibleBackgrounds(): Background[] {
185 | return this.getAllBackgrounds().filter(background => background.isVisible !== false);
186 | }
187 |
188 | getBackground(backgroundId: string): Background | undefined {
189 | return this.settings.backgrounds.find(background => background.id === backgroundId)
190 | || this.settings.customBackgrounds.find(background => background.id === backgroundId);
191 | }
192 |
193 | async addCustomBackground(background: Background) {
194 | background.isPreset = false;
195 | background.isVisible = true; // 默认可见
196 | this.settings.customBackgrounds.push(background);
197 | await this.saveSettings();
198 | }
199 |
200 | async updateBackground(backgroundId: string, updatedBackground: Partial) {
201 | const presetBackgroundIndex = this.settings.backgrounds.findIndex(b => b.id === backgroundId);
202 | if (presetBackgroundIndex !== -1) {
203 | this.settings.backgrounds[presetBackgroundIndex] = {
204 | ...this.settings.backgrounds[presetBackgroundIndex],
205 | ...updatedBackground
206 | };
207 | await this.saveSettings();
208 | return true;
209 | }
210 |
211 | const customBackgroundIndex = this.settings.customBackgrounds.findIndex(b => b.id === backgroundId);
212 | if (customBackgroundIndex !== -1) {
213 | this.settings.customBackgrounds[customBackgroundIndex] = {
214 | ...this.settings.customBackgrounds[customBackgroundIndex],
215 | ...updatedBackground
216 | };
217 | await this.saveSettings();
218 | return true;
219 | }
220 |
221 | return false;
222 | }
223 |
224 | async removeBackground(backgroundId: string): Promise {
225 | const background = this.getBackground(backgroundId);
226 | if (background && !background.isPreset) {
227 | this.settings.customBackgrounds = this.settings.customBackgrounds.filter(b => b.id !== backgroundId);
228 | if (this.settings.backgroundId === backgroundId) {
229 | this.settings.backgroundId = 'default';
230 | }
231 | await this.saveSettings();
232 | return true;
233 | }
234 | return false;
235 | }
236 | }
--------------------------------------------------------------------------------
/src/settings/templatePreviewModal.ts:
--------------------------------------------------------------------------------
1 | import { App, Modal } from 'obsidian';
2 | import { TemplateManager } from '../templateManager';
3 |
4 | export class TemplatePreviewModal extends Modal {
5 | private template: any;
6 | private templateManager: TemplateManager;
7 |
8 | constructor(app: App, template: any, templateManager: TemplateManager) {
9 | super(app);
10 | this.template = template;
11 | this.templateManager = templateManager;
12 | }
13 |
14 | onOpen() {
15 | const { contentEl } = this;
16 | contentEl.empty();
17 | contentEl.addClass('template-preview-modal');
18 |
19 | // 添加标题
20 | contentEl.createEl('h2', { text: `模板预览: ${this.template.name}`, cls: 'mp-template-title' });
21 |
22 | // 添加预览区域
23 | const container = contentEl.createDiv('tp-mp-preview-area');
24 | const content = container.createDiv('tp-mp-content-section');
25 |
26 | // 标题样式
27 | content.createEl('h2', { text: '探索夜半插件的无限可能'});
28 | content.createEl('h3', { text: '探索我的插件,让您的笔记发布变得更加轻松!'});
29 |
30 | // 段落样式
31 | const paragraph1 = content.createEl('p');
32 | paragraph1.createEl('span', { text: '插件为您提供各种' });
33 | paragraph1.createEl('strong', { text: '优雅的操作,' });
34 | paragraph1.createEl('span', { text: '助您轻松发布笔记。' });
35 |
36 | const paragraph2 = content.createEl('p');
37 | paragraph2.createEl('span', { text: '通过插件,您可以快速组织内容,' });
38 | paragraph2.createEl('em', { text: '提升工作效率。' });
39 |
40 | content.createEl('hr');
41 |
42 | // 列表样式
43 | const list = content.createEl('ol');
44 | list.createEl('li', { text: '轻松定制模板样式' });
45 | list.createEl('li', { text: '实时预览模板效果' });
46 |
47 | // 引用样式
48 | const quote = content.createEl('blockquote');
49 | quote.createEl('p', { text: '“让笔记发帖变得如此简单。”' });
50 |
51 | // 代码样式
52 | const codeBlock = content.createEl('pre');
53 | const header = codeBlock.createDiv('mp-code-header'); // 添加窗口按钮
54 | for (let i = 0; i < 3; i++) {
55 | const dot = document.createElement('span');
56 | dot.className = 'mp-code-dot';
57 | header.appendChild(dot);
58 | }
59 | codeBlock.insertBefore(header, codeBlock.firstChild);
60 | codeBlock.createEl('code', { text: 'console.log("欢迎使用夜半插件!");' });
61 |
62 | // 添加打赏引导文案
63 | content.createEl('strong', { text: '如果您觉得我的插件对您有帮助,请打赏支持我。'});
64 |
65 | // 分隔线样式
66 | content.createEl('hr');
67 |
68 | this.templateManager.applyTemplate(container, this.template);
69 | }
70 |
71 | onClose() {
72 | const { contentEl } = this;
73 | contentEl.empty();
74 | }
75 | }
--------------------------------------------------------------------------------
/src/styles/index.css:
--------------------------------------------------------------------------------
1 | @import url('./view/layout.css');
2 | @import url('./view/preview.css');
3 | @import url('./settings/settings.css');
4 | @import url('./settings/font-modal.css');
5 | @import url('./settings/template-modal.css');
6 | @import url('./settings/template-preview-modal.css');
7 | @import url('./settings/background-modal.css');
--------------------------------------------------------------------------------
/src/styles/settings/background-modal.css:
--------------------------------------------------------------------------------
1 | /* 模态框基本样式 */
2 | .mp-background-modal {
3 | display: flex;
4 | flex-direction: column;
5 | padding: 20px 20px 0 20px;
6 | }
7 |
8 | /* 标题样式 */
9 | .mp-background-modal h2 {
10 | margin-top: 0;
11 | margin-bottom: 20px;
12 | text-align: center;
13 | color: var(--text-normal);
14 | }
15 |
16 | /* 各部分容器样式 */
17 | .mp-background-modal .background-basic-section,
18 | .mp-background-modal .background-type-specific-section,
19 | .mp-background-modal .background-preview-section,
20 | .mp-background-modal .background-button-section {
21 | margin-bottom: 20px;
22 | }
23 |
24 | /* 设置项样式 */
25 | .mp-background-modal .setting-item {
26 | padding: 0.8rem 0;
27 | border-bottom: 1px solid var(--background-modifier-border);
28 | display: flex;
29 | }
30 |
31 | /* 设置项名称和描述的容器 */
32 | .mp-background-modal .setting-item-info {
33 | flex: 0 0 30%;
34 | padding-right: 15px;
35 | }
36 |
37 | /* 设置项控件的容器 */
38 | .mp-background-modal .setting-item-control {
39 | flex: 0 0 70%;
40 | display: flex;
41 | align-items: center;
42 | }
43 |
44 | /* 输入控件样式 */
45 | /* 输入框和下拉框统一宽度 */
46 | .mp-background-modal input[type="text"],
47 | .mp-background-modal select,
48 | .mp-background-modal .dropdown {
49 | width: 40%;
50 | height: 32px;
51 | border-radius: 4px;
52 | margin: 0 20px;
53 | }
54 |
55 | /* 文本区域样式调整 */
56 | .mp-background-modal textarea {
57 | width: 40%;
58 | height: 80px;
59 | font-family: var(--font-monospace);
60 | font-size: 0.9em;
61 | border-radius: 4px;
62 | padding: 8px;
63 | resize: vertical;
64 | margin: 0 20px;
65 | }
66 |
67 | /* 颜色选择器和滑块容器 */
68 | .mp-background-modal .color-picker-container,
69 | .mp-background-modal .slider-container {
70 | display: flex;
71 | align-items: center;
72 | margin: 0 20px;
73 | }
74 |
75 | /* 颜色选择器样式 */
76 | .mp-background-modal input[type="color"] {
77 | margin-right: 10px;
78 | }
79 |
80 | /* 滑块样式调整 */
81 | .mp-background-modal .slider {
82 | width: 27%;
83 | margin: 0 20px 0 0;
84 | }
85 |
86 | /* 预览区域样式 */
87 | .mp-background-modal .background-preview-section h3 {
88 | margin-bottom: 10px;
89 | font-size: 1.1em;
90 | color: var(--text-normal);
91 | }
92 |
93 | /* 改善预览区域的样式 */
94 | .mp-background-modal .background-preview {
95 | height: 120px;
96 | border-radius: 8px;
97 | display: flex;
98 | align-items: center;
99 | justify-content: center;
100 | border: 1px solid var(--background-modifier-border);
101 | overflow: hidden;
102 | margin-top: 10px;
103 | }
104 |
105 | .mp-background-modal .background-preview span {
106 | padding: 5px 10px;
107 | background-color: rgba(255, 255, 255, 0.7);
108 | border-radius: 4px;
109 | font-size: 0.9em;
110 | color: var(--text-normal);
111 | }
112 |
113 | .mp-background-modal .background-image-preview {
114 | margin-top: 10px;
115 | max-height: 150px;
116 | overflow: hidden;
117 | border-radius: 6px;
118 | border: 1px solid var(--background-modifier-border);
119 | }
120 |
121 | .mp-background-modal .background-image-preview img {
122 | width: 100%;
123 | height: auto;
124 | object-fit: cover;
125 | }
126 |
127 | /* 按钮区域样式 */
128 | .mp-background-modal .background-button-section .setting-item {
129 | display: flex;
130 | justify-content: flex-end;
131 | gap: 10px;
132 | padding: 0;
133 | border-bottom: none;
134 | }
135 |
136 | .mp-background-modal button {
137 | padding: 6px 12px;
138 | border-radius: 4px;
139 | }
140 |
141 | /* 辅助类 */
142 | .mp-background-modal .is-hidden {
143 | display: none;
144 | }
--------------------------------------------------------------------------------
/src/styles/settings/font-modal.css:
--------------------------------------------------------------------------------
1 | /* 组件容器 */
2 | .mp-font-modal {
3 | --mfd-accent: var(--interactive-accent);
4 | --mfd-bg: var(--background-primary);
5 | --mfd-border: var(--background-modifier-border);
6 | }
7 |
8 | /* 标题容器 */
9 | .mp-font-modal .mfd-header {
10 | display: flex;
11 | align-items: center;
12 | margin: 0 0 1.5rem;
13 | padding: 0.5rem 0;
14 | border-bottom: 1px solid var(--mfd-border);
15 | }
16 |
17 | /* 帮助按钮容器 */
18 | .mp-font-modal .mfd-help-trigger {
19 | position: relative;
20 | margin-left: 0.8rem;
21 | }
22 |
23 | /* 帮助按钮 */
24 | .mp-font-modal .mfd-help-btn {
25 | background: none;
26 | border: none;
27 | padding: 4px;
28 | color: var(--text-muted);
29 | cursor: pointer;
30 | transition: opacity 0.2s ease;
31 | opacity: 0.8;
32 | }
33 | .mp-font-modal .mfd-help-btn:hover {
34 | opacity: 1;
35 | color: var(--mfd-accent);
36 | }
37 |
38 | /* 提示框 */
39 | .mp-font-modal .mfd-help-tooltip {
40 | display: none;
41 | position: absolute;
42 | top: calc(100% + 10px);
43 | width: 390px;
44 | padding: 14px;
45 | background: var(--mfd-bg);
46 | border: 1px solid var(--mfd-border);
47 | border-radius: 6px;
48 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
49 | z-index: calc(var(--layer-popover) + 100);
50 | font-size: 13px;
51 | line-height: 1.6;
52 | white-space: pre-line;
53 | }
54 |
55 | /* 交互状态 */
56 | .mp-font-modal .mfd-help-trigger:hover .mfd-help-tooltip {
57 | display: block;
58 | }
59 |
60 | /* 移动端适配 */
61 | @media (hover: none) and (pointer: coarse) {
62 | .mp-font-modal .mfd-help-tooltip {
63 | width: 95vw;
64 | left: auto;
65 | right: 0;
66 | transform: none;
67 | }
68 | }
--------------------------------------------------------------------------------
/src/styles/settings/settings.css:
--------------------------------------------------------------------------------
1 | /* 整体容器 */
2 | .mp-settings {
3 | margin: 0 auto;
4 | padding: 20px;
5 | background-color: var(--background-primary);
6 | border-radius: 10px;
7 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
8 | }
9 |
10 | /* 标题样式 */
11 | .mp-settings h2 {
12 | margin: 20px 0;
13 | padding-bottom: 1rem;
14 | border-bottom: 2px solid var(--background-modifier-border);
15 | font-size: 1.6em;
16 | color: var(--text-normal);
17 | }
18 |
19 | /* 通用设置区块样式 */
20 | .mp-settings .settings-section {
21 | margin-bottom: 2rem;
22 | border: 1px solid var(--background-modifier-border);
23 | border-radius: 8px;
24 | overflow-y: auto;
25 | background-color: var(--background-secondary);
26 | }
27 |
28 | .mp-settings .settings-section-header {
29 | display: flex;
30 | align-items: center;
31 | cursor: pointer;
32 | padding: 1.2rem;
33 | background-color: var(--background-secondary);
34 | transition: background-color 0.3s ease;
35 | }
36 |
37 | .mp-settings .settings-section-header:hover {
38 | background-color: var(--background-secondary-alt);
39 | }
40 |
41 | .mp-settings .settings-section-header h4 {
42 | margin: 0;
43 | flex: 1;
44 | font-size: 1.2em;
45 | color: var(--text-normal);
46 | }
47 |
48 | .mp-settings .settings-section-toggle {
49 | margin-right: 0.5rem;
50 | color: var(--text-muted);
51 | }
52 |
53 | .mp-settings .settings-section-content {
54 | display: none;
55 | padding: 20px 10px;
56 | background-color: var(--background-primary);
57 | }
58 |
59 | .mp-settings .settings-section.is-expanded .settings-section-content {
60 | display: block;
61 | }
62 |
63 | /* 模板管理区域样式 */
64 | .mp-settings .template-management {
65 | max-height: 400px;
66 | overflow-y: auto;
67 | padding: 0 12px;
68 | }
69 |
70 | /* 自定义滚动条样式 */
71 | .mp-settings .template-management::-webkit-scrollbar {
72 | width: 8px;
73 | }
74 |
75 | .mp-settings .template-management::-webkit-scrollbar-track {
76 | background: var(--background-primary);
77 | border-radius: 4px;
78 | }
79 |
80 | .mp-settings .template-management::-webkit-scrollbar-thumb {
81 | background: var(--background-modifier-border);
82 | border-radius: 4px;
83 | }
84 |
85 | .mp-settings .template-management::-webkit-scrollbar-thumb:hover {
86 | background: var(--background-modifier-border-hover);
87 | }
88 |
89 | /* 模板设置特有样式 */
90 | .mp-settings .template-list {
91 | padding: 15px 0;
92 | }
93 |
94 | .mp-settings .template-list h4 {
95 | margin: 0 0 1.5rem;
96 | color: var(--text-normal);
97 | font-size: 1.2em;
98 | }
99 |
100 | .mp-settings .template-item {
101 | margin-bottom: 0.8rem;
102 | padding: 1rem;
103 | border-radius: 8px;
104 | background-color: var(--background-primary);
105 | border: 1px solid var(--background-modifier-border);
106 | transition: all 0.3s ease;
107 | }
108 |
109 | .mp-settings .template-item:hover {
110 | box-shadow: 0 4px 12px var(--background-modifier-box-shadow);
111 | }
112 |
113 | .mp-settings .template-item .setting-item {
114 | border: none;
115 | padding: 12px;
116 | }
117 |
118 | .mp-settings .template-item .setting-item-info {
119 | margin-right: 1.5rem;
120 | }
121 |
122 | .mp-settings .template-item .setting-item-name {
123 | font-size: 1.2em;
124 | font-weight: 600;
125 | color: var(--text-normal);
126 | }
127 |
128 | .mp-settings .template-item .setting-item-description {
129 | color: var(--text-muted);
130 | }
131 |
132 | .mp-settings .template-item .setting-item-control .clickable-icon {
133 | padding: 6px;
134 | border-radius: 6px;
135 | color: var(--text-muted);
136 | }
137 |
138 | .mp-settings .template-item .setting-item-control .clickable-icon:hover {
139 | color: var(--text-normal);
140 | background-color: var(--background-modifier-hover);
141 | }
142 |
143 | /* 模板显示设置样式 */
144 | .template-visibility-container {
145 | margin-top: 20px;
146 | }
147 |
148 | .template-visibility-container h3 {
149 | margin-bottom: 15px;
150 | font-size: 1.3em;
151 | color: var(--text-normal);
152 | }
153 |
154 | .template-selection-container {
155 | display: flex;
156 | margin-top: 10px;
157 | gap: 15px;
158 | align-items: stretch;
159 | }
160 |
161 | .all-templates-container, .visible-templates-container {
162 | flex: 1;
163 | border: 1px solid var(--background-modifier-border);
164 | border-radius: 8px;
165 | padding: 15px;
166 | background-color: var(--background-primary);
167 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
168 | }
169 |
170 | .all-templates-container h4, .visible-templates-container h4 {
171 | margin: 0 0 15px;
172 | padding-bottom: 10px;
173 | border-bottom: 1px solid var(--background-modifier-border);
174 | font-size: 1.1em;
175 | color: var(--text-normal);
176 | text-align: center;
177 | }
178 |
179 | .control-buttons-container {
180 | display: flex;
181 | flex-direction: column;
182 | justify-content: center;
183 | padding: 0 5px;
184 | }
185 |
186 | .control-buttons-container button {
187 | margin: 5px 0;
188 | padding: 8px 12px;
189 | border-radius: 6px;
190 | background-color: var(--interactive-normal);
191 | color: var(--text-normal);
192 | border: 1px solid var(--background-modifier-border);
193 | cursor: pointer;
194 | transition: all 0.2s ease;
195 | }
196 |
197 | .control-buttons-container button:hover {
198 | background-color: var(--interactive-hover);
199 | }
200 |
201 | .templates-list {
202 | max-height: 250px;
203 | overflow-y: auto;
204 | padding: 5px;
205 | }
206 |
207 | .template-list-item {
208 | padding: 10px;
209 | margin-bottom: 8px;
210 | cursor: pointer;
211 | border-radius: 6px;
212 | background-color: var(--background-secondary);
213 | transition: all 0.2s ease;
214 | }
215 |
216 | .template-list-item:hover {
217 | background-color: var(--background-modifier-hover);
218 | }
219 |
220 | .template-list-item.selected {
221 | background-color: var(--interactive-accent);
222 | color: var(--text-on-accent);
223 | }
224 |
225 | /* 自定义滚动条样式 */
226 | .templates-list::-webkit-scrollbar {
227 | width: 6px;
228 | }
229 |
230 | .templates-list::-webkit-scrollbar-track {
231 | background: var(--background-primary);
232 | border-radius: 3px;
233 | }
234 |
235 | .templates-list::-webkit-scrollbar-thumb {
236 | background: var(--background-modifier-border);
237 | border-radius: 3px;
238 | }
239 |
240 | .templates-list::-webkit-scrollbar-thumb:hover {
241 | background: var(--background-modifier-border-hover);
242 | }
243 |
244 | /* 设置子区域折叠样式 */
245 | .mp-settings .mp-settings-subsection {
246 | margin: 0 12px;
247 | border: 1px solid var(--background-modifier-border);
248 | border-radius: 8px;
249 | overflow-y: auto;
250 | }
251 |
252 | .mp-settings .mp-settings-subsection-header {
253 | display: flex;
254 | align-items: center;
255 | cursor: pointer;
256 | padding: 0.8rem 1rem;
257 | background-color: var(--background-primary);
258 | transition: background-color 0.3s ease;
259 | }
260 |
261 | .mp-settings .mp-settings-subsection-header:hover {
262 | background-color: var(--background-modifier-hover);
263 | }
264 |
265 | .mp-settings .mp-settings-subsection-header h3 {
266 | margin: 0;
267 | flex: 1;
268 | font-size: 1.1em;
269 | color: var(--text-normal);
270 | }
271 |
272 | .mp-settings .mp-settings-subsection-toggle {
273 | margin-right: 0.5rem;
274 | color: var(--text-muted);
275 | }
276 |
277 | .mp-settings .mp-settings-subsection-content {
278 | display: none;
279 | padding: 1rem;
280 | margin: 0 20px;
281 | background-color: var(--background-primary);
282 | }
283 |
284 | .mp-settings .mp-settings-subsection.is-expanded .mp-settings-subsection-content {
285 | display: block;
286 | }
287 |
288 | /* 背景管理区域样式 */
289 | .mp-settings .background-management {
290 | max-height: 400px;
291 | overflow-y: auto;
292 | padding: 0 12px;
293 | }
294 |
295 | /* 背景管理滚动条样式 */
296 | .mp-settings .background-management::-webkit-scrollbar {
297 | width: 8px;
298 | }
299 |
300 | .mp-settings .background-management::-webkit-scrollbar-track {
301 | background: var(--background-primary);
302 | border-radius: 4px;
303 | }
304 |
305 | .mp-settings .background-management::-webkit-scrollbar-thumb {
306 | background: var(--background-modifier-border);
307 | border-radius: 4px;
308 | }
309 |
310 | .mp-settings .background-management::-webkit-scrollbar-thumb:hover {
311 | background: var(--background-modifier-border-hover);
312 | }
313 |
314 | /* 背景项目样式 */
315 | .mp-settings .background-item {
316 | margin-bottom: 0.8rem;
317 | padding: 1rem;
318 | border-radius: 8px;
319 | background-color: var(--background-primary);
320 | border: 1px solid var(--background-modifier-border);
321 | transition: all 0.3s ease;
322 | }
323 |
324 | .mp-settings .background-item:hover {
325 | box-shadow: 0 4px 12px var(--background-modifier-box-shadow);
326 | }
327 |
328 | .mp-settings .background-item .setting-item {
329 | border: none;
330 | padding: 12px;
331 | }
332 |
333 | /* 背景预览区域 */
334 | .mp-settings .background-preview {
335 | height: 20px;
336 | border-radius: 5px;
337 | display: flex;
338 | align-items: center;
339 | justify-content: center;
340 | border: 1px solid var(--background-modifier-border);
341 | }
342 |
343 | /* 背景显示设置样式 */
344 | .background-selection-container {
345 | display: flex;
346 | margin-top: 10px;
347 | gap: 15px;
348 | align-items: stretch;
349 | }
350 |
351 | .all-backgrounds-container, .visible-backgrounds-container {
352 | flex: 1;
353 | border: 1px solid var(--background-modifier-border);
354 | border-radius: 8px;
355 | padding: 15px;
356 | background-color: var(--background-primary);
357 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
358 | }
359 |
360 | .all-backgrounds-container h4, .visible-backgrounds-container h4 {
361 | margin: 0 0 15px;
362 | padding-bottom: 10px;
363 | border-bottom: 1px solid var(--background-modifier-border);
364 | font-size: 1.1em;
365 | color: var(--text-normal);
366 | text-align: center;
367 | }
368 |
369 | .backgrounds-list {
370 | max-height: 250px;
371 | overflow-y: auto;
372 | padding: 5px;
373 | }
374 |
375 | .background-list-item {
376 | padding: 10px;
377 | margin-bottom: 8px;
378 | cursor: pointer;
379 | border-radius: 6px;
380 | background-color: var(--background-secondary);
381 | transition: all 0.2s ease;
382 | }
383 |
384 | .background-list-item:hover {
385 | background-color: var(--background-modifier-hover);
386 | }
387 |
388 | .background-list-item.selected {
389 | background-color: var(--interactive-accent);
390 | color: var(--text-on-accent);
391 | }
392 |
393 | /* 背景列表滚动条样式 */
394 | .backgrounds-list::-webkit-scrollbar {
395 | width: 6px;
396 | }
397 |
398 | .backgrounds-list::-webkit-scrollbar-track {
399 | background: var(--background-primary);
400 | border-radius: 3px;
401 | }
402 |
403 | .backgrounds-list::-webkit-scrollbar-thumb {
404 | background: var(--background-modifier-border);
405 | border-radius: 3px;
406 | }
407 |
408 | .backgrounds-list::-webkit-scrollbar-thumb:hover {
409 | background: var(--background-modifier-border-hover);
410 | }
--------------------------------------------------------------------------------
/src/styles/settings/template-modal.css:
--------------------------------------------------------------------------------
1 | /* 基础变量 */
2 | :root {
3 | --modal-padding: 1.5rem;
4 | --border-radius: 8px;
5 | --transition-duration: 0.2s;
6 | }
7 |
8 | /* 模态框基础布局 */
9 | .mp-template-modal {
10 | display: flex;
11 | flex-direction: column;
12 | height: 75vh;
13 | }
14 |
15 | /* 模态框头部 */
16 | .mp-template-modal .modal-header {
17 | padding: var(--modal-padding);
18 | border-bottom: 1px solid var(--background-modifier-border);
19 | background-color: var(--background-primary);
20 | }
21 |
22 | .mp-template-modal .modal-header h2 {
23 | margin: 0 0 1rem;
24 | font-size: 1.5em;
25 | color: var(--text-normal);
26 | }
27 |
28 | /* 可滚动内容区域 */
29 | .mp-template-modal .modal-scroll-container {
30 | flex: 1;
31 | overflow-y: auto;
32 | padding: var(--modal-padding);
33 | background-color: var(--background-secondary);
34 | }
35 |
36 | /* 自定义滚动条 */
37 | .mp-template-modal .modal-scroll-container::-webkit-scrollbar {
38 | width: 8px;
39 | }
40 |
41 | .mp-template-modal .modal-scroll-container::-webkit-scrollbar-track {
42 | background: var(--background-primary);
43 | }
44 |
45 | .mp-template-modal .modal-scroll-container::-webkit-scrollbar-thumb {
46 | background-color: var(--background-modifier-border);
47 | border-radius: 4px;
48 | }
49 |
50 | .mp-template-modal .modal-scroll-container::-webkit-scrollbar-thumb:hover {
51 | background-color: var(--background-modifier-border-hover);
52 | }
53 |
54 | /* 样式设置区域 */
55 | .mp-template-modal .style-section {
56 | margin-bottom: 1rem;
57 | border: 1px solid var(--background-modifier-border);
58 | border-radius: var(--border-radius);
59 | overflow: hidden;
60 | background-color: var(--background-primary);
61 | }
62 |
63 | .mp-template-modal .style-section-header {
64 | display: flex;
65 | align-items: center;
66 | justify-content: space-between;
67 | padding: 1rem;
68 | background-color: var(--background-secondary);
69 | cursor: pointer;
70 | transition: background-color var(--transition-duration) ease;
71 | }
72 |
73 | .mp-template-modal .style-section-reset {
74 | display: flex;
75 | align-items: center;
76 | padding: 4px;
77 | border-radius: 4px;
78 | cursor: pointer;
79 | color: var(--text-muted);
80 | transition: all 0.2s ease;
81 | }
82 |
83 | .mp-template-modal .style-section-reset:hover {
84 | color: var(--text-normal);
85 | background-color: var(--background-modifier-hover);
86 | }
87 |
88 | .mp-template-modal .style-section-header:hover {
89 | background-color: var(--background-secondary-alt);
90 | }
91 |
92 | .mp-template-modal .style-section-title {
93 | display: flex;
94 | align-items: center;
95 | gap: 0.5rem;
96 | flex: 1;
97 | }
98 |
99 | .mp-template-modal .style-section-title h3 {
100 | margin: 0;
101 | font-size: 1.2em; /* 调整 h3 的字体大小 */
102 | color: var(--text-normal);
103 | }
104 |
105 | .mp-template-modal .style-section-title h4 {
106 | margin: 0;
107 | font-size: 1em; /* 确保 h4 的字体大小小于 h3 */
108 | color: var(--text-normal);
109 | }
110 |
111 | .mp-template-modal .style-section-toggle {
112 | display: flex;
113 | align-items: center;
114 | color: var(--text-muted);
115 | transition: transform var(--transition-duration) ease;
116 | margin-right: 0.5rem;
117 | }
118 |
119 | .mp-template-modal .style-section-content {
120 | display: none;
121 | padding: 1rem;
122 | background-color: var(--background-primary);
123 | }
124 |
125 | .mp-template-modal .style-section.is-expanded .style-section-content {
126 | display: block;
127 | }
128 |
129 | /* 设置项样式 */
130 | .mp-template-modal .setting-item {
131 | padding: 0.8rem 0;
132 | border-bottom: 1px solid var(--background-modifier-border);
133 | display: flex;
134 | }
135 |
136 | .mp-template-modal .setting-item:last-child {
137 | border-bottom: none;
138 | }
139 |
140 | .mp-template-modal .setting-item-info {
141 | margin-right: 1.5rem;
142 | margin-bottom: 0.5rem;
143 | }
144 |
145 | .mp-template-modal .setting-item-name {
146 | font-size: 0.9em; /* 确保设置项名称的字体大小小于 h4 */
147 | font-weight: normal;
148 | color: var(--text-normal);
149 | }
150 |
151 | .mp-template-modal .setting-item-description {
152 | font-size: 0.8em; /* 确保设置项描述的字体大小小于 h4 */
153 | color: var(--text-muted);
154 | }
155 |
156 | /* 输入控件样式 */
157 | .mp-template-modal .setting-item input {
158 | width: 120px;
159 | padding: 6px 10px;
160 | border: 1px solid var(--background-modifier-border);
161 | border-radius: 4px;
162 | background: var(--background-primary);
163 | font-size: 14px;
164 | }
165 |
166 | /* 自定义 CSS 输入框保持原样 */
167 | .mp-template-modal .custom-css-input {
168 | width: 100%;
169 | padding: 8px 12px;
170 | border: 1px solid var(--background-modifier-border);
171 | border-radius: 4px;
172 | background: var(--background-primary);
173 | font-family: var(--font-monospace);
174 | font-size: 13px;
175 | line-height: 1.5;
176 | resize: vertical;
177 | min-height: 100px;
178 | }
179 |
180 | /* 颜色选择器样式优化 */
181 | .mp-template-modal .setting-item input[type="color"] {
182 | width: 32px;
183 | height: 32px;
184 | padding: 0;
185 | border: none;
186 | border-radius: 50%;
187 | cursor: pointer;
188 | overflow: hidden;
189 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
190 | position: relative;
191 | }
192 |
193 | .mp-template-modal .setting-item input[type="color"]::-webkit-color-swatch-wrapper {
194 | padding: 0;
195 | border-radius: 50%;
196 | position: absolute;
197 | top: 0;
198 | left: 0;
199 | width: 100%;
200 | height: 100%;
201 | }
202 |
203 | .mp-template-modal .setting-item input[type="color"]::-webkit-color-swatch {
204 | border: none;
205 | border-radius: 50%;
206 | padding: 0;
207 | width: 100%;
208 | height: 100%;
209 | }
210 |
211 | .mp-template-modal .custom-css-input:focus {
212 | border-color: var(--interactive-accent);
213 | outline: none;
214 | }
215 |
216 | .mp-template-modal .setting-item-control {
217 | display: flex;
218 | align-items: center;
219 | gap: 0.5rem;
220 | }
221 |
222 | .mp-template-modal .setting-item-control .clickable-icon {
223 | padding: 4px;
224 | border-radius: 4px;
225 | color: var(--text-muted);
226 | cursor: pointer;
227 | }
228 |
229 | .mp-template-modal .setting-item-control .clickable-icon:hover {
230 | color: var(--text-normal);
231 | background-color: var(--background-modifier-hover);
232 | }
233 |
234 | /* 按钮样式 */
235 | .mp-modal-button {
236 | padding: 10px 24px;
237 | border-radius: 24px;
238 | transition: all 0.3s ease;
239 | background-color: var(--interactive-accent);
240 | color: var(--text-on-accent);
241 | border: none;
242 | cursor: pointer;
243 | }
244 |
245 | .mp-modal-button:hover {
246 | opacity: 0.85;
247 | transform: translateY(-2px);
248 | }
249 |
250 | /* 错误提示 */
251 | .mp-template-modal .notice {
252 | color: var(--text-error);
253 | margin-top: 6px;
254 | font-size: 0.9em;
255 | }
--------------------------------------------------------------------------------
/src/styles/settings/template-preview-modal.css:
--------------------------------------------------------------------------------
1 | .template-preview-modal {
2 | padding: 20px;
3 | background-color: #f9f9f9;
4 | border-radius: 8px;
5 | }
6 | .template-preview-modal .mp-template-title{
7 | text-align: center;
8 | }
9 |
10 | /* ===== 预览区域 ===== */
11 | .tp-mp-preview-area {
12 | padding: 10px 20px 20px 20px;
13 | margin: 10px;
14 | height: calc(100% - 180px);
15 | overflow-y: auto;
16 | background: #fcfcfc;
17 | flex: 1;
18 | border-radius: 12px;
19 | box-shadow: 0 2px 12px rgba(0, 0, 0, 0.02);
20 | border: 1px solid rgba(82, 144, 220, 0.08);
21 | }
22 |
23 | /* 链接样式 */
24 | .tp-mp-content-section a {
25 | color: var(--text-accent);
26 | text-decoration: none;
27 | }
28 |
29 | /* 表格样式 */
30 | .tp-mp-content-section table {
31 | border-collapse: collapse;
32 | margin: 1em 0;
33 | width: 100%;
34 | }
35 |
36 | .tp-mp-content-section th,
37 | .tp-mp-content-section td {
38 | border: 1px solid var(--background-modifier-border);
39 | padding: 8px;
40 | }
41 |
42 | /* 分割线样式 */
43 | .tp-mp-content-section hr {
44 | border: none;
45 | border-top: 1px solid var(--background-modifier-border);
46 | margin: 20px 0;
47 | }
48 |
49 | /* 删除线样式 */
50 | .tp-mp-content-section del {
51 | text-decoration: line-through;
52 | }
53 |
54 | /* 任务列表样式 */
55 | .tp-mp-content-section .task-list-item {
56 | list-style: none;
57 | }
58 |
59 | .tp-mp-content-section .task-list-item input[type="checkbox"] {
60 | margin-right: 6px;
61 | }
62 |
63 | /* 脚注样式 */
64 | .tp-mp-content-section .footnote-ref,
65 | .tp-mp-content-section .footnote-backref {
66 | color: var(--text-accent);
67 | text-decoration: none;
68 | }
69 |
70 | /* 图片样式 */
71 | .tp-mp-content-section img {
72 | max-width: 100%;
73 | height: auto;
74 | display: block;
75 | margin: 1em auto;
76 | }
77 |
78 | /* 引用块样式 */
79 | .tp-mp-content-section blockquote p {
80 | margin: 0;
81 | padding: 0;
82 | line-height: inherit;
83 | }
84 |
--------------------------------------------------------------------------------
/src/styles/view/layout.css:
--------------------------------------------------------------------------------
1 | /* ===== 基础布局 ===== */
2 | .mp-view-content {
3 | height: 100%;
4 | padding: 10px 10px 0 10px;
5 | overflow: auto;
6 | }
7 |
8 | /* ===== 工具栏 ===== */
9 | .mp-toolbar, .mp-bottom-bar {
10 | padding: 15px 0;
11 | max-width: 580px;
12 | background: var(--background-secondary);
13 | }
14 |
15 | .mp-toolbar {
16 | top: 0;
17 | border-radius: 8px;
18 | border-bottom: 2px solid var(--background-modifier-border);
19 | }
20 |
21 | .mp-bottom-bar {
22 | bottom: 0;
23 | border-radius: 8px;
24 | border-top: 2px solid var(--background-modifier-border);
25 | }
26 |
27 | /* 控件组布局 */
28 | .mp-controls-group {
29 | display: flex;
30 | gap: 12px;
31 | align-items: center;
32 | width: 100%;
33 | justify-content: flex-start; /* 改为靠左对齐 */
34 | flex-wrap: nowrap;
35 | min-width: 0;
36 | }
37 |
38 | /* ===== 按钮基础样式 ===== */
39 | .mp-controls-group button {
40 | height: 36px;
41 | border-radius: 8px;
42 | border: 1px solid var(--background-modifier-border);
43 | background: var(--background-primary);
44 | color: var(--text-normal);
45 | font-size: 14px;
46 | box-shadow: 0 1px 3px var(--background-modifier-box-shadow);
47 | flex: 1;
48 | white-space: nowrap;
49 | overflow: hidden;
50 | }
51 |
52 | /* 按钮文本样式 */
53 | .mp-controls-group button span {
54 | overflow: hidden;
55 | text-overflow: ellipsis;
56 | white-space: nowrap;
57 | min-width: 0;
58 | }
59 |
60 | .mp-controls-group button:hover {
61 | background: var(--background-modifier-hover);
62 | transform: translateY(-1px);
63 | box-shadow: 0 4px 12px var(--background-modifier-box-shadow);
64 | }
65 |
66 | .mp-controls-group button:disabled {
67 | opacity: 0.5;
68 | background: var(--background-primary) !important;
69 | color: var(--text-muted) !important;
70 | cursor: not-allowed !important;
71 | transform: none;
72 | border-color: var(--background-modifier-border) !important;
73 | box-shadow: none;
74 | }
75 |
76 | /* 特殊按钮样式 */
77 | .mp-controls-group .mp-lock-button,
78 | .mp-controls-group .mp-help-button {
79 | width: 36px;
80 | padding: 0;
81 | margin: 0 20px 0 10px;
82 | flex: none ;
83 | }
84 |
85 | .mp-controls-group .mp-copy-button {
86 | background-color: var(--text-accent);
87 | color: var(--text-on-accent);
88 | border: none;
89 | }
90 | .mp-controls-group .mp-copy-button:hover {
91 | color: var(--text-on-accent);
92 | background-color: var(--text-accent);
93 | }
94 |
95 | /* ===== 下拉选择器 ===== */
96 | .custom-select-container {
97 | position: relative;
98 | min-width: 36px;
99 | max-width: 120px;
100 | flex: 1;
101 | }
102 |
103 | .custom-select {
104 | height: 36px;
105 | padding: 0 12px;
106 | border: 1px solid var(--background-modifier-border);
107 | border-radius: 8px;
108 | background: var(--background-primary);
109 | display: flex;
110 | align-items: center;
111 | justify-content: space-between;
112 | cursor: pointer;
113 | user-select: none;
114 | transition: all 0.2s ease;
115 | color: var(--text-normal);
116 | box-shadow: 0 1px 3px var(--background-modifier-box-shadow);
117 | white-space: nowrap;
118 | overflow: hidden;
119 | }
120 | .selected-text {
121 | overflow: hidden;
122 | text-overflow: ellipsis;
123 | white-space: nowrap;
124 | flex: 1;
125 | min-width: 0;
126 | }
127 | .custom-select:hover {
128 | background: var(--background-modifier-hover);
129 | box-shadow: 0 2px 6px var(--background-modifier-box-shadow);
130 | }
131 |
132 | .custom-select.disabled {
133 | opacity: 0.5;
134 | background: var(--background-secondary) !important;
135 | color: var(--text-muted) !important;
136 | border-color: var(--background-modifier-border) !important;
137 | cursor: not-allowed;
138 | box-shadow: none;
139 | }
140 |
141 | .select-arrow {
142 | color: var(--text-normal);
143 | font-size: 12px;
144 | transition: transform 0.2s ease;
145 | margin-left: 4px;
146 | flex-shrink: 0;
147 | }
148 |
149 | .select-dropdown {
150 | position: absolute;
151 | top: calc(100% + 4px);
152 | left: 0;
153 | width: 100%;
154 | background: var(--background-primary);
155 | border: 1px solid var(--background-modifier-border);
156 | border-radius: 8px;
157 | box-shadow: 0 4px 12px var(--background-modifier-box-shadow);
158 | display: none;
159 | z-index: 1000;
160 | overflow: hidden;
161 | }
162 |
163 | .select-dropdown.show {
164 | display: block;
165 | }
166 |
167 | .select-item {
168 | padding: 8px 12px;
169 | cursor: pointer;
170 | transition: all 0.2s ease;
171 | color: var(--text-normal);
172 | }
173 |
174 | .select-item:hover {
175 | background: var(--background-modifier-hover);
176 | }
177 |
178 | .select-item.selected {
179 | background: var(--background-modifier-hover);
180 | color: var(--text-accent);
181 | }
182 |
183 | /* ===== 字号调整组件 ===== */
184 | .mp-font-size-group {
185 | min-width: 36px;
186 | max-width: 120px;
187 | height: 36px;
188 | flex: 1;
189 | display: flex;
190 | align-items: center;
191 | justify-content: space-between;
192 |
193 | background: var(--background-primary);
194 | border: 1px solid var(--background-modifier-border);
195 | border-radius: 8px;
196 | padding: 0;
197 | overflow: hidden;
198 | box-shadow: 0 1px 3px var(--background-modifier-box-shadow);
199 | }
200 |
201 | .mp-font-size-input {
202 | width: 30px;
203 | text-align: center;
204 | }
205 |
206 | /* ===== 帮助提示 ===== */
207 | .mp-help-tooltip {
208 | position: absolute;
209 | left: 30px;
210 | bottom: 80px;
211 | width: 300px;
212 | padding: 12px 16px;
213 | background: var(--background-primary);
214 | border: 2px solid var(--background-modifier-border);
215 | border-radius: 8px;
216 | box-shadow: 0 4px 12px var(--background-modifier-box-shadow);
217 | font-size: 13px;
218 | line-height: 1.6;
219 | color: var(--text-normal);
220 | display: none;
221 | white-space: pre-line;
222 | z-index: 1001; /* 提高层级 */
223 | pointer-events: none; /* 防止tooltip影响鼠标事件 */
224 | }
225 |
226 | /* 修改触发方式 */
227 | .mp-controls-group .mp-help-button {
228 | position: relative; /* 添加相对定位 */
229 | }
230 |
231 | .mp-help-button:hover + .mp-help-tooltip {
232 | display: block;
233 | }
234 |
235 | /* ===== 关于作者弹窗 ===== */
236 | .mp-donate-overlay {
237 | position: fixed;
238 | top: 0;
239 | left: 0;
240 | width: 100%;
241 | height: 100%;
242 | background: var(--background-modifier-cover);
243 | display: flex;
244 | align-items: center;
245 | justify-content: center;
246 | z-index: 1000;
247 | }
248 |
249 | .mp-about-modal {
250 | background: var(--background-primary);
251 | border-radius: 12px;
252 | padding: 20px;
253 | width: 520px;
254 | max-height: 90vh;
255 | position: relative;
256 | box-shadow: 0 4px 10px var(--background-modifier-box-shadow);
257 | display: flex;
258 | flex-direction: column;
259 | gap: 10px;
260 | overflow-y: auto;
261 | }
262 |
263 | /* 滚动条样式 */
264 | .mp-about-modal::-webkit-scrollbar {
265 | width: 8px;
266 | }
267 |
268 | .mp-about-modal::-webkit-scrollbar-track {
269 | background: var(--scrollbar-track-background);
270 | border-radius: 4px;
271 | }
272 |
273 | .mp-about-modal::-webkit-scrollbar-thumb {
274 | background: var(--scrollbar-thumb-background);
275 | border-radius: 4px;
276 | }
277 |
278 | .mp-about-modal::-webkit-scrollbar-thumb:hover {
279 | background: var(--scrollbar-thumb-background-hover);
280 | }
281 |
282 | /* 关闭按钮 */
283 | .mp-donate-close {
284 | position: absolute;
285 | top: 7px;
286 | right: 7px;
287 | width: 28px;
288 | height: 28px;
289 | background: transparent;
290 | border: none;
291 | border-radius: 6px;
292 | font-size: 17px;
293 | color: var(--text-muted);
294 | cursor: pointer;
295 | display: flex;
296 | align-items: center;
297 | justify-content: center;
298 | transition: all 0.2s ease;
299 | }
300 |
301 | @media screen and (-webkit-min-device-pixel-ratio: 0) and (min-color-index:0) {
302 | .mp-donate-close {
303 | right: auto;
304 | left: 7px;
305 | }
306 | }
307 |
308 | .mp-donate-close:hover {
309 | background: var(--background-modifier-hover);
310 | color: var(--text-normal);
311 | }
312 |
313 | /* 弹窗内容区块 */
314 | .mp-about-section {
315 | padding: 10px;
316 | margin: 8px 20px 0 20px;
317 | border-radius: 8px;
318 | background: var(--background-secondary);
319 | border: 1px solid var(--background-modifier-border);
320 | }
321 |
322 | /* 文字样式 */
323 | .mp-about-title {
324 | font-size: 20px;
325 | font-weight: 600;
326 | color: var(--text-normal);
327 | margin: 0 0 1px;
328 | text-align: center;
329 | letter-spacing: 0.5px;
330 | }
331 |
332 | .mp-about-subtitle {
333 | font-size: 18px;
334 | color: var(--text-accent);
335 | margin: 8px 0 12px;
336 | letter-spacing: 0.3px;
337 | font-weight: 600;
338 | }
339 |
340 | .mp-about-intro {
341 | font-size: 15px;
342 | color: var(--text-normal);
343 | margin: 8px 0;
344 | line-height: 1.8;
345 | letter-spacing: 0.2px;
346 | }
347 |
348 | .mp-about-role,
349 | .mp-about-desc {
350 | font-size: 14.5px;
351 | color: var(--text-normal);
352 | margin: 6px 0;
353 | line-height: 1.8;
354 | letter-spacing: 0.2px;
355 | }
356 |
357 | .mp-about-name {
358 | color: var(--text-accent);
359 | font-weight: 600;
360 | font-size: 16px;
361 | padding: 0 2px;
362 | }
363 |
364 | .mp-about-identity {
365 | color: var(--text-normal);
366 | font-weight: 500;
367 | background: var(--background-modifier-hover);
368 | padding: 0 4px;
369 | border-radius: 3px;
370 | }
371 |
372 | .mp-about-highlight,
373 | .mp-about-value,
374 | .mp-about-emphasis {
375 | color: var(--text-accent);
376 | font-weight: 500;
377 | }
378 |
379 | .mp-about-footer {
380 | font-size: 15px;
381 | color: var(--text-normal);
382 | text-align: center;
383 | margin: 1px 0 0;
384 | padding: 1px;
385 | background: var(--background-secondary);
386 | border-radius: 8px;
387 | font-weight: 500;
388 | letter-spacing: 0.3px;
389 | line-height: 1.6;
390 | }
391 |
392 | .mp-about-footer strong {
393 | color: var(--text-accent);
394 | font-weight: 600;
395 | }
396 |
397 | /* 二维码容器 */
398 | .mp-about-qr {
399 | width: 100%;
400 | height: 150px;
401 | margin: 5px 0;
402 | padding: 5px;
403 | background: var(--background-primary);
404 | border-radius: 8px;
405 | display: flex;
406 | align-items: center;
407 | justify-content: center;
408 | box-shadow: 0 2px 8px var(--background-modifier-box-shadow);
409 | }
410 |
411 | .mp-about-qr img {
412 | width: 100%;
413 | height: 100%;
414 | object-fit: contain;
415 | }
--------------------------------------------------------------------------------
/src/styles/view/preview.css:
--------------------------------------------------------------------------------
1 | /* ===== 预览区域 ===== */
2 | .mp-preview-area {
3 | padding: 10px 20px 20px 20px;
4 | margin: 10px;
5 | height: calc(100% - 180px);
6 | overflow-y: auto;
7 | background: #fcfcfc;
8 | flex: 1;
9 | border-radius: 12px;
10 | box-shadow: 0 2px 12px rgba(0, 0, 0, 0.02);
11 | border: 1px solid rgba(82, 144, 220, 0.08);
12 | }
13 |
14 | /* 段落 */
15 | .mp-content-section p {
16 | margin: 0;
17 | padding: 0;
18 | }
19 |
20 | /* 列表 */
21 | .mp-content-section ul,ol {
22 | margin: 0;
23 | padding: 0;
24 | }
25 |
26 | /* 链接样式 */
27 | .mp-content-section a {
28 | color: var(--text-accent);
29 | text-decoration: none;
30 | }
31 |
32 | /* 表格样式 */
33 | .mp-content-section table {
34 | border-collapse: collapse;
35 | margin: 1em 0;
36 | width: 100%;
37 | }
38 |
39 | .mp-content-section th,
40 | .mp-content-section td {
41 | border: 1px solid var(--background-modifier-border);
42 | padding: 8px;
43 | }
44 |
45 | /* 分割线样式 */
46 | .mp-content-section hr {
47 | border: none;
48 | border-top: 1px solid var(--background-modifier-border);
49 | margin: 20px 0;
50 | }
51 |
52 | /* 删除线样式 */
53 | .mp-content-section del {
54 | text-decoration: line-through;
55 | }
56 |
57 | /* 任务列表样式 */
58 | .mp-content-section .task-list-item {
59 | list-style: none;
60 | }
61 |
62 | .mp-content-section .task-list-item input[type="checkbox"] {
63 | margin-right: 6px;
64 | }
65 |
66 | /* 脚注样式 */
67 | .mp-content-section .footnote-ref,
68 | .mp-content-section .footnote-backref {
69 | color: var(--text-accent);
70 | text-decoration: none;
71 | }
72 |
73 | /* 图片样式 */
74 | .mp-content-section img {
75 | max-width: 100%;
76 | height: auto;
77 | display: block;
78 | margin: 1em auto;
79 | }
80 |
81 | /* 引用块样式 */
82 | .mp-content-section blockquote p {
83 | margin: 0;
84 | padding: 0;
85 | line-height: inherit;
86 | }
87 |
88 |
89 |
90 | .mp-empty-message {
91 | display: flex;
92 | justify-content: center;
93 | align-items: center;
94 | height: 100%;
95 | color: var(--text-muted);
96 | font-size: 16px;
97 | }
98 |
99 |
--------------------------------------------------------------------------------
/src/templateManager.ts:
--------------------------------------------------------------------------------
1 | import { App } from 'obsidian';
2 | import { SettingsManager } from './settings/settings';
3 |
4 | export interface Template {
5 | id: string;
6 | name: string;
7 | description: string;
8 | isPreset?: boolean;
9 | isVisible?: boolean;
10 | styles: {
11 | container: string;
12 | title: {
13 | h1: {
14 | base: string;
15 | content: string;
16 | after: string;
17 | };
18 | h2: {
19 | base: string;
20 | content: string;
21 | after: string;
22 | };
23 | h3: {
24 | base: string;
25 | content: string;
26 | after: string;
27 | };
28 | base: {
29 | base: string;
30 | content: string;
31 | after: string;
32 | };
33 | };
34 | paragraph: string;
35 | list: {
36 | container: string;
37 | item: string;
38 | taskList: string;
39 | };
40 | quote: string;
41 | code: {
42 | header: {
43 | container: string;
44 | dot: string;
45 | colors: [string, string, string];
46 | };
47 | block: string;
48 | inline: string;
49 | };
50 | image: string;
51 | link: string;
52 | emphasis: {
53 | strong: string;
54 | em: string;
55 | del: string;
56 | };
57 | table: {
58 | container: string;
59 | header: string;
60 | cell: string;
61 | };
62 | hr: string;
63 | footnote: {
64 | ref: string;
65 | backref: string;
66 | };
67 | };
68 | }
69 |
70 | export class TemplateManager {
71 | private templates: Map = new Map();
72 | private currentTemplate: Template;
73 | private currentFont: string = '-apple-system';
74 | private currentFontSize: number = 16;
75 | private app: App;
76 | private settingsManager: SettingsManager;
77 |
78 | constructor(app: App, settingsManager: SettingsManager) {
79 | this.app = app;
80 | this.settingsManager = settingsManager;
81 | }
82 |
83 | public setCurrentTemplate(id: string): boolean {
84 | const template = this.settingsManager.getTemplate(id);
85 | if (template) {
86 | this.currentTemplate = template;
87 | return true;
88 | }
89 | console.error('主题未找到:', id);
90 | return false;
91 | }
92 |
93 | public setFont(fontFamily: string) {
94 | this.currentFont = fontFamily;
95 | }
96 |
97 | public setFontSize(size: number) {
98 | this.currentFontSize = size;
99 | }
100 |
101 | public applyTemplate(element: HTMLElement, template?: Template): void {
102 | const styles = template ? template.styles : this.currentTemplate.styles;
103 | // 应用标题样式
104 | ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].forEach(tag => {
105 | element.querySelectorAll(tag).forEach(el => {
106 | // 检查是否已经处理过
107 | if (!el.querySelector('.content')) {
108 | const content = document.createElement('span');
109 | content.className = 'content';
110 | // 使用 textContent 替代 innerHTML
111 | while (el.firstChild) {
112 | content.appendChild(el.firstChild);
113 | }
114 | el.textContent = '';
115 | el.appendChild(content);
116 |
117 | const after = document.createElement('span');
118 | after.className = 'after';
119 | el.appendChild(after);
120 | }
121 |
122 | // 根据标签选择对应的样式
123 | const styleKey = (tag === 'h4' || tag === 'h5' || tag === 'h6' ? 'base' : tag) as keyof typeof styles.title;
124 | const titleStyle = styles.title[styleKey];
125 |
126 | // 应用样式
127 | el.setAttribute('style', `${titleStyle.base}; font-family: ${this.currentFont};`);
128 | el.querySelector('.content')?.setAttribute('style', titleStyle.content);
129 | el.querySelector('.after')?.setAttribute('style', titleStyle.after);
130 | });
131 | });
132 |
133 | // 应用段落样式
134 | element.querySelectorAll('p').forEach(el => {
135 | if (!el.parentElement?.closest('p') && !el.parentElement?.closest('blockquote')) {
136 | el.setAttribute('style', `${styles.paragraph}; font-family: ${this.currentFont}; font-size: ${this.currentFontSize}px;`);
137 | }
138 | });
139 |
140 | // 应用列表样式
141 | element.querySelectorAll('ul, ol').forEach(el => {
142 | el.setAttribute('style', styles.list.container);
143 | });
144 | element.querySelectorAll('li').forEach(el => {
145 | el.setAttribute('style', `${styles.list.item}; font-family: ${this.currentFont}; font-size: ${this.currentFontSize}px;`);
146 | });
147 | element.querySelectorAll('.task-list-item').forEach(el => {
148 | el.setAttribute('style', `${styles.list.taskList}; font-family: ${this.currentFont}; font-size: ${this.currentFontSize}px;`);
149 | });
150 |
151 | // 应用引用样式
152 | element.querySelectorAll('blockquote').forEach(el => {
153 | el.setAttribute('style', `${styles.quote}; font-family: ${this.currentFont}; font-size: ${this.currentFontSize}px;`);
154 | });
155 |
156 | // 应用代码样式
157 | element.querySelectorAll('pre').forEach(el => {
158 | // 应用基础代码块样式
159 | el.setAttribute('style', styles.code.block);
160 |
161 | // 设置代码块头部样式
162 | const header = el.querySelector('.mp-code-header');
163 | if (header) {
164 | header.setAttribute('style', styles.code.header.container);
165 | // 设置窗口按钮样式
166 | header.querySelectorAll('.mp-code-dot').forEach((dot, index) => {
167 | dot.setAttribute('style', `${styles.code.header.dot}; background-color: ${styles.code.header.colors[index]};`);
168 | });
169 | }
170 | });
171 |
172 | // 应用内联代码样式
173 | element.querySelectorAll('code:not(pre code)').forEach(el => {
174 | el.setAttribute('style', styles.code.inline);
175 | });
176 |
177 | // 应用链接样式
178 | element.querySelectorAll('a').forEach(el => {
179 | el.setAttribute('style', styles.link);
180 | });
181 |
182 | // 应用强调样式
183 | element.querySelectorAll('strong').forEach(el => {
184 | el.setAttribute('style', styles.emphasis.strong);
185 | });
186 | element.querySelectorAll('em').forEach(el => {
187 | el.setAttribute('style', styles.emphasis.em);
188 | });
189 | element.querySelectorAll('del').forEach(el => {
190 | el.setAttribute('style', styles.emphasis.del);
191 | });
192 |
193 | // 应用表格样式(内容表格,非包裹表格)
194 | element.querySelectorAll('table').forEach(el => {
195 | el.setAttribute('style', styles.table.container);
196 | });
197 | element.querySelectorAll('th').forEach(el => {
198 | el.setAttribute('style', `${styles.table.header}; font-family: ${this.currentFont}; font-size: ${this.currentFontSize}px;`);
199 | });
200 | element.querySelectorAll('td').forEach(el => {
201 | el.setAttribute('style', `${styles.table.cell}; font-family: ${this.currentFont}; font-size: ${this.currentFontSize}px;`);
202 | });
203 |
204 | // 应用分割线样式
205 | element.querySelectorAll('hr').forEach(el => {
206 | el.setAttribute('style', styles.hr);
207 | });
208 |
209 | // 应用脚注样式
210 | element.querySelectorAll('.footnote-ref').forEach(el => {
211 | el.setAttribute('style', styles.footnote.ref);
212 | });
213 | element.querySelectorAll('.footnote-backref').forEach(el => {
214 | el.setAttribute('style', styles.footnote.backref);
215 | });
216 |
217 | // 应用图片样式
218 | element.querySelectorAll('img').forEach(el => {
219 | const img = el as HTMLImageElement;
220 | el.setAttribute('style', styles.image);
221 | });
222 | }
223 | }
224 |
225 | export const templateManager = (app: App, settingsManager: SettingsManager) => new TemplateManager(app, settingsManager);
226 |
--------------------------------------------------------------------------------
/src/templates/academic.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "academic",
3 | "name": "学术主题",
4 | "styles": {
5 | "container": "",
6 | "title": {
7 | "h1": {
8 | "base": "margin: 32px 0 0; font-size: 2em; letter-spacing: -0.03em; line-height: 1.5;",
9 | "content": "font-weight: bold; color: #5D4037;",
10 | "after": ""
11 | },
12 | "h2": {
13 | "base": "margin: 28px 0 0; font-size: 1.5em; letter-spacing: -0.02em; border-left: 4px solid #8D6E63; padding-left: 12px; line-height: 1.5;",
14 | "content": "font-weight: bold; color: #6D4C41;",
15 | "after": ""
16 | },
17 | "h3": {
18 | "base": "margin: 24px 0 0; font-size: 1.25em; letter-spacing: -0.01em; line-height: 1.5;",
19 | "content": "font-weight: bold; color: #795548;",
20 | "after": ""
21 | },
22 | "base": {
23 | "base": "margin: 20px 0 0; font-size: 1em; line-height: 1.5;",
24 | "content": "font-weight: bold; color: #8D6E63;",
25 | "after": ""
26 | }
27 | },
28 | "paragraph": "line-height: 1.8; margin-top: 1em; font-size: 1em; color: #4a4a4a;",
29 | "link": "color: #795548; text-decoration: none; border-bottom: 1px solid #8D6E63; transition: all 0.2s ease;",
30 | "emphasis": {
31 | "strong": "font-weight: 600; color: #4a4a4a;",
32 | "em": "font-style: italic; color: #4a4a4a;",
33 | "del": "text-decoration: line-through; color: #4a4a4a;"
34 | },
35 | "list": {
36 | "container": "padding-left: 32px; color: #4a4a4a;",
37 | "item": "font-size: 1em; color: #4a4a4a; line-height: 1.8;",
38 | "taskList": "list-style: none; font-size: 1em; color: #4a4a4a; line-height: 1.8;"
39 | },
40 | "code": {
41 | "header": {
42 | "container": "margin-bottom: 1em; display: flex; gap: 6px;",
43 | "dot": "width: 12px; height: 12px; border-radius: 50%;",
44 | "colors": [
45 | "#ff5f56",
46 | "#ffbd2e",
47 | "#27c93f"
48 | ]
49 | },
50 | "block": "color: #5D4037; background: #EFEBE9; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin: 1.2em 0; padding: 1em 1em 1em; font-size: 14px; line-height: 1.6; white-space: pre-wrap;",
51 | "inline": "background: #EFEBE9; padding: 2px 6px; border-radius: 4px; color: #5D4037; font-size: 14px; border: 1px solid #8D6E63;"
52 | },
53 | "quote": "border-left: 4px solid #8D6E63; border-radius: 6px; padding: 10px 10px; background: #EFEBE9; margin: 0.8em 0; color: #5D4037; font-style: italic; word-wrap: break-word;",
54 | "image": "max-width: 100%; height: auto; margin: 1em auto; display: block;",
55 | "table": {
56 | "container": "width: 100%; margin: 1em 0; border-collapse: collapse; border: 1px solid #e1e4e8;",
57 | "header": "background: #f6f8fa; font-weight: bold; color: #4a4a4a; border-bottom: 2px solid #e1e4e8; font-size: 1em;",
58 | "cell": "border: 1px solid #f0f0f0; padding: 8px; color: #4a4a4a; font-size: 1em;"
59 | },
60 | "hr": "border: none; border-top: 1px solid #f0f0f0; margin: 20px 0;",
61 | "footnote": {
62 | "ref": "color: #e0e0e0; text-decoration: none; font-size: 0.9em;",
63 | "backref": "color: #e0e0e0; text-decoration: none; font-size: 0.9em;"
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/src/templates/brown.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "brown",
3 | "name": "悦灵雅棕",
4 | "styles": {
5 | "container": "",
6 | "title": {
7 | "h1": {
8 | "base": "margin: 32px 0 0; font-size: 2em; letter-spacing: -0.02em; line-height: 1.5;",
9 | "content": "font-weight: bold; color: #4a4a4a;",
10 | "after": ""
11 | },
12 | "h2": {
13 | "base": "margin: 28px 0 24px 0; font-size: 1.5em; letter-spacing: -0.01em; line-height: 1.5;",
14 | "content": "font-weight: bold; color: #4a4a4a;",
15 | "after": ""
16 | },
17 | "h3": {
18 | "base": "margin: 24px 0 20px 0; font-size: 1.25em; letter-spacing: -0.01em; border-left: 4px solid #4a4a4a; padding-left: 12px; line-height: 1.5;",
19 | "content": "font-weight: bold; color: #4a4a4a;",
20 | "after": ""
21 | },
22 | "base": {
23 | "base": "margin: 20px 0 0; font-size: 1em; line-height: 1.5;",
24 | "content": "font-weight: bold; color: #4a4a4a;",
25 | "after": ""
26 | }
27 | },
28 | "paragraph": "line-height: 1.8; margin-top: 1em; font-size: 1em; color: #4a4a4a;",
29 | "list": {
30 | "container": "padding-left: 32px; color: #4a4a4a;",
31 | "item": "font-size: 1em; color: #4a4a4a; line-height: 1.8;",
32 | "taskList": "list-style: none; font-size: 1em; color: #4a4a4a; line-height: 1.8;"
33 | },
34 | "code": {
35 | "header": {
36 | "container": "margin-bottom: 1em; display: flex; gap: 6px;",
37 | "dot": "width: 12px; height: 12px; border-radius: 50%;",
38 | "colors": [
39 | "#ff5f56",
40 | "#ffbd2e",
41 | "#27c93f"
42 | ]
43 | },
44 | "block": "color: #333; background: #fef7ed; border-radius: 8px; border: 1px solid #f5e2c7; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin: 1.2em 0; padding: 1em 1em 1em; font-size: 14px; line-height: 1.6; white-space: pre-wrap;",
45 | "inline": "background: #fef7ed; padding: 2px 6px; border-radius: 4px; color: #333; font-size: 14px; border: 1px solid #f5e2c7;"
46 | },
47 | "quote": "border-left: 4px solid #c57512; border-radius: 6px; padding: 10px 10px; background: #fef7ed; margin: 0.8em 0; color: #8b6d4d; font-style: italic; word-wrap: break-word;",
48 | "image": "max-width: 100%; height: auto; margin: 1em auto; display: block;",
49 | "link": "color: #c57512; text-decoration: none; border-bottom: 1px solid #c57512;",
50 | "emphasis": {
51 | "strong": "font-weight: bold; color: #c57512;",
52 | "em": "font-style: italic; color: #c57512;",
53 | "del": "text-decoration: line-through; color: #c57512;"
54 | },
55 | "table": {
56 | "container": "width: 100%; margin: 1em 0; border-collapse: collapse; border: 1px solid #f5e2c7;",
57 | "header": "background: #fef7ed; font-weight: bold; color: #c57512; border-bottom: 2px solid #f5e2c7; font-size: 1em;",
58 | "cell": "border: 1px solid #f5e2c7; padding: 8px; color: #4a4a4a; font-size: 1em;"
59 | },
60 | "hr": "border: none; border-top: 1px solid #f5e2c7; margin: 20px 0;",
61 | "footnote": {
62 | "ref": "color: #c57512; text-decoration: none; font-size: 0.9em;",
63 | "backref": "color: #c57512; text-decoration: none; font-size: 0.9em;"
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/src/templates/dark.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "dark",
3 | "name": "深色主题",
4 | "styles": {
5 | "container": "",
6 | "title": {
7 | "h1": {
8 | "base": "margin: 32px 0 0; font-size: 2em; letter-spacing: -0.03em; line-height: 1.5;",
9 | "content": "font-weight: bold; color: #1E90FF;",
10 | "after": ""
11 | },
12 | "h2": {
13 | "base": "margin: 28px 0 0; font-size: 1.5em; letter-spacing: -0.02em; border-left: 4px solid #1E90FF; padding-left: 12px; line-height: 1.5;",
14 | "content": "font-weight: bold; color: #3B9DFF;",
15 | "after": ""
16 | },
17 | "h3": {
18 | "base": "margin: 24px 0 0; font-size: 1.25em; letter-spacing: -0.01em; line-height: 1.5;",
19 | "content": "font-weight: bold; color: #57A9FF;",
20 | "after": ""
21 | },
22 | "base": {
23 | "base": "margin: 20px 0 0; font-size: 1em; line-height: 1.5;",
24 | "content": "font-weight: bold; color: #74B6FF;",
25 | "after": ""
26 | }
27 | },
28 | "paragraph": "line-height: 1.8; margin-top: 1em; font-size: 1em; color: #4a4a4a;",
29 | "list": {
30 | "container": "padding-left: 32px; color: #4a4a4a;",
31 | "item": " font-size: 1em; color: #4a4a4a; line-height: 1.8;",
32 | "taskList": "list-style: none; font-size: 1em; color: #4a4a4a; line-height: 1.8;"
33 | },
34 | "code": {
35 | "header": {
36 | "container": "margin-bottom: 1em; display: flex; gap: 6px;",
37 | "dot": "width: 12px; height: 12px; border-radius: 50%;",
38 | "colors": [
39 | "#ff5f56",
40 | "#ffbd2e",
41 | "#27c93f"
42 | ]
43 | },
44 | "block": "color: #333; background: #F8FBFF; border-radius: 8px; border: 1px solid #E6F0FF; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin: 1.2em 0; padding: 1em 1em 1em; font-size: 14px; line-height: 1.6; white-space: pre-wrap;",
45 | "inline": "background: #F8FBFF; padding: 2px 6px; border-radius: 4px; color: #333; font-size: 14px; border: 1px solid #E6F0FF;"
46 | },
47 | "quote": "border-left: 4px solid #1E90FF; border-radius: 6px; padding: 10px 10px; background: #F5F9FF; margin: 0.8em 0; color: #6a737d; font-style: italic; word-wrap: break-word;",
48 | "image": "max-width: 100%; height: auto; margin: 1em auto; display: block;",
49 | "link": "color: #1E90FF; text-decoration: none; border-bottom: 1px solid #1E90FF; transition: all 0.2s ease;",
50 | "emphasis": {
51 | "strong": "font-weight: bold; color: #4a4a4a;",
52 | "em": "font-style: italic; color: #4a4a4a;",
53 | "del": "text-decoration: line-through; color: #4a4a4a;"
54 | },
55 | "table": {
56 | "container": "width: 100%; margin: 1em 0; border-collapse: collapse; border: 1px solid #E6F0FF;",
57 | "header": "background: #F8FBFF; font-weight: bold; color: #4a4a4a; border-bottom: 2px solid #E6F0FF; font-size: 1em;",
58 | "cell": "border: 1px solid #f0f0f0; padding: 8px; color: #4a4a4a; font-size: 1em;"
59 | },
60 | "hr": "border: none; border-top: 1px solid #E6F0FF; margin: 20px 0;",
61 | "footnote": {
62 | "ref": "color: #e0e0e0; text-decoration: none; font-size: 0.9em;",
63 | "backref": "color: #e0e0e0; text-decoration: none; font-size: 0.9em;"
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/src/templates/darkgreen.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "darkgreen",
3 | "name": "墨绿主题",
4 | "styles": {
5 | "container": "",
6 | "title": {
7 | "h1": {
8 | "base": "margin: 32px 0 0; font-size: 2em; letter-spacing: -0.03em; line-height: 1.5; text-align: center;",
9 | "content": "font-weight: bold; color: #2C5530; display: inline-block; border-bottom: 1px solid #2C5530;",
10 | "after": ""
11 | },
12 | "h2": {
13 | "base": "margin: 28px 0 0; font-size: 1.5em; letter-spacing: -0.02em; line-height: 1.5; border-bottom: 1px solid rgba(46,139,87,0.2);",
14 | "content": "font-weight: bold; color: #ffffff; background: #2E8B57; padding: 1px 4px; border-radius: 3px;",
15 | "after": ""
16 | },
17 | "h3": {
18 | "base": "margin: 24px 0 0; font-size: 1.25em; letter-spacing: -0.01em; line-height: 1.5;",
19 | "content": "font-weight: bold; color: #3CB371; padding: 1px 1px;",
20 | "after": ""
21 | },
22 | "base": {
23 | "base": "margin: 20px 0 0; font-size: 1em;",
24 | "content": "font-weight: bold; color: #66CDAA;",
25 | "after": ""
26 | }
27 | },
28 | "paragraph": "line-height: 1.8; margin-top: 1em; font-size: 1em; color: #4a4a4a;",
29 | "list": {
30 | "container": "padding-left: 32px; color: #4a4a4a;",
31 | "item": " font-size: 1em; color: #4a4a4a; line-height: 1.8;",
32 | "taskList": "list-style: none; font-size: 1em; color: #4a4a4a; line-height: 1.8;"
33 | },
34 | "code": {
35 | "header": {
36 | "container": "margin-bottom: 1em; display: flex; gap: 6px;",
37 | "dot": "width: 12px; height: 12px; border-radius: 50%;",
38 | "colors": [
39 | "#ff5f56",
40 | "#ffbd2e",
41 | "#27c93f"
42 | ]
43 | },
44 | "block": "color: #333; background: #f4faf7; border-radius: 8px; border: 1px solid #e6f3ed; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin: 1.2em 0; padding: 1em 1em 1em; font-size: 14px; line-height: 1.6; white-space: pre-wrap;",
45 | "inline": "background: #f4faf7; padding: 2px 6px; border-radius: 4px; color: #333; font-size: 14px; border: 1px solid #e6f3ed;"
46 | },
47 | "quote": "border-left: 4px solid #e6f3ed; border-radius: 6px; padding: 10px 10px; background: #f4faf7; margin: 0.8em 0; color: #159461; font-style: italic; word-wrap: break-word;",
48 | "image": "max-width: 100%; height: auto; margin: 1em auto; display: block;",
49 | "link": "color: #159461; text-decoration: none; border-bottom: 1px solid #1a9d6a; transition: all 0.2s ease;",
50 | "emphasis": {
51 | "strong": "font-weight: bold; color: #4a4a4a;",
52 | "em": "font-style: italic; color: #4a4a4a;",
53 | "del": "text-decoration: line-through; color: #4a4a4a;"
54 | },
55 | "table": {
56 | "container": "width: 100%; margin: 1em 0; border-collapse: collapse; border: 1px solid #e6f3ed;",
57 | "header": "background: #f4faf7; font-weight: bold; color: #4a4a4a; border-bottom: 2px solid #e6f3ed; font-size: 1em;",
58 | "cell": "border: 1px solid #f0f0f0; padding: 8px; color: #4a4a4a; font-size: 1em;"
59 | },
60 | "hr": "border: none; border-top: 1px solid #e6f3ed; margin: 20px 0;",
61 | "footnote": {
62 | "ref": "color: #e0e0e0; text-decoration: none; font-size: 0.9em;",
63 | "backref": "color: #e0e0e0; text-decoration: none; font-size: 0.9em;"
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/src/templates/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "default",
3 | "name": "默认模板",
4 | "styles": {
5 | "container": "",
6 | "title": {
7 | "h1": {
8 | "base": "margin: 32px 0 0; font-size: 2em; letter-spacing: -0.02em; line-height: 1.5;",
9 | "content": "font-weight: bold; color: #2c3e50;",
10 | "after": ""
11 | },
12 | "h2": {
13 | "base": "margin: 28px 0 0; font-size: 1.5em; letter-spacing: -0.01em; line-height: 1.5;",
14 | "content": "font-weight: bold; color: #34495e;",
15 | "after": ""
16 | },
17 | "h3": {
18 | "base": "margin: 24px 0 0; font-size: 1.25em; line-height: 1.5;",
19 | "content": "font-weight: bold; color: #3d566e;",
20 | "after": ""
21 | },
22 | "base": {
23 | "base": "margin: 20px 0 0; font-size: 1em; line-height: 1.5;",
24 | "content": "font-weight: bold; color: #47637f;",
25 | "after": ""
26 | }
27 | },
28 | "paragraph": "line-height: 1.8; margin-top: 1em; font-size: 1em; color: #4a4a4a;",
29 | "list": {
30 | "container": "padding-left: 32px; color: #4a4a4a;",
31 | "item": " font-size: 1em; color: #4a4a4a; line-height: 1.8;",
32 | "taskList": "list-style: none; font-size: 1em; color: #4a4a4a; line-height: 1.8;"
33 | },
34 | "code": {
35 | "header": {
36 | "container": "margin-bottom: 1em; display: flex; gap: 6px;",
37 | "dot": "width: 12px; height: 12px; border-radius: 50%;",
38 | "colors": [
39 | "#ff5f56",
40 | "#ffbd2e",
41 | "#27c93f"
42 | ]
43 | },
44 | "block": "color: #333; background: #f8f8f8; border-radius: 8px; border: 1px solid #eee; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin: 1.2em 0; padding: 1em 1em 1em; font-size: 14px; line-height: 1.6; white-space: pre-wrap;",
45 | "inline": "background: #f8f8f8; padding: 2px 6px; border-radius: 4px; color: #333; font-size: 14px; border: 1px solid #eee;"
46 | },
47 | "quote": "border-left: 4px solid #e0e0e0; border-radius: 6px; padding: 10px 10px; background: #f6f8fa; margin: 0.8em 0; color: #6a737d; font-style: italic; word-wrap: break-word;",
48 | "image": "max-width: 100%; height: auto; margin: 1em auto; display: block;",
49 | "link": "color: #e0e0e0; text-decoration: none; border-bottom: 1px solid #e0e0e0;",
50 | "emphasis": {
51 | "strong": "font-weight: bold; color: #4a4a4a;",
52 | "em": "font-style: italic; color: #4a4a4a;",
53 | "del": "text-decoration: line-through; color: #4a4a4a;"
54 | },
55 | "table": {
56 | "container": "width: 100%; margin: 1em 0; border-collapse: collapse; border: 1px solid #e1e4e8;",
57 | "header": "background: #f6f8fa; font-weight: bold; color: #4a4a4a; border-bottom: 2px solid #e1e4e8; font-size: 1em;",
58 | "cell": "border: 1px solid #f0f0f0; padding: 8px; color: #4a4a4a; font-size: 1em;"
59 | },
60 | "hr": "border: none; border-top: 1px solid #f0f0f0; margin: 20px 0;",
61 | "footnote": {
62 | "ref": "color: #e0e0e0; text-decoration: none; font-size: 0.9em;",
63 | "backref": "color: #e0e0e0; text-decoration: none; font-size: 0.9em;"
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/src/templates/elegant.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "elegant",
3 | "name": "优雅主题",
4 | "styles": {
5 | "container": "",
6 | "title": {
7 | "h1": {
8 | "base": "margin: 32px 0 0; font-size: 2em; letter-spacing: -0.03em; line-height: 1.5;",
9 | "content": "font-weight: bold; color: #553C9A;",
10 | "after": ""
11 | },
12 | "h2": {
13 | "base": "margin: 28px 0 0; font-size: 1.5em; letter-spacing: -0.02em; border-bottom: 2px solid #9F7AEA; line-height: 1.2;",
14 | "content": "display: inline-block; font-weight: bold; background: #9F7AEA; color: #ffffff; padding: 1px 4px; border-top-right-radius: 3px; border-top-left-radius: 3px; margin-right: 3px;",
15 | "after": "display: inline-block; content: ' '; vertical-align: bottom; border-bottom: 28px solid #f5f3ff; border-right: 20px solid transparent;"
16 | },
17 | "h3": {
18 | "base": "margin: 24px 0 0; font-size: 1.25em; letter-spacing: -0.01em; line-height: 1.5;",
19 | "content": "font-weight: bold; color: #805AD5;",
20 | "after": ""
21 | },
22 | "base": {
23 | "base": "margin: 20px 0 14px; font-size: 1em; line-height: 1.5;",
24 | "content": "font-weight: bold; color: #805AD5;",
25 | "after": ""
26 | }
27 | },
28 | "paragraph": "line-height: 1.8; margin-top: 1em; font-size: 1em; color: #4a4a4a;",
29 | "list": {
30 | "container": "padding-left: 32px; color: #4a4a4a;",
31 | "item": " font-size: 1em; color: #4a4a4a; line-height: 1.8;",
32 | "taskList": "list-style: none; font-size: 1em; color: #4a4a4a; line-height: 1.8;"
33 | },
34 | "code": {
35 | "header": {
36 | "container": "margin-bottom: 1em; display: flex; gap: 6px;",
37 | "dot": "width: 12px; height: 12px; border-radius: 50%;",
38 | "colors": [
39 | "#ff5f56",
40 | "#ffbd2e",
41 | "#27c93f"
42 | ]
43 | },
44 | "block": "color: #333; background: #f8f8f8; border-radius: 8px; border: 1px solid #eee; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin: 1.2em 0; padding: 1em 1em 1em; font-size: 14px; line-height: 1.6; white-space: pre-wrap;",
45 | "inline": "background: #f8f8f8; padding: 2px 6px; border-radius: 4px; color: #333; font-size: 14px; border: 1px solid #eee;"
46 | },
47 | "quote": "border-left: 4px solid #9F7AEA; border-radius: 6px; padding: 10px 10px; background: #f5f3ff; margin: 0.8em 0; color: #6B46C1; font-style: italic; word-wrap: break-word;",
48 | "image": "max-width: 100%; height: auto; margin: 1em auto; display: block;",
49 | "link": "color: #805AD5; text-decoration: none; border-bottom: 1px solid #9F7AEA; transition: all 0.2s ease;",
50 | "emphasis": {
51 | "strong": "font-weight: bold; color: #4a4a4a;",
52 | "em": "font-style: italic; color: #4a4a4a;",
53 | "del": "text-decoration: line-through; color: #4a4a4a;"
54 | },
55 | "table": {
56 | "container": "width: 100%; margin: 1em 0; border-collapse: collapse; border: 1px solid #e1e4e8;",
57 | "header": "background: #f6f8fa; font-weight: bold; color: #4a4a4a; border-bottom: 2px solid #e1e4e8; font-size: 1em;",
58 | "cell": "border: 1px solid #f0f0f0; padding: 8px; color: #4a4a4a; font-size: 1em;"
59 | },
60 | "hr": "border: none; border-top: 1px solid #f0f0f0; margin: 20px 0;",
61 | "footnote": {
62 | "ref": "color: #e0e0e0; text-decoration: none; font-size: 0.9em;",
63 | "backref": "color: #e0e0e0; text-decoration: none; font-size: 0.9em;"
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/src/templates/index.ts:
--------------------------------------------------------------------------------
1 | // 使用 require 导入 JSON 文件以避免 TypeScript 的 JSON 模块解析问题
2 | const defaultTemplate = require('./default.json');
3 | const minimalTemplate = require('./minimal.json');
4 | const scarletTemplate = require('./scarlet.json');
5 | const orangeTemplate = require('./orange.json');
6 | const elegantTemplate = require('./elegant.json');
7 | const darkTemplate = require('./dark.json');
8 | const academicTemplate = require('./academic.json');
9 | const yebanTemplate = require('./yeban.json');
10 | const yebanOrangeTemplate = require('./yeban-orange.json');
11 | const darkgreenTemplate = require('./darkgreen.json');
12 | const brownTemplate = require('./brown.json');
13 |
14 | export const templates = {
15 | default: defaultTemplate,
16 | minimal: minimalTemplate,
17 | scarlet: scarletTemplate,
18 | orange: orangeTemplate,
19 | elegant: elegantTemplate,
20 | dark: darkTemplate,
21 | academic: academicTemplate,
22 | yeban: yebanTemplate,
23 | 'yeban-orange': yebanOrangeTemplate,
24 | darkgreen: darkgreenTemplate,
25 | brown: brownTemplate,
26 | };
--------------------------------------------------------------------------------
/src/templates/minimal.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "minimal",
3 | "name": "极简主题",
4 | "styles": {
5 | "container": "",
6 | "title": {
7 | "h1": {
8 | "base": "margin: 32px 0 0; font-size: 2em; letter-spacing: -0.02em; line-height: 1.5; border-bottom: 1px solid rgba(0,0,0,0.1);",
9 | "content": "font-weight: bold; color: #000000;",
10 | "after": ""
11 | },
12 | "h2": {
13 | "base": "margin: 28px 0 0; font-size: 1.5em; letter-spacing: -0.01em; line-height: 1.5;",
14 | "content": "font-weight: bold; color: #262626;",
15 | "after": ""
16 | },
17 | "h3": {
18 | "base": "margin: 24px 0 0; font-size: 1.25em; line-height: 1.5;",
19 | "content": "font-weight: bold; color: #404040;",
20 | "after": ""
21 | },
22 | "base": {
23 | "base": "margin: 20px 0 0; font-size: 1em; line-height: 1.5;",
24 | "content": "font-weight: bold; color: #595959;",
25 | "after": ""
26 | }
27 | },
28 | "paragraph": "line-height: 1.8; margin-top: 1em; font-size: 1em; color: #4a4a4a;",
29 | "list": {
30 | "container": "padding-left: 32px; color: #4a4a4a;",
31 | "item": "font-size: 1em; color: #4a4a4a; line-height: 1.8;",
32 | "taskList": "list-style: none; font-size: 1em; color: #4a4a4a; line-height: 1.8;"
33 | },
34 | "code": {
35 | "header": {
36 | "container": "margin-bottom: 1em; display: flex; gap: 6px;",
37 | "dot": "width: 12px; height: 12px; border-radius: 50%;",
38 | "colors": [
39 | "#ff5f56",
40 | "#ffbd2e",
41 | "#27c93f"
42 | ]
43 | },
44 | "block": "color: #333; background: #fafafa; border-radius: 8px; border: 1px solid #eee; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin: 1.2em 0; padding: 1em 1em 1em; font-size: 14px; line-height: 1.6; white-space: pre-wrap;",
45 | "inline": "background: #fafafa; padding: 2px 6px; border-radius: 4px; color: #333; font-size: 14px; border: 1px solid #eee;"
46 | },
47 | "quote": "border-left: 4px solid #262626; border-radius: 6px; padding: 10px 10px; background: #fafafa; margin: 0.8em 0; color: #404040; font-style: italic; word-wrap: break-word;",
48 | "image": "max-width: 100%; height: auto; margin: 1em auto; display: block;",
49 | "link": "color: #262626; text-decoration: none; border-bottom: 1px solid #262626;",
50 | "emphasis": {
51 | "strong": "font-weight: bold; color: #262626;",
52 | "em": "font-style: italic; color: #404040;",
53 | "del": "text-decoration: line-through; color: #595959;"
54 | },
55 | "table": {
56 | "container": "width: 100%; margin: 1em 0; border-collapse: collapse; border: 1px solid #f0f0f0;",
57 | "header": "background: #fafafa; font-weight: bold; color: #4a4a4a; border-bottom: 1px solid #f0f0f0; font-size: 1em;",
58 | "cell": "border: 1px solid #f0f0f0; padding: 8px; color: #4a4a4a; font-size: 1em;"
59 | },
60 | "hr": "border: none; border-top: 1px solid #f0f0f0; margin: 20px 0;",
61 | "footnote": {
62 | "ref": "color: #e0e0e0; text-decoration: none; font-size: 0.9em;",
63 | "backref": "color: #e0e0e0; text-decoration: none; font-size: 0.9em;"
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/src/templates/orange.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "orange",
3 | "name": "橙心主题",
4 | "styles": {
5 | "container": "",
6 | "title": {
7 | "h1": {
8 | "base": "margin: 32px 0 0; font-size: 2em; letter-spacing: -0.03em; line-height: 1.5;",
9 | "content": "font-weight: bold; color: #d64b3b;",
10 | "after": ""
11 | },
12 | "h2": {
13 | "base": "margin: 28px 0 0; font-size: 1.5em; letter-spacing: -0.02em; border-bottom: 2px solid #ef7060; line-height: 1.2;",
14 | "content": "display: inline-block; font-weight: bold; background: #ef7060; color: #ffffff; padding: 1px 4px; border-top-right-radius: 3px; border-top-left-radius: 3px; margin-right: 3px;",
15 | "after": "display: inline-block; content: ' '; vertical-align: bottom; border-bottom: 28px solid #fff5f4; border-right: 20px solid transparent;"
16 | },
17 | "h3": {
18 | "base": "margin: 24px 0 0; font-size: 1.25em; letter-spacing: -0.01em; line-height: 1.5;",
19 | "content": "font-weight: bold; color: #f18070;",
20 | "after": ""
21 | },
22 | "base": {
23 | "base": "margin: 20px 0 14px; font-size: 1em; line-height: 1.5;",
24 | "content": "font-weight: bold; color: #f39080;",
25 | "after": ""
26 | }
27 | },
28 | "paragraph": "line-height: 1.8; margin-top: 1em; font-size: 1em; color: #4a4a4a;",
29 | "list": {
30 | "container": "padding-left: 32px; color: #4a4a4a;",
31 | "item": "font-size: 1em; color: #4a4a4a; line-height: 1.8;",
32 | "taskList": "list-style: none; font-size: 1em; color: #4a4a4a; line-height: 1.8;"
33 | },
34 | "code": {
35 | "header": {
36 | "container": "margin-bottom: 1em; display: flex; gap: 6px;",
37 | "dot": "width: 12px; height: 12px; border-radius: 50%;",
38 | "colors": [
39 | "#ff5f56",
40 | "#ffbd2e",
41 | "#27c93f"
42 | ]
43 | },
44 | "block": "color: #333; background: #fff8f7; border-radius: 8px; border: 1px solid #ffe8e6; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin: 1.2em 0; padding: 1em 1em 1em; font-size: 14px; line-height: 1.6; white-space: pre-wrap;",
45 | "inline": "background: #fff8f7; padding: 2px 6px; border-radius: 4px; color: #333; font-size: 14px; border: 1px solid #ffe8e6;"
46 | },
47 | "quote": "border-left: 4px solid #ef7060; border-radius: 6px; padding: 10px 10px; background: #fff5f4; margin: 0.8em 0; color: #d64b3b; font-style: italic; word-wrap: break-word;",
48 | "image": "max-width: 100%; height: auto; margin: 1em auto; display: block;",
49 | "link": "color: #ef7060; text-decoration: none; border-bottom: 1px solid #ef7060; transition: all 0.2s ease;",
50 | "emphasis": {
51 | "strong": "font-weight: bold; color: #4a4a4a;",
52 | "em": "font-style: italic; color: #4a4a4a;",
53 | "del": "text-decoration: line-through; color: #4a4a4a;"
54 | },
55 | "table": {
56 | "container": "width: 100%; margin: 1em 0; border-collapse: collapse; border: 1px solid #ffe8e6;",
57 | "header": "background: #fff8f7; font-weight: bold; color: #4a4a4a; border-bottom: 2px solid #ffe8e6; font-size: 1em;",
58 | "cell": "border: 1px solid #f0f0f0; padding: 8px; color: #4a4a4a; font-size: 1em;"
59 | },
60 | "hr": "border: none; border-top: 1px solid #ffe8e6; margin: 20px 0;",
61 | "footnote": {
62 | "ref": "color: #e0e0e0; text-decoration: none; font-size: 0.9em;",
63 | "backref": "color: #e0e0e0; text-decoration: none; font-size: 0.9em;"
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/src/templates/scarlet.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "scarlet",
3 | "name": "红绯主题",
4 | "styles": {
5 | "container": "",
6 | "title": {
7 | "h1": {
8 | "base": "margin: 32px 0 0; font-size: 2em; letter-spacing: -0.02em; line-height: 1.5;",
9 | "content": "font-weight: bold; color: #DC143C;",
10 | "after": ""
11 | },
12 | "h2": {
13 | "base": "margin: 28px 0 0; font-size: 1.5em; letter-spacing: -0.01em; line-height: 1.5; border-bottom: 1px solid rgba(220,20,60,0.1);",
14 | "content": "font-weight: bold; color: #E34234;",
15 | "after": ""
16 | },
17 | "h3": {
18 | "base": "margin: 24px 0 0; font-size: 1.25em; line-height: 1.5;",
19 | "content": "font-weight: bold; color: #E65D52;",
20 | "after": ""
21 | },
22 | "base": {
23 | "base": "margin: 20px 0 0; font-size: 1em; line-height: 1.5;",
24 | "content": "font-weight: bold; color: #E87A70;",
25 | "after": ""
26 | }
27 | },
28 | "paragraph": "line-height: 1.8; margin-top: 1em; font-size: 1em; color: #4a4a4a;",
29 | "list": {
30 | "container": "padding-left: 32px; color: #4a4a4a;",
31 | "item": "font-size: 1em; color: #4a4a4a; line-height: 1.8;",
32 | "taskList": "list-style: none; font-size: 1em; color: #4a4a4a; line-height: 1.8;"
33 | },
34 | "code": {
35 | "header": {
36 | "container": "margin-bottom: 1em; display: flex; gap: 6px;",
37 | "dot": "width: 12px; height: 12px; border-radius: 50%;",
38 | "colors": [
39 | "#ff5f56",
40 | "#ffbd2e",
41 | "#27c93f"
42 | ]
43 | },
44 | "block": "color: #333; background: #fff8f8; border-radius: 8px; border: 1px solid #ffe8e8; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin: 1.2em 0; padding: 1em 1em 1em; font-size: 14px; line-height: 1.6; white-space: pre-wrap;",
45 | "inline": "background: #fff8f8; padding: 2px 6px; border-radius: 4px; color: #333; font-size: 14px; border: 1px solid #ffe8e8;"
46 | },
47 | "quote": "border-left: 4px solid #DC143C; border-radius: 6px; padding: 10px 10px; background: #fff8f8; margin: 0.8em 0; color: #E34234; font-style: italic; word-wrap: break-word;",
48 | "image": "max-width: 100%; height: auto; margin: 1em auto; display: block;",
49 | "link": "color: #DC143C; text-decoration: none; border-bottom: 1px solid #DC143C; transition: all 0.2s ease;",
50 | "emphasis": {
51 | "strong": "font-weight: bold; color: #4a4a4a;",
52 | "em": "font-style: italic; color: #4a4a4a;",
53 | "del": "text-decoration: line-through; color: #4a4a4a;"
54 | },
55 | "table": {
56 | "container": "width: 100%; margin: 1em 0; border-collapse: collapse; border: 1px solid #ffe8e8;",
57 | "header": "background: #fff8f8; font-weight: bold; color: #4a4a4a; border-bottom: 2px solid #ffe8e8; font-size: 1em;",
58 | "cell": "border: 1px solid #f0f0f0; padding: 8px; color: #4a4a4a; font-size: 1em;"
59 | },
60 | "hr": "border: none; border-top: 1px solid #ffe8e8; margin: 20px 0;",
61 | "footnote": {
62 | "ref": "color: #e0e0e0; text-decoration: none; font-size: 0.9em;",
63 | "backref": "color: #e0e0e0; text-decoration: none; font-size: 0.9em;"
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/src/templates/yeban-orange.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "yeban-orange",
3 | "name": "夜半橙心",
4 | "styles": {
5 | "container": "",
6 | "title": {
7 | "h1": {
8 | "base": "margin: 32px 0 0; font-size: 2em; letter-spacing: -0.03em; line-height: 1.5; text-align: center;",
9 | "content": "font-weight: bold; color: #ef7060; display: inline-block; border-bottom: 1px solid #ef7060;",
10 | "after": ""
11 | },
12 | "h2": {
13 | "base": "margin: 28px 0 0; font-size: 1.5em; letter-spacing: -0.02em; line-height: 1.5; border-bottom: 1px solid rgba(239,112,96,0.2);",
14 | "content": "font-weight: bold; color: #ffffff; background: #ef7060; padding: 1px 4px; border-radius: 3px;",
15 | "after": ""
16 | },
17 | "h3": {
18 | "base": "margin: 24px 0 0; font-size: 1.25em; letter-spacing: -0.01em; line-height: 1.5;",
19 | "content": "font-weight: bold; color: #f18070; padding: 1px 1px;",
20 | "after": ""
21 | },
22 | "base": {
23 | "base": "margin: 20px 0 0; font-size: 1em;",
24 | "content": "font-weight: bold; color: #f39080;",
25 | "after": ""
26 | }
27 | },
28 | "paragraph": "line-height: 1.8; margin-top: 1em; font-size: 1em; color: #4a4a4a;",
29 | "list": {
30 | "container": "padding-left: 32px; color: #4a4a4a;",
31 | "item": "font-size: 1em; color: #4a4a4a; line-height: 1.8;",
32 | "taskList": "list-style: none; font-size: 1em; color: #4a4a4a; line-height: 1.8;"
33 | },
34 | "code": {
35 | "header": {
36 | "container": "margin-bottom: 1em; display: flex; gap: 6px;",
37 | "dot": "width: 12px; height: 12px; border-radius: 50%;",
38 | "colors": [
39 | "#ff5f56",
40 | "#ffbd2e",
41 | "#27c93f"
42 | ]
43 | },
44 | "block": "color: #333; background: #fff8f7; border-radius: 8px; border: 1px solid #ffe8e6; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin: 1.2em 0; padding: 1em 1em 1em; font-size: 14px; line-height: 1.6; white-space: pre-wrap;",
45 | "inline": "background: #fff8f7; padding: 2px 6px; border-radius: 4px; color: #333; font-size: 14px; border: 1px solid #ffe8e6;"
46 | },
47 | "quote": "border-left: 4px solid #7a7da0; border-radius: 6px; padding: 10px 10px; background: #fff8f7; margin: 0.8em 0; color: #d64b3b; font-style: italic; word-wrap: break-word;",
48 |
49 | "image": "max-width: 100%; height: auto; margin: 1em auto; display: block;",
50 | "link": "color: #ef7060; text-decoration: none; border-bottom: 1px solid #ef7060; transition: all 0.2s ease;",
51 | "emphasis": {
52 | "strong": "font-weight: bold; color: #4a4a4a;",
53 | "em": "font-style: italic; color: #4a4a4a;",
54 | "del": "text-decoration: line-through; color: #4a4a4a;"
55 | },
56 | "table": {
57 | "container": "width: 100%; margin: 1em 0; border-collapse: collapse; border: 1px solid #ffe8e6;",
58 | "header": "background: #fff8f7; font-weight: bold; color: #4a4a4a; border-bottom: 2px solid #ffe8e6; font-size: 1em;",
59 | "cell": "border: 1px solid #f0f0f0; padding: 8px; color: #4a4a4a; font-size: 1em;"
60 | },
61 | "hr": "border: none; border-top: 1px solid #ffe8e6; margin: 20px 0;",
62 | "footnote": {
63 | "ref": "color: #e0e0e0; text-decoration: none; font-size: 0.9em;",
64 | "backref": "color: #e0e0e0; text-decoration: none; font-size: 0.9em;"
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/src/templates/yeban.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "yeban",
3 | "name": "夜半主题",
4 | "styles": {
5 | "container": "",
6 | "title": {
7 | "h1": {
8 | "base": "margin: 32px 0 0; font-size: 2em; letter-spacing: -0.03em; line-height: 1.5; text-align: center;",
9 | "content": "font-weight: bold; color: #666b8f; display: inline-block; border-bottom: 1px solid #666b8f;",
10 | "after": ""
11 | },
12 | "h2": {
13 | "base": "margin: 28px 0 0; font-size: 1.5em; letter-spacing: -0.02em; line-height: 1.5; border-bottom: 1px solid rgba(122,125,160,0.2);",
14 | "content": "font-weight: bold; color: #ffffff; background: #7a7da0; padding: 1px 4px; border-radius: 3px;",
15 | "after": ""
16 | },
17 | "h3": {
18 | "base": "margin: 24px 0 0; font-size: 1.25em; letter-spacing: -0.01em; line-height: 1.5;",
19 | "content": "font-weight: bold; color: #7a7da0; padding: 1px 1px;",
20 | "after": ""
21 | },
22 | "base": {
23 | "base": "margin: 20px 0 0; font-size: 1em;",
24 | "content": "font-weight: bold; color: #7a7da0;",
25 | "after": ""
26 | }
27 | },
28 | "paragraph": "line-height: 1.8; margin: 1.2em 0; font-size: 1em; color: #4a4a4a;",
29 | "list": {
30 | "container": "padding-left: 32px; margin-bottom: 1.2em; color: #4a4a4a;",
31 | "item": "margin-bottom: 0.6em; font-size: 1em; color: #4a4a4a; line-height: 1.8;",
32 | "taskList": "list-style: none; margin-left: -24px; font-size: 1em; color: #4a4a4a; line-height: 1.8;"
33 | },
34 | "code": {
35 | "header": {
36 | "container": "margin-bottom: 1em; display: flex; gap: 6px;",
37 | "dot": "width: 12px; height: 12px; border-radius: 50%;",
38 | "colors": ["#ff5f56", "#ffbd2e", "#27c93f"]
39 | },
40 | "block": "color: #333; background: #f8f9fc; border-radius: 8px; border: 1px solid #eef0f7; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin: 1.2em 0; padding: 1em 1em 1em; font-size: 14px; line-height: 1.6; white-space: pre-wrap;",
41 | "inline": "background: #f8f9fc; padding: 2px 6px; border-radius: 4px; color: #333; font-size: 14px; border: 1px solid #eef0f7;"
42 | },
43 | "quote": "border-left: 4px solid #7a7da0; border-radius: 6px; padding: 10px 10px; background: #f8f9fc; margin: 0.8em 0; color: #666b8f; font-style: italic; word-wrap: break-word;",
44 | "image": "max-width: 100%; height: auto; margin: 1em auto; display: block;",
45 | "link": "color: #7a7da0; text-decoration: none; border-bottom: 1px solid #7a7da0; transition: all 0.2s ease;",
46 | "emphasis": {
47 | "strong": "font-weight: bold; color: #4a4a4a;",
48 | "em": "font-style: italic; color: #4a4a4a;",
49 | "del": "text-decoration: line-through; color: #4a4a4a;"
50 | },
51 | "table": {
52 | "container": "width: 100%; margin: 1em 0; border-collapse: collapse; border: 1px solid #e1e4e8;",
53 | "header": "background: #f8f9fc; font-weight: bold; color: #4a4a4a; border-bottom: 2px solid #e1e4e8; font-size: 1em;",
54 | "cell": "border: 1px solid #f0f0f0; padding: 8px; color: #4a4a4a; font-size: 1em;"
55 | },
56 | "hr": "border: none; border-top: 1px solid #eef0f7; margin: 20px 0;",
57 | "footnote": {
58 | "ref": "color: #e0e0e0; text-decoration: none; font-size: 0.9em;",
59 | "backref": "color: #e0e0e0; text-decoration: none; font-size: 0.9em;"
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/src/utils/nanoid.ts:
--------------------------------------------------------------------------------
1 | export { nanoid } from 'nanoid';
--------------------------------------------------------------------------------
/src/view.ts:
--------------------------------------------------------------------------------
1 | import { ItemView, WorkspaceLeaf, MarkdownRenderer, TFile, setIcon } from 'obsidian';
2 | import { MPConverter } from './converter';
3 | import { CopyManager } from './copyManager';
4 | import type { TemplateManager } from './templateManager';
5 | import { DonateManager } from './donateManager';
6 | import type { SettingsManager } from './settings/settings';
7 | import { BackgroundManager } from './backgroundManager';
8 | export const VIEW_TYPE_MP = 'mp-preview';
9 |
10 | export class MPView extends ItemView {
11 | private previewEl: HTMLElement;
12 | private currentFile: TFile | null = null;
13 | private updateTimer: NodeJS.Timeout | null = null;
14 | private isPreviewLocked: boolean = false;
15 | private lockButton: HTMLButtonElement;
16 | private copyButton: HTMLButtonElement;
17 | private templateManager: TemplateManager;
18 | private settingsManager: SettingsManager;
19 | private customTemplateSelect: HTMLElement;
20 | private customFontSelect: HTMLElement;
21 | private fontSizeSelect: HTMLInputElement;
22 | private backgroundManager: BackgroundManager;
23 | private customBackgroundSelect: HTMLElement;
24 |
25 | constructor(
26 | leaf: WorkspaceLeaf,
27 | templateManager: TemplateManager,
28 | settingsManager: SettingsManager
29 | ) {
30 | super(leaf);
31 | this.templateManager = templateManager;
32 | this.settingsManager = settingsManager;
33 | this.backgroundManager = new BackgroundManager(this.settingsManager);
34 | }
35 |
36 | getViewType() {
37 | return VIEW_TYPE_MP;
38 | }
39 |
40 | getDisplayText() {
41 | return '公众号预览';
42 | }
43 |
44 | getIcon() {
45 | return 'eye';
46 | }
47 |
48 | async onOpen() {
49 | const container = this.containerEl.children[1];
50 | container.empty();
51 | container.classList.remove('view-content');
52 | container.classList.add('mp-view-content');
53 |
54 | // 顶部工具栏
55 | const toolbar = container.createEl('div', { cls: 'mp-toolbar' });
56 | const controlsGroup = toolbar.createEl('div', { cls: 'mp-controls-group' });
57 |
58 | // 锁定按钮
59 | this.lockButton = controlsGroup.createEl('button', {
60 | cls: 'mp-lock-button',
61 | attr: { 'aria-label': '关闭实时预览状态' }
62 | });
63 | setIcon(this.lockButton, 'lock');
64 | this.lockButton.setAttribute('aria-label', '开启实时预览状态');
65 | this.lockButton.addEventListener('click', () => this.togglePreviewLock());
66 |
67 |
68 |
69 | // 添加背景选择器
70 | const backgroundOptions = [
71 | { value: '', label: '无背景' },
72 | ...(this.settingsManager.getVisibleBackgrounds()?.map(bg => ({
73 | value: bg.id,
74 | label: bg.name
75 | })) || [])
76 | ];
77 |
78 | this.customBackgroundSelect = this.createCustomSelect(
79 | controlsGroup,
80 | 'mp-background-select',
81 | backgroundOptions
82 | );
83 |
84 | // 添加背景选择器的事件监听
85 | this.customBackgroundSelect.querySelector('.custom-select')?.addEventListener('change', async (e: any) => {
86 | const value = e.detail.value;
87 | this.backgroundManager.setBackground(value);
88 | await this.settingsManager.updateSettings({
89 | backgroundId: value
90 | });
91 | this.backgroundManager.applyBackground(this.previewEl);
92 | });
93 |
94 | // 创建自定义下拉选择器
95 | this.customTemplateSelect = this.createCustomSelect(
96 | controlsGroup,
97 | 'mp-template-select',
98 | await this.getTemplateOptions()
99 | );
100 | this.customTemplateSelect.id = 'template-select';
101 |
102 | // 添加模板选择器的 change 事件监听
103 | this.customTemplateSelect.querySelector('.custom-select')?.addEventListener('change', async (e: any) => {
104 | const value = e.detail.value;
105 | this.templateManager.setCurrentTemplate(value);
106 | await this.settingsManager.updateSettings({
107 | templateId: value
108 | });
109 | this.templateManager.applyTemplate(this.previewEl);
110 | });
111 |
112 | this.customFontSelect = this.createCustomSelect(
113 | controlsGroup,
114 | 'mp-font-select',
115 | this.getFontOptions()
116 | );
117 |
118 | // 添加字体选择器的 change 事件监听
119 | this.customFontSelect.querySelector('.custom-select')?.addEventListener('change', async (e: any) => {
120 | const value = e.detail.value;
121 | this.templateManager.setFont(value);
122 | await this.settingsManager.updateSettings({
123 | fontFamily: value
124 | });
125 | this.templateManager.applyTemplate(this.previewEl);
126 | });
127 | this.customFontSelect.id = 'font-select';
128 |
129 | // 字号调整
130 | const fontSizeGroup = controlsGroup.createEl('div', { cls: 'mp-font-size-group' });
131 | const decreaseButton = fontSizeGroup.createEl('button', {
132 | cls: 'mp-font-size-btn',
133 | text: '-'
134 | });
135 | this.fontSizeSelect = fontSizeGroup.createEl('input', {
136 | cls: 'mp-font-size-input',
137 | type: 'text',
138 | value: '16',
139 | attr: {
140 | style: 'border: none; outline: none; background: transparent;'
141 | }
142 | });
143 | const increaseButton = fontSizeGroup.createEl('button', {
144 | cls: 'mp-font-size-btn',
145 | text: '+'
146 | });
147 |
148 | // 从设置中恢复上次的选择
149 | const settings = this.settingsManager.getSettings();
150 |
151 | // 恢复背景设置
152 | if (settings.backgroundId) {
153 | const backgroundSelect = this.customBackgroundSelect.querySelector('.selected-text');
154 | const backgroundDropdown = this.customBackgroundSelect.querySelector('.select-dropdown');
155 | if (backgroundSelect && backgroundDropdown) {
156 | const option = backgroundOptions.find(o => o.value === settings.backgroundId);
157 | if (option) {
158 | backgroundSelect.textContent = option.label;
159 | this.customBackgroundSelect.querySelector('.custom-select')?.setAttribute('data-value', option.value);
160 | backgroundDropdown.querySelectorAll('.select-item').forEach(el => {
161 | if (el.getAttribute('data-value') === option.value) {
162 | el.classList.add('selected');
163 | } else {
164 | el.classList.remove('selected');
165 | }
166 | });
167 | }
168 | }
169 | this.backgroundManager.setBackground(settings.backgroundId);
170 | }
171 |
172 | // 恢复设置
173 | if (settings.templateId) {
174 | const templateSelect = this.customTemplateSelect.querySelector('.selected-text');
175 | const templateDropdown = this.customTemplateSelect.querySelector('.select-dropdown');
176 | if (templateSelect && templateDropdown) {
177 | const option = await this.getTemplateOptions();
178 | const selected = option.find(o => o.value === settings.templateId);
179 | if (selected) {
180 | templateSelect.textContent = selected.label;
181 | this.customTemplateSelect.querySelector('.custom-select')?.setAttribute('data-value', selected.value);
182 | templateDropdown.querySelectorAll('.select-item').forEach(el => {
183 | if (el.getAttribute('data-value') === selected.value) {
184 | el.classList.add('selected');
185 | } else {
186 | el.classList.remove('selected');
187 | }
188 | });
189 | }
190 | }
191 | this.templateManager.setCurrentTemplate(settings.templateId);
192 | }
193 |
194 | if (settings.fontFamily) {
195 | const fontSelect = this.customFontSelect.querySelector('.selected-text');
196 | const fontDropdown = this.customFontSelect.querySelector('.select-dropdown');
197 | if (fontSelect && fontDropdown) {
198 | const option = this.getFontOptions();
199 | const selected = option.find(o => o.value === settings.fontFamily);
200 | if (selected) {
201 | fontSelect.textContent = selected.label;
202 | this.customFontSelect.querySelector('.custom-select')?.setAttribute('data-value', selected.value);
203 | fontDropdown.querySelectorAll('.select-item').forEach(el => {
204 | if (el.getAttribute('data-value') === selected.value) {
205 | el.classList.add('selected');
206 | } else {
207 | el.classList.remove('selected');
208 | }
209 | });
210 | }
211 | }
212 | this.templateManager.setFont(settings.fontFamily);
213 | }
214 |
215 | if (settings.fontSize) {
216 | this.fontSizeSelect.value = settings.fontSize.toString();
217 | this.templateManager.setFontSize(settings.fontSize);
218 | }
219 |
220 | // 更新字号调整事件
221 | const updateFontSize = async () => {
222 | const size = parseInt(this.fontSizeSelect.value);
223 | this.templateManager.setFontSize(size);
224 | await this.settingsManager.updateSettings({
225 | fontSize: size
226 | });
227 | this.templateManager.applyTemplate(this.previewEl);
228 | };
229 |
230 | // 字号调整按钮事件
231 | decreaseButton.addEventListener('click', () => {
232 | const currentSize = parseInt(this.fontSizeSelect.value);
233 | if (currentSize > 12) {
234 | this.fontSizeSelect.value = (currentSize - 1).toString();
235 | updateFontSize();
236 | }
237 | });
238 |
239 | increaseButton.addEventListener('click', () => {
240 | const currentSize = parseInt(this.fontSizeSelect.value);
241 | if (currentSize < 30) {
242 | this.fontSizeSelect.value = (currentSize + 1).toString();
243 | updateFontSize();
244 | }
245 | });
246 |
247 | this.fontSizeSelect.addEventListener('change', updateFontSize);
248 | // 预览区域
249 | this.previewEl = container.createEl('div', { cls: 'mp-preview-area' });
250 |
251 | // 底部工具栏
252 | const bottomBar = container.createEl('div', { cls: 'mp-bottom-bar' });
253 | // 创建中间控件容器
254 | const bottomControlsGroup = bottomBar.createEl('div', { cls: 'mp-controls-group' });
255 | // 帮助按钮
256 | const helpButton = bottomControlsGroup.createEl('button', {
257 | cls: 'mp-help-button',
258 | attr: { 'aria-label': '使用指南' }
259 | });
260 | setIcon(helpButton, 'help');
261 | // 帮助提示框
262 | bottomControlsGroup.createEl('div', {
263 | cls: 'mp-help-tooltip',
264 | text: `使用指南:
265 | 1. 选择喜欢的主题模板
266 | 2. 调整字体和字号
267 | 3. 实时预览效果
268 | 4. 点击【复制按钮】即可粘贴到公众号
269 | 5. 编辑实时查看效果,点🔓关闭实时刷新
270 | 6. 如果你喜欢这个插件,欢迎关注打赏`
271 | });
272 |
273 |
274 |
275 | // 关于作者按钮
276 | const likeButton = bottomControlsGroup.createEl('button', {
277 | cls: 'mp-like-button'
278 | });
279 | const heartSpan = likeButton.createEl('span', {
280 | text: '❤️',
281 | attr: { style: 'margin-right: 4px' }
282 | });
283 | likeButton.createSpan({ text: '关于作者' });
284 |
285 | likeButton.addEventListener('click', () => {
286 | DonateManager.showDonateModal(this.containerEl);
287 | });
288 |
289 | // 复制按钮
290 | this.copyButton = bottomControlsGroup.createEl('button', {
291 | text: '复制到公众号',
292 | cls: 'mp-copy-button'
293 | });
294 | //新功能按钮
295 | const newButton = bottomControlsGroup.createEl('button', {
296 | text: '敬请期待',
297 | cls: 'mp-new-button'
298 | });
299 |
300 | // 添加复制按钮点击事件
301 | this.copyButton.addEventListener('click', async () => {
302 | if (this.previewEl) {
303 | this.copyButton.disabled = true;
304 | this.copyButton.setText('复制中...');
305 |
306 | try {
307 | await CopyManager.copyToClipboard(this.previewEl);
308 | this.copyButton.setText('复制成功');
309 |
310 | setTimeout(() => {
311 | this.copyButton.disabled = false;
312 | this.copyButton.setText('复制为公众号格式');
313 | }, 2000);
314 | } catch (error) {
315 | this.copyButton.setText('复制失败');
316 | setTimeout(() => {
317 | this.copyButton.disabled = false;
318 | this.copyButton.setText('复制为公众号格式');
319 | }, 2000);
320 | }
321 | }
322 | });
323 |
324 | // 监听文档变化
325 | this.registerEvent(
326 | this.app.workspace.on('file-open', this.onFileOpen.bind(this))
327 | );
328 |
329 | // 监听文档内容变化
330 | this.registerEvent(
331 | this.app.vault.on('modify', this.onFileModify.bind(this))
332 | );
333 |
334 | // 检查当前打开的文件
335 | const currentFile = this.app.workspace.getActiveFile();
336 | await this.onFileOpen(currentFile);
337 | }
338 |
339 | private updateControlsState(enabled: boolean) {
340 | this.lockButton.disabled = !enabled;
341 | // 更新所有自定义选择器的禁用状态
342 | const templateSelect = this.customTemplateSelect.querySelector('.custom-select');
343 | const fontSelect = this.customFontSelect.querySelector('.custom-select');
344 | const backgroundSelect = this.customBackgroundSelect.querySelector('.custom-select');
345 |
346 | [templateSelect, fontSelect, backgroundSelect].forEach(select => {
347 | if (select) {
348 | select.classList.toggle('disabled', !enabled);
349 | select.setAttribute('style', `pointer-events: ${enabled ? 'auto' : 'none'}`);
350 | }
351 | });
352 |
353 | this.fontSizeSelect.disabled = !enabled;
354 | this.copyButton.disabled = !enabled;
355 |
356 | // 字号调节按钮的状态控制
357 | const fontSizeButtons = this.containerEl.querySelectorAll('.mp-font-size-btn');
358 | fontSizeButtons.forEach(button => {
359 | (button as HTMLButtonElement).disabled = !enabled;
360 | });
361 | }
362 |
363 | async onFileOpen(file: TFile | null) {
364 | this.currentFile = file;
365 | if (!file || file.extension !== 'md') {
366 | this.previewEl.empty();
367 | this.previewEl.createEl('div', {
368 | text: '只能预览 markdown 文本文档',
369 | cls: 'mp-empty-message'
370 | });
371 | this.updateControlsState(false);
372 | return;
373 | }
374 |
375 | this.updateControlsState(true);
376 | this.isPreviewLocked = false;
377 | setIcon(this.lockButton, 'unlock');
378 | await this.updatePreview();
379 | }
380 |
381 | private async togglePreviewLock() {
382 | this.isPreviewLocked = !this.isPreviewLocked;
383 | const lockIcon = this.isPreviewLocked ? 'lock' : 'unlock';
384 | const lockStatus = this.isPreviewLocked ? '开启实时预览状态' : '关闭实时预览状态';
385 | setIcon(this.lockButton, lockIcon);
386 | this.lockButton.setAttribute('aria-label', lockStatus);
387 |
388 | if (!this.isPreviewLocked) {
389 | await this.updatePreview();
390 | }
391 | }
392 |
393 | async onFileModify(file: TFile) {
394 | if (file === this.currentFile && !this.isPreviewLocked) {
395 | if (this.updateTimer) {
396 | clearTimeout(this.updateTimer);
397 | }
398 |
399 | this.updateTimer = setTimeout(() => {
400 | this.updatePreview();
401 | }, 500);
402 | }
403 | }
404 |
405 | async updatePreview() {
406 | if (!this.currentFile) return;
407 |
408 | // 保存当前滚动位置和内容高度
409 | const scrollPosition = this.previewEl.scrollTop;
410 | const prevHeight = this.previewEl.scrollHeight;
411 | const isAtBottom = (this.previewEl.scrollHeight - this.previewEl.scrollTop) <= (this.previewEl.clientHeight + 100);
412 |
413 | this.previewEl.empty();
414 | const content = await this.app.vault.cachedRead(this.currentFile);
415 |
416 | await MarkdownRenderer.render(
417 | this.app,
418 | content,
419 | this.previewEl,
420 | this.currentFile.path,
421 | this
422 | );
423 |
424 | MPConverter.formatContent(this.previewEl);
425 | this.templateManager.applyTemplate(this.previewEl);
426 | this.backgroundManager.applyBackground(this.previewEl);
427 |
428 | // 根据滚动位置决定是否自动滚动
429 | if (isAtBottom) {
430 | // 如果用户在底部附近,自动滚动到底部
431 | requestAnimationFrame(() => {
432 | this.previewEl.scrollTop = this.previewEl.scrollHeight;
433 | });
434 | } else {
435 | // 否则保持原来的滚动位置
436 | const heightDiff = this.previewEl.scrollHeight - prevHeight;
437 | this.previewEl.scrollTop = scrollPosition + heightDiff;
438 | }
439 | }
440 |
441 | // 添加自定义下拉选择器创建方法
442 | private createCustomSelect(
443 | parent: HTMLElement,
444 | className: string,
445 | options: { value: string; label: string }[]
446 | ) {
447 | const container = parent.createEl('div', { cls: 'custom-select-container' });
448 | const select = container.createEl('div', { cls: 'custom-select' });
449 | const selectedText = select.createEl('span', { cls: 'selected-text' });
450 | const arrow = select.createEl('span', { cls: 'select-arrow', text: '▾' });
451 |
452 | const dropdown = container.createEl('div', { cls: 'select-dropdown' });
453 |
454 | options.forEach(option => {
455 | const item = dropdown.createEl('div', {
456 | cls: 'select-item',
457 | text: option.label
458 | });
459 |
460 | item.dataset.value = option.value;
461 | item.addEventListener('click', () => {
462 | // 移除其他项的选中状态
463 | dropdown.querySelectorAll('.select-item').forEach(el =>
464 | el.classList.remove('selected'));
465 | // 添加当前项的选中状态
466 | item.classList.add('selected');
467 | selectedText.textContent = option.label;
468 | select.dataset.value = option.value;
469 | dropdown.classList.remove('show');
470 | select.dispatchEvent(new CustomEvent('change', {
471 | detail: { value: option.value }
472 | }));
473 | });
474 | });
475 |
476 | // 设置默认值和选中状态
477 | if (options.length > 0) {
478 | selectedText.textContent = options[0].label;
479 | select.dataset.value = options[0].value;
480 | dropdown.querySelector('.select-item')?.classList.add('selected');
481 | }
482 |
483 | // 点击显示/隐藏下拉列表
484 | select.addEventListener('click', (e) => {
485 | e.stopPropagation();
486 | dropdown.classList.toggle('show');
487 | });
488 |
489 | // 点击其他地方关闭下拉列表
490 | document.addEventListener('click', () => {
491 | dropdown.classList.remove('show');
492 | });
493 |
494 | return container;
495 | }
496 |
497 | // 获取模板选项
498 | private async getTemplateOptions() {
499 |
500 | const templates = this.settingsManager.getVisibleTemplates();
501 |
502 | return templates.length > 0
503 | ? templates.map(t => ({ value: t.id, label: t.name }))
504 | : [{ value: 'default', label: '默认模板' }];
505 | }
506 |
507 | // 获取字体选项
508 | private getFontOptions() {
509 | return this.settingsManager.getFontOptions();
510 | }
511 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "inlineSourceMap": true,
5 | "inlineSources": true,
6 | "module": "ESNext",
7 | "target": "ES6",
8 | "allowJs": true,
9 | "noImplicitAny": true,
10 | "moduleResolution": "node",
11 | "importHelpers": true,
12 | "isolatedModules": true,
13 | "strictNullChecks": true,
14 | "lib": [
15 | "DOM",
16 | "ES5",
17 | "ES6",
18 | "ES7"
19 | ]
20 | },
21 | "include": [
22 | "src/**/*.ts"
23 | , "templates/index.ts" ]
24 | }
--------------------------------------------------------------------------------