diff --git a/assets/themes/wx-mp-pro.css b/assets/themes/wx-mp-pro.css index c5397cd..b3f98e4 100644 --- a/assets/themes/wx-mp-pro.css +++ b/assets/themes/wx-mp-pro.css @@ -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; diff --git a/images/xhs/note2mdtest_1.png b/images/xhs/note2mdtest_1.png index 8d130bf..8bdf8a1 100644 Binary files a/images/xhs/note2mdtest_1.png and b/images/xhs/note2mdtest_1.png differ diff --git a/images/xhs/note2mdtest_2.png b/images/xhs/note2mdtest_2.png index fa99fdc..689dade 100644 Binary files a/images/xhs/note2mdtest_2.png and b/images/xhs/note2mdtest_2.png differ diff --git a/images/xhs/note2mdtest_3.png b/images/xhs/note2mdtest_3.png index fac806b..127b6d2 100644 Binary files a/images/xhs/note2mdtest_3.png and b/images/xhs/note2mdtest_3.png differ diff --git a/images/xhs/note2mdtest_4.png b/images/xhs/note2mdtest_4.png index b9e45d1..e6171e5 100644 Binary files a/images/xhs/note2mdtest_4.png and b/images/xhs/note2mdtest_4.png differ diff --git a/images/xhs/note2mdtest_5.png b/images/xhs/note2mdtest_5.png index 21c8eaa..fac806b 100644 Binary files a/images/xhs/note2mdtest_5.png and b/images/xhs/note2mdtest_5.png differ diff --git a/images/xhs/note2mdtest_6.png b/images/xhs/note2mdtest_6.png index 9103225..e1cd6d8 100644 Binary files a/images/xhs/note2mdtest_6.png and b/images/xhs/note2mdtest_6.png differ diff --git a/images/xhs/note2mdtest_7.png b/images/xhs/note2mdtest_7.png index d60152e..16bb59c 100644 Binary files a/images/xhs/note2mdtest_7.png and b/images/xhs/note2mdtest_7.png differ diff --git a/images/xhs/note2mdtest_8.png b/images/xhs/note2mdtest_8.png index 14126fe..e6f8619 100644 Binary files a/images/xhs/note2mdtest_8.png and b/images/xhs/note2mdtest_8.png differ diff --git a/images/xhs/note2mdtest_9.png b/images/xhs/note2mdtest_9.png index 32abb19..81b4867 100644 Binary files a/images/xhs/note2mdtest_9.png and b/images/xhs/note2mdtest_9.png differ diff --git a/src/xiaohongshu/paginator.ts b/src/xiaohongshu/paginator.ts index 3fd9e89..86653a8 100644 --- a/src/xiaohongshu/paginator.ts +++ b/src/xiaohongshu/paginator.ts @@ -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 { 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 { + await new Promise(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; `; diff --git a/src/xiaohongshu/xhs-preview.ts b/src/xiaohongshu/xhs-preview.ts index 729839b..7585866 100644 --- a/src/xiaohongshu/xhs-preview.ts +++ b/src/xiaohongshu/xhs-preview.ts @@ -37,7 +37,8 @@ export class XiaohongshuPreview { pageContainer!: HTMLDivElement; bottomToolbar!: HTMLDivElement; pageNavigation!: HTMLDivElement; - pageNumberDisplay!: HTMLSpanElement; + pageNumberInput!: HTMLInputElement; + pageTotalLabel!: HTMLSpanElement; styleEl: HTMLStyleElement | null = null; // 主题样式注入节点 currentThemeClass: string = ''; @@ -167,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(); @@ -199,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); @@ -239,7 +266,47 @@ export class XiaohongshuPreview { 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.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; diff --git a/styles.css b/styles.css index 23af5bb..03e909a 100644 --- a/styles.css +++ b/styles.css @@ -604,12 +604,38 @@ label:hover { 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; - 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 { diff --git a/todolist.md b/todolist.md index 477d877..79f057e 100644 --- a/todolist.md +++ b/todolist.md @@ -95,7 +95,8 @@ SOLVE:obsidian控制台打印信息,定位在哪里阻塞,AI修复。 7. 小红书模式,页面渲染使用选择的主题。参考微信公众号模式进行渲染。 ✅ -8. 需求:主题、字体、字大小变化时,需要重新分页。 +8. 需求:小红书模式下,html渲染后按照previewWidth × previewHeight 的预览窗口尺寸切割,内容不能丢失。 + 主题、字体、字大小变化时,需要进行重新分页,保持内容不丢失。 去掉主题设置,使用全局主题设置。(❗️先简化,后续小红书和微信公众号应主题应该需要独立开。) 去掉字体设置,使用主题字体。 字体大小支持直接编辑。 @@ -110,3 +111,9 @@ SOLVE:obsidian控制台打印信息,定位在哪里阻塞,AI修复。 ✅ +## 经验 +1. 在不确定AI是否理解,或者需求是否准确的情况下,先用chat模式提问,看回答确定AI理解是否准确。 + 尤其对于较大规模的重构需求,这点很重要‼️ 。 + + +