update at 2025-10-08 22:26:26

This commit is contained in:
douboer
2025-10-08 22:26:26 +08:00
parent 7b36394427
commit e25dca5fdd
24 changed files with 180 additions and 1485 deletions

View File

@@ -27,7 +27,9 @@ function parseAspectRatio(ratio: string): { width: number; height: number } {
*/
function getTargetPageHeight(settings: NMPSettings): number {
const ratio = parseAspectRatio(settings.sliceImageAspectRatio);
return 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;
}
/**
@@ -150,20 +152,26 @@ function wrapPageContent(elements: Element[]): string {
/**
* 渲染单个页面到容器
* 预览时缩放显示,切图时使用实际尺寸
*/
export function renderPage(
container: HTMLElement,
pageContent: string,
settings: NMPSettings
): void {
const pageHeight = getTargetPageHeight(settings);
const pageWidth = settings.sliceImageWidth;
// 实际内容尺寸(切图使用)
const actualPageWidth = settings.sliceImageWidth;
const actualPageHeight = getTargetPageHeight(settings);
console.log(`[renderPage] 渲染页面: 宽=${actualPageWidth}, 高=${actualPageHeight}`);
container.innerHTML = '';
// 直接设置为实际尺寸,用于切图
// 预览时通过外层 CSS 的 max-width 限制显示宽度,浏览器自动缩放
container.style.cssText = `
width: ${pageWidth}px;
min-height: ${pageHeight}px;
max-height: ${pageHeight}px;
width: ${actualPageWidth}px;
height: ${actualPageHeight}px;
overflow: hidden;
box-sizing: border-box;
padding: 40px;

View File

@@ -52,9 +52,11 @@ export async function sliceCurrentPage(
const originalWidth = pageElement.style.width;
const originalMaxWidth = pageElement.style.maxWidth;
const originalMinWidth = pageElement.style.minWidth;
const originalTransform = pageElement.style.transform;
try {
// 临时设置为目标宽度
// 临时移除 transform 缩放,恢复实际尺寸用于切图
pageElement.style.transform = 'none';
pageElement.style.width = `${sliceImageWidth}px`;
pageElement.style.maxWidth = `${sliceImageWidth}px`;
pageElement.style.minWidth = `${sliceImageWidth}px`;
@@ -78,6 +80,7 @@ export async function sliceCurrentPage(
} finally {
// 恢复样式
pageElement.style.transform = originalTransform;
pageElement.style.width = originalWidth;
pageElement.style.maxWidth = originalMaxWidth;
pageElement.style.minWidth = originalMinWidth;

View File

@@ -28,14 +28,14 @@ export class XiaohongshuPreview {
// UI 元素
topToolbar!: HTMLDivElement;
templateSelect!: HTMLSelectElement;
themeSelect!: HTMLSelectElement;
fontSelect!: HTMLSelectElement;
fontSizeDisplay!: HTMLSpanElement;
fontSizeInput!: HTMLInputElement;
pageContainer!: HTMLDivElement;
bottomToolbar!: HTMLDivElement;
pageNavigation!: HTMLDivElement;
pageNumberDisplay!: HTMLSpanElement;
styleEl: HTMLStyleElement | null = null; // 主题样式注入节点
currentThemeClass: string = '';
// 分页数据
pages: PageInfo[] = [];
@@ -61,6 +61,14 @@ export class XiaohongshuPreview {
build(): void {
this.container.empty();
this.container.addClass('xhs-preview-container');
// 准备样式挂载节点
if (!this.styleEl) {
this.styleEl = document.createElement('style');
this.styleEl.setAttr('data-xhs-style', '');
}
if (!this.container.contains(this.styleEl)) {
this.container.appendChild(this.styleEl);
}
// 顶部工具栏
this.buildTopToolbar();
@@ -102,31 +110,7 @@ export class XiaohongshuPreview {
option.text = name;
});
// 主题选择
const themeLabel = this.topToolbar.createDiv({ cls: 'toolbar-label' });
themeLabel.innerText = '主题';
this.themeSelect = this.topToolbar.createEl('select', { cls: 'xhs-select' });
const themes = this.assetsManager.themes;
themes.forEach(theme => {
const option = this.themeSelect.createEl('option');
option.value = theme.className;
option.text = theme.name;
});
this.themeSelect.value = this.settings.defaultStyle;
this.themeSelect.onchange = () => this.onThemeChanged();
// 字体选择
const fontLabel = this.topToolbar.createDiv({ cls: 'toolbar-label' });
fontLabel.innerText = '字体';
this.fontSelect = this.topToolbar.createEl('select', { cls: 'xhs-select' });
['系统默认', '宋体', '黑体', '楷体', '仿宋'].forEach(name => {
const option = this.fontSelect.createEl('option');
option.value = name;
option.text = name;
});
this.fontSelect.onchange = () => this.onFontChanged();
// 字号控制
// 字号控制(可直接编辑)
const fontSizeLabel = this.topToolbar.createDiv({ cls: 'toolbar-label' });
fontSizeLabel.innerText = '字号';
const fontSizeGroup = this.topToolbar.createDiv({ cls: 'font-size-group' });
@@ -134,7 +118,13 @@ export class XiaohongshuPreview {
const decreaseBtn = fontSizeGroup.createEl('button', { text: '', cls: 'font-size-btn' });
decreaseBtn.onclick = () => this.changeFontSize(-1);
this.fontSizeDisplay = fontSizeGroup.createEl('span', { text: '16', cls: 'font-size-display' });
this.fontSizeInput = fontSizeGroup.createEl('input', {
cls: 'font-size-input',
attr: { type: 'number', min: '12', max: '36', value: '16' }
});
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);
@@ -188,6 +178,8 @@ export class XiaohongshuPreview {
new Notice(`分页完成:共 ${this.pages.length}`);
this.currentPageIndex = 0;
// 初次渲染时应用当前主题
this.applyThemeCSS();
this.renderCurrentPage();
} finally {
document.body.removeChild(tempContainer);
@@ -203,7 +195,15 @@ export class XiaohongshuPreview {
const page = this.pages[this.currentPageIndex];
this.pageContainer.empty();
const pageElement = this.pageContainer.createDiv({ cls: 'xhs-page' });
// 重置滚动位置到顶部
this.pageContainer.scrollTop = 0;
// 创建包裹器,为缩放后的页面预留正确的布局空间
const wrapper = this.pageContainer.createDiv({ cls: 'xhs-page-wrapper' });
const classes = ['xhs-page'];
if (this.currentThemeClass) classes.push('note-to-mp');
const pageElement = wrapper.createDiv({ cls: classes.join(' ') });
renderPage(pageElement, page.content, this.settings);
// 应用字体设置
@@ -214,47 +214,33 @@ export class XiaohongshuPreview {
}
/**
* 应用字体设置
* 应用字体设置(仅字号,字体从主题读取)
*/
private applyFontSettings(element: HTMLElement): void {
const fontFamily = this.fontSelect.value;
const fontSize = this.currentFontSize;
let fontFamilyCSS = '';
switch (fontFamily) {
case '宋体': fontFamilyCSS = 'SimSun, serif'; break;
case '黑体': fontFamilyCSS = 'SimHei, sans-serif'; break;
case '楷体': fontFamilyCSS = 'KaiTi, serif'; break;
case '仿宋': fontFamilyCSS = 'FangSong, serif'; break;
default: fontFamilyCSS = 'system-ui, -apple-system, sans-serif';
element.style.fontSize = `${this.currentFontSize}px`;
}
/**
* 切换字号(± 按钮)
*/
private async changeFontSize(delta: number): Promise<void> {
this.currentFontSize = Math.max(12, Math.min(36, this.currentFontSize + delta));
this.fontSizeInput.value = String(this.currentFontSize);
await this.repaginateAndRender();
}
/**
* 字号输入框改变事件
*/
private async onFontSizeInputChanged(): Promise<void> {
const val = parseInt(this.fontSizeInput.value, 10);
if (isNaN(val) || val < 12 || val > 36) {
this.fontSizeInput.value = String(this.currentFontSize);
new Notice('字号范围: 12-36');
return;
}
element.style.fontFamily = fontFamilyCSS;
element.style.fontSize = `${fontSize}px`;
}
/**
* 切换字号
*/
private changeFontSize(delta: number): void {
this.currentFontSize = Math.max(12, Math.min(24, this.currentFontSize + delta));
this.fontSizeDisplay.innerText = String(this.currentFontSize);
this.renderCurrentPage();
}
/**
* 主题改变
*/
private onThemeChanged(): void {
new Notice('主题已切换,请刷新预览');
// TODO: 重新渲染文章
}
/**
* 字体改变
*/
private onFontChanged(): void {
this.renderCurrentPage();
this.currentFontSize = val;
await this.repaginateAndRender();
}
/**
@@ -379,14 +365,59 @@ export class XiaohongshuPreview {
destroy(): void {
this.topToolbar = null as any;
this.templateSelect = null as any;
this.themeSelect = null as any;
this.fontSelect = null as any;
this.fontSizeDisplay = 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.pages = [];
this.currentFile = null;
this.styleEl = null;
}
/** 组合并注入主题 + 高亮 + 自定义 CSS使用全局默认主题 */
private applyThemeCSS() {
if (!this.styleEl) return;
const themeName = this.settings.defaultStyle;
const highlightName = this.settings.defaultHighlight;
const theme = this.assetsManager.getTheme(themeName);
const highlight = this.assetsManager.getHighlight(highlightName);
const customCSS = (this.settings.useCustomCss || this.settings.customCSSNote.length>0) ? this.assetsManager.customCSS : '';
const baseCSS = this.settings.baseCSS ? `.note-to-mp {${this.settings.baseCSS}}` : '';
const css = `${highlight?.css || ''}\n\n${theme?.css || ''}\n\n${baseCSS}\n\n${customCSS}`;
this.styleEl.textContent = css;
this.currentThemeClass = theme?.className || '';
}
private async repaginateAndRender(): Promise<void> {
if (!this.articleHTML) return;
const totalBefore = this.pages.length || 1;
const posRatio = (this.currentPageIndex + 0.5) / totalBefore; // 以当前页中心作为相对位置
new Notice('重新分页中...');
const tempContainer = document.createElement('div');
tempContainer.innerHTML = this.articleHTML;
tempContainer.style.width = `${this.settings.sliceImageWidth}px`;
tempContainer.style.fontSize = `${this.currentFontSize}px`;
// 字体从全局主题中继承,无需手动指定
tempContainer.classList.add('note-to-mp');
tempContainer.className = this.currentThemeClass ? `note-to-mp ${this.currentThemeClass}` : 'note-to-mp';
document.body.appendChild(tempContainer);
try {
this.pages = await paginateArticle(tempContainer, this.settings);
if (this.pages.length > 0) {
const newIndex = Math.floor(posRatio * this.pages.length - 0.5);
this.currentPageIndex = Math.min(this.pages.length - 1, Math.max(0, newIndex));
} else {
this.currentPageIndex = 0;
}
this.renderCurrentPage();
new Notice(`重新分页完成:共 ${this.pages.length}`);
} catch (e) {
console.error('重新分页失败', e);
new Notice('重新分页失败');
} finally {
document.body.removeChild(tempContainer);
}
}
}