- {pages}
-
- {/* 跳转页码 */}
-
+ return (
+ <>
+ {/* pc端分页按钮 */}
+
+ {/* 上一页 */}
+
+
+
+
+ {locale.PAGINATION.PREV}
+
+
- {/* 下一页 */}
-
+ {/* 分页 */}
+
+ {pages}
-
-
-
- {locale.PAGINATION.NEXT}
-
-
-
+ {/* 跳转页码 */}
+
- {/* 移动端分页 */}
+ {/* 下一页 */}
+
+
+
+
+ {locale.PAGINATION.NEXT}
+
+
+
+
-
- {/* 上一页 */}
-
- {locale.PAGINATION.PREV}
-
+ {/* 移动端分页 */}
- {showPrev && showNext &&
}
+
+ {/* 上一页 */}
+
+ {locale.PAGINATION.PREV}
+
- {/* 下一页 */}
-
- {locale.PAGINATION.NEXT}
-
-
- >)
+ {showPrev && showNext &&
}
+
+ {/* 下一页 */}
+
+ {locale.PAGINATION.NEXT}
+
+
+ >
+ )
}
/**
@@ -127,21 +148,23 @@ const PaginationNumber = ({ page, totalPage }) => {
*/
function getPageElement(page, currentPage, pagePrefix) {
const selected = page + '' === currentPage + ''
+ if (!page) {
+ return <>>
+ }
return (
- (
-
- {page}
-
- )
+
+ {page}
+
)
}
@@ -171,7 +194,11 @@ function generatePages(pagePrefix, page, currentPage, totalPage) {
startPage = totalPage - dynamicGroupCount
}
if (startPage > 2) {
- pages.push(
...
)
+ pages.push(
+
+ ...{' '}
+
+ )
}
for (let i = 0; i < dynamicGroupCount; i++) {
diff --git a/themes/movie/components/Announcement.js b/themes/movie/components/Announcement.js
new file mode 100644
index 00000000..1f265417
--- /dev/null
+++ b/themes/movie/components/Announcement.js
@@ -0,0 +1,22 @@
+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 || Object.keys(post).length === 0) {
+ return <>>
+ }
+ return
+}
+export default Announcement
diff --git a/themes/movie/components/ArticleInfo.js b/themes/movie/components/ArticleInfo.js
new file mode 100644
index 00000000..b502a9c9
--- /dev/null
+++ b/themes/movie/components/ArticleInfo.js
@@ -0,0 +1,51 @@
+import Link from 'next/link'
+import { useGlobal } from '@/lib/global'
+import { formatDateFmt } from '@/lib/utils/formatDate'
+
+export const ArticleInfo = (props) => {
+ const { post } = props
+
+ const { locale } = useGlobal()
+
+ return (
+
+
+ {post?.type !== 'Page' && <>
+
+
+
+ {post?.category}
+
+
+ |
+ >}
+
+ {post?.type !== 'Page' && (<>
+
+
+ {post?.publishDay}
+
+
+ |
+
+ {locale.COMMON.LAST_EDITED_TIME}: {post?.lastEditedDay}
+
+ |
+
+
+
+
+
+ >)}
+
+
+
+
+ )
+}
diff --git a/themes/movie/components/ArticleLock.js b/themes/movie/components/ArticleLock.js
new file mode 100644
index 00000000..3744c183
--- /dev/null
+++ b/themes/movie/components/ArticleLock.js
@@ -0,0 +1,52 @@
+import { useGlobal } from '@/lib/global'
+import { useEffect, useRef } from 'react'
+
+/**
+ * 加密文章校验组件
+ * @param {password, validPassword} props
+ * @param password 正确的密码
+ * @param validPassword(bool) 回调函数,校验正确回调入参为true
+ * @returns
+ */
+export 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/movie/components/BlogListGroupByDate.js b/themes/movie/components/BlogListGroupByDate.js
new file mode 100644
index 00000000..7a8fe083
--- /dev/null
+++ b/themes/movie/components/BlogListGroupByDate.js
@@ -0,0 +1,38 @@
+import { siteConfig } from '@/lib/config'
+import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils'
+import Link from 'next/link'
+
+/**
+ * 按照日期将文章分组
+ * 归档页面用到
+ * @param {*} param0
+ * @returns
+ */
+export default function BlogListGroupByDate({ archiveTitle, archivePosts }) {
+ return
+
+ {archiveTitle}
+
+
+
+
+}
diff --git a/themes/movie/components/BlogListPage.js b/themes/movie/components/BlogListPage.js
new file mode 100644
index 00000000..450f713b
--- /dev/null
+++ b/themes/movie/components/BlogListPage.js
@@ -0,0 +1,41 @@
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import { useRouter } from 'next/router'
+import Link from 'next/link'
+import CONFIG from '../config'
+import BlogPostCard from './BlogPostCard'
+import PaginationNumber from './PaginationNumber'
+
+export const BlogListPage = props => {
+ const { page = 1, posts, postCount } = props
+ const { locale } = useGlobal()
+ const router = useRouter()
+ const totalPage = Math.ceil(
+ postCount / parseInt(siteConfig('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(/\/$/, '')
+
+ const showPageCover = siteConfig('EXAMPLE_POST_LIST_COVER', null, CONFIG)
+
+ return (
+
+
+ {posts?.map(post => (
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/themes/movie/components/BlogListScroll.js b/themes/movie/components/BlogListScroll.js
new file mode 100644
index 00000000..e876f312
--- /dev/null
+++ b/themes/movie/components/BlogListScroll.js
@@ -0,0 +1,66 @@
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import { useCallback, useEffect, useRef, useState } from 'react'
+import throttle from 'lodash.throttle'
+import BlogPostCard from './BlogPostCard'
+import CONFIG from '../config'
+
+export const BlogListScroll = props => {
+ const { posts } = props
+ const { locale } = useGlobal()
+
+ const [page, updatePage] = useState(1)
+
+ let hasMore = false
+ const postsToShow = posts
+ ? Object.assign(posts).slice(0, parseInt(siteConfig('POSTS_PER_PAGE')) * page)
+ : []
+
+ if (posts) {
+ const totalCount = posts.length
+ hasMore = page * parseInt(siteConfig('POSTS_PER_PAGE')) < totalCount
+ }
+ const handleGetMore = () => {
+ if (!hasMore) return
+ updatePage(page + 1)
+ }
+
+ const targetRef = useRef(null)
+
+ // 监听滚动自动分页加载
+ const scrollTrigger = useCallback(throttle(() => {
+ const scrollS = window.scrollY + window.outerHeight
+ const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
+ if (scrollS > clientHeight + 100) {
+ handleGetMore()
+ }
+ }, 500))
+ const showPageCover = siteConfig('EXAMPLE_POST_LIST_COVER', null, CONFIG)
+
+ useEffect(() => {
+ window.addEventListener('scroll', scrollTrigger)
+
+ return () => {
+ window.removeEventListener('scroll', scrollTrigger)
+ }
+ })
+
+ return (
+
+
+
+ {postsToShow?.map(post => (
+
+ ))}
+
+
+ {' '}
+ {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
+
+
+
+ )
+}
diff --git a/themes/movie/components/BlogPostCard.js b/themes/movie/components/BlogPostCard.js
new file mode 100644
index 00000000..0fc149e1
--- /dev/null
+++ b/themes/movie/components/BlogPostCard.js
@@ -0,0 +1,62 @@
+import { siteConfig } from '@/lib/config'
+import Link from 'next/link'
+import TagItemMini from './TagItemMini'
+import CONFIG from '../config'
+import TwikooCommentCount from '@/components/TwikooCommentCount'
+import LazyImage from '@/components/LazyImage'
+import { formatDateFmt } from '@/lib/utils/formatDate'
+import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils'
+import NotionIcon from '@/components/NotionIcon'
+
+const BlogPostCard = ({ index, post, showSummary, siteInfo }) => {
+ // 主题默认强制显示图片
+ if (post && !post.pageCoverThumbnail) {
+ post.pageCoverThumbnail =
+ siteInfo?.pageCover || siteConfig('RANDOM_IMAGE_URL')
+ }
+
+ const url = checkContainHttp(post.slug)
+ ? sliceUrlFromHttp(post.slug)
+ : `${siteConfig('SUB_PATH', '')}/${post.slug}`
+
+ return (
+
+
+ {/* 固定高度 ,空白用图片拉升填充 */}
+
+ {/* 图片 填充卡片 */}
+
+
+
+
+
+ {post?.tagItems && post?.tagItems.length > 0 && (
+ <>
+
+ {post.tagItems.map(tag => (
+
+ ))}
+
+ >
+ )}
+
+ {/* 阴影遮罩 */}
+
+
+ {post.title}
+
+
+
+
+
+
+ )
+}
+
+export default BlogPostCard
diff --git a/themes/movie/components/CategoryItem.js b/themes/movie/components/CategoryItem.js
new file mode 100644
index 00000000..d69de464
--- /dev/null
+++ b/themes/movie/components/CategoryItem.js
@@ -0,0 +1,20 @@
+import Link from 'next/link'
+
+/**
+ * 文章分类
+ * @param {*} param0
+ * @returns
+ */
+export default function CategoryItem({ category }) {
+ return (
+
+
+ {category.name}({category.count})
+
+
+ )
+}
diff --git a/themes/movie/components/ExampleRecentComments.js b/themes/movie/components/ExampleRecentComments.js
new file mode 100644
index 00000000..9dbdfa7f
--- /dev/null
+++ b/themes/movie/components/ExampleRecentComments.js
@@ -0,0 +1,35 @@
+import { useEffect, useState } from 'react'
+import { siteConfig } from '@/lib/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] = useState([])
+ const [onLoading, changeLoading] = useState(true)
+ useEffect(() => {
+ RecentComments({
+ serverURL: siteConfig('COMMENT_WALINE_SERVER_URL'),
+ count: 5
+ }).then(({ comments }) => {
+ changeLoading(false)
+ updateComments(comments)
+ })
+ }, [])
+
+ return <>
+ {onLoading &&
Loading...
}
+ {!onLoading && comments && comments.length === 0 &&
No Comments
}
+ {!onLoading && comments && comments.length > 0 && comments.map((comment) =>
)}
+
+ >
+}
+
+export default ExampleRecentComments
diff --git a/themes/movie/components/Footer.js b/themes/movie/components/Footer.js
new file mode 100644
index 00000000..77c8bad6
--- /dev/null
+++ b/themes/movie/components/Footer.js
@@ -0,0 +1,26 @@
+import DarkModeButton from '@/components/DarkModeButton'
+import { siteConfig } from '@/lib/config'
+
+export const Footer = (props) => {
+ const d = new Date()
+ const currentYear = d.getFullYear()
+ const since = siteConfig('SINCE')
+ const copyrightDate = parseInt(since) < currentYear ? since + '-' + currentYear : currentYear
+
+ return
+}
diff --git a/themes/movie/components/Header.js b/themes/movie/components/Header.js
new file mode 100644
index 00000000..5a8864d8
--- /dev/null
+++ b/themes/movie/components/Header.js
@@ -0,0 +1,26 @@
+import Link from 'next/link'
+import { siteConfig } from '@/lib/config'
+import { MenuListTop } from './MenuListTop'
+
+/**
+ * 网站顶部
+ * @returns
+ */
+export const Header = props => {
+ return (
+
+ )
+}
diff --git a/themes/movie/components/JumpToTopButton.js b/themes/movie/components/JumpToTopButton.js
new file mode 100644
index 00000000..f5e22b61
--- /dev/null
+++ b/themes/movie/components/JumpToTopButton.js
@@ -0,0 +1,18 @@
+import { useGlobal } from '@/lib/global'
+
+/**
+ * 跳转到网页顶部
+ * 当屏幕下滑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/movie/components/LoadingCover.js b/themes/movie/components/LoadingCover.js
new file mode 100644
index 00000000..75976180
--- /dev/null
+++ b/themes/movie/components/LoadingCover.js
@@ -0,0 +1,8 @@
+
+export default function LoadingCover() {
+ return
+}
diff --git a/themes/movie/components/MenuItemDrop.js b/themes/movie/components/MenuItemDrop.js
new file mode 100644
index 00000000..070c9d15
--- /dev/null
+++ b/themes/movie/components/MenuItemDrop.js
@@ -0,0 +1,67 @@
+import Link from 'next/link'
+import { useState } from 'react'
+
+export const MenuItemDrop = ({ link }) => {
+ const [show, changeShow] = useState(false)
+ const hasSubMenu = link?.subMenus?.length > 0
+
+ if (!link || !link.show) {
+ return null
+ }
+
+ return (
+
changeShow(true)}
+ onMouseOut={() => changeShow(false)}
+ >
+ {!hasSubMenu && (
+
+ {link?.icon &&
} {link?.name}
+ {hasSubMenu &&
}
+
+ )}
+
+ {hasSubMenu && (
+ <>
+
+ {link?.icon && } {link?.name}
+
+
+ >
+ )}
+
+ {/* 子菜单 */}
+ {hasSubMenu && (
+
+ {link.subMenus.map((sLink, index) => {
+ return (
+ -
+
+
+ {link?.icon && }
+ {sLink.title}
+
+
+
+ )
+ })}
+
+ )}
+
+ )
+}
diff --git a/themes/movie/components/MenuListTop.js b/themes/movie/components/MenuListTop.js
new file mode 100644
index 00000000..f5d2c9cd
--- /dev/null
+++ b/themes/movie/components/MenuListTop.js
@@ -0,0 +1,42 @@
+import { useGlobal } from '@/lib/global'
+import CONFIG from '../config'
+import { siteConfig } from '@/lib/config'
+import { MenuItemDrop } from './MenuItemDrop'
+
+export const MenuListTop = (props) => {
+ const { customNav, customMenu } = props
+ const { locale } = useGlobal()
+
+ let links = [
+ { id: 1, icon: 'fa-solid fa-house', name: locale.NAV.INDEX, to: '/', show: siteConfig('HEXO_MENU_INDEX', null, CONFIG) },
+ { id: 2, icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: siteConfig('HEXO_MENU_SEARCH', null, CONFIG) },
+ { id: 3, icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: siteConfig('HEXO_MENU_ARCHIVE', null, CONFIG) }
+ // { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: siteConfig('MENU_CATEGORY', null, CONFIG) },
+ // { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: siteConfig('MENU_TAG', null, CONFIG) }
+ ]
+
+ if (customNav) {
+ links = links.concat(customNav)
+ }
+
+ for (let i = 0; i < links.length; i++) {
+ if (links[i].id !== i) {
+ links[i].id = i
+ }
+ }
+
+ // 如果 开启自定义菜单,则覆盖Page生成的菜单
+ if (siteConfig('CUSTOM_MENU')) {
+ links = customMenu
+ }
+
+ if (!links || links.length === 0) {
+ return null
+ }
+
+ return (<>
+
+ >)
+}
diff --git a/themes/movie/components/Nav.js b/themes/movie/components/Nav.js
new file mode 100644
index 00000000..08f5ca56
--- /dev/null
+++ b/themes/movie/components/Nav.js
@@ -0,0 +1,73 @@
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import CONFIG from '../config'
+import { MenuItemDrop } from './MenuItemDrop'
+
+/**
+ * 菜单导航
+ * @param {*} props
+ * @returns
+ */
+export const Nav = props => {
+ const { customNav, customMenu } = props
+ const { locale } = useGlobal()
+
+ let links = [
+ {
+ id: 1,
+ icon: 'fas fa-search',
+ name: locale.NAV.SEARCH,
+ to: '/search',
+ show: siteConfig('EXAMPLE_MENU_SEARCH', null, CONFIG)
+ },
+ {
+ id: 2,
+ icon: 'fas fa-archive',
+ name: locale.NAV.ARCHIVE,
+ to: '/archive',
+ show: siteConfig('EXAMPLE_MENU_ARCHIVE', null, CONFIG)
+ },
+ {
+ id: 3,
+ icon: 'fas fa-folder',
+ name: locale.COMMON.CATEGORY,
+ to: '/category',
+ show: siteConfig('EXAMPLE_MENU_CATEGORY', null, CONFIG)
+ },
+ {
+ id: 4,
+ icon: 'fas fa-tag',
+ name: locale.COMMON.TAGS,
+ to: '/tag',
+ show: siteConfig('EXAMPLE_MENU_TAG', null, CONFIG)
+ }
+ ]
+
+ if (customNav) {
+ links = links.concat(customNav)
+ }
+
+ // 如果 开启自定义菜单,则不再使用 Page生成菜单。
+ if (siteConfig('CUSTOM_MENU')) {
+ links = customMenu
+ }
+
+ if (!links || links.length === 0) {
+ return null
+ }
+
+ return (
+
+ )
+}
diff --git a/themes/movie/components/NormalMenuItem.js b/themes/movie/components/NormalMenuItem.js
new file mode 100644
index 00000000..aec74837
--- /dev/null
+++ b/themes/movie/components/NormalMenuItem.js
@@ -0,0 +1,14 @@
+import Link from 'next/link'
+
+/**
+ * 旧的普通菜单
+ * @param {*} props
+ * @returns
+ */
+export const NormalMenuItem = (props) => {
+ const { link } = props
+ return link?.show &&
+ {link.name}
+
+}
diff --git a/themes/movie/components/PaginationNumber.js b/themes/movie/components/PaginationNumber.js
new file mode 100644
index 00000000..54c1bf34
--- /dev/null
+++ b/themes/movie/components/PaginationNumber.js
@@ -0,0 +1,218 @@
+import { ChevronDoubleRight } from '@/components/HeroIcons'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useState } from 'react'
+
+/**
+ * 数字翻页插件
+ * @param page 当前页码
+ * @param showNext 是否有下一页
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const PaginationNumber = ({ page, totalPage }) => {
+ const router = useRouter()
+ const { locale } = useGlobal()
+ const currentPage = +page
+ const showNext = page < totalPage
+ const showPrev = currentPage !== 1
+ const pagePrefix = router.asPath
+ .split('?')[0]
+ .replace(/\/page\/[1-9]\d*/, '')
+ .replace(/\/$/, '')
+ const pages = generatePages(pagePrefix, page, currentPage, totalPage)
+
+ const [value, setValue] = useState('')
+
+ const handleInputChange = event => {
+ const newValue = event.target.value.replace(/[^0-9]/g, '')
+ setValue(newValue)
+ }
+
+ /**
+ * 调到指定页
+ */
+ const jumpToPage = () => {
+ if (value) {
+ router.push(
+ value === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${value}`
+ )
+ }
+ }
+
+ return (
+ <>
+ {/* pc端分页按钮 */}
+
+ {/* 上一页 */}
+
+
+
+
+ {locale.PAGINATION.PREV}
+
+
+
+
+ {/* 分页 */}
+
+ {pages}
+
+ {/* 跳转页码 */}
+
+
+
+ {/* 下一页 */}
+
+
+
+
+ {locale.PAGINATION.NEXT}
+
+
+
+
+
+ {/* 移动端分页 */}
+
+
+ {/* 上一页 */}
+
+ {locale.PAGINATION.PREV}
+
+
+ {showPrev && showNext &&
}
+
+ {/* 下一页 */}
+
+ {locale.PAGINATION.NEXT}
+
+
+ >
+ )
+}
+
+/**
+ * 页码按钮
+ * @param {*} page
+ * @param {*} currentPage
+ * @param {*} pagePrefix
+ * @returns
+ */
+function getPageElement(page, currentPage, pagePrefix) {
+ const selected = page + '' === currentPage + ''
+ if (!page) {
+ return <>>
+ }
+ return (
+
+ {page}
+
+ )
+}
+
+/**
+ * 获取所有页码
+ * @param {*} pagePrefix
+ * @param {*} page
+ * @param {*} currentPage
+ * @param {*} totalPage
+ * @returns
+ */
+function generatePages(pagePrefix, page, currentPage, totalPage) {
+ const pages = []
+ const groupCount = 7 // 最多显示页签数
+ if (totalPage <= groupCount) {
+ for (let i = 1; i <= totalPage; i++) {
+ pages.push(getPageElement(i, page, pagePrefix))
+ }
+ } else {
+ pages.push(getPageElement(1, page, pagePrefix))
+ const dynamicGroupCount = groupCount - 2
+ let startPage = currentPage - 2
+ if (startPage <= 1) {
+ startPage = 2
+ }
+ if (startPage + dynamicGroupCount > totalPage) {
+ startPage = totalPage - dynamicGroupCount
+ }
+ if (startPage > 2) {
+ pages.push(
+
+ ...{' '}
+
+ )
+ }
+
+ for (let i = 0; i < dynamicGroupCount; i++) {
+ if (startPage + i < totalPage) {
+ pages.push(getPageElement(startPage + i, page, pagePrefix))
+ }
+ }
+
+ if (startPage + dynamicGroupCount < totalPage) {
+ pages.push(
...
)
+ }
+
+ pages.push(getPageElement(totalPage, page, pagePrefix))
+ }
+ return pages
+}
+export default PaginationNumber
diff --git a/themes/movie/components/SearchInput.js b/themes/movie/components/SearchInput.js
new file mode 100644
index 00000000..4f375d4e
--- /dev/null
+++ b/themes/movie/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, keyword, 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/movie/components/SideBar.js b/themes/movie/components/SideBar.js
new file mode 100644
index 00000000..ea0531b7
--- /dev/null
+++ b/themes/movie/components/SideBar.js
@@ -0,0 +1,68 @@
+import { siteConfig } from '@/lib/config'
+import Live2D from '@/components/Live2D'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import dynamic from 'next/dynamic'
+import Announcement from './Announcement'
+const ExampleRecentComments = dynamic(() => import('./ExampleRecentComments'))
+
+export const SideBar = (props) => {
+ const { locale } = useGlobal()
+ const { latestPosts, categoryOptions, notice } = props
+ return (
+
+
+
+
+
+
+
+
+ {siteConfig('COMMENT_WALINE_SERVER_URL') && siteConfig('COMMENT_WALINE_RECENT') && }
+
+
+
+
+ )
+}
diff --git a/themes/movie/components/TagItem.js b/themes/movie/components/TagItem.js
new file mode 100644
index 00000000..dab00325
--- /dev/null
+++ b/themes/movie/components/TagItem.js
@@ -0,0 +1,24 @@
+import Link from 'next/link'
+
+/**
+ * 标签
+ * @param {*} param0
+ * @returns
+ */
+export default function TagItem({ tag }) {
+ return (
+
+
+
+ {' '}
+ {tag.name + (tag.count ? `(${tag.count})` : '')}{' '}
+
+
+
+ )
+}
diff --git a/themes/movie/components/TagItemMini.js b/themes/movie/components/TagItemMini.js
new file mode 100644
index 00000000..e2dcc69f
--- /dev/null
+++ b/themes/movie/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/movie/components/Title.js b/themes/movie/components/Title.js
new file mode 100644
index 00000000..73ca9d7f
--- /dev/null
+++ b/themes/movie/components/Title.js
@@ -0,0 +1,20 @@
+import NotionIcon from '@/components/NotionIcon'
+import { siteConfig } from '@/lib/config'
+
+/**
+ * 标题栏
+ * @param {*} props
+ * @returns
+ */
+export const Title = (props) => {
+ const { post } = props
+ const title = post?.title || siteConfig('TITLE')
+ const description = post?.description || siteConfig('AUTHOR')
+
+ return
+
{title}
+
+ {description}
+
+
+}
diff --git a/themes/movie/config.js b/themes/movie/config.js
new file mode 100644
index 00000000..181d3a93
--- /dev/null
+++ b/themes/movie/config.js
@@ -0,0 +1,14 @@
+/**
+ * 主题配置文件
+ */
+const CONFIG = {
+ // 菜单配置
+ EXAMPLE_MENU_CATEGORY: true, // 显示分类
+ EXAMPLE_MENU_TAG: true, // 显示标签
+ EXAMPLE_MENU_ARCHIVE: true, // 显示归档
+ EXAMPLE_MENU_SEARCH: true, // 显示搜索
+
+ EXAMPLE_POST_LIST_COVER: true // 列表显示文章封面
+
+}
+export default CONFIG
diff --git a/themes/movie/index.js b/themes/movie/index.js
new file mode 100644
index 00000000..15034d73
--- /dev/null
+++ b/themes/movie/index.js
@@ -0,0 +1,303 @@
+'use client'
+
+import CONFIG from './config'
+import { useEffect } from 'react'
+import { Header } from './components/Header'
+import { Nav } from './components/Nav'
+import { Footer } from './components/Footer'
+import { Title } from './components/Title'
+import { SideBar } from './components/SideBar'
+import { BlogListPage } from './components/BlogListPage'
+import { BlogListScroll } from './components/BlogListScroll'
+import { useGlobal } from '@/lib/global'
+import { ArticleLock } from './components/ArticleLock'
+import { ArticleInfo } from './components/ArticleInfo'
+import JumpToTopButton from './components/JumpToTopButton'
+import NotionPage from '@/components/NotionPage'
+import Comment from '@/components/Comment'
+import ShareBar from '@/components/ShareBar'
+import SearchInput from './components/SearchInput'
+import replaceSearchResult from '@/components/Mark'
+import { isBrowser } from '@/lib/utils'
+import BlogListGroupByDate from './components/BlogListGroupByDate'
+import CategoryItem from './components/CategoryItem'
+import TagItem from './components/TagItem'
+import { useRouter } from 'next/router'
+import { Transition } from '@headlessui/react'
+import { Style } from './style'
+import { siteConfig } from '@/lib/config'
+
+/**
+ * 基础布局框架
+ * 1.其它页面都嵌入在LayoutBase中
+ * 2.采用左右两侧布局,移动端使用顶部导航栏
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const LayoutBase = props => {
+ const { children } = props
+ const { onLoading, fullWidth } = useGlobal()
+ const router = useRouter()
+ const { category, tag } = props
+ // 顶部如果是按照分类或标签查看文章列表,列表顶部嵌入一个横幅
+ // 如果是搜索,则列表顶部嵌入 搜索框
+ let slotTop = null
+ if (category) {
+ slotTop = (
+
+
+ {category}
+
+ )
+ } else if (tag) {
+ slotTop =
#{tag}
+ } else if (props.slotTop) {
+ slotTop = props.slotTop
+ } else if (router.route === '/search') {
+ // 嵌入一个搜索框在顶部
+ slotTop = (
+
+
+
+ )
+ }
+
+ // 增加一个状态以触发 Transition 组件的动画
+ // const [showTransition, setShowTransition] = useState(true)
+ // useEffect(() => {
+ // // 当 location 或 children 发生变化时,触发动画
+ // setShowTransition(false)
+ // setTimeout(() => setShowTransition(true), 5)
+ // }, [onLoading])
+
+ return (
+
+
+
+ {/* 页头 */}
+
+
+ {/* 主体 */}
+
+ {/* 标题栏 */}
+ {/* {fullWidth ? null :
} */}
+
+
+ {/* 内容 */}
+
+
+ {/* 嵌入模块 */}
+ {slotTop}
+ {children}
+
+
+
+
+
+ {/* 页脚 */}
+
+
+ {/* 回顶按钮 */}
+
+
+
+
+ )
+}
+
+/**
+ * 首页
+ * @param {*} props
+ * @returns 此主题首页就是列表
+ */
+const LayoutIndex = props => {
+ return
+}
+
+/**
+ * 文章列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutPostList = props => {
+ return (
+ <>
+ {siteConfig('POST_LIST_STYLE') === 'page' ? (
+
+ ) : (
+
+ )}
+ >
+ )
+}
+
+/**
+ * 文章详情页
+ * @param {*} props
+ * @returns
+ */
+const LayoutSlug = props => {
+ const { post, lock, validPassword } = props
+ 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 ? (
+
+ ) : (
+
+ )}
+ >
+ )
+}
+
+/**
+ * 404页
+ * @param {*} props
+ * @returns
+ */
+const Layout404 = props => {
+ return <>404 Not found.>
+}
+
+/**
+ * 搜索页
+ * @param {*} props
+ * @returns
+ */
+const LayoutSearch = props => {
+ const { keyword } = props
+ const router = useRouter()
+ useEffect(() => {
+ if (isBrowser) {
+ // 高亮搜索到的结果
+ const container = document.getElementById('posts-wrapper')
+ if (keyword && container) {
+ replaceSearchResult({
+ doms: container,
+ search: keyword,
+ target: {
+ element: 'span',
+ className: 'text-red-500 border-b border-dashed'
+ }
+ })
+ }
+ }
+ }, [router])
+
+ return
+}
+
+/**
+ * 归档列表
+ * @param {*} props
+ * @returns 按照日期将文章分组排序
+ */
+const LayoutArchive = props => {
+ const { archivePosts } = props
+ return (
+ <>
+
+ {Object.keys(archivePosts).map(archiveTitle => (
+
+ ))}
+
+ >
+ )
+}
+
+/**
+ * 分类列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutCategoryIndex = props => {
+ const { categoryOptions } = props
+ return (
+ <>
+
+ {categoryOptions?.map(category => (
+
+ ))}
+
+ >
+ )
+}
+
+/**
+ * 标签列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutTagIndex = props => {
+ const { tagOptions } = props
+ return (
+ <>
+
+ {tagOptions.map(tag => (
+
+ ))}
+
+ >
+ )
+}
+
+export {
+ CONFIG as THEME_CONFIG,
+ LayoutBase,
+ LayoutIndex,
+ LayoutSearch,
+ LayoutArchive,
+ LayoutSlug,
+ Layout404,
+ LayoutPostList,
+ LayoutCategoryIndex,
+ LayoutTagIndex
+}
diff --git a/themes/movie/style.js b/themes/movie/style.js
new file mode 100644
index 00000000..7aee6f73
--- /dev/null
+++ b/themes/movie/style.js
@@ -0,0 +1,24 @@
+/* eslint-disable react/no-unknown-property */
+/**
+ * 此处样式只对当前主题生效
+ * 此处不支持tailwindCSS的 @apply 语法
+ * @returns
+ */
+const Style = () => {
+ return (
+
+ )
+}
+
+export { Style }