feat(halo-plugin): 新增文章导入、删除和标签分类管理功能
添加从本地 Markdown 文件导入创建文章的功能,支持文件预览和自动发布选项 新增文章删除命令,支持选择性删除 Halo 文章或本地文件 添加标签和分类管理功能,支持创建、编辑和删除操作 更新国际化文案,支持新功能的多种语言界面 扩展服务层以支持文章导入、删除和标签分类管理 API 调用 更新插件版本至 2.1.1 并更新作者信息
This commit is contained in:
Vendored
+7
-2
@@ -2,10 +2,15 @@
|
||||
"sites": [
|
||||
{
|
||||
"name": "Serendipity",
|
||||
"url": "https://blog.metarl.cc.cd",
|
||||
"url": "http://101.133.128.193:8091",
|
||||
"default": true,
|
||||
"token": "pat_eyJraWQiOiI3REcwM05yaUJ0VVBPM2oxbkN4T0hUNXZsSWlJRTVJYndGNWo2NW43eTRBIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2Jsb2cubWV0YXJsLmNjLmNkLyIsInN1YiI6ImxpdWhhbmd5diIsImlhdCI6MTc3NzE5MDgyNSwianRpIjoiMTI4NDdhZWEtY2Q0YS04OTZjLTQ3ODMtYTc4ZDc5MTRhN2Y1IiwicGF0X25hbWUiOiJwYXQteGF1NWJoZmsifQ.WYXv8eXrc9C5PXMbC58LpjzhC1OFgZyOQ2iKc-_M8xfbhD4pgsdb1Q1uGK16_h45x7OAHcA8eThfdqUJGcuYeiYasoffBfOIVRj2l-YjHHP4RqE_tks_LP7GnZoE6jzz52ZkvmiiPwHt7eusIF563CYSr2vHOq8iAcFZPLHtoviokwG8cYmxKntIkdDU3MtvhzKOrUwEnQWjzbNwYeoYDr_9jLvQegoxrX7uosJPmVbMvw2IkD1xFvTKITxreMRiPzZIH2HSBhVMW9vpu_l7FaOFeQU2Cz_hAuuayCj8FIgZ55zA0lcHPYksHWMTa5f5BelPJfms_Ksn9rpVXfiggC6R7TfCRUY2QPY1IDZCS6v3omWjBBTfC2CgYDtiVE2Omec5kQ7XIgIAkp_4tFdVay1_1qLkJXcl9hDVs9gPNgyNMDKlw30mhU0vxM9SbEHR1WtCFEChYyPso84dWcy23NDY5aCbn-49Xr2EQXhjD-3EZ4rs1V13j-LGEQdsKzB72ERhq2Cv9YPjRR0qsvk9rZ5w4Mj8xQ_3ns57vHinmz4jsm7BDa1FyEd-hmgZXt6P2pkyAH3kQzJLaDjglisEej90hhXvQbImf4hDldNpLaKDH3z3cLFqGrujrGtwEE7x0czG5rfxWutUDXcgztSgQppZfk3H71147mChEpZSW_o"
|
||||
}
|
||||
],
|
||||
"publishByDefault": true
|
||||
"publishByDefault": true,
|
||||
"imageUpload": {
|
||||
"enabled": true,
|
||||
"uploadPath": "",
|
||||
"preserveOriginal": false
|
||||
}
|
||||
}
|
||||
Vendored
+6
-6
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -4,7 +4,7 @@
|
||||
"version": "1.1.1",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Halo's Obsidian integration supports publishing content to Halo sites",
|
||||
"author": "Ryan Wang",
|
||||
"authorUrl": "https://github.com/ruibaby",
|
||||
"author": "刘航宇 (LHY)",
|
||||
"authorUrl": "https://github.com/LHY0125/obsidian-halo",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
|
||||
+38
-10
@@ -1,24 +1,56 @@
|
||||
{
|
||||
"recentFiles": [
|
||||
{
|
||||
"basename": "SEO分析报告",
|
||||
"path": "博客/SEO分析报告.md"
|
||||
"basename": "Git团队协作指南(精简版)",
|
||||
"path": "博客/Git团队协作指南(精简版).md"
|
||||
},
|
||||
{
|
||||
"basename": "Git团队协作指南大纲",
|
||||
"path": "博客/Git团队协作指南大纲.md"
|
||||
},
|
||||
{
|
||||
"basename": "uv工具推荐博客",
|
||||
"path": "博客/uv工具推荐博客.md"
|
||||
},
|
||||
{
|
||||
"basename": "OpenClaw安装教程",
|
||||
"path": "博客/OpenClaw安装教程.md"
|
||||
},
|
||||
{
|
||||
"basename": "LinearRegression线性回归",
|
||||
"path": "博客/LinearRegression线性回归.md"
|
||||
},
|
||||
{
|
||||
"basename": "Git团队协作指南",
|
||||
"path": "博客/Git团队协作指南.md"
|
||||
},
|
||||
{
|
||||
"basename": "大数据技术栈",
|
||||
"path": "博客/大数据技术栈.md"
|
||||
},
|
||||
{
|
||||
"basename": "大模型赋能架构设计",
|
||||
"path": "博客/大模型赋能架构设计.md"
|
||||
},
|
||||
{
|
||||
"basename": "博客爬取报告",
|
||||
"path": "博客/博客爬取报告.md"
|
||||
},
|
||||
{
|
||||
"basename": "文章列表",
|
||||
"path": "博客/文章列表.md"
|
||||
},
|
||||
{
|
||||
"basename": "从全连接层到卷积",
|
||||
"path": "博客/从全连接层到卷积.md"
|
||||
},
|
||||
{
|
||||
"basename": "CLI在AI时代的浴火重生",
|
||||
"path": "博客/CLI在AI时代的浴火重生.md"
|
||||
"basename": "SEO分析报告",
|
||||
"path": "博客/SEO分析报告.md"
|
||||
},
|
||||
{
|
||||
"basename": "Git团队协作指南(精简版)",
|
||||
"path": "博客/Git团队协作指南(精简版).md"
|
||||
"basename": "CLI在AI时代的浴火重生",
|
||||
"path": "博客/CLI在AI时代的浴火重生.md"
|
||||
},
|
||||
{
|
||||
"basename": "halo",
|
||||
@@ -40,10 +72,6 @@
|
||||
"basename": "视觉语言模型技术综述",
|
||||
"path": "博客/视觉语言模型技术综述.md"
|
||||
},
|
||||
{
|
||||
"basename": "大数据技术栈",
|
||||
"path": "博客/大数据技术栈.md"
|
||||
},
|
||||
{
|
||||
"basename": "小组会议",
|
||||
"path": "小组会议/26.04.04/小组会议.md"
|
||||
|
||||
Vendored
+19
-20
@@ -13,12 +13,12 @@
|
||||
"state": {
|
||||
"type": "markdown",
|
||||
"state": {
|
||||
"file": "博客/SEO分析报告.md",
|
||||
"file": "博客/Git团队协作指南(精简版).md",
|
||||
"mode": "source",
|
||||
"source": false
|
||||
},
|
||||
"icon": "lucide-file",
|
||||
"title": "SEO分析报告"
|
||||
"title": "Git团队协作指南(精简版)"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -193,8 +193,7 @@
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"width": 426.5,
|
||||
"collapsed": true
|
||||
"width": 426.5
|
||||
},
|
||||
"left-ribbon": {
|
||||
"hiddenItems": {
|
||||
@@ -221,20 +220,29 @@
|
||||
},
|
||||
"active": "e7a7b303c61786dc",
|
||||
"lastOpenFiles": [
|
||||
"博客/Git团队协作指南大纲.md",
|
||||
"博客/uv工具推荐博客.md",
|
||||
"博客/OpenClaw安装教程.md",
|
||||
"博客/LinearRegression线性回归.md",
|
||||
"博客/Git团队协作指南.md",
|
||||
"博客/大数据技术栈.md",
|
||||
"博客/大模型赋能架构设计.md",
|
||||
"博客/博客爬取报告.md",
|
||||
"obsidian-halo/src/commands/manage-taxonomy.ts",
|
||||
"博客/文章列表.md",
|
||||
"博客/从全连接层到卷积.md",
|
||||
"obsidian-halo/src/modals/category-manager-modal.ts",
|
||||
"obsidian-halo/src/modals/tag-manager-modal.ts",
|
||||
"博客/SEO分析报告.md",
|
||||
"obsidian-halo/src/commands/import-markdown.ts",
|
||||
"obsidian-halo/src/modals/file-preview-modal.ts",
|
||||
"obsidian-halo/src/modals",
|
||||
"obsidian-halo/src/commands",
|
||||
"博客/CLI在AI时代的浴火重生.md",
|
||||
"博客/Git团队协作指南(精简版).md",
|
||||
"obsidian-halo/src/service/image-uploader.ts",
|
||||
"obsidian-halo/src/utils/image.ts",
|
||||
"博客/check-credentials.ps1",
|
||||
"博客/get-halo-token.js",
|
||||
"博客/halo-upload.py",
|
||||
"obsidian-halo/node_modules/@halo-dev",
|
||||
"obsidian-halo/node_modules/@rslib",
|
||||
"obsidian-halo/node_modules/@biomejs",
|
||||
"obsidian-halo/node_modules/@types",
|
||||
"obsidian-halo/node_modules",
|
||||
"obsidian-halo-zip-test/images/pat-zh.png",
|
||||
"obsidian-halo-zip-test/images/settings-zh.png",
|
||||
"obsidian-halo-zip-test/images/settings-en.png",
|
||||
@@ -255,18 +263,9 @@
|
||||
"博客/articles/article-02-python-efficiency.md",
|
||||
"博客/articles/article-01-ai-beginner-guide.md",
|
||||
"博客/视觉语言模型技术综述.md",
|
||||
"博客/大数据技术栈.md",
|
||||
"find-skills-0.1.0/SKILL.md",
|
||||
"小组会议/26.04.04/小组会议.md",
|
||||
"小组会议/26.03.21 - 副本/小组会议.md",
|
||||
"小组会议/26.03.21/小组会议.md",
|
||||
"小组会议/26.02.28/小组会议.md",
|
||||
"小组会议/25.10.26/小组成员基本信息/小组成员信息/王宇晗.md",
|
||||
"小组会议/25.10.26/小组成员基本信息/小组成员信息/彭依萍.md",
|
||||
"小组会议/25.10.26/小组成员基本信息/小组成员信息/赵帅尧.md",
|
||||
"小组会议/25.10.26/小组成员基本信息/职位安排.md",
|
||||
"小组会议/25.10.26/小组成员基本信息/小组成员信息/李俊杰.md",
|
||||
"小组会议/26.02.06/小组会议.md",
|
||||
"未命名.canvas"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
# Obsidian Halo 插件功能增强计划
|
||||
|
||||
## 一、功能对比分析
|
||||
|
||||
### Halo CLI 已有功能
|
||||
|
||||
| 分类 | 功能 | CLI 支持 |
|
||||
|------|------|---------|
|
||||
| **文章管理** | 发布文章 | ✅ 完整 |
|
||||
| | 更新文章 | ✅ 完整 |
|
||||
| | 删除文章 | ✅ 完整 |
|
||||
| | 列出文章列表 | ✅ 完整 |
|
||||
| | 查看文章详情 | ✅ 完整 |
|
||||
| | 批量操作 | ✅ PowerShell 脚本 |
|
||||
| **导出/导入** | 导出为 JSON | ✅ 完整 |
|
||||
| | 导出为 Markdown | ✅ 完整 |
|
||||
| | 从 JSON 导入 | ✅ 完整 |
|
||||
| | 从 Markdown 导入 | ✅ 完整 |
|
||||
| **标签管理** | 列出标签 | ✅ 完整 |
|
||||
| | 创建标签 | ✅ 完整 |
|
||||
| | 更新标签 | ✅ 完整 |
|
||||
| | 删除标签 | ✅ 完整 |
|
||||
| | 为文章设置标签 | ✅ 完整 |
|
||||
| **分类管理** | 列出分类 | ✅ 完整 |
|
||||
| | 创建分类 | ✅ 完整 |
|
||||
| | 更新分类 | ✅ 完整 |
|
||||
| | 删除分类 | ✅ 完整 |
|
||||
| | 设置优先级 | ✅ 完整 |
|
||||
| **其他内容** | 单页管理 | ✅ 完整 |
|
||||
| | 搜索内容 | ✅ 完整 |
|
||||
| **系统管理** | 附件管理 | ✅ 完整 |
|
||||
| | 备份管理 | ✅ 完整 |
|
||||
| | 动态管理 | ✅ 完整 |
|
||||
| | 评论管理 | ✅ 完整 |
|
||||
| | 插件管理 | ✅ 完整 |
|
||||
| | 主题管理 | ✅ 完整 |
|
||||
|
||||
---
|
||||
|
||||
## 二、Obsidian 插件当前功能
|
||||
|
||||
| 分类 | 功能 | 状态 |
|
||||
|------|------|------|
|
||||
| **文章管理** | 发布文章 | ✅ 已实现 |
|
||||
| | 更新文章(同步 Halo 到本地) | ✅ 已实现 |
|
||||
| | 列出文章列表 | ❌ 缺失 |
|
||||
| | 查看文章详情 | ❌ 缺失 |
|
||||
| | 删除文章 | ❌ 缺失 |
|
||||
| | 批量发布 | ❌ 缺失 |
|
||||
| **图片上传** | 自动上传图片 | ✅ 已实现(新增) |
|
||||
| **标签/分类** | 为文章设置标签 | ✅ 已实现 |
|
||||
| | 为文章设置分类 | ✅ 已实现 |
|
||||
| | 创建标签 | ❌ 缺失 |
|
||||
| | 创建分类 | ❌ 缺失 |
|
||||
| | 管理标签/分类 | ❌ 缺失 |
|
||||
| **导出/导入** | 从 Markdown 导入 | ❌ 缺失 |
|
||||
| | 导出为 Markdown | ❌ 缺失 |
|
||||
| | 导出为 JSON | ❌ 缺失 |
|
||||
| **其他** | 单页管理 | ❌ 缺失 |
|
||||
| | 搜索 | ❌ 缺失 |
|
||||
|
||||
---
|
||||
|
||||
## 三、功能优先级建议
|
||||
|
||||
### 高优先级(核心功能)
|
||||
|
||||
1. **文章列表查看**
|
||||
- 在命令面板中显示 Halo 文章列表
|
||||
- 支持分页、筛选(草稿/已发布)
|
||||
- 快速预览文章信息
|
||||
|
||||
2. **导入功能增强**
|
||||
- 支持从本地 Markdown 文件导入创建文章
|
||||
- 智能解析文件路径和文件名
|
||||
|
||||
3. **文章删除功能**
|
||||
- 添加删除命令(需二次确认)
|
||||
- 支持删除本地文件或仅删除 Halo 文章
|
||||
|
||||
### 中优先级(常用功能)
|
||||
|
||||
4. **批量操作**
|
||||
- 批量发布多篇文章
|
||||
- 批量同步已发布文章
|
||||
- 批量管理(删除、更新标签等)
|
||||
|
||||
5. **标签/分类管理**
|
||||
- 创建新标签/分类
|
||||
- 查看现有标签/分类列表
|
||||
- 更新/删除标签/分类
|
||||
|
||||
6. **导出功能**
|
||||
- 导出文章为 Markdown
|
||||
- 导出文章为 JSON(备份用)
|
||||
|
||||
### 低优先级(增强功能)
|
||||
|
||||
7. **搜索功能**
|
||||
- 搜索 Halo 文章
|
||||
- 快速定位文章
|
||||
|
||||
8. **单页管理**
|
||||
- 发布/管理独立页面
|
||||
- 与文章类似的工作流
|
||||
|
||||
---
|
||||
|
||||
## 四、实施步骤
|
||||
|
||||
### 第一阶段:核心补充(1-2 周)
|
||||
|
||||
#### 任务 1:文章列表功能
|
||||
- [ ] 创建文章列表 Modal
|
||||
- [ ] 实现分页、筛选功能
|
||||
- [ ] 添加快速操作按钮(查看、编辑、删除)
|
||||
- [ ] 集成到命令面板
|
||||
|
||||
#### 任务 2:导入功能
|
||||
- [ ] 实现从文件导入创建文章
|
||||
- [ ] 智能解析 Markdown 文件
|
||||
- [ ] 支持选择文件对话框
|
||||
|
||||
#### 任务 3:删除功能
|
||||
- [ ] 添加删除命令
|
||||
- [ ] 二次确认机制
|
||||
- [ ] 删除选项(仅 Halo / 仅本地 / 全部)
|
||||
|
||||
### 第二阶段:常用功能(2-4 周)
|
||||
|
||||
#### 任务 4:批量操作
|
||||
- [ ] 批量选择界面
|
||||
- [ ] 批量发布队列
|
||||
- [ ] 进度显示和错误处理
|
||||
|
||||
#### 任务 5:标签/分类管理
|
||||
- [ ] 标签管理 Modal
|
||||
- [ ] 分类管理 Modal
|
||||
- [ ] 创建/编辑/删除功能
|
||||
|
||||
#### 任务 6:导出功能
|
||||
- [ ] 导出为 Markdown
|
||||
- [ ] 导出为 JSON(备份)
|
||||
- [ ] 选择保存路径
|
||||
|
||||
### 第三阶段:增强功能(4+ 周)
|
||||
|
||||
#### 任务 7:搜索功能
|
||||
- [ ] 文章搜索 Modal
|
||||
- [ ] 快速筛选和定位
|
||||
|
||||
#### 任务 8:单页管理
|
||||
- [ ] 单页发布命令
|
||||
- [ ] 单页列表查看
|
||||
- [ ] 单页导入/导出
|
||||
|
||||
---
|
||||
|
||||
## 五、技术实现建议
|
||||
|
||||
### API 端点
|
||||
|
||||
| 功能 | API 端点 |
|
||||
|------|----------|
|
||||
| 列出文章 | `GET /apis/uc.api.content.halo.run/v1alpha1/posts` |
|
||||
| 获取文章 | `GET /apis/uc.api.content.halo.run/v1alpha1/posts/{name}` |
|
||||
| 删除文章 | `DELETE /apis/uc.api.content.halo.run/v1alpha1/posts/{name}` |
|
||||
| 列出标签 | `GET /apis/content.halo.run/v1alpha1/tags` |
|
||||
| 创建标签 | `POST /apis/content.halo.run/v1alpha1/tags` |
|
||||
| 列出分类 | `GET /apis/content.halo.run/v1alpha1/categories` |
|
||||
| 创建分类 | `POST /apis/content.halo.run/v1alpha1/categories` |
|
||||
|
||||
### 文件结构建议
|
||||
|
||||
```
|
||||
src/
|
||||
├── commands/ # 命令相关
|
||||
│ ├── publish.ts
|
||||
│ ├── delete.ts
|
||||
│ ├── list.ts
|
||||
│ ├── import.ts
|
||||
│ └── export.ts
|
||||
├── modals/ # 弹窗相关
|
||||
│ ├── post-list-modal.ts
|
||||
│ ├── tag-manager-modal.ts
|
||||
│ ├── category-manager-modal.ts
|
||||
│ └── search-modal.ts
|
||||
├── service/ # 服务层
|
||||
│ ├── index.ts # 现有
|
||||
│ └── image-uploader.ts # 现有
|
||||
└── utils/ # 工具层
|
||||
├── image.ts # 现有
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、建议实施顺序
|
||||
|
||||
1. **先实现文章列表**(最常用)
|
||||
2. **再实现导入功能**(补全工作流)
|
||||
3. **实现删除功能**(完整性)
|
||||
4. **实现批量操作**(效率提升)
|
||||
5. **实现标签/分类管理**(内容组织)
|
||||
6. **实现导出功能**(备份需求)
|
||||
7. **实现搜索功能**(快速定位)
|
||||
8. **实现单页管理**(可选)
|
||||
|
||||
---
|
||||
|
||||
请确认需要实现哪些功能,我将制定详细的技术方案。
|
||||
@@ -0,0 +1,33 @@
|
||||
# Markdown 文件导入功能检查清单
|
||||
|
||||
## 代码实现检查
|
||||
|
||||
- [ ] `src/commands/import-markdown.ts` 文件创建完成
|
||||
- [ ] 文件选择器正确实现(仅显示 .md 文件)
|
||||
- [ ] 文件读取和解析逻辑正确
|
||||
- [ ] 文章创建逻辑正确调用 Halo API
|
||||
- [ ] `src/modals/file-preview-modal.ts` 文件创建完成
|
||||
- [ ] 文件内容预览正确显示
|
||||
- [ ] frontmatter 信息正确显示
|
||||
- [ ] 导入/取消按钮功能正常
|
||||
- [ ] "导入后发布"选项功能正常
|
||||
|
||||
## 集成检查
|
||||
|
||||
- [ ] `src/main.ts` 中命令注册完成
|
||||
- [ ] 命令名称符合国际化规范
|
||||
- [ ] 命令在命令面板中可见
|
||||
|
||||
## 国际化检查
|
||||
|
||||
- [ ] 英文文案添加完成
|
||||
- [ ] 简体中文文案添加完成
|
||||
- [ ] 繁体中文文案添加完成
|
||||
|
||||
## 测试检查
|
||||
|
||||
- [ ] 编译通过无错误
|
||||
- [ ] 插件文件正确复制到 Obsidian 插件目录
|
||||
- [ ] 文件选择器测试通过
|
||||
- [ ] 导入功能测试通过(创建草稿)
|
||||
- [ ] 导入功能测试通过(导入并发布)
|
||||
@@ -0,0 +1,96 @@
|
||||
# Markdown 文件导入功能规格说明
|
||||
|
||||
## Why
|
||||
|
||||
当前 Obsidian Halo 插件只支持从 Obsidian 发布文章到 Halo,但没有提供从本地 Markdown 文件导入创建文章的功能。用户可能希望:
|
||||
1. 直接从文件系统导入已存在的 Markdown 文件
|
||||
2. 从其他平台迁移文章时批量导入
|
||||
3. 快速创建新文章时选择本地文件而非在编辑器中编写
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增命令:直接从 Markdown 文件导入创建 Halo 文章
|
||||
- 新增文件选择器:支持选择本地 .md 文件
|
||||
- 新增预览功能:导入前预览文件内容和 frontmatter
|
||||
- 新增选项:导入时是否立即发布
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs:
|
||||
- 文章发布功能:提供新的文章创建入口
|
||||
- 设置功能:可能需要添加导入相关配置
|
||||
- Affected code:
|
||||
- 新增 `src/commands/import-markdown.ts`:导入命令
|
||||
- 新增 `src/modals/file-preview-modal.ts`:文件预览弹窗
|
||||
- 修改 `src/main.ts`:注册新命令
|
||||
- 修改 `src/i18n/locales/*.json`:国际化文案
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 从 Markdown 文件导入
|
||||
|
||||
系统 SHALL 提供从本地 Markdown 文件导入创建 Halo 文章的功能。
|
||||
|
||||
#### Scenario: 选择文件导入
|
||||
- **WHEN** 用户执行"从 Markdown 文件导入"命令
|
||||
- **THEN** 系统显示文件选择器,允许用户选择 .md 文件
|
||||
|
||||
#### Scenario: 预览和确认
|
||||
- **WHEN** 用户选择了一个 Markdown 文件
|
||||
- **THEN** 系统显示文件内容预览(标题、内容摘要、frontmatter)
|
||||
- **AND** 显示"导入"和"取消"按钮
|
||||
|
||||
#### Scenario: 导入成功
|
||||
- **WHEN** 用户点击"导入"按钮
|
||||
- **THEN** 系统读取文件内容
|
||||
- **AND** 解析 frontmatter 和正文
|
||||
- **AND** 调用 Halo API 创建文章
|
||||
- **AND** 显示导入成功通知
|
||||
|
||||
#### Scenario: 导入选项
|
||||
- **WHEN** 用户可以勾选"导入后立即发布"选项
|
||||
- **THEN** 文章创建后自动发布
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 命令面板扩展
|
||||
|
||||
现有命令列表扩展,新增:
|
||||
- `Halo: 从 Markdown 文件导入` - 打开文件选择器进行导入
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
无
|
||||
|
||||
---
|
||||
|
||||
## 技术实现细节
|
||||
|
||||
### Obsidian 文件选择器 API
|
||||
|
||||
```typescript
|
||||
const filePicker = app.createFilePicker();
|
||||
filePicker.setFilter(file => file.extension === 'md');
|
||||
filePicker.onChange = async (files) => {
|
||||
// 处理选中的文件
|
||||
};
|
||||
filePicker.open();
|
||||
```
|
||||
|
||||
### 文件读取
|
||||
|
||||
```typescript
|
||||
const content = await app.vault.read(file);
|
||||
```
|
||||
|
||||
### Frontmatter 解析
|
||||
|
||||
参考现有的 `src/utils/yaml.ts` 实现,或使用 gray-matter 库。
|
||||
|
||||
### API 端点
|
||||
|
||||
```
|
||||
POST /apis/uc.api.content.halo.run/v1alpha1/posts
|
||||
```
|
||||
|
||||
请求体格式与发布功能相同。
|
||||
@@ -0,0 +1,47 @@
|
||||
# Markdown 文件导入功能任务清单
|
||||
|
||||
## 任务列表
|
||||
|
||||
- [ ] 任务 1:创建导入命令模块 `src/commands/import-markdown.ts`
|
||||
- [ ] 子任务 1.1:实现 Obsidian 文件选择器调用
|
||||
- [ ] 子任务 1.2:实现文件读取和解析
|
||||
- [ ] 子任务 1.3:实现文章创建逻辑
|
||||
|
||||
- [ ] 任务 2:创建文件预览弹窗 `src/modals/file-preview-modal.ts`
|
||||
- [ ] 子任务 2.1:显示文件内容预览
|
||||
- [ ] 子任务 2.2:显示 frontmatter 信息
|
||||
- [ ] 子任务 2.3:添加导入/取消按钮
|
||||
- [ ] 子任务 2.4:添加"导入后发布"选项
|
||||
|
||||
- [ ] 任务 3:更新主程序注册命令 `src/main.ts`
|
||||
- [ ] 子任务 3.1:导入新命令模块
|
||||
- [ ] 子任务 3.2:注册"从 Markdown 文件导入"命令
|
||||
|
||||
- [ ] 任务 4:添加国际化文案
|
||||
- [ ] 子任务 4.1:更新英文文案 `src/i18n/locales/en.json`
|
||||
- [ ] 子任务 4.2:更新简体中文文案 `src/i18n/locales/zh-cn.json`
|
||||
- [ ] 子任务 4.3:更新繁体中文文案 `src/i18n/locales/zh-tw.json`
|
||||
|
||||
- [ ] 任务 5:编译和测试
|
||||
- [ ] 子任务 5.1:运行 `pnpm build` 确保编译通过
|
||||
- [ ] 子任务 5.2:复制编译产物到插件目录
|
||||
- [ ] 子任务 5.3:手动测试导入功能
|
||||
|
||||
---
|
||||
|
||||
## 任务依赖关系
|
||||
|
||||
- [任务 2] 和 [任务 3] 可并行开发
|
||||
- [任务 1] 依赖 [任务 2] 完成
|
||||
- [任务 4] 可与任何任务并行
|
||||
- [任务 5] 需要 [任务 1]、[任务 3] 完成
|
||||
|
||||
---
|
||||
|
||||
## 实现顺序建议
|
||||
|
||||
1. 先完成 [任务 2](文件预览弹窗)- 核心 UI
|
||||
2. 再完成 [任务 1](导入命令模块)- 核心逻辑
|
||||
3. 然后完成 [任务 3](注册命令)
|
||||
4. [任务 4] 可与 [任务 1] 并行
|
||||
5. 最后 [任务 5](测试)
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "1.1.1",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Halo's Obsidian integration supports publishing content to Halo sites",
|
||||
"author": "Ryan Wang",
|
||||
"authorUrl": "https://github.com/ruibaby",
|
||||
"author": "刘航宇 (LHY)",
|
||||
"authorUrl": "https://github.com/LHY0125/obsidian-halo",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@halo-dev/obsidian-halo",
|
||||
"private": true,
|
||||
"version": "1.1.1",
|
||||
"version": "2.1.1",
|
||||
"description": "Halo's Obsidian integration supports publishing content to Halo sites",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
@@ -10,12 +10,12 @@
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json",
|
||||
"check": "biome check --write src/"
|
||||
},
|
||||
"author": "@halo-dev",
|
||||
"author": "LHY",
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Ryan Wang",
|
||||
"email": "i@ryanc.cc",
|
||||
"url": "https://github.com/ruibaby"
|
||||
"name": "刘航宇",
|
||||
"email": "3364451258@qq.com",
|
||||
"url": "https://github.com/LHY0125/obsidian-halo"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import i18next from "i18next";
|
||||
import { Notice } from "obsidian";
|
||||
import HaloService from "../service";
|
||||
import { openSiteSelectionModal } from "../site-selection-modal";
|
||||
import { openDeleteConfirmModal } from "../modals/delete-confirm-modal";
|
||||
import type HaloPlugin from "../main";
|
||||
import type { HaloSite } from "../settings";
|
||||
|
||||
export interface DeleteOptions {
|
||||
deleteHalo: boolean;
|
||||
deleteLocal: boolean;
|
||||
}
|
||||
|
||||
export async function deletePost(plugin: HaloPlugin): Promise<void> {
|
||||
try {
|
||||
const { activeEditor } = plugin.app.workspace;
|
||||
|
||||
if (!activeEditor || !activeEditor.file) {
|
||||
new Notice(i18next.t("common.error_no_active_editor"));
|
||||
return;
|
||||
}
|
||||
|
||||
const matterData = plugin.app.metadataCache.getFileCache(activeEditor.file)?.frontmatter;
|
||||
|
||||
if (!matterData?.halo?.name) {
|
||||
new Notice(i18next.t("command.delete_post.error_not_published"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!matterData?.halo?.site) {
|
||||
new Notice(i18next.t("service.error_site_not_match"));
|
||||
return;
|
||||
}
|
||||
|
||||
const site = plugin.settings.sites.find((s) => s.url === matterData.halo.site);
|
||||
|
||||
if (!site) {
|
||||
new Notice(i18next.t("command.delete_post.error_no_matched_site"));
|
||||
return;
|
||||
}
|
||||
|
||||
const title = matterData.title || activeEditor.file.basename;
|
||||
|
||||
const result = await openDeleteConfirmModal(plugin, site, title, matterData.halo.name);
|
||||
|
||||
if (!result.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const service = new HaloService(plugin.app, plugin.settings, site);
|
||||
|
||||
if (result.options.deleteHalo) {
|
||||
const success = await service.deletePost(matterData.halo.name);
|
||||
if (!success) {
|
||||
new Notice(i18next.t("service.error_delete_failed"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.options.deleteLocal) {
|
||||
await plugin.app.vault.delete(activeEditor.file);
|
||||
}
|
||||
|
||||
new Notice(i18next.t("service.notice_delete_success"));
|
||||
} catch (error) {
|
||||
console.error("[HaloPlugin] Delete post failed:", error);
|
||||
new Notice(i18next.t("service.error_delete_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
export async function deletePostByName(plugin: HaloPlugin, site: HaloSite, postName: string, title: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await openDeleteConfirmModal(plugin, site, title, postName);
|
||||
|
||||
if (!result.success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const service = new HaloService(plugin.app, plugin.settings, site);
|
||||
|
||||
if (result.options.deleteHalo) {
|
||||
const success = await service.deletePost(postName);
|
||||
if (!success) {
|
||||
new Notice(i18next.t("service.error_delete_failed"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
new Notice(i18next.t("service.notice_delete_success"));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[HaloPlugin] Delete post by name failed:", error);
|
||||
new Notice(i18next.t("service.error_delete_failed"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import i18next from "i18next";
|
||||
import { Notice, TFile } from "obsidian";
|
||||
import { openSiteSelectionModal } from "../site-selection-modal";
|
||||
import { openFilePreviewModal, type ImportOptions } from "../modals/file-preview-modal";
|
||||
import HaloService from "../service";
|
||||
import type HaloPlugin from "../main";
|
||||
import type { HaloSite } from "../settings";
|
||||
|
||||
export async function importFromMarkdownFile(plugin: HaloPlugin): Promise<void> {
|
||||
try {
|
||||
if (plugin.settings.sites.length === 0) {
|
||||
new Notice(i18next.t("command.pull_post.error_no_sites"));
|
||||
return;
|
||||
}
|
||||
|
||||
let site: HaloSite = plugin.settings.sites[0];
|
||||
if (plugin.settings.sites.length > 1) {
|
||||
site = await openSiteSelectionModal(plugin);
|
||||
}
|
||||
|
||||
const options: ImportOptions = {
|
||||
publishImmediately: plugin.settings.publishByDefault,
|
||||
};
|
||||
|
||||
const file = await new Promise<TFile | null>((resolve) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = ".md";
|
||||
|
||||
input.onchange = async () => {
|
||||
const selectedFile = input.files?.[0];
|
||||
if (!selectedFile) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const allFiles = plugin.app.vault.getFiles();
|
||||
const matchedFile = allFiles.find(f => f.name === selectedFile.name);
|
||||
|
||||
if (matchedFile) {
|
||||
resolve(matchedFile);
|
||||
} else {
|
||||
new Notice(i18next.t("import_modal.error_file_not_in_vault"));
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
|
||||
input.oncancel = () => resolve(null);
|
||||
input.click();
|
||||
});
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await openFilePreviewModal(plugin, site, file, options);
|
||||
|
||||
if (!result.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const service = new HaloService(plugin.app, plugin.settings, site);
|
||||
|
||||
const success = await service.importPost(file, result.options.publishImmediately);
|
||||
|
||||
if (success) {
|
||||
new Notice(i18next.t("service.notice_import_success"));
|
||||
} else {
|
||||
new Notice(i18next.t("service.error_import_failed"));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("[HaloPlugin] Import from markdown file failed:", error);
|
||||
new Notice(i18next.t("service.error_import_failed"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import i18next from "i18next";
|
||||
import { Notice } from "obsidian";
|
||||
import { openSiteSelectionModal } from "../site-selection-modal";
|
||||
import { openTagManagerModal } from "../modals/tag-manager-modal";
|
||||
import { openCategoryManagerModal } from "../modals/category-manager-modal";
|
||||
import type HaloPlugin from "../main";
|
||||
|
||||
export async function manageTags(plugin: HaloPlugin): Promise<void> {
|
||||
try {
|
||||
if (plugin.settings.sites.length === 0) {
|
||||
new Notice(i18next.t("command.pull_post.error_no_sites"));
|
||||
return;
|
||||
}
|
||||
|
||||
let site = plugin.settings.sites[0];
|
||||
if (plugin.settings.sites.length > 1) {
|
||||
site = await openSiteSelectionModal(plugin);
|
||||
}
|
||||
|
||||
openTagManagerModal(plugin, site);
|
||||
} catch (error) {
|
||||
console.error("[HaloPlugin] Manage tags failed:", error);
|
||||
new Notice(i18next.t("common.error_connection_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
export async function manageCategories(plugin: HaloPlugin): Promise<void> {
|
||||
try {
|
||||
if (plugin.settings.sites.length === 0) {
|
||||
new Notice(i18next.t("command.pull_post.error_no_sites"));
|
||||
return;
|
||||
}
|
||||
|
||||
let site = plugin.settings.sites[0];
|
||||
if (plugin.settings.sites.length > 1) {
|
||||
site = await openSiteSelectionModal(plugin);
|
||||
}
|
||||
|
||||
openCategoryManagerModal(plugin, site);
|
||||
} catch (error) {
|
||||
console.error("[HaloPlugin] Manage categories failed:", error);
|
||||
new Notice(i18next.t("common.error_connection_failed"));
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,22 @@
|
||||
"pull_post": {
|
||||
"name": "Pull posts from Halo",
|
||||
"error_no_sites": "Please configure sites first"
|
||||
},
|
||||
"list_posts": {
|
||||
"name": "View Halo posts list"
|
||||
},
|
||||
"delete_post": {
|
||||
"name": "Delete Halo post",
|
||||
"error_not_published": "This document is not published to Halo yet"
|
||||
},
|
||||
"import_markdown": {
|
||||
"name": "Import from Markdown file"
|
||||
},
|
||||
"manage_tags": {
|
||||
"name": "Manage tags"
|
||||
},
|
||||
"manage_categories": {
|
||||
"name": "Manage categories"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -53,7 +69,50 @@
|
||||
},
|
||||
"post_selection_modal": {
|
||||
"title": "Pull posts from Halo",
|
||||
"button_pull": "Pull"
|
||||
"button_pull": "Pull",
|
||||
"button_view": "View",
|
||||
"filter_all": "All",
|
||||
"filter_published": "Published",
|
||||
"filter_draft": "Draft",
|
||||
"status_published": "Published",
|
||||
"status_draft": "Draft",
|
||||
"total_items": "posts",
|
||||
"button_prev": "Previous",
|
||||
"button_next": "Next",
|
||||
"untitled": "Untitled",
|
||||
"empty": "No posts found"
|
||||
},
|
||||
"post_list_modal": {
|
||||
"title": "Halo Posts List",
|
||||
"button_pull": "Pull",
|
||||
"button_view": "View",
|
||||
"button_edit": "Edit",
|
||||
"button_delete": "Delete",
|
||||
"filter_all": "All",
|
||||
"filter_published": "Published",
|
||||
"filter_draft": "Draft",
|
||||
"status_published": "Published",
|
||||
"status_draft": "Draft",
|
||||
"total_items": "posts",
|
||||
"button_prev": "Previous",
|
||||
"button_next": "Next",
|
||||
"untitled": "Untitled",
|
||||
"empty": "No posts found",
|
||||
"confirm_delete": "Confirm Delete",
|
||||
"confirm_delete_message": "Are you sure you want to delete the post \"{title}\"? This action cannot be undone."
|
||||
},
|
||||
"post_delete_modal": {
|
||||
"title": "Delete Post",
|
||||
"message": "Are you sure you want to delete the post \"{title}\"?",
|
||||
"message_halo_only": "Only delete the post on Halo, keep local file",
|
||||
"message_local_only": "Only delete local file, keep Halo post",
|
||||
"message_both": "Delete both Halo post and local file",
|
||||
"button_delete_halo": "Delete Halo only",
|
||||
"button_delete_local": "Delete local only",
|
||||
"button_delete_both": "Delete both",
|
||||
"success": "Post deleted",
|
||||
"error_failed": "Delete failed",
|
||||
"error_no_option_selected": "Please select at least one delete option"
|
||||
},
|
||||
"site_editing_modal": {
|
||||
"title": "Halo site",
|
||||
@@ -105,10 +164,72 @@
|
||||
"error_post_not_found": "Post does not exist",
|
||||
"image_upload_success": "Image uploaded successfully",
|
||||
"image_upload_failed": "Image upload failed",
|
||||
"image_uploading": "Uploading image..."
|
||||
"image_uploading": "Uploading image...",
|
||||
"error_delete_failed": "Failed to delete post",
|
||||
"error_import_failed": "Failed to import post",
|
||||
"notice_import_success": "Imported successfully",
|
||||
"notice_delete_success": "Deleted successfully"
|
||||
},
|
||||
"file_preview_modal": {
|
||||
"title": "File Preview",
|
||||
"frontmatter": "Frontmatter Metadata",
|
||||
"content_preview": "Content Preview",
|
||||
"publish_immediately": "Publish immediately after import",
|
||||
"publish_immediately_desc": "If checked, the created post will be published directly",
|
||||
"button_import": "Import",
|
||||
"error_file_not_in_vault": "File is not in the vault"
|
||||
},
|
||||
"tag_manager_modal": {
|
||||
"title": "Tag Manager",
|
||||
"button_create": "Create Tag",
|
||||
"loading": "Loading...",
|
||||
"empty": "No tags",
|
||||
"column_name": "Name",
|
||||
"column_slug": "Slug",
|
||||
"column_color": "Color",
|
||||
"column_actions": "Actions",
|
||||
"button_edit": "Edit",
|
||||
"button_delete": "Delete",
|
||||
"prompt_name": "Enter tag name:",
|
||||
"prompt_slug": "Enter tag slug:",
|
||||
"prompt_color": "Enter tag color (hex):",
|
||||
"notice_create_success": "Tag created successfully",
|
||||
"error_create_failed": "Failed to create tag",
|
||||
"notice_update_success": "Tag updated successfully",
|
||||
"error_update_failed": "Failed to update tag",
|
||||
"confirm_delete": "Are you sure you want to delete tag \"{name}\"?",
|
||||
"notice_delete_success": "Tag deleted successfully",
|
||||
"error_delete_failed": "Failed to delete tag"
|
||||
},
|
||||
"category_manager_modal": {
|
||||
"title": "Category Manager",
|
||||
"button_create": "Create Category",
|
||||
"loading": "Loading...",
|
||||
"empty": "No categories",
|
||||
"column_name": "Name",
|
||||
"column_slug": "Slug",
|
||||
"column_priority": "Priority",
|
||||
"column_actions": "Actions",
|
||||
"button_edit": "Edit",
|
||||
"button_delete": "Delete",
|
||||
"prompt_name": "Enter category name:",
|
||||
"prompt_slug": "Enter category slug:",
|
||||
"prompt_priority": "Enter category priority:",
|
||||
"notice_create_success": "Category created successfully",
|
||||
"error_create_failed": "Failed to create category",
|
||||
"notice_update_success": "Category updated successfully",
|
||||
"error_update_failed": "Failed to update category",
|
||||
"confirm_delete": "Are you sure you want to delete category \"{name}\"?",
|
||||
"notice_delete_success": "Category deleted successfully",
|
||||
"error_delete_failed": "Failed to delete category"
|
||||
},
|
||||
"common": {
|
||||
"error_connection_failed": "Connection failed",
|
||||
"button_close": "Close"
|
||||
"button_close": "Close",
|
||||
"button_cancel": "Cancel",
|
||||
"button_confirm": "Confirm",
|
||||
"button_delete": "Delete",
|
||||
"button_import": "Import",
|
||||
"error_no_active_editor": "No file is open"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,22 @@
|
||||
"pull_post": {
|
||||
"name": "从 Halo 拉取文档",
|
||||
"error_no_sites": "请先配置站点"
|
||||
},
|
||||
"list_posts": {
|
||||
"name": "查看 Halo 文章列表"
|
||||
},
|
||||
"delete_post": {
|
||||
"name": "删除 Halo 文章",
|
||||
"error_not_published": "此文档还未发布到 Halo"
|
||||
},
|
||||
"import_markdown": {
|
||||
"name": "从 Markdown 文件导入"
|
||||
},
|
||||
"manage_tags": {
|
||||
"name": "管理标签"
|
||||
},
|
||||
"manage_categories": {
|
||||
"name": "管理分类"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -53,7 +69,50 @@
|
||||
},
|
||||
"post_selection_modal": {
|
||||
"title": "从 Halo 拉取文章",
|
||||
"button_pull": "拉取"
|
||||
"button_pull": "拉取",
|
||||
"button_view": "查看",
|
||||
"filter_all": "全部",
|
||||
"filter_published": "已发布",
|
||||
"filter_draft": "草稿",
|
||||
"status_published": "已发布",
|
||||
"status_draft": "草稿",
|
||||
"total_items": "篇",
|
||||
"button_prev": "上一页",
|
||||
"button_next": "下一页",
|
||||
"untitled": "无标题",
|
||||
"empty": "没有找到文章"
|
||||
},
|
||||
"post_list_modal": {
|
||||
"title": "Halo 文章列表",
|
||||
"button_pull": "拉取",
|
||||
"button_view": "查看",
|
||||
"button_edit": "编辑",
|
||||
"button_delete": "删除",
|
||||
"filter_all": "全部",
|
||||
"filter_published": "已发布",
|
||||
"filter_draft": "草稿",
|
||||
"status_published": "已发布",
|
||||
"status_draft": "草稿",
|
||||
"total_items": "篇",
|
||||
"button_prev": "上一页",
|
||||
"button_next": "下一页",
|
||||
"untitled": "无标题",
|
||||
"empty": "没有找到文章",
|
||||
"confirm_delete": "确认删除",
|
||||
"confirm_delete_message": "确定要删除文章「{title}」吗?此操作不可撤销。"
|
||||
},
|
||||
"post_delete_modal": {
|
||||
"title": "删除文章",
|
||||
"message": "确定要删除文章「{title}」吗?",
|
||||
"message_halo_only": "仅删除 Halo 上的文章,本地文件保留",
|
||||
"message_local_only": "仅删除本地文件,Halo 文章保留",
|
||||
"message_both": "同时删除 Halo 文章和本地文件",
|
||||
"button_delete_halo": "仅删除 Halo",
|
||||
"button_delete_local": "仅删除本地",
|
||||
"button_delete_both": "全部删除",
|
||||
"success": "文章已删除",
|
||||
"error_failed": "删除失败",
|
||||
"error_no_option_selected": "请至少选择一个删除选项"
|
||||
},
|
||||
"site_editing_modal": {
|
||||
"title": "Halo 站点",
|
||||
@@ -105,10 +164,72 @@
|
||||
"error_post_not_found": "文章不存在",
|
||||
"image_upload_success": "图片上传成功",
|
||||
"image_upload_failed": "图片上传失败",
|
||||
"image_uploading": "正在上传图片..."
|
||||
"image_uploading": "正在上传图片...",
|
||||
"error_delete_failed": "删除文章失败",
|
||||
"error_import_failed": "导入文章失败",
|
||||
"notice_import_success": "导入成功",
|
||||
"notice_delete_success": "删除成功"
|
||||
},
|
||||
"file_preview_modal": {
|
||||
"title": "文件预览",
|
||||
"frontmatter": "Frontmatter 元数据",
|
||||
"content_preview": "内容预览",
|
||||
"publish_immediately": "导入后立即发布",
|
||||
"publish_immediately_desc": "勾选后,创建的文章将直接发布",
|
||||
"button_import": "导入",
|
||||
"error_file_not_in_vault": "文件不在 Vault 中"
|
||||
},
|
||||
"tag_manager_modal": {
|
||||
"title": "标签管理",
|
||||
"button_create": "创建标签",
|
||||
"loading": "加载中...",
|
||||
"empty": "暂无标签",
|
||||
"column_name": "名称",
|
||||
"column_slug": "别名",
|
||||
"column_color": "颜色",
|
||||
"column_actions": "操作",
|
||||
"button_edit": "编辑",
|
||||
"button_delete": "删除",
|
||||
"prompt_name": "请输入标签名称:",
|
||||
"prompt_slug": "请输入标签别名:",
|
||||
"prompt_color": "请输入标签颜色(hex):",
|
||||
"notice_create_success": "标签创建成功",
|
||||
"error_create_failed": "标签创建失败",
|
||||
"notice_update_success": "标签更新成功",
|
||||
"error_update_failed": "标签更新失败",
|
||||
"confirm_delete": "确定要删除标签「{name}」吗?",
|
||||
"notice_delete_success": "标签删除成功",
|
||||
"error_delete_failed": "标签删除失败"
|
||||
},
|
||||
"category_manager_modal": {
|
||||
"title": "分类管理",
|
||||
"button_create": "创建分类",
|
||||
"loading": "加载中...",
|
||||
"empty": "暂无分类",
|
||||
"column_name": "名称",
|
||||
"column_slug": "别名",
|
||||
"column_priority": "优先级",
|
||||
"column_actions": "操作",
|
||||
"button_edit": "编辑",
|
||||
"button_delete": "删除",
|
||||
"prompt_name": "请输入分类名称:",
|
||||
"prompt_slug": "请输入分类别名:",
|
||||
"prompt_priority": "请输入分类优先级:",
|
||||
"notice_create_success": "分类创建成功",
|
||||
"error_create_failed": "分类创建失败",
|
||||
"notice_update_success": "分类更新成功",
|
||||
"error_update_failed": "分类更新失败",
|
||||
"confirm_delete": "确定要删除分类「{name}」吗?",
|
||||
"notice_delete_success": "分类删除成功",
|
||||
"error_delete_failed": "分类删除失败"
|
||||
},
|
||||
"common": {
|
||||
"error_connection_failed": "连接失败",
|
||||
"button_close": "关闭"
|
||||
"button_close": "关闭",
|
||||
"button_cancel": "取消",
|
||||
"button_confirm": "确认",
|
||||
"button_delete": "删除",
|
||||
"button_import": "导入",
|
||||
"error_no_active_editor": "没有打开的文件"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,22 @@
|
||||
"pull_post": {
|
||||
"name": "從 Halo 拉取文件",
|
||||
"error_no_sites": "請先配置網站"
|
||||
},
|
||||
"list_posts": {
|
||||
"name": "查看 Halo 文章列表"
|
||||
},
|
||||
"delete_post": {
|
||||
"name": "刪除 Halo 文章",
|
||||
"error_not_published": "此文件還未發佈到 Halo"
|
||||
},
|
||||
"import_markdown": {
|
||||
"name": "從 Markdown 文件導入"
|
||||
},
|
||||
"manage_tags": {
|
||||
"name": "管理標籤"
|
||||
},
|
||||
"manage_categories": {
|
||||
"name": "管理分類"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -53,7 +69,50 @@
|
||||
},
|
||||
"post_selection_modal": {
|
||||
"title": "從 Halo 拉取文章",
|
||||
"button_pull": "拉取"
|
||||
"button_pull": "拉取",
|
||||
"button_view": "查看",
|
||||
"filter_all": "全部",
|
||||
"filter_published": "已發布",
|
||||
"filter_draft": "草稿",
|
||||
"status_published": "已發布",
|
||||
"status_draft": "草稿",
|
||||
"total_items": "篇",
|
||||
"button_prev": "上一頁",
|
||||
"button_next": "下一頁",
|
||||
"untitled": "無標題",
|
||||
"empty": "沒有找到文章"
|
||||
},
|
||||
"post_list_modal": {
|
||||
"title": "Halo 文章列表",
|
||||
"button_pull": "拉取",
|
||||
"button_view": "查看",
|
||||
"button_edit": "編輯",
|
||||
"button_delete": "刪除",
|
||||
"filter_all": "全部",
|
||||
"filter_published": "已發布",
|
||||
"filter_draft": "草稿",
|
||||
"status_published": "已發布",
|
||||
"status_draft": "草稿",
|
||||
"total_items": "篇",
|
||||
"button_prev": "上一頁",
|
||||
"button_next": "下一頁",
|
||||
"untitled": "無標題",
|
||||
"empty": "沒有找到文章",
|
||||
"confirm_delete": "確認刪除",
|
||||
"confirm_delete_message": "確定要刪除文章「{title}」嗎?此操作不可撤銷。"
|
||||
},
|
||||
"post_delete_modal": {
|
||||
"title": "刪除文章",
|
||||
"message": "確定要刪除文章「{title}」嗎?",
|
||||
"message_halo_only": "僅刪除 Halo 上的文章,本地文件保留",
|
||||
"message_local_only": "僅刪除本地文件,Halo 文章保留",
|
||||
"message_both": "同時刪除 Halo 文章和本地文件",
|
||||
"button_delete_halo": "僅刪除 Halo",
|
||||
"button_delete_local": "僅刪除本地",
|
||||
"button_delete_both": "全部刪除",
|
||||
"success": "文章已刪除",
|
||||
"error_failed": "刪除失敗",
|
||||
"error_no_option_selected": "請至少選擇一個刪除選項"
|
||||
},
|
||||
"site_editing_modal": {
|
||||
"title": "Halo 網站",
|
||||
@@ -105,10 +164,72 @@
|
||||
"error_post_not_found": "文章不存在",
|
||||
"image_upload_success": "圖片上傳成功",
|
||||
"image_upload_failed": "圖片上傳失敗",
|
||||
"image_uploading": "正在上傳圖片..."
|
||||
"image_uploading": "正在上傳圖片...",
|
||||
"error_delete_failed": "刪除文章失敗",
|
||||
"error_import_failed": "導入文章失敗",
|
||||
"notice_import_success": "導入成功",
|
||||
"notice_delete_success": "刪除成功"
|
||||
},
|
||||
"file_preview_modal": {
|
||||
"title": "文件預覽",
|
||||
"frontmatter": "Frontmatter 元數據",
|
||||
"content_preview": "內容預覽",
|
||||
"publish_immediately": "導入後立即發布",
|
||||
"publish_immediately_desc": "勾選後,創建的文章將直接發布",
|
||||
"button_import": "導入",
|
||||
"error_file_not_in_vault": "文件不在 Vault 中"
|
||||
},
|
||||
"tag_manager_modal": {
|
||||
"title": "標籤管理",
|
||||
"button_create": "創建標籤",
|
||||
"loading": "加載中...",
|
||||
"empty": "暫無標籤",
|
||||
"column_name": "名稱",
|
||||
"column_slug": "別名",
|
||||
"column_color": "顏色",
|
||||
"column_actions": "操作",
|
||||
"button_edit": "編輯",
|
||||
"button_delete": "刪除",
|
||||
"prompt_name": "請輸入標籤名稱:",
|
||||
"prompt_slug": "請輸入標籤別名:",
|
||||
"prompt_color": "請輸入標籤顏色(hex):",
|
||||
"notice_create_success": "標籤創建成功",
|
||||
"error_create_failed": "標籤創建失敗",
|
||||
"notice_update_success": "標籤更新成功",
|
||||
"error_update_failed": "標籤更新失敗",
|
||||
"confirm_delete": "確定要刪除標籤「{name}」嗎?",
|
||||
"notice_delete_success": "標籤刪除成功",
|
||||
"error_delete_failed": "標籤刪除失敗"
|
||||
},
|
||||
"category_manager_modal": {
|
||||
"title": "分類管理",
|
||||
"button_create": "創建分類",
|
||||
"loading": "加載中...",
|
||||
"empty": "暫無分類",
|
||||
"column_name": "名稱",
|
||||
"column_slug": "別名",
|
||||
"column_priority": "優先級",
|
||||
"column_actions": "操作",
|
||||
"button_edit": "編輯",
|
||||
"button_delete": "刪除",
|
||||
"prompt_name": "請輸入分類名稱:",
|
||||
"prompt_slug": "請輸入分類別名:",
|
||||
"prompt_priority": "請輸入分類優先級:",
|
||||
"notice_create_success": "分類創建成功",
|
||||
"error_create_failed": "分類創建失敗",
|
||||
"notice_update_success": "分類更新成功",
|
||||
"error_update_failed": "分類更新失敗",
|
||||
"confirm_delete": "確定要刪除分類「{name}」嗎?",
|
||||
"notice_delete_success": "分類刪除成功",
|
||||
"error_delete_failed": "分類刪除失敗"
|
||||
},
|
||||
"common": {
|
||||
"error_connection_failed": "連接失敗",
|
||||
"button_close": "關閉"
|
||||
"button_close": "關閉",
|
||||
"button_cancel": "取消",
|
||||
"button_confirm": "確認",
|
||||
"button_delete": "刪除",
|
||||
"button_import": "導入",
|
||||
"error_no_active_editor": "沒有打開的文件"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,11 @@ import { Notice, Plugin, moment } from "obsidian";
|
||||
import { resources } from "./i18n";
|
||||
import { addHaloIcon } from "./icons";
|
||||
import { openPostSelectionModal } from "./post-selection-model";
|
||||
import HaloService from "./service";
|
||||
import { importFromMarkdownFile } from "./commands/import-markdown";
|
||||
import { deletePost } from "./commands/delete-post";
|
||||
import { manageTags, manageCategories } from "./commands/manage-taxonomy";
|
||||
import { DEFAULT_SETTINGS, type HaloSetting, HaloSettingTab, type HaloSite } from "./settings";
|
||||
import HaloService from "./service";
|
||||
import { openSiteSelectionModal } from "./site-selection-modal";
|
||||
|
||||
export default class HaloPlugin extends Plugin {
|
||||
@@ -106,6 +109,38 @@ export default class HaloPlugin extends Plugin {
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "import-markdown",
|
||||
name: i18next.t("command.import_markdown.name"),
|
||||
callback: async () => {
|
||||
await importFromMarkdownFile(this);
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "delete-post",
|
||||
name: i18next.t("command.delete_post.name"),
|
||||
editorCallback: async () => {
|
||||
await deletePost(this);
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "manage-tags",
|
||||
name: i18next.t("command.manage_tags.name"),
|
||||
callback: async () => {
|
||||
await manageTags(this);
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "manage-categories",
|
||||
name: i18next.t("command.manage_categories.name"),
|
||||
callback: async () => {
|
||||
await manageCategories(this);
|
||||
},
|
||||
});
|
||||
|
||||
this.addSettingTab(new HaloSettingTab(this));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import i18next from "i18next";
|
||||
import { Modal, Notice, Setting } from "obsidian";
|
||||
import type HaloPlugin from "../main";
|
||||
import type { HaloSite } from "../settings";
|
||||
import HaloService from "../service";
|
||||
|
||||
export function openCategoryManagerModal(plugin: HaloPlugin, site: HaloSite): void {
|
||||
const modal = new CategoryManagerModal(plugin, site);
|
||||
modal.open();
|
||||
}
|
||||
|
||||
class CategoryManagerModal extends Modal {
|
||||
private categories: any[] = [];
|
||||
private loading = true;
|
||||
|
||||
constructor(
|
||||
private readonly plugin: HaloPlugin,
|
||||
private readonly site: HaloSite,
|
||||
) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
contentEl.createEl("h2", {
|
||||
text: i18next.t("category_manager_modal.title"),
|
||||
});
|
||||
|
||||
const actionsEl = contentEl.createDiv("category-manager-actions");
|
||||
|
||||
new Setting(actionsEl)
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText(i18next.t("category_manager_modal.button_create"))
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.showCreateCategoryDialog();
|
||||
});
|
||||
})
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText(i18next.t("common.button_close"))
|
||||
.onClick(() => this.close());
|
||||
});
|
||||
|
||||
const listEl = contentEl.createDiv("category-list");
|
||||
listEl.createEl("p", { text: i18next.t("category_manager_modal.loading") });
|
||||
|
||||
try {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
this.categories = await service.getCategories();
|
||||
this.loading = false;
|
||||
this.renderCategoryList();
|
||||
} catch (error) {
|
||||
new Notice(i18next.t("common.error_connection_failed"));
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
private renderCategoryList() {
|
||||
const { contentEl } = this;
|
||||
const listEl = contentEl.querySelector(".category-list") as HTMLElement;
|
||||
|
||||
if (!listEl) return;
|
||||
|
||||
listEl.empty();
|
||||
|
||||
if (this.categories.length === 0) {
|
||||
listEl.createEl("p", { text: i18next.t("category_manager_modal.empty") });
|
||||
return;
|
||||
}
|
||||
|
||||
const table = listEl.createEl("table", { cls: "category-table" });
|
||||
const header = table.createEl("tr");
|
||||
header.createEl("th", { text: i18next.t("category_manager_modal.column_name") });
|
||||
header.createEl("th", { text: i18next.t("category_manager_modal.column_slug") });
|
||||
header.createEl("th", { text: i18next.t("category_manager_modal.column_priority") });
|
||||
header.createEl("th", { text: i18next.t("category_manager_modal.column_actions") });
|
||||
|
||||
for (const category of this.categories) {
|
||||
const row = table.createEl("tr");
|
||||
|
||||
row.createEl("td", { text: category.spec.displayName });
|
||||
row.createEl("td", { text: category.spec.slug });
|
||||
row.createEl("td", { text: String(category.spec.priority || 0) });
|
||||
|
||||
const actionsCell = row.createEl("td");
|
||||
actionsCell.createEl("button", {
|
||||
text: i18next.t("category_manager_modal.button_edit"),
|
||||
cls: "category-action-btn",
|
||||
}).addEventListener("click", () => {
|
||||
this.showEditCategoryDialog(category);
|
||||
});
|
||||
|
||||
actionsCell.createEl("button", {
|
||||
text: i18next.t("category_manager_modal.button_delete"),
|
||||
cls: "category-action-btn danger",
|
||||
}).addEventListener("click", () => {
|
||||
this.showDeleteCategoryDialog(category);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async showCreateCategoryDialog() {
|
||||
const name = prompt(i18next.t("category_manager_modal.prompt_name"));
|
||||
if (!name) return;
|
||||
|
||||
const slug = prompt(i18next.t("category_manager_modal.prompt_slug"), this.generateSlug(name));
|
||||
if (!slug) return;
|
||||
|
||||
const priority = prompt(i18next.t("category_manager_modal.prompt_priority"), "0");
|
||||
if (priority === null) return;
|
||||
|
||||
try {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
await service.createCategory(name, slug, parseInt(priority) || 0);
|
||||
new Notice(i18next.t("category_manager_modal.notice_create_success"));
|
||||
|
||||
this.categories = await service.getCategories();
|
||||
this.renderCategoryList();
|
||||
} catch (error) {
|
||||
new Notice(i18next.t("category_manager_modal.error_create_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
private async showEditCategoryDialog(category: any) {
|
||||
const name = prompt(i18next.t("category_manager_modal.prompt_name"), category.spec.displayName);
|
||||
if (name === null) return;
|
||||
|
||||
const slug = prompt(i18next.t("category_manager_modal.prompt_slug"), category.spec.slug);
|
||||
if (slug === null) return;
|
||||
|
||||
const priority = prompt(i18next.t("category_manager_modal.prompt_priority"), String(category.spec.priority || 0));
|
||||
if (priority === null) return;
|
||||
|
||||
try {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
await service.updateCategory(category.metadata.name, name, slug, parseInt(priority) || 0);
|
||||
new Notice(i18next.t("category_manager_modal.notice_update_success"));
|
||||
|
||||
this.categories = await service.getCategories();
|
||||
this.renderCategoryList();
|
||||
} catch (error) {
|
||||
new Notice(i18next.t("category_manager_modal.error_update_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
private async showDeleteCategoryDialog(category: any) {
|
||||
if (!confirm(i18next.t("category_manager_modal.confirm_delete", { name: category.spec.displayName }))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
await service.deleteCategory(category.metadata.name);
|
||||
new Notice(i18next.t("category_manager_modal.notice_delete_success"));
|
||||
|
||||
this.categories = await service.getCategories();
|
||||
this.renderCategoryList();
|
||||
} catch (error) {
|
||||
new Notice(i18next.t("category_manager_modal.error_delete_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
private generateSlug(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import i18next from "i18next";
|
||||
import { Modal, Notice, Setting } from "obsidian";
|
||||
import type HaloPlugin from "../main";
|
||||
import type { HaloSite } from "../settings";
|
||||
|
||||
export interface DeleteResult {
|
||||
success: boolean;
|
||||
options: {
|
||||
deleteHalo: boolean;
|
||||
deleteLocal: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export function openDeleteConfirmModal(
|
||||
plugin: HaloPlugin,
|
||||
site: HaloSite,
|
||||
title: string,
|
||||
postName: string,
|
||||
): Promise<DeleteResult> {
|
||||
return new Promise((resolve) => {
|
||||
const modal = new DeleteConfirmModal(plugin, site, title, postName, (result) => {
|
||||
resolve(result);
|
||||
});
|
||||
modal.open();
|
||||
});
|
||||
}
|
||||
|
||||
class DeleteConfirmModal extends Modal {
|
||||
private deleteHalo = true;
|
||||
private deleteLocal = false;
|
||||
|
||||
constructor(
|
||||
private readonly plugin: HaloPlugin,
|
||||
private readonly site: HaloSite,
|
||||
private readonly title: string,
|
||||
private readonly postName: string,
|
||||
private readonly onResult: (result: DeleteResult) => void,
|
||||
) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
contentEl.createEl("h2", {
|
||||
text: i18next.t("post_delete_modal.title"),
|
||||
});
|
||||
|
||||
contentEl.createEl("p", {
|
||||
text: i18next.t("post_delete_modal.message", { title: this.title }),
|
||||
cls: "delete-message",
|
||||
});
|
||||
|
||||
const optionsEl = contentEl.createDiv("delete-options");
|
||||
|
||||
new Setting(optionsEl)
|
||||
.setName(i18next.t("post_delete_modal.message_halo_only").split(",")[0])
|
||||
.setDesc(i18next.t("post_delete_modal.message_halo_only").split(",").slice(1).join(","))
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(this.deleteHalo).onChange((value) => {
|
||||
this.deleteHalo = value;
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(optionsEl)
|
||||
.setName(i18next.t("post_delete_modal.message_local_only").split(",")[0])
|
||||
.setDesc(i18next.t("post_delete_modal.message_local_only").split(",").slice(1).join(","))
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(this.deleteLocal).onChange((value) => {
|
||||
this.deleteLocal = value;
|
||||
});
|
||||
});
|
||||
|
||||
const hasLocalFile = this.plugin.app.metadataCache.getFileCache(
|
||||
Array.from(this.plugin.app.vault.getFiles()).find(f => f.basename === this.title) || this.plugin.app.vault.getAbstractFileByPath(`/${this.title}.md`) as any
|
||||
);
|
||||
|
||||
if (!hasLocalFile) {
|
||||
const localFile = this.plugin.app.vault.getAbstractFileByPath(`/${this.title}.md`);
|
||||
if (!localFile) {
|
||||
const localToggle = optionsEl.querySelector('.setting-item:nth-child(2) input[type="checkbox"]') as HTMLInputElement;
|
||||
if (localToggle) {
|
||||
localToggle.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const actionsEl = contentEl.createDiv("delete-actions");
|
||||
|
||||
new Setting(actionsEl)
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText(i18next.t("post_delete_modal.button_delete_both"))
|
||||
.setWarning()
|
||||
.onClick(() => {
|
||||
if (!this.deleteHalo && !this.deleteLocal) {
|
||||
new Notice(i18next.t("post_delete_modal.error_no_option_selected"));
|
||||
return;
|
||||
}
|
||||
this.onResult({
|
||||
success: true,
|
||||
options: {
|
||||
deleteHalo: this.deleteHalo,
|
||||
deleteLocal: this.deleteLocal,
|
||||
},
|
||||
});
|
||||
this.close();
|
||||
});
|
||||
})
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText(i18next.t("common.button_cancel"))
|
||||
.onClick(() => {
|
||||
this.onResult({
|
||||
success: false,
|
||||
options: {
|
||||
deleteHalo: false,
|
||||
deleteLocal: false,
|
||||
},
|
||||
});
|
||||
this.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import i18next from "i18next";
|
||||
import { Modal, Notice, Setting } from "obsidian";
|
||||
import type { TFile } from "obsidian";
|
||||
import type HaloPlugin from "../main";
|
||||
import type { HaloSite } from "../settings";
|
||||
|
||||
export interface ImportOptions {
|
||||
publishImmediately: boolean;
|
||||
}
|
||||
|
||||
export interface ImportResult {
|
||||
file: TFile;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export function openFilePreviewModal(
|
||||
plugin: HaloPlugin,
|
||||
site: HaloSite,
|
||||
file: TFile,
|
||||
options: ImportOptions,
|
||||
): Promise<ImportResult> {
|
||||
return new Promise((resolve) => {
|
||||
const modal = new FilePreviewModal(plugin, site, file, options, (result) => {
|
||||
resolve(result);
|
||||
});
|
||||
modal.open();
|
||||
});
|
||||
}
|
||||
|
||||
class FilePreviewModal extends Modal {
|
||||
constructor(
|
||||
private readonly plugin: HaloPlugin,
|
||||
private readonly site: HaloSite,
|
||||
private readonly file: TFile,
|
||||
private readonly options: ImportOptions,
|
||||
private readonly onResult: (result: ImportResult) => void,
|
||||
) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
const headerEl = contentEl.createDiv("file-preview-header");
|
||||
headerEl.createEl("h2", {
|
||||
text: i18next.t("file_preview_modal.title"),
|
||||
});
|
||||
headerEl.createEl("p", {
|
||||
text: this.file.path,
|
||||
cls: "file-path",
|
||||
});
|
||||
|
||||
try {
|
||||
const content = await this.app.vault.read(this.file);
|
||||
const matterData = this.app.metadataCache.getFileCache(this.file)?.frontmatter;
|
||||
const frontmatterPosition = this.app.metadataCache.getFileCache(this.file)?.frontmatterPosition;
|
||||
|
||||
const previewContent = contentEl.createDiv("file-preview-content");
|
||||
|
||||
if (matterData) {
|
||||
const frontmatterEl = previewContent.createDiv("frontmatter-section");
|
||||
frontmatterEl.createEl("h3", {
|
||||
text: i18next.t("file_preview_modal.frontmatter"),
|
||||
});
|
||||
|
||||
const frontmatterList = frontmatterEl.createEl("ul");
|
||||
if (matterData.title) {
|
||||
frontmatterList.createEl("li", { text: `Title: ${matterData.title}` });
|
||||
}
|
||||
if (matterData.slug) {
|
||||
frontmatterList.createEl("li", { text: `Slug: ${matterData.slug}` });
|
||||
}
|
||||
if (matterData.tags) {
|
||||
frontmatterList.createEl("li", { text: `Tags: ${Array.isArray(matterData.tags) ? matterData.tags.join(", ") : matterData.tags}` });
|
||||
}
|
||||
if (matterData.categories) {
|
||||
frontmatterList.createEl("li", { text: `Categories: ${Array.isArray(matterData.categories) ? matterData.categories.join(", ") : matterData.categories}` });
|
||||
}
|
||||
}
|
||||
|
||||
const contentPreview = previewContent.createDiv("content-preview");
|
||||
contentPreview.createEl("h3", {
|
||||
text: i18next.t("file_preview_modal.content_preview"),
|
||||
});
|
||||
|
||||
const contentText = frontmatterPosition
|
||||
? content.slice(frontmatterPosition.end.offset, Math.min(content.length, 500))
|
||||
: content.slice(0, 500);
|
||||
|
||||
contentPreview.createEl("pre", {
|
||||
text: contentText + (content.length > 500 ? "..." : ""),
|
||||
cls: "content-text",
|
||||
});
|
||||
|
||||
const actionsEl = contentEl.createDiv("file-preview-actions");
|
||||
|
||||
const publishToggle = new Setting(actionsEl)
|
||||
.setName(i18next.t("file_preview_modal.publish_immediately"))
|
||||
.setDesc(i18next.t("file_preview_modal.publish_immediately_desc"))
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(this.options.publishImmediately).onChange((value) => {
|
||||
this.options.publishImmediately = value;
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(actionsEl)
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText(i18next.t("file_preview_modal.button_import"))
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.onResult({ file: this.file, success: true });
|
||||
this.close();
|
||||
});
|
||||
})
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText(i18next.t("common.button_cancel"))
|
||||
.onClick(() => {
|
||||
this.onResult({ file: this.file, success: false });
|
||||
this.close();
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
new Notice(i18next.t("common.error_connection_failed"));
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import i18next from "i18next";
|
||||
import { Modal, Notice, Setting } from "obsidian";
|
||||
import type HaloPlugin from "../main";
|
||||
import type { HaloSite } from "../settings";
|
||||
import HaloService from "../service";
|
||||
|
||||
export function openTagManagerModal(plugin: HaloPlugin, site: HaloSite): void {
|
||||
const modal = new TagManagerModal(plugin, site);
|
||||
modal.open();
|
||||
}
|
||||
|
||||
class TagManagerModal extends Modal {
|
||||
private tags: any[] = [];
|
||||
private loading = true;
|
||||
|
||||
constructor(
|
||||
private readonly plugin: HaloPlugin,
|
||||
private readonly site: HaloSite,
|
||||
) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
contentEl.createEl("h2", {
|
||||
text: i18next.t("tag_manager_modal.title"),
|
||||
});
|
||||
|
||||
const actionsEl = contentEl.createDiv("tag-manager-actions");
|
||||
|
||||
new Setting(actionsEl)
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText(i18next.t("tag_manager_modal.button_create"))
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.showCreateTagDialog();
|
||||
});
|
||||
})
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText(i18next.t("common.button_close"))
|
||||
.onClick(() => this.close());
|
||||
});
|
||||
|
||||
const listEl = contentEl.createDiv("tag-list");
|
||||
listEl.createEl("p", { text: i18next.t("tag_manager_modal.loading") });
|
||||
|
||||
try {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
this.tags = await service.getTags();
|
||||
this.loading = false;
|
||||
this.renderTagList();
|
||||
} catch (error) {
|
||||
new Notice(i18next.t("common.error_connection_failed"));
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
private renderTagList() {
|
||||
const { contentEl } = this;
|
||||
const listEl = contentEl.querySelector(".tag-list") as HTMLElement;
|
||||
|
||||
if (!listEl) return;
|
||||
|
||||
listEl.empty();
|
||||
|
||||
if (this.tags.length === 0) {
|
||||
listEl.createEl("p", { text: i18next.t("tag_manager_modal.empty") });
|
||||
return;
|
||||
}
|
||||
|
||||
const table = listEl.createEl("table", { cls: "tag-table" });
|
||||
const header = table.createEl("tr");
|
||||
header.createEl("th", { text: i18next.t("tag_manager_modal.column_name") });
|
||||
header.createEl("th", { text: i18next.t("tag_manager_modal.column_slug") });
|
||||
header.createEl("th", { text: i18next.t("tag_manager_modal.column_color") });
|
||||
header.createEl("th", { text: i18next.t("tag_manager_modal.column_actions") });
|
||||
|
||||
for (const tag of this.tags) {
|
||||
const row = table.createEl("tr");
|
||||
|
||||
row.createEl("td", { text: tag.spec.displayName });
|
||||
row.createEl("td", { text: tag.spec.slug });
|
||||
|
||||
const colorCell = row.createEl("td");
|
||||
colorCell.createEl("span", {
|
||||
text: tag.spec.color || "#ffffff",
|
||||
cls: "tag-color-preview",
|
||||
attr: { style: `background-color: ${tag.spec.color || "#ffffff"}` }
|
||||
});
|
||||
|
||||
const actionsCell = row.createEl("td");
|
||||
actionsCell.createEl("button", {
|
||||
text: i18next.t("tag_manager_modal.button_edit"),
|
||||
cls: "tag-action-btn",
|
||||
}).addEventListener("click", () => {
|
||||
this.showEditTagDialog(tag);
|
||||
});
|
||||
|
||||
actionsCell.createEl("button", {
|
||||
text: i18next.t("tag_manager_modal.button_delete"),
|
||||
cls: "tag-action-btn danger",
|
||||
}).addEventListener("click", () => {
|
||||
this.showDeleteTagDialog(tag);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async showCreateTagDialog() {
|
||||
const name = prompt(i18next.t("tag_manager_modal.prompt_name"));
|
||||
if (!name) return;
|
||||
|
||||
const slug = prompt(i18next.t("tag_manager_modal.prompt_slug"), this.generateSlug(name));
|
||||
if (!slug) return;
|
||||
|
||||
const color = prompt(i18next.t("tag_manager_modal.prompt_color"), "#4A90E2");
|
||||
if (!color) return;
|
||||
|
||||
try {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
await service.createTag(name, slug, color);
|
||||
new Notice(i18next.t("tag_manager_modal.notice_create_success"));
|
||||
|
||||
this.tags = await service.getTags();
|
||||
this.renderTagList();
|
||||
} catch (error) {
|
||||
new Notice(i18next.t("tag_manager_modal.error_create_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
private async showEditTagDialog(tag: any) {
|
||||
const name = prompt(i18next.t("tag_manager_modal.prompt_name"), tag.spec.displayName);
|
||||
if (name === null) return;
|
||||
|
||||
const slug = prompt(i18next.t("tag_manager_modal.prompt_slug"), tag.spec.slug);
|
||||
if (slug === null) return;
|
||||
|
||||
const color = prompt(i18next.t("tag_manager_modal.prompt_color"), tag.spec.color || "#4A90E2");
|
||||
if (color === null) return;
|
||||
|
||||
try {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
await service.updateTag(tag.metadata.name, name, slug, color);
|
||||
new Notice(i18next.t("tag_manager_modal.notice_update_success"));
|
||||
|
||||
this.tags = await service.getTags();
|
||||
this.renderTagList();
|
||||
} catch (error) {
|
||||
new Notice(i18next.t("tag_manager_modal.error_update_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
private async showDeleteTagDialog(tag: any) {
|
||||
if (!confirm(i18next.t("tag_manager_modal.confirm_delete", { name: tag.spec.displayName }))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
await service.deleteTag(tag.metadata.name);
|
||||
new Notice(i18next.t("tag_manager_modal.notice_delete_success"));
|
||||
|
||||
this.tags = await service.getTags();
|
||||
this.renderTagList();
|
||||
} catch (error) {
|
||||
new Notice(i18next.t("tag_manager_modal.error_delete_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
private generateSlug(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,12 @@ export function openPostSelectionModal(plugin: HaloPlugin, site: HaloSite): Prom
|
||||
}
|
||||
|
||||
class PostSelectionModal extends Modal {
|
||||
private currentPage = 1;
|
||||
private pageSize = 20;
|
||||
private totalItems = 0;
|
||||
private posts: ListedPost[] = [];
|
||||
private filter: "all" | "published" | "draft" = "all";
|
||||
|
||||
constructor(
|
||||
private readonly plugin: HaloPlugin,
|
||||
private readonly site: HaloSite,
|
||||
@@ -22,51 +28,173 @@ class PostSelectionModal extends Modal {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
async onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
const renderPostList = (): void => {
|
||||
contentEl.empty();
|
||||
contentEl.createEl("h2", {
|
||||
text: i18next.t("post_selection_modal.title"),
|
||||
});
|
||||
|
||||
contentEl.createEl("h2", {
|
||||
text: i18next.t("post_selection_modal.title"),
|
||||
});
|
||||
await this.loadPosts();
|
||||
this.render();
|
||||
}
|
||||
|
||||
requestUrl({
|
||||
url: `${this.site.url}/apis/uc.api.content.halo.run/v1alpha1/posts?labelSelector=content.halo.run%2Fdeleted%3Dfalse`,
|
||||
private async loadPosts() {
|
||||
try {
|
||||
let labelSelector = "content.halo.run/deleted=false";
|
||||
|
||||
if (this.filter === "published") {
|
||||
labelSelector = "content.halo.run/deleted=false,content.halo.run/published=true";
|
||||
} else if (this.filter === "draft") {
|
||||
labelSelector = "content.halo.run/deleted=false,content.halo.run/published=false";
|
||||
}
|
||||
|
||||
const response = await requestUrl({
|
||||
url: `${this.site.url}/apis/uc.api.content.halo.run/v1alpha1/posts?labelSelector=${encodeURIComponent(labelSelector)}&page=${this.currentPage}&size=${this.pageSize}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.site.token}`,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
const posts: ListedPost[] = response.json.items;
|
||||
});
|
||||
|
||||
for (const post of posts) {
|
||||
const setting = new Setting(contentEl).setName(post.post.spec.title).setDesc(post.post.spec.slug);
|
||||
this.posts = response.json.items || [];
|
||||
this.totalItems = response.json.total || this.posts.length;
|
||||
} catch (error) {
|
||||
new Notice(i18next.t("common.error_connection_failed"));
|
||||
}
|
||||
}
|
||||
|
||||
setting.addButton((button) =>
|
||||
button.setButtonText(i18next.t("post_selection_modal.button_pull")).onClick(() => {
|
||||
this.onSelect(post);
|
||||
this.close();
|
||||
}),
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
new Notice(i18next.t("common.error_connection_failed"));
|
||||
})
|
||||
.finally(() => {
|
||||
new Setting(contentEl).addButton((button) =>
|
||||
button.setButtonText(i18next.t("common.button_close")).onClick(() => this.close()),
|
||||
);
|
||||
});
|
||||
};
|
||||
private render() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
renderPostList();
|
||||
contentEl.createEl("h2", {
|
||||
text: i18next.t("post_selection_modal.title"),
|
||||
});
|
||||
|
||||
this.renderFilter();
|
||||
|
||||
if (this.posts.length === 0) {
|
||||
contentEl.createEl("p", {
|
||||
text: i18next.t("post_selection_modal.empty"),
|
||||
});
|
||||
} else {
|
||||
for (const post of this.posts) {
|
||||
this.renderPostItem(post);
|
||||
}
|
||||
}
|
||||
|
||||
this.renderPagination();
|
||||
}
|
||||
|
||||
private renderFilter() {
|
||||
const filterEl = this.contentEl.createDiv("post-list-filter");
|
||||
|
||||
const allButton = filterEl.createEl("button", {
|
||||
text: i18next.t("post_selection_modal.filter_all"),
|
||||
cls: this.filter === "all" ? "active" : "",
|
||||
});
|
||||
allButton.addEventListener("click", async () => {
|
||||
this.filter = "all";
|
||||
this.currentPage = 1;
|
||||
await this.loadPosts();
|
||||
this.render();
|
||||
});
|
||||
|
||||
const publishedButton = filterEl.createEl("button", {
|
||||
text: i18next.t("post_selection_modal.filter_published"),
|
||||
cls: this.filter === "published" ? "active" : "",
|
||||
});
|
||||
publishedButton.addEventListener("click", async () => {
|
||||
this.filter = "published";
|
||||
this.currentPage = 1;
|
||||
await this.loadPosts();
|
||||
this.render();
|
||||
});
|
||||
|
||||
const draftButton = filterEl.createEl("button", {
|
||||
text: i18next.t("post_selection_modal.filter_draft"),
|
||||
cls: this.filter === "draft" ? "active" : "",
|
||||
});
|
||||
draftButton.addEventListener("click", async () => {
|
||||
this.filter = "draft";
|
||||
this.currentPage = 1;
|
||||
await this.loadPosts();
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
private renderPostItem(post: ListedPost) {
|
||||
const postEl = this.contentEl.createDiv("post-item");
|
||||
|
||||
const headerEl = postEl.createDiv("post-header");
|
||||
|
||||
const titleEl = headerEl.createEl("h3", {
|
||||
text: post.post.spec.title || i18next.t("post_selection_modal.untitled"),
|
||||
});
|
||||
|
||||
const statusEl = headerEl.createSpan({
|
||||
text: post.post.spec.publish ? i18next.t("post_selection_modal.status_published") : i18next.t("post_selection_modal.status_draft"),
|
||||
cls: post.post.spec.publish ? "status-published" : "status-draft",
|
||||
});
|
||||
|
||||
const metaEl = postEl.createDiv("post-meta");
|
||||
metaEl.createEl("span", { text: `Slug: ${post.post.spec.slug}` });
|
||||
metaEl.createEl("span", { text: ` | ${new Date(post.post.metadata.creationTimestamp).toLocaleDateString()}` });
|
||||
|
||||
const actionsEl = postEl.createDiv("post-actions");
|
||||
|
||||
actionsEl.createEl("button", {
|
||||
text: i18next.t("post_selection_modal.button_pull"),
|
||||
cls: "mod-cta",
|
||||
}).addEventListener("click", () => {
|
||||
this.onSelect(post);
|
||||
this.close();
|
||||
});
|
||||
|
||||
actionsEl.createEl("button", {
|
||||
text: i18next.t("post_selection_modal.button_view"),
|
||||
}).addEventListener("click", () => {
|
||||
window.open(`${this.site.url}/archives/${post.post.spec.slug}`, "_blank");
|
||||
});
|
||||
}
|
||||
|
||||
private renderPagination() {
|
||||
const paginationEl = this.contentEl.createDiv("pagination");
|
||||
|
||||
const totalPages = Math.ceil(this.totalItems / this.pageSize);
|
||||
|
||||
if (this.currentPage > 1) {
|
||||
paginationEl.createEl("button", {
|
||||
text: i18next.t("post_selection_modal.button_prev"),
|
||||
}).addEventListener("click", async () => {
|
||||
this.currentPage--;
|
||||
await this.loadPosts();
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
paginationEl.createEl("span", {
|
||||
text: `${this.currentPage} / ${totalPages} (${this.totalItems} ${i18next.t("post_selection_modal.total_items")})`,
|
||||
});
|
||||
|
||||
if (this.currentPage < totalPages) {
|
||||
paginationEl.createEl("button", {
|
||||
text: i18next.t("post_selection_modal.button_next"),
|
||||
}).addEventListener("click", async () => {
|
||||
this.currentPage++;
|
||||
await this.loadPosts();
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
new Setting(paginationEl).addButton((button) =>
|
||||
button.setButtonText(i18next.t("common.button_close")).onClick(() => this.close()),
|
||||
);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import ImageUploader from "./image-uploader";
|
||||
import i18next from "i18next";
|
||||
import { type App, Notice, requestUrl } from "obsidian";
|
||||
import { type App, Notice, requestUrl, type TFile } from "obsidian";
|
||||
import { randomUUID } from "src/utils/id";
|
||||
import markdownIt from "src/utils/markdown";
|
||||
import { slugify } from "transliteration";
|
||||
@@ -514,6 +514,245 @@ class HaloService {
|
||||
})
|
||||
.filter(Boolean) as string[];
|
||||
}
|
||||
|
||||
public async importPost(file: TFile, publishImmediately: boolean = false): Promise<boolean> {
|
||||
try {
|
||||
const imageUploader = new ImageUploader(this.site.url, this.site.token);
|
||||
|
||||
const md = await this.app.vault.read(file);
|
||||
const matterData = this.app.metadataCache.getFileCache(file)?.frontmatter;
|
||||
const frontmatterPosition = this.app.metadataCache.getFileCache(file)?.frontmatterPosition;
|
||||
|
||||
let raw = frontmatterPosition ? md.slice(frontmatterPosition.end.offset) : md;
|
||||
|
||||
// 检测并上传本地图片
|
||||
if (this.settings.imageUpload?.enabled) {
|
||||
const imageReferences = extractImageReferences(raw);
|
||||
if (imageReferences.length > 0) {
|
||||
const localImages = imageReferences.filter((ref) =>
|
||||
!ref.path.startsWith("http://") &&
|
||||
!ref.path.startsWith("https://") &&
|
||||
!ref.path.startsWith("data:")
|
||||
);
|
||||
|
||||
if (localImages.length > 0) {
|
||||
new Notice(`检测到 ${localImages.length} 个本地图片,正在上传...`);
|
||||
|
||||
const absolutePaths = localImages
|
||||
.map((ref) => ({
|
||||
original: ref.path,
|
||||
absolute: getAbsolutePath(this.app.vault, ref.path, file.path),
|
||||
}))
|
||||
.filter((item) => item.absolute !== null) as { original: string; absolute: string }[];
|
||||
|
||||
if (absolutePaths.length > 0) {
|
||||
const pathMapping = await imageUploader.uploadImages(absolutePaths.map((item) => item.absolute), this.app.vault);
|
||||
|
||||
if (pathMapping.size > 0) {
|
||||
const mapping = new Map<string, string>();
|
||||
absolutePaths.forEach((item) => {
|
||||
const remoteUrl = pathMapping.get(item.absolute);
|
||||
if (remoteUrl) {
|
||||
mapping.set(item.original, remoteUrl);
|
||||
}
|
||||
});
|
||||
|
||||
raw = replaceImagePaths(raw, mapping);
|
||||
|
||||
const successCount = mapping.size;
|
||||
const failCount = absolutePaths.length - successCount;
|
||||
|
||||
if (failCount === 0) {
|
||||
new Notice(`✓ 图片上传成功 (${successCount}/${absolutePaths.length})`);
|
||||
} else {
|
||||
new Notice(`⚠ 部分图片上传失败 (成功: ${successCount}, 失败: ${failCount})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const content: Content = {
|
||||
rawType: "markdown",
|
||||
raw: raw,
|
||||
content: markdownIt.render(raw),
|
||||
};
|
||||
|
||||
let params: Post = {
|
||||
apiVersion: "content.halo.run/v1alpha1",
|
||||
kind: "Post",
|
||||
metadata: {
|
||||
annotations: {
|
||||
"content.halo.run/content-json": JSON.stringify(content),
|
||||
},
|
||||
name: randomUUID(),
|
||||
},
|
||||
spec: {
|
||||
allowComment: true,
|
||||
baseSnapshot: "",
|
||||
categories: [],
|
||||
cover: matterData?.cover || "",
|
||||
deleted: false,
|
||||
excerpt: {
|
||||
autoGenerate: matterData?.excerpt ? false : true,
|
||||
raw: matterData?.excerpt || "",
|
||||
},
|
||||
headSnapshot: "",
|
||||
htmlMetas: [],
|
||||
owner: "",
|
||||
pinned: false,
|
||||
priority: 0,
|
||||
publish: publishImmediately,
|
||||
publishTime: "",
|
||||
releaseSnapshot: "",
|
||||
slug: matterData?.slug || slugify(matterData?.title || file.basename, { trim: true }),
|
||||
tags: [],
|
||||
template: "",
|
||||
title: matterData?.title || file.basename,
|
||||
visible: "PUBLIC",
|
||||
},
|
||||
};
|
||||
|
||||
// 处理分类
|
||||
if (matterData?.categories) {
|
||||
const categoryNames = await this.getCategoryNames(matterData.categories);
|
||||
params.spec.categories = categoryNames;
|
||||
}
|
||||
|
||||
// 处理标签
|
||||
if (matterData?.tags) {
|
||||
const tagNames = await this.getTagNames(matterData.tags);
|
||||
params.spec.tags = tagNames;
|
||||
}
|
||||
|
||||
// 创建文章
|
||||
const post = await requestUrl({
|
||||
url: `${this.site.url}/apis/uc.api.content.halo.run/v1alpha1/posts`,
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(params),
|
||||
}).json;
|
||||
|
||||
return !!post && !!post.metadata;
|
||||
} catch (error) {
|
||||
console.error("[HaloService] 导入文章失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async deletePost(name: string): Promise<boolean> {
|
||||
try {
|
||||
await requestUrl({
|
||||
url: `${this.site.url}/apis/uc.api.content.halo.run/v1alpha1/posts/${name}`,
|
||||
method: "DELETE",
|
||||
headers: this.headers,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[HaloService] 删除文章失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async createTag(displayName: string, slug: string, color: string): Promise<void> {
|
||||
await requestUrl({
|
||||
url: `${this.site.url}/apis/content.halo.run/v1alpha1/tags`,
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify({
|
||||
spec: {
|
||||
displayName,
|
||||
slug,
|
||||
color: color || "#ffffff",
|
||||
cover: "",
|
||||
},
|
||||
apiVersion: "content.halo.run/v1alpha1",
|
||||
kind: "Tag",
|
||||
metadata: { name: "", generateName: "tag-" },
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
public async updateTag(name: string, displayName: string, slug: string, color: string): Promise<void> {
|
||||
const tag = (await requestUrl({
|
||||
url: `${this.site.url}/apis/content.halo.run/v1alpha1/tags/${name}`,
|
||||
headers: this.headers,
|
||||
}).json) as any;
|
||||
|
||||
tag.spec.displayName = displayName;
|
||||
tag.spec.slug = slug;
|
||||
tag.spec.color = color || "#ffffff";
|
||||
|
||||
await requestUrl({
|
||||
url: `${this.site.url}/apis/content.halo.run/v1alpha1/tags/${name}`,
|
||||
method: "PUT",
|
||||
contentType: "application/json",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(tag),
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteTag(name: string): Promise<void> {
|
||||
await requestUrl({
|
||||
url: `${this.site.url}/apis/content.halo.run/v1alpha1/tags/${name}`,
|
||||
method: "DELETE",
|
||||
headers: this.headers,
|
||||
});
|
||||
}
|
||||
|
||||
public async createCategory(displayName: string, slug: string, priority: number): Promise<void> {
|
||||
await requestUrl({
|
||||
url: `${this.site.url}/apis/content.halo.run/v1alpha1/categories`,
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify({
|
||||
spec: {
|
||||
displayName,
|
||||
slug,
|
||||
description: "",
|
||||
cover: "",
|
||||
template: "",
|
||||
priority,
|
||||
children: [],
|
||||
},
|
||||
apiVersion: "content.halo.run/v1alpha1",
|
||||
kind: "Category",
|
||||
metadata: { name: "", generateName: "category-" },
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
public async updateCategory(name: string, displayName: string, slug: string, priority: number): Promise<void> {
|
||||
const category = (await requestUrl({
|
||||
url: `${this.site.url}/apis/content.halo.run/v1alpha1/categories/${name}`,
|
||||
headers: this.headers,
|
||||
}).json) as any;
|
||||
|
||||
category.spec.displayName = displayName;
|
||||
category.spec.slug = slug;
|
||||
category.spec.priority = priority;
|
||||
|
||||
await requestUrl({
|
||||
url: `${this.site.url}/apis/content.halo.run/v1alpha1/categories/${name}`,
|
||||
method: "PUT",
|
||||
contentType: "application/json",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(category),
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteCategory(name: string): Promise<void> {
|
||||
await requestUrl({
|
||||
url: `${this.site.url}/apis/content.halo.run/v1alpha1/categories/${name}`,
|
||||
method: "DELETE",
|
||||
headers: this.headers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default HaloService;
|
||||
|
||||
+13
-2
@@ -1,6 +1,17 @@
|
||||
---
|
||||
title: Git团队协作指南(精简版)
|
||||
slug: gittuan-dui-xie-zuo-zhi-nan-jing-jian-ban
|
||||
cover: https://image.baidu.com/search/down?url=https%3A%2F%2Ftvax3.sinaimg.cn%2Flarge%2F0072Vf1pgy1foxlnu5w7cj31kw0w0hcd.jpg
|
||||
categories: []
|
||||
tags: []
|
||||
halo:
|
||||
site: http://101.133.128.193:8091
|
||||
name: 2dd2a98c-0399-4ad8-8bfa-04326e302afb
|
||||
publish: true
|
||||
---
|
||||
# Git团队协作指南:从入门到精通
|
||||
|
||||
> **作者**:刘航宇(河南工业大学人工智能协会)
|
||||
> **作者**:刘航宇(河南理工大学人工智能协会)
|
||||
> **面向读者**:有一定编程基础,需要团队协作参赛的同学
|
||||
> **代码语言**:Python为主
|
||||
> **预计阅读时间**:20分钟
|
||||
@@ -652,5 +663,5 @@ Git是团队协作的利器,掌握以下核心要点:
|
||||
|
||||
---
|
||||
|
||||
*作者:刘航宇(河南工业大学人工智能协会)*
|
||||
*作者:刘航宇(河南理工大学人工智能协会)*
|
||||
*更新日期:2026年4月23日*
|
||||
@@ -3,13 +3,13 @@ title: Linear Regression 线性回归
|
||||
id: 019d86e4-4749-708c-b0ba-240fc8a8dbf8
|
||||
date: 2026-04-13 20:50:39
|
||||
auther: openknow
|
||||
cover:
|
||||
cover:
|
||||
excerpt: 线性回归通过建立自变量与因变量之间的线性关系来预测数值,通过最小化平方损失函数求解权重,可采用解析解或梯度下降法优化参数。
|
||||
permalink: /archives/linear-regression-xian-xing-hui-gui
|
||||
categories:
|
||||
- cheng-chang-shi-yan-shi
|
||||
tags:
|
||||
- ji-shu-shen-qian
|
||||
- cheng-chang-shi-yan-shi
|
||||
tags:
|
||||
- ji-shu-shen-qian
|
||||
---
|
||||
|
||||
# Linear Regression 线性回归
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
---
|
||||
title: 博客爬取报告
|
||||
slug: bo-ke-pa-qu-bao-gao
|
||||
cover: ""
|
||||
categories: []
|
||||
tags: []
|
||||
halo:
|
||||
site: https://blog.metarl.cc.cd
|
||||
name: e826f389-79f7-451e-8deb-fcbf88786314
|
||||
publish: true
|
||||
---
|
||||
# Serendipity 博客信息爬取报告
|
||||
|
||||
> 爬取时间:2026-04-23
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
---
|
||||
title: 博客爬取报告
|
||||
slug: bo-ke-pa-qu-bao-gao
|
||||
cover: ""
|
||||
categories: []
|
||||
tags: []
|
||||
halo:
|
||||
site: https://blog.metarl.cc.cd
|
||||
name: e826f389-79f7-451e-8deb-fcbf88786314
|
||||
publish: true
|
||||
---
|
||||
# 大模型(LLM)赋能:基于多模型接入引擎的智能运维架构设计
|
||||
|
||||
在传统的储能电站监控系统中,主要依赖硬性的规则引擎(如温度阈值、温差报警)进行被动式的告警。然而,随着电站规模的扩大,海量告警信息往往导致运维人员“信息过载”,且初级运维人员往往缺乏快速、准确处理复杂故障的经验。
|
||||
|
||||
Reference in New Issue
Block a user