Initial commit: Note to RED plugin for Obsidian

This commit is contained in:
Yeban8090 2025-03-13 11:12:58 +08:00
commit eeff220b7c
34 changed files with 6108 additions and 0 deletions

20
.gitignore vendored Normal file
View file

@ -0,0 +1,20 @@
# Node
node_modules/
npm-debug.log
*.log
# Build
main.js
*.js.map
# macOS
.DS_Store
# IDE
.idea/
.vscode/
*.swp
*.swo
# Obsidian
.obsidian/

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 夜半Yeban
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

58
README.md Normal file
View file

@ -0,0 +1,58 @@
# Note to RED
一键将 Obsidian 笔记转换为小红书图片格式的插件。
## 功能特点
- 📝 使用二级标题分割内容,每个标题自动生成一张配图
- 🎨 提供多种精美模板,支持自定义字体和字号
- 👤 可自定义用户头像、昵称和页脚文案
- 🔄 实时预览编辑效果
- 📥 支持单页导出和批量导出
- 🔒 锁定功能避免预览刷新打断书写
## 使用方法
1. 核心用法:用二级标题(##)分割内容,每个标题生成一张小红书配图
2. 首图制作单独调整首节字号至20-24px使用【下载当前页】导出
3. 长文优化内容较多的章节可调小字号至14-16px后单独导出
4. 批量操作:保持统一字号时,用【导出全部页】批量生成
5. 模板切换:顶部选择器可切换不同视觉风格
6. 实时编辑:解锁状态(🔓)下编辑文档即时预览效果
## 安装方法
### 从 Obsidian 社区插件安装(推荐)
1. 打开 Obsidian 设置
2. 转到第三方插件设置
3. 关闭安全模式
4. 点击浏览社区插件
5. 搜索 "Note to RED"
6. 点击安装并启用插件
### 手动安装
1. 下载最新版本的 release 文件
2. 解压后将文件夹复制到 Obsidian 插件目录:`{vault}/.obsidian/plugins/`
3. 重启 Obsidian
4. 在设置中启用插件
## 使用技巧
- 使用二级标题来分割不同的图片内容
- 调整字号大小以适应不同长度的内容
- 可以自定义头像和用户信息
- 支持实时预览和编辑
- 提供多种模板以适应不同场景
## 问题反馈
如果您在使用过程中遇到任何问题,或有任何功能建议,欢迎:
1. 提交 [Issue](https://github.com/YeBan/note-to-red/issues)
2. 发送邮件至:[yuliu8090@gmail.com]
## 许可证
MIT License。查看 [LICENSE](LICENSE) 获取更多信息。

BIN
assets/donate.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

BIN
assets/qrcode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

36
build.sh Executable file
View file

@ -0,0 +1,36 @@
#!/bin/bash
# 获取版本号
version=$(grep '"version"' manifest.json | cut -d '"' -f 4)
zip_name="note-to-red-${version}.zip"
# 检查目标文件是否存在
if [ -f "../${zip_name}" ]; then
read -p "文件 ${zip_name} 已存在,是否覆盖?(y/n) " answer
if [ "$answer" != "y" ]; then
echo "打包已取消"
exit 1
fi
fi
# 创建临时目录
mkdir -p ../temp/note-to-red
# 复制必要文件
cp main.js manifest.json styles.css ../temp/note-to-red/
cp -r assets ../temp/note-to-red/
# 切换到临时目录的上级目录
cd ../temp
# 创建 zip 文件
zip -r "${zip_name}" note-to-red
# 移动 zip 文件到上级目录
mv "${zip_name}" ../
# 清理临时目录
cd ..
rm -rf temp
echo "打包完成:${zip_name}"

13
data.json Normal file

File diff suppressed because one or more lines are too long

38
esbuild.config.mjs Normal file
View file

@ -0,0 +1,38 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const banner =
`/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = process.argv[2] === "production";
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["src/main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
...builtins
],
format: "cjs",
target: "es2016",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}

12
manifest.json Normal file
View file

@ -0,0 +1,12 @@
{
"id": "note-to-red",
"name": "Note to RED",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Convert Obsidian notes to RED (Xiaohongshu) style images",
"author": "Yeban",
"isDesktopOnly": true,
"assets": [
"assets/*"
]
}

2567
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

35
package.json Normal file
View file

@ -0,0 +1,35 @@
{
"name": "note-to-red",
"version": "1.0.0",
"description": "一键将 Obsidian 笔记转换为小红书图片格式",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [
"obsidian",
"plugin",
"xiaohongshu"
],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/dom-to-image": "^2.6.7",
"@types/jszip": "^3.4.0",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
},
"dependencies": {
"dom-to-image": "^2.6.0",
"html2canvas": "^1.4.1",
"jszip": "^3.10.1"
}
}

43
src/backgroundManager.ts Normal file
View file

@ -0,0 +1,43 @@
import { backgrounds } from './backgrounds';
export interface Background {
id: string;
name: string;
style: string;
}
export class BackgroundManager {
private backgrounds: Background[];
private currentBackground: Background | null = null;
constructor() {
this.backgrounds = backgrounds.backgrounds;
}
public getAllBackgrounds(): Background[] {
return this.backgrounds;
}
public setBackground(id: string | null) {
if (!id) {
this.currentBackground = null;
return;
}
const background = this.backgrounds.find(bg => bg.id === id);
if (background) {
this.currentBackground = background;
}
}
public applyBackground(element: HTMLElement) {
// 修改选择器为目标预览容器
const previewContainer = element.querySelector('.red-image-preview');
if (!previewContainer) return;
if (!this.currentBackground) {
previewContainer.setAttribute('style', ''); // 清除样式
return;
}
previewContainer.setAttribute('style', this.currentBackground.style);
}
}

39
src/backgrounds/index.ts Normal file
View file

@ -0,0 +1,39 @@
export const backgrounds = {
backgrounds: [
{
id: "grid",
name: "网格",
style: "box-sizing: border-box; background-color: white; 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;"
},
{
id: "crosshatch",
name: "交叉",
style: "box-sizing: border-box; background-color: white; 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;"
},
{
id: "dots",
name: "圆点",
style: "box-sizing: border-box; background-color: white; background-image: radial-gradient(rgba(50, 0, 0, 0.03) 1px, transparent 1px); background-size: 20px 20px; background-position: center center;"
},
{
id: "dash",
name: "虚线",
style: "box-sizing: border-box; background-color: white; background-image: linear-gradient(90deg, rgba(50, 0, 0, 0.03) 50%, transparent 50%); background-size: 8px 1px; background-position: center center;"
},
{
id: "wave",
name: "波浪",
style: "box-sizing: border-box; background-color: white; 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;"
},
{
id: "checkerboard",
name: "棋盘",
style: "box-sizing: border-box; background-color: white; 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;"
},
{
id: "dark",
name: "暗黑",
style: "box-sizing: border-box; background-color: #1a1a1a; background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.03) 25%, transparent 25%), linear-gradient(-45deg, rgba(255, 255, 255, 0.03) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, rgba(255, 255, 255, 0.03) 75%), linear-gradient(-45deg, transparent 75%, rgba(255, 255, 255, 0.03) 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px;"
}
]
};

244
src/converter.ts Normal file
View file

@ -0,0 +1,244 @@
import { App } from 'obsidian';
export class RedConverter {
private static app: App;
static initialize(app: App) {
this.app = app;
}
static hasValidContent(element: HTMLElement): boolean {
const headers = element.querySelectorAll('h2');
return headers.length > 0;
}
static formatContent(element: HTMLElement): void {
const headers = Array.from(element.querySelectorAll('h2'));
if (headers.length === 0) {
element.empty();
const tip = element.createEl('div', {
cls: 'red-empty-message',
text: `⚠️ 温馨提示
使(##)
`
});
// 触发自定义事件
element.dispatchEvent(new CustomEvent('content-validation-change', {
detail: { isValid: false },
bubbles: true
}));
return;
}
// 触发自定义事件表示内容有效
element.dispatchEvent(new CustomEvent('content-validation-change', {
detail: { isValid: true },
bubbles: true
}));
// 创建预览容器
const previewContainer = document.createElement('div');
previewContainer.className = 'red-preview-container';
// 创建图片预览区域
const imagePreview = document.createElement('div');
imagePreview.className = 'red-image-preview';
// 创建三个主要区域
const headerArea = document.createElement('div');
headerArea.className = 'red-preview-header';
const contentArea = document.createElement('div');
contentArea.className = 'red-preview-content';
const footerArea = document.createElement('div');
footerArea.className = 'red-preview-footer';
// 创建内容容器
const contentContainer = document.createElement('div');
contentContainer.className = 'red-content-container';
// 处理每个二级标题及其内容
headers.forEach((header, index) => {
const section = this.createContentSection(header, index);
if (section) {
contentContainer.appendChild(section);
}
});
// 组装结构
contentArea.appendChild(contentContainer);
imagePreview.appendChild(headerArea);
imagePreview.appendChild(contentArea);
imagePreview.appendChild(footerArea);
previewContainer.appendChild(imagePreview);
// 处理完成后再清空原容器并添加新内容
element.empty();
element.appendChild(previewContainer);
}
private static createContentSection(header: Element, index: number): HTMLElement | null {
// 获取当前标题到下一个标题之间的所有内容
let content = [];
let current = header.nextElementSibling;
while (current && current.tagName !== 'H2') {
content.push(current.cloneNode(true));
current = current.nextElementSibling;
}
// 创建内容区域
const section = document.createElement('section');
section.className = 'red-content-section';
section.setAttribute('data-index', index.toString());
// 添加标题
section.appendChild(header.cloneNode(true));
// 添加内容
content.forEach(el => section.appendChild(el));
// 处理样式和格式
this.processElements(section);
return section;
}
private static processElements(container: HTMLElement | null): void {
if (!container) return;
// 处理强调文本
container.querySelectorAll('strong, em').forEach(el => {
(el as HTMLElement).style.display = 'inline';
});
// 处理链接
container.querySelectorAll('a').forEach(el => {
(el as HTMLElement).style.color = '#576b95';
(el as HTMLElement).style.textDecoration = 'none';
});
// 处理表格
container.querySelectorAll('table').forEach(el => {
if (el === container.closest('table')) return;
(el as HTMLTableElement).style.borderCollapse = 'collapse';
(el as HTMLTableElement).style.margin = '1em 0';
(el as HTMLTableElement).style.width = '100%';
});
// 处理表格单元格
container.querySelectorAll('th, td').forEach(el => {
if (el === container.closest('td')) return;
(el as HTMLTableCellElement).style.border = '1px solid #dfdfdf';
(el as HTMLTableCellElement).style.padding = '8px';
});
// 处理分割线
container.querySelectorAll('hr').forEach(el => {
(el as HTMLElement).style.border = 'none';
(el as HTMLElement).style.borderTop = '1px solid #dfdfdf';
(el as HTMLElement).style.margin = '20px 0';
});
// 处理删除线
container.querySelectorAll('del').forEach(el => {
(el as HTMLElement).style.textDecoration = 'line-through';
});
// 处理任务列表
container.querySelectorAll('.task-list-item').forEach(el => {
(el as HTMLElement).style.listStyle = 'none';
const checkbox = el.querySelector('input[type="checkbox"]');
if (checkbox) {
(checkbox as HTMLElement).style.marginRight = '6px';
}
});
// 处理脚注
container.querySelectorAll('.footnote-ref, .footnote-backref').forEach(el => {
(el as HTMLElement).style.color = '#576b95';
(el as HTMLElement).style.textDecoration = 'none';
});
// 处理代码块
container.querySelectorAll('pre code').forEach(el => {
const pre = el.parentElement;
if (pre) {
// 添加 macOS 风格的窗口按钮
const dots = document.createElement('div');
dots.style.cssText = `
margin: 8px 0 12px 0;
display: flex;
gap: 6px;
`;
const colors = ['#ff5f56', '#ffbd2e', '#27c93f'];
colors.forEach(color => {
const dot = document.createElement('span');
dot.style.cssText = `
width: 12px;
height: 12px;
border-radius: 50%;
background-color: ${color};
`;
dots.appendChild(dot);
});
// 将红绿灯插入到代码块的最前面
pre.insertBefore(dots, pre.firstChild);
pre.style.paddingTop = '32px';
// 移除原有的复制按钮
const copyButton = pre.querySelector('.copy-code-button');
if (copyButton) {
copyButton.remove();
}
}
});
// 处理图片
container.querySelectorAll('span.internal-embed[alt][src]').forEach(async el => {
const originalSpan = el as HTMLElement;
const src = originalSpan.getAttribute('src');
const alt = originalSpan.getAttribute('alt');
if (!src) return;
try {
// 获取文件的元数据
const linktext = src.split('|')[0]; // 处理可能带有别名的链接
const file = this.app.metadataCache.getFirstLinkpathDest(linktext, '');
if (file) {
const absolutePath = this.app.vault.adapter.getResourcePath(file.path);
const newImg = document.createElement('img');
newImg.src = absolutePath;
if (alt) newImg.alt = alt;
// 应用样式
newImg.style.maxWidth = '100%';
newImg.style.height = 'auto';
newImg.style.display = 'block';
newImg.style.margin = '1em auto';
// 直接替换 span 元素,保留段落中的其他内容
originalSpan.parentNode?.replaceChild(newImg, originalSpan);
}
} catch (error) {
console.error('图片处理失败:', error);
}
});
// 处理引用块
container.querySelectorAll('blockquote').forEach(el => {
// 清理引用块内部段落的样式
el.querySelectorAll('p').forEach(p => {
p.style.margin = '0';
p.style.padding = '0';
p.style.lineHeight = 'inherit';
});
});
}
}

129
src/donateManager.ts Normal file
View file

@ -0,0 +1,129 @@
import { App, Plugin } from 'obsidian';
export class DonateManager {
private static overlay: HTMLElement;
private static modal: HTMLElement;
private static app: App;
private static plugin: Plugin;
public static initialize(app: App, plugin: Plugin) {
this.app = app;
this.plugin = plugin;
}
public static showDonateModal(container: HTMLElement) {
this.overlay = container.createEl('div', {
cls: 'mp-donate-overlay'
});
this.modal = this.overlay.createEl('div', {
cls: 'mp-about-modal'
});
// 添加关闭按钮
const closeButton = this.modal.createEl('button', {
cls: 'mp-donate-close',
text: '×'
});
// 添加作者信息区域
const authorSection = this.modal.createEl('div', {
cls: 'mp-about-section mp-about-intro-section'
});
authorSection.createEl('h4', {
text: '关于作者',
cls: 'mp-about-title'
});
const introEl = authorSection.createEl('p', {
cls: 'mp-about-intro'
});
introEl.innerHTML = '你好,我是<span class="mp-about-name">【夜半】</span>,一名<span class="mp-about-identity">全职写作与独立开发者</span>。';
const roleList = authorSection.createEl('div', {
cls: 'mp-about-roles'
});
const roleEl = roleList.createEl('p', {
cls: 'mp-about-role'
});
roleEl.innerHTML = `这款插件是我为了在 Obsidian 写作后,<br>
<br>
<span class="mp-about-highlight"></span>
<span class="mp-about-value"></span>`;
// 添加插件介绍
const descEl = authorSection.createEl('p', {
cls: 'mp-about-desc'
});
descEl.innerHTML = `如果这款插件对你有帮助,<br>或者你愿意支持我的独立开发与写作,欢迎请我喝咖啡☕️。<br>
`;
// 添加打赏区域
const donateSection = this.modal.createEl('div', {
cls: 'mp-about-section mp-about-donate-section'
});
donateSection.createEl('h4', {
text: '请我喝咖啡',
cls: 'mp-about-subtitle'
});
const donateQR = donateSection.createEl('div', {
cls: 'mp-about-qr'
});
donateQR.createEl('img', {
attr: {
src: this.app.vault.adapter.getResourcePath(`${this.plugin.manifest.dir}/assets/donate.png`),
alt: '打赏二维码'
}
});
// 添加公众号区域
const mpSection = this.modal.createEl('div', {
cls: 'mp-about-section mp-about-mp-section'
});
const mpDescEl = mpSection.createEl('p', {
cls: 'mp-about-desc'
});
mpDescEl.innerHTML = `如果你想了解更多关于创作、效率工具的小技巧,<br>
`;
mpSection.createEl('h4', {
text: '微信公众号',
cls: 'mp-about-subtitle'
});
const mpQR = mpSection.createEl('div', {
cls: 'mp-about-qr'
});
mpQR.createEl('img', {
attr: {
src: this.app.vault.adapter.getResourcePath(`${this.plugin.manifest.dir}/assets/qrcode.png`),
alt: '公众号二维码'
}
});
const footerEl = mpSection.createEl('p', {
cls: 'mp-about-footer'
});
footerEl.innerHTML = '期待与你一起,在创作的世界里<strong>找到属于自己的意义</strong>。';
// 添加关闭事件
closeButton.addEventListener('click', () => this.closeDonateModal());
this.overlay.addEventListener('click', (e) => {
if (e.target === this.overlay) {
this.closeDonateModal();
}
});
}
private static closeDonateModal() {
if (this.overlay) {
this.overlay.remove();
}
}
}

89
src/downloadManager.ts Normal file
View file

@ -0,0 +1,89 @@
import domtoimage from 'dom-to-image'; // 替换为 dom-to-image
import JSZip from 'jszip';
export class DownloadManager {
// 添加共用的导出配置方法
private static getExportConfig(imageElement: HTMLElement) {
return {
quality: 1,
width: imageElement.offsetWidth * 4,
height: imageElement.offsetHeight * 4,
style: {
transform: 'scale(4)',
'transform-origin': 'top left',
}
};
}
static async downloadAllImages(element: HTMLElement): Promise<void> {
try {
const zip = new JSZip();
const previewContainer = element.querySelector('.red-preview-container');
if (!previewContainer) {
throw new Error('找不到预览容器');
}
const sections = previewContainer.querySelectorAll('.red-content-section');
const totalSections = sections.length;
const originalDisplayStates = Array.from(sections).map(
s => (s as HTMLElement).style.display
);
for (let i = 0; i < totalSections; i++) {
sections.forEach(s => (s as HTMLElement).style.display = 'none');
(sections[i] as HTMLElement).style.display = 'block';
const imageElement = element.querySelector('.red-image-preview') as HTMLElement;
const blob = await domtoimage.toBlob(imageElement, this.getExportConfig(imageElement));
zip.file(`小红书笔记_第${i + 1}页.png`, blob);
}
// 恢复所有 sections 的原始显示状态
sections.forEach((s, index) => {
(s as HTMLElement).style.display = originalDisplayStates[index];
});
// 生成 zip 文件并下载
const content = await zip.generateAsync({ type: "blob" });
const url = URL.createObjectURL(content);
const link = document.createElement('a');
link.href = url;
link.download = `小红书笔记_${new Date().getTime()}.zip`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
console.error('导出图片失败:', error);
throw error;
}
}
static async downloadSingleImage(element: HTMLElement): Promise<void> {
try {
const imageElement = element.querySelector('.red-image-preview') as HTMLElement;
if (!imageElement) {
throw new Error('找不到预览区域');
}
const blob = await domtoimage.toBlob(imageElement, this.getExportConfig(imageElement));
// 创建下载链接并触发下载
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `小红书笔记_${new Date().getTime()}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
console.error('导出图片失败:', error);
throw error;
}
}
}

