update at 2025-10-08 19:45:28

This commit is contained in:
douboer
2025-10-08 19:45:28 +08:00
parent 5d32c0f5e7
commit 3460669602
20 changed files with 3325 additions and 101 deletions

View File

@@ -42,6 +42,7 @@ export default class AssetsManager {
wasmPath: string;
expertSettings: ExpertSettings;
isLoaded: boolean = false;
private loadingPromise: Promise<void> | null = null; // 防止重复并发加载
private static instance: AssetsManager;
@@ -75,18 +76,42 @@ export default class AssetsManager {
}
async loadAssets() {
await this.loadThemes();
await this.loadHighlights();
await this.loadCustomCSS();
await this.loadExpertSettings();
this.isLoaded = true;
if (this.isLoaded) return;
if (this.loadingPromise) {
// 已经在加载中,复用同一个 promise
return this.loadingPromise;
}
console.time('[Assets] loadAssets');
this.loadingPromise = (async () => {
try {
// 并行加载互不依赖的资源,加速启动
await Promise.all([
this.loadThemes().catch(e => console.error('[Assets] loadThemes 失败', e)),
this.loadHighlights().catch(e => console.error('[Assets] loadHighlights 失败', e)),
this.loadCustomCSS().catch(e => console.error('[Assets] loadCustomCSS 失败', e)),
this.loadExpertSettings().catch(e => console.error('[Assets] loadExpertSettings 失败', e)),
]);
this.isLoaded = true;
console.log('[Assets] 资源加载完成', {
themeCount: this.themes?.length ?? 0,
highlightCount: this.highlights?.length ?? 0,
customCSS: this.customCSS?.length ?? 0
});
} finally {
console.timeEnd('[Assets] loadAssets');
this.loadingPromise = null;
}
})();
return this.loadingPromise;
}
async loadThemes() {
try {
console.log('[Assets] loadThemes:start');
if (!await this.app.vault.adapter.exists(this.themeCfg)) {
new Notice('主题资源未下载,请前往设置下载!');
this.themes = [this.defaultTheme];
console.log('[Assets] loadThemes:themes.json missing -> default only');
return;
}
const data = await this.app.vault.adapter.read(this.themeCfg);
@@ -94,6 +119,7 @@ export default class AssetsManager {
const themes = JSON.parse(data);
await this.loadCSS(themes);
this.themes = [this.defaultTheme, ... themes];
console.log('[Assets] loadThemes:done', { count: this.themes.length });
}
} catch (error) {
console.error(error);
@@ -103,13 +129,19 @@ export default class AssetsManager {
async loadCSS(themes: Theme[]) {
try {
for (const theme of themes) {
const cssFile = this.themesPath + theme.className + '.css';
const cssContent = await this.app.vault.adapter.read(cssFile);
if (cssContent) {
theme.css = cssContent;
}
}
await Promise.all(
themes.map(async (theme) => {
try {
const cssFile = this.themesPath + theme.className + '.css';
const cssContent = await this.app.vault.adapter.read(cssFile);
if (cssContent) {
theme.css = cssContent;
}
} catch (e) {
console.warn('[Assets] 读取主题 CSS 失败', theme.className, e);
}
})
);
} catch (error) {
console.error(error);
new Notice('读取CSS失败');
@@ -118,6 +150,7 @@ export default class AssetsManager {
async loadCustomCSS() {
try {
console.log('[Assets] loadCustomCSS:start');
const customCSSNote = NMPSettings.getInstance().customCSSNote;
if (customCSSNote != '') {
const file = this.searchFile(customCSSNote);
@@ -141,6 +174,7 @@ export default class AssetsManager {
if (cssContent) {
this.customCSS = cssContent;
}
console.log('[Assets] loadCustomCSS:done', { hasContent: this.customCSS.length > 0 });
} catch (error) {
console.error(error);
new Notice('读取CSS失败');
@@ -149,6 +183,7 @@ export default class AssetsManager {
async loadExpertSettings() {
try {
console.log('[Assets] loadExpertSettings:start');
const note = NMPSettings.getInstance().expertSettingsNote;
if (note != '') {
const file = this.searchFile(note);
@@ -170,6 +205,7 @@ export default class AssetsManager {
else {
this.expertSettings = defaultExpertSettings;
}
console.log('[Assets] loadExpertSettings:done');
} catch (error) {
console.error(error);
new Notice('读取专家设置失败!');
@@ -178,10 +214,12 @@ export default class AssetsManager {
async loadHighlights() {
try {
console.log('[Assets] loadHighlights:start');
const defaultHighlight = {name: '默认', url: '', css: DefaultHighlight};
this.highlights = [defaultHighlight];
if (!await this.app.vault.adapter.exists(this.hilightCfg)) {
new Notice('高亮资源未下载,请前往设置下载!');
console.log('[Assets] loadHighlights:highlights.json missing -> default only');
return;
}
@@ -193,6 +231,7 @@ export default class AssetsManager {
const cssContent = await this.app.vault.adapter.read(cssFile);
this.highlights.push({name: item.name, url: item.url, css: cssContent});
}
console.log('[Assets] loadHighlights:done', { count: this.highlights.length });
}
}
catch (error) {
@@ -234,6 +273,10 @@ export default class AssetsManager {
return theme;
}
}
// 找不到主题时返回第一个主题(默认主题)
console.warn(`[Assets] 主题 "${themeName}" 未找到,使用默认主题`);
return this.themes[0];
}
getHighlight(highlightName: string) {
@@ -246,6 +289,10 @@ export default class AssetsManager {
return highlight;
}
}
// 找不到高亮时返回第一个高亮(默认高亮)
console.warn(`[Assets] 高亮 "${highlightName}" 未找到,使用默认高亮`);
return this.highlights[0];
}
getThemeURL() {

View File

@@ -533,14 +533,14 @@ export class BatchPublishModal extends Modal {
* 发布到微信公众号
*/
private async publishToWechat(file: TFile): Promise<void> {
// TODO: 重构后需要重新实现批量发布到微信
// 激活预览视图并发布
await this.plugin.activateView();
const preview = this.plugin.getNotePreview();
if (preview) {
// 确保预览器处于微信模式
preview.currentPlatform = 'wechat';
await preview.renderMarkdown(file);
await preview.postToWechat();
// 临时方案:直接打开文件让用户手动发布
await preview.setFile(file);
throw new Error('批量发布功能正在重构中,请在预览视图中手动发布');
} else {
throw new Error('无法获取预览视图');
}

View File

@@ -8,7 +8,7 @@
*/
import { Plugin, WorkspaceLeaf, App, PluginManifest, Menu, Notice, TAbstractFile, TFile, TFolder } from 'obsidian';
import { NotePreview, VIEW_TYPE_NOTE_PREVIEW } from './note-preview';
import { PreviewView, VIEW_TYPE_NOTE_PREVIEW } from './preview-view';
import { NMPSettings } from './settings';
import { NoteToMpSettingTab } from './setting-tab';
import AssetsManager from './assets';
@@ -51,22 +51,40 @@ export default class NoteToMpPlugin extends Plugin {
}
async onload() {
console.log('Loading NoteToMP');
console.log('Loading NoteToMP (plugin onload start)');
setVersion(this.manifest.version);
uevent('load');
this.app.workspace.onLayoutReady(()=>{
this.loadResource();
// 布局就绪后清理旧视图并自动打开一个新的标准预览(可选
this.cleanupLegacyViews();
// 如果当前没有我们的预览叶子,自动激活一次,改善首次体验
if (this.app.workspace.getLeavesOfType(VIEW_TYPE_NOTE_PREVIEW).length === 0) {
this.activateView();
console.log('[NoteToMpPlugin] workspace.layoutReady at onload =', this.app.workspace.layoutReady);
// 先注册 view 之前,防止旧 snapshot 立即恢复创建大量视图:先临时卸载残留叶子(如果类型匹配
try {
const legacyLeaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_NOTE_PREVIEW);
if (legacyLeaves.length > 0) {
console.log('[NoteToMpPlugin] detach legacy leaves early count=', legacyLeaves.length);
this.app.workspace.detachLeavesOfType(VIEW_TYPE_NOTE_PREVIEW);
}
})
} catch (e) {
console.warn('[NoteToMpPlugin] early detach failed', e);
}
this.app.workspace.onLayoutReady(async () => {
console.log('[NoteToMpPlugin] onLayoutReady callback entered');
console.time('[NoteToMpPlugin] startup:onLayoutReady→loadResource');
try {
await this.loadResource(); // 确保资源完全加载完再继续,避免后续视图初始化反复等待
} catch (e) {
console.error('[NoteToMpPlugin] loadResource 失败', e);
} finally {
console.timeEnd('[NoteToMpPlugin] startup:onLayoutReady→loadResource');
}
// 清理旧视图
this.cleanupLegacyViews();
// 取消自动打开预览视图(用于排查启动卡顿)。用户可通过图标或命令手动打开。
// console.log('[NoteToMpPlugin] 已跳过自动打开预览视图调试模式');
});
this.registerView(
VIEW_TYPE_NOTE_PREVIEW,
(leaf) => new NotePreview(leaf, this)
(leaf) => new PreviewView(leaf, this)
);
this.ribbonIconEl = this.addRibbonIcon('clipboard-paste', '复制到公众号', (evt: MouseEvent) => {
@@ -100,56 +118,66 @@ export default class NoteToMpPlugin extends Plugin {
}
});
// TODO: 重构后需要重新实现批量发布功能
// this.addCommand({
// id: 'note-to-mp-pub',
// name: '发布公众号文章',
// callback: async () => {
// await this.activateView();
// this.getNotePreview()?.postArticle();
// }
// });
// 命令:当前文件发布到微信草稿
this.addCommand({
id: 'note-to-mp-pub',
name: '发布公众号文章',
id: 'note-to-mp-post-current',
name: '发布当前文件到公众号草稿',
callback: async () => {
const file = this.app.workspace.getActiveFile();
if (!file) { new Notice('没有活动文件'); return; }
if (file.extension.toLowerCase() !== 'md') { new Notice('只能发布 Markdown 文件'); return; }
await this.activateView();
this.getNotePreview()?.postArticle();
await this.getNotePreview()?.postWechatDraft(file);
}
});
// 监听右键菜单
this.registerEvent(
this.app.workspace.on('file-menu', (menu, file) => {
// 发布到微信公众号
menu.addItem((item) => {
item
.setTitle('发布公众号')
.setIcon('lucide-send')
.onClick(async () => {
if (file instanceof TFile) {
if (file.extension.toLowerCase() !== 'md') {
new Notice('只能发布 Markdown 文件');
return;
}
await this.activateView();
await this.getNotePreview()?.renderMarkdown(file);
await this.getNotePreview()?.postArticle();
} else if (file instanceof TFolder) {
await this.activateView();
await this.getNotePreview()?.batchPost(file);
}
});
});
// 发布到小红书(新增)
menu.addItem((item) => {
item
.setTitle('发布到小红书')
.setIcon('lucide-heart')
.onClick(async () => {
if (file instanceof TFile) {
if (file.extension.toLowerCase() !== 'md') {
new Notice('只能发布 Markdown 文件');
return;
}
await this.publishToXiaohongshu(file);
}
});
});
})
);
// 监听右键菜单(文件浏览器)
this.registerEvent(
this.app.workspace.on('file-menu', (menu, file) => {
// 发布到公众号草稿
menu.addItem((item) => {
item
.setTitle('发布公众号')
.setIcon('lucide-send')
.onClick(async () => {
if (file instanceof TFile) {
if (file.extension.toLowerCase() !== 'md') {
new Notice('只能发布 Markdown 文件');
return;
}
await this.activateView();
await this.getNotePreview()?.postWechatDraft(file);
}
});
});
// 发布到小红书
menu.addItem((item) => {
item
.setTitle('发布到小红书')
.setIcon('lucide-heart')
.onClick(async () => {
if (file instanceof TFile) {
if (file.extension.toLowerCase() !== 'md') {
new Notice('只能发布 Markdown 文件');
return;
}
await this.publishToXiaohongshu(file);
}
});
});
})
);
}
onunload() {
@@ -208,11 +236,11 @@ export default class NoteToMpPlugin extends Plugin {
if (leaf) workspace.revealLeaf(leaf);
}
getNotePreview(): NotePreview | null {
getNotePreview(): PreviewView | null {
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_NOTE_PREVIEW);
if (leaves.length > 0) {
const leaf = leaves[0];
return leaf.view as NotePreview;
return leaf.view as PreviewView;
}
return null;
}

163
src/platform-chooser.ts Normal file
View File

@@ -0,0 +1,163 @@
/**
* 文件platform-chooser.ts
* 作用:平台选择器组件,负责渲染平台选择 UI 并处理平台切换事件
*
* 这是一个公共组件,独立于具体平台实现,便于未来扩展新平台
*/
import { Platform as ObsidianPlatform } from 'obsidian';
export type PlatformType = 'wechat' | 'xiaohongshu';
/**
* 平台选择器配置
*/
export interface PlatformChooserOptions {
/** 默认选中的平台 */
defaultPlatform?: PlatformType;
/** 平台切换回调 */
onPlatformChange?: (platform: PlatformType) => Promise<void>;
}
/**
* 平台信息接口
*/
interface PlatformInfo {
value: PlatformType;
label: string;
icon?: string;
}
/**
* 支持的平台列表
*/
const SUPPORTED_PLATFORMS: PlatformInfo[] = [
{ value: 'wechat', label: '微信公众号', icon: '📱' },
{ value: 'xiaohongshu', label: '小红书', icon: '📔' }
];
/**
* 平台选择器类
*
* 职责:
* 1. 渲染平台选择 UI
* 2. 处理用户的平台切换操作
* 3. 触发平台切换回调
* 4. 维护当前选中的平台状态
*/
export class PlatformChooser {
private container: HTMLElement;
private selectElement: HTMLSelectElement | null = null;
private currentPlatform: PlatformType;
private onChange?: (platform: PlatformType) => void;
constructor(container: HTMLElement, options: PlatformChooserOptions = {}) {
this.container = container;
this.currentPlatform = options.defaultPlatform || 'wechat';
if (options.onPlatformChange) {
this.onChange = (platform) => {
options.onPlatformChange!(platform);
};
}
}
/**
* 设置平台切换回调
*/
setOnChange(callback: (platform: PlatformType) => void): void {
this.onChange = callback;
}
/**
* 渲染平台选择器 UI
*/
render(): void {
// 创建平台选择行
const lineDiv = this.container.createDiv({ cls: 'toolbar-line platform-selector-line' });
// 创建标签
const platformLabel = lineDiv.createDiv({ cls: 'style-label' });
platformLabel.innerText = '发布平台';
// 创建选择器
const platformSelect = lineDiv.createEl('select', { cls: 'platform-select' });
this.selectElement = platformSelect;
// 添加平台选项
SUPPORTED_PLATFORMS.forEach(platform => {
const option = platformSelect.createEl('option');
option.value = platform.value;
option.text = platform.icon ? `${platform.icon} ${platform.label}` : platform.label;
// 设置默认选中
if (platform.value === this.currentPlatform) {
option.selected = true;
}
});
// 绑定切换事件
platformSelect.onchange = () => {
const newPlatform = platformSelect.value as PlatformType;
this.switchPlatformInternal(newPlatform);
};
}
/**
* 切换平台(内部方法)
*/
private switchPlatformInternal(platform: PlatformType): void {
if (platform === this.currentPlatform) {
return; // 相同平台,不需要切换
}
console.log(`[PlatformChooser] 切换平台: ${this.currentPlatform} -> ${platform}`);
this.currentPlatform = platform;
// 触发平台切换回调
if (this.onChange) {
try {
this.onChange(platform);
} catch (error) {
console.error('[PlatformChooser] 平台切换失败:', error);
}
}
}
/**
* 切换平台(公共方法,供 PreviewManager 调用)
*/
switchPlatform(platform: PlatformType): void {
this.currentPlatform = platform;
if (this.selectElement) {
this.selectElement.value = platform;
}
}
/**
* 获取当前选中的平台
*/
getCurrentPlatform(): PlatformType {
return this.currentPlatform;
}
/**
* 程序化设置平台(不触发回调)
*/
setPlatform(platform: PlatformType): void {
this.currentPlatform = platform;
if (this.selectElement) {
this.selectElement.value = platform;
}
}
/**
* 清理资源
*/
destroy(): void {
if (this.selectElement) {
this.selectElement.onchange = null;
this.selectElement = null;
}
}
}

378
src/preview-manager.ts Normal file
View File

@@ -0,0 +1,378 @@
/**
* 文件preview-manager.ts
* 作用:预览管理器,负责协调所有平台预览组件
*
* 职责:
* 1. 创建和管理所有子组件platform-chooser, wechat-preview, xhs-preview
* 2. 处理平台切换逻辑(唯一入口)
* 3. 管理文章渲染和状态同步
* 4. 提供统一的对外接口
*
* 设计模式:
* - 中介者模式Mediator: 协调各组件交互
* - 外观模式Facade: 提供简单的对外接口
*/
import { TFile, Notice, App } from 'obsidian';
import { PlatformChooser, PlatformType } from './platform-chooser';
import { WechatPreview } from './wechat/wechat-preview';
import { XiaohongshuPreview } from './xiaohongshu/xhs-preview';
import { ArticleRender } from './article-render';
import { NMPSettings } from './settings';
export class PreviewManager {
private container: HTMLElement;
private app: App;
private render: ArticleRender;
private settings: NMPSettings;
// 子组件
private platformChooser: PlatformChooser | null = null;
private wechatPreview: WechatPreview | null = null;
private xhsPreview: XiaohongshuPreview | null = null;
// UI 容器
private mainDiv: HTMLDivElement | null = null;
private wechatContainer: HTMLDivElement | null = null;
private xhsContainer: HTMLDivElement | null = null;
// 状态
private currentPlatform: PlatformType = 'wechat';
private currentFile: TFile | null = null;
constructor(container: HTMLElement, app: App, render: ArticleRender) {
this.container = container;
this.app = app;
this.render = render;
this.settings = NMPSettings.getInstance();
}
/**
* 构建界面(主入口)
*/
async build(): Promise<void> {
console.log('[PreviewManager] 开始构建界面');
// 清空容器
this.container.empty();
// 创建主容器
this.mainDiv = this.container.createDiv({ cls: 'note-preview' });
// 1. 创建并构建平台选择器
this.createPlatformChooser();
// 2. 创建并构建微信预览
this.createWechatPreview();
// 3. 创建并构建小红书预览
this.createXiaohongshuPreview();
// 4. 初始显示微信平台
await this.switchPlatform('wechat');
console.log('[PreviewManager] 界面构建完成');
}
/**
* 创建平台选择器
*/
private createPlatformChooser(): void {
if (!this.mainDiv) return;
// 创建平台选择器容器
const chooserContainer = this.mainDiv.createDiv({ cls: 'platform-chooser-container' });
// 创建平台选择器实例
this.platformChooser = new PlatformChooser(chooserContainer);
// 设置平台切换回调
this.platformChooser.setOnChange((platform) => {
this.switchPlatform(platform as PlatformType);
});
// 构建 UI
this.platformChooser.render();
}
/**
* 创建微信预览组件
*/
private createWechatPreview(): void {
if (!this.mainDiv) return;
// 创建微信预览容器
this.wechatContainer = this.mainDiv.createDiv({ cls: 'wechat-preview-container' });
// 创建微信预览实例
this.wechatPreview = new WechatPreview(
this.wechatContainer,
this.app,
this.render
);
// 设置回调函数
this.wechatPreview.onRefreshCallback = async () => {
await this.refresh();
};
this.wechatPreview.onAppIdChangeCallback = (appId: string) => {
console.log(`[PreviewManager] 公众号切换: ${appId}`);
// 可以在这里处理公众号切换的额外逻辑
};
// 构建 UI
this.wechatPreview.build();
}
/**
* 创建小红书预览组件
*/
private createXiaohongshuPreview(): void {
if (!this.mainDiv) return;
// 创建小红书预览容器
this.xhsContainer = this.mainDiv.createDiv({ cls: 'xiaohongshu-preview-container' });
// 创建小红书预览实例
this.xhsPreview = new XiaohongshuPreview(this.xhsContainer, this.app);
// 设置回调函数
this.xhsPreview.onRefreshCallback = async () => {
await this.refresh();
};
this.xhsPreview.onPublishCallback = async () => {
await this.publishToXiaohongshu();
};
this.xhsPreview.onPlatformChangeCallback = async (platform: string) => {
if (platform === 'wechat') {
await this.switchPlatform('wechat');
}
};
// 构建 UI
this.xhsPreview.build();
}
/**
* 平台切换的唯一入口
*/
// 平台切换:公开以便外部(例如上下文菜单)调用
async switchPlatform(platform: PlatformType): Promise<void> {
console.log(`[PreviewManager] 平台切换: ${this.currentPlatform}${platform}`);
const previousPlatform = this.currentPlatform;
this.currentPlatform = platform;
// 更新平台选择器显示
if (this.platformChooser) {
this.platformChooser.switchPlatform(platform);
}
if (platform === 'wechat') {
// 显示微信,隐藏小红书
this.showWechat();
this.hideXiaohongshu();
// 如果有当前文件且是从其他平台切换过来,重新渲染
if (this.currentFile && previousPlatform !== 'wechat') {
await this.renderForWechat(this.currentFile);
}
} else if (platform === 'xiaohongshu') {
// 显示小红书,隐藏微信
this.showXiaohongshu();
this.hideWechat();
// 如果有当前文件且是从其他平台切换过来,重新渲染
if (this.currentFile && previousPlatform !== 'xiaohongshu') {
await this.renderForXiaohongshu(this.currentFile);
}
}
}
/**
* 显示微信预览
*/
private showWechat(): void {
if (this.wechatContainer) {
this.wechatContainer.style.display = 'flex';
}
if (this.wechatPreview) {
this.wechatPreview.show();
}
}
/**
* 隐藏微信预览
*/
private hideWechat(): void {
if (this.wechatContainer) {
this.wechatContainer.style.display = 'none';
}
if (this.wechatPreview) {
this.wechatPreview.hide();
}
}
/**
* 显示小红书预览
*/
private showXiaohongshu(): void {
if (this.xhsContainer) {
this.xhsContainer.style.display = 'flex';
}
if (this.xhsPreview) {
this.xhsPreview.show();
}
}
/**
* 隐藏小红书预览
*/
private hideXiaohongshu(): void {
if (this.xhsContainer) {
this.xhsContainer.style.display = 'none';
}
if (this.xhsPreview) {
this.xhsPreview.hide();
}
}
/**
* 设置当前文件(对外接口)
*/
async setFile(file: TFile | null): Promise<void> {
if (!file) {
this.currentFile = null;
this.wechatPreview?.setFile(null);
return;
}
// 只处理 Markdown 文件
if (file.extension.toLowerCase() !== 'md') {
return;
}
console.log(`[PreviewManager] 设置文件: ${file.path}`);
this.currentFile = file;
this.wechatPreview?.setFile(file);
// 根据当前平台渲染
if (this.currentPlatform === 'wechat') {
await this.renderForWechat(file);
} else if (this.currentPlatform === 'xiaohongshu') {
await this.renderForXiaohongshu(file);
}
}
/**
* 刷新预览(对外接口)
*/
async refresh(): Promise<void> {
if (!this.currentFile) {
new Notice('请先打开一个笔记文件');
return;
}
console.log(`[PreviewManager] 刷新预览: ${this.currentFile.path}`);
await this.setFile(this.currentFile);
}
/**
* 渲染微信预览
*/
private async renderForWechat(file: TFile): Promise<void> {
try {
console.log(`[PreviewManager] 渲染微信预览: ${file.path}`);
// 使用 ArticleRender 渲染 Markdown
await this.render.renderMarkdown(file);
// 确保预览持有当前文件引用
this.wechatPreview?.setFile(file);
// 微信预览已经通过 ArticleRender 更新了
// 这里可以添加额外的微信特定逻辑
console.log('[PreviewManager] 微信预览渲染完成');
} catch (error) {
console.error('[PreviewManager] 渲染微信预览失败:', error);
new Notice('渲染失败: ' + (error instanceof Error ? error.message : String(error)));
}
}
/**
* 渲染小红书预览
*/
private async renderForXiaohongshu(file: TFile): Promise<void> {
try {
console.log(`[PreviewManager] 渲染小红书预览: ${file.path}`);
// 使用 ArticleRender 渲染 Markdown
await this.render.renderMarkdown(file);
const articleHTML = this.render.articleHTML;
if (articleHTML && this.xhsPreview) {
// 渲染到小红书预览
await this.xhsPreview.renderArticle(articleHTML, file);
console.log('[PreviewManager] 小红书预览渲染完成');
} else {
console.warn('[PreviewManager] 没有可渲染的内容');
}
} catch (error) {
console.error('[PreviewManager] 渲染小红书预览失败:', error);
new Notice('渲染失败: ' + (error instanceof Error ? error.message : String(error)));
}
}
/**
* 发布到小红书
*/
private async publishToXiaohongshu(): Promise<void> {
console.log('[PreviewManager] 发布到小红书');
// 这里实现发布逻辑
// 可以调用 xhsPreview 的相关方法
new Notice('发布功能开发中...');
}
/**
* 获取当前平台
*/
getCurrentPlatform(): PlatformType {
return this.currentPlatform;
}
/**
* 获取当前文件
*/
getCurrentFile(): TFile | null {
return this.currentFile;
}
/**
* 清理资源
*/
destroy(): void {
console.log('[PreviewManager] 清理资源');
if (this.wechatPreview) {
this.wechatPreview.destroy();
this.wechatPreview = null;
}
if (this.xhsPreview) {
this.xhsPreview.destroy();
this.xhsPreview = null;
}
this.platformChooser = null;
this.mainDiv = null;
this.wechatContainer = null;
this.xhsContainer = null;
this.currentFile = null;
}
/** 获取微信预览实例(发布操作需要) */
getWechatPreview(): WechatPreview | null { return this.wechatPreview; }
}

View File

@@ -15,12 +15,16 @@ import { MarkedParser } from './markdown/parser';
import { LocalImageManager, LocalFile } from './markdown/local-file';
import { CardDataManager } from './markdown/code';
import { ArticleRender } from './article-render';
// 平台选择组件
import { PlatformChooser, PlatformType } from './platform-chooser';
// 微信公众号功能模块
import { WechatPreview } from './wechat/wechat-preview';
// 小红书功能模块
import { XiaohongshuContentAdapter } from './xiaohongshu/adapter';
import { XiaohongshuImageManager } from './xiaohongshu/image';
import { XiaohongshuAPIManager } from './xiaohongshu/api';
import { XiaohongshuPost } from './xiaohongshu/types';
import { XiaohongshuPreviewView } from './xiaohongshu/preview-view';
import { XiaohongshuPreview } from './xiaohongshu/xhs-preview';
// 切图功能
import { sliceArticleImage } from './slice-image';
@@ -57,7 +61,9 @@ export class NotePreview extends ItemView {
markedParser: MarkedParser;
cachedElements: Map<string, string> = new Map();
_articleRender: ArticleRender | null = null;
_xiaohongshuPreview: XiaohongshuPreviewView | null = null;
_xiaohongshuPreview: XiaohongshuPreview | null = null;
_wechatPreview: WechatPreview | null = null;
_platformChooser: PlatformChooser | null = null;
isCancelUpload: boolean = false;
isBatchRuning: boolean = false;
@@ -549,7 +555,7 @@ export class NotePreview extends ItemView {
// 创建或显示小红书预览视图
if (!this._xiaohongshuPreview) {
const xhsContainer = this.mainDiv.createDiv({ cls: 'xiaohongshu-preview-container' });
this._xiaohongshuPreview = new XiaohongshuPreviewView(xhsContainer, this.app);
this._xiaohongshuPreview = new XiaohongshuPreview(xhsContainer, this.app);
// 设置回调函数
this._xiaohongshuPreview.onRefreshCallback = async () => {

349
src/preview-view.ts Normal file
View File

@@ -0,0 +1,349 @@
/**
* 文件preview-view.ts
* 作用Obsidian 视图容器,负责与 Obsidian 框架交互
*
* 职责:
* 1. 实现 ItemView 接口,集成 Obsidian 视图系统
* 2. 管理视图生命周期onOpen/onClose
* 3. 监听文件变化事件
* 4. 将实际业务逻辑委托给 PreviewManager
*
* 设计原则:
* - 极简化:只保留 Obsidian 视图必需的代码
* - 单一职责:只负责视图层包装
* - 委托模式:所有业务逻辑委托给 PreviewManager
*/
import { EventRef, ItemView, WorkspaceLeaf, Plugin, TFile, Notice } from 'obsidian';
import { PreviewManager } from './preview-manager';
import { ArticleRender } from './article-render';
import { NMPSettings } from './settings';
import AssetsManager from './assets';
import { waitForLayoutReady, uevent } from './utils';
import { LocalFile } from './markdown/local-file';
export const VIEW_TYPE_NOTE_PREVIEW = 'note-preview';
/**
* 笔记预览视图类
*
* 这是一个简化的视图容器,实际的预览逻辑由 PreviewManager 处理
*/
export class PreviewView extends ItemView {
private plugin: Plugin;
private manager: PreviewManager | null = null;
private settings: NMPSettings;
private assetsManager: AssetsManager;
private listeners: EventRef[] = [];
// ArticleRender 相关
private styleEl: HTMLElement | null = null;
private articleDiv: HTMLDivElement | null = null;
private _articleRender: ArticleRender | null = null;
constructor(leaf: WorkspaceLeaf, plugin: Plugin) {
super(leaf);
this.plugin = plugin;
this.settings = NMPSettings.getInstance();
this.assetsManager = AssetsManager.getInstance();
}
/**
* 获取视图类型
*/
getViewType(): string {
return VIEW_TYPE_NOTE_PREVIEW;
}
/**
* 获取显示名称
*/
getDisplayText(): string {
return '笔记预览';
}
/**
* 获取图标
*/
getIcon(): string {
return 'book-open';
}
/**
* 获取 ArticleRender 实例
*/
get render(): ArticleRender {
if (!this._articleRender) {
// 创建临时容器用于 ArticleRender
if (!this.styleEl) {
this.styleEl = document.createElement('style');
}
if (!this.articleDiv) {
this.articleDiv = document.createElement('div');
}
this._articleRender = new ArticleRender(
this.app,
this,
this.styleEl,
this.articleDiv
);
// 设置默认主题和高亮
this._articleRender.currentTheme = this.settings.defaultStyle;
this._articleRender.currentHighlight = this.settings.defaultHighlight;
}
return this._articleRender;
}
/**
* 视图打开时的回调
*/
async onOpen(): Promise<void> {
console.log('[PreviewView] 视图打开 layoutReady=', this.app.workspace.layoutReady);
// 不在未完成 layoutReady 时做重初始化,改为延迟
if (!this.app.workspace.layoutReady) {
this.showLoading();
console.log('[PreviewView] defer initialization until layoutReady');
this.app.workspace.onLayoutReady(() => {
// 使用微任务再推进,确保其它插件也完成
setTimeout(() => this.performInitialization(), 0);
});
return;
}
await this.performInitialization();
}
private async performInitialization(): Promise<void> {
try {
const start = performance.now();
this.showLoading();
console.time('[PreviewView] initializeSettings');
await this.initializeSettings();
console.timeEnd('[PreviewView] initializeSettings');
console.time('[PreviewView] createManager');
await this.createManager();
console.timeEnd('[PreviewView] createManager');
console.time('[PreviewView] registerEventListeners');
this.registerEventListeners();
console.timeEnd('[PreviewView] registerEventListeners');
// 初始不渲染正文,等用户真正激活 / 文件切换时再渲染(懒加载)
const activeFile = this.app.workspace.getActiveFile();
if (activeFile) {
// 轻量延迟,避免首屏阻塞
setTimeout(() => {
if (this.manager) {
this.manager.setFile(activeFile);
}
}, 200);
}
console.log('[PreviewView] 初始化耗时(ms):', (performance.now() - start).toFixed(1));
uevent('open');
} catch (error) {
console.error('[PreviewView] 初始化失败:', error);
new Notice('预览视图初始化失败: ' + (error instanceof Error ? error.message : String(error)));
const container = this.containerEl.children[1] as HTMLElement;
container.empty();
const errorDiv = container.createDiv({ cls: 'preview-error' });
errorDiv.createEl('h3', { text: '预览视图初始化失败' });
errorDiv.createEl('p', { text: error instanceof Error ? error.message : String(error) });
errorDiv.createEl('p', { text: '请尝试重新加载插件或查看控制台获取更多信息' });
}
}
/**
* 视图关闭时的回调
*/
async onClose(): Promise<void> {
console.log('[PreviewView] 视图关闭');
// 清理事件监听
this.listeners.forEach(listener => {
this.app.workspace.offref(listener);
});
this.listeners = [];
// 清理管理器
if (this.manager) {
this.manager.destroy();
this.manager = null;
}
// 清理缓存
LocalFile.fileCache.clear();
uevent('close');
}
/**
* 显示加载动画
*/
private showLoading(): void {
const container = this.containerEl.children[1];
container.empty();
const loading = container.createDiv({ cls: 'loading-wrapper' });
loading.createDiv({ cls: 'loading-spinner' });
}
/**
* 初始化设置和资源
*/
private async initializeSettings(): Promise<void> {
console.log('[PreviewView]initSettings:start');
const t0 = performance.now();
try {
// 等待布局就绪
console.log('[PreviewView]initSettings:waitForLayoutReady');
await waitForLayoutReady(this.app);
console.log('[PreviewView]initSettings:layoutReady');
// 加载设置
if (!this.settings.isLoaded) {
console.log('[PreviewView]initSettings:loadData:start');
const data = await this.plugin.loadData();
NMPSettings.loadSettings(data);
console.log('[PreviewView]initSettings:loadData:done');
} else {
console.log('[PreviewView]initSettings:settingsAlreadyLoaded');
}
// 加载资源(加超时降级)
if (!this.assetsManager.isLoaded) {
console.log('[PreviewView]initSettings:assets:load:start');
const assetPromise = this.assetsManager.loadAssets();
const timeoutMs = 8000; // 8 秒防护
let timedOut = false;
let timer: number | null = null;
const timeout = new Promise<void>((resolve) => {
timer = window.setTimeout(() => {
timedOut = true;
console.warn('[PreviewView]initSettings:assets:timeout, fallback to minimal defaults');
resolve();
}, timeoutMs);
});
await Promise.race([assetPromise.then(()=>{ /* 成功加载 */ }), timeout]);
if (!timedOut && timer !== null) {
clearTimeout(timer);
}
console.log('[PreviewView]initSettings:assets:load:end', { timedOut });
} else {
console.log('[PreviewView]initSettings:assetsAlreadyLoaded');
}
} catch (e) {
console.error('[PreviewView]initSettings:error', e);
throw e;
} finally {
console.log('[PreviewView]initSettings:done in', (performance.now() - t0).toFixed(1), 'ms');
}
}
/**
* 创建预览管理器
*/
private async createManager(): Promise<void> {
// 获取容器
const container = this.containerEl.children[1] as HTMLElement;
container.empty();
// 创建预览管理器
this.manager = new PreviewManager(
container,
this.app,
this.render
);
// 构建界面
await this.manager.build();
}
/**
* 注册事件监听
*/
private registerEventListeners(): void {
// 监听文件切换
this.listeners.push(
this.app.workspace.on('file-open', async (file: TFile | null) => {
await this.handleFileOpen(file);
})
);
// 监听文件修改
this.listeners.push(
this.app.vault.on('modify', async (file) => {
if (file instanceof TFile) {
await this.handleFileModify(file);
}
})
);
}
/**
* 处理文件打开事件
*/
private async handleFileOpen(file: TFile | null): Promise<void> {
if (this.manager) {
await this.manager.setFile(file);
}
}
/**
* 处理文件修改事件
*/
private async handleFileModify(file: TFile): Promise<void> {
if (!this.manager) return;
const currentFile = this.manager.getCurrentFile();
if (currentFile && currentFile.path === file.path) {
// 当前文件被修改,刷新预览
await this.manager.refresh();
}
}
/**
* 渲染当前打开的文件
*/
private async renderCurrentFile(): Promise<void> {
const activeFile = this.app.workspace.getActiveFile();
if (activeFile && this.manager) {
await this.manager.setFile(activeFile);
}
}
/**
* 对外接口:设置要预览的文件
*/
async setFile(file: TFile): Promise<void> {
if (this.manager) {
await this.manager.setFile(file);
}
}
/**
* 对外接口:刷新预览
*/
async refresh(): Promise<void> {
if (this.manager) {
await this.manager.refresh();
}
}
/** 外部接口:切换平台 */
async changePlatform(platform: 'wechat' | 'xiaohongshu') {
await this.manager?.switchPlatform(platform as any);
}
/** 外部接口:设置当前文件并发布到微信草稿 */
async postWechatDraft(file: TFile) {
await this.setFile(file);
await this.changePlatform('wechat');
const wechat = this.manager?.getWechatPreview();
if (!wechat) throw new Error('微信预览未初始化');
await wechat.postDraft();
}
getManager() { return this.manager; }
}

View File

@@ -225,10 +225,26 @@ export function cleanUrl(href: string) {
}
export async function waitForLayoutReady(app: App): Promise<void> {
if (app.workspace.layoutReady) {
return;
}
return new Promise((resolve) => {
app.workspace.onLayoutReady(() => resolve());
});
if (app.workspace.layoutReady) {
console.log('[waitForLayoutReady] already ready');
return;
}
console.log('[waitForLayoutReady] waiting...');
return new Promise((resolve) => {
let resolved = false;
const timer = setTimeout(() => {
if (!resolved) {
console.warn('[waitForLayoutReady] timeout fallback (5s)');
resolved = true; resolve();
}
}, 5000);
app.workspace.onLayoutReady(() => {
if (!resolved) {
resolved = true;
clearTimeout(timer);
console.log('[waitForLayoutReady] event fired');
resolve();
}
});
});
}

View File

@@ -0,0 +1,423 @@
/**
* 文件wechat/wechat-preview.ts
* 作用:微信公众号预览视图组件,专门处理微信公众号平台的预览和发布功能
*
* 功能:
* 1. 渲染微信公众号专属的工具栏和预览界面
* 2. 处理文章的复制、上传图片、发布草稿等操作
* 3. 管理微信公众号相关的设置(公众号选择、封面、样式等)
* 4. 提供文章导出HTML功能
*/
import { Notice, Platform, TFile, TFolder } from 'obsidian';
import { NMPSettings } from '../settings';
import AssetsManager from '../assets';
import { ArticleRender } from '../article-render';
import { uevent } from '../utils';
/**
* 微信公众号预览视图类
*/
export class WechatPreview {
container: HTMLElement;
settings: NMPSettings;
assetsManager: AssetsManager;
render: ArticleRender;
app: any;
// 当前状态
currentFile: TFile | null = null;
currentAppId: string = '';
currentTheme: string;
currentHighlight: string;
// UI 元素
toolbar: HTMLDivElement | null = null;
renderDiv: HTMLDivElement | null = null;
wechatSelect: HTMLSelectElement | null = null;
themeSelect: HTMLSelectElement | null = null;
highlightSelect: HTMLSelectElement | null = null;
coverEl: HTMLInputElement | null = null;
useDefaultCover: HTMLInputElement | null = null;
useLocalCover: HTMLInputElement | null = null;
// 回调函数
onRefreshCallback?: () => Promise<void>;
onAppIdChangeCallback?: (appId: string) => void;
constructor(container: HTMLElement, app: any, render: ArticleRender) {
this.container = container;
this.app = app;
this.render = render;
this.settings = NMPSettings.getInstance();
this.assetsManager = AssetsManager.getInstance();
this.currentTheme = this.settings.defaultStyle;
this.currentHighlight = this.settings.defaultHighlight;
// 初始化默认公众号
if (this.settings.wxInfo.length > 0) {
this.currentAppId = this.settings.wxInfo[0].appid;
}
}
/**
* 构建微信公众号预览界面
*/
build(): void {
this.container.empty();
// 创建工具栏
this.toolbar = this.container.createDiv({ cls: 'preview-toolbar' });
this.buildToolbar(this.toolbar);
// 创建渲染区域
this.renderDiv = this.container.createDiv({ cls: 'render-div' });
this.renderDiv.id = 'render-div';
// 将 ArticleRender 的 style 与内容节点挂载
try {
if (this.render && this.render.styleEl && !this.renderDiv.contains(this.render.styleEl)) {
this.renderDiv.appendChild(this.render.styleEl);
}
if (this.render && this.render.articleDiv && !this.renderDiv.contains(this.render.articleDiv)) {
// 容器样式:模拟公众号编辑器宽度,更好的排版显示
this.render.articleDiv.addClass('wechat-article-wrapper');
this.renderDiv.appendChild(this.render.articleDiv);
}
} catch (e) {
console.warn('[WechatPreview] 挂载文章容器失败', e);
}
}
/**
* 构建工具栏
*/
private buildToolbar(parent: HTMLDivElement): void {
let lineDiv;
// 公众号选择
if (this.settings.wxInfo.length > 1 || Platform.isDesktop) {
lineDiv = parent.createDiv({ cls: 'toolbar-line' });
const wxLabel = lineDiv.createDiv({ cls: 'style-label' });
wxLabel.innerText = '公众号';
const wxSelect = lineDiv.createEl('select', { cls: 'wechat-select' });
wxSelect.onchange = async () => {
this.currentAppId = wxSelect.value;
this.onAppIdChanged();
};
const defaultOp = wxSelect.createEl('option');
defaultOp.value = '';
defaultOp.text = '请在设置里配置公众号';
for (let i = 0; i < this.settings.wxInfo.length; i++) {
const op = wxSelect.createEl('option');
const wx = this.settings.wxInfo[i];
op.value = wx.appid;
op.text = wx.name;
if (i === 0) {
op.selected = true;
this.currentAppId = wx.appid;
}
}
this.wechatSelect = wxSelect;
if (Platform.isDesktop) {
const separator = lineDiv.createDiv({ cls: 'toolbar-separator' });
const openBtn = lineDiv.createEl('button', { text: '🌐 去公众号后台', cls: 'toolbar-button purple-gradient' });
openBtn.onclick = async () => {
const { shell } = require('electron');
shell.openExternal('https://mp.weixin.qq.com');
uevent('open-mp');
};
}
}
// 操作按钮行
lineDiv = parent.createDiv({ cls: 'toolbar-line flex-wrap' });
const refreshBtn = lineDiv.createEl('button', { text: '🔄 刷新', cls: 'toolbar-button purple-gradient' });
refreshBtn.onclick = async () => {
if (this.onRefreshCallback) {
await this.onRefreshCallback();
}
};
if (Platform.isDesktop) {
const copyBtn = lineDiv.createEl('button', { text: '📋 复制', cls: 'toolbar-button' });
copyBtn.onclick = async () => {
try {
await this.render.copyArticle();
new Notice('复制成功,请到公众号编辑器粘贴。');
uevent('copy');
} catch (error) {
console.error(error);
new Notice('复制失败: ' + error);
}
};
}
const uploadImgBtn = lineDiv.createEl('button', { text: '📤 上传图片', cls: 'toolbar-button' });
uploadImgBtn.onclick = async () => await this.uploadImages();
const postBtn = lineDiv.createEl('button', { text: '📝 发草稿', cls: 'toolbar-button' });
postBtn.onclick = async () => await this.postArticle();
const imagesBtn = lineDiv.createEl('button', { text: '🖼️ 图片/文字', cls: 'toolbar-button' });
imagesBtn.onclick = async () => await this.postImages();
if (Platform.isDesktop && this.settings.isAuthKeyVaild()) {
const htmlBtn = lineDiv.createEl('button', { text: '💾 导出HTML', cls: 'toolbar-button' });
htmlBtn.onclick = async () => await this.exportHTML();
}
// 封面选择
this.buildCoverSelector(parent);
// 样式选择(如果启用)
if (this.settings.showStyleUI) {
this.buildStyleSelector(parent);
}
}
/**
* 构建封面选择器
*/
private buildCoverSelector(parent: HTMLDivElement): void {
const lineDiv = parent.createDiv({ cls: 'toolbar-line' });
const coverTitle = lineDiv.createDiv({ cls: 'style-label' });
coverTitle.innerText = '封面';
this.useDefaultCover = lineDiv.createEl('input', { cls: 'input-style' });
this.useDefaultCover.setAttr('type', 'radio');
this.useDefaultCover.setAttr('name', 'cover');
this.useDefaultCover.setAttr('value', 'default');
this.useDefaultCover.setAttr('checked', true);
this.useDefaultCover.id = 'default-cover';
this.useDefaultCover.onchange = async () => {
if (this.useDefaultCover?.checked && this.coverEl) {
this.coverEl.setAttr('style', 'visibility:hidden;width:0px;');
}
};
const defaultLabel = lineDiv.createEl('label');
defaultLabel.innerText = '默认';
defaultLabel.setAttr('for', 'default-cover');
this.useLocalCover = lineDiv.createEl('input', { cls: 'input-style' });
this.useLocalCover.setAttr('type', 'radio');
this.useLocalCover.setAttr('name', 'cover');
this.useLocalCover.setAttr('value', 'local');
this.useLocalCover.id = 'local-cover';
this.useLocalCover.setAttr('style', 'margin-left:20px;');
this.useLocalCover.onchange = async () => {
if (this.useLocalCover?.checked && this.coverEl) {
this.coverEl.setAttr('style', 'visibility:visible;width:180px;');
}
};
const localLabel = lineDiv.createEl('label');
localLabel.setAttr('for', 'local-cover');
localLabel.innerText = '上传';
this.coverEl = lineDiv.createEl('input', { cls: 'upload-input' });
this.coverEl.setAttr('type', 'file');
this.coverEl.setAttr('placeholder', '封面图片');
this.coverEl.setAttr('accept', '.png, .jpg, .jpeg');
this.coverEl.setAttr('name', 'cover');
this.coverEl.id = 'cover-input';
}
/**
* 构建样式选择器
*/
private buildStyleSelector(parent: HTMLDivElement): void {
const lineDiv = parent.createDiv({ cls: 'toolbar-line flex-wrap' });
const cssStyle = lineDiv.createDiv({ cls: 'style-label' });
cssStyle.innerText = '样式';
const selectBtn = lineDiv.createEl('select', { cls: 'style-select' });
selectBtn.onchange = async () => {
this.currentTheme = selectBtn.value;
this.render.updateStyle(selectBtn.value);
};
for (let s of this.assetsManager.themes) {
const op = selectBtn.createEl('option');
op.value = s.className;
op.text = s.name;
op.selected = s.className === this.settings.defaultStyle;
}
this.themeSelect = selectBtn;
const separator = lineDiv.createDiv({ cls: 'toolbar-separator' });
const highlightStyle = lineDiv.createDiv({ cls: 'style-label' });
highlightStyle.innerText = '代码高亮';
const highlightStyleBtn = lineDiv.createEl('select', { cls: 'style-select' });
highlightStyleBtn.onchange = async () => {
this.currentHighlight = highlightStyleBtn.value;
this.render.updateHighLight(highlightStyleBtn.value);
};
const highlights = this.assetsManager.highlights;
for (let h of highlights) {
const op = highlightStyleBtn.createEl('option');
op.value = h.url;
op.text = h.name;
op.selected = h.url === this.currentHighlight;
}
this.highlightSelect = highlightStyleBtn;
}
/**
* 显示微信预览视图
*/
show(): void {
if (this.container) {
this.container.style.display = 'flex';
}
}
/**
* 隐藏微信预览视图
*/
hide(): void {
if (this.container) {
this.container.style.display = 'none';
}
}
/**
* 公众号切换处理
*/
private onAppIdChanged(): void {
if (this.onAppIdChangeCallback) {
this.onAppIdChangeCallback(this.currentAppId);
}
}
/**
* 上传图片
*/
private async uploadImages(): Promise<void> {
// 待实现 - 从原来的 note-preview.ts 迁移
new Notice('上传图片功能');
uevent('upload');
}
/**
* 发布草稿
*/
private async postArticle(): Promise<void> {
try {
if (!this.currentFile) {
new Notice('请先打开一个 Markdown 文件');
return;
}
if (!this.currentAppId) {
new Notice('请先在设置中配置公众号信息');
return;
}
new Notice('正在创建公众号草稿...');
const mediaId = await this.render.postArticle(this.currentAppId, this.getLocalCoverFile());
if (mediaId) {
new Notice('草稿创建成功');
}
uevent('pub');
} catch (e) {
console.error(e);
new Notice('发布失败: ' + (e instanceof Error ? e.message : e));
}
}
/**
* 发布图片/文字
*/
private async postImages(): Promise<void> {
try {
if (!this.currentFile) {
new Notice('请先打开一个 Markdown 文件');
return;
}
if (!this.currentAppId) {
new Notice('请先在设置中配置公众号信息');
return;
}
new Notice('正在创建图片/文字消息草稿...');
const mediaId = await this.render.postImages(this.currentAppId);
if (mediaId) {
new Notice('图片/文字草稿创建成功');
}
uevent('pub-images');
} catch (e) {
console.error(e);
new Notice('发布失败: ' + (e instanceof Error ? e.message : e));
}
}
/**
* 导出HTML
*/
private async exportHTML(): Promise<void> {
try {
if (!this.currentFile) {
new Notice('请先打开一个 Markdown 文件');
return;
}
await this.render.exportHTML();
new Notice('HTML 导出完成');
uevent('export-html');
} catch (e) {
console.error(e);
new Notice('导出失败: ' + (e instanceof Error ? e.message : e));
}
}
/**
* 更新样式和高亮显示
*/
updateStyleAndHighlight(theme: string, highlight: string): void {
this.currentTheme = theme;
this.currentHighlight = highlight;
if (this.themeSelect) {
this.themeSelect.value = theme;
}
if (this.highlightSelect) {
this.highlightSelect.value = highlight;
}
}
/**
* 清理资源
*/
destroy(): void {
this.toolbar = null;
this.renderDiv = null;
this.wechatSelect = null;
this.themeSelect = null;
this.highlightSelect = null;
this.coverEl = null;
this.useDefaultCover = null;
this.useLocalCover = null;
}
/** 获取本地上传封面(如果选择了“上传”单选并选了文件) */
private getLocalCoverFile(): File | null {
if (this.useLocalCover?.checked && this.coverEl?.files && this.coverEl.files.length > 0) {
return this.coverEl.files[0];
}
return null;
}
/** 对外:发布草稿(供外层菜单调用) */
async postDraft() { await this.postArticle(); }
/** 由上层在切换/渲染时注入当前文件 */
setFile(file: TFile | null) { this.currentFile = file; }
}

View File

@@ -1,4 +1,13 @@
/* 文件xiaohongshu/preview-view.ts — 小红书预览视图组件:顶部工具栏、分页导航、底部切图按钮。 */
/**
* xiaohongshu/xhs-preview.ts
*
*
*
* 1.
* 2.
* 3. /
* 4.
*/
import { Notice, TFile } from 'obsidian';
import { NMPSettings } from '../settings';
@@ -7,9 +16,9 @@ import { paginateArticle, renderPage, PageInfo } from './paginator';
import { sliceCurrentPage, sliceAllPages } from './slice';
/**
*
*
*/
export class XiaohongshuPreviewView {
export class XiaohongshuPreview {
container: HTMLElement;
settings: NMPSettings;
assetsManager: AssetsManager;
@@ -17,16 +26,16 @@ export class XiaohongshuPreviewView {
currentFile: TFile | null = null;
// UI 元素
topToolbar: HTMLDivElement;
templateSelect: HTMLSelectElement;
themeSelect: HTMLSelectElement;
fontSelect: HTMLSelectElement;
fontSizeDisplay: HTMLSpanElement;
topToolbar!: HTMLDivElement;
templateSelect!: HTMLSelectElement;
themeSelect!: HTMLSelectElement;
fontSelect!: HTMLSelectElement;
fontSizeDisplay!: HTMLSpanElement;
pageContainer: HTMLDivElement;
bottomToolbar: HTMLDivElement;
pageNavigation: HTMLDivElement;
pageNumberDisplay: HTMLSpanElement;
pageContainer!: HTMLDivElement;
bottomToolbar!: HTMLDivElement;
pageNavigation!: HTMLDivElement;
pageNumberDisplay!: HTMLSpanElement;
// 分页数据
pages: PageInfo[] = [];
@@ -289,7 +298,7 @@ export class XiaohongshuPreviewView {
new Notice('✅ 当前页切图完成');
} catch (error) {
console.error('切图失败:', error);
new Notice('❌ 切图失败: ' + error.message);
new Notice('❌ 切图失败: ' + (error instanceof Error ? error.message : String(error)));
}
}
@@ -342,7 +351,42 @@ export class XiaohongshuPreviewView {
new Notice(`✅ 全部页切图完成:共 ${this.pages.length}`);
} catch (error) {
console.error('批量切图失败:', error);
new Notice('❌ 批量切图失败: ' + error.message);
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;
}
}