Files
note2any/src/wechat/wechat-preview.ts
2025-10-10 19:13:38 +08:00

418 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 文件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; }
}