update at 2025-10-09 14:46:24
@@ -60,7 +60,7 @@
|
|||||||
|
|
||||||
/* H2:左条卡片 */
|
/* H2:左条卡片 */
|
||||||
.note-to-mp h2 {
|
.note-to-mp h2 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5em;
|
||||||
margin: 2em 0 1.2em;
|
margin: 2em 0 1.2em;
|
||||||
padding: 0.6em 1em;
|
padding: 0.6em 1em;
|
||||||
background: #f5f7fa;
|
background: #f5f7fa;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 1.9 MiB |
@@ -22,13 +22,14 @@ function parseAspectRatio(ratio: string): { width: number; height: number } {
|
|||||||
return { width: 3, height: 4 };
|
return { width: 3, height: 4 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PAGE_PADDING = 40; // 与 renderPage 保持一致的页面内边距
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算目标页面高度
|
* 计算目标页面高度
|
||||||
*/
|
*/
|
||||||
function getTargetPageHeight(settings: NMPSettings): number {
|
function getTargetPageHeight(settings: NMPSettings): number {
|
||||||
const ratio = parseAspectRatio(settings.sliceImageAspectRatio);
|
const ratio = parseAspectRatio(settings.sliceImageAspectRatio);
|
||||||
const height = Math.round((settings.sliceImageWidth * ratio.height) / ratio.width);
|
const height = Math.round((settings.sliceImageWidth * ratio.height) / ratio.width);
|
||||||
console.log(`[paginator] 计算页面高度: 宽度=${settings.sliceImageWidth}, 比例=${settings.sliceImageAspectRatio} (${ratio.width}:${ratio.height}), 高度=${height}`);
|
|
||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,16 +58,37 @@ export async function paginateArticle(
|
|||||||
const pageHeight = getTargetPageHeight(settings);
|
const pageHeight = getTargetPageHeight(settings);
|
||||||
const pageWidth = settings.sliceImageWidth;
|
const pageWidth = settings.sliceImageWidth;
|
||||||
|
|
||||||
// 创建临时容器用于测量
|
// 创建临时测量容器:与实际页面一致的宽度与内边距
|
||||||
const measureContainer = document.createElement('div');
|
const measureHost = document.createElement('div');
|
||||||
measureContainer.style.cssText = `
|
measureHost.style.cssText = `
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -9999px;
|
left: -9999px;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: ${pageWidth}px;
|
width: ${pageWidth}px;
|
||||||
visibility: hidden;
|
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[] = [];
|
const pages: PageInfo[] = [];
|
||||||
let currentPageContent: Element[] = [];
|
let currentPageContent: Element[] = [];
|
||||||
@@ -79,51 +101,45 @@ export async function paginateArticle(
|
|||||||
|
|
||||||
for (const child of children) {
|
for (const child of children) {
|
||||||
const childClone = child.cloneNode(true) as HTMLElement;
|
const childClone = child.cloneNode(true) as HTMLElement;
|
||||||
measureContainer.innerHTML = '';
|
measureContent.appendChild(childClone);
|
||||||
measureContainer.appendChild(childClone);
|
|
||||||
|
|
||||||
// 等待浏览器完成渲染
|
await waitForLayout();
|
||||||
await new Promise(resolve => setTimeout(resolve, 10));
|
|
||||||
|
|
||||||
const childHeight = childClone.offsetHeight;
|
const totalHeight = measurePage.scrollHeight;
|
||||||
const isIndivisible = isIndivisibleElement(child);
|
const isIndivisible = isIndivisibleElement(child);
|
||||||
|
const fitsCurrentPage =
|
||||||
|
totalHeight <= pageHeight ||
|
||||||
|
(!isIndivisible && totalHeight <= pageHeight * 1.1) ||
|
||||||
|
currentPageContent.length === 0;
|
||||||
|
|
||||||
// 判断是否需要换页
|
if (fitsCurrentPage) {
|
||||||
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 {
|
|
||||||
// 加入当前页
|
|
||||||
currentPageContent.push(child);
|
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) {
|
if (currentPageContent.length > 0) {
|
||||||
pages.push({
|
pages.push({
|
||||||
index: pageIndex,
|
index: pageIndex,
|
||||||
@@ -132,12 +148,15 @@ export async function paginateArticle(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理临时容器
|
document.body.removeChild(measureHost);
|
||||||
document.body.removeChild(measureContainer);
|
|
||||||
|
|
||||||
return pages;
|
return pages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function waitForLayout(): Promise<void> {
|
||||||
|
await new Promise<void>(resolve => requestAnimationFrame(() => resolve()));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 包装页面内容为完整的 HTML
|
* 包装页面内容为完整的 HTML
|
||||||
*/
|
*/
|
||||||
@@ -163,8 +182,6 @@ export function renderPage(
|
|||||||
const actualPageWidth = settings.sliceImageWidth;
|
const actualPageWidth = settings.sliceImageWidth;
|
||||||
const actualPageHeight = getTargetPageHeight(settings);
|
const actualPageHeight = getTargetPageHeight(settings);
|
||||||
|
|
||||||
console.log(`[renderPage] 渲染页面: 宽=${actualPageWidth}, 高=${actualPageHeight}`);
|
|
||||||
|
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
// 直接设置为实际尺寸,用于切图
|
// 直接设置为实际尺寸,用于切图
|
||||||
@@ -174,7 +191,7 @@ export function renderPage(
|
|||||||
height: ${actualPageHeight}px;
|
height: ${actualPageHeight}px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 40px;
|
padding: ${PAGE_PADDING}px;
|
||||||
background: white;
|
background: white;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ export class XiaohongshuPreview {
|
|||||||
pageContainer!: HTMLDivElement;
|
pageContainer!: HTMLDivElement;
|
||||||
bottomToolbar!: HTMLDivElement;
|
bottomToolbar!: HTMLDivElement;
|
||||||
pageNavigation!: HTMLDivElement;
|
pageNavigation!: HTMLDivElement;
|
||||||
pageNumberDisplay!: HTMLSpanElement;
|
pageNumberInput!: HTMLInputElement;
|
||||||
|
pageTotalLabel!: HTMLSpanElement;
|
||||||
styleEl: HTMLStyleElement | null = null; // 主题样式注入节点
|
styleEl: HTMLStyleElement | null = null; // 主题样式注入节点
|
||||||
currentThemeClass: string = '';
|
currentThemeClass: string = '';
|
||||||
|
|
||||||
@@ -167,7 +168,27 @@ export class XiaohongshuPreview {
|
|||||||
const prevBtn = this.pageNavigation.createEl('button', { text: '‹', cls: 'xhs-nav-btn' });
|
const prevBtn = this.pageNavigation.createEl('button', { text: '‹', cls: 'xhs-nav-btn' });
|
||||||
prevBtn.onclick = () => this.previousPage();
|
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' });
|
const nextBtn = this.pageNavigation.createEl('button', { text: '›', cls: 'xhs-nav-btn' });
|
||||||
nextBtn.onclick = () => this.nextPage();
|
nextBtn.onclick = () => this.nextPage();
|
||||||
@@ -199,15 +220,21 @@ export class XiaohongshuPreview {
|
|||||||
const tempContainer = document.createElement('div');
|
const tempContainer = document.createElement('div');
|
||||||
tempContainer.innerHTML = articleHTML;
|
tempContainer.innerHTML = articleHTML;
|
||||||
tempContainer.style.width = `${this.settings.sliceImageWidth}px`;
|
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);
|
document.body.appendChild(tempContainer);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 在分页前先应用主题与高亮,确保测量使用正确样式
|
||||||
|
this.applyThemeCSS();
|
||||||
this.pages = await paginateArticle(tempContainer, this.settings);
|
this.pages = await paginateArticle(tempContainer, this.settings);
|
||||||
new Notice(`分页完成:共 ${this.pages.length} 页`);
|
new Notice(`分页完成:共 ${this.pages.length} 页`);
|
||||||
|
|
||||||
this.currentPageIndex = 0;
|
this.currentPageIndex = 0;
|
||||||
// 初次渲染时应用当前主题
|
// 初次渲染时应用当前主题
|
||||||
this.applyThemeCSS();
|
|
||||||
this.renderCurrentPage();
|
this.renderCurrentPage();
|
||||||
} finally {
|
} finally {
|
||||||
document.body.removeChild(tempContainer);
|
document.body.removeChild(tempContainer);
|
||||||
@@ -239,7 +266,47 @@ export class XiaohongshuPreview {
|
|||||||
this.applyFontSettings(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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -453,7 +520,8 @@ export class XiaohongshuPreview {
|
|||||||
this.pageContainer = null as any;
|
this.pageContainer = null as any;
|
||||||
this.bottomToolbar = null as any;
|
this.bottomToolbar = null as any;
|
||||||
this.pageNavigation = 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.pages = [];
|
||||||
this.currentFile = null;
|
this.currentFile = null;
|
||||||
this.styleEl = null;
|
this.styleEl = null;
|
||||||
|
|||||||
34
styles.css
@@ -604,12 +604,38 @@ label:hover { color: var(--c-primary); }
|
|||||||
border-color: var(--c-primary);
|
border-color: var(--c-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.xhs-page-number {
|
.xhs-page-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
min-width: 50px;
|
|
||||||
text-align: center;
|
|
||||||
color: #202124;
|
|
||||||
font-weight: 500;
|
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 {
|
.xhs-bottom-toolbar {
|
||||||
|
|||||||
@@ -95,7 +95,8 @@ SOLVE:obsidian控制台打印信息,定位在哪里阻塞,AI修复。
|
|||||||
7. 小红书模式,页面渲染使用选择的主题。参考微信公众号模式进行渲染。
|
7. 小红书模式,页面渲染使用选择的主题。参考微信公众号模式进行渲染。
|
||||||
✅
|
✅
|
||||||
|
|
||||||
8. 需求:主题、字体、字大小变化时,需要重新分页。
|
8. 需求:小红书模式下,html渲染后按照previewWidth × previewHeight 的预览窗口尺寸切割,内容不能丢失。
|
||||||
|
主题、字体、字大小变化时,需要进行重新分页,保持内容不丢失。
|
||||||
去掉主题设置,使用全局主题设置。(❗️先简化,后续小红书和微信公众号应主题应该需要独立开。)
|
去掉主题设置,使用全局主题设置。(❗️先简化,后续小红书和微信公众号应主题应该需要独立开。)
|
||||||
去掉字体设置,使用主题字体。
|
去掉字体设置,使用主题字体。
|
||||||
字体大小支持直接编辑。
|
字体大小支持直接编辑。
|
||||||
@@ -110,3 +111,9 @@ SOLVE:obsidian控制台打印信息,定位在哪里阻塞,AI修复。
|
|||||||
✅
|
✅
|
||||||
|
|
||||||
|
|
||||||
|
## 经验
|
||||||
|
1. 在不确定AI是否理解,或者需求是否准确的情况下,先用chat模式提问,看回答确定AI理解是否准确。
|
||||||
|
尤其对于较大规模的重构需求,这点很重要‼️ 。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||