feature: 调整Hexo 页面

This commit is contained in:
tangly1024
2022-01-25 16:42:29 +08:00
parent b1e7fd6f50
commit 13f53b2bad
29 changed files with 403 additions and 203 deletions

View File

@@ -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/

View File

@@ -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 (
<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>
{BLOG.COMMENT_CUSDIS_APP_ID && (<div key='Cusdis'>
<CusdisComponent
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()}
/>
<CusdisComponent id={frontMatter.id} url={BLOG.LINK + router.asPath} title={frontMatter.title} />
</div>)}
{BLOG.COMMENT_UTTERRANCES_REPO && (<div key='Utterance'>
<UtterancesComponent issueTerm={frontMatter.id} className='px-2' />
</div>
)}
{BLOG.COMMENT_GITALK_CLIENT_ID && (<div key='GitTalk'>
<GitalkComponent
options={{
@@ -57,10 +51,7 @@ const Comment = ({ frontMatter }) => {
}}
/>
</div>)}
{BLOG.COMMENT_UTTERRANCES_REPO && (<div key='Utterance'>
<UtterancesComponent issueTerm={frontMatter.id} className='px-2' />
</div>
)}
</Tabs>
</div>
)

View File

@@ -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 (
<div id="comments">
<div id="comments-cusdis">
<div
id="cusdis_thread"
data-host={BLOG.COMMENT_CUSDIS_HOST}
@@ -24,6 +21,7 @@ const Cusdis = ({ id, url, title }) => {
data-page-id={id}
data-page-url={url}
data-page-title={title}
lang={BLOG.LANG.toLowerCase()}
/>
</div>
)

View File

@@ -29,8 +29,7 @@ const Utterances = ({ issueTerm, layout }) => {
anchor.innerHTML = ''
}
})
return <div id="comments" >
<div className="utterances-frame h-auto w-auto"/>
return <div id="comments" className='utterances' >
</div>
}

View File

@@ -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',

View File

@@ -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列表为空')

View File

@@ -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
}

View File

@@ -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",

View File

@@ -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 <LayoutBase {...props} meta={meta}>
<h1>Slug - {post?.title}</h1>
<p>

View File

