diff --git a/themes/heo/components/PaginationNumber.js b/themes/heo/components/PaginationNumber.js index 4a1022ee..54c1bf34 100644 --- a/themes/heo/components/PaginationNumber.js +++ b/themes/heo/components/PaginationNumber.js @@ -17,105 +17,126 @@ const PaginationNumber = ({ page, totalPage }) => { const currentPage = +page const showNext = page < totalPage const showPrev = currentPage !== 1 - const pagePrefix = router.asPath.split('?')[0].replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '') + 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 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}`) + router.push( + value === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${value}` + ) } } - return (<> - - {/* pc端分页按钮 */} -
- {/* 上一页 */} - -
- -
- {locale.PAGINATION.PREV} -
-
- - - - {/* 分页 */} -
- {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} +
+ +
    + {archivePosts[archiveTitle].map(post => { + const url = checkContainHttp(post.slug) ? sliceUrlFromHttp(post.slug) : `${siteConfig('SUB_PATH', '')}/${post.slug}` + + return
  • +
    + + {post?.publishDay} + {' '} +   + + {post.title} + +
    +
  • + })} +
+
+} 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) =>
+
+
--{comment.nick}
+
)} + + +} + +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 ( +
+
+ + {siteConfig('TITLE')} + +
+ {/* 右侧文字 */} + +
+
+
+ ) +} 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
+ updateSearchKey(e.target.value)} + defaultValue={keyword || ''} + /> + +
+ +
+ + {(showClean && +
+ +
+ )} +
+} + +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 ( +
+ + ) +} + +export { Style }