feat: Enhance sync status view with filtering and improved UI
- Updated workspace configuration for better tab management. - Added new localization strings for sync status and post details in English, Simplified Chinese, and Traditional Chinese. - Refactored sync status view to include filtering options (all, synced, pending) and improved post grouping by category. - Enhanced post card rendering with detailed information and action buttons for update, publish, and pull. - Implemented a modal for viewing sync history with clear history functionality. - Improved styling for better user experience across various components. - Added metadata to Git团队协作指南大纲.md for Halo integration.
This commit is contained in:
Vendored
+2
-1
@@ -287,7 +287,8 @@
|
|||||||
"apiKey": "sk-cp-flXHABxXJ00bLhkLQbRVgFLswCJRqXOeL53iu9O-BrEEygpzzYRGz7NNR2NpJHk-RaZ-pwXXStDWawkXo4h4u-zI9tjyf-F9FhsyGGhtZk1kurt2VvJ_T1I",
|
"apiKey": "sk-cp-flXHABxXJ00bLhkLQbRVgFLswCJRqXOeL53iu9O-BrEEygpzzYRGz7NNR2NpJHk-RaZ-pwXXStDWawkXo4h4u-zI9tjyf-F9FhsyGGhtZk1kurt2VvJ_T1I",
|
||||||
"isEmbeddingModel": false,
|
"isEmbeddingModel": false,
|
||||||
"capabilities": [],
|
"capabilities": [],
|
||||||
"stream": true
|
"stream": true,
|
||||||
|
"maxTokens": 14600
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"activeEmbeddingModels": [
|
"activeEmbeddingModels": [
|
||||||
|
|||||||
Vendored
+14
@@ -1,4 +1,18 @@
|
|||||||
{
|
{
|
||||||
|
"sites": [
|
||||||
|
{
|
||||||
|
"name": "Halo",
|
||||||
|
"url": "http://101.133.128.193:8091",
|
||||||
|
"default": true,
|
||||||
|
"token": "pat_eyJraWQiOiI3REcwM05yaUJ0VVBPM2oxbkN4T0hUNXZsSWlJRTVJYndGNWo2NW43eTRBIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2Jsb2cubWV0YXJsLmNjLmNkLyIsInN1YiI6ImxpdWhhbmd5diIsImlhdCI6MTc3NzM3MDc2OCwianRpIjoiZmY3MWU1MjgtNGMxNS1mMzA3LWU5MTYtODI2YTExNTZiNmFjIiwicGF0X25hbWUiOiJwYXQtM2plNjRkdjYifQ.a4cZPhWUMJxZa1zhcaGKZlkKMGnLtfIW4-wNQDOoLm1gy5DTdMG71V7gn-4qFzh6_Y9Lh5bYnphufyzoIy6Ul8P_azIANLxkLGwl2PNVhO9sLwwMMuBN9uKOx0X1XQSoZBDSjFIDD7HZMEXxgoulQ87dKUcBqqtsl4TtWJMoJGmlqF39ZCGHQwPH6URHNNC2MUysQ7O6N4rmxGMmdZc8MOg3Ap5E3Vd1j8EFw0X9A-4Gs4iCM81WAvAQt4OKhqir92CO4cPooKQG7KLqBrWOddO4yxwg-C_7-cJcW450eZdVJ_KnEdgXfa331gTb4sWKvAjBy2d2xHjZlhXts5BxtkbzpeXvVRu1cOP0jVwbwZT9CU4gB0Z7kLNsyYHn6DCd6FPwcWSpsuKpYnC805ssSbllcTaGja3dOQLKddTG9f9s7psdodGMkBnwemkKSI_l4tVjZS3QA2DUSOgdDvdor3EHMvylLb9TsGx5aSeYRueAtzn_SsvGiN5fIjoCbcLFWQcBq5PNRkfrQL_UUR86XRdpWs_VAfsQUa-wimv2iGb6X2aWIVEnT6wdsRMYs7F8PH7Jz4cuOqH-lVV_qOZusOiwxxiXCx8JgwD_vP29zSDS12L4ib29HM54N0GDjCaMJU_aYQCknXc3jXH8ynH4zjwN00uDFbpDtQbq1VjAvyU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"publishByDefault": false,
|
||||||
|
"imageUpload": {
|
||||||
|
"enabled": true,
|
||||||
|
"uploadPath": "",
|
||||||
|
"preserveOriginal": false
|
||||||
|
},
|
||||||
"syncHistory": [
|
"syncHistory": [
|
||||||
{
|
{
|
||||||
"id": "1777199537235",
|
"id": "1777199537235",
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
{
|
{
|
||||||
"recentFiles": [
|
"recentFiles": [
|
||||||
|
{
|
||||||
|
"basename": "usage-guide",
|
||||||
|
"path": "obsidian-halo/docs/usage-guide.md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"basename": "AI助你轻松上手LaTeX论文写作",
|
||||||
|
"path": "博客/学术与效率/AI助你轻松上手LaTeX论文写作.md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"basename": "Git团队协作指南大纲",
|
||||||
|
"path": "博客/其他/Git团队协作指南大纲.md"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"basename": "DeepSeek-V4博客大纲",
|
"basename": "DeepSeek-V4博客大纲",
|
||||||
"path": "博客/其他/DeepSeek-V4博客大纲.md"
|
"path": "博客/其他/DeepSeek-V4博客大纲.md"
|
||||||
|
|||||||
Vendored
+13
-17
@@ -11,14 +11,10 @@
|
|||||||
"id": "e7a7b303c61786dc",
|
"id": "e7a7b303c61786dc",
|
||||||
"type": "leaf",
|
"type": "leaf",
|
||||||
"state": {
|
"state": {
|
||||||
"type": "markdown",
|
"type": "empty",
|
||||||
"state": {
|
"state": {},
|
||||||
"file": "博客/其他/DeepSeek-V4博客大纲.md",
|
|
||||||
"mode": "source",
|
|
||||||
"source": false
|
|
||||||
},
|
|
||||||
"icon": "lucide-file",
|
"icon": "lucide-file",
|
||||||
"title": "DeepSeek-V4博客大纲"
|
"title": "新标签页"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -88,7 +84,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"direction": "horizontal",
|
"direction": "horizontal",
|
||||||
"width": 263.5
|
"width": 276.5
|
||||||
},
|
},
|
||||||
"right": {
|
"right": {
|
||||||
"id": "1a950cafdb3ea126",
|
"id": "1a950cafdb3ea126",
|
||||||
@@ -200,7 +196,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"currentTab": 3
|
"currentTab": 5
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"direction": "horizontal",
|
"direction": "horizontal",
|
||||||
@@ -208,7 +204,6 @@
|
|||||||
},
|
},
|
||||||
"left-ribbon": {
|
"left-ribbon": {
|
||||||
"hiddenItems": {
|
"hiddenItems": {
|
||||||
"halo:发布当前文档到 Halo": false,
|
|
||||||
"halo:同步状态面板": false,
|
"halo:同步状态面板": false,
|
||||||
"bases:新建数据库": false,
|
"bases:新建数据库": false,
|
||||||
"switcher:打开快速切换": false,
|
"switcher:打开快速切换": false,
|
||||||
@@ -227,11 +222,17 @@
|
|||||||
"mermaid-tools:Open Mermaid Toolbar": false,
|
"mermaid-tools:Open Mermaid Toolbar": false,
|
||||||
"omnisearch:Omnisearch": false,
|
"omnisearch:Omnisearch": false,
|
||||||
"remotely-save:Remotely Save": false,
|
"remotely-save:Remotely Save": false,
|
||||||
"templater-obsidian:Templater": false
|
"templater-obsidian:Templater": false,
|
||||||
|
"halo:发布当前文档到 Halo": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"active": "e7a7b303c61786dc",
|
"active": "1e741b4d78845bd1",
|
||||||
"lastOpenFiles": [
|
"lastOpenFiles": [
|
||||||
|
"obsidian-halo/docs/usage-guide.md",
|
||||||
|
"博客/学术与效率/AI助你轻松上手LaTeX论文写作.md",
|
||||||
|
"博客/其他/Git团队协作指南大纲.md",
|
||||||
|
"未命名.base",
|
||||||
|
"博客/其他/DeepSeek-V4博客大纲.md",
|
||||||
"copilot/copilot-conversations/你好@20260428_130750.md",
|
"copilot/copilot-conversations/你好@20260428_130750.md",
|
||||||
"copilot/copilot-conversations",
|
"copilot/copilot-conversations",
|
||||||
"copilot",
|
"copilot",
|
||||||
@@ -242,7 +243,6 @@
|
|||||||
"博客/文章列表.md",
|
"博客/文章列表.md",
|
||||||
"未命名 2.base",
|
"未命名 2.base",
|
||||||
"未命名 1.base",
|
"未命名 1.base",
|
||||||
"未命名.base",
|
|
||||||
"Excalidraw/Drawing 2026-04-26 18.22.24.excalidraw.md",
|
"Excalidraw/Drawing 2026-04-26 18.22.24.excalidraw.md",
|
||||||
"Excalidraw",
|
"Excalidraw",
|
||||||
"博客/OpenClaw介绍.md",
|
"博客/OpenClaw介绍.md",
|
||||||
@@ -263,7 +263,6 @@
|
|||||||
"博客/LinearRegression线性回归.md",
|
"博客/LinearRegression线性回归.md",
|
||||||
"博客/Git团队协作指南.md",
|
"博客/Git团队协作指南.md",
|
||||||
"博客/大数据技术栈.md",
|
"博客/大数据技术栈.md",
|
||||||
"博客/大模型赋能架构设计.md",
|
|
||||||
"obsidian-halo/src/commands/manage-taxonomy.ts",
|
"obsidian-halo/src/commands/manage-taxonomy.ts",
|
||||||
"obsidian-halo-zip-test/images/pat-zh.png",
|
"obsidian-halo-zip-test/images/pat-zh.png",
|
||||||
"obsidian-halo-zip-test/images/settings-zh.png",
|
"obsidian-halo-zip-test/images/settings-zh.png",
|
||||||
@@ -275,9 +274,6 @@
|
|||||||
"obsidian-halo/images/settings-en.png",
|
"obsidian-halo/images/settings-en.png",
|
||||||
"obsidian-halo/images/pat-en.png",
|
"obsidian-halo/images/pat-en.png",
|
||||||
"obsidian-halo/images/pat-zh.png",
|
"obsidian-halo/images/pat-zh.png",
|
||||||
"copilot/copilot-conversations/分析@20260425_212055.md",
|
|
||||||
"copilot/copilot-custom-prompts/halo.md",
|
|
||||||
"Excalidraw/Drawing 2026-04-25 21.16.49.excalidraw.md",
|
|
||||||
"未命名.canvas"
|
"未命名.canvas"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -258,10 +258,34 @@
|
|||||||
"button_refresh": "Refresh",
|
"button_refresh": "Refresh",
|
||||||
"button_history": "History",
|
"button_history": "History",
|
||||||
"button_clear_history": "Clear History",
|
"button_clear_history": "Clear History",
|
||||||
|
"button_settings": "Settings",
|
||||||
"button_update": "Update",
|
"button_update": "Update",
|
||||||
"button_pull": "Pull",
|
"button_pull": "Pull",
|
||||||
"empty": "No published posts yet",
|
"empty": "No published posts yet",
|
||||||
"total_posts": "Total {count} posts"
|
"no_results": "No matching posts",
|
||||||
|
"total_posts": "Total {count} posts",
|
||||||
|
"search_placeholder": "Search posts...",
|
||||||
|
"filter_all": "All",
|
||||||
|
"filter_synced": "Synced",
|
||||||
|
"filter_pending": "Pending",
|
||||||
|
"status_synced": "Synced",
|
||||||
|
"status_local_modified": "Local Modified",
|
||||||
|
"status_remote_updated": "Remote Updated",
|
||||||
|
"status_unpublished": "Unpublished",
|
||||||
|
"last_sync": "Last sync",
|
||||||
|
"time_just_now": "Just now",
|
||||||
|
"time_minutes_ago": "{count}m ago",
|
||||||
|
"time_hours_ago": "{count}h ago",
|
||||||
|
"time_days_ago": "{count}d ago",
|
||||||
|
"stats_total": "{count} posts",
|
||||||
|
"stats_synced": "✅ {count} synced",
|
||||||
|
"stats_pending": "🔄 {count} pending",
|
||||||
|
"detail_title": "Post Details",
|
||||||
|
"detail_title_label": "Title",
|
||||||
|
"detail_category": "Category",
|
||||||
|
"detail_tags": "Tags",
|
||||||
|
"detail_local_version": "📄 Local Version",
|
||||||
|
"detail_modified": "Modified"
|
||||||
},
|
},
|
||||||
"sync_history": {
|
"sync_history": {
|
||||||
"title": "Sync History",
|
"title": "Sync History",
|
||||||
|
|||||||
@@ -258,10 +258,34 @@
|
|||||||
"button_refresh": "刷新",
|
"button_refresh": "刷新",
|
||||||
"button_history": "历史",
|
"button_history": "历史",
|
||||||
"button_clear_history": "清除历史",
|
"button_clear_history": "清除历史",
|
||||||
|
"button_settings": "设置",
|
||||||
"button_update": "更新",
|
"button_update": "更新",
|
||||||
"button_pull": "拉取",
|
"button_pull": "拉取",
|
||||||
"empty": "暂无已发布的文章",
|
"empty": "暂无已发布的文章",
|
||||||
"total_posts": "共 {count} 篇文章"
|
"no_results": "没有找到匹配的文章",
|
||||||
|
"total_posts": "共 {count} 篇文章",
|
||||||
|
"search_placeholder": "搜索文章...",
|
||||||
|
"filter_all": "全部",
|
||||||
|
"filter_synced": "已同步",
|
||||||
|
"filter_pending": "待更新",
|
||||||
|
"status_synced": "已同步",
|
||||||
|
"status_local_modified": "本地修改",
|
||||||
|
"status_remote_updated": "远程更新",
|
||||||
|
"status_unpublished": "未发布",
|
||||||
|
"last_sync": "最后同步",
|
||||||
|
"time_just_now": "刚刚",
|
||||||
|
"time_minutes_ago": "{count} 分钟前",
|
||||||
|
"time_hours_ago": "{count} 小时前",
|
||||||
|
"time_days_ago": "{count} 天前",
|
||||||
|
"stats_total": "共 {count} 篇",
|
||||||
|
"stats_synced": "✅ 已同步 {count} 篇",
|
||||||
|
"stats_pending": "🔄 待更新 {count} 篇",
|
||||||
|
"detail_title": "文章详情",
|
||||||
|
"detail_title_label": "标题",
|
||||||
|
"detail_category": "分类",
|
||||||
|
"detail_tags": "标签",
|
||||||
|
"detail_local_version": "📄 本地版本",
|
||||||
|
"detail_modified": "修改时间"
|
||||||
},
|
},
|
||||||
"sync_history": {
|
"sync_history": {
|
||||||
"title": "同步历史",
|
"title": "同步历史",
|
||||||
|
|||||||
@@ -258,10 +258,34 @@
|
|||||||
"button_refresh": "刷新",
|
"button_refresh": "刷新",
|
||||||
"button_history": "歷史",
|
"button_history": "歷史",
|
||||||
"button_clear_history": "清除歷史",
|
"button_clear_history": "清除歷史",
|
||||||
|
"button_settings": "設置",
|
||||||
"button_update": "更新",
|
"button_update": "更新",
|
||||||
"button_pull": "拉取",
|
"button_pull": "拉取",
|
||||||
"empty": "暫無已發佈的文章",
|
"empty": "暫無已發佈的文章",
|
||||||
"total_posts": "共 {count} 篇文章"
|
"no_results": "沒有找到匹配的文章",
|
||||||
|
"total_posts": "共 {count} 篇文章",
|
||||||
|
"search_placeholder": "搜索文章...",
|
||||||
|
"filter_all": "全部",
|
||||||
|
"filter_synced": "已同步",
|
||||||
|
"filter_pending": "待更新",
|
||||||
|
"status_synced": "已同步",
|
||||||
|
"status_local_modified": "本地修改",
|
||||||
|
"status_remote_updated": "遠程更新",
|
||||||
|
"status_unpublished": "未發布",
|
||||||
|
"last_sync": "最後同步",
|
||||||
|
"time_just_now": "剛剛",
|
||||||
|
"time_minutes_ago": "{count} 分鐘前",
|
||||||
|
"time_hours_ago": "{count} 小時前",
|
||||||
|
"time_days_ago": "{count} 天前",
|
||||||
|
"stats_total": "共 {count} 篇",
|
||||||
|
"stats_synced": "✅ 已同步 {count} 篇",
|
||||||
|
"stats_pending": "🔄 待更新 {count} 篇",
|
||||||
|
"detail_title": "文章詳情",
|
||||||
|
"detail_title_label": "標題",
|
||||||
|
"detail_category": "分類",
|
||||||
|
"detail_tags": "標籤",
|
||||||
|
"detail_local_version": "📄 本地版本",
|
||||||
|
"detail_modified": "修改時間"
|
||||||
},
|
},
|
||||||
"sync_history": {
|
"sync_history": {
|
||||||
"title": "同步歷史",
|
"title": "同步歷史",
|
||||||
|
|||||||
@@ -4,17 +4,43 @@ import type HaloPlugin from "../main";
|
|||||||
|
|
||||||
export const SYNC_STATUS_VIEW_TYPE = "halo-sync-status";
|
export const SYNC_STATUS_VIEW_TYPE = "halo-sync-status";
|
||||||
|
|
||||||
|
type SyncAction = "publish" | "update" | "pull" | "delete";
|
||||||
|
|
||||||
interface SyncHistoryItem {
|
interface SyncHistoryItem {
|
||||||
id: string;
|
id: string;
|
||||||
action: "publish" | "update" | "pull" | "delete";
|
action: SyncAction;
|
||||||
title: string;
|
title: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SyncStatus = "synced" | "local-modified" | "remote-updated" | "unpublished";
|
||||||
|
|
||||||
|
interface PostItem {
|
||||||
|
file: TFile;
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
haloName: string;
|
||||||
|
haloSite: string;
|
||||||
|
publishStatus: boolean;
|
||||||
|
syncStatus: SyncStatus;
|
||||||
|
categories: string[];
|
||||||
|
tags: string[];
|
||||||
|
localModified?: number;
|
||||||
|
remoteModified?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupedPosts {
|
||||||
|
category: string;
|
||||||
|
posts: PostItem[];
|
||||||
|
}
|
||||||
|
|
||||||
export class SyncStatusView extends ItemView {
|
export class SyncStatusView extends ItemView {
|
||||||
private plugin: HaloPlugin;
|
private plugin: HaloPlugin;
|
||||||
private history: SyncHistoryItem[] = [];
|
private history: SyncHistoryItem[] = [];
|
||||||
|
private selectedPosts: Set<string> = new Set();
|
||||||
|
private searchQuery: string = "";
|
||||||
|
private filterMode: "all" | "synced" | "pending" = "all";
|
||||||
|
|
||||||
constructor(leaf: WorkspaceLeaf, plugin: HaloPlugin) {
|
constructor(leaf: WorkspaceLeaf, plugin: HaloPlugin) {
|
||||||
super(leaf);
|
super(leaf);
|
||||||
@@ -45,7 +71,7 @@ export class SyncStatusView extends ItemView {
|
|||||||
this.plugin.saveData(data);
|
this.plugin.saveData(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
addToHistory(action: "publish" | "update" | "pull" | "delete", title: string, success: boolean) {
|
addToHistory(action: SyncAction, title: string, success: boolean) {
|
||||||
this.history.push({
|
this.history.push({
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
action,
|
action,
|
||||||
@@ -56,15 +82,8 @@ export class SyncStatusView extends ItemView {
|
|||||||
this.saveHistory();
|
this.saveHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPublishedPosts() {
|
private getPublishedPosts(): PostItem[] {
|
||||||
const publishedPosts: Array<{
|
const posts: PostItem[] = [];
|
||||||
file: TFile;
|
|
||||||
title: string;
|
|
||||||
slug: string;
|
|
||||||
haloName: string;
|
|
||||||
haloSite: string;
|
|
||||||
publishStatus: boolean;
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
for (const file of this.plugin.app.vault.getFiles()) {
|
for (const file of this.plugin.app.vault.getFiles()) {
|
||||||
if (file.extension !== "md") continue;
|
if (file.extension !== "md") continue;
|
||||||
@@ -72,160 +91,589 @@ export class SyncStatusView extends ItemView {
|
|||||||
const cache = this.plugin.app.metadataCache.getFileCache(file);
|
const cache = this.plugin.app.metadataCache.getFileCache(file);
|
||||||
if (!cache?.frontmatter?.halo?.name) continue;
|
if (!cache?.frontmatter?.halo?.name) continue;
|
||||||
|
|
||||||
publishedPosts.push({
|
const haloPublish = cache.frontmatter.halo.publish ?? false;
|
||||||
|
const localModified = cache.frontmatter.halo.localModified ?? false;
|
||||||
|
|
||||||
|
let syncStatus: SyncStatus = "synced";
|
||||||
|
if (!haloPublish) {
|
||||||
|
syncStatus = "unpublished";
|
||||||
|
} else if (localModified) {
|
||||||
|
syncStatus = "local-modified";
|
||||||
|
}
|
||||||
|
|
||||||
|
const categories = this.extractCategories(cache.frontmatter);
|
||||||
|
|
||||||
|
posts.push({
|
||||||
file,
|
file,
|
||||||
title: cache.frontmatter.title || file.basename,
|
title: cache.frontmatter.title || file.basename,
|
||||||
slug: cache.frontmatter.slug || "",
|
slug: cache.frontmatter.slug || "",
|
||||||
haloName: cache.frontmatter.halo.name,
|
haloName: cache.frontmatter.halo.name,
|
||||||
haloSite: cache.frontmatter.halo.site || "",
|
haloSite: cache.frontmatter.halo.site || "",
|
||||||
publishStatus: cache.frontmatter.halo.publish ?? false,
|
publishStatus: haloPublish,
|
||||||
|
syncStatus,
|
||||||
|
categories,
|
||||||
|
tags: this.extractTags(cache.frontmatter),
|
||||||
|
localModified: file.stat.mtime,
|
||||||
|
remoteModified: cache.frontmatter.halo.remoteModified,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return publishedPosts;
|
return posts;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async render() {
|
private extractCategories(frontmatter: Record<string, unknown>): string[] {
|
||||||
|
const categories: string[] = [];
|
||||||
|
if (frontmatter.categories) {
|
||||||
|
if (Array.isArray(frontmatter.categories)) {
|
||||||
|
categories.push(...frontmatter.categories.map(String));
|
||||||
|
} else {
|
||||||
|
categories.push(String(frontmatter.categories));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return categories.length > 0 ? categories : ["未分类"];
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractTags(frontmatter: Record<string, unknown>): string[] {
|
||||||
|
const tags: string[] = [];
|
||||||
|
if (frontmatter.tags) {
|
||||||
|
if (Array.isArray(frontmatter.tags)) {
|
||||||
|
tags.push(...frontmatter.tags.map(String));
|
||||||
|
} else if (typeof frontmatter.tags === "string") {
|
||||||
|
const parsed = frontmatter.tags.replace(/[\[\]]/g, "").split(",").map((t: string) => t.trim());
|
||||||
|
tags.push(...parsed.filter(Boolean));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
private groupPostsByCategory(posts: PostItem[]): GroupedPosts[] {
|
||||||
|
const groups = new Map<string, PostItem[]>();
|
||||||
|
|
||||||
|
for (const post of posts) {
|
||||||
|
const category = post.categories[0] || "未分类";
|
||||||
|
if (!groups.has(category)) {
|
||||||
|
groups.set(category, []);
|
||||||
|
}
|
||||||
|
groups.get(category)!.push(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(groups.entries())
|
||||||
|
.map(([category, posts]) => ({ category, posts }))
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.category === "未分类") return 1;
|
||||||
|
if (b.category === "未分类") return -1;
|
||||||
|
return a.category.localeCompare(b.category);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private filterPosts(posts: PostItem[]): PostItem[] {
|
||||||
|
let filtered = posts;
|
||||||
|
|
||||||
|
if (this.searchQuery) {
|
||||||
|
const query = this.searchQuery.toLowerCase();
|
||||||
|
filtered = filtered.filter(
|
||||||
|
(p) =>
|
||||||
|
p.title.toLowerCase().includes(query) ||
|
||||||
|
p.slug.toLowerCase().includes(query) ||
|
||||||
|
p.categories.some((c) => c.toLowerCase().includes(query)) ||
|
||||||
|
p.tags.some((t) => t.toLowerCase().includes(query))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this.filterMode) {
|
||||||
|
case "synced":
|
||||||
|
return filtered.filter((p) => p.syncStatus === "synced");
|
||||||
|
case "pending":
|
||||||
|
return filtered.filter((p) => p.syncStatus !== "synced");
|
||||||
|
default:
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStatusIcon(status: SyncStatus): string {
|
||||||
|
switch (status) {
|
||||||
|
case "synced":
|
||||||
|
return "🟢";
|
||||||
|
case "local-modified":
|
||||||
|
return "🟡";
|
||||||
|
case "remote-updated":
|
||||||
|
return "🔵";
|
||||||
|
case "unpublished":
|
||||||
|
return "⚪";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStatusText(status: SyncStatus): string {
|
||||||
|
switch (status) {
|
||||||
|
case "synced":
|
||||||
|
return i18next.t("sync_panel.status_synced");
|
||||||
|
case "local-modified":
|
||||||
|
return i18next.t("sync_panel.status_local_modified");
|
||||||
|
case "remote-updated":
|
||||||
|
return i18next.t("sync_panel.status_remote_updated");
|
||||||
|
case "unpublished":
|
||||||
|
return i18next.t("sync_panel.status_unpublished");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatRelativeTime(timestamp: number): string {
|
||||||
|
const diff = Date.now() - timestamp;
|
||||||
|
const minutes = Math.floor(diff / 60000);
|
||||||
|
const hours = Math.floor(diff / 3600000);
|
||||||
|
const days = Math.floor(diff / 86400000);
|
||||||
|
|
||||||
|
if (minutes < 1) return i18next.t("sync_panel.time_just_now");
|
||||||
|
if (minutes < 60) return i18next.t("sync_panel.time_minutes_ago", { count: minutes });
|
||||||
|
if (hours < 24) return i18next.t("sync_panel.time_hours_ago", { count: hours });
|
||||||
|
return i18next.t("sync_panel.time_days_ago", { count: days });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async performAction(post: PostItem, action: "update" | "publish" | "pull") {
|
||||||
|
const site = this.plugin.settings.sites.find((s) => s.url === post.haloSite);
|
||||||
|
if (!site) {
|
||||||
|
new Notice(i18next.t("service.error_site_not_match"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { default: HaloService } = await import("../service");
|
||||||
|
const service = new HaloService(this.plugin.app, this.plugin.settings, site);
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (action) {
|
||||||
|
case "update":
|
||||||
|
await service.updatePost();
|
||||||
|
this.addToHistory("update", post.title, true);
|
||||||
|
new Notice(i18next.t("command.update_post.success"));
|
||||||
|
break;
|
||||||
|
case "publish":
|
||||||
|
await service.publishPost();
|
||||||
|
this.addToHistory("publish", post.title, true);
|
||||||
|
new Notice(i18next.t("service.notice_publish_success"));
|
||||||
|
break;
|
||||||
|
case "pull":
|
||||||
|
await service.pullPost(post.haloName);
|
||||||
|
this.addToHistory("pull", post.title, true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.render();
|
||||||
|
} catch {
|
||||||
|
this.addToHistory(action, post.title, false);
|
||||||
|
new Notice(i18next.t("service.error_publish_failed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderPostDetail(post: PostItem): void {
|
||||||
|
const modal = document.createElement("div");
|
||||||
|
modal.className = "sync-detail-modal";
|
||||||
|
modal.style.cssText = `
|
||||||
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||||
|
background: rgba(0,0,0,0.6); display: flex; align-items: center;
|
||||||
|
justify-content: center; z-index: 1000;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const content = document.createElement("div");
|
||||||
|
content.className = "sync-detail-content";
|
||||||
|
content.style.cssText = `
|
||||||
|
background: var(--background-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 480px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
||||||
|
padding: 24px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const title = document.createElement("h3");
|
||||||
|
title.textContent = i18next.t("sync_panel.detail_title");
|
||||||
|
title.style.cssText = "margin: 0 0 16px 0;";
|
||||||
|
content.appendChild(title);
|
||||||
|
|
||||||
|
const statusBadge = document.createElement("div");
|
||||||
|
statusBadge.style.cssText = `
|
||||||
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
|
padding: 4px 12px; border-radius: 16px;
|
||||||
|
background: var(--background-secondary);
|
||||||
|
font-size: 12px; margin-bottom: 16px;
|
||||||
|
`;
|
||||||
|
statusBadge.textContent = `${this.getStatusIcon(post.syncStatus)} ${this.getStatusText(post.syncStatus)}`;
|
||||||
|
content.appendChild(statusBadge);
|
||||||
|
|
||||||
|
const infoTable = document.createElement("div");
|
||||||
|
infoTable.style.cssText = "display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px;";
|
||||||
|
|
||||||
|
const addRow = (label: string, value: string) => {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.style.cssText = "display: flex; justify-content: space-between;";
|
||||||
|
row.innerHTML = `<span style="color: var(--text-muted)">${label}</span><span>${value}</span>`;
|
||||||
|
infoTable.appendChild(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
addRow(i18next.t("sync_panel.detail_title_label"), post.title);
|
||||||
|
addRow("Slug", post.slug || "-");
|
||||||
|
addRow(i18next.t("sync_panel.detail_category"), post.categories.join(" > "));
|
||||||
|
addRow(i18next.t("sync_panel.detail_tags"), post.tags.length > 0 ? post.tags.map((t) => `#${t}`).join(" ") : "-");
|
||||||
|
|
||||||
|
content.appendChild(infoTable);
|
||||||
|
|
||||||
|
if (post.localModified) {
|
||||||
|
const localSection = document.createElement("div");
|
||||||
|
localSection.style.cssText = "background: var(--background-secondary); padding: 12px; border-radius: 8px; margin-bottom: 12px;";
|
||||||
|
localSection.innerHTML = `
|
||||||
|
<div style="font-weight: 600; margin-bottom: 4px;">${i18next.t("sync_panel.detail_local_version")}</div>
|
||||||
|
<div style="font-size: 12px; color: var(--text-muted);">${i18next.t("sync_panel.detail_modified")}: ${this.formatRelativeTime(post.localModified)}</div>
|
||||||
|
`;
|
||||||
|
content.appendChild(localSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeBtn = document.createElement("button");
|
||||||
|
closeBtn.textContent = i18next.t("common.button_close");
|
||||||
|
closeBtn.style.cssText = "width: 100%; margin-top: 16px; padding: 10px;";
|
||||||
|
closeBtn.addEventListener("click", () => {
|
||||||
|
modal.remove();
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
content.appendChild(closeBtn);
|
||||||
|
|
||||||
|
modal.addEventListener("click", (e) => {
|
||||||
|
if (e.target === modal) {
|
||||||
|
modal.remove();
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private render() {
|
||||||
const container = this.containerEl;
|
const container = this.containerEl;
|
||||||
container.empty();
|
container.empty();
|
||||||
|
this.selectedPosts.clear();
|
||||||
|
|
||||||
const header = container.createDiv("sync-header");
|
const header = container.createDiv("sync-header");
|
||||||
header.createEl("h2", { text: i18next.t("sync_panel.title") });
|
header.style.cssText = "display: flex; justify-content: space-between; align-items: center; padding: 16px; border-bottom: 1px solid var(--border-color);";
|
||||||
|
|
||||||
|
const titleSection = header.createDiv("sync-title-section");
|
||||||
|
titleSection.style.cssText = "display: flex; align-items: center; gap: 8px;";
|
||||||
|
|
||||||
|
const statusDot = titleSection.createSpan("sync-status-dot");
|
||||||
|
statusDot.style.cssText = "width: 10px; height: 10px; border-radius: 50%; background: var(--color-green);";
|
||||||
|
|
||||||
|
titleSection.createEl("h2", { text: i18next.t("sync_panel.title"), cls: "sync-view-title" });
|
||||||
|
|
||||||
const actions = header.createDiv("sync-actions");
|
const actions = header.createDiv("sync-actions");
|
||||||
actions.createEl("button", {
|
actions.style.cssText = "display: flex; gap: 8px;";
|
||||||
text: i18next.t("sync_panel.button_refresh"),
|
|
||||||
cls: "sync-action-btn"
|
|
||||||
}).addEventListener("click", () => this.render());
|
|
||||||
|
|
||||||
actions.createEl("button", {
|
const settingsBtn = actions.createEl("button", {
|
||||||
text: i18next.t("sync_panel.button_history"),
|
text: "⚙️",
|
||||||
cls: "sync-action-btn"
|
cls: "sync-icon-btn",
|
||||||
}).addEventListener("click", () => this.showHistory());
|
attr: { title: i18next.t("sync_panel.button_settings") }
|
||||||
|
});
|
||||||
|
settingsBtn.style.cssText = "border: none; background: transparent; cursor: pointer; font-size: 16px; padding: 4px;";
|
||||||
|
settingsBtn.addEventListener("click", () => {
|
||||||
|
(this.app as unknown as { settings: unknown }).settings;
|
||||||
|
});
|
||||||
|
|
||||||
actions.createEl("button", {
|
const historyBtn = actions.createEl("button", {
|
||||||
text: i18next.t("sync_panel.button_clear_history"),
|
text: "📊",
|
||||||
cls: "sync-action-btn danger"
|
cls: "sync-icon-btn",
|
||||||
}).addEventListener("click", () => this.clearHistory());
|
attr: { title: i18next.t("sync_panel.button_history") }
|
||||||
|
});
|
||||||
|
historyBtn.style.cssText = "border: none; background: transparent; cursor: pointer; font-size: 16px; padding: 4px;";
|
||||||
|
historyBtn.addEventListener("click", () => this.showHistory());
|
||||||
|
|
||||||
|
const searchBar = container.createDiv("sync-search-bar");
|
||||||
|
searchBar.style.cssText = "padding: 12px 16px; border-bottom: 1px solid var(--border-color);";
|
||||||
|
|
||||||
|
const searchInput = searchBar.createEl("input", {
|
||||||
|
type: "text",
|
||||||
|
cls: "sync-search-input"
|
||||||
|
}) as HTMLInputElement;
|
||||||
|
searchInput.setAttribute("placeholder", i18next.t("sync_panel.search_placeholder"));
|
||||||
|
searchInput.style.cssText = `
|
||||||
|
width: 100%; padding: 8px 12px; border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px; background: var(--background-secondary);
|
||||||
|
`;
|
||||||
|
searchInput.value = this.searchQuery;
|
||||||
|
searchInput.addEventListener("input", () => {
|
||||||
|
this.searchQuery = searchInput.value;
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
const filterTabs = container.createDiv("sync-filter-tabs");
|
||||||
|
filterTabs.style.cssText = "display: flex; padding: 0 16px; gap: 8px; border-bottom: 1px solid var(--border-color);";
|
||||||
|
|
||||||
|
const filters: Array<{ key: typeof this.filterMode; label: string }> = [
|
||||||
|
{ key: "all", label: `📁 ${i18next.t("sync_panel.filter_all")}` },
|
||||||
|
{ key: "synced", label: `✅ ${i18next.t("sync_panel.filter_synced")}` },
|
||||||
|
{ key: "pending", label: `🔄 ${i18next.t("sync_panel.filter_pending")}` },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const filter of filters) {
|
||||||
|
const tab = filterTabs.createEl("button");
|
||||||
|
tab.textContent = filter.label;
|
||||||
|
tab.style.cssText = `
|
||||||
|
padding: 8px 16px; border: none; background: transparent;
|
||||||
|
cursor: pointer; border-bottom: 2px solid transparent;
|
||||||
|
font-size: 13px; transition: all 0.2s;
|
||||||
|
`;
|
||||||
|
if (this.filterMode === filter.key) {
|
||||||
|
tab.style.borderBottomColor = "var(--interactive-accent)";
|
||||||
|
tab.style.color = "var(--interactive-accent)";
|
||||||
|
}
|
||||||
|
tab.addEventListener("click", () => {
|
||||||
|
this.filterMode = filter.key;
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = container.createDiv("sync-content");
|
||||||
|
content.style.cssText = "flex: 1; overflow-y: auto; padding: 16px;";
|
||||||
|
|
||||||
const posts = this.getPublishedPosts();
|
const posts = this.getPublishedPosts();
|
||||||
|
const filteredPosts = this.filterPosts(posts);
|
||||||
|
const groupedPosts = this.groupPostsByCategory(filteredPosts);
|
||||||
|
|
||||||
if (posts.length === 0) {
|
if (filteredPosts.length === 0) {
|
||||||
container.createEl("p", {
|
content.createEl("div", {
|
||||||
text: i18next.t("sync_panel.empty"),
|
text: posts.length === 0 ? i18next.t("sync_panel.empty") : i18next.t("sync_panel.no_results"),
|
||||||
cls: "sync-empty"
|
cls: "sync-empty"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const list = container.createDiv("sync-list");
|
for (const group of groupedPosts) {
|
||||||
|
const groupHeader = content.createDiv("sync-group-header");
|
||||||
|
groupHeader.style.cssText = `
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
padding: 8px 0; margin-bottom: 8px;
|
||||||
|
font-weight: 600; font-size: 14px; color: var(--text-muted);
|
||||||
|
`;
|
||||||
|
groupHeader.createSpan({ text: "📂" });
|
||||||
|
groupHeader.createEl("span", { text: `${group.category} (${group.posts.length})` });
|
||||||
|
|
||||||
for (const post of posts) {
|
for (const post of group.posts) {
|
||||||
const item = list.createDiv("sync-item");
|
this.renderPostCard(content, post);
|
||||||
|
}
|
||||||
const statusIcon = item.createSpan({
|
|
||||||
text: post.publishStatus ? "✅" : "📝",
|
|
||||||
cls: "sync-status-icon"
|
|
||||||
});
|
|
||||||
|
|
||||||
const info = item.createDiv("sync-info");
|
|
||||||
info.createEl("span", { text: post.title, cls: "sync-title" });
|
|
||||||
info.createEl("span", { text: `Slug: ${post.slug}`, cls: "sync-slug" });
|
|
||||||
|
|
||||||
const itemActions = item.createDiv("sync-item-actions");
|
|
||||||
|
|
||||||
itemActions.createEl("button", {
|
|
||||||
text: i18next.t("sync_panel.button_update"),
|
|
||||||
cls: "sync-item-btn"
|
|
||||||
}).addEventListener("click", async () => {
|
|
||||||
const site = this.plugin.settings.sites.find(s => s.url === post.haloSite);
|
|
||||||
if (!site) {
|
|
||||||
new Notice(i18next.t("service.error_site_not_match"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { default: HaloService } = await import("../service");
|
|
||||||
const service = new HaloService(this.plugin.app, this.plugin.settings, site);
|
|
||||||
await service.updatePost();
|
|
||||||
this.addToHistory("update", post.title, true);
|
|
||||||
new Notice(i18next.t("command.update_post.success"));
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
|
|
||||||
itemActions.createEl("button", {
|
|
||||||
text: i18next.t("sync_panel.button_pull"),
|
|
||||||
cls: "sync-item-btn"
|
|
||||||
}).addEventListener("click", async () => {
|
|
||||||
const site = this.plugin.settings.sites.find(s => s.url === post.haloSite);
|
|
||||||
if (!site) {
|
|
||||||
new Notice(i18next.t("service.error_site_not_match"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { default: HaloService } = await import("../service");
|
|
||||||
const service = new HaloService(this.plugin.app, this.plugin.settings, site);
|
|
||||||
await service.pullPost(post.haloName);
|
|
||||||
this.addToHistory("pull", post.title, true);
|
|
||||||
this.render();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const stats = container.createDiv("sync-stats");
|
const stats = container.createDiv("sync-stats");
|
||||||
stats.createEl("span", { text: i18next.t("sync_panel.total_posts", { count: posts.length }) });
|
stats.style.cssText = `
|
||||||
|
padding: 12px 16px; border-top: 1px solid var(--border-color);
|
||||||
|
display: flex; justify-content: space-between; font-size: 12px; color: var(--text-muted);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const syncedCount = filteredPosts.filter((p) => p.syncStatus === "synced").length;
|
||||||
|
const pendingCount = filteredPosts.length - syncedCount;
|
||||||
|
|
||||||
|
stats.createEl("span", { text: i18next.t("sync_panel.stats_total", { count: filteredPosts.length }) });
|
||||||
|
stats.createEl("span", { text: i18next.t("sync_panel.stats_synced", { count: syncedCount }) });
|
||||||
|
stats.createEl("span", { text: i18next.t("sync_panel.stats_pending", { count: pendingCount }), cls: "sync-stats-pending" });
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderPostCard(container: HTMLElement, post: PostItem) {
|
||||||
|
const card = container.createDiv("sync-post-card");
|
||||||
|
card.style.cssText = `
|
||||||
|
background: var(--background-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
`;
|
||||||
|
|
||||||
|
card.addEventListener("mouseenter", () => {
|
||||||
|
card.style.borderColor = "var(--interactive-accent)";
|
||||||
|
card.style.boxShadow = "0 2px 8px rgba(0,0,0,0.1)";
|
||||||
|
});
|
||||||
|
card.addEventListener("mouseleave", () => {
|
||||||
|
card.style.borderColor = "var(--border-color)";
|
||||||
|
card.style.boxShadow = "none";
|
||||||
|
});
|
||||||
|
|
||||||
|
const cardHeader = card.createDiv("card-header");
|
||||||
|
cardHeader.style.cssText = "display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px;";
|
||||||
|
|
||||||
|
const cardTitle = cardHeader.createDiv("card-title-section");
|
||||||
|
cardTitle.style.cssText = "display: flex; align-items: center; gap: 8px; flex: 1;";
|
||||||
|
|
||||||
|
const checkbox = cardTitle.createEl("input", {
|
||||||
|
type: "checkbox",
|
||||||
|
cls: "sync-post-checkbox"
|
||||||
|
}) as HTMLInputElement;
|
||||||
|
checkbox.style.cssText = "width: 16px; height: 16px; cursor: pointer;";
|
||||||
|
checkbox.checked = this.selectedPosts.has(post.haloName);
|
||||||
|
checkbox.addEventListener("change", () => {
|
||||||
|
if (checkbox.checked) {
|
||||||
|
this.selectedPosts.add(post.haloName);
|
||||||
|
} else {
|
||||||
|
this.selectedPosts.delete(post.haloName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusIcon = cardTitle.createSpan("card-status-icon");
|
||||||
|
statusIcon.style.cssText = "font-size: 16px;";
|
||||||
|
statusIcon.setText(this.getStatusIcon(post.syncStatus));
|
||||||
|
|
||||||
|
const titleSpan = cardTitle.createEl("span", { text: post.title, cls: "card-title" });
|
||||||
|
titleSpan.style.cssText = "font-weight: 600; font-size: 14px;";
|
||||||
|
|
||||||
|
const statusBadge = cardHeader.createDiv("card-status-badge");
|
||||||
|
statusBadge.style.cssText = `
|
||||||
|
padding: 2px 8px; border-radius: 10px;
|
||||||
|
font-size: 11px; background: var(--background-primary);
|
||||||
|
`;
|
||||||
|
statusBadge.setText(this.getStatusText(post.syncStatus));
|
||||||
|
|
||||||
|
const cardMeta = card.createDiv("card-meta");
|
||||||
|
cardMeta.style.cssText = "font-size: 12px; color: var(--text-muted); margin-bottom: 12px; padding-left: 24px;";
|
||||||
|
|
||||||
|
if (post.slug) {
|
||||||
|
cardMeta.createSpan({ text: `Slug: ${post.slug}`, cls: "card-slug" });
|
||||||
|
cardMeta.createSpan({ text: " • ", cls: "card-meta-sep" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (post.localModified) {
|
||||||
|
cardMeta.createSpan({
|
||||||
|
text: `${i18next.t("sync_panel.last_sync")}: ${this.formatRelativeTime(post.localModified)}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagsContainer = card.createDiv("card-tags");
|
||||||
|
tagsContainer.style.cssText = "display: flex; flex-wrap: wrap; gap: 4px; padding-left: 24px; margin-bottom: 12px;";
|
||||||
|
for (const tag of post.tags.slice(0, 3)) {
|
||||||
|
const tagEl = tagsContainer.createEl("span", { text: `#${tag}`, cls: "card-tag" });
|
||||||
|
tagEl.style.cssText = `
|
||||||
|
padding: 2px 6px; border-radius: 4px;
|
||||||
|
font-size: 11px; background: var(--background-primary);
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
if (post.tags.length > 3) {
|
||||||
|
tagsContainer.createSpan({ text: `+${post.tags.length - 3}`, cls: "card-tag-more" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardActions = card.createDiv("card-actions");
|
||||||
|
cardActions.style.cssText = "display: flex; gap: 6px; padding-left: 24px;";
|
||||||
|
|
||||||
|
const actionButtons: Array<{ text: string; class: string; action: () => void }> = [
|
||||||
|
{
|
||||||
|
text: "👁",
|
||||||
|
class: "card-btn card-btn-preview",
|
||||||
|
action: () => this.renderPostDetail(post),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: post.publishStatus ? "✏️" : "⬆️",
|
||||||
|
class: "card-btn card-btn-primary",
|
||||||
|
action: () => this.performAction(post, post.publishStatus ? "update" : "publish"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "🔄",
|
||||||
|
class: "card-btn",
|
||||||
|
action: () => this.performAction(post, "pull"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const btn of actionButtons) {
|
||||||
|
const button = cardActions.createEl("button", { text: btn.text, cls: btn.class });
|
||||||
|
button.style.cssText = `
|
||||||
|
width: 32px; height: 32px; border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px; background: var(--background-primary);
|
||||||
|
cursor: pointer; font-size: 14px; display: flex;
|
||||||
|
align-items: center; justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
`;
|
||||||
|
button.addEventListener("mouseenter", () => {
|
||||||
|
button.style.background = "var(--interactive-accent)";
|
||||||
|
button.style.borderColor = "var(--interactive-accent)";
|
||||||
|
});
|
||||||
|
button.addEventListener("mouseleave", () => {
|
||||||
|
button.style.background = "var(--background-primary)";
|
||||||
|
button.style.borderColor = "var(--border-color)";
|
||||||
|
});
|
||||||
|
button.addEventListener("click", btn.action);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private showHistory() {
|
private showHistory() {
|
||||||
const modal = document.createElement("div");
|
const modal = document.createElement("div");
|
||||||
modal.className = "sync-history-modal";
|
modal.className = "sync-history-modal";
|
||||||
modal.style.cssText = `
|
modal.style.cssText = `
|
||||||
position: fixed;
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||||
top: 0;
|
background: rgba(0,0,0,0.5); display: flex; align-items: center;
|
||||||
left: 0;
|
justify-content: center; z-index: 1000;
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0,0,0,0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const content = modal.createDiv("sync-history-content");
|
const content = document.createElement("div");
|
||||||
|
content.className = "sync-history-content";
|
||||||
content.style.cssText = `
|
content.style.cssText = `
|
||||||
background: var(--background-primary);
|
background: var(--background-primary); padding: 24px;
|
||||||
padding: 20px;
|
border-radius: 12px; width: 520px; max-height: 80vh;
|
||||||
border-radius: 8px;
|
overflow-y: auto; box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
||||||
max-width: 500px;
|
|
||||||
max-height: 80vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
content.createEl("h3", { text: i18next.t("sync_history.title") });
|
const title = content.createElement("h3");
|
||||||
|
title.textContent = i18next.t("sync_history.title");
|
||||||
|
title.style.cssText = "margin: 0 0 16px 0;";
|
||||||
|
|
||||||
const closeBtn = content.createEl("button", { text: i18next.t("common.button_close") });
|
if (this.history.length > 0) {
|
||||||
closeBtn.style.cssText = "margin-bottom: 15px;";
|
const stats = document.createElement("div");
|
||||||
|
stats.style.cssText = "font-size: 12px; color: var(--text-muted); margin-bottom: 16px;";
|
||||||
|
const successCount = this.history.filter((h) => h.success).length;
|
||||||
|
stats.textContent = i18next.t("sync_history.stats", { total: this.history.length, success: successCount });
|
||||||
|
content.appendChild(stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeBtn = content.createElement("button");
|
||||||
|
closeBtn.textContent = i18next.t("common.button_close");
|
||||||
|
closeBtn.style.cssText = "margin-bottom: 16px;";
|
||||||
closeBtn.addEventListener("click", () => modal.remove());
|
closeBtn.addEventListener("click", () => modal.remove());
|
||||||
|
|
||||||
if (this.history.length === 0) {
|
if (this.history.length === 0) {
|
||||||
content.createEl("p", { text: i18next.t("sync_history.empty") });
|
const empty = content.createElement("p");
|
||||||
|
empty.textContent = i18next.t("sync_history.empty");
|
||||||
|
empty.className = "history-empty";
|
||||||
} else {
|
} else {
|
||||||
const list = content.createDiv("sync-history-list");
|
const list = content.createElement("div");
|
||||||
|
list.className = "sync-history-list";
|
||||||
|
|
||||||
for (const item of [...this.history].reverse()) {
|
for (const item of [...this.history].reverse()) {
|
||||||
const historyItem = list.createDiv("history-item");
|
const historyItem = document.createElement("div");
|
||||||
const time = new Date(item.timestamp).toLocaleString();
|
historyItem.style.cssText = `
|
||||||
const actionText = i18next.t(`sync_history.action_${item.action}`);
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
const icon = item.success ? "✅" : "❌";
|
padding: 10px; border-bottom: 1px solid var(--border-color);
|
||||||
|
`;
|
||||||
|
|
||||||
historyItem.createEl("span", { text: `${icon} ${actionText}: ${item.title}` });
|
const info = document.createElement("div");
|
||||||
historyItem.createEl("span", { text: time, cls: "history-time" });
|
info.style.cssText = "display: flex; align-items: center; gap: 8px;";
|
||||||
|
info.innerHTML = `<span>${item.success ? "✅" : "❌"}</span><span>${i18next.t(`sync_history.action_${item.action}`)}: ${item.title}</span>`;
|
||||||
|
|
||||||
|
const time = document.createElement("span");
|
||||||
|
time.style.cssText = "font-size: 11px; color: var(--text-muted);";
|
||||||
|
time.textContent = new Date(item.timestamp).toLocaleString();
|
||||||
|
|
||||||
|
historyItem.appendChild(info);
|
||||||
|
historyItem.appendChild(time);
|
||||||
|
list.appendChild(historyItem);
|
||||||
}
|
}
|
||||||
|
content.appendChild(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clearBtn = content.createElement("button");
|
||||||
|
clearBtn.textContent = i18next.t("sync_panel.button_clear_history");
|
||||||
|
clearBtn.className = "history-clear-btn";
|
||||||
|
clearBtn.style.cssText = "width: 100%; margin-top: 16px;";
|
||||||
|
clearBtn.addEventListener("click", () => {
|
||||||
|
this.clearHistory();
|
||||||
|
modal.remove();
|
||||||
|
});
|
||||||
|
|
||||||
modal.addEventListener("click", (e) => {
|
modal.addEventListener("click", (e) => {
|
||||||
if (e.target === modal) modal.remove();
|
if (e.target === modal) modal.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
content.insertBefore(closeBtn, content.firstChild);
|
||||||
|
content.appendChild(clearBtn);
|
||||||
|
modal.appendChild(content);
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+263
-4
@@ -1,8 +1,267 @@
|
|||||||
/*
|
/*
|
||||||
|
* Obsidian Halo Plugin Styles
|
||||||
|
*/
|
||||||
|
|
||||||
This CSS file will be included with your plugin, and
|
/* Sync Status View */
|
||||||
available in the app when your plugin is enabled.
|
.sync-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
If your plugin does not need CSS, delete this file.
|
.sync-view-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
*/
|
.sync-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-icon-btn {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 4px;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-icon-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-search-input {
|
||||||
|
outline: none;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-search-input:focus {
|
||||||
|
border-color: var(--interactive-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-filter-tabs {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-filter-tab {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-filter-tab.active {
|
||||||
|
color: var(--interactive-accent);
|
||||||
|
border-bottom-color: var(--interactive-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-content {
|
||||||
|
background: var(--background-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-empty {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-group-header {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-group-header:hover {
|
||||||
|
color: var(--interactive-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Post Card */
|
||||||
|
.sync-post-card {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-post-card:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-meta-sep {
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-tag {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-tag-more {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-btn {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-btn:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-btn-primary {
|
||||||
|
background: var(--interactive-accent);
|
||||||
|
border-color: var(--interactive-accent);
|
||||||
|
color: var(--text-on-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-btn-primary:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detail Modal */
|
||||||
|
.sync-detail-content {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-status-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: var(--background-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-version-section {
|
||||||
|
background: var(--background-secondary);
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-version-title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-version-info {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-close-btn {
|
||||||
|
background: var(--background-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-close-btn:hover {
|
||||||
|
background: var(--interactive-accent);
|
||||||
|
border-color: var(--interactive-accent);
|
||||||
|
color: var(--text-on-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* History Modal */
|
||||||
|
.sync-history-content {
|
||||||
|
max-width: 520px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-modal-title {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-stats {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-empty {
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-title {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-time {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-clear-btn {
|
||||||
|
background: var(--background-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-clear-btn.danger:hover {
|
||||||
|
background: var(--color-red);
|
||||||
|
border-color: var(--color-red);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats Bar */
|
||||||
|
.sync-stats {
|
||||||
|
background: var(--background-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-stats-pending {
|
||||||
|
color: var(--color-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation */
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-status-dot.pulse {
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode support */
|
||||||
|
.is-mobile .sync-post-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
@@ -1,3 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: Git团队协作指南大纲
|
||||||
|
slug: gittuan-dui-xie-zuo-zhi-nan-da-gang
|
||||||
|
cover: ""
|
||||||
|
categories: []
|
||||||
|
tags: []
|
||||||
|
halo:
|
||||||
|
site: http://101.133.128.193:8091
|
||||||
|
name: 89472ee2-4320-44ed-a107-8e3acba90b2a
|
||||||
|
publish: false
|
||||||
|
---
|
||||||
# 《Git团队协作指南:从入门到精通》详细写作大纲
|
# 《Git团队协作指南:从入门到精通》详细写作大纲
|
||||||
|
|
||||||
> **预计总字数**:20000字以上
|
> **预计总字数**:20000字以上
|
||||||
|
|||||||
Reference in New Issue
Block a user