}
export default DarkModeButton
diff --git a/lib/global.js b/lib/global.js
index 599a14cb..a6e45b3f 100644
--- a/lib/global.js
+++ b/lib/global.js
@@ -4,7 +4,6 @@ import { useRouter } from 'next/router'
import BLOG from '@/blog.config'
import { ALL_THEME, initDarkMode } from '@/themes/theme'
import NProgress from 'nprogress'
-import LoadingCover from '@/components/LoadingCover'
import { getQueryVariable, isBrowser } from './utils'
const GlobalContext = createContext()
@@ -22,7 +21,7 @@ export function GlobalContextProvider({ children }) {
const [theme, setTheme] = useState(BLOG.THEME) // 默认博客主题
const [isDarkMode, updateDarkMode] = useState(BLOG.APPEARANCE === 'dark') // 默认深色模式
const [onLoading, setOnLoading] = useState(false) // 抓取文章数据
- const [onReading, setOnReading] = useState(false) // 网页资源加载
+ // const [onReading, setOnReading] = useState(false) // 网页资源加载
useEffect(() => {
initLocale(lang, locale, updateLang, updateLocale)
@@ -34,7 +33,7 @@ export function GlobalContextProvider({ children }) {
// setOnReading(true)
}
// 监听页面元素加载完
- setOnReading(false)
+ // setOnReading(false)
window.addEventListener('beforeunload', handleBeforeUnload)
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload)
@@ -88,10 +87,8 @@ export function GlobalContextProvider({ children }) {
updateDarkMode,
theme,
setTheme,
- switchTheme,
- setOnReading
+ switchTheme
}}>
-
{children}
)
diff --git a/lib/notion/getNotionData.js b/lib/notion/getNotionData.js
index 977193b2..f2550855 100644
--- a/lib/notion/getNotionData.js
+++ b/lib/notion/getNotionData.js
@@ -26,6 +26,7 @@ export async function getGlobalNotionData({
}) {
// 获取Notion数据
const notionPageData = deepClone(await getNotionPageData({ pageId, from }))
+ notionPageData.allNavPages = getNavPages({ allPages: notionPageData.allPages })
delete notionPageData.block
delete notionPageData.schema
delete notionPageData.rawMetadata
@@ -168,6 +169,37 @@ function getSiteInfo({ collection, block }) {
return { title, description, pageCover, icon }
}
+/**
+ * 获取导航pages
+ * 转为gitbook这类文档主题设计,精减的标题和内容
+ * 导航页面的条件,必须是Posts
+ * @param {*} param0
+ */
+export function getNavPages({ allPages }) {
+ const allNavPages = allPages.filter(post => {
+ return post && post?.slug && (!post?.slug?.startsWith('http')) && post?.type === 'Post' && post?.status === 'Published'
+ })
+ const result = allNavPages.map(item => ({ id: item.id, title: item.title, category: item.category || null, tags: item.tags || null, summary: item.summary || null, slug: item.slug }))
+
+ const groupedArray = result.reduce((groups, item) => {
+ const categoryName = item.category ? item.category.join('/') : '' // 将category转换为字符串
+ const lastGroup = groups[groups.length - 1] // 获取最后一个分组
+
+ if (!lastGroup || lastGroup.category !== categoryName) { // 如果当前元素的category与上一个元素不同,则创建新分组
+ groups.push({ category: categoryName, items: [] })
+ }
+
+ groups[groups.length - 1].items.push(item) // 将元素加入对应的分组
+
+ return groups
+ }, [])
+
+ return groupedArray
+}
+
+/**
+ * 获取公告
+ */
async function getNotice(post) {
if (!post) {
return null
@@ -183,6 +215,7 @@ const EmptyData = (pageId) => {
notice: null,
siteInfo: getSiteInfo({}),
allPages: [{ id: 1, title: `无法获取Notion数据,请检查Notion_ID: \n 当前 ${pageId}`, summary: '访问文档获取帮助→ https://tangly1024.com/article/vercel-deploy-notion-next', status: 'Published', type: 'Post', slug: '13a171332816461db29d50e9f575b00d', date: { start_date: '2023-04-24', lastEditedTime: '2023-04-24', tagItems: [] } }],
+ allNavPages: [],
collection: [],
collectionQuery: {},
collectionId: null,
@@ -275,11 +308,13 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
// 新的菜单
const customMenu = await getCustomMenu({ collectionData })
const latestPosts = getLatestPosts({ allPages, from, latestPostCount: 5 })
+ const allNavPages = getNavPages({ allPages })
return {
notice,
siteInfo,
allPages,
+ allNavPages,
collection,
collectionQuery,
collectionId,
diff --git a/package.json b/package.json
index a916caa3..1551bc82 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "notion-next",
- "version": "3.15.2",
+ "version": "3.16.0",
"homepage": "https://github.com/tangly1024/NotionNext.git",
"license": "MIT",
"repository": {
diff --git a/pages/[...slug].js b/pages/[...slug].js
index f28b0a42..511ca761 100644
--- a/pages/[...slug].js
+++ b/pages/[...slug].js
@@ -4,11 +4,11 @@ import { getGlobalNotionData } from '@/lib/notion/getNotionData'
import { useEffect, useState } from 'react'
import { idToUuid } from 'notion-utils'
import { useRouter } from 'next/router'
-import { isBrowser } from '@/lib/utils'
import { getNotion } from '@/lib/notion/getNotion'
import { getPageTableOfContents } from '@/lib/notion/getPageTableOfContents'
-import md5 from 'js-md5'
import { getLayoutByTheme } from '@/themes/theme'
+import md5 from 'js-md5'
+import { isBrowser } from '@/lib/utils'
/**
* 根据notion的slug访问页面
@@ -19,16 +19,13 @@ const Slug = props => {
const { post, siteInfo } = props
const router = useRouter()
- // 根据页面路径加载不同Layout文件
- const Layout = getLayoutByTheme(useRouter())
-
// 文章锁🔐
const [lock, setLock] = useState(post?.password && post?.password !== '')
/**
- * 验证文章密码
- * @param {*} result
- */
+ * 验证文章密码
+ * @param {*} result
+ */
const validPassword = passInput => {
const encrypt = md5(post.slug + passInput)
if (passInput && encrypt === post.password) {
@@ -51,18 +48,18 @@ const Slug = props => {
})
}
}
- }, 8 * 1000) // 404时长 8秒
+ }, 5 * 1000) // 404时长 8秒
}
// 文章加密
if (post?.password && post?.password !== '') {
setLock(true)
} else {
+ setLock(false)
if (!lock && post?.blockMap?.block) {
post.content = Object.keys(post.blockMap.block).filter(key => post.blockMap.block[key]?.value?.parent_id === post.id)
post.toc = getPageTableOfContents(post, post.blockMap)
}
- setLock(false)
}
router.events.on('routeChangeComplete', () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
@@ -79,7 +76,8 @@ const Slug = props => {
tags: post?.tags
}
props = { ...props, lock, meta, setLock, validPassword }
-
+ // 根据页面路径加载不同Layout文件
+ const Layout = getLayoutByTheme(useRouter())
return
}
@@ -124,6 +122,7 @@ export async function getStaticProps({ params: { slug } }) {
// 无法获取文章
if (!props?.post) {
+ props.post = null
return { props, revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND) }
}
diff --git a/styles/notion.css b/styles/notion.css
index c309b68b..746189ec 100644
--- a/styles/notion.css
+++ b/styles/notion.css
@@ -1142,7 +1142,6 @@ code[class*='language-'] {
overflow: hidden;
text-overflow: ellipsis;
- @apply text-blue-600 dark:text-blue-200
}
.notion-table-of-contents-item:hover {
diff --git a/themes/gitbook/Layout404.js b/themes/gitbook/Layout404.js
new file mode 100644
index 00000000..68b656cc
--- /dev/null
+++ b/themes/gitbook/Layout404.js
@@ -0,0 +1,9 @@
+import LayoutBase from './LayoutBase'
+
+export const Layout404 = props => {
+ return
+ 404 Not found.
+
+}
+
+export default Layout404
diff --git a/themes/gitbook/LayoutArchive.js b/themes/gitbook/LayoutArchive.js
new file mode 100644
index 00000000..101b7119
--- /dev/null
+++ b/themes/gitbook/LayoutArchive.js
@@ -0,0 +1,49 @@
+import BLOG from '@/blog.config'
+import Link from 'next/link'
+import LayoutBase from './LayoutBase'
+
+export const LayoutArchive = props => {
+ const { archivePosts } = props
+
+ return (
+
+
+ {Object.keys(archivePosts)?.map(archiveTitle => (
+
+ ))}
+
+
+ )
+}
+
+export default LayoutArchive
diff --git a/themes/gitbook/LayoutBase.js b/themes/gitbook/LayoutBase.js
new file mode 100644
index 00000000..99384af9
--- /dev/null
+++ b/themes/gitbook/LayoutBase.js
@@ -0,0 +1,122 @@
+import CommonHead from '@/components/CommonHead'
+import { useState, createContext, useContext, useEffect } from 'react'
+import Footer from './components/Footer'
+import InfoCard from './components/InfoCard'
+import RevolverMaps from './components/RevolverMaps'
+import CONFIG_GITBOOK from './config_gitbook'
+import TopNavBar from './components/TopNavBar'
+import SearchInput from './components/SearchInput'
+import BottomMenuBar from './components/BottomMenuBar'
+import { useGlobal } from '@/lib/global'
+import Live2D from '@/components/Live2D'
+import BLOG from '@/blog.config'
+import BlogPostListScroll from './components/BlogPostListScroll'
+import ArticleInfo from './components/ArticleInfo'
+import Catalog from './components/Catalog'
+import { useRouter } from 'next/router'
+import Announcement from './components/Announcement'
+import PageNavDrawer from './components/PageNavDrawer'
+const ThemeGlobalMedium = createContext()
+
+/**
+ * 基础布局 采用左右两侧布局,移动端使用顶部导航栏
+
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const LayoutBase = (props) => {
+ const { children, meta, post, allNavPages, slotLeft, slotRight, slotTop, siteInfo } = props
+ const [tocVisible, changeTocVisible] = useState(false)
+ const [pageNavVisible, changePageNavVisible] = useState(false)
+ const [filterPosts, setFilterPosts] = useState(allNavPages)
+ const { onLoading } = useGlobal()
+ const router = useRouter()
+
+ useEffect(() => {
+ setFilterPosts(allNavPages)
+ }, [post])
+
+ const LoadingCover =
+
+ return (
+
+
+
+
+ {/* 顶部导航栏 */}
+
+
+
+
+ {/* 左侧推拉抽屉 */}
+
+
+ {slotLeft}
+
+
+
+ {/* 所有文章列表 */}
+
+
+
+
+
+
+
+
+ {slotTop}
+
+ {onLoading ? LoadingCover : children}
+
+ {/* 回顶按钮 */}
+
+ { window.scrollTo({ top: 0, behavior: 'smooth' }) }} />
+
+
+
+ {/* 底部 */}
+
+
+
+ {/* 右侧侧推拉抽屉 */}
+
+
+
+
+
+
+ {slotRight}
+ {router.route === '/' && <>
+
+ {CONFIG_GITBOOK.WIDGET_REVOLVER_MAPS === 'true' &&
}
+
+ >}
+ {/* gitbook主题首页只显示公告 */}
+
+
+
+
+
+
+
+
+
+
+ {/* 移动端底部导航栏 */}
+
+
+
+ )
+}
+
+export default LayoutBase
+export const useMediumGlobal = () => useContext(ThemeGlobalMedium)
diff --git a/themes/gitbook/LayoutCategory.js b/themes/gitbook/LayoutCategory.js
new file mode 100644
index 00000000..a947b70e
--- /dev/null
+++ b/themes/gitbook/LayoutCategory.js
@@ -0,0 +1,15 @@
+import LayoutBase from './LayoutBase'
+import BlogPostListScroll from './components/BlogPostListScroll'
+import BlogPostListPage from './components/BlogPostListPage'
+import BLOG from '@/blog.config'
+
+export const LayoutCategory = props => {
+ const { category } = props
+ const slotTop =
+
+ return
+ {BLOG.POST_LIST_STYLE === 'page' ? : }
+
+}
+
+export default LayoutCategory
diff --git a/themes/gitbook/LayoutCategoryIndex.js b/themes/gitbook/LayoutCategoryIndex.js
new file mode 100644
index 00000000..2c8121ad
--- /dev/null
+++ b/themes/gitbook/LayoutCategoryIndex.js
@@ -0,0 +1,34 @@
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import LayoutBase from './LayoutBase'
+
+export const LayoutCategoryIndex = (props) => {
+ const { categoryOptions } = props
+ const { locale } = useGlobal()
+ return (
+
+
+
+ {locale.COMMON.CATEGORY}:
+
+
+ {categoryOptions?.map(category => {
+ return (
+
+
+ {category.name}({category.count})
+
+
+ )
+ })}
+
+
+
+ )
+}
+export default LayoutCategoryIndex
diff --git a/themes/gitbook/LayoutIndex.js b/themes/gitbook/LayoutIndex.js
new file mode 100644
index 00000000..e259a80a
--- /dev/null
+++ b/themes/gitbook/LayoutIndex.js
@@ -0,0 +1,34 @@
+import { useRouter } from 'next/router'
+import LayoutBase from './LayoutBase'
+import { useEffect } from 'react'
+import CONFIG_GITBOOK from './config_gitbook'
+import { isBrowser } from '@/lib/utils'
+/**
+ * gitbook的首页
+ * @param {*} props
+ * @returns
+ */
+export const LayoutIndex = (props) => {
+ const router = useRouter()
+ // 直接显示指定页面
+ useEffect(() => {
+ router.push(CONFIG_GITBOOK.INDEX_PAGE).then(() => {
+ // console.log('跳转到指定首页', CONFIG_GITBOOK.INDEX_PAGE)
+ setTimeout(() => {
+ if (isBrowser()) {
+ const article = document.getElementById('notion-article')
+ if (!article) {
+ console.log('请检查您的Notion数据库中是否包含此slug页面: ', CONFIG_GITBOOK.INDEX_PAGE)
+ const containerInner = document.getElementById('container-inner')
+ const newHTML = `配置有误
请在您的notion中添加一个slug为${CONFIG_GITBOOK.INDEX_PAGE}的文章
`
+ containerInner.insertAdjacentHTML('afterbegin', newHTML)
+ }
+ }
+ }, 7 * 1000)
+ })
+ }, [])
+ return
+
+}
+
+export default LayoutIndex
diff --git a/themes/gitbook/LayoutPage.js b/themes/gitbook/LayoutPage.js
new file mode 100644
index 00000000..563df90b
--- /dev/null
+++ b/themes/gitbook/LayoutPage.js
@@ -0,0 +1,10 @@
+import LayoutBase from './LayoutBase'
+import BlogPostListPage from './components/BlogPostListPage'
+
+export const LayoutPage = (props) => {
+ return
+
+
+}
+
+export default LayoutPage
diff --git a/themes/gitbook/LayoutSearch.js b/themes/gitbook/LayoutSearch.js
new file mode 100644
index 00000000..eaecbd84
--- /dev/null
+++ b/themes/gitbook/LayoutSearch.js
@@ -0,0 +1,53 @@
+import LayoutBase from './LayoutBase'
+import SearchInput from './components/SearchInput'
+import { useGlobal } from '@/lib/global'
+import TagGroups from './components/TagGroups'
+import CategoryGroup from './components/CategoryGroup'
+import { useEffect } from 'react'
+import { isBrowser } from '@/lib/utils'
+import BLOG from '@/blog.config'
+import Mark from 'mark.js'
+import BlogPostListScroll from './components/BlogPostListScroll'
+import BlogPostListPage from './components/BlogPostListPage'
+import { useRouter } from 'next/router'
+
+export const LayoutSearch = (props) => {
+ const { locale } = useGlobal()
+ const { keyword } = props
+ const router = useRouter()
+ const currentSearch = keyword || router?.query?.s
+
+ useEffect(() => {
+ setTimeout(() => {
+ const container = isBrowser() && document.getElementById('container')
+ if (container && container.innerHTML) {
+ const re = new RegExp(currentSearch, 'gim')
+ const instance = new Mark(container)
+ instance.markRegExp(re, {
+ element: 'span',
+ className: 'text-red-500 border-b border-dashed'
+ })
+ }
+ },
+ 100)
+ }, [currentSearch])
+ return
+
+
+
{locale.NAV.SEARCH}
+
+
+ {!currentSearch && <>
+
+
+ >}
+
+
+
+ {currentSearch &&
+ {BLOG.POST_LIST_STYLE === 'page' ? : }
+
}
+
+}
+
+export default LayoutSearch
diff --git a/themes/gitbook/LayoutSlug.js b/themes/gitbook/LayoutSlug.js
new file mode 100644
index 00000000..b33d9b61
--- /dev/null
+++ b/themes/gitbook/LayoutSlug.js
@@ -0,0 +1,53 @@
+import LayoutBase from './LayoutBase'
+import React from 'react'
+import { ArticleLock } from './components/ArticleLock'
+import NotionPage from '@/components/NotionPage'
+import CONFIG_GITBOOK from './config_gitbook'
+import Comment from '@/components/Comment'
+import ArticleAround from './components/ArticleAround'
+import TocDrawer from './components/TocDrawer'
+import CategoryItem from './components/CategoryItem'
+import TagItemMini from './components/TagItemMini'
+import ShareBar from '@/components/ShareBar'
+
+export const LayoutSlug = (props) => {
+ const { post, prev, next, lock, validPassword } = props
+
+ return (
+
+ {/* 文章锁 */}
+ {lock && }
+
+ {!lock &&
+
+ {/* title */}
+
{post?.title}
+
+ {/* Notion文章主体 */}
+ {post && (
)}
+
+
+
+ {/* 分享 */}
+
+ {/* 文章分类和标签信息 */}
+
+ {CONFIG_GITBOOK.POST_DETAIL_CATEGORY && post?.category &&
}
+
+ {CONFIG_GITBOOK.POST_DETAIL_TAG && post?.tagItems?.map(tag => )}
+
+
+
+ {post?.type === 'Post' && }
+
+
+
+
+
}
+
+ )
+}
+
+export default LayoutSlug
diff --git a/themes/gitbook/LayoutTag.js b/themes/gitbook/LayoutTag.js
new file mode 100644
index 00000000..bedb51ef
--- /dev/null
+++ b/themes/gitbook/LayoutTag.js
@@ -0,0 +1,15 @@
+import LayoutBase from './LayoutBase'
+import BLOG from '@/blog.config'
+import BlogPostListScroll from './components/BlogPostListScroll'
+import BlogPostListPage from './components/BlogPostListPage'
+
+export const LayoutTag = (props) => {
+ const { tag } = props
+ const slotTop =
+
+ return
+ {BLOG.POST_LIST_STYLE === 'page' ? : }
+
+}
+
+export default LayoutTag
diff --git a/themes/gitbook/LayoutTagIndex.js b/themes/gitbook/LayoutTagIndex.js
new file mode 100644
index 00000000..cc9f8c94
--- /dev/null
+++ b/themes/gitbook/LayoutTagIndex.js
@@ -0,0 +1,29 @@
+import { useGlobal } from '@/lib/global'
+import TagItemMini from './components/TagItemMini'
+import LayoutBase from './LayoutBase'
+
+export const LayoutTagIndex = props => {
+ const { tagOptions } = props
+ const { locale } = useGlobal()
+ return (
+
+
+
+
+ {locale.COMMON.TAGS}:
+
+
+
+
+ )
+}
+
+export default LayoutTagIndex
diff --git a/themes/gitbook/components/Announcement.js b/themes/gitbook/components/Announcement.js
new file mode 100755
index 00000000..62df1ec2
--- /dev/null
+++ b/themes/gitbook/components/Announcement.js
@@ -0,0 +1,21 @@
+// import { useGlobal } from '@/lib/global'
+import dynamic from 'next/dynamic'
+
+const NotionPage = dynamic(() => import('@/components/NotionPage'))
+
+const Announcement = ({ notice, className }) => {
+// const { locale } = useGlobal()
+ if (notice?.blockMap) {
+ return
+
+ {/* {locale.COMMON.ANNOUNCEMENT}
*/}
+ {notice && (
+
+
)}
+
+
+ } else {
+ return <>>
+ }
+}
+export default Announcement
diff --git a/themes/gitbook/components/ArticleAround.js b/themes/gitbook/components/ArticleAround.js
new file mode 100644
index 00000000..95b6f83f
--- /dev/null
+++ b/themes/gitbook/components/ArticleAround.js
@@ -0,0 +1,32 @@
+import Link from 'next/link'
+
+/**
+ * 上一篇,下一篇文章
+ * @param {prev,next} param0
+ * @returns
+ */
+export default function ArticleAround ({ prev, next }) {
+ if (!prev || !next) {
+ return <>>
+ }
+ return (
+
+
+
+ {prev.title}
+
+
+
+ {next.title}
+
+
+
+
+ )
+}
diff --git a/themes/gitbook/components/ArticleInfo.js b/themes/gitbook/components/ArticleInfo.js
new file mode 100644
index 00000000..b2d58efe
--- /dev/null
+++ b/themes/gitbook/components/ArticleInfo.js
@@ -0,0 +1,9 @@
+export default function ArticleInfo({ post }) {
+ if (!post) {
+ return null
+ }
+ return
+
+ Last update: { post.date?.start_date}
+
+}
diff --git a/themes/gitbook/components/ArticleLock.js b/themes/gitbook/components/ArticleLock.js
new file mode 100644
index 00000000..4ba5a3b9
--- /dev/null
+++ b/themes/gitbook/components/ArticleLock.js
@@ -0,0 +1,53 @@
+import { useGlobal } from '@/lib/global'
+import { useEffect, useRef } from 'react'
+
+/**
+ * 加密文章校验组件
+ * @param {password, validPassword} props
+ * @param password 正确的密码
+ * @param validPassword(bool) 回调函数,校验正确回调入参为true
+ * @returns
+ */
+export const ArticleLock = props => {
+ const { validPassword } = props
+ const { locale } = useGlobal()
+
+ const submitPassword = () => {
+ const p = document.getElementById('password')
+ if (!validPassword(p?.value)) {
+ const tips = document.getElementById('tips')
+ if (tips) {
+ tips.innerHTML = ''
+ tips.innerHTML = `${locale.COMMON.PASSWORD_ERROR}
`
+ }
+ }
+ }
+
+ const passwordInputRef = useRef(null)
+ useEffect(() => {
+ // 选中密码输入框并将其聚焦
+ passwordInputRef.current.focus()
+ }, [])
+
+ return
+
+
{locale.COMMON.ARTICLE_LOCK_TIPS}
+
+
{
+ if (e.key === 'Enter') {
+ submitPassword()
+ }
+ }}
+ ref={passwordInputRef} // 绑定ref到passwordInputRef变量
+ className='outline-none w-full text-sm pl-5 rounded-l transition focus:shadow-lg dark:text-gray-300 font-light leading-10 text-black bg-gray-100 dark:bg-gray-500'>
+
+
+ {locale.COMMON.SUBMIT}
+
+
+
+
+
+
+}
diff --git a/themes/gitbook/components/BlogPostCard.js b/themes/gitbook/components/BlogPostCard.js
new file mode 100644
index 00000000..7dc9db14
--- /dev/null
+++ b/themes/gitbook/components/BlogPostCard.js
@@ -0,0 +1,28 @@
+import BLOG from '@/blog.config'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import React from 'react'
+
+const BlogPostCard = ({ post, className }) => {
+ const router = useRouter()
+ const currentSelected = router.asPath.split('?')[0] === '/' + post.slug
+ return (
+
+
+
+
+ {post.title}
+
+
+
+
+
+ )
+}
+
+export default BlogPostCard
diff --git a/themes/gitbook/components/BlogPostListEmpty.js b/themes/gitbook/components/BlogPostListEmpty.js
new file mode 100644
index 00000000..86977fd0
--- /dev/null
+++ b/themes/gitbook/components/BlogPostListEmpty.js
@@ -0,0 +1,12 @@
+
+/**
+ * 空白博客 列表
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const BlogPostListEmpty = ({ currentSearch }) => {
+ return
+
没有找到文章 {(currentSearch &&
{currentSearch}
)}
+
+}
+export default BlogPostListEmpty
diff --git a/themes/gitbook/components/BlogPostListPage.js b/themes/gitbook/components/BlogPostListPage.js
new file mode 100644
index 00000000..db74b74e
--- /dev/null
+++ b/themes/gitbook/components/BlogPostListPage.js
@@ -0,0 +1,34 @@
+import BlogPostCard from './BlogPostCard'
+import BLOG from '@/blog.config'
+import BlogPostListEmpty from './BlogPostListEmpty'
+import PaginationSimple from './PaginationSimple'
+
+/**
+ * 文章列表分页表格
+ * @param page 当前页
+ * @param posts 所有文章
+ * @param tags 所有标签
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const BlogPostListPage = ({ page = 1, posts = [], postCount }) => {
+ const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
+
+ if (!posts || posts.length === 0) {
+ return
+ }
+
+ return (
+
+
+ {/* 文章列表 */}
+ {posts?.map(post => (
+
+ ))}
+
+
+
+ )
+}
+
+export default BlogPostListPage
diff --git a/themes/gitbook/components/BlogPostListScroll.js b/themes/gitbook/components/BlogPostListScroll.js
new file mode 100644
index 00000000..038b7a1d
--- /dev/null
+++ b/themes/gitbook/components/BlogPostListScroll.js
@@ -0,0 +1,35 @@
+import BlogPostCard from './BlogPostCard'
+import BlogPostListEmpty from './BlogPostListEmpty'
+import React, { useRef } from 'react'
+
+/**
+ * 博客列表滚动分页
+ * @param posts 所有文章
+ * @param tags 所有标签
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const BlogPostListScroll = ({ posts = [], currentSearch }) => {
+ const targetRef = useRef(null)
+ const filteredPosts = Object.assign(posts)
+
+ if (!filteredPosts || filteredPosts.length === 0) {
+ return
+ } else {
+ return
+ {/* 文章列表 */}
+ {filteredPosts?.map((group, index) => {
+ if (group.category) {
+ return
+
{group.category}
+ {group.items?.map(post => (
))}
+
+ } else {
+ return
{group.items?.map(post => ())}
+ }
+ })}
+
+ }
+}
+
+export default BlogPostListScroll
diff --git a/themes/gitbook/components/BottomMenuBar.js b/themes/gitbook/components/BottomMenuBar.js
new file mode 100644
index 00000000..c3348bc3
--- /dev/null
+++ b/themes/gitbook/components/BottomMenuBar.js
@@ -0,0 +1,38 @@
+import Link from 'next/link'
+import React from 'react'
+import { useMediumGlobal } from '../LayoutBase'
+import JumpToTopButton from './JumpToTopButton'
+
+export default function BottomMenuBar({ post, className }) {
+ const { tocVisible, changeTocVisible, pageNavVisible, changePageNavVisible } = useMediumGlobal()
+ const showTocBotton = post?.toc?.length > 0
+
+ const toggleToc = () => {
+ changeTocVisible(!tocVisible)
+ }
+
+ const togglePageNavVisible = () => {
+ changePageNavVisible(!pageNavVisible)
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {showTocBotton &&
+
+
}
+ {!showTocBotton &&
+
+
+
+ }
+
+
+ )
+}
diff --git a/themes/gitbook/components/Card.js b/themes/gitbook/components/Card.js
new file mode 100644
index 00000000..d24c046e
--- /dev/null
+++ b/themes/gitbook/components/Card.js
@@ -0,0 +1,9 @@
+const Card = ({ children, headerSlot, className }) => {
+ return
+ <>{headerSlot}>
+
+
+}
+export default Card
diff --git a/themes/gitbook/components/Catalog.js b/themes/gitbook/components/Catalog.js
new file mode 100644
index 00000000..a8f01fcf
--- /dev/null
+++ b/themes/gitbook/components/Catalog.js
@@ -0,0 +1,88 @@
+import { useCallback, useEffect, useState } from 'react'
+import throttle from 'lodash.throttle'
+import { uuidToId } from 'notion-utils'
+import { isBrowser } from '@/lib/utils'
+
+/**
+ * 目录导航组件
+ * @param toc
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const Catalog = ({ post }) => {
+ const tocIds = []
+ const toc = post?.toc
+ // 同步选中目录事件
+ const [activeSection, setActiveSection] = useState(null)
+
+ // 监听滚动事件
+ useEffect(() => {
+ window.addEventListener('scroll', actionSectionScrollSpy)
+ actionSectionScrollSpy()
+ return () => {
+ window.removeEventListener('scroll', actionSectionScrollSpy)
+ }
+ }, [post])
+
+ const throttleMs = 200
+ const actionSectionScrollSpy = useCallback(throttle(() => {
+ const sections = document.getElementsByClassName('notion-h')
+ let prevBBox = null
+ let currentSectionId = activeSection
+ for (let i = 0; i < sections.length; ++i) {
+ const section = sections[i]
+ if (!section || !(section instanceof Element)) continue
+ if (!currentSectionId) {
+ currentSectionId = section.getAttribute('data-id')
+ }
+ const bbox = section.getBoundingClientRect()
+ const prevHeight = prevBBox ? bbox.top - prevBBox.bottom : 0
+ const offset = Math.max(150, prevHeight / 4)
+ // GetBoundingClientRect returns values relative to viewport
+ if (bbox.top - offset < 0) {
+ currentSectionId = section.getAttribute('data-id')
+ prevBBox = bbox
+ continue
+ }
+ // No need to continue loop, if last element has been detected
+ break
+ }
+ setActiveSection(currentSectionId)
+ const index = tocIds.indexOf(currentSectionId) || 0
+ if (isBrowser()) {
+ document?.getElementById('toc-wrapper')?.scrollTo({ top: 28 * index, behavior: 'smooth' })
+ }
+ }, throttleMs))
+
+ // 无目录就直接返回空
+ if (!toc || toc.length < 1) {
+ return null
+ }
+
+ return <>
+
+
+
+ >
+}
+
+export default Catalog
diff --git a/themes/gitbook/components/CategoryGroup.js b/themes/gitbook/components/CategoryGroup.js
new file mode 100644
index 00000000..1516c038
--- /dev/null
+++ b/themes/gitbook/components/CategoryGroup.js
@@ -0,0 +1,19 @@
+import React from 'react'
+import CategoryItem from './CategoryItem'
+
+const CategoryGroup = ({ currentCategory, categoryOptions }) => {
+ if (!categoryOptions) {
+ return <>>
+ }
+ return
+
分类
+
+ {categoryOptions?.map(category => {
+ const selected = currentCategory === category.name
+ return
+ })}
+
+
+}
+
+export default CategoryGroup
diff --git a/themes/gitbook/components/CategoryItem.js b/themes/gitbook/components/CategoryItem.js
new file mode 100644
index 00000000..779488de
--- /dev/null
+++ b/themes/gitbook/components/CategoryItem.js
@@ -0,0 +1,18 @@
+import Link from 'next/link'
+
+export default function CategoryItem ({ selected, category, categoryCount }) {
+ return (
+
+
+ {category} {categoryCount && `(${categoryCount})`}
+
+
+
+ );
+}
diff --git a/themes/gitbook/components/Footer.js b/themes/gitbook/components/Footer.js
new file mode 100644
index 00000000..744f9e61
--- /dev/null
+++ b/themes/gitbook/components/Footer.js
@@ -0,0 +1,39 @@
+import React from 'react'
+import BLOG from '@/blog.config'
+import DarkModeButton from '@/components/DarkModeButton'
+
+const Footer = ({ title }) => {
+ const d = new Date()
+ const currentYear = d.getFullYear()
+ const copyrightDate = (function () {
+ if (Number.isInteger(BLOG.SINCE) && BLOG.SINCE < currentYear) {
+ return BLOG.SINCE + '-' + currentYear
+ }
+ return currentYear
+ })()
+
+ return (
+
+ )
+}
+
+export default Footer
diff --git a/themes/gitbook/components/InfoCard.js b/themes/gitbook/components/InfoCard.js
new file mode 100644
index 00000000..a18408dc
--- /dev/null
+++ b/themes/gitbook/components/InfoCard.js
@@ -0,0 +1,21 @@
+import BLOG from '@/blog.config'
+import Router from 'next/router'
+import React from 'react'
+import SocialButton from './SocialButton'
+
+const InfoCard = (props) => {
+ const { siteInfo } = props
+ return
+
+
{ Router.push('/about') }}>
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+
+
{BLOG.AUTHOR}
+
{BLOG.BIO}
+
+
+
+}
+
+export default InfoCard
diff --git a/themes/gitbook/components/JumpToTopButton.js b/themes/gitbook/components/JumpToTopButton.js
new file mode 100644
index 00000000..f066fcc7
--- /dev/null
+++ b/themes/gitbook/components/JumpToTopButton.js
@@ -0,0 +1,26 @@
+import { useGlobal } from '@/lib/global'
+import React from 'react'
+import CONFIG_GITBOOK from '../config_gitbook'
+
+/**
+ * 跳转到网页顶部
+ * 当屏幕下滑500像素后会出现该控件
+ * @param targetRef 关联高度的目标html标签
+ * @param showPercent 是否显示百分比
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const JumpToTopButton = ({ showPercent = false, percent, className }) => {
+ const { locale } = useGlobal()
+ if (!CONFIG_GITBOOK.WIDGET_TO_TOP) {
+ return <>>
+ }
+ return ( window.scrollTo({ top: 0, behavior: 'smooth' })} >
+
+
+
+ {showPercent && (
{percent}%
)}
+
)
+}
+
+export default JumpToTopButton
diff --git a/themes/gitbook/components/LeftMenuBar.js b/themes/gitbook/components/LeftMenuBar.js
new file mode 100644
index 00000000..e6636e19
--- /dev/null
+++ b/themes/gitbook/components/LeftMenuBar.js
@@ -0,0 +1,16 @@
+import Link from 'next/link'
+import React from 'react'
+
+export default function LeftMenuBar () {
+ return (
+
+ );
+}
diff --git a/themes/gitbook/components/LogoBar.js b/themes/gitbook/components/LogoBar.js
new file mode 100644
index 00000000..a7fceebd
--- /dev/null
+++ b/themes/gitbook/components/LogoBar.js
@@ -0,0 +1,14 @@
+import BLOG from '@/blog.config'
+import Link from 'next/link'
+
+export default function LogoBar(props) {
+ const { siteInfo } = props
+ return (
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

{siteInfo?.title}
+
+
+ )
+}
diff --git a/themes/gitbook/components/MenuBarMobile.js b/themes/gitbook/components/MenuBarMobile.js
new file mode 100644
index 00000000..544598e0
--- /dev/null
+++ b/themes/gitbook/components/MenuBarMobile.js
@@ -0,0 +1,39 @@
+import React from 'react'
+import { useGlobal } from '@/lib/global'
+import CONFIG_GITBOOK from '../config_gitbook'
+import BLOG from '@/blog.config'
+import { MenuItemCollapse } from './MenuItemCollapse'
+
+export const MenuBarMobile = (props) => {
+ const { customMenu, customNav } = props
+ const { locale } = useGlobal()
+
+ let links = [
+ // { name: locale.NAV.INDEX, to: '/' || '/', show: true },
+ { name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_GITBOOK.MENU_CATEGORY },
+ { name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_GITBOOK.MENU_TAG },
+ { name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG_GITBOOK.MENU_ARCHIVE }
+ // { name: locale.NAV.SEARCH, to: '/search', show: CONFIG_GITBOOK.MENU_SEARCH }
+ ]
+
+ if (customNav) {
+ links = links.concat(customNav)
+ }
+
+ // 如果 开启自定义菜单,则不再使用 Page生成菜单。
+ if (BLOG.CUSTOM_MENU) {
+ links = customMenu
+ }
+
+ if (!links || links.length === 0) {
+ return null
+ }
+
+ return (
+
+ )
+}
diff --git a/themes/gitbook/components/MenuItemCollapse.js b/themes/gitbook/components/MenuItemCollapse.js
new file mode 100644
index 00000000..539419eb
--- /dev/null
+++ b/themes/gitbook/components/MenuItemCollapse.js
@@ -0,0 +1,62 @@
+import Collapse from '@/components/Collapse'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useState } from 'react'
+
+/**
+ * 折叠菜单
+ * @param {*} param0
+ * @returns
+ */
+export const MenuItemCollapse = (props) => {
+ const { link } = props
+ const [show, changeShow] = useState(false)
+ const hasSubMenu = link?.subMenus?.length > 0
+
+ const [isOpen, changeIsOpen] = useState(false)
+
+ const router = useRouter()
+
+ if (!link || !link.show) {
+ return null
+ }
+
+ const selected = (router.pathname === link.to) || (router.asPath === link.to)
+
+ const toggleShow = () => {
+ changeShow(!show)
+ }
+
+ const toggleOpenSubMenu = () => {
+ changeIsOpen(!isOpen)
+ }
+
+ return <>
+
+
+ {!hasSubMenu &&
+
+ }
+
+ {hasSubMenu &&
}
+
+
+ {/* 折叠子菜单 */}
+ {hasSubMenu &&
+ {link?.subMenus?.map(sLink => {
+ return
+ })}
+ }
+ >
+}
diff --git a/themes/gitbook/components/MenuItemDrop.js b/themes/gitbook/components/MenuItemDrop.js
new file mode 100644
index 00000000..3ef482b5
--- /dev/null
+++ b/themes/gitbook/components/MenuItemDrop.js
@@ -0,0 +1,50 @@
+import Link from 'next/link'
+import { useState } from 'react'
+import { useRouter } from 'next/router'
+
+export const MenuItemDrop = ({ link }) => {
+ const [show, changeShow] = useState(false)
+ // const show = true
+ // const changeShow = () => {}
+ const router = useRouter()
+
+ if (!link || !link.show) {
+ return null
+ }
+ const hasSubMenu = link?.subMenus?.length > 0
+ const selected = (router.pathname === link.to) || (router.asPath === link.to)
+
+ return changeShow(true)} onMouseOut={() => changeShow(false)} >
+
+ {hasSubMenu &&
+
+
+ {link?.icon && } {link?.name}
+ {hasSubMenu && }
+
+
+ }
+
+ {!hasSubMenu &&
+
+
+ {link?.icon && } {link?.name}
+
+
+ }
+
+ {/* 子菜单 */}
+ {hasSubMenu &&
+ {link?.subMenus?.map(sLink => {
+ return -
+
+ {link?.icon && }{sLink.title}
+
+
+ })}
+
}
+
+
+}
diff --git a/themes/gitbook/components/MenuItemMobileNormal.js b/themes/gitbook/components/MenuItemMobileNormal.js
new file mode 100644
index 00000000..17f0d151
--- /dev/null
+++ b/themes/gitbook/components/MenuItemMobileNormal.js
@@ -0,0 +1,27 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+export const NormalMenu = props => {
+ const { link } = props
+ const router = useRouter()
+
+ if (!link || !link.show) {
+ return null
+ }
+
+ const selected = (router.pathname === link.to) || (router.asPath === link.to)
+
+ return
+
+
+ {link.slot}
+
+
+}
diff --git a/themes/gitbook/components/MenuItemPCNormal.js b/themes/gitbook/components/MenuItemPCNormal.js
new file mode 100644
index 00000000..809ae974
--- /dev/null
+++ b/themes/gitbook/components/MenuItemPCNormal.js
@@ -0,0 +1,24 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+export const MenuItemPCNormal = props => {
+ const { link } = props
+ const router = useRouter()
+ const selected = (router.pathname === link.to) || (router.asPath === link.to)
+ if (!link || !link.show) {
+ return null
+ }
+
+ return
+
+ {link.slot}
+
+}
diff --git a/themes/gitbook/components/PageNavDrawer.js b/themes/gitbook/components/PageNavDrawer.js
new file mode 100644
index 00000000..062629cc
--- /dev/null
+++ b/themes/gitbook/components/PageNavDrawer.js
@@ -0,0 +1,34 @@
+import { useMediumGlobal } from '../LayoutBase'
+import BlogPostListScroll from './BlogPostListScroll'
+
+/**
+ * 悬浮抽屉 页面内导航
+ * @param toc
+ * @param post
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const PageNavDrawer = ({ post, cRef }) => {
+ const { pageNavVisible, changePageNavVisible, filterPosts } = useMediumGlobal()
+ const switchVisible = () => {
+ changePageNavVisible(!pageNavVisible)
+ }
+
+ return <>
+
+ {/* 背景蒙版 */}
+
+ >
+}
+export default PageNavDrawer
diff --git a/themes/gitbook/components/PaginationSimple.js b/themes/gitbook/components/PaginationSimple.js
new file mode 100644
index 00000000..b48259ea
--- /dev/null
+++ b/themes/gitbook/components/PaginationSimple.js
@@ -0,0 +1,54 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useGlobal } from '@/lib/global'
+
+/**
+ * 简易翻页插件
+ * @param page 当前页码
+ * @param totalPage 是否有下一页
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const PaginationSimple = ({ page, totalPage }) => {
+ const { locale } = useGlobal()
+ const router = useRouter()
+ const currentPage = +page
+ const showNext = currentPage < totalPage
+ const pagePrefix = router.asPath.replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '')
+
+ return (
+
+
+ ←{locale.PAGINATION.PREV}
+
+
+
+
+ {locale.PAGINATION.NEXT}→
+
+
+ )
+}
+
+export default PaginationSimple
diff --git a/themes/gitbook/components/Progress.js b/themes/gitbook/components/Progress.js
new file mode 100644
index 00000000..48ba8e14
--- /dev/null
+++ b/themes/gitbook/components/Progress.js
@@ -0,0 +1,44 @@
+import React, { useEffect, useState } from 'react'
+import { isBrowser } from '@/lib/utils'
+
+/**
+ * 顶部页面阅读进度条
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const Progress = ({ targetRef, showPercent = true }) => {
+ const currentRef = targetRef?.current || targetRef
+ const [percent, changePercent] = useState(0)
+ const scrollListener = () => {
+ const target = currentRef || (isBrowser() && document.getElementById('container'))
+ if (target) {
+ const clientHeight = target.clientHeight
+ const scrollY = window.pageYOffset
+ const fullHeight = clientHeight - window.outerHeight
+ let per = parseFloat(((scrollY / fullHeight) * 100).toFixed(0))
+ if (per > 100) per = 100
+ if (per < 0) per = 0
+ changePercent(per)
+ }
+ }
+
+ useEffect(() => {
+ document.addEventListener('scroll', scrollListener)
+ return () => document.removeEventListener('scroll', scrollListener)
+ }, [])
+
+ return (
+
+
+ {showPercent && (
+
{percent}%
+ )}
+
+
+ )
+}
+
+export default Progress
diff --git a/themes/gitbook/components/RevolverMaps.js b/themes/gitbook/components/RevolverMaps.js
new file mode 100644
index 00000000..c6eb6252
--- /dev/null
+++ b/themes/gitbook/components/RevolverMaps.js
@@ -0,0 +1,36 @@
+import { useEffect, useState } from 'react'
+
+export default function RevolverMaps () {
+ const [load, changeLoad] = useState(false)
+ useEffect(() => {
+ if (!load) {
+ initRevolverMaps()
+ changeLoad(true)
+ }
+ }, [])
+ return
+}
+
+function initRevolverMaps () {
+ if (screen.width >= 768) {
+ Promise.all([
+ loadExternalResource('https://rf.revolvermaps.com/0/0/8.js?i=5jnp1havmh9&m=0&c=ff0000&cr1=ffffff&f=arial&l=33')
+ ]).then(() => {
+ console.log('地图加载完成')
+ })
+ }
+}
+
+// 封装异步加载资源的方法
+function loadExternalResource (url) {
+ return new Promise((resolve, reject) => {
+ const container = document.getElementById('revolvermaps')
+ const tag = document.createElement('script')
+ tag.src = url
+ if (tag) {
+ tag.onload = () => resolve(url)
+ tag.onerror = () => reject(url)
+ container.appendChild(tag)
+ }
+ })
+}
diff --git a/themes/gitbook/components/SearchInput.js b/themes/gitbook/components/SearchInput.js
new file mode 100644
index 00000000..6828d111
--- /dev/null
+++ b/themes/gitbook/components/SearchInput.js
@@ -0,0 +1,114 @@
+import { useImperativeHandle, useRef, useState } from 'react'
+import { useMediumGlobal } from '../LayoutBase'
+let lock = false
+
+const SearchInput = ({ currentSearch, cRef, className }) => {
+ const searchInputRef = useRef()
+ const { setFilterPosts, allNavPages } = useMediumGlobal()
+
+ useImperativeHandle(cRef, () => {
+ return {
+ focus: () => {
+ searchInputRef?.current?.focus()
+ }
+ }
+ })
+
+ const handleSearch = () => {
+ let keyword = searchInputRef.current.value
+ const filterPosts = []
+ if (keyword) {
+ keyword = keyword.trim()
+ } else {
+ setFilterPosts(allNavPages)
+ }
+ for (const post of allNavPages) {
+ const tagContent = post.tags && Array.isArray(post.tags) ? post.tags.join(' ') : ''
+ const categoryContent = post.category && Array.isArray(post.category) ? post.category.join(' ') : ''
+ const articleInfo = post.title + post.summary + tagContent + categoryContent
+ let hit = articleInfo.toLowerCase().indexOf(keyword) > -1
+ const indexContent = [post.summary]
+ // console.log('全文搜索缓存', cacheKey, page != null)
+ post.results = []
+ let hitCount = 0
+ for (const i in indexContent) {
+ const c = indexContent[i]
+ if (!c) {
+ continue
+ }
+ const index = c.toLowerCase().indexOf(keyword.toLowerCase())
+ if (index > -1) {
+ hit = true
+ hitCount += 1
+ post.results.push(c)
+ } else {
+ if ((post.results.length - 1) / hitCount < 3 || i === 0) {
+ post.results.push(c)
+ }
+ }
+ }
+ if (hit) {
+ filterPosts.push(post)
+ }
+ }
+ setFilterPosts(filterPosts)
+ }
+ const handleKeyUp = (e) => {
+ if (e.keyCode === 13) { // 回车
+ handleSearch(searchInputRef.current.value)
+ } else if (e.keyCode === 27) { // ESC
+ cleanSearch()
+ }
+ }
+ const cleanSearch = () => {
+ searchInputRef.current.value = ''
+ }
+
+ const [showClean, setShowClean] = useState(false)
+ const updateSearchKey = (val) => {
+ if (lock) {
+ return
+ }
+ searchInputRef.current.value = val
+
+ if (val) {
+ setShowClean(true)
+ } else {
+ setShowClean(false)
+ }
+ }
+ function lockSearchInput () {
+ lock = true
+ }
+
+ function unLockSearchInput () {
+ lock = false
+ }
+
+ return
+
updateSearchKey(e.target.value)}
+ defaultValue={currentSearch}
+ />
+
+
+
+
+
+ {(showClean &&
+
+
+
+ )}
+
+}
+
+export default SearchInput
diff --git a/themes/gitbook/components/SocialButton.js b/themes/gitbook/components/SocialButton.js
new file mode 100644
index 00000000..5f51033d
--- /dev/null
+++ b/themes/gitbook/components/SocialButton.js
@@ -0,0 +1,37 @@
+import BLOG from '@/blog.config'
+import React from 'react'
+
+/**
+ * 社交联系方式按钮组
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const SocialButton = () => {
+ return
+ {BLOG.CONTACT_GITHUB &&
+
+ }
+ {BLOG.CONTACT_TWITTER &&
+
+ }
+ {BLOG.CONTACT_TELEGRAM &&
+
+ }
+ {BLOG.CONTACT_LINKEDIN &&
+
+ }
+ {BLOG.CONTACT_WEIBO &&
+
+ }
+ {BLOG.CONTACT_INSTAGRAM &&
+
+ }
+ {BLOG.CONTACT_EMAIL &&
+
+ }
+ {BLOG.ENABLE_RSS &&
+
+ }
+
+}
+export default SocialButton
diff --git a/themes/gitbook/components/TagGroups.js b/themes/gitbook/components/TagGroups.js
new file mode 100644
index 00000000..390a6306
--- /dev/null
+++ b/themes/gitbook/components/TagGroups.js
@@ -0,0 +1,27 @@
+import TagItemMini from './TagItemMini'
+
+/**
+ * 标签组
+ * @param tags
+ * @param currentTag
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const TagGroups = ({ tagOptions, currentTag }) => {
+ if (!tagOptions) return <>>
+ return (
+
+ )
+}
+
+export default TagGroups
diff --git a/themes/gitbook/components/TagItemMini.js b/themes/gitbook/components/TagItemMini.js
new file mode 100644
index 00000000..9922a069
--- /dev/null
+++ b/themes/gitbook/components/TagItemMini.js
@@ -0,0 +1,21 @@
+import Link from 'next/link'
+
+const TagItemMini = ({ tag, selected = false }) => {
+ return (
+
+
+ {selected && } {tag.name + (tag.count ? `(${tag.count})` : '')}
+
+
+ )
+}
+
+export default TagItemMini
diff --git a/themes/gitbook/components/TocDrawer.js b/themes/gitbook/components/TocDrawer.js
new file mode 100644
index 00000000..b58adb44
--- /dev/null
+++ b/themes/gitbook/components/TocDrawer.js
@@ -0,0 +1,34 @@
+import Catalog from './Catalog'
+import { useMediumGlobal } from '../LayoutBase'
+
+/**
+ * 悬浮抽屉目录
+ * @param toc
+ * @param post
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const TocDrawer = ({ post, cRef }) => {
+ const { tocVisible, changeTocVisible } = useMediumGlobal()
+ const switchVisible = () => {
+ changeTocVisible(!tocVisible)
+ }
+ return <>
+
+ {/* 侧边菜单 */}
+
+ {post && <>
+
+
+
+ >}
+
+
+ {/* 背景蒙版 */}
+
+ >
+}
+export default TocDrawer
diff --git a/themes/gitbook/components/TopNavBar.js b/themes/gitbook/components/TopNavBar.js
new file mode 100644
index 00000000..c683e880
--- /dev/null
+++ b/themes/gitbook/components/TopNavBar.js
@@ -0,0 +1,78 @@
+import LogoBar from './LogoBar'
+import { useRef, useState } from 'react'
+import Collapse from '@/components/Collapse'
+import { MenuBarMobile } from './MenuBarMobile'
+import { useGlobal } from '@/lib/global'
+import CONFIG_GITBOOK from '../config_gitbook'
+import BLOG from '@/blog.config'
+import { MenuItemDrop } from './MenuItemDrop'
+import DarkModeButton from '@/components/DarkModeButton'
+
+/**
+ * 顶部导航栏 + 菜单
+ * @param {} param0
+ * @returns
+ */
+export default function TopNavBar(props) {
+ const { className, customNav, customMenu } = props
+ const [isOpen, changeShow] = useState(false)
+ const collapseRef = useRef(null)
+
+ const { locale } = useGlobal()
+
+ const defaultLinks = [
+ { icon: 'fas fa-th', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_GITBOOK.MENU_CATEGORY },
+ { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_GITBOOK.MENU_TAG },
+ { icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG_GITBOOK.MENU_ARCHIVE },
+ { icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG_GITBOOK.MENU_SEARCH }
+ ]
+
+ let links = defaultLinks.concat(customNav)
+
+ const toggleMenuOpen = () => {
+ changeShow(!isOpen)
+ }
+
+ // 如果 开启自定义菜单,则覆盖Page生成的菜单
+ if (BLOG.CUSTOM_MENU) {
+ links = customMenu
+ }
+
+ if (!links || links.length === 0) {
+ return null
+ }
+
+ return (
+
+
+ {/* 移动端折叠菜单 */}
+
+
+ collapseRef.current?.updateCollapseHeight(param)} />
+
+
+
+ {/* 导航栏菜单 */}
+
+
+ {/* 左侧图标Logo */}
+
+
+ {/* 折叠按钮、仅移动端显示 */}
+
+
+
+
+ {isOpen ? : }
+
+
+
+ {/* 桌面端顶部菜单 */}
+
+ {links && links?.map(link => )}
+
+
+
+
+ )
+}
diff --git a/themes/gitbook/config_gitbook.js b/themes/gitbook/config_gitbook.js
new file mode 100644
index 00000000..2c563632
--- /dev/null
+++ b/themes/gitbook/config_gitbook.js
@@ -0,0 +1,15 @@
+const CONFIG_GITBOOK = {
+
+ INDEX_PAGE: 'about', // 文档首页显示的文章,请确此路径包含在您的notion数据库中
+
+ // 菜单
+ MENU_CATEGORY: true, // 显示分类
+ MENU_TAG: true, // 显示标签
+ MENU_ARCHIVE: true, // 显示归档
+ MENU_SEARCH: true, // 显示搜索
+
+ // Widget
+ WIDGET_REVOLVER_MAPS: process.env.NEXT_PUBLIC_WIDGET_REVOLVER_MAPS || 'false', // 地图插件
+ WIDGET_TO_TOP: true // 跳回顶部
+}
+export default CONFIG_GITBOOK
diff --git a/themes/gitbook/index.js b/themes/gitbook/index.js
new file mode 100644
index 00000000..feffe394
--- /dev/null
+++ b/themes/gitbook/index.js
@@ -0,0 +1,25 @@
+import CONFIG_GITBOOK from './config_gitbook'
+import { LayoutIndex } from './LayoutIndex'
+import { LayoutSearch } from './LayoutSearch'
+import { LayoutArchive } from './LayoutArchive'
+import { LayoutSlug } from './LayoutSlug'
+import { Layout404 } from './Layout404'
+import { LayoutCategory } from './LayoutCategory'
+import { LayoutCategoryIndex } from './LayoutCategoryIndex'
+import { LayoutPage } from './LayoutPage'
+import { LayoutTag } from './LayoutTag'
+import { LayoutTagIndex } from './LayoutTagIndex'
+
+export {
+ CONFIG_GITBOOK as THEME_CONFIG,
+ LayoutIndex,
+ LayoutSearch,
+ LayoutArchive,
+ LayoutSlug,
+ Layout404,
+ LayoutCategory,
+ LayoutCategoryIndex,
+ LayoutPage,
+ LayoutTag,
+ LayoutTagIndex
+}
diff --git a/themes/medium/LayoutBase.js b/themes/medium/LayoutBase.js
index 9d3a9eff..a50a071b 100644
--- a/themes/medium/LayoutBase.js
+++ b/themes/medium/LayoutBase.js
@@ -12,6 +12,7 @@ import { useGlobal } from '@/lib/global'
import { useRouter } from 'next/router'
import Live2D from '@/components/Live2D'
import BLOG from '@/blog.config'
+import Announcement from './components/Announcement'
const ThemeGlobalMedium = createContext()
/**
@@ -21,7 +22,7 @@ const ThemeGlobalMedium = createContext()
* @constructor
*/
const LayoutBase = props => {
- const { children, meta, showInfoCard = true, slotRight, slotTop, siteInfo } = props
+ const { children, meta, showInfoCard = true, slotRight, slotTop, siteInfo, notice } = props
const { locale } = useGlobal()
const router = useRouter()
const [tocVisible, changeTocVisible] = useState(false)
@@ -43,7 +44,7 @@ const LayoutBase = props => {
{/* 桌面端左侧菜单 */}
{/* */}
-
+
{/* 顶部导航栏 */}
@@ -78,6 +79,7 @@ const LayoutBase = props => {
{CONFIG_MEDIUM.WIDGET_REVOLVER_MAPS === 'true' && }
+