diff --git a/themes/magzine/components/Announcement.js b/themes/magzine/components/Announcement.js
new file mode 100644
index 00000000..4796f991
--- /dev/null
+++ b/themes/magzine/components/Announcement.js
@@ -0,0 +1,27 @@
+// import { useGlobal } from '@/lib/global'
+import dynamic from 'next/dynamic'
+
+const NotionPage = dynamic(() => import('@/components/NotionPage'))
+
+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 00000000..95b6f83f
--- /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 00000000..b5a71d26
--- /dev/null
+++ b/themes/magzine/components/ArticleInfo.js
@@ -0,0 +1,57 @@
+import LazyImage from '@/components/LazyImage'
+import NotionIcon from '@/components/NotionIcon'
+import { siteConfig } from '@/lib/config'
+import Link from 'next/link'
+
+/**
+ * 文章详情页介绍
+ * @param {*} props
+ * @returns
+ */
+export default function ArticleInfo(props) {
+ const { post, siteInfo } = props
+
+ return (
+ <>
+ {/* title */}
+
+ {siteConfig('POST_TITLE_ICON') && }
+ {post?.title}
+
+
+ {/* meta */}
+
+
+
+ {' '}
+
+ {post?.publishDay}
+
+
|
+
+
+ {post?.lastEditedDay}
+
+
+
+
+
+
+
+
+
+
+
+ {siteConfig('AUTHOR')}
+
+
+
+
+ >
+ )
+}
diff --git a/themes/magzine/components/ArticleLock.js b/themes/magzine/components/ArticleLock.js
new file mode 100644
index 00000000..6f2ca8cd
--- /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 font-light leading-10 text-black bg-gray-100 dark:bg-gray-500'>
+
+
+ {locale.COMMON.SUBMIT}
+
+
+
+
+
+
+ )
+}
diff --git a/themes/magzine/components/BlogArchiveItem.js b/themes/magzine/components/BlogArchiveItem.js
new file mode 100644
index 00000000..7d7b5a1c
--- /dev/null
+++ b/themes/magzine/components/BlogArchiveItem.js
@@ -0,0 +1,36 @@
+import Link from 'next/link'
+
+/**
+ * 归档分组
+ * @param {*} param0
+ * @returns
+ */
+export default function BlogArchiveItem({ archiveTitle, archivePosts }) {
+ return (
+
+ )
+}
diff --git a/themes/magzine/components/BlogPostBar.js b/themes/magzine/components/BlogPostBar.js
new file mode 100644
index 00000000..9f34aca5
--- /dev/null
+++ b/themes/magzine/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/magzine/components/BlogPostCard.js b/themes/magzine/components/BlogPostCard.js
new file mode 100644
index 00000000..ac82bcc1
--- /dev/null
+++ b/themes/magzine/components/BlogPostCard.js
@@ -0,0 +1,92 @@
+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 Link from 'next/link'
+import CONFIG from '../config'
+import CategoryItem from './CategoryItem'
+import TagItemMini from './TagItemMini'
+
+const BlogPostCard = ({ post, showSummary }) => {
+ const showPreview =
+ siteConfig('MEDIUM_POST_LIST_PREVIEW', null, CONFIG) && post.blockMap
+ const { locale } = useGlobal()
+ return (
+
+ )
+}
+
+export default BlogPostCard
diff --git a/themes/magzine/components/BlogPostCardHorizontal.js b/themes/magzine/components/BlogPostCardHorizontal.js
new file mode 100644
index 00000000..076b2d89
--- /dev/null
+++ b/themes/magzine/components/BlogPostCardHorizontal.js
@@ -0,0 +1,85 @@
+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 CONFIG from '../config'
+import CategoryItem from './CategoryItem'
+import TagItemMini from './TagItemMini'
+
+const BlogPostCardHorizontal = ({ post, showSummary }) => {
+ const showPreview =
+ siteConfig('MEDIUM_POST_LIST_PREVIEW', null, CONFIG) && post.blockMap
+ const { locale } = useGlobal()
+ return (
+
+ {/* 卡牌左侧 */}
+
+
+
+ {siteConfig('POST_TITLE_ICON') && (
+
+ )}
+ {post.title}
+
+
+
+
+
{post.date?.start_date}
+ {siteConfig('MEDIUM_POST_LIST_CATEGORY', null, CONFIG) && (
+
+ )}
+ {siteConfig('MEDIUM_POST_LIST_TAG', null, CONFIG) &&
+ post?.tagItems?.map(tag => (
+
+ ))}
+
+
+ {(!showPreview || showSummary) && (
+
+ {post.summary}
+
+ )}
+
+ {showPreview && (
+
+
+
+
+
+ {locale.COMMON.ARTICLE_DETAIL}
+
+
+
+
+
+ )}
+
+
+ {/* 卡牌右侧图片 */}
+
+
+
+
+ )
+}
+
+export default BlogPostCardHorizontal
diff --git a/themes/magzine/components/BlogPostCardTop.js b/themes/magzine/components/BlogPostCardTop.js
new file mode 100644
index 00000000..2c02d02e
--- /dev/null
+++ b/themes/magzine/components/BlogPostCardTop.js
@@ -0,0 +1,106 @@
+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 CONFIG from '../config'
+import CategoryItem from './CategoryItem'
+import TagItemMini from './TagItemMini'
+
+/**
+ * 置顶头条文章
+ * @param {*} param0
+ * @returns
+ */
+const BlogPostCardTop = ({ post, showSummary }) => {
+ const showPreview =
+ siteConfig('MEDIUM_POST_LIST_PREVIEW', null, CONFIG) && post.blockMap
+ const { locale } = useGlobal()
+ return (
+
+
+ {siteConfig('MEDIUM_POST_LIST_COVER', null, CONFIG) && (
+
+
+
+
+
+ )}
+
+
+ {siteConfig('MEDIUM_POST_LIST_CATEGORY', null, CONFIG) && (
+
+ )}
+
+ {siteConfig('MEDIUM_POST_LIST_TAG', null, CONFIG) &&
+ 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 BlogPostCardTop
diff --git a/themes/magzine/components/BlogPostListEmpty.js b/themes/magzine/components/BlogPostListEmpty.js
new file mode 100644
index 00000000..a26cf292
--- /dev/null
+++ b/themes/magzine/components/BlogPostListEmpty.js
@@ -0,0 +1,14 @@
+import { useGlobal } from '@/lib/global'
+
+/**
+ * 空白博客 列表
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const BlogPostListEmpty = ({ currentSearch }) => {
+ const { locale } = useGlobal()
+ return
+
{locale.COMMON.NO_RESULTS_FOUND} {(currentSearch &&
{currentSearch}
)}
+
+}
+export default BlogPostListEmpty
diff --git a/themes/magzine/components/BlogPostListPage.js b/themes/magzine/components/BlogPostListPage.js
new file mode 100644
index 00000000..79e031dd
--- /dev/null
+++ b/themes/magzine/components/BlogPostListPage.js
@@ -0,0 +1,37 @@
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import BlogPostCard from './BlogPostCard'
+import BlogPostListEmpty from './BlogPostListEmpty'
+import PaginationSimple from './PaginationSimple'
+
+/**
+ * 文章列表分页表格
+ * @param page 当前页
+ * @param posts 所有文章
+ * @param tags 所有标签
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const BlogPostListPage = ({ 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(post => (
+
+ ))}
+
+
+
+ )
+}
+
+export default BlogPostListPage
diff --git a/themes/magzine/components/BlogPostListScroll.js b/themes/magzine/components/BlogPostListScroll.js
new file mode 100644
index 00000000..22960779
--- /dev/null
+++ b/themes/magzine/components/BlogPostListScroll.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 BlogPostCard from './BlogPostCard'
+import BlogPostListEmpty from './BlogPostListEmpty'
+
+/**
+ * 博客列表滚动分页
+ * @param posts 所有文章
+ * @param tags 所有标签
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const BlogPostListScroll = ({ 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 BlogPostListScroll
diff --git a/themes/magzine/components/Card.js b/themes/magzine/components/Card.js
new file mode 100644
index 00000000..d24c046e
--- /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 00000000..b390e686
--- /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 }) => {
+ 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 00000000..3fd6b67d
--- /dev/null
+++ b/themes/magzine/components/CategoryGroup.js
@@ -0,0 +1,37 @@
+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 00000000..399049ef
--- /dev/null
+++ b/themes/magzine/components/CategoryItem.js
@@ -0,0 +1,22 @@
+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 00000000..0c5a9211
--- /dev/null
+++ b/themes/magzine/components/Footer.js
@@ -0,0 +1,30 @@
+import DarkModeButton from '@/components/DarkModeButton'
+import { siteConfig } from '@/lib/config'
+
+const Footer = ({ title }) => {
+ const d = new Date()
+ const currentYear = d.getFullYear()
+ const since = siteConfig('SINCE')
+ const copyrightDate = parseInt(since) < currentYear ? since + '-' + currentYear : currentYear
+
+ return (
+
+ )
+}
+
+export default Footer
diff --git a/themes/magzine/components/Header.js b/themes/magzine/components/Header.js
new file mode 100644
index 00000000..b4a8fd22
--- /dev/null
+++ b/themes/magzine/components/Header.js
@@ -0,0 +1,136 @@
+import Collapse from '@/components/Collapse'
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import throttle from 'lodash.throttle'
+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 { locale } = useGlobal()
+
+ 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
+ const nav = document.querySelector('#top-navbar')
+
+ const narrowNav = scrollS > 50
+ if (narrowNav) {
+ nav && nav.classList.replace('h-20', 'h-14')
+ } else {
+ nav && nav.classList.replace('h-14', 'h-20')
+ }
+ }, throttleMs)
+
+ // 如果 开启自定义菜单,则覆盖Page生成的菜单
+ if (siteConfig('CUSTOM_MENU')) {
+ links = customMenu
+ }
+
+ if (!links || links.length === 0) {
+ return null
+ }
+
+ return (
+
+ {/* 导航栏菜单内容 */}
+
+ {/* 左侧图标Logo */}
+
+
+ {/* 移动端折叠按钮 */}
+
+
+ {isOpen ? (
+
+ ) : (
+
+ )}
+
+
+
+ {/* 桌面端顶部菜单 */}
+
+ {links &&
+ links?.map(link => )}
+
+
+
+ {/* 移动端折叠菜单 */}
+
+
+
+ collapseRef.current?.updateCollapseHeight(param)
+ }
+ />
+
+
+
+ )
+}
diff --git a/themes/magzine/components/InfoCard.js b/themes/magzine/components/InfoCard.js
new file mode 100644
index 00000000..abdc8862
--- /dev/null
+++ b/themes/magzine/components/InfoCard.js
@@ -0,0 +1,39 @@
+import LazyImage from '@/components/LazyImage'
+import { siteConfig } from '@/lib/config'
+import Router from 'next/router'
+import SocialButton from './SocialButton'
+
+/**
+ * 用户信息卡
+ * @param {*} props
+ * @returns
+ */
+const InfoCard = props => {
+ const { siteInfo } = props
+ 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 00000000..6342bf78
--- /dev/null
+++ b/themes/magzine/components/JumpToTopButton.js
@@ -0,0 +1,29 @@
+import CONFIG from '../config'
+import { siteConfig } from '@/lib/config'
+
+/**
+ * 跳转到网页顶部
+ * 当屏幕下滑500像素后会出现该控件
+ * @param targetRef 关联高度的目标html标签
+ * @param showPercent 是否显示百分比
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const JumpToTopButton = ({ showPercent = false, percent, className }) => {
+ if (!siteConfig('MEDIUM_WIDGET_TO_TOP', null, CONFIG)) {
+ 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 00000000..6bde6c51
--- /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/LoadingCover.js b/themes/magzine/components/LoadingCover.js
new file mode 100644
index 00000000..4d8fa828
--- /dev/null
+++ b/themes/magzine/components/LoadingCover.js
@@ -0,0 +1,7 @@
+export default function LoadingCover() {
+ return
+}
diff --git a/themes/magzine/components/LogoBar.js b/themes/magzine/components/LogoBar.js
new file mode 100644
index 00000000..dfe01f9a
--- /dev/null
+++ b/themes/magzine/components/LogoBar.js
@@ -0,0 +1,20 @@
+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 00000000..8e505130
--- /dev/null
+++ b/themes/magzine/components/MenuBarMobile.js
@@ -0,0 +1,54 @@
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import CONFIG from '../config'
+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('MEDIUM_MENU_CATEGORY', null, CONFIG)
+ },
+ {
+ name: locale.COMMON.TAGS,
+ href: '/tag',
+ show: siteConfig('MEDIUM_MENU_TAG', null, CONFIG)
+ },
+ {
+ name: locale.NAV.ARCHIVE,
+ href: '/archive',
+ show: siteConfig('MEDIUM_MENU_ARCHIVE', null, CONFIG)
+ }
+ // { name: locale.NAV.SEARCH, href: '/search', show: siteConfig('MENU_SEARCH', 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/magzine/components/MenuItemCollapse.js b/themes/magzine/components/MenuItemCollapse.js
new file mode 100644
index 00000000..fcd8b756
--- /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 00000000..38484029
--- /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 00000000..33569bfb
--- /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 00000000..e93ca07f
--- /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 00000000..0fcbebab
--- /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/Progress.js b/themes/magzine/components/Progress.js
new file mode 100644
index 00000000..f6fa94a6
--- /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 00000000..a65fc4cf
--- /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 00000000..f6c84d9c
--- /dev/null
+++ b/themes/magzine/components/SearchInput.js
@@ -0,0 +1,86 @@
+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 00000000..97aa6824
--- /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 00000000..dc0895f8
--- /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 00000000..9922a069
--- /dev/null
+++ b/themes/magzine/components/TagItemMini.js
@@ -0,0 +1,21 @@
+import Link from 'next/link'
+
+const TagItemMini = ({ tag, selected = false }) => {
+ return (
+
+
+ {selected && } {tag.name + (tag.count ? `(${tag.count})` : '')}
+
+
+ )
+}
+
+export default TagItemMini
diff --git a/themes/magzine/components/TocDrawer.js b/themes/magzine/components/TocDrawer.js
new file mode 100644
index 00000000..e8367486
--- /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 00000000..244a02d7
--- /dev/null
+++ b/themes/magzine/components/TopNavBar.js
@@ -0,0 +1,110 @@
+import Collapse from '@/components/Collapse'
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import { 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 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('MEDIUM_MENU_CATEGORY', null, CONFIG)
+ },
+ {
+ icon: 'fas fa-tag',
+ name: locale.COMMON.TAGS,
+ href: '/tag',
+ show: siteConfig('MEDIUM_MENU_TAG', null, CONFIG)
+ },
+ {
+ icon: 'fas fa-archive',
+ name: locale.NAV.ARCHIVE,
+ href: '/archive',
+ show: siteConfig('MEDIUM_MENU_ARCHIVE', null, CONFIG)
+ },
+ {
+ icon: 'fas fa-search',
+ name: locale.NAV.SEARCH,
+ href: '/search',
+ show: siteConfig('MEDIUM_MENU_SEARCH', null, CONFIG)
+ }
+ ]
+
+ 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/config.js b/themes/magzine/config.js
new file mode 100644
index 00000000..c29a2ef2
--- /dev/null
+++ b/themes/magzine/config.js
@@ -0,0 +1,24 @@
+const CONFIG = {
+
+ // Style
+ MEDIUM_RIGHT_PANEL_DARK: process.env.NEXT_PUBLIC_MEDIUM_RIGHT_DARK || false, // 右侧面板深色模式
+
+ MEDIUM_POST_LIST_COVER: true, // 文章列表显示图片封面
+ MEDIUM_POST_LIST_PREVIEW: true, // 列表显示文章预览
+ MEDIUM_POST_LIST_CATEGORY: true, // 列表显示文章分类
+ MEDIUM_POST_LIST_TAG: true, // 列表显示文章标签
+
+ MEDIUM_POST_DETAIL_CATEGORY: true, // 文章显示分类
+ MEDIUM_POST_DETAIL_TAG: true, // 文章显示标签
+
+ // 菜单
+ MEDIUM_MENU_CATEGORY: true, // 显示分类
+ MEDIUM_MENU_TAG: true, // 显示标签
+ MEDIUM_MENU_ARCHIVE: true, // 显示归档
+ MEDIUM_MENU_SEARCH: true, // 显示搜索
+
+ // Widget
+ MEDIUM_WIDGET_REVOLVER_MAPS: process.env.NEXT_PUBLIC_WIDGET_REVOLVER_MAPS || 'false', // 地图插件
+ MEDIUM_WIDGET_TO_TOP: true // 跳回顶部
+}
+export default CONFIG
diff --git a/themes/magzine/index.js b/themes/magzine/index.js
new file mode 100644
index 00000000..db46d13e
--- /dev/null
+++ b/themes/magzine/index.js
@@ -0,0 +1,374 @@
+import Comment from '@/components/Comment'
+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 ArticleAround from './components/ArticleAround'
+import ArticleInfo from './components/ArticleInfo'
+import { ArticleLock } from './components/ArticleLock'
+import BlogArchiveItem from './components/BlogArchiveItem'
+import BlogPostCardHorizontal from './components/BlogPostCardHorizontal'
+import BlogPostCardTop from './components/BlogPostCardTop'
+import BlogPostListPage from './components/BlogPostListPage'
+import BlogPostListScroll from './components/BlogPostListScroll'
+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 InfoCard from './components/InfoCard'
+import SearchInput from './components/SearchInput'
+import TagGroups from './components/TagGroups'
+import TagItemMini from './components/TagItemMini'
+import TocDrawer from './components/TocDrawer'
+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, showInfoCard = true, post, notice } = props
+ const { locale } = useGlobal()
+ const router = useRouter()
+ const [tocVisible, changeTocVisible] = useState(false)
+ const { onLoading, fullWidth } = useGlobal()
+ const [slotRight, setSlotRight] = useState(null)
+
+ return (
+
+ {/* CSS样式 */}
+
+
+
+
+ {/* 主区 */}
+
+
+
+ {children}
+
+ {/* 底部 */}
+
+
+
+
+
+ )
+}
+
+/**
+ * 首页
+ * 首页就是一个博客列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutIndex = props => {
+ const { posts, notice } = props
+ const top = posts[0]
+ const post1 = posts[1]
+ const post2 = posts[2]
+ return (
+
+ {/* 首屏文章 */}
+
+
+
+
+
+
+
+ {/* 两篇主要文章 */}
+
+
+
+
+
+
+
+ )
+}
+
+/**
+ * 博客列表
+ * @returns
+ */
+const LayoutPostList = props => {
+ return (
+ <>
+ {siteConfig('POST_LIST_STYLE') === 'page' ? (
+
+ ) : (
+
+ )}
+ >
+ )
+}
+
+/**
+ * 文章详情
+ * @param {*} props
+ * @returns
+ */
+const LayoutSlug = props => {
+ const { post, prev, next, lock, validPassword } = props
+ const { locale } = useGlobal()
+ const slotRight = post?.toc && post?.toc?.length >= 3 && (
+
+
+
+ )
+
+ const router = useRouter()
+ useEffect(() => {
+ // 404
+ if (!post) {
+ setTimeout(
+ () => {
+ if (isBrowser) {
+ const article = document.getElementById('notion-article')
+ if (!article) {
+ router.push('/404').then(() => {
+ console.warn('找不到页面', router.asPath)
+ })
+ }
+ }
+ },
+ siteConfig('POST_WAITING_TIME_FOR_404') * 1000
+ )
+ }
+ }, [post])
+
+ return (
+
+ {/* 文章锁 */}
+ {lock &&
}
+
+ {!lock && (
+
+ {/* 文章信息 */}
+
+
+ {/* Notion文章主体 */}
+
+ {post && }
+
+
+ {/* 文章底部区域 */}
+
+ {/* 分享 */}
+
+ {/* 文章分类和标签信息 */}
+
+ {siteConfig('MEDIUM_POST_DETAIL_CATEGORY', null, CONFIG) &&
+ post?.category &&
}
+
+ {siteConfig('MEDIUM_POST_DETAIL_TAG', null, CONFIG) &&
+ post?.tagItems?.map(tag => (
+
+ ))}
+
+
+ {/* 上一篇下一篇文章 */}
+ {post?.type === 'Post' && }
+ {/* 评论区 */}
+
+
+
+ {/* 移动端目录 */}
+
+
+ )}
+
+ )
+}
+
+/**
+ * 搜索
+ * @param {*} props
+ * @returns
+ */
+const LayoutSearch = props => {
+ const { locale } = useGlobal()
+ const { keyword } = props
+ const router = useRouter()
+ const currentSearch = keyword || router?.query?.s
+
+ useEffect(() => {
+ if (isBrowser) {
+ replaceSearchResult({
+ doms: document.getElementById('posts-wrapper'),
+ search: keyword,
+ target: {
+ element: 'span',
+ className: 'text-red-500 border-b border-dashed'
+ }
+ })
+ }
+ }, [])
+
+ return (
+ <>
+ {/* 搜索导航栏 */}
+
+
{locale.NAV.SEARCH}
+
+ {!currentSearch && (
+ <>
+
+
+ >
+ )}
+
+
+ {/* 文章列表 */}
+ {currentSearch && (
+
+ {siteConfig('POST_LIST_STYLE') === 'page' ? (
+
+ ) : (
+
+ )}
+
+ )}
+ >
+ )
+}
+
+/**
+ * 归档
+ * @param {*} props
+ * @returns
+ */
+const LayoutArchive = props => {
+ const { archivePosts } = props
+ return (
+ <>
+
+ {Object.keys(archivePosts)?.map(archiveTitle => (
+
+ ))}
+
+ >
+ )
+}
+
+/**
+ * 404
+ * @param {*} props
+ * @returns
+ */
+const Layout404 = props => {
+ return (
+ <>
+
+ 404 Not found.
+
+ >
+ )
+}
+
+/**
+ * 分类列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutCategoryIndex = props => {
+ const { categoryOptions } = props
+ const { locale } = useGlobal()
+ return (
+ <>
+
+
+
+ {locale.COMMON.CATEGORY}:
+
+
+ {categoryOptions?.map(category => {
+ return (
+
+
+
+ {category.name}({category.count})
+
+
+ )
+ })}
+
+
+ >
+ )
+}
+
+/**
+ * 标签列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutTagIndex = props => {
+ const { tagOptions } = props
+ const { locale } = useGlobal()
+ return (
+ <>
+
+
+
+ {locale.COMMON.TAGS}:
+
+
+
+ >
+ )
+}
+
+export {
+ Layout404,
+ LayoutArchive,
+ LayoutBase,
+ LayoutCategoryIndex,
+ LayoutIndex,
+ LayoutPostList,
+ LayoutSearch,
+ LayoutSlug,
+ LayoutTagIndex,
+ CONFIG as THEME_CONFIG
+}
diff --git a/themes/magzine/style.js b/themes/magzine/style.js
new file mode 100644
index 00000000..5e8eaa5a
--- /dev/null
+++ b/themes/magzine/style.js
@@ -0,0 +1,18 @@
+/* eslint-disable react/no-unknown-property */
+/**
+ * 此处样式只对当前主题生效
+ * 此处不支持tailwindCSS的 @apply 语法
+ * @returns
+ */
+const Style = () => {
+ return
+}
+
+export { Style }