diff --git a/blog.config.js b/blog.config.js index 72752320..df1cd74c 100644 --- a/blog.config.js +++ b/blog.config.js @@ -25,10 +25,10 @@ const BLOG = { // 社交链接,不需要可留空白,例如 CONTACT_WEIBO:'' CONTACT_EMAIL: 'tlyong1992@hotmail.com', - CONTACT_WEIBO: 'https://weibo.com/tangly1024', - CONTACT_TWITTER: 'https://twitter.com/troy1024_1', + CONTACT_WEIBO: '', + CONTACT_TWITTER: '', CONTACT_GITHUB: 'https://github.com/tangly1024', - CONTACT_TELEGRAM: 'https://t.me/tangly_1024', + CONTACT_TELEGRAM: '', // 评论互动 可同时开启 CUSDIS UTTERRANCES GITALK COMMENT_CUSDIS_APP_ID: process.env.NEXT_PUBLIC_COMMENT_CUSDIS_APP_ID || '', // data-app-id 36位 see https://cusdis.com/ diff --git a/components/Comment.js b/components/Comment.js index fcb7879b..a1cc1c04 100644 --- a/components/Comment.js +++ b/components/Comment.js @@ -1,7 +1,6 @@ import BLOG from '@/blog.config' import dynamic from 'next/dynamic' import { useRouter } from 'next/router' -import { useGlobal } from '@/lib/global' import 'gitalk/dist/gitalk.css' import Tabs from '@/components/Tabs' @@ -19,30 +18,25 @@ const UtterancesComponent = dynamic( ) const CusdisComponent = dynamic( () => { - return import('react-cusdis').then(m => m.ReactCusdis) + return import('@/components/Cusdis') }, { ssr: false } ) const Comment = ({ frontMatter }) => { const router = useRouter() - const { theme } = useGlobal() return ( -
+
{BLOG.COMMENT_CUSDIS_APP_ID && (
- +
)} + + {BLOG.COMMENT_UTTERRANCES_REPO && (
+ +
+ )} + {BLOG.COMMENT_GITALK_CLIENT_ID && (
{ }} />
)} - {BLOG.COMMENT_UTTERRANCES_REPO && (
- -
- )} +
) diff --git a/components/Cusdis.js b/components/Cusdis.js index a05f14ec..5ff88319 100644 --- a/components/Cusdis.js +++ b/components/Cusdis.js @@ -3,7 +3,7 @@ import { useEffect } from 'react' const Cusdis = ({ id, url, title }) => { useEffect(() => { const script = document.createElement('script') - const anchor = document.getElementById('comments') + const anchor = document.getElementById('comments-cusdis') script.setAttribute( 'src', BLOG.COMMENT_CUSDIS_SCRIPT_SRC @@ -11,12 +11,9 @@ const Cusdis = ({ id, url, title }) => { script.setAttribute('async', true) script.setAttribute('defer', true) anchor.appendChild(script) - return () => { - anchor.innerHTML = '' - } }) return ( -
+
{ data-page-id={id} data-page-url={url} data-page-title={title} + lang={BLOG.LANG.toLowerCase()} />
) diff --git a/components/Utterances.js b/components/Utterances.js index 39d3d826..82b5d60c 100644 --- a/components/Utterances.js +++ b/components/Utterances.js @@ -29,8 +29,7 @@ const Utterances = ({ issueTerm, layout }) => { anchor.innerHTML = '' } }) - return
-
+ return
} diff --git a/lib/lang/en-US.js b/lib/lang/en-US.js index 87a4f034..928e7295 100644 --- a/lib/lang/en-US.js +++ b/lib/lang/en-US.js @@ -18,7 +18,7 @@ export default { SHARE: 'Share', SCAN_QR_CODE: 'Scan QRCode', URL_COPIED: 'URL has copied!', - TABLE_OF_CONTENTS: 'Table of Contents', + TABLE_OF_CONTENTS: 'Catalog', RELATE_POSTS: 'Relate Posts', COPYRIGHT: 'Copyright', AUTHOR: 'Author', diff --git a/lib/notion/getAllPosts.js b/lib/notion/getAllPosts.js index 6f38bf79..d0c68d7c 100644 --- a/lib/notion/getAllPosts.js +++ b/lib/notion/getAllPosts.js @@ -25,9 +25,6 @@ export async function getAllPosts ({ notionPageData, from, includePage = false } const collectionQuery = notionPageData.collectionQuery const data = [] - if (!collectionQuery || collectionQuery.toString === '{}') { - console.warn('列表查询条件为空', notionPageData) - } const pageIds = getAllPageIds(collectionQuery) if (!pageIds || pageIds.length === 0) { console.warn('页面ID列表为空') diff --git a/lib/notion/getPageTableOfContents.js b/lib/notion/getPageTableOfContents.js deleted file mode 100644 index 75edab48..00000000 --- a/lib/notion/getPageTableOfContents.js +++ /dev/null @@ -1,68 +0,0 @@ - -const indentLevels = { - header: 0, - sub_header: 1, - sub_sub_header: 2 -} - -export const getPageTableOfContents = (page, recordMap) => { - // 获取 header sub_header sub_sub_header - const toc = (page.content ?? []) - .map((blockId) => { - const block = recordMap.block[blockId]?.value - - if (block) { - const { type } = block - - if ( - type === 'header' || - type === 'sub_header' || - type === 'sub_sub_header' - ) { - return { - id: blockId, - type, - indentLevel: indentLevels[type] - } - } - } - - return null - }) - .filter(Boolean) - - const indentLevelStack = [ - { - actual: -1, - effective: -1 - } - ] - - // Adjust indent levels to always change smoothly. - // This is a little tricky, but the key is that when increasing indent levels, - // they should never jump more than one at a time. - for (const tocItem of toc) { - const { indentLevel } = tocItem - const actual = indentLevel - - do { - const prevIndent = indentLevelStack[indentLevelStack.length - 1] - const { actual: prevActual, effective: prevEffective } = prevIndent - - if (actual > prevActual) { - tocItem.indentLevel = prevEffective + 1 - indentLevelStack.push({ - actual, - effective: tocItem.indentLevel - }) - } else if (actual === prevActual) { - tocItem.indentLevel = prevEffective - break - } else { - indentLevelStack.pop() - } - } while (true) - } - - return toc -} diff --git a/package.json b/package.json index 3328bd09..be8c26f9 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "qrcode.react": "^1.0.1", "react": "17.0.2", "react-cookies": "^0.1.1", - "react-cusdis": "^2.0.1", "react-dom": "17.0.2", "react-notion-x": "4.13.0", "smoothscroll-polyfill": "^0.4.4", diff --git a/themes/Empty/LayoutSlug.js b/themes/Empty/LayoutSlug.js index 4def0283..485446ca 100644 --- a/themes/Empty/LayoutSlug.js +++ b/themes/Empty/LayoutSlug.js @@ -1,4 +1,5 @@ import BLOG from '@/blog.config' +import { getPageTableOfContents } from 'notion-utils' import 'prismjs' import 'prismjs/components/prism-bash' import 'prismjs/components/prism-javascript' @@ -21,6 +22,11 @@ export const LayoutSlug = (props) => { tags: post.tags } + if (post?.blockMap?.block) { + post.content = Object.keys(post.blockMap.block) + post.toc = getPageTableOfContents(post, post.blockMap) + } + return

Slug - {post?.title}

diff --git a/themes/Hexo/LayoutBase.js b/themes/Hexo/LayoutBase.js index 26f55831..5f7e5b16 100644 --- a/themes/Hexo/LayoutBase.js +++ b/themes/Hexo/LayoutBase.js @@ -44,10 +44,10 @@ const LayoutBase = (props) => { {headerSlot} -

+
-
- {children} +
+
{children}
diff --git a/themes/Hexo/LayoutSlug.js b/themes/Hexo/LayoutSlug.js index c4d5ce7e..5800b844 100644 --- a/themes/Hexo/LayoutSlug.js +++ b/themes/Hexo/LayoutSlug.js @@ -1,6 +1,7 @@ import BLOG from '@/blog.config' import formatDate from '@/lib/formatDate' import { useGlobal } from '@/lib/global' +import { getPageTableOfContents } from 'notion-utils' import { faFolderOpen } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import Link from 'next/link' @@ -13,8 +14,10 @@ import 'prismjs/components/prism-python' import 'prismjs/components/prism-typescript' import CONFIG_NEXT from '../NEXT/config_next' import ArticleDetail from './components/ArticleDetail' -import Card from './components/Card' import LayoutBase from './LayoutBase' +import TocDrawerButton from './components/TocDrawerButton' +import { useRef } from 'react' +import TocDrawer from './components/TocDrawer' export const LayoutSlug = props => { const { post } = props @@ -31,10 +34,16 @@ export const LayoutSlug = props => { locale.LOCALE ) - const headerSlot = ( -
+ if (post?.blockMap?.block) { + post.content = Object.keys(post.blockMap.block) + post.toc = getPageTableOfContents(post, post.blockMap) + } + const headerSlot = ( +
{/* 文章Title */} @@ -52,7 +61,8 @@ export const LayoutSlug = props => { | - {post.type[0] !== 'Page' && (<> + {post.type[0] !== 'Page' && ( + <> { {date} - )} + + )}
| - 次访问 + + 次访问
@@ -73,12 +85,39 @@ export const LayoutSlug = props => {
) + const drawerRight = useRef(null) + const targetRef = typeof window !== 'undefined' ? document.getElementById('container') : null + + const floatSlot = + post?.toc?.length > 1 + ? ( +
+ { + drawerRight?.current?.handleSwitchVisible() + }} + /> +
+ ) + : null return ( - - + +
- +
+ +
+ +
+
) } diff --git a/themes/Hexo/components/ArticleDetail.js b/themes/Hexo/components/ArticleDetail.js index a30585b9..e006309d 100644 --- a/themes/Hexo/components/ArticleDetail.js +++ b/themes/Hexo/components/ArticleDetail.js @@ -33,13 +33,13 @@ export default function ArticleDetail ({ post, recommendPosts, prev, next }) { } }) - return (
+ return (
{/* Notion文章主体 */} -
+
{post.blockMap && ( +
+ {/* 评论互动 */} -
-
发表评论
+
) diff --git a/themes/Hexo/components/BlogPostCard.js b/themes/Hexo/components/BlogPostCard.js index 2c98753a..93a2735b 100644 --- a/themes/Hexo/components/BlogPostCard.js +++ b/themes/Hexo/components/BlogPostCard.js @@ -29,7 +29,7 @@ const BlogPostCard = ({ post, showSummary }) => {
- {(!showPreview || showSummary) &&

+ {(!showPreview || showSummary) &&

{post.summary}

} diff --git a/themes/Hexo/components/BlogPostListEmpty.js b/themes/Hexo/components/BlogPostListEmpty.js index b1d87253..86977fd0 100644 --- a/themes/Hexo/components/BlogPostListEmpty.js +++ b/themes/Hexo/components/BlogPostListEmpty.js @@ -5,7 +5,7 @@ * @constructor */ const BlogPostListEmpty = ({ currentSearch }) => { - return
+ return

没有找到文章 {(currentSearch &&

{currentSearch}
)}

} diff --git a/themes/Hexo/components/BlogPostListPage.js b/themes/Hexo/components/BlogPostListPage.js index f10811af..b08a0da6 100644 --- a/themes/Hexo/components/BlogPostListPage.js +++ b/themes/Hexo/components/BlogPostListPage.js @@ -20,7 +20,7 @@ const BlogPostListPage = ({ page = 1, posts = [], postCount }) => { return (
{/* 文章列表 */} -
+
{posts.map(post => ( ))} diff --git a/themes/Hexo/components/Catalog.js b/themes/Hexo/components/Catalog.js new file mode 100644 index 00000000..830e6b04 --- /dev/null +++ b/themes/Hexo/components/Catalog.js @@ -0,0 +1,88 @@ +import React from 'react' +import throttle from 'lodash.throttle' +import { uuidToId } from 'notion-utils' +import Progress from './Progress' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faStream } from '@fortawesome/free-solid-svg-icons' +// import { cs } from 'react-notion-x' + +/** + * 目录导航组件 + * @param toc + * @returns {JSX.Element} + * @constructor + */ +const Catalog = ({ toc }) => { + // 无目录就直接返回空 + if (!toc || toc.length < 1) { + return <> + } + // 监听滚动事件 + React.useEffect(() => { + window.addEventListener('scroll', actionSectionScrollSpy) + actionSectionScrollSpy() + return () => { + window.removeEventListener('scroll', actionSectionScrollSpy) + } + }, []) + + // 同步选中目录事件 + const [activeSection, setActiveSection] = React.useState(null) + const throttleMs = 100 + const actionSectionScrollSpy = React.useCallback(throttle(() => { + const sections = document.getElementsByClassName('notion-h') + let prevBBox = null + let currentSectionId = activeSection + for (let i = 0; i < sections.length; ++i) { + const section = sections[i] + if (!section || !(section instanceof Element)) continue + if (!currentSectionId) { + currentSectionId = section.getAttribute('data-id') + } + const bbox = section.getBoundingClientRect() + const prevHeight = prevBBox ? bbox.top - prevBBox.bottom : 0 + const offset = Math.max(150, prevHeight / 4) + // GetBoundingClientRect returns values relative to viewport + if (bbox.top - offset < 0) { + currentSectionId = section.getAttribute('data-id') + prevBBox = bbox + continue + } + // No need to continue loop, if last element has been detected + break + } + setActiveSection(currentSectionId) + }, throttleMs)) + + return
+
目录
+
+ +
+ +
+} + +export default Catalog diff --git a/themes/Hexo/components/Collapse.js b/themes/Hexo/components/Collapse.js index 7607b732..8af6f100 100644 --- a/themes/Hexo/components/Collapse.js +++ b/themes/Hexo/components/Collapse.js @@ -1,6 +1,7 @@ import React, { useEffect, useRef } from 'react' const Collapse = props => { + const { id, className } = props const collapseRef = useRef(null) const collapseSection = element => { const sectionHeight = element.scrollHeight @@ -28,7 +29,7 @@ const Collapse = props => { } }, [props.isOpen]) return ( -
+
{props.children}
) diff --git a/themes/Hexo/components/LatestPostsGroup.js b/themes/Hexo/components/LatestPostsGroup.js index 78782e69..8e9d7bc8 100644 --- a/themes/Hexo/components/LatestPostsGroup.js +++ b/themes/Hexo/components/LatestPostsGroup.js @@ -20,14 +20,14 @@ const LatestPostsGroup = ({ posts }) => { const { locale } = useGlobal() return <> -
-
{locale.COMMON.LATEST_POSTS}
+
+
{locale.COMMON.LATEST_POSTS}
{posts.map(post => { const selected = currentPath === `${BLOG.PATH}/article/${post.slug}` return ( - +
diff --git a/themes/Hexo/components/Logo.js b/themes/Hexo/components/Logo.js index a2852287..b0b1e70f 100644 --- a/themes/Hexo/components/Logo.js +++ b/themes/Hexo/components/Logo.js @@ -4,7 +4,7 @@ import React from 'react' const Logo = () => { return -
+
{BLOG.TITLE}
diff --git a/themes/Hexo/components/MenuButtonGroup.js b/themes/Hexo/components/MenuButtonGroup.js index 53528653..e62a3215 100644 --- a/themes/Hexo/components/MenuButtonGroup.js +++ b/themes/Hexo/components/MenuButtonGroup.js @@ -23,7 +23,7 @@ const MenuButtonGroup = ({ postCount }) => { if (link.show) { const selected = (router.pathname === link.to) || (router.asPath === link.to) return -
diff --git a/themes/Hexo/components/PaginationNumber.js b/themes/Hexo/components/PaginationNumber.js index f37d8704..8b99f564 100644 --- a/themes/Hexo/components/PaginationNumber.js +++ b/themes/Hexo/components/PaginationNumber.js @@ -18,7 +18,7 @@ const PaginationNumber = ({ page, totalPage }) => { const pages = generatePages(page, currentPage, totalPage) return ( -
+
{/* 上一页 */} { } } passHref > @@ -39,7 +39,7 @@ const PaginationNumber = ({ page, totalPage }) => { @@ -51,11 +51,12 @@ const PaginationNumber = ({ page, totalPage }) => { function getPageElement (page, currentPage) { return + ' border-white dark:border-blue-700 dark:hover:border-blue-400 cursor-pointer pb-0.5 w-6 text-center font-light hover:font-bold'}> {page} } + function generatePages (page, currentPage, totalPage) { const pages = [] const groupCount = 7 // 最多显示页签数 diff --git a/themes/Hexo/components/Progress.js b/themes/Hexo/components/Progress.js new file mode 100644 index 00000000..8dcde403 --- /dev/null +++ b/themes/Hexo/components/Progress.js @@ -0,0 +1,43 @@ +import React, { useEffect, useState } from 'react' + +/** + * 顶部页面阅读进度条 + * @returns {JSX.Element} + * @constructor + */ +const Progress = ({ targetRef, showPercent = true }) => { + const currentRef = targetRef?.current || targetRef + const [percent, changePercent] = useState(0) + const scrollListener = () => { + const target = currentRef || document.getElementById('container') + if (target) { + const clientHeight = target.clientHeight + const scrollY = window.pageYOffset + const fullHeight = clientHeight - window.outerHeight + let per = parseFloat(((scrollY / fullHeight) * 100).toFixed(0)) + if (per > 100) per = 100 + if (per < 0) per = 0 + changePercent(per) + } + } + + useEffect(() => { + document.addEventListener('scroll', scrollListener) + return () => document.removeEventListener('scroll', scrollListener) + }, [percent]) + + return ( +
+
+ {showPercent && ( +
{percent}%
+ )} +
+
+ ) +} + +export default Progress diff --git a/themes/Hexo/components/SearchInput.js b/themes/Hexo/components/SearchInput.js index 57e16633..e728c320 100644 --- a/themes/Hexo/components/SearchInput.js +++ b/themes/Hexo/components/SearchInput.js @@ -46,19 +46,19 @@ const SearchInput = ({ currentTag, currentSearch, cRef }) => { updateSearchKey(e.target.value)} defaultValue={searchKey} /> -
{ handleSearch(searchKey) }}>
{(searchKey && searchKey.length && -
+
)} diff --git a/themes/Hexo/components/SideRight.js b/themes/Hexo/components/SideRight.js index adac7e09..167d4ba4 100644 --- a/themes/Hexo/components/SideRight.js +++ b/themes/Hexo/components/SideRight.js @@ -10,55 +10,96 @@ import CategoryGroup from './CategoryGroup' import LatestPostsGroup from './LatestPostsGroup' import TagGroups from './TagGroups' import SocialButton from './SocialButton' +import Catalog from './Catalog' export default function SideRight (props) { - const { postCount, currentCategory, categories, latestPosts, tags, currentTag } = props - return
- -
{ Router.push('/') }}> - {BLOG.AUTHOR} -
-
{BLOG.TITLE}
- -
- - - - - -
- 统计 -
-
+ const { + post, + postCount, + currentCategory, + categories, + latestPosts, + tags, + currentTag, + showCategory, + showTag + } = props + return ( +
+ +
{ + Router.push('/') + }} + > + {BLOG.AUTHOR} +
+
{BLOG.TITLE}
+ +
+ + + + + +
+ 统计 +
+
-
文章数:
{postCount}
+
+
文章数:
+
{postCount}
+
-
-
访问量:
+
+
+
访问量:
+
+
-
-
访客数:
+
+
+
访客数:
+
+
- - -
- 分类 -
- -
- - - - - - -
+ + + {showCategory && ( + +
+ 分类 +
+ +
+ )} + {showTag && ( + + + + )} + {latestPosts && + + } + + {post && post.toc && ( + + + + )} +
+ ) } diff --git a/themes/Hexo/components/TocDrawer.js b/themes/Hexo/components/TocDrawer.js new file mode 100644 index 00000000..70f8a41a --- /dev/null +++ b/themes/Hexo/components/TocDrawer.js @@ -0,0 +1,42 @@ +import Catalog from './Catalog' +import React, { useImperativeHandle, useState } from 'react' + +/** + * 目录抽屉栏 + * @param toc + * @param post + * @returns {JSX.Element} + * @constructor + */ +const TocDrawer = ({ post, cRef }) => { + // 暴露给父组件 通过cRef.current.handleMenuClick 调用 + useImperativeHandle(cRef, () => { + return { + handleSwitchVisible: () => switchVisible() + } + }) + const [showDrawer, switchShowDrawer] = useState(false) + const switchVisible = () => { + switchShowDrawer(!showDrawer) + } + return <> +
+ {/* 侧边菜单 */} +
+ {post && <> +
+ +
+ + } +
+
+ {/* 背景蒙版 */} +
+ +} +export default TocDrawer diff --git a/themes/Hexo/components/TocDrawerButton.js b/themes/Hexo/components/TocDrawerButton.js new file mode 100644 index 00000000..60553462 --- /dev/null +++ b/themes/Hexo/components/TocDrawerButton.js @@ -0,0 +1,24 @@ +import { useGlobal } from '@/lib/global' +import { faListOl } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import React from 'react' +import CONFIG_HEXO from '../config_hexo' + +/** + * 点击召唤目录抽屉 + * 当屏幕下滑500像素后会出现该控件 + * @param props 父组件传入props + * @returns {JSX.Element} + * @constructor + */ +const TocDrawerButton = (props) => { + if (!CONFIG_HEXO.WIDGET_TOC) { + return <> + } + const { locale } = useGlobal() + return (
+ +
) +} + +export default TocDrawerButton diff --git a/themes/Hexo/components/TopNav.js b/themes/Hexo/components/TopNav.js index fc94ff5b..b3ddc1e8 100644 --- a/themes/Hexo/components/TopNav.js +++ b/themes/Hexo/components/TopNav.js @@ -1,9 +1,9 @@ import { useGlobal } from '@/lib/global' -import { faAngleDoubleRight, faBars, faSearch, faTag, faThList, faTimes } from '@fortawesome/free-solid-svg-icons' +import { faAngleDoubleRight, faBars, faTag, faThList, faTimes } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import throttle from 'lodash.throttle' import Link from 'next/link' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import CategoryGroup from './CategoryGroup' import Collapse from './Collapse' import Logo from './Logo' @@ -11,6 +11,7 @@ import MenuButtonGroup from './MenuButtonGroup' import SearchDrawer from './SearchDrawer' import TagGroups from './TagGroups' import CONFIG_HEXO from '../config_hexo' +import SearchInput from './SearchInput' let windowTop = 0 @@ -23,18 +24,18 @@ const TopNav = ({ tags, currentTag, categories, currentCategory, postCount }) => const { locale } = useGlobal() const searchDrawer = useRef() - const scrollTrigger = useCallback(throttle(() => { + const scrollTrigger = throttle(() => { const scrollS = window.scrollY if (scrollS >= windowTop && scrollS > 10) { const nav = document.querySelector('#sticky-nav') - nav && nav.classList.replace('top-0', '-top-40') + nav && nav.classList.replace('top-0', '-top-16') windowTop = scrollS } else { const nav = document.querySelector('#sticky-nav') - nav && nav.classList.replace('-top-40', 'top-0') + nav && nav.classList.replace('-top-16', 'top-0') windowTop = scrollS } - }, 200), []) + }, 200) // 监听滚动 useEffect(() => { @@ -85,35 +86,29 @@ const TopNav = ({ tags, currentTag, categories, currentCategory, postCount }) => ) } - return (
+ return (
{/* 导航栏 */} -