update at 2025-10-08 22:26:26
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user