From f7ceb2f9ec2c7194e75ebc3208a0b5c5e2ab452c Mon Sep 17 00:00:00 2001
From: BlackBerry009 <765042894@qq.com>
Date: Sun, 8 Jun 2025 00:50:22 +0800
Subject: [PATCH 01/58] feat: new theme
---
blog.config.js | 38 +-
themes/typography/components/Announcement.js | 13 +
themes/typography/components/ArticleAround.js | 32 ++
themes/typography/components/ArticleInfo.js | 65 ++++
themes/typography/components/ArticleLock.js | 52 +++
.../typography/components/BlogArchiveItem.js | 37 ++
themes/typography/components/BlogItem.js | 106 ++++++
themes/typography/components/BlogListPage.js | 74 ++++
.../typography/components/BlogListScroll.js | 70 ++++
themes/typography/components/BlogPostBar.js | 29 ++
themes/typography/components/Catalog.js | 97 +++++
.../components/ExampleRecentComments.js | 35 ++
themes/typography/components/Footer.js | 37 ++
themes/typography/components/Header.js | 54 +++
.../typography/components/JumpToTopButton.js | 35 ++
.../typography/components/MenuItemCollapse.js | 92 +++++
themes/typography/components/MenuItemDrop.js | 21 ++
themes/typography/components/MenuList.js | 84 +++++
themes/typography/components/NavBar.js | 33 ++
.../typography/components/RecommendPosts.js | 32 ++
themes/typography/components/SearchInput.js | 86 +++++
themes/typography/components/SocialButton.js | 115 ++++++
themes/typography/components/Title.js | 19 +
themes/typography/components/TopBar.js | 19 +
themes/typography/config.js | 22 ++
themes/typography/index.js | 357 ++++++++++++++++++
themes/typography/style.js | 84 +++++
27 files changed, 1719 insertions(+), 19 deletions(-)
create mode 100644 themes/typography/components/Announcement.js
create mode 100644 themes/typography/components/ArticleAround.js
create mode 100644 themes/typography/components/ArticleInfo.js
create mode 100644 themes/typography/components/ArticleLock.js
create mode 100644 themes/typography/components/BlogArchiveItem.js
create mode 100644 themes/typography/components/BlogItem.js
create mode 100644 themes/typography/components/BlogListPage.js
create mode 100644 themes/typography/components/BlogListScroll.js
create mode 100644 themes/typography/components/BlogPostBar.js
create mode 100644 themes/typography/components/Catalog.js
create mode 100644 themes/typography/components/ExampleRecentComments.js
create mode 100644 themes/typography/components/Footer.js
create mode 100644 themes/typography/components/Header.js
create mode 100644 themes/typography/components/JumpToTopButton.js
create mode 100644 themes/typography/components/MenuItemCollapse.js
create mode 100644 themes/typography/components/MenuItemDrop.js
create mode 100644 themes/typography/components/MenuList.js
create mode 100644 themes/typography/components/NavBar.js
create mode 100644 themes/typography/components/RecommendPosts.js
create mode 100644 themes/typography/components/SearchInput.js
create mode 100644 themes/typography/components/SocialButton.js
create mode 100644 themes/typography/components/Title.js
create mode 100644 themes/typography/components/TopBar.js
create mode 100644 themes/typography/config.js
create mode 100644 themes/typography/index.js
create mode 100644 themes/typography/style.js
diff --git a/blog.config.js b/blog.config.js
index a8a972a7..81c91c5e 100644
--- a/blog.config.js
+++ b/blog.config.js
@@ -1,32 +1,32 @@
-// 注: process.env.XX是Vercel的环境变量,配置方式见:https://docs.tangly1024.com/article/how-to-config-notion-next#c4768010ae7d44609b744e79e2f9959a
+// 注:process.env.XX 是 Vercel 的环境变量,配置方式见:https://docs.tangly1024.com/article/how-to-config-notion-next#c4768010ae7d44609b744e79e2f9959a
const BLOG = {
// Important page_id!!!Duplicate Template from https://www.notion.so/tanghh/02ab3b8678004aa69e9e415905ef32a5
NOTION_PAGE_ID:
process.env.NOTION_PAGE_ID ||
'02ab3b8678004aa69e9e415905ef32a5,en:7c1d570661754c8fbc568e00a01fd70e',
- THEME: process.env.NEXT_PUBLIC_THEME || 'simple', // 当前主题,在themes文件夹下可找到所有支持的主题;主题名称就是文件夹名,例如 example,fukasawa,gitbook,heo,hexo,landing,matery,medium,next,nobelium,plog,simple
+ THEME: process.env.NEXT_PUBLIC_THEME || 'typography', // 当前主题,在 themes 文件夹下可找到所有支持的主题;主题名称就是文件夹名,例如 example,fukasawa,gitbook,heo,hexo,landing,matery,medium,next,nobelium,plog,simple
LANG: process.env.NEXT_PUBLIC_LANG || 'zh-CN', // e.g 'zh-CN','en-US' see /lib/lang.js for more.
SINCE: process.env.NEXT_PUBLIC_SINCE || 2021, // e.g if leave this empty, current year will be used.
- PSEUDO_STATIC: process.env.NEXT_PUBLIC_PSEUDO_STATIC || false, // 伪静态路径,开启后所有文章URL都以 .html 结尾。
- NEXT_REVALIDATE_SECOND: process.env.NEXT_PUBLIC_REVALIDATE_SECOND || 5, // 更新缓存间隔 单位(秒);即每个页面有5秒的纯静态期、此期间无论多少次访问都不会抓取notion数据;调大该值有助于节省Vercel资源、同时提升访问速率,但也会使文章更新有延迟。
- APPEARANCE: process.env.NEXT_PUBLIC_APPEARANCE || 'light', // ['light', 'dark', 'auto'], // light 日间模式 , dark夜间模式, auto根据时间和主题自动夜间模式
- APPEARANCE_DARK_TIME: process.env.NEXT_PUBLIC_APPEARANCE_DARK_TIME || [18, 6], // 夜间模式起至时间,false时关闭根据时间自动切换夜间模式
+ PSEUDO_STATIC: process.env.NEXT_PUBLIC_PSEUDO_STATIC || false, // 伪静态路径,开启后所有文章 URL 都以 .html 结尾。
+ NEXT_REVALIDATE_SECOND: process.env.NEXT_PUBLIC_REVALIDATE_SECOND || 5, // 更新缓存间隔 单位 (秒);即每个页面有 5 秒的纯静态期、此期间无论多少次访问都不会抓取 notion 数据;调大该值有助于节省 Vercel 资源、同时提升访问速率,但也会使文章更新有延迟。
+ APPEARANCE: process.env.NEXT_PUBLIC_APPEARANCE || 'light', // ['light', 'dark', 'auto'], // light 日间模式,dark 夜间模式,auto 根据时间和主题自动夜间模式
+ APPEARANCE_DARK_TIME: process.env.NEXT_PUBLIC_APPEARANCE_DARK_TIME || [18, 6], // 夜间模式起至时间,false 时关闭根据时间自动切换夜间模式
AUTHOR: process.env.NEXT_PUBLIC_AUTHOR || 'NotionNext', // 您的昵称 例如 tangly1024
BIO: process.env.NEXT_PUBLIC_BIO || '一个普通的干饭人🍚', // 作者简介
LINK: process.env.NEXT_PUBLIC_LINK || 'https://tangly1024.com', // 网站地址
KEYWORDS: process.env.NEXT_PUBLIC_KEYWORD || 'Notion, 博客', // 网站关键词 英文逗号隔开
- BLOG_FAVICON: process.env.NEXT_PUBLIC_FAVICON || '/favicon.ico', // blog favicon 配置, 默认使用 /public/favicon.ico,支持在线图片,如 https://img.imesong.com/favicon.png
- BEI_AN: process.env.NEXT_PUBLIC_BEI_AN || '', // 备案号 闽ICP备XXXXXX
+ BLOG_FAVICON: process.env.NEXT_PUBLIC_FAVICON || '/favicon.ico', // blog favicon 配置,默认使用 /public/favicon.ico,支持在线图片,如 https://img.imesong.com/favicon.png
+ BEI_AN: process.env.NEXT_PUBLIC_BEI_AN || '', // 备案号 闽 ICP 备 XXXXXX
BEI_AN_LINK: process.env.NEXT_PUBLIC_BEI_AN_LINK || 'https://beian.miit.gov.cn/', // 备案查询链接,如果用了萌备等备案请在这里填写
- // RSS订阅
- ENABLE_RSS: process.env.NEXT_PUBLIC_ENABLE_RSS || true, // 是否开启RSS订阅功能
+ // RSS 订阅
+ ENABLE_RSS: process.env.NEXT_PUBLIC_ENABLE_RSS || true, // 是否开启 RSS 订阅功能
// 其它复杂配置
- // 原配置文件过长,且并非所有人都会用到,故此将配置拆分到/conf/目录下, 按需找到对应文件并修改即可
+ // 原配置文件过长,且并非所有人都会用到,故此将配置拆分到/conf/目录下,按需找到对应文件并修改即可
...require('./conf/comment.config'), // 评论插件
...require('./conf/contact.config'), // 作者联系方式配置
...require('./conf/post.config'), // 文章与列表配置
@@ -38,11 +38,11 @@ const BLOG = {
...require('./conf/animation.config'), // 动效美化效果
...require('./conf/widget.config'), // 悬浮在网页上的挂件,聊天客服、宠物挂件、音乐播放器等
...require('./conf/ad.config'), // 广告营收插件
- ...require('./conf/plugin.config'), // 其他第三方插件 algolia全文索引
+ ...require('./conf/plugin.config'), // 其他第三方插件 algolia 全文索引
// 高级用法
...require('./conf/layout-map.config'), // 路由与布局映射自定义,例如自定义特定路由的页面布局
- ...require('./conf/notion.config'), // 读取notion数据库相关的扩展配置,例如自定义表头
+ ...require('./conf/notion.config'), // 读取 notion 数据库相关的扩展配置,例如自定义表头
...require('./conf/dev.config'), // 开发、调试时需要关注的配置
// 自定义外部脚本,外部样式
@@ -50,21 +50,21 @@ const BLOG = {
CUSTOM_EXTERNAL_CSS: [''], // e.g. ['http://xx.com/style.css','http://xx.com/style.css']
// 自定义菜单
- CUSTOM_MENU: process.env.NEXT_PUBLIC_CUSTOM_MENU || true, // 支持Menu类型的菜单,替代了3.12版本前的Page类型
+ CUSTOM_MENU: process.env.NEXT_PUBLIC_CUSTOM_MENU || true, // 支持 Menu 类型的菜单,替代了 3.12 版本前的 Page 类型
// 文章列表相关设置
- CAN_COPY: process.env.NEXT_PUBLIC_CAN_COPY || true, // 是否允许复制页面内容 默认允许,如果设置为false、则全栈禁止复制内容。
+ CAN_COPY: process.env.NEXT_PUBLIC_CAN_COPY || true, // 是否允许复制页面内容 默认允许,如果设置为 false、则全栈禁止复制内容。
- // 侧栏布局 是否反转(左变右,右变左) 已支持主题: hexo next medium fukasawa example
+ // 侧栏布局 是否反转 (左变右,右变左) 已支持主题:hexo next medium fukasawa example
LAYOUT_SIDEBAR_REVERSE:
process.env.NEXT_PUBLIC_LAYOUT_SIDEBAR_REVERSE || false,
- // 欢迎语打字效果,Hexo,Matery主题支持, 英文逗号隔开多个欢迎语。
+ // 欢迎语打字效果,Hexo,Matery 主题支持,英文逗号隔开多个欢迎语。
GREETING_WORDS:
process.env.NEXT_PUBLIC_GREETING_WORDS ||
- 'Hi,我是一个程序员, Hi,我是一个打工人,Hi,我是一个干饭人,欢迎来到我的博客🎉',
+ 'Hi,我是一个程序员,Hi,我是一个打工人,Hi,我是一个干饭人,欢迎来到我的博客🎉',
- // uuid重定向至 slug
+ // uuid 重定向至 slug
UUID_REDIRECT: process.env.UUID_REDIRECT || false
}
diff --git a/themes/typography/components/Announcement.js b/themes/typography/components/Announcement.js
new file mode 100644
index 00000000..13d069d8
--- /dev/null
+++ b/themes/typography/components/Announcement.js
@@ -0,0 +1,13 @@
+import dynamic from 'next/dynamic'
+
+const NotionPage = dynamic(() => import('@/components/NotionPage'))
+
+const Announcement = ({ post, className }) => {
+ if (!post) {
+ return <>>
+ }
+ return <>{post && (
+
+
)} >
+}
+export default Announcement
diff --git a/themes/typography/components/ArticleAround.js b/themes/typography/components/ArticleAround.js
new file mode 100644
index 00000000..b4cee232
--- /dev/null
+++ b/themes/typography/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 &&
+
+ {prev.title}
+
+ }
+ {next &&
+ {next.title}
+
+
+ }
+
+ )
+}
diff --git a/themes/typography/components/ArticleInfo.js b/themes/typography/components/ArticleInfo.js
new file mode 100644
index 00000000..ed1d39a9
--- /dev/null
+++ b/themes/typography/components/ArticleInfo.js
@@ -0,0 +1,65 @@
+import Link from 'next/link'
+import { useGlobal } from '@/lib/global'
+import CONFIG from '../config'
+import { siteConfig } from '@/lib/config'
+import { formatDateFmt } from '@/lib/utils/formatDate'
+import NotionIcon from '@/components/NotionIcon'
+
+/**
+ * 文章描述
+ * @param {*} props
+ * @returns
+ */
+export default function ArticleInfo(props) {
+ const { post } = props
+
+ const { locale } = useGlobal()
+
+ return (
+
+
+ {siteConfig('POST_TITLE_ICON') && }
+ {post?.title}
+
+
+
+ {post?.type !== 'Page' && (
+
+
+
+ 发布于
+
+ {post.date?.start_date || post.createdTime}
+
+
+
+
+
+ {/* {post.category && (
+
+ {' '}
+
+
+ {post.category}
+
+
+ )} */}
+ {post?.tags &&
+ post?.tags?.length > 0 &&
+ post?.tags.map(t => (
+
+ #{t}
+
+ ))}
+
+
+ )}
+
+
+ )
+}
diff --git a/themes/typography/components/ArticleLock.js b/themes/typography/components/ArticleLock.js
new file mode 100644
index 00000000..d9f6e978
--- /dev/null
+++ b/themes/typography/components/ArticleLock.js
@@ -0,0 +1,52 @@
+import { useGlobal } from '@/lib/global'
+import { useEffect, useRef } from 'react'
+
+/**
+ * 加密文章校验组件
+ * @param {password, validPassword} props
+ * @param password 正确的密码
+ * @param validPassword(bool) 回调函数,校验正确回调入参为true
+ * @returns
+ */
+export default function 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 text-black dark:bg-gray-500 bg-gray-50'
+ >
+
+ {locale.COMMON.SUBMIT}
+
+
+
+
+
+
+}
diff --git a/themes/typography/components/BlogArchiveItem.js b/themes/typography/components/BlogArchiveItem.js
new file mode 100644
index 00000000..4a92959e
--- /dev/null
+++ b/themes/typography/components/BlogArchiveItem.js
@@ -0,0 +1,37 @@
+import Link from 'next/link'
+
+/**
+ * 归档分组文章
+ * @param {*} param0
+ * @returns
+ */
+export default function BlogArchiveItem({ archiveTitle, archivePosts }) {
+ return (
+
+
+ {archiveTitle}
+
+
+
+
+ )
+}
diff --git a/themes/typography/components/BlogItem.js b/themes/typography/components/BlogItem.js
new file mode 100644
index 00000000..b880f1fe
--- /dev/null
+++ b/themes/typography/components/BlogItem.js
@@ -0,0 +1,106 @@
+import LazyImage from '@/components/LazyImage'
+import NotionIcon from '@/components/NotionIcon'
+import NotionPage from '@/components/NotionPage'
+import TwikooCommentCount from '@/components/TwikooCommentCount'
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import { formatDateFmt } from '@/lib/utils/formatDate'
+import Link from 'next/link'
+import CONFIG from '../config'
+
+export const BlogItem = props => {
+ const { post } = props
+ const { NOTION_CONFIG } = useGlobal()
+ const showPageCover = siteConfig('SIMPLE_POST_COVER_ENABLE', false, CONFIG)
+ const showPreview =
+ siteConfig('POST_LIST_PREVIEW', false, NOTION_CONFIG) && post.blockMap
+ console.log(post);
+ return (
+
+ {/* 文章标题 */}
+
+
+
+ {/* 图片封面 */}
+ {showPageCover && (
+
+
+
+
+
+ )}
+
+
+
+
+
+ {siteConfig('POST_TITLE_ICON') && (
+
+ )}
+ {post.title}
+
+
+
+ {/* 文章信息 */}
+
+
+
+ 发布于
+
+ {post.date?.start_date || post.createdTime}
+
+
+
+
+
+ {/* {post.category && (
+
+ {' '}
+
+
+ {post.category}
+
+
+ )} */}
+ {post?.tags &&
+ post?.tags?.length > 0 &&
+ post?.tags.map(t => (
+
+ #{t}
+
+ ))}
+
+
+
+
+ {!showPreview && (
+ <>
+ {post.summary}
+ {post.summary && ...}
+ >
+ )}
+ {showPreview && post?.blockMap && (
+
+
+
+
+ )}
+
+
+
+
+
+ )
+}
diff --git a/themes/typography/components/BlogListPage.js b/themes/typography/components/BlogListPage.js
new file mode 100644
index 00000000..ab245ed4
--- /dev/null
+++ b/themes/typography/components/BlogListPage.js
@@ -0,0 +1,74 @@
+import { AdSlot } from '@/components/GoogleAdsense'
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import CONFIG from '../config'
+import { BlogItem } from './BlogItem'
+
+/**
+ * 博客列表
+ * @param {*} props
+ * @returns
+ */
+export default function BlogListPage(props) {
+ const { page = 1, posts, postCount } = props
+ const router = useRouter()
+ const { NOTION_CONFIG } = useGlobal()
+ const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG)
+ const totalPage = Math.ceil(postCount / POSTS_PER_PAGE)
+ const currentPage = +page
+
+ // 博客列表嵌入广告
+ const SIMPLE_POST_AD_ENABLE = siteConfig(
+ 'SIMPLE_POST_AD_ENABLE',
+ false,
+ CONFIG
+ )
+
+ const showPrev = currentPage > 1
+ const showNext = page < totalPage
+ const pagePrefix = router.asPath
+ .split('?')[0]
+ .replace(/\/page\/[1-9]\d*/, '')
+ .replace(/\/$/, '')
+ .replace('.html', '')
+
+ return (
+
+
+ {posts?.map((p, index) => (
+
+ {SIMPLE_POST_AD_ENABLE && (index + 1) % 3 === 0 && (
+
+ )}
+ {SIMPLE_POST_AD_ENABLE && index + 1 === 4 &&
}
+
+
+ ))}
+
+
+
+
+ NEWER POSTS
+
+
+ OLDER POSTS
+
+
+
+ )
+}
diff --git a/themes/typography/components/BlogListScroll.js b/themes/typography/components/BlogListScroll.js
new file mode 100644
index 00000000..2252a995
--- /dev/null
+++ b/themes/typography/components/BlogListScroll.js
@@ -0,0 +1,70 @@
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import throttle from 'lodash.throttle'
+import { useCallback, useEffect, useRef, useState } from 'react'
+import { BlogItem } from './BlogItem'
+
+/**
+ * 滚动博客列表
+ * @param {*} props
+ * @returns
+ */
+export default function BlogListScroll(props) {
+ const { posts } = props
+ const { locale, NOTION_CONFIG } = useGlobal()
+ const [page, updatePage] = useState(1)
+ const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG)
+ let hasMore = false
+ const postsToShow = posts
+ ? Object.assign(posts).slice(0, POSTS_PER_PAGE * page)
+ : []
+
+ if (posts) {
+ const totalCount = posts.length
+ hasMore = page * POSTS_PER_PAGE < totalCount
+ }
+ const handleGetMore = () => {
+ if (!hasMore) return
+ updatePage(page + 1)
+ }
+
+ const targetRef = useRef(null)
+
+ // 监听滚动自动分页加载
+ const scrollTrigger = useCallback(
+ throttle(() => {
+ const scrollS = window.scrollY + window.outerHeight
+ const clientHeight = targetRef
+ ? targetRef.current
+ ? targetRef.current.clientHeight
+ : 0
+ : 0
+ if (scrollS > clientHeight + 100) {
+ handleGetMore()
+ }
+ }, 500)
+ )
+
+ useEffect(() => {
+ window.addEventListener('scroll', scrollTrigger)
+
+ return () => {
+ window.removeEventListener('scroll', scrollTrigger)
+ }
+ })
+
+ return (
+
+ {postsToShow.map(p => (
+
+ ))}
+
+
+ {' '}
+ {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
+
+
+ )
+}
diff --git a/themes/typography/components/BlogPostBar.js b/themes/typography/components/BlogPostBar.js
new file mode 100644
index 00000000..9efac113
--- /dev/null
+++ b/themes/typography/components/BlogPostBar.js
@@ -0,0 +1,29 @@
+import { useGlobal } from '@/lib/global'
+
+/**
+ * 文章列表上方嵌入
+ * @param {*} props
+ * @returns
+ */
+export default function BlogPostBar(props) {
+ const { tag, category } = props
+ const { locale } = useGlobal()
+
+ if (tag) {
+ return (
+
+
+ {locale.COMMON.TAGS}: {tag}
+
+ )
+ } else if (category) {
+ return (
+
+
+ {locale.COMMON.CATEGORY}: {category}
+
+ )
+ } else {
+ return <>>
+ }
+}
diff --git a/themes/typography/components/Catalog.js b/themes/typography/components/Catalog.js
new file mode 100644
index 00000000..6bea8bab
--- /dev/null
+++ b/themes/typography/components/Catalog.js
@@ -0,0 +1,97 @@
+import { useGlobal } from '@/lib/global'
+import throttle from 'lodash.throttle'
+import { uuidToId } from 'notion-utils'
+import { useEffect, useRef, useState } from 'react'
+
+/**
+ * 目录导航组件
+ * @param toc
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const Catalog = ({ post }) => {
+ const { locale } = useGlobal()
+ // 目录自动滚动
+ const tRef = useRef(null)
+ // 同步选中目录事件
+ const [activeSection, setActiveSection] = useState(null)
+
+ // 监听滚动事件
+ useEffect(() => {
+ const throttleMs = 200
+ const actionSectionScrollSpy = 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)
+ if (bbox.top - offset < 0) {
+ currentSectionId = section.getAttribute('data-id')
+ prevBBox = bbox
+ continue
+ }
+ break
+ }
+ setActiveSection(currentSectionId)
+ const index = post?.toc?.findIndex(
+ obj => uuidToId(obj.id) === currentSectionId
+ )
+ tRef?.current?.scrollTo({ top: 28 * index, behavior: 'smooth' })
+ }, throttleMs)
+
+ window.addEventListener('scroll', actionSectionScrollSpy)
+ actionSectionScrollSpy()
+ return () => {
+ window.removeEventListener('scroll', actionSectionScrollSpy)
+ }
+ }, [post])
+
+ // 无目录就直接返回空
+ if (!post || !post?.toc || post?.toc?.length < 1) {
+ return <>>
+ }
+
+ return (
+
+
+
+ {locale.COMMON.TABLE_OF_CONTENTS}
+
+
+
+
+
+
+ )
+}
+
+export default Catalog
diff --git a/themes/typography/components/ExampleRecentComments.js b/themes/typography/components/ExampleRecentComments.js
new file mode 100644
index 00000000..93cde585
--- /dev/null
+++ b/themes/typography/components/ExampleRecentComments.js
@@ -0,0 +1,35 @@
+import Link from 'next/link'
+import { RecentComments } from '@waline/client'
+import { useEffect, useState } from 'react'
+import { siteConfig } from '@/lib/config'
+
+/**
+ * @see https://waline.js.org/guide/get-started.html
+ * @param {*} props
+ * @returns
+ */
+const ExampleRecentComments = (props) => {
+ const [comments, updateComments] = useState([])
+ const [onLoading, changeLoading] = useState(true)
+ useEffect(() => {
+ RecentComments({
+ serverURL: siteConfig('COMMENT_WALINE_SERVER_URL'),
+ count: 5
+ }).then(({ comments }) => {
+ changeLoading(false)
+ updateComments(comments)
+ })
+ }, [])
+
+ return <>
+ {onLoading && Loading...
}
+ {!onLoading && comments && comments.length === 0 && No Comments
}
+ {!onLoading && comments && comments.length > 0 && comments.map((comment) => )}
+
+ >
+}
+
+export default ExampleRecentComments
diff --git a/themes/typography/components/Footer.js b/themes/typography/components/Footer.js
new file mode 100644
index 00000000..a762960e
--- /dev/null
+++ b/themes/typography/components/Footer.js
@@ -0,0 +1,37 @@
+import { BeiAnGongAn } from '@/components/BeiAnGongAn'
+import DarkModeButton from '@/components/DarkModeButton'
+import { siteConfig } from '@/lib/config'
+
+/**
+ * 页脚
+ * @param {*} props
+ * @returns
+ */
+export default function Footer(props) {
+ const d = new Date()
+ const currentYear = d.getFullYear()
+ const since = siteConfig('SINCE')
+ const copyrightDate =
+ parseInt(since) < currentYear ? since + '-' + currentYear : currentYear
+
+ return (
+
+ )
+}
diff --git a/themes/typography/components/Header.js b/themes/typography/components/Header.js
new file mode 100644
index 00000000..e5d71756
--- /dev/null
+++ b/themes/typography/components/Header.js
@@ -0,0 +1,54 @@
+import LazyImage from '@/components/LazyImage'
+import { siteConfig } from '@/lib/config'
+import Link from 'next/link'
+import CONFIG from '../config'
+import SocialButton from './SocialButton'
+
+/**
+ * 网站顶部
+ * @returns
+ */
+export default function Header(props) {
+ const { siteInfo } = props
+
+ return (
+
+ )
+}
diff --git a/themes/typography/components/JumpToTopButton.js b/themes/typography/components/JumpToTopButton.js
new file mode 100644
index 00000000..358c9372
--- /dev/null
+++ b/themes/typography/components/JumpToTopButton.js
@@ -0,0 +1,35 @@
+import { useGlobal } from '@/lib/global'
+import { useEffect, useState } from 'react'
+
+/**
+ * 跳转到网页顶部
+ * 当屏幕下滑500像素后会出现该控件
+ * @param targetRef 关联高度的目标html标签
+ * @param showPercent 是否显示百分比
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const JumpToTopButton = () => {
+ const { locale } = useGlobal()
+ const [show, switchShow] = useState(false)
+ const scrollListener = () => {
+ const scrollY = window.pageYOffset
+ const shouldShow = scrollY > 200
+ if (shouldShow !== show) {
+ switchShow(shouldShow)
+ }
+ }
+
+ useEffect(() => {
+ document.addEventListener('scroll', scrollListener)
+ return () => document.removeEventListener('scroll', scrollListener)
+ }, [show])
+
+ return window.scrollTo({ top: 0, behavior: 'smooth' })}
+ >
+
+}
+
+export default JumpToTopButton
diff --git a/themes/typography/components/MenuItemCollapse.js b/themes/typography/components/MenuItemCollapse.js
new file mode 100644
index 00000000..50174dc3
--- /dev/null
+++ b/themes/typography/components/MenuItemCollapse.js
@@ -0,0 +1,92 @@
+import Collapse from '@/components/Collapse'
+import Link from 'next/link'
+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 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, index) => {
+ return (
+
+
+
+ {sLink?.icon && (
+
+
+
+ )}
+ {sLink.title}
+
+
+
+ )
+ })}
+
+ )}
+ >
+ )
+}
diff --git a/themes/typography/components/MenuItemDrop.js b/themes/typography/components/MenuItemDrop.js
new file mode 100644
index 00000000..6302e4f3
--- /dev/null
+++ b/themes/typography/components/MenuItemDrop.js
@@ -0,0 +1,21 @@
+import Link from 'next/link'
+import { useState } from 'react'
+
+export const MenuItemDrop = ({ link }) => {
+ const hasSubMenu = link?.subMenus?.length > 0
+
+ if (!link || !link.show) {
+ return null
+ }
+
+ return (
+
+
+ {link?.name}
+
+
+ )
+}
diff --git a/themes/typography/components/MenuList.js b/themes/typography/components/MenuList.js
new file mode 100644
index 00000000..06f2bdf8
--- /dev/null
+++ b/themes/typography/components/MenuList.js
@@ -0,0 +1,84 @@
+import Collapse from '@/components/Collapse'
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import { useRouter } from 'next/router'
+import { useEffect, useRef, useState } from 'react'
+import CONFIG from '../config'
+import { MenuItemCollapse } from './MenuItemCollapse'
+import { MenuItemDrop } from './MenuItemDrop'
+
+/**
+ * 菜单导航
+ * @param {*} props
+ * @returns
+ */
+export const MenuList = ({ customNav, customMenu }) => {
+ const { locale } = useGlobal()
+ const [isOpen, changeIsOpen] = useState(false)
+ const toggleIsOpen = () => {
+ changeIsOpen(!isOpen)
+ }
+ const closeMenu = e => {
+ changeIsOpen(false)
+ }
+ const router = useRouter()
+ const collapseRef = useRef(null)
+
+ useEffect(() => {
+ router.events.on('routeChangeStart', closeMenu)
+ })
+
+ let links = [
+
+ {
+ icon: 'fas fa-archive',
+ name: locale.NAV.ARCHIVE,
+ href: '/archive',
+ show: siteConfig('SIMPLE_MENU_ARCHIVE', null, CONFIG)
+ },
+ {
+ icon: 'fas fa-folder',
+ name: locale.COMMON.CATEGORY,
+ href: '/category',
+ show: siteConfig('SIMPLE_MENU_CATEGORY', null, CONFIG)
+ },
+ {
+ icon: 'fas fa-tag',
+ name: locale.COMMON.TAGS,
+ href: '/tag',
+ show: siteConfig('SIMPLE_MENU_TAG', null, CONFIG)
+ }
+ ]
+
+ if (customNav) {
+ links = links.concat(customNav)
+ }
+
+ // 如果 开启自定义菜单,则覆盖 Page 生成的菜单
+ if (siteConfig('CUSTOM_MENU')) {
+ links = customMenu
+ }
+
+ if (!links || links.length === 0) {
+ return null
+ }
+
+ return (
+ <>
+ {/* 大屏模式菜单 - 垂直排列 */}
+
+ {/* 移动端小屏菜单 - 水平排列 */}
+
+ >
+ )
+}
diff --git a/themes/typography/components/NavBar.js b/themes/typography/components/NavBar.js
new file mode 100644
index 00000000..314bedaa
--- /dev/null
+++ b/themes/typography/components/NavBar.js
@@ -0,0 +1,33 @@
+import { siteConfig } from '@/lib/config'
+import { useRouter } from 'next/router'
+import { useState } from 'react'
+import { useSimpleGlobal } from '..'
+import { MenuList } from './MenuList'
+import SocialButton from './SocialButton'
+import Link from 'next/link'
+
+/**
+ * 菜单导航
+ * @param {*} props
+ * @returns
+ */
+export default function NavBar(props) {
+ return (
+
+ )
+}
diff --git a/themes/typography/components/RecommendPosts.js b/themes/typography/components/RecommendPosts.js
new file mode 100644
index 00000000..fd214fa0
--- /dev/null
+++ b/themes/typography/components/RecommendPosts.js
@@ -0,0 +1,32 @@
+import Link from 'next/link'
+import { useGlobal } from '@/lib/global'
+import CONFIG from '../config'
+import { siteConfig } from '@/lib/config'
+
+/**
+ * 展示文章推荐
+ */
+const RecommendPosts = ({ recommendPosts }) => {
+ const { locale } = useGlobal()
+ if (!siteConfig('SIMPLE_ARTICLE_RECOMMEND_POSTS', null, CONFIG) || !recommendPosts || recommendPosts.length < 1) {
+ return <>>
+ }
+
+ return (
+
+
{locale.COMMON.RELATE_POSTS} :
+
+ {recommendPosts.map(post => (
+ -
+
+
+ {post.title}
+
+
+
+ ))}
+
+
+ )
+}
+export default RecommendPosts
diff --git a/themes/typography/components/SearchInput.js b/themes/typography/components/SearchInput.js
new file mode 100644
index 00000000..f0b43a39
--- /dev/null
+++ b/themes/typography/components/SearchInput.js
@@ -0,0 +1,86 @@
+import { useRouter } from 'next/router'
+import { useImperativeHandle, useRef, useState } from 'react'
+let lock = false
+
+const SearchInput = ({ keyword, cRef, className }) => {
+ const [onLoading, setLoadingState] = useState(false)
+ const router = useRouter()
+ const searchInputRef = useRef()
+ useImperativeHandle(cRef, () => {
+ return {
+ focus: () => {
+ searchInputRef?.current?.focus()
+ }
+ }
+ })
+
+ const handleSearch = () => {
+ const key = searchInputRef.current.value
+
+ if (key && key !== '') {
+ setLoadingState(true)
+ 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={keyword}
+ />
+
+
+
+
+
+ {(showClean &&
+
+
+
+ )}
+
+}
+
+export default SearchInput
diff --git a/themes/typography/components/SocialButton.js b/themes/typography/components/SocialButton.js
new file mode 100644
index 00000000..ced5c850
--- /dev/null
+++ b/themes/typography/components/SocialButton.js
@@ -0,0 +1,115 @@
+import { siteConfig } from '@/lib/config'
+
+/**
+ * 社交联系方式按钮组
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const SocialButton = () => {
+ return (
+
+
+ {siteConfig('CONTACT_GITHUB') && (
+
+
+
+ )}
+ {siteConfig('CONTACT_TWITTER') && (
+
+
+
+ )}
+ {siteConfig('CONTACT_TELEGRAM') && (
+
+
+
+ )}
+ {siteConfig('CONTACT_LINKEDIN') && (
+
+
+
+ )}
+ {siteConfig('CONTACT_WEIBO') && (
+
+
+
+ )}
+ {siteConfig('CONTACT_INSTAGRAM') && (
+
+
+
+ )}
+ {siteConfig('CONTACT_EMAIL') && (
+
+
+
+ )}
+ {JSON.parse(siteConfig('ENABLE_RSS')) && (
+
+
+
+ )}
+ {siteConfig('CONTACT_BILIBILI') && (
+
+
+
+ )}
+ {siteConfig('CONTACT_YOUTUBE') && (
+
+
+
+ )}
+ {siteConfig('CONTACT_THREADS') && (
+
+
+
+ )}
+
+
+ )
+}
+export default SocialButton
diff --git a/themes/typography/components/Title.js b/themes/typography/components/Title.js
new file mode 100644
index 00000000..151d736e
--- /dev/null
+++ b/themes/typography/components/Title.js
@@ -0,0 +1,19 @@
+import { siteConfig } from '@/lib/config'
+
+/**
+ * 标题栏
+ * @param {*} props
+ * @returns
+ */
+export const Title = (props) => {
+ const { post } = props
+ const title = post?.title || siteConfig('DESCRIPTION')
+ const description = post?.description || siteConfig('AUTHOR')
+
+ return
+
{title}
+
+ {description}
+
+
+}
diff --git a/themes/typography/components/TopBar.js b/themes/typography/components/TopBar.js
new file mode 100644
index 00000000..e9224de8
--- /dev/null
+++ b/themes/typography/components/TopBar.js
@@ -0,0 +1,19 @@
+import CONFIG from '../config'
+import { siteConfig } from '@/lib/config'
+
+/**
+ * 网站顶部 提示栏
+ * @returns
+ */
+export default function TopBar (props) {
+ const content = siteConfig('SIMPLE_TOP_BAR_CONTENT', null, CONFIG)
+
+ if (content) {
+ return
+ }
+ return <>>
+}
diff --git a/themes/typography/config.js b/themes/typography/config.js
new file mode 100644
index 00000000..bc0b8fc4
--- /dev/null
+++ b/themes/typography/config.js
@@ -0,0 +1,22 @@
+const CONFIG = {
+
+ SIMPLE_LOGO_IMG: '/Logo.webp',
+ SIMPLE_TOP_BAR: true, // 显示顶栏
+ SIMPLE_TOP_BAR_CONTENT: process.env.NEXT_PUBLIC_THEME_SIMPLE_TOP_TIPS || '',
+ SIMPLE_LOGO_DESCRIPTION: process.env.NEXT_PUBLIC_THEME_SIMPLE_LOGO_DESCRIPTION || '编程爱好者
/互联网从业者
/知识分享博主
',
+
+ SIMPLE_AUTHOR_LINK: process.env.NEXT_PUBLIC_AUTHOR_LINK || '#',
+
+ SIMPLE_POST_AD_ENABLE: process.env.NEXT_PUBLIC_SIMPLE_POST_AD_ENABLE || false, // 文章列表是否插入广告
+
+ SIMPLE_POST_COVER_ENABLE: process.env.NEXT_PUBLIC_SIMPLE_POST_COVER_ENABLE || false, // 是否展示博客封面
+
+ SIMPLE_ARTICLE_RECOMMEND_POSTS: process.env.NEXT_PUBLIC_SIMPLE_ARTICLE_RECOMMEND_POSTS || true, // 文章详情底部显示推荐
+
+ // 菜单配置
+ SIMPLE_MENU_CATEGORY: true, // 显示分类
+ SIMPLE_MENU_TAG: true, // 显示标签
+ SIMPLE_MENU_ARCHIVE: true, // 显示归档
+ SIMPLE_MENU_SEARCH: true // 显示搜索
+}
+export default CONFIG
diff --git a/themes/typography/index.js b/themes/typography/index.js
new file mode 100644
index 00000000..437596f9
--- /dev/null
+++ b/themes/typography/index.js
@@ -0,0 +1,357 @@
+import { AdSlot } from '@/components/GoogleAdsense'
+import replaceSearchResult from '@/components/Mark'
+import NotionPage from '@/components/NotionPage'
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import { isBrowser } from '@/lib/utils'
+import { Transition } from '@headlessui/react'
+import dynamic from 'next/dynamic'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { createContext, useContext, useEffect, useRef } from 'react'
+import BlogPostBar from './components/BlogPostBar'
+import CONFIG from './config'
+import { Style } from './style'
+
+const AlgoliaSearchModal = dynamic(
+ () => import('@/components/AlgoliaSearchModal'),
+ { ssr: false }
+)
+
+// 主题组件
+const BlogListScroll = dynamic(() => import('./components/BlogListScroll'), {
+ ssr: false
+})
+const BlogArchiveItem = dynamic(() => import('./components/BlogArchiveItem'), {
+ ssr: false
+})
+const ArticleLock = dynamic(() => import('./components/ArticleLock'), {
+ ssr: false
+})
+const ArticleInfo = dynamic(() => import('./components/ArticleInfo'), {
+ ssr: false
+})
+const Comment = dynamic(() => import('@/components/Comment'), { ssr: false })
+const ArticleAround = dynamic(() => import('./components/ArticleAround'), {
+ ssr: false
+})
+const ShareBar = dynamic(() => import('@/components/ShareBar'), { ssr: false })
+const TopBar = dynamic(() => import('./components/TopBar'), { ssr: false })
+const Header = dynamic(() => import('./components/Header'), { ssr: false })
+const NavBar = dynamic(() => import('./components/NavBar'), { ssr: false })
+const JumpToTopButton = dynamic(() => import('./components/JumpToTopButton'), {
+ ssr: false
+})
+const Footer = dynamic(() => import('./components/Footer'), { ssr: false })
+const SearchInput = dynamic(() => import('./components/SearchInput'), {
+ ssr: false
+})
+const WWAds = dynamic(() => import('@/components/WWAds'), { ssr: false })
+const BlogListPage = dynamic(() => import('./components/BlogListPage'), {
+ ssr: false
+})
+const RecommendPosts = dynamic(() => import('./components/RecommendPosts'), {
+ ssr: false
+})
+
+// 主题全局状态
+const ThemeGlobalSimple = createContext()
+export const useSimpleGlobal = () => useContext(ThemeGlobalSimple)
+
+/**
+ * 基础布局
+ *
+ * @param {*} props
+ * @returns
+ */
+const LayoutBase = props => {
+ const { children, slotTop } = props
+ const { onLoading, fullWidth } = useGlobal()
+ const searchModal = useRef(null)
+
+ return (
+
+
+
+
+ {siteConfig('SIMPLE_TOP_BAR', null, CONFIG) &&
}
+
+
+ {/* 主体 - 使用 flex 布局 */}
+
+ {/* 左侧内容区域 - 可滚动 */}
+
+ {/* 移动端导航 - 显示在顶部 */}
+
+
+
+
+ {slotTop}
+
+ {children}
+
+
+ {/* 移动端页脚 - 显示在底部 */}
+
+
+
+
+
+ {/* 右侧导航和页脚 - 固定不滚动 */}
+
+
+
+
+
+
+
+
+
+
+ {/* 搜索框 */}
+
+
+
+ )
+}
+
+/**
+ * 博客首页
+ * 首页就是列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutIndex = props => {
+ return
+}
+/**
+ * 博客列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutPostList = props => {
+ return (
+ <>
+
+
+ >
+ )
+}
+
+/**
+ * 搜索页
+ * 也是博客列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutSearch = props => {
+ const { keyword } = props
+
+ useEffect(() => {
+ if (isBrowser) {
+ replaceSearchResult({
+ doms: document.getElementById('posts-wrapper'),
+ search: keyword,
+ target: {
+ element: 'span',
+ className: 'text-red-500 border-b border-dashed'
+ }
+ })
+ }
+ }, [])
+
+ const slotTop = siteConfig('ALGOLIA_APP_ID') ? null : (
+
+ )
+
+ return
+}
+
+/**
+ * 归档页
+ * @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, prev, next, recommendPosts } = props
+ const { fullWidth } = useGlobal()
+
+ return (
+ <>
+ {lock && }
+
+ {!lock && post && (
+
+ {/* 文章信息 */}
+
+
+ {/* 广告嵌入 */}
+ {/*
*/}
+
+
+
+ {/* Notion 文章主体 */}
+ {!lock && }
+
+
+ {/* 分享 */}
+ {/*
*/}
+
+ {/* 广告嵌入 */}
+
+
+ {post?.type === 'Post' && (
+ <>
+
+
+ >
+ )}
+
+ {/* 评论区 */}
+
+
+ )}
+ >
+ )
+}
+
+/**
+ * 404
+ * @param {*} props
+ * @returns
+ */
+const Layout404 = props => {
+ const { post } = props
+ const router = useRouter()
+ const waiting404 = siteConfig('POST_WAITING_TIME_FOR_404') * 1000
+ useEffect(() => {
+ // 404
+ if (!post) {
+ setTimeout(() => {
+ if (isBrowser) {
+ const article = document.querySelector(
+ '#article-wrapper #notion-article'
+ )
+ if (!article) {
+ router.push('/404').then(() => {
+ console.warn('找不到页面', router.asPath)
+ })
+ }
+ }
+ }, waiting404)
+ }
+ }, [post])
+ return <>404 Not found.>
+}
+
+/**
+ * 分类列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutCategoryIndex = props => {
+ const { categoryOptions } = props
+ return (
+ <>
+
+ {categoryOptions?.map(category => {
+ return (
+
+
+
+ {category.name}({category.count})
+
+
+ )
+ })}
+
+ >
+ )
+}
+
+/**
+ * 标签列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutTagIndex = props => {
+ const { tagOptions } = props
+ return (
+ <>
+
+ >
+ )
+}
+
+export {
+ Layout404,
+ LayoutArchive,
+ LayoutBase,
+ LayoutCategoryIndex,
+ LayoutIndex,
+ LayoutPostList,
+ LayoutSearch,
+ LayoutSlug,
+ LayoutTagIndex,
+ CONFIG as THEME_CONFIG
+}
diff --git a/themes/typography/style.js b/themes/typography/style.js
new file mode 100644
index 00000000..5561872b
--- /dev/null
+++ b/themes/typography/style.js
@@ -0,0 +1,84 @@
+/* eslint-disable react/no-unknown-property */
+/**
+ * 此处样式只对当前主题生效
+ * 此处不支持 tailwindCSS 的 @apply 语法
+ * @returns
+ */
+const Style = () => {
+ return (
+
+ )
+}
+
+export { Style }
From a8192e01795ed71192cef4c196588f71a4c615fb Mon Sep 17 00:00:00 2001
From: Blackberry009 <765042894@qq.com>
Date: Mon, 9 Jun 2025 17:46:08 +0800
Subject: [PATCH 02/58] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96css?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
blog.config.js | 6 +-
themes/typography/components/BlogItem.js | 1 -
themes/typography/components/BlogListPage.js | 2 +-
themes/typography/components/Catalog.js | 83 ++++++++++++++------
themes/typography/components/NavBar.js | 10 ++-
themes/typography/index.js | 37 ++++++---
themes/typography/style.js | 2 +-
7 files changed, 100 insertions(+), 41 deletions(-)
diff --git a/blog.config.js b/blog.config.js
index 81c91c5e..2ca1c0fc 100644
--- a/blog.config.js
+++ b/blog.config.js
@@ -3,8 +3,7 @@
const BLOG = {
// Important page_id!!!Duplicate Template from https://www.notion.so/tanghh/02ab3b8678004aa69e9e415905ef32a5
NOTION_PAGE_ID:
- process.env.NOTION_PAGE_ID ||
- '02ab3b8678004aa69e9e415905ef32a5,en:7c1d570661754c8fbc568e00a01fd70e',
+ process.env.NOTION_PAGE_ID || '20cb7f4f464a80db8541cbd0da70a64b',
THEME: process.env.NEXT_PUBLIC_THEME || 'typography', // 当前主题,在 themes 文件夹下可找到所有支持的主题;主题名称就是文件夹名,例如 example,fukasawa,gitbook,heo,hexo,landing,matery,medium,next,nobelium,plog,simple
LANG: process.env.NEXT_PUBLIC_LANG || 'zh-CN', // e.g 'zh-CN','en-US' see /lib/lang.js for more.
SINCE: process.env.NEXT_PUBLIC_SINCE || 2021, // e.g if leave this empty, current year will be used.
@@ -20,7 +19,8 @@ const BLOG = {
KEYWORDS: process.env.NEXT_PUBLIC_KEYWORD || 'Notion, 博客', // 网站关键词 英文逗号隔开
BLOG_FAVICON: process.env.NEXT_PUBLIC_FAVICON || '/favicon.ico', // blog favicon 配置,默认使用 /public/favicon.ico,支持在线图片,如 https://img.imesong.com/favicon.png
BEI_AN: process.env.NEXT_PUBLIC_BEI_AN || '', // 备案号 闽 ICP 备 XXXXXX
- BEI_AN_LINK: process.env.NEXT_PUBLIC_BEI_AN_LINK || 'https://beian.miit.gov.cn/', // 备案查询链接,如果用了萌备等备案请在这里填写
+ BEI_AN_LINK:
+ process.env.NEXT_PUBLIC_BEI_AN_LINK || 'https://beian.miit.gov.cn/', // 备案查询链接,如果用了萌备等备案请在这里填写
// RSS 订阅
ENABLE_RSS: process.env.NEXT_PUBLIC_ENABLE_RSS || true, // 是否开启 RSS 订阅功能
diff --git a/themes/typography/components/BlogItem.js b/themes/typography/components/BlogItem.js
index b880f1fe..195f9be2 100644
--- a/themes/typography/components/BlogItem.js
+++ b/themes/typography/components/BlogItem.js
@@ -14,7 +14,6 @@ export const BlogItem = props => {
const showPageCover = siteConfig('SIMPLE_POST_COVER_ENABLE', false, CONFIG)
const showPreview =
siteConfig('POST_LIST_PREVIEW', false, NOTION_CONFIG) && post.blockMap
- console.log(post);
return (
+
{posts?.map((p, index) => (
diff --git a/themes/typography/components/Catalog.js b/themes/typography/components/Catalog.js
index 6bea8bab..e92e586a 100644
--- a/themes/typography/components/Catalog.js
+++ b/themes/typography/components/Catalog.js
@@ -18,38 +18,71 @@ const Catalog = ({ post }) => {
// 监听滚动事件
useEffect(() => {
- const throttleMs = 200
+ // 如果没有文章或目录,不执行任何操作
+ if (!post || !post?.toc || post?.toc?.length < 1) {
+ return
+ }
+
+ const throttleMs = 100 // 降低节流时间提高响应速度
+
const actionSectionScrollSpy = throttle(() => {
const sections = document.getElementsByClassName('notion-h')
+ if (!sections || sections.length === 0) return
+
let prevBBox = null
- let currentSectionId = activeSection
+ let currentSectionId = null
+
+ // 先检查当前视口中的所有标题
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)
+ const offset = 100 // 固定偏移量,避免计算不稳定
+
+ // 如果标题在视口上方或接近顶部,认为是当前标题
if (bbox.top - offset < 0) {
currentSectionId = section.getAttribute('data-id')
prevBBox = bbox
- continue
+ } else {
+ // 找到第一个在视口下方的标题就停止
+ break
+ }
+ }
+
+ // 如果没找到任何标题在视口上方,使用第一个标题
+ if (!currentSectionId && sections.length > 0) {
+ currentSectionId = sections[0].getAttribute('data-id')
+ }
+
+ // 只有当 ID 变化时才更新状态,减少不必要的渲染
+ if (currentSectionId !== activeSection) {
+ setActiveSection(currentSectionId)
+
+ // 查找目录中对应的索引并滚动
+ const index = post?.toc?.findIndex(
+ obj => uuidToId(obj.id) === currentSectionId
+ )
+
+ if (index !== -1 && tRef?.current) {
+ tRef.current.scrollTo({ top: 28 * index, behavior: 'smooth' })
}
- break
}
- setActiveSection(currentSectionId)
- const index = post?.toc?.findIndex(
- obj => uuidToId(obj.id) === currentSectionId
- )
- tRef?.current?.scrollTo({ top: 28 * index, behavior: 'smooth' })
}, throttleMs)
-
- window.addEventListener('scroll', actionSectionScrollSpy)
- actionSectionScrollSpy()
+
+ const content = document.querySelector('#container-inner')
+ if (!content) return // 防止 content 不存在
+
+ // 添加滚动和内容变化的监听
+ content.addEventListener('scroll', actionSectionScrollSpy)
+
+ // 初始执行一次
+ setTimeout(() => {
+ actionSectionScrollSpy()
+ }, 300) // 延迟执行确保 DOM 已完全加载
+
return () => {
- window.removeEventListener('scroll', actionSectionScrollSpy)
+ content?.removeEventListener('scroll', actionSectionScrollSpy)
}
}, [post])
@@ -68,21 +101,27 @@ const Catalog = ({ post }) => {
-