418 lines
14 KiB
TypeScript
418 lines
14 KiB
TypeScript
/**
|
||
* 文件: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<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.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<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.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<void> {
|
||
await this.postDraft();
|
||
}
|
||
|
||
async refresh(): Promise<void> {
|
||
if (this.onRefreshCallback) {
|
||
await this.onRefreshCallback();
|
||
}
|
||
}
|
||
|
||
/** 由上层在切换/渲染时注入当前文件 */
|
||
setFile(file: TFile | null) { this.currentFile = file; }
|
||
}
|