updateSearchKey(e.target.value)}
- defaultValue={searchKey}
+ defaultValue={keyword || ''}
/>
- {(searchKey && searchKey.length &&
+ {(keyword && keyword.length &&
diff --git a/themes/fukasawa/components/SiteInfo.js b/themes/fukasawa/components/SiteInfo.js
index 20cb50c0..3a18bcd1 100644
--- a/themes/fukasawa/components/SiteInfo.js
+++ b/themes/fukasawa/components/SiteInfo.js
@@ -12,16 +12,16 @@ function SiteInfo ({ title }) {
return (
)
diff --git a/themes/fukasawa/components/SocialButton.js b/themes/fukasawa/components/SocialButton.js
new file mode 100644
index 00000000..234182cf
--- /dev/null
+++ b/themes/fukasawa/components/SocialButton.js
@@ -0,0 +1,45 @@
+import BLOG from '@/blog.config'
+import React from 'react'
+
+/**
+ * 社交联系方式按钮组
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const SocialButton = () => {
+ return
+}
+export default SocialButton
diff --git a/themes/fukasawa/components/TopNav.js b/themes/fukasawa/components/TopNav.js
index fa4902db..21ca59a9 100644
--- a/themes/fukasawa/components/TopNav.js
+++ b/themes/fukasawa/components/TopNav.js
@@ -37,7 +37,7 @@ const TopNav = props => {
{/* 右侧功能 */}
-
diff --git a/themes/fukasawa/config.js b/themes/fukasawa/config.js
new file mode 100644
index 00000000..f9265e8c
--- /dev/null
+++ b/themes/fukasawa/config.js
@@ -0,0 +1,17 @@
+const CONFIG = {
+
+ POST_LIST_COVER: true, // 文章列表显示图片封面
+ POST_LIST_COVER_FORCE: false, // 即使没有封面也将站点背景图设置为封面
+ POST_LIST_PREVIEW: false, // 显示文章预览
+
+ // 菜单
+ MENU_CATEGORY: true, // 显示分类
+ MENU_TAG: true, // 显示标签
+ MENU_ARCHIVE: true, // 显示归档
+ MENU_SEARCH: false, // 显示搜索
+
+ SIDEBAR_COLLAPSE_BUTTON: true, // 侧边栏折叠按钮
+ SIDEBAR_COLLAPSE_SATUS_DEFAULT: false // 侧边栏默认折叠收起
+
+}
+export default CONFIG
diff --git a/themes/fukasawa/config_fuka.js b/themes/fukasawa/config_fuka.js
deleted file mode 100644
index a10fe869..00000000
--- a/themes/fukasawa/config_fuka.js
+++ /dev/null
@@ -1,13 +0,0 @@
-const FUKA_CONFIG = {
-
- POST_LIST_COVER: true, // 文章列表显示图片封面
- POST_LIST_PREVIEW: false, // 显示文章预览
-
- // 菜单
- MENU_CATEGORY: true, // 显示分类
- MENU_TAG: true, // 显示标签
- MENU_ARCHIVE: true, // 显示归档
- MENU_SEARCH: false // 显示搜索
-
-}
-export default FUKA_CONFIG
diff --git a/themes/fukasawa/index.js b/themes/fukasawa/index.js
index 58968469..52b55b5c 100644
--- a/themes/fukasawa/index.js
+++ b/themes/fukasawa/index.js
@@ -1,25 +1,261 @@
-import CONFIG_FUKA from './config_fuka'
-import { LayoutIndex } from './LayoutIndex'
-import { LayoutSearch } from './LayoutSearch'
-import { LayoutArchive } from './LayoutArchive'
-import { LayoutSlug } from './LayoutSlug'
-import { Layout404 } from './Layout404'
-import { LayoutCategory } from './LayoutCategory'
-import { LayoutCategoryIndex } from './LayoutCategoryIndex'
-import { LayoutPage } from './LayoutPage'
-import { LayoutTag } from './LayoutTag'
-import { LayoutTagIndex } from './LayoutTagIndex'
+'use client'
+
+import CONFIG from './config'
+import CommonHead from '@/components/CommonHead'
+import TopNav from './components/TopNav'
+import AsideLeft from './components/AsideLeft'
+import BLOG from '@/blog.config'
+import { isBrowser } from '@/lib/utils'
+import { useGlobal } from '@/lib/global'
+import BlogListPage from './components/BlogListPage'
+import BlogListScroll from './components/BlogListScroll'
+import BlogArchiveItem from './components/BlogPostArchive'
+import ArticleDetail from './components/ArticleDetail'
+import ArticleLock from './components/ArticleLock'
+import TagItemMini from './components/TagItemMini'
+import { useRouter } from 'next/router'
+import { createContext, useContext, useEffect, useState } from 'react'
+import Link from 'next/link'
+import { Transition } from '@headlessui/react'
+import dynamic from 'next/dynamic'
+import { AdSlot } from '@/components/GoogleAdsense'
+import { Style } from './style'
+import replaceSearchResult from '@/components/Mark'
+
+const Live2D = dynamic(() => import('@/components/Live2D'))
+
+// 主题全局状态
+const ThemeGlobalFukasawa = createContext()
+export const useFukasawaGlobal = () => useContext(ThemeGlobalFukasawa)
+
+/**
+ * 基础布局 采用左右两侧布局,移动端使用顶部导航栏
+ * @param children
+ * @param layout
+ * @param tags
+ * @param meta
+ * @param post
+ * @param currentSearch
+ * @param currentCategory
+ * @param currentTag
+ * @param categories
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const LayoutBase = (props) => {
+ const { children, headerSlot, meta } = props
+ const leftAreaSlot =
+ const { onLoading } = useGlobal()
+
+ // 侧边栏折叠从 本地存储中获取 open 状态的初始值
+ const [isCollapsed, setIsCollapse] = useState(() => {
+ if (typeof window !== 'undefined') {
+ return localStorage.getItem('fukasawa-sidebar-collapse') === 'true' || CONFIG.SIDEBAR_COLLAPSE_SATUS_DEFAULT
+ }
+ return CONFIG.SIDEBAR_COLLAPSE_SATUS_DEFAULT
+ })
+
+ // 在组件卸载时保存 open 状态到本地存储中
+ useEffect(() => {
+ if (isBrowser()) {
+ localStorage.setItem('fukasawa-sidebar-collapse', isCollapsed)
+ }
+ }, [isCollapsed])
+
+ return (
+
+
+
+
+
+
+
+
+
+ {/* 侧边抽屉 */}
+
+
+
+
+
+ {headerSlot}
+ {children}
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+/**
+ * 首页
+ * @param {*} props notion数据
+ * @returns 首页就是一个博客列表
+ */
+const LayoutIndex = (props) => {
+ return
+}
+
+/**
+ * 博客列表
+ * @param {*} props
+ */
+const LayoutPostList = (props) => {
+ return
+ {BLOG.POST_LIST_STYLE === 'page' ? : }
+
+}
+
+/**
+ * 文章详情
+ * @param {*} props
+ * @returns
+ */
+const LayoutSlug = (props) => {
+ const { lock, validPassword } = props
+ return (
+
+ {lock ? : }
+
+ )
+}
+
+/**
+ * 搜索页
+ */
+const LayoutSearch = props => {
+ const { keyword } = props
+ const router = useRouter()
+ useEffect(() => {
+ if (isBrowser()) {
+ replaceSearchResult({
+ doms: document.getElementById('posts-wrapper'),
+ search: keyword,
+ target: {
+ element: 'span',
+ className: 'text-red-500 border-b border-dashed'
+ }
+ })
+ }
+ }, [router])
+ return
+}
+
+/**
+ * 归档页面
+ */
+const LayoutArchive = (props) => {
+ const { archivePosts } = props
+ return
+
+ {Object.keys(archivePosts).map(archiveTitle => (
+
+ ))}
+
+
+}
+
+/**
+ * 404
+ * @param {*} props
+ * @returns
+ */
+const Layout404 = props => {
+ return
404
+}
+
+/**
+ * 分类列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutCategoryIndex = (props) => {
+ const { locale } = useGlobal()
+ const { categoryOptions } = props
+ return (
+
+
+
+ {locale.COMMON.CATEGORY}:
+
+
+ {categoryOptions?.map(category => {
+ return (
+
+
+ {category.name}({category.count})
+
+
+ )
+ })}
+
+
+
+ )
+}
+
+/**
+ * 标签列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutTagIndex = (props) => {
+ const { locale } = useGlobal()
+ const { tagOptions } = props
+ return
+
+
{locale.COMMON.TAGS}:
+
+
+
+}
export {
- CONFIG_FUKA as THEME_CONFIG,
+ CONFIG as THEME_CONFIG,
LayoutIndex,
LayoutSearch,
LayoutArchive,
LayoutSlug,
Layout404,
- LayoutCategory,
+ LayoutPostList,
LayoutCategoryIndex,
- LayoutPage,
- LayoutTag,
LayoutTagIndex
}
diff --git a/themes/fukasawa/style.js b/themes/fukasawa/style.js
new file mode 100644
index 00000000..e730d919
--- /dev/null
+++ b/themes/fukasawa/style.js
@@ -0,0 +1,50 @@
+/* eslint-disable react/no-unknown-property */
+/**
+ * 此处样式只对当前主题生效
+ * 此处不支持tailwindCSS的 @apply 语法
+ * @returns
+ */
+const Style = () => {
+ return
+}
+
+export { Style }
diff --git a/themes/gitbook/components/Announcement.js b/themes/gitbook/components/Announcement.js
new file mode 100755
index 00000000..62df1ec2
--- /dev/null
+++ b/themes/gitbook/components/Announcement.js
@@ -0,0 +1,21 @@
+// import { useGlobal } from '@/lib/global'
+import dynamic from 'next/dynamic'
+
+const NotionPage = dynamic(() => import('@/components/NotionPage'))
+
+const Announcement = ({ notice, className }) => {
+// const { locale } = useGlobal()
+ if (notice?.blockMap) {
+ return
+
+ {/* {locale.COMMON.ANNOUNCEMENT}
*/}
+ {notice && (
+
+
)}
+
+
+ } else {
+ return <>>
+ }
+}
+export default Announcement
diff --git a/themes/gitbook/components/ArticleAround.js b/themes/gitbook/components/ArticleAround.js
new file mode 100644
index 00000000..95b6f83f
--- /dev/null
+++ b/themes/gitbook/components/ArticleAround.js
@@ -0,0 +1,32 @@
+import Link from 'next/link'
+
+/**
+ * 上一篇,下一篇文章
+ * @param {prev,next} param0
+ * @returns
+ */
+export default function ArticleAround ({ prev, next }) {
+ if (!prev || !next) {
+ return <>>
+ }
+ return (
+
+
+
+ {prev.title}
+
+
+
+ {next.title}
+
+
+
+
+ )
+}
diff --git a/themes/gitbook/components/ArticleInfo.js b/themes/gitbook/components/ArticleInfo.js
new file mode 100644
index 00000000..b2d58efe
--- /dev/null
+++ b/themes/gitbook/components/ArticleInfo.js
@@ -0,0 +1,9 @@
+export default function ArticleInfo({ post }) {
+ if (!post) {
+ return null
+ }
+ return
+
+ Last update: { post.date?.start_date}
+
+}
diff --git a/themes/gitbook/components/ArticleLock.js b/themes/gitbook/components/ArticleLock.js
new file mode 100644
index 00000000..4ba5a3b9
--- /dev/null
+++ b/themes/gitbook/components/ArticleLock.js
@@ -0,0 +1,53 @@
+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 dark:text-gray-300 font-light leading-10 text-black bg-gray-100 dark:bg-gray-500'>
+
+
+ {locale.COMMON.SUBMIT}
+
+
+
+
+
+
+}
diff --git a/themes/gitbook/components/BlogPostCard.js b/themes/gitbook/components/BlogPostCard.js
new file mode 100644
index 00000000..15ff21ef
--- /dev/null
+++ b/themes/gitbook/components/BlogPostCard.js
@@ -0,0 +1,20 @@
+import BLOG from '@/blog.config'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import React from 'react'
+
+const BlogPostCard = ({ post, className }) => {
+ const router = useRouter()
+ const currentSelected = router.asPath.split('?')[0] === '/' + post.slug
+ return (
+
+ )
+}
+
+export default BlogPostCard
diff --git a/themes/gitbook/components/BlogPostListPage.js b/themes/gitbook/components/BlogPostListPage.js
new file mode 100644
index 00000000..d9228b51
--- /dev/null
+++ b/themes/gitbook/components/BlogPostListPage.js
@@ -0,0 +1,34 @@
+import BlogPostCard from './BlogPostCard'
+import BLOG from '@/blog.config'
+import NavPostListEmpty from './NavPostListEmpty'
+import PaginationSimple from './PaginationSimple'
+
+/**
+ * 文章列表分页表格
+ * @param page 当前页
+ * @param posts 所有文章
+ * @param tags 所有标签
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const BlogPostListPage = ({ page = 1, posts = [], postCount }) => {
+ const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
+
+ if (!posts || posts.length === 0) {
+ return
+ }
+
+ return (
+
+
+ {/* 文章列表 */}
+ {posts?.map(post => (
+
+ ))}
+
+
+
+ )
+}
+
+export default BlogPostListPage
diff --git a/themes/gitbook/components/BottomMenuBar.js b/themes/gitbook/components/BottomMenuBar.js
new file mode 100644
index 00000000..101fd572
--- /dev/null
+++ b/themes/gitbook/components/BottomMenuBar.js
@@ -0,0 +1,24 @@
+import { useGitBookGlobal } from '@/themes/gitbook'
+import React from 'react'
+import JumpToTopButton from './JumpToTopButton'
+
+export default function BottomMenuBar({ post, className }) {
+ const { pageNavVisible, changePageNavVisible } = useGitBookGlobal()
+
+ const togglePageNavVisible = () => {
+ changePageNavVisible(!pageNavVisible)
+ }
+
+ return (
+
+ )
+}
diff --git a/themes/gitbook/components/Card.js b/themes/gitbook/components/Card.js
new file mode 100644
index 00000000..d24c046e
--- /dev/null
+++ b/themes/gitbook/components/Card.js
@@ -0,0 +1,9 @@
+const Card = ({ children, headerSlot, className }) => {
+ return
+ <>{headerSlot}>
+
+
+}
+export default Card
diff --git a/themes/gitbook/components/Catalog.js b/themes/gitbook/components/Catalog.js
new file mode 100644
index 00000000..5a51bee6
--- /dev/null
+++ b/themes/gitbook/components/Catalog.js
@@ -0,0 +1,90 @@
+import { useCallback, useEffect, useState } from 'react'
+import throttle from 'lodash.throttle'
+import { uuidToId } from 'notion-utils'
+import { isBrowser } from '@/lib/utils'
+
+/**
+ * 目录导航组件
+ * @param toc
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const Catalog = ({ post }) => {
+ const tocIds = []
+ const toc = post?.toc
+ // 同步选中目录事件
+ const [activeSection, setActiveSection] = useState(null)
+
+ // 监听滚动事件
+ useEffect(() => {
+ window.addEventListener('scroll', actionSectionScrollSpy)
+ actionSectionScrollSpy()
+ return () => {
+ window.removeEventListener('scroll', actionSectionScrollSpy)
+ }
+ }, [post])
+
+ const throttleMs = 200
+ const actionSectionScrollSpy = 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
+ if (isBrowser() && tocIds?.length > 0) {
+ for (const tocWrapper of document?.getElementsByClassName('toc-wrapper')) {
+ tocWrapper?.scrollTo({ top: 28 * index, behavior: 'smooth' })
+ }
+ }
+ }, throttleMs))
+
+ // 无目录就直接返回空
+ if (!toc || toc.length < 1) {
+ return null
+ }
+
+ return <>
+
+
+
+ >
+}
+
+export default Catalog
diff --git a/themes/gitbook/components/CategoryGroup.js b/themes/gitbook/components/CategoryGroup.js
new file mode 100644
index 00000000..1516c038
--- /dev/null
+++ b/themes/gitbook/components/CategoryGroup.js
@@ -0,0 +1,19 @@
+import React from 'react'
+import CategoryItem from './CategoryItem'
+
+const CategoryGroup = ({ currentCategory, categoryOptions }) => {
+ if (!categoryOptions) {
+ return <>>
+ }
+ return
+
分类
+
+ {categoryOptions?.map(category => {
+ const selected = currentCategory === category.name
+ return
+ })}
+
+
+}
+
+export default CategoryGroup
diff --git a/themes/gitbook/components/CategoryItem.js b/themes/gitbook/components/CategoryItem.js
new file mode 100644
index 00000000..779488de
--- /dev/null
+++ b/themes/gitbook/components/CategoryItem.js
@@ -0,0 +1,18 @@
+import Link from 'next/link'
+
+export default function CategoryItem ({ selected, category, categoryCount }) {
+ return (
+
+
+
{category} {categoryCount && `(${categoryCount})`}
+
+
+
+ );
+}
diff --git a/themes/gitbook/components/FloatTocButton.js b/themes/gitbook/components/FloatTocButton.js
new file mode 100644
index 00000000..4215f447
--- /dev/null
+++ b/themes/gitbook/components/FloatTocButton.js
@@ -0,0 +1,25 @@
+import { useGitBookGlobal } from '@/themes/gitbook'
+
+/**
+ * 移动端悬浮目录按钮
+ */
+export default function FloatTocButton () {
+ const { tocVisible, changeTocVisible } = useGitBookGlobal()
+
+ const toggleToc = () => {
+ changeTocVisible(!tocVisible)
+ }
+
+ return (
+
+ )
+}
diff --git a/themes/gitbook/components/Footer.js b/themes/gitbook/components/Footer.js
new file mode 100644
index 00000000..fb1d339c
--- /dev/null
+++ b/themes/gitbook/components/Footer.js
@@ -0,0 +1,39 @@
+import React from 'react'
+import BLOG from '@/blog.config'
+
+const Footer = ({ siteInfo }) => {
+ 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/gitbook/components/InfoCard.js b/themes/gitbook/components/InfoCard.js
new file mode 100644
index 00000000..13fe31c8
--- /dev/null
+++ b/themes/gitbook/components/InfoCard.js
@@ -0,0 +1,21 @@
+import BLOG from '@/blog.config'
+import LazyImage from '@/components/LazyImage'
+import Router from 'next/router'
+import React from 'react'
+import SocialButton from './SocialButton'
+
+const InfoCard = (props) => {
+ const { siteInfo } = props
+ return
+
+
{ Router.push('/about') }}>
+
+
+
{BLOG.AUTHOR}
+
{BLOG.BIO}
+
+
+
+}
+
+export default InfoCard
diff --git a/themes/gitbook/components/JumpToTopButton.js b/themes/gitbook/components/JumpToTopButton.js
new file mode 100644
index 00000000..0afdbb6c
--- /dev/null
+++ b/themes/gitbook/components/JumpToTopButton.js
@@ -0,0 +1,24 @@
+
+/**
+ * 跳转到网页顶部
+ * 当屏幕下滑500像素后会出现该控件
+ * @param targetRef 关联高度的目标html标签
+ * @param showPercent 是否显示百分比
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const JumpToTopButton = ({ showPercent = false, percent, className }) => {
+ return (
+
+ { window.scrollTo({ top: 0, behavior: 'smooth' }) }} />
+
+ )
+}
+
+export default JumpToTopButton
diff --git a/themes/gitbook/components/LeftMenuBar.js b/themes/gitbook/components/LeftMenuBar.js
new file mode 100644
index 00000000..e6636e19
--- /dev/null
+++ b/themes/gitbook/components/LeftMenuBar.js
@@ -0,0 +1,16 @@
+import Link from 'next/link'
+import React from 'react'
+
+export default function LeftMenuBar () {
+ return (
+
+ );
+}
diff --git a/themes/gitbook/components/LoadingCover.js b/themes/gitbook/components/LoadingCover.js
new file mode 100644
index 00000000..f74757ef
--- /dev/null
+++ b/themes/gitbook/components/LoadingCover.js
@@ -0,0 +1,7 @@
+export default function LoadingCover() {
+ return
+}
diff --git a/themes/gitbook/components/LogoBar.js b/themes/gitbook/components/LogoBar.js
new file mode 100644
index 00000000..70c96b46
--- /dev/null
+++ b/themes/gitbook/components/LogoBar.js
@@ -0,0 +1,29 @@
+import BLOG from '@/blog.config'
+import LazyImage from '@/components/LazyImage'
+import { useGitBookGlobal } from '@/themes/gitbook'
+import Link from 'next/link'
+
+/**
+ * Logo区域
+ * @param {*} props
+ * @returns
+ */
+export default function LogoBar(props) {
+ const { siteInfo } = props
+ const { pageNavVisible, changePageNavVisible } = useGitBookGlobal()
+
+ const togglePageNavVisible = () => {
+ changePageNavVisible(!pageNavVisible)
+ }
+ return (
+
+
+
+
+
+
+ {siteInfo?.title}
+
+
+ )
+}
diff --git a/themes/gitbook/components/MenuBarMobile.js b/themes/gitbook/components/MenuBarMobile.js
new file mode 100644
index 00000000..d21fbfe5
--- /dev/null
+++ b/themes/gitbook/components/MenuBarMobile.js
@@ -0,0 +1,39 @@
+import React from 'react'
+import { useGlobal } from '@/lib/global'
+import CONFIG from '../config'
+import BLOG from '@/blog.config'
+import { MenuItemCollapse } from './MenuItemCollapse'
+
+export const MenuBarMobile = (props) => {
+ const { customMenu, customNav } = props
+ const { locale } = useGlobal()
+
+ let links = [
+ // { name: locale.NAV.INDEX, to: '/' || '/', show: true },
+ { name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY },
+ { name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG },
+ { name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE }
+ // { name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH }
+ ]
+
+ 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/gitbook/components/MenuItemCollapse.js b/themes/gitbook/components/MenuItemCollapse.js
new file mode 100644
index 00000000..5cdb6973
--- /dev/null
+++ b/themes/gitbook/components/MenuItemCollapse.js
@@ -0,0 +1,62 @@
+import Collapse from '@/components/Collapse'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useState } from 'react'
+
+/**
+ * 折叠菜单
+ * @param {*} param0
+ * @returns
+ */
+export const MenuItemCollapse = (props) => {
+ const { link } = props
+ const [show, changeShow] = useState(false)
+ const hasSubMenu = link?.subMenus?.length > 0
+
+ const [isOpen, changeIsOpen] = useState(false)
+
+ const router = useRouter()
+
+ if (!link || !link.show) {
+ return null
+ }
+
+ const selected = (router.pathname === link.to) || (router.asPath === link.to)
+
+ const toggleShow = () => {
+ changeShow(!show)
+ }
+
+ const toggleOpenSubMenu = () => {
+ changeIsOpen(!isOpen)
+ }
+
+ return <>
+
+
+ {!hasSubMenu &&
+
+ }
+
+ {hasSubMenu &&
}
+
+
+ {/* 折叠子菜单 */}
+ {hasSubMenu &&
+ {link?.subMenus?.map(sLink => {
+ return
+ })}
+ }
+ >
+}
diff --git a/themes/gitbook/components/MenuItemDrop.js b/themes/gitbook/components/MenuItemDrop.js
new file mode 100644
index 00000000..78a5a0dd
--- /dev/null
+++ b/themes/gitbook/components/MenuItemDrop.js
@@ -0,0 +1,50 @@
+import Link from 'next/link'
+import { useState } from 'react'
+import { useRouter } from 'next/router'
+
+export const MenuItemDrop = ({ link }) => {
+ const [show, changeShow] = useState(false)
+ // const show = true
+ // const changeShow = () => {}
+ const router = useRouter()
+
+ if (!link || !link.show) {
+ return null
+ }
+ const hasSubMenu = link?.subMenus?.length > 0
+ const selected = (router.pathname === link.to) || (router.asPath === link.to)
+
+ 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/gitbook/components/MenuItemMobileNormal.js b/themes/gitbook/components/MenuItemMobileNormal.js
new file mode 100644
index 00000000..17f0d151
--- /dev/null
+++ b/themes/gitbook/components/MenuItemMobileNormal.js
@@ -0,0 +1,27 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+export const NormalMenu = props => {
+ const { link } = props
+ const router = useRouter()
+
+ if (!link || !link.show) {
+ return null
+ }
+
+ const selected = (router.pathname === link.to) || (router.asPath === link.to)
+
+ return
+
+
+ {link.slot}
+
+
+}
diff --git a/themes/gitbook/components/MenuItemPCNormal.js b/themes/gitbook/components/MenuItemPCNormal.js
new file mode 100644
index 00000000..809ae974
--- /dev/null
+++ b/themes/gitbook/components/MenuItemPCNormal.js
@@ -0,0 +1,24 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+export const MenuItemPCNormal = props => {
+ const { link } = props
+ const router = useRouter()
+ const selected = (router.pathname === link.to) || (router.asPath === link.to)
+ if (!link || !link.show) {
+ return null
+ }
+
+ return
+
+ {link.slot}
+
+}
diff --git a/themes/gitbook/components/NavPostItem.js b/themes/gitbook/components/NavPostItem.js
new file mode 100644
index 00000000..66ffa6dd
--- /dev/null
+++ b/themes/gitbook/components/NavPostItem.js
@@ -0,0 +1,43 @@
+import BlogPostCard from './BlogPostCard'
+import React, { useState } from 'react'
+import Collapse from '@/components/Collapse'
+
+/**
+ * 导航列表
+ * @param posts 所有文章
+ * @param tags 所有标签
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const NavPostItem = (props) => {
+ const { group } = props
+ const [isOpen, changeIsOpen] = useState(group?.selected)
+
+ const toggleOpenSubMenu = () => {
+ changeIsOpen(!isOpen)
+ }
+
+ if (group?.category) {
+ return <>
+
+
+ {group?.items?.map(post => (
+
))
+ }
+
+ >
+ } else {
+ return <>
+ {group?.items?.map(post => (
+
))
+ }
+ >
+ }
+}
+
+export default NavPostItem
diff --git a/themes/gitbook/components/NavPostList.js b/themes/gitbook/components/NavPostList.js
new file mode 100644
index 00000000..1dff3722
--- /dev/null
+++ b/themes/gitbook/components/NavPostList.js
@@ -0,0 +1,45 @@
+import NavPostListEmpty from './NavPostListEmpty'
+import { useRouter } from 'next/router'
+import NavPostItem from './NavPostItem'
+
+/**
+ * 博客列表滚动分页
+ * @param posts 所有文章
+ * @param tags 所有标签
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const NavPostList = (props) => {
+ const { filteredPostGroups } = props
+ const router = useRouter()
+ let selectedSth = false
+
+ // 处理是否选中
+ filteredPostGroups?.map((group) => {
+ let groupSelected = false
+ for (const post of group?.items) {
+ if (router.asPath.split('?')[0] === '/' + post.slug) {
+ groupSelected = true
+ selectedSth = true
+ }
+ }
+ group.selected = groupSelected
+ return null
+ })
+
+ // 如果都没有选中默认打开第一个
+ if (!selectedSth && filteredPostGroups && filteredPostGroups?.length > 0) {
+ filteredPostGroups[0].selected = true
+ }
+
+ if (!filteredPostGroups || filteredPostGroups.length === 0) {
+ return
+ } else {
+ return
+ {/* 文章列表 */}
+ {filteredPostGroups?.map((group, index) => )}
+
+ }
+}
+
+export default NavPostList
diff --git a/themes/gitbook/components/NavPostListEmpty.js b/themes/gitbook/components/NavPostListEmpty.js
new file mode 100644
index 00000000..207599db
--- /dev/null
+++ b/themes/gitbook/components/NavPostListEmpty.js
@@ -0,0 +1,12 @@
+
+/**
+ * 空白博客 列表
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const NavPostListEmpty = ({ currentSearch }) => {
+ return
+
没有找到文章 {(currentSearch &&
{currentSearch}
)}
+
+}
+export default NavPostListEmpty
diff --git a/themes/gitbook/components/PageNavDrawer.js b/themes/gitbook/components/PageNavDrawer.js
new file mode 100644
index 00000000..17448c94
--- /dev/null
+++ b/themes/gitbook/components/PageNavDrawer.js
@@ -0,0 +1,35 @@
+import { useGitBookGlobal } from '@/themes/gitbook'
+import NavPostList from './NavPostList'
+
+/**
+ * 悬浮抽屉 页面内导航
+ * @param toc
+ * @param post
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const PageNavDrawer = (props) => {
+ const { pageNavVisible, changePageNavVisible } = useGitBookGlobal()
+ const { filteredPostGroups } = props
+ const switchVisible = () => {
+ changePageNavVisible(!pageNavVisible)
+ }
+
+ return <>
+
+ {/* 背景蒙版 */}
+
+ >
+}
+export default PageNavDrawer
diff --git a/themes/gitbook/components/PaginationSimple.js b/themes/gitbook/components/PaginationSimple.js
new file mode 100644
index 00000000..b48259ea
--- /dev/null
+++ b/themes/gitbook/components/PaginationSimple.js
@@ -0,0 +1,54 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useGlobal } from '@/lib/global'
+
+/**
+ * 简易翻页插件
+ * @param page 当前页码
+ * @param totalPage 是否有下一页
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const PaginationSimple = ({ page, totalPage }) => {
+ const { locale } = useGlobal()
+ const router = useRouter()
+ const currentPage = +page
+ const showNext = currentPage < totalPage
+ const pagePrefix = router.asPath.replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '')
+
+ return (
+
+
+ ←{locale.PAGINATION.PREV}
+
+
+
+
+ {locale.PAGINATION.NEXT}→
+
+
+ )
+}
+
+export default PaginationSimple
diff --git a/themes/gitbook/components/Progress.js b/themes/gitbook/components/Progress.js
new file mode 100644
index 00000000..bca7a17d
--- /dev/null
+++ b/themes/gitbook/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('posts-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/gitbook/components/RevolverMaps.js b/themes/gitbook/components/RevolverMaps.js
new file mode 100644
index 00000000..c6eb6252
--- /dev/null
+++ b/themes/gitbook/components/RevolverMaps.js
@@ -0,0 +1,36 @@
+import { useEffect, useState } from 'react'
+
+export default function RevolverMaps () {
+ const [load, changeLoad] = useState(false)
+ useEffect(() => {
+ if (!load) {
+ initRevolverMaps()
+ changeLoad(true)
+ }
+ }, [])
+ return
+}
+
+function initRevolverMaps () {
+ if (screen.width >= 768) {
+ Promise.all([
+ loadExternalResource('https://rf.revolvermaps.com/0/0/8.js?i=5jnp1havmh9&m=0&c=ff0000&cr1=ffffff&f=arial&l=33')
+ ]).then(() => {
+ console.log('地图加载完成')
+ })
+ }
+}
+
+// 封装异步加载资源的方法
+function loadExternalResource (url) {
+ return new Promise((resolve, reject) => {
+ const container = document.getElementById('revolvermaps')
+ const tag = document.createElement('script')
+ tag.src = url
+ if (tag) {
+ tag.onload = () => resolve(url)
+ tag.onerror = () => reject(url)
+ container.appendChild(tag)
+ }
+ })
+}
diff --git a/themes/gitbook/components/SearchInput.js b/themes/gitbook/components/SearchInput.js
new file mode 100644
index 00000000..43062eb2
--- /dev/null
+++ b/themes/gitbook/components/SearchInput.js
@@ -0,0 +1,104 @@
+import { useImperativeHandle, useRef, useState } from 'react'
+import { deepClone } from '@/lib/utils'
+import { useGitBookGlobal } from '@/themes/gitbook'
+let lock = false
+
+const SearchInput = ({ currentSearch, cRef, className }) => {
+ const searchInputRef = useRef()
+ const { setFilterPosts, allNavPages } = useGitBookGlobal()
+
+ useImperativeHandle(cRef, () => {
+ return {
+ focus: () => {
+ searchInputRef?.current?.focus()
+ }
+ }
+ })
+
+ const handleSearch = () => {
+ let keyword = searchInputRef.current.value
+ const filterPosts = []
+ if (keyword) {
+ keyword = keyword.trim()
+ } else {
+ setFilterPosts(allNavPages)
+ }
+ const filterAllNavPages = deepClone(allNavPages)
+ for (const filterGroup of filterAllNavPages) {
+ for (let i = filterGroup.items.length - 1; i >= 0; i--) {
+ const post = filterGroup.items[i]
+ const articleInfo = post.title + ''
+ const hit = articleInfo.toLowerCase().indexOf(keyword.toLowerCase()) > -1
+ if (!hit) {
+ // 删除
+ filterGroup.items.splice(i, 1)
+ }
+ }
+ if (filterGroup.items && filterGroup.items.length > 0) {
+ filterPosts.push(filterGroup)
+ }
+ }
+
+ // 更新完
+ setFilterPosts(filterPosts)
+ }
+ const handleKeyUp = (e) => {
+ if (e.keyCode === 13) { // 回车
+ handleSearch(searchInputRef.current.value)
+ } else if (e.keyCode === 27) { // ESC
+ cleanSearch()
+ }
+ }
+ const cleanSearch = () => {
+ searchInputRef.current.value = ''
+ handleSearch()
+ }
+
+ 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/gitbook/components/SocialButton.js b/themes/gitbook/components/SocialButton.js
new file mode 100644
index 00000000..164a71fc
--- /dev/null
+++ b/themes/gitbook/components/SocialButton.js
@@ -0,0 +1,37 @@
+import BLOG from '@/blog.config'
+import React from 'react'
+
+/**
+ * 社交联系方式按钮组
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const SocialButton = () => {
+ return
+ {BLOG.CONTACT_GITHUB &&
+
+ }
+ {BLOG.CONTACT_TWITTER &&
+
+ }
+ {BLOG.CONTACT_TELEGRAM &&
+
+ }
+ {BLOG.CONTACT_LINKEDIN &&
+
+ }
+ {BLOG.CONTACT_WEIBO &&
+
+ }
+ {BLOG.CONTACT_INSTAGRAM &&
+
+ }
+ {BLOG.CONTACT_EMAIL &&
+
+ }
+ {JSON.parse(BLOG.ENABLE_RSS) &&
+
+ }
+
+}
+export default SocialButton
diff --git a/themes/gitbook/components/TagGroups.js b/themes/gitbook/components/TagGroups.js
new file mode 100644
index 00000000..390a6306
--- /dev/null
+++ b/themes/gitbook/components/TagGroups.js
@@ -0,0 +1,27 @@
+import TagItemMini from './TagItemMini'
+
+/**
+ * 标签组
+ * @param tags
+ * @param currentTag
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const TagGroups = ({ tagOptions, currentTag }) => {
+ if (!tagOptions) return <>>
+ return (
+
+ )
+}
+
+export default TagGroups
diff --git a/themes/gitbook/components/TagItemMini.js b/themes/gitbook/components/TagItemMini.js
new file mode 100644
index 00000000..9922a069
--- /dev/null
+++ b/themes/gitbook/components/TagItemMini.js
@@ -0,0 +1,21 @@
+import Link from 'next/link'
+
+const TagItemMini = ({ tag, selected = false }) => {
+ return (
+
+
+
{selected && } {tag.name + (tag.count ? `(${tag.count})` : '')}
+
+
+ )
+}
+
+export default TagItemMini
diff --git a/themes/gitbook/components/TocDrawer.js b/themes/gitbook/components/TocDrawer.js
new file mode 100644
index 00000000..7b07044a
--- /dev/null
+++ b/themes/gitbook/components/TocDrawer.js
@@ -0,0 +1,34 @@
+import { useGitBookGlobal } from '@/themes/gitbook'
+import Catalog from './Catalog'
+
+/**
+ * 悬浮抽屉目录
+ * @param toc
+ * @param post
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const TocDrawer = ({ post, cRef }) => {
+ const { tocVisible, changeTocVisible } = useGitBookGlobal()
+ const switchVisible = () => {
+ changeTocVisible(!tocVisible)
+ }
+ return <>
+
+ {/* 侧边菜单 */}
+
+ {post && <>
+
+
+
+ >}
+
+
+ {/* 背景蒙版 */}
+
+ >
+}
+export default TocDrawer
diff --git a/themes/gitbook/components/TopNavBar.js b/themes/gitbook/components/TopNavBar.js
new file mode 100644
index 00000000..09e14982
--- /dev/null
+++ b/themes/gitbook/components/TopNavBar.js
@@ -0,0 +1,73 @@
+import LogoBar from './LogoBar'
+import { useRef, useState } from 'react'
+import Collapse from '@/components/Collapse'
+import { MenuBarMobile } from './MenuBarMobile'
+import { useGlobal } from '@/lib/global'
+import CONFIG from '../config'
+import BLOG from '@/blog.config'
+import { MenuItemDrop } from './MenuItemDrop'
+import DarkModeButton from '@/components/DarkModeButton'
+
+/**
+ * 顶部导航栏 + 菜单
+ * @param {} param0
+ * @returns
+ */
+export default function TopNavBar(props) {
+ const { className, customNav, customMenu } = props
+ const [isOpen, changeShow] = useState(false)
+ const collapseRef = useRef(null)
+
+ const { locale } = useGlobal()
+
+ const defaultLinks = [
+ { icon: 'fas fa-th', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY },
+ { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG },
+ { 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 }
+ ]
+
+ let links = defaultLinks.concat(customNav)
+
+ const toggleMenuOpen = () => {
+ changeShow(!isOpen)
+ }
+
+ // 如果 开启自定义菜单,则覆盖Page生成的菜单
+ if (BLOG.CUSTOM_MENU) {
+ links = customMenu
+ }
+
+ return (
+
+
+ {/* 移动端折叠菜单 */}
+
+
+ collapseRef.current?.updateCollapseHeight(param)} />
+
+
+
+ {/* 导航栏菜单 */}
+
+
+ {/* 左侧图标Logo */}
+
+
+ {/* 折叠按钮、仅移动端显示 */}
+
+
+ {/* 桌面端顶部菜单 */}
+
+ {links && links?.map(link => )}
+
+
+
+
+ )
+}
diff --git a/themes/gitbook/config.js b/themes/gitbook/config.js
new file mode 100644
index 00000000..ce747181
--- /dev/null
+++ b/themes/gitbook/config.js
@@ -0,0 +1,15 @@
+const CONFIG = {
+
+ INDEX_PAGE: 'about', // 文档首页显示的文章,请确此路径包含在您的notion数据库中
+
+ // 菜单
+ MENU_CATEGORY: true, // 显示分类
+ MENU_TAG: true, // 显示标签
+ MENU_ARCHIVE: true, // 显示归档
+ MENU_SEARCH: true, // 显示搜索
+
+ // Widget
+ WIDGET_REVOLVER_MAPS: process.env.NEXT_PUBLIC_WIDGET_REVOLVER_MAPS || 'false', // 地图插件
+ WIDGET_TO_TOP: true // 跳回顶部
+}
+export default CONFIG
diff --git a/themes/gitbook/index.js b/themes/gitbook/index.js
new file mode 100644
index 00000000..2b034195
--- /dev/null
+++ b/themes/gitbook/index.js
@@ -0,0 +1,295 @@
+'use client'
+
+import CONFIG from './config'
+import { useRouter } from 'next/router'
+import { useEffect, useState, createContext, useContext } from 'react'
+import { isBrowser } from '@/lib/utils'
+import CommonHead from '@/components/CommonHead'
+import Footer from './components/Footer'
+import InfoCard from './components/InfoCard'
+import RevolverMaps from './components/RevolverMaps'
+import TopNavBar from './components/TopNavBar'
+import SearchInput from './components/SearchInput'
+import { useGlobal } from '@/lib/global'
+import Live2D from '@/components/Live2D'
+import BLOG from '@/blog.config'
+import NavPostList from './components/NavPostList'
+import ArticleInfo from './components/ArticleInfo'
+import Catalog from './components/Catalog'
+import Announcement from './components/Announcement'
+import PageNavDrawer from './components/PageNavDrawer'
+import FloatTocButton from './components/FloatTocButton'
+import { AdSlot } from '@/components/GoogleAdsense'
+import JumpToTopButton from './components/JumpToTopButton'
+import ShareBar from '@/components/ShareBar'
+import CategoryItem from './components/CategoryItem'
+import TagItemMini from './components/TagItemMini'
+import ArticleAround from './components/ArticleAround'
+import Comment from '@/components/Comment'
+import TocDrawer from './components/TocDrawer'
+import NotionPage from '@/components/NotionPage'
+import { ArticleLock } from './components/ArticleLock'
+import { Transition } from '@headlessui/react'
+import { Style } from './style'
+
+// 主题全局变量
+const ThemeGlobalGitbook = createContext()
+export const useGitBookGlobal = () => useContext(ThemeGlobalGitbook)
+
+/**
+ * 基础布局
+ * 采用左右两侧布局,移动端使用顶部导航栏
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const LayoutBase = (props) => {
+ const { children, meta, post, allNavPages, slotLeft, slotRight, slotTop } = props
+ const { onLoading } = useGlobal()
+ const router = useRouter()
+ const [tocVisible, changeTocVisible] = useState(false)
+ const [pageNavVisible, changePageNavVisible] = useState(false)
+ const [filteredPostGroups, setFilteredPostGroups] = useState(allNavPages)
+
+ const showTocButton = post?.toc?.length > 1
+
+ useEffect(() => {
+ setFilteredPostGroups(allNavPages)
+ }, [post])
+
+ return (
+
+
+
+
+
+ {/* 顶部导航栏 */}
+
+
+
+
+ {/* 左侧推拉抽屉 */}
+
+
+ {slotLeft}
+
+ {/* 所有文章列表 */}
+
+
+
+
+
+
+
+
+
+
+
+
+ {slotTop}
+
+
+
+ {children}
+
+
+
+ {/* 回顶按钮 */}
+
+
+
+ {/* 底部 */}
+
+
+
+
+
+
+ {/* 右侧侧推拉抽屉 */}
+
+
+
+
+
+
+ {slotRight}
+ {router.route === '/' && <>
+
+ {CONFIG.WIDGET_REVOLVER_MAPS === 'true' &&
}
+
+ >}
+ {/* gitbook主题首页只显示公告 */}
+
+
+
+
+
+
+
+
+
+
+ {/* 移动端悬浮目录按钮 */}
+ {showTocButton && !tocVisible &&
+
+
}
+
+ {/* 移动端导航抽屉 */}
+
+
+ {/* 移动端底部导航栏 */}
+ {/*
*/}
+
+
+
+ )
+}
+
+/**
+ * 首页
+ * 重定向到某个文章详情页
+ * @param {*} props
+ * @returns
+ */
+const LayoutIndex = (props) => {
+ const router = useRouter()
+ useEffect(() => {
+ router.push(CONFIG.INDEX_PAGE).then(() => {
+ // console.log('跳转到指定首页', CONFIG.INDEX_PAGE)
+ setTimeout(() => {
+ if (isBrowser()) {
+ const article = document.getElementById('notion-article')
+ if (!article) {
+ console.log('请检查您的Notion数据库中是否包含此slug页面: ', CONFIG.INDEX_PAGE)
+ const containerInner = document.querySelector('#theme-gitbook #container-inner')
+ const newHTML = `
配置有误
请在您的notion中添加一个slug为${CONFIG.INDEX_PAGE}的文章
`
+ containerInner?.insertAdjacentHTML('afterbegin', newHTML)
+ }
+ }
+ }, 7 * 1000)
+ })
+ }, [])
+
+ return
+}
+
+/**
+ * 文章列表 无
+ * 全靠页面导航
+ * @param {*} props
+ * @returns
+ */
+const LayoutPostList = (props) => {
+ return
+}
+
+/**
+ * 文章详情
+ * @param {*} props
+ * @returns
+ */
+const LayoutSlug = (props) => {
+ const { post, prev, next, lock, validPassword } = props
+
+ return (
+
+ {/* 文章锁 */}
+ {lock && }
+
+ {!lock &&
+
+ {/* title */}
+
{post?.title}
+
+ {/* Notion文章主体 */}
+ {post && (
+
+
+ {/* 分享 */}
+
+ {/* 文章分类和标签信息 */}
+
+ {CONFIG.POST_DETAIL_CATEGORY && post?.category &&
}
+
+ {CONFIG.POST_DETAIL_TAG && post?.tagItems?.map(tag => )}
+
+
+
+ {post?.type === 'Post' && }
+
+
+
+
+ )}
+
+
+
}
+
+ )
+}
+
+/**
+ * 没有搜索
+ * 全靠页面导航
+ * @param {*} props
+ * @returns
+ */
+const LayoutSearch = (props) => {
+ return
+}
+
+/**
+ * 没有归档
+ * 全靠页面导航
+ * @param {*} props
+ * @returns
+ */
+const LayoutArchive = (props) => {
+ return
+}
+
+/**
+ * 404
+ */
+const Layout404 = props => {
+ return
+ 404 Not found.
+
+}
+
+/**
+ * 分类列表
+ */
+const LayoutCategoryIndex = (props) => {
+ return
+}
+
+/**
+ * 标签列表
+ */
+const LayoutTagIndex = (props) => {
+ return
+}
+
+export {
+ CONFIG as THEME_CONFIG,
+ LayoutIndex,
+ LayoutSearch,
+ LayoutArchive,
+ LayoutSlug,
+ Layout404,
+ LayoutCategoryIndex,
+ LayoutPostList,
+ LayoutTagIndex
+}
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..030a5dd7
--- /dev/null
+++ b/themes/heo/components/ArticleAdjacent.js
@@ -0,0 +1,86 @@
+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 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 || !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..78500ddf
--- /dev/null
+++ b/themes/heo/components/ArticleCopyright.js
@@ -0,0 +1,43 @@
+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 () {
+ const router = useRouter()
+ const [path, setPath] = useState(BLOG.LINK + router.asPath)
+ useEffect(() => {
+ setPath(window.location.href)
+ })
+
+ const { locale } = useGlobal()
+
+ if (!CONFIG.ARTICLE_COPYRIGHT) {
+ return <>>
+ }
+
+ 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..f7f6c738
--- /dev/null
+++ b/themes/heo/components/ArticleRecommend.js
@@ -0,0 +1,65 @@
+import Link from 'next/link'
+import CONFIG from '../config'
+import BLOG from '@/blog.config'
+import { useGlobal } from '@/lib/global'
+import LazyImage from '@/components/LazyImage'
+
+/**
+ * 关联推荐文章
+ * @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
+ ? post.pageCoverThumbnail
+ : siteInfo?.pageCover
+
+ return (
+ (
+
+
+
+ )
+ )
+ })}
+
+
+ )
+}
diff --git a/themes/heo/components/BlogPostArchive.js b/themes/heo/components/BlogPostArchive.js
new file mode 100644
index 00000000..4f471444
--- /dev/null
+++ b/themes/heo/components/BlogPostArchive.js
@@ -0,0 +1,87 @@
+import React from 'react'
+import Link from 'next/link'
+import BLOG from '@/blog.config'
+import CONFIG from '../config'
+import TagItemMini from './TagItemMini'
+import LazyImage from '@/components/LazyImage'
+/**
+ * 博客归档列表
+ * @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..d44c5fe4
--- /dev/null
+++ b/themes/heo/components/BlogPostCard.js
@@ -0,0 +1,85 @@
+import Link from 'next/link'
+import CONFIG from '../config'
+import BLOG from '@/blog.config'
+import TagItemMini from './TagItemMini'
+import LazyImage from '@/components/LazyImage'
+
+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, index) => (
+ {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..52683984
--- /dev/null
+++ b/themes/heo/components/BlogPostListPage.js
@@ -0,0 +1,35 @@
+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..20202a9f
--- /dev/null
+++ b/themes/heo/components/BlogPostListScroll.js
@@ -0,0 +1,76 @@
+import BLOG from '@/blog.config'
+import BlogPostCard from './BlogPostCard'
+import BlogPostListEmpty from './BlogPostListEmpty'
+import { useGlobal } from '@/lib/global'
+import React, { useEffect, useRef, useState } 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] = 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()
+ }
+ })
+ }
+
+ // 监听滚动
+ useEffect(() => {
+ window.addEventListener('scroll', scrollTrigger)
+ return () => {
+ window.removeEventListener('scroll', scrollTrigger)
+ }
+ })
+
+ const targetRef = 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..81ea8d1a
--- /dev/null
+++ b/themes/heo/components/Catalog.js
@@ -0,0 +1,90 @@
+import { useCallback, useEffect, useRef, useState } from 'react'
+import throttle from 'lodash.throttle'
+import { uuidToId } from 'notion-utils'
+import { useGlobal } from '@/lib/global'
+
+/**
+ * 目录导航组件
+ * @param toc
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const Catalog = ({ toc }) => {
+ const { locale } = useGlobal()
+ // 监听滚动事件
+ useEffect(() => {
+ window.addEventListener('scroll', actionSectionScrollSpy)
+ actionSectionScrollSpy()
+ return () => {
+ window.removeEventListener('scroll', actionSectionScrollSpy)
+ }
+ }, [])
+
+ // 目录自动滚动
+ const tRef = useRef(null)
+ const tocIds = []
+
+ // 同步选中目录事件
+ const [activeSection, setActiveSection] = useState(null)
+
+ const actionSectionScrollSpy = 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' })
+ }, 200))
+
+ // 无目录就直接返回空
+ 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..ee0a7a0c
--- /dev/null
+++ b/themes/heo/components/CategoryBar.js
@@ -0,0 +1,61 @@
+import { ChevronDoubleLeft, ChevronDoubleRight } from '@/components/HeroIcons'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useRef, useState } from 'react'
+
+/**
+ * 博客列表上方嵌入条
+ * @param {*} props
+ * @returns
+ */
+export default function CategoryBar(props) {
+ const { categoryOptions, border = true } = props
+ const { locale } = useGlobal()
+ const [scrollRight, setScrollRight] = useState(false)
+ // 创建一个ref引用
+ const categoryBarItemsRef = useRef(null)
+
+ // 点击#right时,滚动#category-bar-items到最右边
+ const handleToggleScroll = () => {
+ if (categoryBarItemsRef.current) {
+ const { scrollWidth, clientWidth } = categoryBarItemsRef.current
+ if (scrollRight) {
+ categoryBarItemsRef.current.scrollLeft = 0
+ } else {
+ categoryBarItemsRef.current.scrollLeft = scrollWidth - clientWidth
+ }
+ setScrollRight(!scrollRight)
+ }
+ }
+ return
+
+
+
+ {categoryOptions?.map((c, index) => )}
+
+
+
+
+ {scrollRight ? : }
+
+
+ {locale.MENU.CATEGORY}
+
+
+
+}
+
+/**
+ * 按钮
+ * @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/DarkModeButton.js b/themes/heo/components/DarkModeButton.js
new file mode 100644
index 00000000..9b56e6da
--- /dev/null
+++ b/themes/heo/components/DarkModeButton.js
@@ -0,0 +1,38 @@
+import { useGlobal } from '@/lib/global'
+import { saveDarkModeToCookies } from '@/themes/theme'
+import { Moon, Sun } from '@/components/HeroIcons'
+import { useImperativeHandle } from 'react'
+
+/**
+ * 深色模式按钮
+ */
+const DarkModeButton = (props) => {
+ const { cRef, className } = props
+ const { isDarkMode, updateDarkMode } = useGlobal()
+
+ /**
+ * 对外暴露方法
+ */
+ useImperativeHandle(cRef, () => {
+ return {
+ handleChangeDarkMode: () => {
+ handleChangeDarkMode()
+ }
+ }
+ })
+
+ // 用户手动设置主题
+ 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
+}
+export default DarkModeButton
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/FloatTocButton.js b/themes/heo/components/FloatTocButton.js
new file mode 100644
index 00000000..7ce1eda0
--- /dev/null
+++ b/themes/heo/components/FloatTocButton.js
@@ -0,0 +1,44 @@
+import { useState } from 'react'
+import Catalog from './Catalog'
+
+/**
+ * 移动端悬浮目录按钮
+ */
+export default function FloatTocButton(props) {
+ const [tocVisible, changeTocVisible] = useState(false)
+
+ const { post } = props
+
+ const toggleToc = () => {
+ changeTocVisible(!tocVisible)
+ }
+
+ // 没有目录就隐藏该按钮
+ if (!post || !post.toc || post.toc.length < 1) {
+ return <>>
+ }
+
+ return (
+ {/* 按钮 */}
+
+
+
+
+ {/* 目录Modal */}
+
+ {/* 侧边菜单 */}
+
+ {post && <>
+
+
+
+ >
+ }
+
+
+ {/* 背景蒙版 */}
+
+
)
+}
diff --git a/themes/heo/components/Footer.js b/themes/heo/components/Footer.js
new file mode 100644
index 00000000..fc8f1465
--- /dev/null
+++ b/themes/heo/components/Footer.js
@@ -0,0 +1,58 @@
+import React from 'react'
+import BLOG from '@/blog.config'
+import SocialButton from './SocialButton'
+import { AdSlot } from '@/components/GoogleAdsense'
+// 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..ff815b9b
--- /dev/null
+++ b/themes/heo/components/Hero.js
@@ -0,0 +1,238 @@
+// import Image from 'next/image'
+
+import BLOG from '@/blog.config'
+import { ArrowSmallRight, PlusSmall } from '@/components/HeroIcons'
+import LazyImage from '@/components/LazyImage'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useImperativeHandle, useRef, useState } from 'react'
+import CONFIG from '../config'
+
+/**
+ * 顶部英雄区
+ * 左右布局,
+ * 左侧:banner组
+ * 右侧:今日卡牌遮罩
+ * @returns
+ */
+const Hero = props => {
+ 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(`${BLOG.SUB_PATH}/${randomPost?.slug}`)
+ }
+
+ return
+
+
+
{CONFIG.HERO_TITLE_1}
{CONFIG.HERO_TITLE_2}
+
{CONFIG.HERO_TITLE_3}
+
+
+ {/* 斜向滚动的图标 */}
+
+
+ {/* 遮罩 */}
+
+
+
+}
+
+/**
+ * 图标滚动标签组
+ * 英雄区左上角banner条中斜向滚动的图标
+ */
+function TagsGroupBar() {
+ const groupIcons = CONFIG.GROUP_ICONS.concat(CONFIG.GROUP_ICONS)
+
+ return (
+
+
+ {groupIcons?.map((g, index) => {
+ return (
)
+ })}
+
+
+ )
+}
+
+/**
+ * 英雄区左下角3个指定分类按钮
+ * @returns
+ */
+function GroupMenu() {
+ return (
+
+
+
+ {CONFIG.HERO_CATEGORY_1?.title}
+
+
+
+
+
+
+
+
+ {CONFIG.HERO_CATEGORY_2?.title}
+
+
+
+
+
+
+ {/* 第三个标签在小屏上不显示 */}
+
+
+ {CONFIG.HERO_CATEGORY_3?.title}
+
+
+
+
+
+
+
+
+ )
+}
+
+/**
+ * 置顶文章区域
+ */
+function TopGroup(props) {
+ const { latestPosts, siteInfo } = props
+ const todayCardRef = useRef()
+ function handleMouseLeave() {
+ todayCardRef.current.coverUp()
+ }
+ return (
+
+ {/* 置顶最新文章 */}
+
+ {latestPosts?.map((p, index) => {
+ return
+
+
+
{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(CONFIG.HERO_TITLE_LINK)
+ }
+
+ return
+
+
+
+
{CONFIG.HERO_TITLE_4}
+
{CONFIG.HERO_TITLE_5}
+
+
+
+
+
+
+}
+
+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..ad4727c8
--- /dev/null
+++ b/themes/heo/components/InfoCard.js
@@ -0,0 +1,87 @@
+import BLOG from '@/blog.config'
+import { ArrowRightCircle, GlobeAlt } from '@/components/HeroIcons'
+import LazyImage from '@/components/LazyImage'
+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 (
+
+ {/* 信息卡牌第一行 */}
+
+
+
+ {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..cd836cee
--- /dev/null
+++ b/themes/heo/components/LatestPostsGroup.js
@@ -0,0 +1,49 @@
+import BLOG from '@/blog.config'
+import LazyImage from '@/components/LazyImage'
+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 (
+ (
+
+
+
+
+
+
+
+ )
+ )
+ })}
+
+}
+export default LatestPostsGroup
diff --git a/themes/heo/components/LatestPostsGroupMini.js b/themes/heo/components/LatestPostsGroupMini.js
new file mode 100644
index 00000000..bcf4d80a
--- /dev/null
+++ b/themes/heo/components/LatestPostsGroupMini.js
@@ -0,0 +1,63 @@
+import BLOG from '@/blog.config'
+import LazyImage from '@/components/LazyImage'
+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 (
+ (
+
+
+
+
+
+
+
{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..b6321836
--- /dev/null
+++ b/themes/heo/components/Logo.js
@@ -0,0 +1,25 @@
+import BLOG from '@/blog.config'
+import { Home } from '@/components/HeroIcons'
+import LazyImage from '@/components/LazyImage'
+import Link from 'next/link'
+import React from 'react'
+
+const Logo = props => {
+ const { siteInfo } = props
+ return (
+
+
+
+
+
+ {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..c69ca231
--- /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..7a56aa83
--- /dev/null
+++ b/themes/heo/components/MenuItemDrop.js
@@ -0,0 +1,41 @@
+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 && <>
+
+ {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..19f70863
--- /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..381b5adb
--- /dev/null
+++ b/themes/heo/components/MenuListTop.js
@@ -0,0 +1,35 @@
+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 }
+ ]
+
+ 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..af69384a
--- /dev/null
+++ b/themes/heo/components/NavBar.js
@@ -0,0 +1,165 @@
+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 BLOG from '@/blog.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/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..b4af5086
--- /dev/null
+++ b/themes/heo/components/NoticeBar.js
@@ -0,0 +1,27 @@
+
+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 (
+
+ )
+}
diff --git a/themes/heo/components/PaginationNumber.js b/themes/heo/components/PaginationNumber.js
new file mode 100644
index 00000000..4a1022ee
--- /dev/null
+++ b/themes/heo/components/PaginationNumber.js
@@ -0,0 +1,191 @@
+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 showPrev = currentPage !== 1
+ 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 (<>
+
+ {/* pc端分页按钮 */}
+
+ {/* 上一页 */}
+
+
+
+
+ {locale.PAGINATION.PREV}
+
+
+
+
+
+ {/* 分页 */}
+
+ {pages}
+
+ {/* 跳转页码 */}
+
+
+
+ {/* 下一页 */}
+
+
+
+
+
+ {locale.PAGINATION.NEXT}
+
+
+
+
+
+ {/* 移动端分页 */}
+
+
+ {/* 上一页 */}
+
+ {locale.PAGINATION.PREV}
+
+
+ {showPrev && showNext &&
}
+
+ {/* 下一页 */}
+
+ {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..5a707477
--- /dev/null
+++ b/themes/heo/components/PostHeader.js
@@ -0,0 +1,105 @@
+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'
+import LazyImage from '@/components/LazyImage'
+import { formatDateFmt } from '@/lib/formatDate'
+
+export default function PostHeader({ post, siteInfo }) {
+ if (!post) {
+ return <>>
+ }
+ // 文章头图
+ 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 */}
+
+ {post.title}
+
+
+ {/* 标题底部补充信息 */}
+
+
+
+
+ {post?.type !== 'Page' && (
+ <>
+
+
{post?.publishTime}
+
+ >
+ )}
+
+
+ {post.lastEditedTime}
+
+
+
+
+ {JSON.parse(BLOG.ANALYTICS_BUSUANZI_ENABLE) &&
+
+
}
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/themes/heo/components/RandomPostButton.js b/themes/heo/components/RandomPostButton.js
new file mode 100644
index 00000000..a8a0597f
--- /dev/null
+++ b/themes/heo/components/RandomPostButton.js
@@ -0,0 +1,26 @@
+import BLOG from '@/blog.config'
+import { useGlobal } from '@/lib/global'
+import { useRouter } from 'next/router'
+
+/**
+ * 随机跳转到一个文章
+ */
+export default function RandomPostButton(props) {
+ const { latestPosts } = props
+ const router = useRouter()
+ const { locale } = useGlobal()
+ /**
+ * 随机跳转文章
+ */
+ function handleClick() {
+ const randomIndex = Math.floor(Math.random() * latestPosts.length)
+ const randomPost = latestPosts[randomIndex]
+ router.push(`${BLOG.SUB_PATH}/${randomPost?.slug}`)
+ }
+
+ return (
+
+
+
+ )
+}
diff --git a/themes/heo/components/ReadingProgress.js b/themes/heo/components/ReadingProgress.js
new file mode 100644
index 00000000..0f9c55bb
--- /dev/null
+++ b/themes/heo/components/ReadingProgress.js
@@ -0,0 +1,56 @@
+import { ArrowSmallUp } from '@/components/HeroIcons'
+import { useEffect, useState } from 'react'
+
+/**
+ * 回顶按钮
+ * @returns
+ */
+export default function ReadingProgress() {
+ const [scrollPercentage, setScrollPercentage] = useState(0)
+
+ 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 - 20)) * 100)
+ setScrollPercentage(percent)
+ }
+
+ // 监听滚动事件
+ useEffect(() => {
+ let requestId
+
+ function updateScrollPercentage() {
+ handleScroll()
+ requestId = null
+ }
+
+ function handleAnimationFrame() {
+ if (requestId) {
+ return
+ }
+ requestId = requestAnimationFrame(updateScrollPercentage)
+ }
+
+ window.addEventListener('scroll', handleAnimationFrame)
+ return () => {
+ window.removeEventListener('scroll', handleAnimationFrame)
+ if (requestId) {
+ 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/SearchButton.js b/themes/heo/components/SearchButton.js
new file mode 100644
index 00000000..e23907a4
--- /dev/null
+++ b/themes/heo/components/SearchButton.js
@@ -0,0 +1,30 @@
+import BLOG from '@/blog.config'
+import { useGlobal } from '@/lib/global'
+import { useRouter } from 'next/router'
+import AlgoliaSearchModal from '@/components/AlgoliaSearchModal'
+import { useRef } from 'react'
+
+/**
+ * 搜索按钮
+ * @returns
+ */
+export default function SearchButton() {
+ const { locale } = useGlobal()
+ const router = useRouter()
+ const searchModal = useRef(null)
+
+ function handleSearch() {
+ if (BLOG.ALGOLIA_APP_ID) {
+ searchModal.current.openSearch()
+ } else {
+ router.push('/search')
+ }
+ }
+
+ 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..cb7427ac
--- /dev/null
+++ b/themes/heo/components/SideBar.js
@@ -0,0 +1,34 @@
+import BLOG from '@/blog.config'
+import LazyImage from '@/components/LazyImage'
+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..b8f1198a
--- /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..86ed342e
--- /dev/null
+++ b/themes/heo/config.js
@@ -0,0 +1,130 @@
+const CONFIG = {
+ HOME_BANNER_ENABLE: true,
+
+ SITE_CREATE_TIME: '2021-09-21', // 建站日期,用于计算网站运行的第几天
+
+ // 首页顶部通知条滚动内容,如不需要可以留空 []
+ NOTICE_BAR: [
+ { title: '欢迎来到我的博客', url: 'https://blog.tangly1024.com' },
+ { title: '访问文档中心获取更多帮助', url: 'https://docs.tangly1024.com' }
+ ],
+
+ // 英雄区(首页顶部大卡)
+ HERO_TITLE_1: '分享编程',
+ HERO_TITLE_2: '与思维认知',
+ HERO_TITLE_3: 'TANGLY1024.COM',
+ HERO_TITLE_4: '新版上线',
+ HERO_TITLE_5: 'NotionNext4.0 轻松定制主题',
+ HERO_TITLE_LINK: 'https://tangly1024.com',
+
+ // 英雄区显示三个置顶分类
+ HERO_CATEGORY_1: { title: '必看精选', url: '/tag/必看精选' },
+ HERO_CATEGORY_2: { title: '热门文章', url: '/tag/热门文章' },
+ HERO_CATEGORY_3: { title: '实用教程', url: '/tag/实用教程' },
+
+ // 右侧个人资料卡牌欢迎语,点击可自动切换
+ INFOCARD_GREETINGS: [
+ '你好!我是',
+ '🔍 分享与热心帮助',
+ '🤝 专修交互与设计',
+ '🏃 脚踏实地行动派',
+ '🏠 智能家居小能手',
+ '🤖️ 数码科技爱好者',
+ '🧱 团队小组发动机'
+ ],
+ INFO_CARD_URL: 'https://github.com/tangly1024/NotionNext', // 个人资料底部按钮链接
+
+ // 用户技能图标
+ GROUP_ICONS: [
+ {
+ title_1: 'AfterEffect',
+ img_1: '/images/heo/20239df3f66615b532ce571eac6d14ff21cf072602.webp',
+ color_1: '#989bf8',
+ title_2: 'Sketch',
+ img_2: '/images/heo/2023e0ded7b724a39f12d59c3dc8fbdc7cbe074202.webp',
+ color_2: '#ffffff'
+ },
+ {
+ title_1: 'Docker',
+ img_1: '/images/heo/20231108a540b2862d26f8850172e4ea58ed075102.webp',
+ color_1: '#57b6e6',
+ title_2: 'Photoshop',
+ img_2: '/images/heo/2023e4058a91608ea41751c4f102b131f267075902.webp',
+ color_2: '#4082c3'
+ },
+ {
+ title_1: 'FinalCutPro',
+ img_1: '/images/heo/20233e777652412247dd57fd9b48cf997c01070702.webp',
+ color_1: '#ffffff',
+ title_2: 'Python',
+ img_2: '/images/heo/20235c0731cd4c0c95fc136a8db961fdf963071502.webp',
+ color_2: '#ffffff'
+ },
+ {
+ title_1: 'Swift',
+ img_1: '/images/heo/202328bbee0b314297917b327df4a704db5c072402.webp',
+ color_1: '#eb6840',
+ title_2: 'Principle',
+ img_2: '/images/heo/2023f76570d2770c8e84801f7e107cd911b5073202.webp',
+ color_2: '#8f55ba'
+ },
+ {
+ title_1: 'illustrator',
+ img_1: '/images/heo/20237359d71b45ab77829cee5972e36f8c30073902.webp',
+ color_1: '#f29e39',
+ title_2: 'CSS3',
+ img_2: '/images/heo/20237c548846044a20dad68a13c0f0e1502f074602.webp',
+ color_2: '#2c51db'
+ },
+ {
+ title_1: 'JS',
+ img_1: '/images/heo/2023786e7fc488f453d5fb2be760c96185c0075502.webp',
+ color_1: '#f7cb4f',
+ title_2: 'HTML',
+ img_2: '/images/heo/202372b4d760fd8a497d442140c295655426070302.webp',
+ color_2: '#e9572b'
+ },
+ {
+ title_1: 'Git',
+ img_1: '/images/heo/2023ffa5707c4e25b6beb3e6a3d286ede4c6071102.webp',
+ color_1: '#df5b40',
+ title_2: 'Rhino',
+ img_2: '/images/heo/20231ca53fa0b09a3ff1df89acd7515e9516173302.webp',
+ color_2: '#1f1f1f'
+ }
+ ],
+
+ SOCIAL_CARD: true, // 是否显示右侧,点击加入社群按钮
+ SOCIAL_CARD_TITLE_1: '交流频道',
+ SOCIAL_CARD_TITLE_2: '加入我们的社群讨论分享',
+ SOCIAL_CARD_TITLE_3: '点击加入社群',
+ SOCIAL_CARD_URL: 'https://docs.tangly1024.com/article/how-to-question',
+
+ // ***** 以下配置无效,只是预留开发 ****
+ // 菜单配置
+ 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..60b955b8
--- /dev/null
+++ b/themes/heo/index.js
@@ -0,0 +1,423 @@
+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 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'
+
+/**
+ * 基础布局 采用上中下布局,移动端使用顶部侧边导航栏
+ * @param props
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const LayoutBase = props => {
+ const { children, headerSlot, slotTop, slotRight, meta, siteInfo, className } = props
+
+ 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(() => {
+ // 高亮搜索结果
+ if (currentSearch) {
+ setTimeout(() => {
+ replaceSearchResult({
+ doms: document.getElementsByClassName('replace'),
+ search: currentSearch,
+ target: {
+ 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卡牌 */}
+
+ {/* 左侧动图 */}
+
+
+ {/* 右侧文字 */}
+
+
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()
+ const headerSlot =
+ 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..b0de7c2d
--- /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/Layout404.js b/themes/hexo/Layout404.js
deleted file mode 100644
index 609b6e7c..00000000
--- a/themes/hexo/Layout404.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import LayoutBase from './LayoutBase'
-import { useRouter } from 'next/router'
-import { useEffect } from 'react'
-
-export const Layout404 = props => {
- const router = useRouter()
- useEffect(() => {
- // 延时3秒如果加载失败就返回首页
- setTimeout(() => {
- const article = typeof document !== 'undefined' && document.getElementById('container')
- if (!article) {
- router.push('/').then(() => {
- // console.log('找不到页面', router.asPath)
- })
- }
- }, 3000)
- })
- return (
-
-
-
- )
-}
diff --git a/themes/hexo/LayoutArchive.js b/themes/hexo/LayoutArchive.js
deleted file mode 100644
index 94dc9267..00000000
--- a/themes/hexo/LayoutArchive.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { useEffect } from 'react'
-import BlogPostArchive from './components/BlogPostArchive'
-import Card from './components/Card'
-import LayoutBase from './LayoutBase'
-
-export const LayoutArchive = (props) => {
- const { archivePosts } = props
-
- useEffect(() => {
- const anchor = window.location.hash
- if (anchor) {
- setTimeout(() => {
- const anchorElement = document.getElementById(anchor.substring(1))
- if (anchorElement) {
- anchorElement.scrollIntoView({ block: 'start', behavior: 'smooth' })
- }
- }, 300)
- }
- }, [])
-
- return
-
-
- {Object.keys(archivePosts).map(archiveTitle => (
-
- ))}
-
-
-
-}
diff --git a/themes/hexo/LayoutBase.js b/themes/hexo/LayoutBase.js
deleted file mode 100644
index 27c8e1a9..00000000
--- a/themes/hexo/LayoutBase.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import CommonHead from '@/components/CommonHead'
-import { useCallback, useEffect, useState } from 'react'
-import throttle from 'lodash.throttle'
-import Footer from './components/Footer'
-import JumpToTopButton from './components/JumpToTopButton'
-import SideRight from './components/SideRight'
-import TopNav from './components/TopNav'
-import FloatDarkModeButton from './components/FloatDarkModeButton'
-import Live2D from '@/components/Live2D'
-import LoadingCover from './components/LoadingCover'
-import { useGlobal } from '@/lib/global'
-import BLOG from '@/blog.config'
-import dynamic from 'next/dynamic'
-import { isBrowser, loadExternalResource } from '@/lib/utils'
-
-const FacebookPage = dynamic(
- () => {
- let facebook = <>>
- try {
- facebook = import('@/components/FacebookPage')
- } catch (err) {
- console.error(err)
- }
- return facebook
- },
- { ssr: false }
-)
-
-/**
- * 基础布局 采用左右两侧布局,移动端使用顶部导航栏
- * @param props
- * @returns {JSX.Element}
- * @constructor
- */
-const LayoutBase = props => {
- const { children, headerSlot, floatSlot, meta, siteInfo } = props
- const [showFloatButton, switchShow] = useState(false)
- // const [percent, changePercent] = useState(0) // 页面阅读百分比
- const rightAreaSlot = (
- <>
-
-
- >
- )
- const { onLoading } = useGlobal()
- const throttleMs = 200
- 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)
- }
- }, throttleMs))
- useEffect(() => {
- document.addEventListener('scroll', scrollListener)
- return () => document.removeEventListener('scroll', scrollListener)
- }, [])
-
- if (isBrowser()) {
- loadExternalResource('/css/theme-hexo.css', 'css')
- }
- return (
-
-
-
-
-
- {headerSlot}
-
-
-
-
- {onLoading ? : children}
-
-
-
-
-
- {/* 右下角悬浮 */}
-
-
-
-
- )
-}
-
-export default LayoutBase
diff --git a/themes/hexo/LayoutCategory.js b/themes/hexo/LayoutCategory.js
deleted file mode 100644
index 7df32650..00000000
--- a/themes/hexo/LayoutCategory.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import BlogPostListScroll from './components/BlogPostListScroll'
-import BlogPostListPage from './components/BlogPostListPage'
-import LayoutBase from './LayoutBase'
-import BLOG from '@/blog.config'
-
-export const LayoutCategory = props => {
- const { category } = props
- return
-
-
- {category}
-
- {BLOG.POST_LIST_STYLE === 'page' ? : }
-
-}
diff --git a/themes/hexo/LayoutCategoryIndex.js b/themes/hexo/LayoutCategoryIndex.js
deleted file mode 100644
index 0d0a24d0..00000000
--- a/themes/hexo/LayoutCategoryIndex.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { useGlobal } from '@/lib/global'
-import Link from 'next/link'
-import Card from './components/Card'
-import LayoutBase from './LayoutBase'
-
-export const LayoutCategoryIndex = props => {
- const { categoryOptions } = props
- const { locale } = useGlobal()
- return (
-
-
-
-
- {locale.COMMON.CATEGORY}:
-
-
- {categoryOptions.map(category => {
- return (
-
-
-
- {category.name}({category.count})
-
-
- )
- })}
-
-
-
- )
-}
diff --git a/themes/hexo/LayoutIndex.js b/themes/hexo/LayoutIndex.js
deleted file mode 100644
index 48cc29e4..00000000
--- a/themes/hexo/LayoutIndex.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import BLOG from '@/blog.config'
-import BlogPostListPage from './components/BlogPostListPage'
-import BlogPostListScroll from './components/BlogPostListScroll'
-import Header from './components/Header'
-import CONFIG_HEXO from './config_hexo'
-import LayoutBase from './LayoutBase'
-import React from 'react'
-
-export const LayoutIndex = (props) => {
- const headerSlot = CONFIG_HEXO.HOME_BANNER_ENABLE &&
- return
- {BLOG.POST_LIST_STYLE === 'page' ? : }
-
-}
diff --git a/themes/hexo/LayoutPage.js b/themes/hexo/LayoutPage.js
deleted file mode 100644
index ac32c985..00000000
--- a/themes/hexo/LayoutPage.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import BlogPostListPage from './components/BlogPostListPage'
-import LayoutBase from './LayoutBase'
-
-export const LayoutPage = (props) => {
- return
-
-
-}
diff --git a/themes/hexo/LayoutSearch.js b/themes/hexo/LayoutSearch.js
deleted file mode 100644
index bf9c6daa..00000000
--- a/themes/hexo/LayoutSearch.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import { useRouter } from 'next/router'
-import { useEffect, useRef } from 'react'
-import BLOG from '@/blog.config'
-import BlogPostListScroll from './components/BlogPostListScroll'
-import BlogPostListPage from './components/BlogPostListPage'
-import LayoutBase from './LayoutBase'
-import SearchInput from './components/SearchInput'
-import { useGlobal } from '@/lib/global'
-import Mark from 'mark.js'
-import TagItemMini from './components/TagItemMini'
-import Card from './components/Card'
-import Link from 'next/link'
-
-export const LayoutSearch = props => {
- const { keyword, tagOptions, categoryOptions } = props
- const { locale } = useGlobal()
- const router = useRouter()
- const currentSearch = keyword || router?.query?.s
- const cRef = useRef(null)
-
- useEffect(() => {
- setTimeout(() => {
- // 自动聚焦到搜索框
- cRef?.current?.focus()
- 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 && <>
-
-
- {/* 分类 */}
-
-
-
- {locale.COMMON.CATEGORY}:
-
-
- {categoryOptions?.map(category => {
- return (
-
-
-
- {category.name}({category.count})
-
-
- )
- })}
-
-
- {/* 标签 */}
-
-
-
- {locale.COMMON.TAGS}:
-
-
-
-
- >}
-
- {currentSearch && <>
-
- {BLOG.POST_LIST_STYLE === 'page' ? : }
-
- >}
-
-
- )
-}
diff --git a/themes/hexo/LayoutSlug.js b/themes/hexo/LayoutSlug.js
deleted file mode 100644
index 69478a09..00000000
--- a/themes/hexo/LayoutSlug.js
+++ /dev/null
@@ -1,95 +0,0 @@
-import { useRef } from 'react'
-import { ArticleLock } from './components/ArticleLock'
-import HeaderArticle from './components/HeaderArticle'
-import JumpToCommentButton from './components/JumpToCommentButton'
-import TocDrawer from './components/TocDrawer'
-import TocDrawerButton from './components/TocDrawerButton'
-import LayoutBase from './LayoutBase'
-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 { isBrowser } from '@/lib/utils'
-import ShareBar from '@/components/ShareBar'
-
-export const LayoutSlug = props => {
- const { post, lock, validPassword } = props
- const drawerRight = useRef(null)
-
- if (!post) {
- return
}
- {...props}
- showCategory={false}
- showTag={false}
- >
- }
-
- const targetRef = isBrowser() ? document.getElementById('container') : null
-
- const floatSlot = <>
- {post?.toc?.length > 1 &&
- {
- drawerRight?.current?.handleSwitchVisible()
- }}
- />
-
}
-
- >
-
- return (
-
}
- {...props}
- showCategory={false}
- showTag={false}
- floatSlot={floatSlot}
- >
-
- {lock &&
}
-
- {!lock &&
-
-
- {/* Notion文章主体 */}
-
-
-
-
- {/* 分享 */}
-
- {post.type === 'Post' && }
- {post.type === 'Post' && }
- {post.type === 'Post' && }
-
-
-
-
-
- {/* 评论互动 */}
-
-
-
-
}
-
-
-
-
-
-
-
- )
-}
diff --git a/themes/hexo/LayoutTag.js b/themes/hexo/LayoutTag.js
deleted file mode 100644
index 36a9ce7d..00000000
--- a/themes/hexo/LayoutTag.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import BLOG from '@/blog.config'
-import BlogPostListScroll from './components/BlogPostListScroll'
-import BlogPostListPage from './components/BlogPostListPage'
-import LayoutBase from './LayoutBase'
-import React from 'react'
-import Link from 'next/link'
-
-export const LayoutTag = (props) => {
- const tag = props.tagOptions.find((t) => {
- return t.name === props.tag
- })
-
- return (
-
- {tag && (
-
-
-
-
#{tag.name + (tag.count ? `(${tag.count})` : '')}
-
-
-
- )}
- {BLOG.POST_LIST_STYLE === 'page' ? : }
-
- )
-}
diff --git a/themes/hexo/LayoutTagIndex.js b/themes/hexo/LayoutTagIndex.js
deleted file mode 100644
index 5e83966a..00000000
--- a/themes/hexo/LayoutTagIndex.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { useGlobal } from '@/lib/global'
-import Card from './components/Card'
-import TagItemMini from './components/TagItemMini'
-import LayoutBase from './LayoutBase'
-
-export const LayoutTagIndex = props => {
- const { tagOptions } = props
- const { locale } = useGlobal()
- return (
-
-
-
-
- {locale.COMMON.TAGS}:
-
-
-
-
- )
-}
diff --git a/themes/hexo/components/Announcement.js b/themes/hexo/components/Announcement.js
index 677cb467..695c26a4 100644
--- a/themes/hexo/components/Announcement.js
+++ b/themes/hexo/components/Announcement.js
@@ -7,10 +7,10 @@ const Announcement = ({ post, className }) => {
const { locale } = useGlobal()
if (post?.blockMap) {
return
-
+
{locale.COMMON.ANNOUNCEMENT}
{post && (
-
+
)}
diff --git a/themes/hexo/components/ArticleAdjacent.js b/themes/hexo/components/ArticleAdjacent.js
index 6e1face7..21ca9e32 100644
--- a/themes/hexo/components/ArticleAdjacent.js
+++ b/themes/hexo/components/ArticleAdjacent.js
@@ -1,5 +1,5 @@
import Link from 'next/link'
-import CONFIG_HEXO from '../config_hexo'
+import CONFIG from '../config'
/**
* 上一篇,下一篇文章
@@ -7,11 +7,11 @@ import CONFIG_HEXO from '../config_hexo'
* @returns
*/
export default function ArticleAdjacent ({ prev, next }) {
- if (!prev || !next || !CONFIG_HEXO.ARTICLE_ADJACENT) {
+ if (!prev || !next || !CONFIG.ARTICLE_ADJACENT) {
return <>>
}
return (
-
+
- );
+ )
}
diff --git a/themes/hexo/components/ArticleCopyright.js b/themes/hexo/components/ArticleCopyright.js
index 80ffd8c9..4664573c 100644
--- a/themes/hexo/components/ArticleCopyright.js
+++ b/themes/hexo/components/ArticleCopyright.js
@@ -3,10 +3,10 @@ import { useGlobal } from '@/lib/global'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
-import CONFIG_HEXO from '../config_hexo'
+import CONFIG from '../config'
export default function ArticleCopyright () {
- if (!CONFIG_HEXO.ARTICLE_COPYRIGHT) {
+ if (!CONFIG.ARTICLE_COPYRIGHT) {
return <>>
}
const router = useRouter()
@@ -27,7 +27,7 @@ export default function ArticleCopyright () {
{locale.COMMON.URL}:
-
+
{path}
diff --git a/themes/hexo/components/ArticleLock.js b/themes/hexo/components/ArticleLock.js
index 0a56f39e..7f1da728 100644
--- a/themes/hexo/components/ArticleLock.js
+++ b/themes/hexo/components/ArticleLock.js
@@ -1,4 +1,5 @@
import { useGlobal } from '@/lib/global'
+import { useEffect, useRef } from 'react'
/**
* 加密文章校验组件
@@ -20,12 +21,25 @@ export const ArticleLock = props => {
}
}
}
+ const passwordInputRef = useRef(null)
+ useEffect(() => {
+ // 选中密码输入框并将其聚焦
+ passwordInputRef.current.focus()
+ }, [])
return
-
{locale.COMMON.ARTICLE_LOCK_TIPS}
+
{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/hexo/components/ArticleRecommend.js b/themes/hexo/components/ArticleRecommend.js
index 30373f67..dc5ef58e 100644
--- a/themes/hexo/components/ArticleRecommend.js
+++ b/themes/hexo/components/ArticleRecommend.js
@@ -1,7 +1,8 @@
import Link from 'next/link'
-import CONFIG_HEXO from '../config_hexo'
+import CONFIG from '../config'
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
+import LazyImage from '@/components/LazyImage'
/**
* 关联推荐文章
@@ -12,54 +13,48 @@ export default function ArticleRecommend({ recommendPosts, siteInfo }) {
const { locale } = useGlobal()
if (
- !CONFIG_HEXO.ARTICLE_RECOMMEND ||
- !recommendPosts ||
- recommendPosts.length === 0
+ !CONFIG.ARTICLE_RECOMMEND ||
+ !recommendPosts ||
+ recommendPosts.length === 0
) {
return <>>
}
return (
-
-
-
-
- {locale.COMMON.RELATE_POSTS}
-
-
-
- {recommendPosts.map(post => {
- const headerImage = post?.page_cover
- ? `url("${post.page_cover}")`
- : `url("${siteInfo?.pageCover}")`
-
- return (
- (
-
-
-
-
-
-
- {post.date?.start_date}
-
-
{post.title}
-
+
+
+
+
+ {locale.COMMON.RELATE_POSTS}
-
+
+
+ {recommendPosts.map(post => {
+ const headerImage = post?.pageCoverThumbnail
+ ? post.pageCoverThumbnail
+ : siteInfo?.pageCover
- )
- )
- })}
-
-
+ return (
+ (
+
+
+
+ )
+ )
+ })}
+
+
)
}
diff --git a/themes/hexo/components/BlogPostArchive.js b/themes/hexo/components/BlogPostArchive.js
index 20887d45..08feff4c 100644
--- a/themes/hexo/components/BlogPostArchive.js
+++ b/themes/hexo/components/BlogPostArchive.js
@@ -21,12 +21,12 @@ const BlogPostArchive = ({ posts = [], archiveTitle }) => {
{archiveTitle}
- {posts.map(post => (
+ {posts?.map(post => (
-
-
+
{post.date?.start_date}{' '}
{
))}
- );
+ )
}
}
diff --git a/themes/hexo/components/BlogPostCard.js b/themes/hexo/components/BlogPostCard.js
index 56a69368..dc1d9759 100644
--- a/themes/hexo/components/BlogPostCard.js
+++ b/themes/hexo/components/BlogPostCard.js
@@ -1,32 +1,30 @@
-import BLOG from '@/blog.config'
import Link from 'next/link'
import React from 'react'
-import CONFIG_HEXO from '../config_hexo'
+import CONFIG from '../config'
import { BlogPostCardInfo } from './BlogPostCardInfo'
+import BLOG from '@/blog.config'
+import LazyImage from '@/components/LazyImage'
// import Image from 'next/image'
const BlogPostCard = ({ index, post, showSummary, siteInfo }) => {
- const showPreview = CONFIG_HEXO.POST_LIST_PREVIEW && post.blockMap
- if (post && !post.page_cover && CONFIG_HEXO.POST_LIST_COVER_DEFAULT) {
- post.page_cover = siteInfo?.pageCover
+ const showPreview = CONFIG.POST_LIST_PREVIEW && post.blockMap
+ if (post && !post.pageCoverThumbnail && CONFIG.POST_LIST_COVER_DEFAULT) {
+ post.pageCover = siteInfo?.pageCoverThumbnail
}
- const showPageCover = CONFIG_HEXO.POST_LIST_COVER && post?.page_cover && !showPreview
+ const showPageCover = CONFIG.POST_LIST_COVER && post?.pageCoverThumbnail && !showPreview
// const delay = (index % 2) * 200
return (
-
-
+
{/* 文字内容 */}
@@ -36,7 +34,7 @@ const BlogPostCard = ({ index, post, showSummary, siteInfo }) => {
{showPageCover && (
)}
diff --git a/themes/hexo/components/BlogPostCardInfo.js b/themes/hexo/components/BlogPostCardInfo.js
index 7336b64a..135064a7 100644
--- a/themes/hexo/components/BlogPostCardInfo.js
+++ b/themes/hexo/components/BlogPostCardInfo.js
@@ -1,7 +1,9 @@
-import BLOG from '@/blog.config'
import NotionPage from '@/components/NotionPage'
import Link from 'next/link'
import TagItemMini from './TagItemMini'
+import TwikooCommentCount from '@/components/TwikooCommentCount'
+import BLOG from '@/blog.config'
+import { formatDateFmt } from '@/lib/formatDate'
/**
* 博客列表的文字内容
@@ -9,49 +11,50 @@ import TagItemMini from './TagItemMini'
* @returns
*/
export const BlogPostCardInfo = ({ post, showPreview, showPageCover, showSummary }) => {
- return
-
+ return
{/* 标题 */}
+ className={`line-clamp-2 replace cursor-pointer text-2xl ${showPreview ? 'text-center' : ''
+ } leading-tight font-normal text-gray-600 dark:text-gray-100 hover:text-indigo-700 dark:hover:text-indigo-400`}>
- {post.title}
+
{post.title}
- {/* 日期 */}
-
+ className="cursor-pointer font-light text-sm menu-link hover:text-indigo-700 dark:hover:text-indigo-400 transform">
-
- {post.date?.start_date || post.lastEditedTime}
+
+ {post.category}
-
+
+
+ }
{/* 摘要 */}
{(!showPreview || showSummary) && !post.results && (
-
+
{post.summary}
)}
{/* 搜索结果 */}
{post.results && (
-
- {post.results.map(r => (
- {r}
- ))}
+
+ {post.results.map((r, index) => (
+ {r}
+ ))}
)}
{/* 预览 */}
@@ -64,17 +67,19 @@ export const BlogPostCardInfo = ({ post, showPreview, showPageCover, showSummary
- {/* 分类标签 */}
+ {/* 日期标签 */}
+ {/* 日期 */}
+ className="font-light menu-link cursor-pointer text-sm leading-4 mr-3">
-
- {post.category}
+
+ {post?.publishTime || post.lastEditedTime}
+
{' '}
diff --git a/themes/hexo/components/BlogPostListPage.js b/themes/hexo/components/BlogPostListPage.js
index 9a2c68f7..92008f83 100644
--- a/themes/hexo/components/BlogPostListPage.js
+++ b/themes/hexo/components/BlogPostListPage.js
@@ -21,7 +21,7 @@ const BlogPostListPage = ({ page = 1, posts = [], postCount, siteInfo }) => {
{/* 文章列表 */}
- {posts.map(post => (
+ {posts?.map(post => (
))}
diff --git a/themes/hexo/components/BlogPostListScroll.js b/themes/hexo/components/BlogPostListScroll.js
index 2123700d..7646b056 100644
--- a/themes/hexo/components/BlogPostListScroll.js
+++ b/themes/hexo/components/BlogPostListScroll.js
@@ -3,7 +3,7 @@ import BlogPostCard from './BlogPostCard'
import BlogPostListEmpty from './BlogPostListEmpty'
import { useGlobal } from '@/lib/global'
import React from 'react'
-import CONFIG_HEXO from '../config_hexo'
+import CONFIG from '../config'
import { getListByPage } from '@/lib/utils'
/**
@@ -13,7 +13,7 @@ import { getListByPage } from '@/lib/utils'
* @returns {JSX.Element}
* @constructor
*/
-const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = CONFIG_HEXO.POST_LIST_SUMMARY, siteInfo }) => {
+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)
@@ -57,7 +57,7 @@ const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = CONFIG_HE
return
{/* 文章列表 */}
-
+
{postsToShow.map(post => (
))}
diff --git a/themes/hexo/components/Card.js b/themes/hexo/components/Card.js
index c166ffc1..c2db0e49 100644
--- a/themes/hexo/components/Card.js
+++ b/themes/hexo/components/Card.js
@@ -1,7 +1,7 @@
const Card = ({ children, headerSlot, className }) => {
return
<>{headerSlot}>
-
diff --git a/themes/hexo/components/Catalog.js b/themes/hexo/components/Catalog.js
index 1e7d5b40..980be47b 100644
--- a/themes/hexo/components/Catalog.js
+++ b/themes/hexo/components/Catalog.js
@@ -61,7 +61,7 @@ const Catalog = ({ toc }) => {
return <>>
}
- return
+ return
{locale.COMMON.TABLE_OF_CONTENTS}
@@ -79,7 +79,7 @@ const Catalog = ({ toc }) => {
notion-table-of-contents-item-indent-level-${tocItem.indentLevel} `}
>
{tocItem.text}
diff --git a/themes/hexo/components/FloatDarkModeButton.js b/themes/hexo/components/FloatDarkModeButton.js
index c23f32bd..f693d1f0 100644
--- a/themes/hexo/components/FloatDarkModeButton.js
+++ b/themes/hexo/components/FloatDarkModeButton.js
@@ -1,11 +1,11 @@
import { useGlobal } from '@/lib/global'
-import { saveDarkModeToCookies } from '@/lib/theme'
-import CONFIG_HEXO from '../config_hexo'
+import { saveDarkModeToCookies } from '@/themes/theme'
+import CONFIG from '../config'
export default function FloatDarkModeButton () {
const { isDarkMode, updateDarkMode } = useGlobal()
- if (!CONFIG_HEXO.WIDGET_DARK_MODE) {
+ if (!CONFIG.WIDGET_DARK_MODE) {
return <>>
}
diff --git a/themes/hexo/components/Footer.js b/themes/hexo/components/Footer.js
index 5a5c50d4..8025e7b9 100644
--- a/themes/hexo/components/Footer.js
+++ b/themes/hexo/components/Footer.js
@@ -26,9 +26,8 @@ const Footer = ({ title }) => {
-
-
{title}
-
Powered by NotionNext {BLOG.VERSION}.
+
{title} {BLOG.BIO && <>|>} {BLOG.BIO}
+
Powered by NotionNext {BLOG.VERSION}.
)
diff --git a/themes/hexo/components/Header.js b/themes/hexo/components/Header.js
deleted file mode 100644
index 60edd4e8..00000000
--- a/themes/hexo/components/Header.js
+++ /dev/null
@@ -1,112 +0,0 @@
-// import Image from 'next/image'
-import { useCallback, useEffect, useState } from 'react'
-import Typed from 'typed.js'
-import CONFIG_HEXO from '../config_hexo'
-import NavButtonGroup from './NavButtonGroup'
-import throttle from 'lodash.throttle'
-
-let wrapperTop = 0
-let windowTop = 0
-let autoScroll = false
-const enableAutoScroll = false // 是否开启自动吸附滚动
-
-/**
- *
- * @returns 头图
- */
-const Header = props => {
- const [typed, changeType] = useState()
- const { siteInfo } = props
- useEffect(() => {
- updateHeaderHeight()
-
- if (!typed && window && document.getElementById('typed')) {
- changeType(
- new Typed('#typed', {
- strings: CONFIG_HEXO.HOME_BANNER_GREETINGS,
- typeSpeed: 200,
- backSpeed: 100,
- backDelay: 400,
- showCursor: true,
- smartBackspace: true
- })
- )
- }
-
- if (enableAutoScroll) {
- scrollTrigger()
- window.addEventListener('scroll', scrollTrigger)
- }
-
- window.addEventListener('resize', updateHeaderHeight)
- return () => {
- if (enableAutoScroll) {
- window.removeEventListener('scroll', scrollTrigger)
- }
- window.removeEventListener('resize', updateHeaderHeight)
- }
- })
-
- function updateHeaderHeight() {
- requestAnimationFrame(() => {
- const wrapperElement = document.getElementById('wrapper')
- wrapperTop = wrapperElement?.offsetTop
- })
- }
-
- const autoScrollEnd = () => {
- if (autoScroll) {
- windowTop = window.scrollY
- autoScroll = false
- }
- }
- const throttleMs = 200
- const scrollTrigger = useCallback(throttle(() => {
- if (screen.width <= 768) {
- return
- }
-
- const scrollS = window.scrollY
- // 自动滚动
- if ((scrollS > windowTop) & (scrollS < window.innerHeight) && !autoScroll
- ) {
- autoScroll = true
- window.scrollTo({ top: wrapperTop, behavior: 'smooth' })
- autoScrollEnd()
- }
- if ((scrollS < windowTop) && (scrollS < window.innerHeight) && !autoScroll) {
- autoScroll = true
- window.scrollTo({ top: 0, behavior: 'smooth' })
- autoScrollEnd()
- }
- windowTop = scrollS
- }, throttleMs))
-
- return (
-
- )
-}
-
-export default Header
diff --git a/themes/hexo/components/HeaderArticle.js b/themes/hexo/components/HeaderArticle.js
deleted file mode 100644
index 2abfc9b7..00000000
--- a/themes/hexo/components/HeaderArticle.js
+++ /dev/null
@@ -1,75 +0,0 @@
-import Link from 'next/link'
-import { useGlobal } from '@/lib/global'
-import formatDate from '@/lib/formatDate'
-import BLOG from '@/blog.config'
-
-export default function HeaderArticle({ post, siteInfo }) {
- const { locale } = useGlobal()
-
- if (!post) {
- return <>>
- }
- const headerImage = post?.page_cover ? `url("${post.page_cover}")` : `url("${siteInfo?.pageCover}")`
-
- const date = formatDate(
- post?.date?.start_date || post?.createdTime,
- locale.LOCALE
- )
-
- return (
-
- )
-}
diff --git a/themes/hexo/components/Hero.js b/themes/hexo/components/Hero.js
new file mode 100644
index 00000000..c07a401e
--- /dev/null
+++ b/themes/hexo/components/Hero.js
@@ -0,0 +1,83 @@
+// import Image from 'next/image'
+import { useEffect, useState } from 'react'
+import Typed from 'typed.js'
+import CONFIG from '../config'
+import NavButtonGroup from './NavButtonGroup'
+import { useGlobal } from '@/lib/global'
+import BLOG from '@/blog.config'
+import LazyImage from '@/components/LazyImage'
+
+let wrapperTop = 0
+
+/**
+ * 顶部全屏大图
+ * @returns
+ */
+const Hero = props => {
+ const [typed, changeType] = useState()
+ const { siteInfo } = props
+ const { locale } = useGlobal()
+ const scrollToWrapper = () => {
+ window.scrollTo({ top: wrapperTop, behavior: 'smooth' })
+ }
+ useEffect(() => {
+ updateHeaderHeight()
+
+ if (!typed && window && document.getElementById('typed')) {
+ changeType(
+ new Typed('#typed', {
+ strings: BLOG.GREETING_WORDS.split(','),
+ typeSpeed: 200,
+ backSpeed: 100,
+ backDelay: 400,
+ showCursor: true,
+ smartBackspace: true
+ })
+ )
+ }
+
+ window.addEventListener('resize', updateHeaderHeight)
+ return () => {
+ window.removeEventListener('resize', updateHeaderHeight)
+ }
+ })
+
+ function updateHeaderHeight() {
+ requestAnimationFrame(() => {
+ const wrapperElement = document.getElementById('wrapper')
+ wrapperTop = wrapperElement?.offsetTop
+ })
+ }
+
+ return (
+
+ )
+}
+
+export default Hero
diff --git a/themes/hexo/components/HexoRecentComments.js b/themes/hexo/components/HexoRecentComments.js
index 9c2042e6..2ebf00c8 100644
--- a/themes/hexo/components/HexoRecentComments.js
+++ b/themes/hexo/components/HexoRecentComments.js
@@ -25,21 +25,23 @@ const HexoRecentComments = (props) => {
}, [])
return (
-
-
-
- {locale.COMMON.RECENT_COMMENTS}
-
+
+
+
+ {locale.COMMON.RECENT_COMMENTS}
+
- {onLoading && Loading...
}
- {!onLoading && comments && comments.length === 0 && No Comments
}
- {!onLoading && comments && comments.length > 0 && comments.map((comment) => )}
+ {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/hexo/components/InfoCard.js b/themes/hexo/components/InfoCard.js
index 1393211a..5f1116e8 100644
--- a/themes/hexo/components/InfoCard.js
+++ b/themes/hexo/components/InfoCard.js
@@ -3,22 +3,31 @@ import { useRouter } from 'next/router'
import Card from './Card'
import SocialButton from './SocialButton'
import MenuGroupCard from './MenuGroupCard'
-export function InfoCard (props) {
+import LazyImage from '@/components/LazyImage'
+
+/**
+ * 社交信息卡
+ * @param {*} props
+ * @returns
+ */
+export function InfoCard(props) {
const { className, siteInfo } = props
const router = useRouter()
- return
- {
- router.push('/')
- }}
- >
- {/* eslint-disable-next-line @next/next/no-img-element */}
-

-
- {BLOG.AUTHOR}
- {BLOG.BIO}
-
-
-
+ return (
+
+ {
+ router.push('/')
+ }}
+ >
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
+
+ {BLOG.AUTHOR}
+ {BLOG.BIO}
+
+
+
+ )
}
diff --git a/themes/hexo/components/JumpToCommentButton.js b/themes/hexo/components/JumpToCommentButton.js
index ce82883e..fb007712 100644
--- a/themes/hexo/components/JumpToCommentButton.js
+++ b/themes/hexo/components/JumpToCommentButton.js
@@ -1,5 +1,5 @@
import React from 'react'
-import CONFIG_HEXO from '../config_hexo'
+import CONFIG from '../config'
/**
* 跳转到评论区
@@ -7,7 +7,7 @@ import CONFIG_HEXO from '../config_hexo'
* @constructor
*/
const JumpToCommentButton = () => {
- if (!CONFIG_HEXO.WIDGET_TO_COMMENT) {
+ if (!CONFIG.WIDGET_TO_COMMENT) {
return <>>
}
diff --git a/themes/hexo/components/JumpToTopButton.js b/themes/hexo/components/JumpToTopButton.js
index 3602ad99..77313f46 100644
--- a/themes/hexo/components/JumpToTopButton.js
+++ b/themes/hexo/components/JumpToTopButton.js
@@ -1,6 +1,6 @@
import { useGlobal } from '@/lib/global'
import React from 'react'
-import CONFIG_HEXO from '../config_hexo'
+import CONFIG from '../config'
/**
* 跳转到网页顶部
@@ -13,7 +13,7 @@ import CONFIG_HEXO from '../config_hexo'
const JumpToTopButton = ({ showPercent = true, percent }) => {
const { locale } = useGlobal()
- if (!CONFIG_HEXO.WIDGET_TO_TOP) {
+ if (!CONFIG.WIDGET_TO_TOP) {
return <>>
}
return (
window.scrollTo({ top: 0, behavior: 'smooth' })} >
diff --git a/themes/hexo/components/LatestPostsGroup.js b/themes/hexo/components/LatestPostsGroup.js
index e5d02a71..d315367a 100644
--- a/themes/hexo/components/LatestPostsGroup.js
+++ b/themes/hexo/components/LatestPostsGroup.js
@@ -1,4 +1,5 @@
import BLOG from '@/blog.config'
+import LazyImage from '@/components/LazyImage'
import { useGlobal } from '@/lib/global'
// import Image from 'next/image'
import Link from 'next/link'
@@ -29,7 +30,7 @@ const LatestPostsGroup = ({ latestPosts, siteInfo }) => {
{latestPosts.map(post => {
const selected = currentPath === `${BLOG.SUB_PATH}/${post.slug}`
- const headerImage = post?.page_cover ? post.page_cover : siteInfo?.pageCover
+ const headerImage = post?.pageCoverThumbnail ? post.pageCoverThumbnail : siteInfo?.pageCover
return (
(
{
title={post.title}
href={`${BLOG.SUB_PATH}/${post.slug}`}
passHref
- className={'my-2 flex'}>
+ className={'my-3 flex'}>
-
- {/*
*/}
- {/* eslint-disable-next-line @next/next/no-img-element */}
-

+
+
{
}
>
-
{post.title}
+
{post.title}
{post.lastEditedTime}
diff --git a/themes/hexo/components/Logo.js b/themes/hexo/components/Logo.js
index 61408026..2dd392b7 100644
--- a/themes/hexo/components/Logo.js
+++ b/themes/hexo/components/Logo.js
@@ -7,9 +7,9 @@ const Logo = props => {
return (
-
{siteInfo?.title || BLOG.TITLE}
+
{siteInfo?.title || BLOG.TITLE}
- );
+ )
}
export default Logo
diff --git a/themes/hexo/components/MenuGroupCard.js b/themes/hexo/components/MenuGroupCard.js
index e481c2e6..25dba50c 100644
--- a/themes/hexo/components/MenuGroupCard.js
+++ b/themes/hexo/components/MenuGroupCard.js
@@ -1,7 +1,7 @@
import React from 'react'
import Link from 'next/link'
import { useGlobal } from '@/lib/global'
-import CONFIG_HEXO from '../config_hexo'
+import CONFIG from '../config'
const MenuGroupCard = (props) => {
const { postCount, categoryOptions, tagOptions } = props
@@ -11,9 +11,9 @@ const MenuGroupCard = (props) => {
const tagSlot =
{tagOptions?.length}
const links = [
- { name: locale.COMMON.ARTICLE, to: '/archive', slot: archiveSlot, show: CONFIG_HEXO.MENU_ARCHIVE },
- { name: locale.COMMON.CATEGORY, to: '/category', slot: categorySlot, show: CONFIG_HEXO.MENU_CATEGORY },
- { name: locale.COMMON.TAGS, to: '/tag', slot: tagSlot, show: CONFIG_HEXO.MENU_TAG }
+ { 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 (
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/MenuItemDrop.js b/themes/hexo/components/MenuItemDrop.js
index 476baadf..2dfb6f79 100644
--- a/themes/hexo/components/MenuItemDrop.js
+++ b/themes/hexo/components/MenuItemDrop.js
@@ -28,10 +28,10 @@ export const MenuItemDrop = ({ link }) => {
{/* 子菜单 */}
{hasSubMenu &&
- {link.subMenus.map(sLink => {
- return -
+ {link.subMenus.map((sLink, index) => {
+ return
-
- {link?.icon && }{sLink.title}
+ {link?.icon && }{sLink.title}
})}
diff --git a/themes/hexo/components/MenuListSide.js b/themes/hexo/components/MenuListSide.js
index cda25159..1a3b2f5b 100644
--- a/themes/hexo/components/MenuListSide.js
+++ b/themes/hexo/components/MenuListSide.js
@@ -2,17 +2,17 @@ import React from 'react'
import { useGlobal } from '@/lib/global'
import BLOG from '@/blog.config'
import { MenuItemCollapse } from './MenuItemCollapse'
-import CONFIG_HEXO from '../config_hexo'
+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_HEXO.MENU_ARCHIVE },
- { icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG_HEXO.MENU_SEARCH },
- { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_HEXO.MENU_CATEGORY },
- { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_HEXO.MENU_TAG }
+ { 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) {
diff --git a/themes/hexo/components/MenuListTop.js b/themes/hexo/components/MenuListTop.js
index 39c64ed3..a2fd66ad 100644
--- a/themes/hexo/components/MenuListTop.js
+++ b/themes/hexo/components/MenuListTop.js
@@ -1,6 +1,6 @@
import React from 'react'
import { useGlobal } from '@/lib/global'
-import CONFIG_HEXO from '../config_hexo'
+import CONFIG from '../config'
import BLOG from '@/blog.config'
import { MenuItemDrop } from './MenuItemDrop'
@@ -9,11 +9,11 @@ export const MenuListTop = (props) => {
const { locale } = useGlobal()
let links = [
- { icon: 'fa-solid fa-house', name: locale.NAV.INDEX, to: '/', show: CONFIG_HEXO.MENU_INDEX },
- { icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG_HEXO.MENU_SEARCH },
- { icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG_HEXO.MENU_ARCHIVE }
- // { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_HEXO.MENU_CATEGORY },
- // { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_HEXO.MENU_TAG }
+ { 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) {
diff --git a/themes/hexo/components/NavButtonGroup.js b/themes/hexo/components/NavButtonGroup.js
index f1ca4297..38709213 100644
--- a/themes/hexo/components/NavButtonGroup.js
+++ b/themes/hexo/components/NavButtonGroup.js
@@ -14,7 +14,7 @@ const NavButtonGroup = (props) => {
}
return (
-
- );
+ )
}
function getPageElement(page, currentPage, pagePrefix) {
@@ -66,7 +66,7 @@ function getPageElement(page, currentPage, pagePrefix) {
{page}
)
- );
+ )
}
function generatePages(pagePrefix, page, currentPage, totalPage) {
diff --git a/themes/hexo/components/PostHeader.js b/themes/hexo/components/PostHeader.js
new file mode 100644
index 00000000..21eec51a
--- /dev/null
+++ b/themes/hexo/components/PostHeader.js
@@ -0,0 +1,79 @@
+import Link from 'next/link'
+import TagItemMini from './TagItemMini'
+import { useGlobal } from '@/lib/global'
+import BLOG from '@/blog.config'
+import NotionIcon from '@/components/NotionIcon'
+import LazyImage from '@/components/LazyImage'
+import { formatDateFmt } from '@/lib/formatDate'
+
+export default function PostHeader({ post, siteInfo }) {
+ const { locale } = useGlobal()
+
+ if (!post) {
+ return <>>
+ }
+ const headerImage = post?.pageCover ? post.pageCover : siteInfo?.pageCover
+
+ return (
+
+ )
+}
diff --git a/themes/hexo/components/Progress.js b/themes/hexo/components/Progress.js
index c54f422b..5b4f9b20 100644
--- a/themes/hexo/components/Progress.js
+++ b/themes/hexo/components/Progress.js
@@ -10,7 +10,7 @@ const Progress = ({ targetRef, showPercent = true }) => {
const currentRef = targetRef?.current || targetRef
const [percent, changePercent] = useState(0)
const scrollListener = () => {
- const target = currentRef || (isBrowser() && document.getElementById('container'))
+ const target = currentRef || (isBrowser() && document.getElementById('article-wrapper'))
if (target) {
const clientHeight = target.clientHeight
const scrollY = window.pageYOffset
@@ -28,9 +28,9 @@ const Progress = ({ targetRef, showPercent = true }) => {
}, [])
return (
-
+
{showPercent && (
diff --git a/themes/hexo/components/RightFloatArea.js b/themes/hexo/components/RightFloatArea.js
new file mode 100644
index 00000000..d7fadce5
--- /dev/null
+++ b/themes/hexo/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/hexo/components/SearchNav.js b/themes/hexo/components/SearchNav.js
new file mode 100644
index 00000000..359f5c8c
--- /dev/null
+++ b/themes/hexo/components/SearchNav.js
@@ -0,0 +1,70 @@
+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/hexo/components/SideBar.js b/themes/hexo/components/SideBar.js
index 6f0fc4e9..c6a1340f 100644
--- a/themes/hexo/components/SideBar.js
+++ b/themes/hexo/components/SideBar.js
@@ -1,4 +1,5 @@
import BLOG from '@/blog.config'
+import LazyImage from '@/components/LazyImage'
import { useRouter } from 'next/router'
import MenuGroupCard from './MenuGroupCard'
import { MenuListSide } from './MenuListSide'
@@ -19,13 +20,11 @@ const SideBar = (props) => {
{ 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 */}
-

+
-
)
diff --git a/themes/hexo/components/SideBarDrawer.js b/themes/hexo/components/SideBarDrawer.js
index 629889b9..87125c05 100644
--- a/themes/hexo/components/SideBarDrawer.js
+++ b/themes/hexo/components/SideBarDrawer.js
@@ -1,5 +1,5 @@
import { useRouter } from 'next/router'
-import React from 'react'
+import { useEffect } from 'react'
/**
* 侧边栏抽屉面板,可以从侧面拉出
@@ -8,7 +8,7 @@ import React from 'react'
*/
const SideBarDrawer = ({ children, isOpen, onOpen, onClose, className }) => {
const router = useRouter()
- React.useEffect(() => {
+ useEffect(() => {
const sideBarDrawerRouteListener = () => {
switchSideDrawerVisible(false)
}
@@ -29,17 +29,17 @@ const SideBarDrawer = ({ children, isOpen, onOpen, onClose, className }) => {
const sideBarDrawerBackground = window.document.getElementById('sidebar-drawer-background')
if (showStatus) {
- sideBarDrawer.classList.replace('-mr-72', 'mr-0')
- sideBarDrawerBackground.classList.replace('hidden', 'block')
+ 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')
+ sideBarDrawer?.classList.replace('mr-0', '-mr-72')
+ sideBarDrawerBackground?.classList.replace('block', 'hidden')
}
}
return
}
diff --git a/themes/hexo/components/TocDrawer.js b/themes/hexo/components/TocDrawer.js
index 7df6d309..13fad1a9 100644
--- a/themes/hexo/components/TocDrawer.js
+++ b/themes/hexo/components/TocDrawer.js
@@ -25,7 +25,7 @@ const TocDrawer = ({ post, cRef }) => {
+ ' w-60 duration-200 fixed right-12 bottom-12 rounded py-2 bg-white dark:bg-gray-900'}>
{post && <>
diff --git a/themes/hexo/components/TocDrawerButton.js b/themes/hexo/components/TocDrawerButton.js
index fc87cefc..8e5053b6 100644
--- a/themes/hexo/components/TocDrawerButton.js
+++ b/themes/hexo/components/TocDrawerButton.js
@@ -1,6 +1,6 @@
import { useGlobal } from '@/lib/global'
import React from 'react'
-import CONFIG_HEXO from '../config_hexo'
+import CONFIG from '../config'
/**
* 点击召唤目录抽屉
@@ -11,7 +11,7 @@ import CONFIG_HEXO from '../config_hexo'
*/
const TocDrawerButton = (props) => {
const { locale } = useGlobal()
- if (!CONFIG_HEXO.WIDGET_TOC) {
+ if (!CONFIG.WIDGET_TOC) {
return <>>
}
return (
diff --git a/themes/hexo/config_hexo.js b/themes/hexo/config.js
similarity index 76%
rename from themes/hexo/config_hexo.js
rename to themes/hexo/config.js
index ded4489f..b24d4fd1 100644
--- a/themes/hexo/config_hexo.js
+++ b/themes/hexo/config.js
@@ -1,9 +1,11 @@
-const CONFIG_HEXO = {
+const CONFIG = {
HOME_BANNER_ENABLE: true,
+ // 3.14.1以后的版本中,欢迎语在blog.config.js中配置,用英文逗号','隔开多个。
HOME_BANNER_GREETINGS: ['Hi,我是一个程序员', 'Hi,我是一个打工人', 'Hi,我是一个干饭人', '欢迎来到我的博客🎉'], // 首页大图标语文字
HOME_NAV_BUTTONS: true, // 首页是否显示分类大图标按钮
- HOME_NAV_BACKGROUND_IMG_FIXED: true, // 首页背景图滚动时是否固定,true 则滚动时图片不懂; false则随鼠标滚动
+ // 已知未修复bug, 在移动端开启true后会加载不出图片; 暂时建议设置为false。
+ HOME_NAV_BACKGROUND_IMG_FIXED: false, // 首页背景图滚动时是否固定,true 则滚动时图片不懂动; false则随鼠标滚动 ;
// 菜单配置
MENU_INDEX: true, // 显示首页
@@ -31,4 +33,4 @@ const CONFIG_HEXO = {
WIDGET_DARK_MODE: true, // 夜间模式
WIDGET_TOC: true // 移动端悬浮目录
}
-export default CONFIG_HEXO
+export default CONFIG
diff --git a/themes/hexo/index.js b/themes/hexo/index.js
index 8528d5da..78f3ab69 100644
--- a/themes/hexo/index.js
+++ b/themes/hexo/index.js
@@ -1,25 +1,347 @@
-import CONFIG_HEXO from './config_hexo'
-import { LayoutIndex } from './LayoutIndex'
-import { LayoutSearch } from './LayoutSearch'
-import { LayoutArchive } from './LayoutArchive'
-import { LayoutSlug } from './LayoutSlug'
-import { Layout404 } from './Layout404'
-import { LayoutCategory } from './LayoutCategory'
-import { LayoutCategoryIndex } from './LayoutCategoryIndex'
-import { LayoutPage } from './LayoutPage'
-import { LayoutTag } from './LayoutTag'
-import { LayoutTagIndex } from './LayoutTagIndex'
+import CONFIG from './config'
+
+import CommonHead from '@/components/CommonHead'
+import { useEffect, useRef } from 'react'
+import Footer from './components/Footer'
+import SideRight from './components/SideRight'
+import TopNav from './components/TopNav'
+import { useGlobal } from '@/lib/global'
+import BLOG from '@/blog.config'
+import { isBrowser } from '@/lib/utils'
+import BlogPostListPage from './components/BlogPostListPage'
+import BlogPostListScroll from './components/BlogPostListScroll'
+import Hero from './components/Hero'
+import { useRouter } from 'next/router'
+import Card from './components/Card'
+import RightFloatArea from './components/RightFloatArea'
+import SearchNav from './components/SearchNav'
+import BlogPostArchive from './components/BlogPostArchive'
+import { ArticleLock } from './components/ArticleLock'
+import PostHeader from './components/PostHeader'
+import JumpToCommentButton from './components/JumpToCommentButton'
+import TocDrawer from './components/TocDrawer'
+import TocDrawerButton from './components/TocDrawerButton'
+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 TagItemMini from './components/TagItemMini'
+import Link from 'next/link'
+import SlotBar from './components/SlotBar'
+import { Transition } from '@headlessui/react'
+import { Style } from './style'
+import replaceSearchResult from '@/components/Mark'
+
+/**
+ * 基础布局 采用左右两侧布局,移动端使用顶部导航栏
+ * @param props
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const LayoutBase = props => {
+ const { children, headerSlot, floatSlot, slotTop, meta, siteInfo, className } = props
+ const { onLoading } = useGlobal()
+
+ return (
+
+ {/* 网页SEO */}
+
+
+
+ {/* 顶部导航 */}
+
+
+ {/* 顶部嵌入 */}
+
+ {headerSlot}
+
+
+ {/* 主区块 */}
+
+
+
+
+
+ {/* 主区上部嵌入 */}
+ {slotTop}
+
+ {children}
+
+
+
+ {/* 右侧栏 */}
+
+
+
+
+ {/* 悬浮菜单 */}
+
+
+ {/* 页脚 */}
+
+
+ )
+}
+
+/**
+ * 首页
+ * 是一个博客列表,嵌入一个Hero大图
+ * @param {*} props
+ * @returns
+ */
+const LayoutIndex = (props) => {
+ const headerSlot = CONFIG.HOME_BANNER_ENABLE &&
+ return
+}
+
+/**
+ * 博客列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutPostList = (props) => {
+ return
+
+ {BLOG.POST_LIST_STYLE === 'page' ? : }
+
+}
+
+/**
+ * 搜索
+ * @param {*} props
+ * @returns
+ */
+const LayoutSearch = props => {
+ const { keyword } = props
+ const router = useRouter()
+ const currentSearch = keyword || router?.query?.s
+
+ useEffect(() => {
+ if (currentSearch) {
+ replaceSearchResult({
+ doms: document.getElementsByClassName('replace'),
+ search: keyword,
+ target: {
+ element: 'span',
+ className: 'text-red-500 border-b border-dashed'
+ }
+ })
+ }
+ })
+
+ return (
+
+ {!currentSearch
+ ?
+ : {BLOG.POST_LIST_STYLE === 'page' ? : }
}
+
+ )
+}
+
+/**
+ * 归档
+ * @param {*} props
+ * @returns
+ */
+const LayoutArchive = (props) => {
+ const { archivePosts } = props
+ return
+
+
+ {Object.keys(archivePosts).map(archiveTitle => (
+
+ ))}
+
+
+
+}
+
+/**
+ * 文章详情
+ * @param {*} props
+ * @returns
+ */
+const LayoutSlug = props => {
+ const { post, lock, validPassword } = props
+ const drawerRight = useRef(null)
+
+ const targetRef = isBrowser() ? document.getElementById('article-wrapper') : null
+
+ const floatSlot = <>
+ {post?.toc?.length > 1 &&
+ {
+ drawerRight?.current?.handleSwitchVisible()
+ }}
+ />
+
}
+
+ >
+
+ return (
+
} showCategory={false} showTag={false} floatSlot={floatSlot} >
+
+ {lock &&
}
+
+ {!lock &&
+
+
+ {/* Notion文章主体 */}
+
+
+ {/* 分享 */}
+
+ {post?.type === 'Post' && <>
+
+
+
+ >}
+
+
+
+
+
+ {/* 评论互动 */}
+
+
+
+
}
+
+
+
+
+
+
+
+ )
+}
+
+/**
+ * 404
+ * @param {*} props
+ * @returns
+ */
+const Layout404 = props => {
+ const router = useRouter()
+ useEffect(() => {
+ // 延时3秒如果加载失败就返回首页
+ setTimeout(() => {
+ if (isBrowser()) {
+ const article = document.getElementById('notion-article')
+ if (!article) {
+ router.push('/').then(() => {
+ // console.log('找不到页面', router.asPath)
+ })
+ }
+ }
+ }, 3000)
+ })
+ return (
+
+
+
+ )
+}
+
+/**
+ * 分类列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutCategoryIndex = props => {
+ const { categoryOptions } = props
+ const { locale } = useGlobal()
+ 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}:
+
+
+
+
+ )
+}
export {
- CONFIG_HEXO as THEME_CONFIG,
+ CONFIG as THEME_CONFIG,
LayoutIndex,
LayoutSearch,
LayoutArchive,
LayoutSlug,
Layout404,
- LayoutCategory,
LayoutCategoryIndex,
- LayoutPage,
- LayoutTag,
+ LayoutPostList,
LayoutTagIndex
}
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/index.js b/themes/index.js
deleted file mode 100644
index 7f87efb1..00000000
--- a/themes/index.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * 切换主题请修改 blog.config.js 中的 THEME 字段
- */
-import * as next from './next'
-import * as fukasawa from './fukasawa'
-import * as hexo from './hexo'
-import * as medium from './medium'
-import * as nobelium from './nobelium'
-import * as matery from './matery'
-import * as example from './example'
-import * as simple from './simple'
-
-export const ALL_THEME = [
- 'hexo',
- 'matery',
- 'next',
- 'medium',
- 'fukasawa',
- 'nobelium',
- 'example',
- 'simple'
-]
-export {
- hexo,
- next,
- medium,
- fukasawa,
- nobelium,
- matery,
- example,
- simple
-}
diff --git a/themes/landing/components/Features.js b/themes/landing/components/Features.js
new file mode 100644
index 00000000..1139ce7b
--- /dev/null
+++ b/themes/landing/components/Features.js
@@ -0,0 +1,171 @@
+/* eslint-disable @next/next/no-img-element */
+'use client'
+
+import { useState, useRef, useEffect } from 'react'
+import { Transition } from '@headlessui/react'
+import CONFIG from '../config'
+import LazyImage from '@/components/LazyImage'
+// import FeaturesElement from '@/public/images/features-element.png'
+
+export default function Features() {
+ const [tab, setTab] = useState(1)
+
+ const tabs = useRef(null)
+
+ const heightFix = () => {
+ if (tabs.current && tabs.current.parentElement) tabs.current.parentElement.style.height = `${tabs.current.clientHeight}px`
+ }
+
+ useEffect(() => {
+ heightFix()
+ }, [])
+
+ return (
+
+
+ {/* Section background (needs .relative class on parent and next sibling elements) */}
+
+
+
+
+
+
+ {/* Section header */}
+
+
{CONFIG.FEATURES_HEADER_1}
+
+
+
+ {/* Section content */}
+
+
+ {/* Content */}
+
+
+
{CONFIG.FEATURES_HEADER_2}
+
{CONFIG.FEATURES_HEADER_2_P}
+
+ {/* Tabs buttons */}
+
+
+
+ {/* Tabs items */}
+
+
+
+ {/* Item 1 */}
+
heightFix()}
+ unmount={false}
+ >
+
+
+
+
+
+ {/* Item 2 */}
+
heightFix()}
+ unmount={false}
+ >
+
+ {/*
+ */}
+
+
+
+ {/* Item 3 */}
+
heightFix()}
+ unmount={false}
+ >
+
+ {/*
+ */}
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/themes/landing/components/FeaturesBlocks.js b/themes/landing/components/FeaturesBlocks.js
new file mode 100644
index 00000000..1c0a3e2f
--- /dev/null
+++ b/themes/landing/components/FeaturesBlocks.js
@@ -0,0 +1,127 @@
+import CONFIG from '../config'
+
+export default function FeaturesBlocks() {
+ return (
+
+
+ {/* Section background (needs .relative class on parent and next sibling elements) */}
+
+
+
+
+
+
+ {/* Section header */}
+
+
{CONFIG.FEATURES_BLOCK_HEADER}
+
+
+
+ {/* Items */}
+
+
+ {/* 1st item */}
+
+
+
{CONFIG.FEATURES_BLOCK_1_TITLE}
+
{CONFIG.FEATURES_BLOCK_1_P}
+
+
+ {/* 2nd item */}
+
+
+
{CONFIG.FEATURES_BLOCK_2_TITLE}
+
{CONFIG.FEATURES_BLOCK_2_P}
+
+
+ {/* 3rd item */}
+
+
+
{CONFIG.FEATURES_BLOCK_3_TITLE}
+
{CONFIG.FEATURES_BLOCK_3_P}
+
+
+ {/* 4th item */}
+
+
+
{CONFIG.FEATURES_BLOCK_4_TITLE}
+
{CONFIG.FEATURES_BLOCK_4_P}
+
+
+ {/* 5th item */}
+
+
+
{CONFIG.FEATURES_BLOCK_5_TITLE}
+
{CONFIG.FEATURES_BLOCK_5_P}
+
+
+ {/* 6th item */}
+
+
+
{CONFIG.FEATURES_BLOCK_6_TITLE}
+
{CONFIG.FEATURES_BLOCK_6_P}
+
+
+
+
+
+
+
+ )
+}
diff --git a/themes/landing/components/Footer.js b/themes/landing/components/Footer.js
new file mode 100644
index 00000000..9a4f8527
--- /dev/null
+++ b/themes/landing/components/Footer.js
@@ -0,0 +1,182 @@
+import { subscribeToNewsletter } from '@/lib/mailchimp'
+import Link from 'next/link'
+import { useEffect, useRef, useState } from 'react'
+import CONFIG from '../config'
+import Logo from './Logo'
+
+/**
+ * 页脚
+ */
+export default function Footer() {
+ const formRef = useRef()
+ const [success, setSuccess] = useState(false)
+ useEffect(() => {
+ const form = formRef.current
+ const handleSubmit = (e) => {
+ e.preventDefault()
+ const email = document.querySelector('#newsletter').value
+ subscribeToNewsletter(email).then(response => {
+ console.log('Subscription succeeded:', response)
+ // 在此处添加成功订阅后的操作
+ setSuccess(true)
+ })
+ .catch(error => {
+ console.error('Subscription failed:', error)
+ // 在此处添加订阅失败后的操作
+ })
+ }
+ form?.addEventListener('submit', handleSubmit)
+ return () => {
+ form?.removeEventListener('submit', handleSubmit)
+ }
+ }, [subscribeToNewsletter])
+
+ return (
+
+ )
+}
diff --git a/themes/landing/components/Header.js b/themes/landing/components/Header.js
new file mode 100644
index 00000000..ee6fe3ff
--- /dev/null
+++ b/themes/landing/components/Header.js
@@ -0,0 +1,61 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+
+import Link from 'next/link'
+import Logo from './Logo'
+import MobileMenu from './MobileMenu'
+import CONFIG from '../config'
+
+export default function Header() {
+ const [top, setTop] = useState(true)
+
+ // detect whether user has scrolled the page down by 10px
+ const scrollHandler = () => {
+ window.pageYOffset > 10 ? setTop(false) : setTop(true)
+ }
+
+ useEffect(() => {
+ scrollHandler()
+ window.addEventListener('scroll', scrollHandler)
+ return () => window.removeEventListener('scroll', scrollHandler)
+ }, [top])
+
+ return (
+
+
+
+
+ {/* Site branding */}
+
+
+
+
+ {/* Desktop navigation */}
+
+ {/* Desktop sign in links */}
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/themes/landing/components/Hero.js b/themes/landing/components/Hero.js
new file mode 100644
index 00000000..ff7527f6
--- /dev/null
+++ b/themes/landing/components/Hero.js
@@ -0,0 +1,69 @@
+import CONFIG from '../config'
+import ModalVideo from './ModalVideo'
+
+export default function Hero() {
+ return (
+
+
+ {/* Illustration behind hero content */}
+
+
+
+
+
+
+ {/* Hero content */}
+
+
+ {/* Section header */}
+
+
+ {CONFIG.HERO_TITLE_1}
+
+
+
+
+ {/* Hero image */}
+
+
+
+
+
+
+ )
+}
diff --git a/themes/landing/components/Logo.js b/themes/landing/components/Logo.js
new file mode 100644
index 00000000..313d7656
--- /dev/null
+++ b/themes/landing/components/Logo.js
@@ -0,0 +1,21 @@
+import Link from 'next/link'
+
+export default function Logo() {
+ return (
+
+
+
+ )
+}
diff --git a/themes/landing/components/MobileMenu.js b/themes/landing/components/MobileMenu.js
new file mode 100644
index 00000000..7b5d1944
--- /dev/null
+++ b/themes/landing/components/MobileMenu.js
@@ -0,0 +1,86 @@
+'use client'
+
+import { useState, useRef, useEffect } from 'react'
+import { Transition } from '@headlessui/react'
+import Link from 'next/link'
+import CONFIG from '../config'
+
+export default function MobileMenu() {
+ const [mobileNavOpen, setMobileNavOpen] = useState(false)
+
+ const trigger = useRef(null)
+ const mobileNav = useRef(null)
+
+ // close the mobile menu on click outside
+ useEffect(() => {
+ const clickHandler = ({ target }) => {
+ if (!mobileNav.current || !trigger.current) return
+ if (!mobileNavOpen || mobileNav.current.contains(target) || trigger.current.contains(target)) return
+ setMobileNavOpen(false)
+ }
+ document.addEventListener('click', clickHandler)
+ return () => document.removeEventListener('click', clickHandler)
+ })
+
+ // close the mobile menu if the esc key is pressed
+ useEffect(() => {
+ const keyHandler = ({ keyCode }) => {
+ if (!mobileNavOpen || keyCode !== 27) return
+ setMobileNavOpen(false)
+ }
+ document.addEventListener('keydown', keyHandler)
+ return () => document.removeEventListener('keydown', keyHandler)
+ })
+
+ return (
+
+ {/* Hamburger button */}
+
+
+ {/* Mobile navigation */}
+
+
+ )
+}
diff --git a/themes/landing/components/ModalVideo.js b/themes/landing/components/ModalVideo.js
new file mode 100644
index 00000000..82cb7352
--- /dev/null
+++ b/themes/landing/components/ModalVideo.js
@@ -0,0 +1,121 @@
+'use client'
+
+import { useState, useRef, Fragment } from 'react'
+import { Dialog, Transition } from '@headlessui/react'
+import CONFIG from '../config'
+import LazyImage from '@/components/LazyImage'
+
+export default function ModalVideo({
+ thumb,
+ thumbWidth,
+ thumbHeight,
+ thumbAlt,
+ video,
+ videoWidth,
+ videoHeight
+}) {
+ const [modalOpen, setModalOpen] = useState(false)
+ const videoRef = useRef(null)
+
+ return (
+
+
+ {/* Video thumbnail */}
+
+
+
+
+
+
+
+
+
+ {/* End: Video thumbnail */}
+
+
videoRef.current?.play()}>
+
+
+
+
+ )
+}
diff --git a/themes/landing/components/Newsletter.js b/themes/landing/components/Newsletter.js
new file mode 100644
index 00000000..18ad893a
--- /dev/null
+++ b/themes/landing/components/Newsletter.js
@@ -0,0 +1,94 @@
+import { subscribeToNewsletter } from '@/lib/mailchimp'
+import { useEffect, useRef, useState } from 'react'
+import CONFIG from '../config'
+
+export default function Newsletter() {
+ const formRef = useRef()
+ const [success, setSuccess] = useState(false)
+ useEffect(() => {
+ const form = formRef.current
+ const handleSubmit = (e) => {
+ e.preventDefault()
+ const email = document.querySelector('#newsletter').value
+ subscribeToNewsletter(email).then(response => {
+ console.log('Subscription succeeded:', response)
+ // 在此处添加成功订阅后的操作
+ setSuccess(true)
+ })
+ .catch(error => {
+ console.error('Subscription failed:', error)
+ // 在此处添加订阅失败后的操作
+ })
+ }
+ form?.addEventListener('submit', handleSubmit)
+ return () => {
+ form?.removeEventListener('submit', handleSubmit)
+ }
+ }, [subscribeToNewsletter])
+
+ if (!JSON.parse(CONFIG.NEWSLETTER)) {
+ return <>>
+ }
+
+ return (
+
+
+
+
+ {/* CTA box */}
+
+
+ {/* Background illustration */}
+
+
+
+
+
+
+ {/* CTA content */}
+
+
需要更多的教程和帮助?
+
请留下您的电子邮件,我会第一时间与您取得联系
+
+ {/* CTA form */}
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/themes/landing/components/Testimonials.js b/themes/landing/components/Testimonials.js
new file mode 100644
index 00000000..377040e3
--- /dev/null
+++ b/themes/landing/components/Testimonials.js
@@ -0,0 +1,114 @@
+// import Image from 'next/image'
+// import TestimonialImage from '@/public/images/testimonial.jpg'
+
+import LazyImage from '@/components/LazyImage'
+import CONFIG from '../config'
+
+export default function Testimonials() {
+ return (
+
+
+ {/* Illustration behind content */}
+
+
+
+
+
+
+
+ {/* Section header */}
+
+
{CONFIG.TESTIMONIALS_HEADER}
+
{CONFIG.TESTIMONIALS_P}
+
+
+ {/* Testimonials */}
+
+
+
+ {/* Testimonial */}
+
+
+
+ {CONFIG.TESTIMONIALS_WORD}
+
+
{CONFIG.TESTIMONIALS_NICKNAME}
+
+
+
+
+
+
+
+
+
+ )
+}
+
+/**
+ * 各种品牌标
+ * @returns
+ */
+// eslint-disable-next-line no-unused-vars
+const brands = () => {
+ return <>
+ {/* Items */}
+
+
+ {/* Item */}
+
+
+ {/* Item */}
+
+
+ {/* Item */}
+
+
+ {/* Item */}
+
+
+ {/* Item */}
+
+
+
+ >
+}
diff --git a/themes/landing/config.js b/themes/landing/config.js
new file mode 100644
index 00000000..b3cb3a6a
--- /dev/null
+++ b/themes/landing/config.js
@@ -0,0 +1,62 @@
+const CONFIG = {
+
+ HEADER_BUTTON_1_TITLE: 'Github开源',
+ HEDEAR_BUTTON_1_URL: 'https://github.com/tangly1024/NotionNext',
+
+ HEADER_BUTTON_2_TITLE: '作者博客',
+ HEDEAR_BUTTON_2_URL: 'https://blog.tangly1024.com/',
+
+ // 首页大图英雄板块
+ HERO_TITLE_1: 'NotionNext',
+ HERO_P_1: '快速搭建独立站、轻松放大品牌价值!',
+ HERO_BUTTON_1_TEXT: '开始体验',
+ HERO_BUTTON_1_LINK: 'https://docs.tangly1024.com/article/vercel-deploy-notion-next',
+ HERO_BUTTON_2_TEXT: '了解更多',
+ HERO_BUTTON_2_LINK: 'https://docs.tangly1024.com/about',
+ HERO_VIDEO_IMAGE: '/images/home.png',
+ // HERO_VIDEO_URL: '/videos/video.mp4',
+ HERO_VIDEO_IFRAME: '//player.bilibili.com/player.html?aid=913088616&bvid=BV1fM4y1L7Qi&cid=1187218697&page=1&&high_quality=1',
+ HERO_VIDEO_TIPS: 'Watch the full video (2 min)',
+
+ // 特性介绍
+ FEATURES_HEADER_1: '探索的过程',
+ FEATURES_HEADER_1_P: "如何搭建自己的门户网站,塑造一个品牌展示中心?
曾经,它是系统
繁重的Wordpress、是操作
复杂的Hexo、是
昂贵且
不稳定的技术团队;
现在,只要一个Notion笔记就够了",
+ FEATURES_HEADER_2: 'Notion+NextJs组合方案',
+ FEATURES_HEADER_2_P: 'Notion作为CMS管理您的站点配置和网页数据,NextJs作为渲染博客的脚本,借助第三方的Vercel等托管平台提供网络服务。',
+ FEATURES_CARD_1_TITLE: '简单快速的系统',
+ FEATURES_CARD_1_P: '在Notion中写下一篇文章,内容立刻在您的网站首页中呈现给互联网',
+ FEATURES_CARD_2_TITLE: '高效传播的媒介',
+ FEATURES_CARD_2_P: '优秀的SEO、快速的响应速度,让您的产品和宣传触达到更多的受众',
+ FEATURES_CARD_3_TITLE: '人性化的定制工具',
+ FEATURES_CARD_3_P: '多款主题供您挑选,可以搭建各种不同风格和作用的网站,更多的主题正在陆续加入中。',
+
+ // 特性介绍2
+ FEATURES_BLOCK_HEADER: '解决方案',
+ FEATURES_BLOCK_P: '人人自媒体的时代,一个网站将帮您链接更多的人,带给你无限的机会和客户。
您还在等什么呢?',
+ FEATURES_BLOCK_1_TITLE: '用网站来展示品牌',
+ FEATURES_BLOCK_1_P: '比起线下渠道、一个公开域名和网站更有说服力',
+ FEATURES_BLOCK_2_TITLE: 'SEO带来更多流量',
+ FEATURES_BLOCK_2_P: '借助搜索引擎,精准定位您的受众',
+ FEATURES_BLOCK_3_TITLE: '网站的性能很重要',
+ FEATURES_BLOCK_3_P: '更快的响应,更好的用户体验',
+ FEATURES_BLOCK_4_TITLE: '打造您的个人品牌',
+ FEATURES_BLOCK_4_P: '继马斯克、乔布斯之后,您将是下一个传奇',
+ FEATURES_BLOCK_5_TITLE: '写作表达是核心技能',
+ FEATURES_BLOCK_5_P: '比起只阅读输入,更重要的是反思和输出',
+ FEATURES_BLOCK_6_TITLE: '开始写博客吧',
+ FEATURES_BLOCK_6_P: 'NotionNext,助您轻松开始写作',
+
+ // 感言
+ TESTIMONIALS_HEADER: '已搭建近4000个网站、浏览量突破 100,000,000+',
+ TESTIMONIALS_P: '网站内容涵盖地产、教育、建筑、医学、机械、IT、电子、软件、自媒体、数位游民、短视频、电商、学生、摄影爱好者、旅行爱好者等等各行各业',
+
+ TESTIMONIALS_AVATAR: 'https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F22de3fcb-d90d-4271-bc01-f815f476122b%2F4FE0A0C0-E487-4C74-BF8E-6F01A27461B8-14186-000008094BC289A6.jpg?table=collection&id=a320a2cc-6ebe-4a8d-95cc-ea94e63bced9&width=200',
+ TESTIMONIALS_NICKNAME: 'Ryan_G',
+ TESTIMONIALS_ID: 'Ryan`Log 站长',
+ TESTIMONIALS_SOCIAL_NAME: '@Gaoran',
+ TESTIMONIALS_SOCIAL_URL: 'https://blog.gaoran.xyz/',
+ TESTIMONIALS_WORD: '“ 感谢大佬的方法。之前尝试过Super、Potion等国外的第三方平台,实现效果一般,个性化程度远不如这个方法,已经用起来了! “',
+
+ NEWSLETTER: process.env.NEXT_PUBLIC_THEME_LANDING_NEWSLETTER || false // 是否开启邮件订阅 请先配置mailchimp功能 https://docs.tangly1024.com/article/notion-next-mailchimp
+}
+export default CONFIG
diff --git a/themes/landing/index.js b/themes/landing/index.js
new file mode 100644
index 00000000..f668f8fd
--- /dev/null
+++ b/themes/landing/index.js
@@ -0,0 +1,99 @@
+
+/**
+ * 这是一个空白主题,方便您用作创建新主题时的模板,从而开发出您自己喜欢的主题
+ * 1. 禁用了代码质量检查功能,提高了代码的宽容度;您可以使用标准的html写法
+ * 2. 内容大部分是在此文件中写死,notion数据从props参数中传进来
+ * 3. 您可在此网站找到更多喜欢的组件 https://www.tailwind-kit.com/
+ */
+/* eslint-disable*/
+import NotionPage from '@/components/NotionPage'
+import Header from './components/Header'
+import Footer from './components/Footer'
+import Hero from './components/Hero'
+import Features from './components/Features'
+import FeaturesBlocks from './components/FeaturesBlocks'
+import Testimonials from './components/Testimonials'
+import Newsletter from './components/Newsletter'
+import CommonHead from '@/components/CommonHead'
+
+/**
+ * 这是个配置文件,可以方便在此统一配置信息
+ */
+const THEME_CONFIG = { THEME: 'landing' }
+
+/**
+ * 布局框架
+ * 作为一个基础框架使用,定义了整个主题每个页面必备的顶部导航栏和页脚
+ * 其它页面都嵌入到此框架中使用
+ * @param {*} props
+ * @returns
+ */
+const LayoutBase = (props) => {
+ const { meta, siteInfo, children } = props
+
+ return
+
+ {/* 网页SEO */}
+
+
+ {/* 顶部导航栏 */}
+
+
+ {/* 内容 */}
+
+ {children}
+
+
+ {/* 底部页脚 */}
+
+
+}
+
+
+/**
+ * 首页布局
+ * @param {*} props
+ * @returns
+ */
+const LayoutIndex = (props) => {
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+/**
+ * 文章详情页布局
+ * @param {*} props
+ * @returns
+ */
+const LayoutSlug = (props) =>
+
+
+
+
+
+// 其他布局暂时留空
+const LayoutSearch = (props) =>
+const LayoutArchive = (props) =>
+const Layout404 = (props) =>
+const LayoutCategoryIndex = (props) =>
+const LayoutPostList = (props) =>
+const LayoutTagIndex = (props) =>
+
+export {
+ THEME_CONFIG,
+ LayoutIndex,
+ LayoutSearch,
+ LayoutArchive,
+ LayoutSlug,
+ Layout404,
+ LayoutPostList,
+ LayoutCategoryIndex,
+ LayoutTagIndex
+}
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/Layout404.js b/themes/matery/Layout404.js
deleted file mode 100644
index 609b6e7c..00000000
--- a/themes/matery/Layout404.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import LayoutBase from './LayoutBase'
-import { useRouter } from 'next/router'
-import { useEffect } from 'react'
-
-export const Layout404 = props => {
- const router = useRouter()
- useEffect(() => {
- // 延时3秒如果加载失败就返回首页
- setTimeout(() => {
- const article = typeof document !== 'undefined' && document.getElementById('container')
- if (!article) {
- router.push('/').then(() => {
- // console.log('找不到页面', router.asPath)
- })
- }
- }, 3000)
- })
- return (
-
-
-
- )
-}
diff --git a/themes/matery/LayoutArchive.js b/themes/matery/LayoutArchive.js
deleted file mode 100644
index 49accca0..00000000
--- a/themes/matery/LayoutArchive.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { useEffect } from 'react'
-import BlogPostArchive from './components/BlogPostArchive'
-import Card from './components/Card'
-import HeaderArticle from './components/HeaderArticle'
-import LayoutBase from './LayoutBase'
-
-export const LayoutArchive = (props) => {
- const { archivePosts } = props
-
- useEffect(() => {
- const anchor = window.location.hash
- if (anchor) {
- setTimeout(() => {
- const anchorElement = document.getElementById(anchor.substring(1))
- if (anchorElement) {
- anchorElement.scrollIntoView({ block: 'start', behavior: 'smooth' })
- }
- }, 300)
- }
- }, [])
- return
} >
-
-
- {Object.keys(archivePosts).map(archiveTitle => (
-
- ))}
-
-
-
-}
diff --git a/themes/matery/LayoutBase.js b/themes/matery/LayoutBase.js
deleted file mode 100644
index fb5df18e..00000000
--- a/themes/matery/LayoutBase.js
+++ /dev/null
@@ -1,80 +0,0 @@
-import CommonHead from '@/components/CommonHead'
-import { useCallback, useEffect, useState } from 'react'
-
-import Footer from './components/Footer'
-import JumpToTopButton from './components/JumpToTopButton'
-import TopNav from './components/TopNav'
-import Live2D from '@/components/Live2D'
-import LoadingCover from './components/LoadingCover'
-import { useGlobal } from '@/lib/global'
-import BLOG from '@/blog.config'
-import FloatDarkModeButton from './components/FloatDarkModeButton'
-import throttle from 'lodash.throttle'
-import { isBrowser, loadExternalResource } from '@/lib/utils'
-
-/**
- * 基础布局 采用左右两侧布局,移动端使用顶部导航栏
- * @param props
- * @returns {JSX.Element}
- * @constructor
- */
-const LayoutBase = props => {
- const { children, headerSlot, meta, siteInfo } = props
- const [show, switchShow] = useState(false)
- const { onLoading } = useGlobal()
-
- const throttleMs = 200
- const scrollListener = useCallback(throttle(() => {
- const scrollY = window.pageYOffset
- const shouldShow = scrollY > 300
- if (shouldShow !== show) {
- switchShow(shouldShow)
- }
- }, throttleMs))
-
- useEffect(() => {
- document.addEventListener('scroll', scrollListener)
- return () => document.removeEventListener('scroll', scrollListener)
- }, [])
-
- if (isBrowser()) {
- loadExternalResource('/css/theme-matery.css', 'css')
- }
-
- return (
-
-
-
-
-
-
- {headerSlot}
-
-
-
- {onLoading ? : children}
-
-
-
- {/* 左下角悬浮 */}
-
-
-
-
-
-
-
-
- {/* 右下角悬浮 */}
-
-
-
-
- )
-}
-
-export default LayoutBase
diff --git a/themes/matery/LayoutCategory.js b/themes/matery/LayoutCategory.js
deleted file mode 100644
index 6e9524ab..00000000
--- a/themes/matery/LayoutCategory.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import BlogPostListScroll from './components/BlogPostListScroll'
-import BlogPostListPage from './components/BlogPostListPage'
-import LayoutBase from './LayoutBase'
-import BLOG from '@/blog.config'
-import Link from 'next/link'
-import HeaderArticle from './components/HeaderArticle'
-
-export const LayoutCategory = props => {
- const { category, categoryOptions } = props
- return (
-
} >
-
-
-
-
-
-
- {categoryOptions.map(e => {
- const selected = e.name === category
- return (
-
-
-
- {e.name}({e.count})
-
-
- )
- })}
-
-
-
- {BLOG.POST_LIST_STYLE === 'page' ?
:
}
-
-
-
-
- )
-}
diff --git a/themes/matery/LayoutCategoryIndex.js b/themes/matery/LayoutCategoryIndex.js
deleted file mode 100644
index b6eda2bb..00000000
--- a/themes/matery/LayoutCategoryIndex.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import Link from 'next/link'
-import HeaderArticle from './components/HeaderArticle'
-import LayoutBase from './LayoutBase'
-
-export const LayoutCategoryIndex = props => {
- const { categoryOptions } = props
-
- return (
-
} >
-
-
-
-
-
-
- {categoryOptions.map(e => {
- return (
-
-
-
- {e.name}({e.count})
-
-
- )
- })}
-
-
-
-
-
- )
-}
diff --git a/themes/matery/LayoutIndex.js b/themes/matery/LayoutIndex.js
deleted file mode 100644
index d5488ebb..00000000
--- a/themes/matery/LayoutIndex.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import BLOG from '@/blog.config'
-import BlogPostListPage from './components/BlogPostListPage'
-import BlogPostListScroll from './components/BlogPostListScroll'
-import Header from './components/Header'
-import CONFIG_MATERY from './config_matery'
-import LayoutBase from './LayoutBase'
-import React from 'react'
-
-export const LayoutIndex = (props) => {
- return
}>
- {BLOG.POST_LIST_STYLE === 'page' ?
:
}
-
-}
diff --git a/themes/matery/LayoutPage.js b/themes/matery/LayoutPage.js
deleted file mode 100644
index b0841c1c..00000000
--- a/themes/matery/LayoutPage.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import BlogPostListPage from './components/BlogPostListPage'
-import LayoutBase from './LayoutBase'
-
-export const LayoutPage = (props) => {
- return
-
-
-}
diff --git a/themes/matery/LayoutSearch.js b/themes/matery/LayoutSearch.js
deleted file mode 100644
index 94ccbb96..00000000
--- a/themes/matery/LayoutSearch.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import { useRouter } from 'next/router'
-import { useEffect, useRef } from 'react'
-import BLOG from '@/blog.config'
-import BlogPostListScroll from './components/BlogPostListScroll'
-import BlogPostListPage from './components/BlogPostListPage'
-import LayoutBase from './LayoutBase'
-import SearchInput from './components/SearchInput'
-import { useGlobal } from '@/lib/global'
-import Mark from 'mark.js'
-import TagItemMini from './components/TagItemMini'
-import Card from './components/Card'
-import Link from 'next/link'
-
-export const LayoutSearch = props => {
- const { keyword, tagOptions, categoryOptions } = props
- const { locale } = useGlobal()
- const router = useRouter()
- const currentSearch = keyword || router?.query?.s
- const cRef = useRef(null)
-
- useEffect(() => {
- setTimeout(() => {
- // 自动聚焦到搜索框
- cRef?.current?.focus()
- 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 && <>
-
-
- {/* 分类 */}
-
-
-
- {locale.COMMON.CATEGORY}:
-
-
- {categoryOptions?.map(category => {
- return (
-
-
-
- {category.name}({category.count})
-
-
- )
- })}
-
-
- {/* 标签 */}
-
-
-
- {locale.COMMON.TAGS}:
-
-
-
-
- >}
-
- {currentSearch && <>
-
- {BLOG.POST_LIST_STYLE === 'page' ? : }
-
- >}
-
-
- )
-}
diff --git a/themes/matery/LayoutSlug.js b/themes/matery/LayoutSlug.js
deleted file mode 100644
index da07db06..00000000
--- a/themes/matery/LayoutSlug.js
+++ /dev/null
@@ -1,131 +0,0 @@
-import React, { useCallback, useEffect } from 'react'
-import { ArticleLock } from './components/ArticleLock'
-import HeaderArticle from './components/HeaderArticle'
-import LayoutBase from './LayoutBase'
-import Comment from '@/components/Comment'
-import NotionPage from '@/components/NotionPage'
-import ArticleAdjacent from './components/ArticleAdjacent'
-import ArticleCopyright from './components/ArticleCopyright'
-import { ArticleInfo } from './components/ArticleInfo'
-import Catalog from './components/Catalog'
-import JumpToCommentButton from './components/JumpToCommentButton'
-import throttle from 'lodash.throttle'
-import ShareBar from '@/components/ShareBar'
-
-export const LayoutSlug = props => {
- const { post, lock, validPassword } = props
-
- const [show, switchShow] = React.useState(false)
- const throttleMs = 200
-
- const scrollListener = useCallback(throttle(() => {
- const scrollY = window.pageYOffset
- const shouldShow = scrollY > 220 && post?.toc?.length > 0
- if (shouldShow !== show) {
- switchShow(shouldShow)
- }
- }, throttleMs))
- useEffect(() => {
- document.addEventListener('scroll', scrollListener)
- return () => document.removeEventListener('scroll', scrollListener)
- }, [])
-
- if (!post) {
- return
}
- {...props}
- showCategory={false}
- showTag={false}
- >
- }
-
- return (
}
- {...props}
- showCategory={false}
- showTag={false}
- >
-
-
-
-
- {lock &&
}
-
- {!lock &&
- {post?.type && post?.type === 'Post' && <>
-
-
- >}
-
-
-
- {/* Notion文章主体 */}
-
-
-
- {/* 分享 */}
-
- {/* 文章版权说明 */}
- {post.type === 'Post' && }
-
-
-
-
-
- {/* 评论互动 */}
-
-
-
-
-
-
}
-
-
- {/* 文章推荐 */}
- {post.type === 'Post' &&
}
-
- {/* 文章目录 */}
- {post?.toc?.length > 0 &&
}
-
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/themes/matery/LayoutTag.js b/themes/matery/LayoutTag.js
deleted file mode 100644
index 3572d019..00000000
--- a/themes/matery/LayoutTag.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import BLOG from '@/blog.config'
-import BlogPostListScroll from './components/BlogPostListScroll'
-import BlogPostListPage from './components/BlogPostListPage'
-import LayoutBase from './LayoutBase'
-import React from 'react'
-import HeaderArticle from './components/HeaderArticle'
-import { useGlobal } from '@/lib/global'
-import TagItemMiddle from './components/TagItemMiddle'
-
-export const LayoutTag = (props) => {
- const { tagOptions, tag } = props
-
- const { locale } = useGlobal()
-
- return
} >
-
-
-
-
-
-
- {locale.COMMON.TAGS}
-
-
-
-
-
- {BLOG.POST_LIST_STYLE === 'page' ?
:
}
-
-
-
-
-}
diff --git a/themes/matery/LayoutTagIndex.js b/themes/matery/LayoutTagIndex.js
deleted file mode 100644
index bfcb41b1..00000000
--- a/themes/matery/LayoutTagIndex.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import { useGlobal } from '@/lib/global'
-import HeaderArticle from './components/HeaderArticle'
-import TagItemMiddle from './components/TagItemMiddle'
-import LayoutBase from './LayoutBase'
-
-export const LayoutTagIndex = props => {
- const { tagOptions } = props
- const { locale } = useGlobal()
- return (
-
} >
-
-
-
-
-
- {locale.COMMON.TAGS}
-
-
-
-
-
-
- )
-}
diff --git a/themes/matery/components/Announcement.js b/themes/matery/components/Announcement.js
new file mode 100644
index 00000000..9ec52443
--- /dev/null
+++ b/themes/matery/components/Announcement.js
@@ -0,0 +1,29 @@
+import { useGlobal } from '@/lib/global'
+import dynamic from 'next/dynamic'
+
+const NotionPage = dynamic(() => import('@/components/NotionPage'))
+
+const Announcement = ({ notice }) => {
+ const { locale } = useGlobal()
+ if (!notice || Object.keys(notice).length === 0) {
+ return <>>
+ }
+ return
+
+
+
+ {locale.COMMON.ANNOUNCEMENT}
+
+
+ {notice && (
+
+
)}
+
+
+}
+export default Announcement
diff --git a/themes/matery/components/ArticleAdjacent.js b/themes/matery/components/ArticleAdjacent.js
index 365f0884..c9991c8c 100644
--- a/themes/matery/components/ArticleAdjacent.js
+++ b/themes/matery/components/ArticleAdjacent.js
@@ -1,4 +1,4 @@
-import CONFIG_MATERY from '../config_matery'
+import CONFIG from '../config'
import BlogPostCard from './BlogPostCard'
/**
@@ -7,7 +7,7 @@ import BlogPostCard from './BlogPostCard'
* @returns
*/
export default function ArticleAdjacent ({ prev, next, siteInfo }) {
- if (!prev || !next || !CONFIG_MATERY.ARTICLE_ADJACENT) {
+ if (!prev || !next || !CONFIG.ARTICLE_ADJACENT) {
return <>>
}
return
diff --git a/themes/matery/components/ArticleCopyright.js b/themes/matery/components/ArticleCopyright.js
index 4bd32b7e..d8d9beb9 100644
--- a/themes/matery/components/ArticleCopyright.js
+++ b/themes/matery/components/ArticleCopyright.js
@@ -3,7 +3,7 @@ import { useGlobal } from '@/lib/global'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
-import CONFIG_MATERY from '../config_matery'
+import CONFIG from '../config'
export default function ArticleCopyright () {
const router = useRouter()
@@ -14,7 +14,7 @@ export default function ArticleCopyright () {
const { locale } = useGlobal()
- if (!CONFIG_MATERY.ARTICLE_COPYRIGHT) {
+ if (!CONFIG.ARTICLE_COPYRIGHT) {
return <>>
}
diff --git a/themes/matery/components/ArticleInfo.js b/themes/matery/components/ArticleInfo.js
index da62ac81..293eb3e7 100644
--- a/themes/matery/components/ArticleInfo.js
+++ b/themes/matery/components/ArticleInfo.js
@@ -1,14 +1,13 @@
import Link from 'next/link'
import { useGlobal } from '@/lib/global'
-import formatDate from '@/lib/formatDate'
import TagItemMiddle from './TagItemMiddle'
import WordCount from './WordCount'
+import { formatDateFmt } from '@/lib/formatDate'
export const ArticleInfo = (props) => {
const { post } = props
const { locale } = useGlobal()
- const date = formatDate(post?.date?.start_date || post?.createdTime, locale.LOCALE)
return (
@@ -25,15 +24,15 @@ export const ArticleInfo = (props) => {
{post?.type !== 'Page' && (<>
-
发布日期:{date}
+
{locale.COMMON.POST_TIME}: {post?.publishTime}
- 更新日期: {post.lastEditedTime}
+ {locale.COMMON.LAST_EDITED_TIME}: {post.lastEditedTime}
diff --git a/themes/matery/components/ArticleLock.js b/themes/matery/components/ArticleLock.js
index 0a56f39e..ad63d0de 100644
--- a/themes/matery/components/ArticleLock.js
+++ b/themes/matery/components/ArticleLock.js
@@ -1,4 +1,5 @@
import { useGlobal } from '@/lib/global'
+import { useEffect, useRef } from 'react'
/**
* 加密文章校验组件
@@ -21,17 +22,31 @@ export const ArticleLock = props => {
}
}
- return
-
-
{locale.COMMON.ARTICLE_LOCK_TIPS}
-
-
-
-
{locale.COMMON.SUBMIT}
+ 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/matery/components/ArticleRecommend.js b/themes/matery/components/ArticleRecommend.js
index 67620599..8fc53a84 100644
--- a/themes/matery/components/ArticleRecommend.js
+++ b/themes/matery/components/ArticleRecommend.js
@@ -1,7 +1,8 @@
import Link from 'next/link'
-import CONFIG_MATERY from '../config_matery'
+import CONFIG from '../config'
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
+import LazyImage from '@/components/LazyImage'
/**
* 关联推荐文章
@@ -12,54 +13,54 @@ export default function ArticleRecommend({ recommendPosts, siteInfo }) {
const { locale } = useGlobal()
if (
- !CONFIG_MATERY.ARTICLE_RECOMMEND ||
- !recommendPosts ||
- recommendPosts.length === 0
+ !CONFIG.ARTICLE_RECOMMEND ||
+ !recommendPosts ||
+ recommendPosts.length === 0
) {
return <>>
}
return (
-
-
-
-
- {locale.COMMON.RELATE_POSTS}
-
-
-
- {recommendPosts.map(post => {
- const headerImage = post?.page_cover
- ? `url("${post.page_cover}")`
- : `url("${siteInfo?.pageCover}")`
-
- return (
- (
-
-
-
-
-
-
- {post.date?.start_date}
-
-
{post.title}
-
+
+
+
+
+ {locale.COMMON.RELATE_POSTS}
-
+
+
+ {recommendPosts.map(post => {
+ const headerImage = post?.pageCoverThumbnail
+ ? post.pageCoverThumbnail
+ : siteInfo?.pageCover
- )
- )
- })}
-
-
+ return (
+ (
+
+
+
+
+
+
+
+
+
+ {post.date?.start_date}
+
+
{post.title}
+
+
+
+
+ )
+ )
+ })}
+
+
)
}
diff --git a/themes/matery/components/BlogListBar.js b/themes/matery/components/BlogListBar.js
new file mode 100644
index 00000000..8453e7ea
--- /dev/null
+++ b/themes/matery/components/BlogListBar.js
@@ -0,0 +1,47 @@
+import Link from 'next/link'
+import { useGlobal } from '@/lib/global'
+import TagItemMiddle from './TagItemMiddle'
+
+export default function BlogListBar(props) {
+ const { category, categoryOptions, tag, tagOptions } = props
+ const { locale } = useGlobal()
+
+ if (category) {
+ return (
+
+
+ {categoryOptions?.map(e => {
+ const selected = e.name === category
+ return (
+
+
+
+ {e.name}({e.count})
+
+
+ )
+ })}
+
+
+
+ )
+ } else if (tag) {
+ return
+
+ {locale.COMMON.TAGS}
+
+
+
+ } else {
+ return <>>
+ }
+}
diff --git a/themes/matery/components/BlogPostArchive.js b/themes/matery/components/BlogPostArchive.js
index 226f77eb..08feff4c 100644
--- a/themes/matery/components/BlogPostArchive.js
+++ b/themes/matery/components/BlogPostArchive.js
@@ -21,12 +21,12 @@ const BlogPostArchive = ({ posts = [], archiveTitle }) => {
{archiveTitle}
- {posts.map(post => (
+ {posts?.map(post => (
-
-
+
{post.date?.start_date}{' '}
{
- const showPreview = CONFIG_MATERY.POST_LIST_PREVIEW && post.blockMap
+ const showPreview = CONFIG.POST_LIST_PREVIEW && post.blockMap
// matery 主题默认强制显示图片
- if (post && !post.page_cover) {
- post.page_cover = siteInfo?.pageCover
+ if (post && !post.pageCoverThumbnail) {
+ post.pageCoverThumbnail = siteInfo?.pageCover
}
- const showPageCover = CONFIG_MATERY.POST_LIST_COVER && post?.page_cover
+ const showPageCover = CONFIG.POST_LIST_COVER && post?.pageCoverThumbnail
const delay = (index % 3) * 300
return (
-
+
- {/* 固定高度 ,空白用图片拉升填充 */}
-
+ {/* 固定高度 ,空白用图片拉升填充 */}
+
- {/* 头部图片 填充卡片 */}
- {showPageCover && (
-
-
- {/* eslint-disable-next-line @next/next/no-img-element */}
-

-
{post.title}
-
-
- )}
+ {/* 头部图片 填充卡片 */}
+ {showPageCover && (
+
+
+
+ )}
- {/* 文字描述 */}
-
- {/* 描述 */}
-
+ {/* 文字描述 */}
+
+ {/* 描述 */}
+
- {(!showPreview || showSummary) && post.summary && (
-
- {post.summary}
-
- )}
+ {(!showPreview || showSummary) && post.summary && (
+
+ {post.summary}
+
+ )}
-
-
+
+
+
-
- {post.date?.start_date || post.lastEditedTime}
+
+ {post.date?.start_date || post.lastEditedTime}
-
-
+
+
+
+
-
- {post.category}
+
+ {post.category}
-
-
-
+
+
+
- {post?.tagItems && post?.tagItems.length > 0 && (<>
-
-
-
-
- {' '}
- {post.tagItems.map(tag => (
-
- ))}
-
-
-
- >)}
-
-
+ {post?.tagItems && post?.tagItems.length > 0 && (<>
+
+
+
+
+ {' '}
+ {post.tagItems.map(tag => (
+
+ ))}
+
+
+
+ >)}
+
+
-
+
)
}
diff --git a/themes/matery/components/BlogPostListPage.js b/themes/matery/components/BlogPostListPage.js
index 82028115..7d5226dc 100644
--- a/themes/matery/components/BlogPostListPage.js
+++ b/themes/matery/components/BlogPostListPage.js
@@ -18,11 +18,11 @@ const BlogPostListPage = ({ page = 1, posts = [], postCount, siteInfo }) => {
return
} else {
return (
-
+
{/* 文章列表 */}
-
- {posts.map(post => (
+
+ {posts?.map(post => (
))}
diff --git a/themes/matery/components/BlogPostListScroll.js b/themes/matery/components/BlogPostListScroll.js
index e0c9afd5..6d0f4743 100644
--- a/themes/matery/components/BlogPostListScroll.js
+++ b/themes/matery/components/BlogPostListScroll.js
@@ -3,7 +3,7 @@ import BlogPostCard from './BlogPostCard'
import BlogPostListEmpty from './BlogPostListEmpty'
import { useGlobal } from '@/lib/global'
import React, { useCallback } from 'react'
-import CONFIG_MATERY from '../config_matery'
+import CONFIG from '../config'
import { getListByPage } from '@/lib/utils'
import throttle from 'lodash.throttle'
@@ -14,7 +14,7 @@ import throttle from 'lodash.throttle'
* @returns {JSX.Element}
* @constructor
*/
-const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = CONFIG_MATERY.POST_LIST_SUMMARY, siteInfo }) => {
+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)
@@ -57,7 +57,7 @@ const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = CONFIG_MA
return
{/* 文章列表 */}
-
+
{postsToShow.map(post => (
diff --git a/themes/matery/components/Catalog.js b/themes/matery/components/Catalog.js
index 56bff03e..45817545 100644
--- a/themes/matery/components/Catalog.js
+++ b/themes/matery/components/Catalog.js
@@ -1,4 +1,4 @@
-import React, { useRef } from 'react'
+import { useCallback, useEffect, useRef, useState } from 'react'
import throttle from 'lodash.throttle'
import { uuidToId } from 'notion-utils'
import Progress from './Progress'
@@ -13,7 +13,7 @@ import { useGlobal } from '@/lib/global'
const Catalog = ({ toc }) => {
const { locale } = useGlobal()
// 监听滚动事件
- React.useEffect(() => {
+ useEffect(() => {
window.addEventListener('scroll', actionSectionScrollSpy)
actionSectionScrollSpy()
return () => {
@@ -26,9 +26,9 @@ const Catalog = ({ toc }) => {
const tocIds = []
// 同步选中目录事件
- const [activeSection, setActiveSection] = React.useState(null)
+ const [activeSection, setActiveSection] = useState(null)
const throttleMs = 200
- const actionSectionScrollSpy = React.useCallback(throttle(() => {
+ const actionSectionScrollSpy = useCallback(throttle(() => {
const sections = document.getElementsByClassName('notion-h')
let prevBBox = null
let currentSectionId = activeSection
diff --git a/themes/matery/components/CatalogWrapper.js b/themes/matery/components/CatalogWrapper.js
new file mode 100644
index 00000000..7d64e2ad
--- /dev/null
+++ b/themes/matery/components/CatalogWrapper.js
@@ -0,0 +1,27 @@
+import Catalog from './Catalog'
+
+/**
+ * 目录
+ * @param {*} param0
+ * @returns
+ */
+export default function CatalogWrapper({ post }) {
+ if (post?.toc?.length > 0) {
+ return
+ } else {
+ return <>>
+ }
+}
diff --git a/themes/matery/components/FloatDarkModeButton.js b/themes/matery/components/FloatDarkModeButton.js
index a8821123..f912c57b 100644
--- a/themes/matery/components/FloatDarkModeButton.js
+++ b/themes/matery/components/FloatDarkModeButton.js
@@ -1,11 +1,11 @@
import { useGlobal } from '@/lib/global'
-import { saveDarkModeToCookies } from '@/lib/theme'
-import CONFIG_MATERY from '../config_matery'
+import { saveDarkModeToCookies } from '@/themes/theme'
+import CONFIG from '../config'
export default function FloatDarkModeButton() {
const { isDarkMode, updateDarkMode } = useGlobal()
- if (!CONFIG_MATERY.WIDGET_DARK_MODE) {
+ if (!CONFIG.WIDGET_DARK_MODE) {
return <>>
}
@@ -21,9 +21,8 @@ export default function FloatDarkModeButton() {
return (
-
+
)
}
diff --git a/themes/matery/components/Header.js b/themes/matery/components/Header.js
deleted file mode 100644
index 4e84e70c..00000000
--- a/themes/matery/components/Header.js
+++ /dev/null
@@ -1,111 +0,0 @@
-// import Image from 'next/image'
-import { useCallback, useEffect, useState } from 'react'
-import Typed from 'typed.js'
-import CONFIG_MATERY from '../config_matery'
-import throttle from 'lodash.throttle'
-
-let wrapperTop = 0
-let windowTop = 0
-let autoScroll = false
-const enableAutoScroll = false // 是否开启自动吸附滚动
-const throttleMs = 200
-
-/**
- *
- * @returns 头图
- */
-const Header = props => {
- const [typed, changeType] = useState()
- const { siteInfo } = props
- useEffect(() => {
- scrollTrigger()
- updateHeaderHeight()
- if (!typed && window && document.getElementById('typed')) {
- changeType(
- new Typed('#typed', {
- strings: CONFIG_MATERY.HOME_BANNER_GREETINGS,
- typeSpeed: 200,
- backSpeed: 100,
- backDelay: 400,
- showCursor: true,
- smartBackspace: true
- })
- )
- }
- if (enableAutoScroll) {
- scrollTrigger()
- window.addEventListener('scroll', scrollTrigger)
- }
- window.addEventListener('resize', updateHeaderHeight)
- return () => {
- if (enableAutoScroll) {
- window.removeEventListener('scroll', scrollTrigger)
- } window.removeEventListener('resize', updateHeaderHeight)
- }
- }, [])
-
- const autoScrollEnd = () => {
- if (autoScroll) {
- windowTop = window.scrollY
- autoScroll = false
- }
- }
-
- /**
- * 吸附滚动,移动端关闭
- * @returns
- */
- const scrollTrigger = useCallback(throttle(() => {
- if (screen.width <= 768) {
- return
- }
-
- const scrollS = window.scrollY
-
- // 自动滚动
- if ((scrollS > windowTop) & (scrollS < window.innerHeight) && !autoScroll
- ) {
- autoScroll = true
- window.scrollTo({ top: wrapperTop, behavior: 'smooth' })
- setTimeout(autoScrollEnd, 500)
- }
- if ((scrollS < windowTop) && (scrollS < window.innerHeight) && !autoScroll) {
- autoScroll = true
- window.scrollTo({ top: 0, behavior: 'smooth' })
- setTimeout(autoScrollEnd, 500)
- }
- windowTop = scrollS
- }, throttleMs))
-
- function updateHeaderHeight() {
- requestAnimationFrame(() => {
- const wrapperElement = document.getElementById('wrapper')
- wrapperTop = wrapperElement?.offsetTop
- })
- }
-
- return (
-
- )
-}
-
-export default Header
diff --git a/themes/matery/components/HeaderArticle.js b/themes/matery/components/HeaderArticle.js
deleted file mode 100644
index 1f2fe89e..00000000
--- a/themes/matery/components/HeaderArticle.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import Image from 'next/image'
-
-export default function HeaderArticle({ post, siteInfo }) {
- const headerImage = post?.page_cover ? post?.page_cover : siteInfo?.pageCover
- const title = post?.title
- return (
-
- )
-}
diff --git a/themes/matery/components/Hero.js b/themes/matery/components/Hero.js
new file mode 100644
index 00000000..6c94cb1c
--- /dev/null
+++ b/themes/matery/components/Hero.js
@@ -0,0 +1,75 @@
+// import Image from 'next/image'
+import { useEffect, useState } from 'react'
+import Typed from 'typed.js'
+import CONFIG from '../config'
+import { useGlobal } from '@/lib/global'
+import BLOG from '@/blog.config'
+import LazyImage from '@/components/LazyImage'
+
+let wrapperTop = 0
+
+/**
+ *
+ * @returns 头图
+ */
+const Hero = props => {
+ const [typed, changeType] = useState()
+ const { siteInfo } = props
+ const { locale } = useGlobal()
+
+ useEffect(() => {
+ updateHeaderHeight()
+ if (!typed && window && document.getElementById('typed')) {
+ changeType(
+ new Typed('#typed', {
+ strings: BLOG.GREETING_WORDS.split(','),
+ typeSpeed: 200,
+ backSpeed: 100,
+ backDelay: 400,
+ showCursor: true,
+ smartBackspace: true
+ })
+ )
+ }
+
+ window.addEventListener('resize', updateHeaderHeight)
+ return () => {
+ window.removeEventListener('resize', updateHeaderHeight)
+ }
+ }, [])
+
+ function updateHeaderHeight() {
+ requestAnimationFrame(() => {
+ const wrapperElement = document.getElementById('wrapper')
+ wrapperTop = wrapperElement?.offsetTop
+ })
+ }
+
+ return (
+
+ )
+}
+
+export default Hero
diff --git a/themes/matery/components/InfoCard.js b/themes/matery/components/InfoCard.js
index 1393211a..0591d3a2 100644
--- a/themes/matery/components/InfoCard.js
+++ b/themes/matery/components/InfoCard.js
@@ -3,6 +3,7 @@ import { useRouter } from 'next/router'
import Card from './Card'
import SocialButton from './SocialButton'
import MenuGroupCard from './MenuGroupCard'
+import LazyImage from '@/components/LazyImage'
export function InfoCard (props) {
const { className, siteInfo } = props
const router = useRouter()
@@ -13,8 +14,7 @@ export function InfoCard (props) {
router.push('/')
}}
>
- {/* eslint-disable-next-line @next/next/no-img-element */}
-

+
{BLOG.AUTHOR}
{BLOG.BIO}
diff --git a/themes/matery/components/JumpToCommentButton.js b/themes/matery/components/JumpToCommentButton.js
index 1a11e765..98abb03f 100644
--- a/themes/matery/components/JumpToCommentButton.js
+++ b/themes/matery/components/JumpToCommentButton.js
@@ -1,5 +1,5 @@
import React from 'react'
-import CONFIG_MATERY from '../config_matery'
+import CONFIG from '../config'
/**
* 跳转到评论区
@@ -7,7 +7,7 @@ import CONFIG_MATERY from '../config_matery'
* @constructor
*/
const JumpToCommentButton = () => {
- if (!CONFIG_MATERY.WIDGET_TO_COMMENT) {
+ if (!CONFIG.WIDGET_TO_COMMENT) {
return <>>
}
@@ -17,11 +17,10 @@ const JumpToCommentButton = () => {
}
}
- return (
-
-
)
+ return
+
+
}
export default JumpToCommentButton
diff --git a/themes/matery/components/JumpToTopButton.js b/themes/matery/components/JumpToTopButton.js
index 984e76ca..2b1e3018 100644
--- a/themes/matery/components/JumpToTopButton.js
+++ b/themes/matery/components/JumpToTopButton.js
@@ -1,6 +1,6 @@
import { useGlobal } from '@/lib/global'
import React from 'react'
-import CONFIG_MATERY from '../config_matery'
+import CONFIG from '../config'
/**
* 跳转到网页顶部
@@ -13,14 +13,17 @@ import CONFIG_MATERY from '../config_matery'
const JumpToTopButton = ({ showPercent = true, percent }) => {
const { locale } = useGlobal()
- if (!CONFIG_MATERY.WIDGET_TO_TOP) {
+ if (!CONFIG.WIDGET_TO_TOP) {
return <>>
}
- return (
window.scrollTo({ top: 0, behavior: 'smooth' })} >
-
- {showPercent && (
{percent}
)}
-
)
+
+ return
window.scrollTo({ top: 0, behavior: 'smooth' })} >
+
+
}
export default JumpToTopButton
diff --git a/themes/matery/components/LatestPostsGroup.js b/themes/matery/components/LatestPostsGroup.js
deleted file mode 100644
index cfb44018..00000000
--- a/themes/matery/components/LatestPostsGroup.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import BLOG from '@/blog.config'
-import { useGlobal } from '@/lib/global'
-import Link from 'next/link'
-import { useRouter } from 'next/router'
-
-/**
- * 最新文章列表
- * @param posts 所有文章数据
- * @param sliceCount 截取展示的数量 默认6
- * @constructor
- */
-const LatestPostsGroup = ({ 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?.page_cover
- ? `url("${post.page_cover}")`
- : `url("${siteInfo?.pageCover}")`
-
- return (
- (
-
-
-
-
-
{post.title}
-
{post.lastEditedTime}
-
-
-
- )
- )
- })}
- >
-}
-export default LatestPostsGroup
diff --git a/themes/matery/components/MenuGroupCard.js b/themes/matery/components/MenuGroupCard.js
index 9012b936..2c2d2c04 100644
--- a/themes/matery/components/MenuGroupCard.js
+++ b/themes/matery/components/MenuGroupCard.js
@@ -1,7 +1,7 @@
import React from 'react'
import Link from 'next/link'
import { useGlobal } from '@/lib/global'
-import CONFIG_MATERY from '../config_matery'
+import CONFIG from '../config'
const MenuGroupCard = (props) => {
const { postCount, categories, tags } = props
@@ -11,9 +11,9 @@ const MenuGroupCard = (props) => {
const tagSlot =
{tags?.length}
const links = [
- { name: locale.COMMON.ARTICLE, to: '/archive', slot: archiveSlot, show: CONFIG_MATERY.MENU_ARCHIVE },
- { name: locale.COMMON.CATEGORY, to: '/category', slot: categorySlot, show: CONFIG_MATERY.MENU_CATEGORY },
- { name: locale.COMMON.TAGS, to: '/tag', slot: tagSlot, show: CONFIG_MATERY.MENU_TAG }
+ { 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 (
diff --git a/themes/matery/components/MenuItemDrop.js b/themes/matery/components/MenuItemDrop.js
index a4a2858a..5ffb0e36 100644
--- a/themes/matery/components/MenuItemDrop.js
+++ b/themes/matery/components/MenuItemDrop.js
@@ -29,7 +29,7 @@ export const MenuItemDrop = ({ link }) => {
{/* 子菜单 */}
{hasSubMenu &&
{link.subMenus.map(sLink => {
- return -
+ return
-
{link?.icon && }{sLink.title}
diff --git a/themes/matery/components/MenuList.js b/themes/matery/components/MenuList.js
index d059498c..6ea230f2 100644
--- a/themes/matery/components/MenuList.js
+++ b/themes/matery/components/MenuList.js
@@ -2,7 +2,7 @@ import React from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useGlobal } from '@/lib/global'
-import CONFIG_MATERY from '../config_matery'
+import CONFIG from '../config'
const MenuList = (props) => {
const { postCount, customNav } = props
@@ -12,10 +12,10 @@ const MenuList = (props) => {
let links = [
{ icon: 'fas fa-home', name: locale.NAV.INDEX, to: '/' || '/', show: true },
- { icon: 'fas fa-th', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_MATERY.MENU_CATEGORY },
- { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_MATERY.MENU_TAG },
- { icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', slot: archiveSlot, show: CONFIG_MATERY.MENU_ARCHIVE },
- { icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG_MATERY.MENU_SEARCH }
+ { icon: 'fas fa-th', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY },
+ { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG },
+ { icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', slot: archiveSlot, show: CONFIG.MENU_ARCHIVE },
+ { icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH }
]
if (customNav) {
links = links.concat(customNav)
diff --git a/themes/matery/components/MenuListSide.js b/themes/matery/components/MenuListSide.js
index eed55d6c..1dc44c93 100644
--- a/themes/matery/components/MenuListSide.js
+++ b/themes/matery/components/MenuListSide.js
@@ -1,6 +1,6 @@
import React from 'react'
import { useGlobal } from '@/lib/global'
-import CONFIG_MATERY from '../config_matery'
+import CONFIG from '../config'
import BLOG from '@/blog.config'
import { MenuItemCollapse } from './MenuItemCollapse'
@@ -9,10 +9,10 @@ export const MenuListSide = (props) => {
const { locale } = useGlobal()
let links = [
- { icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG_MATERY.MENU_ARCHIVE },
- { icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG_MATERY.MENU_SEARCH },
- { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_MATERY.MENU_CATEGORY },
- { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_MATERY.MENU_TAG }
+ { 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) {
diff --git a/themes/matery/components/MenuListTop.js b/themes/matery/components/MenuListTop.js
index 8094e543..58f06cba 100644
--- a/themes/matery/components/MenuListTop.js
+++ b/themes/matery/components/MenuListTop.js
@@ -1,6 +1,6 @@
import React from 'react'
import { useGlobal } from '@/lib/global'
-import CONFIG_MATERY from '../config_matery'
+import CONFIG from '../config'
import { MenuItemDrop } from './MenuItemDrop'
import BLOG from '@/blog.config'
@@ -9,10 +9,10 @@ export const MenuListTop = (props) => {
const { locale } = useGlobal()
let links = [
- { icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG_MATERY.MENU_ARCHIVE },
- { icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG_MATERY.MENU_SEARCH },
- { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_MATERY.MENU_CATEGORY },
- { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_MATERY.MENU_TAG }
+ { 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) {
diff --git a/themes/matery/components/PaginationNumber.js b/themes/matery/components/PaginationNumber.js
index 0ebdade7..42281b93 100644
--- a/themes/matery/components/PaginationNumber.js
+++ b/themes/matery/components/PaginationNumber.js
@@ -12,7 +12,7 @@ const PaginationNumber = ({ page, totalPage }) => {
const router = useRouter()
const currentPage = +page
const showNext = page < totalPage
- const pagePrefix = router.asPath.replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '')
+ const pagePrefix = router.asPath.split('?')[0].replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '')
const pages = generatePages(pagePrefix, page, currentPage, totalPage)
return (
diff --git a/themes/matery/components/PaginationSimple.js b/themes/matery/components/PaginationSimple.js
index 6605e64c..df2a3f1a 100644
--- a/themes/matery/components/PaginationSimple.js
+++ b/themes/matery/components/PaginationSimple.js
@@ -1,4 +1,3 @@
-import BLOG from '@/blog.config'
import Link from 'next/link'
import { useRouter } from 'next/router'
@@ -13,15 +12,15 @@ const PaginationSimple = ({ page, totalPage }) => {
const router = useRouter()
const currentPage = +page
const showNext = currentPage < totalPage
-
+ const pagePrefix = router.asPath.split('?')[0].replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '')
return (
{
{