@@ -44,10 +44,10 @@ const LayoutBase = (props) => {
{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'>
{children}
<div id='container-inner' className='w-full mx-auto flex justify-between space-x-4 max-w-6xl'>
<div className='w-full'>{children}</div>
<SideRight {...props}/>
</div>

View File

@@ -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 = (
<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}")` }}>
if (post?.blockMap?.block) {
post.content = Object.keys(post.blockMap.block)
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">
<div>
{/* 文章Title */}
@@ -52,7 +61,8 @@ export const LayoutSlug = props => {
</Link>
<span className="mr-2">|</span>
{post.type[0] !== 'Page' && (<>
{post.type[0] !== 'Page' && (
<>
<Link
href={`/archive#${post?.date?.start_date?.substr(0, 7)}`}
passHref
@@ -61,11 +71,13 @@ export const LayoutSlug = props => {
{date}
</a>
</Link>
</>)}
</>
)}
<div className="hidden busuanzi_container_page_pv font-light mr-2">
<span className="mr-2">|</span>
<span className="mr-2 busuanzi_value_page_pv" />次访问
<span className="mr-2 busuanzi_value_page_pv" />
次访问
</div>
</div>
</section>
@@ -73,12 +85,39 @@ export const LayoutSlug = props => {
</header>
</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 (
<LayoutBase headerSlot={headerSlot} {...props} meta={meta}>
<Card className="w-full">
<LayoutBase
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} />
</Card>
</div>
<div className='block lg:hidden'>
<TocDrawer post={post} cRef={drawerRight} targetRef={targetRef} />
</div>
</LayoutBase>
)
}

View File

@@ -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"
className="subpixel-antialiased dark:border-gray-700 bg-white dark:bg-gray-800"
>
{/* Notion文章主体 */}
<section id='notion-article' className='px-1'>
<section id='notion-article' className='px-5'>
{post.blockMap && (
<NotionRenderer
recordMap={post.blockMap}
@@ -67,9 +67,10 @@ export default function ArticleDetail ({ post, recommendPosts, prev, next }) {
</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='text-2xl mt-8 mx-8'>发表评论</div>
<div className="duration-200 overflow-x-auto bg-white dark:bg-gray-800">
<Comment frontMatter={post} />
</div>
</div>)

View File

@@ -29,7 +29,7 @@ const BlogPostCard = ({ post, showSummary }) => {
</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}
</p>}

View File

@@ -5,7 +5,7 @@
* @constructor
*/
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>
</div>
}

View File

@@ -20,7 +20,7 @@ const BlogPostListPage = ({ page = 1, posts = [], postCount }) => {
return (
<div id="container" className='w-full'>
{/* 文章列表 */}
<div className="flex lg:space-y-4 space-y-1">
<div className="space-y-6">
{posts.map(post => (
<BlogPostCard key={post.id} post={post} />
))}

View 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

View File

@@ -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 (
<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}
</div>
)

View File

@@ -20,14 +20,14 @@ const LatestPostsGroup = ({ posts }) => {
const { locale } = useGlobal()
return <>
<div className='text-xs 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 className='font-sans mb-2 px-1 flex flex-nowrap justify-between'>
<div><FontAwesomeIcon icon={faArchive} className='mr-2' />{locale.COMMON.LATEST_POSTS}</div>
</div>
{posts.map(post => {
const selected = currentPath === `${BLOG.PATH}/article/${post.slug}`
return (
<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 ' +
'hover:text-white dark:hover:text-white cursor-pointer' }>
<FontAwesomeIcon icon={faFileAlt} className='mr-2'/>

View File

@@ -4,7 +4,7 @@ import React from 'react'
const Logo = () => {
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>
</Link>

View File

@@ -23,7 +23,7 @@ const MenuButtonGroup = ({ postCount }) => {
if (link.show) {
const selected = (router.pathname === link.to) || (router.asPath === 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' : ' ')} >
<div className='my-auto items-center justify-center flex '>
<FontAwesomeIcon icon={link.icon} />

View File

@@ -18,7 +18,7 @@ const PaginationNumber = ({ page, totalPage }) => {
const pages = generatePages(page, currentPage, totalPage)
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
@@ -27,7 +27,7 @@ const PaginationNumber = ({ page, totalPage }) => {
} } passHref >
<div
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}/>
</div>
@@ -39,7 +39,7 @@ const PaginationNumber = ({ page, totalPage }) => {
<Link href={ { pathname: `/page/${currentPage + 1}`, query: router.query.s ? { s: router.query.s } : {} } } passHref>
<div
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}/>
</div>
@@ -51,11 +51,12 @@ const PaginationNumber = ({ page, totalPage }) => {
function getPageElement (page, currentPage) {
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 ') +
' 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}
</a>
</Link>
}
function generatePages (page, currentPage, totalPage) {
const pages = []
const groupCount = 7 // 最多显示页签数

View 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

View File

@@ -46,19 +46,19 @@ const SearchInput = ({ currentTag, currentSearch, cRef }) => {
<input
ref={searchInputRef}
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}
onChange={e => updateSearchKey(e.target.value)}
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) }}>
<FontAwesomeIcon spin={onLoading} icon={onLoading ? faSpinner : faSearch} className='hover:text-black transform duration-200 text-gray-500 cursor-pointer' />
</div>
{(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} />
</div>
)}

View File

@@ -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 <div id='left' className='w-96 mx-4 space-y-4 hidden lg:block'>
<Card>
<div className='justify-center items-center flex hover:rotate-45 py-6 hover:scale-105 transform duration-200 cursor-pointer' onClick={ () => { Router.push('/') }}>
<Image
alt={BLOG.AUTHOR}
width={120}
height={120}
loading='lazy'
src='/avatar.jpg'
className='rounded-full'
/>
</div>
<div className='text-center text-xl pb-4'>{BLOG.TITLE}</div>
<SocialButton/>
</Card>
<Card>
<MenuButtonGroup/>
<SearchInput/>
</Card>
<Card>
<div className='text-xs font-light ml-2 mb-3 font-sans'>
<FontAwesomeIcon icon={faChartArea}/> 统计
</div>
<div className='text-xs font-sans font-light justify-center mx-6'>
const {
post,
postCount,
currentCategory,
categories,
latestPosts,
tags,
currentTag,
showCategory,
showTag
} = props
return (
<div className='w-80 space-y-4 hidden lg:block'>
<Card>
<div
className='justify-center items-center flex hover:rotate-45 py-6 hover:scale-105 transform duration-200 cursor-pointer'
onClick={() => {
Router.push('/')
}}
>
<Image
alt={BLOG.AUTHOR}
width={120}
height={120}
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='flex justify-between'><div>文章数:</div> <div>{postCount}</div></div>
<div className='flex justify-between'>
<div>文章数:</div>
<div>{postCount}</div>
</div>
</div>
<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='hidden busuanzi_container_page_pv ml-2'>
<div className='flex justify-between'>
<div>访问量:</div>
<div className='busuanzi_value_page_pv' />
</div>
</div>
<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='hidden busuanzi_container_site_uv ml-2'>
<div className='flex justify-between'>
<div>访客数:</div>
<div className='busuanzi_value_site_uv' />
</div>
</div>
</div>
</Card>
<Card>
<div className='text-xs font-light ml-2 mb-1 font-sans'>
<FontAwesomeIcon icon={faTh}/> 分类
</div>
<CategoryGroup currentCategory={currentCategory} categories={categories}/>
</Card>
<Card>
<TagGroups tags={tags} currentTag={currentTag}/>
</Card>
<Card>
<LatestPostsGroup posts={latestPosts}/>
</Card>
</div>
</Card>
{showCategory && (
<Card>
<div className='ml-2 mb-1 font-sans'>
<FontAwesomeIcon icon={faTh} /> 分类
</div>
<CategoryGroup
currentCategory={currentCategory}
categories={categories}
/>
</Card>
)}
{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>
)
}

View 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

View 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

View File

@@ -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 (<div id='top-nav' className='z-40 block lg:hidden'>
return (<div id='top-nav' className='z-40'>
<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 className='w-full flex justify-between items-center p-4 bg-black 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 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 shadow-md bg-opacity-70 text-white'>
<div className='flex'>
<Logo/>
</div>
{/* 右侧功能 */}
<div className='mr-1 flex 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() }}>
<FontAwesomeIcon icon={faSearch} className="mr-2" />{locale.NAV.SEARCH}
<div className='mr-1 flex lg:hidden justify-end items-center text-sm space-x-4 font-serif dark:text-gray-200'>
<div onClick={toggleMenuOpen} className='w-8 cursor-pointer'>
{ isOpen ? <FontAwesomeIcon icon={faTimes} size={'lg'}/> : <FontAwesomeIcon icon={faBars} size={'lg'}/> }
</div>
</div>
</div>
<Collapse isOpen={isOpen}>
<div className='bg-white py-1 px-5'>
<Collapse isOpen={isOpen} className='shadow-xl'>
<div className='bg-white pt-1 py-2 px-5'>
<MenuButtonGroup postCount={postCount}/>
</div>
<SearchInput/>
</div>
</Collapse>
</div>

View File

@@ -12,7 +12,10 @@ const CONFIG_HEXO = {
POST_LIST_COVER: true, // 文章封面
POST_LIST_SUMMARY: true, // 文章摘要
POST_LIST_PREVIEW: false,
NAV_TYPE: 'autoCollapse', // ['fixed','autoCollapse','normal'] 分别是固定屏幕顶部、屏幕顶部自动折叠,不固定
WIDGET_TO_TOP: true
WIDGET_TO_TOP: true,
WIDGET_TOC: true // 移动端悬浮目录
}
export default CONFIG_HEXO

View File

@@ -5,4 +5,4 @@
// export * from './Empty' // 空主题
// export * from './NEXT'
// export * from './Fukasawa'
export * from './Hexo' //
export * from './Hexo'