From ac91695d75f1dfcb4b4012ffb2701bc678031e14 Mon Sep 17 00:00:00 2001 From: "tangly1024.com" Date: Thu, 14 Mar 2024 18:32:37 +0800 Subject: [PATCH 01/13] theme-movie --- themes/heo/components/PaginationNumber.js | 211 ++++++------ themes/movie/components/Announcement.js | 22 ++ themes/movie/components/ArticleInfo.js | 51 +++ themes/movie/components/ArticleLock.js | 52 +++ .../movie/components/BlogListGroupByDate.js | 38 +++ themes/movie/components/BlogListPage.js | 41 +++ themes/movie/components/BlogListScroll.js | 66 ++++ themes/movie/components/BlogPostCard.js | 62 ++++ themes/movie/components/CategoryItem.js | 20 ++ .../movie/components/ExampleRecentComments.js | 35 ++ themes/movie/components/Footer.js | 26 ++ themes/movie/components/Header.js | 26 ++ themes/movie/components/JumpToTopButton.js | 18 ++ themes/movie/components/LoadingCover.js | 8 + themes/movie/components/MenuItemDrop.js | 67 ++++ themes/movie/components/MenuListTop.js | 42 +++ themes/movie/components/Nav.js | 73 +++++ themes/movie/components/NormalMenuItem.js | 14 + themes/movie/components/PaginationNumber.js | 218 +++++++++++++ themes/movie/components/SearchInput.js | 87 +++++ themes/movie/components/SideBar.js | 68 ++++ themes/movie/components/TagItem.js | 24 ++ themes/movie/components/TagItemMini.js | 21 ++ themes/movie/components/Title.js | 20 ++ themes/movie/config.js | 14 + themes/movie/index.js | 303 ++++++++++++++++++ themes/movie/style.js | 24 ++ 27 files changed, 1559 insertions(+), 92 deletions(-) create mode 100644 themes/movie/components/Announcement.js create mode 100644 themes/movie/components/ArticleInfo.js create mode 100644 themes/movie/components/ArticleLock.js create mode 100644 themes/movie/components/BlogListGroupByDate.js create mode 100644 themes/movie/components/BlogListPage.js create mode 100644 themes/movie/components/BlogListScroll.js create mode 100644 themes/movie/components/BlogPostCard.js create mode 100644 themes/movie/components/CategoryItem.js create mode 100644 themes/movie/components/ExampleRecentComments.js create mode 100644 themes/movie/components/Footer.js create mode 100644 themes/movie/components/Header.js create mode 100644 themes/movie/components/JumpToTopButton.js create mode 100644 themes/movie/components/LoadingCover.js create mode 100644 themes/movie/components/MenuItemDrop.js create mode 100644 themes/movie/components/MenuListTop.js create mode 100644 themes/movie/components/Nav.js create mode 100644 themes/movie/components/NormalMenuItem.js create mode 100644 themes/movie/components/PaginationNumber.js create mode 100644 themes/movie/components/SearchInput.js create mode 100644 themes/movie/components/SideBar.js create mode 100644 themes/movie/components/TagItem.js create mode 100644 themes/movie/components/TagItemMini.js create mode 100644 themes/movie/components/Title.js create mode 100644 themes/movie/config.js create mode 100644 themes/movie/index.js create mode 100644 themes/movie/style.js 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 } From 3ed5f16a2054863fd42531e7e35c8a59e322fabc Mon Sep 17 00:00:00 2001 From: "tangly1024.com" Date: Fri, 15 Mar 2024 12:44:34 +0800 Subject: [PATCH 02/13] movie 404 --- lib/db/getSiteData.js | 152 +++++++++++++---- themes/movie/components/ArchiveDateList.js | 38 +++++ themes/movie/components/CategoryGroup.js | 43 +++++ themes/movie/components/Header.js | 4 +- themes/movie/components/LatestPostsGroup.js | 62 +++++++ themes/movie/components/MenuItemDrop.js | 6 +- themes/movie/components/MenuListTop.js | 41 ++++- themes/movie/components/TagGroups.js | 51 ++++++ themes/movie/components/TagItemMini.js | 6 +- themes/movie/index.js | 180 ++++++++++++-------- themes/simple/components/NavBar.js | 52 ++++-- 11 files changed, 496 insertions(+), 139 deletions(-) create mode 100644 themes/movie/components/ArchiveDateList.js create mode 100644 themes/movie/components/CategoryGroup.js create mode 100644 themes/movie/components/LatestPostsGroup.js create mode 100644 themes/movie/components/TagGroups.js diff --git a/lib/db/getSiteData.js b/lib/db/getSiteData.js index 85dd0c7a..997a97c6 100755 --- a/lib/db/getSiteData.js +++ b/lib/db/getSiteData.js @@ -25,10 +25,7 @@ export { getPost } from '../notion/getNotionPost' * @returns * */ -export async function getGlobalData({ - pageId = BLOG.NOTION_PAGE_ID, - from -}) { +export async function getGlobalData({ pageId = BLOG.NOTION_PAGE_ID, from }) { // 从notion获取 const data = await getNotionPageData({ pageId, from }) const db = deepClone(data) @@ -89,7 +86,9 @@ function cleanBlock(post) { * @returns */ function getLatestPosts({ allPages, from, latestPostCount }) { - const allPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published') + const allPosts = allPages?.filter( + page => page.type === 'Post' && page.status === 'Published' + ) const latestPosts = Object.create(allPosts).sort((a, b) => { const dateA = new Date(a?.lastEditedDate || a?.publishDate) @@ -140,7 +139,13 @@ function getCustomNav({ allPages }) { p.to = '/' + p.slug } } - customNav.push({ icon: p.icon || null, name: p.title, to: p.slug, target: '_blank', show: true }) + customNav.push({ + icon: p.icon || null, + name: p.title, + to: p.slug, + target: '_blank', + show: true + }) }) } return customNav @@ -152,7 +157,12 @@ function getCustomNav({ allPages }) { * @returns */ function getCustomMenu({ collectionData }) { - const menuPages = collectionData.filter(post => post.status === 'Published' && (post?.type === BLOG.NOTION_PROPERTY_NAME.type_menu || post?.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu)) + const menuPages = collectionData.filter( + post => + post.status === 'Published' && + (post?.type === BLOG.NOTION_PROPERTY_NAME.type_menu || + post?.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu) + ) const menus = [] if (menuPages && menuPages.length > 0) { menuPages.forEach(e => { @@ -190,7 +200,9 @@ function getCustomMenu({ collectionData }) { */ function getTagOptions(schema) { if (!schema) return {} - const tagSchema = Object.values(schema).find(e => e.name === BLOG.NOTION_PROPERTY_NAME.tags) + const tagSchema = Object.values(schema).find( + e => e.name === BLOG.NOTION_PROPERTY_NAME.tags + ) return tagSchema?.options || [] } @@ -201,7 +213,9 @@ function getTagOptions(schema) { */ function getCategoryOptions(schema) { if (!schema) return {} - const categorySchema = Object.values(schema).find(e => e.name === BLOG.NOTION_PROPERTY_NAME.category) + const categorySchema = Object.values(schema).find( + e => e.name === BLOG.NOTION_PROPERTY_NAME.category + ) return categorySchema?.options || [] } @@ -213,9 +227,15 @@ function getCategoryOptions(schema) { */ function getSiteInfo({ collection, block }) { const title = collection?.name?.[0][0] || BLOG.TITLE - const description = collection?.description ? Object.assign(collection).description[0][0] : BLOG.DESCRIPTION - const pageCover = collection?.cover ? mapImgUrl(collection?.cover, block[idToUuid(BLOG.NOTION_PAGE_ID)]?.value) : BLOG.HOME_BANNER_IMAGE - let icon = collection?.icon ? mapImgUrl(collection?.icon, collection, 'collection') : BLOG.AVATAR + const description = collection?.description + ? Object.assign(collection).description[0][0] + : BLOG.DESCRIPTION + const pageCover = collection?.cover + ? mapImgUrl(collection?.cover, block[idToUuid(BLOG.NOTION_PAGE_ID)]?.value) + : BLOG.HOME_BANNER_IMAGE + let icon = collection?.icon + ? mapImgUrl(collection?.icon, collection, 'collection') + : BLOG.AVATAR // 用户头像压缩一下 icon = compressImage(icon) @@ -236,7 +256,13 @@ function getSiteInfo({ collection, block }) { */ export function getNavPages({ allPages }) { const allNavPages = allPages?.filter(post => { - return post && post?.slug && (!post?.slug?.startsWith('http')) && post?.type === 'Post' && post?.status === 'Published' + return ( + post && + post?.slug && + !post?.slug?.startsWith('http') && + post?.type === 'Post' && + post?.status === 'Published' + ) }) return allNavPages.map(item => ({ @@ -248,7 +274,8 @@ export function getNavPages({ allPages }) { summary: item.summary || null, slug: item.slug, pageIcon: item.pageIcon || '', - lastEditedDate: item.lastEditedDate + lastEditedDate: item.lastEditedDate, + publishDate: item.publishDate })) } @@ -265,19 +292,26 @@ async function getNotice(post) { } // 没有数据时返回 -const EmptyData = (pageId) => { +const EmptyData = pageId => { const empty = { notice: null, siteInfo: getSiteInfo({}), - allPages: [{ - id: 1, - title: `无法获取Notion数据,请检查Notion_ID: \n 当前 ${pageId}`, - summary: '访问文档获取帮助→ https://tangly1024.com/article/vercel-deploy-notion-next', - status: 'Published', - type: 'Post', - slug: '13a171332816461db29d50e9f575b00d', - date: { start_date: '2023-04-24', lastEditedDay: '2023-04-24', tagItems: [] } - }], + allPages: [ + { + id: 1, + title: `无法获取Notion数据,请检查Notion_ID: \n 当前 ${pageId}`, + summary: + '访问文档获取帮助→ https://tangly1024.com/article/vercel-deploy-notion-next', + status: 'Published', + type: 'Post', + slug: '13a171332816461db29d50e9f575b00d', + date: { + start_date: '2023-04-24', + lastEditedDay: '2023-04-24', + tagItems: [] + } + } + ], allNavPages: [], collection: [], collectionQuery: {}, @@ -313,7 +347,8 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { const rawMetadata = block[pageId]?.value // Check Type Page-Database和Inline-Database if ( - rawMetadata?.type !== 'collection_view_page' && rawMetadata?.type !== 'collection_view' + rawMetadata?.type !== 'collection_view_page' && + rawMetadata?.type !== 'collection_view' ) { console.error(`pageId "${pageId}" is not a database`) return EmptyData(pageId) @@ -328,9 +363,21 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { const viewIds = rawMetadata?.view_ids const collectionData = [] - const pageIds = getAllPageIds(collectionQuery, collectionId, collectionView, viewIds) + const pageIds = getAllPageIds( + collectionQuery, + collectionId, + collectionView, + viewIds + ) if (pageIds?.length === 0) { - console.error('获取到的文章列表为空,请检查notion模板', collectionQuery, collection, collectionView, viewIds, pageRecordMap) + console.error( + '获取到的文章列表为空,请检查notion模板', + collectionQuery, + collection, + collectionView, + viewIds, + pageRecordMap + ) } else { // console.log('有效Page数量', pageIds?.length) } @@ -343,7 +390,14 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { // 如果找不到文章对应的block,说明发生了溢出,使用pageID再去请求 const pageBlock = await getSingleBlock(id, from) if (pageBlock.block[id].value) { - const properties = (await getPageProperties(id, pageBlock.block[id].value, schema, null, getTagOptions(schema))) || null + const properties = + (await getPageProperties( + id, + pageBlock.block[id].value, + schema, + null, + getTagOptions(schema) + )) || null if (properties) { collectionData.push(properties) } @@ -351,7 +405,14 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { continue } - const properties = (await getPageProperties(id, value, schema, null, getTagOptions(schema))) || null + const properties = + (await getPageProperties( + id, + value, + schema, + null, + getTagOptions(schema) + )) || null if (properties) { collectionData.push(properties) } @@ -365,13 +426,16 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { if (post?.type === 'Post' && post.status === 'Published') { postCount++ } - return post && post?.slug && - (!post?.slug?.startsWith('http')) && + return ( + post && + post?.slug && + !post?.slug?.startsWith('http') && (post?.status === 'Invisible' || post?.status === 'Published') + ) }) // 站点配置优先读取配置表格,否则读取blog.config.js 文件 - const NOTION_CONFIG = await getConfigMapFromConfigPage(collectionData) || {} + const NOTION_CONFIG = (await getConfigMapFromConfigPage(collectionData)) || {} // Sort by date if (BLOG.POSTS_SORT_BY === 'date') { @@ -380,13 +444,27 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { }) } - const notice = await getNotice(collectionData.filter(post => { - return post && post?.type && post?.type === 'Notice' && post.status === 'Published' - })?.[0]) - const categoryOptions = getAllCategories({ allPages, categoryOptions: getCategoryOptions(schema) }) + const notice = await getNotice( + collectionData.filter(post => { + return ( + post && + post?.type && + post?.type === 'Notice' && + post.status === 'Published' + ) + })?.[0] + ) + const categoryOptions = getAllCategories({ + allPages, + categoryOptions: getCategoryOptions(schema) + }) const tagOptions = getAllTags({ allPages, tagOptions: getTagOptions(schema) }) // 旧的菜单 - const customNav = getCustomNav({ allPages: collectionData.filter(post => post?.type === 'Page' && post.status === 'Published') }) + const customNav = getCustomNav({ + allPages: collectionData.filter( + post => post?.type === 'Page' && post.status === 'Published' + ) + }) // 新的菜单 const customMenu = await getCustomMenu({ collectionData }) const latestPosts = getLatestPosts({ allPages, from, latestPostCount: 6 }) diff --git a/themes/movie/components/ArchiveDateList.js b/themes/movie/components/ArchiveDateList.js new file mode 100644 index 00000000..cd389028 --- /dev/null +++ b/themes/movie/components/ArchiveDateList.js @@ -0,0 +1,38 @@ +import { useGlobal } from '@/lib/global' +import { formatDateFmt } from '@/lib/utils/formatDate' +import Link from 'next/link' + +export default function ArchiveDateList(props) { + const postsSortByDate = Object.create(props.allNavPages) + const { locale } = useGlobal() + + postsSortByDate.sort((a, b) => { + return b?.publishDate - a?.publishDate + }) + + let dates = [] + postsSortByDate.forEach(post => { + const date = formatDateFmt(post.publishDate, 'yyyy-MM') + if (!dates[date]) { + dates.push(date) + } + }) + dates = dates.slice(0, 5) + return ( +
+
{locale.NAV.ARCHIVE}
+ {dates?.map((date, index) => { + return ( +
+ + {date} + +
+ ) + })} +
+ ) +} diff --git a/themes/movie/components/CategoryGroup.js b/themes/movie/components/CategoryGroup.js new file mode 100644 index 00000000..63c3917c --- /dev/null +++ b/themes/movie/components/CategoryGroup.js @@ -0,0 +1,43 @@ +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' + +const CategoryGroup = props => { + const { currentCategory, categoryOptions } = props + const { locale } = useGlobal() + if (!categoryOptions || categoryOptions.length === 0) return <> + const categoryCount = siteConfig('PREVIEW_CATEGORY_COUNT') + const categories = categoryOptions.slice(0, categoryCount) + return ( + <> +
+

