feat: add bookmark/favorites feature with sidebar list
Remove unused GitLinkAction and ShareButton components. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
c35ceb7bb5
commit
d18c56c27d
|
|
@ -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 (
|
||||
<>
|
||||
<button
|
||||
className={`${styles.btn} ${bookmarked ? styles.active : ''}`}
|
||||
onClick={toggleBookmark}
|
||||
title={bookmarked ? '取消收藏' : '收藏本文'}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill={bookmarked ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
|
||||
</svg>
|
||||
{bookmarked ? '已收藏' : '收藏本文'}
|
||||
</button>
|
||||
{toast && <div className={styles.toast}>{toast}</div>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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); }
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.heading}>收藏</div>
|
||||
<ul className={styles.list}>
|
||||
{bookmarks.slice(0, 10).map((bm) => (
|
||||
<li key={bm.path} className={styles.item}>
|
||||
<Link to={bm.path} className={styles.link}>
|
||||
<svg className={styles.starIcon} width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
|
||||
</svg>
|
||||
<span className={styles.title}>{bm.title}</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className={styles.container}>
|
||||
<a
|
||||
href={action.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.button}
|
||||
>
|
||||
<svg className={styles.icon} width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15 3 21 3 21 9"/>
|
||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||
</svg>
|
||||
<span>{action.label}</span>
|
||||
<svg className={styles.arrow} width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M7 17l9.2-9.2M17 17V7H7"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className={styles.container}>
|
||||
<button className={styles.btn} onClick={copyLink} title="复制链接">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
||||
</svg>
|
||||
复制链接分享
|
||||
</button>
|
||||
|
||||
{toast && <div className={styles.toast}>{toast}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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<typeof FooterType>;
|
||||
|
||||
|
|
@ -11,7 +11,9 @@ export default function FooterWrapper(props: Props): ReactNode {
|
|||
return (
|
||||
<>
|
||||
<Footer {...props} />
|
||||
<GitLinkAction />
|
||||
<div style={{marginTop: '0.5rem'}}>
|
||||
<BookmarkButton />
|
||||
</div>
|
||||
<DocFeedback />
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 && <Logo tabIndex={-1} className={styles.sidebarLogo} />}
|
||||
<div className={styles.recentSection}>
|
||||
<RecentDocs />
|
||||
<BookmarkList />
|
||||
</div>
|
||||
<Content path={path} sidebar={sidebar} />
|
||||
{hideable && <CollapseButton onClick={onCollapse} />}
|
||||
|
|
|
|||
Loading…
Reference in New Issue