update at 2025-10-08 19:45:28

This commit is contained in:
douboer
2025-10-08 19:45:28 +08:00
parent 5d32c0f5e7
commit 3460669602
20 changed files with 3325 additions and 101 deletions

View File

@@ -0,0 +1,423 @@
/**
* 文件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; }
}