feat(halo-plugin): 新增文章导入、删除和标签分类管理功能

添加从本地 Markdown 文件导入创建文章的功能,支持文件预览和自动发布选项
新增文章删除命令,支持选择性删除 Halo 文章或本地文件
添加标签和分类管理功能,支持创建、编辑和删除操作
更新国际化文案,支持新功能的多种语言界面
扩展服务层以支持文章导入、删除和标签分类管理 API 调用
更新插件版本至 2.1.1 并更新作者信息
This commit is contained in:
2026-04-26 17:23:35 +08:00
parent 8ccc32be0b
commit 5c4a16dc3a
28 changed files with 2161 additions and 99 deletions
+2 -2
View File
@@ -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
}
+5 -5
View File
@@ -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",
+96
View File
@@ -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"));
}
}
+125 -4
View File
@@ -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"
}
}
}
+125 -4
View File
@@ -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": "没有打开的文件"
}
}
}
+125 -4
View File
@@ -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": "沒有打開的文件"
}
}
}
+36 -1
View File
@@ -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();
}
}
+160 -32
View File
@@ -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();
}
}
}
+240 -1
View File
@@ -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;