77
src/main.ts Normal file
View file

@ -0,0 +1,77 @@
import { Plugin, Notice } from 'obsidian';
import { RedView, VIEW_TYPE_RED } from './view'; // 暂时改回原来的导入
import { TemplateManager } from './templateManager';
import { SettingsManager } from './settings';
import { RedConverter } from './converter'; // 暂时使用原来的转换器
import { DonateManager } from './donateManager';
export default class RedPlugin extends Plugin {
private settingsManager: SettingsManager;
async onload() {
// 初始化设置管理器
this.settingsManager = new SettingsManager(this);
await this.settingsManager.loadSettings();
// 初始化模板管理器
const templateManager = new TemplateManager(this.app);
// 初始化转换器
RedConverter.initialize(this.app);
DonateManager.initialize(this.app, this);
// 注册视图
this.registerView(
VIEW_TYPE_RED,
(leaf) => new RedView(leaf, templateManager, this.settingsManager)
);
// 添加首次加载自动打开视图的逻辑
this.app.workspace.onLayoutReady(() => {
if (this.app.workspace.getLeavesOfType(VIEW_TYPE_RED).length === 0) {
const leaf = this.app.workspace.getRightLeaf(false);
if (leaf) {
leaf.setViewState({
type: VIEW_TYPE_RED,
active: false // 设置为 false 表示不聚焦
});
}
}
});
// 添加命令到命令面板
this.addCommand({
id: 'open-mp-preview',
name: '打开小红书图片预览',
callback: async () => {
await this.activateView();
}
});
}
async onunload() {
// 清理视图
this.app.workspace.detachLeavesOfType(VIEW_TYPE_RED); // 使用原来的视图类型
}
async activateView() {
// 如果视图已经存在,激活它
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_RED); // 使用原来的视图类型
if (leaves.length > 0) {
this.app.workspace.revealLeaf(leaves[0]);
return;
}
// 创建新视图
const rightLeaf = this.app.workspace.getRightLeaf(false);
if (rightLeaf) {
await rightLeaf.setViewState({
type: VIEW_TYPE_RED,
active: true,
});
} else {
new Notice('无法创建视图面板');
}
}
}

156
src/previewManager.ts Normal file
View file

@ -0,0 +1,156 @@
import type { SettingsManager } from './settings';
export class PreviewManager {
constructor(private settingsManager: SettingsManager) {}
// 创建页头内容
createHeaderContent(headerArea: HTMLElement, handleAvatarClick: () => void,
handleUserNameEdit: (el: HTMLElement) => void,
handleUserIdEdit: (el: HTMLElement) => void) {
headerArea.empty();
const settings = this.settingsManager.getSettings();
const userInfo = headerArea.createEl('div', { cls: 'red-user-info' });
// 左侧用户信息区
const userLeft = userInfo.createEl('div', { cls: 'red-user-left' });
// 用户头像
const avatar = userLeft.createEl('div', {
cls: 'red-user-avatar',
attr: { 'title': '点击上传头像' }
});
if (settings.userAvatar) {
avatar.createEl('img', {
attr: {
src: settings.userAvatar,
alt: '用户头像'
}
});
} else {
const placeholder = avatar.createEl('div', {
cls: 'red-avatar-placeholder'
});
placeholder.createEl('span', {
cls: 'red-avatar-upload-icon',
text: '📷'
});
}
avatar.addEventListener('click', handleAvatarClick);
// 用户信息区
const userMeta = userLeft.createEl('div', { cls: 'red-user-meta' });
const userNameContainer = userMeta.createEl('div', {
cls: 'red-user-name-container'
});
const userName = userNameContainer.createEl('div', {
cls: 'red-user-name',
text: settings.userName,
attr: { 'title': '点击编辑用户名' }
});
userNameContainer.createEl('div', {
cls: 'red-verified-icon',
text: '✓'
});
const userId = userMeta.createEl('div', {
cls: 'red-user-id',
text: settings.userId,
attr: { 'title': '点击编辑用户ID' }
});
userName.addEventListener('click', () => handleUserNameEdit(userName));
userId.addEventListener('click', () => handleUserIdEdit(userId));
if (settings.showTime) {
const userRight = userInfo.createEl('div', { cls: 'red-user-right' });
userRight.createEl('div', {
cls: 'red-post-time',
text: new Date().toLocaleDateString(settings.timeFormat)
});
}
}
// 创建页脚内容
createFooterContent(footerArea: HTMLElement, handleFooterTextEdit: (el: HTMLElement, position: 'left' | 'right') => void) {
footerArea.empty();
const settings = this.settingsManager.getSettings();
// 直接使用 footerArea移除多余的容器创建
// 创建左侧文本
const leftText = footerArea.createEl('div', {
cls: 'red-footer-text',
text: settings.footerLeftText,
attr: { 'title': '点击编辑文本' }
});
// 创建分隔符
footerArea.createEl('div', {
cls: 'red-footer-separator',
text: '|'
});
// 创建右侧文本
const rightText = footerArea.createEl('div', {
cls: 'red-footer-text',
text: settings.footerRightText,
attr: { 'title': '点击编辑文本' }
});
// 添加点击编辑事件
leftText.addEventListener('click', () => handleFooterTextEdit(leftText, 'left'));
rightText.addEventListener('click', () => handleFooterTextEdit(rightText, 'right'));
}
// 创建导航按钮
createNavigationButtons(container: HTMLElement, totalImages: number, onNavigate: (direction: 'prev' | 'next') => void) {
const previewContainer = container.querySelector('.red-preview-container');
if (!previewContainer) return;
const navContainer = previewContainer.createEl('div', {
cls: 'red-nav-container'
});
const prevButton = navContainer.createEl('button', {
cls: 'red-nav-button',
text: '←'
});
const indicator = navContainer.createEl('span', {
cls: 'red-page-indicator',
text: `1/${totalImages}`
});
const nextButton = navContainer.createEl('button', {
cls: 'red-nav-button',
text: '→'
});
// 添加导航事件
prevButton.addEventListener('click', () => onNavigate('prev'));
nextButton.addEventListener('click', () => onNavigate('next'));
return { prev: prevButton, next: nextButton, indicator };
}
// 显示指定索引的图片
showImage(index: number, sections: NodeListOf<Element>, navigationButtons?: {
prev: HTMLButtonElement;
next: HTMLButtonElement;
indicator: HTMLElement;
}) {
sections.forEach((section, i) => {
(section as HTMLElement).style.display = i === index ? 'block' : 'none';
});
if (navigationButtons) {
navigationButtons.prev.style.visibility = index === 0 ? 'hidden' : 'visible';
navigationButtons.next.style.visibility = index === sections.length - 1 ? 'hidden' : 'visible';
navigationButtons.indicator.textContent = `${index + 1}/${sections.length}`;
}
}
}

57
src/settings.ts Normal file
View file

@ -0,0 +1,57 @@
interface RedSettings {
templateId: string;
fontFamily: string;
fontSize: number;
backgroundId: string;
// 添加用户信息设置
userAvatar: string;
userName: string;
userId: string;
showTime: boolean;
timeFormat: string;
footerLeftText: string;
footerRightText: string;
}
const DEFAULT_SETTINGS: RedSettings = {
templateId: 'default',
fontFamily: 'Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, "PingFang SC"',
fontSize: 16,
backgroundId: '',
// 修改默认用户信息
userAvatar: '', // 默认为空,提示用户上传
userName: '夜半',
userId: '@Yeban',
showTime: true,
timeFormat: 'zh-CN',
footerLeftText: '夜半过后,光明便启程',
footerRightText: '欢迎关注公众号:夜半'
}
export class SettingsManager {
private plugin: any;
private settings: RedSettings;
constructor(plugin: any) {
this.plugin = plugin;
this.settings = DEFAULT_SETTINGS;
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.plugin.loadData());
}
async saveSettings() {
await this.plugin.saveData(this.settings);
}
getSettings(): RedSettings {
return this.settings;
}
async updateSettings(settings: Partial<RedSettings>) {
this.settings = { ...this.settings, ...settings };
await this.saveSettings();
}
}

319
src/templateManager.ts Normal file
View file

