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

424 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, 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; }
}