diff --git a/src/components/BookmarkButton/index.jsx b/src/components/BookmarkButton/index.jsx
new file mode 100644
index 0000000..e32fe28
--- /dev/null
+++ b/src/components/BookmarkButton/index.jsx
@@ -0,0 +1,104 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import { useLocation } from '@docusaurus/router';
+import { useAllDocsData } from '@docusaurus/plugin-content-docs/client';
+import styles from './styles.module.css';
+
+const STORAGE_KEY = 'gitlink_bookmarks';
+
+function getBookmarks() {
+ try {
+ return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
+ } catch {
+ return [];
+ }
+}
+
+function deriveTitle(id) {
+ if (!id) return '';
+ const segments = id.split('/');
+ return segments[segments.length - 1];
+}
+
+export default function BookmarkButton() {
+ const location = useLocation();
+ const allDocsData = useAllDocsData();
+ const [bookmarked, setBookmarked] = useState(false);
+ const [toast, setToast] = useState(null);
+
+ // Check if current doc is bookmarked
+ useEffect(() => {
+ const stored = getBookmarks();
+ setBookmarked(stored.some((d) => d.path === location.pathname));
+ }, [location.pathname]);
+
+ const showToast = useCallback((msg) => {
+ setToast(msg);
+ setTimeout(() => setToast(null), 2000);
+ }, []);
+
+ const toggleBookmark = useCallback(() => {
+ if (!allDocsData) return;
+
+ const pluginIds = Object.keys(allDocsData);
+ if (pluginIds.length === 0) return;
+ const plugin = allDocsData[pluginIds[0]];
+
+ let activeVersion = null;
+ for (const version of plugin.versions) {
+ if (!version.docs) continue;
+ const docs = Object.values(version.docs);
+ if (docs.find((doc) => doc.path && location.pathname.endsWith(doc.path))) {
+ activeVersion = version;
+ break;
+ }
+ }
+ if (!activeVersion || !activeVersion.docs) return;
+
+ const allDocs = Object.values(activeVersion.docs);
+ const currentDoc = allDocs.find(
+ (doc) => doc.path && location.pathname.endsWith(doc.path)
+ );
+ if (!currentDoc) return;
+
+ const stored = getBookmarks();
+ const existing = stored.findIndex((d) => d.path === currentDoc.path);
+
+ if (existing >= 0) {
+ // Remove bookmark
+ stored.splice(existing, 1);
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
+ setBookmarked(false);
+ showToast('已取消收藏');
+ } else {
+ // Add bookmark
+ stored.unshift({
+ path: currentDoc.path,
+ id: currentDoc.id,
+ title: deriveTitle(currentDoc.id),
+ timestamp: Date.now(),
+ });
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
+ setBookmarked(true);
+ showToast('已收藏,可在侧边栏查看');
+ }
+
+ // Dispatch custom event so BookmarkList updates
+ window.dispatchEvent(new Event('bookmark-change'));
+ }, [location.pathname, allDocsData, showToast]);
+
+ return (
+ <>
+
+ {toast &&
{toast}
}
+ >
+ );
+}
diff --git a/src/components/BookmarkButton/styles.module.css b/src/components/BookmarkButton/styles.module.css
new file mode 100644
index 0000000..78f5d13
--- /dev/null
+++ b/src/components/BookmarkButton/styles.module.css
@@ -0,0 +1,49 @@
+.btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+ padding: 0.35rem 0.75rem;
+ border: 1px solid var(--ifm-color-emphasis-300);
+ border-radius: 6px;
+ background: var(--ifm-background-color);
+ color: var(--ifm-color-emphasis-700);
+ font-size: 0.8rem;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ line-height: 1.4;
+}
+
+.btn:hover {
+ border-color: var(--ifm-color-primary);
+ color: var(--ifm-color-primary);
+ background: var(--ifm-color-primary-contrast-background);
+}
+
+.btn.active {
+ border-color: var(--ifm-color-primary);
+ color: var(--ifm-color-primary);
+ background: var(--ifm-color-primary-contrast-background);
+}
+
+.toast {
+ position: fixed;
+ bottom: 2rem;
+ left: 50%;
+ transform: translateX(-50%);
+ padding: 0.5rem 1.2rem;
+ border-radius: 6px;
+ background: var(--ifm-color-emphasis-700);
+ color: var(--ifm-color-emphasis-100);
+ font-size: 0.85rem;
+ z-index: var(--ifm-z-index-fixed);
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
+ animation: fadeInOut 2s ease forwards;
+ pointer-events: none;
+}
+
+@keyframes fadeInOut {
+ 0% { opacity: 0; transform: translateX(-50%) translateY(8px); }
+ 15% { opacity: 1; transform: translateX(-50%) translateY(0); }
+ 85% { opacity: 1; transform: translateX(-50%) translateY(0); }
+ 100% { opacity: 0; transform: translateX(-50%) translateY(-8px); }
+}
diff --git a/src/components/BookmarkList/index.jsx b/src/components/BookmarkList/index.jsx
new file mode 100644
index 0000000..95c6332
--- /dev/null
+++ b/src/components/BookmarkList/index.jsx
@@ -0,0 +1,45 @@
+import React, { useState, useEffect } from 'react';
+import Link from '@docusaurus/Link';
+import styles from './styles.module.css';
+
+const STORAGE_KEY = 'gitlink_bookmarks';
+
+function getBookmarks() {
+ try {
+ return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
+ } catch {
+ return [];
+ }
+}
+
+export default function BookmarkList() {
+ const [bookmarks, setBookmarks] = useState([]);
+
+ useEffect(() => {
+ setBookmarks(getBookmarks());
+
+ const handler = () => setBookmarks(getBookmarks());
+ window.addEventListener('bookmark-change', handler);
+ return () => window.removeEventListener('bookmark-change', handler);
+ }, []);
+
+ if (bookmarks.length === 0) return null;
+
+ return (
+
+
收藏
+
+ {bookmarks.slice(0, 10).map((bm) => (
+ -
+
+
+ {bm.title}
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/BookmarkList/styles.module.css b/src/components/BookmarkList/styles.module.css
new file mode 100644
index 0000000..0bf733a
--- /dev/null
+++ b/src/components/BookmarkList/styles.module.css
@@ -0,0 +1,57 @@
+.container {
+}
+
+.heading {
+ font-size: 0.72rem;
+ font-weight: 600;
+ color: var(--ifm-color-emphasis-500);
+ margin-bottom: 0.35rem;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ padding: 0 0.25rem;
+}
+
+.list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+}
+
+.item {
+ margin: 0;
+}
+
+.link {
+ display: flex;
+ align-items: center;
+ gap: 0.3rem;
+ padding: 0.3rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.8rem;
+ text-decoration: none !important;
+ color: var(--ifm-font-color-base);
+ transition: all 0.12s ease;
+}
+
+.link:hover {
+ background: var(--ifm-menu-color-background-hover);
+ color: var(--ifm-menu-color-active);
+}
+
+.starIcon {
+ flex-shrink: 0;
+ color: var(--ifm-color-primary);
+ opacity: 0.7;
+}
+
+.title {
+ font-weight: 500;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ flex: 1;
+ min-width: 0;
+}
diff --git a/src/components/GitLinkAction/index.jsx b/src/components/GitLinkAction/index.jsx
deleted file mode 100644
index e80a19f..0000000
--- a/src/components/GitLinkAction/index.jsx
+++ /dev/null
@@ -1,138 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { useLocation } from '@docusaurus/router';
-import { useAllDocsData } from '@docusaurus/plugin-content-docs/client';
-import styles from './styles.module.css';
-
-const ACTION_LINKS = [
- // 快速开始
- { match: '快速开始/注册GitLink账号', url: 'https://www.gitlink.org.cn/sign_up', label: '去 GitLink 注册账号' },
- { match: '快速开始/创建第一个开源项目', url: 'https://www.gitlink.org.cn/repo/create', label: '去 GitLink 创建项目' },
- { match: '快速开始/提交第一行代码', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 提交代码' },
- { match: '快速开始/搜索开源项目', url: 'https://www.gitlink.org.cn/explore', label: '去 GitLink 发现项目' },
- { match: '快速开始/导入GitHub等第三方Git项目', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 导入项目' },
-
- // 代码库管理
- { match: '代码库管理/仓库创建', url: 'https://www.gitlink.org.cn/repo/create', label: '去 GitLink 创建仓库' },
- { match: '代码库管理/代码提交', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 提交代码' },
- { match: '代码库管理/分支管理', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 管理分支' },
- { match: '代码库管理/文件管理', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 管理文件' },
- { match: '代码库管理/成员管理', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 管理成员' },
- { match: '代码库管理/仓库设置', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 配置仓库' },
- { match: '代码库管理/标签和发行版管理', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 管理发行版' },
- { match: '代码库管理/Webhook', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 配置 Webhook' },
- { match: '代码库管理/WebIDE', url: 'https://www.gitlink.org.cn/user/projects', label: '打开 GitLink WebIDE' },
-
- // 合并请求
- { match: '合并请求/合并请求简介', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 查看合并请求' },
- { match: '合并请求/创建合并请求', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 创建合并请求' },
- { match: '合并请求/代码评审', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 评审代码' },
- { match: '合并请求/合并模式简介', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 合并代码' },
- { match: '合并请求/合并请求关联疑修', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 关联疑修' },
-
- // 疑修 (Issues)
- { match: '疑修/疑修简介', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 查看疑修' },
- { match: '疑修/疑修创建', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 提交疑修' },
- { match: '疑修/疑修列表', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 查看疑修列表' },
- { match: '疑修/疑修状态变更', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 管理疑修' },
- { match: '疑修/评论及操作记录', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 参与讨论' },
- { match: '疑修/标记管理', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 管理标记' },
- { match: '疑修/里程碑管理', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 管理里程碑' },
-
- // 组织管理
- { match: '组织管理/组织简介', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 管理组织' },
- { match: '组织管理/组织创建及设置', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 创建组织' },
- { match: '组织管理/组织成员管理', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 管理组织成员' },
- { match: '组织管理/组织团队管理', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 管理团队' },
- { match: '组织管理/组织项目管理', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 管理组织项目' },
-
- // DevOps 引擎
- { match: 'DevOps引擎/引擎简介', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 使用 DevOps' },
- { match: 'DevOps引擎/代码流水线', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 配置流水线' },
- { match: 'DevOps引擎/图形流水线', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 编辑流水线' },
- { match: 'DevOps引擎/参数配置', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 配置参数' },
- { match: 'DevOps引擎/密钥设置', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 配置密钥' },
- { match: 'DevOps引擎/执行记录查询', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 查看执行记录' },
-
- // 维基
- { match: '维基/维基页面管理', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 编辑维基' },
- { match: '维基/模板导入及导出', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 管理维基模板' },
-
- // 其他
- { match: '第三方服务/跨平台代码同步', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 配置同步' },
- { match: '第三方服务/重睛鸟代码溯源', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 查看溯源' },
- { match: '第三方服务/WebIDE', url: 'https://www.gitlink.org.cn/user/projects', label: '打开 GitLink WebIDE' },
- { match: '通知/通知简介', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 管理通知' },
- { match: '通知/通知设置', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 配置通知' },
- { match: 'Bot市场/bot市场', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 浏览 Bot 市场' },
- { match: 'Bot市场/bot安装', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 安装 Bot' },
- { match: 'Bot市场/bot配置', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 配置 Bot' },
- { match: 'Bot市场/bot开发', url: 'https://www.gitlink.org.cn/user/projects', label: '去 GitLink 开发 Bot' },
- { match: '服务协议/GitLink服务协议', url: 'https://www.gitlink.org.cn', label: '查看 GitLink 首页' },
-];
-
-export default function GitLinkAction() {
- const location = useLocation();
- const allDocsData = useAllDocsData();
- const [action, setAction] = useState(null);
-
- useEffect(() => {
- if (!allDocsData) return;
-
- const pluginIds = Object.keys(allDocsData);
- if (pluginIds.length === 0) return;
-
- const plugin = allDocsData[pluginIds[0]];
-
- let activeVersion = null;
- for (const version of plugin.versions) {
- if (!version.docs) continue;
- const docs = Object.values(version.docs);
- const found = docs.find(
- (doc) => doc.path && location.pathname.endsWith(doc.path)
- );
- if (found) {
- activeVersion = version;
- break;
- }
- }
-
- if (!activeVersion || !activeVersion.docs) return;
-
- const allDocs = Object.values(activeVersion.docs);
- const currentDoc = allDocs.find(
- (doc) => doc.path && location.pathname.endsWith(doc.path)
- );
- if (!currentDoc) return;
-
- // Find matching action link
- const matched = ACTION_LINKS.find((link) => currentDoc.id.endsWith(link.match));
- if (matched) {
- setAction(matched);
- } else {
- setAction(null);
- }
- }, [location.pathname, allDocsData]);
-
- if (!action) return null;
-
- return (
-
- );
-}
diff --git a/src/components/GitLinkAction/styles.module.css b/src/components/GitLinkAction/styles.module.css
deleted file mode 100644
index 9551868..0000000
--- a/src/components/GitLinkAction/styles.module.css
+++ /dev/null
@@ -1,41 +0,0 @@
-.container {
- margin-top: 1rem;
- padding-top: 1rem;
- border-top: 1px solid var(--ifm-color-emphasis-200);
-}
-
-.button {
- display: inline-flex;
- align-items: center;
- gap: 0.4rem;
- padding: 0.5rem 1rem;
- border: 1px solid var(--ifm-color-primary);
- border-radius: 6px;
- background: var(--ifm-color-primary-contrast-background);
- color: var(--ifm-color-primary);
- font-size: 0.88rem;
- font-weight: 500;
- text-decoration: none !important;
- transition: all 0.15s ease;
-}
-
-.button:hover {
- background: var(--ifm-color-primary);
- color: var(--ifm-color-primary-contrast-background);
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-}
-
-.icon {
- flex-shrink: 0;
-}
-
-.arrow {
- flex-shrink: 0;
- opacity: 0.6;
- transition: transform 0.15s ease;
-}
-
-.button:hover .arrow {
- opacity: 1;
- transform: translate(2px, -2px);
-}
diff --git a/src/components/ShareButton/index.jsx b/src/components/ShareButton/index.jsx
deleted file mode 100644
index ba321e2..0000000
--- a/src/components/ShareButton/index.jsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React, { useState, useCallback } from 'react';
-import styles from './styles.module.css';
-
-export default function ShareButton() {
- const [toast, setToast] = useState(null);
-
- const url = typeof window !== 'undefined' ? window.location.href : '';
- const pageTitle = typeof document !== 'undefined' ? document.title : '';
-
- const showToast = useCallback((msg) => {
- setToast(msg);
- setTimeout(() => setToast(null), 2500);
- }, []);
-
- const copyLink = useCallback(() => {
- const text = `${pageTitle} ${url}`;
- navigator.clipboard.writeText(text).then(
- () => showToast('链接已复制,可粘贴发送给好友'),
- () => showToast('复制失败,请手动复制地址栏链接')
- );
- }, [pageTitle, url, showToast]);
-
- if (!url) return null;
-
- return (
-
-
-
- {toast &&
{toast}
}
-
- );
-}
diff --git a/src/components/ShareButton/styles.module.css b/src/components/ShareButton/styles.module.css
deleted file mode 100644
index 6821b93..0000000
--- a/src/components/ShareButton/styles.module.css
+++ /dev/null
@@ -1,56 +0,0 @@
-.container {
- margin-top: 1rem;
- padding-top: 1rem;
- border-top: 1px solid var(--ifm-color-emphasis-200);
-}
-
-.btn {
- display: inline-flex;
- align-items: center;
- gap: 0.35rem;
- padding: 0.35rem 0.85rem;
- border: 1px solid var(--ifm-color-emphasis-300);
- border-radius: 6px;
- background: var(--ifm-card-background-color);
- color: var(--ifm-color-emphasis-700);
- font-size: 0.82rem;
- cursor: pointer;
- transition: all 0.15s ease;
- line-height: 1.4;
-}
-
-.btn:hover {
- border-color: var(--ifm-color-primary);
- color: var(--ifm-color-primary);
- background: var(--ifm-color-primary-contrast-background);
-}
-
-.btn svg {
- opacity: 0.65;
-}
-
-.btn:hover svg {
- opacity: 1;
-}
-
-/* Toast */
-.toast {
- position: fixed;
- bottom: 2rem;
- left: 50%;
- transform: translateX(-50%);
- z-index: 9999;
- background: var(--ifm-color-emphasis-800);
- color: #fff;
- padding: 0.6rem 1.2rem;
- border-radius: 8px;
- font-size: 0.85rem;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
- animation: fadeIn 0.2s ease;
- white-space: nowrap;
-}
-
-@keyframes fadeIn {
- from { opacity: 0; }
- to { opacity: 1; }
-}
diff --git a/src/theme/DocItem/Footer/index.tsx b/src/theme/DocItem/Footer/index.tsx
index 0999a38..9684771 100644
--- a/src/theme/DocItem/Footer/index.tsx
+++ b/src/theme/DocItem/Footer/index.tsx
@@ -3,7 +3,7 @@ import Footer from '@theme-original/DocItem/Footer';
import type FooterType from '@theme/DocItem/Footer';
import type {WrapperProps} from '@docusaurus/types';
import DocFeedback from '../../../components/DocFeedback';
-import GitLinkAction from '../../../components/GitLinkAction';
+import BookmarkButton from '../../../components/BookmarkButton';
type Props = WrapperProps;
@@ -11,7 +11,9 @@ export default function FooterWrapper(props: Props): ReactNode {
return (
<>
-
+
+
+
>
);
diff --git a/src/theme/DocSidebar/Desktop/index.tsx b/src/theme/DocSidebar/Desktop/index.tsx
index 6c411fb..69b3ffb 100644
--- a/src/theme/DocSidebar/Desktop/index.tsx
+++ b/src/theme/DocSidebar/Desktop/index.tsx
@@ -6,6 +6,7 @@ import CollapseButton from '@theme/DocSidebar/Desktop/CollapseButton';
import Content from '@theme/DocSidebar/Desktop/Content';
import type {Props} from '@theme/DocSidebar/Desktop';
import RecentDocs from '../../../components/RecentDocs';
+import BookmarkList from '../../../components/BookmarkList';
import styles from './styles.module.css';
@@ -27,6 +28,7 @@ function DocSidebarDesktop({path, sidebar, onCollapse, isHidden}: Props) {
{hideOnScroll && }
+
{hideable && }