6 Commits

Author SHA1 Message Date
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
57 changed files with 1306 additions and 289 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

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,15 @@
set -e # 出错立即退出
# 1. 构建
npm run build
echo "🏗️ 开始构建..."
if npm run build; then
echo "✅ 构建成功"
echo
else
echo "❌ 构建失败,脚本终止"
echo
exit 1
fi
# 2. 目标目录
PLUGIN_DIR=~/myweb/.obsidian/plugins/note-to-mp
@@ -16,12 +24,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 +39,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 风格。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 145 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: 37 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
}

62
release.md Normal file
View File

@@ -0,0 +1,62 @@
# 版本信息(发布版本时填写,供脚本使用)
## 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)。
现在分页依据真实渲染高度,预览窗口内不会再丢失底部内容。建议在小红书预览里多翻几页、调整字号后重新分页验证结果。

View File

@@ -53,7 +53,7 @@ export class PlatformChooser {
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 +72,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;
// 添加平台选项

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] 界面构建完成');
}

View File

@@ -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,
}
}
@@ -221,4 +228,4 @@ export class NMPSettings {
if (this.expireat == null) return false;
return this.expireat > new Date();
}
}
}

View File

@@ -93,25 +93,23 @@ export class WechatPreview {
* 构建工具栏
*/
private buildToolbar(parent: HTMLDivElement): void {
let lineDiv;
// 单层 Grid所有控件直接挂到 parent.preview-toolbar
// 公众号选择
if (this.settings.wxInfo.length > 1 || Platform.isDesktop) {
lineDiv = parent.createDiv({ cls: 'toolbar-line' });
const wxLabel = lineDiv.createDiv({ cls: 'style-label' });
const wxLabel = parent.createDiv({ cls: 'style-label' });
wxLabel.innerText = '公众号';
const wxSelect = lineDiv.createEl('select', { cls: 'wechat-select' });
const wxSelect = parent.createEl('select', { cls: 'wechat-select' });
wxSelect.onchange = async () => {
this.currentAppId = wxSelect.value;
this.onAppIdChanged();
};
const defaultOp = wxSelect.createEl('option');
defaultOp.value = '';
defaultOp.text = '请在设置里配置公众号';
for (let i = 0; i < this.settings.wxInfo.length; i++) {
const op = wxSelect.createEl('option');
const wx = this.settings.wxInfo[i];
@@ -125,8 +123,8 @@ export class WechatPreview {
this.wechatSelect = wxSelect;
if (Platform.isDesktop) {
const separator = lineDiv.createDiv({ cls: 'toolbar-separator' });
const openBtn = lineDiv.createEl('button', { text: '🌐 去公众号后台', cls: 'toolbar-button purple-gradient' });
//parent.createDiv({ cls: 'toolbar-separator' });
const openBtn = parent.createEl('button', { text: '🌐 去公众号后台', cls: 'toolbar-button purple-gradient' });
openBtn.onclick = async () => {
const { shell } = require('electron');
shell.openExternal('https://mp.weixin.qq.com');
@@ -135,21 +133,15 @@ export class WechatPreview {
}
}
// 操作按钮
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();
}
};
// 操作按钮(直接平铺在 Grid 中)
const refreshBtn = parent.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' });
const postBtn = parent.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' });
const htmlBtn = parent.createEl('button', { text: '💾 导出HTML', cls: 'toolbar-button' });
htmlBtn.onclick = async () => await this.exportHTML();
}
@@ -166,11 +158,10 @@ export class WechatPreview {
* 构建封面选择器
*/
private buildCoverSelector(parent: HTMLDivElement): void {
const lineDiv = parent.createDiv({ cls: 'toolbar-line' });
const coverTitle = lineDiv.createDiv({ cls: 'style-label' });
const coverTitle = parent.createDiv({ cls: 'style-label' });
coverTitle.innerText = '封面';
this.useDefaultCover = lineDiv.createEl('input', { cls: 'input-style' });
this.useDefaultCover = parent.createEl('input', { cls: 'input-style' });
this.useDefaultCover.setAttr('type', 'radio');
this.useDefaultCover.setAttr('name', 'cover');
this.useDefaultCover.setAttr('value', 'default');
@@ -182,11 +173,11 @@ export class WechatPreview {
}
};
const defaultLabel = lineDiv.createEl('label');
const defaultLabel = parent.createEl('label');
defaultLabel.innerText = '默认';
defaultLabel.setAttr('for', 'default-cover');
this.useLocalCover = lineDiv.createEl('input', { cls: 'input-style' });
this.useLocalCover = parent.createEl('input', { cls: 'input-style' });
this.useLocalCover.setAttr('type', 'radio');
this.useLocalCover.setAttr('name', 'cover');
this.useLocalCover.setAttr('value', 'local');
@@ -198,11 +189,11 @@ export class WechatPreview {
}
};
const localLabel = lineDiv.createEl('label');
const localLabel = parent.createEl('label');
localLabel.setAttr('for', 'local-cover');
localLabel.innerText = '上传';
this.coverEl = lineDiv.createEl('input', { cls: 'upload-input' });
this.coverEl = parent.createEl('input', { cls: 'upload-input' });
this.coverEl.setAttr('type', 'file');
this.coverEl.setAttr('placeholder', '封面图片');
this.coverEl.setAttr('accept', '.png, .jpg, .jpeg');
@@ -214,12 +205,10 @@ export class WechatPreview {
* 构建样式选择器
*/
private buildStyleSelector(parent: HTMLDivElement): void {
const lineDiv = parent.createDiv({ cls: 'toolbar-line flex-wrap' });
const cssStyle = lineDiv.createDiv({ cls: 'style-label' });
const cssStyle = parent.createDiv({ cls: 'style-label' });
cssStyle.innerText = '样式';
const selectBtn = lineDiv.createEl('select', { cls: 'style-select' });
const selectBtn = parent.createEl('select', { cls: 'style-select' });
selectBtn.onchange = async () => {
this.currentTheme = selectBtn.value;
this.render.updateStyle(selectBtn.value);
@@ -233,12 +222,12 @@ export class WechatPreview {
}
this.themeSelect = selectBtn;
const separator = lineDiv.createDiv({ cls: 'toolbar-separator' });
parent.createDiv({ cls: 'toolbar-separator' });
const highlightStyle = lineDiv.createDiv({ cls: 'style-label' });
const highlightStyle = parent.createDiv({ cls: 'style-label' });
highlightStyle.innerText = '代码高亮';
const highlightStyleBtn = lineDiv.createEl('select', { cls: 'style-select' });
const highlightStyleBtn = parent.createEl('select', { cls: 'style-select' });
highlightStyleBtn.onchange = async () => {
this.currentHighlight = highlightStyleBtn.value;
this.render.updateHighLight(highlightStyleBtn.value);

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;
}
@@ -56,17 +57,38 @@ export async function paginateArticle(
): Promise<PageInfo[]> {
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);
// 等待浏览器完成渲染
await new Promise(resolve => setTimeout(resolve, 10));
const childHeight = childClone.offsetHeight;
const isIndivisible = isIndivisibleElement(child);
measureContent.appendChild(childClone);
// 判断是否需要换页
if (currentPageHeight + childHeight > pageHeight && currentPageContent.length > 0) {
// 如果是不可分割元素且加入后会超出,先保存当前页
if (isIndivisible) {
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 {
// 加入当前页
await waitForLayout();
const totalHeight = measurePage.scrollHeight;
const isIndivisible = isIndivisibleElement(child);
const fitsCurrentPage =
totalHeight <= pageHeight ||
(!isIndivisible && totalHeight <= pageHeight * 1.1) ||
currentPageContent.length === 0;
if (fitsCurrentPage) {
currentPageContent.push(child);
currentPageHeight += childHeight;
currentPageHeight = totalHeight;
continue;
}
// 当前页已放不下:移除刚刚加入的克隆节点
measureContent.removeChild(childClone);
await waitForLayout();
if (currentPageContent.length > 0) {
pages.push({
index: pageIndex++,
content: wrapPageContent(currentPageContent),
height: currentPageHeight
});
}
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,9 @@ 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];
/**
* 小红书预览视图类
*/
@@ -29,11 +32,13 @@ export class XiaohongshuPreview {
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 = '';
@@ -110,6 +115,30 @@ export class XiaohongshuPreview {
option.text = name;
});
const previewWidthLabel = this.topToolbar.createDiv({ cls: 'toolbar-label' });
previewWidthLabel.innerText = '预览宽度';
this.previewWidthSelect = this.topToolbar.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 fontSizeLabel = this.topToolbar.createDiv({ cls: 'toolbar-label' });
fontSizeLabel.innerText = '字号';
@@ -139,7 +168,27 @@ export class XiaohongshuPreview {
const prevBtn = this.pageNavigation.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 = this.pageNavigation.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();
this.pageTotalLabel = indicator.createEl('span', { cls: 'xhs-page-number-total', text: '/1' });
const nextBtn = this.pageNavigation.createEl('button', { text: '', cls: 'xhs-nav-btn' });
nextBtn.onclick = () => this.nextPage();
@@ -171,15 +220,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 +260,96 @@ 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.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 };
}
/**
@@ -341,6 +480,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);
}
}
/**
* 显示小红书预览视图
*/
@@ -365,11 +515,13 @@ 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,26 +2,94 @@
/* =========================================================== */
/* 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: grid !important;
grid-template-rows: auto 1fr;
min-height: 0; /* 允许内部滚动区域正确计算高度 */
}
.render-div {
flex: 1;
overflow-y: auto;
padding: 10px;
-webkit-user-select: text;
@@ -44,55 +112,43 @@
.preview-toolbar {
position: relative;
min-height: 100px;
padding: 4px 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, max-content));
gap: 12px;
align-items: center;
min-height: auto;
padding: 8px 12px;
border-bottom: 1px solid #e8eaed;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
box-shadow: 0 2px 4px rgba(0,0,0,0.04);
background: var(--grad-toolbar);
box-shadow: var(--shadow-overlay);
}
.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 +158,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 +171,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 +197,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 +230,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 +250,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 +284,10 @@ label:hover {
}
.loading-wrapper {
display: flex;
display: grid;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
place-items: center;
}
.loading-spinner {
@@ -276,23 +313,38 @@ label:hover {
/* =========================================================== */
.toolbar-line {
display: flex;
display: grid;
grid-auto-flow: column;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: white;
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;
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 #1e88e5;
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);
}
/* =========================================================== */
@@ -301,81 +353,39 @@ 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;
}
.platform-select:focus {
outline: none;
border-color: #1e88e5;
}
/* =========================================================== */
/* 微信公众号选择器样式 */
/* =========================================================== */
.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;
}
/* =========================================================== */
/* 按钮样式 */
/* =========================================================== */
.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-button.purple-gradient:hover { box-shadow: var(--shadow-purple-4); }
/* =========================================================== */
/* 分隔线样式 */
@@ -384,7 +394,7 @@ label:hover {
.toolbar-separator {
width: 1px;
height: 24px;
background: #dadce0;
background: var(--c-border);
margin: 0 4px;
}
@@ -398,8 +408,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 +425,7 @@ label:hover {
}
.doc-modal-iframe {
flex: 1;
min-height: 0;
}
/* =========================================================== */
@@ -421,9 +433,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 {
@@ -449,40 +462,36 @@ label:hover {
height: 100%;
}
.xhs-preview-container {
display: flex;
flex-direction: column;
.xhs-preview-container:not([style*="display: none"]) {
display: grid !important;
grid-template-rows: auto 1fr auto;
height: 100%;
background: linear-gradient(135deg, #f5f7fa 0%, #e8eaf6 100%);
background: var(--grad-xhs-bg);
min-height: 0;
}
.xhs-page-container {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
align-items: center;
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; /* 允许 flex 子项正确收缩和滚动 */
min-height: 0; /* 允许子项正确收缩和滚动 */
}
/* 小红书单页包裹器:为缩放后的页面预留正确的布局空间 */
.xhs-page-wrapper {
/* 显示尺寸缩放后540 × 720 */
width: 540px;
height: 720px;
margin: 0px auto;
position: relative;
overflow: visible;
overflow: hidden;
}
/* 小红书单页样式:实际尺寸 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;
@@ -494,26 +503,26 @@ label:hover {
}
.xhs-top-toolbar {
display: flex;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, max-content));
align-items: center;
gap: 12px;
padding: 8px 12px;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
background: var(--grad-toolbar);
border-bottom: 1px solid #e8eaed;
box-shadow: 0 2px 4px rgba(0,0,0,0.04);
flex-wrap: wrap;
box-shadow: var(--shadow-overlay);
}
.toolbar-label {
font-size: 11px;
color: #5f6368;
color: var(--c-text-muted);
font-weight: 500;
white-space: nowrap;
}
.xhs-select {
padding: 4px 8px;
border: 1px solid #dadce0;
border: 1px solid var(--c-border);
border-radius: 4px;
background: white;
font-size: 11px;
@@ -522,20 +531,21 @@ label:hover {
}
.xhs-select:hover {
border-color: #1e88e5;
border-color: var(--c-primary);
}
.xhs-select:focus {
outline: none;
border-color: #1e88e5;
border-color: var(--c-primary);
}
.font-size-group {
display: flex;
display: grid;
grid-auto-flow: column;
align-items: center;
gap: 6px;
background: white;
border: 1px solid #dadce0;
border: 1px solid var(--c-border);
border-radius: 4px;
padding: 2px;
}
@@ -565,7 +575,8 @@ label:hover {
}
.xhs-page-navigation {
display: flex;
display: grid;
grid-auto-flow: column;
justify-content: center;
align-items: center;
gap: 16px;
@@ -577,51 +588,73 @@ label:hover {
.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-page-number-input {
width: 56px;
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-bottom-toolbar {
display: flex;
display: grid;
grid-auto-flow: column;
justify-content: center;
gap: 12px;
padding: 12px 16px;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
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: 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 +663,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 +693,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 +738,8 @@ label:hover {
}
.xhs-button-container {
display: flex;
display: grid;
grid-auto-flow: column;
justify-content: center;
gap: 15px;
margin-top: 20px;

View File

@@ -95,7 +95,8 @@ SOLVEobsidian控制台打印信息定位在哪里阻塞AI修复。
7. 小红书模式,页面渲染使用选择的主题。参考微信公众号模式进行渲染。
8. 需求:主题、字体、字大小变化时,需要重新分页
8. 需求:小红书模式下html渲染后按照previewWidth × previewHeight 的预览窗口尺寸切割,内容不能丢失
主题、字体、字大小变化时,需要进行重新分页,保持内容不丢失。
去掉主题设置,使用全局主题设置。(❗️先简化,后续小红书和微信公众号应主题应该需要独立开。)
去掉字体设置,使用主题字体。
字体大小支持直接编辑。
@@ -103,7 +104,16 @@ SOLVEobsidian控制台打印信息定位在哪里阻塞AI修复。
- 字变大时,一页的内容放不下,重新分页应该会增加页数。但现在重新分页当前页放不下的内容只是被剪掉了。
- 表格显示不完整。
9. styles.css中有很多冗余。
9. styles.css中有很多冗余。改为grid布局。部分完成这部分需要后面**手动调整**重构。🧶 ♻️ ❇️
问题:小红书预览布局有问题❓
10. 新建docs文件夹把除了README和todolist以外的markdown文件放到docs中。
## 经验
1. 在不确定AI是否理解或者需求是否准确的情况下先用chat模式提问看回答确定AI理解是否准确。
尤其对于较大规模的重构需求,这点很重要‼️ 。

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;
}