diff --git a/components/AlgoliaSearchModal.js b/components/AlgoliaSearchModal.js index ea727a48..eac9d8bf 100644 --- a/components/AlgoliaSearchModal.js +++ b/components/AlgoliaSearchModal.js @@ -1,11 +1,18 @@ -import { useState, useImperativeHandle, useRef, useEffect, Fragment } from 'react' -import algoliasearch from 'algoliasearch' import replaceSearchResult from '@/components/Mark' -import Link from 'next/link' -import { useGlobal } from '@/lib/global' -import throttle from 'lodash/throttle' import { siteConfig } from '@/lib/config' -import { useHotkeys } from 'react-hotkeys-hook'; +import { useGlobal } from '@/lib/global' +import algoliasearch from 'algoliasearch' +import throttle from 'lodash/throttle' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { + Fragment, + useEffect, + useImperativeHandle, + useRef, + useState +} from 'react' +import { useHotkeys } from 'react-hotkeys-hook' const ShortCutActions = [ { @@ -20,7 +27,6 @@ const ShortCutActions = [ key: 'Esc', action: '关闭' } - ] /** @@ -36,31 +42,49 @@ export default function AlgoliaSearchModal({ cRef }) { const [totalPage, setTotalPage] = useState(0) const [totalHit, setTotalHit] = useState(0) const [useTime, setUseTime] = useState(0) - const inputRef = useRef(null) const [activeIndex, setActiveIndex] = useState(0) const [isLoading, setIsLoading] = useState(false) - useHotkeys('ctrl+k', (e) => { + const inputRef = useRef(null) + const router = useRouter() + + /** + * 快捷键设置 + */ + useHotkeys('ctrl+k', e => { e.preventDefault() setIsModalOpen(true) }) // 方向键调整选中 - useHotkeys('down', (e) => { - e.preventDefault() - if (activeIndex < searchResults.length - 1) { - setActiveIndex(activeIndex + 1) - } - }, { enableOnFormTags: true }) - useHotkeys('up', (e) => { - e.preventDefault() - if (activeIndex > 0) { - setActiveIndex(activeIndex - 1) - } - }, { enableOnFormTags: true }) + useHotkeys( + 'down', + e => { + e.preventDefault() + if (activeIndex < searchResults.length - 1) { + setActiveIndex(activeIndex + 1) + } + }, + { enableOnFormTags: true } + ) + useHotkeys( + 'up', + e => { + e.preventDefault() + if (activeIndex > 0) { + setActiveIndex(activeIndex - 1) + } + }, + { enableOnFormTags: true } + ) // esc关闭 - useHotkeys('esc', (e) => { - e.preventDefault() - setIsModalOpen(false) - }, { enableOnFormTags: true }) + useHotkeys( + 'esc', + e => { + e.preventDefault() + setIsModalOpen(false) + }, + { enableOnFormTags: true } + ) + // 跳转Search结果 const onJumpSearchResult = () => { if (searchResults.length > 0) { @@ -68,11 +92,15 @@ export default function AlgoliaSearchModal({ cRef }) { } } // enter跳转 - useHotkeys('enter', (e) => { - if (searchResults.length > 0) { - onJumpSearchResult(index) - } - }, { enableOnFormTags: true }) + useHotkeys( + 'enter', + e => { + if (searchResults.length > 0) { + onJumpSearchResult(index) + } + }, + { enableOnFormTags: true } + ) const resetSearch = () => { setActiveIndex(0) @@ -84,6 +112,16 @@ export default function AlgoliaSearchModal({ cRef }) { if (inputRef.current) inputRef.current.value = '' } + /** + * 页面路径变化后,自动关闭此modal + */ + useEffect(() => { + setIsModalOpen(false) + }, [router]) + + /** + * 自动聚焦搜索框 + */ useEffect(() => { if (isModalOpen) { setTimeout(() => { @@ -93,9 +131,10 @@ export default function AlgoliaSearchModal({ cRef }) { resetSearch() } }, [isModalOpen]) + /** - * 对外暴露方法 - */ + * 对外暴露方法 + **/ useImperativeHandle(cRef, () => { return { openSearch: () => { @@ -104,7 +143,10 @@ export default function AlgoliaSearchModal({ cRef }) { } }) - const client = algoliasearch(siteConfig('ALGOLIA_APP_ID'), siteConfig('ALGOLIA_SEARCH_ONLY_APP_KEY')) + const client = algoliasearch( + siteConfig('ALGOLIA_APP_ID'), + siteConfig('ALGOLIA_SEARCH_ONLY_APP_KEY') + ) const index = client.initIndex(siteConfig('ALGOLIA_INDEX')) /** @@ -131,7 +173,9 @@ export default function AlgoliaSearchModal({ cRef }) { setTotalHit(nbHits) setSearchResults(hits) setIsLoading(false) - const doms = document.getElementById('search-wrapper').getElementsByClassName('replace') + const doms = document + .getElementById('search-wrapper') + .getElementsByClassName('replace') setTimeout(() => { replaceSearchResult({ @@ -149,33 +193,35 @@ export default function AlgoliaSearchModal({ cRef }) { } // 定义节流函数,确保在用户停止输入一段时间后才会调用处理搜索的方法 - const throttledHandleInputChange = useRef(throttle((query, page = 0) => { - handleSearch(query, page); - }, 1000)); + const throttledHandleInputChange = useRef( + throttle((query, page = 0) => { + handleSearch(query, page) + }, 1000) + ) // 用于存储搜索延迟的计时器 - const searchTimer = useRef(null); + const searchTimer = useRef(null) // 修改input的onChange事件处理函数 - const handleInputChange = (e) => { - const query = e.target.value; + const handleInputChange = e => { + const query = e.target.value // 如果已经有计时器在等待搜索,先清除之前的计时器 if (searchTimer.current) { - clearTimeout(searchTimer.current); + clearTimeout(searchTimer.current) } // 设置新的计时器,在用户停止输入一段时间后触发搜索 searchTimer.current = setTimeout(() => { - throttledHandleInputChange.current(query); - }, 800); - }; + throttledHandleInputChange.current(query) + }, 800) + } /** * 切换页码 * @param {*} page */ - const switchPage = (page) => { + const switchPage = page => { throttledHandleInputChange.current(keyword, page) } @@ -191,58 +237,58 @@ export default function AlgoliaSearchModal({ cRef }) { } return (
+ id='search-wrapper' + className={`${ + isModalOpen ? 'opacity-100' : 'invisible opacity-0 pointer-events-none' + } z-30 fixed h-screen w-screen left-0 top-0 sm:mt-12 flex items-start justify-center mt-0`}> {/* 模态框 */}
-
-
搜索
+ className={`${ + isModalOpen ? 'opacity-100' : 'invisible opacity-0 translate-y-10' + } flex flex-col justify-between w-full min-h-[10rem] h-full md:h-fit max-w-xl dark:bg-hexo-black-gray dark:border-gray-800 bg-white dark:bg- p-5 rounded-lg z-50 shadow border hover:border-blue-600 duration-300 transition-all `}> +
+
+ 搜索 +
+ className='text-gray-600 fa-solid fa-xmark p-1 cursor-pointer hover:text-blue-600' + onClick={closeModal}>
handleInputChange(e)} - className="text-black dark:text-gray-200 bg-gray-50 dark:bg-gray-600 outline-blue-500 w-full px-4 my-2 py-1 mb-4 border rounded-md" + className='text-black dark:text-gray-200 bg-gray-50 dark:bg-gray-600 outline-blue-500 w-full px-4 my-2 py-1 mb-4 border rounded-md' ref={inputRef} /> {/* 标签组 */} -
+
- { - searchResults.length === 0 && keyword && !isLoading && ( -
-

无法找到相关结果 - "{keyword}"

-
- ) - } + {searchResults.length === 0 && keyword && !isLoading && ( +
+

+ {' '} + 无法找到相关结果 + "{keyword}" +

+
+ )}
- {totalHit === 0 && (
- { - ShortCutActions.map((action, index) => { - return
{action.key}
- {action.action}
- }) - } -
) - } + {totalHit === 0 && ( +
+ {ShortCutActions.map((action, index) => { + return ( + +
+ {action.key} +
+ + {action.action} + +
+ ) + })} +
+ )}
{totalHit > 0 && (

@@ -266,19 +319,18 @@ export default function AlgoliaSearchModal({ cRef }) {

)}
-
- - Algolia 提供搜索服务 +
+ + Algolia 提供搜索服务
-
{/* 遮罩 */}
) @@ -292,21 +344,31 @@ function TagGroups() { // 获取tagOptions数组前十个 const firstTenTags = tagOptions?.slice(0, 10) - return
- { - firstTenTags?.map((tag, index) => { - return -
-
{tag.name}
{tag.count ? {tag.count} : <>} -
- - - }) - } -
+ return ( +
+ {firstTenTags?.map((tag, index) => { + return ( + +
+
{tag.name}
+ {tag.count ? ( + {tag.count} + ) : ( + <> + )} +
+ + ) + })} +
+ ) } /** @@ -321,16 +383,16 @@ function Pagination(props) { return (
{Array.from({ length: totalPage }, (_, i) => { - const classNames = page === i - ? 'font-bold text-white bg-blue-600 dark:bg-yellow-600 rounded' - : 'hover:text-blue-600 hover:font-bold dark:text-gray-300' + const classNames = + page === i + ? 'font-bold text-white bg-blue-600 dark:bg-yellow-600 rounded' + : 'hover:text-blue-600 hover:font-bold dark:text-gray-300' return (
switchPage(i)} className={`text-center cursor-pointer w-6 h-6 ${classNames}`} - key={i} - > + key={i}> {i + 1}
) diff --git a/components/CustomContextMenu.js b/components/CustomContextMenu.js index 46ffb2a0..229d830f 100644 --- a/components/CustomContextMenu.js +++ b/components/CustomContextMenu.js @@ -1,10 +1,10 @@ -import Link from 'next/link' -import { useRouter } from 'next/router' -import { useEffect, useState, useRef, useLayoutEffect } from 'react' -import { useGlobal } from '@/lib/global' -import { saveDarkModeToLocalStorage, THEMES } from '@/themes/theme' import useWindowSize from '@/hooks/useWindowSize' import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import { THEMES, saveDarkModeToLocalStorage } from '@/themes/theme' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useEffect, useLayoutEffect, useRef, useState } from 'react' /** * 自定义右键菜单 @@ -20,14 +20,14 @@ export default function CustomContextMenu(props) { const [width, setWidth] = useState(0) const [height, setHeight] = useState(0) - const { latestPosts } = props + const { allNavPages } = props const router = useRouter() /** * 随机跳转文章 */ function handleJumpToRandomPost() { - const randomIndex = Math.floor(Math.random() * latestPosts.length) - const randomPost = latestPosts[randomIndex] + const randomIndex = Math.floor(Math.random() * allNavPages.length) + const randomPost = allNavPages[randomIndex] router.push(`${siteConfig('SUB_PATH', '')}/${randomPost?.slug}`) } @@ -37,16 +37,26 @@ export default function CustomContextMenu(props) { }, []) useEffect(() => { - const handleContextMenu = (event) => { + setShow(false) + }, [router]) + + useEffect(() => { + const handleContextMenu = event => { event.preventDefault() // 计算点击位置加菜单宽高是否超出屏幕,如果超出则贴边弹出 - const x = (event.clientX < windowSize.width - width) ? event.clientX : windowSize.width - width - const y = (event.clientY < windowSize.height - height) ? event.clientY : windowSize.height - height + const x = + event.clientX < windowSize.width - width + ? event.clientX + : windowSize.width - width + const y = + event.clientY < windowSize.height - height + ? event.clientY + : windowSize.height - height setPosition({ y: `${y}px`, x: `${x}px` }) setShow(true) } - const handleClick = (event) => { + const handleClick = event => { if (menuRef.current && !menuRef.current.contains(event.target)) { setShow(false) } @@ -80,19 +90,20 @@ export default function CustomContextMenu(props) { function handleCopyLink() { const url = window.location.href - navigator.clipboard.writeText(url) + navigator.clipboard + .writeText(url) .then(() => { console.log('页面地址已复制') }) - .catch((error) => { + .catch(error => { console.error('复制页面地址失败:', error) }) setShow(false) } /** - * 切换主题 - */ + * 切换主题 + */ function handleChangeTheme() { const randomTheme = THEMES[Math.floor(Math.random() * THEMES.length)] // 从THEMES数组中 随机取一个主题 const query = router.query @@ -104,14 +115,14 @@ export default function CustomContextMenu(props) { * 复制内容 */ function handleCopy() { - const selectedText = document.getSelection().toString(); + const selectedText = document.getSelection().toString() if (selectedText) { - const tempInput = document.createElement('input'); - tempInput.value = selectedText; - document.body.appendChild(tempInput); - tempInput.select(); - document.execCommand('copy'); - document.body.removeChild(tempInput); + const tempInput = document.createElement('input') + tempInput.value = selectedText + document.body.appendChild(tempInput) + tempInput.select() + document.execCommand('copy') + document.body.removeChild(tempInput) // alert("Text copied: " + selectedText); } else { // alert("Please select some text first."); @@ -130,76 +141,119 @@ export default function CustomContextMenu(props) { } return ( -
+
+ {/* 菜单内容 */} +
+ {/* 顶部导航按钮 */} +
+ + + + +
- {/* 菜单内容 */} -
- {/* 顶部导航按钮 */} -
- - - - -
- -
- - {/* 跳转导航按钮 */} -
- - {siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_RANDOM_POST') &&
- -
{locale.MENU.WALK_AROUND}
-
} - - {siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_CATEGORY') && - -
{locale.MENU.CATEGORY}
- } - - {siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_TAG') && - -
{locale.MENU.TAGS}
- } - -
- -
- - {/* 功能按钮 */} -
- - {siteConfig('CAN_COPY') && ( -
- -
{locale.MENU.COPY}
-
- )} - - {siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_SHARE_LINK') &&
- -
{locale.MENU.SHARE_URL}
-
} - - {siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_DARK_MODE') &&
- {isDarkMode ? : } -
{isDarkMode ? locale.MENU.LIGHT_MODE : locale.MENU.DARK_MODE}
-
} - - {siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH') && ( -
- -
{locale.MENU.THEME_SWITCH}
-
- )} - -
+
+ {/* 跳转导航按钮 */} +
+ {siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_RANDOM_POST') && ( +
+ +
{locale.MENU.WALK_AROUND}
-
+ )} + + {siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_CATEGORY') && ( + + +
{locale.MENU.CATEGORY}
+ + )} + + {siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_TAG') && ( + + +
{locale.MENU.TAGS}
+ + )} +
+ +
+ + {/* 功能按钮 */} +
+ {siteConfig('CAN_COPY') && ( +
+ +
{locale.MENU.COPY}
+
+ )} + + {siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_SHARE_LINK') && ( +
+ +
{locale.MENU.SHARE_URL}
+
+ )} + + {siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_DARK_MODE') && ( +
+ {isDarkMode ? ( + + ) : ( + + )} +
+ {' '} + {isDarkMode ? locale.MENU.LIGHT_MODE : locale.MENU.DARK_MODE} +
+
+ )} + + {siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH') && ( +
+ +
+ {locale.MENU.THEME_SWITCH} +
+
+ )} +
+
+
) } diff --git a/lib/plugins/algolia.js b/lib/plugins/algolia.js index fe2f9a83..d161bf58 100644 --- a/lib/plugins/algolia.js +++ b/lib/plugins/algolia.js @@ -6,7 +6,7 @@ import algoliasearch from 'algoliasearch' * 生成全文索引 * @param {*} allPages */ -const generateAlgoliaSearch = async({ allPages, force = false }) => { +const generateAlgoliaSearch = async ({ allPages, force = false }) => { allPages?.forEach(p => { // 判断这篇文章是否需要重新创建索引 if (p && !p.password) { @@ -19,7 +19,7 @@ const generateAlgoliaSearch = async({ allPages, force = false }) => { * 上传数据 * 根据上次修改文章日期和上次更新索引数据判断是否需要更新algolia索引 */ -const uploadDataToAlgolia = async(post) => { +const uploadDataToAlgolia = async post => { // Connect and authenticate with your Algolia app const client = algoliasearch(BLOG.ALGOLIA_APP_ID, BLOG.ALGOLIA_ADMIN_APP_KEY) @@ -61,14 +61,18 @@ const uploadDataToAlgolia = async(post) => { summary: post.summary, lastEditedDate: post.lastEditedDate, // 更新文章时间 lastIndexDate: new Date(), // 更新索引时间 - content: truncate(getPageContentText(post, post.blockMap), 9000) // 索引9000个字节,因为api限制总请求内容上限1万个字节 + content: truncate(getPageContentText(post, post.blockMap), 8192) // 索引8192个字符,API限制总请求内容上限1万个字节 } // console.log('更新Algolia索引', record) - index.saveObject(record).wait().then(r => { - console.log('Algolia索引更新', r) - }).catch(err => { - console.log('Algolia异常', err) - }) + index + .saveObject(record) + .wait() + .then(r => { + console.log('Algolia索引更新', r) + }) + .catch(err => { + console.log('Algolia异常', err) + }) } } diff --git a/styles/notion.css b/styles/notion.css index 0991011e..e5f8c14f 100644 --- a/styles/notion.css +++ b/styles/notion.css @@ -523,8 +523,10 @@ summary > .notion-h { .notion-page { /* width: var(--notion-max-width); */ width: 100% !important; - padding-left: calc(min(12px, 8vw)); - padding-right: calc(min(12px, 8vw)); + padding-left: 0px !important; + padding-right: 0px !important; + /* padding-left: calc(min(12px, 8vw)); */ + /* padding-right: calc(min(12px, 8vw)); */ } .notion-full-width { diff --git a/themes/heo/components/ArticleAdjacent.js b/themes/heo/components/ArticleAdjacent.js deleted file mode 100644 index d905be08..00000000 --- a/themes/heo/components/ArticleAdjacent.js +++ /dev/null @@ -1,87 +0,0 @@ -import Link from 'next/link' -import { useRouter } from 'next/router' -import { useEffect, useState } from 'react' -import CONFIG from '../config' -import { siteConfig } from '@/lib/config' - -/** - * 上一篇,下一篇文章 - * @param {prev,next} param0 - * @returns - */ -export default function ArticleAdjacent({ prev, next }) { - const [isScrollEnd, setIsScrollEnd] = useState(false) - const router = useRouter() - - useEffect(() => { - setIsScrollEnd(false) - }, [router]) - - useEffect(() => { - // 文章是否已经到了底部 - const targetElement = document.getElementById('article-end') - - const handleIntersect = (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - setIsScrollEnd(true) - } - }) - } - - const options = { - root: null, - rootMargin: '0px', - threshold: 0.1 - } - - const observer = new IntersectionObserver(handleIntersect, options) - observer.observe(targetElement) - - return () => { - observer.disconnect() - } - }, []) - - if (!prev || !next || !siteConfig('HEO_ARTICLE_ADJACENT', null, CONFIG)) { - return <> - } - - return ( -
- {/* 移动端 */} -
- -
上一篇
-
{prev.title}
- - -
下一篇
-
{next.title}
- -
- - {/* 桌面端 */} - -
- -
下一篇
-
-
{next?.title}
- -
- -
- ) -} diff --git a/themes/heo/components/Header.js b/themes/heo/components/Header.js new file mode 100644 index 00000000..35b6f058 --- /dev/null +++ b/themes/heo/components/Header.js @@ -0,0 +1,187 @@ +import { siteConfig } from '@/lib/config' +import { isBrowser } from '@/lib/utils' +import throttle from 'lodash.throttle' +import { useCallback, useEffect, useRef, useState } from 'react' +import DarkModeButton from './DarkModeButton' +import Logo from './Logo' +import { MenuListTop } from './MenuListTop' +import RandomPostButton from './RandomPostButton' +import ReadingProgress from './ReadingProgress' +import SearchButton from './SearchButton' +import SlideOver from './SlideOver' + +/** + * 页头:顶部导航 + * @param {*} param0 + * @returns + */ +const Header = props => { + const [fixedNav, setFixedNav] = useState(false) + const [textWhite, setTextWhite] = useState(false) + const [navBgWhite, setBgWhite] = useState(false) + const [activeIndex, setActiveIndex] = useState(0) + + const slideOverRef = useRef() + + const toggleMenuOpen = () => { + slideOverRef?.current?.toggleSlideOvers() + } + + /** + * 根据滚动条,切换导航栏样式 + */ + const scrollTrigger = useCallback( + throttle(() => { + const scrollS = window.scrollY + // 导航栏设置 白色背景 + if (scrollS <= 0) { + setFixedNav(false) + setBgWhite(false) + + // 文章详情页特殊处理 + if (document.querySelector('#post-bg')) { + setFixedNav(true) + setTextWhite(true) + setBgWhite(false) + } + } else { + // 向下滚动后的导航样式 + setFixedNav(true) + setTextWhite(false) + setBgWhite(true) + } + }, 200) + ) + + // 监听滚动 + useEffect(() => { + scrollTrigger() + window.addEventListener('scroll', scrollTrigger) + return () => { + window.removeEventListener('scroll', scrollTrigger) + } + }, []) + + // 监听导航栏显示文字 + useEffect(() => { + let prevScrollY = 0 + let ticking = false + + const handleScroll = () => { + if (!ticking) { + window.requestAnimationFrame(() => { + const currentScrollY = window.scrollY + + if (currentScrollY > prevScrollY) { + setActiveIndex(1) // 向下滚动时设置activeIndex为1 + } else { + setActiveIndex(0) // 向上滚动时设置activeIndex为0 + } + + prevScrollY = currentScrollY + ticking = false + }) + + ticking = true + } + } + + if (isBrowser) { + window.addEventListener('scroll', handleScroll) + } + + return () => { + if (isBrowser) { + window.removeEventListener('scroll', handleScroll) + } + } + }, []) + + return ( + <> + + + {/* 顶部导航菜单栏 */} + + + ) +} + +export default Header diff --git a/themes/heo/components/Hero.js b/themes/heo/components/Hero.js index d5462707..656bca55 100644 --- a/themes/heo/components/Hero.js +++ b/themes/heo/components/Hero.js @@ -62,13 +62,13 @@ function BannerGroup(props) { */ function Banner(props) { const router = useRouter() - const { latestPosts } = props + const { allNavPages } = props /** * 随机跳转文章 */ function handleClickBanner() { - const randomIndex = Math.floor(Math.random() * latestPosts.length) - const randomPost = latestPosts[randomIndex] + const randomIndex = Math.floor(Math.random() * allNavPages.length) + const randomPost = allNavPages[randomIndex] router.push(`${siteConfig('SUB_PATH', '')}/${randomPost?.slug}`) } diff --git a/themes/heo/components/NavBar.js b/themes/heo/components/NavBar.js deleted file mode 100644 index f244c742..00000000 --- a/themes/heo/components/NavBar.js +++ /dev/null @@ -1,170 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from 'react' -import Logo from './Logo' -import throttle from 'lodash.throttle' -import RandomPostButton from './RandomPostButton' -import SearchButton from './SearchButton' -import DarkModeButton from './DarkModeButton' -import SlideOver from './SlideOver' -import ReadingProgress from './ReadingProgress' -import { MenuListTop } from './MenuListTop' -import { isBrowser } from '@/lib/utils' -import { siteConfig } from '@/lib/config' - -/** - * 顶部导航 - * @param {*} param0 - * @returns - */ -const NavBar = props => { - const [fixedNav, setFixedNav] = useState(false) - const [textWhite, setTextWhite] = useState(false) - const [navBgWhite, setBgWhite] = useState(false) - - const [activeIndex, setActiveIndex] = useState(0) - - const slideOverRef = useRef() - - const toggleMenuOpen = () => { - slideOverRef?.current?.toggleSlideOvers() - } - - /** - * 根据滚动条,切换导航栏样式 - */ - const scrollTrigger = useCallback(throttle(() => { - const scrollS = window.scrollY - // 导航栏设置 白色背景 - if (scrollS <= 0) { - setFixedNav(false) - setBgWhite(false) - - // 文章详情页特殊处理 - if (document.querySelector('#post-bg')) { - setFixedNav(true) - setTextWhite(true) - setBgWhite(false) - } - } else { - // 向下滚动后的导航样式 - setFixedNav(true) - setTextWhite(false) - setBgWhite(true) - } - }, 200)) - - // 监听滚动 - useEffect(() => { - scrollTrigger() - window.addEventListener('scroll', scrollTrigger) - return () => { - window.removeEventListener('scroll', scrollTrigger) - } - }, []) - - // 监听导航栏显示文字 - useEffect(() => { - let prevScrollY = 0 - let ticking = false - - const handleScroll = () => { - if (!ticking) { - window.requestAnimationFrame(() => { - const currentScrollY = window.scrollY - - if (currentScrollY > prevScrollY) { - setActiveIndex(1) // 向下滚动时设置activeIndex为1 - } else { - setActiveIndex(0) // 向上滚动时设置activeIndex为0 - } - - prevScrollY = currentScrollY - ticking = false - }) - - ticking = true - } - } - - if (isBrowser) { - window.addEventListener('scroll', handleScroll) - } - - return () => { - if (isBrowser) { - window.removeEventListener('scroll', handleScroll) - } - } - }, []) - - return (<> - - - {/* 顶部导航菜单栏 */} - - ) -} - -export default NavBar diff --git a/themes/heo/components/PostAdjacent.js b/themes/heo/components/PostAdjacent.js new file mode 100644 index 00000000..b854e946 --- /dev/null +++ b/themes/heo/components/PostAdjacent.js @@ -0,0 +1,89 @@ +import { siteConfig } from '@/lib/config' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' +import CONFIG from '../config' + +/** + * 上一篇,下一篇文章 + * @param {prev,next} param0 + * @returns + */ +export default function PostAdjacent({ prev, next }) { + const [isScrollEnd, setIsScrollEnd] = useState(false) + const router = useRouter() + + useEffect(() => { + setIsScrollEnd(false) + }, [router]) + + useEffect(() => { + // 文章是否已经到了底部 + const targetElement = document.getElementById('article-end') + + const handleIntersect = entries => { + entries.forEach(entry => { + if (entry.isIntersecting) { + setIsScrollEnd(true) + } + }) + } + + const options = { + root: null, + rootMargin: '0px', + threshold: 0.1 + } + + const observer = new IntersectionObserver(handleIntersect, options) + observer.observe(targetElement) + + return () => { + observer.disconnect() + } + }, []) + + if (!prev || !next || !siteConfig('HEO_ARTICLE_ADJACENT', null, CONFIG)) { + return <> + } + + return ( +
+ {/* 移动端 */} +
+ +
上一篇
+
+ {prev.title} +
+ + +
下一篇
+
+ {next.title} +
+ +
+ + {/* 桌面端 */} + +
+ +
下一篇
+
+
{next?.title}
+ +
+
+ ) +} diff --git a/themes/heo/components/ArticleCopyright.js b/themes/heo/components/PostCopyright.js similarity index 68% rename from themes/heo/components/ArticleCopyright.js rename to themes/heo/components/PostCopyright.js index 285eb2d4..a209d25e 100644 --- a/themes/heo/components/ArticleCopyright.js +++ b/themes/heo/components/PostCopyright.js @@ -1,11 +1,15 @@ +import { siteConfig } from '@/lib/config' import { useGlobal } from '@/lib/global' import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import CONFIG from '../config' -import { siteConfig } from '@/lib/config' -export default function ArticleCopyright () { +/** + * 版权声明 + * @returns + */ +export default function PostCopyright() { const router = useRouter() const [path, setPath] = useState(siteConfig('LINK') + router.asPath) useEffect(() => { @@ -19,17 +23,19 @@ export default function ArticleCopyright () { } return ( -
-
    +
    +
    • {locale.COMMON.AUTHOR}: - + {siteConfig('AUTHOR')}
    • - {locale.COMMON.URL}: - + {locale.COMMON.URL}: + {path}
    • diff --git a/themes/heo/components/PostHeader.js b/themes/heo/components/PostHeader.js index 97ce4fff..f2aa5e9b 100644 --- a/themes/heo/components/PostHeader.js +++ b/themes/heo/components/PostHeader.js @@ -1,12 +1,17 @@ -import Link from 'next/link' -import { siteConfig } from '@/lib/config' -import NotionIcon from '@/components/NotionIcon' -import WavesArea from './WavesArea' import { HashTag } from '@/components/HeroIcons' -import WordCount from '@/components/WordCount' import LazyImage from '@/components/LazyImage' +import NotionIcon from '@/components/NotionIcon' +import WordCount from '@/components/WordCount' +import { siteConfig } from '@/lib/config' import { formatDateFmt } from '@/lib/utils/formatDate' +import Link from 'next/link' +import WavesArea from './WavesArea' +/** + * 文章页头 + * @param {*} param0 + * @returns + */ export default function PostHeader({ post, siteInfo }) { if (!post) { return <> @@ -15,91 +20,121 @@ export default function PostHeader({ post, siteInfo }) { const headerImage = post?.pageCover ? post.pageCover : siteInfo?.pageCover return ( -
      - +
      + -
      - - {/* 文章背景图 */} -
      - -
      - - {/* 文章文字描述 */} -
      - {/* 分类+标签 */} -
      - {post.category && <> - -
      - {post.category} -
      - - } - - {post.tagItems && ( -
      - {post.tagItems.map((tag, index) => ( - -
      {tag.name + (tag.count ? `(${tag.count})` : '')}
      - - - ))} -
      - )} -
      - - {/* 文章Title */} -
      - {siteConfig('POST_TITLE_ICON') && }{post.title} -
      - - {/* 标题底部补充信息 */} -
      - -
      -
      - {post?.type !== 'Page' && ( - <> - - {post?.publishDay} - - - )} - -
      - {post.lastEditedDay} -
      - -
      - - {JSON.parse(siteConfig('ANALYTICS_BUSUANZI_ENABLE')) &&
      - -
      } -
      - -
      - - - -
      +
      + {/* 文章背景图 */} +
      +
      + + {/* 文章文字描述 */} +
      + {/* 分类+标签 */} +
      + {post.category && ( + <> + +
      + {post.category} +
      + + + )} + + {post.tagItems && ( +
      + {post.tagItems.map((tag, index) => ( + +
      + {' '} + {tag.name + (tag.count ? `(${tag.count})` : '')}{' '} +
      + + ))} +
      + )} +
      + + {/* 文章Title */} +
      + {siteConfig('POST_TITLE_ICON') && ( + + )} + {post.title} +
      + + {/* 标题底部补充信息 */} +
      +
      +
      + +
      + {post?.type !== 'Page' && ( + <> + + {' '} + {post?.publishDay} + + + )} + +
      + {' '} + {post.lastEditedDay} +
      +
      + + {JSON.parse(siteConfig('ANALYTICS_BUSUANZI_ENABLE')) && ( +
      + {' '} + +
      + )} +
      +
      + + +
      +
      ) } diff --git a/themes/heo/components/ArticleLock.js b/themes/heo/components/PostLock.js similarity index 58% rename from themes/heo/components/ArticleLock.js rename to themes/heo/components/PostLock.js index 7f1da728..1e87d7e8 100644 --- a/themes/heo/components/ArticleLock.js +++ b/themes/heo/components/PostLock.js @@ -8,7 +8,7 @@ import { useEffect, useRef } from 'react' * @param validPassword(bool) 回调函数,校验正确回调入参为true * @returns */ -export const ArticleLock = props => { +export const PostLock = props => { const { validPassword } = props const { locale } = useGlobal() const submitPassword = () => { @@ -27,25 +27,35 @@ export const ArticleLock = props => { passwordInputRef.current.focus() }, []) - return
      -
      -
      {locale.COMMON.ARTICLE_LOCK_TIPS}
      -
      - { + return ( +
      +
      +
      + {locale.COMMON.ARTICLE_LOCK_TIPS} +
      +
      + { if (e.key === 'Enter') { submitPassword() } }} ref={passwordInputRef} // 绑定ref到passwordInputRef变量 - className='outline-none w-full text-sm pl-5 rounded-l transition focus:shadow-lg font-light leading-10 bg-gray-100 dark:bg-gray-500'> - -
      -  {locale.COMMON.SUBMIT} + className='outline-none w-full text-sm pl-5 rounded-l transition focus:shadow-lg font-light leading-10 bg-gray-100 dark:bg-gray-500'> +
      + +  {locale.COMMON.SUBMIT} + +
      -
      -
      +
      -
      + ) } diff --git a/themes/heo/components/ArticleRecommend.js b/themes/heo/components/PostRecommend.js similarity index 82% rename from themes/heo/components/ArticleRecommend.js rename to themes/heo/components/PostRecommend.js index 02874675..c66b8d4f 100644 --- a/themes/heo/components/ArticleRecommend.js +++ b/themes/heo/components/PostRecommend.js @@ -9,7 +9,7 @@ import CONFIG from '../config' * @param {prev,next} param0 * @returns */ -export default function ArticleRecommend({ recommendPosts, siteInfo }) { +export default function PostRecommend({ recommendPosts, siteInfo }) { const { locale } = useGlobal() if ( @@ -55,6 +55,10 @@ export default function ArticleRecommend({ recommendPosts, siteInfo }) { src={headerImage} className='absolute top-0 w-full h-full object-cover object-center group-hover:scale-110 group-hover:brightness-50 transform duration-200' /> + {/* 卡片的阴影遮罩,为了凸显图片上的文字 */} +
      +
      +
      ) diff --git a/themes/heo/index.js b/themes/heo/index.js index 36f84b1f..f7725939 100644 --- a/themes/heo/index.js +++ b/themes/heo/index.js @@ -6,41 +6,41 @@ * 2. 更多说明参考此[文档](https://docs.tangly1024.com/article/notionnext-heo) */ -import CONFIG from './config' -import { useEffect, useState } from 'react' -import Footer from './components/Footer' -import SideRight from './components/SideRight' -import NavBar from './components/NavBar' +import Comment from '@/components/Comment' +import { AdSlot } from '@/components/GoogleAdsense' +import { HashTag } from '@/components/HeroIcons' +import LazyImage from '@/components/LazyImage' +import replaceSearchResult from '@/components/Mark' +import NotionPage from '@/components/NotionPage' +import ShareBar from '@/components/ShareBar' +import WWAds from '@/components/WWAds' +import { siteConfig } from '@/lib/config' import { useGlobal } from '@/lib/global' +import { loadWowJS } from '@/lib/plugins/wow' +import { isBrowser } from '@/lib/utils' +import { Transition } from '@headlessui/react' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' +import BlogPostArchive from './components/BlogPostArchive' import BlogPostListPage from './components/BlogPostListPage' import BlogPostListScroll from './components/BlogPostListScroll' -import Hero from './components/Hero' -import { useRouter } from 'next/router' -import SearchNav from './components/SearchNav' -import BlogPostArchive from './components/BlogPostArchive' -import { ArticleLock } from './components/ArticleLock' -import PostHeader from './components/PostHeader' -import Comment from '@/components/Comment' -import NotionPage from '@/components/NotionPage' -import ArticleAdjacent from './components/ArticleAdjacent' -import ArticleCopyright from './components/ArticleCopyright' -import ArticleRecommend from './components/ArticleRecommend' -import ShareBar from '@/components/ShareBar' -import Link from 'next/link' import CategoryBar from './components/CategoryBar' -import { Transition } from '@headlessui/react' -import { Style } from './style' -import { NoticeBar } from './components/NoticeBar' -import { HashTag } from '@/components/HeroIcons' -import LatestPostsGroup from './components/LatestPostsGroup' import FloatTocButton from './components/FloatTocButton' -import replaceSearchResult from '@/components/Mark' -import LazyImage from '@/components/LazyImage' -import WWAds from '@/components/WWAds' -import { AdSlot } from '@/components/GoogleAdsense' -import { siteConfig } from '@/lib/config' -import { isBrowser } from '@/lib/utils' -import { loadWowJS } from '@/lib/plugins/wow' +import Footer from './components/Footer' +import Header from './components/Header' +import Hero from './components/Hero' +import LatestPostsGroup from './components/LatestPostsGroup' +import { NoticeBar } from './components/NoticeBar' +import PostAdjacent from './components/PostAdjacent' +import PostCopyright from './components/PostCopyright' +import PostHeader from './components/PostHeader' +import { PostLock } from './components/PostLock' +import PostRecommend from './components/PostRecommend' +import SearchNav from './components/SearchNav' +import SideRight from './components/SideRight' +import CONFIG from './config' +import { Style } from './style' /** * 基础布局 采用上中下布局,移动端使用顶部侧边导航栏 @@ -49,38 +49,39 @@ import { loadWowJS } from '@/lib/plugins/wow' * @constructor */ const LayoutBase = props => { - const { - children, - slotTop, - className - } = props + const { children, slotTop, className } = props // 全屏模式下的最大宽度 const { fullWidth } = useGlobal() const router = useRouter() const headerSlot = ( -
      +
      {/* 顶部导航 */} - +
      {/* 通知横幅 */} - {router.route === '/' - ? <> - - + {router.route === '/' ? ( + <> + + - : null} + ) : null} {fullWidth ? null : }
      ) // 右侧栏 用户信息+标签列表 - const slotRight = (router.route === '/404' || fullWidth) ? null : + const slotRight = + router.route === '/404' || fullWidth ? null : const maxWidth = fullWidth ? 'max-w-[96rem] mx-auto' : 'max-w-[86rem]' // 普通最大宽度是86rem和顶部菜单栏对齐,留空则与窗口对齐 - const HEO_HERO_BODY_REVERSE = siteConfig('HEO_HERO_BODY_REVERSE', false, CONFIG) + const HEO_HERO_BODY_REVERSE = siteConfig( + 'HEO_HERO_BODY_REVERSE', + false, + CONFIG + ) // 加载wow动画 useEffect(() => { @@ -89,10 +90,8 @@ const LayoutBase = props => { return (
      - + id='theme-heo' + className={`${siteConfig('FONT_STYLE')} bg-[#f7f9fe] dark:bg-[#18171d] h-full min-h-screen flex flex-col scroll-smooth`}>