@ -0,0 +1,319 @@
import { App } from 'obsidian';
import { templates } from './templates';
interface Template {
id: string;
name: string;
styles: {
// 容器基础样式
imagePreview: string;
// 页眉样式组
header: {
avatar: {
container: string;
placeholder: string;
image: string;
};
nameContainer: string;
userName: string;
userId: string;
postTime: string;
verifiedIcon: string;
};
// 页脚样式组
footer: {
container: string;
text: string;
separator: string;
};
title: {
h1: {
base: string;
content: string;
after: string;
};
h2: {
base: string;
content: string;
after: string;
};
h3: {
base: string;
content: string;
after: string;
};
base: {
base: string;
content: string;
after: string;
};
};
paragraph: string;
list: {
container: string;
item: string;
taskList: string;
};
quote: string;
code: {
block: string;
inline: string;
};
image: string;
link: string;
emphasis: {
strong: string;
em: string;
del: string;
};
table: {
container: string;
header: string;
cell: string;
};
hr: string;
footnote: {
ref: string;
backref: string;
};
};
}
export class TemplateManager {
private templates: Map<string, Template> = new Map();
private currentTemplate: Template;
private currentFont: string = '-apple-system';
private currentFontSize: number = 16;
private app: App;
constructor(app: App) {
this.app = app;
this.loadTemplates(); // 加载模板
}
public async loadTemplates() {
try {
// 直接从内置模板加载
Object.values(templates).forEach(template => {
this.templates.set(template.id, template);
if (template.id === 'default') {
this.currentTemplate = template;
}
});
} catch (error) {
console.error('加载模板失败:', error);
throw new Error('无法加载模板文件');
}
}
public getTemplate(id: string): Template | undefined {
return this.templates.get(id);
}
public getCurrentTemplate(): Template {
return this.currentTemplate;
}
public setCurrentTemplate(id: string): boolean {
const template = this.templates.get(id);
if (template) {
this.currentTemplate = template;
return true;
}
return false;
}
public getAllTemplates(): Template[] {
return Array.from(this.templates.values());
}
public setFont(fontFamily: string) {
this.currentFont = fontFamily;
}
public setFontSize(size: number) {
this.currentFontSize = size;
}
// 修改 applyTemplate 方法
public applyTemplate(element: HTMLElement): void {
const styles = this.currentTemplate.styles;
// 修改应用基础样式的方式
const imagePreview = element.querySelector('.red-image-preview');
if (imagePreview) {
imagePreview.setAttribute('style', styles.imagePreview);
}
// 应用页眉样式
const header = element.querySelector('.red-preview-header');
if (header) {
// 用户头像
header.querySelectorAll('.red-user-avatar').forEach(el => {
el.setAttribute('style', styles.header.avatar.container);
});
header.querySelectorAll('.red-avatar-placeholder').forEach(el => {
el.setAttribute('style', styles.header.avatar.placeholder);
});
header.querySelectorAll('.red-user-avatar img').forEach(el => {
el.setAttribute('style', styles.header.avatar.image);
});
// 用户名容器
header.querySelectorAll('.red-user-name-container').forEach(el => {
el.setAttribute('style', styles.header.nameContainer);
});
// 用户名
header.querySelectorAll('.red-user-name').forEach(el => {
el.setAttribute('style', styles.header.userName);
});
// 用户ID
header.querySelectorAll('.red-user-id').forEach(el => {
el.setAttribute('style', styles.header.userId);
});
// 发布时间
header.querySelectorAll('.red-post-time').forEach(el => {
el.setAttribute('style', styles.header.postTime);
});
// 认证图标
header.querySelectorAll('.red-verified-icon').forEach(el => {
el.setAttribute('style', styles.header.verifiedIcon);
});
}
// 应用页脚样式
const footer = element.querySelector('.red-preview-footer');
if (footer) {
// 页脚容器
footer.setAttribute('style', styles.footer.container);
// 页脚文本
footer.querySelectorAll('.red-footer-text').forEach(el => {
el.setAttribute('style', styles.footer.text);
});
// 分隔符
footer.querySelectorAll('.red-footer-separator').forEach(el => {
el.setAttribute('style', styles.footer.separator);
});
}
// 应用标题样式
['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].forEach(tag => {
element.querySelectorAll(tag).forEach(el => {
// 检查是否已经处理过
if (!el.querySelector('.content')) {
const content = document.createElement('span');
content.className = 'content';
content.innerHTML = el.innerHTML;
el.innerHTML = '';
el.appendChild(content);
const after = document.createElement('span');
after.className = 'after';
el.appendChild(after);
}
// 根据标签选择对应的样式
const styleKey = (tag === 'h4' || tag === 'h5' || tag === 'h6' ? 'base' : tag) as keyof typeof styles.title;
const titleStyle = styles.title[styleKey];
// 应用样式
el.setAttribute('style', `${titleStyle.base}; font-family: ${this.currentFont};`);
el.querySelector('.content')?.setAttribute('style', titleStyle.content);
el.querySelector('.after')?.setAttribute('style', titleStyle.after);
});
});
// 应用段落样式
element.querySelectorAll('p').forEach(el => {
if (!el.parentElement?.closest('p') && !el.parentElement?.closest('blockquote')) {
el.setAttribute('style', `${styles.paragraph}; font-family: ${this.currentFont}; font-size: ${this.currentFontSize}px;`);
}
});
// 应用列表样式
element.querySelectorAll('ul, ol').forEach(el => {
el.setAttribute('style', styles.list.container);
});
element.querySelectorAll('li').forEach(el => {
el.setAttribute('style', `${styles.list.item}; font-family: ${this.currentFont}; font-size: ${this.currentFontSize}px;`);
});
element.querySelectorAll('.task-list-item').forEach(el => {
el.setAttribute('style', `${styles.list.taskList}; font-family: ${this.currentFont}; font-size: ${this.currentFontSize}px;`);
});
// 应用引用样式
element.querySelectorAll('blockquote').forEach(el => {
el.setAttribute('style', `${styles.quote}; font-family: ${this.currentFont}; font-size: ${this.currentFontSize}px;`);
});
// 应用代码样式
element.querySelectorAll('pre').forEach(el => {
el.setAttribute('style', `${styles.code.block}; font-size: ${this.currentFontSize}px;`);
});
element.querySelectorAll('code:not(pre code)').forEach(el => {
el.setAttribute('style', `${styles.code.inline}; font-size: ${this.currentFontSize}px;`);
});
// 应用链接样式
element.querySelectorAll('a').forEach(el => {
el.setAttribute('style', styles.link);
});
// 应用强调样式
element.querySelectorAll('strong').forEach(el => {
el.setAttribute('style', styles.emphasis.strong);
});
element.querySelectorAll('em').forEach(el => {
el.setAttribute('style', styles.emphasis.em);
});
element.querySelectorAll('del').forEach(el => {
el.setAttribute('style', styles.emphasis.del);
});
// 应用表格样式(内容表格,非包裹表格)
element.querySelectorAll('table').forEach(el => {
el.setAttribute('style', styles.table.container);
});
element.querySelectorAll('th').forEach(el => {
el.setAttribute('style', `${styles.table.header}; font-family: ${this.currentFont}; font-size: ${this.currentFontSize}px;`);
});
element.querySelectorAll('td').forEach(el => {
el.setAttribute('style', `${styles.table.cell}; font-family: ${this.currentFont}; font-size: ${this.currentFontSize}px;`);
});
// 应用分割线样式
element.querySelectorAll('hr').forEach(el => {
el.setAttribute('style', styles.hr);
});
// 应用脚注样式
element.querySelectorAll('.footnote-ref').forEach(el => {
el.setAttribute('style', styles.footnote.ref);
});
element.querySelectorAll('.footnote-backref').forEach(el => {
el.setAttribute('style', styles.footnote.backref);
});
// 应用图片样式
element.querySelectorAll('img').forEach(el => {
const img = el as HTMLImageElement;
el.setAttribute('style', `${styles.image}; font-family: ${this.currentFont};`);
const parent = img.parentElement;
if (parent && parent.tagName.toLowerCase() === 'p') {
if (parent.childNodes.length === 1) {
parent.style.textAlign = 'center';
parent.style.margin = '1em 0';
}
}
});
}
}
export const templateManager = (app: App) => new TemplateManager(app);

69
src/templates/cyber.json Normal file
View file

@ -0,0 +1,69 @@
{
"id": "cyber",
"name": "赛博朋克",
"styles": {
"imagePreview": "background: #0a0a16; padding: 32px 28px;",
"header": {
"avatar": {
"container": "width: 40px; height: 40px; border-radius: 2px; overflow: hidden; border: 1px solid #00ffaa80; box-shadow: 0 0 12px #00ffaa40;",
"placeholder": "background: #1a1a2e; transition: all 0.3s ease;",
"image": "object-fit: cover; transition: transform 0.3s ease; filter: saturate(1.4);"
},
"nameContainer": "display: flex; align-items: center; gap: 10px;",
"userName": "font-size: 16px; font-weight: 700; color: #00ffaa; text-shadow: 0 0 12px #00ffaa80;",
"userId": "font-size: 14px; color: #4dffbe; font-family: 'JetBrains Mono', monospace;",
"postTime": "font-size: 13px; color: #2f8f98; font-family: 'JetBrains Mono', monospace;",
"verifiedIcon": "width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; color: #000000; background-color: #00ffaa; border-radius: 2px; font-size: 10px; flex-shrink: 0; border: 1px solid #00ffaa80; box-shadow: 0 0 12px #00ffaa40;"
},
"footer": {
"container": "position: absolute; bottom: 0; left: 0; right: 0; display: flex; align-items: center; justify-content: center; gap: 14px; padding: 16px; color: #4dffbe; font-size: 13px; border-top: 1px solid #00ffaa80; background: #0a0a16; font-family: 'JetBrains Mono', monospace;",
"text": "color: inherit; transition: color 0.2s ease; white-space: nowrap;",
"separator": "color: #2f8f98;"
},
"title": {
"h2": {
"base": "margin: 0; font-size: 1.5em; letter-spacing: 0.02em; line-height: 1.5;",
"content": "font-weight: 700; color: #00ffaa; text-shadow: 0 0 16px #00ffaa80;",
"after": "content: ' //'; color: #2f8f98;"
},
"h3": {
"base": "margin: 26px 0 0; font-size: 1.25em; line-height: 1.5;",
"content": "font-weight: 700; color: #00ffaa;",
"after": "content: ' //';"
},
"base": {
"base": "margin: 22px 0 0; font-size: 1.1em; line-height: 1.5;",
"content": "font-weight: 700; color: #00ffaa;",
"after": ""
}
},
"paragraph": "line-height: 1.75; margin-bottom: 1.1em; font-size: 15px; color: #e0e0e0;",
"emphasis": {
"strong": "font-weight: 700; color: #00ffaa; text-shadow: 0 0 12px #00ffaa80;",
"em": "font-style: normal; color: #4dffbe; background: rgba(77,255,190,0.15); padding: 0 4px;",
"del": "text-decoration: line-through; color: #2f8f98; opacity: 0.9;"
},
"list": {
"container": "padding-left: 26px; margin-bottom: 1.1em; color: #7feceb;",
"item": "margin-bottom: 0.7em; font-size: 15px; color: #7feceb; line-height: 1.75;",
"taskList": "list-style: none; margin-left: -22px; font-size: 15px; color: #7feceb; line-height: 1.75;"
},
"code": {
"block": "background: #1a1a2e; padding: 1.2em; border-radius: 4px; font-size: 14px; font-family: 'JetBrains Mono', monospace; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; color: #00ffaa; margin: 1.3em 0; border: 1px solid #00ffaa80; box-shadow: 0 0 24px #00ffaa20;",
"inline": "background: #1a1a2e; padding: 4px 10px; border-radius: 4px; color: #00ffaa; font-size: 14px; font-family: 'JetBrains Mono', monospace; border: 1px solid #00ffaa80;"
},
"quote": "border-left: 3px solid #00ffaa; padding: 0 0 0 20px; margin: 1.5em 0; color: #4dffbe; font-style: normal; font-size: 15px; line-height: 1.75; text-shadow: 0 0 12px #00ffaa40;",
"image": "max-width: 100%; height: auto; margin: 1.5em auto; border: 2px solid #00ffaa80; box-shadow: 0 0 32px #00ffaa30; filter: saturate(1.2);",
"link": "color: #00ffaa; text-decoration: none; border-bottom: 2px solid #00ffaa80; transition: all 0.2s ease; text-shadow: 0 0 12px #00ffaa40;",
"table": {
"container": "width: 100%; margin: 1.5em 0; border-collapse: collapse; border: 2px solid #00ffaa80; box-shadow: 0 0 24px #00ffaa20;",
"header": "background: #1a1a2e; font-weight: 700; color: #00ffaa; padding: 14px; border: 2px solid #00ffaa80; text-shadow: 0 0 12px #00ffaa40;",
"cell": "padding: 14px; color: #7feceb; border: 2px solid #00ffaa80; font-family: 'JetBrains Mono', monospace;"
},
"hr": "border: none; border-top: 2px solid #00ffaa80; margin: 32px 0; box-shadow: 0 0 24px #00ffaa30;",
"footnote": {
"ref": "color: #4dffbe; text-decoration: none; font-size: 0.9em; font-family: 'JetBrains Mono', monospace;",
"backref": "color: #4dffbe; text-decoration: none; font-size: 0.9em; font-family: 'JetBrains Mono', monospace;"
}
}
}

View file

@ -0,0 +1,69 @@
{
"id": "default",
"name": "默认主题",
"styles": {
"imagePreview": "background: #1c1c1e; padding: 28px 24px;",
"header": {
"avatar": {
"container": "width: 40px; height: 40px; border-radius: 12px; overflow: hidden;",
"placeholder": "background: #2c2c2e; transition: all 0.3s ease;",
"image": "object-fit: cover; transition: transform 0.3s ease;"
},
"nameContainer": "display: flex; align-items: center; gap: 8px;",
"userName": "font-size: 16px; font-weight: 600; color: #ffffff;",
"userId": "font-size: 14px; color: #98989d;",
"postTime": "font-size: 13px; color: #636366;",
"verifiedIcon": "width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; color: #ffffff; background-color: #0A84FF; border-radius: 50%; font-size: 10px; flex-shrink: 0;"
},
"footer": {
"container": "position: absolute; bottom: 0; left: 0; right: 0; display: flex; align-items: center; justify-content: center; gap: 14px; padding: 16px; color: #98989d; font-size: 13px; border-top: 1px solid #2c2c2e; background: #1c1c1e;",
"text": "color: inherit; transition: color 0.2s ease; white-space: nowrap;",
"separator": "color: #636366;"
},
"title": {
"h2": {
"base": "margin: 0; font-size: 1.5em; letter-spacing: -0.01em; line-height: 1.5;",
"content": "font-weight: 600; color: #f2f2f7;",
"after": ""
},
"h3": {
"base": "margin: 26px 0 0; font-size: 1.25em; line-height: 1.5;",
"content": "font-weight: 600; color: #f2f2f7;",
"after": ""
},
"base": {
"base": "margin: 22px 0 0; font-size: 1.1em; line-height: 1.5;",
"content": "font-weight: 600; color: #f2f2f7;",
"after": ""
}
},
"paragraph": "line-height: 1.8; margin-bottom: 1.1em; font-size: 15px; color: #f2f2f7;",
"emphasis": {
"strong": "font-weight: 700; color: #0A84FF;",
"em": "font-style: normal; color: #98989d; background: rgba(152,152,157,0.1); padding: 0 4px;",
"del": "text-decoration: line-through; color: #636366;"
},
"list": {
"container": "padding-left: 26px; margin-bottom: 1.1em; color: #d1d1d6;",
"item": "margin-bottom: 0.7em; font-size: 15px; color: #d1d1d6; line-height: 1.75;",
"taskList": "list-style: none; margin-left: -22px; font-size: 15px; color: #d1d1d6; line-height: 1.75;"
},
"code": {
"block": "background: #2c2c2e; padding: 1em 1.2em; border-radius: 12px; font-size: 14px; font-family: SF Mono, Menlo, monospace; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; color: #ffffff; margin: 1.3em 0;",
"inline": "background: #2c2c2e; padding: 3px 8px; border-radius: 6px; color: #ffffff; font-size: 14px; font-family: SF Mono, Menlo, monospace;"
},
"quote": "border-left: 3px solid #0A84FF; padding: 0 0 0 18px; margin: 1.2em 0; color: #f2f2f7; font-style: normal; font-size: 15px; line-height: 1.75;",
"image": "max-width: 100%; height: auto; margin: 1.3em auto; border-radius: 12px;",
"link": "color: #0A84FF; text-decoration: underline; text-underline-offset: 2px; transition: opacity 0.2s ease;",
"table": {
"container": "width: 100%; margin: 1.3em 0; border-collapse: separate; border-spacing: 0; border-radius: 12px; overflow: hidden; border: 1px solid #2c2c2e;",
"header": "background: #2c2c2e; font-weight: 600; color: #f2f2f7; padding: 12px;",
"cell": "padding: 12px; color: #d1d1d6; border-top: 1px solid #2c2c2e;"
},
"hr": "border: none; border-top: 1px solid #2c2c2e; margin: 26px 0;",
"footnote": {
"ref": "color: #98989d; text-decoration: none; font-size: 0.9em;",
"backref": "color: #98989d; text-decoration: none; font-size: 0.9em;"
}
}
}

