update at 2025-10-08 19:45:28
This commit is contained in:
423
src/wechat/wechat-preview.ts
Normal file
423
src/wechat/wechat-preview.ts
Normal 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; }
|
||||
}
|
||||
Reference in New Issue
Block a user