/** * 文件: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; 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 { // 待实现 - 从原来的 note-preview.ts 迁移 new Notice('上传图片功能'); uevent('upload'); } /** * 发布草稿 */ private async postArticle(): Promise { 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 { 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 { 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; } }