View file

@ -0,0 +1,69 @@
{
"id": "elegant",
"name": "优雅暗色",
"styles": {
"imagePreview": "background: #1a1721; padding: 32px 28px;",
"header": {
"avatar": {
"container": "width: 40px; height: 40px; border-radius: 14px; overflow: hidden; box-shadow: 0 2px 8px rgba(180,144,255,0.15);",
"placeholder": "background: #2a2433; transition: all 0.3s ease;",
"image": "object-fit: cover; transition: transform 0.3s ease;"
},
"nameContainer": "display: flex; align-items: center; gap: 8px;",
"userName": "font-size: 16px; font-weight: 600; color: #e2d9f3; letter-spacing: 0.02em;",
"userId": "font-size: 14px; color: #a393c9;",
"postTime": "font-size: 13px; color: #6d617f;",
"verifiedIcon": "width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; color: #1a1721; background-color: #b490ff; border-radius: 6px; font-size: 10px; flex-shrink: 0; box-shadow: 0 2px 8px rgba(180,144,255,0.2);"
},
"footer": {
"container": "position: absolute; bottom: 0; left: 0; right: 0; display: flex; align-items: center; justify-content: center; gap: 14px; padding: 16px; color: #a393c9; font-size: 13px; border-top: 1px solid #2a2433; background: rgba(26,23,33,0.95); backdrop-filter: blur(12px);",
"text": "color: inherit; transition: color 0.2s ease; white-space: nowrap;",
"separator": "color: #6d617f;"
},
"title": {
"h2": {
"base": "margin: 0; font-size: 1.5em; letter-spacing: -0.01em; line-height: 1.5;",
"content": "font-weight: 600; color: #e2d9f3; text-shadow: 0 2px 4px rgba(0,0,0,0.1);",
"after": ""
},
"h3": {
"base": "margin: 28px 0 0; font-size: 1.25em; line-height: 1.5;",
"content": "font-weight: 600; color: #e2d9f3;",
"after": ""
},
"base": {
"base": "margin: 24px 0 0; font-size: 1.1em; line-height: 1.5;",
"content": "font-weight: 600; color: #e2d9f3;",
"after": ""
}
},
"paragraph": "line-height: 1.75; margin-bottom: 1.1em; font-size: 15px; color: #c4b8dd;",
"emphasis": {
"strong": "font-weight: 600; color: #b490ff; text-shadow: 0 1px 2px rgba(180,144,255,0.2);",
"em": "font-style: normal; color: #a393c9; background: rgba(163,147,201,0.1); padding: 0 4px; border-radius: 2px;",
"del": "text-decoration: line-through; color: #6d617f; opacity: 0.8;"
},
"list": {
"container": "padding-left: 26px; margin-bottom: 1.2em; color: #c4b8dd;",
"item": "margin-bottom: 0.7em; font-size: 15px; color: #c4b8dd; line-height: 1.75;",
"taskList": "list-style: none; margin-left: -22px; font-size: 15px; color: #c4b8dd; line-height: 1.75;"
},
"code": {
"block": "background: #2a2433; padding: 1.2em; border-radius: 14px; font-size: 14px; font-family: 'SF Mono', Menlo, monospace; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; color: #c4b8dd; margin: 1.3em 0; box-shadow: inset 0 4px 12px rgba(0,0,0,0.1); border: 1px solid #3a3443;",
"inline": "background: #2a2433; padding: 4px 10px; border-radius: 6px; color: #c4b8dd; font-size: 14px; font-family: 'SF Mono', Menlo, monospace; border: 1px solid #3a3443;"
},
"quote": "border-left: 3px solid #b490ff; padding: 0 0 0 18px; margin: 1.5em 0; color: #a393c9; font-style: normal; font-size: 15px; line-height: 1.75; background: rgba(180,144,255,0.05); border-radius: 0 4px 4px 0;",
"image": "max-width: 100%; height: auto; margin: 1.5em auto; border-radius: 14px; box-shadow: 0 8px 24px rgba(0,0,0,0.2);",
"link": "color: #b490ff; text-decoration: none; transition: all 0.2s ease; border-bottom: 1px solid rgba(180,144,255,0.3);",
"table": {
"container": "width: 100%; margin: 1.5em 0; border-collapse: separate; border-spacing: 0; border-radius: 14px; overflow: hidden; border: 1px solid #3a3443; box-shadow: 0 4px 12px rgba(0,0,0,0.1);",
"header": "background: #2a2433; font-weight: 600; color: #e2d9f3; padding: 14px;",
"cell": "padding: 14px; color: #c4b8dd; border-top: 1px solid #3a3443;"
},
"hr": "border: none; border-top: 1px solid #3a3443; margin: 32px 0;",
"footnote": {
"ref": "color: #a393c9; text-decoration: none; font-size: 0.9em;",
"backref": "color: #a393c9; text-decoration: none; font-size: 0.9em;"
}
}
}

69
src/templates/forest.json Normal file
View file

@ -0,0 +1,69 @@
{
"id": "forest",
"name": "森林清晨",
"styles": {
"imagePreview": "background: #1a2420; padding: 32px 28px; border: 1px solid #2ecc7180;",
"header": {
"avatar": {
"container": "width: 40px; height: 40px; border-radius: 14px; overflow: hidden; box-shadow: 0 4px 16px rgba(46,204,113,0.15); border: 1px solid #2ecc7180;",
"placeholder": "background: #243830; transition: all 0.3s ease;",
"image": "object-fit: cover; transition: transform 0.3s ease; filter: brightness(1.05);"
},
"nameContainer": "display: flex; align-items: center; gap: 10px;",
"userName": "font-size: 16px; font-weight: 600; color: #2ecc71; text-shadow: 0 2px 4px rgba(46,204,113,0.1);",
"userId": "font-size: 14px; color: #27ae60; font-family: 'SF Pro Text';",
"postTime": "font-size: 13px; color: #16a085; letter-spacing: 0.02em;",
"verifiedIcon": "width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; color: #1a2420; background-color: #2ecc71; border-radius: 6px; font-size: 10px; flex-shrink: 0; box-shadow: 0 2px 8px rgba(46,204,113,0.3); border: 1px solid #2ecc7180;"
},
"footer": {
"container": "position: absolute; bottom: 0; left: 0; right: 0; display: flex; align-items: center; justify-content: center; gap: 14px; padding: 16px; color: #27ae60; font-size: 13px; border-top: 1px solid #2ecc7140; background: rgba(26,36,32,0.95); backdrop-filter: blur(12px);",
"text": "color: inherit; transition: color 0.2s ease; white-space: nowrap;",
"separator": "color: #16a085;"
},
"title": {
"h2": {
"base": "margin: 0; font-size: 1.5em; letter-spacing: -0.01em; line-height: 1.5;",
"content": "font-weight: 600; color: #2ecc71; text-shadow: 0 2px 4px rgba(46,204,113,0.1);",
"after": ""
},
"h3": {
"base": "margin: 28px 0 0; font-size: 1.25em; line-height: 1.5;",
"content": "font-weight: 600; color: #2ecc71;",
"after": ""
},
"base": {
"base": "margin: 24px 0 0; font-size: 1.1em; line-height: 1.5;",
"content": "font-weight: 600; color: #2ecc71;",
"after": ""
}
},
"paragraph": "line-height: 1.8; margin-bottom: 1.1em; font-size: 15px; color: #e8f5e9;",
"emphasis": {
"strong": "font-weight: 700; color: #2ecc71; text-shadow: 0 2px 4px rgba(46,204,113,0.2);",
"em": "font-style: normal; color: #27ae60; background: rgba(39,174,96,0.1); padding: 0 4px; border-radius: 2px;",
"del": "text-decoration: line-through; color: #16a085; opacity: 0.8;"
},
"list": {
"container": "padding-left: 26px; margin-bottom: 1.2em; color: #a8e6cf;",
"item": "margin-bottom: 0.7em; font-size: 15px; color: #a8e6cf; line-height: 1.75;",
"taskList": "list-style: none; margin-left: -22px; font-size: 15px; color: #a8e6cf; line-height: 1.75;"
},
"code": {
"block": "background: #243830; padding: 1.2em; border-radius: 14px; font-size: 14px; font-family: 'SF Mono', Menlo, monospace; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; color: #a8e6cf; margin: 1.5em 0; box-shadow: inset 0 4px 12px rgba(0,0,0,0.1); border: 1px solid #2ecc7180;",
"inline": "background: #243830; padding: 4px 10px; border-radius: 6px; color: #a8e6cf; font-size: 14px; font-family: 'SF Mono', Menlo, monospace; border: 1px solid #2ecc7180;"
},
"quote": "border-left: 3px solid #2ecc71; padding: 0 0 0 20px; margin: 1.5em 0; color: #27ae60; font-style: normal; font-size: 15px; line-height: 1.75; background: rgba(46,204,113,0.05);",
"image": "max-width: 100%; height: auto; margin: 1.5em auto; border-radius: 14px; box-shadow: 0 8px 24px rgba(46,204,113,0.1); border: 1px solid #2ecc7180;",
"link": "color: #2ecc71; text-decoration: none; border-bottom: 1px solid #2ecc7180; transition: all 0.2s ease; text-shadow: 0 2px 4px rgba(46,204,113,0.1);",
"table": {
"container": "width: 100%; margin: 1.5em 0; border-collapse: separate; border-spacing: 0; border-radius: 14px; overflow: hidden; border: 1px solid #2ecc7180; box-shadow: 0 4px 12px rgba(46,204,113,0.1);",
"header": "background: #243830; font-weight: 600; color: #2ecc71; padding: 14px; border-bottom: 1px solid #2ecc7180;",
"cell": "padding: 14px; color: #a8e6cf; border-top: 1px solid #2ecc7180;"
},
"hr": "border: none; border-top: 1px solid #2ecc7180; margin: 32px 0; box-shadow: 0 2px 8px rgba(46,204,113,0.1);",
"footnote": {
"ref": "color: #27ae60; text-decoration: none; font-size: 0.9em;",
"backref": "color: #27ae60; text-decoration: none; font-size: 0.9em;"
}
}
}

24
src/templates/index.ts Normal file
View file

@ -0,0 +1,24 @@
// 使用 require 导入 JSON 文件以避免 TypeScript 的 JSON 模块解析问题
const defaultTemplate = require('./default.json');
const minimalTemplate = require('./minimal.json');
const elegantTemplate = require('./elegant.json');
const cyberTemplate = require('./cyber.json');
const warmTemplate = require('./warm.json');
const forestTemplate = require('./forest.json');
const oceanTemplate = require('./ocean.json');
const sakuraTemplate = require('./sakura.json');
const starryTemplate = require('./starry.json');
const metalTemplate = require('./metal.json');
export const templates = {
default: defaultTemplate,
minimal: minimalTemplate,
elegant: elegantTemplate,
cyber: cyberTemplate,
warm: warmTemplate,
forest: forestTemplate,
ocean: oceanTemplate,
sakura: sakuraTemplate,
starry: starryTemplate,
metal: metalTemplate
};

69
src/templates/metal.json Normal file
View file

@ -0,0 +1,69 @@
{
"id": "metal",
"name": "金属科技",
"styles": {
"imagePreview": "background: #1e2228; padding: 32px 28px; border: 1px solid #4d4d4d80;",
"header": {
"avatar": {
"container": "width: 40px; height: 40px; border-radius: 4px; overflow: hidden; box-shadow: 0 4px 16px rgba(192,192,192,0.3); border: 1px solid #4d4d4d80;",
"placeholder": "background: #282c34; transition: all 0.3s ease;",
"image": "object-fit: cover; transition: transform 0.3s ease; filter: contrast(1.1) saturate(1.2);"
},
"nameContainer": "display: flex; align-items: center; gap: 10px;",
"userName": "font-size: 16px; font-weight: 700; color: #f0f0f0; text-shadow: 0 2px 4px rgba(0,0,0,0.3);",
"userId": "font-size: 14px; color: #a0a0a0; font-family: 'SF Pro Display';",
"postTime": "font-size: 13px; color: #808080; letter-spacing: 0.03em;",
"verifiedIcon": "width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; color: #1e2228; background: linear-gradient(135deg, #d0d0d0, #ffffff); border-radius: 4px; font-size: 10px; flex-shrink: 0; box-shadow: 0 2px 8px rgba(0,0,0,0.3); border: 1px solid #4d4d4d80;"
},
"footer": {
"container": "position: absolute; bottom: 0; left: 0; right: 0; display: flex; align-items: center; justify-content: center; gap: 14px; padding: 16px; color: #a0a0a0; font-size: 13px; border-top: 1px solid #4d4d4d80; background: rgba(30,34,40,0.95); backdrop-filter: blur(12px);",
"text": "color: inherit; transition: color 0.2s ease; white-space: nowrap;",
"separator": "color: #808080;"
},
"title": {
"h2": {
"base": "margin: 0; font-size: 1.5em; letter-spacing: -0.01em; line-height: 1.5;",
"content": "font-weight: 700; color: #f0f0f0; text-shadow: 0 2px 4px rgba(0,0,0,0.3);",
"after": ""
},
"h3": {
"base": "margin: 28px 0 0; font-size: 1.25em; line-height: 1.5;",
"content": "font-weight: 700; color: #f0f0f0;",
"after": ""
},
"base": {
"base": "margin: 24px 0 0; font-size: 1.1em; line-height: 1.5;",
"content": "font-weight: 700; color: #f0f0f0;",
"after": ""
}
},
"paragraph": "line-height: 1.8; margin-bottom: 1.1em; font-size: 15px; color: #e0e0e0;",
"emphasis": {
"strong": "font-weight: 700; color: #4d8ee7; text-shadow: 0 2px 4px rgba(77,142,231,0.2);",
"em": "font-style: normal; color: #a0a0a0; background: rgba(160,160,160,0.1); padding: 0 4px; border-left: 2px solid #4d4d4d80;",
"del": "text-decoration: line-through; color: #808080; opacity: 0.8;"
},
"list": {
"container": "padding-left: 26px; margin-bottom: 1.2em; color: #c0c0c0;",
"item": "margin-bottom: 0.7em; font-size: 15px; color: #c0c0c0; line-height: 1.75;",
"taskList": "list-style: none; margin-left: -22px; font-size: 15px; color: #c0c0c0; line-height: 1.75;"
},
"code": {
"block": "background: #282c34; padding: 1.2em; border-radius: 4px; font-size: 14px; font-family: 'JetBrains Mono', monospace; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; color: #e0e0e0; margin: 1.5em 0; border: 1px solid #4d4d4d80; box-shadow: inset 0 4px 12px rgba(0,0,0,0.2);",
"inline": "background: #282c34; padding: 4px 10px; border-radius: 2px; color: #e0e0e0; font-size: 14px; font-family: 'JetBrains Mono', monospace; border: 1px solid #4d4d4d80;"
},
"quote": "border-left: 3px solid #4d8ee7; padding: 0 0 0 20px; margin: 1.5em 0; color: #a0a0a0; font-style: normal; font-size: 15px; line-height: 1.75; background: linear-gradient(to right, rgba(77,142,231,0.1), transparent);",
"image": "max-width: 100%; height: auto; margin: 1.5em auto; border-radius: 4px; box-shadow: 0 8px 24px rgba(0,0,0,0.3); border: 1px solid #4d4d4d80;",
"link": "color: #4d8ee7; text-decoration: none; border-bottom: 1px solid #4d8ee780; transition: all 0.2s ease; text-shadow: 0 2px 4px rgba(77,142,231,0.1);",
"table": {
"container": "width: 100%; margin: 1.5em 0; border-collapse: separate; border-spacing: 0; border-radius: 4px; overflow: hidden; border: 1px solid #4d4d4d80; box-shadow: 0 4px 12px rgba(0,0,0,0.2);",
"header": "background: linear-gradient(135deg, #282c34, #1e2228); font-weight: 700; color: #4d8ee7; padding: 14px; border-bottom: 2px solid #4d4d4d80;",
"cell": "padding: 14px; color: #c0c0c0; border-top: 1px solid #4d4d4d80;"
},
"hr": "border: none; border-top: 2px solid #4d4d4d80; margin: 32px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.3);",
"footnote": {
"ref": "color: #a0a0a0; text-decoration: none; font-size: 0.9em;",
"backref": "color: #a0a0a0; text-decoration: none; font-size: 0.9em;"
}
}
}

