From a4d9007a2a4ce91ddee634af4d2e1b068510c69f Mon Sep 17 00:00:00 2001 From: emengweb Date: Fri, 13 Oct 2023 03:14:03 +0000 Subject: [PATCH] add theme nav & add pageIcon support of @lib/getNotionData --- .gitignore | 1 + lib/notion/getNotionData.js | 2 +- themes/nav/components/Announcement.js | 21 + themes/nav/components/ArticleAround.js | 32 ++ themes/nav/components/ArticleInfo.js | 9 + themes/nav/components/ArticleLock.js | 53 ++ themes/nav/components/BlogArchiveItem.js | 36 ++ themes/nav/components/BlogPostCard.js | 49 ++ themes/nav/components/BlogPostItem.js | 49 ++ themes/nav/components/BlogPostListAll.js | 139 +++++ themes/nav/components/BlogPostListEmpty.js | 12 + themes/nav/components/BlogPostListPage.js | 34 ++ themes/nav/components/BottomMenuBar.js | 24 + themes/nav/components/Card.js | 9 + themes/nav/components/Catalog.js | 89 +++ themes/nav/components/CategoryGroup.js | 19 + themes/nav/components/CategoryItem.js | 18 + themes/nav/components/Collapse.js | 94 ++++ themes/nav/components/FloatTocButton.js | 25 + themes/nav/components/Footer.js | 39 ++ themes/nav/components/InfoCard.js | 21 + themes/nav/components/JumpToTopButton.js | 24 + themes/nav/components/LeftMenuBar.js | 16 + themes/nav/components/LoadingCover.js | 7 + themes/nav/components/LogoBar.js | 32 ++ themes/nav/components/MenuBarMobile.js | 39 ++ themes/nav/components/MenuItem.js | 84 +++ themes/nav/components/MenuItemCollapse.js | 62 +++ themes/nav/components/MenuItemDrop.js | 50 ++ themes/nav/components/MenuItemMobileNormal.js | 27 + themes/nav/components/MenuItemPCNormal.js | 24 + themes/nav/components/NavPostItem.js | 45 ++ themes/nav/components/NavPostList.js | 102 ++++ themes/nav/components/NavPostListEmpty.js | 12 + themes/nav/components/NotionIcon.js | 22 + themes/nav/components/PageNavDrawer.js | 36 ++ themes/nav/components/PaginationSimple.js | 54 ++ themes/nav/components/Progress.js | 44 ++ themes/nav/components/RevolverMaps.js | 36 ++ themes/nav/components/SearchInput.js | 123 +++++ themes/nav/components/SocialButton.js | 37 ++ themes/nav/components/TagGroups.js | 27 + themes/nav/components/TagItemMini.js | 21 + themes/nav/components/TocDrawer.js | 34 ++ themes/nav/components/TopNavBar.js | 115 ++++ themes/nav/config.js | 20 + themes/nav/index.js | 512 ++++++++++++++++++ themes/nav/style.js | 104 ++++ 48 files changed, 2483 insertions(+), 1 deletion(-) mode change 100644 => 100755 lib/notion/getNotionData.js create mode 100755 themes/nav/components/Announcement.js create mode 100755 themes/nav/components/ArticleAround.js create mode 100755 themes/nav/components/ArticleInfo.js create mode 100755 themes/nav/components/ArticleLock.js create mode 100755 themes/nav/components/BlogArchiveItem.js create mode 100755 themes/nav/components/BlogPostCard.js create mode 100755 themes/nav/components/BlogPostItem.js create mode 100755 themes/nav/components/BlogPostListAll.js create mode 100755 themes/nav/components/BlogPostListEmpty.js create mode 100755 themes/nav/components/BlogPostListPage.js create mode 100755 themes/nav/components/BottomMenuBar.js create mode 100755 themes/nav/components/Card.js create mode 100755 themes/nav/components/Catalog.js create mode 100755 themes/nav/components/CategoryGroup.js create mode 100755 themes/nav/components/CategoryItem.js create mode 100755 themes/nav/components/Collapse.js create mode 100755 themes/nav/components/FloatTocButton.js create mode 100755 themes/nav/components/Footer.js create mode 100755 themes/nav/components/InfoCard.js create mode 100755 themes/nav/components/JumpToTopButton.js create mode 100755 themes/nav/components/LeftMenuBar.js create mode 100755 themes/nav/components/LoadingCover.js create mode 100755 themes/nav/components/LogoBar.js create mode 100755 themes/nav/components/MenuBarMobile.js create mode 100644 themes/nav/components/MenuItem.js create mode 100755 themes/nav/components/MenuItemCollapse.js create mode 100755 themes/nav/components/MenuItemDrop.js create mode 100755 themes/nav/components/MenuItemMobileNormal.js create mode 100755 themes/nav/components/MenuItemPCNormal.js create mode 100755 themes/nav/components/NavPostItem.js create mode 100755 themes/nav/components/NavPostList.js create mode 100755 themes/nav/components/NavPostListEmpty.js create mode 100755 themes/nav/components/NotionIcon.js create mode 100755 themes/nav/components/PageNavDrawer.js create mode 100755 themes/nav/components/PaginationSimple.js create mode 100755 themes/nav/components/Progress.js create mode 100755 themes/nav/components/RevolverMaps.js create mode 100755 themes/nav/components/SearchInput.js create mode 100755 themes/nav/components/SocialButton.js create mode 100755 themes/nav/components/TagGroups.js create mode 100755 themes/nav/components/TagItemMini.js create mode 100755 themes/nav/components/TocDrawer.js create mode 100755 themes/nav/components/TopNavBar.js create mode 100755 themes/nav/config.js create mode 100755 themes/nav/index.js create mode 100755 themes/nav/style.js 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 ( +
+
+ {archiveTitle} +
+
    + {archivePosts[archiveTitle]?.map(post => ( +
  • +
    + + {post.date?.start_date} + {' '} +   + + + {post.title} + +
    +
  • + ))} +
+
+ ) +} 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} +
+ {children} +
+
+} +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 && +
    {link.name}
    + } + + {hasSubMenu &&
    +
    {link.name}
    +
    +
    } +
    + + {/* 折叠子菜单 */} + {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.name}
    +
    + {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.name}
    +
    + {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?.category} +
    +
    + + {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 ( +
    +
    标签
    +
    + { + tagOptions?.map(tag => { + const selected = tag.name === currentTag + 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 ( + + + + +} + +export { Style }