mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-06-02 07:26:45 +00:00
group-nav-page
This commit is contained in:
@@ -1,27 +1,25 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
|
||||
const BlogPostCard = ({ post, showSummary }) => {
|
||||
const BlogPostCard = ({ post }) => {
|
||||
const router = useRouter()
|
||||
const currentSelected = router.asPath.split('?')[0] === '/' + post.slug
|
||||
return (
|
||||
<div
|
||||
key={post.id}
|
||||
className="mb-6"
|
||||
>
|
||||
|
||||
<div key={post.id} className="py-0.5">
|
||||
<div className="flex flex-col w-full">
|
||||
<Link
|
||||
href={`${BLOG.SUB_PATH}/${post.slug}`}
|
||||
passHref
|
||||
className={
|
||||
'cursor-pointer font-bold hover:underline leading-tight text-gray-700 dark:text-gray-300 hover:text-green-500 dark:hover:text-green-400'
|
||||
`${currentSelected ? 'bg-gray-500 text-white' : 'text-gray-700 dark:text-gray-300 '} hover:font-bold py-0.5 px-1 text-sm cursor-pointer`
|
||||
}>
|
||||
<div>
|
||||
{post.category} - {post.title}
|
||||
</div>
|
||||
|
||||
</Link>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import BlogPostCard from './BlogPostCard'
|
||||
import BlogPostListEmpty from './BlogPostListEmpty'
|
||||
import throttle from 'lodash.throttle'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useRef } from 'react'
|
||||
|
||||
/**
|
||||
* 博客列表滚动分页
|
||||
@@ -13,85 +10,19 @@ import { useRouter } from 'next/router'
|
||||
* @constructor
|
||||
*/
|
||||
const BlogPostListScroll = ({ posts = [], currentSearch }) => {
|
||||
const postsPerPage = BLOG.POSTS_PER_PAGE
|
||||
const [page, updatePage] = useState(1)
|
||||
let filteredPosts = Object.assign(posts)
|
||||
const router = useRouter()
|
||||
const searchKey = getSearchKey(router)
|
||||
if (searchKey) {
|
||||
filteredPosts = posts.filter(post => {
|
||||
const tagContent = post.tags ? post.tags.join(' ') : ''
|
||||
const searchContent = post.title + post.summary + tagContent
|
||||
return searchContent.toLowerCase().includes(searchKey.toLowerCase())
|
||||
})
|
||||
}
|
||||
const postsToShow = getPostByPage(page, filteredPosts, postsPerPage)
|
||||
|
||||
let hasMore = false
|
||||
if (filteredPosts) {
|
||||
const totalCount = filteredPosts.length
|
||||
hasMore = page * postsPerPage < totalCount
|
||||
}
|
||||
|
||||
const handleGetMore = () => {
|
||||
if (!hasMore) return
|
||||
updatePage(page + 1)
|
||||
}
|
||||
|
||||
// 监听滚动自动分页加载
|
||||
const scrollTrigger = useCallback(throttle(() => {
|
||||
const scrollS = window.scrollY + window.outerHeight
|
||||
const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
|
||||
if (scrollS > clientHeight + 100) {
|
||||
handleGetMore()
|
||||
}
|
||||
}, 500))
|
||||
|
||||
// 监听滚动
|
||||
useEffect(() => {
|
||||
window.addEventListener('scroll', scrollTrigger)
|
||||
return () => {
|
||||
window.removeEventListener('scroll', scrollTrigger)
|
||||
}
|
||||
})
|
||||
|
||||
const targetRef = useRef(null)
|
||||
const filteredPosts = Object.assign(posts)
|
||||
|
||||
if (!postsToShow || postsToShow.length === 0) {
|
||||
if (!filteredPosts || filteredPosts.length === 0) {
|
||||
return <BlogPostListEmpty currentSearch={currentSearch} />
|
||||
} else {
|
||||
return <div id='container' ref={targetRef} className='w-full'>
|
||||
|
||||
{/* 文章列表 */}
|
||||
<div className='space-y-1 lg:space-y-4'>
|
||||
{postsToShow?.map(post => (
|
||||
<BlogPostCard key={post.id} post={post} showSummary={true} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{/* 文章列表 */}
|
||||
{filteredPosts?.map(post => (
|
||||
<BlogPostCard key={post.id} post={post} showSummary={true} />
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取从第1页到指定页码的文章
|
||||
* @param page 第几页
|
||||
* @param totalPosts 所有文章
|
||||
* @param postsPerPage 每页文章数量
|
||||
* @returns {*}
|
||||
*/
|
||||
const getPostByPage = function (page, totalPosts, postsPerPage) {
|
||||
return totalPosts.slice(
|
||||
0,
|
||||
postsPerPage * page
|
||||
)
|
||||
}
|
||||
|
||||
function getSearchKey(router) {
|
||||
if (router.query && router.query.s) {
|
||||
return router.query.s
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export default BlogPostListScroll
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import throttle from 'lodash.throttle'
|
||||
import { uuidToId } from 'notion-utils'
|
||||
import Progress from './Progress'
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
|
||||
/**
|
||||
* 目录导航组件
|
||||
@@ -9,11 +9,9 @@ import Progress from './Progress'
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const Catalog = ({ toc }) => {
|
||||
const Catalog = ({ post }) => {
|
||||
const tocIds = []
|
||||
|
||||
// 目录自动滚动
|
||||
const tRef = useRef(null)
|
||||
const toc = post?.toc
|
||||
// 同步选中目录事件
|
||||
const [activeSection, setActiveSection] = useState(null)
|
||||
|
||||
@@ -24,7 +22,7 @@ const Catalog = ({ toc }) => {
|
||||
return () => {
|
||||
window.removeEventListener('scroll', actionSectionScrollSpy)
|
||||
}
|
||||
}, [])
|
||||
}, [post])
|
||||
|
||||
const throttleMs = 200
|
||||
const actionSectionScrollSpy = useCallback(throttle(() => {
|
||||
@@ -51,19 +49,18 @@ const Catalog = ({ toc }) => {
|
||||
}
|
||||
setActiveSection(currentSectionId)
|
||||
const index = tocIds.indexOf(currentSectionId) || 0
|
||||
tRef?.current?.scrollTo({ top: 28 * index, behavior: 'smooth' })
|
||||
if (isBrowser()) {
|
||||
document?.getElementById('toc-wrapper')?.scrollTo({ top: 28 * index, behavior: 'smooth' })
|
||||
}
|
||||
}, throttleMs))
|
||||
|
||||
// 无目录就直接返回空
|
||||
if (!toc || toc.length < 1) {
|
||||
return <></>
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className='px-3'>
|
||||
<div className='w-full mt-2 mb-4'>
|
||||
<Progress />
|
||||
</div>
|
||||
<div className='overflow-y-auto max-h-96 overscroll-none scroll-hidden' ref={tRef}>
|
||||
return <>
|
||||
<div id='toc-wrapper' className='overflow-y-auto max-h-96 overscroll-none scroll-hidden'>
|
||||
<nav className='h-full text-black'>
|
||||
{toc.map((tocItem) => {
|
||||
const id = uuidToId(tocItem.id)
|
||||
@@ -76,7 +73,7 @@ const Catalog = ({ toc }) => {
|
||||
notion-table-of-contents-item-indent-level-${tocItem.indentLevel} `}
|
||||
>
|
||||
<span style={{ display: 'inline-block', marginLeft: tocItem.indentLevel * 16 }}
|
||||
className={`${activeSection === id && ' font-bold text-green-500 underline'}`}
|
||||
className={`${activeSection === id && ' font-bold text-gray-500 underline'}`}
|
||||
>
|
||||
{tocItem.text}
|
||||
</span>
|
||||
@@ -85,7 +82,7 @@ const Catalog = ({ toc }) => {
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
export default Catalog
|
||||
|
||||
@@ -5,7 +5,7 @@ import DarkModeButton from '@/components/DarkModeButton'
|
||||
const Footer = ({ title }) => {
|
||||
const d = new Date()
|
||||
const currentYear = d.getFullYear()
|
||||
const copyrightDate = (function() {
|
||||
const copyrightDate = (function () {
|
||||
if (Number.isInteger(BLOG.SINCE) && BLOG.SINCE < currentYear) {
|
||||
return BLOG.SINCE + '-' + currentYear
|
||||
}
|
||||
@@ -13,22 +13,26 @@ const Footer = ({ title }) => {
|
||||
})()
|
||||
|
||||
return (
|
||||
<footer
|
||||
className='z-10 dark:bg-hexo-black-gray flex-shrink-0 justify-center text-center mx-auto w-full leading-6 text-sm px-6 pb-6 relative'
|
||||
>
|
||||
<DarkModeButton/>
|
||||
<i className='fas fa-copyright' /> {`${copyrightDate}`} <span><i className='mx-1 animate-pulse fas fa-heart'/> <a href={BLOG.LINK} className='underline font-bold text-gray-500 dark:text-gray-300 '>{BLOG.AUTHOR}</a>.<br/>
|
||||
<footer
|
||||
className='max-w-3xl z-10 dark:bg-hexo-black-gray flex-shrink-0 justify-center text-center mx-auto w-full leading-6 text-sm px-6 pb-6 relative'
|
||||
>
|
||||
<hr />
|
||||
|
||||
{BLOG.BEI_AN && <><i className='fas fa-shield-alt'/> <a href='https://beian.miit.gov.cn/' className='mr-2'>{BLOG.BEI_AN}</a><br/></>}
|
||||
<div className='flex justify-between'>
|
||||
<div className='flex'>© {`${copyrightDate}`} <div><i className='mx-1 animate-pulse fas fa-heart' /> <a href={BLOG.LINK} className='underline font-bold text-gray-500 dark:text-gray-300 '>{BLOG.AUTHOR}</a>.<br /></div></div>
|
||||
<div className='text-xs font-serif'><a href='https://github.com/tangly1024/NotionNext' className='underline text-gray-500 dark:text-gray-300'>NotionNext {BLOG.VERSION}</a></div>
|
||||
</div>
|
||||
|
||||
<span className='hidden busuanzi_container_site_pv'>
|
||||
<i className='fas fa-eye'/><span className='px-1 busuanzi_value_site_pv'> </span> </span>
|
||||
<span className='pl-2 hidden busuanzi_container_site_uv'>
|
||||
<i className='fas fa-users'/> <span className='px-1 busuanzi_value_site_uv'> </span> </span>
|
||||
<br/>
|
||||
<h1>{title}</h1>
|
||||
<span className='text-xs font-serif'>Powered by <a href='https://github.com/tangly1024/NotionNext' className='underline text-gray-500 dark:text-gray-300'>NotionNext {BLOG.VERSION}</a>.</span></span>
|
||||
</footer>
|
||||
{BLOG.BEI_AN && <><i className='fas fa-shield-alt' /> <a href='https://beian.miit.gov.cn/' className='mr-2'>{BLOG.BEI_AN}</a><br /></>}
|
||||
|
||||
<span className='hidden busuanzi_container_site_pv'>
|
||||
<i className='fas fa-eye' /><span className='px-1 busuanzi_value_site_pv'> </span> </span>
|
||||
<span className='pl-2 hidden busuanzi_container_site_uv'>
|
||||
<i className='fas fa-users' /> <span className='px-1 busuanzi_value_site_uv'> </span> </span>
|
||||
<br />
|
||||
<h1>{title}</h1>
|
||||
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export const MenuItemDrop = ({ link }) => {
|
||||
return <li className='cursor-pointer list-none items-center flex mx-2' onMouseOver={() => changeShow(true)} onMouseOut={() => changeShow(false)} >
|
||||
|
||||
{hasSubMenu &&
|
||||
<div className={'px-3 h-full whitespace-nowrap duration-300 text-sm justify-between dark:text-gray-300 cursor-pointer flex flex-nowrap items-center ' +
|
||||
<div className={'px-2 h-full whitespace-nowrap duration-300 text-sm justify-between dark:text-gray-300 cursor-pointer flex flex-nowrap items-center ' +
|
||||
(selected ? 'bg-green-600 text-white hover:text-white' : 'hover:text-green-600')}>
|
||||
<div>
|
||||
{link?.icon && <i className={link?.icon} />} {link?.name}
|
||||
@@ -27,7 +27,7 @@ export const MenuItemDrop = ({ link }) => {
|
||||
}
|
||||
|
||||
{!hasSubMenu &&
|
||||
<div className={'px-3 h-full whitespace-nowrap duration-300 text-sm justify-between dark:text-gray-300 cursor-pointer flex flex-nowrap items-center ' +
|
||||
<div className={'px-2 h-full whitespace-nowrap duration-300 text-sm justify-between dark:text-gray-300 cursor-pointer flex flex-nowrap items-center ' +
|
||||
(selected ? 'bg-green-600 text-white hover:text-white' : 'hover:text-green-600')}>
|
||||
<Link href={link?.to}>
|
||||
{link?.icon && <i className={link?.icon} />} {link?.name}
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function RevolverMaps () {
|
||||
initRevolverMaps()
|
||||
changeLoad(true)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
return <div id="revolvermaps" className='p-4'/>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useImperativeHandle, useRef, useState } from 'react'
|
||||
import { useMediumGlobal } from '../LayoutBase'
|
||||
let lock = false
|
||||
|
||||
const SearchInput = ({ currentTag, currentSearch, cRef, className }) => {
|
||||
const [onLoading, setLoadingState] = useState(false)
|
||||
const router = useRouter()
|
||||
const SearchInput = ({ currentSearch, cRef, className }) => {
|
||||
const searchInputRef = useRef()
|
||||
const { setFilterPosts, allNavPages } = useMediumGlobal()
|
||||
|
||||
useImperativeHandle(cRef, () => {
|
||||
return {
|
||||
focus: () => {
|
||||
@@ -15,15 +15,43 @@ const SearchInput = ({ currentTag, currentSearch, cRef, className }) => {
|
||||
})
|
||||
|
||||
const handleSearch = () => {
|
||||
const key = searchInputRef.current.value
|
||||
|
||||
if (key && key !== '') {
|
||||
setLoadingState(true)
|
||||
location.href = '/search/' + key
|
||||
let keyword = searchInputRef.current.value
|
||||
const filterPosts = []
|
||||
if (keyword) {
|
||||
keyword = keyword.trim()
|
||||
} else {
|
||||
router.push({ pathname: '/' }).then(r => {
|
||||
})
|
||||
setFilterPosts(allNavPages)
|
||||
}
|
||||
for (const post of allNavPages) {
|
||||
const tagContent = post.tags && Array.isArray(post.tags) ? post.tags.join(' ') : ''
|
||||
const categoryContent = post.category && Array.isArray(post.category) ? post.category.join(' ') : ''
|
||||
const articleInfo = post.title + post.summary + tagContent + categoryContent
|
||||
let hit = articleInfo.toLowerCase().indexOf(keyword) > -1
|
||||
const indexContent = [post.summary]
|
||||
// console.log('全文搜索缓存', cacheKey, page != null)
|
||||
post.results = []
|
||||
let hitCount = 0
|
||||
for (const i in indexContent) {
|
||||
const c = indexContent[i]
|
||||
if (!c) {
|
||||
continue
|
||||
}
|
||||
const index = c.toLowerCase().indexOf(keyword.toLowerCase())
|
||||
if (index > -1) {
|
||||
hit = true
|
||||
hitCount += 1
|
||||
post.results.push(c)
|
||||
} else {
|
||||
if ((post.results.length - 1) / hitCount < 3 || i === 0) {
|
||||
post.results.push(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hit) {
|
||||
filterPosts.push(post)
|
||||
}
|
||||
}
|
||||
setFilterPosts(filterPosts)
|
||||
}
|
||||
const handleKeyUp = (e) => {
|
||||
if (e.keyCode === 13) { // 回车
|
||||
@@ -72,7 +100,7 @@ const SearchInput = ({ currentTag, currentSearch, cRef, className }) => {
|
||||
|
||||
<div className='-ml-8 cursor-pointer float-right items-center justify-center py-2'
|
||||
onClick={handleSearch}>
|
||||
<i className={`hover:text-black transform duration-200 text-gray-500 dark:hover:text-gray-300 cursor-pointer fas ${onLoading ? 'fa-spinner animate-spin' : 'fa-search'} `} />
|
||||
<i className={'hover:text-black transform duration-200 text-gray-500 dark:hover:text-gray-300 cursor-pointer fas fa-search'} />
|
||||
</div>
|
||||
|
||||
{(showClean &&
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useGlobal } from '@/lib/global'
|
||||
import CONFIG_MEDIUM from '../config_medium'
|
||||
import BLOG from '@/blog.config'
|
||||
import { MenuItemDrop } from './MenuItemDrop'
|
||||
import DarkModeButton from '@/components/DarkModeButton'
|
||||
|
||||
/**
|
||||
* 顶部导航栏 + 菜单
|
||||
@@ -67,6 +68,7 @@ export default function TopNavBar(props) {
|
||||
{/* 桌面端顶部菜单 */}
|
||||
<div className='hidden md:flex'>
|
||||
{links && links?.map(link => <MenuItemDrop key={link?.id} link={link}/>)}
|
||||
<DarkModeButton className='text-sm flex items-center h-full' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user