View file

@ -0,0 +1,69 @@
{
"id": "minimal",
"name": "极简主题",
"styles": {
"imagePreview": "background: #ffffff; padding: 36px 32px; border: 1px solid #eeeeee;",
"header": {
"avatar": {
"container": "width: 40px; height: 40px; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.05);",
"placeholder": "background: #f5f5f5; transition: all 0.2s ease;",
"image": "object-fit: cover; transition: transform 0.3s ease;"
},
"nameContainer": "display: flex; align-items: center; gap: 8px;",
"userName": "font-size: 16px; font-weight: 600; color: #333333; letter-spacing: 0.02em;",
"userId": "font-size: 14px; color: #666666;",
"postTime": "font-size: 13px; color: #999999;",
"verifiedIcon": "width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; color: #ffffff; background-color: #6c9eb8; border-radius: 4px; font-size: 10px; flex-shrink: 0; box-shadow: 0 2px 4px rgba(108,158,184,0.1);"
},
"footer": {
"container": "position: absolute; bottom: 0; left: 0; right: 0; display: flex; align-items: center; justify-content: center; gap: 16px; padding: 16px; color: #999999; font-size: 13px; border-top: 1px solid #eeeeee; background: rgba(250,250,250,0.95); backdrop-filter: blur(8px);",
"text": "color: inherit; transition: opacity 0.2s ease; white-space: nowrap;",
"separator": "color: #dddddd;"
},
"title": {
"h2": {
"base": "margin: 0; font-size: 1.5em; line-height: 1.5;",
"content": "font-weight: 600; color: #333333;",
"after": ""
},
"h3": {
"base": "margin: 32px 0 0; font-size: 1.25em; line-height: 1.5;",
"content": "font-weight: 600; color: #333333;",
"after": ""
},
"base": {
"base": "margin: 28px 0 0; font-size: 1.1em; line-height: 1.5;",
"content": "font-weight: 600; color: #333333;",
"after": ""
}
},
"paragraph": "line-height: 1.8; margin-bottom: 1.2em; font-size: 15px; color: #444444;",
"emphasis": {
"strong": "font-weight: 700; color: #4a4a4a; background: rgba(74,74,74,0.05); padding: 0 2px;",
"em": "font-style: normal; color: #6c9eb8; background: rgba(108,158,184,0.05); padding: 0 2px;",
"del": "text-decoration: line-through; color: #999999; opacity: 0.8;"
},
"list": {
"container": "padding-left: 28px; margin-bottom: 1.2em; color: #666666;",
"item": "margin-bottom: 0.8em; font-size: 15px; color: #666666; line-height: 1.8;",
"taskList": "list-style: none; margin-left: -20px; font-size: 15px; color: #666666; line-height: 1.8;"
},
"code": {
"block": "background: #f5f5f5; padding: 1.2em; border-radius: 8px; font-size: 14px; font-family: 'SF Mono', Menlo, monospace; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; color: #555555; margin: 1.5em 0; border: 1px solid #eeeeee;",
"inline": "background: #f5f5f5; padding: 4px 8px; border-radius: 4px; color: #555555; font-size: 14px; font-family: 'SF Mono', Menlo, monospace; border: 1px solid #eeeeee;"
},
"quote": "border-left: 3px solid #eeeeee; padding: 0 0 0 20px; margin: 1.5em 0; color: #666666; font-style: normal; font-size: 15px; line-height: 1.8;",
"image": "max-width: 100%; height: auto; margin: 1.5em auto; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.05);",
"link": "color: #6c9eb8; text-decoration: none; border-bottom: 1px solid rgba(108,158,184,0.3); transition: all 0.2s ease;",
"table": {
"container": "width: 100%; margin: 1.5em 0; border-collapse: collapse;",
"header": "background: #f5f5f5; font-weight: 600; color: #333333; border-bottom: 2px solid #eeeeee; padding: 14px;",
"cell": "padding: 14px; color: #666666; border-bottom: 1px solid #eeeeee;"
},
"hr": "border: none; border-top: 1px solid #eeeeee; margin: 32px 0;",
"footnote": {
"ref": "color: #999999; text-decoration: none; font-size: 0.9em;",
"backref": "color: #999999; text-decoration: none; font-size: 0.9em;"
}
}
}

69
src/templates/ocean.json Normal file
View file

@ -0,0 +1,69 @@
{
"id": "ocean",
"name": "深海之境",
"styles": {
"imagePreview": "background: #0a192f; padding: 32px 28px; border: 1px solid #112a4580;",
"header": {
"avatar": {
"container": "width: 40px; height: 40px; border-radius: 14px; overflow: hidden; box-shadow: 0 4px 16px rgba(64,169,255,0.25); border: 1px solid #1d3552;",
"placeholder": "background: linear-gradient(135deg, #112a45, #1d3552); transition: all 0.3s ease;",
"image": "object-fit: cover; transition: transform 0.3s ease; filter: brightness(1.1) contrast(1.05);"
},
"nameContainer": "display: flex; align-items: center; gap: 10px;",
"userName": "font-size: 16px; font-weight: 700; color: #40a9ff; text-shadow: 0 2px 8px rgba(64,169,255,0.2);",
"userId": "font-size: 14px; color: #69c0ff; font-family: 'SF Pro Text';",
"postTime": "font-size: 13px; color: #91d5ff; letter-spacing: 0.03em;",
"verifiedIcon": "width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; color: #0a192f; background: linear-gradient(135deg, #40a9ff, #69c0ff); border-radius: 6px; font-size: 10px; flex-shrink: 0; box-shadow: 0 2px 12px rgba(64,169,255,0.3); border: 1px solid #69c0ff80;"
},
"footer": {
"container": "position: absolute; bottom: 0; left: 0; right: 0; display: flex; align-items: center; justify-content: center; gap: 14px; padding: 16px; color: #69c0ff; font-size: 13px; border-top: 1px solid #1d3552; background: rgba(10,25,47,0.95); backdrop-filter: blur(12px);",
"text": "color: inherit; transition: color 0.2s ease; white-space: nowrap;",
"separator": "color: #91d5ff;"
},
"title": {
"h2": {
"base": "margin: 0; font-size: 1.5em; letter-spacing: -0.01em; line-height: 1.5;",
"content": "font-weight: 700; color: #40a9ff; text-shadow: 0 2px 8px rgba(64,169,255,0.15);",
"after": ""
},
"h3": {
"base": "margin: 28px 0 0; font-size: 1.25em; line-height: 1.5;",
"content": "font-weight: 700; color: #40a9ff;",
"after": ""
},
"base": {
"base": "margin: 24px 0 0; font-size: 1.1em; line-height: 1.5;",
"content": "font-weight: 700; color: #40a9ff;",
"after": ""
}
},
"paragraph": "line-height: 1.8; margin-bottom: 1.2em; font-size: 15px; color: #e6f7ff;",
"emphasis": {
"strong": "font-weight: 700; color: #40a9ff; text-shadow: 0 2px 8px rgba(64,169,255,0.2);",
"em": "font-style: normal; color: #69c0ff; background: rgba(105,192,255,0.1); padding: 0 4px; border-left: 2px solid #69c0ff80;",
"del": "text-decoration: line-through; color: #91d5ff; opacity: 0.8;"
},
"list": {
"container": "padding-left: 28px; margin-bottom: 1.2em; color: #bae7ff;",
"item": "margin-bottom: 0.8em; font-size: 15px; color: #bae7ff; line-height: 1.8;",
"taskList": "list-style: none; margin-left: -24px; font-size: 15px; color: #bae7ff; line-height: 1.8;"
},
"code": {
"block": "background: #112a45; padding: 1.2em; border-radius: 14px; font-size: 14px; font-family: 'SF Mono', Menlo, monospace; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; color: #bae7ff; margin: 1.5em 0; box-shadow: inset 0 4px 12px rgba(0,0,0,0.15), 0 2px 8px rgba(64,169,255,0.1); border: 1px solid #1d3552;",
"inline": "background: #112a45; padding: 4px 10px; border-radius: 6px; color: #bae7ff; font-size: 14px; font-family: 'SF Mono', Menlo, monospace; border: 1px solid #1d3552;"
},
"quote": "border-left: 3px solid #40a9ff; padding: 0 0 0 20px; margin: 1.5em 0; color: #69c0ff; font-style: normal; font-size: 15px; line-height: 1.8; background: linear-gradient(to right, rgba(64,169,255,0.05), transparent);",
"image": "max-width: 100%; height: auto; margin: 1.5em auto; border-radius: 14px; box-shadow: 0 8px 24px rgba(64,169,255,0.25); border: 1px solid #69c0ff80;",
"link": "color: #40a9ff; text-decoration: none; border-bottom: 1px solid rgba(64,169,255,0.3); transition: all 0.2s ease; text-shadow: 0 2px 4px rgba(64,169,255,0.1);",
"table": {
"container": "width: 100%; margin: 1.5em 0; border-collapse: separate; border-spacing: 0; border-radius: 14px; overflow: hidden; border: 1px solid #1d3552; box-shadow: 0 4px 12px rgba(64,169,255,0.15);",
"header": "background: linear-gradient(135deg, #112a45, #0a192f); font-weight: 700; color: #40a9ff; padding: 14px; border-bottom: 2px solid #1d3552;",
"cell": "padding: 14px; color: #bae7ff; border-top: 1px solid #1d3552;"
},
"hr": "border: none; border-top: 2px solid #1d3552; margin: 32px 0; box-shadow: 0 2px 8px rgba(64,169,255,0.15);",
"footnote": {
"ref": "color: #69c0ff; text-decoration: none; font-size: 0.9em;",
"backref": "color: #69c0ff; text-decoration: none; font-size: 0.9em;"
}
}
}

69
src/templates/sakura.json Normal file
View file

@ -0,0 +1,69 @@
{
"id": "sakura",
"name": "樱花飞舞",
"styles": {
"imagePreview": "background: #1f1a1d; padding: 32px 28px; border: 1px solid #ffb6c180;",
"header": {
"avatar": {
"container": "width: 40px; height: 40px; border-radius: 14px; overflow: hidden; box-shadow: 0 4px 16px rgba(255,182,193,0.25); border: 1px solid #ffb6c180;",
"placeholder": "background: linear-gradient(135deg, #2d2428, #3a2d33); transition: all 0.3s ease;",
"image": "object-fit: cover; transition: transform 0.3s ease; filter: brightness(1.05) contrast(1.02);"
},
"nameContainer": "display: flex; align-items: center; gap: 10px;",
"userName": "font-size: 16px; font-weight: 700; color: #ffb6c1; text-shadow: 0 2px 8px rgba(255,182,193,0.2);",
"userId": "font-size: 14px; color: #ff8da1; font-family: 'SF Pro Text';",
"postTime": "font-size: 13px; color: #ff6b88; letter-spacing: 0.03em;",
"verifiedIcon": "width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; color: #1f1a1d; background: linear-gradient(135deg, #ffb6c1, #ff8da1); border-radius: 50%; font-size: 10px; flex-shrink: 0; box-shadow: 0 2px 12px rgba(255,182,193,0.3); border: 1px solid #ffb6c180;"
},
"footer": {
"container": "position: absolute; bottom: 0; left: 0; right: 0; display: flex; align-items: center; justify-content: center; gap: 14px; padding: 16px; color: #ff8da1; font-size: 13px; border-top: 1px solid #ffb6c140; background: rgba(31,26,29,0.95); backdrop-filter: blur(12px);",
"text": "color: inherit; transition: color 0.2s ease; white-space: nowrap;",
"separator": "color: #ff6b88;"
},
"title": {
"h2": {
"base": "margin: 0; font-size: 1.5em; letter-spacing: -0.01em; line-height: 1.5;",
"content": "font-weight: 700; color: #ffb6c1; text-shadow: 0 2px 8px rgba(255,182,193,0.15);",
"after": ""
},
"h3": {
"base": "margin: 28px 0 0; font-size: 1.25em; line-height: 1.5;",
"content": "font-weight: 700; color: #ffb6c1;",
"after": ""
},
"base": {
"base": "margin: 24px 0 0; font-size: 1.1em; line-height: 1.5;",
"content": "font-weight: 700; color: #ffb6c1;",
"after": ""
}
},
"paragraph": "line-height: 1.8; margin-bottom: 1.2em; font-size: 15px; color: #fff5f7;",
"emphasis": {
"strong": "font-weight: 700; color: #ff69b4; text-shadow: 0 2px 8px rgba(255,105,180,0.2);",
"em": "font-style: normal; color: #ff8da1; background: rgba(255,141,161,0.1); padding: 0 4px; border-left: 2px solid #ff8da180;",
"del": "text-decoration: line-through; color: #ff6b88; opacity: 0.8;"
},
"list": {
"container": "padding-left: 28px; margin-bottom: 1.2em; color: #ffd1d9;",
"item": "margin-bottom: 0.8em; font-size: 15px; color: #ffd1d9; line-height: 1.8;",
"taskList": "list-style: none; margin-left: -24px; font-size: 15px; color: #ffd1d9; line-height: 1.8;"
},
"code": {
"block": "background: #2d2428; padding: 1.2em; border-radius: 14px; font-size: 14px; font-family: 'SF Mono', Menlo, monospace; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; color: #ffd1d9; margin: 1.5em 0; box-shadow: inset 0 4px 12px rgba(0,0,0,0.15), 0 2px 8px rgba(255,182,193,0.1); border: 1px solid #ffb6c180;",
"inline": "background: #2d2428; padding: 4px 10px; border-radius: 6px; color: #ffd1d9; font-size: 14px; font-family: 'SF Mono', Menlo, monospace; border: 1px solid #ffb6c180;"
},
"quote": "border-left: 3px solid #ff8da1; padding: 0 0 0 20px; margin: 1.5em 0; color: #ff8da1; font-style: normal; font-size: 15px; line-height: 1.8; background: linear-gradient(to right, rgba(255,141,161,0.05), transparent);",
"image": "max-width: 100%; height: auto; margin: 1.5em auto; border-radius: 14px; box-shadow: 0 8px 24px rgba(255,182,193,0.25); border: 1px solid #ffb6c180;",
"link": "color: #ffb6c1; text-decoration: none; border-bottom: 1px solid rgba(255,182,193,0.3); transition: all 0.2s ease; text-shadow: 0 2px 4px rgba(255,182,193,0.1);",
"table": {
"container": "width: 100%; margin: 1.5em 0; border-collapse: separate; border-spacing: 0; border-radius: 14px; overflow: hidden; border: 1px solid #ffb6c180; box-shadow: 0 4px 12px rgba(255,182,193,0.15);",
"header": "background: linear-gradient(135deg, #2d2428, #1f1a1d); font-weight: 700; color: #ffb6c1; padding: 14px; border-bottom: 2px solid #ffb6c180;",
"cell": "padding: 14px; color: #ffd1d9; border-top: 1px solid #ffb6c180;"
},
"hr": "border: none; border-top: 2px solid #ffb6c180; margin: 32px 0; box-shadow: 0 2px 8px rgba(255,182,193,0.15);",
"footnote": {
"ref": "color: #ff8da1; text-decoration: none; font-size: 0.9em;",
"backref": "color: #ff8da1; text-decoration: none; font-size: 0.9em;"
}
}
}

