forked from Gitlink/gitlink_help_center
Compare commits
9 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
211f20eef4 | |
|
|
a074a9c25c | |
|
|
a094f464ec | |
|
|
3422b32669 | |
|
|
b74b92f66e | |
|
|
96231da6fe | |
|
|
8b39d43460 | |
|
|
75ce405ca7 | |
|
|
4f085a23e2 |
|
|
@ -7,10 +7,6 @@ trigger:
|
|||
webhook: gitlink@1.0.0
|
||||
event:
|
||||
- ref: push
|
||||
ruleset:
|
||||
- param-ref: branch
|
||||
operator: EQ
|
||||
value: '""'
|
||||
ruleset-operator: AND
|
||||
workflow:
|
||||
- ref: start
|
||||
|
|
@ -20,9 +16,9 @@ workflow:
|
|||
name: git clone
|
||||
task: git_clone@1.2.9
|
||||
input:
|
||||
username: ((yjh.gitlink_name))
|
||||
password: ((yjh.gitlink_passwd))
|
||||
remote_url: '"https://www.gitlink.org.cn/jkcl/gitlink_help_center.git"'
|
||||
username: ((ganshihao21.ganshihao21_user))
|
||||
password: ((ganshihao21.ganshihao21))
|
||||
remote_url: '"https://gitlink.org.cn/ganshihao21/reposync.git"'
|
||||
ref: '"refs/heads/master"'
|
||||
commit_id: '""'
|
||||
depth: 1
|
||||
|
|
@ -32,11 +28,11 @@ workflow:
|
|||
name: docker镜像构建
|
||||
task: docker_image_build@1.6.0
|
||||
input:
|
||||
docker_username: ((yjh.aliyun_name))
|
||||
docker_password: ((yjh.aliyun_passwd))
|
||||
image_name: '"crpi-rlxgl92hhxkh8r6q.cn-hangzhou.personal.cr.aliyuncs.com/yuanjiahong/group_07"'
|
||||
docker_username: ((nudt_devops.nudt_devops))
|
||||
docker_password: ((nudt_devops.nudt_devops))
|
||||
image_name: '"registry.cn-guangzhou.aliyuncs.com/nudt_devops/reposync_group5"'
|
||||
image_tag: '"latest"'
|
||||
registry_address: '"crpi-rlxgl92hhxkh8r6q.cn-hangzhou.personal.cr.aliyuncs.com"'
|
||||
registry_address: '"registry.cn-guangzhou.aliyuncs.com"'
|
||||
docker_file: '"Dockerfile"'
|
||||
docker_build_path: '"."'
|
||||
workspace: git_clone_0.git_path
|
||||
|
|
@ -44,24 +40,23 @@ workflow:
|
|||
build_args: '""'
|
||||
needs:
|
||||
- git_clone_0
|
||||
- ref: ssh_cmd_0
|
||||
name: ssh执行命令
|
||||
task: ssh_cmd@1.1.1
|
||||
input:
|
||||
ssh_pass: ((ssh_key.ssh_key))
|
||||
ssh_ip: '"47.96.164.145"'
|
||||
ssh_port: '"22"'
|
||||
ssh_user: '"root"'
|
||||
ssh_cmd: '"docker stop abc_group5 && docker rm abc_group5 && docker pull
|
||||
registry.cn-guangzhou.aliyuncs.com/nudt_devops/reposync_group5:latest &&
|
||||
docker run -d -p 3000:3000 --name abc_group5
|
||||
registry.cn-guangzhou.aliyuncs.com/nudt_devops/reposync_group5:latest"'
|
||||
needs:
|
||||
- docker_image_build_0
|
||||
- ref: end
|
||||
name: 结束
|
||||
task: end
|
||||
needs:
|
||||
- ssh_cmd_0
|
||||
- ref: ssh_cmd_0
|
||||
name: ssh执行命令
|
||||
task: ssh_cmd@1.1.1
|
||||
input:
|
||||
ssh_pass: ((yjh.ssh_passwd))
|
||||
ssh_ip: '"114.55.237.214"'
|
||||
ssh_port: '"22"'
|
||||
ssh_user: '"root"'
|
||||
ssh_cmd: '"docker stop group_07 || true && docker rm group_07 || true && docker
|
||||
pull
|
||||
crpi-rlxgl92hhxkh8r6q.cn-hangzhou.personal.cr.aliyuncs.com/yuanjiahong/group_07:latest;
|
||||
docker run -d -p 3000:3000 --name group_07
|
||||
crpi-rlxgl92hhxkh8r6q.cn-hangzhou.personal.cr.aliyuncs.com/yuanjiahong/group_07:latest"'
|
||||
needs:
|
||||
- docker_image_build_0
|
||||
|
||||
|
|
|
|||
76
FEATURES.md
76
FEATURES.md
|
|
@ -1,76 +0,0 @@
|
|||
# GitLink 帮助中心新增功能说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
为 GitLink 帮助中心添加了三个实用功能,提升用户阅读体验:
|
||||
|
||||
### 1. 📖 保留上次阅读位置
|
||||
- **自动保存**:自动记录每个页面的滚动位置
|
||||
- **智能恢复**:重新访问页面时自动滚动到上次阅读位置
|
||||
- **时效管理**:只保留最近7天的阅读记录,自动清理过期数据
|
||||
- **提示反馈**:恢复位置时显示友好的提示信息
|
||||
|
||||
### 2. 📄 导出PDF功能
|
||||
- **一键导出**:将当前文档页面导出为PDF文件
|
||||
- **高质量渲染**:使用2倍缩放确保导出内容清晰
|
||||
- **智能分页**:长文档自动分页处理,添加页码
|
||||
- **进度提示**:导出过程中显示实时进度信息
|
||||
- **文件命名**:使用文档标题作为PDF文件名
|
||||
|
||||
### 3. 🔤 字体大小调节
|
||||
- **多级选择**:提供6种字体大小选项(极小、小号、标准、大号、特大、巨大)
|
||||
- **实时预览**:调节后立即生效,无需刷新页面
|
||||
- **智能适配**:同时调节标题、正文、代码等不同元素的字体大小
|
||||
- **设置保存**:字体大小偏好自动保存到本地存储
|
||||
- **可隐藏界面**:右侧浮动按钮设计,可随时显示/隐藏
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 阅读位置保存
|
||||
- **自动功能**:无需任何操作,系统自动工作
|
||||
- **手动书签**:可通过文档工具栏手动保存/跳转书签位置
|
||||
|
||||
### PDF导出
|
||||
1. 在任意文档页面向下滚动
|
||||
2. 点击右侧悬浮的"🛠️"工具按钮
|
||||
3. 点击"📄 导出PDF"按钮
|
||||
4. 等待导出完成并自动下载
|
||||
|
||||
### 字体大小调节
|
||||
1. 在文档页面右侧找到"Aa"悬浮按钮
|
||||
2. 点击按钮显示字体调节面板
|
||||
3. 选择合适的字体大小
|
||||
4. 可点击"重置"恢复默认大小
|
||||
5. 点击"隐藏"或"✕"收起面板
|
||||
|
||||
## 技术特点
|
||||
|
||||
- **响应式设计**:适配不同设备和屏幕尺寸
|
||||
- **性能优化**:使用节流和防抖技术避免性能问题
|
||||
- **用户体验**:提供清晰的视觉反馈和操作提示
|
||||
- **数据持久化**:使用localStorage保存用户偏好设置
|
||||
- **兼容性良好**:与现有Docusaurus主题完美集成
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **浏览器兼容性**:建议使用现代浏览器以获得最佳体验
|
||||
2. **PDF导出**:较长文档导出时间可能较长,请耐心等待
|
||||
3. **存储空间**:阅读位置数据存储在浏览器本地,清除浏览器数据会丢失设置
|
||||
4. **字体调节**:仅影响文档内容区域,不影响导航栏等界面元素
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ ├── DocEnhancer.js # 主集成组件
|
||||
│ ├── FontSizeController.js # 字体大小控制
|
||||
│ ├── PDFExport.js # PDF导出功能
|
||||
│ └── ReadingPosition.js # 阅读位置保存
|
||||
├── theme/
|
||||
│ └── Root.js # 根组件包装器
|
||||
└── css/
|
||||
└── custom.css # 样式定义
|
||||
```
|
||||
|
||||
这些功能已完全集成到现有系统中,无需修改任何现有代码即可使用。
|
||||
|
|
@ -84,7 +84,8 @@
|
|||

|
||||
|
||||
```
|
||||
|
||||
初始化项目
|
||||
node v20.x npm install --force
|
||||
## 前端build成中文 (i18n中可编辑对应中文内容)
|
||||
```
|
||||
npm run build -- --locale zh-CN
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
const lightCodeTheme = require('prism-react-renderer/themes/github');
|
||||
const darkCodeTheme = require('prism-react-renderer/themes/dracula');
|
||||
//Docusaurus v3写法
|
||||
// const { themes } = require('prism-react-renderer');
|
||||
// import { themes } from 'prism-react-renderer';
|
||||
import("@easyops-cn/docusaurus-search-local").PluginOptions;
|
||||
|
||||
/** @type {import('@docusaurus/types').DocusaurusConfig} */
|
||||
|
|
@ -8,15 +11,14 @@ module.exports = {
|
|||
tagline:'gitlink帮助中心',
|
||||
url: 'https://help.gitlink.org.cn/',
|
||||
baseUrl: '/',
|
||||
onBrokenLinks: 'ignore',
|
||||
onBrokenMarkdownLinks: 'ignore',
|
||||
onBrokenLinks: 'warn',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
favicon: 'img/icon.ico',
|
||||
organizationName: 'luffyZh', // Usually your GitHub org/user name.
|
||||
projectName: 'docusaurus-luffyzh-website', // Usually your repo name.
|
||||
scripts: [],
|
||||
// stylesheets: ['styles/dark-mode.css'],
|
||||
themeConfig: {
|
||||
|
||||
docs:{
|
||||
sidebar:{
|
||||
hideable:true,
|
||||
|
|
@ -29,7 +31,7 @@ module.exports = {
|
|||
{property:"og:image:alt",content:"GitLink(确实开源)是中国计算机学会CCF官方指定的开源创新服务平台 - Gitlink/forgeplus:GitLink(确实开源)是中国计算机学会CCF官方指定的开源创新服务平台 for gitlink Gitlink/forgeplus for git"}
|
||||
],
|
||||
prism: {
|
||||
theme: lightCodeTheme,
|
||||
theme:lightCodeTheme,
|
||||
darkTheme: darkCodeTheme,
|
||||
},
|
||||
zoom: {
|
||||
|
|
@ -71,6 +73,26 @@ module.exports = {
|
|||
dropdownActiveClassDisabled: true
|
||||
},
|
||||
// {
|
||||
// type: 'localeDropdown',
|
||||
// position: 'right',
|
||||
// },
|
||||
// {
|
||||
// position: 'right',
|
||||
// component:'./src/components/LanguageSwitcher',
|
||||
// label:"language",
|
||||
// to:"#"
|
||||
// items: [
|
||||
// {
|
||||
// label: 'English',
|
||||
// onClick: () => handleLocaleChange('en')
|
||||
// },
|
||||
// {
|
||||
// label: '简体中文',
|
||||
// onClick: () => handleLocaleChange('zh-Hans')
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// {
|
||||
// href: 'https://github.com/boxyhq',
|
||||
// position: 'right',
|
||||
// className: 'header-github-link',
|
||||
|
|
@ -182,6 +204,16 @@ module.exports = {
|
|||
],
|
||||
i18n: {
|
||||
defaultLocale: 'zh-cn',
|
||||
locales: ['zh-cn'],
|
||||
locales: ['zh-cn','en'],
|
||||
localeConfigs: {
|
||||
en: {
|
||||
label: 'English',
|
||||
htmlLang: 'en',
|
||||
},
|
||||
'zh-cn': {
|
||||
label: '简体中文',
|
||||
htmlLang: 'zh-CN',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
为了构建更好的平台生态环境,我们推出了基于OAuth2的API版本。
|
||||
API 接口使用方式以及Url都参照GitHub,为了各位开发者更好的兼容已经存在的第三方应用。
|
||||
|
||||
# API 使用条款
|
||||
|
||||
未经用户允许,不准爬取或存储用户的资源。
|
||||
禁止滥用 API,请求频率过快将导致请求终止。
|
||||
OAuth2 认证基本流程
|
||||

|
||||
|
||||
# OAuth2 获取 AccessToken 认证步骤
|
||||
|
||||
### 1. 授权码模式
|
||||
* 应用通过 浏览器 或 Webview 将用户引导到平台三方认证页面上( GET请求 )
|
||||
|
||||
`https://gitlink.org.cn/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code`
|
||||
|
||||
* 用户对应用进行授权
|
||||
注意: 如果之前已经授权过的需要跳过授权页面,需要在上面第一步的 URL 加上 scope 参数,且 scope 的值需要和用户上次授权的勾选的一致。如用户在上次授权了user_info。则步骤A 中 GET 请求应为:
|
||||
|
||||
`https://gitlink.org.cn/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&scope=user_info`
|
||||
|
||||
* 平台认证服务器通过回调地址`{redirect_uri}`将 用户授权码 传递给 应用服务器 或者直接在 Webview 中跳转到携带 用户授权码的回调地址上,Webview 直接获取code即可(`{redirect_uri}?code=abc&state=xyz`)
|
||||
* 应用服务器 或 Webview 使用 access_token API 向平台认证服务器发送post请求传入 用户授权码 以及 回调地址( POST请求 )
|
||||
注:请求过程建议将 client_secret 放在 Body 中传值,以保证数据安全。
|
||||
|
||||
`https://gitlink.org.cn/oauth/token?grant_type=authorization_code&code={code}&client_id={client_id}&redirect_uri={redirect_uri}&client_secret={client_secret}`
|
||||
|
||||
* 平台认证服务器返回 access_token
|
||||
|
||||
应用通过 access_token 访问 Open API 使用用户数据。
|
||||
当 access_token 过期后(有效期为一天),你可以通过以下 refresh_token 方式重新获取 access_token( POST请求 )
|
||||
|
||||
`https://gitlink.org.cn/oauth/token?grant_type=refresh_token&refresh_token={refresh_token}`
|
||||
|
||||
> 注意:如果获取 access_token 返回 403,可能是没有设置User-Agent的原因。
|
||||
详见:获取Token时服务端响应状态403是什么情况
|
||||
|
||||
### 2. 密码模式
|
||||
* 用户向客户端提供邮箱地址和密码。客户端将邮箱地址和密码发给平台认证服务器,并向平台认证服务器请求令牌。( POST请求。Content-Type: application/x-www-form-urlencoded )
|
||||
|
||||
`curl -X POST --data-urlencode "grant_type=password" --data-urlencode "username={email}" --data-urlencode "password={password}" --data-urlencode "client_id={client_id}" --data-urlencode "client_secret={client_secret}" --data-urlencode "scope=user_info" https://gitlink.org.cn/oauth/token`
|
||||
|
||||
scope表示权限范围,有以下选项,请求时使用空格隔开
|
||||
`user_info` `projects` `pull_requests` `issues` `notes` `keys` `hook groups` `gists` `enterprises`
|
||||
* 认证服务器返回 access_token
|
||||
应用通过 access_token 访问 Open API 使用用户数据。
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 135 KiB |
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "docusaurus",
|
||||
"version": "0.0.0",
|
||||
"version": "3.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
|
|
@ -15,19 +15,20 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@cmfcmf/docusaurus-search-local": "1.1.0",
|
||||
"@docusaurus/core": "2.4.1",
|
||||
"@docusaurus/plugin-client-redirects": "2.4.1",
|
||||
"@docusaurus/preset-classic": "2.4.1",
|
||||
"@docusaurus/core": "3.9.2",
|
||||
"@docusaurus/plugin-client-redirects": "3.9.2",
|
||||
"@docusaurus/preset-classic": "3.9.2",
|
||||
"@easyops-cn/docusaurus-search-local": "^0.35.0",
|
||||
"@mdx-js/react": "1.6.22",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"@node-rs/jieba": "^1.7.0",
|
||||
"clsx": "1.2.1",
|
||||
"docusaurus-gtm-plugin": "0.0.2",
|
||||
"docusaurus-plugin-image-zoom": "1.0.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^2.5.1",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"or": "^0.2.0",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"prismjs": "^1.30.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-modal": "3.16.1"
|
||||
},
|
||||
"browserslist": {
|
||||
|
|
@ -43,6 +44,6 @@
|
|||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18.1 <=18.x"
|
||||
"node": ">=18.x"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,199 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import ReadingPosition from './ReadingPosition';
|
||||
import FontSizeController from './FontSizeController';
|
||||
import PDFExport from './PDFExport';
|
||||
import { useLocation } from '@docusaurus/router';
|
||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
||||
|
||||
const DocEnhancer = () => {
|
||||
const [documentTitle, setDocumentTitle] = useState('');
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
// 确保在客户端环境
|
||||
if (ExecutionEnvironment.canUseDOM) {
|
||||
setIsClient(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isClient) return;
|
||||
|
||||
// 获取文档标题,用于PDF导出
|
||||
const getDocumentTitle = () => {
|
||||
// 优先使用页面标题
|
||||
const pageTitle = document.title;
|
||||
if (pageTitle && pageTitle !== 'gitlink') {
|
||||
return pageTitle.replace(' | gitlink', '');
|
||||
}
|
||||
|
||||
// 备选:使用h1标题
|
||||
const h1Element = document.querySelector('article h1, .markdown h1');
|
||||
if (h1Element) {
|
||||
return h1Element.textContent;
|
||||
}
|
||||
|
||||
// 最后使用路径
|
||||
return location.pathname.split('/').pop() || '文档';
|
||||
};
|
||||
|
||||
// 延迟获取标题,确保页面完全加载
|
||||
const timer = setTimeout(() => {
|
||||
const title = getDocumentTitle();
|
||||
setDocumentTitle(title);
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [location.pathname, isClient]);
|
||||
|
||||
// 检查是否在文档页面
|
||||
const isDocPage = () => {
|
||||
if (!isClient || !ExecutionEnvironment.canUseDOM) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 方法1:检查页面结构特征
|
||||
const hasDocStructure = () => {
|
||||
// 检查是否有文档侧边栏
|
||||
const hasSidebar = document.querySelector('.theme-doc-sidebar-container, .sidebar, [class*="sidebar"]');
|
||||
// 检查是否有文档内容
|
||||
const hasDocContent = document.querySelector('article, .markdown, [class*="doc"], .theme-doc-markdown');
|
||||
// 检查是否有面包屑导航
|
||||
const hasBreadcrumb = document.querySelector('.theme-doc-breadcrumbs, [class*="breadcrumb"]');
|
||||
|
||||
return !!(hasSidebar || hasDocContent || hasBreadcrumb);
|
||||
};
|
||||
|
||||
// 方法2:路径检查(作为辅助)
|
||||
const isHomePage = location.pathname === '/' ||
|
||||
location.pathname === '/zh-cn/' ||
|
||||
location.pathname === '/zh-cn' ||
|
||||
location.pathname.endsWith('/search');
|
||||
|
||||
// 如果明确是首页,则不显示工具
|
||||
if (isHomePage && !hasDocStructure()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果有文档结构,就显示工具,无论路径如何
|
||||
return hasDocStructure();
|
||||
};
|
||||
|
||||
// 如果不是客户端环境,返回null
|
||||
if (!isClient) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const shouldShowTools = isDocPage();
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 阅读位置保存 - 在所有页面都启用 */}
|
||||
<ReadingPosition />
|
||||
|
||||
{/* 在文档页面显示字体控制器和PDF导出 */}
|
||||
{shouldShowTools && (
|
||||
<>
|
||||
<FontSizeController />
|
||||
<DocToolbar documentTitle={documentTitle} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// 简化的文档工具栏组件 - 只保留PDF导出
|
||||
const DocToolbar = ({ documentTitle }) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const toolbarStyle = {
|
||||
position: 'fixed',
|
||||
top: '120px',
|
||||
right: isVisible ? '20px' : '-200px',
|
||||
zIndex: 1000,
|
||||
transition: 'right 0.3s ease',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
||||
padding: '12px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px',
|
||||
border: '1px solid #e1e5e9',
|
||||
};
|
||||
|
||||
const toggleButtonStyle = {
|
||||
position: 'fixed',
|
||||
top: '120px',
|
||||
right: '20px',
|
||||
zIndex: 1001,
|
||||
backgroundColor: '#25c2a0',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '50%',
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
|
||||
transition: 'all 0.3s ease',
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 切换按钮 */}
|
||||
<button
|
||||
style={toggleButtonStyle}
|
||||
onClick={() => setIsVisible(!isVisible)}
|
||||
title="PDF导出工具"
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.backgroundColor = '#21af90';
|
||||
e.target.style.transform = 'scale(1.1)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.backgroundColor = '#25c2a0';
|
||||
e.target.style.transform = 'scale(1)';
|
||||
}}
|
||||
>
|
||||
📄
|
||||
</button>
|
||||
|
||||
{/* 工具栏 */}
|
||||
<div style={toolbarStyle}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '4px',
|
||||
color: '#333',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
PDF导出
|
||||
</div>
|
||||
|
||||
<PDFExport documentTitle={documentTitle} />
|
||||
|
||||
<button
|
||||
onClick={() => setIsVisible(false)}
|
||||
style={{
|
||||
backgroundColor: '#95a5a6',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
padding: '6px 12px',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px',
|
||||
marginTop: '4px',
|
||||
}}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocEnhancer;
|
||||
|
|
@ -1,389 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
||||
|
||||
const FontSizeController = () => {
|
||||
const [fontSize, setFontSize] = useState('medium');
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
|
||||
// 字体大小配置
|
||||
const fontSizes = {
|
||||
extraSmall: {
|
||||
name: '极小',
|
||||
icon: '🔍',
|
||||
scale: 0.75,
|
||||
description: '超密集阅读'
|
||||
},
|
||||
small: {
|
||||
name: '小号',
|
||||
icon: '🔤',
|
||||
scale: 0.875,
|
||||
description: '密集阅读'
|
||||
},
|
||||
medium: {
|
||||
name: '标准',
|
||||
icon: '📝',
|
||||
scale: 1,
|
||||
description: '推荐大小'
|
||||
},
|
||||
large: {
|
||||
name: '大号',
|
||||
icon: '📖',
|
||||
scale: 1.125,
|
||||
description: '舒适阅读'
|
||||
},
|
||||
extraLarge: {
|
||||
name: '特大',
|
||||
icon: '📚',
|
||||
scale: 1.25,
|
||||
description: '长时间阅读'
|
||||
},
|
||||
huge: {
|
||||
name: '巨大',
|
||||
icon: '🔍',
|
||||
scale: 1.375,
|
||||
description: '视力辅助'
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 确保在客户端环境
|
||||
if (!ExecutionEnvironment.canUseDOM) return;
|
||||
|
||||
setMounted(true);
|
||||
// 从localStorage读取保存的字体大小设置
|
||||
const savedFontSize = localStorage.getItem('doc-font-size') || 'medium';
|
||||
const savedVisibility = localStorage.getItem('font-controller-visible') !== 'false'; // 默认显示
|
||||
setFontSize(savedFontSize);
|
||||
setIsVisible(savedVisibility);
|
||||
applyFontSize(savedFontSize);
|
||||
}, []);
|
||||
|
||||
// 应用字体大小到文档
|
||||
const applyFontSize = (size) => {
|
||||
if (!ExecutionEnvironment.canUseDOM) return;
|
||||
|
||||
const scale = fontSizes[size].scale;
|
||||
|
||||
// 设置CSS变量
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--doc-font-scale', scale);
|
||||
|
||||
// 直接应用样式作为备用方案
|
||||
const contentSelectors = [
|
||||
'article',
|
||||
'.markdown',
|
||||
'.theme-doc-markdown',
|
||||
'[class*="docItemContainer"]',
|
||||
'[class*="docItemCol"]'
|
||||
];
|
||||
|
||||
contentSelectors.forEach(selector => {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
elements.forEach(element => {
|
||||
element.style.fontSize = `${scale}rem`;
|
||||
});
|
||||
});
|
||||
|
||||
// 调整标题
|
||||
const headingRules = {
|
||||
'h1': scale * 2.5,
|
||||
'h2': scale * 2,
|
||||
'h3': scale * 1.75,
|
||||
'h4': scale * 1.5,
|
||||
'h5': scale * 1.25,
|
||||
'h6': scale * 1.125
|
||||
};
|
||||
|
||||
Object.entries(headingRules).forEach(([tag, size]) => {
|
||||
const elements = document.querySelectorAll(`article ${tag}, .markdown ${tag}, .theme-doc-markdown ${tag}`);
|
||||
elements.forEach(element => {
|
||||
element.style.fontSize = `${size}rem`;
|
||||
});
|
||||
});
|
||||
|
||||
// 调整段落和其他文本元素
|
||||
const textElements = document.querySelectorAll(`
|
||||
article p, article li, article td, article th, article blockquote,
|
||||
.markdown p, .markdown li, .markdown td, .markdown th, .markdown blockquote,
|
||||
.theme-doc-markdown p, .theme-doc-markdown li, .theme-doc-markdown td, .theme-doc-markdown th, .theme-doc-markdown blockquote
|
||||
`);
|
||||
textElements.forEach(element => {
|
||||
element.style.fontSize = `${scale}rem`;
|
||||
});
|
||||
|
||||
// 调整代码字体
|
||||
const codeElements = document.querySelectorAll(`
|
||||
article code, .markdown code, .theme-doc-markdown code
|
||||
`);
|
||||
codeElements.forEach(element => {
|
||||
element.style.fontSize = `${scale * 0.875}rem`;
|
||||
});
|
||||
};
|
||||
|
||||
// 切换字体大小
|
||||
const changeFontSize = (size) => {
|
||||
if (!ExecutionEnvironment.canUseDOM) return;
|
||||
|
||||
setFontSize(size);
|
||||
localStorage.setItem('doc-font-size', size);
|
||||
applyFontSize(size);
|
||||
showNotification(`✅ 字体大小已切换为:${fontSizes[size].name}`, 'success');
|
||||
};
|
||||
|
||||
// 切换显示/隐藏
|
||||
const toggleVisibility = () => {
|
||||
if (!ExecutionEnvironment.canUseDOM) return;
|
||||
|
||||
const newVisibility = !isVisible;
|
||||
setIsVisible(newVisibility);
|
||||
localStorage.setItem('font-controller-visible', newVisibility.toString());
|
||||
showNotification(newVisibility ? '📝 字体调节器已显示' : '👁️ 字体调节器已隐藏', 'info');
|
||||
};
|
||||
|
||||
// 显示通知
|
||||
const showNotification = (message, type = 'info') => {
|
||||
if (!ExecutionEnvironment.canUseDOM) return;
|
||||
|
||||
try {
|
||||
const notification = document.createElement('div');
|
||||
notification.textContent = message;
|
||||
|
||||
const bgColors = {
|
||||
success: '#25c2a0',
|
||||
info: '#3498db',
|
||||
warning: '#f39c12'
|
||||
};
|
||||
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: ${bgColors[type] || bgColors.info};
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
animation: slideIn 0.3s ease;
|
||||
max-width: 300px;
|
||||
word-wrap: break-word;
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.remove();
|
||||
}
|
||||
}, 3000);
|
||||
} catch (error) {
|
||||
console.error('显示通知失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 如果组件还没挂载或不是客户端环境,不渲染任何内容
|
||||
if (!mounted || !ExecutionEnvironment.canUseDOM) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const containerStyle = {
|
||||
position: 'fixed',
|
||||
top: '180px', // 移到DocToolbar下方
|
||||
right: isVisible ? '20px' : '-320px',
|
||||
transform: 'none',
|
||||
zIndex: 999, // 比DocToolbar低一点
|
||||
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||
transition: 'right 0.3s ease'
|
||||
};
|
||||
|
||||
const toggleButtonStyle = {
|
||||
position: 'fixed',
|
||||
top: '180px',
|
||||
right: '20px',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
backgroundColor: '#3b82f6',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '50%',
|
||||
cursor: 'pointer',
|
||||
fontSize: '16px',
|
||||
boxShadow: '0 4px 12px rgba(59, 130, 246, 0.3)',
|
||||
transition: 'all 0.3s ease',
|
||||
fontFamily: 'system-ui, -apple-system, sans-serif'
|
||||
};
|
||||
|
||||
const buttonStyle = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
padding: '10px',
|
||||
backgroundColor: 'white',
|
||||
border: '2px solid #e5e7eb',
|
||||
borderRadius: '10px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '11px',
|
||||
fontWeight: '600',
|
||||
color: '#374151',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
||||
transition: 'all 0.2s ease',
|
||||
minWidth: '50px',
|
||||
textAlign: 'center'
|
||||
};
|
||||
|
||||
const activeButtonStyle = {
|
||||
...buttonStyle,
|
||||
backgroundColor: '#3b82f6',
|
||||
borderColor: '#3b82f6',
|
||||
color: 'white',
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 6px 16px rgba(59, 130, 246, 0.3)'
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 切换显示/隐藏按钮 */}
|
||||
<button
|
||||
onClick={toggleVisibility}
|
||||
style={toggleButtonStyle}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.backgroundColor = '#2563eb';
|
||||
e.target.style.transform = 'scale(1.1)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.backgroundColor = '#3b82f6';
|
||||
e.target.style.transform = 'scale(1)';
|
||||
}}
|
||||
title={isVisible ? '隐藏字体调节器' : '显示字体调节器'}
|
||||
>
|
||||
{isVisible ? '✕' : 'Aa'}
|
||||
</button>
|
||||
|
||||
{/* 字体大小控制面板 */}
|
||||
<div style={containerStyle}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '6px',
|
||||
backgroundColor: 'white',
|
||||
padding: '12px',
|
||||
borderRadius: '16px',
|
||||
boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
|
||||
border: '1px solid #e5e7eb',
|
||||
minWidth: '280px'
|
||||
}}>
|
||||
{/* 标题 */}
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
fontWeight: '700',
|
||||
color: '#6b7280',
|
||||
textAlign: 'center',
|
||||
marginBottom: '4px',
|
||||
letterSpacing: '0.5px'
|
||||
}}>
|
||||
字体大小调节
|
||||
</div>
|
||||
|
||||
{/* 当前字体大小显示 */}
|
||||
<div style={{
|
||||
fontSize: '10px',
|
||||
fontWeight: '600',
|
||||
color: '#3b82f6',
|
||||
textAlign: 'center',
|
||||
marginBottom: '8px',
|
||||
padding: '4px 8px',
|
||||
backgroundColor: '#eff6ff',
|
||||
borderRadius: '6px'
|
||||
}}>
|
||||
当前: {fontSizes[fontSize].name} ({Math.round(fontSizes[fontSize].scale * 16)}px)
|
||||
</div>
|
||||
|
||||
{/* 字体大小选项 */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr 1fr',
|
||||
gap: '6px'
|
||||
}}>
|
||||
{Object.entries(fontSizes).map(([size, config]) => (
|
||||
<button
|
||||
key={size}
|
||||
onClick={() => changeFontSize(size)}
|
||||
style={fontSize === size ? activeButtonStyle : buttonStyle}
|
||||
onMouseEnter={(e) => {
|
||||
if (fontSize !== size) {
|
||||
e.target.style.backgroundColor = '#f3f4f6';
|
||||
e.target.style.borderColor = '#d1d5db';
|
||||
e.target.style.transform = 'translateY(-1px)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (fontSize !== size) {
|
||||
e.target.style.backgroundColor = 'white';
|
||||
e.target.style.borderColor = '#e5e7eb';
|
||||
e.target.style.transform = 'translateY(0)';
|
||||
}
|
||||
}}
|
||||
title={`${config.name} (${Math.round(config.scale * 16)}px) - ${config.description}`}
|
||||
>
|
||||
<span style={{ fontSize: '14px' }}>{config.icon}</span>
|
||||
<span style={{ fontSize: '9px', fontWeight: '600' }}>
|
||||
{config.name}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 快捷操作按钮 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '6px',
|
||||
marginTop: '8px'
|
||||
}}>
|
||||
<button
|
||||
onClick={() => changeFontSize('medium')}
|
||||
style={{
|
||||
...buttonStyle,
|
||||
backgroundColor: fontSize === 'medium' ? '#6b7280' : '#f9fafb',
|
||||
borderColor: fontSize === 'medium' ? '#6b7280' : '#e5e7eb',
|
||||
color: fontSize === 'medium' ? 'white' : '#6b7280',
|
||||
fontSize: '9px',
|
||||
padding: '6px 10px',
|
||||
minWidth: 'auto',
|
||||
flex: 1
|
||||
}}
|
||||
title="重置为标准大小"
|
||||
>
|
||||
重置
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={toggleVisibility}
|
||||
style={{
|
||||
...buttonStyle,
|
||||
backgroundColor: '#ef4444',
|
||||
borderColor: '#ef4444',
|
||||
color: 'white',
|
||||
fontSize: '9px',
|
||||
padding: '6px 10px',
|
||||
minWidth: 'auto',
|
||||
flex: 1
|
||||
}}
|
||||
title="隐藏字体调节器"
|
||||
>
|
||||
隐藏
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FontSizeController;
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
// src/components/LanguageSwitcher.js
|
||||
import React from 'react';
|
||||
import { useDocusaurusContext } from '@docusaurus/useDocusaurusContext';
|
||||
|
||||
export default function LanguageSwitcher() {
|
||||
const { i18n } = useDocusaurusContext();
|
||||
|
||||
const locales = [
|
||||
{ code: 'en', label: 'English' },
|
||||
{ code: 'zh-Hans', label: '简体中文' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ marginRight: '1rem' }}>
|
||||
<select
|
||||
value={i18n.currentLocale}
|
||||
onChange={(e) => {
|
||||
localStorage.setItem('preferredLocale', e.target.value);
|
||||
window.location.href = `/${e.target.value}/`;
|
||||
}}
|
||||
style={{
|
||||
padding: '0.5rem 1rem',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'white',
|
||||
fontSize: '0.9rem',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{locales.map((locale) => (
|
||||
<option key={locale.code} value={locale.code}>
|
||||
{locale.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,287 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import html2canvas from 'html2canvas';
|
||||
import jsPDF from 'jspdf';
|
||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
||||
|
||||
const PDFExport = ({ documentTitle }) => {
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
|
||||
const exportToPDF = async () => {
|
||||
if (!ExecutionEnvironment.canUseDOM) return;
|
||||
|
||||
setIsExporting(true);
|
||||
|
||||
try {
|
||||
// 显示进度提示
|
||||
showProgress('开始导出PDF...');
|
||||
|
||||
// 获取主要内容区域
|
||||
const element = document.querySelector('article') ||
|
||||
document.querySelector('.markdown') ||
|
||||
document.querySelector('.theme-doc-markdown') ||
|
||||
document.querySelector('main');
|
||||
|
||||
if (!element) {
|
||||
throw new Error('未找到可导出的内容');
|
||||
}
|
||||
|
||||
// 临时调整页面样式以优化PDF输出
|
||||
const originalStyles = prepareForPDF();
|
||||
|
||||
showProgress('正在生成图片...');
|
||||
|
||||
// 配置 html2canvas 选项
|
||||
const canvas = await html2canvas(element, {
|
||||
scale: 2, // 提高清晰度
|
||||
useCORS: true,
|
||||
allowTaint: false,
|
||||
backgroundColor: '#ffffff',
|
||||
width: element.scrollWidth,
|
||||
height: element.scrollHeight,
|
||||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
windowWidth: 1200, // 固定宽度以确保一致性
|
||||
logging: false, // 关闭日志
|
||||
onclone: (clonedDoc) => {
|
||||
// 在克隆文档中移除一些不需要的元素
|
||||
const elementsToRemove = clonedDoc.querySelectorAll(
|
||||
'.navbar, .footer, [style*="position: fixed"], [style*="position: sticky"]'
|
||||
);
|
||||
elementsToRemove.forEach(el => el.remove());
|
||||
}
|
||||
});
|
||||
|
||||
showProgress('正在生成PDF...');
|
||||
|
||||
// 创建 PDF
|
||||
const imgData = canvas.toDataURL('image/png', 0.8);
|
||||
const pdf = new jsPDF({
|
||||
orientation: 'portrait',
|
||||
unit: 'mm',
|
||||
format: 'a4',
|
||||
compress: true
|
||||
});
|
||||
|
||||
// 计算图片在PDF中的尺寸
|
||||
const pdfWidth = pdf.internal.pageSize.getWidth();
|
||||
const pdfHeight = pdf.internal.pageSize.getHeight();
|
||||
const margin = 10; // 边距
|
||||
const availableWidth = pdfWidth - 2 * margin;
|
||||
const availableHeight = pdfHeight - 2 * margin;
|
||||
|
||||
const imgWidth = canvas.width;
|
||||
const imgHeight = canvas.height;
|
||||
const ratio = Math.min(availableWidth / imgWidth, availableHeight / imgHeight);
|
||||
|
||||
const finalWidth = imgWidth * ratio;
|
||||
const finalHeight = imgHeight * ratio;
|
||||
|
||||
// 如果内容高度超过一页,需要分页
|
||||
if (finalHeight > availableHeight) {
|
||||
const totalPages = Math.ceil(finalHeight / availableHeight);
|
||||
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
if (i > 0) pdf.addPage();
|
||||
|
||||
const yOffset = -i * availableHeight / ratio;
|
||||
const imgX = margin + (availableWidth - finalWidth) / 2;
|
||||
const imgY = margin + yOffset * ratio;
|
||||
|
||||
pdf.addImage(
|
||||
imgData,
|
||||
'PNG',
|
||||
imgX,
|
||||
imgY,
|
||||
finalWidth,
|
||||
finalHeight
|
||||
);
|
||||
|
||||
// 添加页码
|
||||
pdf.setFontSize(10);
|
||||
pdf.setTextColor(128, 128, 128);
|
||||
pdf.text(`${i + 1} / ${totalPages}`, pdfWidth - 20, pdfHeight - 10);
|
||||
}
|
||||
} else {
|
||||
const imgX = margin + (availableWidth - finalWidth) / 2;
|
||||
const imgY = margin + (availableHeight - finalHeight) / 2;
|
||||
|
||||
pdf.addImage(
|
||||
imgData,
|
||||
'PNG',
|
||||
imgX,
|
||||
imgY,
|
||||
finalWidth,
|
||||
finalHeight
|
||||
);
|
||||
}
|
||||
|
||||
// 添加元数据
|
||||
pdf.setProperties({
|
||||
title: documentTitle || '文档',
|
||||
subject: '从 GitLink 帮助中心导出',
|
||||
author: 'GitLink',
|
||||
creator: 'GitLink Help Center'
|
||||
});
|
||||
|
||||
// 恢复原始样式
|
||||
restoreStyles(originalStyles);
|
||||
|
||||
showProgress('下载PDF文件...');
|
||||
|
||||
// 下载PDF
|
||||
const fileName = sanitizeFileName(documentTitle) || '文档';
|
||||
pdf.save(`${fileName}.pdf`);
|
||||
|
||||
showProgress('PDF导出成功!', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('PDF导出失败:', error);
|
||||
showProgress(`导出失败: ${error.message}`, 'error');
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 准备PDF导出的样式调整
|
||||
const prepareForPDF = () => {
|
||||
if (!ExecutionEnvironment.canUseDOM) return new Map();
|
||||
|
||||
const originalStyles = new Map();
|
||||
|
||||
// 隐藏一些不需要的元素
|
||||
const elementsToHide = document.querySelectorAll(
|
||||
'.navbar, .footer, [style*="position: fixed"], [style*="position: sticky"], .doc-toolbar'
|
||||
);
|
||||
|
||||
elementsToHide.forEach(el => {
|
||||
originalStyles.set(el, el.style.cssText);
|
||||
el.style.display = 'none';
|
||||
});
|
||||
|
||||
// 确保内容可见
|
||||
const content = document.querySelector('article, .markdown, .theme-doc-markdown');
|
||||
if (content) {
|
||||
originalStyles.set(content, content.style.cssText);
|
||||
content.style.maxWidth = 'none';
|
||||
content.style.padding = '20px';
|
||||
}
|
||||
|
||||
return originalStyles;
|
||||
};
|
||||
|
||||
// 恢复原始样式
|
||||
const restoreStyles = (originalStyles) => {
|
||||
if (!ExecutionEnvironment.canUseDOM || !originalStyles) return;
|
||||
|
||||
originalStyles.forEach((style, element) => {
|
||||
element.style.cssText = style;
|
||||
});
|
||||
};
|
||||
|
||||
// 清理文件名
|
||||
const sanitizeFileName = (fileName) => {
|
||||
if (!fileName) return '';
|
||||
return fileName.replace(/[<>:"/\\|?*]/g, '').trim();
|
||||
};
|
||||
|
||||
// 显示进度提示
|
||||
const showProgress = (message, type = 'info') => {
|
||||
if (!ExecutionEnvironment.canUseDOM) return;
|
||||
|
||||
const existingNotification = document.querySelector('.pdf-export-notification');
|
||||
if (existingNotification) {
|
||||
existingNotification.remove();
|
||||
}
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.className = 'pdf-export-notification';
|
||||
notification.textContent = message;
|
||||
|
||||
const bgColors = {
|
||||
info: '#3498db',
|
||||
success: '#25c2a0',
|
||||
error: '#e74c3c'
|
||||
};
|
||||
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: ${bgColors[type]};
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
max-width: 300px;
|
||||
word-wrap: break-word;
|
||||
animation: slideIn 0.3s ease;
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
if (type !== 'error') {
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.remove();
|
||||
}
|
||||
}, type === 'success' ? 3000 : 2000);
|
||||
} else {
|
||||
// 错误消息需要手动关闭
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={exportToPDF}
|
||||
disabled={isExporting}
|
||||
style={{
|
||||
backgroundColor: isExporting ? '#95a5a6' : '#25c2a0',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '4px',
|
||||
cursor: isExporting ? 'not-allowed' : 'pointer',
|
||||
fontSize: '14px',
|
||||
transition: 'all 0.2s ease',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
minWidth: '120px',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isExporting) {
|
||||
e.target.style.backgroundColor = '#21af90';
|
||||
e.target.style.transform = 'translateY(-1px)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isExporting) {
|
||||
e.target.style.backgroundColor = '#25c2a0';
|
||||
e.target.style.transform = 'translateY(0)';
|
||||
}
|
||||
}}
|
||||
title={isExporting ? '正在导出PDF...' : '将当前页面导出为PDF文件'}
|
||||
>
|
||||
{isExporting ? (
|
||||
<>
|
||||
<span style={{ animation: 'spin 1s linear infinite' }}>⏳</span>
|
||||
导出中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
📄 导出PDF
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default PDFExport;
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
import { useEffect, useCallback } from 'react';
|
||||
import { useLocation } from '@docusaurus/router';
|
||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
||||
|
||||
const ReadingPosition = () => {
|
||||
const location = useLocation();
|
||||
|
||||
// 保存滚动位置
|
||||
const saveScrollPosition = useCallback(() => {
|
||||
if (!ExecutionEnvironment.canUseDOM) return;
|
||||
|
||||
const scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
|
||||
const currentPath = location.pathname;
|
||||
|
||||
// 使用localStorage保存每个页面的滚动位置
|
||||
const savedPositions = JSON.parse(localStorage.getItem('docReadingPositions') || '{}');
|
||||
savedPositions[currentPath] = {
|
||||
scrollTop: scrollPosition,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// 只保留最近30天的阅读位置
|
||||
const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
|
||||
Object.keys(savedPositions).forEach(path => {
|
||||
if (savedPositions[path].timestamp < thirtyDaysAgo) {
|
||||
delete savedPositions[path];
|
||||
}
|
||||
});
|
||||
|
||||
localStorage.setItem('docReadingPositions', JSON.stringify(savedPositions));
|
||||
}, [location.pathname]);
|
||||
|
||||
// 恢复滚动位置
|
||||
const restoreScrollPosition = useCallback(() => {
|
||||
if (!ExecutionEnvironment.canUseDOM) return;
|
||||
|
||||
const currentPath = location.pathname;
|
||||
const savedPositions = JSON.parse(localStorage.getItem('docReadingPositions') || '{}');
|
||||
|
||||
if (savedPositions[currentPath]) {
|
||||
const { scrollTop, timestamp } = savedPositions[currentPath];
|
||||
|
||||
// 如果保存的位置是在最近7天内的,才恢复位置
|
||||
const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
|
||||
if (timestamp > sevenDaysAgo && scrollTop > 100) { // 只有滚动超过100px才恢复
|
||||
// 延迟恢复,确保页面完全加载
|
||||
setTimeout(() => {
|
||||
window.scrollTo({
|
||||
top: scrollTop,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
|
||||
// 显示提示信息
|
||||
showPositionNotification(scrollTop);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
// 显示位置恢复提示
|
||||
const showPositionNotification = (scrollTop) => {
|
||||
if (!ExecutionEnvironment.canUseDOM) return;
|
||||
|
||||
// 创建提示元素
|
||||
const notification = document.createElement('div');
|
||||
notification.innerHTML = `
|
||||
<div style="
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
right: 20px;
|
||||
background: #25c2a0;
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
z-index: 9999;
|
||||
font-size: 14px;
|
||||
max-width: 280px;
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span>📖</span>
|
||||
<span>已恢复到上次阅读位置</span>
|
||||
<button onclick="this.parentElement.parentElement.parentElement.remove()"
|
||||
style="background: none; border: none; color: white; font-size: 16px; cursor: pointer; margin-left: auto;">×</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// 添加动画样式
|
||||
if (!document.querySelector('#reading-position-styles')) {
|
||||
const styles = document.createElement('style');
|
||||
styles.id = 'reading-position-styles';
|
||||
styles.textContent = `
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styles);
|
||||
}
|
||||
|
||||
// 3秒后自动隐藏
|
||||
setTimeout(() => {
|
||||
if (notification.parentElement) {
|
||||
notification.remove();
|
||||
}
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
// 节流函数
|
||||
const throttle = (func, delay) => {
|
||||
let timeoutId;
|
||||
let lastExecTime = 0;
|
||||
return function (...args) {
|
||||
const currentTime = Date.now();
|
||||
|
||||
if (currentTime - lastExecTime > delay) {
|
||||
func.apply(this, args);
|
||||
lastExecTime = currentTime;
|
||||
} else {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
func.apply(this, args);
|
||||
lastExecTime = Date.now();
|
||||
}, delay - (currentTime - lastExecTime));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!ExecutionEnvironment.canUseDOM) return;
|
||||
|
||||
// 页面加载时恢复滚动位置
|
||||
restoreScrollPosition();
|
||||
|
||||
// 监听滚动事件,节流保存位置
|
||||
const throttledSavePosition = throttle(saveScrollPosition, 1000);
|
||||
window.addEventListener('scroll', throttledSavePosition);
|
||||
|
||||
// 页面卸载时保存位置
|
||||
const handleBeforeUnload = () => {
|
||||
saveScrollPosition();
|
||||
};
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', throttledSavePosition);
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
};
|
||||
}, [saveScrollPosition, restoreScrollPosition]);
|
||||
|
||||
// 路径变化时保存当前位置并尝试恢复新位置
|
||||
useEffect(() => {
|
||||
if (!ExecutionEnvironment.canUseDOM) return;
|
||||
|
||||
saveScrollPosition();
|
||||
// 短暂延迟后恢复新页面的位置
|
||||
const timer = setTimeout(() => {
|
||||
restoreScrollPosition();
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [location.pathname, saveScrollPosition, restoreScrollPosition]);
|
||||
|
||||
return null; // 这是一个功能性组件,不渲染任何内容
|
||||
};
|
||||
|
||||
export default ReadingPosition;
|
||||
|
|
@ -16,120 +16,7 @@
|
|||
--ifm-color-primary-lightest: rgb(146, 224, 208);
|
||||
--ifm-code-font-size: 95%;
|
||||
--search-local-modal-background:#1b2440;
|
||||
|
||||
/* 字体大小控制变量 */
|
||||
--doc-font-scale: 1;
|
||||
}
|
||||
|
||||
/* 字体大小动态控制 */
|
||||
article,
|
||||
.markdown,
|
||||
.theme-doc-markdown,
|
||||
[class*="docItemContainer"],
|
||||
[class*="docItemCol"] {
|
||||
font-size: calc(var(--doc-font-scale) * 1rem);
|
||||
}
|
||||
|
||||
/* 标题字体大小响应 */
|
||||
article h1,
|
||||
.markdown h1,
|
||||
.theme-doc-markdown h1 {
|
||||
font-size: calc(var(--doc-font-scale) * 2.5rem);
|
||||
}
|
||||
|
||||
article h2,
|
||||
.markdown h2,
|
||||
.theme-doc-markdown h2 {
|
||||
font-size: calc(var(--doc-font-scale) * 2rem);
|
||||
}
|
||||
|
||||
article h3,
|
||||
.markdown h3,
|
||||
.theme-doc-markdown h3 {
|
||||
font-size: calc(var(--doc-font-scale) * 1.75rem);
|
||||
}
|
||||
|
||||
article h4,
|
||||
.markdown h4,
|
||||
.theme-doc-markdown h4 {
|
||||
font-size: calc(var(--doc-font-scale) * 1.5rem);
|
||||
}
|
||||
|
||||
article h5,
|
||||
.markdown h5,
|
||||
.theme-doc-markdown h5 {
|
||||
font-size: calc(var(--doc-font-scale) * 1.25rem);
|
||||
}
|
||||
|
||||
article h6,
|
||||
.markdown h6,
|
||||
.theme-doc-markdown h6 {
|
||||
font-size: calc(var(--doc-font-scale) * 1.125rem);
|
||||
}
|
||||
|
||||
/* 段落和列表字体大小 */
|
||||
article p,
|
||||
article li,
|
||||
article td,
|
||||
article th,
|
||||
article blockquote,
|
||||
.markdown p,
|
||||
.markdown li,
|
||||
.markdown td,
|
||||
.markdown th,
|
||||
.markdown blockquote,
|
||||
.theme-doc-markdown p,
|
||||
.theme-doc-markdown li,
|
||||
.theme-doc-markdown td,
|
||||
.theme-doc-markdown th,
|
||||
.theme-doc-markdown blockquote {
|
||||
font-size: calc(var(--doc-font-scale) * 1rem);
|
||||
}
|
||||
|
||||
/* 代码字体大小 */
|
||||
article code,
|
||||
.markdown code,
|
||||
.theme-doc-markdown code {
|
||||
font-size: calc(var(--doc-font-scale) * 0.875rem);
|
||||
}
|
||||
|
||||
/* 目录字体大小 */
|
||||
.table-of-contents {
|
||||
font-size: calc(var(--doc-font-scale) * 0.875rem);
|
||||
}
|
||||
|
||||
/* 动画定义 */
|
||||
@keyframes slideInFromRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.navbar{
|
||||
background-color: rgba(27, 36, 64, 1);
|
||||
}
|
||||
|
|
@ -170,7 +57,6 @@ html[data-theme='dark'] .docusaurus-highlight-code-line {
|
|||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.footer__bottom .margin-bottom--sm{
|
||||
position: absolute;
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
import React from 'react';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
import DocEnhancer from '../components/DocEnhancer';
|
||||
|
||||
// 默认根组件包装器
|
||||
export default function Root({ children }) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<BrowserOnly fallback={<div></div>}>
|
||||
{() => <DocEnhancer />}
|
||||
</BrowserOnly>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -69,6 +69,26 @@ GitLink(确实开源)是CCF官方指定的开源创新服务平台,旨在
|
|||
<h2 class="text--truncate cardTitle" title="第三方服务">第三方服务</h2>
|
||||
<p>第三方服务使用及设置[3个文档]</p>
|
||||
</a></article>
|
||||
<article class="col col--6 margin-bottom--lg">
|
||||
<a class="card padding--lg cardContainer" href="/通知/通知简介">
|
||||
<h2 class="text--truncate cardTitle" title="通知">通知</h2>
|
||||
<p>通知使用及设置[2个文档]</p>
|
||||
</a></article>
|
||||
<article class="col col--6 margin-bottom--lg">
|
||||
<a class="card padding--lg cardContainer" href="/个人主页建站/站点创建流程">
|
||||
<h2 class="text--truncate cardTitle" title="个人主页建站">个人主页建站</h2>
|
||||
<p>个人主页建站使用及设置[2个文档]</p>
|
||||
</a></article>
|
||||
<article class="col col--6 margin-bottom--lg">
|
||||
<a class="card padding--lg cardContainer" href="/平台公告/">
|
||||
<h2 class="text--truncate cardTitle" title="平台公告">平台公告</h2>
|
||||
<p>平台公告使用及设置</p>
|
||||
</a></article>
|
||||
<article class="col col--6 margin-bottom--lg">
|
||||
<a class="card padding--lg cardContainer" href="/服务协议/GitLink服务协议">
|
||||
<h2 class="text--truncate cardTitle" title="服务协议">服务协议</h2>
|
||||
<p>服务协议使用及设置[1个文档]</p>
|
||||
</a></article>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
148
功能完成总结.md
148
功能完成总结.md
|
|
@ -1,148 +0,0 @@
|
|||
# GitLink 帮助中心功能完成总结
|
||||
|
||||
## 🎉 任务完成状态
|
||||
|
||||
✅ **保留上次阅读位置** - 已完成
|
||||
✅ **导出PDF功能** - 已完成
|
||||
✅ **改变字体大小** - 已完成
|
||||
|
||||
## 📋 功能详细实现
|
||||
|
||||
### 1. 📖 保留上次阅读位置
|
||||
|
||||
**实现状态**: ✅ 完全实现
|
||||
|
||||
**功能特点**:
|
||||
- ✅ 自动保存用户在每个页面的滚动位置
|
||||
- ✅ 刷新或重新访问页面时自动恢复到上次阅读位置
|
||||
- ✅ 智能时效管理:保留7天内的阅读记录,自动清理过期数据
|
||||
- ✅ 只有滚动距离超过100px才会保存和恢复
|
||||
- ✅ 友好的视觉提示,显示"已恢复到上次阅读位置"
|
||||
|
||||
**技术实现**:
|
||||
- 使用`localStorage`持久化存储
|
||||
- 节流优化,1秒内最多保存一次位置
|
||||
- `beforeunload`事件确保页面关闭时保存位置
|
||||
|
||||
### 2. 📄 导出PDF功能
|
||||
|
||||
**实现状态**: ✅ 完全实现
|
||||
|
||||
**功能特点**:
|
||||
- ✅ 一键导出当前文档页面为PDF文件
|
||||
- ✅ 高质量渲染(2倍缩放)确保清晰度
|
||||
- ✅ 智能分页处理,长文档自动分页并添加页码
|
||||
- ✅ 实时进度提示:开始导出→生成图片→生成PDF→下载
|
||||
- ✅ 自动使用文档标题作为PDF文件名
|
||||
- ✅ 导出前自动隐藏不必要的页面元素(导航栏、工具栏等)
|
||||
- ✅ 完善的错误处理和用户提示
|
||||
|
||||
**技术实现**:
|
||||
- `html2canvas` - 将页面转换为图片
|
||||
- `jsPDF` - 生成PDF文件
|
||||
- 智能样式调整和元素隐藏
|
||||
- A4格式,自动计算最佳显示比例
|
||||
|
||||
### 3. 🔤 改变字体大小
|
||||
|
||||
**实现状态**: ✅ 完全实现,按要求放置在右边且可隐藏
|
||||
|
||||
**功能特点**:
|
||||
- ✅ 提供6种字体大小选项:极小、小号、标准、大号、特大、巨大
|
||||
- ✅ 按要求放置在页面右侧
|
||||
- ✅ 可以隐藏/显示调节面板
|
||||
- ✅ 实时预览:调节后立即生效
|
||||
- ✅ 智能适配:同时调节标题、正文、代码等不同元素
|
||||
- ✅ 设置保存:字体大小和显示状态自动保存到本地
|
||||
- ✅ 美观的界面设计,显示当前字体大小和像素值
|
||||
- ✅ 一键重置为标准大小
|
||||
|
||||
**技术实现**:
|
||||
- CSS变量 + 直接样式设置双重保障
|
||||
- 智能选择器匹配各种内容元素
|
||||
- `localStorage`保存用户偏好
|
||||
|
||||
## 🎛️ 界面布局
|
||||
|
||||
**右侧悬浮工具设计**:
|
||||
- **📄** **PDF导出按钮** (top: 120px) - 绿色圆形按钮,点击显示PDF导出面板
|
||||
- **Aa** **字体调节按钮** (top: 180px) - 蓝色圆形按钮,点击显示字体调节面板
|
||||
|
||||
## 📁 代码结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ ├── DocEnhancer.js # 主集成组件
|
||||
│ ├── FontSizeController.js # 字体大小控制 ✅
|
||||
│ ├── PDFExport.js # PDF导出功能 ✅
|
||||
│ └── ReadingPosition.js # 阅读位置保存 ✅
|
||||
├── theme/
|
||||
│ └── Root.js # 根组件包装器
|
||||
└── css/
|
||||
└── custom.css # 样式和动画定义
|
||||
```
|
||||
|
||||
## 🔧 技术特性
|
||||
|
||||
### 兼容性保障
|
||||
- ✅ 服务器端渲染(SSR)兼容 - 使用`BrowserOnly`和`ExecutionEnvironment`
|
||||
- ✅ 客户端水合安全 - 所有DOM操作都有环境检查
|
||||
- ✅ 浏览器兼容性 - 现代浏览器完美支持
|
||||
|
||||
### 性能优化
|
||||
- ✅ 防抖节流 - 滚动事件优化
|
||||
- ✅ 内存管理 - 自动清理过期数据和事件监听
|
||||
- ✅ 智能显示条件 - 只在文档页面显示工具
|
||||
|
||||
### 用户体验
|
||||
- ✅ 视觉反馈 - 所有操作都有明确提示
|
||||
- ✅ 无侵入设计 - 不影响现有页面布局
|
||||
- ✅ 响应式界面 - 适配不同设备
|
||||
- ✅ 简洁易用 - 精简功能,避免冗余
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
### 阅读位置保存
|
||||
- **自动功能** - 无需操作,系统自动运行
|
||||
|
||||
### PDF导出
|
||||
1. 在文档页面(非首页)
|
||||
2. 点击右侧"📄"按钮
|
||||
3. 点击"📄 导出PDF"
|
||||
4. 等待导出完成并自动下载
|
||||
|
||||
### 字体大小调节
|
||||
1. 在文档页面右侧找到"Aa"按钮
|
||||
2. 点击显示字体调节面板
|
||||
3. 选择合适的字体大小
|
||||
4. 使用"重置"或"隐藏"按钮
|
||||
|
||||
## ✨ 项目状态
|
||||
|
||||
- ✅ **开发完成** - 所有功能已实现
|
||||
- ✅ **测试通过** - 构建成功,功能正常
|
||||
- ✅ **部署就绪** - 可直接用于生产环境
|
||||
- ✅ **文档完善** - 提供详细使用说明
|
||||
- ✅ **用户优化** - 根据反馈简化了功能
|
||||
|
||||
## 📦 依赖项
|
||||
|
||||
新增依赖包(已安装):
|
||||
- `html2canvas: ^1.4.1` - PDF导出图片生成
|
||||
- `jspdf: ^2.5.1` - PDF文件生成
|
||||
|
||||
## 🎯 功能说明
|
||||
|
||||
**最终实现的功能**:
|
||||
1. **阅读位置保存** - 自动记录和恢复阅读位置
|
||||
2. **PDF导出** - 简洁的一键导出功能
|
||||
3. **字体大小调节** - 6级字体大小调节
|
||||
|
||||
**设计理念**:
|
||||
- 功能精简,避免重复
|
||||
- 界面清爽,不干扰阅读
|
||||
- 只在需要时显示(文档页面)
|
||||
- 所有设置自动保存
|
||||
|
||||
所有功能已完美集成到现有的Docusaurus项目中,无需修改任何现有代码!
|
||||
Loading…
Reference in New Issue