11 Commits

Author SHA1 Message Date
douboer
90933673f3 update at 2025-10-10 19:13:38 2025-10-10 19:13:38 +08:00
douboer
86c3beea49 update at 2025-10-10 17:00:09 2025-10-10 17:00:09 +08:00
douboer
1309caddc3 update at 2025-10-09 21:19:57 2025-10-09 21:19:57 +08:00
douboer
002feedbe1 update at 2025-10-09 16:23:27 2025-10-09 16:23:27 +08:00
douboer
a71b4c4d4f update at 2025-10-09 15:46:43 2025-10-09 15:46:43 +08:00
douboer
643fe9fad4 update at 2025-10-09 15:44:26 2025-10-09 15:44:26 +08:00
douboer
bb131861ad update at 2025-10-09 15:19:06 2025-10-09 15:19:07 +08:00
douboer
52110c6024 update at 2025-10-09 14:46:24 2025-10-09 14:46:24 +08:00
douboer
cab675abcc update at 2025-10-09 13:09:20 2025-10-09 13:09:20 +08:00
douboer
6f51916b50 update at 2025-10-09 12:39:24 2025-10-09 12:39:24 +08:00
douboer
a891153be0 update at 2025-10-09 08:58:43 2025-10-09 08:58:43 +08:00
76 changed files with 3898 additions and 1877 deletions