69
src/templates/starry.json Normal file
View file

@ -0,0 +1,69 @@
{
"id": "starry",
"name": "星空梦境",
"styles": {
"imagePreview": "background: #0d0f1a; padding: 32px 28px; border: 1px solid #9370db80;",
"header": {
"avatar": {
"container": "width: 40px; height: 40px; border-radius: 14px; overflow: hidden; box-shadow: 0 4px 24px rgba(147,112,219,0.3); border: 1px solid #9370db80;",
"placeholder": "background: linear-gradient(135deg, #252640, #1a1b2e); transition: all 0.3s ease;",
"image": "object-fit: cover; transition: transform 0.3s ease; filter: brightness(1.1) contrast(1.05);"
},
"nameContainer": "display: flex; align-items: center; gap: 10px;",
"userName": "font-size: 16px; font-weight: 700; color: #9370db; text-shadow: 0 2px 8px rgba(147,112,219,0.2);",
"userId": "font-size: 14px; color: #b19cd9; font-family: 'SF Pro Text';",
"postTime": "font-size: 13px; color: #8a7bb5; letter-spacing: 0.03em;",
"verifiedIcon": "width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; color: #0d0f1a; background: linear-gradient(135deg, #9370db, #b19cd9); border-radius: 50%; font-size: 10px; flex-shrink: 0; box-shadow: 0 2px 12px rgba(147,112,219,0.3); border: 1px solid #9370db80;"
},
"footer": {
"container": "position: absolute; bottom: 0; left: 0; right: 0; display: flex; align-items: center; justify-content: center; gap: 14px; padding: 16px; color: #b19cd9; font-size: 13px; border-top: 1px solid #252640; background: rgba(13,15,26,0.95); backdrop-filter: blur(12px);",
"text": "color: inherit; transition: color 0.2s ease; white-space: nowrap;",
"separator": "color: #8a7bb5;"
},
"title": {
"h2": {
"base": "margin: 0; font-size: 1.5em; letter-spacing: -0.01em; line-height: 1.5;",
"content": "font-weight: 700; color: #9370db; text-shadow: 0 2px 8px rgba(147,112,219,0.15);",
"after": ""
},
"h3": {
"base": "margin: 28px 0 0; font-size: 1.25em; line-height: 1.5;",
"content": "font-weight: 700; color: #9370db;",
"after": ""
},
"base": {
"base": "margin: 24px 0 0; font-size: 1.1em; line-height: 1.5;",
"content": "font-weight: 700; color: #9370db;",
"after": ""
}
},
"paragraph": "line-height: 1.8; margin-bottom: 1.2em; font-size: 15px; color: #f0e6ff;",
"emphasis": {
"strong": "font-weight: 700; color: #9b59b6; text-shadow: 0 2px 8px rgba(155,89,182,0.2);",
"em": "font-style: normal; color: #b19cd9; background: rgba(177,156,217,0.1); padding: 0 4px; border-left: 2px solid #b19cd980;",
"del": "text-decoration: line-through; color: #8a7bb5; opacity: 0.8;"
},
"list": {
"container": "padding-left: 28px; margin-bottom: 1.2em; color: #d8cef6;",
"item": "margin-bottom: 0.8em; font-size: 15px; color: #d8cef6; line-height: 1.8;",
"taskList": "list-style: none; margin-left: -24px; font-size: 15px; color: #d8cef6; line-height: 1.8;"
},
"code": {
"block": "background: #252640; padding: 1.2em; border-radius: 14px; font-size: 14px; font-family: 'SF Mono', Menlo, monospace; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; color: #d8cef6; margin: 1.5em 0; box-shadow: inset 0 4px 12px rgba(0,0,0,0.15), 0 2px 8px rgba(147,112,219,0.1); border: 1px solid #9370db80;",
"inline": "background: #252640; padding: 4px 10px; border-radius: 6px; color: #d8cef6; font-size: 14px; font-family: 'SF Mono', Menlo, monospace; border: 1px solid #9370db80;"
},
"quote": "border-left: 3px solid #b19cd9; padding: 0 0 0 20px; margin: 1.5em 0; color: #b19cd9; font-style: normal; font-size: 15px; line-height: 1.8; background: linear-gradient(to right, rgba(177,156,217,0.05), transparent);",
"image": "max-width: 100%; height: auto; margin: 1.5em auto; border-radius: 14px; box-shadow: 0 8px 24px rgba(147,112,219,0.25); border: 1px solid #9370db80;",
"link": "color: #9370db; text-decoration: none; background-image: linear-gradient(to right, #9370db80, #b19cd980); background-size: 0% 1px; background-repeat: no-repeat; background-position: 0 100%; transition: all 0.3s ease; text-shadow: 0 2px 4px rgba(147,112,219,0.1);",
"table": {
"container": "width: 100%; margin: 1.5em 0; border-collapse: separate; border-spacing: 0; border-radius: 14px; overflow: hidden; border: 1px solid #9370db80; box-shadow: 0 4px 12px rgba(147,112,219,0.15);",
"header": "background: linear-gradient(135deg, #252640, #0d0f1a); font-weight: 700; color: #9370db; padding: 14px; border-bottom: 2px solid #9370db80;",
"cell": "padding: 14px; color: #d8cef6; border-top: 1px solid #9370db80;"
},
"hr": "border: none; border-top: 2px solid #9370db80; margin: 32px 0; box-shadow: 0 2px 8px rgba(147,112,219,0.15);",
"footnote": {
"ref": "color: #b19cd9; text-decoration: none; font-size: 0.9em;",
"backref": "color: #b19cd9; text-decoration: none; font-size: 0.9em;"
}
}
}

69
src/templates/warm.json Normal file
View file

@ -0,0 +1,69 @@
{
"id": "warm",
"name": "暖阳文艺",
"styles": {
"imagePreview": "background: #fffaf5; padding: 32px 28px; border: 1px solid #b8733380;",
"header": {
"avatar": {
"container": "width: 42px; height: 42px; border-radius: 14px; overflow: hidden; box-shadow: 0 4px 16px rgba(184,115,51,0.1); border: 1px solid #b8733380;",
"placeholder": "background: linear-gradient(135deg, #fff0e6, #ffe4c4); transition: all 0.3s ease;",
"image": "object-fit: cover; transition: transform 0.3s ease; filter: brightness(1.05) contrast(1.02);"
},
"nameContainer": "display: flex; align-items: center; gap: 8px;",
"userName": "font-size: 17px; font-weight: 600; color: #8b4513; font-family: 'Noto Serif SC', serif; text-shadow: 0 2px 4px rgba(139,69,19,0.1);",
"userId": "font-size: 14px; color: #b87333; font-family: 'Noto Serif SC', serif;",
"postTime": "font-size: 13px; color: #d2691e; font-style: italic;",
"verifiedIcon": "width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; color: #fffaf5; background: linear-gradient(135deg, #d2691e, #b87333); border-radius: 50%; font-size: 10px; flex-shrink: 0; box-shadow: 0 2px 8px rgba(210,105,30,0.2); border: 1px solid #b8733380;"
},
"footer": {
"container": "position: absolute; bottom: 0; left: 0; right: 0; display: flex; align-items: center; justify-content: center; gap: 14px; padding: 16px; color: #b87333; font-size: 13px; border-top: 1px solid #b8733380; background: rgba(255,250,245,0.95); backdrop-filter: blur(8px); font-family: 'Noto Serif SC', serif;",
"text": "color: inherit; transition: color 0.2s ease; font-style: italic; white-space: nowrap;",
"separator": "color: #deb887;"
},
"title": {
"h2": {
"base": "margin: 0; font-size: 1.6em; letter-spacing: 0.01em; line-height: 1.5;",
"content": "font-weight: 600; color: #8b4513; font-family: 'Noto Serif SC', serif;",
"after": ""
},
"h3": {
"base": "margin: 28px 0 0; font-size: 1.3em; line-height: 1.5;",
"content": "font-weight: 600; color: #8b4513; font-family: 'Noto Serif SC', serif;",
"after": ""
},
"base": {
"base": "margin: 24px 0 0; font-size: 1.15em; line-height: 1.5;",
"content": "font-weight: 600; color: #8b4513; font-family: 'Noto Serif SC', serif;",
"after": ""
}
},
"paragraph": "line-height: 1.75; margin-bottom: 1.1em; font-size: 15px; color: #5a4a42;",
"emphasis": {
"strong": "font-weight: 600; color: #d2691e; text-shadow: 0 2px 4px rgba(210,105,30,0.1);",
"em": "font-style: normal; color: #b87333; background: rgba(184,115,51,0.1); padding: 0 4px; border-left: 2px solid #b8733380;",
"del": "text-decoration: line-through; color: #8b4513; opacity: 0.8;"
},
"list": {
"container": "padding-left: 26px; margin-bottom: 1.2em; color: #6b4423;",
"item": "margin-bottom: 0.8em; font-size: 15px; color: #6b4423; line-height: 1.85; font-family: 'Noto Serif SC', serif;",
"taskList": "list-style: none; margin-left: -22px; font-size: 15px; color: #6b4423; line-height: 1.85; font-family: 'Noto Serif SC', serif;"
},
"code": {
"block": "background: #fff6e9; padding: 1.2em; border-radius: 8px; font-size: 14px; font-family: 'Fira Code', monospace; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; color: #8b4513; margin: 1.3em 0; border: 1px solid #b8733380; box-shadow: inset 0 4px 12px rgba(184,115,51,0.05), 0 2px 8px rgba(184,115,51,0.05);",
"inline": "background: #fff6e9; padding: 4px 10px; border-radius: 4px; color: #8b4513; font-size: 14px; font-family: 'Fira Code', monospace; border: 1px solid #b8733380;"
},
"quote": "border-left: 3px solid #deb887; padding: 0 0 0 20px; margin: 1.3em 0; color: #b87333; font-style: italic; font-size: 15px; line-height: 1.85; font-family: 'Noto Serif SC', serif; background: linear-gradient(to right, rgba(222,184,135,0.1), transparent);",
"image": "max-width: 100%; height: auto; margin: 1.5em auto; border-radius: 12px; box-shadow: 0 4px 16px rgba(184,115,51,0.1); border: 1px solid #b8733380;",
"link": "color: #d2691e; text-decoration: none; background-image: linear-gradient(to right, #d2691e80, #b8733380); background-size: 0% 1px; background-repeat: no-repeat; background-position: 0 100%; transition: all 0.3s ease; font-family: 'Noto Serif SC', serif;",
"table": {
"container": "width: 100%; margin: 1.4em 0; border-collapse: separate; border-spacing: 0; border: 1px solid #b8733380; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 12px rgba(184,115,51,0.05);",
"header": "background: linear-gradient(135deg, #fff6e9, #fffaf5); font-weight: 600; color: #8b4513; padding: 14px; font-family: 'Noto Serif SC', serif; border-bottom: 2px solid #b8733380;",
"cell": "padding: 14px; color: #6b4423; border-top: 1px solid #b8733380; font-family: 'Noto Serif SC', serif;"
},
"hr": "border: none; border-top: 2px solid #b8733380; margin: 28px 0; box-shadow: 0 2px 8px rgba(184,115,51,0.05);",
"footnote": {
"ref": "color: #b87333; text-decoration: none; font-size: 0.9em; font-style: italic;",
"backref": "color: #b87333; text-decoration: none; font-size: 0.9em; font-style: italic;"
}
}
}

680
src/view.ts Normal file
View file

