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 ( +
+
收藏
+ +
+ ); +} 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 ( -
- - - - - - - {action.label} - - - - -
- ); -} 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 ( <>