View File

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 sunbooshi
Copyright (c) 2025 Gavin Chan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -95,8 +95,6 @@
检查样式无误后,点击**复制**按钮,然后到公众号粘贴即可。
![](images/20240630221748.jpg)
**★ 公众号**
插件支持多公众号,在下拉菜单中进行不同公众号的切换。该功能需要订阅才能使用。
@@ -158,6 +156,15 @@ c=+-sqrt(a^2+b^2)
数学公式的渲染效果可以看这篇文章:[公众号文章里的数学公式排版秘籍](https://mp.weixin.qq.com/s/-kpT2U1gT_5W3TsDCAVgsw)👈️
### 自定义CSS使用指南
## 开发与构建
本仓库的构建脚本分为“未混淆”和“启用混淆”两个版本:
- `npm run build`:生成未混淆的 `main.js`,便于调试。
- `npm run build:obf`:在生产模式下启用 `javascript-obfuscator`,生成混淆后的 `main.js`。
- `./build.sh`:执行默认(未混淆)构建并同步到本地 Obsidian 插件目录。
- `./build.sh obf`:执行混淆构建后再进行同步。
如需自定义混淆行为,可在执行命令时设置环境变量(例如 `OBFUSCATE=1 npm run build:bundle`),详细参数见 `esbuild.config.mjs`。
新建一篇笔记,例如**自定义样式**,直接将如下内容粘贴进笔记:
````CSS
@@ -356,7 +363,7 @@ NoteToMP插件支持该语法。
- 你想要把所有标记为 `篆刻` 的文章筛选出来,批量上传到公众号草稿箱并逐条完善后发布。
- 按文件夹 `content/post` 筛选并批量发布该文件夹下的近期文章。
### 详细使用指南(一步步)
### 使用指南
1. 打开模态
- 命令面板Ctrl/Cmd+P→ 输入“批量发布文章”,回车打开模态窗口。
@@ -537,13 +544,6 @@ https://www.bilibili.com/video/BV15XWVeEEJa/
---
```
视频教程https://www.bilibili.com/video/BV15XWVeEEmA/
## 4、反馈交流群
**微信群:**
加微信:**Genius35Plus**,备注:**NoteToMP**
## 附:批量发布 - 快速交互速览与截图占位
如果你想把功能教学放到 README 中,这里是推荐的简短速览(已在模态中实现):
@@ -559,3 +559,6 @@ https://www.bilibili.com/video/BV15XWVeEEJa/
2. 在 README 中替换占位为图片预览并附带关键交互标注说明。
如果你更愿意手动截屏我也可以把一个标注模板SVG 或说明)发给你,方便手动粘贴到 `images/` 目录。

View File

View File

@@ -60,7 +60,7 @@
/* H2左条卡片 */
.note-to-mp h2 {
font-size: 1.5rem;
font-size: 1.5em;
margin: 2em 0 1.2em;
padding: 0.6em 1em;
background: #f5f7fa;

View File

@@ -2,7 +2,24 @@
set -e # 出错立即退出
# 1. 构建
npm run build
MODE="$1"
BUILD_CMD=(npm run build)
if [[ "$MODE" == "obf" ]]; then
BUILD_CMD=(npm run build:obf)
echo "🏗️ 开始构建(启用混淆)..."
else
echo "🏗️ 开始构建(不启用混淆)..."
fi
if "${BUILD_CMD[@]}"; then
echo "✅ 构建成功"
echo
else
echo "❌ 构建失败,脚本终止"
echo
exit 1
fi
# 2. 目标目录
PLUGIN_DIR=~/myweb/.obsidian/plugins/note-to-mp
@@ -16,12 +33,12 @@ for FILE in "${FILES[@]}"; do
if [ -f "$TARGET" ]; then
mkdir -p "$(dirname "$BACKUP")"
cp -f "$TARGET" "$BACKUP"
echo "已备份 $TARGET -> $BACKUP"
echo "💾 已备份 $TARGET -> $BACKUP"
fi
if [ -f "$FILE" ]; then
cp -f "$FILE" "$TARGET"
echo "已更新 $TARGET"
echo "📂 已更新 $TARGET"
else
echo "⚠️ 源文件 $FILE 不存在,跳过"
fi
@@ -31,7 +48,12 @@ done
if [ -d "assets" ]; then
mkdir -p "$PLUGIN_DIR/assets"
rsync -a --delete assets/ "$PLUGIN_DIR/assets/" >/dev/null
echo "已同步 assets -> $PLUGIN_DIR/assets/"
echo "🎨 已同步 assets -> $PLUGIN_DIR/assets/"
echo
else
echo "⚠️ 源目录 assets 不存在,跳过"
echo
fi
echo "✅ 部署完成!"
echo

View File

@@ -34,3 +34,5 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve
## 维护指引
- 发布新版本:更新 `package.json` / `manifest.json` 的版本号;追加 `versions.json`;将当前 Unreleased 条目移动为新的版本号,并添加日期;再创建新的 Unreleased 模板。
- 提交信息建议:`feat: ...`, `fix: ...`, `docs: ...`, `refactor: ...` 等 Conventional Commits 风格。

View File

@@ -75,14 +75,17 @@ git checkout -b release/v1.3.0
### 4. 构建项目
构建项目生成生产版本文件
构建项目生成发布所需的产物
```bash
# 执行项目构建
# 执行未混淆构建(推荐用于验证与调试)
npm run build
# 检查构建输出
ls -la main.js manifest.json
# 如需生成混淆后的发布包,可执行:
npm run build:obf
```
### 5. 创建归档目录

View File

@@ -1,6 +1,7 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
import javascriptObfuscatorPlugin from "./tools/esbuild-obfuscator-plugin.mjs";
const banner =
`/*
@@ -10,6 +11,25 @@ if you want to view the source, please visit the github repository of this plugi
`;
const prod = (process.argv[2] === "production");
const obfuscate = prod && process.env.OBFUSCATE === "1";
const plugins = [];
if (obfuscate) {
plugins.push(javascriptObfuscatorPlugin({
compact: false,
controlFlowFlattening: false,
deadCodeInjection: false,
debugProtection: false,
disableConsoleOutput: true,
identifierNamesGenerator: "hexadecimal",
log: false,
renameGlobals: false,
simplify: true,
splitStrings: false,
transformObjectKeys: true,
}));
}
const context = await esbuild.context({
banner: {
@@ -38,6 +58,7 @@ const context = await esbuild.context({
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
plugins,
});
if (prod) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

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

1020
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,9 @@
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"build": "npm run build:bundle --",
"build:obf": "OBFUSCATE=1 npm run build:bundle --",
"build:bundle": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"download": "node tools/download.mjs",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
@@ -18,6 +20,7 @@
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"javascript-obfuscator": "^4.1.1",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"

66
release.md Normal file
View File

@@ -0,0 +1,66 @@
# 版本信息
‼️ 发布版本时填写,供脚本~/pubsh/release.sh使用
## v1.3.4
### 重构
#### 新的架构图
```
┌─────────────────────────────────────────────┐
│ Obsidian Framework Layer │
│ preview-view.ts (ItemView 容器) │
│ - 视图生命周期管理 │
│ - 事件监听注册 │
│ - 委托所有业务逻辑 │
└──────────────┬──────────────────────────────┘
│ 持有并委托
┌─────────────────────────────────────────────┐
│ Business Logic Layer │
│ preview-manager.ts (中央调度器) ★ │
│ - 创建和管理所有子组件 │
│ - 处理平台切换(唯一入口) │
│ - 协调组件交互 │
│ - 管理渲染流程 │
└──────────────┬──────────────────────────────┘
│ 管理
┌───────┼───────┐
↓ ↓ ↓
┌──────────┐ ┌──────────┐ ┌──────────┐
│Platform │ │Wechat │ │Xiaohong- │
│Chooser │ │Preview │ │shu │
│(UI选择器)│ │(微信实现)│ │Preview │
└──────────┘ └──────────┘ │(小红书) │
└──────────┘
```
#### 单向数据流
```
用户操作 → PlatformChooser.onChange()
PreviewManager.switchPlatform()
┌────┴────┐
↓ ↓
show/hide show/hide
Wechat Xiaohongshu
```
## bug修复
**症状1**Obsidian 一直处于"加载工作区中"状态
**症状2**:进入安全模式后关闭安全模式,插件能加载但提示:
> "获取样式失败defaultldefault请检查主题是否正确安装。"
## v1.3.8
重新实现分页测量,清理多余日志。
- 重新实现分页测量构建隐藏的“测量页面”与真实页面同样的宽度、内边距40px和 class逐个把克隆元素追加进去利用 scrollHeight 决定是否换页,保证 margin 折叠后计算准确 (src/xiaohongshu/paginator.ts:57waitForLayout 新增)。
- 当元素放不下当前页时,移除测量克隆并把已排内容写入分页,再以该元素开启新页;不可分割元素允许独占一页即便超高 (src/xiaohongshu/paginator.ts:101)。
- 清理多余日志,同时共用 PAGE_PADDING 常量让 renderPage 和测量逻辑保持一致 (src/xiaohongshu/paginator.ts:182)。
现在分页依据真实渲染高度,预览窗口内不会再丢失底部内容。建议在小红书预览里多翻几页、调整字号后重新分页验证结果。
## v1.3.9
重构xhs和wechat布局统一使用grid便于维护。

View File

@@ -1,195 +0,0 @@
#!/bin/bash
# create_milestone.sh - 自动创建项目里程碑版本
# 使用方法: ./create_milestone.sh v1.3.0 "里程碑版本描述"
set -e # 遇到错误立即退出
VERSION=$1
DESCRIPTION=${2:-"里程碑版本"}
if [ -z "$VERSION" ]; then
echo "❌ 错误: 请提供版本号"
echo "使用方法: $0 <version> [description]"
echo "示例: $0 v1.3.0 '批量发布系统完成'"
exit 1
fi
# 版本号格式验证
if [[ ! $VERSION =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "❌ 版本号格式错误,应为 vX.X.X 格式 (如: v1.3.0)"
exit 1
fi
# 检查版本是否已存在
if git tag | grep -q "^$VERSION$"; then
echo "❌ 版本 $VERSION 已存在"
exit 1
fi
echo "🚀 开始创建里程碑版本: $VERSION"
# 1. 检查工作目录状态
echo "📋 检查Git状态..."
if ! git diff-index --quiet HEAD --; then
echo "⚠️ 发现未提交的更改,正在自动提交..."
git add .
git commit -m "feat: $DESCRIPTION
版本: $VERSION (里程碑版本)"
fi
# 2. 创建Git标签
echo "🏷️ 创建Git标签..."
git tag -a "$VERSION" -m "$DESCRIPTION
此版本为稳定的里程碑版本,用于后续大规模修改前的对比和回滚基准。
创建时间: $(date '+%Y-%m-%d %H:%M:%S')
Git提交: $(git rev-parse HEAD)"
# 3. 创建发布分支
echo "🌿 创建发布分支..."
git checkout -b "release/$VERSION"
# 4. 构建项目
echo "🔨 构建项目..."
if [ -f package.json ]; then
npm run build
else
echo "⚠️ 未找到package.json跳过构建步骤"
fi
# 5. 创建归档目录
echo "📁 创建归档目录..."
mkdir -p "archives/$VERSION"
# 6. 复制关键文件
echo "📋 复制构建文件..."
for file in main.js manifest.json styles.css package.json; do
if [ -f "$file" ]; then
cp "$file" "archives/$VERSION/"
echo " ✅ 复制 $file"
else
echo " ⚠️ $file 不存在,跳过"
fi
done
echo "📄 复制文档文件..."
for file in README.md CHANGELOG.md detaildesign.md diagrams.md; do
if [ -f "$file" ]; then
cp "$file" "archives/$VERSION/"
echo " ✅ 复制 $file"
else
echo " ⚠️ $file 不存在,跳过"
fi
done
# 7. 创建源码快照
echo "📦 创建源码快照..."
PROJECT_NAME=$(basename "$(pwd)")
cd .. && tar -czf "$PROJECT_NAME/archives/$VERSION/source-snapshot-$VERSION.tar.gz" \
--exclude='node_modules' \
--exclude='.git' \
--exclude='archives' \
"$PROJECT_NAME/"
cd "$PROJECT_NAME"
# 8. 创建版本信息文档
echo "📋 创建版本信息文档..."
cat > "archives/$VERSION/VERSION_INFO.md" << EOF
# 里程碑版本 $VERSION
## 版本信息
- **版本号**: $VERSION
- **发布日期**: $(date +%Y年%m月%d日)
- **Git Tag**: $VERSION
- **Git Branch**: release/$VERSION
- **Git Commit**: $(git rev-parse HEAD)
- **描述**: $DESCRIPTION
## 主要内容
$(if [ -f "archives/$VERSION/main.js" ]; then echo "- 构建文件: main.js ($(du -h "archives/$VERSION/main.js" | cut -f1))"; fi)
$(if [ -f "archives/$VERSION/manifest.json" ]; then echo "- 插件清单: manifest.json"; fi)
$(if [ -f "archives/$VERSION/styles.css" ]; then echo "- 样式文件: styles.css"; fi)
$(if [ -f "archives/$VERSION/README.md" ]; then echo "- 项目文档: README.md"; fi)
$(if [ -f "archives/$VERSION/CHANGELOG.md" ]; then echo "- 变更日志: CHANGELOG.md"; fi)
$(if [ -f "archives/$VERSION/detaildesign.md" ]; then echo "- 设计文档: detaildesign.md"; fi)
$(if [ -f "archives/$VERSION/diagrams.md" ]; then echo "- 架构图表: diagrams.md"; fi)
- 源码快照: source-snapshot-$VERSION.tar.gz ($(du -h "archives/$VERSION/source-snapshot-$VERSION.tar.gz" | cut -f1))
## 回滚说明
如需回滚到此版本:
### 方法1: 使用Git Tag回滚
\`\`\`bash
git checkout $VERSION
git checkout -b rollback-to-$VERSION
\`\`\`
### 方法2: 使用发布分支
\`\`\`bash
git checkout release/$VERSION
\`\`\`
### 方法3: 使用源代码快照
\`\`\`bash
tar -xzf archives/$VERSION/source-snapshot-$VERSION.tar.gz
\`\`\`
### 方法4: 使用构建文件
\`\`\`bash
$(if [ -f "archives/$VERSION/main.js" ]; then echo "cp archives/$VERSION/main.js ./"; fi)
$(if [ -f "archives/$VERSION/manifest.json" ]; then echo "cp archives/$VERSION/manifest.json ./"; fi)
$(if [ -f "archives/$VERSION/styles.css" ]; then echo "cp archives/$VERSION/styles.css ./"; fi)
\`\`\`
## 版本对比
此版本可作为后续重大修改的对比基准,主要用于:
- 功能回归测试
- 性能对比分析
- 代码架构变更评估
- 稳定性基准对比
---
*此版本为稳定的里程碑版本,建议在进行大规模代码修改前保存此状态。*
*创建脚本: scripts/create_milestone.sh*
EOF
# 9. 切换回主分支
echo "🔄 切换回主分支..."
git checkout main
# 10. 推送到远程
echo "☁️ 推送到远程仓库..."
if git remote | grep -q origin; then
echo " 推送主分支和标签..."
git push origin main --tags
echo " 推送发布分支..."
git push origin "release/$VERSION"
echo "✅ 已推送到远程仓库"
else
echo "⚠️ 无远程仓库配置,跳过推送"
fi
# 11. 验证创建结果
echo ""
echo "🔍 验证里程碑创建结果..."
echo "📁 归档目录内容:"
ls -la "archives/$VERSION/" | while read line; do echo " $line"; done
echo ""
echo "🎯 里程碑版本 $VERSION 创建完成!"
echo ""
echo "📋 创建内容:"
echo " - Git标签: $VERSION"
echo " - 发布分支: release/$VERSION"
echo " - 归档目录: archives/$VERSION/"
echo " - 源码快照: source-snapshot-$VERSION.tar.gz"
echo " - 版本文档: VERSION_INFO.md"
echo ""
echo "🔄 快速回滚命令:"
echo " git checkout $VERSION # 使用标签"
echo " git checkout release/$VERSION # 使用分支"
echo " tar -xzf archives/$VERSION/source-snapshot-$VERSION.tar.gz # 使用快照"
echo ""
echo "📖 详细信息请查看: archives/$VERSION/VERSION_INFO.md"

View File

@@ -9,7 +9,7 @@ import { UploadImageToWx } from './imagelib';
import { NMPSettings } from './settings';
import AssetsManager from './assets';
import InlineCSS from './inline-css';
import { wxGetToken, wxAddDraft, wxBatchGetMaterial, DraftArticle, DraftImageMediaId, DraftImages, wxAddDraftImages } from './weixin-api';
import { wxGetToken, wxAddDraft, wxBatchGetMaterial, DraftArticle, DraftImageMediaId, DraftImages, wxAddDraftImages } from './wechat/weixin-api';
import { MDRendererCallback } from './markdown/extension';
import { MarkedParser } from './markdown/parser';
import { LocalImageManager, LocalFile } from './markdown/local-file';

View File

@@ -4,7 +4,7 @@
*/
import { getBlobArrayBuffer } from "obsidian";
import { wxUploadImage } from "./weixin-api";
import { wxUploadImage } from "./wechat/weixin-api";
import { NMPSettings } from "./settings";
import { IsWasmReady, LoadWasm } from "./wasm/wasm";
import AssetsManager from "./assets";

View File

@@ -3,7 +3,7 @@
import { Tokens, MarkedExtension} from "marked";
import { Extension } from "./extension";
import AssetsManager from "src/assets";
import { wxWidget } from "src/weixin-api";
import { wxWidget } from 'src/wechat/weixin-api';
const icon_note = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-pencil"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"></path><path d="m15 5 4 4"></path></svg>`
const icon_abstract = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-clipboard-list"><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><path d="M12 11h4"></path><path d="M12 16h4"></path><path d="M8 11h.01"></path><path d="M8 16h.01"></path></svg>`

View File

@@ -7,7 +7,7 @@ import { MathRendererQueue } from "./math";
import { Extension } from "./extension";
import { UploadImageToWx } from "../imagelib";
import AssetsManager from "src/assets";
import { wxWidget } from "src/weixin-api";
import { wxWidget } from 'src/wechat/weixin-api';
export class CardDataManager {
private cardData: Map<string, string>;

View File

@@ -4,7 +4,7 @@ import { Tokens, MarkedExtension } from "marked";
import { Extension } from "./extension";
import AssetsManager from "src/assets";
import { ExpertSettings } from "src/expert-settings";
import { wxWidget } from "src/weixin-api";
import { wxWidget } from 'src/wechat/weixin-api';
export class HeadingRenderer extends Extension {
index = [0, 0, 0, 0];

View File

@@ -4,7 +4,7 @@ import { Tokens, MarkedExtension } from "marked";
import { Extension } from "./extension";
import { NMPSettings } from "src/settings";
import { uevent } from "src/utils";
import { wxWidget } from "src/weixin-api";
import { wxWidget } from 'src/wechat/weixin-api';
const widgetCache = new Map<string, string>();

View File

@@ -19,6 +19,17 @@ export interface PlatformChooserOptions {
onPlatformChange?: (platform: PlatformType) => Promise<void>;
}
export interface PlatformActionLabels {
refresh: string;
publish: string;
}
export interface PlatformChooserActions {
onRefresh?: () => void | Promise<void>;
onPublish?: () => void | Promise<void>;
getLabels?: (platform: PlatformType) => PlatformActionLabels;
}
/**
* 平台信息接口
*/
@@ -48,12 +59,15 @@ const SUPPORTED_PLATFORMS: PlatformInfo[] = [
export class PlatformChooser {
private container: HTMLElement;
private selectElement: HTMLSelectElement | null = null;
private refreshButton: HTMLButtonElement | null = null;
private publishButton: HTMLButtonElement | null = null;
private currentPlatform: PlatformType;
private onChange?: (platform: PlatformType) => void;
private actions: PlatformChooserActions | null = null;
constructor(container: HTMLElement, options: PlatformChooserOptions = {}) {
this.container = container;
this.currentPlatform = options.defaultPlatform || 'wechat';
this.currentPlatform = options.defaultPlatform || 'xiaohongshu';
if (options.onPlatformChange) {
this.onChange = (platform) => {
options.onPlatformChange!(platform);
@@ -72,15 +86,16 @@ export class PlatformChooser {
* 渲染平台选择器 UI
*/
render(): void {
// 创建平台选择行
const lineDiv = this.container.createDiv({ cls: 'toolbar-line platform-selector-line' });
// 将容器作为单层 Grid 行使用
this.container.addClass('platform-selector-line');
this.container.addClass('platform-chooser-grid');
// 创建标签
const platformLabel = lineDiv.createDiv({ cls: 'style-label' });
const platformLabel = this.container.createDiv({ cls: 'style-label' });
platformLabel.innerText = '发布平台';
// 创建选择器
const platformSelect = lineDiv.createEl('select', { cls: 'platform-select' });
const platformSelect = this.container.createEl('select', { cls: 'platform-select' });
this.selectElement = platformSelect;
// 添加平台选项
@@ -100,6 +115,16 @@ export class PlatformChooser {
const newPlatform = platformSelect.value as PlatformType;
this.switchPlatformInternal(newPlatform);
};
// 刷新按钮
this.refreshButton = this.container.createEl('button', { cls: 'toolbar-button purple-gradient' });
this.refreshButton.onclick = () => this.handleRefreshClick();
// 发布按钮
this.publishButton = this.container.createEl('button', { cls: 'toolbar-button' });
this.publishButton.onclick = () => this.handlePublishClick();
this.updateActionLabels();
}
/**
@@ -122,6 +147,8 @@ export class PlatformChooser {
console.error('[PlatformChooser] 平台切换失败:', error);
}
}
this.updateActionLabels();
}
/**
@@ -132,6 +159,7 @@ export class PlatformChooser {
if (this.selectElement) {
this.selectElement.value = platform;
}
this.updateActionLabels();
}
/**
@@ -159,5 +187,45 @@ export class PlatformChooser {
this.selectElement.onchange = null;
this.selectElement = null;
}
if (this.refreshButton) {
this.refreshButton.onclick = null;
this.refreshButton = null;
}
if (this.publishButton) {
this.publishButton.onclick = null;
this.publishButton = null;
}
}
/**
* 设置通用操作按钮
*/
setActions(actions: PlatformChooserActions): void {
this.actions = actions;
this.updateActionLabels();
}
private handleRefreshClick(): void {
if (!this.actions?.onRefresh) return;
Promise.resolve(this.actions.onRefresh()).catch((error) => {
console.error('[PlatformChooser] 刷新操作失败:', error);
});
}
private handlePublishClick(): void {
if (!this.actions?.onPublish) return;
Promise.resolve(this.actions.onPublish()).catch((error) => {
console.error('[PlatformChooser] 发布操作失败:', error);
});
}
private updateActionLabels(): void {
if (!this.refreshButton || !this.publishButton) return;
const labels = this.actions?.getLabels?.(this.currentPlatform) ?? {
refresh: '🔄 刷新',
publish: '📤 发布',
};
this.refreshButton.innerText = labels.refresh;
this.publishButton.innerText = labels.publish;
}
}

View File

@@ -37,7 +37,7 @@ export class PreviewManager {
private xhsContainer: HTMLDivElement | null = null;
// 状态
private currentPlatform: PlatformType = 'wechat';
private currentPlatform: PlatformType = 'xiaohongshu';
private currentFile: TFile | null = null;
constructor(container: HTMLElement, app: App, render: ArticleRender) {
@@ -68,8 +68,8 @@ export class PreviewManager {
// 3. 创建并构建小红书预览
this.createXiaohongshuPreview();
// 4. 初始显示微信平台
await this.switchPlatform('wechat');
// 4. 初始显示小红书平台
await this.switchPlatform('xiaohongshu');
console.log('[PreviewManager] 界面构建完成');
}
@@ -93,6 +93,18 @@ export class PreviewManager {
// 构建 UI
this.platformChooser.render();
// 共享操作按钮
this.platformChooser.setActions({
onRefresh: () => this.refresh(),
onPublish: () => this.publishCurrentPlatform(),
getLabels: (platform) => {
if (platform === 'wechat') {
return { refresh: '🔄 刷新', publish: '📝 发布' };
}
return { refresh: '🔄 刷新', publish: '📤 发布' };
},
});
}
/**
@@ -125,6 +137,22 @@ export class PreviewManager {
this.wechatPreview.build();
}
private async publishCurrentPlatform(): Promise<void> {
if (this.currentPlatform === 'wechat') {
if (!this.wechatPreview) {
new Notice('微信预览未初始化');
return;
}
await this.wechatPreview.publish();
} else if (this.currentPlatform === 'xiaohongshu') {
if (!this.xhsPreview) {
new Notice('小红书预览未初始化');
return;
}
await this.xhsPreview.publish();
}
}
/**
* 创建小红书预览组件
*/

View File

@@ -1,895 +0,0 @@
/**
* 文件note-preview.ts
* 功能:侧边预览视图;支持多平台预览(公众号/小红书)与发布触发。
* - 渲染 Markdown
* - 平台切换下拉
* - 单篇发布入口
* - 与批量发布/图片处理集成预留
*/
import { EventRef, ItemView, Workspace, WorkspaceLeaf, Notice, Platform, TFile, TFolder, TAbstractFile, Plugin } from 'obsidian';
import { uevent, debounce, waitForLayoutReady } from './utils';
import { NMPSettings } from './settings';
import AssetsManager from './assets';
import { MarkedParser } from './markdown/parser';
import { LocalImageManager, LocalFile } from './markdown/local-file';
import { CardDataManager } from './markdown/code';
import { ArticleRender } from './article-render';
// 平台选择组件
import { PlatformChooser, PlatformType } from './platform-chooser';
// 微信公众号功能模块
import { WechatPreview } from './wechat/wechat-preview';
// 小红书功能模块
import { XiaohongshuContentAdapter } from './xiaohongshu/adapter';
import { XiaohongshuImageManager } from './xiaohongshu/image';
import { XiaohongshuAPIManager } from './xiaohongshu/api';
import { XiaohongshuPost } from './xiaohongshu/types';
import { XiaohongshuPreview } from './xiaohongshu/xhs-preview';
// 切图功能
import { sliceArticleImage } from './slice-image';
export const VIEW_TYPE_NOTE_PREVIEW = 'note-preview';
export class NotePreview extends ItemView {
workspace: Workspace;
plugin: Plugin;
mainDiv: HTMLDivElement;
toolbar: HTMLDivElement;
renderDiv: HTMLDivElement;
articleDiv: HTMLDivElement;
styleEl: HTMLElement;
coverEl: HTMLInputElement;
useDefaultCover: HTMLInputElement;
useLocalCover: HTMLInputElement;
msgView: HTMLDivElement;
wechatSelect: HTMLSelectElement;
platformSelect: HTMLSelectElement; // 新增:平台选择器
themeSelect: HTMLSelectElement;
highlightSelect: HTMLSelectElement;
listeners?: EventRef[];
container: Element;
settings: NMPSettings;
assetsManager: AssetsManager;
articleHTML: string;
title: string;
currentFile?: TFile;
currentTheme: string;
currentHighlight: string;
currentAppId: string;
currentPlatform: string = 'wechat'; // 新增:当前选择的平台,默认微信
markedParser: MarkedParser;
cachedElements: Map<string, string> = new Map();
_articleRender: ArticleRender | null = null;
_xiaohongshuPreview: XiaohongshuPreview | null = null;
_wechatPreview: WechatPreview | null = null;
_platformChooser: PlatformChooser | null = null;
isCancelUpload: boolean = false;
isBatchRuning: boolean = false;
constructor(leaf: WorkspaceLeaf, plugin: Plugin) {
super(leaf);
this.workspace = this.app.workspace;
this.plugin = plugin;
this.settings = NMPSettings.getInstance();
this.assetsManager = AssetsManager.getInstance();
this.currentTheme = this.settings.defaultStyle;
this.currentHighlight = this.settings.defaultHighlight;
}
getViewType() {
return VIEW_TYPE_NOTE_PREVIEW;
}
getIcon() {
return 'clipboard-paste';
}
getDisplayText() {
return '笔记预览';
}
get render() {
if (!this._articleRender) {
this._articleRender = new ArticleRender(this.app, this, this.styleEl, this.articleDiv);
this._articleRender.currentTheme = this.currentTheme;
this._articleRender.currentHighlight = this.currentHighlight;
}
return this._articleRender;
}
async onOpen() {
this.viewLoading();
this.setup();
uevent('open');
}
async setup() {
await waitForLayoutReady(this.app);
if (!this.settings.isLoaded) {
const data = await this.plugin.loadData();
NMPSettings.loadSettings(data);
}
if (!this.assetsManager.isLoaded) {
await this.assetsManager.loadAssets();
}
this.buildUI();
this.listeners = [
this.workspace.on('file-open', () => {
this.update();
}),
this.app.vault.on("modify", (file) => {
if (this.currentFile?.path == file.path) {
this.renderMarkdown();
}
} )
];
this.renderMarkdown();
}
async onClose() {
this.listeners?.forEach(listener => this.workspace.offref(listener));
LocalFile.fileCache.clear();
uevent('close');
}
onAppIdChanged() {
// 清理上传过的图片
this.cleanArticleData();
}
async update() {
if (this.isBatchRuning) {
return;
}
this.cleanArticleData();
this.renderMarkdown();
}
cleanArticleData() {
LocalImageManager.getInstance().cleanup();
CardDataManager.getInstance().cleanup();
}
buildMsgView(parent: HTMLDivElement) {
this.msgView = parent.createDiv({ cls: 'msg-view' });
const title = this.msgView.createDiv({ cls: 'msg-title' });
title.id = 'msg-title';
title.innerText = '加载中...';
const okBtn = this.msgView.createEl('button', { cls: 'msg-ok-btn' }, async (button) => {
});
okBtn.id = 'msg-ok-btn';
okBtn.innerText = '确定';
okBtn.onclick = async () => {
this.msgView.setAttr('style', 'display: none;');
}
const cancelBtn = this.msgView.createEl('button', { cls: 'msg-ok-btn' }, async (button) => {
});
cancelBtn.id = 'msg-cancel-btn';
cancelBtn.innerText = '取消';
cancelBtn.onclick = async () => {
this.isCancelUpload = true;
this.msgView.setAttr('style', 'display: none;');
}
}
showLoading(msg: string, cancelable: boolean = false) {
const title = this.msgView.querySelector('#msg-title') as HTMLElement;
title!.innerText = msg;
const btn = this.msgView.querySelector('#msg-ok-btn') as HTMLElement;
btn.setAttr('style', 'display: none;');
this.msgView.setAttr('style', 'display: flex;');
const cancelBtn = this.msgView.querySelector('#msg-cancel-btn') as HTMLElement;
cancelBtn.setAttr('style', cancelable ? 'display: block;': 'display: none;');
this.msgView.setAttr('style', 'display: flex;');
}
showMsg(msg: string) {
const title = this.msgView.querySelector('#msg-title') as HTMLElement;
title!.innerText = msg;
const btn = this.msgView.querySelector('#msg-ok-btn') as HTMLElement;
btn.setAttr('style', 'display: block;');
this.msgView.setAttr('style', 'display: flex;');
const cancelBtn = this.msgView.querySelector('#msg-cancel-btn') as HTMLElement;
cancelBtn.setAttr('style', 'display: none;');
this.msgView.setAttr('style', 'display: flex;');
}
buildToolbar(parent: HTMLDivElement) {
this.toolbar = parent.createDiv({ cls: 'preview-toolbar' });
let lineDiv;
// 平台选择器(新增)- 始终显示
lineDiv = this.toolbar.createDiv({ cls: 'toolbar-line platform-selector-line' });
const platformLabel = lineDiv.createDiv({ cls: 'style-label' });
platformLabel.innerText = '发布平台';
const platformSelect = lineDiv.createEl('select', { cls: 'platform-select' });
// 添加平台选项
const wechatOption = platformSelect.createEl('option');
wechatOption.value = 'wechat';
wechatOption.text = '微信公众号';
wechatOption.selected = true;
const xiaohongshuOption = platformSelect.createEl('option');
xiaohongshuOption.value = 'xiaohongshu';
xiaohongshuOption.text = '小红书';
platformSelect.onchange = async () => {
this.currentPlatform = platformSelect.value;
await this.onPlatformChanged();
};
this.platformSelect = platformSelect;
// 公众号
if (this.settings.wxInfo.length > 1 || Platform.isDesktop) {
lineDiv = this.toolbar.createDiv({ cls: 'toolbar-line wechat-only' });
const wxLabel = lineDiv.createDiv({ cls: 'style-label' });
wxLabel.innerText = '公众号';
const wxSelect = lineDiv.createEl('select', { cls: 'wechat-select' });
wxSelect.onchange = async () => {
this.currentAppId = wxSelect.value;
this.onAppIdChanged();
}
const defautlOp =wxSelect.createEl('option');
defautlOp.value = '';
defautlOp.text = '请在设置里配置公众号';
for (let i = 0; i < this.settings.wxInfo.length; i++) {
const op = wxSelect.createEl('option');
const wx = this.settings.wxInfo[i];
op.value = wx.appid;
op.text = wx.name;
if (i== 0) {
op.selected = true
this.currentAppId = wx.appid;
}
}
this.wechatSelect = wxSelect;
if (Platform.isDesktop) {
// 分隔线
const separator = lineDiv.createDiv({ cls: 'toolbar-separator' });
const openBtn = lineDiv.createEl('button', { text: '🌐 去公众号后台', cls: 'toolbar-button purple-gradient' });
openBtn.onclick = async () => {
const { shell } = require('electron');
shell.openExternal('https://mp.weixin.qq.com')
uevent('open-mp');
}
}
}
else if (this.settings.wxInfo.length > 0) {
this.currentAppId = this.settings.wxInfo[0].appid;
}
// 复制,刷新,带图片复制,发草稿箱
lineDiv = this.toolbar.createDiv({ cls: 'toolbar-line wechat-only flex-wrap' });
const refreshBtn = lineDiv.createEl('button', { text: '🔄 刷新', cls: 'toolbar-button purple-gradient' });
refreshBtn.onclick = async () => {
await this.assetsManager.loadCustomCSS();
await this.assetsManager.loadExpertSettings();
this.render.reloadStyle();
await this.renderMarkdown();
uevent('refresh');
}
if (Platform.isDesktop) {
const copyBtn = lineDiv.createEl('button', { text: '📋 复制', cls: 'toolbar-button' });
copyBtn.onclick = async() => {
try {
await this.render.copyArticle();
new Notice('复制成功,请到公众号编辑器粘贴。');
uevent('copy');
} catch (error) {
console.error(error);
new Notice('复制失败: ' + error);
}
}
}
const uploadImgBtn = lineDiv.createEl('button', { text: '📤 上传图片', cls: 'toolbar-button' });
uploadImgBtn.onclick = async() => {
await this.uploadImages();
uevent('upload');
}
const postBtn = lineDiv.createEl('button', { text: '📝 发草稿', cls: 'toolbar-button' });
postBtn.onclick = async() => {
await this.postArticle();
uevent('pub');
}
const imagesBtn = lineDiv.createEl('button', { text: '🖼️ 图片/文字', cls: 'toolbar-button' });
imagesBtn.onclick = async() => {
await this.postImages();
uevent('pub-images');
}
if (Platform.isDesktop && this.settings.isAuthKeyVaild()) {
const htmlBtn = lineDiv.createEl('button', { text: '💾 导出HTML', cls: 'toolbar-button' });
htmlBtn.onclick = async() => {
await this.exportHTML();
uevent('export-html');
}
}
// 封面
lineDiv = this.toolbar.createDiv({ cls: 'toolbar-line wechat-only' });
const coverTitle = lineDiv.createDiv({ cls: 'style-label' });
coverTitle.innerText = '封面';
this.useDefaultCover = lineDiv.createEl('input', { cls: 'input-style' });
this.useDefaultCover.setAttr('type', 'radio');
this.useDefaultCover.setAttr('name', 'cover');
this.useDefaultCover.setAttr('value', 'default');
this.useDefaultCover.setAttr('checked', true);
this.useDefaultCover.id = 'default-cover';
this.useDefaultCover.onchange = async () => {
if (this.useDefaultCover.checked) {
this.coverEl.setAttr('style', 'visibility:hidden;width:0px;');
}
else {
this.coverEl.setAttr('style', 'visibility:visible;width:180px;');
}
}
const defaultLable = lineDiv.createEl('label');
defaultLable.innerText = '默认';
defaultLable.setAttr('for', 'default-cover');
this.useLocalCover = lineDiv.createEl('input', { cls: 'input-style' });
this.useLocalCover.setAttr('type', 'radio');
this.useLocalCover.setAttr('name', 'cover');
this.useLocalCover.setAttr('value', 'local');
this.useLocalCover.id = 'local-cover';
this.useLocalCover.setAttr('style', 'margin-left:20px;');
this.useLocalCover.onchange = async () => {
if (this.useLocalCover.checked) {
this.coverEl.setAttr('style', 'visibility:visible;width:180px;');
}
else {
this.coverEl.setAttr('style', 'visibility:hidden;width:0px;');
}
}
const localLabel = lineDiv.createEl('label');
localLabel.setAttr('for', 'local-cover');
localLabel.innerText = '上传';
this.coverEl = lineDiv.createEl('input', { cls: 'upload-input' });
this.coverEl.setAttr('type', 'file');
this.coverEl.setAttr('placeholder', '封面图片');
this.coverEl.setAttr('accept', '.png, .jpg, .jpeg');
this.coverEl.setAttr('name', 'cover');
this.coverEl.id = 'cover-input';
// 样式
if (this.settings.showStyleUI) {
lineDiv = this.toolbar.createDiv({ cls: 'toolbar-line wechat-only flex-wrap' });
const cssStyle = lineDiv.createDiv({ cls: 'style-label' });
cssStyle.innerText = '样式';
const selectBtn = lineDiv.createEl('select', { cls: 'style-select' });
selectBtn.onchange = async () => {
this.currentTheme = selectBtn.value;
this.render.updateStyle(selectBtn.value);
}
for (let s of this.assetsManager.themes) {
const op = selectBtn.createEl('option');
op.value = s.className;
op.text = s.name;
op.selected = s.className == this.settings.defaultStyle;
}
this.themeSelect = selectBtn;
// 分隔线
const separator = lineDiv.createDiv({ cls: 'toolbar-separator' });
const highlightStyle = lineDiv.createDiv({ cls: 'style-label' });
highlightStyle.innerText = '代码高亮';
const highlightStyleBtn = lineDiv.createEl('select', { cls: 'style-select' });
highlightStyleBtn.onchange = async () => {
this.currentHighlight = highlightStyleBtn.value;
this.render.updateHighLight(highlightStyleBtn.value);
}
for (let s of this.assetsManager.highlights) {
const op = highlightStyleBtn.createEl('option');
op.value = s.name;
op.text = s.name;
op.selected = s.name == this.settings.defaultHighlight;
}
this.highlightSelect = highlightStyleBtn;
}
this.buildMsgView(this.toolbar);
}
async buildUI() {
this.container = this.containerEl.children[1];
this.container.empty();
this.mainDiv = this.container.createDiv({ cls: 'note-preview' });
this.buildToolbar(this.mainDiv);
this.renderDiv = this.mainDiv.createDiv({cls: 'render-div'});
this.renderDiv.id = 'render-div';
this.styleEl = this.renderDiv.createEl('style');
this.styleEl.setAttr('title', 'note-to-mp-style');
this.articleDiv = this.renderDiv.createEl('div');
}
async viewLoading() {
const container = this.containerEl.children[1]
container.empty();
const loading = container.createDiv({cls: 'loading-wrapper'})
loading.createDiv({cls: 'loading-spinner'})
}
async renderMarkdown(af: TFile | null = null) {
if (!af) {
af = this.app.workspace.getActiveFile();
}
if (!af || af.extension.toLocaleLowerCase() !== 'md') {
return;
}
this.currentFile = af;
// 如果关闭了样式 UI则在渲染前强制使用全局默认样式/高亮(忽略 frontmatter 中的 theme/highlight
if (!this.settings.showStyleUI) {
const globalStyle = this.settings.defaultStyle;
const globalHighlight = this.settings.defaultHighlight;
// 仅当变更时更新当前与 articleRender 中的值,避免不必要的刷新
if (this.currentTheme !== globalStyle) {
this.currentTheme = globalStyle;
if (this._articleRender) {
this._articleRender.currentTheme = globalStyle;
}
}
if (this.currentHighlight !== globalHighlight) {
this.currentHighlight = globalHighlight;
if (this._articleRender) {
this._articleRender.currentHighlight = globalHighlight;
}
}
}
await this.render.renderMarkdown(af);
const metadata = this.render.getMetadata();
if (metadata.appid) {
this.wechatSelect.value = metadata.appid;
}
else {
this.wechatSelect.value = this.currentAppId;
}
// 仅当 UI 开启时才允许 frontmatter 覆盖与下拉同步;关闭时忽略 frontmatter 的 theme/highlight
if (this.settings.showStyleUI) {
if (metadata.theme) {
this.assetsManager.themes.forEach(theme => {
if (theme.name === metadata.theme) {
this.currentTheme = theme.className;
if (this.themeSelect) this.themeSelect.value = theme.className;
if (this._articleRender) this._articleRender.currentTheme = theme.className;
}
});
} else if (this.themeSelect) {
this.themeSelect.value = this.currentTheme;
}
if (metadata.highlight) {
this.currentHighlight = metadata.highlight;
if (this.highlightSelect) this.highlightSelect.value = metadata.highlight;
if (this._articleRender) this._articleRender.currentHighlight = metadata.highlight;
} else if (this.highlightSelect) {
this.highlightSelect.value = this.currentHighlight;
}
}
// 如果当前是小红书平台,更新小红书预览
if (this.currentPlatform === 'xiaohongshu' && this._xiaohongshuPreview) {
this.articleHTML = this.render.articleHTML;
await this._xiaohongshuPreview.renderArticle(this.articleHTML, af);
}
}
/**
* 平台切换处理
* 当用户切换发布平台时调用
*/
async onPlatformChanged() {
console.log(`[NotePreview] 平台切换至: ${this.currentPlatform}`);
if (this.currentPlatform === 'xiaohongshu') {
// 切换到小红书预览模式
await this.switchToXiaohongshuMode();
} else {
// 切换到微信公众号模式
this.switchToWechatMode();
}
}
/**
* 切换到小红书预览模式
*/
private async switchToXiaohongshuMode() {
// 隐藏微信相关的工具栏行和平台选择器
if (this.toolbar) {
const wechatLines = this.toolbar.querySelectorAll('.wechat-only');
wechatLines.forEach((line: HTMLElement) => {
line.style.display = 'none';
});
// 也隐藏平台选择器行
// const platformLine = this.toolbar.querySelector('.platform-selector-line') as HTMLElement;
// if (platformLine) {
// platformLine.style.display = 'none';
// }
}
// 隐藏渲染区域
if (this.renderDiv) this.renderDiv.style.display = 'none';
// 创建或显示小红书预览视图
if (!this._xiaohongshuPreview) {
const xhsContainer = this.mainDiv.createDiv({ cls: 'xiaohongshu-preview-container' });
this._xiaohongshuPreview = new XiaohongshuPreview(xhsContainer, this.app);
// 设置回调函数
this._xiaohongshuPreview.onRefreshCallback = async () => {
await this.onXiaohongshuRefresh();
};
this._xiaohongshuPreview.onPublishCallback = async () => {
await this.onXiaohongshuPublish();
};
this._xiaohongshuPreview.onPlatformChangeCallback = async (platform: string) => {
this.currentPlatform = platform;
if (platform === 'wechat') {
await this.onPlatformChanged();
}
};
this._xiaohongshuPreview.build();
} else {
const xhsContainer = this.mainDiv.querySelector('.xiaohongshu-preview-container') as HTMLElement;
if (xhsContainer) xhsContainer.style.display = 'flex';
}
// 如果有当前文件,渲染小红书预览
if (this.currentFile) {
// 如果还没有生成 articleHTML先生成它
if (!this.articleHTML) {
await this.render.renderMarkdown(this.currentFile);
this.articleHTML = this.render.articleHTML;
}
// 渲染到小红书预览
if (this.articleHTML) {
await this._xiaohongshuPreview.renderArticle(this.articleHTML, this.currentFile);
}
}
}
/**
* 切换到微信公众号模式
*/
private switchToWechatMode() {
// 显示微信相关的工具栏行和平台选择器
if (this.toolbar) {
const wechatLines = this.toolbar.querySelectorAll('.wechat-only');
wechatLines.forEach((line: HTMLElement) => {
line.style.display = 'flex';
});
// 也显示平台选择器行
const platformLine = this.toolbar.querySelector('.platform-selector-line') as HTMLElement;
if (platformLine) {
platformLine.style.display = 'flex';
}
}
// 显示渲染区域
if (this.renderDiv) this.renderDiv.style.display = 'block';
// 隐藏小红书预览视图
const xhsContainer = this.mainDiv.querySelector('.xiaohongshu-preview-container') as HTMLElement;
if (xhsContainer) xhsContainer.style.display = 'none';
}
/**
* 更新按钮文本为微信公众号相关
*/
private updateButtonsForWechat() {
const buttons = this.toolbar.querySelectorAll('button');
buttons.forEach(button => {
const text = button.textContent;
if (text === '发布到小红书') {
button.textContent = '发草稿';
} else if (text === '上传图片(小红书)') {
button.textContent = '上传图片';
}
});
}
/**
* 更新按钮文本为小红书相关
*/
private updateButtonsForXiaohongshu() {
const buttons = this.toolbar.querySelectorAll('button');
buttons.forEach(button => {
const text = button.textContent;
if (text === '发草稿') {
button.textContent = '发布到小红书';
} else if (text === '上传图片') {
button.textContent = '上传图片(小红书)';
}
});
}
async uploadImages() {
if (this.currentPlatform === 'wechat') {
await this.uploadImagesToWechat();
} else if (this.currentPlatform === 'xiaohongshu') {
await this.uploadImagesToXiaohongshu();
}
}
/**
* 上传图片到微信公众号
*/
async uploadImagesToWechat() {
this.showLoading('图片上传中...');
try {
await this.render.uploadImages(this.currentAppId);
this.showMsg('图片上传成功,并且文章内容已复制,请到公众号编辑器粘贴。');
} catch (error) {
this.showMsg('图片上传失败: ' + error.message);
}
}
/**
* 上传图片到小红书
*/
async uploadImagesToXiaohongshu() {
this.showLoading('处理图片中...');
try {
// 获取小红书适配器和图片处理器
const adapter = new XiaohongshuContentAdapter();
const imageHandler = XiaohongshuImageManager.getInstance();
// 获取当前文档的图片
const imageManager = LocalImageManager.getInstance();
const images = imageManager.getImageInfos(this.articleDiv);
if (images.length === 0) {
this.showMsg('当前文档没有图片需要处理');
return;
}
// 处理图片转换为PNG格式
const imageBlobs: { name: string; blob: Blob }[] = [];
for (const img of images) {
// 从filePath获取文件
const file = this.app.vault.getAbstractFileByPath(img.filePath);
if (file && file instanceof TFile) {
const fileData = await this.app.vault.readBinary(file);
imageBlobs.push({
name: file.name,
blob: new Blob([fileData])
});
}
}
const processedImages = await imageHandler.processImages(imageBlobs);
this.showMsg(`成功处理 ${processedImages.length} 张图片已转换为PNG格式`);
} catch (error) {
this.showMsg('图片处理失败: ' + error.message);
}
}
async postArticle() {
if (this.currentPlatform === 'wechat') {
await this.postToWechat();
} else if (this.currentPlatform === 'xiaohongshu') {
await this.postToXiaohongshu();
}
}
/**
* 发布到微信公众号草稿
*/
async postToWechat() {
let localCover = null;
if (this.useLocalCover.checked) {
const fileInput = this.coverEl;
if (!fileInput.files || fileInput.files.length === 0) {
this.showMsg('请选择封面文件');
return;
}
localCover = fileInput.files[0];
if (!localCover) {
this.showMsg('请选择封面文件');
return;
}
}
this.showLoading('发布中...');
try {
await this.render.postArticle(this.currentAppId, localCover);
this.showMsg('发布成功');
}
catch (error) {
this.showMsg('发布失败: ' + error.message);
}
}
/**
* 发布到小红书
*/
async postToXiaohongshu() {
this.showLoading('发布到小红书中...');
try {
if (!this.currentFile) {
this.showMsg('没有可发布的文件');
return;
}
// 读取文件内容
const fileContent = await this.app.vault.read(this.currentFile);
// 使用小红书适配器转换内容
const adapter = new XiaohongshuContentAdapter();
const xiaohongshuPost = adapter.adaptMarkdownToXiaohongshu(fileContent, {
addStyle: true,
generateTitle: true
});
// 验证内容
const validation = adapter.validatePost(xiaohongshuPost);
if (!validation.valid) {
this.showMsg('内容验证失败: ' + validation.errors.join('; '));
return;
}
// 获取小红书API实例
const api = XiaohongshuAPIManager.getInstance(false); // 暂时使用false
// 检查登录状态
const isLoggedIn = await api.checkLoginStatus();
if (!isLoggedIn) {
this.showMsg('请先登录小红书,或检查登录状态');
return;
}
// 发布内容
const result = await api.createPost(xiaohongshuPost);
if (result.success) {
this.showMsg('发布到小红书成功!');
} else {
this.showMsg('发布失败: ' + result.message);
}
}
catch (error) {
this.showMsg('发布失败: ' + error.message);
}
}
/**
* 小红书预览的刷新回调
*/
async onXiaohongshuRefresh() {
await this.assetsManager.loadCustomCSS();
await this.assetsManager.loadExpertSettings();
// 更新小红书预览的样式
if (this._xiaohongshuPreview) {
this._xiaohongshuPreview.assetsManager = this.assetsManager;
}
await this.renderMarkdown();
new Notice('刷新成功');
}
/**
* 小红书预览的发布回调
*/
async onXiaohongshuPublish() {
await this.postToXiaohongshu();
}
async postImages() {
this.showLoading('发布图片中...');
try {
await this.render.postImages(this.currentAppId);
this.showMsg('图片发布成功');
} catch (error) {
this.showMsg('图片发布失败: ' + error.message);
}
}
async exportHTML() {
this.showLoading('导出HTML中...');
try {
await this.render.exportHTML();
this.showMsg('HTML导出成功');
} catch (error) {
this.showMsg('HTML导出失败: ' + error.message);
}
}
async sliceArticleImage() {
if (!this.currentFile) {
new Notice('请先打开一个笔记文件');
return;
}
this.showLoading('切图处理中...');
try {
const articleSection = this.render.getArticleSection();
if (!articleSection) {
throw new Error('未找到预览区域');
}
await sliceArticleImage(articleSection, this.currentFile, this.app);
this.showMsg('切图完成');
} catch (error) {
console.error('切图失败:', error);
this.showMsg('切图失败: ' + error.message);
}
}
async batchPost(folder: TFolder) {
const files = folder.children.filter((child: TAbstractFile) => child.path.toLocaleLowerCase().endsWith('.md'));
if (!files) {
new Notice('没有可渲染的笔记或文件不支持渲染');
return;
}
this.isCancelUpload = false;
this.isBatchRuning = true;
try {
for (let file of files) {
this.showLoading(`即将发布: ${file.name}`, true);
await sleep(5000);
if (this.isCancelUpload) {
break;
}
this.cleanArticleData();
await this.renderMarkdown(file as TFile);
await this.postArticle();
}
if (!this.isCancelUpload) {
this.showMsg(`批量发布完成:成功发布 ${files.length} 篇笔记`);
}
}
catch (e) {
console.error(e);
new Notice('批量发布失败: ' + e.message);
}
finally {
this.isBatchRuning = false;
this.isCancelUpload = false;
}
}
}

View File

@@ -5,7 +5,7 @@
import { App, TextAreaComponent, PluginSettingTab, Setting, Notice, sanitizeHTMLToDom } from 'obsidian';
import NoteToMpPlugin from './main';
import { wxGetToken,wxEncrypt } from './weixin-api';
import { wxGetToken, wxEncrypt } from './wechat/weixin-api';
import { cleanMathCache } from './markdown/math';
import { NMPSettings } from './settings';
import { DocModal } from './doc-modal';

View File

@@ -9,7 +9,7 @@
* - 批量发布预设 / 图片处理 / 样式控制等选项
*/
import { wxKeyInfo } from './weixin-api';
import { wxKeyInfo } from './wechat/weixin-api';
export class NMPSettings {
defaultStyle: string;
@@ -51,6 +51,7 @@ export class NMPSettings {
sliceImageSavePath: string; // 切图保存路径
sliceImageWidth: number; // 切图宽度(像素)
sliceImageAspectRatio: string; // 横竖比例,格式 "3:4"
xhsPreviewWidth: number; // 小红书预览宽度(像素)
private static instance: NMPSettings;
@@ -100,6 +101,7 @@ export class NMPSettings {
this.sliceImageSavePath = '/Users/gavin/note2mp/images/xhs';
this.sliceImageWidth = 1080;
this.sliceImageAspectRatio = '3:4';
this.xhsPreviewWidth = 540;
}
resetStyelAndHighlight() {
@@ -136,7 +138,8 @@ export class NMPSettings {
batchPublishPresets = [],
sliceImageSavePath,
sliceImageWidth,
sliceImageAspectRatio
sliceImageAspectRatio,
xhsPreviewWidth
} = data;
const settings = NMPSettings.getInstance();
@@ -166,6 +169,9 @@ export class NMPSettings {
if (sliceImageSavePath) settings.sliceImageSavePath = sliceImageSavePath;
if (sliceImageWidth !== undefined && Number.isFinite(sliceImageWidth)) settings.sliceImageWidth = Math.max(100, parseInt(sliceImageWidth));
if (sliceImageAspectRatio) settings.sliceImageAspectRatio = sliceImageAspectRatio;
if (xhsPreviewWidth !== undefined && Number.isFinite(xhsPreviewWidth)) {
settings.xhsPreviewWidth = Math.max(100, parseInt(xhsPreviewWidth));
}
settings.getExpiredDate();
settings.isLoaded = true;
@@ -200,6 +206,7 @@ export class NMPSettings {
'sliceImageSavePath': settings.sliceImageSavePath,
'sliceImageWidth': settings.sliceImageWidth,
'sliceImageAspectRatio': settings.sliceImageAspectRatio,
'xhsPreviewWidth': settings.xhsPreviewWidth,
}
}

View File

@@ -1,161 +0,0 @@
/* 文件slice-image.ts — 预览页面切图功能:将渲染完的 HTML 页面转为长图,再按比例裁剪为多张 PNG 图片。 */
import { toPng } from 'html-to-image';
import { Notice, TFile } from 'obsidian';
import { NMPSettings } from './settings';
import * as fs from 'fs';
import * as path from 'path';
/**
* 解析横竖比例字符串(如 "3:4")为数值
*/
function parseAspectRatio(ratio: string): { width: number; height: number } {
const parts = ratio.split(':').map(p => parseFloat(p.trim()));
if (parts.length === 2 && parts[0] > 0 && parts[1] > 0) {
return { width: parts[0], height: parts[1] };
}
// 默认 3:4
return { width: 3, height: 4 };
}
/**
* 从 frontmatter 获取 slug若不存在则使用文件名去除扩展名
*/
function getSlugFromFile(file: TFile, app: any): string {
const cache = app.metadataCache.getFileCache(file);
if (cache?.frontmatter?.slug) {
return String(cache.frontmatter.slug).trim();
}
return file.basename;
}
/**
* 确保目录存在
*/
function ensureDir(dirPath: string) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
/**
* 将 base64 dataURL 转为 Buffer
*/
function dataURLToBuffer(dataURL: string): Buffer {
const base64 = dataURL.split(',')[1];
return Buffer.from(base64, 'base64');
}
/**
* 切图主函数
* @param articleElement 预览文章的 HTML 元素(#article-section
* @param file 当前文件
* @param app Obsidian App 实例
*/
export async function sliceArticleImage(articleElement: HTMLElement, file: TFile, app: any) {
const settings = NMPSettings.getInstance();
const { sliceImageSavePath, sliceImageWidth, sliceImageAspectRatio } = settings;
// 解析比例
const ratio = parseAspectRatio(sliceImageAspectRatio);
const sliceHeight = Math.round((sliceImageWidth * ratio.height) / ratio.width);
// 获取 slug
const slug = getSlugFromFile(file, app);
new Notice(`开始切图:${slug},宽度=${sliceImageWidth},比例=${sliceImageAspectRatio}`);
try {
// 1. 保存原始样式
const originalWidth = articleElement.style.width;
const originalMaxWidth = articleElement.style.maxWidth;
const originalMinWidth = articleElement.style.minWidth;
// 2. 临时设置为目标宽度进行渲染
articleElement.style.width = `${sliceImageWidth}px`;
articleElement.style.maxWidth = `${sliceImageWidth}px`;
articleElement.style.minWidth = `${sliceImageWidth}px`;
// 等待样式生效和重排
await new Promise(resolve => setTimeout(resolve, 100));
new Notice(`设置渲染宽度: ${sliceImageWidth}px`);
// 3. 生成长图 - 使用实际渲染宽度
new Notice('正在生成长图...');
const longImageDataURL = await toPng(articleElement, {
width: sliceImageWidth,
pixelRatio: 1,
cacheBust: true,
});
// 4. 恢复原始样式
articleElement.style.width = originalWidth;
articleElement.style.maxWidth = originalMaxWidth;
articleElement.style.minWidth = originalMinWidth;
// 5. 创建临时 Image 对象以获取长图实际高度
const img = new Image();
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();
img.onerror = reject;
img.src = longImageDataURL;
});
const fullHeight = img.height;
const fullWidth = img.width;
new Notice(`长图生成完成:${fullWidth}x${fullHeight}px`);
// 3. 保存完整长图
ensureDir(sliceImageSavePath);
const longImagePath = path.join(sliceImageSavePath, `${slug}.png`);
const longImageBuffer = dataURLToBuffer(longImageDataURL);
fs.writeFileSync(longImagePath, new Uint8Array(longImageBuffer));
new Notice(`长图已保存:${longImagePath}`);
// 4. 计算需要切多少片
const sliceCount = Math.ceil(fullHeight / sliceHeight);
new Notice(`开始切图:共 ${sliceCount} 张,每张 ${sliceImageWidth}x${sliceHeight}px`);
// 5. 使用 Canvas 裁剪
const canvas = document.createElement('canvas');
canvas.width = sliceImageWidth;
canvas.height = sliceHeight;
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('无法创建 Canvas 上下文');
}
for (let i = 0; i < sliceCount; i++) {
const yOffset = i * sliceHeight;
const actualHeight = Math.min(sliceHeight, fullHeight - yOffset);
// 清空画布(处理最后一张可能不足高度的情况,用白色填充)
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, sliceImageWidth, sliceHeight);
// 绘制裁剪区域
ctx.drawImage(
img,
0, yOffset, fullWidth, actualHeight, // 源区域
0, 0, sliceImageWidth, actualHeight // 目标区域
);
// 导出为 PNG
const sliceDataURL = canvas.toDataURL('image/png');
const sliceBuffer = dataURLToBuffer(sliceDataURL);
const sliceFilename = `${slug}_${i + 1}.png`;
const slicePath = path.join(sliceImageSavePath, sliceFilename);
fs.writeFileSync(slicePath, new Uint8Array(sliceBuffer));
new Notice(`已保存:${sliceFilename}`);
}
new Notice(`✅ 切图完成!共 ${sliceCount} 张图片,保存在:${sliceImageSavePath}`);
} catch (error) {
console.error('切图失败:', error);
new Notice(`❌ 切图失败:${error.message}`);
}
}

View File

@@ -3,8 +3,8 @@
* 作用:通用工具函数集合(事件、版本、字符串处理等)。
*/
import { App, sanitizeHTMLToDom, requestUrl, Platform } from "obsidian";
import * as postcss from "./postcss/postcss";
import { App, sanitizeHTMLToDom, Platform } from "obsidian";
import * as postcss from "./postcss/postcss"; // 内置 PostCSS runtime解析主题 CSS 用于内联样式
let PluginVersion = "0.0.0";
let PlugPlatform = "obsidian";
@@ -185,10 +185,7 @@ export function applyCSS(html: string, css: string) {
}
export function uevent(name: string) {
const url = `https://u.sunboshi.tech/event?name=${name}&platform=${PlugPlatform}&v=${PluginVersion}`;
requestUrl(url).then().catch(error => {
console.error("Failed to send event: " + url, error);
});
console.debug(`[uevent] ${name} @${PlugPlatform} v${PluginVersion}`);
}
/**

View File

@@ -9,7 +9,7 @@
* 4. 提供文章导出HTML功能
*/
import { Notice, Platform, TFile, TFolder } from 'obsidian';
import { Notice, Platform, TFile } from 'obsidian';
import { NMPSettings } from '../settings';
import AssetsManager from '../assets';
import { ArticleRender } from '../article-render';
@@ -32,8 +32,9 @@ export class WechatPreview {
currentHighlight: string;
// UI 元素
toolbar: HTMLDivElement | null = null;
renderDiv: HTMLDivElement | null = null;
board: HTMLDivElement | null = null;
contentCell: HTMLElement | null = null;
contentEl: HTMLElement | null = null;
wechatSelect: HTMLSelectElement | null = null;
themeSelect: HTMLSelectElement | null = null;
highlightSelect: HTMLSelectElement | null = null;
@@ -65,44 +66,46 @@ export class WechatPreview {
*/
build(): void {
this.container.empty();
this.container.addClass('wechat-preview-container');
// 创建工具栏
this.toolbar = this.container.createDiv({ cls: 'preview-toolbar' });
this.buildToolbar(this.toolbar);
this.board = this.container.createDiv({ cls: 'wechat-board' });
// 创建渲染区域
this.renderDiv = this.container.createDiv({ cls: 'render-div' });
this.renderDiv.id = 'render-div';
this.buildAccountRow();
// 将 ArticleRender 的 style 与内容节点挂载
try {
if (this.render && this.render.styleEl && !this.renderDiv.contains(this.render.styleEl)) {
this.renderDiv.appendChild(this.render.styleEl);
}
if (this.render && this.render.articleDiv && !this.renderDiv.contains(this.render.articleDiv)) {
// 容器样式:模拟公众号编辑器宽度,更好的排版显示
this.render.articleDiv.addClass('wechat-article-wrapper');
this.renderDiv.appendChild(this.render.articleDiv);
}
} catch (e) {
console.warn('[WechatPreview] 挂载文章容器失败', e);
}
//this.buildCoverRow();
//this.buildStyleRow();
this.contentCell = this.createCell('content');
this.contentCell.addClass('wechat-cell-content');
this.mountArticle(this.board);
}
/**
* 构建工具栏
*/
private buildToolbar(parent: HTMLDivElement): void {
let lineDiv;
private createCell(area: string, tag: keyof HTMLElementTagNameMap = 'div', extraClasses: string[] = []): HTMLElement {
if (!this.board) {
throw new Error('Wechat board not initialized');
}
const cell = this.board.createEl(tag, { attr: { 'data-area': area } });
cell.addClass('wechat-cell');
for (const cls of extraClasses) {
cell.addClass(cls);
}
return cell;
}
// 公众号选择
if (this.settings.wxInfo.length > 1 || Platform.isDesktop) {
lineDiv = parent.createDiv({ cls: 'toolbar-line' });
private buildAccountRow(): void {
const selectCell = this.createCell('account-select');
const selectLabel = selectCell.createEl('label', {
cls: 'style-label',
attr: { for: 'wechat-account-select' },
text: '公众号'
});
selectLabel.addClass('wechat-account-label');
const wxLabel = lineDiv.createDiv({ cls: 'style-label' });
wxLabel.innerText = '公众号';
const wxSelect = lineDiv.createEl('select', { cls: 'wechat-select' });
const wxSelect = selectCell.createEl('select', {
cls: 'wechat-select',
attr: { id: 'wechat-account-select' }
}) as HTMLSelectElement;
wxSelect.onchange = async () => {
this.currentAppId = wxSelect.value;
this.onAppIdChanged();
@@ -124,53 +127,34 @@ export class WechatPreview {
}
this.wechatSelect = wxSelect;
const actionsCell = this.createCell('account-back-export');
if (Platform.isDesktop) {
const separator = lineDiv.createDiv({ cls: 'toolbar-separator' });
const openBtn = lineDiv.createEl('button', { text: '🌐 去公众号后台', cls: 'toolbar-button purple-gradient' });
const openBtn = actionsCell.createEl('button', {
text: '访问后台',
cls: 'toolbar-button purple-gradient wechat-action-button'
});
openBtn.onclick = async () => {
const { shell } = require('electron');
shell.openExternal('https://mp.weixin.qq.com');
uevent('open-mp');
};
}
}
// 操作按钮行
lineDiv = parent.createDiv({ cls: 'toolbar-line flex-wrap' });
const refreshBtn = lineDiv.createEl('button', { text: '🔄 刷新', cls: 'toolbar-button purple-gradient' });
refreshBtn.onclick = async () => {
if (this.onRefreshCallback) {
await this.onRefreshCallback();
}
};
const postBtn = lineDiv.createEl('button', { text: '📝 发布', cls: 'toolbar-button' });
postBtn.onclick = async () => await this.postArticle();
if (Platform.isDesktop && this.settings.isAuthKeyVaild()) {
const htmlBtn = lineDiv.createEl('button', { text: '💾 导出HTML', cls: 'toolbar-button' });
htmlBtn.onclick = async () => await this.exportHTML();
const exportBtn = actionsCell.createEl('button', { text: '导出页面', cls: 'toolbar-button wechat-action-button' });
exportBtn.onclick = async () => await this.exportHTML();
}
// 封面选择
this.buildCoverSelector(parent);
// 样式选择(如果启用)
if (this.settings.showStyleUI) {
this.buildStyleSelector(parent);
if (actionsCell.childElementCount === 0) {
actionsCell.addClass('wechat-cell-placeholder');
}
}
/**
* 构建封面选择器
*/
private buildCoverSelector(parent: HTMLDivElement): void {
const lineDiv = parent.createDiv({ cls: 'toolbar-line' });
const coverTitle = lineDiv.createDiv({ cls: 'style-label' });
coverTitle.innerText = '封面';
private buildCoverRow(): void {
const selectCell = this.createCell('cover-select');
selectCell.createDiv({ cls: 'style-label', text: '封面' });
this.useDefaultCover = lineDiv.createEl('input', { cls: 'input-style' });
this.useDefaultCover = selectCell.createEl('input', { cls: 'input-style' }) as HTMLInputElement;
this.useDefaultCover.setAttr('type', 'radio');
this.useDefaultCover.setAttr('name', 'cover');
this.useDefaultCover.setAttr('value', 'default');
@@ -181,45 +165,42 @@ export class WechatPreview {
this.coverEl.setAttr('style', 'visibility:hidden;width:0px;');
}
};
selectCell.createEl('label', { text: '默认', attr: { for: 'default-cover' } });
const defaultLabel = lineDiv.createEl('label');
defaultLabel.innerText = '默认';
defaultLabel.setAttr('for', 'default-cover');
this.useLocalCover = lineDiv.createEl('input', { cls: 'input-style' });
this.useLocalCover = selectCell.createEl('input', { cls: 'input-style' }) as HTMLInputElement;
this.useLocalCover.setAttr('type', 'radio');
this.useLocalCover.setAttr('name', 'cover');
this.useLocalCover.setAttr('value', 'local');
this.useLocalCover.id = 'local-cover';
this.useLocalCover.setAttr('style', 'margin-left:20px;');
this.useLocalCover.onchange = async () => {
if (this.useLocalCover?.checked && this.coverEl) {
this.coverEl.setAttr('style', 'visibility:visible;width:180px;');
}
};
selectCell.createEl('label', { text: '上传', attr: { for: 'local-cover' } });
const localLabel = lineDiv.createEl('label');
localLabel.setAttr('for', 'local-cover');
localLabel.innerText = '上传';
this.coverEl = lineDiv.createEl('input', { cls: 'upload-input' });
this.coverEl.setAttr('type', 'file');
this.coverEl.setAttr('placeholder', '封面图片');
this.coverEl.setAttr('accept', '.png, .jpg, .jpeg');
this.coverEl.setAttr('name', 'cover');
this.coverEl.id = 'cover-input';
const inputCell = this.createCell('cover-input');
this.coverEl = inputCell.createEl('input', {
cls: 'upload-input',
attr: {
type: 'file',
placeholder: '封面图片',
accept: '.png, .jpg, .jpeg',
name: 'cover',
id: 'cover-input'
}
}) as HTMLInputElement;
if (this.useDefaultCover?.checked) {
this.coverEl.setAttr('style', 'visibility:hidden;width:0px;');
}
}
/**
* 构建样式选择器
*/
private buildStyleSelector(parent: HTMLDivElement): void {
const lineDiv = parent.createDiv({ cls: 'toolbar-line flex-wrap' });
private buildStyleRow(): void {
const styleLabelCell = this.createCell('style-label', 'div', ['style-label']);
styleLabelCell.setText('样式');
const cssStyle = lineDiv.createDiv({ cls: 'style-label' });
cssStyle.innerText = '样式';
const selectBtn = lineDiv.createEl('select', { cls: 'style-select' });
const styleSelectCell = this.createCell('style-select');
const selectBtn = styleSelectCell.createEl('select', { cls: 'style-select wechat-style-select' }) as HTMLSelectElement;
selectBtn.onchange = async () => {
this.currentTheme = selectBtn.value;
this.render.updateStyle(selectBtn.value);
@@ -233,12 +214,11 @@ export class WechatPreview {
}
this.themeSelect = selectBtn;
const separator = lineDiv.createDiv({ cls: 'toolbar-separator' });
const highlightLabelCell = this.createCell('highlight-label', 'div', ['style-label']);
highlightLabelCell.setText('代码高亮');
const highlightStyle = lineDiv.createDiv({ cls: 'style-label' });
highlightStyle.innerText = '代码高亮';
const highlightStyleBtn = lineDiv.createEl('select', { cls: 'style-select' });
const highlightSelectCell = this.createCell('highlight-select');
const highlightStyleBtn = highlightSelectCell.createEl('select', { cls: 'style-select wechat-style-select' }) as HTMLSelectElement;
highlightStyleBtn.onchange = async () => {
this.currentHighlight = highlightStyleBtn.value;
this.render.updateHighLight(highlightStyleBtn.value);
@@ -254,6 +234,29 @@ export class WechatPreview {
this.highlightSelect = highlightStyleBtn;
}
private mountArticle(_parent: HTMLElement): void {
if (!this.contentCell) {
return;
}
try {
if (this.render?.styleEl && !this.contentCell.contains(this.render.styleEl)) {
this.contentCell.appendChild(this.render.styleEl);
}
if (this.render?.articleDiv) {
this.render.articleDiv.addClass('wechat-article-wrapper');
if (this.render.articleDiv.parentElement !== this.contentCell) {
this.contentCell.appendChild(this.render.articleDiv);
}
this.contentEl = this.render.articleDiv;
}
} catch (error) {
console.warn('[WechatPreview] 挂载文章容器失败', error);
}
}
/**
* 构建封面选择器
*/
/**
* 显示微信预览视图
*/
@@ -377,8 +380,9 @@ export class WechatPreview {
* 清理资源
*/
destroy(): void {
this.toolbar = null;
this.renderDiv = null;
this.board = null;
this.contentCell = null;
this.contentEl = null;
this.wechatSelect = null;
this.themeSelect = null;
this.highlightSelect = null;
@@ -398,6 +402,16 @@ export class WechatPreview {
/** 对外:发布草稿(供外层菜单调用) */
async postDraft() { await this.postArticle(); }
async publish(): Promise<void> {
await this.postDraft();
}
async refresh(): Promise<void> {
if (this.onRefreshCallback) {
await this.onRefreshCallback();
}
}
/** 由上层在切换/渲染时注入当前文件 */
setFile(file: TFile | null) { this.currentFile = file; }
}

View File

@@ -22,13 +22,14 @@ function parseAspectRatio(ratio: string): { width: number; height: number } {
return { width: 3, height: 4 };
}
const PAGE_PADDING = 40; // 与 renderPage 保持一致的页面内边距
/**
* 计算目标页面高度
*/
function getTargetPageHeight(settings: NMPSettings): number {
const ratio = parseAspectRatio(settings.sliceImageAspectRatio);
const height = Math.round((settings.sliceImageWidth * ratio.height) / ratio.width);
console.log(`[paginator] 计算页面高度: 宽度=${settings.sliceImageWidth}, 比例=${settings.sliceImageAspectRatio} (${ratio.width}:${ratio.height}), 高度=${height}`);
return height;
}
@@ -57,16 +58,37 @@ export async function paginateArticle(
const pageHeight = getTargetPageHeight(settings);
const pageWidth = settings.sliceImageWidth;
// 创建临时容器用于测量
const measureContainer = document.createElement('div');
measureContainer.style.cssText = `
// 创建临时测量容器:与实际页面一致的宽度与内边距
const measureHost = document.createElement('div');
measureHost.style.cssText = `
position: absolute;
left: -9999px;
top: 0;
width: ${pageWidth}px;
visibility: hidden;
box-sizing: border-box;
`;
document.body.appendChild(measureContainer);
document.body.appendChild(measureHost);
const measurePage = document.createElement('div');
measurePage.className = 'xhs-page';
measurePage.style.boxSizing = 'border-box';
measurePage.style.width = `${pageWidth}px`;
measurePage.style.padding = `${PAGE_PADDING}px`;
measurePage.style.background = 'white';
measurePage.style.position = 'relative';
measureHost.appendChild(measurePage);
const measureContent = document.createElement('div');
measureContent.className = 'xhs-page-content';
measurePage.appendChild(measureContent);
if (articleElement.classList.length > 0) {
measureContent.classList.add(...Array.from(articleElement.classList));
}
const measuredFontSize = window.getComputedStyle(articleElement).fontSize;
if (measuredFontSize) {
measureContent.style.fontSize = measuredFontSize;
}
const pages: PageInfo[] = [];
let currentPageContent: Element[] = [];
@@ -79,51 +101,45 @@ export async function paginateArticle(
for (const child of children) {
const childClone = child.cloneNode(true) as HTMLElement;
measureContainer.innerHTML = '';
measureContainer.appendChild(childClone);
measureContent.appendChild(childClone);
// 等待浏览器完成渲染
await new Promise(resolve => setTimeout(resolve, 10));
await waitForLayout();
const childHeight = childClone.offsetHeight;
const totalHeight = measurePage.scrollHeight;
const isIndivisible = isIndivisibleElement(child);
const fitsCurrentPage =
totalHeight <= pageHeight ||
(!isIndivisible && totalHeight <= pageHeight * 1.1) ||
currentPageContent.length === 0;
// 判断是否需要换页
if (currentPageHeight + childHeight > pageHeight && currentPageContent.length > 0) {
// 如果是不可分割元素且加入后会超出,先保存当前页
if (isIndivisible) {
if (fitsCurrentPage) {
currentPageContent.push(child);
currentPageHeight = totalHeight;
continue;
}
// 当前页已放不下:移除刚刚加入的克隆节点
measureContent.removeChild(childClone);
await waitForLayout();
if (currentPageContent.length > 0) {
pages.push({
index: pageIndex++,
content: wrapPageContent(currentPageContent),
height: currentPageHeight
});
currentPageContent = [child];
currentPageHeight = childHeight;
} else {
// 可分割元素(段落等),尝试加入当前页
if (currentPageHeight + childHeight <= pageHeight * 1.1) {
// 允许 10% 的溢出容差
currentPageContent.push(child);
currentPageHeight += childHeight;
} else {
// 超出太多,换页
pages.push({
index: pageIndex++,
content: wrapPageContent(currentPageContent),
height: currentPageHeight
});
currentPageContent = [child];
currentPageHeight = childHeight;
}
}
} else {
// 加入当前页
currentPageContent.push(child);
currentPageHeight += childHeight;
}
}
// 保存最后一页
currentPageContent = [child];
measureContent.innerHTML = '';
const firstClone = child.cloneNode(true) as HTMLElement;
measureContent.appendChild(firstClone);
await waitForLayout();
currentPageHeight = measurePage.scrollHeight;
// 不可分割元素即使超过高度也直接保留在新页
}
if (currentPageContent.length > 0) {
pages.push({
index: pageIndex,
@@ -132,12 +148,15 @@ export async function paginateArticle(
});
}
// 清理临时容器
document.body.removeChild(measureContainer);
document.body.removeChild(measureHost);
return pages;
}
async function waitForLayout(): Promise<void> {
await new Promise<void>(resolve => requestAnimationFrame(() => resolve()));
}
/**
* 包装页面内容为完整的 HTML
*/
@@ -163,8 +182,6 @@ export function renderPage(
const actualPageWidth = settings.sliceImageWidth;
const actualPageHeight = getTargetPageHeight(settings);
console.log(`[renderPage] 渲染页面: 宽=${actualPageWidth}, 高=${actualPageHeight}`);
container.innerHTML = '';
// 直接设置为实际尺寸,用于切图
@@ -174,7 +191,7 @@ export function renderPage(
height: ${actualPageHeight}px;
overflow: hidden;
box-sizing: border-box;
padding: 40px;
padding: ${PAGE_PADDING}px;
background: white;
`;

View File

@@ -15,6 +15,14 @@ import AssetsManager from '../assets';
import { paginateArticle, renderPage, PageInfo } from './paginator';
import { sliceCurrentPage, sliceAllPages } from './slice';
const XHS_PREVIEW_DEFAULT_WIDTH = 540;
const XHS_PREVIEW_WIDTH_OPTIONS = [1080, 720, 540, 360];
// 字号控制常量:一处修改即可同步 UI 显示、输入校验和渲染逻辑
const XHS_FONT_SIZE_MIN = 18;
const XHS_FONT_SIZE_MAX = 45;
const XHS_FONT_SIZE_DEFAULT = 36;
/**
* 小红书预览视图类
*/
@@ -26,21 +34,20 @@ export class XiaohongshuPreview {
currentFile: TFile | null = null;
// UI 元素
topToolbar!: HTMLDivElement;
templateSelect!: HTMLSelectElement;
fontSizeInput!: HTMLInputElement;
previewWidthSelect!: HTMLSelectElement;
pageContainer!: HTMLDivElement;
bottomToolbar!: HTMLDivElement;
pageNavigation!: HTMLDivElement;
pageNumberDisplay!: HTMLSpanElement;
pageNumberInput!: HTMLInputElement;
pageTotalLabel!: HTMLSpanElement;
styleEl: HTMLStyleElement | null = null; // 主题样式注入节点
currentThemeClass: string = '';
// 分页数据
pages: PageInfo[] = [];
currentPageIndex: number = 0;
currentFontSize: number = 16;
currentFontSize: number = XHS_FONT_SIZE_DEFAULT;
articleHTML: string = '';
// 回调函数
@@ -70,92 +77,103 @@ export class XiaohongshuPreview {
this.container.appendChild(this.styleEl);
}
// 顶部工具栏
this.buildTopToolbar();
const board = this.container.createDiv({ cls: 'xhs-board' });
// 页面容器
this.pageContainer = this.container.createDiv({ cls: 'xhs-page-container' });
// 分页导航
this.buildPageNavigation();
// 底部操作栏
this.buildBottomToolbar();
}
/**
* 构建顶部工具栏
*/
private buildTopToolbar(): void {
this.topToolbar = this.container.createDiv({ cls: 'xhs-top-toolbar' });
// 刷新按钮
const refreshBtn = this.topToolbar.createEl('button', { text: '🔄 刷新', cls: 'toolbar-button purple-gradient' });
refreshBtn.onclick = () => this.onRefresh();
// 发布按钮
const publishBtn = this.topToolbar.createEl('button', { text: '📤 发布', cls: 'toolbar-button' });
publishBtn.onclick = () => this.onPublish();
// 分隔线
const separator2 = this.topToolbar.createDiv({ cls: 'toolbar-separator' });
// 模板选择
const templateLabel = this.topToolbar.createDiv({ cls: 'toolbar-label' });
templateLabel.innerText = '模板';
this.templateSelect = this.topToolbar.createEl('select', { cls: 'xhs-select' });
const templateCard = this.createGridCard(board, 'xhs-area-template');
const templateLabel = templateCard.createDiv({ cls: 'xhs-label', text: '模板' });
this.templateSelect = templateCard.createEl('select', { cls: 'xhs-select' });
['默认模板', '简约模板', '杂志模板'].forEach(name => {
const option = this.templateSelect.createEl('option');
option.value = name;
option.text = name;
});
// 字号控制(可直接编辑)
const fontSizeLabel = this.topToolbar.createDiv({ cls: 'toolbar-label' });
fontSizeLabel.innerText = '字号';
const fontSizeGroup = this.topToolbar.createDiv({ cls: 'font-size-group' });
const previewCard = this.createGridCard(board, 'xhs-area-preview');
const previewLabel = previewCard.createDiv({ cls: 'xhs-label', text: '宽度' });
this.previewWidthSelect = previewCard.createEl('select', { cls: 'xhs-select' });
const currentPreviewWidth = this.settings.xhsPreviewWidth || XHS_PREVIEW_DEFAULT_WIDTH;
XHS_PREVIEW_WIDTH_OPTIONS.forEach(value => {
const option = this.previewWidthSelect.createEl('option');
option.value = String(value);
option.text = `${value}px`;
});
if (!XHS_PREVIEW_WIDTH_OPTIONS.includes(currentPreviewWidth)) {
const customOption = this.previewWidthSelect.createEl('option');
customOption.value = String(currentPreviewWidth);
customOption.text = `${currentPreviewWidth}px`;
}
this.previewWidthSelect.value = String(currentPreviewWidth);
this.previewWidthSelect.onchange = async () => {
const value = parseInt(this.previewWidthSelect.value, 10);
if (Number.isFinite(value) && value > 0) {
await this.onPreviewWidthChanged(value);
} else {
this.previewWidthSelect.value = String(this.settings.xhsPreviewWidth || XHS_PREVIEW_DEFAULT_WIDTH);
}
};
const fontCard = this.createGridCard(board, 'xhs-area-font');
//fontCard.createDiv({ cls: 'xhs-label', text: '字号' });
const fontSizeGroup = fontCard.createDiv({ cls: 'font-size-group' });
const decreaseBtn = fontSizeGroup.createEl('button', { text: '', cls: 'font-size-btn' });
decreaseBtn.onclick = () => this.changeFontSize(-1);
this.fontSizeInput = fontSizeGroup.createEl('input', {
cls: 'font-size-input',
attr: { type: 'number', min: '12', max: '36', value: '16' }
attr: {
type: 'number',
min: String(XHS_FONT_SIZE_MIN),
max: String(XHS_FONT_SIZE_MAX),
value: String(XHS_FONT_SIZE_DEFAULT)
}
});
this.fontSizeInput.style.width = '50px';
this.fontSizeInput.style.textAlign = 'center';
this.fontSizeInput.onchange = () => this.onFontSizeInputChanged();
const increaseBtn = fontSizeGroup.createEl('button', { text: '', cls: 'font-size-btn' });
increaseBtn.onclick = () => this.changeFontSize(1);
}
/**
* 构建分页导航
*/
private buildPageNavigation(): void {
this.pageNavigation = this.container.createDiv({ cls: 'xhs-page-navigation' });
const contentWrapper = board.createDiv({ cls: 'xhs-area-content' });
this.pageContainer = contentWrapper.createDiv({ cls: 'xhs-page-container' });
const prevBtn = this.pageNavigation.createEl('button', { text: '', cls: 'xhs-nav-btn' });
const paginationCard = this.createGridCard(board, 'xhs-area-pagination xhs-pagination');
const prevBtn = paginationCard.createEl('button', { text: '', cls: 'xhs-nav-btn' });
prevBtn.onclick = () => this.previousPage();
this.pageNumberDisplay = this.pageNavigation.createEl('span', { text: '1/1', cls: 'xhs-page-number' });
const indicator = paginationCard.createDiv({ cls: 'xhs-page-indicator' });
this.pageNumberInput = indicator.createEl('input', {
cls: 'xhs-page-number-input',
attr: { type: 'text', value: '1', inputmode: 'numeric', 'aria-label': '当前页码' }
}) as HTMLInputElement;
this.pageNumberInput.onfocus = () => this.pageNumberInput.select();
this.pageNumberInput.onkeydown = (evt: KeyboardEvent) => {
if (evt.key === 'Enter') {
evt.preventDefault();
this.handlePageNumberInput();
}
};
this.pageNumberInput.oninput = () => {
const sanitized = this.pageNumberInput.value.replace(/\D/g, '');
if (sanitized !== this.pageNumberInput.value) {
this.pageNumberInput.value = sanitized;
}
};
this.pageNumberInput.onblur = () => this.handlePageNumberInput();
const nextBtn = this.pageNavigation.createEl('button', { text: '', cls: 'xhs-nav-btn' });
this.pageTotalLabel = indicator.createEl('span', { cls: 'xhs-page-number-total', text: '/1' });
const nextBtn = paginationCard.createEl('button', { text: '', cls: 'xhs-nav-btn' });
nextBtn.onclick = () => this.nextPage();
const sliceCard = this.createGridCard(board, 'xhs-area-slice');
const sliceCurrentBtn = sliceCard.createEl('button', { text: '当前页切图', cls: 'xhs-slice-btn' });
sliceCurrentBtn.onclick = () => this.sliceCurrentPage();
const sliceAllBtn = sliceCard.createEl('button', { text: '全部页切图', cls: 'xhs-slice-btn secondary' });
sliceAllBtn.onclick = () => this.sliceAllPages();
}
/**
* 构建底部操作栏
*/
private buildBottomToolbar(): void {
this.bottomToolbar = this.container.createDiv({ cls: 'xhs-bottom-toolbar' });
const currentPageBtn = this.bottomToolbar.createEl('button', { text: '⬇ 当前页切图', cls: 'xhs-slice-btn' });
currentPageBtn.onclick = () => this.sliceCurrentPage();
const allPagesBtn = this.bottomToolbar.createEl('button', { text: '⇓ 全部页切图', cls: 'xhs-slice-btn secondary' });
allPagesBtn.onclick = () => this.sliceAllPages();
private createGridCard(parent: HTMLElement, areaClass: string): HTMLDivElement {
return parent.createDiv({ cls: `xhs-card ${areaClass}` });
}
/**
@@ -171,15 +189,21 @@ export class XiaohongshuPreview {
const tempContainer = document.createElement('div');
tempContainer.innerHTML = articleHTML;
tempContainer.style.width = `${this.settings.sliceImageWidth}px`;
tempContainer.classList.add('note-to-mp');
if (this.currentThemeClass) {
tempContainer.classList.add(this.currentThemeClass);
}
tempContainer.style.fontSize = `${this.currentFontSize}px`;
document.body.appendChild(tempContainer);
try {
// 在分页前先应用主题与高亮,确保测量使用正确样式
this.applyThemeCSS();
this.pages = await paginateArticle(tempContainer, this.settings);
new Notice(`分页完成:共 ${this.pages.length}`);
this.currentPageIndex = 0;
// 初次渲染时应用当前主题
this.applyThemeCSS();
this.renderCurrentPage();
} finally {
document.body.removeChild(tempContainer);
@@ -205,12 +229,97 @@ export class XiaohongshuPreview {
if (this.currentThemeClass) classes.push('note-to-mp');
const pageElement = wrapper.createDiv({ cls: classes.join(' ') });
renderPage(pageElement, page.content, this.settings);
this.applyPreviewSizing(wrapper, pageElement);
// 应用字体设置
this.applyFontSettings(pageElement);
// 更新页码显示
this.pageNumberDisplay.innerText = `${this.currentPageIndex + 1}/${this.pages.length}`;
this.updatePageNumberDisplay();
}
private updatePageNumberDisplay(): void {
if (!this.pageNumberInput || !this.pageTotalLabel) return;
const total = this.pages.length;
if (total === 0) {
this.pageNumberInput.value = '0';
this.pageTotalLabel.innerText = '/0';
return;
}
const current = Math.min(this.currentPageIndex + 1, total);
this.pageNumberInput.value = String(current);
this.pageTotalLabel.innerText = `/${total}`;
}
private handlePageNumberInput(): void {
if (!this.pageNumberInput) return;
const total = this.pages.length;
if (total === 0) {
this.pageNumberInput.value = '0';
if (this.pageTotalLabel) this.pageTotalLabel.innerText = '/0';
return;
}
const raw = this.pageNumberInput.value.trim();
if (raw.length === 0) {
this.updatePageNumberDisplay();
return;
}
const parsed = parseInt(raw, 10);
if (!Number.isFinite(parsed)) {
this.updatePageNumberDisplay();
return;
}
const target = Math.min(Math.max(parsed, 1), total) - 1;
if (target !== this.currentPageIndex) {
this.currentPageIndex = target;
this.renderCurrentPage();
} else {
this.updatePageNumberDisplay();
}
}
/**
* 根据设置的宽度和横竖比应用预览尺寸与缩放
*/
private applyPreviewSizing(wrapper: HTMLElement, pageElement: HTMLElement): void {
const configuredWidth = this.settings.sliceImageWidth || 1080;
const actualWidth = Math.max(1, configuredWidth);
const ratio = this.parseAspectRatio(this.settings.sliceImageAspectRatio);
const actualHeight = Math.round((actualWidth * ratio.height) / ratio.width);
const previewWidthSetting = this.settings.xhsPreviewWidth || XHS_PREVIEW_DEFAULT_WIDTH;
const previewWidth = Math.max(1, previewWidthSetting);
const scale = Math.max(previewWidth / actualWidth, 0.01);
const previewHeight = Math.max(1, Math.round(actualHeight * scale));
wrapper.style.width = `${previewWidth}px`;
wrapper.style.height = `${previewHeight}px`;
pageElement.style.width = `${actualWidth}px`;
pageElement.style.height = `${actualHeight}px`;
pageElement.style.transform = `scale(${scale})`;
pageElement.style.transformOrigin = 'top left';
pageElement.style.position = 'absolute';
pageElement.style.top = '0';
pageElement.style.left = '0';
}
private async onPreviewWidthChanged(newWidth: number): Promise<void> {
if (newWidth <= 0) return;
if (this.settings.xhsPreviewWidth === newWidth) return;
this.settings.xhsPreviewWidth = newWidth;
await this.persistSettings();
this.renderCurrentPage();
}
/**
* 解析横竖比例字符串
*/
private parseAspectRatio(ratio: string | undefined): { width: number; height: number } {
const parts = (ratio ?? '').split(':').map(part => parseFloat(part.trim()));
if (parts.length === 2 && isFinite(parts[0]) && isFinite(parts[1]) && parts[0] > 0 && parts[1] > 0) {
return { width: parts[0], height: parts[1] };
}
return { width: 3, height: 4 };
}
/**
@@ -224,7 +333,7 @@ export class XiaohongshuPreview {
* 切换字号(± 按钮)
*/
private async changeFontSize(delta: number): Promise<void> {
this.currentFontSize = Math.max(12, Math.min(36, this.currentFontSize + delta));
this.currentFontSize = Math.max(XHS_FONT_SIZE_MIN, Math.min(XHS_FONT_SIZE_MAX, this.currentFontSize + delta));
this.fontSizeInput.value = String(this.currentFontSize);
await this.repaginateAndRender();
}
@@ -234,9 +343,9 @@ export class XiaohongshuPreview {
*/
private async onFontSizeInputChanged(): Promise<void> {
const val = parseInt(this.fontSizeInput.value, 10);
if (isNaN(val) || val < 12 || val > 36) {
if (isNaN(val) || val < XHS_FONT_SIZE_MIN || val > XHS_FONT_SIZE_MAX) {
this.fontSizeInput.value = String(this.currentFontSize);
new Notice('字号范围: 12-36');
new Notice(`字号范围: ${XHS_FONT_SIZE_MIN}-${XHS_FONT_SIZE_MAX}`);
return;
}
this.currentFontSize = val;
@@ -306,6 +415,14 @@ export class XiaohongshuPreview {
}
}
async refresh(): Promise<void> {
await this.onRefresh();
}
async publish(): Promise<void> {
await this.onPublish();
}
/**
* 全部页切图
*/
@@ -341,6 +458,17 @@ export class XiaohongshuPreview {
}
}
private async persistSettings(): Promise<void> {
try {
const plugin = (this.app as any)?.plugins?.getPlugin?.('note-to-mp');
if (plugin?.saveSettings) {
await plugin.saveSettings();
}
} catch (error) {
console.warn('[XiaohongshuPreview] 保存设置失败', error);
}
}
/**
* 显示小红书预览视图
*/
@@ -363,13 +491,12 @@ export class XiaohongshuPreview {
* 清理资源
*/
destroy(): void {
this.topToolbar = null as any;
this.templateSelect = null as any;
this.previewWidthSelect = null as any;
this.fontSizeInput = null as any;
this.pageContainer = null as any;
this.bottomToolbar = null as any;
this.pageNavigation = null as any;
this.pageNumberDisplay = null as any;
this.pageNumberInput = null as any;
this.pageTotalLabel = null as any;
this.pages = [];
this.currentFile = null;
this.styleEl = null;

View File

@@ -2,36 +2,96 @@
/* =========================================================== */
/* UI 样式 */
/* 共用样式与去重 */
/* =========================================================== */
/* 主题变量统一常用色值/阴影/渐变 */
:root {
--c-bg: #ffffff;
--c-border: #dadce0;
--c-text-muted: #5f6368;
--c-primary: #1e88e5;
--c-primary-dark: #1565c0;
--c-purple: #667eea;
--c-purple-dark: #764ba2;
--c-blue-2: #42a5f5;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
--shadow-overlay: 0 2px 4px rgba(0,0,0,0.04);
--shadow-primary-2: 0 2px 6px rgba(30, 136, 229, 0.3);
--shadow-primary-4: 0 4px 8px rgba(30, 136, 229, 0.4);
--shadow-purple-2: 0 2px 6px rgba(102, 126, 234, 0.3);
--shadow-purple-4: 0 4px 8px rgba(102, 126, 234, 0.4);
--grad-primary: linear-gradient(135deg, var(--c-primary) 0%, var(--c-primary-dark) 100%);
--grad-purple: linear-gradient(135deg, var(--c-purple) 0%, var(--c-purple-dark) 100%);
--grad-blue: linear-gradient(135deg, var(--c-blue-2) 0%, var(--c-primary) 100%);
--grad-toolbar: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
--grad-toolbar-bottom: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
--grad-xhs-bg: linear-gradient(135deg, #f5f7fa 0%, #e8eaf6 100%);
}
/* 通用按钮外观(不含背景与尺寸) */
.copy-button,
.refresh-button,
.toolbar-button,
.msg-ok-btn,
.xhs-slice-btn {
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
}
/* 通用按钮 hover 的位移效果(各自保留独立阴影) */
.copy-button:hover,
.refresh-button:hover,
.toolbar-button:hover,
.msg-ok-btn:hover {
transform: translateY(-1px);
}
/* 下拉选择的通用外观(各自保留尺寸差异) */
.platform-select,
.wechat-select,
.style-select {
border: 1px solid var(--c-border);
border-radius: 6px;
background: white;
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: var(--shadow-sm);
}
/* 平台与公众号选择的相同 hover/focus 效果style-select 单独增强) */
.platform-select:hover,
.wechat-select:hover { border-color: var(--c-primary); }
.platform-select:focus,
.wechat-select:focus { outline: none; border-color: var(--c-primary); }
.note-preview {
grid-template-rows: auto 1fr;
grid-template-columns: 1fr;
display: grid;
min-height: 100%;
width: 100%;
height: 100%;
background-color: #fff;
display: flex;
flex-direction: column;
background-color: var(--c-bg);
}
/* 预览内部平台容器需要可伸缩: */
.wechat-preview-container, .xiaohongshu-preview-container {
.wechat-preview-container:not([style*="display: none"]),
.xiaohongshu-preview-container:not([style*="display: none"]) {
flex: 1;
display: flex;
flex-direction: column;
display: flex !important;
min-height: 0; /* 允许内部滚动区域正确计算高度 */
}
.render-div {
flex: 1;
overflow-y: auto;
padding: 10px;
-webkit-user-select: text;
user-select: text;
min-height: 0;
}
/* 文章包裹:模拟公众号编辑器阅读宽度 */
.wechat-article-wrapper {
max-width: 720px;
width: 100%;
max-width: clamp(360px, 80vw, 760px);
margin: 0 auto;
padding: 12px 18px 80px 18px; /* 底部留白方便滚动到底部操作 */
box-sizing: border-box;
@@ -42,57 +102,32 @@
background: transparent;
}
.preview-toolbar {
position: relative;
min-height: 100px;
padding: 4px 0;
border-bottom: 1px solid #e8eaed;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
box-shadow: 0 2px 4px rgba(0,0,0,0.04);
}
.copy-button {
margin-right: 10px;
padding: 6px 14px;
background: linear-gradient(135deg, #1e88e5 0%, #1565c0 100%);
background: var(--grad-primary);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s ease;
box-shadow: 0 2px 6px rgba(30, 136, 229, 0.3);
box-shadow: var(--shadow-primary-2);
}
.copy-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(30, 136, 229, 0.4);
}
.copy-button:hover { box-shadow: var(--shadow-primary-4); }
.refresh-button {
margin-right: 10px;
padding: 6px 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: var(--grad-purple);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s ease;
box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
box-shadow: var(--shadow-purple-2);
}
.refresh-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(102, 126, 234, 0.4);
}
.refresh-button:hover { box-shadow: var(--shadow-purple-4); }
.upload-input {
margin-left: 10px;
padding: 6px 10px;
border: 1px solid #dadce0;
border: 1px solid var(--c-border);
border-radius: 6px;
font-size: 13px;
transition: all 0.2s ease;
@@ -102,9 +137,10 @@
cursor: pointer;
}
.upload-input:focus {
.upload-input:focus,
.style-select:focus {
outline: none;
border-color: #1e88e5;
border-color: var(--c-primary);
box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.1);
}
@@ -114,26 +150,24 @@
height: 16px;
margin: 0 6px 0 0;
cursor: pointer;
accent-color: #1e88e5;
accent-color: var(--c-primary);
}
/* Label 标签样式 */
label {
font-size: 13px;
color: #5f6368;
color: var(--c-text-muted);
cursor: pointer;
user-select: none;
transition: color 0.2s ease;
}
label:hover {
color: #1e88e5;
}
label:hover { color: var(--c-primary); }
.style-label {
margin-right: 10px;
font-size: 13px;
color: #5f6368;
color: var(--c-text-muted);
font-weight: 500;
white-space: nowrap;
}
@@ -142,25 +176,14 @@ label:hover {
margin-right: 10px;
width: 120px;
padding: 6px 10px;
border: 1px solid #dadce0;
border-radius: 6px;
background: white;
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.style-select:hover {
border-color: #1e88e5;
border-color: var(--c-primary);
box-shadow: 0 2px 6px rgba(30, 136, 229, 0.2);
}
.style-select:focus {
outline: none;
border-color: #1e88e5;
box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.1);
}
/* focus 规则见与 .upload-input:focus 的组合声明 */
.msg-view {
position: absolute;
@@ -186,22 +209,14 @@ label:hover {
.msg-ok-btn {
padding: 10px 24px;
margin: 0 8px;
background: linear-gradient(135deg, #1e88e5 0%, #1565c0 100%);
background: var(--grad-primary);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
box-shadow: 0 2px 6px rgba(30, 136, 229, 0.3);
box-shadow: var(--shadow-primary-2);
min-width: 80px;
}
.msg-ok-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(30, 136, 229, 0.4);
}
.msg-ok-btn:hover { box-shadow: var(--shadow-primary-4); }
.msg-ok-btn:active {
transform: translateY(0);
@@ -214,7 +229,9 @@ label:hover {
border-radius: 10px;
}
.note-mpcard-content {
display: flex;
display: grid;
grid-auto-flow: column;
align-items: center;
}
.note-mpcard-headimg {
border: none !important;
@@ -246,11 +263,10 @@ label:hover {
}
.loading-wrapper {
display: flex;
display: grid;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
place-items: center;
}
.loading-spinner {
@@ -275,24 +291,32 @@ label:hover {
/* Toolbar 行样式 */
/* =========================================================== */
.toolbar-line {
display: flex;
.platform-selector-line {
background: linear-gradient(135deg, #fff3e0 0%, #ffffff 100%) !important;
border-left: 4px solid var(--c-primary);
}
/* 平台选择容器:单层 Grid 排列 */
.platform-chooser-container.platform-chooser-grid {
display: grid;
grid-template-columns: auto minmax(160px, 1fr) auto auto;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: white;
background: white; /* 被 .platform-selector-line 的背景覆写 */
border-radius: 6px;
margin: 8px 10px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
box-shadow: var(--shadow-sm);
}
.toolbar-line.flex-wrap {
flex-wrap: wrap;
.platform-chooser-container .toolbar-button {
justify-self: start;
}
.platform-selector-line {
background: linear-gradient(135deg, #fff3e0 0%, #ffffff 100%) !important;
border-left: 4px solid #1e88e5;
@media (max-width: 720px) {
.platform-chooser-container.platform-chooser-grid {
grid-template-columns: repeat(auto-fit, minmax(140px, max-content));
}
}
/* =========================================================== */
@@ -301,24 +325,119 @@ label:hover {
.platform-select {
padding: 6px 12px;
border: 1px solid #dadce0;
border-radius: 6px;
background: white;
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
min-width: 150px;
font-weight: 500;
}
.platform-select:hover {
border-color: #1e88e5;
/* =========================================================== */
/* 微信公众号预览布局 */
/* =========================================================== */
.wechat-preview-container {
width: 100%;
height: 100%;
display: flex;
padding: 12px;
box-sizing: border-box;
}
.platform-select:focus {
outline: none;
border-color: #1e88e5;
.wechat-board {
flex: 1;
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
grid-template-rows: auto auto auto minmax(0, 1fr);
grid-template-areas:
"account-select account-select account-select account-back-export account-back-export account-back-export"
"cover-select cover-select cover-select cover-select cover-input cover-input"
"style-label style-select highlight-label highlight-select highlight-select highlight-select"
"content content content content content content";
gap: 5px;
background: var(--grad-toolbar);
border-radius: 12px;
padding: 5px;
box-shadow: var(--shadow-sm);
min-height: 0;
}
.wechat-cell {
background: white;
border-radius: 10px;
padding: 12px 16px;
box-shadow: var(--shadow-sm);
display: flex;
align-items: center;
gap: 12px;
}
.wechat-cell[data-area="account-select"] {
grid-area: account-select;
justify-content: flex-start;
}
.wechat-cell[data-area="account-back-export"] {
grid-area: account-back-export;
justify-content: flex-center;
}
.wechat-cell[data-area="account-select"] .style-label {
white-space: nowrap;
}
.wechat-cell[data-area="account-select"] .wechat-select {
width: 100%;
}
.wechat-cell[data-area="account-back-export"] .wechat-action-button { white-space: nowrap; }
.wechat-cell[data-area="cover-select"] {
grid-area: cover-select;
justify-content: flex-start;
gap: 18px;
}
.wechat-cell[data-area="cover-input"] { grid-area: cover-input; }
.wechat-cell[data-area="cover-select"] label {
white-space: nowrap;
}
.wechat-cell[data-area="cover-input"] .upload-input {
margin-left: 0;
width: 100%;
}
.wechat-cell[data-area="style-label"] { grid-area: style-label; white-space: nowrap; justify-content: flex-start; }
.wechat-cell[data-area="style-select"] { grid-area: style-select; width: 100%; }
.wechat-cell[data-area="highlight-label"] { grid-area: highlight-label; white-space: nowrap; justify-content: flex-start; }
.wechat-cell[data-area="highlight-select"] { grid-area: highlight-select; width: 100%; }
.wechat-cell[data-area="style-select"] .style-select,
.wechat-cell[data-area="style-select"] .wechat-style-select,
.wechat-cell[data-area="highlight-select"] .style-select,
.wechat-cell[data-area="highlight-select"] .wechat-style-select {
width: 100%;
min-width: 0;
}
.wechat-cell[data-area="content"] {
grid-area: content;
overflow-y: auto;
padding: 10px;
-webkit-user-select: text;
user-select: text;
min-height: 0;
background: white;
border-radius: 12px;
box-shadow: var(--shadow-sm);
display: block;
}
.wechat-cell-placeholder {
background: transparent;
box-shadow: none;
padding: 0;
}
.wechat-cell-placeholder .toolbar-button {
display: none;
}
/* =========================================================== */
@@ -327,23 +446,8 @@ label:hover {
.wechat-select {
padding: 6px 12px;
border: 1px solid #dadce0;
border-radius: 6px;
background: white;
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
min-width: 200px;
}
.wechat-select:hover {
border-color: #1e88e5;
}
.wechat-select:focus {
outline: none;
border-color: #1e88e5;
min-width: 100px;
width: 100%;
}
/* =========================================================== */
@@ -352,41 +456,20 @@ label:hover {
.toolbar-button {
padding: 6px 14px;
background: linear-gradient(135deg, #1e88e5 0%, #1565c0 100%);
background: var(--grad-primary);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s ease;
box-shadow: 0 2px 6px rgba(30, 136, 229, 0.3);
box-shadow: var(--shadow-primary-2);
}
.toolbar-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(30, 136, 229, 0.4);
}
.toolbar-button:hover { box-shadow: var(--shadow-primary-4); }
.toolbar-button.purple-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
background: var(--grad-purple);
box-shadow: var(--shadow-purple-2);
}
.toolbar-button.purple-gradient:hover {
box-shadow: 0 4px 8px rgba(102, 126, 234, 0.4);
}
/* =========================================================== */
/* 分隔线样式 */
/* =========================================================== */
.toolbar-separator {
width: 1px;
height: 24px;
background: #dadce0;
margin: 0 4px;
}
.toolbar-button.purple-gradient:hover { box-shadow: var(--shadow-purple-4); }
/* =========================================================== */
/* Doc Modal 样式 */
@@ -398,8 +481,10 @@ label:hover {
}
.doc-modal-content {
display: flex;
flex-direction: column;
display: grid;
grid-template-rows: auto auto 1fr;
row-gap: 8px;
min-height: 0;
}
.doc-modal-title {
@@ -413,7 +498,7 @@ label:hover {
}
.doc-modal-iframe {
flex: 1;
min-height: 0;
}
/* =========================================================== */
@@ -421,9 +506,10 @@ label:hover {
/* =========================================================== */
.setting-help-section {
display: flex;
flex-direction: row;
display: grid;
grid-auto-flow: column;
align-items: center;
column-gap: 10px;
}
.setting-help-title {
@@ -447,42 +533,138 @@ label:hover {
.xiaohongshu-preview-container {
width: 100%;
height: 100%;
display: flex;
padding: 12px;
box-sizing: border-box;
}
.xhs-preview-container {
.xhs-board {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
grid-template-rows: auto auto auto 1fr auto;
grid-template-areas:
"template template preview preview font font"
"content content content content content content"
"content content content content content content"
"content content content content content content"
"pagination pagination pagination slice slice slice";
gap: 5px;
width: 100%;
background: var(--grad-xhs-bg);
border-radius: 12px;
padding: 5px;
box-shadow: var(--shadow-sm);
min-height: 0;
}
.xhs-card {
display: flex;
align-items: center;
gap: 10px;
background: white;
border-radius: 10px;
padding: 10px 14px;
box-shadow: var(--shadow-sm);
}
.xhs-label {
font-size: 13px;
font-weight: 600;
color: var(--c-text-muted);
white-space: nowrap;
}
.xhs-select {
flex: 1 1 auto;
padding: 6px 10px;
border: 1px solid var(--c-border);
border-radius: 6px;
background: white;
font-size: 13px;
cursor: pointer;
max-width: 100px;
transition: border-color 0.2s ease;
}
.xhs-select:hover { border-color: var(--c-primary); }
.xhs-select:focus {
outline: none;
border-color: var(--c-primary);
box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.1);
}
.font-size-group {
display: grid;
grid-auto-flow: column;
align-items: center;
gap: 6px;
padding: 2px;
border: 1px solid var(--c-border);
border-radius: 6px;
background: #f7f9fb;
}
.font-size-btn {
width: 26px;
height: 26px;
border: none;
background: transparent;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
color: #5f6368;
transition: background 0.2s ease;
}
.font-size-btn:hover { background: #eaf1fe; }
.font-size-input {
width: 35px;
border: none;
background: transparent;
text-align: center;
font: inherit;
color: inherit;
}
.font-size-input:focus { outline: none; }
.xhs-area-template { grid-area: template; }
.xhs-area-preview { grid-area: preview; }
.xhs-area-font {
grid-area: font;
flex-wrap: nowrap;
}
.xhs-area-pagination { grid-area: pagination; justify-content: center; gap: 16px; }
.xhs-area-slice { grid-area: slice; justify-content: center; gap: 16px; }
.xhs-area-content {
grid-area: content;
background: white;
border-radius: 12px;
padding: 0;
box-shadow: var(--shadow-sm);
display: flex;
flex-direction: column;
height: 100%;
background: linear-gradient(135deg, #f5f7fa 0%, #e8eaf6 100%);
}
.xhs-page-container {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
align-items: center;
padding: 0px;
background: radial-gradient(ellipse at top, rgba(255,255,255,0.1) 0%, transparent 70%);
min-height: 0; /* 允许 flex 子项正确收缩和滚动 */
}
/* 小红书单页包裹器:为缩放后的页面预留正确的布局空间 */
.xhs-page-wrapper {
/* 显示尺寸缩放后540 × 720 */
width: 540px;
height: 720px;
margin: 0px auto;
border-radius: 12px;
min-height: 0;
position: relative;
overflow: visible;
}
/* 小红书单页样式:实际尺寸 1080×1440通过 scale 缩放到 540×720 */
.xhs-page-wrapper {
margin: 0 auto;
position: relative;
overflow: hidden;
}
.xhs-page {
/* 实际尺寸由 renderPage 设置1080×1440 */
transform-origin: top left;
transform: scale(0.5); /* 540/1080 = 0.5 */
background: white;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border-radius: 8px;
@@ -493,135 +675,71 @@ label:hover {
height: auto;
}
.xhs-top-toolbar {
display: flex;
.xhs-pagination {
display: grid;
grid-auto-flow: column;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border-bottom: 1px solid #e8eaed;
box-shadow: 0 2px 4px rgba(0,0,0,0.04);
flex-wrap: wrap;
}
.toolbar-label {
font-size: 11px;
color: #5f6368;
font-weight: 500;
white-space: nowrap;
}
.xhs-select {
padding: 4px 8px;
border: 1px solid #dadce0;
border-radius: 4px;
background: white;
font-size: 11px;
cursor: pointer;
transition: border-color 0.2s ease;
}
.xhs-select:hover {
border-color: #1e88e5;
}
.xhs-select:focus {
outline: none;
border-color: #1e88e5;
}
.font-size-group {
display: flex;
align-items: center;
gap: 6px;
background: white;
border: 1px solid #dadce0;
border-radius: 4px;
padding: 2px;
}
.font-size-btn {
width: 24px;
height: 24px;
border: none;
background: transparent;
border-radius: 3px;
cursor: pointer;
font-size: 16px;
color: #5f6368;
transition: background 0.2s ease;
}
.font-size-btn:hover {
background: #f1f3f4;
}
.font-size-display {
min-width: 24px;
text-align: center;
font-size: 12px;
color: #202124;
font-weight: 500;
}
.xhs-page-navigation {
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
padding: 12px;
background: white;
border-bottom: 1px solid #e8eaed;
}
.xhs-nav-btn {
width: 36px;
height: 36px;
border: 1px solid #dadce0;
border: 1px solid var(--c-border);
border-radius: 50%;
cursor: pointer;
font-size: 20px;
background: white;
color: #5f6368;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
box-shadow: var(--shadow-sm);
}
.xhs-nav-btn:hover {
background: linear-gradient(135deg, #1e88e5 0%, #1565c0 100%);
background: var(--grad-primary);
color: white;
border-color: #1e88e5;
border-color: var(--c-primary);
}
.xhs-page-number {
.xhs-page-indicator {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
min-width: 50px;
text-align: center;
color: #202124;
font-weight: 500;
color: #202124;
}
.xhs-bottom-toolbar {
display: flex;
justify-content: center;
gap: 12px;
padding: 12px 16px;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border-top: 1px solid #e8eaed;
box-shadow: 0 -2px 4px rgba(0,0,0,0.04);
.xhs-page-number-input {
width: 35px;
padding: 4px 6px;
text-align: center;
border: 1px solid var(--c-border);
border-radius: 6px;
background: white;
color: inherit;
font: inherit;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.08);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.xhs-page-number-input:focus {
outline: none;
border-color: var(--c-primary);
box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.15);
}
.xhs-page-number-total {
font-size: 14px;
color: #5f6368;
user-select: none;
}
.xhs-slice-btn {
padding: 8px 20px;
background: linear-gradient(135deg, #1e88e5 0%, #1565c0 100%);
background: var(--grad-primary);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s ease;
box-shadow: 0 2px 6px rgba(30, 136, 229, 0.3);
box-shadow: var(--shadow-primary-2);
}
.xhs-slice-btn:hover {
@@ -630,7 +748,7 @@ label:hover {
}
.xhs-slice-btn.secondary {
background: linear-gradient(135deg, #42a5f5 0%, #1e88e5 100%);
background: var(--grad-blue);
box-shadow: 0 2px 6px rgba(66, 165, 245, 0.3);
}
@@ -660,7 +778,8 @@ label:hover {
}
.xhs-code-container {
display: flex;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 10px;
margin-bottom: 20px;
@@ -704,7 +823,8 @@ label:hover {
}
.xhs-button-container {
display: flex;
display: grid;
grid-auto-flow: column;
justify-content: center;
gap: 15px;
margin-top: 20px;

829
styles.css.bk Normal file
View File

@@ -0,0 +1,829 @@
/* styles.css — 全局样式表,用于渲染及导出样式。 */
/* =========================================================== */
/* UI 样式 */
/* 共用样式与去重 */
/* =========================================================== */
/* 主题变量统一常用色值/阴影/渐变 */
:root {
--c-bg: #ffffff;
--c-border: #dadce0;
--c-text-muted: #5f6368;
--c-primary: #1e88e5;
--c-primary-dark: #1565c0;
--c-purple: #667eea;
--c-purple-dark: #764ba2;
--c-blue-2: #42a5f5;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
--shadow-overlay: 0 2px 4px rgba(0,0,0,0.04);
--shadow-primary-2: 0 2px 6px rgba(30, 136, 229, 0.3);
--shadow-primary-4: 0 4px 8px rgba(30, 136, 229, 0.4);
--shadow-purple-2: 0 2px 6px rgba(102, 126, 234, 0.3);
--shadow-purple-4: 0 4px 8px rgba(102, 126, 234, 0.4);
--grad-primary: linear-gradient(135deg, var(--c-primary) 0%, var(--c-primary-dark) 100%);
--grad-purple: linear-gradient(135deg, var(--c-purple) 0%, var(--c-purple-dark) 100%);
--grad-blue: linear-gradient(135deg, var(--c-blue-2) 0%, var(--c-primary) 100%);
--grad-toolbar: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
--grad-toolbar-bottom: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
--grad-xhs-bg: linear-gradient(135deg, #f5f7fa 0%, #e8eaf6 100%);
}
/* 通用按钮外观(不含背景与尺寸) */
.copy-button,
.refresh-button,
.toolbar-button,
.msg-ok-btn,
.xhs-slice-btn {
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
}
/* 通用按钮 hover 的位移效果(各自保留独立阴影) */
.copy-button:hover,
.refresh-button:hover,
.toolbar-button:hover,
.msg-ok-btn:hover {
transform: translateY(-1px);
}
/* 下拉选择的通用外观(各自保留尺寸差异) */
.platform-select,
.wechat-select,
.style-select {
border: 1px solid var(--c-border);
border-radius: 6px;
background: white;
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: var(--shadow-sm);
}
/* 平台与公众号选择的相同 hover/focus 效果style-select 单独增强) */
.platform-select:hover,
.wechat-select:hover { border-color: var(--c-primary); }
.platform-select:focus,
.wechat-select:focus { outline: none; border-color: var(--c-primary); }
.note-preview {
grid-template-rows: auto 1fr;
grid-template-columns: 1fr;
display: grid;
min-height: 100%;
width: 100%;
height: 100%;
background-color: var(--c-bg);
}
/* 预览内部平台容器需要可伸缩: */
.wechat-preview-container:not([style*="display: none"]),
.xiaohongshu-preview-container:not([style*="display: none"]) {
flex: 1;
display: flex !important;
min-height: 0; /* 允许内部滚动区域正确计算高度 */
}
/* 文章包裹:模拟公众号编辑器阅读宽度 */
.wechat-article-wrapper {
width: 100%;
max-width: clamp(360px, 80vw, 760px);
margin: 0 auto;
padding: 12px 18px 80px 18px; /* 底部留白方便滚动到底部操作 */
box-sizing: border-box;
}
/* 若内部 section.note-to-mp 主题没有撑开,确保文本可见基色 */
.wechat-article-wrapper .note-to-mp {
background: transparent;
}
.copy-button {
margin-right: 10px;
padding: 6px 14px;
background: var(--grad-primary);
color: white;
font-size: 13px;
box-shadow: var(--shadow-primary-2);
}
.copy-button:hover { box-shadow: var(--shadow-primary-4); }
.refresh-button {
margin-right: 10px;
padding: 6px 14px;
background: var(--grad-purple);
color: white;
font-size: 13px;
box-shadow: var(--shadow-purple-2);
}
.refresh-button:hover { box-shadow: var(--shadow-purple-4); }
.upload-input {
margin-left: 10px;
padding: 6px 10px;
border: 1px solid var(--c-border);
border-radius: 6px;
font-size: 13px;
transition: all 0.2s ease;
}
.upload-input[type="file"] {
cursor: pointer;
}
.upload-input:focus,
.style-select:focus {
outline: none;
border-color: var(--c-primary);
box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.1);
}
/* 单选按钮样式 */
.input-style[type="radio"] {
width: 16px;
height: 16px;
margin: 0 6px 0 0;
cursor: pointer;
accent-color: var(--c-primary);
}
/* Label 标签样式 */
label {
font-size: 13px;
color: var(--c-text-muted);
cursor: pointer;
user-select: none;
transition: color 0.2s ease;
}
label:hover { color: var(--c-primary); }
.style-label {
margin-right: 10px;
font-size: 13px;
color: var(--c-text-muted);
font-weight: 500;
white-space: nowrap;
}
.style-select {
margin-right: 10px;
width: 120px;
padding: 6px 10px;
}
.style-select:hover {
border-color: var(--c-primary);
box-shadow: 0 2px 6px rgba(30, 136, 229, 0.2);
}
/* focus 规则见与 .upload-input:focus 的组合声明 */
.msg-view {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--background-primary);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 18px;
z-index: 9999;
display: none;
}
.msg-title {
margin-bottom: 20px;
max-width: 90%;
}
.msg-ok-btn {
padding: 10px 24px;
margin: 0 8px;
background: var(--grad-primary);
color: white;
font-size: 14px;
box-shadow: var(--shadow-primary-2);
min-width: 80px;
}
.msg-ok-btn:hover { box-shadow: var(--shadow-primary-4); }
.msg-ok-btn:active {
transform: translateY(0);
}
.note-mpcard-wrapper {
margin: 20px 20px;
background-color: rgb(250, 250, 250);
padding: 10px 20px;
border-radius: 10px;
}
.note-mpcard-content {
display: grid;
grid-auto-flow: column;
align-items: center;
}
.note-mpcard-headimg {
border: none !important;
border-radius: 27px !important;
box-shadow: none !important;
width: 54px !important;
height: 54px !important;
margin: 0 !important;
}
.note-mpcard-info {
margin-left: 10px;
}
.note-mpcard-nickname {
font-size: 17px;
font-weight: 500;
color: rgba(0, 0, 0, 0.9);
}
.note-mpcard-signature {
font-size: 14px;
color: rgba(0, 0, 0, 0.55);
}
.note-mpcard-foot {
margin-top: 20px;
padding-top: 10px;
border-top: 1px solid #ececec;
font-size: 14px;
color: rgba(0, 0, 0, 0.3);
}
.loading-wrapper {
display: grid;
width: 100%;
height: 100%;
place-items: center;
}
.loading-spinner {
width: 50px; /* 可调整大小 */
height: 50px;
border: 4px solid #fcd6ff; /* 底色,浅灰 */
border-top: 4px solid #bb0cdf; /* 主色,蓝色顶部产生旋转感 */
border-radius: 50%; /* 圆形 */
animation: spin 1s linear infinite; /* 旋转动画 */
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* =========================================================== */
/* Toolbar 行样式 */
/* =========================================================== */
.platform-selector-line {
background: linear-gradient(135deg, #fff3e0 0%, #ffffff 100%) !important;
border-left: 4px solid var(--c-primary);
}
/* 平台选择容器:单层 Grid 排列 */
.platform-chooser-container.platform-chooser-grid {
display: grid;
grid-template-columns: auto minmax(160px, 1fr) auto auto;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: white; /* 被 .platform-selector-line 的背景覆写 */
border-radius: 6px;
margin: 8px 10px;
box-shadow: var(--shadow-sm);
}
.platform-chooser-container .toolbar-button {
justify-self: start;
}
@media (max-width: 720px) {
.platform-chooser-container.platform-chooser-grid {
grid-template-columns: repeat(auto-fit, minmax(140px, max-content));
}
}
/* =========================================================== */
/* 平台选择器样式 */
/* =========================================================== */
.platform-select {
padding: 6px 12px;
min-width: 150px;
font-weight: 500;
}
/* =========================================================== */
/* 微信公众号预览布局 */
/* =========================================================== */
.wechat-preview-container {
width: 100%;
height: 100%;
display: flex;
padding: 12px;
box-sizing: border-box;
}
.wechat-board {
flex: 1;
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
grid-template-rows: auto auto auto minmax(0, 1fr);
grid-template-areas:
"account-label account-select account-select account-select account-backend account-export"
"cover-label cover-default cover-upload cover-input cover-input cover-input"
"style-label style-select highlight-label highlight-select highlight-select highlight-select"
"content content content content content content";
gap: 12px;
background: var(--grad-toolbar);
border-radius: 12px;
padding: 16px;
box-shadow: var(--shadow-sm);
min-height: 0;
}
.wechat-cell {
background: white;
border-radius: 10px;
padding: 12px 16px;
box-shadow: var(--shadow-sm);
display: flex;
align-items: center;
gap: 12px;
}
.wechat-cell[data-area="account-label"] { grid-area: account-label; white-space: nowrap; justify-content: flex-start; }
.wechat-cell[data-area="account-select"] { grid-area: account-select; }
.wechat-cell[data-area="account-backend"] { grid-area: account-backend; justify-content: flex-end; }
.wechat-cell[data-area="account-export"] { grid-area: account-export; justify-content: flex-end; }
.wechat-cell[data-area="account-select"] .wechat-select {
width: 100%;
}
.wechat-cell[data-area="account-backend"] .wechat-action-button,
.wechat-cell[data-area="account-export"] .wechat-action-button {
white-space: nowrap;
}
.wechat-cell[data-area="cover-label"] { grid-area: cover-label; white-space: nowrap; justify-content: flex-start; }
.wechat-cell[data-area="cover-default"] { grid-area: cover-default; gap: 8px; }
.wechat-cell[data-area="cover-upload"] { grid-area: cover-upload; gap: 8px; }
.wechat-cell[data-area="cover-input"] { grid-area: cover-input; }
.wechat-cell[data-area="cover-default"] label,
.wechat-cell[data-area="cover-upload"] label {
white-space: nowrap;
}
.wechat-cell[data-area="cover-input"] .upload-input {
margin-left: 0;
width: 100%;
}
.wechat-cell[data-area="style-label"] { grid-area: style-label; white-space: nowrap; justify-content: flex-start; }
.wechat-cell[data-area="style-select"] { grid-area: style-select; width: 100%; }
.wechat-cell[data-area="highlight-label"] { grid-area: highlight-label; white-space: nowrap; justify-content: flex-start; }
.wechat-cell[data-area="highlight-select"] { grid-area: highlight-select; width: 100%; }
.wechat-cell[data-area="style-select"] .style-select,
.wechat-cell[data-area="style-select"] .wechat-style-select,
.wechat-cell[data-area="highlight-select"] .style-select,
.wechat-cell[data-area="highlight-select"] .wechat-style-select {
width: 100%;
min-width: 0;
}
.wechat-cell[data-area="content"] {
grid-area: content;
overflow-y: auto;
padding: 10px;
-webkit-user-select: text;
user-select: text;
min-height: 0;
background: white;
border-radius: 12px;
box-shadow: var(--shadow-sm);
display: block;
}
.wechat-cell-placeholder {
background: transparent;
box-shadow: none;
padding: 0;
}
.wechat-cell-placeholder .toolbar-button {
display: none;
}
/* =========================================================== */
/* 微信公众号选择器样式 */
/* =========================================================== */
.wechat-select {
padding: 6px 12px;
min-width: 100px;
width: 100%;
}
/* =========================================================== */
/* 按钮样式 */
/* =========================================================== */
.toolbar-button {
padding: 6px 14px;
background: var(--grad-primary);
color: white;
font-size: 13px;
box-shadow: var(--shadow-primary-2);
}
.toolbar-button:hover { box-shadow: var(--shadow-primary-4); }
.toolbar-button.purple-gradient {
background: var(--grad-purple);
box-shadow: var(--shadow-purple-2);
}
.toolbar-button.purple-gradient:hover { box-shadow: var(--shadow-purple-4); }
/* =========================================================== */
/* Doc Modal 样式 */
/* =========================================================== */
.doc-modal {
width: 640px;
height: 720px;
}
.doc-modal-content {
display: grid;
grid-template-rows: auto auto 1fr;
row-gap: 8px;
min-height: 0;
}
.doc-modal-title {
margin-top: 0.5em;
}
.doc-modal-desc {
margin-bottom: 1em;
-webkit-user-select: text;
user-select: text;
}
.doc-modal-iframe {
min-height: 0;
}
/* =========================================================== */
/* Setting Tab 帮助文档样式 */
/* =========================================================== */
.setting-help-section {
display: grid;
grid-auto-flow: column;
align-items: center;
column-gap: 10px;
}
.setting-help-title {
margin-right: 10px;
}
/* =========================================================== */
/* Xiaohongshu WebView 样式 */
/* =========================================================== */
.xhs-webview {
display: none;
width: 1200px;
height: 800px;
}
/* =========================================================== */
/* Xiaohongshu Preview View 样式 */
/* =========================================================== */
.xiaohongshu-preview-container {
width: 100%;
height: 100%;
display: flex;
padding: 12px;
box-sizing: border-box;
}
.xhs-board {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
grid-template-rows: auto auto auto 1fr auto;
grid-template-areas:
"template template preview preview font font"
"content content content content content content"
"content content content content content content"
"content content content content content content"
"pagination pagination pagination slice slice slice";
gap: 5px;
width: 100%;
background: var(--grad-xhs-bg);
border-radius: 12px;
padding: 5px;
box-shadow: var(--shadow-sm);
min-height: 0;
}
.xhs-card {
display: flex;
align-items: center;
gap: 10px;
background: white;
border-radius: 10px;
padding: 10px 14px;
box-shadow: var(--shadow-sm);
}
.xhs-label {
font-size: 13px;
font-weight: 600;
color: var(--c-text-muted);
white-space: nowrap;
}
.xhs-select {
flex: 1 1 auto;
padding: 6px 10px;
border: 1px solid var(--c-border);
border-radius: 6px;
background: white;
font-size: 13px;
cursor: pointer;
max-width: 100px;
transition: border-color 0.2s ease;
}
.xhs-select:hover { border-color: var(--c-primary); }
.xhs-select:focus {
outline: none;
border-color: var(--c-primary);
box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.1);
}
.font-size-group {
display: grid;
grid-auto-flow: column;
align-items: center;
gap: 6px;
padding: 2px;
border: 1px solid var(--c-border);
border-radius: 6px;
background: #f7f9fb;
}
.font-size-btn {
width: 26px;
height: 26px;
border: none;
background: transparent;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
color: #5f6368;
transition: background 0.2s ease;
}
.font-size-btn:hover { background: #eaf1fe; }
.font-size-input {
width: 35px;
border: none;
background: transparent;
text-align: center;
font: inherit;
color: inherit;
}
.font-size-input:focus { outline: none; }
.xhs-area-template { grid-area: template; }
.xhs-area-preview { grid-area: preview; }
.xhs-area-font {
grid-area: font;
flex-wrap: nowrap;
}
.xhs-area-pagination { grid-area: pagination; justify-content: center; gap: 16px; }
.xhs-area-slice { grid-area: slice; justify-content: center; gap: 16px; }
.xhs-area-content {
grid-area: content;
background: white;
border-radius: 12px;
padding: 0;
box-shadow: var(--shadow-sm);
display: flex;
}
.xhs-page-container {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
background: radial-gradient(ellipse at top, rgba(255,255,255,0.1) 0%, transparent 70%);
border-radius: 12px;
min-height: 0;
position: relative;
}
.xhs-page-wrapper {
margin: 0 auto;
position: relative;
overflow: hidden;
}
.xhs-page {
background: white;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border-radius: 8px;
}
.xhs-page img {
max-width: 100%;
height: auto;
}
.xhs-pagination {
display: grid;
grid-auto-flow: column;
align-items: center;
}
.xhs-nav-btn {
width: 36px;
height: 36px;
border: 1px solid var(--c-border);
border-radius: 50%;
cursor: pointer;
font-size: 20px;
background: white;
color: #5f6368;
transition: all 0.2s ease;
box-shadow: var(--shadow-sm);
}
.xhs-nav-btn:hover {
background: var(--grad-primary);
color: white;
border-color: var(--c-primary);
}
.xhs-page-indicator {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 500;
color: #202124;
}
.xhs-page-number-input {
width: 35px;
padding: 4px 6px;
text-align: center;
border: 1px solid var(--c-border);
border-radius: 6px;
background: white;
color: inherit;
font: inherit;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.08);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.xhs-page-number-input:focus {
outline: none;
border-color: var(--c-primary);
box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.15);
}
.xhs-page-number-total {
font-size: 14px;
color: #5f6368;
user-select: none;
}
.xhs-slice-btn {
padding: 8px 20px;
background: var(--grad-primary);
color: white;
font-size: 13px;
box-shadow: var(--shadow-primary-2);
}
.xhs-slice-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(30, 136, 229, 0.4);
}
.xhs-slice-btn.secondary {
background: var(--grad-blue);
box-shadow: 0 2px 6px rgba(66, 165, 245, 0.3);
}
.xhs-slice-btn.secondary:hover {
box-shadow: 0 4px 12px rgba(66, 165, 245, 0.4);
}
/* =========================================================== */
/* Xiaohongshu Login Modal 样式 */
/* =========================================================== */
.xiaohongshu-login-modal {
width: 400px;
padding: 20px;
}
.xhs-login-title {
text-align: center;
margin-bottom: 20px;
color: #ff4757;
}
.xhs-login-desc {
text-align: center;
color: #666;
margin-bottom: 30px;
}
.xhs-code-container {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 10px;
margin-bottom: 20px;
}
.xhs-code-label {
min-width: 80px;
}
.xhs-code-input-wrapper {
flex: 1;
}
.xhs-input-full {
width: 100%;
font-size: 16px;
}
.xhs-send-code-btn {
min-width: 120px;
margin-left: 10px;
}
.xhs-status-message {
min-height: 30px;
margin-bottom: 20px;
text-align: center;
font-size: 14px;
}
.xhs-status-message.success {
color: #27ae60;
}
.xhs-status-message.error {
color: #e74c3c;
}
.xhs-status-message.info {
color: #3498db;
}
.xhs-button-container {
display: grid;
grid-auto-flow: column;
justify-content: center;
gap: 15px;
margin-top: 20px;
}
.xhs-login-btn {
min-width: 100px;
}

170
t2.html Normal file
View File

@@ -0,0 +1,170 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>CSS Grid 5列布局示例</title>
<style>
:root{
--gap: 12px;
--radius: 12px;
--bg: gray;
--panel: #111827;
--muted: #1f2937;
--text: #e5e7eb;
--sub: #9ca3af;
--accent: #3b82f6;
}
*{box-sizing: border-box}
body{
margin:0;
min-height:100vh;
background: var(--bg);
color: var(--text);
font: 15px/1.6 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
display: flex;
flex-direction: column;
}
.app{
flex: 1;
display: flex;
flex-direction: column;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.board{
flex: 1;
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-rows: auto auto 1fr auto;
grid-template-areas:
"label1_list1 label1_list1 label1_list1 btnA btnB"
"label2_list2 label2_list2 label3_list3 label3_list3 label4"
"content content content content content"
"component component component btnC btnD";
gap: var(--gap);
background: var(--panel);
border-radius: 12px;
padding: 16px;
min-height: 0;
}
.card{
background: var(--muted);
border-radius: var(--radius);
padding: 10px;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
min-height: 40px;
}
.label{
color: var(--sub);
font-weight: 600;
white-space: nowrap;
flex-shrink: 0;
}
.select{
flex: 1 1 auto;
background: #0b1220;
border: 1px solid #0f1a30;
border-radius: 8px;
padding: 6px 8px;
color: var(--text);
width: 100%;
}
.btn{
background: var(--accent);
color: white;
border: 0;
border-radius: 8px;
padding: 10px 20px;
font-weight: 600;
cursor: pointer;
text-align: center;
transition: all 0.2s ease;
}
.btn:hover{
background: #2563eb; /* 深蓝色 hover */
box-shadow: 0 4px 8px rgba(0,0,0,0.25);
}
.btn:active{
transform: translateY(1px);
}
/* 让按钮所在卡片居中对齐 */
.btnA, .btnB, .btnC, .btnD{
justify-content: center !important;
}
.content{
grid-area: content;
background: #0b1020;
border: 1px dashed #223253;
border-radius: 12px;
padding: 18px;
min-height: 0;
}
.label1_list1{ grid-area: label1_list1; }
.label2_list2{ grid-area: label2_list2; }
.label3_list3{ grid-area: label3_list3; }
.btnA { grid-area: btnA; }
.btnB { grid-area: btnB; }
.label4{ grid-area: label4; }
.component{ grid-area: component; }
.btnC { grid-area: btnC; }
.btnD { grid-area: btnD; }
</style>
</head>
<body>
<div class="app">
<h1>Grid 5列布局 Demo</h1>
<section class="board">
<div class="card label1_list1">
<span class="label">发布平台</span>
<select class="select">
<option>公众号</option>
<option>小红书</option>
</select>
</div>
<div class="card btnA"><button class="btn">刷新</button></div>
<div class="card btnB"><button class="btn">发布</button></div>
<div class="card label2_list2">
<span class="label">模版</span>
<select class="select">
<option>模版 1</option>
<option>模版 2</option>
</select>
</div>
<div class="card label3_list3">
<span class="label">预览宽度</span>
<select class="select">
<option>1080px</option>
<option>720px</option>
<option>540px</option>
<option>360px</option>
</select>
</div>
<div class="card label4">调节文字大小组件</div>
<div class="content">小红书预览区域</div>
<div class="card component">页面选择组件</div>
<div class="card btnC"><button class="btn">按钮</button></div>
<div class="card btnD"><button class="btn">按钮</button></div>
</section>
</div>
</body>
</html>

View File

@@ -1,4 +1,4 @@
# todo list
# todo list & recording
## 功能
1. 实现markdown预览页面切图功能预览页面是以完成渲染的页面生成一整张长图。再按文章顺序裁剪为图片(png格式)。(v1.3.1)
@@ -59,11 +59,15 @@
## 问题
1. "发布平台"首次选“小红书”时,预览页面没有加载当前文章。
2. 顶部按钮适应窗口宽度,超出窗口,折行显示。
3. 小红书模式html分页预览不是从顶部开始显示显示不完整。
小红书模式,预览窗口似乎只显示了一部分?上面部分被挡住了吗?
参考微信公众号模式下的预览窗口,不同点在于小红书模式下,每页的宽高比按配置要求
3. 小红书模式问题:
- html分页预览不是从顶部开始显示显示不完整
- 预览窗口似乎只显示了一部分?上面部分被挡住了吗?
- 参考微信公众号模式下的预览窗口,不同点在于小红书模式下,每页的宽高比按配置要求。
4. 修改:
- 公共部分独立出来如“发布平台”放在新建platform-choose.ts中“发布平台”选择切换平台逻辑放在该模块中便于以后其他平台扩展。
@@ -95,15 +99,31 @@ SOLVEobsidian控制台打印信息定位在哪里阻塞AI修复。
7. 小红书模式,页面渲染使用选择的主题。参考微信公众号模式进行渲染。
8. 需求:主题、字体、字大小变化时,需要重新分页
8. 需求:小红书模式下html渲染后按照previewWidth × previewHeight 的预览窗口尺寸切割,内容不能丢失
主题、字体、字大小变化时,需要进行重新分页,保持内容不丢失。
去掉主题设置,使用全局主题设置。(❗️先简化,后续小红书和微信公众号应主题应该需要独立开。)
去掉字体设置,使用主题字体。
字体大小支持直接编辑。
问题:
- 字变大时,一页的内容放不下,重新分页应该会增加页数。但现在重新分页当前页放不下的内容只是被剪掉了。
- 表格显示不完整。
9. styles.css中有很多冗余。
9. styles.css中有很多冗余。改为grid布局。部分完成这部分需要后面**手动调整**重构。🧶 ♻️ ❇️
问题:小红书预览布局有问题❓
小红书布局改为grid但平台选择器部分没有完成修改。
保持微信和小红书页面一致性都用grid重构复用css样式代码。
10. 新建docs文件夹把除了README和todolist以外的markdown文件放到docs中。
## 经验
1. 在不确定AI是否理解或者需求是否准确的情况下先用codex chat模式提问看回答确定AI理解是否准确。
尤其对于较大规模的重构需求,这点很重要 ‼️ 。
2. 复杂页面codex生成的css可能无比复杂不便于维护修改。
自己写布局demo原型让codex参考布局修改(原来元素美化的css可保留)。
demo原型可以手绘后拍照让chatgpt生成在此基础上自己修改。

View File

@@ -0,0 +1,48 @@
import { promises as fs } from "fs";
// Esbuild plugin that optionally obfuscates the emitted bundle with javascript-obfuscator.
export default function javascriptObfuscatorPlugin(obfuscatorOptions = {}) {
let obfuscatorPromise;
const loadObfuscator = async () => {
if (!obfuscatorPromise) {
obfuscatorPromise = import("javascript-obfuscator")
.then((module) => module.default ?? module)
.catch((error) => {
console.warn("[esbuild] javascript-obfuscator unavailable, skipping obfuscation.", error);
return null;
});
}
return obfuscatorPromise;
};
// Default to preserving line breaks unless explicitly overridden.
if (typeof obfuscatorOptions.compact === "undefined") {
obfuscatorOptions.compact = false;
}
return {
name: "javascript-obfuscator",
setup(build) {
build.onEnd(async (result) => {
if (result.errors.length) return;
const outfile = build.initialOptions.outfile;
if (!outfile) return;
try {
const obfuscator = await loadObfuscator();
if (!obfuscator?.obfuscate) return;
const source = await fs.readFile(outfile, "utf8");
const obfuscatedCode = obfuscator
.obfuscate(source, obfuscatorOptions)
.getObfuscatedCode();
await fs.writeFile(outfile, obfuscatedCode, "utf8");
} catch (error) {
console.warn("[esbuild] javascript-obfuscator plugin failed, continuing without obfuscation.", error);
}
});
},
};
}

View File

@@ -1,3 +1,3 @@
{
"1.3.0": "1.4.5"
"1.3.7": "1.4.5"
}

729
x Normal file
View File

@@ -0,0 +1,729 @@
/* styles.css — 全局样式表,用于渲染及导出样式。 */
/* =========================================================== */
/* UI 样式 */
/* 共用样式与去重 */
/* =========================================================== */
/* 主题变量统一常用色值/阴影/渐变 */
:root {
--c-bg: #ffffff;
--c-border: #dadce0;
--c-text-muted: #5f6368;
--c-primary: #1e88e5;
--c-primary-dark: #1565c0;
--c-purple: #667eea;
--c-purple-dark: #764ba2;
--c-blue-2: #42a5f5;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
--shadow-overlay: 0 2px 4px rgba(0,0,0,0.04);
--shadow-primary-2: 0 2px 6px rgba(30, 136, 229, 0.3);
--shadow-primary-4: 0 4px 8px rgba(30, 136, 229, 0.4);
--shadow-purple-2: 0 2px 6px rgba(102, 126, 234, 0.3);
--shadow-purple-4: 0 4px 8px rgba(102, 126, 234, 0.4);
--grad-primary: linear-gradient(135deg, var(--c-primary) 0%, var(--c-primary-dark) 100%);
--grad-purple: linear-gradient(135deg, var(--c-purple) 0%, var(--c-purple-dark) 100%);
--grad-blue: linear-gradient(135deg, var(--c-blue-2) 0%, var(--c-primary) 100%);
--grad-toolbar: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
--grad-toolbar-bottom: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
--grad-xhs-bg: linear-gradient(135deg, #f5f7fa 0%, #e8eaf6 100%);
}
/* 通用按钮外观(不含背景与尺寸) */
.copy-button,
.refresh-button,
.toolbar-button,
.msg-ok-btn,
.xhs-slice-btn {
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
}
/* 通用按钮 hover 的位移效果(各自保留独立阴影) */
.copy-button:hover,
.refresh-button:hover,
.toolbar-button:hover,
.msg-ok-btn:hover {
transform: translateY(-1px);
}
/* 下拉选择的通用外观(各自保留尺寸差异) */
.platform-select,
.wechat-select,
.style-select {
border: 1px solid var(--c-border);
border-radius: 6px;
background: white;
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: var(--shadow-sm);
}
/* 平台与公众号选择的相同 hover/focus 效果style-select 单独增强) */
.platform-select:hover,
.wechat-select:hover { border-color: var(--c-primary); }
.platform-select:focus,
.wechat-select:focus { outline: none; border-color: var(--c-primary); }
.note-preview {
grid-template-rows: auto 1fr;
grid-template-columns: 1fr;
display: grid;
min-height: 100%;
width: 100%;
height: 100%;
background-color: var(--c-bg);
}
/* 预览内部平台容器需要可伸缩: */
.wechat-preview-container:not([style*="display: none"]),
.xiaohongshu-preview-container:not([style*="display: none"]) {
flex: 1;
display: grid !important;
grid-template-rows: auto 1fr;
min-height: 0; /* 允许内部滚动区域正确计算高度 */
}
.render-div {
overflow-y: auto;
padding: 10px;
-webkit-user-select: text;
user-select: text;
min-height: 0;
}
/* 文章包裹:模拟公众号编辑器阅读宽度 */
.wechat-article-wrapper {
max-width: 720px;
margin: 0 auto;
padding: 12px 18px 80px 18px; /* 底部留白方便滚动到底部操作 */
box-sizing: border-box;
}
/* 若内部 section.note-to-mp 主题没有撑开,确保文本可见基色 */
.wechat-article-wrapper .note-to-mp {
background: transparent;
}
.preview-toolbar {
display: grid;
position: relative;
grid-template-columns: repeat(auto-fit, minmax(160px, max-content));
gap: 12px;
padding: 8px 12px;
align-items: center;
min-height: auto;
border-bottom: 1px solid #e8eaed;
background: var(--grad-toolbar);
box-shadow: var(--shadow-overlay);
}
.copy-button {
margin-right: 10px;
padding: 6px 14px;
background: var(--grad-primary);
color: white;
font-size: 13px;
box-shadow: var(--shadow-primary-2);
}
.copy-button:hover { box-shadow: var(--shadow-primary-4); }
.refresh-button {
margin-right: 10px;
padding: 6px 14px;
background: var(--grad-purple);
color: white;
font-size: 13px;
box-shadow: var(--shadow-purple-2);
}
.refresh-button:hover { box-shadow: var(--shadow-purple-4); }
.upload-input {
margin-left: 10px;
padding: 6px 10px;
border: 1px solid var(--c-border);
border-radius: 6px;
font-size: 13px;
transition: all 0.2s ease;
}
.upload-input[type="file"] {
cursor: pointer;
}
.upload-input:focus,
.style-select:focus {
outline: none;
border-color: var(--c-primary);
box-shadow: 0 0 0 3px rgba(30, 136, 229, 0.1);
}
/* 单选按钮样式 */
.input-style[type="radio"] {
width: 16px;
height: 16px;
margin: 0 6px 0 0;
cursor: pointer;
accent-color: var(--c-primary);
}
/* Label 标签样式 */
label {
font-size: 13px;
color: var(--c-text-muted);
cursor: pointer;
user-select: none;
transition: color 0.2s ease;
}
label:hover { color: var(--c-primary); }
.style-label {
margin-right: 10px;
font-size: 13px;
color: var(--c-text-muted);
font-weight: 500;
white-space: nowrap;
}
.style-select {
margin-right: 10px;
width: 120px;
padding: 6px 10px;
}
.style-select:hover {
border-color: var(--c-primary);
box-shadow: 0 2px 6px rgba(30, 136, 229, 0.2);
}
/* focus 规则见与 .upload-input:focus 的组合声明 */
.msg-view {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--background-primary);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 18px;
z-index: 9999;
display: none;
}
.msg-title {
margin-bottom: 20px;
max-width: 90%;
}
.msg-ok-btn {
padding: 10px 24px;
margin: 0 8px;
background: var(--grad-primary);
color: white;
font-size: 14px;
box-shadow: var(--shadow-primary-2);
min-width: 80px;
}
.msg-ok-btn:hover { box-shadow: var(--shadow-primary-4); }
.msg-ok-btn:active {
transform: translateY(0);
}
.note-mpcard-wrapper {
margin: 20px 20px;
background-color: rgb(250, 250, 250);
padding: 10px 20px;
border-radius: 10px;
}
.note-mpcard-content {
display: grid;
grid-auto-flow: column;
align-items: center;
}
.note-mpcard-headimg {
border: none !important;
border-radius: 27px !important;
box-shadow: none !important;
width: 54px !important;
height: 54px !important;
margin: 0 !important;
}
.note-mpcard-info {
margin-left: 10px;
}
.note-mpcard-nickname {
font-size: 17px;
font-weight: 500;
color: rgba(0, 0, 0, 0.9);
}
.note-mpcard-signature {
font-size: 14px;
color: rgba(0, 0, 0, 0.55);
}
.note-mpcard-foot {
margin-top: 20px;
padding-top: 10px;
border-top: 1px solid #ececec;
font-size: 14px;
color: rgba(0, 0, 0, 0.3);
}
.loading-wrapper {
display: grid;
width: 100%;
height: 100%;
place-items: center;
}
.loading-spinner {
width: 50px; /* 可调整大小 */
height: 50px;
border: 4px solid #fcd6ff; /* 底色,浅灰 */
border-top: 4px solid #bb0cdf; /* 主色,蓝色顶部产生旋转感 */
border-radius: 50%; /* 圆形 */
animation: spin 1s linear infinite; /* 旋转动画 */
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* =========================================================== */
/* Toolbar 行样式 */
/* =========================================================== */
.toolbar-line {
display: grid;
grid-auto-flow: column;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: white;
border-radius: 6px;
margin: 8px 10px;
box-shadow: var(--shadow-sm);
}
.toolbar-line.flex-wrap {
grid-auto-flow: row;
grid-template-columns: repeat(auto-fit, minmax(160px, max-content));
}
.platform-selector-line {
background: linear-gradient(135deg, #fff3e0 0%, #ffffff 100%) !important;
border-left: 4px solid var(--c-primary);
}
/* 平台选择容器:单层 Grid 排列 */
.platform-chooser-container.platform-chooser-grid {
display: grid;
grid-auto-flow: column;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: white; /* 被 .platform-selector-line 的背景覆写 */
border-radius: 6px;
margin: 8px 10px;
box-shadow: var(--shadow-sm);
}
/* =========================================================== */
/* 平台选择器样式 */
/* =========================================================== */
.platform-select {
padding: 6px 12px;
min-width: 150px;
font-weight: 500;
}
/* =========================================================== */
/* 微信公众号选择器样式 */
/* =========================================================== */
.wechat-select {
padding: 6px 12px;
min-width: 200px;
}
/* =========================================================== */
/* 按钮样式 */
/* =========================================================== */
.toolbar-button {
padding: 6px 14px;
background: var(--grad-primary);
color: white;
font-size: 13px;
box-shadow: var(--shadow-primary-2);
}
.toolbar-button:hover { box-shadow: var(--shadow-primary-4); }
.toolbar-button.purple-gradient {
background: var(--grad-purple);
box-shadow: var(--shadow-purple-2);
}
.toolbar-button.purple-gradient:hover { box-shadow: var(--shadow-purple-4); }
/* =========================================================== */
/* 分隔线样式 */
/* =========================================================== */
.toolbar-separator {
width: 1px;
height: 24px;
background: var(--c-border);
margin: 0 4px;
}
/* =========================================================== */
/* Doc Modal 样式 */
/* =========================================================== */
.doc-modal {
width: 640px;
height: 720px;
}
.doc-modal-content {
display: grid;
grid-template-rows: auto auto 1fr;
row-gap: 8px;
min-height: 0;
}
.doc-modal-title {
margin-top: 0.5em;
}
.doc-modal-desc {
margin-bottom: 1em;
-webkit-user-select: text;
user-select: text;
}
.doc-modal-iframe {
min-height: 0;
}
/* =========================================================== */
/* Setting Tab 帮助文档样式 */
/* =========================================================== */
.setting-help-section {
display: grid;
grid-auto-flow: column;
align-items: center;
column-gap: 10px;
}
.setting-help-title {
margin-right: 10px;
}
/* =========================================================== */
/* Xiaohongshu WebView 样式 */
/* =========================================================== */
.xhs-webview {
display: none;
width: 1200px;
height: 800px;
}
/* =========================================================== */
/* Xiaohongshu Preview View 样式 */
/* =========================================================== */
.xiaohongshu-preview-container {
width: 100%;
height: 100%;
}
.xhs-preview-container:not([style*="display: none"]) {
display: grid !important;
grid-template-rows: auto 1fr auto;
height: 100%;
background: var(--grad-xhs-bg);
min-height: 0;
}
.xhs-page-container {
overflow-y: auto;
overflow-x: hidden;
display: grid;
align-content: start;
justify-content: center;
padding: 0px;
background: radial-gradient(ellipse at top, rgba(255,255,255,0.1) 0%, transparent 70%);
min-height: 0; /* 允许子项正确收缩和滚动 */
}
/* 小红书单页包裹器:为缩放后的页面预留正确的布局空间 */
.xhs-page-wrapper {
/* 显示尺寸缩放后540 × 720 */
width: 540px;
height: 720px;
margin: 0px auto;
position: relative;
overflow: visible;
}
/* 小红书单页样式:实际尺寸 1080×1440通过 scale 缩放到 540×720 */
.xhs-page {
/* 实际尺寸由 renderPage 设置1080×1440 */
transform-origin: top left;
transform: scale(0.5); /* 540/1080 = 0.5 */
background: white;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border-radius: 8px;
}
.xhs-page img {
max-width: 100%;
height: auto;
}
.xhs-top-toolbar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, max-content));
align-items: center;
gap: 12px;
padding: 8px 12px;
background: var(--grad-toolbar);
border-bottom: 1px solid #e8eaed;
box-shadow: var(--shadow-overlay);
}
.toolbar-label {
font-size: 11px;
color: var(--c-text-muted);
font-weight: 500;
white-space: nowrap;
}
.xhs-select {
padding: 4px 8px;
border: 1px solid var(--c-border);
border-radius: 4px;
background: white;
font-size: 11px;
cursor: pointer;
transition: border-color 0.2s ease;
}
.xhs-select:hover {
border-color: var(--c-primary);
}
.xhs-select:focus {
outline: none;
border-color: var(--c-primary);
}
.font-size-group {
display: grid;
grid-auto-flow: column;
align-items: center;
gap: 6px;
background: white;
border: 1px solid var(--c-border);
border-radius: 4px;
padding: 2px;
}
.font-size-btn {
width: 24px;
height: 24px;
border: none;
background: transparent;
border-radius: 3px;
cursor: pointer;
font-size: 16px;
color: #5f6368;
transition: background 0.2s ease;
}
.font-size-btn:hover {
background: #f1f3f4;
}
.font-size-display {
min-width: 24px;
text-align: center;
font-size: 12px;
color: #202124;
font-weight: 500;
}
.xhs-page-navigation {
display: grid;
grid-auto-flow: column;
justify-content: center;
align-items: center;
gap: 16px;
padding: 12px;
background: white;
border-bottom: 1px solid #e8eaed;
}
.xhs-nav-btn {
width: 36px;
height: 36px;
border: 1px solid var(--c-border);
border-radius: 50%;
cursor: pointer;
font-size: 20px;
background: white;
color: #5f6368;
transition: all 0.2s ease;
box-shadow: var(--shadow-sm);
}
.xhs-nav-btn:hover {
background: var(--grad-primary);
color: white;
border-color: var(--c-primary);
}
.xhs-page-number {
font-size: 14px;
min-width: 50px;
text-align: center;
color: #202124;
font-weight: 500;
}
.xhs-bottom-toolbar {
display: grid;
grid-auto-flow: column;
justify-content: center;
gap: 12px;
padding: 12px 16px;
background: var(--grad-toolbar-bottom);
border-top: 1px solid #e8eaed;
box-shadow: 0 -2px 4px rgba(0,0,0,0.04);
}
.xhs-slice-btn {
padding: 8px 20px;
background: var(--grad-primary);
color: white;
font-size: 13px;
box-shadow: var(--shadow-primary-2);
}
.xhs-slice-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(30, 136, 229, 0.4);
}
.xhs-slice-btn.secondary {
background: var(--grad-blue);
box-shadow: 0 2px 6px rgba(66, 165, 245, 0.3);
}
.xhs-slice-btn.secondary:hover {
box-shadow: 0 4px 12px rgba(66, 165, 245, 0.4);
}
/* =========================================================== */
/* Xiaohongshu Login Modal 样式 */
/* =========================================================== */
.xiaohongshu-login-modal {
width: 400px;
padding: 20px;
}
.xhs-login-title {
text-align: center;
margin-bottom: 20px;
color: #ff4757;
}
.xhs-login-desc {
text-align: center;
color: #666;
margin-bottom: 30px;
}
.xhs-code-container {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 10px;
margin-bottom: 20px;
}
.xhs-code-label {
min-width: 80px;
}
.xhs-code-input-wrapper {
flex: 1;
}
.xhs-input-full {
width: 100%;
font-size: 16px;
}
.xhs-send-code-btn {
min-width: 120px;
margin-left: 10px;
}
.xhs-status-message {
min-height: 30px;
margin-bottom: 20px;
text-align: center;
font-size: 14px;
}
.xhs-status-message.success {
color: #27ae60;
}
.xhs-status-message.error {
color: #e74c3c;
}
.xhs-status-message.info {
color: #3498db;
}
.xhs-button-container {
display: grid;
grid-auto-flow: column;
justify-content: center;
gap: 15px;
margin-top: 20px;
}
.xhs-login-btn {
min-width: 100px;
}