@ -0,0 +1,680 @@
import { ItemView, WorkspaceLeaf, MarkdownRenderer, TFile } from 'obsidian';
import { RedConverter } from './converter';
import { DownloadManager } from './downloadManager';
import type { TemplateManager } from './templateManager';
import { DonateManager } from './donateManager';
import type { SettingsManager } from './settings';
import { PreviewManager } from './previewManager';
export const VIEW_TYPE_RED = 'note-to-red';
export class RedView extends ItemView {
private previewEl: HTMLElement;
private currentFile: TFile | null = null;
private updateTimer: NodeJS.Timeout | null = null;
private isPreviewLocked: boolean = false;
private lockButton: HTMLButtonElement;
private copyButton: HTMLButtonElement;
private templateManager: TemplateManager;
private settingsManager: SettingsManager;
private customTemplateSelect: HTMLElement;
private customFontSelect: HTMLElement;
private fontSizeSelect: HTMLInputElement;
private customBackgroundSelect: HTMLElement;
private currentImageIndex: number = 0;
private navigationButtons: {
prev: HTMLButtonElement;
next: HTMLButtonElement;
indicator: HTMLElement;
} | undefined;
private previewManager: PreviewManager;
constructor(
leaf: WorkspaceLeaf,
templateManager: TemplateManager,
settingsManager: SettingsManager
) {
super(leaf);
this.templateManager = templateManager;
this.settingsManager = settingsManager;
this.previewManager = new PreviewManager(settingsManager);
}
getViewType() {
return VIEW_TYPE_RED;
}
getDisplayText() {
return '小红书预览';
}
getIcon() {
return 'image';
}
async onOpen() {
const container = this.containerEl.children[1];
container.empty();
const toolbar = container.createEl('div', { cls: 'red-toolbar' });
// 锁定按钮
this.lockButton = toolbar.createEl('button', {
cls: 'red-lock-button',
attr: { 'aria-label': '关闭实时预览状态' }
});
this.lockButton.innerHTML = '🔓';
this.lockButton.addEventListener('click', () => this.togglePreviewLock());
// 创建中间控件容器
const controlsGroup = toolbar.createEl('div', { cls: 'red-controls-group' });
// 创建自定义下拉选择器
this.customTemplateSelect = this.createCustomSelect(
controlsGroup,
'red-template-select',
await this.getTemplateOptions()
);
this.customTemplateSelect.id = 'template-select';
// 添加模板选择器的 change 事件监听
this.customTemplateSelect.querySelector('.red-select')?.addEventListener('change', async (e: any) => {
const value = e.detail.value;
this.templateManager.setCurrentTemplate(value);
await this.settingsManager.updateSettings({
templateId: value
});
this.templateManager.applyTemplate(this.previewEl);
});
this.customFontSelect = this.createCustomSelect(
controlsGroup,
'red-font-select',
this.getFontOptions()
);
// 添加字体选择器的 change 事件监听
this.customFontSelect.querySelector('.red-select')?.addEventListener('change', async (e: any) => {
const value = e.detail.value;
this.templateManager.setFont(value);
await this.settingsManager.updateSettings({
fontFamily: value
});
this.templateManager.applyTemplate(this.previewEl);
});
this.customFontSelect.id = 'font-select';
// 字号调整
const fontSizeGroup = controlsGroup.createEl('div', { cls: 'red-font-size-group' });
const decreaseButton = fontSizeGroup.createEl('button', {
cls: 'red-font-size-btn',
text: '-'
});
this.fontSizeSelect = fontSizeGroup.createEl('input', {
cls: 'red-font-size-input',
type: 'text',
value: '16',
attr: {
style: 'border: none; outline: none; background: transparent;'
}
});
const increaseButton = fontSizeGroup.createEl('button', {
cls: 'red-font-size-btn',
text: '+'
});
// 从设置中恢复上次的选择
const settings = this.settingsManager.getSettings();
// 恢复设置
if (settings.templateId) {
const templateSelect = this.customTemplateSelect.querySelector('.red-select-text');
const templateDropdown = this.customTemplateSelect.querySelector('.red-select-dropdown');
if (templateSelect && templateDropdown) {
const option = await this.getTemplateOptions();
const selected = option.find(o => o.value === settings.templateId);
if (selected) {
// 更新选中文本和值
templateSelect.textContent = selected.label;
this.customTemplateSelect.querySelector('.red-select')?.setAttribute('data-value', selected.value);
// 更新下拉列表中的选中状态
templateDropdown.querySelectorAll('.red-select-item').forEach(el => {
if (el.getAttribute('data-value') === selected.value) {
el.classList.add('red-selected');
} else {
el.classList.remove('red-selected');
}
});
}
}
this.templateManager.setCurrentTemplate(settings.templateId);
}
if (settings.fontFamily) {
const fontSelect = this.customFontSelect.querySelector('.red-select-text');
const fontDropdown = this.customFontSelect.querySelector('.red-select-dropdown');
if (fontSelect && fontDropdown) {
const option = this.getFontOptions();
const selected = option.find(o => o.value === settings.fontFamily);
if (selected) {
// 更新选中文本和值
fontSelect.textContent = selected.label;
this.customFontSelect.querySelector('.red-select')?.setAttribute('data-value', selected.value);
// 更新下拉列表中的选中状态
fontDropdown.querySelectorAll('.red-select-item').forEach(el => {
if (el.getAttribute('data-value') === selected.value) {
el.classList.add('red-selected');
} else {
el.classList.remove('red-selected');
}
});
}
}
this.templateManager.setFont(settings.fontFamily);
}
if (settings.fontSize) {
this.fontSizeSelect.value = settings.fontSize.toString();
this.templateManager.setFontSize(settings.fontSize);
}
// 更新字号调整事件
const updateFontSize = async () => {
const size = parseInt(this.fontSizeSelect.value);
this.templateManager.setFontSize(size);
await this.settingsManager.updateSettings({
fontSize: size
});
this.templateManager.applyTemplate(this.previewEl);
};
// 字号调整按钮事件
decreaseButton.addEventListener('click', () => {
const currentSize = parseInt(this.fontSizeSelect.value);
if (currentSize > 12) {
this.fontSizeSelect.value = (currentSize - 1).toString();
updateFontSize();
}
});
increaseButton.addEventListener('click', () => {
const currentSize = parseInt(this.fontSizeSelect.value);
if (currentSize < 30) {
this.fontSizeSelect.value = (currentSize + 1).toString();
updateFontSize();
}
});
this.fontSizeSelect.addEventListener('change', updateFontSize);
// 预览区域
this.previewEl = container.createEl('div', { cls: 'red-preview-wrapper' });
// 底部工具栏
const bottomBar = container.createEl('div', { cls: 'red-bottom-bar' });
// 添加使用说明按钮
const helpButton = bottomBar.createEl('button', {
cls: 'red-help-button',
attr: { 'aria-label': '使用指南' }
});
helpButton.innerHTML = '❓';
// 更新帮助文本
const tooltip = bottomBar.createEl('div', {
cls: 'red-help-tooltip',
text: `使用指南:
1. (##)
2. 20-24px使
3. 14-16px后单独导出
4.
5.
6. (🔓)
7. `
});
// 创建中间控件容器
const bottomControlsGroup = bottomBar.createEl('div', { cls: 'red-bottom-controls-group' });
// 请作者喝咖啡按钮
const likeButton = bottomControlsGroup.createEl('button', {
cls: 'red-like-button'
});
likeButton.innerHTML = '<span style="margin-right: 4px">❤️</span>关于作者';
likeButton.addEventListener('click', () => {
DonateManager.showDonateModal(this.containerEl);
});
// 单张下载按钮
const singleDownloadButton = bottomControlsGroup.createEl('button', {
text: '下载当前页',
cls: 'red-export-button'
});
singleDownloadButton.addEventListener('click', async () => {
if (this.previewEl) {
singleDownloadButton.disabled = true;
singleDownloadButton.setText('导出中...');
try {
await DownloadManager.downloadSingleImage(this.previewEl);
singleDownloadButton.setText('导出成功');
setTimeout(() => {
singleDownloadButton.disabled = false;
singleDownloadButton.setText('下载当前页');
}, 2000);
} catch (error) {
singleDownloadButton.setText('导出失败');
setTimeout(() => {
singleDownloadButton.disabled = false;
singleDownloadButton.setText('下载当前页');
}, 2000);
}
}
});
// 更新按钮文本和类名
this.copyButton = bottomControlsGroup.createEl('button', {
text: '导出全部页',
cls: 'red-export-button'
});
// 添加导出按钮点击事件
this.copyButton.addEventListener('click', async () => {
if (this.previewEl) {
this.copyButton.disabled = true;
this.copyButton.setText('导出中...');
try {
await DownloadManager.downloadAllImages(this.previewEl);
this.copyButton.setText('导出成功');
setTimeout(() => {
this.copyButton.disabled = false;
this.copyButton.setText('导出全部页');
}, 2000);
} catch (error) {
this.copyButton.setText('导出失败');
setTimeout(() => {
this.copyButton.disabled = false;
this.copyButton.setText('导出全部页');
}, 2000);
}
}
});
const newButton = bottomControlsGroup.createEl('button', {
text: '敬请期待',
cls: 'red-feature-button'
});
// 监听文档变化
this.registerEvent(
this.app.workspace.on('file-open', this.onFileOpen.bind(this))
);
// 监听文档内容变化
this.registerEvent(
this.app.vault.on('modify', this.onFileModify.bind(this))
);
// 检查当前打开的文件
const currentFile = this.app.workspace.getActiveFile();
await this.onFileOpen(currentFile);
}
private updateControlsState(enabled: boolean) {
this.lockButton.disabled = !enabled;
// 更新自定义选择器的禁用状态
const templateSelect = this.customTemplateSelect.querySelector('.red-select');
const fontSelect = this.customFontSelect.querySelector('.red-select');
if (templateSelect) {
templateSelect.classList.toggle('disabled', !enabled);
templateSelect.setAttribute('style', `pointer-events: ${enabled ? 'auto' : 'none'}`);
}
if (fontSelect) {
fontSelect.classList.toggle('disabled', !enabled);
fontSelect.setAttribute('style', `pointer-events: ${enabled ? 'auto' : 'none'}`);
}
// 字号相关控件
this.fontSizeSelect.disabled = !enabled;
const fontSizeButtons = this.containerEl.querySelectorAll('.red-font-size-btn');
fontSizeButtons.forEach(button => {
(button as HTMLButtonElement).disabled = !enabled;
});
// 导出按钮
this.copyButton.disabled = !enabled;
const singleDownloadButton = this.containerEl.querySelector('.red-export-button');
if (singleDownloadButton) {
(singleDownloadButton as HTMLButtonElement).disabled = !enabled;
}
}
async onFileOpen(file: TFile | null) {
this.currentFile = file;
this.currentImageIndex = 0; // 重置图片索引
if (!file || file.extension !== 'md') {
this.previewEl.empty();
this.previewEl.createEl('div', {
text: '只能预览 Markdown 文本文档',
cls: 'red-empty-state'
});
this.updateControlsState(false);
return;
}
this.updateControlsState(true);
this.isPreviewLocked = false;
this.lockButton.innerHTML = '🔓';
await this.updatePreview();
}
private async togglePreviewLock() {
this.isPreviewLocked = !this.isPreviewLocked;
const lockIcon = this.isPreviewLocked ? '🔒' : '🔓';
const lockStatus = this.isPreviewLocked ? '开启实时预览状态' : '关闭实时预览状态';
this.lockButton.innerHTML = lockIcon;
this.lockButton.setAttribute('aria-label', lockStatus);
if (!this.isPreviewLocked) {
await this.updatePreview();
}
}
async onFileModify(file: TFile) {
if (file === this.currentFile && !this.isPreviewLocked) {
if (this.updateTimer) {
clearTimeout(this.updateTimer);
}
this.updateTimer = setTimeout(() => {
this.updatePreview();
}, 500);
}
}
async updatePreview() {
if (!this.currentFile) return;
this.previewEl.empty();
const content = await this.app.vault.read(this.currentFile);
// 渲染 Markdown 内容
await MarkdownRenderer.renderMarkdown(
content,
this.previewEl,
this.currentFile.path,
this
);
// 转换内容并检查有效性
RedConverter.formatContent(this.previewEl);
const hasValidContent = RedConverter.hasValidContent(this.previewEl);
// 更新所有控件状态
this.updateControlsState(hasValidContent);
if (!hasValidContent) {
this.copyButton.setAttribute('title', '请先添加二级标题内容');
} else {
this.copyButton.removeAttribute('title');
}
// 添加用户信息到页头
const header = this.previewEl.querySelector('.red-preview-header');
if (header) {
this.previewManager.createHeaderContent(
header as HTMLElement,
() => this.handleAvatarClick(),
(el) => this.handleUserNameEdit(el),
(el) => this.handleUserIdEdit(el)
);
}
// 添加页脚内容
const footer = this.previewEl.querySelector('.red-preview-footer');
if (footer) {
this.previewManager.createFooterContent(
footer as HTMLElement,
(el, position) => this.handleFooterTextEdit(el, position)
);
}
// 应用模板和背景
this.templateManager.applyTemplate(this.previewEl);
// 获取所有内容区块
const sections = this.previewEl.querySelectorAll('.red-content-section');
if (sections.length === 0) return;
// 创建导航按钮
const navButtons = this.previewManager.createNavigationButtons(
this.previewEl,
sections.length,
(direction) => this.navigateImages(direction)
);
this.navigationButtons = navButtons;
// 显示当前图片
this.previewManager.showImage(this.currentImageIndex, sections, this.navigationButtons);
}
// 处理头像点击
private async handleAvatarClick() {
// 创建文件选择器
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.addEventListener('change', async () => {
const file = input.files?.[0];
if (file) {
try {
// 转换为 base64
const reader = new FileReader();
reader.onload = async (e) => {
const base64 = e.target?.result as string;
// 更新设置
await this.settingsManager.updateSettings({
userAvatar: base64
});
// 刷新预览
await this.updatePreview();
};
reader.readAsDataURL(file);
} catch (error) {
console.error('头像更新失败:', error);
}
}
});
input.click();
}
// 处理用户名编辑
private async handleUserNameEdit(element: HTMLElement) {
const input = document.createElement('input');
input.value = element.textContent || '';
input.className = 'red-user-edit-input';
input.placeholder = '请输入用户名';
element.replaceWith(input);
input.focus();
const handleBlur = async () => {
const newName = input.value.trim();
await this.settingsManager.updateSettings({
userName: newName || '夜半' // 设置默认值
});
await this.updatePreview();
input.remove();
};
input.addEventListener('blur', handleBlur);
input.addEventListener('keypress', async (e) => {
if (e.key === 'Enter') {
await handleBlur();
}
});
}
// 处理用户ID编辑
private async handleUserIdEdit(element: HTMLElement) {
const input = document.createElement('input');
input.value = element.textContent || '';
input.className = 'red-user-edit-input';
input.placeholder = '请输入用户ID';
element.replaceWith(input);
input.focus();
const handleBlur = async () => {
let newId = input.value.trim();
if (!newId) {
newId = '@Yeban'; // 设置默认值
} else if (!newId.startsWith('@')) {
newId = '@' + newId;
}
await this.settingsManager.updateSettings({
userId: newId
});
await this.updatePreview();
input.remove();
};
input.addEventListener('blur', handleBlur);
input.addEventListener('keypress', async (e) => {
if (e.key === 'Enter') {
await handleBlur();
}
});
}
// ... existing code ...
private async handleFooterTextEdit(element: HTMLElement, position: 'left' | 'right') {
const input = document.createElement('input');
input.value = element.textContent || '';
input.className = 'red-user-edit-input';
input.placeholder = '请输入页脚文本';
element.replaceWith(input);
input.focus();
const handleBlur = async () => {
const newText = input.value.trim();
await this.settingsManager.updateSettings({
[`footer${position === 'left' ? 'Left' : 'Right'}Text`]: newText ||
(position === 'left' ? '夜半过后,光明便启程' : '欢迎关注公众号:夜半')
});
await this.updatePreview();
input.remove();
};
input.addEventListener('blur', handleBlur);
input.addEventListener('keypress', async (e) => {
if (e.key === 'Enter') {
await handleBlur();
}
});
}
// 修改 navigateImages 方法
private navigateImages(direction: 'prev' | 'next') {
const sections = this.previewEl.querySelectorAll('.red-content-section');
if (direction === 'prev' && this.currentImageIndex > 0) {
this.currentImageIndex--;
} else if (direction === 'next' && this.currentImageIndex < sections.length - 1) {
this.currentImageIndex++;
}
this.previewManager.showImage(this.currentImageIndex, sections, this.navigationButtons);
}
// 添加自定义下拉选择器创建方法
private createCustomSelect(
parent: HTMLElement,
className: string,
options: { value: string; label: string }[]
) {
const container = parent.createEl('div', { cls: `red-select-container ${className}` });
const select = container.createEl('div', { cls: 'red-select' });
const selectedText = select.createEl('span', { cls: 'red-select-text' });
const arrow = select.createEl('span', { cls: 'red-select-arrow', text: '▾' });
const dropdown = container.createEl('div', { cls: 'red-select-dropdown' });
options.forEach(option => {
const item = dropdown.createEl('div', {
cls: 'red-select-item',
text: option.label
});
item.dataset.value = option.value;
item.addEventListener('click', () => {
dropdown.querySelectorAll('.red-select-item').forEach(el =>
el.classList.remove('red-selected'));
item.classList.add('red-selected');
selectedText.textContent = option.label;
select.dataset.value = option.value;
dropdown.classList.remove('red-show');
select.dispatchEvent(new CustomEvent('change', {
detail: { value: option.value }
}));
});
});
// 设置默认值和选中状态
if (options.length > 0) {
selectedText.textContent = options[0].label;
select.dataset.value = options[0].value;
dropdown.querySelector('.red-select-item')?.classList.add('red-selected');
}
// 点击显示/隐藏下拉列表
select.addEventListener('click', (e) => {
e.stopPropagation();
dropdown.classList.toggle('red-show');
});
// 点击其他地方关闭下拉列表
document.addEventListener('click', () => {
dropdown.classList.remove('red-show');
});
return container;
}
// 获取模板选项
private async getTemplateOptions() {
await this.templateManager.loadTemplates();
const templates = this.templateManager.getAllTemplates();
return templates.length > 0
? templates.map(t => ({ value: t.id, label: t.name }))
: [{ value: 'default', label: '默认模板' }];
}
// 获取字体选项
private getFontOptions() {
return [
{
value: 'Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, "PingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif',
label: '默认字体'
},
{
value: 'SimSun, "宋体", serif',
label: '宋体'
},
{
value: 'SimHei, "黑体", sans-serif',
label: '黑体'
},
{
value: 'KaiTi, "楷体", serif',
label: '楷体'
},
{
value: '"Microsoft YaHei", "微软雅黑", sans-serif',
label: '雅黑'
}
];
}
}

