diff --git a/components/FullScreenButton.js b/components/FullScreenButton.js
new file mode 100644
index 00000000..053de666
--- /dev/null
+++ b/components/FullScreenButton.js
@@ -0,0 +1,48 @@
+import { isBrowser } from '@/lib/utils'
+import React, { useState } from 'react'
+
+/**
+ * 全屏按钮
+ * @returns
+ */
+const FullScreenButton = () => {
+ const [isFullScreen, setIsFullScreen] = useState(false)
+
+ const handleFullScreenClick = () => {
+ if (!isBrowser()) {
+ return
+ }
+ const element = document.documentElement
+ if (!isFullScreen) {
+ if (element.requestFullscreen) {
+ element.requestFullscreen()
+ } else if (element.webkitRequestFullscreen) {
+ element.webkitRequestFullscreen()
+ } else if (element.mozRequestFullScreen) {
+ element.mozRequestFullScreen()
+ } else if (element.msRequestFullscreen) {
+ element.msRequestFullscreen()
+ }
+ setIsFullScreen(true)
+ } else {
+ if (document.exitFullscreen) {
+ document.exitFullscreen()
+ } else if (document.webkitExitFullscreen) {
+ document.webkitExitFullscreen()
+ } else if (document.mozCancelFullScreen) {
+ document.mozCancelFullScreen()
+ } else if (document.msExitFullscreen) {
+ document.msExitFullscreen()
+ }
+ setIsFullScreen(false)
+ }
+ }
+
+ return (
+
+ )
+}
+
+export default FullScreenButton
diff --git a/themes/hexo/components/Header.js b/themes/hexo/components/Header.js
index f64263ab..76e86c25 100644
--- a/themes/hexo/components/Header.js
+++ b/themes/hexo/components/Header.js
@@ -72,7 +72,7 @@ const Hero = props => {
-
diff --git a/themes/matery/components/Hero.js b/themes/matery/components/Hero.js
index 412f9cf1..6bcd445f 100644
--- a/themes/matery/components/Hero.js
+++ b/themes/matery/components/Hero.js
@@ -64,7 +64,7 @@ const Hero = props => {
-
diff --git a/themes/plog/components/Announcement.js b/themes/plog/components/Announcement.js
new file mode 100644
index 00000000..088c412e
--- /dev/null
+++ b/themes/plog/components/Announcement.js
@@ -0,0 +1,18 @@
+import dynamic from 'next/dynamic'
+
+const NotionPage = dynamic(() => import('@/components/NotionPage'))
+
+const Announcement = ({ notice, className }) => {
+ if (notice?.blockMap) {
+ return
+ } else {
+ return null
+ }
+}
+export default Announcement
diff --git a/themes/plog/components/ArticleFooter.js b/themes/plog/components/ArticleFooter.js
new file mode 100644
index 00000000..87cf7baf
--- /dev/null
+++ b/themes/plog/components/ArticleFooter.js
@@ -0,0 +1,34 @@
+import BLOG from '@/blog.config'
+import { useRouter } from 'next/router'
+import { useGlobal } from '@/lib/global'
+
+/**
+ * 加密文章校验组件
+ * @param {password, validPassword} props
+ * @param password 正确的密码
+ * @param validPassword(bool) 回调函数,校验正确回调入参为true
+ * @returns
+ */
+export const ArticleFooter = props => {
+ const router = useRouter()
+ const { locale } = useGlobal()
+
+ return
+}
diff --git a/themes/plog/components/ArticleInfo.js b/themes/plog/components/ArticleInfo.js
new file mode 100644
index 00000000..fc6e70fc
--- /dev/null
+++ b/themes/plog/components/ArticleInfo.js
@@ -0,0 +1,59 @@
+
+import formatDate from '@/lib/formatDate'
+import Image from 'next/image'
+import BLOG from '@/blog.config'
+import TagItem from './TagItem'
+import md5 from 'js-md5'
+
+export const ArticleInfo = (props) => {
+ const { post } = props
+
+ const emailHash = md5(BLOG.CONTACT_EMAIL)
+
+ return
+
+
+
+ {post?.title}
+
+
+ {post?.type !== 'Page' && <>
+
+ >}
+
+
+
+
+}
diff --git a/themes/plog/components/ArticleLock.js b/themes/plog/components/ArticleLock.js
new file mode 100644
index 00000000..8bf57e52
--- /dev/null
+++ b/themes/plog/components/ArticleLock.js
@@ -0,0 +1,53 @@
+import { useGlobal } from '@/lib/global'
+import { useEffect, useRef } from 'react'
+
+/**
+ * 加密文章校验组件
+ * @param {password, validPassword} props
+ * @param password 正确的密码
+ * @param validPassword(bool) 回调函数,校验正确回调入参为true
+ * @returns
+ */
+export const ArticleLock = props => {
+ const { validPassword } = props
+ const { locale } = useGlobal()
+
+ const submitPassword = () => {
+ const p = document.getElementById('password')
+ if (!validPassword(p?.value)) {
+ const tips = document.getElementById('tips')
+ if (tips) {
+ tips.innerHTML = ''
+ tips.innerHTML = `${locale.COMMON.PASSWORD_ERROR}
`
+ }
+ }
+ }
+
+ const passwordInputRef = useRef(null)
+ useEffect(() => {
+ // 选中密码输入框并将其聚焦
+ passwordInputRef.current.focus()
+ }, [])
+
+ return
+
+
{locale.COMMON.ARTICLE_LOCK_TIPS}
+
+
{
+ if (e.key === 'Enter') {
+ submitPassword()
+ }
+ }}
+ ref={passwordInputRef} // 绑定ref到passwordInputRef变量
+ className='outline-none w-full text-sm pl-5 rounded-l transition focus:shadow-lg font-light leading-10 text-black dark:bg-gray-500 bg-gray-50'
+ >
+
+ {locale.COMMON.SUBMIT}
+
+
+
+
+
+
+}
diff --git a/themes/plog/components/BlogArchiveItem.js b/themes/plog/components/BlogArchiveItem.js
new file mode 100644
index 00000000..3b16fc3b
--- /dev/null
+++ b/themes/plog/components/BlogArchiveItem.js
@@ -0,0 +1,41 @@
+import BLOG from '@/blog.config'
+import Link from 'next/link'
+
+/**
+ * 归档分组文章
+ * @param {*} param0
+ * @returns
+ */
+export default function BlogArchiveItem({ archiveTitle, archivePosts }) {
+ return (
+
+
+ {archiveTitle}
+
+
+
+
+ )
+}
diff --git a/themes/plog/components/BlogListBar.js b/themes/plog/components/BlogListBar.js
new file mode 100644
index 00000000..69076937
--- /dev/null
+++ b/themes/plog/components/BlogListBar.js
@@ -0,0 +1,39 @@
+import Tags from './Tags'
+
+export default function BlogListBar(props) {
+ const { tag, setFilterKey } = props
+ const handleSearchChange = (val) => {
+ setFilterKey(val)
+ }
+ if (tag) {
+ return (
+
+
handleSearchChange(e.target.value)}
+ />
+
+
+
+
)
+ } else {
+ return <>>
+ }
+}
diff --git a/themes/plog/components/BlogListPage.js b/themes/plog/components/BlogListPage.js
new file mode 100644
index 00000000..9dbf6bd0
--- /dev/null
+++ b/themes/plog/components/BlogListPage.js
@@ -0,0 +1,50 @@
+
+import BLOG from '@/blog.config'
+import { useGlobal } from '@/lib/global'
+import { useRouter } from 'next/router'
+import Link from 'next/link'
+import BlogPost from './BlogPost'
+
+export const BlogListPage = props => {
+ const { page = 1, posts, postCount } = props
+ const { locale } = useGlobal()
+ const router = useRouter()
+ const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
+ const currentPage = +page
+
+ const showPrev = currentPage > 1
+ const showNext = page < totalPage
+ const pagePrefix = router.asPath.split('?')[0].replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '')
+
+ return (
+
+
+
+ {posts?.map(post => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/themes/plog/components/BlogListScroll.js b/themes/plog/components/BlogListScroll.js
new file mode 100644
index 00000000..157e80e4
--- /dev/null
+++ b/themes/plog/components/BlogListScroll.js
@@ -0,0 +1,82 @@
+import BLOG from '@/blog.config'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import React from 'react'
+import throttle from 'lodash.throttle'
+
+export const BlogListScroll = props => {
+ const { posts } = props
+ const { locale } = useGlobal()
+
+ const [page, updatePage] = React.useState(1)
+
+ let hasMore = false
+ const postsToShow = posts
+ ? Object.assign(posts).slice(0, BLOG.POSTS_PER_PAGE * page)
+ : []
+
+ if (posts) {
+ const totalCount = posts.length
+ hasMore = page * BLOG.POSTS_PER_PAGE < totalCount
+ }
+ const handleGetMore = () => {
+ if (!hasMore) return
+ updatePage(page + 1)
+ }
+
+ const targetRef = React.useRef(null)
+
+ // 监听滚动自动分页加载
+ const scrollTrigger = React.useCallback(throttle(() => {
+ const scrollS = window.scrollY + window.outerHeight
+ const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
+ if (scrollS > clientHeight + 100) {
+ handleGetMore()
+ }
+ }, 500))
+
+ React.useEffect(() => {
+ window.addEventListener('scroll', scrollTrigger)
+
+ return () => {
+ window.removeEventListener('scroll', scrollTrigger)
+ }
+ })
+
+ return (
+
+ {postsToShow.map(p => (
+
+
+
+ {p.title}
+
+
+
+
+
+
+ {p.summary}
+
+
+ ))}
+
+
+ {' '}
+ {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
+
+
+
+ )
+}
diff --git a/themes/plog/components/BlogPost.js b/themes/plog/components/BlogPost.js
new file mode 100644
index 00000000..fc7f710a
--- /dev/null
+++ b/themes/plog/components/BlogPost.js
@@ -0,0 +1,23 @@
+
+/**
+ * 博客照片卡牌
+ * @param {*} props
+ * @returns
+ */
+const BlogPost = (props) => {
+ const { post, siteInfo } = props
+ const pageThumbnail = post?.pageCoverThumbnail || siteInfo?.pageCover
+ console.log('缩略图', pageThumbnail, siteInfo)
+ return (
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
+
+ {post?.title}
+
+
+
+ )
+}
+
+export default BlogPost
diff --git a/themes/plog/components/BottomNav.js b/themes/plog/components/BottomNav.js
new file mode 100644
index 00000000..186f2741
--- /dev/null
+++ b/themes/plog/components/BottomNav.js
@@ -0,0 +1,88 @@
+import Link from 'next/link'
+import BLOG from '@/blog.config'
+import { useGlobal } from '@/lib/global'
+import CONFIG from '../config'
+import { SvgIcon } from './SvgIcon'
+import { MenuItemDrop } from './MenuItemDrop'
+import FullScreenButton from '@/components/FullScreenButton'
+
+/**
+ * 桌面端底部导航
+ * @param {*} props
+ * @returns
+ */
+const BottomNav = props => {
+ const { navBarTitle, siteInfo } = props
+
+ return <>
+
+
+
+
+ {/*
*/}
+ {CONFIG.NAV_NOTION_ICON
+ /* eslint-disable-next-line @next/next/no-img-element */
+ ?

+ :
}
+
+
+ {navBarTitle
+ ? (
+
+ {navBarTitle}
+
+ )
+ : (
+
+ {siteInfo?.title}
+ {' '}{siteInfo?.description}
+
+ )}
+
+
+
+ >
+}
+
+/**
+ * 菜单
+ * @param {*} props
+ * @returns
+ */
+const MenuList = props => {
+ const { customMenu, customNav } = props
+
+ const { locale } = useGlobal()
+ let links = [
+ { id: 2, name: locale.NAV.RSS, to: '/feed', show: BLOG.ENABLE_RSS && CONFIG.MENU_RSS, target: '_blank' },
+ { icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH },
+ { icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE },
+ { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY },
+ { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG }
+ ]
+ if (customNav) {
+ links = links.concat(customNav)
+ }
+
+ // 如果 开启自定义菜单,则覆盖Page生成的菜单
+ if (BLOG.CUSTOM_MENU) {
+ links = customMenu
+ }
+
+ if (!links || links.length === 0) {
+ return null
+ }
+
+ return (
+
+
+ {links?.map(link => )}
+ -
+
+
+
+
+ )
+}
+
+export default BottomNav
diff --git a/themes/plog/components/ExampleRecentComments.js b/themes/plog/components/ExampleRecentComments.js
new file mode 100644
index 00000000..b1555c1d
--- /dev/null
+++ b/themes/plog/components/ExampleRecentComments.js
@@ -0,0 +1,35 @@
+import React from 'react'
+import BLOG from '@/blog.config'
+import Link from 'next/link'
+import { RecentComments } from '@waline/client'
+
+/**
+ * @see https://waline.js.org/guide/get-started.html
+ * @param {*} props
+ * @returns
+ */
+const ExampleRecentComments = (props) => {
+ const [comments, updateComments] = React.useState([])
+ const [onLoading, changeLoading] = React.useState(true)
+ React.useEffect(() => {
+ RecentComments({
+ serverURL: BLOG.COMMENT_WALINE_SERVER_URL,
+ count: 5
+ }).then(({ comments }) => {
+ changeLoading(false)
+ updateComments(comments)
+ })
+ }, [])
+
+ return <>
+ {onLoading && Loading...
}
+ {!onLoading && comments && comments.length === 0 && No Comments
}
+ {!onLoading && comments && comments.length > 0 && comments.map((comment) => )}
+
+ >
+}
+
+export default ExampleRecentComments
diff --git a/themes/plog/components/Footer.js b/themes/plog/components/Footer.js
new file mode 100644
index 00000000..e2b296b5
--- /dev/null
+++ b/themes/plog/components/Footer.js
@@ -0,0 +1,34 @@
+import BLOG from '@/blog.config'
+import DarkModeButton from '@/components/DarkModeButton'
+import Vercel from '@/components/Vercel'
+
+export const Footer = (props) => {
+ const d = new Date()
+ const currentYear = d.getFullYear()
+ const { post } = props
+ const fullWidth = post?.fullWidth ?? false
+
+ const copyrightDate = (function() {
+ if (Number.isInteger(BLOG.SINCE) && BLOG.SINCE < currentYear) {
+ return BLOG.SINCE + '-' + currentYear
+ }
+ return currentYear
+ })()
+
+ return
+}
diff --git a/themes/plog/components/JumpToTopButton.js b/themes/plog/components/JumpToTopButton.js
new file mode 100644
index 00000000..30e684a8
--- /dev/null
+++ b/themes/plog/components/JumpToTopButton.js
@@ -0,0 +1,19 @@
+import { useGlobal } from '@/lib/global'
+import React from 'react'
+
+/**
+ * 跳转到网页顶部
+ * 当屏幕下滑500像素后会出现该控件
+ * @param targetRef 关联高度的目标html标签
+ * @param showPercent 是否显示百分比
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const JumpToTopButton = () => {
+ const { locale } = useGlobal()
+ return window.scrollTo({ top: 0, behavior: 'smooth' })}
+ >
+
+}
+
+export default JumpToTopButton
diff --git a/themes/plog/components/MenuItemCollapse.js b/themes/plog/components/MenuItemCollapse.js
new file mode 100644
index 00000000..323514a9
--- /dev/null
+++ b/themes/plog/components/MenuItemCollapse.js
@@ -0,0 +1,55 @@
+import Collapse from '@/components/Collapse'
+import Link from 'next/link'
+import { useState } from 'react'
+
+/**
+ * 折叠菜单
+ * @param {*} param0
+ * @returns
+ */
+export const MenuItemCollapse = (props) => {
+ const { link } = props
+ const [show, changeShow] = useState(false)
+ const hasSubMenu = link?.subMenus?.length > 0
+
+ const [isOpen, changeIsOpen] = useState(false)
+
+ const toggleShow = () => {
+ changeShow(!show)
+ }
+
+ const toggleOpenSubMenu = () => {
+ changeIsOpen(!isOpen)
+ }
+
+ if (!link || !link.show) {
+ return null
+ }
+
+ return <>
+
+ {!hasSubMenu &&
+
{link?.icon && }{link?.name}
+ }
+ {hasSubMenu &&
+ {link?.icon && }{link?.name}
+
+
}
+
+
+ {/* 折叠子菜单 */}
+ {hasSubMenu &&
+ {link.subMenus.map(sLink => {
+ return
+
+ {sLink.title}
+
+
+ })}
+ }
+ >
+}
diff --git a/themes/plog/components/MenuItemDrop.js b/themes/plog/components/MenuItemDrop.js
new file mode 100644
index 00000000..d1448a18
--- /dev/null
+++ b/themes/plog/components/MenuItemDrop.js
@@ -0,0 +1,43 @@
+import Link from 'next/link'
+import { useState } from 'react'
+
+export const MenuItemDrop = ({ link }) => {
+ const [show, changeShow] = useState(false)
+ if (!link || !link.show) {
+ return null
+ }
+
+ const hasSubMenu = link?.subMenus?.length > 0
+
+ return
+ changeShow(true)} onMouseOut={() => changeShow(false)}>
+ {!hasSubMenu &&
+
+
+ {link?.icon && } {link?.name}
+
+
+ }
+
+ {hasSubMenu &&
+
+ {link?.icon && } {link?.name}
+
+
+ }
+
+ {/* 子菜单 */}
+ {hasSubMenu &&
+ {link.subMenus.map(sLink => {
+ return -
+
+ {link?.icon && }{sLink.title}
+
+
+ })}
+
}
+
+
+
+
+}
diff --git a/themes/plog/components/Nav.js b/themes/plog/components/Nav.js
new file mode 100644
index 00000000..52d2a09e
--- /dev/null
+++ b/themes/plog/components/Nav.js
@@ -0,0 +1,93 @@
+import { useRef, useState } from 'react'
+import Link from 'next/link'
+import BLOG from '@/blog.config'
+import { useGlobal } from '@/lib/global'
+import CONFIG from '../config'
+import { SvgIcon } from './SvgIcon'
+import { MenuItemDrop } from './MenuItemDrop'
+import Collapse from '@/components/Collapse'
+import { MenuItemCollapse } from './MenuItemCollapse'
+
+const Nav = props => {
+ const { navBarTitle, fullWidth, siteInfo } = props
+ return
+
+
+
+
+
+ {/*
*/}
+ {CONFIG.NAV_NOTION_ICON
+ /* eslint-disable-next-line @next/next/no-img-element */
+ ?

+ :
}
+
+
+
+
+ {navBarTitle
+ ? (
+
+ {navBarTitle}
+
+ )
+ : (
+
+ {siteInfo?.title}
+ {/* ,{' '}{siteInfo?.description} */}
+
+ )}
+
+
+
+
+}
+
+const NavBar = props => {
+ const { customMenu, customNav } = props
+ const [isOpen, changeOpen] = useState(false)
+ const toggleOpen = () => {
+ changeOpen(!isOpen)
+ }
+ const collapseRef = useRef(null)
+
+ const { locale } = useGlobal()
+ let links = [
+ { id: 2, name: locale.NAV.RSS, to: '/feed', show: BLOG.ENABLE_RSS && CONFIG.MENU_RSS, target: '_blank' },
+ { icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH },
+ { icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE },
+ { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY },
+ { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG }
+ ]
+ if (customNav) {
+ links = links.concat(customNav)
+ }
+
+ // 如果 开启自定义菜单,则覆盖Page生成的菜单
+ if (BLOG.CUSTOM_MENU) {
+ links = customMenu
+ }
+
+ if (!links || links.length === 0) {
+ return null
+ }
+
+ return (
+
+
+ {links?.map(link => )}
+
+
+
+
+ {links?.map(link => collapseRef.current?.updateCollapseHeight(param)}/>)}
+
+
+
+
+ )
+}
+
+export default Nav
diff --git a/themes/plog/components/PlogModal.js b/themes/plog/components/PlogModal.js
new file mode 100644
index 00000000..f08938f2
--- /dev/null
+++ b/themes/plog/components/PlogModal.js
@@ -0,0 +1,28 @@
+import { useState } from 'react'
+import { Dialog } from '@headlessui/react'
+
+/**
+ * 图片弹出模态框
+ */
+export default function PlogModal(props) {
+ const [isOpen, setIsOpen] = useState(true)
+
+ return (
+
+ )
+}
diff --git a/themes/plog/components/SearchInput.js b/themes/plog/components/SearchInput.js
new file mode 100644
index 00000000..a6affd9c
--- /dev/null
+++ b/themes/plog/components/SearchInput.js
@@ -0,0 +1,87 @@
+import { useRouter } from 'next/router'
+import { useGlobal } from '@/lib/global'
+import { useImperativeHandle, useRef, useState } from 'react'
+
+let lock = false
+
+const SearchInput = ({ currentTag, currentSearch, cRef }) => {
+ const { locale } = useGlobal()
+ const router = useRouter()
+ const searchInputRef = useRef(null)
+ useImperativeHandle(cRef, () => {
+ return {
+ focus: () => {
+ searchInputRef?.current?.focus()
+ }
+ }
+ })
+ const handleSearch = () => {
+ const key = searchInputRef.current.value
+ if (key && key !== '') {
+ router.push({ pathname: '/search/' + key }).then(r => {
+ // console.log('搜索', 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 = ''
+ setShowClean(false)
+ }
+ function lockSearchInput () {
+ lock = true
+ }
+
+ function unLockSearchInput () {
+ lock = false
+ }
+ const [showClean, setShowClean] = useState(false)
+ const updateSearchKey = (val) => {
+ if (lock) {
+ return
+ }
+ searchInputRef.current.value = val
+ if (val) {
+ setShowClean(true)
+ } else {
+ setShowClean(false)
+ }
+ }
+
+ return
+}
+
+export default SearchInput
diff --git a/themes/plog/components/SearchNavBar.js b/themes/plog/components/SearchNavBar.js
new file mode 100644
index 00000000..79557cff
--- /dev/null
+++ b/themes/plog/components/SearchNavBar.js
@@ -0,0 +1,17 @@
+import SearchInput from './SearchInput'
+import Tags from './Tags'
+
+/**
+ * 搜索页面上方嵌入内容
+ * @param {*} props
+ * @returns
+ */
+export default function SearchNavBar(props) {
+ return (<>
+
+
+
+
+
+ >)
+}
diff --git a/themes/plog/components/SideBar.js b/themes/plog/components/SideBar.js
new file mode 100644
index 00000000..986063b0
--- /dev/null
+++ b/themes/plog/components/SideBar.js
@@ -0,0 +1,65 @@
+import BLOG from '@/blog.config'
+import Live2D from '@/components/Live2D'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import dynamic from 'next/dynamic'
+const ExampleRecentComments = dynamic(() => import('./ExampleRecentComments'))
+
+export const SideBar = (props) => {
+ const { locale } = useGlobal()
+ const { latestPosts, categories } = props
+ return (
+
+
+
+
+
+
+ {BLOG.COMMENT_WALINE_SERVER_URL && BLOG.COMMENT_WALINE_RECENT &&
}
+
+
+
+
+ );
+}
diff --git a/themes/plog/components/SvgIcon.js b/themes/plog/components/SvgIcon.js
new file mode 100644
index 00000000..1ae326ca
--- /dev/null
+++ b/themes/plog/components/SvgIcon.js
@@ -0,0 +1,29 @@
+export const SvgIcon = () => {
+ return
+}
diff --git a/themes/plog/components/TagItem.js b/themes/plog/components/TagItem.js
new file mode 100644
index 00000000..6c385b9b
--- /dev/null
+++ b/themes/plog/components/TagItem.js
@@ -0,0 +1,13 @@
+import Link from 'next/link'
+
+const TagItem = ({ tag }) => (
+ (
+
+
+ {tag}
+
+
+ )
+)
+
+export default TagItem
diff --git a/themes/plog/components/Tags.js b/themes/plog/components/Tags.js
new file mode 100644
index 00000000..bdab3ee5
--- /dev/null
+++ b/themes/plog/components/Tags.js
@@ -0,0 +1,38 @@
+import Link from 'next/link'
+
+const Tags = props => {
+ const { tagOptions, tag } = props
+ const currentTag = tag
+ if (!tagOptions) return null
+ return (
+
+
+ {Object.keys(tagOptions).map(key => {
+ const tag = tagOptions[key]
+ const selected = tag.name === currentTag
+ return (
+ -
+
+
+ {`${tag.name} (${tag.count})`}
+
+
+
+ )
+ })}
+
+
+ )
+}
+
+export default Tags
diff --git a/themes/plog/components/Title.js b/themes/plog/components/Title.js
new file mode 100644
index 00000000..e57e2347
--- /dev/null
+++ b/themes/plog/components/Title.js
@@ -0,0 +1,19 @@
+import BLOG from '@/blog.config'
+
+/**
+ * 标题栏
+ * @param {*} props
+ * @returns
+ */
+export const Title = (props) => {
+ const { siteInfo, post } = props
+ const title = post?.title || siteInfo?.description
+ const description = post?.description || BLOG.AUTHOR
+
+ return
+
{title}
+
+ {description}
+
+
+}
diff --git a/themes/plog/config.js b/themes/plog/config.js
new file mode 100644
index 00000000..75f2e597
--- /dev/null
+++ b/themes/plog/config.js
@@ -0,0 +1,13 @@
+const CONFIG = {
+
+ // 菜单配置
+ MENU_CATEGORY: false, // 显示分类
+ MENU_TAG: true, // 显示标签
+ MENU_ARCHIVE: false, // 显示归档
+ MENU_SEARCH: true, // 显示搜索
+ MENU_RSS: false, // 显示订阅
+
+ NAV_NOTION_ICON: true // 是否读取Notion图标作为站点头像
+
+}
+export default CONFIG
diff --git a/themes/plog/index.js b/themes/plog/index.js
new file mode 100644
index 00000000..c7da235d
--- /dev/null
+++ b/themes/plog/index.js
@@ -0,0 +1,262 @@
+import CONFIG from './config'
+import CommonHead from '@/components/CommonHead'
+import React, { useEffect } from 'react'
+import Nav from './components/Nav'
+import { useGlobal } from '@/lib/global'
+
+import BLOG from '@/blog.config'
+import { BlogListPage } from './components/BlogListPage'
+import { BlogListScroll } from './components/BlogListScroll'
+
+import { useRouter } from 'next/router'
+
+import Mark from 'mark.js'
+import { isBrowser } from '@/lib/utils'
+import SearchNavBar from './components/SearchNavBar'
+import BlogArchiveItem from './components/BlogArchiveItem'
+import { ArticleLock } from './components/ArticleLock'
+import NotionPage from '@/components/NotionPage'
+import { ArticleInfo } from './components/ArticleInfo'
+import Comment from '@/components/Comment'
+import { ArticleFooter } from './components/ArticleFooter'
+import ShareBar from '@/components/ShareBar'
+import Link from 'next/link'
+import { Transition } from '@headlessui/react'
+import BottomNav from './components/BottomNav'
+import { saveDarkModeToCookies } from '@/themes/theme'
+
+/**
+ * 基础布局 采用左右两侧布局,移动端使用顶部导航栏
+
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const LayoutBase = props => {
+ const { children, meta, topSlot } = props
+ const { onLoading, updateDarkMode } = useGlobal()
+
+ // 用户手动设置主题
+ const setDarkMode = () => {
+ saveDarkModeToCookies(true)
+ updateDarkMode(true)
+ const htmlElement = document.getElementsByTagName('html')[0]
+ htmlElement.classList?.remove('light')
+ htmlElement.classList?.add('dark')
+ }
+
+ // plog主题默认 深色模式
+ useEffect(() => {
+ setTimeout(() => {
+ setDarkMode()
+ }, 100)
+ }, [])
+
+ return (
+
+ {/* SEO相关 */}
+
+
+ {/* 移动端顶部导航栏 */}
+
+
+ {/* 主区 */}
+
+
+
+ {/* 顶部插槽 */}
+ {topSlot}
+ {children}
+
+
+
+
+ {/* 桌面端底部导航栏 */}
+
+
+ )
+}
+
+/**
+ * 首页
+ * 首页是个博客列表,加上顶部嵌入一个公告
+ * @param {*} props
+ * @returns
+ */
+const LayoutIndex = props => {
+ return (
+
+ )
+}
+
+/**
+ * 博客列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutPostList = props => {
+ return (
+
+ {BLOG.POST_LIST_STYLE === 'page' ? : }
+
+ )
+}
+
+/**
+ * 搜索
+ * 页面是博客列表,上方嵌入一个搜索引导条
+ * @param {*} props
+ * @returns
+ */
+const LayoutSearch = props => {
+ const { keyword } = props
+ const router = useRouter()
+
+ useEffect(() => {
+ setTimeout(() => {
+ const container = isBrowser() && document.getElementById('posts-wrapper')
+ if (container && container.innerHTML) {
+ const re = new RegExp(keyword, 'gim')
+ const instance = new Mark(container)
+ instance.markRegExp(re, {
+ element: 'span',
+ className: 'text-red-500 border-b border-dashed'
+ })
+ }
+ }, 100)
+ }, [router.events])
+
+ return } />
+}
+
+/**
+ * 归档
+ * @param {*} props
+ * @returns
+ */
+const LayoutArchive = props => {
+ const { archivePosts } = props
+ return (
+
+
+ {Object.keys(archivePosts).map(archiveTitle => )}
+
+
+ )
+}
+
+/**
+ * 文章详情
+ * @param {*} props
+ * @returns
+ */
+const LayoutSlug = props => {
+ const { post, lock, validPassword } = props
+
+ return (
+
+
+ {lock && }
+
+ {!lock && }
+
+
+ )
+}
+
+/**
+ * 404 页面
+ * @param {*} props
+ * @returns
+ */
+const Layout404 = (props) => {
+ return
+ 404 Not found.
+
+}
+
+/**
+ * 文章分类列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutCategoryIndex = (props) => {
+ const { categoryOptions } = props
+
+ return (
+
+
+ {categoryOptions?.map(category => {
+ return (
+
+
+ {category.name}({category.count})
+
+
+ )
+ })}
+
+
+ )
+}
+
+/**
+ * 文章标签列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutTagIndex = (props) => {
+ const { tagOptions } = props
+ return (
+
+
+
+ )
+}
+
+export {
+ CONFIG as THEME_CONFIG,
+ LayoutIndex,
+ LayoutSearch,
+ LayoutArchive,
+ LayoutSlug,
+ Layout404,
+ LayoutPostList,
+ LayoutCategoryIndex,
+ LayoutTagIndex
+}