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:
2026-04-28 18:32:42 +08:00
parent 12a7aebeff
commit 86514309cf
10 changed files with 949 additions and 136 deletions
+2 -1
View File
@@ -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": [
+14
View File
@@ -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",
+12
View File
@@ -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"
+13 -17
View File
@@ -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"
] ]
} }
+25 -1
View File
@@ -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",
+25 -1
View File
@@ -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": "同步历史",
+25 -1
View File
@@ -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": "同步歷史",
+556 -108
View File
@@ -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);
} }
+265 -6
View File
@@ -1,8 +1,267 @@
/* /*
* Obsidian Halo Plugin Styles
This CSS file will be included with your plugin, and
available in the app when your plugin is enabled.
If your plugin does not need CSS, delete this file.
*/ */
/* Sync Status View */
.sync-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.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字以上