736
styles.css Normal file
View file

@ -0,0 +1,736 @@
/* 状态栏样式覆盖 */
.workspace-leaf-content .view-content {
padding: 10px 10px 13px 10px;
}
/* 工具栏基础样式 */
.red-toolbar, .red-bottom-bar {
padding: 20px 32px;
position: sticky;
border-radius: 6px;
z-index: 100;
display: flex;
align-items: center;
min-width: 680px;
}
.red-toolbar {
top: 0;
background: rgba(82, 144, 220, 0.03);
border-bottom: 1px solid rgba(82, 144, 220, 0.1);
}
.red-bottom-bar {
bottom: 0;
background: rgba(82, 144, 220, 0.02);
border-top: 1px solid rgba(82, 144, 220, 0.1);
}
/* 控件组布局 */
.red-controls-group, .red-bottom-controls-group {
display: flex;
gap: 12px;
align-items: center;
margin: 0 auto;
width: fit-content;
min-width: fit-content;
margin-left: 40px;
}
/* 锁定按钮 */
.red-lock-button {
width: 32px;
height: 32px;
border: 1px solid #e8e8e8;
border-radius: 8px;
background: white;
color: #666;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
font-size: 16px;
box-shadow: 0 1px 3px rgba(82, 144, 220, 0.1);
}
.red-lock-button:hover {
background: #f5f8fc;
border-color: #5C5F77;
color: #5C5F77;
box-shadow: 0 2px 6px rgba(82, 144, 220, 0.15);
}
/* 下拉选择器 */
.red-select-container {
position: relative;
width: 160px;
}
.red-select {
height: 32px;
padding: 0 12px;
border: 1px solid #5C5F77;
border-radius: 8px;
background: white;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
user-select: none;
transition: all 0.2s ease;
color: #5C5F77;
box-shadow: 0 1px 3px rgba(82, 144, 220, 0.1);
}
.red-select:hover {
background: #f5f8fc;
box-shadow: 0 2px 6px rgba(82, 144, 220, 0.15);
}
.red-select.disabled {
opacity: 0.5;
background: #f5f5f5 !important;
color: #999 !important;
border-color: #e8e8e8 !important;
cursor: not-allowed;
box-shadow: none;
}
.red-select-arrow {
color: #5C5F77;
font-size: 12px;
transition: transform 0.2s ease;
}
.red-select-dropdown {
position: absolute;
top: calc(100% + 4px);
left: 0;
width: 100%;
background: white;
border: 1px solid #e8e8e8;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(82, 144, 220, 0.15);
display: none;
z-index: 1000;
overflow: hidden;
}
.red-select-dropdown.red-show {
display: block;
}
.red-select-item {
padding: 8px 12px;
cursor: pointer;
transition: all 0.2s ease;
color: #333;
}
.red-select-item:hover {
background: #f5f8fc;
}
.red-select-item.red-selected {
background-color: #f5f8fc;
color: #5C5F77;
}
/* 字号调整组件 */
.red-font-size-group {
width: 120px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
height: 32px;
background: white;
border: 1px solid #5C5F77;
border-radius: 8px;
padding: 0;
overflow: hidden;
box-shadow: 0 1px 3px rgba(82, 144, 220, 0.1);
}
.red-font-size-btn {
width: 55px;
height: 32px;
border: none;
background: white;
color: #5C5F77;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
font-size: 16px;
}
.red-font-size-btn:hover {
background: rgba(82, 144, 220, 0.05);
}
.red-font-size-input {
width: 40px;
text-align: center;
font-size: 16px;
padding: 0;
margin: 0;
border: none;
background: transparent;
color: #5C5F77;
outline: none;
}
/* 预览区域 */
.red-preview-wrapper {
padding: 10px 20px 20px 20px;
margin: 10px;
height: calc(100% - 180px);
overflow-y: auto;
background: white;
flex: 1;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
border: 1px solid rgba(82, 144, 220, 0.1);
}
.red-empty-state {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #999;
font-size: 16px;
}
/* 帮助按钮 */
.red-help-button {
width: 32px;
height: 32px;
border: 1px solid #e8e8e8;
border-radius: 8px;
background: white;
color: #666;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
font-size: 16px;
box-shadow: 0 1px 3px rgba(82, 144, 220, 0.1);
}
.red-help-button:hover {
background: #f5f8fc;
border-color: #5C5F77;
color: #5C5F77;
box-shadow: 0 2px 6px rgba(82, 144, 220, 0.15);
}
/* 提示框位置调整 */
.red-help-tooltip {
position: absolute;
left: 32px;
bottom: 48px;
width: 450px;
padding: 12px 16px;
background: white;
border: 2px solid rgba(82, 144, 220, 0.1);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
font-size: 13px;
line-height: 1.6;
color: #5290DC;
display: none;
white-space: pre-line;
z-index: 1000;
}
.red-help-button:hover + .red-help-tooltip {
display: block;
}
/* 底部按钮 */
.red-bottom-controls-group button {
padding: 8px 24px;
width: 120px;
height: 36px;
border-radius: 8px;
border: 1px solid #5C5F77;
background: white;
color: #5C5F77;
cursor: pointer;
transition: all 0.2s ease;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
box-shadow: 0 1px 3px rgba(82, 144, 220, 0.1);
}
.red-bottom-controls-group button:hover {
background: rgba(82, 144, 220, 0.05);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(82, 144, 220, 0.2);
}
.red-export-button:not(:disabled) {
background: #5290DC !important;
color: white !important;
border: none !important;
box-shadow: 0 2px 6px rgba(82, 144, 220, 0.2) !important;
}
.red-export-button:not(:disabled):hover {
background: #4a83c8 !important;
box-shadow: 0 4px 12px rgba(82, 144, 220, 0.3) !important;
}
.red-feature-button {
opacity: 0.5;
border-color: #5C5F77 !important;
background: white !important;
color: #5C5F77 !important;
cursor: not-allowed !important;
}
.red-bottom-controls-group button:disabled {
opacity: 0.5;
background: white !important;
color: #5C5F77 !important;
cursor: not-allowed !important;
transform: none;
border-color: #5C5F77 !important;
box-shadow: none;
}
/* 关于作者的框 */
.red-about-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
/* 弹框主体 */
.red-about-modal {
background: white;
border-radius: 12px;
padding: 20px 20px;
width: 520px;
max-height: 90vh;
position: relative;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
}
/* 自定义滚动条样式 */
.red-about-modal::-webkit-scrollbar {
width: 8px;
}
.red-about-modal::-webkit-scrollbar-track {
background: #f5f5f5;
border-radius: 4px;
}
.red-about-modal::-webkit-scrollbar-thumb {
background: #ddd;
border-radius: 4px;
}
.red-about-modal::-webkit-scrollbar-thumb:hover {
background: #ccc;
}
/* 关闭按钮 */
.red-about-close {
position: absolute;
top: 7px;
right: 7px;
width: 28px;
height: 28px;
background: transparent;
border: none;
border-radius: 6px;
font-size: 17px;
color: #bfbfbf;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
/* macOS 系统下的样式 */
@media screen and (-webkit-min-device-pixel-ratio: 0) and (min-color-index:0) {
.red-about-close {
right: auto;
left: 7px;
}
}
.red-about-close:hover {
background: #f5f5f5;
color: #595959;
}
/* 内容区块 */
.red-about-section {
padding: 10px;
margin: 8px 20px 0 20px;
border-radius: 8px;
}
.red-about-intro-section {
background: #fafafa;
border: 1px solid #f0f0f0;
}
.red-about-donate-section {
background: #fff9f5;
border: 1px solid #ffe4d9;
}
.red-about-mp-section {
background: #f5fff7;
border: 1px solid #e6f7e9;
}
/* 文字样式 */
.red-about-title {
font-size: 20px;
font-weight: 600;
color: #2c3e50;
margin: 0 0 1px;
text-align: center;
letter-spacing: 0.5px;
}
.red-about-name {
color: #5290DC;
font-weight: 600;
font-size: 16px;
padding: 0 2px;
}
.red-about-identity {
color: #34495e;
font-weight: 500;
background: rgba(8, 98, 209, 0.08);
padding: 0 4px;
border-radius: 3px;
}
.red-about-highlight {
color: #e67e22;
font-weight: 500;
}
.red-about-value {
color: #16a085;
font-weight: 500;
}
.red-about-subtitle {
font-size: 18px;
color: #d35400;
margin: 8px 0 12px;
letter-spacing: 0.3px;
font-weight: 600;
}
.red-about-intro {
font-size: 15px;
color: #2c3e50;
margin: 8px 0;
line-height: 1.8;
letter-spacing: 0.2px;
}
.red-about-role,
.red-about-desc {
font-size: 14.5px;
color: #34495e;
margin: 6px 0;
line-height: 1.8;
letter-spacing: 0.2px;
}
.red-about-emphasis {
font-weight: 500;
color: #c0392b;
}
.red-about-footer {
font-size: 15px;
color: #2c3e50;
text-align: center;
margin: 1px 0 0;
padding: 1px 1px;
background: #f8f9fa;
border-radius: 8px;
font-weight: 500;
letter-spacing: 0.3px;
line-height: 1.6;
}
.red-about-footer strong {
color: #8e44ad;
font-weight: 600;
}
/* 二维码容器 */
.red-about-qr {
width: 100%;
height: 150px;
margin: 5px 0;
padding: 5px;
background: white;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.red-about-qr img {
width: 100%;
height: 100%;
object-fit: contain;
}
/* 小红书预览容器 */
.red-preview-container {
position: relative;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: flex-start;
background: #f5f5f5;
border-radius: 12px;
padding: 20px;
overflow: auto;
}
/* 图片预览区域 */
.red-image-preview {
width: 100%;
max-width: 450px;
aspect-ratio: 3/4;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 20px;
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 内容区域容器 */
.red-content-wrapper {
flex: 1;
overflow: hidden;
}
/* 导航按钮 */
.red-nav-button {
width: 40px;
height: 40px;
border: none;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
color: #333;
font-size: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.red-nav-button:hover {
background: white;
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* 页码指示器 */
.red-page-indicator {
padding: 6px 12px;
background: rgba(255, 255, 255, 0.9);
border-radius: 16px;
font-size: 14px;
color: #333;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 导航容器 */
.red-nav-container {
position: absolute;
bottom: 20px;
left: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
z-index: 10;
}
/* 内容区块样式 */
.red-content-section {
display: none;
margin: 0 13px;
}
.red-content-section[data-index="0"] {
display: block;
}
/* 空状态提示 */
.red-empty-message {
text-align: center;
padding: 40px 20px;
color: #ff4757;
font-size: 15px;
background: rgba(255, 71, 87, 0.03);
border: 1px dashed rgba(255, 71, 87, 0.2);
border-radius: 12px;
margin: 40px auto;
max-width: 320px;
line-height: 1.8;
white-space: pre-line;
box-shadow: 0 2px 8px rgba(255, 71, 87, 0.05);
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 用户信息区域样式 */
.red-user-info {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px 3px 12px;
line-height: normal;
}
.red-user-left {
display: flex;
align-items: center;
gap: 12px;
}
.red-user-avatar {
width: 42px;
height: 42px;
border-radius: 50%;
overflow: hidden;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.red-user-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
.red-avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: #f7f7f7;
border-radius: 50%;
transition: all 0.3s ease;
}
.red-avatar-upload-icon {
font-size: 20px;
opacity: 0.6;
}
.red-avatar-placeholder:hover {
background-color: #efefef;
}
.red-avatar-placeholder:hover .red-avatar-upload-icon {
opacity: 0.8;
}
.red-user-meta {
display: flex;
flex-direction: column;
gap: 1px;
}
.red-user-name-container {
display: flex;
align-items: center;
gap: 4px;
}
.red-user-name {
font-size: 16px;
font-weight: 600;
color: #333;
cursor: pointer;
}
.red-user-id {
font-size: 14px;
color: #999;
cursor: pointer;
}
.red-user-right {
color: #999;
font-size: 14px;
}
/* 编辑输入框样式 */
.red-user-edit-input {
border: none;
border-bottom: 1px solid #ccc;
outline: none;
padding: 2px 4px;
font-size: inherit;
font-family: inherit;
background: transparent;
}
/* 悬停效果 */
.red-user-avatar:hover,
.red-user-name:hover,
.red-user-id:hover {
opacity: 0.8;
}

25
tsconfig.json Normal file
View file

@ -0,0 +1,25 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
],
"allowSyntheticDefaultImports": true,
},
"include": [
"src/**/*.ts"
, "templates/index.ts" ]
}