diff --git a/.gitignore b/.gitignore
index 332741bc..1befa8c4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,6 +29,7 @@ yarn-error.log*
.env.development.local
.env.test.local
.env.production.local
+.env
# vercel
.vercel
diff --git a/lib/notion/getNotionData.js b/lib/notion/getNotionData.js
old mode 100644
new mode 100755
index 4d73bc99..f885f22b
--- a/lib/notion/getNotionData.js
+++ b/lib/notion/getNotionData.js
@@ -184,7 +184,7 @@ export function getNavPages({ allPages }) {
return post && post?.slug && (!post?.slug?.startsWith('http')) && post?.type === 'Post' && post?.status === 'Published'
})
- return allNavPages.map(item => ({ id: item.id, title: item.title || '', pageCoverThumbnail: item.pageCoverThumbnail || '', category: item.category || null, tags: item.tags || null, summary: item.summary || null, slug: item.slug, lastEditedDate: item.lastEditedDate }))
+ return allNavPages.map(item => ({ id: item.id, title: item.title || '', pageCoverThumbnail: item.pageCoverThumbnail || '', category: item.category || null, tags: item.tags || null, summary: item.summary || null, slug: item.slug, pageIcon: item.pageIcon || '', lastEditedDate: item.lastEditedDate }))
}
/**
diff --git a/themes/nav/components/Announcement.js b/themes/nav/components/Announcement.js
new file mode 100755
index 00000000..397d1aea
--- /dev/null
+++ b/themes/nav/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/nav/components/ArticleAround.js b/themes/nav/components/ArticleAround.js
new file mode 100755
index 00000000..95b6f83f
--- /dev/null
+++ b/themes/nav/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/nav/components/ArticleInfo.js b/themes/nav/components/ArticleInfo.js
new file mode 100755
index 00000000..b2d58efe
--- /dev/null
+++ b/themes/nav/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/nav/components/ArticleLock.js b/themes/nav/components/ArticleLock.js
new file mode 100755
index 00000000..4ba5a3b9
--- /dev/null
+++ b/themes/nav/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/nav/components/BlogArchiveItem.js b/themes/nav/components/BlogArchiveItem.js
new file mode 100755
index 00000000..8e9693dc
--- /dev/null
+++ b/themes/nav/components/BlogArchiveItem.js
@@ -0,0 +1,36 @@
+import BLOG from '@/blog.config'
+import Link from 'next/link'
+
+/**
+ * 归档分组
+ * @param {*} param0
+ * @returns
+ */
+export default function BlogArchiveItem({ archiveTitle, archivePosts }) {
+ return (
+
+ )
+}
diff --git a/themes/nav/components/BlogPostCard.js b/themes/nav/components/BlogPostCard.js
new file mode 100755
index 00000000..0b64cb40
--- /dev/null
+++ b/themes/nav/components/BlogPostCard.js
@@ -0,0 +1,49 @@
+import BLOG from '@/blog.config'
+import Link from 'next/link'
+import NotionIcon from './NotionIcon'
+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}
+
{post.summary ? post.summary : '暂无简介'}
+
+
+
+
+ )
+ function removeHttp(str) {
+ // 检查字符串是否包含http
+ if (str.includes("http")) {
+ // 如果包含,找到http的位置
+ let index = str.indexOf("http");
+ // 返回http之后的部分
+ return str.slice(index, str.length);
+ } else {
+ // 如果不包含,返回原字符串
+ return str;
+ }
+ }
+ function checkRemoveHttp(str) {
+ // 检查字符串是否包含http
+ if (str.includes("http")) {
+ // 如果包含,找到http的位置
+ let index = str.indexOf("http");
+ // 包含
+ return true;
+ } else {
+ // 不包含
+ return false;
+ }
+ }
+}
+
+export default BlogPostCard
diff --git a/themes/nav/components/BlogPostItem.js b/themes/nav/components/BlogPostItem.js
new file mode 100755
index 00000000..7500e8ff
--- /dev/null
+++ b/themes/nav/components/BlogPostItem.js
@@ -0,0 +1,49 @@
+import BlogPostCard from './BlogPostCard'
+import React, { useState } from 'react'
+import NotionIcon from './NotionIcon'
+// import Collapse from '@/components/Collapse'
+
+/**
+ * 导航列表
+ * @param posts 所有文章
+ * @param tags 所有标签
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const BlogPostItem = (props) => {
+ const { group, filterLinks } = props
+// const [isOpen, changeIsOpen] = useState(group?.selected)
+
+// const toggleOpenSubMenu = () => {
+// changeIsOpen(!isOpen)
+// }
+
+ console.log('####### group')
+ console.log(group)
+
+ if (group?.category) {
+ return <>
+
+
{group?.category}
+
+
+ {group?.items?.map(post => (
+
+ ))}
+
+ >
+ } else {
+ return <>
+
+ 未分类
+
+
+ {group?.items?.map(post => (
+
+ ))}
+
+ >
+ }
+}
+
+export default BlogPostItem
diff --git a/themes/nav/components/BlogPostListAll.js b/themes/nav/components/BlogPostListAll.js
new file mode 100755
index 00000000..458c790b
--- /dev/null
+++ b/themes/nav/components/BlogPostListAll.js
@@ -0,0 +1,139 @@
+import BlogPostListEmpty from './BlogPostListEmpty'
+import { useRouter } from 'next/router'
+import BlogPostItem from './BlogPostItem'
+import { useNavGlobal } from '@/themes/nav'
+import CONFIG from '../config'
+import { deepClone } from '@/lib/utils'
+import { useEffect, useState, createContext, useContext } from 'react'
+
+/**
+ * 博客列表滚动分页
+ * @param posts 所有文章
+ * @param tags 所有标签
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const BlogPostListAll = (props) => {
+ // const { customMenu, posts, category, tag, allNavPages, categoryOptions } = props
+ // const [filteredNavPages, setFilteredNavPages] = useState(allNavPages)
+ const { customMenu } = props
+
+ // const [filteredNavPages, setFilteredNavPages] = useState(allNavPages)
+ const { filteredNavPages, setFilteredNavPages, allNavPages } = useNavGlobal()
+ // const [filteredNavPages] = useState(allNavPages)
+
+ // const router = useRouter()
+ // 对自定义分类格式化,方便后续使用分类名称做索引,检索同步图标信息
+ // 目前只支持二级分类
+ let links = customMenu
+ let filterLinks = {}
+ // for循环遍历数组
+ links?.map((link, i) => {
+ let linkTitle = link.title + ''
+ // console.log('####### link')
+ // console.log(link)
+ // filterLinks[linkTitle] = link
+ filterLinks[linkTitle] = { title: link.title, icon: link.icon, pageIcon: link.pageIcon }
+ if(link?.subMenus){
+ link.subMenus?.map((group, index) => {
+ let subMenuTitle = group?.title + ''
+ // 自定义分类图标与post的category共用
+ // 判断自定义分类与Post中category同名的项,将icon的值传递给post
+ // filterLinks[subMenuTitle] = group
+ filterLinks[subMenuTitle] = { title: group.title, icon: group.icon, pageIcon: group.pageIcon }
+ })
+ }
+ })
+
+ console.log('####### filterLinks')
+ console.log(filterLinks)
+
+
+ // console.log('####### filterLinks')
+ // console.log(filterLinks)
+
+ let selectedSth = false
+ const groupedArray = filteredNavPages?.reduce((groups, item) => {
+ let categoryName = item?.category ? item?.category : '' // 将category转换为字符串
+ let categoryIcon = filterLinks[categoryName]?.icon ? filterLinks[categoryName]?.icon : '' // 将pageIcon转换为字符串
+
+ // console.log('####### categoryName')
+ // console.log(categoryName)
+ // console.log('####### categoryIcon')
+ // console.log(categoryIcon)
+
+ let existingGroup = null
+ // 开启自动分组排序
+ if (JSON.parse(CONFIG.AUTO_SORT)) {
+ existingGroup = groups.find(group => group.category === categoryName) // 搜索同名的最后一个分组
+ } else {
+ existingGroup = groups[groups.length - 1] // 获取最后一个分组
+ }
+
+ // 添加数据
+ if (existingGroup && existingGroup.category === categoryName) {
+ existingGroup.items.push(item)
+ } else {
+ groups.push({ category: categoryName, icon: categoryIcon, items: [item] })
+ }
+ return groups
+ }, [])
+
+ // 处理是否选中
+ groupedArray?.map((group) => {
+ // 自定义分类图标与post的category共用
+ // 判断自定义分类与Post中category同名的项,将icon的值传递给post
+ // let groupTitle = group?.category
+ // item.icon = filterLinks[categoryName]?.icon ? filterLinks[categoryName]?.icon : ''
+ // console.log('####### item')
+ // console.log(item)
+ let groupSelected = false
+ // for (const post of group?.items) {
+ // if (router.asPath.split('?')[0] === '/' + post.slug) {
+ // groupSelected = true
+ // selectedSth = true
+ // }
+ // }
+ group.selected = groupSelected
+ return null
+ })
+
+ // 如果都没有选中默认打开第一个
+ if (!selectedSth && groupedArray && groupedArray?.length > 0) {
+ groupedArray[0].selected = true
+ }
+
+ if (!groupedArray || groupedArray.length === 0) {
+ return
+ } else {
+ return
+ {/* 文章列表 */}
+ {groupedArray?.map((group, index) => )}
+
+ }
+
+ // 处理自定义导航菜单项
+ // let keyword = searchInputRef.current.value
+ // if (keyword) {
+ // keyword = keyword.trim()
+ // } else {
+ // setFilteredNavPages(allNavPages)
+ // }
+ // for (const filterGroup of filterAllNavPages) {
+ // for (let i = filterGroup.items.length - 1; i >= 0; i--) {
+ // const post = filterGroup.items[i]
+ // const articleInfo = post.title + ''
+ // const hit = articleInfo.toLowerCase().indexOf(keyword.toLowerCase()) > -1
+ // if (!hit) {
+ // // 删除
+ // filterGroup.items.splice(i, 1)
+ // }
+ // }
+ // if (filterGroup.items && filterGroup.items.length > 0) {
+ // filterPosts.push(filterGroup)
+ // }
+ // }
+
+}
+
+export default BlogPostListAll
diff --git a/themes/nav/components/BlogPostListEmpty.js b/themes/nav/components/BlogPostListEmpty.js
new file mode 100755
index 00000000..86977fd0
--- /dev/null
+++ b/themes/nav/components/BlogPostListEmpty.js
@@ -0,0 +1,12 @@
+
+/**
+ * 空白博客 列表
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const BlogPostListEmpty = ({ currentSearch }) => {
+ return
+
没有找到文章 {(currentSearch &&
{currentSearch}
)}
+
+}
+export default BlogPostListEmpty
diff --git a/themes/nav/components/BlogPostListPage.js b/themes/nav/components/BlogPostListPage.js
new file mode 100755
index 00000000..d9228b51
--- /dev/null
+++ b/themes/nav/components/BlogPostListPage.js
@@ -0,0 +1,34 @@
+import BlogPostCard from './BlogPostCard'
+import BLOG from '@/blog.config'
+import NavPostListEmpty from './NavPostListEmpty'
+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/nav/components/BottomMenuBar.js b/themes/nav/components/BottomMenuBar.js
new file mode 100755
index 00000000..84a13f04
--- /dev/null
+++ b/themes/nav/components/BottomMenuBar.js
@@ -0,0 +1,24 @@
+import { useNavGlobal } from '@/themes/nav'
+import React from 'react'
+import JumpToTopButton from './JumpToTopButton'
+
+export default function BottomMenuBar({ post, className }) {
+ const { pageNavVisible, changePageNavVisible } = useNavGlobal()
+
+ const togglePageNavVisible = () => {
+ changePageNavVisible(!pageNavVisible)
+ }
+
+ return (
+
+ )
+}
diff --git a/themes/nav/components/Card.js b/themes/nav/components/Card.js
new file mode 100755
index 00000000..d24c046e
--- /dev/null
+++ b/themes/nav/components/Card.js
@@ -0,0 +1,9 @@
+const Card = ({ children, headerSlot, className }) => {
+ return
+ <>{headerSlot}>
+
+
+}
+export default Card
diff --git a/themes/nav/components/Catalog.js b/themes/nav/components/Catalog.js
new file mode 100755
index 00000000..ebee78ae
--- /dev/null
+++ b/themes/nav/components/Catalog.js
@@ -0,0 +1,89 @@
+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 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 = null
+ 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 tocIds = post?.toc?.map((t) => uuidToId(t.id)) || []
+ const index = tocIds.indexOf(currentSectionId) || 0
+ if (isBrowser && tocIds?.length > 0) {
+ for (const tocWrapper of document?.getElementsByClassName('toc-wrapper')) {
+ tocWrapper?.scrollTo({ top: 28 * index, behavior: 'smooth' })
+ }
+ }
+ }, throttleMs))
+
+ // 无目录就直接返回空
+ if (!toc || toc.length < 1) {
+ return null
+ }
+
+ return <>
+
+
+
+ >
+}
+
+export default Catalog
diff --git a/themes/nav/components/CategoryGroup.js b/themes/nav/components/CategoryGroup.js
new file mode 100755
index 00000000..1516c038
--- /dev/null
+++ b/themes/nav/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/nav/components/CategoryItem.js b/themes/nav/components/CategoryItem.js
new file mode 100755
index 00000000..779488de
--- /dev/null
+++ b/themes/nav/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/nav/components/Collapse.js b/themes/nav/components/Collapse.js
new file mode 100755
index 00000000..caf86152
--- /dev/null
+++ b/themes/nav/components/Collapse.js
@@ -0,0 +1,94 @@
+import React, { useEffect, useImperativeHandle } from 'react'
+
+/**
+ * 折叠面板组件,支持水平折叠、垂直折叠
+ * @param {type:['horizontal','vertical'],isOpen} props
+ * @returns
+ */
+const Collapse = props => {
+ const { collapseRef } = props
+ const ref = React.useRef(null)
+ const type = props.type || 'vertical'
+
+ useImperativeHandle(collapseRef, () => {
+ return {
+ /**
+ * 当子元素高度变化时,可调用此方法更新折叠组件的高度
+ * @param {*} param0
+ */
+ updateCollapseHeight: ({ height, increase }) => {
+ ref.current.style.height = ref.current.scrollHeight
+ ref.current.style.height = 'auto'
+ }
+ }
+ })
+
+ /**
+ * 折叠
+ * @param {*} element
+ */
+ const collapseSection = element => {
+ const sectionHeight = element.scrollHeight
+ const sectionWidth = element.scrollWidth
+
+ requestAnimationFrame(function () {
+ switch (type) {
+ case 'horizontal':
+ element.style.width = sectionWidth + 'px'
+ requestAnimationFrame(function () {
+ element.style.width = 0 + 'px'
+ })
+ break
+ case 'vertical':
+ element.style.height = sectionHeight + 'px'
+ requestAnimationFrame(function () {
+ element.style.height = 0 + 'px'
+ })
+ }
+ })
+ }
+
+ /**
+ * 展开
+ * @param {*} element
+ */
+ const expandSection = element => {
+ const sectionHeight = element.scrollHeight + 8
+ const sectionWidth = element.scrollWidth
+ let clearTime = 0
+ switch (type) {
+ case 'horizontal':
+ element.style.width = sectionWidth + 'px'
+ clearTime = setTimeout(() => {
+ element.style.width = 'auto'
+ }, 400)
+ break
+ case 'vertical':
+ element.style.height = sectionHeight + 'px'
+ clearTime = setTimeout(() => {
+ element.style.height = 'auto'
+ }, 400)
+ }
+
+ clearTimeout(clearTime)
+ }
+
+ useEffect(() => {
+ if (props.isOpen) {
+ expandSection(ref.current)
+ } else {
+ collapseSection(ref.current)
+ }
+ // 通知父组件高度变化
+ props?.onHeightChange && props.onHeightChange({ height: ref.current.scrollHeight, increase: props.isOpen })
+ }, [props.isOpen])
+
+ return (
+
+ {props.children}
+
+ )
+}
+Collapse.defaultProps = { isOpen: false }
+
+export default Collapse
diff --git a/themes/nav/components/FloatTocButton.js b/themes/nav/components/FloatTocButton.js
new file mode 100755
index 00000000..d4ff317d
--- /dev/null
+++ b/themes/nav/components/FloatTocButton.js
@@ -0,0 +1,25 @@
+import { useNavGlobal } from '@/themes/nav'
+
+/**
+ * 移动端悬浮目录按钮
+ */
+export default function FloatTocButton () {
+ const { tocVisible, changeTocVisible } = useNavGlobal()
+
+ const toggleToc = () => {
+ changeTocVisible(!tocVisible)
+ }
+
+ return (
+
+ )
+}
diff --git a/themes/nav/components/Footer.js b/themes/nav/components/Footer.js
new file mode 100755
index 00000000..4df06b6d
--- /dev/null
+++ b/themes/nav/components/Footer.js
@@ -0,0 +1,39 @@
+import React from 'react'
+import BLOG from '@/blog.config'
+
+const Footer = ({ siteInfo }) => {
+ 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/nav/components/InfoCard.js b/themes/nav/components/InfoCard.js
new file mode 100755
index 00000000..13fe31c8
--- /dev/null
+++ b/themes/nav/components/InfoCard.js
@@ -0,0 +1,21 @@
+import BLOG from '@/blog.config'
+import LazyImage from '@/components/LazyImage'
+import Router from 'next/router'
+import React from 'react'
+import SocialButton from './SocialButton'
+
+const InfoCard = (props) => {
+ const { siteInfo } = props
+ return
+
+
{ Router.push('/about') }}>
+
+
+
{BLOG.AUTHOR}
+
{BLOG.BIO}
+
+
+
+}
+
+export default InfoCard
diff --git a/themes/nav/components/JumpToTopButton.js b/themes/nav/components/JumpToTopButton.js
new file mode 100755
index 00000000..7252c9d6
--- /dev/null
+++ b/themes/nav/components/JumpToTopButton.js
@@ -0,0 +1,24 @@
+
+/**
+ * 跳转到网页顶部
+ * 当屏幕下滑500像素后会出现该控件
+ * @param targetRef 关联高度的目标html标签
+ * @param showPercent 是否显示百分比
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const JumpToTopButton = ({ showPercent = false, percent, className }) => {
+ return (
+
+ { window.scrollTo({ top: 0, behavior: 'smooth' }) }} />
+
+ )
+}
+
+export default JumpToTopButton
diff --git a/themes/nav/components/LeftMenuBar.js b/themes/nav/components/LeftMenuBar.js
new file mode 100755
index 00000000..e6636e19
--- /dev/null
+++ b/themes/nav/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/nav/components/LoadingCover.js b/themes/nav/components/LoadingCover.js
new file mode 100755
index 00000000..f74757ef
--- /dev/null
+++ b/themes/nav/components/LoadingCover.js
@@ -0,0 +1,7 @@
+export default function LoadingCover() {
+ return
+}
diff --git a/themes/nav/components/LogoBar.js b/themes/nav/components/LogoBar.js
new file mode 100755
index 00000000..771992bf
--- /dev/null
+++ b/themes/nav/components/LogoBar.js
@@ -0,0 +1,32 @@
+import BLOG from '@/blog.config'
+import LazyImage from '@/components/LazyImage'
+import { useNavGlobal } from '@/themes/nav'
+import Link from 'next/link'
+import CONFIG from '../config'
+
+/**
+ * Logo区域
+ * @param {*} props
+ * @returns
+ */
+export default function LogoBar(props) {
+ const { siteInfo } = props
+ const { pageNavVisible, changePageNavVisible } = useNavGlobal()
+
+ const togglePageNavVisible = () => {
+ changePageNavVisible(!pageNavVisible)
+ }
+ return (
+
+ )
+}
diff --git a/themes/nav/components/MenuBarMobile.js b/themes/nav/components/MenuBarMobile.js
new file mode 100755
index 00000000..2db19133
--- /dev/null
+++ b/themes/nav/components/MenuBarMobile.js
@@ -0,0 +1,39 @@
+import React from 'react'
+import { useGlobal } from '@/lib/global'
+import CONFIG from '../config'
+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.MENU_CATEGORY },
+ { name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG },
+ { name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE }
+ // { name: locale.NAV.SEARCH, to: '/search', show: CONFIG.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/nav/components/MenuItem.js b/themes/nav/components/MenuItem.js
new file mode 100644
index 00000000..60c57bab
--- /dev/null
+++ b/themes/nav/components/MenuItem.js
@@ -0,0 +1,84 @@
+import Link from 'next/link'
+import { useState } from 'react'
+import { useRouter } from 'next/router'
+import Collapse from './Collapse'
+
+export const MenuItem = ({ 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)
+
+ link.selected = true
+
+// const { group } = props
+ const [isOpen, changeIsOpen] = useState(link?.selected)
+
+ const toggleOpenSubMenu = () => {
+ changeIsOpen(!isOpen)
+ }
+ console.log('link::')
+ console.log(link)
+
+ return <>
+
+
{link?.icon && }{link?.name}
+
+
+
+ {link?.subMenus?.map((sLink, index) => (
+
+ {/*
*/}
+
+ {sLink.title}
+
+
+ ))
+ }git fetch origin
+
+ >
+
+
+
+// return changeShow(true)} onMouseOut={() => changeShow(false)} >
+
+// {hasSubMenu &&
+//
+//
+// {link?.icon && } {link?.name}
+// {hasSubMenu && }
+//
+//
+// }
+
+// {!hasSubMenu &&
+//
+//
+// {link?.icon && } {link?.name}
+//
+//
+// }
+
+// {/* 子菜单 */}
+// {hasSubMenu &&
+// {link?.subMenus?.map((sLink, index) => {
+// return -
+//
+// {link?.icon && }{sLink.title}
+//
+//
+// })}
+//
}
+
+//
+}
diff --git a/themes/nav/components/MenuItemCollapse.js b/themes/nav/components/MenuItemCollapse.js
new file mode 100755
index 00000000..4bbf09e4
--- /dev/null
+++ b/themes/nav/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, index) => {
+ return
+ })}
+ }
+ >
+}
diff --git a/themes/nav/components/MenuItemDrop.js b/themes/nav/components/MenuItemDrop.js
new file mode 100755
index 00000000..c3636ba0
--- /dev/null
+++ b/themes/nav/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, index) => {
+ return -
+
+ {link?.icon && }{sLink.title}
+
+
+ })}
+
}
+
+
+}
diff --git a/themes/nav/components/MenuItemMobileNormal.js b/themes/nav/components/MenuItemMobileNormal.js
new file mode 100755
index 00000000..17f0d151
--- /dev/null
+++ b/themes/nav/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/nav/components/MenuItemPCNormal.js b/themes/nav/components/MenuItemPCNormal.js
new file mode 100755
index 00000000..809ae974
--- /dev/null
+++ b/themes/nav/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/nav/components/NavPostItem.js b/themes/nav/components/NavPostItem.js
new file mode 100755
index 00000000..ed869d69
--- /dev/null
+++ b/themes/nav/components/NavPostItem.js
@@ -0,0 +1,45 @@
+import BlogPostCard from './BlogPostCard'
+import React, { useState } from 'react'
+import Collapse from '@/components/Collapse'
+
+/**
+ * 导航列表
+ * @param posts 所有文章
+ * @param tags 所有标签
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const NavPostItem = (props) => {
+ const { group } = props
+ const [isOpen, changeIsOpen] = useState(group?.selected)
+
+ const toggleOpenSubMenu = () => {
+ changeIsOpen(!isOpen)
+ }
+ console.log('group::')
+ console.log(group)
+
+ if (group?.category) {
+ return <>
+
+
+ {group?.items?.map(post => (
+
))
+ }
+
+ >
+ } else {
+ return <>
+ {group?.items?.map(post => (
+
))
+ }
+ >
+ }
+}
+
+export default NavPostItem
diff --git a/themes/nav/components/NavPostList.js b/themes/nav/components/NavPostList.js
new file mode 100755
index 00000000..0f06a008
--- /dev/null
+++ b/themes/nav/components/NavPostList.js
@@ -0,0 +1,102 @@
+import NavPostListEmpty from './NavPostListEmpty'
+import { useRouter } from 'next/router'
+import NavPostItem from './NavPostItem'
+import CONFIG from '../config'
+import Link from 'next/link'
+
+/**
+ * 博客列表滚动分页
+ * @param posts 所有文章
+ * @param tags 所有标签
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const NavPostList = (props) => {
+ const { customMenu, categoryOptions } = props
+ // let groupedArray = categoryOptions
+ // const { filteredNavPages, categoryOptions, categories } = props
+ // const router = useRouter()
+ // let selectedSth = false
+
+ // let groupedArray = categoryOptions?.map(item) => {
+ // // let groups = [];
+ // groupedArray.push({ category: item.name, id: item.id, count: item.count, selected: false,items: [] })
+ // return groups
+ // })
+
+ // const groupedArray = categoryOptions?.reduce((groups, item) => {
+ // const categoryName = item?.name ? item?.name : '' // 将category转换为字符串
+ // // let existingGroup = null
+ // console.log('categoryOptions => item::')
+ // console.log(item)
+ // // 添加数据
+ // groups.push({ category: item.name, id: item.id, count: item.count, selected: false, items: [] })
+ // return groups
+ // }, [])
+
+ // 处理是否选中
+ // groupedArray?.map((group) => {
+ // let groupSelected = false
+ // for (const post of group?.items) {
+ // if (router.asPath.split('?')[0] === '/' + post.slug) {
+ // groupSelected = true
+ // selectedSth = true
+ // }
+ // }
+ // group.selected = groupSelected
+ // return null
+ // })
+
+ // 如果都没有选中默认打开第一个
+ // if (!selectedSth && groupedArray && groupedArray?.length > 0) {
+ // groupedArray[0].selected = true
+ // }
+
+
+
+ // console.log('groupedArray::')
+ // console.log(groupedArray)
+
+ // 如果 开启自定义菜单,则覆盖Page生成的菜单
+ // if (BLOG.CUSTOM_MENU) {
+ // links = customMenu
+ // }
+ let links = customMenu
+ return
+ {links && links?.map((link, index) => )}
+
+
+ console.log('categoryOptions::')
+ console.log(categoryOptions)
+ if (!categoryOptions) {
+ return
+ } else {
+ return
+
+
+ {categoryOptions.map(category => {
+ // const selected = currentCategory === category.name
+ let selected = false
+ return (
+
+
+
{category.name}({category.count})
+
+
+ )
+ })}
+
+ }
+
+
+
+}
+
+export default NavPostList
diff --git a/themes/nav/components/NavPostListEmpty.js b/themes/nav/components/NavPostListEmpty.js
new file mode 100755
index 00000000..207599db
--- /dev/null
+++ b/themes/nav/components/NavPostListEmpty.js
@@ -0,0 +1,12 @@
+
+/**
+ * 空白博客 列表
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const NavPostListEmpty = ({ currentSearch }) => {
+ return
+
没有找到文章 {(currentSearch &&
{currentSearch}
)}
+
+}
+export default NavPostListEmpty
diff --git a/themes/nav/components/NotionIcon.js b/themes/nav/components/NotionIcon.js
new file mode 100755
index 00000000..45fd2d76
--- /dev/null
+++ b/themes/nav/components/NotionIcon.js
@@ -0,0 +1,22 @@
+import LazyImage from '@/components/LazyImage'
+
+/**
+ * notion的图标icon
+ * 可能是emoji 可能是 svg 也可能是 图片
+ * @returns
+ */
+const NotionIcon = ({ icon }) => {
+ let imgSize = 8
+ let fontSize = ''
+ if (!icon) {
+ return <>>
+ }
+ fontSize = (Math.round(imgSize / 2) - 1) > 0 ? (Math.round(imgSize / 2) - 1) : ''
+ if (icon.startsWith('http') || icon.startsWith('data:')) {
+ return
+ }
+
+ return {icon}
+}
+
+export default NotionIcon
diff --git a/themes/nav/components/PageNavDrawer.js b/themes/nav/components/PageNavDrawer.js
new file mode 100755
index 00000000..dbf953e0
--- /dev/null
+++ b/themes/nav/components/PageNavDrawer.js
@@ -0,0 +1,36 @@
+import { useNavGlobal } from '@/themes/nav'
+import NavPostList from './NavPostList'
+
+/**
+ * 悬浮抽屉 页面内导航
+ * @param toc
+ * @param post
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const PageNavDrawer = (props) => {
+ const { pageNavVisible, changePageNavVisible } = useNavGlobal()
+ const { filteredNavPages } = props
+
+ const switchVisible = () => {
+ changePageNavVisible(!pageNavVisible)
+ }
+
+ return <>
+
+ {/* 背景蒙版 */}
+
+ >
+}
+export default PageNavDrawer
diff --git a/themes/nav/components/PaginationSimple.js b/themes/nav/components/PaginationSimple.js
new file mode 100755
index 00000000..b48259ea
--- /dev/null
+++ b/themes/nav/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/nav/components/Progress.js b/themes/nav/components/Progress.js
new file mode 100755
index 00000000..669d09a8
--- /dev/null
+++ b/themes/nav/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('posts-wrapper'))
+ 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/nav/components/RevolverMaps.js b/themes/nav/components/RevolverMaps.js
new file mode 100755
index 00000000..c6eb6252
--- /dev/null
+++ b/themes/nav/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/nav/components/SearchInput.js b/themes/nav/components/SearchInput.js
new file mode 100755
index 00000000..45dfc4e9
--- /dev/null
+++ b/themes/nav/components/SearchInput.js
@@ -0,0 +1,123 @@
+import { useImperativeHandle, useRef, useState } from 'react'
+import { deepClone } from '@/lib/utils'
+import { useNavGlobal } from '@/themes/nav'
+let lock = false
+
+const SearchInput = ({ currentSearch, cRef, className }) => {
+ const searchInputRef = useRef()
+ const { setFilteredNavPages, allNavPages } = useNavGlobal()
+ const [filteredNavPages] = useState(allNavPages)
+
+ useImperativeHandle(cRef, () => {
+ return {
+ focus: () => {
+ searchInputRef?.current?.focus()
+ }
+ }
+ })
+
+ const handleSearch = () => {
+ let keyword = searchInputRef.current.value
+ if (keyword) {
+ keyword = keyword.trim()
+ } else {
+ setFilteredNavPages(allNavPages)
+ }
+ const filterAllNavPages = deepClone(allNavPages)
+ // for (const filterGroup of filterAllNavPages) {
+ // for (let i = filterGroup.items.length - 1; i >= 0; i--) {
+ // const post = filterGroup.items[i]
+ // const articleInfo = post.title + ''
+ // const hit = articleInfo.toLowerCase().indexOf(keyword.toLowerCase()) > -1
+ // if (!hit) {
+ // // 删除
+ // filterGroup.items.splice(i, 1)
+ // }
+ // }
+ // if (filterGroup.items && filterGroup.items.length > 0) {
+ // filterPosts.push(filterGroup)
+ // }
+ // }
+ for (let i = filterAllNavPages.length - 1; i >= 0; i--) {
+ const post = filterAllNavPages[i]
+ const articleInfo = post.title + ''
+ const hit = articleInfo.toLowerCase().indexOf(keyword.toLowerCase()) > -1
+ if (!hit) {
+ // 删除
+ filterAllNavPages.splice(i, 1)
+ }
+ }
+
+ // 更新完
+ setFilteredNavPages(filterAllNavPages)
+ }
+
+ /**
+ * 回车键
+ * @param {*} e
+ */
+ const handleKeyUp = (e) => {
+ if (e.keyCode === 13) { // 回车
+ handleSearch(searchInputRef.current.value)
+ } else if (e.keyCode === 27) { // ESC
+ cleanSearch()
+ }
+ }
+
+ /**
+ * 清理搜索
+ */
+ const cleanSearch = () => {
+ searchInputRef.current.value = ''
+ handleSearch()
+ setShowClean(false)
+ }
+
+ 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/nav/components/SocialButton.js b/themes/nav/components/SocialButton.js
new file mode 100755
index 00000000..164a71fc
--- /dev/null
+++ b/themes/nav/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 &&
+
+ }
+ {JSON.parse(BLOG.ENABLE_RSS) &&
+
+ }
+
+}
+export default SocialButton
diff --git a/themes/nav/components/TagGroups.js b/themes/nav/components/TagGroups.js
new file mode 100755
index 00000000..390a6306
--- /dev/null
+++ b/themes/nav/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/nav/components/TagItemMini.js b/themes/nav/components/TagItemMini.js
new file mode 100755
index 00000000..9922a069
--- /dev/null
+++ b/themes/nav/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/nav/components/TocDrawer.js b/themes/nav/components/TocDrawer.js
new file mode 100755
index 00000000..2d6a75d1
--- /dev/null
+++ b/themes/nav/components/TocDrawer.js
@@ -0,0 +1,34 @@
+import { useNavGlobal } from '@/themes/nav'
+import Catalog from './Catalog'
+
+/**
+ * 悬浮抽屉目录
+ * @param toc
+ * @param post
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const TocDrawer = ({ post, cRef }) => {
+ const { tocVisible, changeTocVisible } = useNavGlobal()
+ const switchVisible = () => {
+ changeTocVisible(!tocVisible)
+ }
+ return <>
+
+ {/* 侧边菜单 */}
+
+ {post && <>
+
+
+
+ >}
+
+
+ {/* 背景蒙版 */}
+
+ >
+}
+export default TocDrawer
diff --git a/themes/nav/components/TopNavBar.js b/themes/nav/components/TopNavBar.js
new file mode 100755
index 00000000..11325fab
--- /dev/null
+++ b/themes/nav/components/TopNavBar.js
@@ -0,0 +1,115 @@
+import LogoBar from './LogoBar'
+import { useCallback, useEffect, useRef, useState } from 'react'
+import Collapse from '@/components/Collapse'
+import { MenuBarMobile } from './MenuBarMobile'
+import { useGlobal } from '@/lib/global'
+import CONFIG from '../config'
+import BLOG from '@/blog.config'
+import throttle from 'lodash.throttle'
+import { useRouter } from 'next/router'
+import { MenuItemDrop } from './MenuItemDrop'
+import SearchInput from './SearchInput'
+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()
+
+
+ let windowTop = 0
+
+ // 监听滚动
+ useEffect(() => {
+ scrollTrigger()
+ window.addEventListener('scroll', scrollTrigger)
+ return () => {
+ window.removeEventListener('scroll', scrollTrigger)
+ }
+ }, [])
+
+ const throttleMs = 200
+
+ const scrollTrigger = useCallback(throttle(() => {
+ const scrollS = window.scrollY
+ const nav = document.querySelector('#nav-bg')
+ // const header = document.querySelector('#top-nav')
+ const header = document.querySelector('#container-inner')
+ const showNav = scrollS <= windowTop || scrollS < 5 || (scrollS <= header.clientHeight)// 非首页无大图时影藏顶部 滚动条置顶时隐藏
+ if (!showNav) {
+ nav && nav.classList.replace('-top-20', 'top-0')
+ windowTop = scrollS
+ } else {
+ nav && nav.classList.replace('top-0', '-top-20')
+ windowTop = scrollS
+ }
+ }, throttleMs)
+ )
+
+
+
+ const defaultLinks = [
+ { icon: 'fas fa-th', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY },
+ { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG },
+ { icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE },
+ { icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH }
+ ]
+
+ let links = defaultLinks.concat(customNav)
+
+ const toggleMenuOpen = () => {
+ changeShow(!isOpen)
+ }
+
+ // 如果 开启自定义菜单,则覆盖Page生成的菜单
+ if (BLOG.CUSTOM_MENU) {
+ links = customMenu
+ }
+
+ return (
+
+
+ {/* 移动端折叠菜单 */}
+
+
+ collapseRef.current?.updateCollapseHeight(param)} />
+
+
+
+ {/* 导航栏菜单 */}
+
+
+ {/* 左侧图标Logo */}
+ {/*
+
+
*/}
+
+ {/* 搜索框、折叠按钮、仅移动端显示 */}
+
+
+
+
+
+
+ {isOpen ? : }
+
+
+
+ {/* 桌面端顶部菜单 */}
+
+ {/* {links && links?.map((link, index) => )} */}
+
+
+
+
+
+
+ )
+}
diff --git a/themes/nav/config.js b/themes/nav/config.js
new file mode 100755
index 00000000..29a97c24
--- /dev/null
+++ b/themes/nav/config.js
@@ -0,0 +1,20 @@
+const CONFIG = {
+
+ INDEX_PAGE: 'about', // 文档首页显示的文章,请确此路径包含在您的notion数据库中
+
+ AUTO_SORT: process.env.NEXT_PUBLIC_GITBOOK_AUTO_SORT || true, // 是否自动按分类名 归组排序文章;自动归组可能会打乱您Notion中的文章顺序
+
+ SHOW_TITLE_TEXT: false, // 标题栏显示文本
+ USE_CUSTEM_MENU: 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
diff --git a/themes/nav/index.js b/themes/nav/index.js
new file mode 100755
index 00000000..3e7e3db0
--- /dev/null
+++ b/themes/nav/index.js
@@ -0,0 +1,512 @@
+'use client'
+
+import CONFIG from './config'
+import { useRouter } from 'next/router'
+import { useEffect, useState, createContext, useContext } from 'react'
+import { isBrowser } from '@/lib/utils'
+import Footer from './components/Footer'
+import InfoCard from './components/InfoCard'
+import RevolverMaps from './components/RevolverMaps'
+import TopNavBar from './components/TopNavBar'
+import SearchInput from './components/SearchInput'
+import { useGlobal } from '@/lib/global'
+import Live2D from '@/components/Live2D'
+import BLOG from '@/blog.config'
+import NavPostList from './components/NavPostList'
+import ArticleInfo from './components/ArticleInfo'
+import Catalog from './components/Catalog'
+import Announcement from './components/Announcement'
+import PageNavDrawer from './components/PageNavDrawer'
+import FloatTocButton from './components/FloatTocButton'
+import { AdSlot } from '@/components/GoogleAdsense'
+import JumpToTopButton from './components/JumpToTopButton'
+import ShareBar from '@/components/ShareBar'
+import CategoryItem from './components/CategoryItem'
+import TagItemMini from './components/TagItemMini'
+import ArticleAround from './components/ArticleAround'
+import Comment from '@/components/Comment'
+import TocDrawer from './components/TocDrawer'
+import NotionPage from '@/components/NotionPage'
+import { ArticleLock } from './components/ArticleLock'
+import { Transition } from '@headlessui/react'
+import { Style } from './style'
+import CommonHead from '@/components/CommonHead'
+import BlogArchiveItem from './components/BlogArchiveItem'
+import BlogPostListAll from './components/BlogPostListAll'
+import BlogPostListPage from './components/BlogPostListPage'
+import BlogPostCard from './components/BlogPostCard'
+import LogoBar from './components/LogoBar'
+import Link from 'next/link'
+import dynamic from 'next/dynamic'
+const WWAds = dynamic(() => import('@/components/WWAds'), { ssr: false })
+
+import { MenuItem } from './components/MenuItem'
+
+// 主题全局变量
+const ThemeGlobalNav = createContext()
+export const useNavGlobal = () => useContext(ThemeGlobalNav)
+
+/**
+ * 基础布局
+ * 采用左右两侧布局,移动端使用顶部导航栏
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const LayoutBase = (props) => {
+ const { customMenu, children, post, allNavPages, categoryOptions, slotLeft, slotRight, slotTop, meta } = props
+ const { onLoading } = useGlobal()
+ const router = useRouter()
+ const [tocVisible, changeTocVisible] = useState(false)
+ const [pageNavVisible, changePageNavVisible] = useState(false)
+ const [filteredNavPages, setFilteredNavPages] = useState(allNavPages)
+
+ const showTocButton = post?.toc?.length > 1
+
+ useEffect(() => {
+ setFilteredNavPages(allNavPages)
+ }, [post])
+
+ let links = customMenu
+ // let categoryOptions = filteredNavPages
+
+ return (
+
+
+
+
+
+
+ {/* 顶部导航栏 */}
+
+
a
+
+
+
+ {/* 左侧图标Logo */}
+
+
+
+ {/* 左侧推拉抽屉 */}
+
+
+
+ {slotLeft}
+
+ {/* 所有文章列表 */}
+ {CONFIG.USE_CUSTEM_MENU && links && links?.map((link, index) =>
)}
+ {/* {!CONFIG.USE_CUSTEM_MENU &&
} */}
+ {/* {links && links?.map((link, index) => {
+
+ })} */}
+
+ {/* href={`/category/${category.name}`} */}
+ {!CONFIG.USE_CUSTEM_MENU && categoryOptions && categoryOptions?.map(category => {
+ // let selected = currentCategory === category.name
+ let selected = false;
+ return (
+
+
+
{category.name}({category.count})
+
+
+ )
+ })}
+
+
+
+
+
+
+
+
+
+
+
+
+ {slotTop}
+
+
+
+ {children}
+
+
+ {/* Google广告 */}
+
+
+
+ {/* 回顶按钮 */}
+
+
+
+ {/* 底部 */}
+
+
+
+
+
+ {/* 右侧侧推拉抽屉 */}
+ {/*
+
+
+
+
+
+ {slotRight}
+ {router.route === '/' && <>
+
+ {CONFIG.WIDGET_REVOLVER_MAPS === 'true' &&
}
+
+ >}*/}
+ {/* onenav主题首页只显示公告 */}
+ {/*
+
+
+
+
+
+
+
*/}
+
+
+
+ {/* 移动端悬浮目录按钮 */}
+ {showTocButton && !tocVisible &&
+
+
}
+
+ {/* 移动端导航抽屉 */}
+
+
+ {/* 移动端底部导航栏 */}
+ {/*
*/}
+
+
+
+ )
+}
+
+
+/**
+ * 首页
+ * @param {*} props
+ * @returns 此主题首页就是列表
+ */
+const LayoutIndex = props => {
+ return
+}
+
+/**
+ * 首页列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutPostListIndex = props => {
+ // const { customMenu, children, post, allNavPages, categoryOptions, slotLeft, slotRight, slotTop, meta } = props
+ // const [filteredNavPages, setFilteredNavPages] = useState(allNavPages)
+ return (
+
+
+
+
+ )
+}
+
+/**
+ * 首页
+ * @param {*} props
+ * @returns 此主题首页就是列表
+ */
+const LayoutIndexNew = props => {
+ return
+}
+
+/**
+ * 文章列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutPostList = props => {
+ const { posts, category, tag } = props
+ // 顶部如果是按照分类或标签查看文章列表,列表顶部嵌入一个横幅
+ // 如果是搜索,则列表顶部嵌入 搜索框
+ return (
+
+
+
+ {posts?.map(post => (
+
+ ))}
+
+
+
+ )
+}
+
+/**
+ * 首页
+ * 重定向到某个文章详情页
+ * @param {*} props
+ * @returns
+ */
+const LayoutIndexCustemPage = (props) => {
+ const router = useRouter()
+ useEffect(() => {
+ router.push(CONFIG.INDEX_PAGE).then(() => {
+ // console.log('跳转到指定首页', CONFIG.INDEX_PAGE)
+ setTimeout(() => {
+ if (isBrowser) {
+ const article = document.getElementById('notion-article')
+ if (!article) {
+ console.log('请检查您的Notion数据库中是否包含此slug页面: ', CONFIG.INDEX_PAGE)
+ const containerInner = document.querySelector('#theme-onenav #container-inner')
+ const newHTML = `配置有误
请在您的notion中添加一个slug为${CONFIG.INDEX_PAGE}的文章
`
+ containerInner?.insertAdjacentHTML('afterbegin', newHTML)
+ }
+ }
+ }, 7 * 1000)
+ })
+ }, [])
+
+ return
+}
+
+/**
+ * 文章列表 无
+ * 全靠页面导航
+ * @param {*} props
+ * @returns
+ */
+const LayoutPostListOld = (props) => {
+ return
+
+
+}
+
+/**
+ * 文章详情
+ * @param {*} props
+ * @returns
+ */
+const LayoutSlug = (props) => {
+ const { post, prev, next, lock, validPassword } = props
+
+ return (
+
+ {/* 文章锁 */}
+ {lock && }
+
+ {!lock &&
+
+ {/* title */}
+
{post?.title}
+
+ {/* Notion文章主体 */}
+ {post && (
+
+
+ {/* 分享 */}
+ {/* */}
+ {/* 文章分类和标签信息 */}
+
+ {CONFIG.POST_DETAIL_CATEGORY && post?.category &&
}
+
+ {CONFIG.POST_DETAIL_TAG && post?.tagItems?.map(tag => )}
+
+
+
+ {/* 上一篇、下一篇文章 */}
+ {/* {post?.type === 'Post' && } */}
+
+
+
+
+
+ )}
+
+
+
}
+
+ )
+}
+
+/**
+ * 没有搜索
+ * 全靠页面导航
+ * @param {*} props
+ * @returns
+ */
+const LayoutSearch = (props) => {
+ return
+}
+
+/**
+ * 归档页面基本不会用到
+ * 全靠页面导航
+ * @param {*} props
+ * @returns
+ */
+const LayoutArchive = (props) => {
+ const { archivePosts } = props
+
+ return
+
+ {Object.keys(archivePosts)?.map(archiveTitle => )}
+
+
+}
+
+/**
+ * 404
+ */
+const Layout404 = props => {
+ return
+ 404 Not found.
+
+}
+
+/**
+ * 分类列表
+ */
+const LayoutCategoryIndex = (props) => {
+ const { categoryOptions } = props
+ const { locale } = useGlobal()
+ return
+
+
+ {locale.COMMON.CATEGORY}:
+
+
+ {categoryOptions?.map(category => {
+ return (
+
+
+ {category.name}({category.count})
+
+
+ )
+ })}
+
+
+
+}
+
+/**
+ * 标签列表
+ */
+const LayoutTagIndex = (props) => {
+ const { tagOptions } = props
+ const { locale } = useGlobal()
+
+ return
+
+
+
+ {locale.COMMON.TAGS}:
+
+
+
+
+}
+
+export {
+ CONFIG as THEME_CONFIG,
+ LayoutIndex,
+ LayoutSearch,
+ LayoutArchive,
+ LayoutSlug,
+ Layout404,
+ LayoutCategoryIndex,
+ LayoutPostList,
+ LayoutTagIndex
+}
diff --git a/themes/nav/style.js b/themes/nav/style.js
new file mode 100755
index 00000000..1630a910
--- /dev/null
+++ b/themes/nav/style.js
@@ -0,0 +1,104 @@
+/* eslint-disable react/no-unknown-property */
+/**
+ * 此处样式只对当前主题生效
+ * 此处不支持tailwindCSS的 @apply 语法
+ * @returns
+ */
+const Style = () => {
+ return
+}
+
+export { Style }