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..09cb681c
--- /dev/null
+++ b/themes/gitbook/LayoutBase.js
@@ -0,0 +1,93 @@
+import CommonHead from '@/components/CommonHead'
+import { useState, createContext, useContext } from 'react'
+import Footer from './components/Footer'
+import InfoCard from './components/InfoCard'
+import RevolverMaps from './components/RevolverMaps'
+import CONFIG_MEDIUM from './config_medium'
+import Tabs from '@/components/Tabs'
+import TopNavBar from './components/TopNavBar'
+import SearchInput from './components/SearchInput'
+import BottomMenuBar from './components/BottomMenuBar'
+import { useGlobal } from '@/lib/global'
+import { useRouter } from 'next/router'
+import Live2D from '@/components/Live2D'
+import BLOG from '@/blog.config'
+const ThemeGlobalMedium = createContext()
+
+/**
+ * 基础布局 采用左右两侧布局,移动端使用顶部导航栏
+
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const LayoutBase = props => {
+ const { children, meta, showInfoCard = true, slotLeft, slotTop, siteInfo } = props
+ const { locale } = useGlobal()
+ const router = useRouter()
+ const [tocVisible, changeTocVisible] = useState(false)
+ const { onLoading } = useGlobal()
+
+ const LoadingCover =
+
+ return (
+
+
+
+
+
+
+
+ {/* 左侧推拉抽屉 */}
+
+
+
+ {slotLeft}
+
+ {router.pathname !== '/search' && }
+ {showInfoCard && }
+ {CONFIG_MEDIUM.WIDGET_REVOLVER_MAPS === 'true' && }
+
+
+
+
+
+
+
+ {/* 顶部导航栏 */}
+
+
+
+ {slotTop}
+
+ {onLoading ? LoadingCover : children}
+
+ {/* 回顶按钮 */}
+
+ { window.scrollTo({ top: 0, behavior: 'smooth' }) }} />
+
+
+
+ {/* 底部 */}
+
+
+
+
+
+ {/* 移动端底部导航栏 */}
+
+
+
+ )
+}
+
+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..41410467
--- /dev/null
+++ b/themes/gitbook/LayoutIndex.js
@@ -0,0 +1,12 @@
+import BLOG from '@/blog.config'
+import BlogPostListPage from './components/BlogPostListPage'
+import BlogPostListScroll from './components/BlogPostListScroll'
+import LayoutBase from './LayoutBase'
+
+export const LayoutIndex = (props) => {
+ return
+ {BLOG.POST_LIST_STYLE === 'page' ? : }
+
+}
+
+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..1aaf3d20
--- /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)
+ })
+ 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..68308ab7
--- /dev/null
+++ b/themes/gitbook/LayoutSlug.js
@@ -0,0 +1,97 @@
+import LayoutBase from './LayoutBase'
+import { useGlobal } from '@/lib/global'
+import React from 'react'
+import Catalog from './components/Catalog'
+import { ArticleLock } from './components/ArticleLock'
+import formatDate from '@/lib/formatDate'
+import BLOG from '@/blog.config'
+import Link from 'next/link'
+import NotionPage from '@/components/NotionPage'
+import CONFIG_MEDIUM from './config_medium'
+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, siteInfo, lock, validPassword } = props
+ const { locale } = useGlobal()
+
+ const date = formatDate(
+ post?.date?.start_date || post?.createdTime,
+ locale.LOCALE
+ )
+ if (!post) {
+ return
+ }
+
+ const slotLeft = post?.toc && post?.toc?.length > 3 && (
+
+
+ {/* */}
+
+ )
+
+ return (
+
+ {/* 文章锁 */}
+ {lock && }
+
+ {!lock &&
+
+ {/* title */}
+
{post?.title}
+
+ {/* meta */}
+
+
+
{date}
+
|
+
{post.lastEditedTime}
+
+
+
+
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+
+
+ {BLOG.AUTHOR}
+
+
+
+
+
+ {/* Notion文章主体 */}
+
+
+
+
+ {/* 分享 */}
+
+ {/* 文章分类和标签信息 */}
+
+ {CONFIG_MEDIUM.POST_DETAIL_CATEGORY && post.category &&
}
+
+ {CONFIG_MEDIUM.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/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/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..998dd086
--- /dev/null
+++ b/themes/gitbook/components/BlogPostCard.js
@@ -0,0 +1,83 @@
+import BLOG from '@/blog.config'
+import NotionPage from '@/components/NotionPage'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import React from 'react'
+import CONFIG_MEDIUM from '../config_medium'
+import CategoryItem from './CategoryItem'
+import TagItemMini from './TagItemMini'
+import TwikooCommentCount from '@/components/TwikooCommentCount'
+
+const BlogPostCard = ({ post, showSummary }) => {
+ const showPreview = CONFIG_MEDIUM.POST_LIST_PREVIEW && post.blockMap
+ const { locale } = useGlobal()
+ return (
+
+
+
+
+
+ {CONFIG_MEDIUM.POST_LIST_COVER &&
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+
}
+ {post.title}
+
+
+
+
+
+
{post.date?.start_date}
+ {CONFIG_MEDIUM.POST_LIST_CATEGORY &&
}
+ {CONFIG_MEDIUM.POST_LIST_TAG && post?.tagItems?.map(tag =>
)}
+
+
+
+
+
+ {(!showPreview || showSummary) && (
+
+ {post.summary}
+
+ )}
+
+ {showPreview && (
+
+
+
+
+
+
+ {locale.COMMON.ARTICLE_DETAIL}
+
+
+
+
+
+
+ )}
+
+
+ )
+}
+
+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..2c7eafbd
--- /dev/null
+++ b/themes/gitbook/components/BlogPostListScroll.js
@@ -0,0 +1,106 @@
+import BLOG from '@/blog.config'
+import BlogPostCard from './BlogPostCard'
+import BlogPostListEmpty from './BlogPostListEmpty'
+import { useGlobal } from '@/lib/global'
+import throttle from 'lodash.throttle'
+import React, { useCallback, useEffect, useRef, useState } from 'react'
+import { useRouter } from 'next/router'
+
+/**
+ * 博客列表滚动分页
+ * @param posts 所有文章
+ * @param tags 所有标签
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const BlogPostListScroll = ({ posts = [], currentSearch }) => {
+ const postsPerPage = BLOG.POSTS_PER_PAGE
+ const [page, updatePage] = useState(1)
+ let filteredPosts = Object.assign(posts)
+ const searchKey = getSearchKey()
+ if (searchKey) {
+ filteredPosts = posts.filter(post => {
+ const tagContent = post.tags ? post.tags.join(' ') : ''
+ const searchContent = post.title + post.summary + tagContent
+ return searchContent.toLowerCase().includes(searchKey.toLowerCase())
+ })
+ }
+ const postsToShow = getPostByPage(page, filteredPosts, postsPerPage)
+
+ let hasMore = false
+ if (filteredPosts) {
+ const totalCount = filteredPosts.length
+ hasMore = page * postsPerPage < totalCount
+ }
+
+ const handleGetMore = () => {
+ if (!hasMore) return
+ updatePage(page + 1)
+ }
+
+ // 监听滚动自动分页加载
+ const scrollTrigger = useCallback(throttle(() => {
+ const scrollS = window.scrollY + window.outerHeight
+ const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
+ if (scrollS > clientHeight + 100) {
+ handleGetMore()
+ }
+ }, 500))
+
+ // 监听滚动
+ useEffect(() => {
+ window.addEventListener('scroll', scrollTrigger)
+ return () => {
+ window.removeEventListener('scroll', scrollTrigger)
+ }
+ })
+
+ const targetRef = useRef(null)
+ const { locale } = useGlobal()
+
+ if (!postsToShow || postsToShow.length === 0) {
+ return
+ } else {
+ return
+
+ {/* 文章列表 */}
+
+ {postsToShow?.map(post => (
+
+ ))}
+
+
+
+
{
+ handleGetMore()
+ }}
+ className='w-full my-4 py-4 text-center cursor-pointer dark:text-gray-200'
+ > {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}
+
+
+ }
+}
+
+/**
+ * 获取从第1页到指定页码的文章
+ * @param page 第几页
+ * @param totalPosts 所有文章
+ * @param postsPerPage 每页文章数量
+ * @returns {*}
+ */
+const getPostByPage = function (page, totalPosts, postsPerPage) {
+ return totalPosts.slice(
+ 0,
+ postsPerPage * page
+ )
+}
+
+function getSearchKey() {
+ const router = useRouter()
+ if (router.query && router.query.s) {
+ return router.query.s
+ }
+ return null
+}
+
+export default BlogPostListScroll
diff --git a/themes/gitbook/components/BottomMenuBar.js b/themes/gitbook/components/BottomMenuBar.js
new file mode 100644
index 00000000..45215f50
--- /dev/null
+++ b/themes/gitbook/components/BottomMenuBar.js
@@ -0,0 +1,36 @@
+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 } = useMediumGlobal()
+ const showTocBotton = post?.toc?.length > 0
+
+ const toggleToc = () => {
+ changeTocVisible(!tocVisible)
+ }
+
+ 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..c9592dd2
--- /dev/null
+++ b/themes/gitbook/components/Catalog.js
@@ -0,0 +1,91 @@
+import { useCallback, useEffect, useRef, useState } from 'react'
+import throttle from 'lodash.throttle'
+import { uuidToId } from 'notion-utils'
+import Progress from './Progress'
+
+/**
+ * 目录导航组件
+ * @param toc
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const Catalog = ({ toc }) => {
+ const tocIds = []
+
+ // 目录自动滚动
+ const tRef = useRef(null)
+ // 同步选中目录事件
+ const [activeSection, setActiveSection] = useState(null)
+
+ // 监听滚动事件
+ useEffect(() => {
+ window.addEventListener('scroll', actionSectionScrollSpy)
+ actionSectionScrollSpy()
+ return () => {
+ window.removeEventListener('scroll', actionSectionScrollSpy)
+ }
+ }, [])
+
+ 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
+ tRef?.current?.scrollTo({ top: 28 * index, behavior: 'smooth' })
+ }, throttleMs))
+
+ // 无目录就直接返回空
+ if (!toc || toc.length < 1) {
+ return <>>
+ }
+
+ 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..b9d4f49c
--- /dev/null
+++ b/themes/gitbook/components/Footer.js
@@ -0,0 +1,35 @@
+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..2d83338c
--- /dev/null
+++ b/themes/gitbook/components/JumpToTopButton.js
@@ -0,0 +1,26 @@
+import { useGlobal } from '@/lib/global'
+import React from 'react'
+import CONFIG_MEDIUM from '../config_medium'
+
+/**
+ * 跳转到网页顶部
+ * 当屏幕下滑500像素后会出现该控件
+ * @param targetRef 关联高度的目标html标签
+ * @param showPercent 是否显示百分比
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const JumpToTopButton = ({ showPercent = false, percent, className }) => {
+ const { locale } = useGlobal()
+ if (!CONFIG_MEDIUM.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..cbe59474
--- /dev/null
+++ b/themes/gitbook/components/LogoBar.js
@@ -0,0 +1,12 @@
+import Link from 'next/link'
+
+export default function LogoBar (props) {
+ const { siteInfo } = props
+ return (
+
+
+ {siteInfo?.title}
+
+
+ );
+}
diff --git a/themes/gitbook/components/MenuBarMobile.js b/themes/gitbook/components/MenuBarMobile.js
new file mode 100644
index 00000000..538a9782
--- /dev/null
+++ b/themes/gitbook/components/MenuBarMobile.js
@@ -0,0 +1,39 @@
+import React from 'react'
+import { useGlobal } from '@/lib/global'
+import CONFIG_MEDIUM from '../config_medium'
+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_MEDIUM.MENU_CATEGORY },
+ { name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_MEDIUM.MENU_TAG },
+ { name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG_MEDIUM.MENU_ARCHIVE }
+ // { name: locale.NAV.SEARCH, to: '/search', show: CONFIG_MEDIUM.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..e9d1b9f3
--- /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/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..d839d85d
--- /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..f6c84d9c
--- /dev/null
+++ b/themes/gitbook/components/SearchInput.js
@@ -0,0 +1,86 @@
+import { useRouter } from 'next/router'
+import { useImperativeHandle, useRef, useState } from 'react'
+let lock = false
+
+const SearchInput = ({ currentTag, currentSearch, cRef, className }) => {
+ const [onLoading, setLoadingState] = useState(false)
+ const router = useRouter()
+ const searchInputRef = useRef()
+ useImperativeHandle(cRef, () => {
+ return {
+ focus: () => {
+ searchInputRef?.current?.focus()
+ }
+ }
+ })
+
+ const handleSearch = () => {
+ const key = searchInputRef.current.value
+
+ if (key && key !== '') {
+ setLoadingState(true)
+ location.href = '/search/' + key
+ } else {
+ router.push({ pathname: '/' }).then(r => {
+ })
+ }
+ }
+ 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..9a9270db
--- /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..b4e01345
--- /dev/null
+++ b/themes/gitbook/components/TopNavBar.js
@@ -0,0 +1,74 @@
+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_MEDIUM from '../config_medium'
+import BLOG from '@/blog.config'
+import { MenuItemDrop } from './MenuItemDrop'
+
+/**
+ * 顶部导航栏 + 菜单
+ * @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_MEDIUM.MENU_CATEGORY },
+ { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_MEDIUM.MENU_TAG },
+ { icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG_MEDIUM.MENU_ARCHIVE },
+ { icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG_MEDIUM.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 */}
+
+
+ {/* 折叠按钮、仅移动端显示 */}
+
+
+ {/* 桌面端顶部菜单 */}
+
+ {links && links?.map(link => )}
+
+
+
+ )
+}
diff --git a/themes/gitbook/config_medium.js b/themes/gitbook/config_medium.js
new file mode 100644
index 00000000..40f41c97
--- /dev/null
+++ b/themes/gitbook/config_medium.js
@@ -0,0 +1,24 @@
+const CONFIG_MEDIUM = {
+
+ // Style
+ RIGHT_PANEL_DARK: process.env.NEXT_PUBLIC_MEDIUM_RIGHT_DARK || false, // 右侧面板深色模式
+
+ POST_LIST_COVER: true, // 文章列表显示图片封面
+ POST_LIST_PREVIEW: true, // 列表显示文章预览
+ POST_LIST_CATEGORY: true, // 列表显示文章分类
+ POST_LIST_TAG: true, // 列表显示文章标签
+
+ POST_DETAIL_CATEGORY: true, // 文章显示分类
+ POST_DETAIL_TAG: true, // 文章显示标签
+
+ // 菜单
+ 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_MEDIUM
diff --git a/themes/gitbook/index.js b/themes/gitbook/index.js
new file mode 100644
index 00000000..81961c58
--- /dev/null
+++ b/themes/gitbook/index.js
@@ -0,0 +1,25 @@
+import CONFIG_MEDIUM from './config_medium'
+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_MEDIUM 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..19948d25 100644
--- a/themes/medium/LayoutBase.js
+++ b/themes/medium/LayoutBase.js
@@ -21,7 +21,7 @@ const ThemeGlobalMedium = createContext()
* @constructor
*/
const LayoutBase = props => {
- const { children, meta, showInfoCard = true, slotRight, slotTop, siteInfo } = props
+ const { children, meta, showInfoCard = true, slotLeft, slotTop, siteInfo } = props
const { locale } = useGlobal()
const router = useRouter()
const [tocVisible, changeTocVisible] = useState(false)
@@ -71,7 +71,7 @@ const LayoutBase = props => {
- {slotRight}
+ {slotLeft}
{router.pathname !== '/search' &&
}
{showInfoCard &&
}
diff --git a/themes/medium/LayoutSlug.js b/themes/medium/LayoutSlug.js
index 9217f281..68308ab7 100644
--- a/themes/medium/LayoutSlug.js
+++ b/themes/medium/LayoutSlug.js
@@ -28,7 +28,7 @@ export const LayoutSlug = props => {
/>
}
- const slotRight = post?.toc && post?.toc?.length > 3 && (
+ const slotLeft = post?.toc && post?.toc?.length > 3 && (
{/*
*/}
@@ -36,7 +36,7 @@ export const LayoutSlug = props => {
)
return (
-
+
{/* 文章锁 */}
{lock && }
diff --git a/themes/theme.js b/themes/theme.js
index f0db2291..0214c1bc 100644
--- a/themes/theme.js
+++ b/themes/theme.js
@@ -8,7 +8,7 @@ import * as ThemeComponents from '@theme-components'
* 所有主题枚举
*/
export const ALL_THEME = [
- 'hexo', 'matery', 'next', 'medium', 'fukasawa', 'nobelium', 'example', 'simple'
+ 'hexo', 'matery', 'next', 'medium', 'fukasawa', 'nobelium', 'example', 'simple', 'gitbook'
]
/**