{/* 顶部导航栏 */}
diff --git a/themes/gitbook/style.js b/themes/gitbook/style.js
new file mode 100644
index 00000000..5e8eaa5a
--- /dev/null
+++ b/themes/gitbook/style.js
@@ -0,0 +1,18 @@
+/* eslint-disable react/no-unknown-property */
+/**
+ * 此处样式只对当前主题生效
+ * 此处不支持tailwindCSS的 @apply 语法
+ * @returns
+ */
+const Style = () => {
+ return
+}
+
+export { Style }
diff --git a/themes/heo/components/AnalyticsCard.js b/themes/heo/components/AnalyticsCard.js
new file mode 100644
index 00000000..a9d30477
--- /dev/null
+++ b/themes/heo/components/AnalyticsCard.js
@@ -0,0 +1,43 @@
+import CONFIG from '../config'
+
+/**
+ * 博客统计卡牌
+ * @param {*} props
+ * @returns
+ */
+export function AnalyticsCard(props) {
+ const targetDate = new Date(CONFIG.SITE_CREATE_TIME)
+ const today = new Date()
+ const diffTime = today.getTime() - targetDate.getTime() // 获取两个日期之间的毫秒数差值
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) // 将毫秒数差值转换为天数差值
+
+ const { postCount } = props
+ return <>
+
+ >
+}
diff --git a/themes/heo/components/Announcement.js b/themes/heo/components/Announcement.js
new file mode 100644
index 00000000..6c3feb2a
--- /dev/null
+++ b/themes/heo/components/Announcement.js
@@ -0,0 +1,18 @@
+import dynamic from 'next/dynamic'
+
+const NotionPage = dynamic(() => import('@/components/NotionPage'))
+
+const Announcement = ({ post, className }) => {
+ if (post?.blockMap) {
+ return
+ {post && (
+
+
+
+ )}
+
+ } else {
+ return <>>
+ }
+}
+export default Announcement
diff --git a/themes/heo/components/ArticleAdjacent.js b/themes/heo/components/ArticleAdjacent.js
new file mode 100644
index 00000000..bebc7e69
--- /dev/null
+++ b/themes/heo/components/ArticleAdjacent.js
@@ -0,0 +1,83 @@
+import Link from 'next/link'
+import { useEffect, useState } from 'react'
+import CONFIG from '../config'
+
+/**
+ * 上一篇,下一篇文章
+ * @param {prev,next} param0
+ * @returns
+ */
+export default function ArticleAdjacent({ prev, next }) {
+ const [isScrollEnd, setIsScrollEnd] = useState(false)
+
+ useEffect(() => {
+ // 文章是否已经到了底部
+ const targetElement = document.getElementById('article-end')
+
+ const handleIntersect = (entries) => {
+ entries.forEach((entry) => {
+ console.log(entry.isIntersecting)
+ if (entry.isIntersecting) {
+ setIsScrollEnd(true)
+ } else {
+ // setIsScrollEnd(false)
+ }
+ })
+ }
+
+ const options = {
+ root: null,
+ rootMargin: '0px',
+ threshold: 0.1
+ }
+
+ const observer = new IntersectionObserver(handleIntersect, options)
+ observer.observe(targetElement)
+
+ return () => {
+ observer.disconnect()
+ }
+ }, [])
+
+ if (!prev || !next || !CONFIG.ARTICLE_ADJACENT) {
+ return <>>
+ }
+
+ return (
+
+ {/* 移动端 */}
+
+
+ 上一篇
+ {prev.title}
+
+
+ 下一篇
+ {next.title}
+
+
+
+ {/* 桌面端 */}
+
+
+
+
下一篇
+
+
{next?.title}
+
+
+
+
+ )
+}
diff --git a/themes/heo/components/ArticleCopyright.js b/themes/heo/components/ArticleCopyright.js
new file mode 100644
index 00000000..4664573c
--- /dev/null
+++ b/themes/heo/components/ArticleCopyright.js
@@ -0,0 +1,41 @@
+import BLOG from '@/blog.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'
+
+export default function ArticleCopyright () {
+ if (!CONFIG.ARTICLE_COPYRIGHT) {
+ return <>>
+ }
+ const router = useRouter()
+ const [path, setPath] = useState(BLOG.LINK + router.asPath)
+ useEffect(() => {
+ setPath(window.location.href)
+ })
+
+ const { locale } = useGlobal()
+ return (
+
+
+ -
+ {locale.COMMON.AUTHOR}:
+
+ {BLOG.AUTHOR}
+
+
+ -
+ {locale.COMMON.URL}:
+
+ {path}
+
+
+ -
+ {locale.COMMON.COPYRIGHT}:
+ {locale.COMMON.COPYRIGHT_NOTICE}
+
+
+
+ );
+}
diff --git a/themes/heo/components/ArticleLock.js b/themes/heo/components/ArticleLock.js
new file mode 100644
index 00000000..7f1da728
--- /dev/null
+++ b/themes/heo/components/ArticleLock.js
@@ -0,0 +1,51 @@
+import { useGlobal } from '@/lib/global'
+import { useEffect, useRef } from 'react'
+
+/**
+ * 加密文章校验组件
+ * @param {password, validPassword} props
+ * @param password 正确的密码
+ * @param validPassword(bool) 回调函数,校验正确回调入参为true
+ * @returns
+ */
+export const ArticleLock = props => {
+ const { validPassword } = props
+ const { locale } = useGlobal()
+ const submitPassword = () => {
+ const p = document.getElementById('password')
+ if (!validPassword(p?.value)) {
+ const tips = document.getElementById('tips')
+ if (tips) {
+ tips.innerHTML = ''
+ tips.innerHTML = `
${locale.COMMON.PASSWORD_ERROR}
`
+ }
+ }
+ }
+ const passwordInputRef = useRef(null)
+ useEffect(() => {
+ // 选中密码输入框并将其聚焦
+ passwordInputRef.current.focus()
+ }, [])
+
+ 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}
+
+
+
+
+
+
+}
diff --git a/themes/heo/components/ArticleRecommend.js b/themes/heo/components/ArticleRecommend.js
new file mode 100644
index 00000000..031feb24
--- /dev/null
+++ b/themes/heo/components/ArticleRecommend.js
@@ -0,0 +1,70 @@
+import Link from 'next/link'
+import CONFIG from '../config'
+import BLOG from '@/blog.config'
+import { useGlobal } from '@/lib/global'
+
+/**
+ * 关联推荐文章
+ * @param {prev,next} param0
+ * @returns
+ */
+export default function ArticleRecommend({ recommendPosts, siteInfo }) {
+ const { locale } = useGlobal()
+
+ if (
+ !CONFIG.ARTICLE_RECOMMEND ||
+ !recommendPosts ||
+ recommendPosts.length === 0
+ ) {
+ return <>>
+ }
+
+ return (
+
+
+ {/* 推荐文章 */}
+
+
+
+ {locale.COMMON.RELATE_POSTS}
+
+
+
+ {/* 文章列表 */}
+
+
+ {recommendPosts.map(post => {
+ const headerImage = post?.pageCoverThumbnail
+ ? `url("${post.pageCoverThumbnail}")`
+ : `url("${siteInfo?.pageCover}")`
+
+ return (
+ (
+
+
+
+
+
+
+ {post.date?.start_date}
+
+
{post.title}
+
+
+
+
+ )
+ )
+ })}
+
+
+ )
+}
diff --git a/themes/heo/components/BlogPostArchive.js b/themes/heo/components/BlogPostArchive.js
new file mode 100644
index 00000000..0b092fb2
--- /dev/null
+++ b/themes/heo/components/BlogPostArchive.js
@@ -0,0 +1,86 @@
+import React from 'react'
+import Link from 'next/link'
+import BLOG from '@/blog.config'
+import CONFIG from '../config'
+import TagItemMini from './TagItemMini'
+/**
+ * 博客归档列表
+ * @param posts 所有文章
+ * @param archiveTitle 归档标题
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const BlogPostArchive = ({ posts = [], archiveTitle, siteInfo }) => {
+ if (!posts || posts.length === 0) {
+ return <>>
+ } else {
+ return (
+
+ )
+ }
+}
+
+export default BlogPostArchive
diff --git a/themes/heo/components/BlogPostCard.js b/themes/heo/components/BlogPostCard.js
new file mode 100644
index 00000000..d89aab5c
--- /dev/null
+++ b/themes/heo/components/BlogPostCard.js
@@ -0,0 +1,79 @@
+import Link from 'next/link'
+import CONFIG from '../config'
+import BLOG from '@/blog.config'
+import TagItemMini from './TagItemMini'
+// import Image from 'next/image'
+
+const BlogPostCard = ({ index, post, showSummary, siteInfo }) => {
+ const showPreview = CONFIG.POST_LIST_PREVIEW && post.blockMap
+ if (post && !post.pageCoverThumbnail && CONFIG.POST_LIST_COVER_DEFAULT) {
+ post.pageCoverThumbnail = siteInfo?.pageCover
+ }
+ const showPageCover = CONFIG.POST_LIST_COVER && post?.pageCoverThumbnail && !showPreview
+ return (
+
+
+
+ {/* 图片封面 */}
+ {showPageCover && (
+
+ )}
+
+ {/* 文字区块 */}
+
+
+ {/* 分类 */}
+ {post?.category &&
+
+ {post.category}
+
+
}
+
+ {/* 标题 */}
+
+
{post.title}
+
+
+
+ {/* 摘要 */}
+ {(!showPreview || showSummary) && !post.results && (
+
+ {post.summary}
+
+ )}
+
+ {/* 搜索结果 */}
+ {post.results && (
+
+ {post.results.map(r => (
+ {r}
+ ))}
+
+ )}
+
+
+
+ {' '}
+ {post.tagItems?.map(tag => (
+
+ ))}
+
+
+
+
+
+
+
+
+ )
+}
+
+export default BlogPostCard
diff --git a/themes/heo/components/BlogPostListEmpty.js b/themes/heo/components/BlogPostListEmpty.js
new file mode 100644
index 00000000..5f75c3e7
--- /dev/null
+++ b/themes/heo/components/BlogPostListEmpty.js
@@ -0,0 +1,14 @@
+import { useGlobal } from '@/lib/global'
+
+/**
+ * 空白博客 列表
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const BlogPostListEmpty = ({ currentSearch }) => {
+ const { locale } = useGlobal()
+ return
+
{locale.COMMON.NO_MORE} {(currentSearch &&
{currentSearch}
)}
+
+}
+export default BlogPostListEmpty
diff --git a/themes/heo/components/BlogPostListPage.js b/themes/heo/components/BlogPostListPage.js
new file mode 100644
index 00000000..8c9e1fd6
--- /dev/null
+++ b/themes/heo/components/BlogPostListPage.js
@@ -0,0 +1,34 @@
+import BlogPostCard from './BlogPostCard'
+import PaginationNumber from './PaginationNumber'
+import BLOG from '@/blog.config'
+import BlogPostListEmpty from './BlogPostListEmpty'
+
+/**
+ * 文章列表分页表格
+ * @param page 当前页
+ * @param posts 所有文章
+ * @param tags 所有标签
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const BlogPostListPage = ({ page = 1, posts = [], postCount, siteInfo }) => {
+ const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
+ const showPagination = postCount >= BLOG.POSTS_PER_PAGE
+ if (!posts || posts.length === 0 || page > totalPage) {
+ return
+ } else {
+ return (
+
+ {/* 文章列表 */}
+
+ {posts?.map(post => (
+
+ ))}
+
+ {showPagination &&
}
+
+ )
+ }
+}
+
+export default BlogPostListPage
diff --git a/themes/heo/components/BlogPostListScroll.js b/themes/heo/components/BlogPostListScroll.js
new file mode 100644
index 00000000..830e8177
--- /dev/null
+++ b/themes/heo/components/BlogPostListScroll.js
@@ -0,0 +1,75 @@
+import BLOG from '@/blog.config'
+import BlogPostCard from './BlogPostCard'
+import BlogPostListEmpty from './BlogPostListEmpty'
+import { useGlobal } from '@/lib/global'
+import React from 'react'
+import CONFIG from '../config'
+import { getListByPage } from '@/lib/utils'
+
+/**
+ * 博客列表滚动分页
+ * @param posts 所有文章
+ * @param tags 所有标签
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = CONFIG.POST_LIST_SUMMARY, siteInfo }) => {
+ const postsPerPage = BLOG.POSTS_PER_PAGE
+ const [page, updatePage] = React.useState(1)
+ const postsToShow = getListByPage(posts, page, postsPerPage)
+
+ let hasMore = false
+ if (posts) {
+ const totalCount = posts.length
+ hasMore = page * postsPerPage < totalCount
+ }
+
+ const handleGetMore = () => {
+ if (!hasMore) return
+ updatePage(page + 1)
+ }
+
+ // 监听滚动自动分页加载
+ const scrollTrigger = () => {
+ requestAnimationFrame(() => {
+ const scrollS = window.scrollY + window.outerHeight
+ const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
+ if (scrollS > clientHeight + 100) {
+ handleGetMore()
+ }
+ })
+ }
+
+ // 监听滚动
+ React.useEffect(() => {
+ window.addEventListener('scroll', scrollTrigger)
+ return () => {
+ window.removeEventListener('scroll', scrollTrigger)
+ }
+ })
+
+ const targetRef = React.useRef(null)
+ const { locale } = useGlobal()
+
+ if (!postsToShow || postsToShow.length === 0) {
+ return
+ } else {
+ return
+
+ {/* 文章列表 */}
+
+ {postsToShow.map(post => (
+
+ ))}
+
+
+
+
{ handleGetMore() }}
+ className='w-full my-4 py-4 text-center cursor-pointer rounded-xl dark:text-gray-200'
+ > {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE}`}
+
+
+ }
+}
+
+export default BlogPostListScroll
diff --git a/themes/heo/components/Card.js b/themes/heo/components/Card.js
new file mode 100644
index 00000000..0859dec0
--- /dev/null
+++ b/themes/heo/components/Card.js
@@ -0,0 +1,9 @@
+const Card = ({ children, headerSlot, className }) => {
+ return
+ <>{headerSlot}>
+
+
+}
+export default Card
diff --git a/themes/heo/components/Catalog.js b/themes/heo/components/Catalog.js
new file mode 100644
index 00000000..115ddd0c
--- /dev/null
+++ b/themes/heo/components/Catalog.js
@@ -0,0 +1,95 @@
+import React, { useRef } from 'react'
+import throttle from 'lodash.throttle'
+import { uuidToId } from 'notion-utils'
+import Progress from './Progress'
+import { useGlobal } from '@/lib/global'
+
+/**
+ * 目录导航组件
+ * @param toc
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const Catalog = ({ toc }) => {
+ const { locale } = useGlobal()
+ // 监听滚动事件
+ React.useEffect(() => {
+ window.addEventListener('scroll', actionSectionScrollSpy)
+ actionSectionScrollSpy()
+ return () => {
+ window.removeEventListener('scroll', actionSectionScrollSpy)
+ }
+ }, [])
+
+ // 目录自动滚动
+ const tRef = useRef(null)
+ const tocIds = []
+
+ // 同步选中目录事件
+ const [activeSection, setActiveSection] = React.useState(null)
+
+ const throttleMs = 200
+ 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)
+ const index = tocIds.indexOf(currentSectionId) || 0
+ tRef?.current?.scrollTo({ top: 28 * index, behavior: 'smooth' })
+ }, throttleMs))
+
+ // 无目录就直接返回空
+ if (!toc || toc.length < 1) {
+ return <>>
+ }
+
+ return
+
{locale.COMMON.TABLE_OF_CONTENTS}
+
+
+
+
+
+
+}
+
+export default Catalog
diff --git a/themes/heo/components/CategoryBar.js b/themes/heo/components/CategoryBar.js
new file mode 100644
index 00000000..31ee3260
--- /dev/null
+++ b/themes/heo/components/CategoryBar.js
@@ -0,0 +1,41 @@
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+/**
+ * 博客列表上方嵌入条
+ * @param {*} props
+ * @returns
+ */
+export default function CategoryBar(props) {
+ const { categoryOptions, border = true } = props
+ const { locale } = useGlobal()
+
+ return
+
+
+
+ {categoryOptions?.map((c, index) => )}
+
+
+
+
+ {locale.COMMON.MORE}
+
+
+
+}
+
+/**
+ * 按钮
+ * @param {*} param0
+ * @returns
+ */
+const MenuItem = ({ href, name }) => {
+ const router = useRouter()
+ const selected = router.pathname === href
+ return
+ {name}
+
+}
diff --git a/themes/heo/components/CategoryGroup.js b/themes/heo/components/CategoryGroup.js
new file mode 100644
index 00000000..811ad9ff
--- /dev/null
+++ b/themes/heo/components/CategoryGroup.js
@@ -0,0 +1,31 @@
+import Link from 'next/link'
+import React from 'react'
+
+const CategoryGroup = ({ currentCategory, categories }) => {
+ if (!categories) {
+ return <>>
+ }
+ return <>
+
+ {categories.map(category => {
+ const selected = currentCategory === category.name
+ return (
+
+
+
{category.name}({category.count})
+
+
+ )
+ })}
+
+ >
+}
+
+export default CategoryGroup
diff --git a/themes/heo/components/FloatDarkModeButton.js b/themes/heo/components/FloatDarkModeButton.js
new file mode 100644
index 00000000..f693d1f0
--- /dev/null
+++ b/themes/heo/components/FloatDarkModeButton.js
@@ -0,0 +1,31 @@
+import { useGlobal } from '@/lib/global'
+import { saveDarkModeToCookies } from '@/themes/theme'
+import CONFIG from '../config'
+
+export default function FloatDarkModeButton () {
+ const { isDarkMode, updateDarkMode } = useGlobal()
+
+ if (!CONFIG.WIDGET_DARK_MODE) {
+ return <>>
+ }
+
+ // 用户手动设置主题
+ const handleChangeDarkMode = () => {
+ const newStatus = !isDarkMode
+ saveDarkModeToCookies(newStatus)
+ updateDarkMode(newStatus)
+ const htmlElement = document.getElementsByTagName('html')[0]
+ htmlElement.classList?.remove(newStatus ? 'light' : 'dark')
+ htmlElement.classList?.add(newStatus ? 'dark' : 'light')
+ }
+
+ return (
+
+
+
+ )
+}
diff --git a/themes/heo/components/Footer.js b/themes/heo/components/Footer.js
new file mode 100644
index 00000000..749d6048
--- /dev/null
+++ b/themes/heo/components/Footer.js
@@ -0,0 +1,54 @@
+import React from 'react'
+import BLOG from '@/blog.config'
+import SocialButton from './SocialButton'
+// import DarkModeButton from '@/components/DarkModeButton'
+
+const Footer = ({ title }) => {
+ const d = new Date()
+ const currentYear = d.getFullYear()
+ const copyrightDate = (function () {
+ if (Number.isInteger(BLOG.SINCE) && BLOG.SINCE < currentYear) {
+ return BLOG.SINCE + '-' + currentYear
+ }
+ return currentYear
+ })()
+
+ return (
+
+ )
+}
+
+export default Footer
diff --git a/themes/heo/components/Hero.js b/themes/heo/components/Hero.js
new file mode 100644
index 00000000..d34530c2
--- /dev/null
+++ b/themes/heo/components/Hero.js
@@ -0,0 +1,251 @@
+// import Image from 'next/image'
+
+import BLOG from '@/blog.config'
+import { ArrowSmallRight, PlusSmall } from '@/components/HeroIcons'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useImperativeHandle, useRef, useState } from 'react'
+import CONFIG from '../config'
+import { useGlobal } from '@/lib/global'
+import { Transition } from '@headlessui/react'
+
+/**
+ * 顶部英雄区
+ * 左右布局,
+ * 左侧:banner组
+ * 右侧:今日卡牌遮罩
+ * @returns
+ */
+const Hero = props => {
+ const { onLoading } = useGlobal()
+ return (
+
+
+
+ {/* 左侧banner组 */}
+
+
+ {/* 右侧置顶文章组 */}
+
+
+
+
+ )
+}
+
+/**
+ * 英雄区左侧banner组
+ * @returns
+ */
+function BannerGroup(props) {
+ return (
+ // 左侧英雄区
+
+ {/* 动图 */}
+
+ {/* 导航分类 */}
+
+
+ )
+}
+
+/**
+ * 英雄区左上角banner动图
+ * @returns
+ */
+function Banner(props) {
+ const router = useRouter()
+ const { latestPosts } = props
+ // 跳转到任意文章
+ function handleClickBanner() {
+ const randomIndex = Math.floor(Math.random() * latestPosts.length)
+ const randomPost = latestPosts[randomIndex]
+ router.push(randomPost.slug)
+ }
+
+ return
+
+
+
分享编程
与思维认知
+
TANGLY1024.COM
+
+
+ {/* 斜向滚动的图标 */}
+
+
+ {/* 遮罩 */}
+
+
+
+}
+
+/**
+ * 图标滚动标签组
+ * 英雄区左上角banner条中斜向滚动的图标
+ */
+function TagsGroupBar() {
+ const groupIcons = CONFIG.GROUP_ICONS.concat(CONFIG.GROUP_ICONS)
+ return (
+
+
+ {groupIcons?.map(g => {
+ return (<>
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+
+
+ >)
+ })}
+
+
+ )
+}
+
+/**
+ * 英雄区左下角3个分类按钮
+ * @returns
+ */
+function GroupMenu() {
+ return (
+
+
+
+ 必看精选
+
+
+
+
+
+
+
+
+ 热门文章
+
+
+
+
+
+
+ {/* 第三个标签在小屏上不显示 */}
+
+
+ 实用教程
+
+
+
+
+
+
+
+
+ )
+}
+
+/**
+ * 置顶文章区域
+ */
+function TopGroup(props) {
+ const { latestPosts, siteInfo } = props
+ const todayCardRef = useRef()
+ function handleMouseLeave() {
+ todayCardRef.current.coverUp()
+ }
+ return (
+
+ {/* 制定最新文章 */}
+
+ {latestPosts?.map(p => {
+ return
+
+ {/* eslint-disable-next-line */}
+

+
{p?.title}
+ {/* hover 悬浮的 ‘荐’ 字 */}
+
+ 荐
+
+
+
+ })}
+
+
+
+ )
+}
+
+/**
+ * 英雄区右侧,今日卡牌
+ * @returns
+ */
+function TodayCard({ cRef }) {
+ const router = useRouter()
+ // 卡牌是否盖住下层
+ const [isCoverUp, setIsCoverUp] = useState(true)
+
+ /**
+ * 外部可以调用此方法
+ */
+ useImperativeHandle(cRef, () => {
+ return {
+ coverUp: () => {
+ setIsCoverUp(true)
+ }
+ }
+ })
+
+ /**
+ * 点击更多
+ * @param {*} e
+ */
+ function handleClickMore(e) {
+ e.stopPropagation()
+ setIsCoverUp(false)
+ }
+
+ /**
+ * 点击卡片跳转的链接
+ * @param {*} e
+ */
+ function handleCardClick(e) {
+ router.push('https://tangly1024.com')
+ }
+
+ return
+
+
+
+
新版上线
+
NotionNext4.0 轻松定制主题
+
+
+
+
+
+
+}
+
+export default Hero
diff --git a/themes/heo/components/HexoRecentComments.js b/themes/heo/components/HexoRecentComments.js
new file mode 100644
index 00000000..2ebf00c8
--- /dev/null
+++ b/themes/heo/components/HexoRecentComments.js
@@ -0,0 +1,47 @@
+import React from 'react'
+import BLOG from '@/blog.config'
+import Card from '@/themes/hexo/components/Card'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import { RecentComments } from '@waline/client'
+
+/**
+ * @see https://waline.js.org/guide/get-started.html
+ * @param {*} props
+ * @returns
+ */
+const HexoRecentComments = (props) => {
+ const [comments, updateComments] = React.useState([])
+ const { locale } = useGlobal()
+ const [onLoading, changeLoading] = React.useState(true)
+ React.useEffect(() => {
+ RecentComments({
+ serverURL: BLOG.COMMENT_WALINE_SERVER_URL,
+ count: 5
+ }).then(({ comments }) => {
+ changeLoading(false)
+ updateComments(comments)
+ })
+ }, [])
+
+ return (
+
+
+
+ {locale.COMMON.RECENT_COMMENTS}
+
+
+ {onLoading && Loading...
}
+ {!onLoading && comments && comments.length === 0 && No Comments
}
+ {!onLoading && comments && comments.length > 0 && comments.map((comment) =>
+
+
+ --{comment.nick}
+
+
)}
+
+
+ )
+}
+
+export default HexoRecentComments
diff --git a/themes/heo/components/InfoCard.js b/themes/heo/components/InfoCard.js
new file mode 100644
index 00000000..9dd4e1e9
--- /dev/null
+++ b/themes/heo/components/InfoCard.js
@@ -0,0 +1,87 @@
+import BLOG from '@/blog.config'
+import { ArrowRightCircle, GlobeAlt } from '@/components/HeroIcons'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useState } from 'react'
+import CONFIG from '../config'
+import Announcement from './Announcement'
+import Card from './Card'
+
+/**
+ * 社交信息卡
+ * @param {*} props
+ * @returns
+ */
+export function InfoCard(props) {
+ const { siteInfo, notice } = props
+ const router = useRouter()
+ // 在文章详情页特殊处理
+ const isSlugPage = router.pathname === '/[...slug]'
+
+ return (
+
+ {/* 信息卡牌第一行 */}
+
+ {/* 问候语 */}
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+
+
+
+
+ {BLOG.AUTHOR}
+
+
+ {/* 公告栏 */}
+
+
+
+
+ {/* 两个社交按钮 */}
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+/**
+ * 欢迎语
+ */
+function GreetingsWords() {
+ const greetings = CONFIG.INFOCARD_GREETINGS
+ const [greeting, setGreeting] = useState(greetings[0])
+ // 每次点击,随机获取greetings中的一个
+ const handleChangeGreeting = () => {
+ const randomIndex = Math.floor(Math.random() * greetings.length)
+ setGreeting(greetings[randomIndex])
+ }
+
+ return
+ {greeting}
+
+}
+
+/**
+ * 了解更多按鈕
+ * @returns
+ */
+function MoreButton() {
+ return
+
+
+}
diff --git a/themes/heo/components/JumpToCommentButton.js b/themes/heo/components/JumpToCommentButton.js
new file mode 100644
index 00000000..fb007712
--- /dev/null
+++ b/themes/heo/components/JumpToCommentButton.js
@@ -0,0 +1,29 @@
+import React from 'react'
+import CONFIG from '../config'
+
+/**
+ * 跳转到评论区
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const JumpToCommentButton = () => {
+ if (!CONFIG.WIDGET_TO_COMMENT) {
+ return <>>
+ }
+
+ function navToComment() {
+ if (document.getElementById('comment')) {
+ window.scrollTo({ top: document.getElementById('comment').offsetTop, behavior: 'smooth' })
+ }
+ // 兼容性不好
+ // const commentElement = document.getElementById('comment')
+ // if (commentElement) {
+ // commentElement?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' })
+ }
+
+ return (
+
+
)
+}
+
+export default JumpToCommentButton
diff --git a/themes/heo/components/JumpToTopButton.js b/themes/heo/components/JumpToTopButton.js
new file mode 100644
index 00000000..77313f46
--- /dev/null
+++ b/themes/heo/components/JumpToTopButton.js
@@ -0,0 +1,25 @@
+import { useGlobal } from '@/lib/global'
+import React from 'react'
+import CONFIG from '../config'
+
+/**
+ * 跳转到网页顶部
+ * 当屏幕下滑500像素后会出现该控件
+ * @param targetRef 关联高度的目标html标签
+ * @param showPercent 是否显示百分比
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const JumpToTopButton = ({ showPercent = true, percent }) => {
+ const { locale } = useGlobal()
+
+ if (!CONFIG.WIDGET_TO_TOP) {
+ return <>>
+ }
+ return (
window.scrollTo({ top: 0, behavior: 'smooth' })} >
+
+ {showPercent && (
{percent}
)}
+
)
+}
+
+export default JumpToTopButton
diff --git a/themes/heo/components/LatestPostsGroup.js b/themes/heo/components/LatestPostsGroup.js
new file mode 100644
index 00000000..80f0363f
--- /dev/null
+++ b/themes/heo/components/LatestPostsGroup.js
@@ -0,0 +1,50 @@
+import BLOG from '@/blog.config'
+// import Image from 'next/image'
+import Link from 'next/link'
+
+/**
+ * 最新文章列表
+ * @param posts 所有文章数据
+ * @param sliceCount 截取展示的数量 默认6
+ * @constructor
+ */
+const LatestPostsGroup = ({ latestPosts, siteInfo }) => {
+ // 获取当前路径
+
+ if (!latestPosts) {
+ return <>>
+ }
+
+ return
+ {latestPosts.map(post => {
+ const headerImage = post?.pageCoverThumbnail ? post.pageCoverThumbnail : siteInfo?.pageCover
+
+ return (
+ (
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+
+
+
+
+ )
+ )
+ })}
+
+}
+export default LatestPostsGroup
diff --git a/themes/heo/components/LatestPostsGroupMini.js b/themes/heo/components/LatestPostsGroupMini.js
new file mode 100644
index 00000000..d69662a3
--- /dev/null
+++ b/themes/heo/components/LatestPostsGroupMini.js
@@ -0,0 +1,63 @@
+import BLOG from '@/blog.config'
+import { useGlobal } from '@/lib/global'
+// import Image from 'next/image'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+/**
+ * 最新文章列表
+ * @param posts 所有文章数据
+ * @param sliceCount 截取展示的数量 默认6
+ * @constructor
+ */
+export default function LatestPostsGroupMini ({ latestPosts, siteInfo }) {
+ // 获取当前路径
+ const currentPath = useRouter().asPath
+ const { locale } = useGlobal()
+
+ if (!latestPosts) {
+ return <>>
+ }
+
+ return <>
+
+
+
+ {locale.COMMON.LATEST_POSTS}
+
+
+ {latestPosts.map(post => {
+ const selected = currentPath === `${BLOG.SUB_PATH}/${post.slug}`
+
+ const headerImage = post?.pageCoverThumbnail ? post.pageCoverThumbnail : siteInfo?.pageCover
+
+ return (
+ (
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+
+
+
+
{post.title}
+
{post.lastEditedTime}
+
+
+
+ )
+ )
+ })}
+ >
+}
diff --git a/themes/heo/components/LoadingCover.js b/themes/heo/components/LoadingCover.js
new file mode 100644
index 00000000..c6418fad
--- /dev/null
+++ b/themes/heo/components/LoadingCover.js
@@ -0,0 +1,8 @@
+export default function LoadingCover () {
+ return (
+ )
+}
diff --git a/themes/heo/components/Logo.js b/themes/heo/components/Logo.js
new file mode 100644
index 00000000..a57cea2e
--- /dev/null
+++ b/themes/heo/components/Logo.js
@@ -0,0 +1,25 @@
+import BLOG from '@/blog.config'
+import { Home } from '@/components/HeroIcons'
+import Link from 'next/link'
+import React from 'react'
+
+const Logo = props => {
+ const { siteInfo } = props
+ return (
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+
+
+ {siteInfo?.title || BLOG.TITLE}
+
+
+
+
+
+
+
+ )
+}
+export default Logo
diff --git a/themes/heo/components/MenuGroupCard.js b/themes/heo/components/MenuGroupCard.js
new file mode 100644
index 00000000..89591369
--- /dev/null
+++ b/themes/heo/components/MenuGroupCard.js
@@ -0,0 +1,44 @@
+import React from 'react'
+import Link from 'next/link'
+import { useGlobal } from '@/lib/global'
+import CONFIG from '../config'
+
+const MenuGroupCard = (props) => {
+ const { postCount, categoryOptions, tagOptions } = props
+ const { locale } = useGlobal()
+ const archiveSlot =
{postCount}
+ const categorySlot =
{categoryOptions?.length}
+ const tagSlot =
{tagOptions?.length}
+
+ const links = [
+ { name: locale.COMMON.ARTICLE, to: '/archive', slot: archiveSlot, show: CONFIG.MENU_ARCHIVE },
+ { name: locale.COMMON.CATEGORY, to: '/category', slot: categorySlot, show: CONFIG.MENU_CATEGORY },
+ { name: locale.COMMON.TAGS, to: '/tag', slot: tagSlot, show: CONFIG.MENU_TAG }
+ ]
+
+ return (
+
+ )
+}
+export default MenuGroupCard
diff --git a/themes/heo/components/MenuItemCollapse.js b/themes/heo/components/MenuItemCollapse.js
new file mode 100644
index 00000000..3ec10f5e
--- /dev/null
+++ b/themes/heo/components/MenuItemCollapse.js
@@ -0,0 +1,54 @@
+import Collapse from '@/components/Collapse'
+import Link from 'next/link'
+import { useState } from 'react'
+
+/**
+ * 折叠菜单
+ * @param {*} param0
+ * @returns
+ */
+export const MenuItemCollapse = ({ link }) => {
+ const [show, changeShow] = useState(false)
+ const hasSubMenu = link?.subMenus?.length > 0
+
+ const [isOpen, changeIsOpen] = useState(false)
+
+ const toggleShow = () => {
+ changeShow(!show)
+ }
+
+ const toggleOpenSubMenu = () => {
+ changeIsOpen(!isOpen)
+ }
+
+ if (!link || !link.show) {
+ return null
+ }
+
+ return <>
+
+ {!hasSubMenu &&
+
{link?.icon && }{link?.name}
+ }
+ {hasSubMenu &&
+ {link?.icon && }{link?.name}
+
+
}
+
+
+ {/* 折叠子菜单 */}
+ {hasSubMenu &&
+ {link.subMenus.map(sLink => {
+ return
+
+ {link?.icon && } {sLink.title}
+
+
+ })}
+ }
+ >
+}
diff --git a/themes/heo/components/MenuItemDrop.js b/themes/heo/components/MenuItemDrop.js
new file mode 100644
index 00000000..fe8fdf53
--- /dev/null
+++ b/themes/heo/components/MenuItemDrop.js
@@ -0,0 +1,42 @@
+import Link from 'next/link'
+import { useState } from 'react'
+
+export const MenuItemDrop = ({ link }) => {
+ const [show, changeShow] = useState(false)
+ const hasSubMenu = link?.subMenus?.length > 0
+
+ if (!link || !link.show) {
+ return null
+ }
+
+ return
changeShow(true)} onMouseOut={() => changeShow(false)} >
+
+ {/* 不含子菜单 */}
+ {!hasSubMenu &&
+
+ {link?.icon &&
} {link?.name}
+ {hasSubMenu &&
}
+ }
+
+ {/* 含子菜单的按钮 */}
+ {hasSubMenu && <>
+
+ {link?.icon && } {link?.name}
+
+ >}
+
+ {/* 子菜单 */}
+ {hasSubMenu &&
+ {link.subMenus.map((sLink, index) => {
+ return -
+
+ {link?.icon && }{sLink.title}
+
+
+ })}
+
}
+
+
+}
diff --git a/themes/heo/components/MenuListSide.js b/themes/heo/components/MenuListSide.js
new file mode 100644
index 00000000..1a3b2f5b
--- /dev/null
+++ b/themes/heo/components/MenuListSide.js
@@ -0,0 +1,37 @@
+import React from 'react'
+import { useGlobal } from '@/lib/global'
+import BLOG from '@/blog.config'
+import { MenuItemCollapse } from './MenuItemCollapse'
+import CONFIG from '../config'
+
+export const MenuListSide = (props) => {
+ const { customNav, customMenu } = props
+ const { locale } = useGlobal()
+
+ let links = [
+ { icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE },
+ { icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH },
+ { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY },
+ { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG }
+ ]
+
+ if (customNav) {
+ links = customNav.concat(links)
+ }
+
+ // 如果 开启自定义菜单,则覆盖Page生成的菜单
+ if (BLOG.CUSTOM_MENU) {
+ links = customMenu
+ }
+
+ if (!links || links.length === 0) {
+ return null
+ }
+
+ return (
+
+ )
+}
diff --git a/themes/heo/components/MenuListTop.js b/themes/heo/components/MenuListTop.js
new file mode 100644
index 00000000..a2fd66ad
--- /dev/null
+++ b/themes/heo/components/MenuListTop.js
@@ -0,0 +1,37 @@
+import React from 'react'
+import { useGlobal } from '@/lib/global'
+import CONFIG from '../config'
+import BLOG from '@/blog.config'
+import { MenuItemDrop } from './MenuItemDrop'
+
+export const MenuListTop = (props) => {
+ const { customNav, customMenu } = props
+ const { locale } = useGlobal()
+
+ let links = [
+ { id: 1, icon: 'fa-solid fa-house', name: locale.NAV.INDEX, to: '/', show: CONFIG.MENU_INDEX },
+ { id: 2, icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH },
+ { id: 3, icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE }
+ // { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY },
+ // { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG }
+ ]
+
+ if (customNav) {
+ links = links.concat(customNav)
+ }
+
+ // 如果 开启自定义菜单,则覆盖Page生成的菜单
+ if (BLOG.CUSTOM_MENU) {
+ links = customMenu
+ }
+
+ if (!links || links.length === 0) {
+ return null
+ }
+
+ return (<>
+
+ >)
+}
diff --git a/themes/heo/components/NavBar.js b/themes/heo/components/NavBar.js
new file mode 100644
index 00000000..297df23b
--- /dev/null
+++ b/themes/heo/components/NavBar.js
@@ -0,0 +1,94 @@
+import { useCallback, useEffect, useRef, useState } from 'react'
+import Logo from './Logo'
+
+import { MenuListTop } from './MenuListTop'
+import throttle from 'lodash.throttle'
+import RandomPostButton from './RandomPostButton'
+import SearchButton from './SearchButton'
+import SlideOver from './SlideOver'
+import ReadingProgress from './ReadingProgress'
+/**
+ * 顶部导航
+ * @param {*} param0
+ * @returns
+ */
+const NavBar = props => {
+ const [fixedNav, setFixedNav] = useState(false)
+ const [textWhite, setTextWhite] = useState(false)
+ const [navBgWhite, setBgWhite] = useState(false)
+ const slideOverRef = useRef()
+
+ const toggleMenuOpen = () => {
+ slideOverRef?.current?.toggleSlideOvers()
+ }
+
+ // 监听滚动
+ useEffect(() => {
+ scrollTrigger()
+ window.addEventListener('scroll', scrollTrigger)
+ return () => {
+ window.removeEventListener('scroll', scrollTrigger)
+ }
+ }, [])
+
+ const throttleMs = 200
+
+ /**
+ * 根据滚动条,切换导航栏样式
+ */
+ 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)
+ }
+ }, throttleMs))
+
+ return (<>
+ {/* 顶部导航菜单栏 */}
+
+ >)
+}
+
+export default NavBar
diff --git a/themes/heo/components/NavButtonGroup.js b/themes/heo/components/NavButtonGroup.js
new file mode 100644
index 00000000..2a3fc898
--- /dev/null
+++ b/themes/heo/components/NavButtonGroup.js
@@ -0,0 +1,33 @@
+
+import React from 'react'
+import Link from 'next/link'
+
+/**
+ * 首页导航大按钮组件
+ * @param {*} props
+ * @returns
+ */
+const NavButtonGroup = (props) => {
+ const { categoryOptions } = props
+ if (!categoryOptions || categoryOptions.length === 0) {
+ return <>>
+ }
+
+ return (
+
+ )
+}
+export default NavButtonGroup
diff --git a/themes/heo/components/NoticeBar.js b/themes/heo/components/NoticeBar.js
new file mode 100644
index 00000000..b7ec3f3b
--- /dev/null
+++ b/themes/heo/components/NoticeBar.js
@@ -0,0 +1,37 @@
+
+import { ArrowRightCircle } from '@/components/HeroIcons'
+import CONFIG from '../config'
+import Swipe from './Swipe'
+
+/**
+ * 通知横幅
+ */
+export function NoticeBar() {
+ const notices = CONFIG.NOTICE_BAR
+
+ if (!notices || notices?.length === 0) {
+ return <>>
+ }
+
+ return (
+
+
+
+ )
+}
+
+/**
+ * {notices?.map((n, index) => {
+ return
+
{n.category}
+
{n.title}
+
+
+ })}
+ */
diff --git a/themes/heo/components/PaginationNumber.js b/themes/heo/components/PaginationNumber.js
new file mode 100644
index 00000000..1b96b42a
--- /dev/null
+++ b/themes/heo/components/PaginationNumber.js
@@ -0,0 +1,160 @@
+import { ChevronDoubleRight } from '@/components/HeroIcons'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useState } from 'react'
+
+/**
+ * 数字翻页插件
+ * @param page 当前页码
+ * @param showNext 是否有下一页
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const PaginationNumber = ({ page, totalPage }) => {
+ const router = useRouter()
+ const { locale } = useGlobal()
+ const currentPage = +page
+ const showNext = page < totalPage
+ const pagePrefix = router.asPath.split('?')[0].replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '')
+ const pages = generatePages(pagePrefix, page, currentPage, totalPage)
+
+ const [value, setValue] = useState('')
+
+ const handleInputChange = (event) => {
+ const newValue = event.target.value.replace(/[^0-9]/g, '')
+ setValue(newValue)
+ }
+
+ /**
+ * 调到指定页
+ */
+ const jumpToPage = () => {
+ if (value) {
+ router.push(value === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${value}`)
+ }
+ }
+
+ return (
+
+ {/* 上一页 */}
+
+
+
+
+ {locale.PAGINATION.PREV}
+
+
+
+
+
+ {/* 分页 */}
+
+ {pages}
+
+ {/* 跳转页码 */}
+
+
+
+ {/* 下一页 */}
+
+
+
+
+
+ {locale.PAGINATION.NEXT}
+
+
+
+
+ )
+}
+
+/**
+ * 页码按钮
+ * @param {*} page
+ * @param {*} currentPage
+ * @param {*} pagePrefix
+ * @returns
+ */
+function getPageElement(page, currentPage, pagePrefix) {
+ const selected = page + '' === currentPage + ''
+ return (
+ (
+
+ {page}
+
+ )
+ )
+}
+
+/**
+ * 获取所有页码
+ * @param {*} pagePrefix
+ * @param {*} page
+ * @param {*} currentPage
+ * @param {*} totalPage
+ * @returns
+ */
+function generatePages(pagePrefix, page, currentPage, totalPage) {
+ const pages = []
+ const groupCount = 7 // 最多显示页签数
+ if (totalPage <= groupCount) {
+ for (let i = 1; i <= totalPage; i++) {
+ pages.push(getPageElement(i, page, pagePrefix))
+ }
+ } else {
+ pages.push(getPageElement(1, page, pagePrefix))
+ const dynamicGroupCount = groupCount - 2
+ let startPage = currentPage - 2
+ if (startPage <= 1) {
+ startPage = 2
+ }
+ if (startPage + dynamicGroupCount > totalPage) {
+ startPage = totalPage - dynamicGroupCount
+ }
+ if (startPage > 2) {
+ pages.push(
...
)
+ }
+
+ for (let i = 0; i < dynamicGroupCount; i++) {
+ if (startPage + i < totalPage) {
+ pages.push(getPageElement(startPage + i, page, pagePrefix))
+ }
+ }
+
+ if (startPage + dynamicGroupCount < totalPage) {
+ pages.push(
...
)
+ }
+
+ pages.push(getPageElement(totalPage, page, pagePrefix))
+ }
+ return pages
+}
+export default PaginationNumber
diff --git a/themes/heo/components/PostHeader.js b/themes/heo/components/PostHeader.js
new file mode 100644
index 00000000..9eab504e
--- /dev/null
+++ b/themes/heo/components/PostHeader.js
@@ -0,0 +1,100 @@
+import Link from 'next/link'
+import BLOG from '@/blog.config'
+import NotionIcon from '@/components/NotionIcon'
+import WavesArea from './WavesArea'
+import { HashTag } from '@/components/HeroIcons'
+import WordCount from '@/components/WordCount'
+
+export default function PostHeader({ post, siteInfo }) {
+ if (!post) {
+ return <>>
+ }
+ // 文章头图
+ const headerImage = post?.pageCover ? post.pageCover : siteInfo?.pageCover
+
+ return (
+
+
+
+
+
+ {/* 文章背景图 */}
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+
+
+ {/* 文章文字描述 */}
+
+ {/* 分类+标签 */}
+
+ {post.category && <>
+
+
+ {post.category}
+
+
+ >}
+
+ {post.tagItems && (
+
+ {post.tagItems.map(tag => (
+
+
{tag.name + (tag.count ? `(${tag.count})` : '')}
+
+
+ ))}
+
+ )}
+
+
+ {/* 文章Title */}
+
+ {post.title}
+
+
+ {/* 标题底部补充信息 */}
+
+
+
+
+ {post?.type !== 'Page' && (
+ <>
+
+
{post?.publishTime}
+
+ >
+ )}
+
+
+
+ {BLOG.ANALYTICS_BUSUANZI_ENABLE &&
+
+
}
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/themes/heo/components/Progress.js b/themes/heo/components/Progress.js
new file mode 100644
index 00000000..5b4f9b20
--- /dev/null
+++ b/themes/heo/components/Progress.js
@@ -0,0 +1,44 @@
+import React, { useEffect, useState } from 'react'
+import { isBrowser } from '@/lib/utils'
+
+/**
+ * 顶部页面阅读进度条
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const Progress = ({ targetRef, showPercent = true }) => {
+ const currentRef = targetRef?.current || targetRef
+ const [percent, changePercent] = useState(0)
+ const scrollListener = () => {
+ const target = currentRef || (isBrowser() && document.getElementById('article-wrapper'))
+ 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)
+ }, [])
+
+ return (
+
+
+ {showPercent && (
+
{percent}%
+ )}
+
+
+ )
+}
+
+export default Progress
diff --git a/themes/heo/components/RandomPostButton.js b/themes/heo/components/RandomPostButton.js
new file mode 100644
index 00000000..66d6cc85
--- /dev/null
+++ b/themes/heo/components/RandomPostButton.js
@@ -0,0 +1,19 @@
+import { useRouter } from 'next/router'
+
+/**
+ * 随机跳转到一个文章
+ */
+export default function RandomPostButton(props) {
+ const { latestPosts } = props
+ const router = useRouter()
+ function handleClick() {
+ const randomIndex = Math.floor(Math.random() * latestPosts.length)
+ const randomPost = latestPosts[randomIndex]
+ router.push(randomPost.slug)
+ }
+ return (
+
+
+
+ )
+}
diff --git a/themes/heo/components/ReadingProgress.js b/themes/heo/components/ReadingProgress.js
new file mode 100644
index 00000000..fed74a54
--- /dev/null
+++ b/themes/heo/components/ReadingProgress.js
@@ -0,0 +1,42 @@
+import { ArrowSmallUp } from '@/components/HeroIcons'
+import { useEffect, useState } from 'react'
+
+/**
+ * 回顶按钮
+ * @returns
+ */
+export default function ReadingProgress() {
+ const [scrollPercentage, setScrollPercentage] = useState(0)
+ useEffect(() => {
+ let requestId
+
+ function handleScroll() {
+ const scrollHeight = document.documentElement.scrollHeight
+ const clientHeight = document.documentElement.clientHeight
+ const scrollY = window.scrollY || window.pageYOffset
+
+ const percent = Math.floor((scrollY / (scrollHeight - clientHeight)) * 100)
+ setScrollPercentage(percent)
+
+ requestId = requestAnimationFrame(handleScroll)
+ }
+
+ handleScroll() // 初始化滚动位置
+
+ return () => {
+ cancelAnimationFrame(requestId)
+ }
+ }, [])
+
+ return (<>
+
window.scrollTo({ top: 0, behavior: 'smooth' })}
+ className={`${scrollPercentage > 0 ? 'w-10 h-10 ' : 'w-0 h-0 opacity-0'} group cursor-pointer hover:bg-black hover:bg-opacity-10 rounded-full flex justify-center items-center duration-200 transition-all`} >
+
+
+ {scrollPercentage < 100 ? scrollPercentage :
}
+
+
+
+ >)
+}
diff --git a/themes/heo/components/RightFloatArea.js b/themes/heo/components/RightFloatArea.js
new file mode 100644
index 00000000..d7fadce5
--- /dev/null
+++ b/themes/heo/components/RightFloatArea.js
@@ -0,0 +1,42 @@
+import throttle from 'lodash.throttle'
+import { useCallback, useEffect, useState } from 'react'
+import FloatDarkModeButton from './FloatDarkModeButton'
+import JumpToTopButton from './JumpToTopButton'
+
+/**
+ * 悬浮在右下角的按钮,当页面向下滚动100px时会出现
+ * @param {*} param0
+ * @returns
+ */
+export default function RightFloatArea({ floatSlot }) {
+ const [showFloatButton, switchShow] = useState(false)
+ const scrollListener = useCallback(throttle(() => {
+ const targetRef = document.getElementById('wrapper')
+ const clientHeight = targetRef?.clientHeight
+ const scrollY = window.pageYOffset
+ const fullHeight = clientHeight - window.outerHeight
+ let per = parseFloat(((scrollY / fullHeight) * 100).toFixed(0))
+ if (per > 100) per = 100
+ const shouldShow = scrollY > 100 && per > 0
+
+ // 右下角显示悬浮按钮
+ if (shouldShow !== showFloatButton) {
+ switchShow(shouldShow)
+ }
+ }, 200))
+
+ useEffect(() => {
+ document.addEventListener('scroll', scrollListener)
+ return () => document.removeEventListener('scroll', scrollListener)
+ }, [])
+
+ return (
+
+ )
+}
diff --git a/themes/heo/components/SearchButton.js b/themes/heo/components/SearchButton.js
new file mode 100644
index 00000000..137d6c95
--- /dev/null
+++ b/themes/heo/components/SearchButton.js
@@ -0,0 +1,7 @@
+import Link from 'next/link'
+
+export default function SearchButton() {
+ return
+
+
+}
diff --git a/themes/heo/components/SearchDrawer.js b/themes/heo/components/SearchDrawer.js
new file mode 100644
index 00000000..c7ec88a7
--- /dev/null
+++ b/themes/heo/components/SearchDrawer.js
@@ -0,0 +1,36 @@
+import { Router } from 'next/router'
+import { useImperativeHandle, useRef } from 'react'
+import SearchInput from './SearchInput'
+const SearchDrawer = ({ cRef, slot }) => {
+ const searchDrawer = useRef()
+ const searchInputRef = useRef()
+ useImperativeHandle(cRef, () => {
+ return {
+ show: () => {
+ searchDrawer?.current?.classList?.remove('hidden')
+ searchInputRef?.current?.focus()
+ }
+ }
+ })
+ const hidden = () => {
+ searchDrawer?.current?.classList?.add('hidden')
+ }
+ Router.events.on('routeChangeComplete', (...args) => {
+ hidden()
+ })
+ return (
+
+ )
+}
+
+export default SearchDrawer
diff --git a/themes/heo/components/SearchInput.js b/themes/heo/components/SearchInput.js
new file mode 100644
index 00000000..6e577bba
--- /dev/null
+++ b/themes/heo/components/SearchInput.js
@@ -0,0 +1,106 @@
+import { useRouter } from 'next/router'
+import { useImperativeHandle, useRef, useState } from 'react'
+import { useGlobal } from '@/lib/global'
+let lock = false
+
+const SearchInput = props => {
+ const { currentSearch, cRef, className } = props
+ const [onLoading, setLoadingState] = useState(false)
+ const router = useRouter()
+ const searchInputRef = useRef()
+ const { locale } = useGlobal()
+ useImperativeHandle(cRef, () => {
+ return {
+ focus: () => {
+ searchInputRef?.current?.focus()
+ }
+ }
+ })
+
+ const handleSearch = () => {
+ const key = searchInputRef.current.value
+ if (key && key !== '') {
+ setLoadingState(true)
+ router.push({ pathname: '/search/' + key }).then(r => {
+ setLoadingState(false)
+ })
+ // location.href = '/search/' + key
+ } else {
+ router.push({ pathname: '/' }).then(r => {})
+ }
+ }
+ const handleKeyUp = e => {
+ if (e.keyCode === 13) {
+ // 回车
+ handleSearch(searchInputRef.current.value)
+ } else if (e.keyCode === 27) {
+ // ESC
+ cleanSearch()
+ }
+ }
+ const cleanSearch = () => {
+ searchInputRef.current.value = ''
+ }
+
+ const [showClean, setShowClean] = useState(false)
+ const updateSearchKey = val => {
+ if (lock) {
+ return
+ }
+ searchInputRef.current.value = val
+
+ if (val) {
+ setShowClean(true)
+ } else {
+ setShowClean(false)
+ }
+ }
+ function lockSearchInput () {
+ lock = true
+ }
+
+ function unLockSearchInput () {
+ lock = false
+ }
+
+ return (
+
+
updateSearchKey(e.target.value)}
+ defaultValue={currentSearch || ''}
+ />
+
+
+
+
+
+ {showClean && (
+
+
+
+ )}
+
+ )
+}
+
+export default SearchInput
diff --git a/themes/heo/components/SearchNav.js b/themes/heo/components/SearchNav.js
new file mode 100644
index 00000000..7be2b8ce
--- /dev/null
+++ b/themes/heo/components/SearchNav.js
@@ -0,0 +1,68 @@
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import { useEffect, useRef } from 'react'
+import Card from './Card'
+import SearchInput from './SearchInput'
+import TagItemMini from './TagItemMini'
+
+/**
+ * 搜索页面的导航
+ * @param {*} props
+ * @returns
+ */
+export default function SearchNav(props) {
+ const { tagOptions, categoryOptions } = props
+ const cRef = useRef(null)
+ const { locale } = useGlobal()
+ useEffect(() => {
+ // 自动聚焦到搜索框
+ cRef?.current?.focus()
+ }, [])
+
+ return <>
+
+
+ {/* 分类 */}
+
+
+ {locale.COMMON.CATEGORY}:
+
+
+ {categoryOptions?.map(category => {
+ return (
+
+
+
+ {category.name}({category.count})
+
+
+ )
+ })}
+
+
+ {/* 标签 */}
+
+
+ {locale.COMMON.TAGS}:
+
+
+
+
+>
+}
diff --git a/themes/heo/components/SideBar.js b/themes/heo/components/SideBar.js
new file mode 100644
index 00000000..e43c4e12
--- /dev/null
+++ b/themes/heo/components/SideBar.js
@@ -0,0 +1,33 @@
+import BLOG from '@/blog.config'
+import { useRouter } from 'next/router'
+import MenuGroupCard from './MenuGroupCard'
+import { MenuListSide } from './MenuListSide'
+
+/**
+ * 侧边抽屉
+ * @param tags
+ * @param currentTag
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const SideBar = (props) => {
+ const { siteInfo } = props
+ const router = useRouter()
+ return (
+
+
+
+
{ router.push('/') }}
+ className='justify-center items-center flex hover:rotate-45 py-6 hover:scale-105 dark:text-gray-100 transform duration-200 cursor-pointer'>
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+
+
+
+
+
+
+ )
+}
+
+export default SideBar
diff --git a/themes/heo/components/SideBarDrawer.js b/themes/heo/components/SideBarDrawer.js
new file mode 100644
index 00000000..87125c05
--- /dev/null
+++ b/themes/heo/components/SideBarDrawer.js
@@ -0,0 +1,51 @@
+import { useRouter } from 'next/router'
+import { useEffect } from 'react'
+
+/**
+ * 侧边栏抽屉面板,可以从侧面拉出
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const SideBarDrawer = ({ children, isOpen, onOpen, onClose, className }) => {
+ const router = useRouter()
+ useEffect(() => {
+ const sideBarDrawerRouteListener = () => {
+ switchSideDrawerVisible(false)
+ }
+ router.events.on('routeChangeComplete', sideBarDrawerRouteListener)
+ return () => {
+ router.events.off('routeChangeComplete', sideBarDrawerRouteListener)
+ }
+ }, [router.events])
+
+ // 点击按钮更改侧边抽屉状态
+ const switchSideDrawerVisible = (showStatus) => {
+ if (showStatus) {
+ onOpen && onOpen()
+ } else {
+ onClose && onClose()
+ }
+ const sideBarDrawer = window.document.getElementById('sidebar-drawer')
+ const sideBarDrawerBackground = window.document.getElementById('sidebar-drawer-background')
+
+ if (showStatus) {
+ sideBarDrawer?.classList.replace('-mr-72', 'mr-0')
+ sideBarDrawerBackground?.classList.replace('hidden', 'block')
+ } else {
+ sideBarDrawer?.classList.replace('mr-0', '-mr-72')
+ sideBarDrawerBackground?.classList.replace('block', 'hidden')
+ }
+ }
+
+ return
+ )
+}
diff --git a/themes/heo/components/WavesArea.js b/themes/heo/components/WavesArea.js
new file mode 100644
index 00000000..073b4670
--- /dev/null
+++ b/themes/heo/components/WavesArea.js
@@ -0,0 +1,65 @@
+import { useGlobal } from '@/lib/global'
+
+/**
+ * 文章波浪动画
+ */
+export default function WavesArea() {
+ const { isDarkMode } = useGlobal()
+ const color = isDarkMode ? '#18171d' : '#f7f9fe'
+
+ return (
+
+
+
+
+ )
+}
diff --git a/themes/heo/config.js b/themes/heo/config.js
new file mode 100644
index 00000000..ee494980
--- /dev/null
+++ b/themes/heo/config.js
@@ -0,0 +1,113 @@
+const CONFIG = {
+ HOME_BANNER_ENABLE: true,
+
+ SITE_CREATE_TIME: '2021-09-21', // 建站日期,用于计算网站运行的第几天
+
+ // 首页顶部通知条滚动内容,如不需要可以留空 []
+ NOTICE_BAR: [
+ { url: 'https://blog.tangly1024.com', title: '欢迎来到我的博客' },
+ { url: 'https://docs.tangly1024.com', title: '访问文档中心获取更多帮助' }
+ ],
+
+ // 个人资料卡牌欢迎语
+ INFOCARD_GREETINGS: [
+ '你好!我是',
+ '🔍 分享与热心帮助',
+ '🤝 专修交互与设计',
+ '🏃 脚踏实地行动派',
+ '🏠 智能家居小能手',
+ '🤖️ 数码科技爱好者',
+ '🧱 团队小组发动机'
+ ],
+
+ // 用户技能图标
+ GROUP_ICONS: [
+ {
+ title_1: 'AfterEffect',
+ img_1: 'https://p.zhheo.com/20239df3f66615b532ce571eac6d14ff21cf072602.png!cover',
+ color_1: '#989bf8',
+ title_2: 'Sketch',
+ img_2: 'https://p.zhheo.com/2023e0ded7b724a39f12d59c3dc8fbdc7cbe074202.png!cover',
+ color_2: '#ffffff'
+ },
+ {
+ title_1: 'Docker',
+ img_1: 'https://p.zhheo.com/20231108a540b2862d26f8850172e4ea58ed075102.png!cover',
+ color_1: '#57b6e6',
+ title_2: 'Photoshop',
+ img_2: 'https://p.zhheo.com/2023e4058a91608ea41751c4f102b131f267075902.png!cover',
+ color_2: '#4082c3'
+ },
+ {
+ title_1: 'FinalCutPro',
+ img_1: 'https://p.zhheo.com/20233e777652412247dd57fd9b48cf997c01070702.png!cover',
+ color_1: '#ffffff',
+ title_2: 'Python',
+ img_2: 'https://p.zhheo.com/20235c0731cd4c0c95fc136a8db961fdf963071502.png!cover',
+ color_2: '#ffffff'
+ },
+ {
+ title_1: 'Swift',
+ img_1: 'https://p.zhheo.com/202328bbee0b314297917b327df4a704db5c072402.png!cover',
+ color_1: '#eb6840',
+ title_2: 'Principle',
+ img_2: 'https://p.zhheo.com/2023f76570d2770c8e84801f7e107cd911b5073202.png!cover',
+ color_2: '#8f55ba'
+ },
+ {
+ title_1: 'illustrator',
+ img_1: 'https://p.zhheo.com/20237359d71b45ab77829cee5972e36f8c30073902.png!cover',
+ color_1: '#f29e39',
+ title_2: 'CSS3',
+ img_2: 'https://p.zhheo.com/20237c548846044a20dad68a13c0f0e1502f074602.png!cover',
+ color_2: '#2c51db'
+ },
+ {
+ title_1: 'JS',
+ img_1: 'https://p.zhheo.com/2023786e7fc488f453d5fb2be760c96185c0075502.png!cover',
+ color_1: '#f7cb4f',
+ title_2: 'HTML',
+ img_2: 'https://p.zhheo.com/202372b4d760fd8a497d442140c295655426070302.png!cover',
+ color_2: '#e9572b'
+ },
+ {
+ title_1: 'Git',
+ img_1: 'https://p.zhheo.com/2023ffa5707c4e25b6beb3e6a3d286ede4c6071102.png!cover',
+ color_1: '#df5b40',
+ title_2: 'Rhino',
+ img_2: 'https://p.zhheo.com/20231ca53fa0b09a3ff1df89acd7515e9516173302.png!cover',
+ color_2: '#1f1f1f'
+ }
+ ],
+
+ HOME_NAV_BUTTONS: true, // 首页是否显示分类大图标按钮
+ // 已知未修复bug, 在移动端开启true后会加载不出图片; 暂时建议设置为false。
+ HOME_NAV_BACKGROUND_IMG_FIXED: false, // 首页背景图滚动时是否固定,true 则滚动时图片不懂动; false则随鼠标滚动 ;
+
+ // 菜单配置
+ MENU_INDEX: true, // 显示首页
+ MENU_CATEGORY: true, // 显示分类
+ MENU_TAG: true, // 显示标签
+ MENU_ARCHIVE: true, // 显示归档
+ MENU_SEARCH: true, // 显示搜索
+
+ POST_LIST_COVER: true, // 列表显示文章封面
+ POST_LIST_COVER_HOVER_ENLARGE: false, // 列表鼠标悬停放大
+
+ POST_LIST_COVER_DEFAULT: true, // 封面为空时用站点背景做默认封面
+ POST_LIST_SUMMARY: true, // 文章摘要
+ POST_LIST_PREVIEW: false, // 读取文章预览
+ POST_LIST_IMG_CROSSOVER: true, // 博客列表图片左右交错
+
+ ARTICLE_ADJACENT: true, // 显示上一篇下一篇文章推荐
+ ARTICLE_COPYRIGHT: true, // 显示文章版权声明
+ ARTICLE_RECOMMEND: true, // 文章关联推荐
+
+ WIDGET_LATEST_POSTS: true, // 显示最新文章卡
+ WIDGET_ANALYTICS: false, // 显示统计卡
+ WIDGET_TO_TOP: true,
+ WIDGET_TO_COMMENT: true, // 跳到评论区
+ WIDGET_DARK_MODE: true, // 夜间模式
+ WIDGET_TOC: true // 移动端悬浮目录
+}
+export default CONFIG
diff --git a/themes/heo/index.js b/themes/heo/index.js
new file mode 100644
index 00000000..304216ec
--- /dev/null
+++ b/themes/heo/index.js
@@ -0,0 +1,430 @@
+import CONFIG from './config'
+
+import CommonHead from '@/components/CommonHead'
+import { useEffect } from 'react'
+import Footer from './components/Footer'
+import SideRight from './components/SideRight'
+import NavBar from './components/NavBar'
+import { useGlobal } from '@/lib/global'
+import BLOG from '@/blog.config'
+import { isBrowser, loadExternalResource } from '@/lib/utils'
+import BlogPostListPage from './components/BlogPostListPage'
+import BlogPostListScroll from './components/BlogPostListScroll'
+import Hero from './components/Hero'
+import { useRouter } from 'next/router'
+import Mark from 'mark.js'
+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'
+
+/**
+ * 基础布局 采用上中下布局,移动端使用顶部侧边导航栏
+ * @param props
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const LayoutBase = props => {
+ const { children, headerSlot, slotTop, slotRight, meta, siteInfo } = props
+ const { onLoading } = useGlobal()
+
+ // 加载主题样式
+ if (isBrowser()) {
+ loadExternalResource('/css/theme-hexo.css', 'css')
+ }
+
+ return (
+
+ {/* 网页SEO */}
+
+
+
+ {/* 顶部嵌入 导航栏,首页放hero,文章页放文章详情 */}
+ {headerSlot}
+
+ {/* 主区块 */}
+
+
+
+
+
+ {/* 主区上部嵌入 */}
+ {slotTop}
+
+ {children}
+
+
+
+ {/* 主区快右侧 */}
+ {slotRight}
+
+
+
+
+
+ {/* 页脚 */}
+
+
+ )
+}
+
+/**
+ * 首页
+ * 是一个博客列表,嵌入一个Hero大图
+ * @param {*} props
+ * @returns
+ */
+const LayoutIndex = (props) => {
+ const headerSlot =
+ {/* 顶部导航 */}
+
+ {/* 通知横幅 */}
+
+
+
+
+ // 右侧栏 用户信息+标签列表
+ const slotRight =
+
+ return
+ {/* 文章分类条 */}
+
+ {BLOG.POST_LIST_STYLE === 'page' ? : }
+
+}
+
+/**
+ * 博客列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutPostList = (props) => {
+ // 右侧栏
+ const slotRight =
+ const headerSlot =
+
+ return
+ {/* 文章分类条 */}
+
+ {BLOG.POST_LIST_STYLE === 'page' ? : }
+
+}
+
+/**
+ * 搜索
+ * @param {*} props
+ * @returns
+ */
+const LayoutSearch = props => {
+ const { keyword } = props
+ const router = useRouter()
+ const currentSearch = keyword || router?.query?.s
+ const headerSlot =
+
+ useEffect(() => {
+ setTimeout(() => {
+ if (currentSearch) {
+ const targets = document.getElementsByClassName('replace')
+ for (const container of targets) {
+ if (container && container.innerHTML) {
+ const re = new RegExp(currentSearch, 'gim')
+ const instance = new Mark(container)
+ instance.markRegExp(re, {
+ element: 'span',
+ className: 'text-red-500 border-b border-dashed'
+ })
+ }
+ }
+ }
+ }, 100)
+ })
+
+ return (
+
+ {!currentSearch
+ ?
+ : {BLOG.POST_LIST_STYLE === 'page' ? : }
}
+
+ )
+}
+
+/**
+ * 归档
+ * @param {*} props
+ * @returns
+ */
+const LayoutArchive = (props) => {
+ const { archivePosts } = props
+
+ // 右侧栏
+ const slotRight =
+ const headerSlot =
+
+ // 归档页顶部显示条,如果是默认归档则不显示。分类详情页显示分类列表,标签详情页显示当前标签
+
+ return
+
+ {/* 文章分类条 */}
+
+
+
+ {Object.keys(archivePosts).map(archiveTitle => (
+
+ ))}
+
+
+
+}
+
+/**
+ * 文章详情
+ * @param {*} props
+ * @returns
+ */
+const LayoutSlug = props => {
+ const { post, lock, validPassword } = props
+ const { locale } = useGlobal()
+
+ // 右侧栏
+ const slotRight =
+ const headerSlot =
+
+ return (
+
+
+ {lock &&
}
+
+ {!lock &&
+
+
+ {/* Notion文章主体 */}
+
+
+ {/* 分享 */}
+
+ {post?.type === 'Post' && <>
+
+ {/* 版权 */}
+
+ {/* 文章推荐 */}
+
+ {/* 上一篇\下一篇文章 */}
+
+ >}
+
+
+
+
+
+ {/* 评论互动 */}
+
+
{locale.COMMON.COMMENTS}
+
+
+
}
+
+
+
+ )
+}
+
+/**
+ * 404
+ * @param {*} props
+ * @returns
+ */
+const Layout404 = props => {
+ const { meta, siteInfo } = props
+ const { onLoading } = useGlobal()
+ return (
+
+ {/* 网页SEO */}
+
+
+
+ {/* 顶部嵌入 导航栏,首页放hero,文章页放文章详情 */}
+
+
+ {/* 主区块 */}
+
+
+
+
+
+
+ {/* 404卡牌 */}
+
+ {/* 左侧动图 */}
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+
+ {/* 右侧文字 */}
+
+
404
+
请尝试站内搜索寻找文章
+
+
+
+
+
+
+ {/* 404页面底部显示最新文章 */}
+
+
+
+
+
+
+
+
+
+ )
+}
+
+/**
+ * 分类列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutCategoryIndex = props => {
+ const { categoryOptions } = props
+ const { locale } = useGlobal()
+ const headerSlot =
+
+ return (
+
+
+ {locale.COMMON.CATEGORY}
+
+
+ {categoryOptions.map(category => {
+ return (
+
+
+
+ {category.name}
+
+ {category.count}
+
+
+
+ )
+ })}
+
+
+ )
+}
+
+/**
+ * 标签列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutTagIndex = props => {
+ const { tagOptions } = props
+ const { locale } = useGlobal()
+ return (
+
+
+ {locale.COMMON.TAGS}
+
+
+ {tagOptions.map(tag => {
+ return (
+
+
+
+ {tag.name}
+
+ {tag.count}
+
+
+
+ )
+ })}
+
+
+ )
+}
+
+export {
+ CONFIG as THEME_CONFIG,
+ LayoutIndex,
+ LayoutSearch,
+ LayoutArchive,
+ LayoutSlug,
+ Layout404,
+ LayoutCategoryIndex,
+ LayoutPostList,
+ LayoutTagIndex
+}
diff --git a/themes/heo/style.js b/themes/heo/style.js
new file mode 100644
index 00000000..632ec93a
--- /dev/null
+++ b/themes/heo/style.js
@@ -0,0 +1,64 @@
+/* eslint-disable react/no-unknown-property */
+/**
+ * 此处样式只对当前主题生效
+ * 此处不支持tailwindCSS的 @apply 语法
+ * @returns
+ */
+const Style = () => {
+ return
+}
+
+export { Style }
diff --git a/themes/hexo/components/Header.js b/themes/hexo/components/Hero.js
similarity index 100%
rename from themes/hexo/components/Header.js
rename to themes/hexo/components/Hero.js
diff --git a/themes/hexo/components/MenuItemCollapse.js b/themes/hexo/components/MenuItemCollapse.js
index 3ec10f5e..a05c627f 100644
--- a/themes/hexo/components/MenuItemCollapse.js
+++ b/themes/hexo/components/MenuItemCollapse.js
@@ -42,8 +42,8 @@ export const MenuItemCollapse = ({ link }) => {
{/* 折叠子菜单 */}
{hasSubMenu &&
- {link.subMenus.map(sLink => {
- return
+ {link.subMenus.map((sLink, index) => {
+ return
{link?.icon && } {sLink.title}
diff --git a/themes/hexo/components/HeaderArticle.js b/themes/hexo/components/PostHeader.js
similarity index 98%
rename from themes/hexo/components/HeaderArticle.js
rename to themes/hexo/components/PostHeader.js
index b5384a1f..e383672b 100644
--- a/themes/hexo/components/HeaderArticle.js
+++ b/themes/hexo/components/PostHeader.js
@@ -4,7 +4,7 @@ import { useGlobal } from '@/lib/global'
import BLOG from '@/blog.config'
import NotionIcon from '@/components/NotionIcon'
-export default function HeaderArticle({ post, siteInfo }) {
+export default function PostHeader({ post, siteInfo }) {
const { locale } = useGlobal()
if (!post) {
diff --git a/themes/hexo/index.js b/themes/hexo/index.js
index dd4e8aa5..d42831eb 100644
--- a/themes/hexo/index.js
+++ b/themes/hexo/index.js
@@ -7,10 +7,10 @@ import SideRight from './components/SideRight'
import TopNav from './components/TopNav'
import { useGlobal } from '@/lib/global'
import BLOG from '@/blog.config'
-import { isBrowser, loadExternalResource } from '@/lib/utils'
+import { isBrowser } from '@/lib/utils'
import BlogPostListPage from './components/BlogPostListPage'
import BlogPostListScroll from './components/BlogPostListScroll'
-import Hero from './components/Header'
+import Hero from './components/Hero'
import { useRouter } from 'next/router'
import Mark from 'mark.js'
import Card from './components/Card'
@@ -18,7 +18,7 @@ import RightFloatArea from './components/RightFloatArea'
import SearchNav from './components/SearchNav'
import BlogPostArchive from './components/BlogPostArchive'
import { ArticleLock } from './components/ArticleLock'
-import HeaderArticle from './components/HeaderArticle'
+import PostHeader from './components/PostHeader'
import JumpToCommentButton from './components/JumpToCommentButton'
import TocDrawer from './components/TocDrawer'
import TocDrawerButton from './components/TocDrawerButton'
@@ -32,6 +32,7 @@ import TagItemMini from './components/TagItemMini'
import Link from 'next/link'
import SlotBar from './components/SlotBar'
import { Transition } from '@headlessui/react'
+import { Style } from './style'
/**
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
@@ -43,15 +44,11 @@ const LayoutBase = props => {
const { children, headerSlot, floatSlot, slotTop, meta, siteInfo, className } = props
const { onLoading } = useGlobal()
- // 加载主题样式
- if (isBrowser()) {
- loadExternalResource('/css/theme-hexo.css', 'css')
- }
-
return (
{/* 网页SEO */}
+
{/* 顶部导航 */}
@@ -213,7 +210,7 @@ const LayoutSlug = props => {
>
return (
-
} showCategory={false} showTag={false} floatSlot={floatSlot} >
+
} showCategory={false} showTag={false} floatSlot={floatSlot} >
{lock &&
}
diff --git a/themes/hexo/style.js b/themes/hexo/style.js
new file mode 100644
index 00000000..33d2878a
--- /dev/null
+++ b/themes/hexo/style.js
@@ -0,0 +1,76 @@
+/* eslint-disable react/no-unknown-property */
+/**
+ * 这里的css样式只对当前主题生效
+ * 主题客制化css
+ * @returns
+ */
+const Style = () => {
+ return ()
+}
+
+export { Style }
diff --git a/themes/landing/style.js b/themes/landing/style.js
new file mode 100644
index 00000000..7a59e053
--- /dev/null
+++ b/themes/landing/style.js
@@ -0,0 +1,17 @@
+/* eslint-disable react/no-unknown-property */
+/**
+ * 此处样式只对当前主题生效
+ * 此处不支持tailwindCSS的 @apply 语法
+ * @returns
+ */
+const Style = () => {
+ return
+}
+
+export { Style }
diff --git a/themes/matery/components/HeaderArticle.js b/themes/matery/components/PostHeader.js
similarity index 91%
rename from themes/matery/components/HeaderArticle.js
rename to themes/matery/components/PostHeader.js
index 3b9c85a1..dd58bd5f 100644
--- a/themes/matery/components/HeaderArticle.js
+++ b/themes/matery/components/PostHeader.js
@@ -1,6 +1,9 @@
import Image from 'next/image'
-export default function HeaderArticle({ post, siteInfo }) {
+/**
+ * 文章背景图
+ */
+export default function PostHeader({ post, siteInfo }) {
const headerImage = post?.pageCoverThumbnail ? post?.pageCoverThumbnail : siteInfo?.pageCover
const title = post?.title
return (
diff --git a/themes/matery/index.js b/themes/matery/index.js
index 05744dcd..8c6331a9 100644
--- a/themes/matery/index.js
+++ b/themes/matery/index.js
@@ -4,7 +4,6 @@ import TopNav from './components/TopNav'
import Live2D from '@/components/Live2D'
import { useGlobal } from '@/lib/global'
import BLOG from '@/blog.config'
-import { isBrowser, loadExternalResource } from '@/lib/utils'
import Footer from './components/Footer'
import { useEffect } from 'react'
import RightFloatButtons from './components/RightFloatButtons'
@@ -17,7 +16,7 @@ import Hero from './components/Hero'
import Announcement from './components/Announcement'
import CatalogWrapper from './components/CatalogWrapper'
import TagItemMiddle from './components/TagItemMiddle'
-import HeaderArticle from './components/HeaderArticle'
+import PostHeader from './components/PostHeader'
import Link from 'next/link'
import ArticleAdjacent from './components/ArticleAdjacent'
import Comment from '@/components/Comment'
@@ -31,6 +30,7 @@ import Card from './components/Card'
import JumpToCommentButton from './components/JumpToCommentButton'
import BlogListBar from './components/BlogListBar'
import { Transition } from '@headlessui/react'
+import { Style } from './style'
/**
* 基础布局
@@ -43,14 +43,11 @@ const LayoutBase = props => {
const { children, headerSlot, meta, siteInfo, containerSlot, post } = props
const { onLoading } = useGlobal()
- if (isBrowser()) {
- loadExternalResource('/css/theme-matery.css', 'css')
- }
-
return (
{/* SEO相关 */}
+
{/* 顶部导航栏 */}
@@ -177,7 +174,7 @@ const LayoutSearch = props => {
*/
const LayoutArchive = (props) => {
const { archivePosts } = props
- return
} >
+ return
} >
{Object.keys(archivePosts).map(archiveTitle => (
@@ -200,7 +197,7 @@ const LayoutArchive = (props) => {
const LayoutSlug = props => {
const { post, lock, validPassword } = props
- return (
} showCategory={false} showTag={false} floatRightBottom={
}>
+ return (
} showCategory={false} showTag={false} floatRightBottom={
}>
@@ -310,7 +307,7 @@ const LayoutCategoryIndex = props => {
const { categoryOptions } = props
return (
-
} >
+
} >
@@ -340,7 +337,7 @@ const LayoutTagIndex = props => {
const { tagOptions } = props
const { locale } = useGlobal()
return (
-
} >
+
} >
diff --git a/themes/matery/style.js b/themes/matery/style.js
new file mode 100644
index 00000000..711670c9
--- /dev/null
+++ b/themes/matery/style.js
@@ -0,0 +1,31 @@
+/* eslint-disable react/no-unknown-property */
+/**
+ * 此处样式只对当前主题生效
+ * 此处不支持tailwindCSS的 @apply 语法
+ * @returns
+ */
+const Style = () => {
+ return
+}
+
+export { Style }
diff --git a/themes/medium/index.js b/themes/medium/index.js
index a0884129..039652ea 100644
--- a/themes/medium/index.js
+++ b/themes/medium/index.js
@@ -34,6 +34,7 @@ import TagItemMini from './components/TagItemMini'
import ShareBar from '@/components/ShareBar'
import Link from 'next/link'
import { Transition } from '@headlessui/react'
+import { Style } from './style'
// 主题全局状态
const ThemeGlobalMedium = createContext()
@@ -54,7 +55,10 @@ const LayoutBase = props => {
return (
+ {/* SEO相关 */}
+ {/* CSS样式 */}
+
diff --git a/themes/medium/style.js b/themes/medium/style.js
new file mode 100644
index 00000000..5e8eaa5a
--- /dev/null
+++ b/themes/medium/style.js
@@ -0,0 +1,18 @@
+/* eslint-disable react/no-unknown-property */
+/**
+ * 此处样式只对当前主题生效
+ * 此处不支持tailwindCSS的 @apply 语法
+ * @returns
+ */
+const Style = () => {
+ return
+}
+
+export { Style }
diff --git a/themes/next/index.js b/themes/next/index.js
index 98582634..621dd912 100644
--- a/themes/next/index.js
+++ b/themes/next/index.js
@@ -27,6 +27,7 @@ import ArticleDetail from './components/ArticleDetail'
import Link from 'next/link'
import BlogListBar from './components/BlogListBar'
import { Transition } from '@headlessui/react'
+import { Style } from './style'
/**
* 基础布局 采用左中右三栏布局,移动端使用顶部导航栏
@@ -73,6 +74,8 @@ const LayoutBase = (props) => {
{/* SEO相关 */}
+
+
{/* 移动端顶部导航栏 */}
diff --git a/themes/next/style.js b/themes/next/style.js
new file mode 100644
index 00000000..d01b13b3
--- /dev/null
+++ b/themes/next/style.js
@@ -0,0 +1,21 @@
+/* eslint-disable react/no-unknown-property */
+/**
+ * 此处样式只对当前主题生效
+ * 此处不支持tailwindCSS的 @apply 语法
+ * @returns
+ */
+const Style = () => {
+ return
+}
+
+export { Style }
diff --git a/themes/nobelium/components/BlogPost.js b/themes/nobelium/components/BlogPost.js
index 48652cf6..91ad3bd4 100644
--- a/themes/nobelium/components/BlogPost.js
+++ b/themes/nobelium/components/BlogPost.js
@@ -23,7 +23,7 @@ const BlogPost = ({ post }) => {
)
- );
+ )
}
export default BlogPost
diff --git a/themes/nobelium/index.js b/themes/nobelium/index.js
index 7462d8f1..ffbd7f75 100644
--- a/themes/nobelium/index.js
+++ b/themes/nobelium/index.js
@@ -27,6 +27,7 @@ import ShareBar from '@/components/ShareBar'
import Link from 'next/link'
import BlogListBar from './components/BlogListBar'
import { Transition } from '@headlessui/react'
+import { Style } from './style'
/**
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
@@ -44,6 +45,7 @@ const LayoutBase = props => {
{/* SEO相关 */}
+
{/* 顶部导航栏 */}
diff --git a/themes/nobelium/style.js b/themes/nobelium/style.js
new file mode 100644
index 00000000..5e8eaa5a
--- /dev/null
+++ b/themes/nobelium/style.js
@@ -0,0 +1,18 @@
+/* eslint-disable react/no-unknown-property */
+/**
+ * 此处样式只对当前主题生效
+ * 此处不支持tailwindCSS的 @apply 语法
+ * @returns
+ */
+const Style = () => {
+ return
+}
+
+export { Style }
diff --git a/themes/plog/components/BottomNav.js b/themes/plog/components/BottomNav.js
index 0a1d3182..91efb30d 100644
--- a/themes/plog/components/BottomNav.js
+++ b/themes/plog/components/BottomNav.js
@@ -1,11 +1,10 @@
-import Link from 'next/link'
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
import CONFIG from '../config'
-import { SvgIcon } from './SvgIcon'
import { MenuItemDrop } from './MenuItemDrop'
import FullScreenButton from '@/components/FullScreenButton'
import InformationButton from './InformationButton'
+import LogoBar from './LogoBar'
/**
* 桌面端底部导航
@@ -13,35 +12,10 @@ import InformationButton from './InformationButton'
* @returns
*/
const BottomNav = props => {
- const { navBarTitle, siteInfo } = props
-
return <>
-
-
-
- {/*
*/}
- {CONFIG.NAV_NOTION_ICON
- /* eslint-disable-next-line @next/next/no-img-element */
- ?

- :
}
-
-
- {navBarTitle
- ? (
-
-
- {navBarTitle}
-
-
- )
- : (
-
- {siteInfo?.title}
- {' '}{siteInfo?.description}
-
- )}
-
+ {/* 左侧logo文字栏 */}
+
{/* 右下角菜单栏 */}
diff --git a/themes/plog/components/InformationButton.js b/themes/plog/components/InformationButton.js
index a0c94ebf..f73f2a61 100644
--- a/themes/plog/components/InformationButton.js
+++ b/themes/plog/components/InformationButton.js
@@ -9,7 +9,6 @@ import { useRef } from 'react'
export default function InformationButton() {
const slideOversRef = useRef({})
const toggleCollapsed = () => {
- console.log(slideOversRef)
slideOversRef.current.toggleSlideOvers()
}
diff --git a/themes/plog/components/LogoBar.js b/themes/plog/components/LogoBar.js
new file mode 100644
index 00000000..5f68f396
--- /dev/null
+++ b/themes/plog/components/LogoBar.js
@@ -0,0 +1,39 @@
+import BLOG from '@/blog.config'
+import Link from 'next/link'
+import CONFIG from '../config'
+import { SvgIcon } from './SvgIcon'
+
+/**
+ * logo文字栏
+ * @param {*} props
+ * @returns
+ */
+export default function LogoBar(props) {
+ const { navBarTitle, siteInfo } = props
+
+ return
+
+
+ {/*
*/}
+ {CONFIG.NAV_NOTION_ICON
+ /* eslint-disable-next-line @next/next/no-img-element */
+ ?

+ :
}
+
+
+ {navBarTitle
+ ? (
+
+
+ {navBarTitle}
+
+
+ )
+ : (
+
+ {siteInfo?.title}
+ {' '}{siteInfo?.description}
+
+ )}
+
+}
diff --git a/themes/plog/index.js b/themes/plog/index.js
index 43844a92..98ef1e66 100644
--- a/themes/plog/index.js
+++ b/themes/plog/index.js
@@ -25,6 +25,7 @@ import { Transition } from '@headlessui/react'
import BottomNav from './components/BottomNav'
import { saveDarkModeToCookies } from '@/themes/theme'
import Modal from './components/Modal'
+import { Style } from './style'
// 主题全局状态
const ThemeGlobalPlog = createContext()
@@ -63,6 +64,7 @@ const LayoutBase = props => {
{/* SEO相关 */}
+
{/* 移动端顶部导航栏 */}
@@ -88,7 +90,7 @@ const LayoutBase = props => {
- {/* 弹出框 */}
+ {/* 弹出框 - 用于放大显示首页图片等作用 */}
{/* 桌面端底部导航栏 */}
diff --git a/themes/plog/style.js b/themes/plog/style.js
new file mode 100644
index 00000000..23643fa9
--- /dev/null
+++ b/themes/plog/style.js
@@ -0,0 +1,16 @@
+/* eslint-disable react/no-unknown-property */
+/**
+ * 此处样式只对当前主题生效
+ * 此处不支持tailwindCSS的 @apply 语法
+ * @returns
+ */
+const Style = () => {
+ return
+}
+
+export { Style }
diff --git a/themes/simple/index.js b/themes/simple/index.js
index 059fb0b1..9676254b 100644
--- a/themes/simple/index.js
+++ b/themes/simple/index.js
@@ -25,6 +25,7 @@ import { Footer } from './components/Footer'
import { useGlobal } from '@/lib/global'
import SearchInput from './components/SearchInput'
import { Transition } from '@headlessui/react'
+import { Style } from './style'
/**
* 基础布局
@@ -42,6 +43,7 @@ const LayoutBase = props => {
return (
+
{CONFIG.TOP_BAR && }
diff --git a/themes/simple/style.js b/themes/simple/style.js
new file mode 100644
index 00000000..647a404e
--- /dev/null
+++ b/themes/simple/style.js
@@ -0,0 +1,54 @@
+/* eslint-disable react/no-unknown-property */
+/**
+ * 此处样式只对当前主题生效
+ * 此处不支持tailwindCSS的 @apply 语法
+ * @returns
+ */
+const Style = () => {
+ return
+}
+
+export { Style }