mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-31 07:26:43 +00:00
feature: 调整Hexo 页面
This commit is contained in:
@@ -25,10 +25,10 @@ const BLOG = {
|
|||||||
|
|
||||||
// 社交链接,不需要可留空白,例如 CONTACT_WEIBO:''
|
// 社交链接,不需要可留空白,例如 CONTACT_WEIBO:''
|
||||||
CONTACT_EMAIL: 'tlyong1992@hotmail.com',
|
CONTACT_EMAIL: 'tlyong1992@hotmail.com',
|
||||||
CONTACT_WEIBO: 'https://weibo.com/tangly1024',
|
CONTACT_WEIBO: '',
|
||||||
CONTACT_TWITTER: 'https://twitter.com/troy1024_1',
|
CONTACT_TWITTER: '',
|
||||||
CONTACT_GITHUB: 'https://github.com/tangly1024',
|
CONTACT_GITHUB: 'https://github.com/tangly1024',
|
||||||
CONTACT_TELEGRAM: 'https://t.me/tangly_1024',
|
CONTACT_TELEGRAM: '',
|
||||||
|
|
||||||
// 评论互动 可同时开启 CUSDIS UTTERRANCES GITALK
|
// 评论互动 可同时开启 CUSDIS UTTERRANCES GITALK
|
||||||
COMMENT_CUSDIS_APP_ID: process.env.NEXT_PUBLIC_COMMENT_CUSDIS_APP_ID || '', // data-app-id 36位 see https://cusdis.com/
|
COMMENT_CUSDIS_APP_ID: process.env.NEXT_PUBLIC_COMMENT_CUSDIS_APP_ID || '', // data-app-id 36位 see https://cusdis.com/
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import BLOG from '@/blog.config'
|
import BLOG from '@/blog.config'
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useGlobal } from '@/lib/global'
|
|
||||||
import 'gitalk/dist/gitalk.css'
|
import 'gitalk/dist/gitalk.css'
|
||||||
import Tabs from '@/components/Tabs'
|
import Tabs from '@/components/Tabs'
|
||||||
|
|
||||||
@@ -19,30 +18,25 @@ const UtterancesComponent = dynamic(
|
|||||||
)
|
)
|
||||||
const CusdisComponent = dynamic(
|
const CusdisComponent = dynamic(
|
||||||
() => {
|
() => {
|
||||||
return import('react-cusdis').then(m => m.ReactCusdis)
|
return import('@/components/Cusdis')
|
||||||
},
|
},
|
||||||
{ ssr: false }
|
{ ssr: false }
|
||||||
)
|
)
|
||||||
|
|
||||||
const Comment = ({ frontMatter }) => {
|
const Comment = ({ frontMatter }) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { theme } = useGlobal()
|
|
||||||
return (
|
return (
|
||||||
<div className='comment mt-5 px-10 text-gray-800 dark:text-gray-300'>
|
<div className='comment mt-5 px-5 text-gray-800 dark:text-gray-300'>
|
||||||
<Tabs>
|
<Tabs>
|
||||||
{BLOG.COMMENT_CUSDIS_APP_ID && (<div key='Cusdis'>
|
{BLOG.COMMENT_CUSDIS_APP_ID && (<div key='Cusdis'>
|
||||||
<CusdisComponent
|
<CusdisComponent id={frontMatter.id} url={BLOG.LINK + router.asPath} title={frontMatter.title} />
|
||||||
attrs={{
|
|
||||||
host: BLOG.COMMENT_CUSDIS_HOST,
|
|
||||||
appId: BLOG.COMMENT_CUSDIS_APP_ID,
|
|
||||||
pageId: frontMatter.id,
|
|
||||||
pageTitle: frontMatter.title,
|
|
||||||
pageUrl: BLOG.LINK + router.asPath,
|
|
||||||
theme: theme
|
|
||||||
}}
|
|
||||||
lang={BLOG.LANG.toLowerCase()}
|
|
||||||
/>
|
|
||||||
</div>)}
|
</div>)}
|
||||||
|
|
||||||
|
{BLOG.COMMENT_UTTERRANCES_REPO && (<div key='Utterance'>
|
||||||
|
<UtterancesComponent issueTerm={frontMatter.id} className='px-2' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{BLOG.COMMENT_GITALK_CLIENT_ID && (<div key='GitTalk'>
|
{BLOG.COMMENT_GITALK_CLIENT_ID && (<div key='GitTalk'>
|
||||||
<GitalkComponent
|
<GitalkComponent
|
||||||
options={{
|
options={{
|
||||||
@@ -57,10 +51,7 @@ const Comment = ({ frontMatter }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>)}
|
</div>)}
|
||||||
{BLOG.COMMENT_UTTERRANCES_REPO && (<div key='Utterance'>
|
|
||||||
<UtterancesComponent issueTerm={frontMatter.id} className='px-2' />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useEffect } from 'react'
|
|||||||
const Cusdis = ({ id, url, title }) => {
|
const Cusdis = ({ id, url, title }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const script = document.createElement('script')
|
const script = document.createElement('script')
|
||||||
const anchor = document.getElementById('comments')
|
const anchor = document.getElementById('comments-cusdis')
|
||||||
script.setAttribute(
|
script.setAttribute(
|
||||||
'src',
|
'src',
|
||||||
BLOG.COMMENT_CUSDIS_SCRIPT_SRC
|
BLOG.COMMENT_CUSDIS_SCRIPT_SRC
|
||||||
@@ -11,12 +11,9 @@ const Cusdis = ({ id, url, title }) => {
|
|||||||
script.setAttribute('async', true)
|
script.setAttribute('async', true)
|
||||||
script.setAttribute('defer', true)
|
script.setAttribute('defer', true)
|
||||||
anchor.appendChild(script)
|
anchor.appendChild(script)
|
||||||
return () => {
|
|
||||||
anchor.innerHTML = ''
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<div id="comments">
|
<div id="comments-cusdis">
|
||||||
<div
|
<div
|
||||||
id="cusdis_thread"
|
id="cusdis_thread"
|
||||||
data-host={BLOG.COMMENT_CUSDIS_HOST}
|
data-host={BLOG.COMMENT_CUSDIS_HOST}
|
||||||
@@ -24,6 +21,7 @@ const Cusdis = ({ id, url, title }) => {
|
|||||||
data-page-id={id}
|
data-page-id={id}
|
||||||
data-page-url={url}
|
data-page-url={url}
|
||||||
data-page-title={title}
|
data-page-title={title}
|
||||||
|
lang={BLOG.LANG.toLowerCase()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ const Utterances = ({ issueTerm, layout }) => {
|
|||||||
anchor.innerHTML = ''
|
anchor.innerHTML = ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return <div id="comments" >
|
return <div id="comments" className='utterances' >
|
||||||
<div className="utterances-frame h-auto w-auto"/>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default {
|
|||||||
SHARE: 'Share',
|
SHARE: 'Share',
|
||||||
SCAN_QR_CODE: 'Scan QRCode',
|
SCAN_QR_CODE: 'Scan QRCode',
|
||||||
URL_COPIED: 'URL has copied!',
|
URL_COPIED: 'URL has copied!',
|
||||||
TABLE_OF_CONTENTS: 'Table of Contents',
|
TABLE_OF_CONTENTS: 'Catalog',
|
||||||
RELATE_POSTS: 'Relate Posts',
|
RELATE_POSTS: 'Relate Posts',
|
||||||
COPYRIGHT: 'Copyright',
|
COPYRIGHT: 'Copyright',
|
||||||
AUTHOR: 'Author',
|
AUTHOR: 'Author',
|
||||||
|
|||||||
@@ -25,9 +25,6 @@ export async function getAllPosts ({ notionPageData, from, includePage = false }
|
|||||||
const collectionQuery = notionPageData.collectionQuery
|
const collectionQuery = notionPageData.collectionQuery
|
||||||
|
|
||||||
const data = []
|
const data = []
|
||||||
if (!collectionQuery || collectionQuery.toString === '{}') {
|
|
||||||
console.warn('列表查询条件为空', notionPageData)
|
|
||||||
}
|
|
||||||
const pageIds = getAllPageIds(collectionQuery)
|
const pageIds = getAllPageIds(collectionQuery)
|
||||||
if (!pageIds || pageIds.length === 0) {
|
if (!pageIds || pageIds.length === 0) {
|
||||||
console.warn('页面ID列表为空')
|
console.warn('页面ID列表为空')
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -39,7 +39,6 @@
|
|||||||
"qrcode.react": "^1.0.1",
|
"qrcode.react": "^1.0.1",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-cookies": "^0.1.1",
|
"react-cookies": "^0.1.1",
|
||||||
"react-cusdis": "^2.0.1",
|
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-notion-x": "4.13.0",
|
"react-notion-x": "4.13.0",
|
||||||
"smoothscroll-polyfill": "^0.4.4",
|
"smoothscroll-polyfill": "^0.4.4",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import BLOG from '@/blog.config'
|
import BLOG from '@/blog.config'
|
||||||
|
import { getPageTableOfContents } from 'notion-utils'
|
||||||
import 'prismjs'
|
import 'prismjs'
|
||||||
import 'prismjs/components/prism-bash'
|
import 'prismjs/components/prism-bash'
|
||||||
import 'prismjs/components/prism-javascript'
|
import 'prismjs/components/prism-javascript'
|
||||||
@@ -21,6 +22,11 @@ export const LayoutSlug = (props) => {
|
|||||||
tags: post.tags
|
tags: post.tags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (post?.blockMap?.block) {
|
||||||
|
post.content = Object.keys(post.blockMap.block)
|
||||||
|
post.toc = getPageTableOfContents(post, post.blockMap)
|
||||||
|
}
|
||||||
|
|
||||||
return <LayoutBase {...props} meta={meta}>
|
return <LayoutBase {...props} meta={meta}>
|
||||||
<h1>Slug - {post?.title}</h1>
|
<h1>Slug - {post?.title}</h1>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ const LayoutBase = (props) => {
|
|||||||
|
|
||||||
{headerSlot}
|
{headerSlot}
|
||||||
|
|
||||||
<main id='wrapper' className='flex w-full justify-center py-8 min-h-screen'>
|
<main id='wrapper' className='mt-12 lg:mt-0 flex w-full justify-center py-8 min-h-screen'>
|
||||||
|
|
||||||
<div id='container-inner' className='w-full mx-auto flex justify-between max-w-6xl'>
|
<div id='container-inner' className='w-full mx-auto flex justify-between space-x-4 max-w-6xl'>
|
||||||
{children}
|
<div className='w-full'>{children}</div>
|
||||||
<SideRight {...props}/>
|
<SideRight {...props}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import BLOG from '@/blog.config'
|
import BLOG from '@/blog.config'
|
||||||
import formatDate from '@/lib/formatDate'
|
import formatDate from '@/lib/formatDate'
|
||||||
import { useGlobal } from '@/lib/global'
|
import { useGlobal } from '@/lib/global'
|
||||||
|
import { getPageTableOfContents } from 'notion-utils'
|
||||||
import { faFolderOpen } from '@fortawesome/free-solid-svg-icons'
|
import { faFolderOpen } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
@@ -13,8 +14,10 @@ import 'prismjs/components/prism-python'
|
|||||||
import 'prismjs/components/prism-typescript'
|
import 'prismjs/components/prism-typescript'
|
||||||
import CONFIG_NEXT from '../NEXT/config_next'
|
import CONFIG_NEXT from '../NEXT/config_next'
|
||||||
import ArticleDetail from './components/ArticleDetail'
|
import ArticleDetail from './components/ArticleDetail'
|
||||||
import Card from './components/Card'
|
|
||||||
import LayoutBase from './LayoutBase'
|
import LayoutBase from './LayoutBase'
|
||||||
|
import TocDrawerButton from './components/TocDrawerButton'
|
||||||
|
import { useRef } from 'react'
|
||||||
|
import TocDrawer from './components/TocDrawer'
|
||||||
|
|
||||||
export const LayoutSlug = props => {
|
export const LayoutSlug = props => {
|
||||||
const { post } = props
|
const { post } = props
|
||||||
@@ -31,10 +34,16 @@ export const LayoutSlug = props => {
|
|||||||
locale.LOCALE
|
locale.LOCALE
|
||||||
)
|
)
|
||||||
|
|
||||||
const headerSlot = (
|
if (post?.blockMap?.block) {
|
||||||
<div className="w-full h-96 relative md:flex-shrink-0 overflow-hidden bg-cover bg-center bg-no-repeat"
|
post.content = Object.keys(post.blockMap.block)
|
||||||
style={{ backgroundImage: `url("/${CONFIG_NEXT.HOME_BANNER_IMAGE}")` }}>
|
post.toc = getPageTableOfContents(post, post.blockMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerSlot = (
|
||||||
|
<div
|
||||||
|
className="w-full h-96 relative md:flex-shrink-0 overflow-hidden bg-cover bg-center bg-no-repeat"
|
||||||
|
style={{ backgroundImage: `url("/${CONFIG_NEXT.HOME_BANNER_IMAGE}")` }}
|
||||||
|
>
|
||||||
<header className="animate__slideInDown animate__animated bg-black bg-opacity-50 absolute top-0 w-full h-96 py-10 flex justify-center items-center font-sans">
|
<header className="animate__slideInDown animate__animated bg-black bg-opacity-50 absolute top-0 w-full h-96 py-10 flex justify-center items-center font-sans">
|
||||||
<div>
|
<div>
|
||||||
{/* 文章Title */}
|
{/* 文章Title */}
|
||||||
@@ -52,7 +61,8 @@ export const LayoutSlug = props => {
|
|||||||
</Link>
|
</Link>
|
||||||
<span className="mr-2">|</span>
|
<span className="mr-2">|</span>
|
||||||
|
|
||||||
{post.type[0] !== 'Page' && (<>
|
{post.type[0] !== 'Page' && (
|
||||||
|
<>
|
||||||
<Link
|
<Link
|
||||||
href={`/archive#${post?.date?.start_date?.substr(0, 7)}`}
|
href={`/archive#${post?.date?.start_date?.substr(0, 7)}`}
|
||||||
passHref
|
passHref
|
||||||
@@ -61,11 +71,13 @@ export const LayoutSlug = props => {
|
|||||||
{date}
|
{date}
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</>)}
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="hidden busuanzi_container_page_pv font-light mr-2">
|
<div className="hidden busuanzi_container_page_pv font-light mr-2">
|
||||||
<span className="mr-2">|</span>
|
<span className="mr-2">|</span>
|
||||||
<span className="mr-2 busuanzi_value_page_pv" />次访问
|
<span className="mr-2 busuanzi_value_page_pv" />
|
||||||
|
次访问
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -73,12 +85,39 @@ export const LayoutSlug = props => {
|
|||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
const drawerRight = useRef(null)
|
||||||
|
const targetRef = typeof window !== 'undefined' ? document.getElementById('container') : null
|
||||||
|
|
||||||
|
const floatSlot =
|
||||||
|
post?.toc?.length > 1
|
||||||
|
? (
|
||||||
|
<div className="block lg:hidden">
|
||||||
|
<TocDrawerButton
|
||||||
|
onClick={() => {
|
||||||
|
drawerRight?.current?.handleSwitchVisible()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LayoutBase headerSlot={headerSlot} {...props} meta={meta}>
|
<LayoutBase
|
||||||
<Card className="w-full">
|
headerSlot={headerSlot}
|
||||||
|
{...props}
|
||||||
|
meta={meta}
|
||||||
|
showCategory={false}
|
||||||
|
showTag={false}
|
||||||
|
floatSlot={floatSlot}
|
||||||
|
>
|
||||||
|
<div className="w-full lg:shadow-xl lg:hover:shadow-2xl lg:border lg:border-gray-100 lg:rounded-xl lg:px-2 lg:py-4 lg:bg-white lg:dark:bg-gray-800 lg:duration-300">
|
||||||
<ArticleDetail {...props} />
|
<ArticleDetail {...props} />
|
||||||
</Card>
|
</div>
|
||||||
|
|
||||||
|
<div className='block lg:hidden'>
|
||||||
|
<TocDrawer post={post} cRef={drawerRight} targetRef={targetRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
</LayoutBase>
|
</LayoutBase>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,13 +33,13 @@ export default function ArticleDetail ({ post, recommendPosts, prev, next }) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return (<div id="container" className="max-w-5xl overflow-x-auto flex-grow mx-auto w-screen md:w-full md:px-5 ">
|
return (<div id="container" className="max-w-5xl overflow-x-auto flex-grow mx-auto md:w-full md:px-5 ">
|
||||||
<article itemScope itemType="https://schema.org/Movie"
|
<article itemScope itemType="https://schema.org/Movie"
|
||||||
className="subpixel-antialiased dark:border-gray-700 bg-white dark:bg-gray-800"
|
className="subpixel-antialiased dark:border-gray-700 bg-white dark:bg-gray-800"
|
||||||
>
|
>
|
||||||
|
|
||||||
{/* Notion文章主体 */}
|
{/* Notion文章主体 */}
|
||||||
<section id='notion-article' className='px-1'>
|
<section id='notion-article' className='px-5'>
|
||||||
{post.blockMap && (
|
{post.blockMap && (
|
||||||
<NotionRenderer
|
<NotionRenderer
|
||||||
recordMap={post.blockMap}
|
recordMap={post.blockMap}
|
||||||
@@ -67,9 +67,10 @@ export default function ArticleDetail ({ post, recommendPosts, prev, next }) {
|
|||||||
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<hr className='border-dashed'/>
|
||||||
|
|
||||||
{/* 评论互动 */}
|
{/* 评论互动 */}
|
||||||
<div className="duration-200 px-12 w-screen md:w-full overflow-x-auto bg-white dark:bg-gray-800">
|
<div className="duration-200 overflow-x-auto bg-white dark:bg-gray-800">
|
||||||
<div className='text-2xl mt-8 mx-8'>发表评论</div>
|
|
||||||
<Comment frontMatter={post} />
|
<Comment frontMatter={post} />
|
||||||
</div>
|
</div>
|
||||||
</div>)
|
</div>)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const BlogPostCard = ({ post, showSummary }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(!showPreview || showSummary) && <p className='mt-4 mb-24 text-gray-700 dark:text-gray-300 text-sm font-light leading-7'>
|
{(!showPreview || showSummary) && <p className='my-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7'>
|
||||||
{post.summary}
|
{post.summary}
|
||||||
</p>}
|
</p>}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
const BlogPostListEmpty = ({ currentSearch }) => {
|
const BlogPostListEmpty = ({ currentSearch }) => {
|
||||||
return <div className='flex items-center justify-center min-h-screen mx-auto md:-mt-20'>
|
return <div className='flex w-full items-center justify-center min-h-screen mx-auto md:-mt-20'>
|
||||||
<p className='text-gray-500 dark:text-gray-300'>没有找到文章 {(currentSearch && <div>{currentSearch}</div>)}</p>
|
<p className='text-gray-500 dark:text-gray-300'>没有找到文章 {(currentSearch && <div>{currentSearch}</div>)}</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const BlogPostListPage = ({ page = 1, posts = [], postCount }) => {
|
|||||||
return (
|
return (
|
||||||
<div id="container" className='w-full'>
|
<div id="container" className='w-full'>
|
||||||
{/* 文章列表 */}
|
{/* 文章列表 */}
|
||||||
<div className="flex lg:space-y-4 space-y-1">
|
<div className="space-y-6">
|
||||||
{posts.map(post => (
|
{posts.map(post => (
|
||||||
<BlogPostCard key={post.id} post={post} />
|
<BlogPostCard key={post.id} post={post} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
88
themes/Hexo/components/Catalog.js
Normal file
88
themes/Hexo/components/Catalog.js
Normal file
@@ -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 <div className='px-3'>
|
||||||
|
<div className='w-full'><FontAwesomeIcon className='mr-1' icon={faStream}/> 目录</div>
|
||||||
|
<div className='w-full py-1'>
|
||||||
|
<Progress/>
|
||||||
|
</div>
|
||||||
|
<nav className='font-sans overflow-y-auto scroll-hidden text-black'>
|
||||||
|
{toc.map((tocItem) => {
|
||||||
|
const id = uuidToId(tocItem.id)
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={id}
|
||||||
|
href={`#${id}`}
|
||||||
|
className={`notion-table-of-contents-item duration-300 transform font-light
|
||||||
|
notion-table-of-contents-item-indent-level-${tocItem.indentLevel} `}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
marginLeft: tocItem.indentLevel * 16
|
||||||
|
}}
|
||||||
|
className={`${activeSection === id && ' font-bold text-red-400 underline'}`}
|
||||||
|
>
|
||||||
|
{tocItem.text}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Catalog
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useRef } from 'react'
|
import React, { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
const Collapse = props => {
|
const Collapse = props => {
|
||||||
|
const { id, className } = props
|
||||||
const collapseRef = useRef(null)
|
const collapseRef = useRef(null)
|
||||||
const collapseSection = element => {
|
const collapseSection = element => {
|
||||||
const sectionHeight = element.scrollHeight
|
const sectionHeight = element.scrollHeight
|
||||||
@@ -28,7 +29,7 @@ const Collapse = props => {
|
|||||||
}
|
}
|
||||||
}, [props.isOpen])
|
}, [props.isOpen])
|
||||||
return (
|
return (
|
||||||
<div ref={collapseRef} style={{ height: '0px' }} className='overflow-hidden duration-200'>
|
<div id={id} ref={collapseRef} style={{ height: '0px' }} className={'overflow-hidden duration-200 ' + className}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ const LatestPostsGroup = ({ posts }) => {
|
|||||||
const { locale } = useGlobal()
|
const { locale } = useGlobal()
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div className='text-xs mb-2 px-1 flex flex-nowrap justify-between'>
|
<div className='font-sans mb-2 px-1 flex flex-nowrap justify-between'>
|
||||||
<div className='font-light text-gray-600 dark:text-gray-200'><FontAwesomeIcon icon={faArchive} className='mr-2' />{locale.COMMON.LATEST_POSTS}</div>
|
<div><FontAwesomeIcon icon={faArchive} className='mr-2' />{locale.COMMON.LATEST_POSTS}</div>
|
||||||
</div>
|
</div>
|
||||||
{posts.map(post => {
|
{posts.map(post => {
|
||||||
const selected = currentPath === `${BLOG.PATH}/article/${post.slug}`
|
const selected = currentPath === `${BLOG.PATH}/article/${post.slug}`
|
||||||
return (
|
return (
|
||||||
<Link key={post.id} title={post.title} href={`${BLOG.PATH}/article/${post.slug}`} passHref>
|
<Link key={post.id} title={post.title} href={`${BLOG.PATH}/article/${post.slug}`} passHref>
|
||||||
<a className={ 'my-1 mx-4 flex font-light'}>
|
<a className={ 'my-1 flex font-light'}>
|
||||||
<div className={ (selected ? 'text-white bg-blue-600 ' : 'text-gray-500 dark:text-gray-400 ') + ' text-xs py-1.5 flex overflow-x-hidden whitespace-nowrap hover:bg-blue-600 px-2 duration-200 w-full ' +
|
<div className={ (selected ? 'text-white bg-blue-600 ' : 'text-gray-500 dark:text-gray-400 ') + ' text-xs py-1.5 flex overflow-x-hidden whitespace-nowrap hover:bg-blue-600 px-2 duration-200 w-full ' +
|
||||||
'hover:text-white dark:hover:text-white cursor-pointer' }>
|
'hover:text-white dark:hover:text-white cursor-pointer' }>
|
||||||
<FontAwesomeIcon icon={faFileAlt} className='mr-2'/>
|
<FontAwesomeIcon icon={faFileAlt} className='mr-2'/>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import React from 'react'
|
|||||||
|
|
||||||
const Logo = () => {
|
const Logo = () => {
|
||||||
return <Link href='/' passHref>
|
return <Link href='/' passHref>
|
||||||
<div className='flex flex-col justify-center items-center cursor-pointer bg-black space-y-3 font-bold'>
|
<div className='flex flex-col justify-center items-center cursor-pointer space-y-3 font-bold'>
|
||||||
<div className='font-serif text-xl text-white'> {BLOG.TITLE}</div>
|
<div className='font-serif text-xl text-white'> {BLOG.TITLE}</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const MenuButtonGroup = ({ postCount }) => {
|
|||||||
if (link.show) {
|
if (link.show) {
|
||||||
const selected = (router.pathname === link.to) || (router.asPath === link.to)
|
const selected = (router.pathname === link.to) || (router.asPath === link.to)
|
||||||
return <Link key={`${link.id}-${link.to}`} title={link.to} href={link.to} >
|
return <Link key={`${link.id}-${link.to}`} title={link.to} href={link.to} >
|
||||||
<a className={'py-1.5 my-1 px-5 duration-300 text-base justify-between hover:bg-blue-600 rounded-lg hover:text-white hover:shadow-lg cursor-pointer font-light flex flex-nowrap items-center ' +
|
<a className={'py-1.5 my-1 px-5 duration-300 text-base justify-between hover:bg-blue-600 hover:text-white hover:shadow-lg cursor-pointer flex flex-nowrap items-center ' +
|
||||||
(selected ? 'bg-blue-600 text-white' : ' ')} >
|
(selected ? 'bg-blue-600 text-white' : ' ')} >
|
||||||
<div className='my-auto items-center justify-center flex '>
|
<div className='my-auto items-center justify-center flex '>
|
||||||
<FontAwesomeIcon icon={link.icon} />
|
<FontAwesomeIcon icon={link.icon} />
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const PaginationNumber = ({ page, totalPage }) => {
|
|||||||
const pages = generatePages(page, currentPage, totalPage)
|
const pages = generatePages(page, currentPage, totalPage)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='my-5 flex justify-center items-end font-medium text-black duration-500 bg-white dark:bg-blue-700 dark:text-gray-300 py-3 space-x-2'>
|
<div className='mt-10 mb-5 font-sans flex justify-center items-end font-medium text-black duration-500 dark:text-gray-300 py-3 space-x-2'>
|
||||||
|
|
||||||
{/* 上一页 */}
|
{/* 上一页 */}
|
||||||
<Link
|
<Link
|
||||||
@@ -27,7 +27,7 @@ const PaginationNumber = ({ page, totalPage }) => {
|
|||||||
} } passHref >
|
} } passHref >
|
||||||
<div
|
<div
|
||||||
rel='prev'
|
rel='prev'
|
||||||
className={`${currentPage === 1 ? 'invisible' : 'block'} border-white dark:border-blue-700 hover:border-blue-400 dark:hover:border-blue-400 w-6 text-center cursor-pointer duration-200 hover:font-bold`}
|
className={`${currentPage === 1 ? 'invisible' : 'block'} pb-0.5 border-white dark:border-blue-700 hover:border-blue-400 dark:hover:border-blue-400 w-6 text-center cursor-pointer duration-200 hover:font-bold`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faAngleLeft}/>
|
<FontAwesomeIcon icon={faAngleLeft}/>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,7 +39,7 @@ const PaginationNumber = ({ page, totalPage }) => {
|
|||||||
<Link href={ { pathname: `/page/${currentPage + 1}`, query: router.query.s ? { s: router.query.s } : {} } } passHref>
|
<Link href={ { pathname: `/page/${currentPage + 1}`, query: router.query.s ? { s: router.query.s } : {} } } passHref>
|
||||||
<div
|
<div
|
||||||
rel='next'
|
rel='next'
|
||||||
className={`${+showNext ? 'block' : 'invisible'} border-t-2 border-white dark:border-blue-700 hover:border-blue-400 dark:hover:border-blue-400 w-6 text-center cursor-pointer duration-500 hover:font-bold`}
|
className={`${+showNext ? 'block' : 'invisible'} pb-0.5 border-t-2 border-white dark:border-blue-700 hover:border-blue-400 dark:hover:border-blue-400 w-6 text-center cursor-pointer duration-500 hover:font-bold`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faAngleRight}/>
|
<FontAwesomeIcon icon={faAngleRight}/>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,11 +51,12 @@ const PaginationNumber = ({ page, totalPage }) => {
|
|||||||
function getPageElement (page, currentPage) {
|
function getPageElement (page, currentPage) {
|
||||||
return <Link href={page === 1 ? '/' : `/page/${page}`} key={page} passHref>
|
return <Link href={page === 1 ? '/' : `/page/${page}`} key={page} passHref>
|
||||||
<a className={(page + '' === currentPage + '' ? 'font-bold bg-blue-500 dark:bg-blue-400 text-white ' : 'border-t-2 duration-500 border-white hover:border-blue-400 ') +
|
<a className={(page + '' === currentPage + '' ? 'font-bold bg-blue-500 dark:bg-blue-400 text-white ' : 'border-t-2 duration-500 border-white hover:border-blue-400 ') +
|
||||||
' border-white dark:border-blue-700 dark:hover:border-blue-400 cursor-pointer w-6 text-center font-light hover:font-bold'}>
|
' 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}
|
{page}
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
|
|
||||||
function generatePages (page, currentPage, totalPage) {
|
function generatePages (page, currentPage, totalPage) {
|
||||||
const pages = []
|
const pages = []
|
||||||
const groupCount = 7 // 最多显示页签数
|
const groupCount = 7 // 最多显示页签数
|
||||||
|
|||||||
43
themes/Hexo/components/Progress.js
Normal file
43
themes/Hexo/components/Progress.js
Normal file
@@ -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 (
|
||||||
|
<div className="h-4 w-full shadow-2xl bg-gray-400 font-sans">
|
||||||
|
<div
|
||||||
|
className="h-4 bg-blue-600 duration-200"
|
||||||
|
style={{ width: `${percent}%` }}
|
||||||
|
>
|
||||||
|
{showPercent && (
|
||||||
|
<div className="text-right text-white text-xs">{percent}%</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Progress
|
||||||
@@ -46,19 +46,19 @@ const SearchInput = ({ currentTag, currentSearch, cRef }) => {
|
|||||||
<input
|
<input
|
||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
type='text'
|
type='text'
|
||||||
className={'w-full rounded-lg text-sm pl-2 transition focus:shadow-lg font-light leading-10 text-black bg-gray-100 dark:bg-gray-900 dark:text-white'}
|
className={'w-full rounded-lg bg-white text-sm pl-2 transition focus:shadow-lg font-light leading-10 text-black bg-gray-100'}
|
||||||
onKeyUp={handleKeyUp}
|
onKeyUp={handleKeyUp}
|
||||||
onChange={e => updateSearchKey(e.target.value)}
|
onChange={e => updateSearchKey(e.target.value)}
|
||||||
defaultValue={searchKey}
|
defaultValue={searchKey}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='-ml-8 cursor-pointer dark:bg-gray-600 dark:hover:bg-gray-800 float-right items-center justify-center py-2'
|
<div className='-ml-8 cursor-pointer dark:hover:bg-gray-800 float-right items-center justify-center py-2'
|
||||||
onClick={() => { handleSearch(searchKey) }}>
|
onClick={() => { handleSearch(searchKey) }}>
|
||||||
<FontAwesomeIcon spin={onLoading} icon={onLoading ? faSpinner : faSearch} className='hover:text-black transform duration-200 text-gray-500 cursor-pointer' />
|
<FontAwesomeIcon spin={onLoading} icon={onLoading ? faSpinner : faSearch} className='hover:text-black transform duration-200 text-gray-500 cursor-pointer' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(searchKey && searchKey.length &&
|
{(searchKey && searchKey.length &&
|
||||||
<div className='-ml-12 cursor-pointer dark:bg-gray-600 dark:hover:bg-gray-800 float-right items-center justify-center py-2'>
|
<div className='-ml-12 cursor-pointer dark:hover:bg-gray-800 float-right items-center justify-center py-2'>
|
||||||
<FontAwesomeIcon icon={faTimes} className='hover:text-black transform duration-200 text-gray-400 cursor-pointer' onClick={cleanSearch} />
|
<FontAwesomeIcon icon={faTimes} className='hover:text-black transform duration-200 text-gray-400 cursor-pointer' onClick={cleanSearch} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,55 +10,96 @@ import CategoryGroup from './CategoryGroup'
|
|||||||
import LatestPostsGroup from './LatestPostsGroup'
|
import LatestPostsGroup from './LatestPostsGroup'
|
||||||
import TagGroups from './TagGroups'
|
import TagGroups from './TagGroups'
|
||||||
import SocialButton from './SocialButton'
|
import SocialButton from './SocialButton'
|
||||||
|
import Catalog from './Catalog'
|
||||||
|
|
||||||
export default function SideRight (props) {
|
export default function SideRight (props) {
|
||||||
const { postCount, currentCategory, categories, latestPosts, tags, currentTag } = props
|
const {
|
||||||
return <div id='left' className='w-96 mx-4 space-y-4 hidden lg:block'>
|
post,
|
||||||
<Card>
|
postCount,
|
||||||
<div className='justify-center items-center flex hover:rotate-45 py-6 hover:scale-105 transform duration-200 cursor-pointer' onClick={ () => { Router.push('/') }}>
|
currentCategory,
|
||||||
<Image
|
categories,
|
||||||
alt={BLOG.AUTHOR}
|
latestPosts,
|
||||||
width={120}
|
tags,
|
||||||
height={120}
|
currentTag,
|
||||||
loading='lazy'
|
showCategory,
|
||||||
src='/avatar.jpg'
|
showTag
|
||||||
className='rounded-full'
|
} = props
|
||||||
/>
|
return (
|
||||||
</div>
|
<div className='w-80 space-y-4 hidden lg:block'>
|
||||||
<div className='text-center text-xl pb-4'>{BLOG.TITLE}</div>
|
<Card>
|
||||||
<SocialButton/>
|
<div
|
||||||
</Card>
|
className='justify-center items-center flex hover:rotate-45 py-6 hover:scale-105 transform duration-200 cursor-pointer'
|
||||||
<Card>
|
onClick={() => {
|
||||||
<MenuButtonGroup/>
|
Router.push('/')
|
||||||
<SearchInput/>
|
}}
|
||||||
</Card>
|
>
|
||||||
<Card>
|
<Image
|
||||||
<div className='text-xs font-light ml-2 mb-3 font-sans'>
|
alt={BLOG.AUTHOR}
|
||||||
<FontAwesomeIcon icon={faChartArea}/> 统计
|
width={120}
|
||||||
</div>
|
height={120}
|
||||||
<div className='text-xs font-sans font-light justify-center mx-6'>
|
loading='lazy'
|
||||||
|
src='/avatar.jpg'
|
||||||
|
className='rounded-full'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='text-center font-sans text-xl pb-4'>{BLOG.TITLE}</div>
|
||||||
|
<SocialButton />
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<MenuButtonGroup {...props}/>
|
||||||
|
<SearchInput />
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<div className='ml-2 mb-3 font-sans'>
|
||||||
|
<FontAwesomeIcon icon={faChartArea} /> 统计
|
||||||
|
</div>
|
||||||
|
<div className='text-xs font-sans font-light justify-center mx-7'>
|
||||||
<div className='inline'>
|
<div className='inline'>
|
||||||
<div className='flex justify-between'><div>文章数:</div> <div>{postCount}</div></div>
|
<div className='flex justify-between'>
|
||||||
|
<div>文章数:</div>
|
||||||
|
<div>{postCount}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden busuanzi_container_page_pv ml-2">
|
<div className='hidden busuanzi_container_page_pv ml-2'>
|
||||||
<div className='flex justify-between'><div>访问量:</div><div className="busuanzi_value_page_pv"/></div>
|
<div className='flex justify-between'>
|
||||||
|
<div>访问量:</div>
|
||||||
|
<div className='busuanzi_value_page_pv' />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden busuanzi_container_site_uv ml-2">
|
<div className='hidden busuanzi_container_site_uv ml-2'>
|
||||||
<div className='flex justify-between'><div>访客数:</div><div className="busuanzi_value_site_uv"/></div>
|
<div className='flex justify-between'>
|
||||||
|
<div>访客数:</div>
|
||||||
|
<div className='busuanzi_value_site_uv' />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
|
||||||
<div className='text-xs font-light ml-2 mb-1 font-sans'>
|
{showCategory && (
|
||||||
<FontAwesomeIcon icon={faTh}/> 分类
|
<Card>
|
||||||
</div>
|
<div className='ml-2 mb-1 font-sans'>
|
||||||
<CategoryGroup currentCategory={currentCategory} categories={categories}/>
|
<FontAwesomeIcon icon={faTh} /> 分类
|
||||||
</Card>
|
</div>
|
||||||
<Card>
|
<CategoryGroup
|
||||||
<TagGroups tags={tags} currentTag={currentTag}/>
|
currentCategory={currentCategory}
|
||||||
</Card>
|
categories={categories}
|
||||||
<Card>
|
/>
|
||||||
<LatestPostsGroup posts={latestPosts}/>
|
</Card>
|
||||||
</Card>
|
)}
|
||||||
</div>
|
{showTag && (
|
||||||
|
<Card>
|
||||||
|
<TagGroups tags={tags} currentTag={currentTag} />
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
{latestPosts && <Card>
|
||||||
|
<LatestPostsGroup posts={latestPosts} />
|
||||||
|
</Card>}
|
||||||
|
|
||||||
|
{post && post.toc && (
|
||||||
|
<Card className='sticky top-4'>
|
||||||
|
<Catalog toc={post.toc} />
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
42
themes/Hexo/components/TocDrawer.js
Normal file
42
themes/Hexo/components/TocDrawer.js
Normal file
@@ -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 <>
|
||||||
|
<div className='fixed top-0 right-0 z-40 '>
|
||||||
|
{/* 侧边菜单 */}
|
||||||
|
<div
|
||||||
|
className={(showDrawer ? 'animate__slideInRight ' : ' -mr-72 animate__slideOutRight') +
|
||||||
|
' shadow-card animate__animated animate__faster max-h-36 ' +
|
||||||
|
' w-60 duration-200 fixed right-8 bottom-24 rounded overflow-y-auto py-2 bg-white dark:bg-gray-600'}>
|
||||||
|
{post && <>
|
||||||
|
<div className='dark:text-gray-400 text-gray-600 dark:bg-gray-800'>
|
||||||
|
<Catalog toc={post.toc}/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 背景蒙版 */}
|
||||||
|
<div id='right-drawer-background' className={(showDrawer ? 'block' : 'hidden') + ' fixed top-0 left-0 z-30 w-full h-full'}
|
||||||
|
onClick={switchVisible} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
export default TocDrawer
|
||||||
24
themes/Hexo/components/TocDrawerButton.js
Normal file
24
themes/Hexo/components/TocDrawerButton.js
Normal file
@@ -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 (<div onClick={props.onClick} className='py-2 px-3 cursor-pointer dark:text-gray-200 text-center transform hover:scale-150 duration-200 flex justify-center items-center' title={locale.POST.TOP} >
|
||||||
|
<FontAwesomeIcon icon={faListOl}/>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TocDrawerButton
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useGlobal } from '@/lib/global'
|
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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import throttle from 'lodash.throttle'
|
import throttle from 'lodash.throttle'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import CategoryGroup from './CategoryGroup'
|
import CategoryGroup from './CategoryGroup'
|
||||||
import Collapse from './Collapse'
|
import Collapse from './Collapse'
|
||||||
import Logo from './Logo'
|
import Logo from './Logo'
|
||||||
@@ -11,6 +11,7 @@ import MenuButtonGroup from './MenuButtonGroup'
|
|||||||
import SearchDrawer from './SearchDrawer'
|
import SearchDrawer from './SearchDrawer'
|
||||||
import TagGroups from './TagGroups'
|
import TagGroups from './TagGroups'
|
||||||
import CONFIG_HEXO from '../config_hexo'
|
import CONFIG_HEXO from '../config_hexo'
|
||||||
|
import SearchInput from './SearchInput'
|
||||||
|
|
||||||
let windowTop = 0
|
let windowTop = 0
|
||||||
|
|
||||||
@@ -23,18 +24,18 @@ const TopNav = ({ tags, currentTag, categories, currentCategory, postCount }) =>
|
|||||||
const { locale } = useGlobal()
|
const { locale } = useGlobal()
|
||||||
const searchDrawer = useRef()
|
const searchDrawer = useRef()
|
||||||
|
|
||||||
const scrollTrigger = useCallback(throttle(() => {
|
const scrollTrigger = throttle(() => {
|
||||||
const scrollS = window.scrollY
|
const scrollS = window.scrollY
|
||||||
if (scrollS >= windowTop && scrollS > 10) {
|
if (scrollS >= windowTop && scrollS > 10) {
|
||||||
const nav = document.querySelector('#sticky-nav')
|
const nav = document.querySelector('#sticky-nav')
|
||||||
nav && nav.classList.replace('top-0', '-top-40')
|
nav && nav.classList.replace('top-0', '-top-16')
|
||||||
windowTop = scrollS
|
windowTop = scrollS
|
||||||
} else {
|
} else {
|
||||||
const nav = document.querySelector('#sticky-nav')
|
const nav = document.querySelector('#sticky-nav')
|
||||||
nav && nav.classList.replace('-top-40', 'top-0')
|
nav && nav.classList.replace('-top-16', 'top-0')
|
||||||
windowTop = scrollS
|
windowTop = scrollS
|
||||||
}
|
}
|
||||||
}, 200), [])
|
}, 200)
|
||||||
|
|
||||||
// 监听滚动
|
// 监听滚动
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -85,35 +86,29 @@ const TopNav = ({ tags, currentTag, categories, currentCategory, postCount }) =>
|
|||||||
) }
|
) }
|
||||||
</>
|
</>
|
||||||
|
|
||||||
return (<div id='top-nav' className='z-40 block lg:hidden'>
|
return (<div id='top-nav' className='z-40'>
|
||||||
<SearchDrawer cRef={searchDrawer} slot={searchDrawerSlot}/>
|
<SearchDrawer cRef={searchDrawer} slot={searchDrawerSlot}/>
|
||||||
|
|
||||||
{/* 导航栏 */}
|
{/* 导航栏 */}
|
||||||
<div id='sticky-nav' className={`${CONFIG_HEXO.NAV_TYPE !== 'normal' ? 'fixed' : ''} lg:relative w-full top-0 z-20 transform duration-500`}>
|
<div id='sticky-nav' className={`${CONFIG_HEXO.NAV_TYPE !== 'normal' ? 'fixed' : ''} w-full top-0 z-20 transform duration-500`}>
|
||||||
<div className='w-full flex justify-between items-center p-4 bg-black text-white'>
|
<div className='w-full flex justify-between items-center p-4 bg-black shadow-md bg-opacity-70 text-white'>
|
||||||
{/* 左侧LOGO 标题 */}
|
|
||||||
<div className='flex flex-none flex-grow-0'>
|
|
||||||
<div onClick={toggleMenuOpen} className='w-8 cursor-pointer'>
|
|
||||||
{ isOpen ? <FontAwesomeIcon icon={faTimes} size={'lg'}/> : <FontAwesomeIcon icon={faBars} size={'lg'}/> }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex'>
|
<div className='flex'>
|
||||||
<Logo/>
|
<Logo/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 右侧功能 */}
|
{/* 右侧功能 */}
|
||||||
<div className='mr-1 flex justify-end items-center text-sm space-x-4 font-serif dark:text-gray-200'>
|
<div className='mr-1 flex lg:hidden justify-end items-center text-sm space-x-4 font-serif dark:text-gray-200'>
|
||||||
<div className="cursor-pointer block lg:hidden" onClick={() => { searchDrawer?.current?.show() }}>
|
<div onClick={toggleMenuOpen} className='w-8 cursor-pointer'>
|
||||||
<FontAwesomeIcon icon={faSearch} className="mr-2" />{locale.NAV.SEARCH}
|
{ isOpen ? <FontAwesomeIcon icon={faTimes} size={'lg'}/> : <FontAwesomeIcon icon={faBars} size={'lg'}/> }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Collapse isOpen={isOpen}>
|
<Collapse isOpen={isOpen} className='shadow-xl'>
|
||||||
<div className='bg-white py-1 px-5'>
|
<div className='bg-white pt-1 py-2 px-5'>
|
||||||
<MenuButtonGroup postCount={postCount}/>
|
<MenuButtonGroup postCount={postCount}/>
|
||||||
</div>
|
<SearchInput/>
|
||||||
|
</div>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ const CONFIG_HEXO = {
|
|||||||
|
|
||||||
POST_LIST_COVER: true, // 文章封面
|
POST_LIST_COVER: true, // 文章封面
|
||||||
POST_LIST_SUMMARY: true, // 文章摘要
|
POST_LIST_SUMMARY: true, // 文章摘要
|
||||||
|
POST_LIST_PREVIEW: false,
|
||||||
NAV_TYPE: 'autoCollapse', // ['fixed','autoCollapse','normal'] 分别是固定屏幕顶部、屏幕顶部自动折叠,不固定
|
NAV_TYPE: 'autoCollapse', // ['fixed','autoCollapse','normal'] 分别是固定屏幕顶部、屏幕顶部自动折叠,不固定
|
||||||
WIDGET_TO_TOP: true
|
|
||||||
|
WIDGET_TO_TOP: true,
|
||||||
|
WIDGET_TOC: true // 移动端悬浮目录
|
||||||
}
|
}
|
||||||
export default CONFIG_HEXO
|
export default CONFIG_HEXO
|
||||||
|
|||||||
@@ -5,4 +5,4 @@
|
|||||||
// export * from './Empty' // 空主题
|
// export * from './Empty' // 空主题
|
||||||
// export * from './NEXT'
|
// export * from './NEXT'
|
||||||
// export * from './Fukasawa'
|
// export * from './Fukasawa'
|
||||||
export * from './Hexo' //
|
export * from './Hexo'
|
||||||
|
|||||||
Reference in New Issue
Block a user