From 86514309cf2711024aecd33944a60456b2eeb9d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E8=88=AA=E5=AE=87?= <3364451258@qq.com> Date: Tue, 28 Apr 2026 18:32:42 +0800 Subject: [PATCH] feat: Enhance sync status view with filtering and improved UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- .obsidian/plugins/copilot/data.json | 3 +- .obsidian/plugins/halo/data.json | 14 + .../plugins/recent-files-obsidian/data.json | 12 + .obsidian/workspace.json | 30 +- obsidian-halo/src/i18n/locales/en.json | 26 +- obsidian-halo/src/i18n/locales/zh-cn.json | 26 +- obsidian-halo/src/i18n/locales/zh-tw.json | 26 +- obsidian-halo/src/views/sync-status-view.ts | 670 +++++++++++++++--- obsidian-halo/styles.css | 267 ++++++- 博客/其他/Git团队协作指南大纲.md | 11 + 10 files changed, 949 insertions(+), 136 deletions(-) diff --git a/.obsidian/plugins/copilot/data.json b/.obsidian/plugins/copilot/data.json index 1b7897f..0e9460f 100644 --- a/.obsidian/plugins/copilot/data.json +++ b/.obsidian/plugins/copilot/data.json @@ -287,7 +287,8 @@ "apiKey": "sk-cp-flXHABxXJ00bLhkLQbRVgFLswCJRqXOeL53iu9O-BrEEygpzzYRGz7NNR2NpJHk-RaZ-pwXXStDWawkXo4h4u-zI9tjyf-F9FhsyGGhtZk1kurt2VvJ_T1I", "isEmbeddingModel": false, "capabilities": [], - "stream": true + "stream": true, + "maxTokens": 14600 } ], "activeEmbeddingModels": [ diff --git a/.obsidian/plugins/halo/data.json b/.obsidian/plugins/halo/data.json index f4afdda..6b4bc5b 100644 --- a/.obsidian/plugins/halo/data.json +++ b/.obsidian/plugins/halo/data.json @@ -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": [ { "id": "1777199537235", diff --git a/.obsidian/plugins/recent-files-obsidian/data.json b/.obsidian/plugins/recent-files-obsidian/data.json index afa8d36..e5e59f6 100644 --- a/.obsidian/plugins/recent-files-obsidian/data.json +++ b/.obsidian/plugins/recent-files-obsidian/data.json @@ -1,5 +1,17 @@ { "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博客大纲", "path": "博客/其他/DeepSeek-V4博客大纲.md" diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json index 4701203..33ea25a 100644 --- a/.obsidian/workspace.json +++ b/.obsidian/workspace.json @@ -11,14 +11,10 @@ "id": "e7a7b303c61786dc", "type": "leaf", "state": { - "type": "markdown", - "state": { - "file": "博客/其他/DeepSeek-V4博客大纲.md", - "mode": "source", - "source": false - }, + "type": "empty", + "state": {}, "icon": "lucide-file", - "title": "DeepSeek-V4博客大纲" + "title": "新标签页" } } ] @@ -88,7 +84,7 @@ } ], "direction": "horizontal", - "width": 263.5 + "width": 276.5 }, "right": { "id": "1a950cafdb3ea126", @@ -200,7 +196,7 @@ } } ], - "currentTab": 3 + "currentTab": 5 } ], "direction": "horizontal", @@ -208,7 +204,6 @@ }, "left-ribbon": { "hiddenItems": { - "halo:发布当前文档到 Halo": false, "halo:同步状态面板": false, "bases:新建数据库": false, "switcher:打开快速切换": false, @@ -227,11 +222,17 @@ "mermaid-tools:Open Mermaid Toolbar": false, "omnisearch:Omnisearch": false, "remotely-save:Remotely Save": false, - "templater-obsidian:Templater": false + "templater-obsidian:Templater": false, + "halo:发布当前文档到 Halo": false } }, - "active": "e7a7b303c61786dc", + "active": "1e741b4d78845bd1", "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", "copilot", @@ -242,7 +243,6 @@ "博客/文章列表.md", "未命名 2.base", "未命名 1.base", - "未命名.base", "Excalidraw/Drawing 2026-04-26 18.22.24.excalidraw.md", "Excalidraw", "博客/OpenClaw介绍.md", @@ -263,7 +263,6 @@ "博客/LinearRegression线性回归.md", "博客/Git团队协作指南.md", "博客/大数据技术栈.md", - "博客/大模型赋能架构设计.md", "obsidian-halo/src/commands/manage-taxonomy.ts", "obsidian-halo-zip-test/images/pat-zh.png", "obsidian-halo-zip-test/images/settings-zh.png", @@ -275,9 +274,6 @@ "obsidian-halo/images/settings-en.png", "obsidian-halo/images/pat-en.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" ] } \ No newline at end of file diff --git a/obsidian-halo/src/i18n/locales/en.json b/obsidian-halo/src/i18n/locales/en.json index 579027f..1f985e5 100644 --- a/obsidian-halo/src/i18n/locales/en.json +++ b/obsidian-halo/src/i18n/locales/en.json @@ -258,10 +258,34 @@ "button_refresh": "Refresh", "button_history": "History", "button_clear_history": "Clear History", + "button_settings": "Settings", "button_update": "Update", "button_pull": "Pull", "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": { "title": "Sync History", diff --git a/obsidian-halo/src/i18n/locales/zh-cn.json b/obsidian-halo/src/i18n/locales/zh-cn.json index a4aea33..f05401a 100644 --- a/obsidian-halo/src/i18n/locales/zh-cn.json +++ b/obsidian-halo/src/i18n/locales/zh-cn.json @@ -258,10 +258,34 @@ "button_refresh": "刷新", "button_history": "历史", "button_clear_history": "清除历史", + "button_settings": "设置", "button_update": "更新", "button_pull": "拉取", "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": { "title": "同步历史", diff --git a/obsidian-halo/src/i18n/locales/zh-tw.json b/obsidian-halo/src/i18n/locales/zh-tw.json index 40b100a..746fe38 100644 --- a/obsidian-halo/src/i18n/locales/zh-tw.json +++ b/obsidian-halo/src/i18n/locales/zh-tw.json @@ -258,10 +258,34 @@ "button_refresh": "刷新", "button_history": "歷史", "button_clear_history": "清除歷史", + "button_settings": "設置", "button_update": "更新", "button_pull": "拉取", "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": { "title": "同步歷史", diff --git a/obsidian-halo/src/views/sync-status-view.ts b/obsidian-halo/src/views/sync-status-view.ts index 133bf94..5c639bb 100644 --- a/obsidian-halo/src/views/sync-status-view.ts +++ b/obsidian-halo/src/views/sync-status-view.ts @@ -4,17 +4,43 @@ import type HaloPlugin from "../main"; export const SYNC_STATUS_VIEW_TYPE = "halo-sync-status"; +type SyncAction = "publish" | "update" | "pull" | "delete"; + interface SyncHistoryItem { id: string; - action: "publish" | "update" | "pull" | "delete"; + action: SyncAction; title: string; timestamp: number; 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 { private plugin: HaloPlugin; private history: SyncHistoryItem[] = []; + private selectedPosts: Set = new Set(); + private searchQuery: string = ""; + private filterMode: "all" | "synced" | "pending" = "all"; constructor(leaf: WorkspaceLeaf, plugin: HaloPlugin) { super(leaf); @@ -45,7 +71,7 @@ export class SyncStatusView extends ItemView { this.plugin.saveData(data); } - addToHistory(action: "publish" | "update" | "pull" | "delete", title: string, success: boolean) { + addToHistory(action: SyncAction, title: string, success: boolean) { this.history.push({ id: Date.now().toString(), action, @@ -56,15 +82,8 @@ export class SyncStatusView extends ItemView { this.saveHistory(); } - private getPublishedPosts() { - const publishedPosts: Array<{ - file: TFile; - title: string; - slug: string; - haloName: string; - haloSite: string; - publishStatus: boolean; - }> = []; + private getPublishedPosts(): PostItem[] { + const posts: PostItem[] = []; for (const file of this.plugin.app.vault.getFiles()) { if (file.extension !== "md") continue; @@ -72,160 +91,589 @@ export class SyncStatusView extends ItemView { const cache = this.plugin.app.metadataCache.getFileCache(file); 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, title: cache.frontmatter.title || file.basename, slug: cache.frontmatter.slug || "", haloName: cache.frontmatter.halo.name, 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[] { + 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[] { + 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(); + + 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 = `${label}${value}`; + 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 = ` +
${i18next.t("sync_panel.detail_local_version")}
+
${i18next.t("sync_panel.detail_modified")}: ${this.formatRelativeTime(post.localModified)}
+ `; + 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; container.empty(); + this.selectedPosts.clear(); 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"); - actions.createEl("button", { - text: i18next.t("sync_panel.button_refresh"), - cls: "sync-action-btn" - }).addEventListener("click", () => this.render()); + actions.style.cssText = "display: flex; gap: 8px;"; - actions.createEl("button", { - text: i18next.t("sync_panel.button_history"), - cls: "sync-action-btn" - }).addEventListener("click", () => this.showHistory()); + const settingsBtn = actions.createEl("button", { + text: "⚙️", + cls: "sync-icon-btn", + 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", { - text: i18next.t("sync_panel.button_clear_history"), - cls: "sync-action-btn danger" - }).addEventListener("click", () => this.clearHistory()); + const historyBtn = actions.createEl("button", { + text: "📊", + cls: "sync-icon-btn", + 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 filteredPosts = this.filterPosts(posts); + const groupedPosts = this.groupPostsByCategory(filteredPosts); - if (posts.length === 0) { - container.createEl("p", { - text: i18next.t("sync_panel.empty"), + if (filteredPosts.length === 0) { + content.createEl("div", { + text: posts.length === 0 ? i18next.t("sync_panel.empty") : i18next.t("sync_panel.no_results"), cls: "sync-empty" }); 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) { - const item = list.createDiv("sync-item"); - - 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(); - }); + for (const post of group.posts) { + this.renderPostCard(content, post); + } } 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() { const modal = document.createElement("div"); modal.className = "sync-history-modal"; modal.style.cssText = ` - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0,0,0,0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; + position: fixed; top: 0; left: 0; 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 = ` - background: var(--background-primary); - padding: 20px; - border-radius: 8px; - max-width: 500px; - max-height: 80vh; - overflow-y: auto; + background: var(--background-primary); padding: 24px; + border-radius: 12px; width: 520px; max-height: 80vh; + overflow-y: auto; box-shadow: 0 8px 32px rgba(0,0,0,0.3); `; - 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") }); - closeBtn.style.cssText = "margin-bottom: 15px;"; + if (this.history.length > 0) { + 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()); 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 { - const list = content.createDiv("sync-history-list"); - + const list = content.createElement("div"); + list.className = "sync-history-list"; + for (const item of [...this.history].reverse()) { - const historyItem = list.createDiv("history-item"); - const time = new Date(item.timestamp).toLocaleString(); - const actionText = i18next.t(`sync_history.action_${item.action}`); - const icon = item.success ? "✅" : "❌"; - - historyItem.createEl("span", { text: `${icon} ${actionText}: ${item.title}` }); - historyItem.createEl("span", { text: time, cls: "history-time" }); + const historyItem = document.createElement("div"); + historyItem.style.cssText = ` + display: flex; justify-content: space-between; align-items: center; + padding: 10px; border-bottom: 1px solid var(--border-color); + `; + + const info = document.createElement("div"); + info.style.cssText = "display: flex; align-items: center; gap: 8px;"; + info.innerHTML = `${item.success ? "✅" : "❌"}${i18next.t(`sync_history.action_${item.action}`)}: ${item.title}`; + + 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) => { if (e.target === modal) modal.remove(); }); + content.insertBefore(closeBtn, content.firstChild); + content.appendChild(clearBtn); + modal.appendChild(content); document.body.appendChild(modal); } diff --git a/obsidian-halo/styles.css b/obsidian-halo/styles.css index 71cc60f..e5a6476 100644 --- a/obsidian-halo/styles.css +++ b/obsidian-halo/styles.css @@ -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. +/* Sync Status View */ +.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; +} \ No newline at end of file diff --git a/博客/其他/Git团队协作指南大纲.md b/博客/其他/Git团队协作指南大纲.md index 4ee4a11..bea4d89 100644 --- a/博客/其他/Git团队协作指南大纲.md +++ b/博客/其他/Git团队协作指南大纲.md @@ -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团队协作指南:从入门到精通》详细写作大纲 > **预计总字数**:20000字以上