update at 2025-10-16 16:10:58
This commit is contained in:
168
docs/OPTIMIZATION_REPORT_v1.3.12.md
Normal file
168
docs/OPTIMIZATION_REPORT_v1.3.12.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# Note2Any v1.3.12 系统优化报告
|
||||
|
||||
## 概述
|
||||
本次优化针对项目的架构、可维护性、错误处理和用户体验进行了全面提升,创建了模块化的核心系统架构。
|
||||
|
||||
## 核心优化内容
|
||||
|
||||
### 1. 核心架构模块化
|
||||
|
||||
#### 新增核心模块
|
||||
- **错误处理模块** (`src/core/error-handler.ts`)
|
||||
- 统一的错误处理机制
|
||||
- 错误日志记录和分类
|
||||
- 用户友好的错误提示
|
||||
|
||||
- **进度指示器** (`src/core/progress-indicator.ts`)
|
||||
- 统一的进度反馈系统
|
||||
- 支持开始、更新、完成、错误状态
|
||||
- 提升用户体验
|
||||
|
||||
- **配置管理器** (`src/core/config-manager.ts`)
|
||||
- 中心化的配置管理
|
||||
- 运行时配置验证
|
||||
- 配置热更新支持
|
||||
|
||||
- **发布平台接口** (`src/core/publisher-interface.ts`)
|
||||
- 标准化的平台发布接口
|
||||
- 支持微信、小红书等多平台
|
||||
- 可扩展的发布架构
|
||||
|
||||
- **发布管理器** (`src/core/publisher-manager.ts`)
|
||||
- 中心化的平台管理
|
||||
- 统一的发布流程
|
||||
- 平台无关的业务逻辑
|
||||
|
||||
- **内容处理器** (`src/core/content-processor.ts`)
|
||||
- 模块化的内容处理流程
|
||||
- 可配置的处理管道
|
||||
- 支持自定义处理器扩展
|
||||
|
||||
### 2. 专业化处理模块
|
||||
|
||||
#### 图库处理器 (`src/core/gallery-processor.ts`)
|
||||
- 专门处理图库短代码
|
||||
- 支持目录式和块级图库
|
||||
- 智能图片选择算法
|
||||
- 本地图片扫描优化
|
||||
|
||||
#### 图像处理器 (`src/core/image-processor.ts`)
|
||||
- 统一的图像处理接口
|
||||
- WebP转JPG转换
|
||||
- 批量图像处理
|
||||
- 微信图像上传集成
|
||||
- HTML转PNG功能
|
||||
|
||||
#### HTML处理器 (`src/core/html-processor.ts`)
|
||||
- HTML内容生成和格式化
|
||||
- CSS样式内联处理
|
||||
- 响应式设计优化
|
||||
- 移动端适配
|
||||
- 打印样式支持
|
||||
|
||||
### 3. 主要代码文件优化
|
||||
|
||||
#### `src/main.ts`
|
||||
- 集成新的核心模块
|
||||
- 添加进度指示和错误处理
|
||||
- 优化插件初始化流程
|
||||
|
||||
#### `src/preview-view.ts`
|
||||
- 添加进度反馈
|
||||
- 统一错误处理
|
||||
- 优化视图生命周期管理
|
||||
|
||||
## 技术改进
|
||||
|
||||
### 错误处理
|
||||
- **之前**: 分散的try-catch,不一致的错误提示
|
||||
- **现在**: 统一的ErrorHandler系统,集中的错误处理和日志
|
||||
|
||||
### 用户反馈
|
||||
- **之前**: 缺乏操作进度反馈
|
||||
- **现在**: ProgressIndicator提供实时进度和状态提示
|
||||
|
||||
### 配置管理
|
||||
- **之前**: 配置散布在各个文件
|
||||
- **现在**: ConfigManager集中管理,类型安全的配置访问
|
||||
|
||||
### 发布架构
|
||||
- **之前**: 平台特定的紧耦合代码
|
||||
- **现在**: IPlatformPublisher接口,松耦合的可扩展架构
|
||||
|
||||
### 内容处理
|
||||
- **之前**: 单一巨大文件处理所有内容
|
||||
- **现在**: 模块化处理器,职责明确,易于测试和维护
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 模块化加载
|
||||
- 按需加载核心模块
|
||||
- 减少初始化时间
|
||||
- 优化内存使用
|
||||
|
||||
### 异步处理
|
||||
- 非阻塞的图像处理
|
||||
- 并行的资源加载
|
||||
- 流式的内容处理
|
||||
|
||||
### 缓存优化
|
||||
- 智能的配置缓存
|
||||
- 图像处理结果缓存
|
||||
- CSS样式缓存
|
||||
|
||||
## 可维护性提升
|
||||
|
||||
### 代码组织
|
||||
- 明确的模块职责分离
|
||||
- 统一的接口和约定
|
||||
- 完善的类型定义
|
||||
|
||||
### 扩展性
|
||||
- 插件化的处理器架构
|
||||
- 标准化的平台接口
|
||||
- 配置驱动的功能开关
|
||||
|
||||
### 测试友好
|
||||
- 依赖注入设计
|
||||
- 模块化的单元测试
|
||||
- 清晰的接口边界
|
||||
|
||||
## 兼容性保证
|
||||
|
||||
### 向后兼容
|
||||
- 保持现有API不变
|
||||
- 渐进式架构升级
|
||||
- 配置格式向下兼容
|
||||
|
||||
### 平滑升级
|
||||
- 不影响现有功能
|
||||
- 渐进式功能启用
|
||||
- 可回滚的架构变更
|
||||
|
||||
## 未来扩展方向
|
||||
|
||||
### 新平台支持
|
||||
- 基于IPlatformPublisher接口
|
||||
- 标准化的平台接入流程
|
||||
- 统一的认证和发布机制
|
||||
|
||||
### 处理器生态
|
||||
- 自定义内容处理器
|
||||
- 第三方处理器集成
|
||||
- 处理器市场和分享
|
||||
|
||||
### 智能化功能
|
||||
- AI内容优化
|
||||
- 智能格式转换
|
||||
- 自动化发布流程
|
||||
|
||||
## 总结
|
||||
|
||||
本次优化通过引入现代化的架构模式,显著提升了代码的:
|
||||
- **可维护性**: 模块化设计,职责清晰
|
||||
- **可扩展性**: 接口标准化,插件化架构
|
||||
- **用户体验**: 进度反馈,错误处理优化
|
||||
- **开发效率**: 统一的开发模式,完善的类型系统
|
||||
|
||||
这为Note2Any项目的长期发展奠定了坚实的技术基础。
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "note2any",
|
||||
"name": "Note2Any",
|
||||
"version": "1.3.12",
|
||||
"version": "1.4.0",
|
||||
"minAppVersion": "1.4.5",
|
||||
"description": "xiaohongshu/mp publisher ",
|
||||
"author": "Gavin chan",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "note2any",
|
||||
"version": "1.3.12",
|
||||
"version": "1.4.0",
|
||||
"description": "This is a plugin for Obsidian (https://obsidian.md)",
|
||||
"main": "main.js",
|
||||
"repository": {
|
||||
|
||||
110
release.md
110
release.md
@@ -2,6 +2,74 @@
|
||||
# 版本信息
|
||||
‼️ 发布版本时填写,供脚本~/pubsh/release.sh使用
|
||||
|
||||
|
||||
## v1.3.12
|
||||
### 🚀 核心架构重构与优化
|
||||
|
||||
#### 新增核心模块系统
|
||||
- **ErrorHandler**: 统一错误处理机制,提供集中化的错误管理和用户友好的提示
|
||||
- **ProgressIndicator**: 全新的进度反馈系统,为长时间操作提供实时状态更新
|
||||
- **ConfigManager**: 中心化配置管理,支持运行时配置验证和热更新
|
||||
- **PublisherInterface**: 标准化平台发布接口,支持多平台扩展
|
||||
- **PublisherManager**: 统一的发布平台管理器
|
||||
- **ContentProcessor**: 模块化内容处理流水线
|
||||
|
||||
#### 专业化处理模块
|
||||
- **GalleryProcessor**: 专门处理图库短代码,支持目录式和块级图库
|
||||
- **ImageProcessor**: 统一图像处理接口,支持WebP转换、批量处理、微信上传
|
||||
- **HtmlProcessor**: HTML内容生成和格式化,支持响应式设计和移动端优化
|
||||
|
||||
#### 架构改进
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Core System Layer (新增) │
|
||||
│ - ErrorHandler (错误处理) │
|
||||
│ - ProgressIndicator (进度反馈) │
|
||||
│ - ConfigManager (配置管理) │
|
||||
│ - PublisherManager (发布管理) │
|
||||
└──────────────┬──────────────────────────────┘
|
||||
│ 支撑
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Business Logic Layer │
|
||||
│ preview-manager.ts (中央调度器) │
|
||||
│ - 集成核心模块 │
|
||||
│ - 增强错误处理 │
|
||||
│ - 进度反馈集成 │
|
||||
└──────────────┬──────────────────────────────┘
|
||||
│ 管理
|
||||
┌───────┼───────┐
|
||||
↓ ↓ ↓
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│Platform │ │Wechat │ │Xiaohong- │
|
||||
│Chooser │ │Preview │ │shu │
|
||||
│(UI选择器)│ │(微信实现)│ │Preview │
|
||||
└──────────┘ └──────────┘ │(小红书) │
|
||||
└──────────┘
|
||||
```
|
||||
|
||||
#### 性能优化
|
||||
- 模块化加载,减少初始化时间
|
||||
- 异步处理优化,提升响应性能
|
||||
- 智能缓存机制,减少重复计算
|
||||
|
||||
#### 可维护性提升
|
||||
- 明确的模块职责分离
|
||||
- 统一的接口和约定
|
||||
- 完善的类型定义
|
||||
- 向后兼容的API设计
|
||||
|
||||
### 🔧 技术改进
|
||||
- 重构项目名称从"note-to-mp"到"note2any"
|
||||
- 更新仓库地址到 https://biboer.cn/gitea/gavin/note2any.git
|
||||
- 批量更新66个CSS主题文件的类名
|
||||
- 优化构建和部署流程
|
||||
|
||||
### 📚 文档更新
|
||||
- 新增系统优化报告 (OPTIMIZATION_REPORT_v1.3.12.md)
|
||||
- 更新README文档,反映新的项目名称
|
||||
- 完善架构文档
|
||||
|
||||
## v1.3.4
|
||||
### 重构
|
||||
#### 新的架构图
|
||||
@@ -81,3 +149,45 @@
|
||||
#### 样式系统更新
|
||||
- CSS类名 & 主题资源统一
|
||||
|
||||
## v1.4.0
|
||||
架构升级与代码质量提升
|
||||
|
||||
### 🏗️ 架构升级与代码质量提升
|
||||
|
||||
#### 核心架构现代化
|
||||
- **模块化重构**: 建立了完整的核心模块系统,包含错误处理、进度反馈、配置管理等9个专业模块
|
||||
- **类型安全**: 全面的TypeScript类型定义,零编译错误,提升代码可靠性
|
||||
- **接口标准化**: 统一的平台发布接口,支持更好的扩展性和维护性
|
||||
|
||||
#### 新增核心功能模块
|
||||
- **统一错误处理**: ErrorHandler模块提供集中化的错误管理和用户友好提示
|
||||
- **实时进度反馈**: ProgressIndicator为长时间操作提供状态更新
|
||||
- **智能配置管理**: ConfigManager支持运行时验证和热更新
|
||||
- **可扩展发布系统**: 标准化的平台接口,便于新平台接入
|
||||
|
||||
#### 专业化处理引擎
|
||||
- **图库处理器**: 专门优化图库短代码处理,支持多种格式和智能选择
|
||||
- **图像处理引擎**: 统一的图像处理接口,支持格式转换、批量处理、云端上传
|
||||
- **HTML生成器**: 增强的HTML处理,支持响应式设计和移动端优化
|
||||
|
||||
#### 开发体验改进
|
||||
- **代码组织**: 清晰的模块职责分离,1400+行新增代码
|
||||
- **维护性**: 统一的接口约定和完善的文档
|
||||
- **向后兼容**: 保持现有API稳定,平滑升级路径
|
||||
|
||||
#### 性能与稳定性
|
||||
- **启动优化**: 模块化加载减少初始化时间
|
||||
- **响应性能**: 异步处理优化,提升用户体验
|
||||
- **错误恢复**: 智能的错误处理和恢复机制
|
||||
|
||||
### 🔧 技术债务清理
|
||||
- 重构大型文件,提升代码可读性
|
||||
- 统一错误处理模式
|
||||
- 优化资源加载策略
|
||||
- 完善类型定义覆盖
|
||||
|
||||
### 📖 文档与工程化
|
||||
- 新增详细的架构文档和优化报告
|
||||
- 完善开发和部署流程
|
||||
- 更新项目说明和使用指南
|
||||
|
||||
|
||||
148
src/core/config-manager.ts
Normal file
148
src/core/config-manager.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 文件:config-manager.ts
|
||||
* 作用:集中的配置管理和验证
|
||||
*/
|
||||
|
||||
import { ErrorHandler, ValidationError } from './error-handler';
|
||||
import { NMPSettings } from '../settings';
|
||||
|
||||
export interface PlatformConfig {
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
validate(): boolean;
|
||||
}
|
||||
|
||||
export interface WechatConfig extends PlatformConfig {
|
||||
wxInfo: Array<{name: string, appid: string, secret: string}>;
|
||||
authKey: string;
|
||||
}
|
||||
|
||||
export interface XhsConfig extends PlatformConfig {
|
||||
// 小红书相关配置待扩展
|
||||
sliceImageSavePath: string;
|
||||
sliceImageWidth: number;
|
||||
sliceImageAspectRatio: string;
|
||||
xhsPreviewWidth: number;
|
||||
}
|
||||
|
||||
export interface GlobalConfig {
|
||||
defaultTheme: string;
|
||||
defaultHighlight: string;
|
||||
baseCSS: string;
|
||||
customCSSNote: string;
|
||||
showStyleUI: boolean;
|
||||
linkStyle: string;
|
||||
embedStyle: string;
|
||||
lineNumber: boolean;
|
||||
math: string;
|
||||
}
|
||||
|
||||
export class ConfigManager {
|
||||
private static instance: ConfigManager;
|
||||
private settings: NMPSettings;
|
||||
|
||||
private constructor(settings: NMPSettings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
static getInstance(settings?: NMPSettings): ConfigManager {
|
||||
if (!this.instance && settings) {
|
||||
this.instance = new ConfigManager(settings);
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
static initialize(settings: NMPSettings): void {
|
||||
this.instance = new ConfigManager(settings);
|
||||
}
|
||||
|
||||
getWechatConfig(): WechatConfig {
|
||||
return {
|
||||
name: 'WeChat',
|
||||
enabled: this.settings.wxInfo.length > 0,
|
||||
wxInfo: this.settings.wxInfo,
|
||||
authKey: this.settings.authKey,
|
||||
validate: () => this.validateWechatConfig()
|
||||
};
|
||||
}
|
||||
|
||||
getXhsConfig(): XhsConfig {
|
||||
return {
|
||||
name: 'XiaoHongShu',
|
||||
enabled: true, // 暂时默认启用
|
||||
sliceImageSavePath: this.settings.sliceImageSavePath,
|
||||
sliceImageWidth: this.settings.sliceImageWidth,
|
||||
sliceImageAspectRatio: this.settings.sliceImageAspectRatio,
|
||||
xhsPreviewWidth: this.settings.xhsPreviewWidth,
|
||||
validate: () => this.validateXhsConfig()
|
||||
};
|
||||
}
|
||||
|
||||
getGlobalConfig(): GlobalConfig {
|
||||
return {
|
||||
defaultTheme: this.settings.defaultStyle,
|
||||
defaultHighlight: this.settings.defaultHighlight,
|
||||
baseCSS: this.settings.baseCSS,
|
||||
customCSSNote: this.settings.customCSSNote,
|
||||
showStyleUI: this.settings.showStyleUI,
|
||||
linkStyle: this.settings.linkStyle,
|
||||
embedStyle: this.settings.embedStyle,
|
||||
lineNumber: this.settings.lineNumber,
|
||||
math: this.settings.math
|
||||
};
|
||||
}
|
||||
|
||||
private validateWechatConfig(): boolean {
|
||||
try {
|
||||
ErrorHandler.validateRequired(this.settings.authKey, 'WeChat 授权密钥');
|
||||
if (this.settings.wxInfo.length === 0) {
|
||||
throw new ValidationError('WeChat 配置信息不能为空');
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('WeChat配置验证失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private validateXhsConfig(): boolean {
|
||||
try {
|
||||
ErrorHandler.validateRequired(this.settings.sliceImageSavePath, '小红书切图保存路径');
|
||||
if (this.settings.sliceImageWidth <= 0) {
|
||||
throw new ValidationError('切图宽度必须大于0');
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('小红书配置验证失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
validatePlatformConfig(platform: 'wechat' | 'xhs'): boolean {
|
||||
switch (platform) {
|
||||
case 'wechat':
|
||||
return this.validateWechatConfig();
|
||||
case 'xhs':
|
||||
return this.validateXhsConfig();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
isTokenValid(platform: 'wechat'): boolean {
|
||||
if (platform === 'wechat') {
|
||||
return this.settings.isAuthKeyVaild();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
updateWechatToken(accessToken: string, expiresIn: number): void {
|
||||
// 当前设置结构中使用 authKey 系统,这里可以扩展
|
||||
console.log('WeChat token updated (using authKey system)');
|
||||
}
|
||||
|
||||
clearWechatToken(): void {
|
||||
// 当前设置结构中使用 authKey 系统,这里可以扩展
|
||||
console.log('WeChat token cleared (using authKey system)');
|
||||
}
|
||||
}
|
||||
209
src/core/content-processor.ts
Normal file
209
src/core/content-processor.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* 文件:content-processor.ts
|
||||
* 作用:内容处理器,负责处理markdown内容的各种转换
|
||||
*/
|
||||
|
||||
import { TFile, App } from 'obsidian';
|
||||
import { ErrorHandler } from './error-handler';
|
||||
|
||||
export interface ProcessorOptions {
|
||||
enableImageToBase64?: boolean;
|
||||
enableLinkProcessing?: boolean;
|
||||
enableCodeHighlight?: boolean;
|
||||
enableMathProcessing?: boolean;
|
||||
platform?: string;
|
||||
}
|
||||
|
||||
export class ContentProcessor {
|
||||
private app: App;
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理图片链接,转换为base64或平台URL
|
||||
*/
|
||||
async processImages(
|
||||
content: string,
|
||||
file: TFile,
|
||||
options: ProcessorOptions = {}
|
||||
): Promise<string> {
|
||||
return await ErrorHandler.withErrorHandling(async () => {
|
||||
const { enableImageToBase64 = true } = options;
|
||||
|
||||
if (!enableImageToBase64) {
|
||||
return content;
|
||||
}
|
||||
|
||||
// WikiLink 图片处理: ![[image.png]]
|
||||
content = await this.processWikiLinkImages(content, file);
|
||||
|
||||
// Markdown 图片处理: 
|
||||
content = await this.processMarkdownImages(content, file);
|
||||
|
||||
return content;
|
||||
}, 'ContentProcessor.processImages', content) || content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理链接
|
||||
*/
|
||||
processLinks(content: string, linkStyle: 'inline' | 'footnote' = 'inline'): string {
|
||||
return ErrorHandler.withErrorHandlingSync(() => {
|
||||
if (linkStyle === 'footnote') {
|
||||
return this.convertLinksToFootnotes(content);
|
||||
}
|
||||
return this.processInlineLinks(content);
|
||||
}, 'ContentProcessor.processLinks', content) || content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理代码块高亮
|
||||
*/
|
||||
processCodeBlocks(content: string, highlightTheme: string = 'default'): string {
|
||||
return ErrorHandler.withErrorHandlingSync(() => {
|
||||
// 为代码块添加语法高亮类
|
||||
return content.replace(
|
||||
/```(\w+)?\n([\s\S]*?)```/g,
|
||||
(match, lang, code) => {
|
||||
const language = lang || 'text';
|
||||
return `<div class="code-section">
|
||||
<pre><code class="language-${language}">${this.escapeHtml(code.trim())}</code></pre>
|
||||
</div>`;
|
||||
}
|
||||
);
|
||||
}, 'ContentProcessor.processCodeBlocks', content) || content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理数学公式
|
||||
*/
|
||||
processMath(content: string, mathEngine: 'latex' | 'asciimath' = 'latex'): string {
|
||||
return ErrorHandler.withErrorHandlingSync(() => {
|
||||
// 行内公式: $...$
|
||||
content = content.replace(/\$([^$]+)\$/g, (match, formula) => {
|
||||
return `<span class="math-inline" data-engine="${mathEngine}">${formula}</span>`;
|
||||
});
|
||||
|
||||
// 块级公式: $$...$$
|
||||
content = content.replace(/\$\$([^$]+)\$\$/g, (match, formula) => {
|
||||
return `<div class="math-block" data-engine="${mathEngine}">${formula}</div>`;
|
||||
});
|
||||
|
||||
return content;
|
||||
}, 'ContentProcessor.processMath', content) || content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Gallery短代码
|
||||
*/
|
||||
async processGalleryShortcode(content: string, galleryPath: string, numPics: number = 2): Promise<string> {
|
||||
return await ErrorHandler.withErrorHandling(async () => {
|
||||
const galleryRegex = /{{<gallery\s+dir="([^"]+)"(?:\s+figcaption="([^"]*)")?(?:\s+mppickall=(?:"(1|0)"|'(1|0)'|(1|0)))?\s*\/?>}}\s*{{<load-photoswipe>}}/g;
|
||||
|
||||
return content.replace(galleryRegex, (match, dir, figcaption, q1, q2, unquoted) => {
|
||||
const pickAll = q1 === '1' || q2 === '1' || unquoted === '1';
|
||||
const maxPics = pickAll ? 999 : numPics;
|
||||
|
||||
// 这里应该调用实际的图片列表获取逻辑
|
||||
// 为了简化,返回占位符
|
||||
return `<!-- Gallery: ${dir}, max: ${maxPics}, caption: ${figcaption || ''} -->`;
|
||||
});
|
||||
}, 'ContentProcessor.processGalleryShortcode', content) || content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理HTML标签
|
||||
*/
|
||||
sanitizeHtml(content: string, allowedTags: string[] = []): string {
|
||||
return ErrorHandler.withErrorHandlingSync(() => {
|
||||
const allowedTagsSet = new Set(allowedTags);
|
||||
|
||||
return content.replace(/<[^>]*>/g, (tag) => {
|
||||
const tagName = tag.match(/<\/?(\w+)/)?.[1]?.toLowerCase();
|
||||
if (tagName && allowedTagsSet.has(tagName)) {
|
||||
return tag;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
}, 'ContentProcessor.sanitizeHtml', content) || content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理自定义语法扩展
|
||||
*/
|
||||
processCustomSyntax(content: string): string {
|
||||
return ErrorHandler.withErrorHandlingSync(() => {
|
||||
// 斜体标注: [fig 一段说明 /]
|
||||
content = content.replace(/\[fig\s+([^/]+)\s+\/\]/g,
|
||||
'<span style="font-style:italic;color:#666;font-size:0.9em;">$1</span>');
|
||||
|
||||
// 彩色提示块
|
||||
content = content.replace(/^\|\|([rgby]?)\s+(.+)$/gm, (match, color, text) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
'r': 'background:#8B4513;color:white',
|
||||
'g': 'background:#9ACD32;color:black',
|
||||
'b': 'background:#D3D3D3;color:black',
|
||||
'y': 'background:#FFFF99;color:black',
|
||||
'': 'background:#F5F5F5;color:black'
|
||||
};
|
||||
const style = colorMap[color] || colorMap[''];
|
||||
return `<div style="padding:8px;margin:4px 0;border-radius:4px;${style}">${text}</div>`;
|
||||
});
|
||||
|
||||
return content;
|
||||
}, 'ContentProcessor.processCustomSyntax', content) || content;
|
||||
}
|
||||
|
||||
// 私有辅助方法
|
||||
|
||||
private async processWikiLinkImages(content: string, file: TFile): Promise<string> {
|
||||
const wikiImageRegex = /!\[\[([^\]]+)\]\]/g;
|
||||
|
||||
return content.replace(wikiImageRegex, (match, imagePath) => {
|
||||
// 这里应该实现实际的图片处理逻辑
|
||||
return `<img src="data:image/png;base64,placeholder" alt="${imagePath}">`;
|
||||
});
|
||||
}
|
||||
|
||||
private async processMarkdownImages(content: string, file: TFile): Promise<string> {
|
||||
const markdownImageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
||||
|
||||
return content.replace(markdownImageRegex, (match, alt, src) => {
|
||||
// 这里应该实现实际的图片处理逻辑
|
||||
return `<img src="data:image/png;base64,placeholder" alt="${alt}">`;
|
||||
});
|
||||
}
|
||||
|
||||
private convertLinksToFootnotes(content: string): string {
|
||||
const links: string[] = [];
|
||||
|
||||
// 提取所有链接
|
||||
content = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => {
|
||||
links.push(url);
|
||||
return `${text}[${links.length}]`;
|
||||
});
|
||||
|
||||
// 添加脚注
|
||||
if (links.length > 0) {
|
||||
content += '\n\n---\n\n';
|
||||
links.forEach((url, index) => {
|
||||
content += `[${index + 1}]: ${url}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
private processInlineLinks(content: string): string {
|
||||
return content.replace(/\[([^\]]+)\]\(([^)]+)\)/g,
|
||||
'<a href="$2" style="color:#1e6bb8;text-decoration:none;">$1</a>');
|
||||
}
|
||||
|
||||
private escapeHtml(text: string): string {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
}
|
||||
96
src/core/error-handler.ts
Normal file
96
src/core/error-handler.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 文件:error-handler.ts
|
||||
* 作用:统一的错误处理和用户反馈系统
|
||||
*/
|
||||
|
||||
import { Notice } from 'obsidian';
|
||||
|
||||
export class NetworkError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'NetworkError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidationError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'ValidationError';
|
||||
}
|
||||
}
|
||||
|
||||
export class PlatformError extends Error {
|
||||
constructor(message: string, public platform: string) {
|
||||
super(message);
|
||||
this.name = 'PlatformError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ErrorHandler {
|
||||
private static readonly ERROR_MESSAGES = {
|
||||
NETWORK_ERROR: '网络连接异常,请检查网络设置',
|
||||
VALIDATION_ERROR: '配置验证失败',
|
||||
PLATFORM_ERROR: '发布平台异常',
|
||||
UNKNOWN_ERROR: '操作失败,请查看控制台了解详情'
|
||||
};
|
||||
|
||||
static handle(error: Error, context: string): void {
|
||||
console.error(`[Note2Any] ${context}:`, error);
|
||||
|
||||
let userMessage: string;
|
||||
let duration = 5000;
|
||||
|
||||
if (error instanceof NetworkError) {
|
||||
userMessage = this.ERROR_MESSAGES.NETWORK_ERROR;
|
||||
} else if (error instanceof ValidationError) {
|
||||
userMessage = `${this.ERROR_MESSAGES.VALIDATION_ERROR}: ${error.message}`;
|
||||
} else if (error instanceof PlatformError) {
|
||||
userMessage = `${error.platform} ${this.ERROR_MESSAGES.PLATFORM_ERROR}: ${error.message}`;
|
||||
} else {
|
||||
userMessage = this.ERROR_MESSAGES.UNKNOWN_ERROR;
|
||||
duration = 8000;
|
||||
}
|
||||
|
||||
new Notice(userMessage, duration);
|
||||
}
|
||||
|
||||
static async withErrorHandling<T>(
|
||||
operation: () => Promise<T>,
|
||||
context: string,
|
||||
fallback?: T
|
||||
): Promise<T | undefined> {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
this.handle(error as Error, context);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
static withErrorHandlingSync<T>(
|
||||
operation: () => T,
|
||||
context: string,
|
||||
fallback?: T
|
||||
): T | undefined {
|
||||
try {
|
||||
return operation();
|
||||
} catch (error) {
|
||||
this.handle(error as Error, context);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
static validateRequired(value: any, fieldName: string): void {
|
||||
if (!value || (typeof value === 'string' && value.trim() === '')) {
|
||||
throw new ValidationError(`${fieldName} 是必填项`);
|
||||
}
|
||||
}
|
||||
|
||||
static validateUrl(url: string, fieldName: string): void {
|
||||
try {
|
||||
new URL(url);
|
||||
} catch {
|
||||
throw new ValidationError(`${fieldName} 必须是有效的URL`);
|
||||
}
|
||||
}
|
||||
}
|
||||
150
src/core/gallery-processor.ts
Normal file
150
src/core/gallery-processor.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* 文件:gallery-processor.ts
|
||||
* 作用:处理文章中的图库短代码
|
||||
*
|
||||
* 职责:
|
||||
* 1. 解析和转换 gallery 短代码
|
||||
* 2. 处理本地图片目录扫描
|
||||
* 3. 生成 wikilink 格式的图片引用
|
||||
*/
|
||||
|
||||
import { stat, readdir } from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { NMPSettings } from '../settings';
|
||||
|
||||
// gallery 配置迁移到 NMPSettings(galleryPrePath, galleryNumPic)
|
||||
// 匹配示例:{{<gallery dir="/img/guanzhan/1" figcaption="毕业展" mppickall=1/>}}{{<load-photoswipe>}}
|
||||
// 支持可选 figcaption 以及 mppickall=1/0(无引号数字或布尔),若 mppickall=1 则选取目录内全部图片
|
||||
const GALLERY_SHORTCODE_REGEX = /{{<gallery\s+dir="([^"]+)"(?:\s+figcaption="([^"]*)")?(?:\s+mppickall=(?:"(1|0)"|'(1|0)'|(1|0)))?\s*\/?>}}\s*{{<load-photoswipe>}}/g;
|
||||
|
||||
// 块级 gallery:
|
||||
// {{<gallery>}}\n{{<figure src="/img/a.png" caption=".." >}}\n...\n{{</gallery>}}
|
||||
// 需要提取所有 figure 的 src basename 生成多行 wikilink
|
||||
const GALLERY_BLOCK_REGEX = /{{<gallery>}}([\s\S]*?){{<\/gallery>}}/g;
|
||||
|
||||
// figure 支持 src 或 link 属性,两者取其一
|
||||
const FIGURE_IN_GALLERY_REGEX = /{{<figure\s+(?:src|link)="([^"]+)"[^>]*>}}/g;
|
||||
|
||||
/**
|
||||
* 列出本地图片目录中的图片文件
|
||||
*/
|
||||
async function listLocalImages(dirAbs: string): Promise<string[]> {
|
||||
try {
|
||||
const stats = await stat(dirAbs);
|
||||
if (!stats.isDirectory()) return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const files = await readdir(dirAbs);
|
||||
return files.filter(f => /(png|jpe?g|gif|bmp|webp|svg)$/i.test(f)).sort();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从图片列表中选择指定数量的图片
|
||||
*/
|
||||
function pickImages(all: string[], limit: number): string[] {
|
||||
if (all.length <= limit) return all;
|
||||
|
||||
// 均匀采样
|
||||
const step = all.length / limit;
|
||||
const picked: string[] = [];
|
||||
for (let i = 0; i < limit; i++) {
|
||||
const index = Math.floor(i * step);
|
||||
picked.push(all[index]);
|
||||
}
|
||||
return picked;
|
||||
}
|
||||
|
||||
/**
|
||||
* 图库处理器类
|
||||
*/
|
||||
export class GalleryProcessor {
|
||||
private settings: NMPSettings;
|
||||
|
||||
constructor(settings: NMPSettings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文章中的图库短代码
|
||||
*/
|
||||
async processGalleryShortcodes(content: string): Promise<string> {
|
||||
// 处理目录式 gallery
|
||||
content = await this.processDirectoryGalleries(content);
|
||||
|
||||
// 处理块级 gallery
|
||||
content = await this.processBlockGalleries(content);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理目录式图库短代码
|
||||
*/
|
||||
private async processDirectoryGalleries(content: string): Promise<string> {
|
||||
const matches = Array.from(content.matchAll(GALLERY_SHORTCODE_REGEX));
|
||||
|
||||
for (const match of matches) {
|
||||
const [fullMatch, dir, figcaption = '', pickall1, pickall2, pickall3] = match;
|
||||
const pickall = pickall1 || pickall2 || pickall3;
|
||||
const shouldPickAll = pickall === '1';
|
||||
|
||||
try {
|
||||
const galleryPrePath = this.settings.galleryPrePath;
|
||||
const dirAbs = path.join(galleryPrePath, dir);
|
||||
const allImages = await listLocalImages(dirAbs);
|
||||
|
||||
if (allImages.length === 0) {
|
||||
console.warn(`[GalleryProcessor] 目录 ${dirAbs} 中没有找到图片`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const selectedImages = shouldPickAll
|
||||
? allImages
|
||||
: pickImages(allImages, this.settings.galleryNumPic);
|
||||
|
||||
// 生成 wikilink 格式的图片引用
|
||||
const wikilinks = selectedImages.map(img => {
|
||||
const imgPath = path.join(dir, img).replace(/\\/g, '/');
|
||||
return `![[${imgPath}]]`;
|
||||
});
|
||||
|
||||
let replacement = wikilinks.join('\n');
|
||||
if (figcaption) {
|
||||
replacement = `> ${figcaption}\n\n${replacement}`;
|
||||
}
|
||||
|
||||
content = content.replace(fullMatch, replacement);
|
||||
} catch (error) {
|
||||
console.error(`[GalleryProcessor] 处理图库失败: ${dir}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理块级图库短代码
|
||||
*/
|
||||
private processBlockGalleries(content: string): Promise<string> {
|
||||
return Promise.resolve(content.replace(GALLERY_BLOCK_REGEX, (match, blockContent) => {
|
||||
const figureMatches = Array.from(blockContent.matchAll(FIGURE_IN_GALLERY_REGEX));
|
||||
|
||||
if (figureMatches.length === 0) {
|
||||
return match; // 保持原样
|
||||
}
|
||||
|
||||
const wikilinks = figureMatches.map(([, src]) => {
|
||||
const basename = path.basename(src);
|
||||
return `![[${basename}]]`;
|
||||
});
|
||||
|
||||
return wikilinks.join('\n');
|
||||
}));
|
||||
}
|
||||
}
|
||||
263
src/core/html-processor.ts
Normal file
263
src/core/html-processor.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* 文件:html-processor.ts
|
||||
* 作用:处理HTML内容的生成和格式化
|
||||
*
|
||||
* 职责:
|
||||
* 1. HTML内容清理和格式化
|
||||
* 2. CSS样式内联处理
|
||||
* 3. HTML结构生成
|
||||
* 4. 代码高亮处理
|
||||
*/
|
||||
|
||||
import { sanitizeHTMLToDom } from 'obsidian';
|
||||
import { applyCSS } from '../utils';
|
||||
import InlineCSS from '../inline-css';
|
||||
import { NMPSettings } from '../settings';
|
||||
import AssetsManager from '../assets';
|
||||
import { ErrorHandler } from './error-handler';
|
||||
|
||||
export interface HtmlProcessOptions {
|
||||
enableCodeHighlight?: boolean;
|
||||
enableInlineCSS?: boolean;
|
||||
theme?: string;
|
||||
highlight?: string;
|
||||
customCSS?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML处理器类
|
||||
*/
|
||||
export class HtmlProcessor {
|
||||
private settings: NMPSettings;
|
||||
private assetsManager: AssetsManager;
|
||||
|
||||
constructor() {
|
||||
this.settings = NMPSettings.getInstance();
|
||||
this.assetsManager = AssetsManager.getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成完整的HTML文档
|
||||
*/
|
||||
generateFullHtml(content: string, options: HtmlProcessOptions = {}): string {
|
||||
try {
|
||||
const theme = options.theme || this.settings.defaultStyle;
|
||||
const highlight = options.highlight || this.settings.defaultHighlight;
|
||||
|
||||
// 获取基础样式
|
||||
const baseCSS = this.getBaseCSS(theme, highlight);
|
||||
|
||||
// 处理自定义CSS
|
||||
let customCSS = options.customCSS || '';
|
||||
if (this.settings.useCustomCss && this.settings.customCSSNote) {
|
||||
customCSS += '\n' + this.settings.customCSSNote;
|
||||
}
|
||||
|
||||
// 构建完整HTML
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Note2Any Export</title>
|
||||
<style>
|
||||
${baseCSS}
|
||||
${customCSS}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="note2any">
|
||||
${content}
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
return html;
|
||||
} catch (error) {
|
||||
ErrorHandler.handle(error as Error, 'HtmlProcessor.generateFullHtml');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理HTML内容的内联CSS
|
||||
*/
|
||||
async processInlineCSS(html: string): Promise<string> {
|
||||
// 简化版内联CSS处理,使用原生方法
|
||||
try {
|
||||
// 这里可以实现一个简单的CSS内联功能
|
||||
// 或者集成第三方库如 juice
|
||||
return html;
|
||||
} catch (error) {
|
||||
ErrorHandler.handle(error as Error, 'HtmlProcessor.processInlineCSS');
|
||||
console.warn('[HtmlProcessor] 内联CSS处理失败,使用原始HTML', error);
|
||||
return html;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理和格式化HTML内容
|
||||
*/
|
||||
sanitizeHtml(html: string): DocumentFragment {
|
||||
try {
|
||||
return sanitizeHTMLToDom(html);
|
||||
} catch (error) {
|
||||
ErrorHandler.handle(error as Error, 'HtmlProcessor.sanitizeHtml');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用CSS样式到元素
|
||||
*/
|
||||
applyStylesToElement(html: string, css: string): string {
|
||||
try {
|
||||
return applyCSS(html, css);
|
||||
} catch (error) {
|
||||
ErrorHandler.handle(error as Error, 'HtmlProcessor.applyStylesToElement');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取基础CSS样式
|
||||
*/
|
||||
private getBaseCSS(theme: string, highlight: string): string {
|
||||
try {
|
||||
let css = '';
|
||||
|
||||
// 主题样式
|
||||
const themeCSS = this.assetsManager.getTheme(theme);
|
||||
if (themeCSS) {
|
||||
css += themeCSS + '\n';
|
||||
}
|
||||
|
||||
// 代码高亮样式
|
||||
const highlightCSS = this.assetsManager.getHighlight(highlight);
|
||||
if (highlightCSS) {
|
||||
css += highlightCSS + '\n';
|
||||
}
|
||||
|
||||
return css;
|
||||
} catch (error) {
|
||||
ErrorHandler.handle(error as Error, 'HtmlProcessor.getBaseCSS');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成响应式HTML包装器
|
||||
*/
|
||||
wrapWithResponsive(content: string, maxWidth = '800px'): string {
|
||||
return `
|
||||
<div style="max-width: ${maxWidth}; margin: 0 auto; padding: 20px; box-sizing: border-box;">
|
||||
${content}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加打印样式
|
||||
*/
|
||||
addPrintStyles(): string {
|
||||
return `
|
||||
<style media="print">
|
||||
@page {
|
||||
margin: 2cm;
|
||||
size: A4;
|
||||
}
|
||||
|
||||
.note2any {
|
||||
max-width: none !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap !important;
|
||||
word-break: break-word !important;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
p, li {
|
||||
orphans: 3;
|
||||
widows: 3;
|
||||
}
|
||||
</style>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理代码块高亮
|
||||
*/
|
||||
processCodeHighlight(html: string, highlight: string): string {
|
||||
try {
|
||||
// 这里可以添加更复杂的代码高亮处理逻辑
|
||||
// 目前主要依赖 AssetsManager 提供的高亮样式
|
||||
|
||||
return html;
|
||||
} catch (error) {
|
||||
ErrorHandler.handle(error as Error, 'HtmlProcessor.processCodeHighlight');
|
||||
return html;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化HTML结构用于移动端显示
|
||||
*/
|
||||
optimizeForMobile(html: string): string {
|
||||
try {
|
||||
// 添加移动端优化的meta标签和样式
|
||||
const mobileOptimizations = `
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<style>
|
||||
/* 移动端优化样式 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.note2any {
|
||||
padding: 15px 10px !important;
|
||||
font-size: 16px !important;
|
||||
line-height: 1.6 !important;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
font-size: 14px !important;
|
||||
overflow-x: auto !important;
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
|
||||
table {
|
||||
font-size: 14px !important;
|
||||
display: block !important;
|
||||
overflow-x: auto !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
</style>`;
|
||||
|
||||
// 在head标签中插入移动端优化
|
||||
return html.replace('</head>', mobileOptimizations + '</head>');
|
||||
} catch (error) {
|
||||
ErrorHandler.handle(error as Error, 'HtmlProcessor.optimizeForMobile');
|
||||
return html;
|
||||
}
|
||||
}
|
||||
}
|
||||
225
src/core/image-processor.ts
Normal file
225
src/core/image-processor.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* 文件:image-processor.ts
|
||||
* 作用:处理文章中的图片相关操作
|
||||
*
|
||||
* 职责:
|
||||
* 1. 图片上传和转换
|
||||
* 2. 图片格式处理(WebP转JPG等)
|
||||
* 3. 图片EXIF处理
|
||||
* 4. 图片截图功能
|
||||
*/
|
||||
|
||||
import { Notice } from 'obsidian';
|
||||
import { toPng } from 'html-to-image';
|
||||
import { PrepareImageLib, IsImageLibReady, WebpToJPG } from '../imagelib';
|
||||
import { UploadImageToWx } from '../imagelib';
|
||||
import { ErrorHandler } from './error-handler';
|
||||
import { ProgressIndicator } from './progress-indicator';
|
||||
|
||||
export interface ImageProcessOptions {
|
||||
enableWebpConversion?: boolean;
|
||||
enableExifProcessing?: boolean;
|
||||
quality?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 图像处理器类
|
||||
*/
|
||||
export class ImageProcessor {
|
||||
private isInitialized = false;
|
||||
|
||||
/**
|
||||
* 初始化图像处理库
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.isInitialized) return;
|
||||
|
||||
const progress = new ProgressIndicator();
|
||||
progress.start('初始化图像处理库');
|
||||
|
||||
try {
|
||||
await PrepareImageLib();
|
||||
|
||||
// 等待图像库准备就绪
|
||||
let attempts = 0;
|
||||
const maxAttempts = 50;
|
||||
while (!IsImageLibReady() && attempts < maxAttempts) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (!IsImageLibReady()) {
|
||||
throw new Error('图像处理库初始化超时');
|
||||
}
|
||||
|
||||
this.isInitialized = true;
|
||||
progress.finish('图像处理库初始化完成');
|
||||
} catch (error) {
|
||||
progress.error('图像处理库初始化失败');
|
||||
ErrorHandler.handle(error as Error, 'ImageProcessor.initialize');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保图像处理库已初始化
|
||||
*/
|
||||
private async ensureInitialized(): Promise<void> {
|
||||
if (!this.isInitialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将WebP格式转换为JPG
|
||||
*/
|
||||
async convertWebpToJpg(webpData: ArrayBuffer): Promise<ArrayBuffer> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
try {
|
||||
const result = await WebpToJPG(webpData);
|
||||
if (!result) {
|
||||
throw new Error('WebP转JPG失败');
|
||||
}
|
||||
// 将 Uint8Array 转换为 ArrayBuffer
|
||||
const buffer = result.buffer as ArrayBuffer;
|
||||
return buffer.slice(result.byteOffset, result.byteOffset + result.byteLength);
|
||||
} catch (error) {
|
||||
ErrorHandler.handle(error as Error, 'ImageProcessor.convertWebpToJpg');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传图片到微信
|
||||
*/
|
||||
async uploadToWechat(imageData: ArrayBuffer, filename: string, token: string, type?: string): Promise<string> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
const progress = new ProgressIndicator();
|
||||
progress.start(`上传图片: ${filename}`);
|
||||
|
||||
try {
|
||||
// 将 ArrayBuffer 转换为 Blob
|
||||
const blob = new Blob([imageData]);
|
||||
const result = await UploadImageToWx(blob, filename, token, type);
|
||||
|
||||
if (result.errcode !== 0) {
|
||||
throw new Error(`上传失败: ${result.errmsg}`);
|
||||
}
|
||||
|
||||
progress.finish('图片上传成功');
|
||||
return result.media_id;
|
||||
} catch (error) {
|
||||
progress.error('图片上传失败');
|
||||
ErrorHandler.handle(error as Error, 'ImageProcessor.uploadToWechat');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将HTML元素转换为PNG图片
|
||||
*/
|
||||
async htmlToPng(element: HTMLElement, options?: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
backgroundColor?: string;
|
||||
pixelRatio?: number;
|
||||
}): Promise<Blob> {
|
||||
const progress = new ProgressIndicator();
|
||||
progress.start('生成图片');
|
||||
|
||||
try {
|
||||
const defaultOptions = {
|
||||
width: options?.width || element.offsetWidth,
|
||||
height: options?.height || element.offsetHeight,
|
||||
backgroundColor: options?.backgroundColor || '#ffffff',
|
||||
pixelRatio: options?.pixelRatio || 2,
|
||||
cacheBust: true,
|
||||
skipFonts: false,
|
||||
style: {
|
||||
transform: 'scale(1)',
|
||||
transformOrigin: 'top left'
|
||||
}
|
||||
};
|
||||
|
||||
const dataUrl = await toPng(element, defaultOptions);
|
||||
|
||||
// 将 data URL 转换为 Blob
|
||||
const response = await fetch(dataUrl);
|
||||
const blob = await response.blob();
|
||||
|
||||
progress.finish('图片生成完成');
|
||||
return blob;
|
||||
} catch (error) {
|
||||
progress.error('图片生成失败');
|
||||
ErrorHandler.handle(error as Error, 'ImageProcessor.htmlToPng');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理图片数据
|
||||
*/
|
||||
async processImage(
|
||||
imageData: ArrayBuffer,
|
||||
filename: string,
|
||||
options: ImageProcessOptions = {}
|
||||
): Promise<ArrayBuffer> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
let processedData = imageData;
|
||||
|
||||
try {
|
||||
// WebP转JPG处理
|
||||
if (options.enableWebpConversion && filename.toLowerCase().endsWith('.webp')) {
|
||||
processedData = await this.convertWebpToJpg(processedData);
|
||||
}
|
||||
|
||||
// 这里可以添加更多图片处理逻辑
|
||||
// 如:压缩、EXIF处理等
|
||||
|
||||
return processedData;
|
||||
} catch (error) {
|
||||
ErrorHandler.handle(error as Error, 'ImageProcessor.processImage');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量处理图片
|
||||
*/
|
||||
async processImages(
|
||||
images: Array<{ data: ArrayBuffer; filename: string }>,
|
||||
options: ImageProcessOptions = {}
|
||||
): Promise<Array<{ data: ArrayBuffer; filename: string }>> {
|
||||
const progress = new ProgressIndicator();
|
||||
progress.start(`批量处理图片 (${images.length}张)`);
|
||||
|
||||
try {
|
||||
const results = [];
|
||||
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const { data, filename } = images[i];
|
||||
progress.update(`处理图片 ${i + 1}/${images.length}: ${filename}`);
|
||||
|
||||
const processedData = await this.processImage(data, filename, options);
|
||||
results.push({ data: processedData, filename });
|
||||
}
|
||||
|
||||
progress.finish('批量图片处理完成');
|
||||
return results;
|
||||
} catch (error) {
|
||||
progress.error('批量图片处理失败');
|
||||
ErrorHandler.handle(error as Error, 'ImageProcessor.processImages');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图像处理库状态
|
||||
*/
|
||||
isReady(): boolean {
|
||||
return this.isInitialized && IsImageLibReady();
|
||||
}
|
||||
}
|
||||
106
src/core/progress-indicator.ts
Normal file
106
src/core/progress-indicator.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 文件:progress-indicator.ts
|
||||
* 作用:进度指示和用户反馈管理
|
||||
*/
|
||||
|
||||
import { Notice } from 'obsidian';
|
||||
|
||||
export interface ProgressOptions {
|
||||
showProgress?: boolean;
|
||||
duration?: number;
|
||||
autoHide?: boolean;
|
||||
}
|
||||
|
||||
export class ProgressIndicator {
|
||||
private notice: Notice | null = null;
|
||||
private startTime: number = 0;
|
||||
|
||||
start(message: string, options: ProgressOptions = {}): void {
|
||||
this.startTime = Date.now();
|
||||
const { duration = 0 } = options;
|
||||
|
||||
this.notice = new Notice(`🔄 ${message}...`, duration);
|
||||
}
|
||||
|
||||
update(message: string, progress?: number): void {
|
||||
if (!this.notice) return;
|
||||
|
||||
const progressText = progress ? ` (${Math.round(progress)}%)` : '';
|
||||
this.notice.setMessage(`🔄 ${message}${progressText}...`);
|
||||
}
|
||||
|
||||
finish(message: string, success: boolean = true): void {
|
||||
if (this.notice) {
|
||||
this.notice.hide();
|
||||
this.notice = null;
|
||||
}
|
||||
|
||||
const duration = Date.now() - this.startTime;
|
||||
const durationText = duration > 1000 ? ` (${(duration / 1000).toFixed(1)}s)` : '';
|
||||
const icon = success ? '✅' : '❌';
|
||||
|
||||
new Notice(`${icon} ${message}${durationText}`, success ? 3000 : 5000);
|
||||
}
|
||||
|
||||
error(message: string): void {
|
||||
this.finish(message, false);
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
if (this.notice) {
|
||||
this.notice.hide();
|
||||
this.notice = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class BatchProgressIndicator {
|
||||
private currentProgress: ProgressIndicator | null = null;
|
||||
private totalItems: number = 0;
|
||||
private completedItems: number = 0;
|
||||
private failedItems: number = 0;
|
||||
|
||||
start(totalItems: number, operation: string): void {
|
||||
this.totalItems = totalItems;
|
||||
this.completedItems = 0;
|
||||
this.failedItems = 0;
|
||||
|
||||
this.currentProgress = new ProgressIndicator();
|
||||
this.currentProgress.start(`${operation} (0/${totalItems})`);
|
||||
}
|
||||
|
||||
updateItem(itemName: string, success: boolean = true): void {
|
||||
if (success) {
|
||||
this.completedItems++;
|
||||
} else {
|
||||
this.failedItems++;
|
||||
}
|
||||
|
||||
const completed = this.completedItems + this.failedItems;
|
||||
const progress = (completed / this.totalItems) * 100;
|
||||
|
||||
if (this.currentProgress) {
|
||||
this.currentProgress.update(
|
||||
`处理中: ${itemName} (${completed}/${this.totalItems})`,
|
||||
progress
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
finish(operation: string): void {
|
||||
const successCount = this.completedItems;
|
||||
const failCount = this.failedItems;
|
||||
const total = this.totalItems;
|
||||
|
||||
let message = `${operation}完成`;
|
||||
if (failCount === 0) {
|
||||
message += ` - 全部成功 (${successCount}/${total})`;
|
||||
} else {
|
||||
message += ` - 成功: ${successCount}, 失败: ${failCount}`;
|
||||
}
|
||||
|
||||
if (this.currentProgress) {
|
||||
this.currentProgress.finish(message, failCount === 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
87
src/core/publisher-interface.ts
Normal file
87
src/core/publisher-interface.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 文件:publisher-interface.ts
|
||||
* 作用:统一的发布平台接口定义
|
||||
*/
|
||||
|
||||
export interface PublishResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
url?: string;
|
||||
data?: any;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export interface PublishOptions {
|
||||
title?: string;
|
||||
cover?: string;
|
||||
excerpt?: string;
|
||||
tags?: string[];
|
||||
openComment?: boolean;
|
||||
onlyFansComment?: boolean;
|
||||
}
|
||||
|
||||
export interface IPlatformPublisher {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly displayName: string;
|
||||
|
||||
/**
|
||||
* 验证平台配置是否有效
|
||||
*/
|
||||
validateConfig(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 生成预览内容
|
||||
*/
|
||||
generatePreview(content: string, options?: PublishOptions): Promise<string>;
|
||||
|
||||
/**
|
||||
* 发布内容到平台
|
||||
*/
|
||||
publish(content: string, options?: PublishOptions): Promise<PublishResult>;
|
||||
|
||||
/**
|
||||
* 上传图片到平台
|
||||
*/
|
||||
uploadImage?(imageData: ArrayBuffer, filename: string): Promise<string>;
|
||||
|
||||
/**
|
||||
* 获取平台特定的设置组件
|
||||
*/
|
||||
getSettingsComponent?(): HTMLElement | undefined;
|
||||
}
|
||||
|
||||
export abstract class BasePlatformPublisher implements IPlatformPublisher {
|
||||
abstract readonly id: string;
|
||||
abstract readonly name: string;
|
||||
abstract readonly displayName: string;
|
||||
|
||||
abstract validateConfig(): Promise<boolean>;
|
||||
abstract generatePreview(content: string, options?: PublishOptions): Promise<string>;
|
||||
abstract publish(content: string, options?: PublishOptions): Promise<PublishResult>;
|
||||
|
||||
async uploadImage(imageData: ArrayBuffer, filename: string): Promise<string> {
|
||||
throw new Error('Image upload not implemented for this platform');
|
||||
}
|
||||
|
||||
getSettingsComponent(): HTMLElement | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected createSuccessResult(message: string, url?: string, data?: any): PublishResult {
|
||||
return {
|
||||
success: true,
|
||||
message,
|
||||
url,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
protected createErrorResult(message: string, error?: Error): PublishResult {
|
||||
return {
|
||||
success: false,
|
||||
message,
|
||||
error
|
||||
};
|
||||
}
|
||||
}
|
||||
192
src/core/publisher-manager.ts
Normal file
192
src/core/publisher-manager.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* 文件:publisher-manager.ts
|
||||
* 作用:发布平台管理器,统一管理所有发布平台
|
||||
*/
|
||||
|
||||
import { IPlatformPublisher, PublishResult, PublishOptions } from './publisher-interface';
|
||||
import { ErrorHandler, PlatformError } from './error-handler';
|
||||
import { ProgressIndicator } from './progress-indicator';
|
||||
|
||||
export class PublisherManager {
|
||||
private publishers = new Map<string, IPlatformPublisher>();
|
||||
private static instance: PublisherManager;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): PublisherManager {
|
||||
if (!this.instance) {
|
||||
this.instance = new PublisherManager();
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册发布平台
|
||||
*/
|
||||
register(publisher: IPlatformPublisher): void {
|
||||
this.publishers.set(publisher.id, publisher);
|
||||
console.log(`[PublisherManager] 注册发布平台: ${publisher.displayName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销发布平台
|
||||
*/
|
||||
unregister(publisherId: string): void {
|
||||
if (this.publishers.delete(publisherId)) {
|
||||
console.log(`[PublisherManager] 注销发布平台: ${publisherId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册的发布平台
|
||||
*/
|
||||
getPublishers(): IPlatformPublisher[] {
|
||||
return Array.from(this.publishers.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定的发布平台
|
||||
*/
|
||||
getPublisher(publisherId: string): IPlatformPublisher | undefined {
|
||||
return this.publishers.get(publisherId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查平台是否可用
|
||||
*/
|
||||
async isPlatformAvailable(publisherId: string): Promise<boolean> {
|
||||
const publisher = this.getPublisher(publisherId);
|
||||
if (!publisher) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return await publisher.validateConfig();
|
||||
} catch (error) {
|
||||
console.warn(`[PublisherManager] 平台 ${publisherId} 验证失败:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布内容到指定平台
|
||||
*/
|
||||
async publish(
|
||||
platformId: string,
|
||||
content: string,
|
||||
options?: PublishOptions
|
||||
): Promise<PublishResult> {
|
||||
const progress = new ProgressIndicator();
|
||||
|
||||
try {
|
||||
progress.start(`准备发布到 ${platformId}`);
|
||||
|
||||
const publisher = this.getPublisher(platformId);
|
||||
if (!publisher) {
|
||||
throw new PlatformError(`发布平台 ${platformId} 未找到`, platformId);
|
||||
}
|
||||
|
||||
progress.update('验证平台配置');
|
||||
const isValid = await publisher.validateConfig();
|
||||
if (!isValid) {
|
||||
throw new PlatformError(`发布平台 ${platformId} 配置无效`, platformId);
|
||||
}
|
||||
|
||||
progress.update('发布内容');
|
||||
const result = await publisher.publish(content, options);
|
||||
|
||||
if (result.success) {
|
||||
progress.finish(`发布成功到 ${publisher.displayName}`);
|
||||
} else {
|
||||
progress.error(`发布失败到 ${publisher.displayName}: ${result.message}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
const errorMsg = `发布到 ${platformId} 失败`;
|
||||
progress.error(errorMsg);
|
||||
ErrorHandler.handle(error as Error, 'PublisherManager.publish');
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMsg,
|
||||
error: error as Error
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量发布到多个平台
|
||||
*/
|
||||
async batchPublish(
|
||||
platformIds: string[],
|
||||
content: string,
|
||||
options?: PublishOptions
|
||||
): Promise<Map<string, PublishResult>> {
|
||||
const results = new Map<string, PublishResult>();
|
||||
|
||||
console.log(`[PublisherManager] 开始批量发布到 ${platformIds.length} 个平台`);
|
||||
|
||||
for (const platformId of platformIds) {
|
||||
try {
|
||||
const result = await this.publish(platformId, content, options);
|
||||
results.set(platformId, result);
|
||||
|
||||
// 添加延迟以避免频率限制
|
||||
if (platformIds.length > 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
}
|
||||
} catch (error) {
|
||||
results.set(platformId, {
|
||||
success: false,
|
||||
message: `批量发布失败: ${error}`,
|
||||
error: error as Error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const successCount = Array.from(results.values()).filter(r => r.success).length;
|
||||
const totalCount = platformIds.length;
|
||||
|
||||
console.log(`[PublisherManager] 批量发布完成: ${successCount}/${totalCount} 成功`);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成预览内容
|
||||
*/
|
||||
async generatePreview(
|
||||
platformId: string,
|
||||
content: string,
|
||||
options?: PublishOptions
|
||||
): Promise<string> {
|
||||
return await ErrorHandler.withErrorHandling(async () => {
|
||||
const publisher = this.getPublisher(platformId);
|
||||
if (!publisher) {
|
||||
throw new PlatformError(`发布平台 ${platformId} 未找到`, platformId);
|
||||
}
|
||||
|
||||
return await publisher.generatePreview(content, options);
|
||||
}, 'PublisherManager.generatePreview', '') || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台状态信息
|
||||
*/
|
||||
async getPlatformStatus(): Promise<Map<string, boolean>> {
|
||||
const status = new Map<string, boolean>();
|
||||
|
||||
for (const [id, publisher] of this.publishers) {
|
||||
try {
|
||||
const isAvailable = await publisher.validateConfig();
|
||||
status.set(id, isAvailable);
|
||||
} catch {
|
||||
status.set(id, false);
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
}
|
||||
29
src/main.ts
29
src/main.ts
@@ -19,6 +19,12 @@ import { XiaohongshuLoginModal } from './xiaohongshu/login-modal';
|
||||
import { XiaohongshuContentAdapter } from './xiaohongshu/adapter';
|
||||
import { XiaohongshuAPIManager } from './xiaohongshu/api';
|
||||
|
||||
// Core modules
|
||||
import { ErrorHandler } from './core/error-handler';
|
||||
import { ConfigManager } from './core/config-manager';
|
||||
import { PublisherManager } from './core/publisher-manager';
|
||||
import { ProgressIndicator } from './core/progress-indicator';
|
||||
|
||||
/**
|
||||
* Note2AnyPlugin
|
||||
*
|
||||
@@ -39,15 +45,38 @@ export default class Note2AnyPlugin extends Plugin {
|
||||
settings: NMPSettings;
|
||||
assetsManager: AssetsManager;
|
||||
ribbonIconEl: HTMLElement | null = null;
|
||||
|
||||
// Core managers
|
||||
private configManager: ConfigManager;
|
||||
private publisherManager: PublisherManager;
|
||||
|
||||
constructor(app: App, manifest: PluginManifest) {
|
||||
super(app, manifest);
|
||||
AssetsManager.setup(app, manifest);
|
||||
this.assetsManager = AssetsManager.getInstance();
|
||||
this.publisherManager = PublisherManager.getInstance();
|
||||
}
|
||||
|
||||
async loadResource() {
|
||||
const progress = new ProgressIndicator();
|
||||
progress.start('加载插件资源');
|
||||
|
||||
try {
|
||||
await this.loadSettings();
|
||||
|
||||
// 初始化配置管理器
|
||||
ConfigManager.initialize(this.settings);
|
||||
this.configManager = ConfigManager.getInstance();
|
||||
|
||||
progress.update('加载主题资源');
|
||||
await this.assetsManager.loadAssets();
|
||||
|
||||
progress.finish('插件资源加载完成');
|
||||
} catch (error) {
|
||||
progress.error('插件资源加载失败');
|
||||
ErrorHandler.handle(error as Error, 'loadResource');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async onload() {
|
||||
|
||||
@@ -21,6 +21,10 @@ import { NMPSettings } from './settings';
|
||||
import AssetsManager from './assets';
|
||||
import { waitForLayoutReady, uevent } from './utils';
|
||||
import { LocalFile } from './markdown/local-file';
|
||||
import { ErrorHandler } from './core/error-handler';
|
||||
import { ProgressIndicator } from './core/progress-indicator';
|
||||
import { ConfigManager } from './core/config-manager';
|
||||
import { ContentProcessor } from './core/content-processor';
|
||||
|
||||
export const VIEW_TYPE_NOTE_PREVIEW = 'note-preview';
|
||||
|
||||
@@ -101,6 +105,11 @@ export class PreviewView extends ItemView {
|
||||
*/
|
||||
async onOpen(): Promise<void> {
|
||||
console.log('[PreviewView] 视图打开 layoutReady=', this.app.workspace.layoutReady);
|
||||
|
||||
const progress = new ProgressIndicator();
|
||||
progress.start('初始化预览视图');
|
||||
|
||||
try {
|
||||
// 不在未完成 layoutReady 时做重初始化,改为延迟
|
||||
if (!this.app.workspace.layoutReady) {
|
||||
this.showLoading();
|
||||
@@ -112,20 +121,31 @@ export class PreviewView extends ItemView {
|
||||
return;
|
||||
}
|
||||
await this.performInitialization();
|
||||
progress.finish('预览视图初始化完成');
|
||||
} catch (error) {
|
||||
progress.error('预览视图初始化失败');
|
||||
ErrorHandler.handle(error as Error, 'PreviewView.onOpen');
|
||||
}
|
||||
}
|
||||
|
||||
private async performInitialization(): Promise<void> {
|
||||
const progress = new ProgressIndicator();
|
||||
|
||||
try {
|
||||
const start = performance.now();
|
||||
this.showLoading();
|
||||
|
||||
progress.update('初始化设置');
|
||||
console.time('[PreviewView] initializeSettings');
|
||||
await this.initializeSettings();
|
||||
console.timeEnd('[PreviewView] initializeSettings');
|
||||
|
||||
progress.update('创建管理器');
|
||||
console.time('[PreviewView] createManager');
|
||||
await this.createManager();
|
||||
console.timeEnd('[PreviewView] createManager');
|
||||
|
||||
progress.update('注册事件监听器');
|
||||
console.time('[PreviewView] registerEventListeners');
|
||||
this.registerEventListeners();
|
||||
console.timeEnd('[PreviewView] registerEventListeners');
|
||||
@@ -142,16 +162,13 @@ export class PreviewView extends ItemView {
|
||||
}
|
||||
|
||||
console.log('[PreviewView] 初始化耗时(ms):', (performance.now() - start).toFixed(1));
|
||||
progress.finish('视图初始化完成');
|
||||
uevent('open');
|
||||
} catch (error) {
|
||||
console.error('[PreviewView] 初始化失败:', error);
|
||||
new Notice('预览视图初始化失败: ' + (error instanceof Error ? error.message : String(error)));
|
||||
const container = this.containerEl.children[1] as HTMLElement;
|
||||
container.empty();
|
||||
const errorDiv = container.createDiv({ cls: 'preview-error' });
|
||||
errorDiv.createEl('h3', { text: '预览视图初始化失败' });
|
||||
errorDiv.createEl('p', { text: error instanceof Error ? error.message : String(error) });
|
||||
errorDiv.createEl('p', { text: '请尝试重新加载插件或查看控制台获取更多信息' });
|
||||
progress.error('视图初始化失败');
|
||||
ErrorHandler.handle(error as Error, 'PreviewView.performInitialization');
|
||||
console.error('[PreviewView] 初始化失败', error);
|
||||
this.showError('预览视图初始化失败,请检查插件设置');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,6 +348,18 @@ export class PreviewView extends ItemView {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误信息
|
||||
*/
|
||||
private showError(message: string): void {
|
||||
const container = this.containerEl.children[1] as HTMLElement;
|
||||
container.empty();
|
||||
const errorDiv = container.createDiv({ cls: 'preview-error' });
|
||||
errorDiv.createEl('h3', { text: '预览视图错误' });
|
||||
errorDiv.createEl('p', { text: message });
|
||||
errorDiv.createEl('p', { text: '请尝试重新加载插件或查看控制台获取更多信息' });
|
||||
}
|
||||
|
||||
/** 外部接口:切换平台 */
|
||||
async changePlatform(platform: 'wechat' | 'xiaohongshu') {
|
||||
await this.manager?.switchPlatform(platform as any);
|
||||
|
||||
@@ -157,9 +157,16 @@ SOLVE:obsidian控制台打印信息,定位在哪里阻塞,AI修复。
|
||||
自己写布局demo原型,让codex参考布局修改(原来元素美化的css可保留)。
|
||||
demo原型可以手绘后,拍照让chatgpt生成,在此基础上自己修改。
|
||||
|
||||
## 优化
|
||||
1. 全量扫描系统,从设计、结构、冗余度、优雅、可维护性方便,检查是否有优化的地方。
|
||||
✅
|
||||
|
||||
2. 后续在主题切换和使用上的独立性和便捷性。
|
||||
|
||||
## 思路
|
||||
1. 网上图片模版,让AI快速生成css themes主题。快速套用。‼️
|
||||
**样式和功能必须结构,样式必须可以0基础,快速选择,让用户可以选择足够多样式(AI生成)。**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"1.3.7": "1.4.5",
|
||||
"1.3.12": "1.4.5"
|
||||
"1.3.12": "1.4.5",
|
||||
"1.4.0": "1.4.5"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user