/** * 文件:wechat/wechat-preview.ts * 作用:微信公众号预览视图组件,专门处理微信公众号平台的预览和发布功能 * * 功能: * 1. 渲染微信公众号专属的工具栏和预览界面 * 2. 处理文章的复制、上传图片、发布草稿等操作 * 3. 管理微信公众号相关的设置(公众号选择、封面、样式等) * 4. 提供文章导出HTML功能 */ import { Notice, Platform, TFile } 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 元素 board: HTMLDivElement | null = null; contentCell: HTMLElement | null = null; contentEl: HTMLElement | 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.container.addClass('wechat-preview-container'); this.board = this.container.createDiv({ cls: 'wechat-board' }); this.buildAccountRow(); //this.buildCoverRow(); //this.buildStyleRow(); this.contentCell = this.createCell('content'); this.contentCell.addClass('wechat-cell-content'); this.mountArticle(this.board); } private createCell(area: string, tag: keyof HTMLElementTagNameMap = 'div', extraClasses: string[] = []): HTMLElement { if (!this.board) { throw new Error('Wechat board not initialized'); } const cell = this.board.createEl(tag, { attr: { 'data-area': area } }); cell.addClass('wechat-cell'); for (const cls of extraClasses) { cell.addClass(cls); } return cell; } private buildAccountRow(): void { const selectCell = this.createCell('account-select'); const selectLabel = selectCell.createEl('label', { cls: 'style-label', attr: { for: 'wechat-account-select' }, text: '公众号' }); selectLabel.addClass('wechat-account-label'); const wxSelect = selectCell.createEl('select', { cls: 'wechat-select', attr: { id: 'wechat-account-select' } }) as HTMLSelectElement; 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; const actionsCell = this.createCell('account-back-export'); if (Platform.isDesktop) { const openBtn = actionsCell.createEl('button', { text: '访问后台', cls: 'toolbar-button purple-gradient wechat-action-button' }); openBtn.onclick = async () => { const { shell } = require('electron'); shell.openExternal('https://mp.weixin.qq.com'); uevent('open-mp'); }; } if (Platform.isDesktop && this.settings.isAuthKeyVaild()) { const exportBtn = actionsCell.createEl('button', { text: '导出页面', cls: 'toolbar-button wechat-action-button' }); exportBtn.onclick = async () => await this.exportHTML(); } if (actionsCell.childElementCount === 0) { actionsCell.addClass('wechat-cell-placeholder'); } } private buildCoverRow(): void { const selectCell = this.createCell('cover-select'); selectCell.createDiv({ cls: 'style-label', text: '封面' }); this.useDefaultCover = selectCell.createEl('input', { cls: 'input-style' }) as HTMLInputElement; 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;'); } }; selectCell.createEl('label', { text: '默认', attr: { for: 'default-cover' } }); this.useLocalCover = selectCell.createEl('input', { cls: 'input-style' }) as HTMLInputElement; this.useLocalCover.setAttr('type', 'radio'); this.useLocalCover.setAttr('name', 'cover'); this.useLocalCover.setAttr('value', 'local'); this.useLocalCover.id = 'local-cover'; this.useLocalCover.onchange = async () => { if (this.useLocalCover?.checked && this.coverEl) { this.coverEl.setAttr('style', 'visibility:visible;width:180px;'); } }; selectCell.createEl('label', { text: '上传', attr: { for: 'local-cover' } }); const inputCell = this.createCell('cover-input'); this.coverEl = inputCell.createEl('input', { cls: 'upload-input', attr: { type: 'file', placeholder: '封面图片', accept: '.png, .jpg, .jpeg', name: 'cover', id: 'cover-input' } }) as HTMLInputElement; if (this.useDefaultCover?.checked) { this.coverEl.setAttr('style', 'visibility:hidden;width:0px;'); } } private buildStyleRow(): void { const styleLabelCell = this.createCell('style-label', 'div', ['style-label']); styleLabelCell.setText('样式'); const styleSelectCell = this.createCell('style-select'); const selectBtn = styleSelectCell.createEl('select', { cls: 'style-select wechat-style-select' }) as HTMLSelectElement; 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 highlightLabelCell = this.createCell('highlight-label', 'div', ['style-label']); highlightLabelCell.setText('代码高亮'); const highlightSelectCell = this.createCell('highlight-select'); const highlightStyleBtn = highlightSelectCell.createEl('select', { cls: 'style-select wechat-style-select' }) as HTMLSelectElement; 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; } private mountArticle(_parent: HTMLElement): void { if (!this.contentCell) { return; } try { if (this.render?.styleEl && !this.contentCell.contains(this.render.styleEl)) { this.contentCell.appendChild(this.render.styleEl); } if (this.render?.articleDiv) { this.render.articleDiv.addClass('wechat-article-wrapper'); if (this.render.articleDiv.parentElement !== this.contentCell) { this.contentCell.appendChild(this.render.articleDiv); } this.contentEl = this.render.articleDiv; } } catch (error) { console.warn('[WechatPreview] 挂载文章容器失败', error); } } /** * 构建封面选择器 */ /** * 显示微信预览视图 */ 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.board = null; this.contentCell = null; this.contentEl = 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(); } async publish(): Promise { await this.postDraft(); } async refresh(): Promise { if (this.onRefreshCallback) { await this.onRefreshCallback(); } } /** 由上层在切换/渲染时注入当前文件 */ setFile(file: TFile | null) { this.currentFile = file; } }