{locale.COMMON.CATEGORY}

+
+ {categories.map(category => { + const selected = currentCategory === category.name + return ( + + + {category.name}({category.count}) + + ) + })} +
+
+ + ) +} + +export default CategoryGroup diff --git a/themes/movie/components/Header.js b/themes/movie/components/Header.js index 5a8864d8..ae5e3a2a 100644 --- a/themes/movie/components/Header.js +++ b/themes/movie/components/Header.js @@ -8,7 +8,7 @@ import { MenuListTop } from './MenuListTop' */ export const Header = props => { return ( -
+
{ {siteConfig('TITLE')}
- {/* 右侧文字 */} + {/* 右侧菜单 */}
diff --git a/themes/movie/components/LatestPostsGroup.js b/themes/movie/components/LatestPostsGroup.js new file mode 100644 index 00000000..b7ada599 --- /dev/null +++ b/themes/movie/components/LatestPostsGroup.js @@ -0,0 +1,62 @@ +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils' + +/** + * 最新文章列表 + * @param posts 所有文章数据 + * @param sliceCount 截取展示的数量 默认6 + * @constructor + */ +const LatestPostsGroup = ({ latestPosts }) => { + // 获取当前路径 + const currentPath = useRouter().asPath + const { locale } = useGlobal() + + if (!latestPosts) { + return <> + } + + return ( +
+
+
+ + {locale.COMMON.LATEST_POSTS} +
+
+ + {latestPosts.map(post => { + const selected = + currentPath === `${siteConfig('SUB_PATH', '')}/${post.slug}` + const url = checkContainHttp(post.slug) + ? sliceUrlFromHttp(post.slug) + : `${siteConfig('SUB_PATH', '')}/${post.slug}` + return ( + +
+
  • {post.title}
  • +
    + + ) + })} +
    + ) +} +export default LatestPostsGroup diff --git a/themes/movie/components/MenuItemDrop.js b/themes/movie/components/MenuItemDrop.js index 070c9d15..501140a4 100644 --- a/themes/movie/components/MenuItemDrop.js +++ b/themes/movie/components/MenuItemDrop.js @@ -40,19 +40,19 @@ export const MenuItemDrop = ({ link }) => { {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 index f5d2c9cd..d79b2ab5 100644 --- a/themes/movie/components/MenuListTop.js +++ b/themes/movie/components/MenuListTop.js @@ -3,14 +3,32 @@ import CONFIG from '../config' import { siteConfig } from '@/lib/config' import { MenuItemDrop } from './MenuItemDrop' -export const MenuListTop = (props) => { +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) } + { + 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) } ] @@ -34,9 +52,14 @@ export const MenuListTop = (props) => { return null } - return (<> - - ) + return ( + <> + + + ) } diff --git a/themes/movie/components/TagGroups.js b/themes/movie/components/TagGroups.js new file mode 100644 index 00000000..597cddc1 --- /dev/null +++ b/themes/movie/components/TagGroups.js @@ -0,0 +1,51 @@ +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import { useRouter } from 'next/router' + +/** + * 标签组 + * @param tags + * @param currentTag + * @returns {JSX.Element} + * @constructor + */ +const TagGroups = ({ tagOptions, className }) => { + const router = useRouter() + const { locale } = useGlobal() + const { tag: currentTag } = router.query + if (!tagOptions) return <> + + return ( +
      +
      {locale.COMMON.TAGS}
      +
      + {tagOptions.map((tag, index) => { + const selected = currentTag === tag.name + return ( + +
      +
      {tag.name}
      + {tag.count ? ( + {tag.count} + ) : ( + <> + )} +
      + + ) + })} +
      +
      + ) +} + +export default TagGroups diff --git a/themes/movie/components/TagItemMini.js b/themes/movie/components/TagItemMini.js index e2dcc69f..e5d29782 100644 --- a/themes/movie/components/TagItemMini.js +++ b/themes/movie/components/TagItemMini.js @@ -6,11 +6,9 @@ const TagItemMini = ({ tag, selected = false }) => { key={tag} href={selected ? '/' : `/tag/${encodeURIComponent(tag.name)}`} passHref - className={ - 'inline-block text-md font-extrabold rounded-xl text-shadow py-0.5 mr-2 text-[#2EBF8B]' - } + className={'inline-block rounded-xl py-0.5 mr-2 text-[#2EBF8B]'} > -
      +
      {selected && }{' '} {tag.name + (tag.count ? `(${tag.count})` : '')}{' '}
      diff --git a/themes/movie/index.js b/themes/movie/index.js index 15034d73..2aaa95da 100644 --- a/themes/movie/index.js +++ b/themes/movie/index.js @@ -1,7 +1,7 @@ 'use client' import CONFIG from './config' -import { useEffect } from 'react' +import { createContext, useContext, useEffect, useRef } from 'react' import { Header } from './components/Header' import { Nav } from './components/Nav' import { Footer } from './components/Footer' @@ -26,6 +26,16 @@ import { useRouter } from 'next/router' import { Transition } from '@headlessui/react' import { Style } from './style' import { siteConfig } from '@/lib/config' +import AlgoliaSearchModal from '@/components/AlgoliaSearchModal' +import LatestPostsGroup from './components/LatestPostsGroup' +import CategoryGroup from './components/CategoryGroup' +import { formatDateFmt } from '@/lib/utils/formatDate' +import ArchiveDateList from './components/ArchiveDateList' +import TagGroups from './components/TagGroups' + +// 主题全局状态 +const ThemeGlobalMovie = createContext() +export const useMovieGlobal = () => useContext(ThemeGlobalMovie) /** * 基础布局框架 @@ -35,32 +45,11 @@ import { siteConfig } from '@/lib/config' * @constructor */ const LayoutBase = props => { - const { children } = props + const { children, slotTop } = 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 = ( -
      - -
      - ) - } + const searchModal = useRef(null) // 增加一个状态以触发 Transition 组件的动画 // const [showTransition, setShowTransition] = useState(true) @@ -71,58 +60,63 @@ const LayoutBase = props => { // }, [onLoading]) return ( -
      - ) } From 237ac41c9da4a4529224f6ef6b87c302a1c683bf Mon Sep 17 00:00:00 2001 From: tangly1024 Date: Fri, 15 Mar 2024 22:44:23 +0800 Subject: [PATCH 11/13] =?UTF-8?q?waline=20component=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/WalineComponent.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/WalineComponent.js b/components/WalineComponent.js index 37247d48..cd2d78f5 100644 --- a/components/WalineComponent.js +++ b/components/WalineComponent.js @@ -1,4 +1,4 @@ -import React, { createRef } from 'react' +import { createRef } from 'react' import { init } from '@waline/client' import { useRouter } from 'next/router' import '@waline/client/dist/waline.css' @@ -21,7 +21,7 @@ const WalineComponent = (props) => { } } - React.useEffect(() => { + useEffect(() => { if (!waline) { waline = init({ ...props, From ae10ad1ebf9a3de46350f97f2062c969034368bd Mon Sep 17 00:00:00 2001 From: tangly1024 Date: Fri, 15 Mar 2024 23:30:21 +0800 Subject: [PATCH 12/13] =?UTF-8?q?=E8=A7=86=E9=A2=91=E8=81=9A=E5=90=88?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- themes/movie/components/BlogRecommend.js | 2 +- themes/movie/index.js | 65 +++++++++++++++++++----- themes/movie/style.js | 9 ++++ 3 files changed, 61 insertions(+), 15 deletions(-) diff --git a/themes/movie/components/BlogRecommend.js b/themes/movie/components/BlogRecommend.js index c1b857f3..9e434558 100644 --- a/themes/movie/components/BlogRecommend.js +++ b/themes/movie/components/BlogRecommend.js @@ -38,7 +38,7 @@ export default function BlogRecommend(props) { title={post.title} href={url} passHref - className='flex rounded-lg h-72 w-52 cursor-pointer overflow-hidden'> + className='flex rounded-lg h-60 w-48 cursor-pointer overflow-hidden'>
      {post.title}
      diff --git a/themes/movie/index.js b/themes/movie/index.js index e31e6a36..75e9abae 100644 --- a/themes/movie/index.js +++ b/themes/movie/index.js @@ -149,6 +149,15 @@ const LayoutSlug = props => { // 找到所有的 .notion-asset-wrapper 元素 const assetWrappers = document.querySelectorAll('.notion-asset-wrapper') + if (!assetWrappers || assetWrappers.length === 0) return // 如果找不到对应的元素,则退出函数 + + // 不要重复创建 + const exists = document.querySelectorAll('.video-wrapper') + if (exists && exists.length > 0) return + + // 创建一个新的容器元素 + const videoWrapper = document.createElement('div') + videoWrapper.className = 'video-wrapper p-2 bg-gray-100 dark:bg-hexo-black-gray max-w-5xl mx-auto' // 创建一个新的容器元素 const carouselWrapper = document.createElement('div') @@ -171,43 +180,61 @@ const LayoutSlug = props => { return // 获取 figcaption 的文本内容并添加到数组中 - figCaptionValues.push(figCaption.textContent.trim()) + const figCaptionValue = figCaption.textContent.trim() + figCaptionValues.push(figCaptionValue) // 创建一个新的 div 元素用于包裹当前的 .notion-asset-wrapper 元素 const carouselItem = document.createElement('div') carouselItem.classList.add('notion-carousel') - carouselItem.appendChild(wrapper.cloneNode(true)) - + carouselItem.appendChild(wrapper) // 如果是第一个元素,设置为 active if (index === 0) { carouselItem.classList.add('active') } - - // 将新创建的元素添加到容器中 + // 将元素添加到容器中 carouselWrapper.appendChild(carouselItem) - // 从 DOM 中移除原始的 .notion-asset-wrapper 元素 - wrapper.parentNode.removeChild(wrapper) + // wrapper.parentNode.removeChild(wrapper) }) // 创建一个用于保存 figcaption 值的容器元素 const figCaptionWrapper = document.createElement('div') - figCaptionWrapper.classList.add('notion-carousel-route') + figCaptionWrapper.className = 'notion-carousel-route py-2 max-h-36 overflow-y-auto' // 遍历 figCaptionValues 数组,并将每个值添加到容器元素中 figCaptionValues.forEach(value => { const div = document.createElement('div') div.textContent = value + div.addEventListener('click', function () { + // 遍历所有的 carouselItem 元素 + document.querySelectorAll('.notion-carousel').forEach(item => { + // 判断当前元素是否包含该 figCaption 的文本内容,如果是则设置为 active,否则取消 active + if (item.querySelector('figcaption').textContent.trim() === value) { + item.classList.add('active') + } else { + item.classList.remove('active') + // 不活跃窗口暂停播放,仅支持notion上传视频、不支持外链 + item.querySelectorAll('video')?.forEach(video => { + video.pause() + }) + } + }) + }) figCaptionWrapper.appendChild(div) }) - // 将包含 figcaption 值的容器元素添加到 notion-article 的第一个子元素插入 - notionArticle.insertBefore(figCaptionWrapper, notionArticle.firstChild) - // 将新创建的容器元素作为 notion-article 的第一个子元素插入 - notionArticle.insertBefore(carouselWrapper, notionArticle.firstChild) + // 将包含 figcaption 值的容器元素添加到 notion-article 的第一个子元素插入 + videoWrapper.appendChild(carouselWrapper) + if (figCaptionValues.length > 1) { + videoWrapper.appendChild(figCaptionWrapper) + } + notionArticle.insertBefore(videoWrapper, notionArticle.firstChild) } - combineVideo() + setTimeout(() => { + combineVideo() + }, 100) + // 404 if (!post) { setTimeout( @@ -224,11 +251,21 @@ const LayoutSlug = props => { siteConfig('POST_WAITING_TIME_FOR_404') * 1000 ) } + return () => { + // 获取所有 class="video-wrapper" 的元素 + const videoWrappers = document.querySelectorAll('.video-wrapper') + + // 遍历所有匹配的元素并移除它们 + videoWrappers.forEach(wrapper => { + wrapper.parentNode.removeChild(wrapper) // 从 DOM 中移除元素 + }) + } }, [post]) + return ( <> {!lock ? ( -
      +
      {/* 标题 */} {/* 页面元素 */} diff --git a/themes/movie/style.js b/themes/movie/style.js index 04fa9e3a..c619b509 100644 --- a/themes/movie/style.js +++ b/themes/movie/style.js @@ -31,6 +31,15 @@ const Style = () => { .notion-carousel-wrapper .notion-carousel.active { display: block; } + + .notion-carousel-route div { + cursor: pointer; + margin-bottom: 0.2rem; + } + + .notion-carousel-route div:hover { + text-decoration: underline; + } `} ) } From e2080eabf9050d38103e0b892b7f9a72074d627b Mon Sep 17 00:00:00 2001 From: tangly1024 Date: Sat, 16 Mar 2024 23:20:25 +0800 Subject: [PATCH 13/13] =?UTF-8?q?movie=E4=B8=BB=E9=A2=98=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E3=80=81dplayer=E5=8A=A0=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/videos/dplayer.html | 50 +++++++++++++++++++++++++ themes/movie/components/BlogPostCard.js | 4 +- themes/movie/components/CategoryItem.js | 2 +- themes/movie/components/TagItem.js | 5 +-- themes/movie/components/TagItemMini.js | 4 +- themes/movie/config.js | 1 + themes/movie/index.js | 9 +++-- 7 files changed, 63 insertions(+), 12 deletions(-) create mode 100644 public/videos/dplayer.html diff --git a/public/videos/dplayer.html b/public/videos/dplayer.html new file mode 100644 index 00000000..66573c2b --- /dev/null +++ b/public/videos/dplayer.html @@ -0,0 +1,50 @@ + + + + + + + DPlayer Video Player + + + + + + + +
      + + + + + + + + + + \ No newline at end of file diff --git a/themes/movie/components/BlogPostCard.js b/themes/movie/components/BlogPostCard.js index eadcfb7c..61954610 100644 --- a/themes/movie/components/BlogPostCard.js +++ b/themes/movie/components/BlogPostCard.js @@ -23,11 +23,11 @@ const BlogPostCard = ({ index, post, showSummary, siteInfo }) => {
      -
      +
      {post?.tagItems && post?.tagItems.length > 0 && ( <>
      diff --git a/themes/movie/components/CategoryItem.js b/themes/movie/components/CategoryItem.js index d69de464..1190965b 100644 --- a/themes/movie/components/CategoryItem.js +++ b/themes/movie/components/CategoryItem.js @@ -12,7 +12,7 @@ export default function CategoryItem({ category }) { href={`/category/${category.name}`} passHref legacyBehavior> -
      +
      {category.name}({category.count})
      diff --git a/themes/movie/components/TagItem.js b/themes/movie/components/TagItem.js index dab00325..c608c8cb 100644 --- a/themes/movie/components/TagItem.js +++ b/themes/movie/components/TagItem.js @@ -12,10 +12,9 @@ export default function TagItem({ tag }) { key={tag} href={`/tag/${encodeURIComponent(tag.name)}`} passHref - className={`cursor-pointer inline-block rounded hover:bg-gray-500 hover:text-white duration-200 mr-2 py-1 px-2 text-xs whitespace-nowrap dark:hover:text-white text-gray-600 hover:shadow-xl notion-${tag.color}_background dark:bg-gray-800`} + className={`cursor-pointer inline-block rounded duration-200 mr-2 py-1 px-2 text-xs whitespace-nowrap`} > -
      - {' '} +
      {tag.name + (tag.count ? `(${tag.count})` : '')}{' '}
      diff --git a/themes/movie/components/TagItemMini.js b/themes/movie/components/TagItemMini.js index e5d29782..a4b7da1c 100644 --- a/themes/movie/components/TagItemMini.js +++ b/themes/movie/components/TagItemMini.js @@ -6,9 +6,9 @@ const TagItemMini = ({ tag, selected = false }) => { key={tag} href={selected ? '/' : `/tag/${encodeURIComponent(tag.name)}`} passHref - className={'inline-block rounded-xl py-0.5 mr-2 text-[#2EBF8B]'} + className={'inline-block rounded-xl py-0.5 mr-2'} > -
      +
      {selected && }{' '} {tag.name + (tag.count ? `(${tag.count})` : '')}{' '}
      diff --git a/themes/movie/config.js b/themes/movie/config.js index ad775bbd..a2c5fd4d 100644 --- a/themes/movie/config.js +++ b/themes/movie/config.js @@ -9,6 +9,7 @@ const CONFIG = { MOVIE_MENU_SEARCH: true, // 显示搜索 MOVIE_ARTICLE_RECOMMEND: true, // 推荐关联内容在文章底部 + MOVIE_VIDEO_COMBINE: true, // 聚合视频,开启后一篇文章内的多个含caption的视频会被合并到文章开头,并展示分集按钮 MOVIE_POST_LIST_COVER: true // 列表显示文章封面 } diff --git a/themes/movie/index.js b/themes/movie/index.js index 75e9abae..5b7f9184 100644 --- a/themes/movie/index.js +++ b/themes/movie/index.js @@ -64,7 +64,7 @@ const LayoutBase = props => {
      {/* 主体 */} -
      +
      {/* 标题栏 */} {/* {fullWidth ? null : } */} @@ -223,12 +223,13 @@ const LayoutSlug = props => { figCaptionWrapper.appendChild(div) }) - // 将包含 figcaption 值的容器元素添加到 notion-article 的第一个子元素插入 - videoWrapper.appendChild(carouselWrapper) + // 条件是带有caption的视频数量大于1个,否则不处理 if (figCaptionValues.length > 1) { + // 将包含 figcaption 值的容器元素添加到 notion-article 的第一个子元素插入 + videoWrapper.appendChild(carouselWrapper) videoWrapper.appendChild(figCaptionWrapper) + notionArticle.insertBefore(videoWrapper, notionArticle.firstChild) } - notionArticle.insertBefore(videoWrapper, notionArticle.firstChild) } setTimeout(() => {