diff --git a/.env.local b/.env.local
index 4d4824d5bb6..63ecde97f4c 100644
--- a/.env.local
+++ b/.env.local
@@ -1,5 +1,5 @@
# 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables
-NEXT_PUBLIC_VERSION=4.7.0
+NEXT_PUBLIC_VERSION=4.7.1
# 可在此添加环境变量,去掉最左边的(# )注释即可
diff --git a/components/ExternalPlugins.js b/components/ExternalPlugins.js
index ce0b5e63ab0..d798a940cdd 100644
--- a/components/ExternalPlugins.js
+++ b/components/ExternalPlugins.js
@@ -63,7 +63,8 @@ const ExternalPlugin = props => {
const MOUSE_FOLLOW = siteConfig('MOUSE_FOLLOW')
const CUSTOM_EXTERNAL_CSS = siteConfig('CUSTOM_EXTERNAL_CSS')
const CUSTOM_EXTERNAL_JS = siteConfig('CUSTOM_EXTERNAL_JS')
- const ENABLE_NPROGRSS = siteConfig('ENABLE_NPROGRSS', true)
+ // 默认关闭NProgress
+ const ENABLE_NPROGRSS = siteConfig('ENABLE_NPROGRSS', false)
// 自定义样式css和js引入
if (isBrowser) {
diff --git a/components/OpenWrite.js b/components/OpenWrite.js
index 973931a80b9..5caf5d7b105 100644
--- a/components/OpenWrite.js
+++ b/components/OpenWrite.js
@@ -73,8 +73,11 @@ const OpenWrite = () => {
console.error('OpenWrite 加载异常', error)
}
}
-
useEffect(() => {
+ if (process.env.NODE_ENV === 'development') {
+ console.log('开发环境:屏蔽OpenWrite')
+ return
+ }
if (isBrowser && blogId) {
// Check if the element with id 'read-more-wrap' already exists
const readMoreWrap = document.getElementById('read-more-wrap')
diff --git a/components/ShareBar.js b/components/ShareBar.js
index 334a5f128c2..1acbef4e991 100644
--- a/components/ShareBar.js
+++ b/components/ShareBar.js
@@ -1,7 +1,9 @@
import { siteConfig } from '@/lib/config'
import dynamic from 'next/dynamic'
-const ShareButtons = dynamic(() => import('@/components/ShareButtons'), { ssr: false })
+const ShareButtons = dynamic(() => import('@/components/ShareButtons'), {
+ ssr: false
+})
/**
* 分享栏
@@ -9,14 +11,20 @@ const ShareButtons = dynamic(() => import('@/components/ShareButtons'), { ssr: f
* @returns
*/
const ShareBar = ({ post }) => {
- if (!JSON.parse(siteConfig('POST_SHARE_BAR_ENABLE')) || !post || post?.type !== 'Post') {
+ if (
+ !JSON.parse(siteConfig('POST_SHARE_BAR_ENABLE')) ||
+ !post ||
+ post?.type !== 'Post'
+ ) {
return <>>
}
- return
-
-
-
+ return (
+
+ )
}
export default ShareBar
diff --git a/lib/cache/cache_manager.js b/lib/cache/cache_manager.js
index 6070958cc23..bbfbfdacb68 100644
--- a/lib/cache/cache_manager.js
+++ b/lib/cache/cache_manager.js
@@ -8,7 +8,7 @@ import MemoryCache from './memory_cache'
* @returns
*/
export async function getDataFromCache(key, force) {
- if (JSON.parse(BLOG.ENABLE_CACHE) || force) {
+ if (BLOG.ENABLE_CACHE || force) {
const dataFromCache = await getApi().getCache(key)
if (JSON.stringify(dataFromCache) === '[]') {
return null
diff --git a/package.json b/package.json
index 2e523fbcd40..cb5c337f52f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "notion-next",
- "version": "4.7.0",
+ "version": "4.7.1",
"homepage": "https://github.com/tangly1024/NotionNext.git",
"license": "MIT",
"repository": {
diff --git a/styles/notion.css b/styles/notion.css
index ba794f88772..0685e3042e9 100644
--- a/styles/notion.css
+++ b/styles/notion.css
@@ -403,7 +403,8 @@ summary > .notion-h {
.notion-h1 {
font-size: 1.575em;
margin-top: 1.08em;
- @apply border-b w-full;
+ @apply w-full;
+ /* @apply border-b w-full; */
}
.notion-h2 {
@apply w-full;
@@ -554,7 +555,7 @@ summary > .notion-h {
width: 100%;
margin: 6px 0;
padding: 0;
- border-bottom-width: 1px;
+ /* border-bottom-width: 1px; */
}
.notion-link {
@@ -1664,9 +1665,8 @@ code[class*='language-'] {
}
/* NOTION CSS OVERRIDE */
-
.notion {
- @apply dark:text-gray-300;
+ @apply dark:text-gray-100;
overflow-wrap: break-word;
}
.notion,
diff --git a/themes/magzine/components/Announcement.js b/themes/magzine/components/Announcement.js
new file mode 100644
index 00000000000..62ab01d00bc
--- /dev/null
+++ b/themes/magzine/components/Announcement.js
@@ -0,0 +1,30 @@
+// import { useGlobal } from '@/lib/global'
+import dynamic from 'next/dynamic'
+
+const NotionPage = dynamic(() => import('@/components/NotionPage'))
+
+/**
+ * Magzine主题的公告
+ */
+const Announcement = ({ post, className }) => {
+ // const { locale } = useGlobal()
+ if (post?.blockMap) {
+ return (
+
+
+ {/* {locale.COMMON.ANNOUNCEMENT}
*/}
+ {post && (
+
+
+
+ )}
+
+
+ )
+ } else {
+ return <>>
+ }
+}
+export default Announcement
diff --git a/themes/magzine/components/ArticleAround.js b/themes/magzine/components/ArticleAround.js
new file mode 100644
index 00000000000..95b6f83fc1d
--- /dev/null
+++ b/themes/magzine/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/magzine/components/ArticleInfo.js b/themes/magzine/components/ArticleInfo.js
new file mode 100644
index 00000000000..f41654b0a3a
--- /dev/null
+++ b/themes/magzine/components/ArticleInfo.js
@@ -0,0 +1,55 @@
+import LazyImage from '@/components/LazyImage'
+import NotionIcon from '@/components/NotionIcon'
+import { siteConfig } from '@/lib/config'
+import CategoryItem from './CategoryItem'
+import TagItemMini from './TagItemMini'
+
+/**
+ * 文章详情页介绍
+ * @param {*} props
+ * @returns
+ */
+export default function ArticleInfo(props) {
+ const { post, siteInfo } = props
+
+ return (
+ <>
+
+
+ {siteConfig('MAGZINE_POST_LIST_CATEGORY') && (
+
+ )}
+
+ {siteConfig('MAGZINE_POST_LIST_TAG') &&
+ post?.tagItems?.map(tag => (
+
+ ))}
+
+
+
+ {/* title */}
+
+ {siteConfig('POST_TITLE_ICON') && (
+
+ )}
+ {post?.title}
+
+
+
{post?.summary}
+
+
+ {post?.type && !post?.type !== 'Page' && post?.pageCover && (
+
+
+
+ )}
+ >
+ )
+}
diff --git a/themes/magzine/components/ArticleLock.js b/themes/magzine/components/ArticleLock.js
new file mode 100644
index 00000000000..3047e7d24f6
--- /dev/null
+++ b/themes/magzine/components/ArticleLock.js
@@ -0,0 +1,61 @@
+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 leading-10 text-black bg-gray-100 dark:bg-gray-500'>
+
+
+ {locale.COMMON.SUBMIT}
+
+
+
+
+
+
+ )
+}
diff --git a/themes/magzine/components/BannerFullWidth.js b/themes/magzine/components/BannerFullWidth.js
new file mode 100644
index 00000000000..fadebaa6008
--- /dev/null
+++ b/themes/magzine/components/BannerFullWidth.js
@@ -0,0 +1,30 @@
+import LazyImage from '@/components/LazyImage'
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import BannerItem from './BannerItem'
+
+/**
+ * 全宽
+ * @param {*} props
+ * @returns
+ */
+export default function BannerFullWidth() {
+ const { siteInfo } = useGlobal()
+ const banner = siteConfig('MAGZINE_HOME_BANNER_ENABLE')
+ if (!banner) {
+ return null
+ }
+ return (
+
+ )
+}
diff --git a/themes/magzine/components/BannerItem.js b/themes/magzine/components/BannerItem.js
new file mode 100644
index 00000000000..3e77271a010
--- /dev/null
+++ b/themes/magzine/components/BannerItem.js
@@ -0,0 +1,35 @@
+import { siteConfig } from '@/lib/config'
+import Link from 'next/link'
+
+/**
+ * 文字广告Banner
+ * @param {*} props
+ * @returns
+ */
+export default function BannerItem() {
+ // 首屏信息栏按钮文字
+ const banner = siteConfig('MAGZINE_HOME_BANNER_ENABLE')
+ const button = siteConfig('MAGZINE_HOME_BUTTON')
+ const text = siteConfig('MAGZINE_HOME_BUTTON_TEXT')
+ const url = siteConfig('MAGZINE_HOME_BUTTON_URL')
+ const title = siteConfig('MAGZINE_HOME_TITLE')
+ const description = siteConfig('MAGZINE_HOME_DESCRIPTION')
+ const tips = siteConfig('MAGZINE_HOME_TIPS')
+ if (!banner) {
+ return null
+ }
+
+ return (
+
+ {/* 首屏导航按钮 */}
+
{title}
+
{description}
+ {button && (
+
+ {text}
+
+ )}
+
{tips}
+
+ )
+}
diff --git a/themes/magzine/components/Card.js b/themes/magzine/components/Card.js
new file mode 100644
index 00000000000..d24c046e476
--- /dev/null
+++ b/themes/magzine/components/Card.js
@@ -0,0 +1,9 @@
+const Card = ({ children, headerSlot, className }) => {
+ return
+ <>{headerSlot}>
+
+
+}
+export default Card
diff --git a/themes/magzine/components/Catalog.js b/themes/magzine/components/Catalog.js
new file mode 100644
index 00000000000..b127d24aad2
--- /dev/null
+++ b/themes/magzine/components/Catalog.js
@@ -0,0 +1,99 @@
+import throttle from 'lodash.throttle'
+import { uuidToId } from 'notion-utils'
+import { useCallback, useEffect, useRef, useState } from 'react'
+import Progress from './Progress'
+
+/**
+ * 目录导航组件
+ * @param toc
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const Catalog = ({ toc, className }) => {
+ const tocIds = []
+
+ // 目录自动滚动
+ const tRef = useRef(null)
+ // 同步选中目录事件
+ const [activeSection, setActiveSection] = useState(null)
+
+ // 监听滚动事件
+ useEffect(() => {
+ window.addEventListener('scroll', actionSectionScrollSpy)
+ actionSectionScrollSpy()
+ return () => {
+ window.removeEventListener('scroll', actionSectionScrollSpy)
+ }
+ }, [])
+
+ 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
+ tRef?.current?.scrollTo({ top: 28 * index, behavior: 'smooth' })
+ }, throttleMs)
+ )
+
+ // 无目录就直接返回空
+ if (!toc || toc.length < 1) {
+ return <>>
+ }
+
+ return (
+
+
+
+
+
+
+ )
+}
+
+export default Catalog
diff --git a/themes/magzine/components/CategoryGroup.js b/themes/magzine/components/CategoryGroup.js
new file mode 100644
index 00000000000..063eb58cbb9
--- /dev/null
+++ b/themes/magzine/components/CategoryGroup.js
@@ -0,0 +1,34 @@
+import { useGlobal } from '@/lib/global'
+import CategoryItem from './CategoryItem'
+
+/**
+ * 分类
+ * @param {*} param0
+ * @returns
+ */
+const CategoryGroup = ({ currentCategory, categoryOptions }) => {
+ const { locale } = useGlobal()
+ if (!categoryOptions) {
+ return <>>
+ }
+ return (
+
+
{locale.COMMON.CATEGORY}
+
+ {categoryOptions?.map(category => {
+ const selected = currentCategory === category.name
+ return (
+
+ )
+ })}
+
+
+ )
+}
+
+export default CategoryGroup
diff --git a/themes/magzine/components/CategoryItem.js b/themes/magzine/components/CategoryItem.js
new file mode 100644
index 00000000000..44fd3845f82
--- /dev/null
+++ b/themes/magzine/components/CategoryItem.js
@@ -0,0 +1,19 @@
+import Link from 'next/link'
+
+export default function CategoryItem({ selected, category, categoryCount }) {
+ return (
+
+
+ {category} {categoryCount && `(${categoryCount})`}
+
+
+ )
+}
diff --git a/themes/magzine/components/Footer.js b/themes/magzine/components/Footer.js
new file mode 100644
index 00000000000..4d20657d668
--- /dev/null
+++ b/themes/magzine/components/Footer.js
@@ -0,0 +1,114 @@
+import DarkModeButton from '@/components/DarkModeButton'
+import LazyImage from '@/components/LazyImage'
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import SocialButton from './SocialButton'
+
+/**
+ * 网页底脚
+ */
+const Footer = ({ title }) => {
+ const d = new Date()
+ const currentYear = d.getFullYear()
+ const since = siteConfig('SINCE')
+ const copyrightDate =
+ parseInt(since) < currentYear ? since + '-' + currentYear : currentYear
+ const { siteInfo } = useGlobal()
+ const MAGZINE_FOOTER_LINKS = siteConfig('MAGZINE_FOOTER_LINKS', [])
+
+ return (
+
+ )
+}
+
+export default Footer
diff --git a/themes/magzine/components/Header.js b/themes/magzine/components/Header.js
new file mode 100644
index 00000000000..a7c4afdebd0
--- /dev/null
+++ b/themes/magzine/components/Header.js
@@ -0,0 +1,196 @@
+import Collapse from '@/components/Collapse'
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import throttle from 'lodash.throttle'
+import { useRouter } from 'next/router'
+import { useEffect, useRef, useState } from 'react'
+import CONFIG from '../config'
+import LogoBar from './LogoBar'
+import { MenuBarMobile } from './MenuBarMobile'
+import { MenuItemDrop } from './MenuItemDrop'
+
+/**
+ * 顶部导航栏 + 菜单
+ * @param {} param0
+ * @returns
+ */
+export default function Header(props) {
+ const { customNav, customMenu } = props
+ const [isOpen, changeShow] = useState(false)
+ const collapseRef = useRef(null)
+ const lastScrollY = useRef(0) // 用于存储上一次的滚动位置
+ const { locale } = useGlobal()
+ const router = useRouter()
+
+ const defaultLinks = [
+ {
+ icon: 'fas fa-th',
+ name: locale.COMMON.CATEGORY,
+ href: '/category',
+ show: CONFIG.MENU_CATEGORY
+ },
+ {
+ icon: 'fas fa-tag',
+ name: locale.COMMON.TAGS,
+ href: '/tag',
+ show: CONFIG.MENU_TAG
+ },
+ {
+ icon: 'fas fa-archive',
+ name: locale.NAV.ARCHIVE,
+ href: '/archive',
+ show: CONFIG.MENU_ARCHIVE
+ },
+ {
+ icon: 'fas fa-search',
+ name: locale.NAV.SEARCH,
+ href: '/search',
+ show: CONFIG.MENU_SEARCH
+ }
+ ]
+
+ let links = defaultLinks.concat(customNav)
+
+ const toggleMenuOpen = () => {
+ changeShow(!isOpen)
+ }
+
+ // 向下滚动时,调整导航条高度
+ useEffect(() => {
+ scrollTrigger()
+ window.addEventListener('scroll', scrollTrigger)
+ return () => {
+ window.removeEventListener('scroll', scrollTrigger)
+ }
+ }, [])
+
+ const throttleMs = 150
+
+ const scrollTrigger = throttle(() => {
+ const scrollS = window.scrollY
+ if (scrollS === lastScrollY.current) return // 如果滚动位置没有变化,则不做任何操作
+
+ const nav = document.querySelector('#top-navbar')
+ const narrowNav = scrollS > 60
+ if (narrowNav) {
+ nav && nav.classList.replace('h-20', 'h-14')
+ } else {
+ nav && nav.classList.replace('h-14', 'h-20')
+ }
+
+ lastScrollY.current = scrollS // 更新上一次的滚动位置
+ }, throttleMs)
+
+ const [showSearchInput, changeShowSearchInput] = useState(false)
+
+ // 展示搜索框
+ const toggleShowSearchInput = () => {
+ if (siteConfig('ALGOLIA_APP_ID')) {
+ searchModal.current.openSearch()
+ } else {
+ changeShowSearchInput(!showSearchInput)
+ }
+ }
+
+ const onKeyUp = e => {
+ if (e.keyCode === 13) {
+ const search = document.getElementById('simple-search').value
+ if (search) {
+ router.push({ pathname: '/search/' + search })
+ }
+ }
+ }
+
+ // 如果 开启自定义菜单,则覆盖Page生成的菜单
+ if (siteConfig('CUSTOM_MENU')) {
+ links = customMenu
+ }
+
+ if (!links || links.length === 0) {
+ return null
+ }
+
+ return (
+
+ {/* 导航栏菜单内容 */}
+
+ {/* 搜索栏 */}
+ {showSearchInput && (
+
+ )}
+
+ {/* 默认菜单 */}
+ {!showSearchInput && (
+ <>
+ {/* 左侧图标Logo */}
+
+
+ {/* 桌面端顶部菜单 */}
+
+ {links &&
+ links?.map((link, index) => (
+
+ ))}
+
+
+ >
+ )}
+
+ {/* 右侧移动端折叠按钮 */}
+
+ {/* 搜索按钮 */}
+
+
+
+
+
+ {isOpen ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {/* 移动端折叠菜单 */}
+
+
+
+ collapseRef.current?.updateCollapseHeight(param)
+ }
+ />
+
+
+
+ )
+}
diff --git a/themes/magzine/components/Hero.js b/themes/magzine/components/Hero.js
new file mode 100644
index 00000000000..2e463a61f96
--- /dev/null
+++ b/themes/magzine/components/Hero.js
@@ -0,0 +1,40 @@
+// import { useGlobal } from '@/lib/global'
+import BannerItem from './BannerItem'
+import PostItemCardTop from './PostItemCardTop'
+import PostItemCardWide from './PostItemCardWide'
+
+/**
+ * 首页主宣传
+ * @param {*} param0
+ * @returns
+ */
+const Hero = ({ posts }) => {
+ // 获取置顶文章与次要文章
+ const postTop = posts[0]
+ const post1 = posts[1]
+ const post2 = posts[2]
+ return (
+ <>
+
+ {/* 左侧一篇主要置顶文章 */}
+
+ {/* 右侧 */}
+
+ {/* 首屏介绍 */}
+
+
+ {/* 两篇次要文章 */}
+
+
+
+ >
+ )
+}
+export default Hero
diff --git a/themes/magzine/components/InfoCard.js b/themes/magzine/components/InfoCard.js
new file mode 100644
index 00000000000..e6009de1e79
--- /dev/null
+++ b/themes/magzine/components/InfoCard.js
@@ -0,0 +1,38 @@
+import LazyImage from '@/components/LazyImage'
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+
+/**
+ * 用户信息卡
+ * @param {*} props
+ * @returns
+ */
+const InfoCard = props => {
+ const { siteInfo } = useGlobal()
+
+ return (
+
+
+
{
+ Router.push('/about')
+ }}>
+
+
+
+ {siteConfig('AUTHOR')}
+
+
+ {siteConfig('BIO')}
+
+
+
+ )
+}
+
+export default InfoCard
diff --git a/themes/magzine/components/JumpToTopButton.js b/themes/magzine/components/JumpToTopButton.js
new file mode 100644
index 00000000000..5e8a5418d2a
--- /dev/null
+++ b/themes/magzine/components/JumpToTopButton.js
@@ -0,0 +1,33 @@
+import { siteConfig } from '@/lib/config'
+
+/**
+ * 跳转到网页顶部
+ * 当屏幕下滑500像素后会出现该控件
+ * @param targetRef 关联高度的目标html标签
+ * @param showPercent 是否显示百分比
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const JumpToTopButton = ({ showPercent = false, percent, className }) => {
+ if (!siteConfig('MAGZINE_WIDGET_TO_TOP')) {
+ return <>>
+ }
+ return (
+
+ {
+ window.scrollTo({ top: 0, behavior: 'smooth' })
+ }}
+ />
+
+ )
+}
+
+export default JumpToTopButton
diff --git a/themes/magzine/components/LeftMenuBar.js b/themes/magzine/components/LeftMenuBar.js
new file mode 100644
index 00000000000..6bde6c51b85
--- /dev/null
+++ b/themes/magzine/components/LeftMenuBar.js
@@ -0,0 +1,15 @@
+import Link from 'next/link'
+
+export default function LeftMenuBar () {
+ return (
+
+ );
+}
diff --git a/themes/magzine/components/LogoBar.js b/themes/magzine/components/LogoBar.js
new file mode 100644
index 00000000000..c484f34aebe
--- /dev/null
+++ b/themes/magzine/components/LogoBar.js
@@ -0,0 +1,22 @@
+import { siteConfig } from '@/lib/config'
+import Link from 'next/link'
+
+export default function LogoBar(props) {
+ const { siteInfo } = props
+ return (
+
+
+ {/* */}
+ {siteConfig('TITLE')}
+
+
+ )
+}
diff --git a/themes/magzine/components/MenuBarMobile.js b/themes/magzine/components/MenuBarMobile.js
new file mode 100644
index 00000000000..9e32e9f53b1
--- /dev/null
+++ b/themes/magzine/components/MenuBarMobile.js
@@ -0,0 +1,53 @@
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import { MenuItemCollapse } from './MenuItemCollapse'
+
+export const MenuBarMobile = props => {
+ const { customMenu, customNav } = props
+ const { locale } = useGlobal()
+
+ let links = [
+ // { name: locale.NAV.INDEX, href: '/' || '/', show: true },
+ {
+ name: locale.COMMON.CATEGORY,
+ href: '/category',
+ show: siteConfig('MAGZINE_MENU_CATEGORY')
+ },
+ {
+ name: locale.COMMON.TAGS,
+ href: '/tag',
+ show: siteConfig('MAGZINE_MENU_TAG')
+ },
+ {
+ name: locale.NAV.ARCHIVE,
+ href: '/archive',
+ show: siteConfig('MAGZINE_MENU_ARCHIVE')
+ }
+ // { name: locale.NAV.SEARCH, href: '/search', show: siteConfig('MENU_SEARCH', ) }
+ ]
+
+ 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/magzine/components/MenuItemCollapse.js b/themes/magzine/components/MenuItemCollapse.js
new file mode 100644
index 00000000000..fcd8b75612b
--- /dev/null
+++ b/themes/magzine/components/MenuItemCollapse.js
@@ -0,0 +1,97 @@
+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.href || router.asPath === link.href
+
+ const toggleShow = () => {
+ changeShow(!show)
+ }
+
+ const toggleOpenSubMenu = () => {
+ changeIsOpen(!isOpen)
+ }
+
+ return (
+ <>
+
+ {!hasSubMenu && (
+
+
+
+ )}
+
+ {hasSubMenu && (
+
+ )}
+
+
+ {/* 折叠子菜单 */}
+ {hasSubMenu && (
+
+ {link?.subMenus?.map(sLink => {
+ return (
+
+ )
+ })}
+
+ )}
+ >
+ )
+}
diff --git a/themes/magzine/components/MenuItemDrop.js b/themes/magzine/components/MenuItemDrop.js
new file mode 100644
index 00000000000..1a629248b55
--- /dev/null
+++ b/themes/magzine/components/MenuItemDrop.js
@@ -0,0 +1,76 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useState } from 'react'
+
+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.href || router.asPath === link.href
+
+ return (
+
changeShow(true)}
+ onMouseOut={() => changeShow(false)}>
+ {hasSubMenu && (
+
+
+ {link?.icon && } {link?.name}
+ {hasSubMenu && (
+
+ )}
+
+
+ )}
+
+ {!hasSubMenu && (
+
+
+ {link?.icon && } {link?.name}
+
+
+ )}
+
+ {/* 子菜单 */}
+ {hasSubMenu && (
+
+ {link?.subMenus?.map(sLink => {
+ return (
+ -
+
+
+ {link?.icon && }
+ {sLink.title}
+
+
+
+ )
+ })}
+
+ )}
+
+ )
+}
diff --git a/themes/magzine/components/MenuItemMobileNormal.js b/themes/magzine/components/MenuItemMobileNormal.js
new file mode 100644
index 00000000000..33569bfb5ca
--- /dev/null
+++ b/themes/magzine/components/MenuItemMobileNormal.js
@@ -0,0 +1,29 @@
+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.href || router.asPath === link.href
+
+ return (
+
+
+ {link.slot}
+
+ )
+}
diff --git a/themes/magzine/components/MenuItemPCNormal.js b/themes/magzine/components/MenuItemPCNormal.js
new file mode 100644
index 00000000000..e93ca07f070
--- /dev/null
+++ b/themes/magzine/components/MenuItemPCNormal.js
@@ -0,0 +1,30 @@
+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.href || router.asPath === link.href
+ if (!link || !link.show) {
+ return null
+ }
+
+ return (
+
+
+ {link.slot}
+
+ )
+}
diff --git a/themes/magzine/components/PaginationSimple.js b/themes/magzine/components/PaginationSimple.js
new file mode 100644
index 00000000000..0fcbebab48e
--- /dev/null
+++ b/themes/magzine/components/PaginationSimple.js
@@ -0,0 +1,55 @@
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+/**
+ * 简易翻页插件
+ * @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
+ .split('?')[0]
+ .replace(/\/page\/[1-9]\d*/, '')
+ .replace(/\/$/, '')
+
+ return (
+
+
+ ←{locale.PAGINATION.PREV}
+
+
+ {locale.PAGINATION.NEXT}→
+
+
+ )
+}
+
+export default PaginationSimple
diff --git a/themes/magzine/components/PostBannerGroupByCategory.js b/themes/magzine/components/PostBannerGroupByCategory.js
new file mode 100644
index 00000000000..ee16008c435
--- /dev/null
+++ b/themes/magzine/components/PostBannerGroupByCategory.js
@@ -0,0 +1,87 @@
+import { siteConfig } from '@/lib/config'
+import PostListHorizontal from './PostListHorizontal'
+
+/**
+ * 按文章类别分组的文章列表区块
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const PostBannerGroupByCategory = props => {
+ const { posts, categoryOptions, allNavPages, latestPosts } = props
+ if (!posts || posts.length === 0) {
+ return null
+ }
+
+ // 按分类将文章分组成文件夹
+ const categoryFolders = groupArticles(categoryOptions, allNavPages.slice(8))
+
+ return (
+ <>
+ {/* 不同的分类文章列表 */}
+ {categoryFolders?.map((categoryGroup, index) => {
+ if (
+ !categoryGroup ||
+ !categoryGroup.items ||
+ categoryGroup.items.length < 1
+ ) {
+ return null
+ }
+
+ return (
+
+ )
+ })}
+ >
+ )
+}
+
+// 按照分类将文章分组成文件夹
+function groupArticles(categoryOptions, allPosts) {
+ if (!allPosts) {
+ return []
+ }
+ const groups = []
+
+ for (let i = 0; i < allPosts.length; i++) {
+ const item = allPosts[i]
+ const categoryName = item?.category ? item?.category : '' // 将 category 转换为字符串
+
+ let existingGroup = groups.find(group => group.category === categoryName) // 搜索同名的最后一个分组
+
+ if (existingGroup && existingGroup.category === categoryName) {
+ // 如果分组已存在,并且该分组中的文章数量小于4,添加文章
+ if (existingGroup.items.length < 4) {
+ existingGroup.items.push(item)
+ }
+ } else {
+ // 新建分组,并添加当前文章
+ groups.push({ category: categoryName, items: [item] })
+ }
+ }
+ const hiddenCategory = siteConfig('MAGZINE_HOME_HIDDEN_CATEGORY')
+ // 按照 categoryOptions 的顺序排序 groups
+ const sortedGroups = []
+ for (let i = 0; i < categoryOptions.length; i++) {
+ const option = categoryOptions[i]
+ const matchingGroup = groups.find(group => group.category === option.name)
+
+ if (matchingGroup) {
+ if (
+ hiddenCategory &&
+ hiddenCategory.indexOf(matchingGroup.category) >= 0
+ ) {
+ continue
+ }
+ sortedGroups.push(matchingGroup)
+ }
+ }
+ return sortedGroups
+}
+
+export default PostBannerGroupByCategory
diff --git a/themes/magzine/components/PostGroupArchive.js b/themes/magzine/components/PostGroupArchive.js
new file mode 100644
index 00000000000..c4369e9a2c1
--- /dev/null
+++ b/themes/magzine/components/PostGroupArchive.js
@@ -0,0 +1,34 @@
+import PostItemCard from './PostItemCard'
+
+/**
+ * 博客归档列表
+ * @param posts 所有文章
+ * @param archiveTitle 归档标题
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const PostGroupArchive = ({ posts = [], archiveTitle }) => {
+ if (!posts || posts.length === 0) {
+ return <>>
+ }
+
+ return (
+
+ {/* 分组标题 */}
+
+ {archiveTitle}
+
+
+ {/* 列表 */}
+
+ {posts?.map((p, index) => {
+ return
+ })}
+
+
+ )
+}
+
+export default PostGroupArchive
diff --git a/themes/magzine/components/PostGroupLatest.js b/themes/magzine/components/PostGroupLatest.js
new file mode 100644
index 00000000000..8731e7d6317
--- /dev/null
+++ b/themes/magzine/components/PostGroupLatest.js
@@ -0,0 +1,71 @@
+import LazyImage from '@/components/LazyImage'
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+// import Image from 'next/image'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+/**
+ * 最新文章列表
+ * @param posts 所有文章数据
+ * @param sliceCount 截取展示的数量 默认6
+ * @constructor
+ */
+const PostGroupLatest = props => {
+ const { latestPosts } = props
+ // 获取当前路径
+ const currentPath = useRouter().asPath
+ const { locale, siteInfo } = useGlobal()
+ if (!latestPosts) {
+ return <>>
+ }
+
+ return (
+ <>
+ {/* 标题 */}
+
+
{locale.COMMON.LATEST_POSTS}
+
+
+ {/* 文章列表 */}
+
+ {latestPosts.map(post => {
+ const selected =
+ currentPath === `${siteConfig('SUB_PATH', '')}/${post.slug}`
+
+ const headerImage = post?.pageCoverThumbnail
+ ? post.pageCoverThumbnail
+ : siteInfo?.pageCover
+
+ return (
+
+
+
+
+
+
+
{post.title}
+
{post.lastEditedDay}
+
+
+
+ )
+ })}
+
+ >
+ )
+}
+export default PostGroupLatest
diff --git a/themes/magzine/components/PostItemCard.js b/themes/magzine/components/PostItemCard.js
new file mode 100644
index 00000000000..6b4f811ad53
--- /dev/null
+++ b/themes/magzine/components/PostItemCard.js
@@ -0,0 +1,58 @@
+import LazyImage from '@/components/LazyImage'
+import NotionIcon from '@/components/NotionIcon'
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import CategoryItem from './CategoryItem'
+
+/**
+ * 普通的博客卡牌
+ * 带封面图
+ */
+const PostItemCard = ({ post }) => {
+ const { siteInfo } = useGlobal()
+ const cover = post?.pageCoverThumbnail || siteInfo?.pageCover
+ return (
+
+
+ {siteConfig('MAGZINE_POST_LIST_COVER') && (
+
+
+
+
+
+ )}
+ {siteConfig('MAGZINE_POST_LIST_CATEGORY') && (
+
+ )}
+
+
+
+ {siteConfig('POST_TITLE_ICON') && (
+
+ )}
+ {post.title}
+
+
+
+
{post.date?.start_date}
+
+
+ )
+}
+
+export default PostItemCard
diff --git a/themes/magzine/components/PostItemCardSimple.js b/themes/magzine/components/PostItemCardSimple.js
new file mode 100644
index 00000000000..3cf8a1ecfd1
--- /dev/null
+++ b/themes/magzine/components/PostItemCardSimple.js
@@ -0,0 +1,43 @@
+import NotionIcon from '@/components/NotionIcon'
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import CategoryItem from './CategoryItem'
+
+/**
+ * 不带图片
+ * @param {*} param0
+ * @returns
+ */
+const PostItemCardSimple = ({ post, showSummary }) => {
+ const showPreview = siteConfig('MAGZINE_POST_LIST_PREVIEW') && post.blockMap
+ const { locale } = useGlobal()
+ return (
+
+
+ {siteConfig('MAGZINE_POST_LIST_CATEGORY') && (
+
+ )}
+
+
+ {/* 文章标题 */}
+
+
+ {siteConfig('POST_TITLE_ICON') && }
+ {post.title}
+
+
+
+
{post.date?.start_date}
+
+ )
+}
+
+export default PostItemCardSimple
diff --git a/themes/magzine/components/PostItemCardTop.js b/themes/magzine/components/PostItemCardTop.js
new file mode 100644
index 00000000000..24f2a00df2a
--- /dev/null
+++ b/themes/magzine/components/PostItemCardTop.js
@@ -0,0 +1,104 @@
+import LazyImage from '@/components/LazyImage'
+import NotionIcon from '@/components/NotionIcon'
+import NotionPage from '@/components/NotionPage'
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import CategoryItem from './CategoryItem'
+import TagItemMini from './TagItemMini'
+
+/**
+ * 置顶头条文章
+ * @param {*} param0
+ * @returns
+ */
+const PostItemCardTop = ({ post, showSummary }) => {
+ const showPreview = siteConfig('MAGZINE_POST_LIST_PREVIEW') && post.blockMap
+ const { locale } = useGlobal()
+ return (
+
+
+ {siteConfig('MAGZINE_POST_LIST_COVER') && (
+
+
+
+
+
+ )}
+
+
+ {siteConfig('MAGZINE_POST_LIST_CATEGORY') && (
+
+ )}
+
+ {siteConfig('MAGZINE_POST_LIST_TAG') &&
+ post?.tagItems?.map(tag => (
+
+ ))}
+
+
+
+
+
+ {siteConfig('POST_TITLE_ICON') && (
+
+ )}
+ {post.title}
+
+
+
+
+
+ {(!showPreview || showSummary) && (
+
+ {post.summary}
+
+ )}
+
+ {showPreview && (
+
+
+
+
+
+ {locale.COMMON.ARTICLE_DETAIL}
+
+
+
+
+
+ )}
+
+
{post.date?.start_date}
+
+
+ )
+}
+
+export default PostItemCardTop
diff --git a/themes/magzine/components/PostItemCardWide.js b/themes/magzine/components/PostItemCardWide.js
new file mode 100644
index 00000000000..11487e7f9a4
--- /dev/null
+++ b/themes/magzine/components/PostItemCardWide.js
@@ -0,0 +1,86 @@
+import LazyImage from '@/components/LazyImage'
+import NotionIcon from '@/components/NotionIcon'
+import NotionPage from '@/components/NotionPage'
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import CategoryItem from './CategoryItem'
+import TagItemMini from './TagItemMini'
+
+/**
+ * 水平左右布局的博客卡片
+ * @param {*} param0
+ * @returns
+ */
+const PostItemCardWide = ({ post, showSummary }) => {
+ const showPreview = siteConfig('MAGZINE_POST_LIST_PREVIEW') && post.blockMap
+ const { locale } = useGlobal()
+ return (
+
+ {/* 卡牌左侧 */}
+
+
+
+ {siteConfig('POST_TITLE_ICON') && (
+
+ )}
+ {post.title}
+
+
+
+ {(!showPreview || showSummary) && (
+
+ {post.summary}
+
+ )}
+
+ {showPreview && (
+
+
+
+
+
+ {locale.COMMON.ARTICLE_DETAIL}
+
+
+
+
+
+ )}
+
+
+ {siteConfig('MAGZINE_POST_LIST_CATEGORY') && (
+
+ )}
+ {siteConfig('MAGZINE_POST_LIST_TAG') &&
+ post?.tagItems?.map(tag => (
+
+ ))}
+
{post.date?.start_date}
+
+
+
+ {/* 卡牌右侧图片 */}
+
+
+
+
+ )
+}
+
+export default PostItemCardWide
diff --git a/themes/magzine/components/PostListEmpty.js b/themes/magzine/components/PostListEmpty.js
new file mode 100644
index 00000000000..cc037cea0da
--- /dev/null
+++ b/themes/magzine/components/PostListEmpty.js
@@ -0,0 +1,19 @@
+import { useGlobal } from '@/lib/global'
+
+/**
+ * 空白博客 列表
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const PostListEmpty = ({ currentSearch }) => {
+ const { locale } = useGlobal()
+ return (
+
+
+ {locale.COMMON.NO_RESULTS_FOUND}{' '}
+ {currentSearch &&
{currentSearch}
}
+
+
+ )
+}
+export default PostListEmpty
diff --git a/themes/magzine/components/PostListHorizontal.js b/themes/magzine/components/PostListHorizontal.js
new file mode 100644
index 00000000000..a857af617b9
--- /dev/null
+++ b/themes/magzine/components/PostListHorizontal.js
@@ -0,0 +1,39 @@
+import Link from 'next/link'
+import PostItemCard from './PostItemCard'
+import PostListEmpty from './PostListEmpty'
+
+/**
+ * 博文水平列表
+ * 含封面
+ * 可以指定是否有模块背景色
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const PostListHorizontal = ({ title, href, posts, hasBg }) => {
+ if (!posts || posts.length === 0) {
+ return
+ }
+
+ return (
+
+
+ {/* 标题 */}
+
+
{title}
+
+ 查看全部
+
+
+
+ {/* 列表 */}
+
+ {posts?.map((p, index) => {
+ return
+ })}
+
+
+
+ )
+}
+
+export default PostListHorizontal
diff --git a/themes/magzine/components/PostListPage.js b/themes/magzine/components/PostListPage.js
new file mode 100644
index 00000000000..ba35d0d50ab
--- /dev/null
+++ b/themes/magzine/components/PostListPage.js
@@ -0,0 +1,39 @@
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import PaginationSimple from './PaginationSimple'
+import PostItemCard from './PostItemCard'
+import PostListEmpty from './PostListEmpty'
+
+/**
+ * 文章列表分页表格
+ * @param page 当前页
+ * @param posts 所有文章
+ * @param tags 所有标签
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const PostListPage = ({ page = 1, posts = [], postCount }) => {
+ const { NOTION_CONFIG } = useGlobal()
+ const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG)
+ const totalPage = Math.ceil(postCount / POSTS_PER_PAGE)
+
+ if (!posts || posts.length === 0) {
+ return
+ }
+
+ return (
+
+
+ {/* 列表 */}
+
+ {posts?.map((p, index) => {
+ return
+ })}
+
+
+
+
+ )
+}
+
+export default PostListPage
diff --git a/themes/magzine/components/PostListRecommend.js b/themes/magzine/components/PostListRecommend.js
new file mode 100644
index 00000000000..84025732432
--- /dev/null
+++ b/themes/magzine/components/PostListRecommend.js
@@ -0,0 +1,79 @@
+import { siteConfig } from '@/lib/config'
+import PostItemCard from './PostItemCard'
+import PostListEmpty from './PostListEmpty'
+
+/**
+ * 博文水平列表
+ * 含封面
+ * 可以指定是否有模块背景色
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const PostListRecommend = ({ latestPosts, allNavPages }) => {
+ // 获取推荐文章
+ const recommendPosts = getTopPosts({ latestPosts, allNavPages })
+ if (!recommendPosts || recommendPosts.length === 0) {
+ return
+ }
+ const title = siteConfig('MAGZINE_RECOMMEND_POST_TITLE')
+
+ return (
+
+
+ {/* 标题 */}
+
+
{title}
+
+ {/* 列表 */}
+
+ {recommendPosts?.map(p => {
+ return
+ })}
+
+
+
+ )
+}
+
+/**
+ * 获取推荐置顶文章
+ */
+function getTopPosts({ latestPosts, allNavPages }) {
+ // 默认展示最近更新
+ if (
+ !siteConfig('MAGZINE_RECOMMEND_POST_TAG') ||
+ siteConfig('MAGZINE_RECOMMEND_POST_TAG') === ''
+ ) {
+ return latestPosts
+ }
+
+ // 显示包含‘推荐’标签的文章
+ let sortPosts = []
+
+ // 排序方式
+ if (siteConfig('MAGZINE_RECOMMEND_POST_SORT_BY_UPDATE_TIME')) {
+ sortPosts = Object.create(allNavPages).sort((a, b) => {
+ const dateA = new Date(a?.lastEditedDate)
+ const dateB = new Date(b?.lastEditedDate)
+ return dateB - dateA
+ })
+ } else {
+ sortPosts = Object.create(allNavPages)
+ }
+
+ const count = siteConfig('MAGZINE_RECOMMEND_POST_COUNT', 6)
+ // 只取前4篇
+ const topPosts = []
+ for (const post of sortPosts) {
+ if (topPosts.length === count) {
+ break
+ }
+ // 查找标签
+ if (post?.tags?.indexOf(siteConfig('MAGZINE_RECOMMEND_POST_TAG')) >= 0) {
+ topPosts.push(post)
+ }
+ }
+ return topPosts
+}
+
+export default PostListRecommend
diff --git a/themes/magzine/components/PostListScroll.js b/themes/magzine/components/PostListScroll.js
new file mode 100644
index 00000000000..a33aa1e9166
--- /dev/null
+++ b/themes/magzine/components/PostListScroll.js
@@ -0,0 +1,107 @@
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import throttle from 'lodash.throttle'
+import { useRouter } from 'next/router'
+import { useCallback, useEffect, useRef, useState } from 'react'
+import PostItemCard from './PostItemCard'
+import PostListEmpty from './PostListEmpty'
+
+/**
+ * 博客列表滚动分页
+ * @param posts 所有文章
+ * @param tags 所有标签
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const PostListScroll = ({ posts = [], currentSearch }) => {
+ const { NOTION_CONFIG } = useGlobal()
+ const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG)
+ const [page, updatePage] = useState(1)
+ const router = useRouter()
+ let filteredPosts = Object.assign(posts)
+ const searchKey = router?.query?.s || null
+ if (searchKey) {
+ filteredPosts = posts.filter(post => {
+ const tagContent = post?.tags ? post?.tags.join(' ') : ''
+ const searchContent = post.title + post.summary + tagContent
+ return searchContent.toLowerCase().includes(searchKey.toLowerCase())
+ })
+ }
+ const postsToShow = getPostByPage(page, filteredPosts, POSTS_PER_PAGE)
+
+ let hasMore = false
+ if (filteredPosts) {
+ const totalCount = filteredPosts.length
+ hasMore = page * POSTS_PER_PAGE < totalCount
+ }
+
+ const handleGetMore = () => {
+ if (!hasMore) return
+ updatePage(page + 1)
+ }
+
+ // 监听滚动自动分页加载
+ const scrollTrigger = useCallback(
+ throttle(() => {
+ const scrollS = window.scrollY + window.outerHeight
+ const clientHeight = targetRef
+ ? targetRef.current
+ ? targetRef.current.clientHeight
+ : 0
+ : 0
+ if (scrollS > clientHeight + 100) {
+ handleGetMore()
+ }
+ }, 500)
+ )
+
+ // 监听滚动
+ useEffect(() => {
+ window.addEventListener('scroll', scrollTrigger)
+ return () => {
+ window.removeEventListener('scroll', scrollTrigger)
+ }
+ })
+
+ const targetRef = useRef(null)
+ const { 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 dark:text-gray-200'>
+ {' '}
+ {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
+
+
+
+ )
+ }
+}
+
+/**
+ * 获取从第1页到指定页码的文章
+ * @param page 第几页
+ * @param totalPosts 所有文章
+ * @param POSTS_PER_PAGE 每页文章数量
+ * @returns {*}
+ */
+const getPostByPage = function (page, totalPosts, POSTS_PER_PAGE) {
+ return totalPosts.slice(0, POSTS_PER_PAGE * page)
+}
+
+export default PostListScroll
diff --git a/themes/magzine/components/PostListSimpleHorizontal.js b/themes/magzine/components/PostListSimpleHorizontal.js
new file mode 100644
index 00000000000..af1ece3a17c
--- /dev/null
+++ b/themes/magzine/components/PostListSimpleHorizontal.js
@@ -0,0 +1,37 @@
+import Link from 'next/link'
+import PostItemCardSimple from './PostItemCardSimple'
+import PostListEmpty from './PostListEmpty'
+
+/**
+ * 博文水平列表;不带封面图
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const PostSimpleListHorizontal = ({ title, href, posts }) => {
+ if (!posts || posts.length === 0) {
+ return
+ }
+
+ return (
+
+
+ {/* 标题 */}
+
+
{title}
+
+ 查看全部
+
+
+
+ {/* 列表 */}
+
+ {posts?.map(p => {
+ return
+ })}
+
+
+
+ )
+}
+
+export default PostSimpleListHorizontal
diff --git a/themes/magzine/components/PostListSlotBar.js b/themes/magzine/components/PostListSlotBar.js
new file mode 100644
index 00000000000..228d1f3c05f
--- /dev/null
+++ b/themes/magzine/components/PostListSlotBar.js
@@ -0,0 +1,29 @@
+import { useGlobal } from '@/lib/global'
+
+/**
+ * 文章列表上方嵌入
+ * @param {*} props
+ * @returns
+ */
+export default function PostListSlotBar(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/magzine/components/Progress.js b/themes/magzine/components/Progress.js
new file mode 100644
index 00000000000..f6fa94a680b
--- /dev/null
+++ b/themes/magzine/components/Progress.js
@@ -0,0 +1,44 @@
+import { useEffect, useState } from 'react'
+import { isBrowser } from '@/lib/utils'
+
+/**
+ * 顶部页面阅读进度条
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const Progress = ({ targetRef, showPercent = true }) => {
+ const currentRef = targetRef?.current || targetRef
+ const [percent, changePercent] = useState(0)
+ const scrollListener = () => {
+ const target = currentRef || (isBrowser && document.getElementById('article-wrapper'))
+ if (target) {
+ const clientHeight = target.clientHeight
+ const scrollY = window.pageYOffset
+ const fullHeight = clientHeight - window.outerHeight
+ let per = parseFloat(((scrollY / fullHeight) * 100).toFixed(0))
+ if (per > 100) per = 100
+ if (per < 0) per = 0
+ changePercent(per)
+ }
+ }
+
+ useEffect(() => {
+ document.addEventListener('scroll', scrollListener)
+ return () => document.removeEventListener('scroll', scrollListener)
+ }, [])
+
+ return (
+
+
+ {showPercent && (
+
{percent}%
+ )}
+
+
+ )
+}
+
+export default Progress
diff --git a/themes/magzine/components/RevolverMaps.js b/themes/magzine/components/RevolverMaps.js
new file mode 100644
index 00000000000..a65fc4cf6de
--- /dev/null
+++ b/themes/magzine/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/magzine/components/SearchInput.js b/themes/magzine/components/SearchInput.js
new file mode 100644
index 00000000000..4ebcade0bd7
--- /dev/null
+++ b/themes/magzine/components/SearchInput.js
@@ -0,0 +1,97 @@
+import { useRouter } from 'next/router'
+import { useImperativeHandle, useRef, useState } from 'react'
+let lock = false
+
+const SearchInput = ({ currentTag, currentSearch, 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={currentSearch}
+ />
+
+
+
+
+
+ {showClean && (
+
+
+
+ )}
+
+ )
+}
+
+export default SearchInput
diff --git a/themes/magzine/components/SocialButton.js b/themes/magzine/components/SocialButton.js
new file mode 100644
index 00000000000..5ef31889296
--- /dev/null
+++ b/themes/magzine/components/SocialButton.js
@@ -0,0 +1,104 @@
+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') && (
+
+
+
+ )}
+
+ )
+}
+export default SocialButton
diff --git a/themes/magzine/components/TagGroups.js b/themes/magzine/components/TagGroups.js
new file mode 100644
index 00000000000..dc0895f88a9
--- /dev/null
+++ b/themes/magzine/components/TagGroups.js
@@ -0,0 +1,30 @@
+import { useGlobal } from '@/lib/global'
+import TagItemMini from './TagItemMini'
+
+/**
+ * 标签组
+ * @param tags
+ * @param currentTag
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const TagGroups = ({ tagOptions, currentTag }) => {
+ const { locale } = useGlobal()
+ if (!tagOptions) return <>>
+ return (
+
+ )
+}
+
+export default TagGroups
diff --git a/themes/magzine/components/TagItemMini.js b/themes/magzine/components/TagItemMini.js
new file mode 100644
index 00000000000..c08b33f1ce9
--- /dev/null
+++ b/themes/magzine/components/TagItemMini.js
@@ -0,0 +1,24 @@
+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/magzine/components/TocDrawer.js b/themes/magzine/components/TocDrawer.js
new file mode 100644
index 00000000000..e8367486f7b
--- /dev/null
+++ b/themes/magzine/components/TocDrawer.js
@@ -0,0 +1,48 @@
+import { useMagzineGlobal } from '..'
+import Catalog from './Catalog'
+
+/**
+ * 悬浮抽屉目录
+ * @param toc
+ * @param post
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const TocDrawer = ({ post, cRef }) => {
+ const { tocVisible, changeTocVisible } = useMagzineGlobal()
+ const switchVisible = () => {
+ changeTocVisible(!tocVisible)
+ }
+ return (
+ <>
+
+ {/* 侧边菜单 */}
+
+ {post && (
+ <>
+
+
+
+ >
+ )}
+
+
+ {/* 背景蒙版 */}
+
+ >
+ )
+}
+export default TocDrawer
diff --git a/themes/magzine/components/TopNavBar.js b/themes/magzine/components/TopNavBar.js
new file mode 100644
index 00000000000..370e542abd0
--- /dev/null
+++ b/themes/magzine/components/TopNavBar.js
@@ -0,0 +1,109 @@
+import Collapse from '@/components/Collapse'
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import { useRef, useState } from 'react'
+import LogoBar from './LogoBar'
+import { MenuBarMobile } from './MenuBarMobile'
+import { MenuItemDrop } from './MenuItemDrop'
+
+/**
+ * 顶部导航栏 + 菜单
+ * @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,
+ href: '/category',
+ show: siteConfig('MAGZINE_MENU_CATEGORY')
+ },
+ {
+ icon: 'fas fa-tag',
+ name: locale.COMMON.TAGS,
+ href: '/tag',
+ show: siteConfig('MAGZINE_MENU_TAG')
+ },
+ {
+ icon: 'fas fa-archive',
+ name: locale.NAV.ARCHIVE,
+ href: '/archive',
+ show: siteConfig('MAGZINE_MENU_ARCHIVE')
+ },
+ {
+ icon: 'fas fa-search',
+ name: locale.NAV.SEARCH,
+ href: '/search',
+ show: siteConfig('MAGZINE_MENU_SEARCH')
+ }
+ ]
+
+ let links = defaultLinks.concat(customNav)
+
+ const toggleMenuOpen = () => {
+ changeShow(!isOpen)
+ }
+
+ // 如果 开启自定义菜单,则覆盖Page生成的菜单
+ if (siteConfig('CUSTOM_MENU')) {
+ links = customMenu
+ }
+
+ if (!links || links.length === 0) {
+ return null
+ }
+
+ return (
+
+ {/* 移动端折叠菜单 */}
+
+
+
+ collapseRef.current?.updateCollapseHeight(param)
+ }
+ />
+
+
+
+ {/* 导航栏菜单 */}
+
+ {/* 左侧图标Logo */}
+
+
+ {/* 折叠按钮、仅移动端显示 */}
+
+
+ {isOpen ? (
+
+ ) : (
+
+ )}
+
+
+
+ {/* 桌面端顶部菜单 */}
+
+ {links &&
+ links?.map((link, index) => (
+
+ ))}
+
+
+
+ )
+}
diff --git a/themes/magzine/components/TouchMeCard.js b/themes/magzine/components/TouchMeCard.js
new file mode 100644
index 00000000000..767ccf4ee97
--- /dev/null
+++ b/themes/magzine/components/TouchMeCard.js
@@ -0,0 +1,39 @@
+import FlipCard from '@/components/FlipCard'
+import { siteConfig } from '@/lib/config'
+import Link from 'next/link'
+
+/**
+ * 交流频道
+ * @returns
+ */
+export default function TouchMeCard() {
+ // 开关
+ if (!siteConfig('MAGZINE_SOCIAL_CARD', null)) {
+ return <>>
+ }
+
+ return (
+
+
+
+ {siteConfig('MAGZINE_SOCIAL_CARD_TITLE_1')}
+
+
+ {siteConfig('MAGZINE_SOCIAL_CARD_TITLE_2')}
+
+
+ }
+ backContent={
+
+
+ {siteConfig('MAGZINE_SOCIAL_CARD_TITLE_3')}
+
+
+ }
+ />
+
+ )
+}
diff --git a/themes/magzine/config.js b/themes/magzine/config.js
new file mode 100644
index 00000000000..f49dc000dfb
--- /dev/null
+++ b/themes/magzine/config.js
@@ -0,0 +1,130 @@
+const CONFIG = {
+ // 首屏信息栏按钮文字
+ MAGZINE_HOME_BANNER_ENABLE: true, // 首屏右上角的宣传位
+ MAGZINE_HOME_BUTTON: true,
+ MAGZINE_HOME_BUTTON_URL: '/about',
+ MAGZINE_HOME_BUTTON_TEXT: '了解更多',
+
+ MAGZINE_HOME_HIDDEN_CATEGORY: '分享杂文', //不希望在首页展示的文章分类,用英文逗号隔开
+
+ MAGZINE_HOME_TITLE: '立即开创您的在线业务。完全免费。',
+ MAGZINE_HOME_DESCRIPTION:
+ '借助NotionNext,获得助您开创、经营和扩展业务所需的全部工具和帮助。',
+ MAGZINE_HOME_TIPS: 'AI时代来临,这是属于超级个体的狂欢盛宴!',
+
+ // 首页底部推荐文章标签, 例如 [推荐] , 最多六篇文章; 若留空白'',则推荐最近更新文章
+ MAGZINE_RECOMMEND_POST_TAG: '推荐',
+ MAGZINE_RECOMMEND_POST_COUNT: 6,
+ MAGZINE_RECOMMEND_POST_TITLE: '推荐文章',
+ MAGZINE_RECOMMEND_POST_SORT_BY_UPDATE_TIME: false, // 推荐文章排序,为`true`时将强制按最后修改时间倒序
+
+ // Style
+ MAGZINE_RIGHT_PANEL_DARK: process.env.NEXT_PUBLIC_MAGZINE_RIGHT_DARK || false, // 右侧面板深色模式
+
+ MAGZINE_POST_LIST_COVER: true, // 文章列表显示图片封面
+ MAGZINE_POST_LIST_PREVIEW: true, // 列表显示文章预览
+ MAGZINE_POST_LIST_CATEGORY: true, // 列表显示文章分类
+ MAGZINE_POST_LIST_TAG: true, // 列表显示文章标签
+
+ MAGZINE_POST_DETAIL_CATEGORY: true, // 文章显示分类
+ MAGZINE_POST_DETAIL_TAG: true, // 文章显示标签
+
+ // 文章页面联系卡
+ MAGZINE_SOCIAL_CARD: true, // 是否显示右侧,点击加入社群按钮
+ MAGZINE_SOCIAL_CARD_TITLE_1: '交流频道',
+ MAGZINE_SOCIAL_CARD_TITLE_2: '加入社群讨论分享',
+ MAGZINE_SOCIAL_CARD_TITLE_3: '点击加入社群',
+ MAGZINE_SOCIAL_CARD_URL: 'https://docs.tangly1024.com/article/chat-community',
+
+ // 页脚菜单
+ MAGZINE_FOOTER_LINKS: [
+ {
+ name: '友情链接',
+ menus: [
+ { title: '尘世の歌', href: 'https://chenge.ink' },
+ {
+ title: '设计狮网址导航',
+ href: 'https://ct.ued.cat/'
+ },
+ {
+ title: '积极的长腿怪',
+ href: 'https://jjdctg.com'
+ },
+ {
+ title: '自动驾驶小白说',
+ href: 'https://www.helloxiaobai.cn/about'
+ },
+ {
+ title: 'AI-皇帝',
+ href: 'https://www.ai-hd.com/'
+ },
+ {
+ title: 'Andy`s Pro',
+ href: 'https://tw.andys.pro/'
+ },
+ { title: 'LUCEN', href: 'https://www.lucenczz.top/' }
+ ]
+ },
+ {
+ name: '开发者',
+ menus: [
+ { title: 'Github', href: 'https://github.com/tangly1024/NotionNext' },
+ {
+ title: '开发帮助',
+ href: 'https://docs.tangly1024.com/article/how-to-develop-with-notion-next'
+ },
+ {
+ title: '功能反馈',
+ href: 'https://github.com/tangly1024/NotionNext/issues/new/choose'
+ },
+ {
+ title: '技术讨论',
+ href: 'https://github.com/tangly1024/NotionNext/discussions'
+ },
+ {
+ title: '关于作者',
+ href: 'https://blog.tangly1024.com/about'
+ }
+ ]
+ },
+
+ {
+ name: '支持',
+ menus: [
+ {
+ title: '站长社群',
+ href: 'https://docs.tangly1024.com/article/chat-community'
+ },
+ {
+ title: '咨询与定制',
+ href: 'https://docs.tangly1024.com/article/my-service'
+ },
+ {
+ title: '升级手册',
+ href: 'https://docs.tangly1024.com/article/my-service'
+ },
+ {
+ title: '安装教程',
+ href: 'https://docs.tangly1024.com/article/how-to-update-notionnext'
+ },
+ { title: 'SEO推广', href: 'https://seo.tangly1024.com/' }
+ ]
+ },
+ {
+ name: '解决方案',
+ menus: [
+ { title: '建站工具', href: 'https://www.tangly1024.com/' },
+ { title: 'NotionNext', href: 'https://docs.tangly1024.com/about' }
+ ]
+ }
+ ],
+
+ // 旧版本顶部菜单
+ MAGZINE_MENU_CATEGORY: true, // 显示分类
+ MAGZINE_MENU_TAG: true, // 显示标签
+ MAGZINE_MENU_ARCHIVE: true, // 显示归档
+ MAGZINE_MENU_SEARCH: true, // 显示搜索
+
+ MAGZINE_WIDGET_TO_TOP: true // 跳回顶部
+}
+export default CONFIG
diff --git a/themes/magzine/index.js b/themes/magzine/index.js
new file mode 100644
index 00000000000..431f8ca8268
--- /dev/null
+++ b/themes/magzine/index.js
@@ -0,0 +1,435 @@
+import Comment from '@/components/Comment'
+import LoadingCover from '@/components/LoadingCover'
+import replaceSearchResult from '@/components/Mark'
+import NotionPage from '@/components/NotionPage'
+import ShareBar from '@/components/ShareBar'
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import { isBrowser } from '@/lib/utils'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { createContext, useContext, useEffect, useState } from 'react'
+import Announcement from './components/Announcement'
+import ArticleInfo from './components/ArticleInfo'
+import { ArticleLock } from './components/ArticleLock'
+import BannerFullWidth from './components/BannerFullWidth'
+import Catalog from './components/Catalog'
+import CategoryGroup from './components/CategoryGroup'
+import CategoryItem from './components/CategoryItem'
+import Footer from './components/Footer'
+import Header from './components/Header'
+import Hero from './components/Hero'
+import PostBannerGroupByCategory from './components/PostBannerGroupByCategory'
+import PostGroupArchive from './components/PostGroupArchive'
+import PostGroupLatest from './components/PostGroupLatest'
+import PostListPage from './components/PostListPage'
+import PostListRecommend from './components/PostListRecommend'
+import PostListScroll from './components/PostListScroll'
+import PostSimpleListHorizontal from './components/PostListSimpleHorizontal'
+import TagGroups from './components/TagGroups'
+import TagItemMini from './components/TagItemMini'
+import TocDrawer from './components/TocDrawer'
+import TouchMeCard from './components/TouchMeCard'
+import CONFIG from './config'
+import { Style } from './style'
+
+// 主题全局状态
+const ThemeGlobalMagzine = createContext()
+export const useMagzineGlobal = () => useContext(ThemeGlobalMagzine)
+
+/**
+ * 基础布局
+ * 采用左右两侧布局,移动端使用顶部导航栏
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const LayoutBase = props => {
+ const { children, notice, showInfoCard = true, post } = props
+ const { locale } = useGlobal()
+ const router = useRouter()
+ const [tocVisible, changeTocVisible] = useState(false)
+ const { onLoading, fullWidth } = useGlobal()
+ const [slotRight, setSlotRight] = useState(null)
+
+ return (
+