update at 2025-10-08 19:45:28
This commit is contained in:
392
src/xiaohongshu/xhs-preview.ts
Normal file
392
src/xiaohongshu/xhs-preview.ts
Normal file
@@ -0,0 +1,392 @@
|
||||
/**
|
||||
* 文件:xiaohongshu/xhs-preview.ts
|
||||
* 作用:小红书预览视图组件,专门处理小红书平台的预览、分页和切图功能
|
||||
*
|
||||
* 功能:
|
||||
* 1. 渲染小红书专属的预览界面(顶部工具栏、分页导航、底部切图按钮)
|
||||
* 2. 处理文章内容的小红书格式化和分页
|
||||
* 3. 提供切图功能(当前页/全部页)
|
||||
* 4. 管理小红书特有的样式和字体设置
|
||||
*/
|
||||
|
||||
import { Notice, TFile } from 'obsidian';
|
||||
import { NMPSettings } from '../settings';
|
||||
import AssetsManager from '../assets';
|
||||
import { paginateArticle, renderPage, PageInfo } from './paginator';
|
||||
import { sliceCurrentPage, sliceAllPages } from './slice';
|
||||
|
||||
/**
|
||||
* 小红书预览视图类
|
||||
*/
|
||||
export class XiaohongshuPreview {
|
||||
container: HTMLElement;
|
||||
settings: NMPSettings;
|
||||
assetsManager: AssetsManager;
|
||||
app: any;
|
||||
currentFile: TFile | null = null;
|
||||
|
||||
// UI 元素
|
||||
topToolbar!: HTMLDivElement;
|
||||
templateSelect!: HTMLSelectElement;
|
||||
themeSelect!: HTMLSelectElement;
|
||||
fontSelect!: HTMLSelectElement;
|
||||
fontSizeDisplay!: HTMLSpanElement;
|
||||
|
||||
pageContainer!: HTMLDivElement;
|
||||
bottomToolbar!: HTMLDivElement;
|
||||
pageNavigation!: HTMLDivElement;
|
||||
pageNumberDisplay!: HTMLSpanElement;
|
||||
|
||||
// 分页数据
|
||||
pages: PageInfo[] = [];
|
||||
currentPageIndex: number = 0;
|
||||
currentFontSize: number = 16;
|
||||
articleHTML: string = '';
|
||||
|
||||
// 回调函数
|
||||
onRefreshCallback?: () => Promise<void>;
|
||||
onPublishCallback?: () => Promise<void>;
|
||||
onPlatformChangeCallback?: (platform: string) => Promise<void>;
|
||||
|
||||
constructor(container: HTMLElement, app: any) {
|
||||
this.container = container;
|
||||
this.app = app;
|
||||
this.settings = NMPSettings.getInstance();
|
||||
this.assetsManager = AssetsManager.getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建完整的小红书预览界面
|
||||
*/
|
||||
build(): void {
|
||||
this.container.empty();
|
||||
this.container.addClass('xhs-preview-container');
|
||||
|
||||
// 顶部工具栏
|
||||
this.buildTopToolbar();
|
||||
|
||||
// 页面容器
|
||||
this.pageContainer = this.container.createDiv({ cls: 'xhs-page-container' });
|
||||
|
||||
// 分页导航
|
||||
this.buildPageNavigation();
|
||||
|
||||
// 底部操作栏
|
||||
this.buildBottomToolbar();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建顶部工具栏
|
||||
*/
|
||||
private buildTopToolbar(): void {
|
||||
this.topToolbar = this.container.createDiv({ cls: 'xhs-top-toolbar' });
|
||||
|
||||
// 刷新按钮
|
||||
const refreshBtn = this.topToolbar.createEl('button', { text: '🔄 刷新', cls: 'toolbar-button purple-gradient' });
|
||||
refreshBtn.onclick = () => this.onRefresh();
|
||||
|
||||
// 发布按钮
|
||||
const publishBtn = this.topToolbar.createEl('button', { text: '📤 发布', cls: 'toolbar-button' });
|
||||
publishBtn.onclick = () => this.onPublish();
|
||||
|
||||
// 分隔线
|
||||
const separator2 = this.topToolbar.createDiv({ cls: 'toolbar-separator' });
|
||||
|
||||
// 模板选择
|
||||
const templateLabel = this.topToolbar.createDiv({ cls: 'toolbar-label' });
|
||||
templateLabel.innerText = '模板';
|
||||
this.templateSelect = this.topToolbar.createEl('select', { cls: 'xhs-select' });
|
||||
['默认模板', '简约模板', '杂志模板'].forEach(name => {
|
||||
const option = this.templateSelect.createEl('option');
|
||||
option.value = name;
|
||||
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' });
|
||||
|
||||
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' });
|
||||
|
||||
const increaseBtn = fontSizeGroup.createEl('button', { text: '+', cls: 'font-size-btn' });
|
||||
increaseBtn.onclick = () => this.changeFontSize(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建分页导航
|
||||
*/
|
||||
private buildPageNavigation(): void {
|
||||
this.pageNavigation = this.container.createDiv({ cls: 'xhs-page-navigation' });
|
||||
|
||||
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 nextBtn = this.pageNavigation.createEl('button', { text: '›', cls: 'xhs-nav-btn' });
|
||||
nextBtn.onclick = () => this.nextPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建底部操作栏
|
||||
*/
|
||||
private buildBottomToolbar(): void {
|
||||
this.bottomToolbar = this.container.createDiv({ cls: 'xhs-bottom-toolbar' });
|
||||
|
||||
const currentPageBtn = this.bottomToolbar.createEl('button', { text: '⬇ 当前页切图', cls: 'xhs-slice-btn' });
|
||||
currentPageBtn.onclick = () => this.sliceCurrentPage();
|
||||
|
||||
const allPagesBtn = this.bottomToolbar.createEl('button', { text: '⇓ 全部页切图', cls: 'xhs-slice-btn secondary' });
|
||||
allPagesBtn.onclick = () => this.sliceAllPages();
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染文章内容并分页
|
||||
*/
|
||||
async renderArticle(articleHTML: string, file: TFile): Promise<void> {
|
||||
this.articleHTML = articleHTML;
|
||||
this.currentFile = file;
|
||||
|
||||
new Notice('正在分页...');
|
||||
|
||||
// 创建临时容器用于分页
|
||||
const tempContainer = document.createElement('div');
|
||||
tempContainer.innerHTML = articleHTML;
|
||||
tempContainer.style.width = `${this.settings.sliceImageWidth}px`;
|
||||
document.body.appendChild(tempContainer);
|
||||
|
||||
try {
|
||||
this.pages = await paginateArticle(tempContainer, this.settings);
|
||||
new Notice(`分页完成:共 ${this.pages.length} 页`);
|
||||
|
||||
this.currentPageIndex = 0;
|
||||
this.renderCurrentPage();
|
||||
} finally {
|
||||
document.body.removeChild(tempContainer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染当前页
|
||||
*/
|
||||
private renderCurrentPage(): void {
|
||||
if (this.pages.length === 0) return;
|
||||
|
||||
const page = this.pages[this.currentPageIndex];
|
||||
this.pageContainer.empty();
|
||||
|
||||
const pageElement = this.pageContainer.createDiv({ cls: 'xhs-page' });
|
||||
renderPage(pageElement, page.content, this.settings);
|
||||
|
||||
// 应用字体设置
|
||||
this.applyFontSettings(pageElement);
|
||||
|
||||
// 更新页码显示
|
||||
this.pageNumberDisplay.innerText = `${this.currentPageIndex + 1}/${this.pages.length}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用字体设置
|
||||
*/
|
||||
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.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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 上一页
|
||||
*/
|
||||
private previousPage(): void {
|
||||
if (this.currentPageIndex > 0) {
|
||||
this.currentPageIndex--;
|
||||
this.renderCurrentPage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下一页
|
||||
*/
|
||||
private nextPage(): void {
|
||||
if (this.currentPageIndex < this.pages.length - 1) {
|
||||
this.currentPageIndex++;
|
||||
this.renderCurrentPage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前页切图
|
||||
*/
|
||||
private async sliceCurrentPage(): Promise<void> {
|
||||
if (!this.currentFile) {
|
||||
new Notice('请先打开一个笔记');
|
||||
return;
|
||||
}
|
||||
|
||||
const pageElement = this.pageContainer.querySelector('.xhs-page') as HTMLElement;
|
||||
if (!pageElement) {
|
||||
new Notice('未找到页面元素');
|
||||
return;
|
||||
}
|
||||
|
||||
new Notice('正在切图...');
|
||||
try {
|
||||
await sliceCurrentPage(pageElement, this.currentFile, this.currentPageIndex, this.app);
|
||||
new Notice('✅ 当前页切图完成');
|
||||
} catch (error) {
|
||||
console.error('切图失败:', error);
|
||||
new Notice('❌ 切图失败: ' + (error instanceof Error ? error.message : String(error)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新按钮点击
|
||||
*/
|
||||
private async onRefresh(): Promise<void> {
|
||||
if (this.onRefreshCallback) {
|
||||
await this.onRefreshCallback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布按钮点击
|
||||
*/
|
||||
private async onPublish(): Promise<void> {
|
||||
if (this.onPublishCallback) {
|
||||
await this.onPublishCallback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全部页切图
|
||||
*/
|
||||
private async sliceAllPages(): Promise<void> {
|
||||
if (!this.currentFile) {
|
||||
new Notice('请先打开一个笔记');
|
||||
return;
|
||||
}
|
||||
|
||||
new Notice(`开始切图:共 ${this.pages.length} 页`);
|
||||
|
||||
try {
|
||||
for (let i = 0; i < this.pages.length; i++) {
|
||||
new Notice(`正在处理第 ${i + 1}/${this.pages.length} 页...`);
|
||||
|
||||
// 临时渲染这一页
|
||||
this.currentPageIndex = i;
|
||||
this.renderCurrentPage();
|
||||
|
||||
// 等待渲染完成
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
const pageElement = this.pageContainer.querySelector('.xhs-page') as HTMLElement;
|
||||
if (pageElement) {
|
||||
await sliceCurrentPage(pageElement, this.currentFile, i, this.app);
|
||||
}
|
||||
}
|
||||
|
||||
new Notice(`✅ 全部页切图完成:共 ${this.pages.length} 张`);
|
||||
} catch (error) {
|
||||
console.error('批量切图失败:', error);
|
||||
new Notice('❌ 批量切图失败: ' + (error instanceof Error ? error.message : String(error)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示小红书预览视图
|
||||
*/
|
||||
show(): void {
|
||||
if (this.container) {
|
||||
this.container.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏小红书预览视图
|
||||
*/
|
||||
hide(): void {
|
||||
if (this.container) {
|
||||
this.container.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
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.pageContainer = null as any;
|
||||
this.bottomToolbar = null as any;
|
||||
this.pageNavigation = null as any;
|
||||
this.pageNumberDisplay = null as any;
|
||||
this.pages = [];
|
||||
this.currentFile = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user