From 56f94bf8f0dc0bec0ed659be23a70ace5dd72f2a Mon Sep 17 00:00:00 2001 From: "tangly1024.com" Date: Mon, 4 Nov 2024 10:57:09 +0800 Subject: [PATCH] =?UTF-8?q?photo-=E4=B8=BB=E9=A2=98=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- themes/photo/components/Announcement.js | 22 + themes/photo/components/ArchiveDateList.js | 38 ++ themes/photo/components/ArticleFooter.js | 69 +++ themes/photo/components/ArticleInfo.js | 23 + themes/photo/components/ArticleLock.js | 52 ++ .../photo/components/BlogListGroupByDate.js | 36 ++ themes/photo/components/BlogListPage.js | 31 ++ themes/photo/components/BlogListScroll.js | 76 +++ themes/photo/components/BlogPostCard.js | 62 +++ themes/photo/components/BlogRecommend.js | 66 +++ themes/photo/components/CategoryGroup.js | 43 ++ themes/photo/components/CategoryItem.js | 20 + .../photo/components/ExampleRecentComments.js | 35 ++ themes/photo/components/Footer.js | 48 ++ themes/photo/components/Header.js | 27 + .../photo/components/HomeBackgroundImage.js | 21 + themes/photo/components/JumpToTopButton.js | 18 + themes/photo/components/LatestPostsGroup.js | 57 ++ themes/photo/components/LoadingCover.js | 8 + themes/photo/components/MenuHierarchical.js | 92 ++++ themes/photo/components/MenuItemCollapse.js | 83 +++ themes/photo/components/MenuItemDrop.js | 59 +++ themes/photo/components/NormalMenuItem.js | 20 + themes/photo/components/PaginationNumber.js | 214 ++++++++ themes/photo/components/PostItemCard.js | 57 ++ themes/photo/components/SearchInput.js | 87 +++ themes/photo/components/SideBar.js | 68 +++ themes/photo/components/SlotBar.js | 34 ++ themes/photo/components/Swiper.js | 111 ++++ themes/photo/components/TagGroups.js | 51 ++ themes/photo/components/TagItem.js | 23 + themes/photo/components/TagItemMini.js | 19 + themes/photo/components/Title.js | 20 + themes/photo/config.js | 18 + themes/photo/index.js | 494 ++++++++++++++++++ themes/photo/style.js | 52 ++ 36 files changed, 2254 insertions(+) create mode 100644 themes/photo/components/Announcement.js create mode 100644 themes/photo/components/ArchiveDateList.js create mode 100644 themes/photo/components/ArticleFooter.js create mode 100644 themes/photo/components/ArticleInfo.js create mode 100644 themes/photo/components/ArticleLock.js create mode 100644 themes/photo/components/BlogListGroupByDate.js create mode 100644 themes/photo/components/BlogListPage.js create mode 100644 themes/photo/components/BlogListScroll.js create mode 100644 themes/photo/components/BlogPostCard.js create mode 100644 themes/photo/components/BlogRecommend.js create mode 100644 themes/photo/components/CategoryGroup.js create mode 100644 themes/photo/components/CategoryItem.js create mode 100644 themes/photo/components/ExampleRecentComments.js create mode 100644 themes/photo/components/Footer.js create mode 100644 themes/photo/components/Header.js create mode 100644 themes/photo/components/HomeBackgroundImage.js create mode 100644 themes/photo/components/JumpToTopButton.js create mode 100644 themes/photo/components/LatestPostsGroup.js create mode 100644 themes/photo/components/LoadingCover.js create mode 100644 themes/photo/components/MenuHierarchical.js create mode 100644 themes/photo/components/MenuItemCollapse.js create mode 100644 themes/photo/components/MenuItemDrop.js create mode 100644 themes/photo/components/NormalMenuItem.js create mode 100644 themes/photo/components/PaginationNumber.js create mode 100644 themes/photo/components/PostItemCard.js create mode 100644 themes/photo/components/SearchInput.js create mode 100644 themes/photo/components/SideBar.js create mode 100644 themes/photo/components/SlotBar.js create mode 100644 themes/photo/components/Swiper.js create mode 100644 themes/photo/components/TagGroups.js create mode 100644 themes/photo/components/TagItem.js create mode 100644 themes/photo/components/TagItemMini.js create mode 100644 themes/photo/components/Title.js create mode 100644 themes/photo/config.js create mode 100644 themes/photo/index.js create mode 100644 themes/photo/style.js diff --git a/themes/photo/components/Announcement.js b/themes/photo/components/Announcement.js new file mode 100644 index 00000000..1f265417 --- /dev/null +++ b/themes/photo/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/photo/components/ArchiveDateList.js b/themes/photo/components/ArchiveDateList.js new file mode 100644 index 00000000..cd389028 --- /dev/null +++ b/themes/photo/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/photo/components/ArticleFooter.js b/themes/photo/components/ArticleFooter.js new file mode 100644 index 00000000..9a928cce --- /dev/null +++ b/themes/photo/components/ArticleFooter.js @@ -0,0 +1,69 @@ +import { useGlobal } from '@/lib/global' +import { formatDateFmt } from '@/lib/utils/formatDate' +import Link from 'next/link' + +/** + * 文章页脚 + * @param {*} props + * @returns + */ +export default function ArticleFooter(props) { + const { post } = props + const { locale } = useGlobal() + + return ( + <> + {/* 分类和标签部分 */} +
+ {/* 分类标签(如果文章不是“页面”类型) */} + {post?.type !== 'Page' && ( + <> + + {post?.category} + + + )} + + {/* 标签部分(若文章有标签) */} +
+ {post?.tags?.length > 0 && ( + <> + {locale.COMMON.TAGS} : + + )} + {/* 显示所有标签 */} + {post?.tags?.map(tag => { + return ( + + {tag} + + ) + })} +
+
+ + {/* 发布日期信息 */} + {/* 将发布日期移至文章底部并设置样式 */} +
+ + {post?.publishDay} + +
+ + ) +} diff --git a/themes/photo/components/ArticleInfo.js b/themes/photo/components/ArticleInfo.js new file mode 100644 index 00000000..c0cd89e0 --- /dev/null +++ b/themes/photo/components/ArticleInfo.js @@ -0,0 +1,23 @@ +/** + * 文章页头 + * @param {*} props + * @returns + */ +export const ArticleHeader = props => { + const { post } = props + + return ( +
+ {/* 标题部分 */} + {/* 将标题字体大小设置为 16px,并将字体粗细设置为细体 */} +

+ {post?.title} +

+
+ ) +} diff --git a/themes/photo/components/ArticleLock.js b/themes/photo/components/ArticleLock.js new file mode 100644 index 00000000..3744c183 --- /dev/null +++ b/themes/photo/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/photo/components/BlogListGroupByDate.js b/themes/photo/components/BlogListGroupByDate.js new file mode 100644 index 00000000..ebc30aa5 --- /dev/null +++ b/themes/photo/components/BlogListGroupByDate.js @@ -0,0 +1,36 @@ +import Link from 'next/link' + +/** + * 按照日期将文章分组 + * 归档页面用到 + * @param {*} param0 + * @returns + */ +export default function BlogListGroupByDate({ archiveTitle, archivePosts }) { + return ( +
+
+ {archiveTitle} +
+ +
    + {archivePosts[archiveTitle].map(post => { + return ( +
  • +
    + {post?.publishDay}   + + {post.title} + +
    +
  • + ) + })} +
+
+ ) +} diff --git a/themes/photo/components/BlogListPage.js b/themes/photo/components/BlogListPage.js new file mode 100644 index 00000000..2a3126ca --- /dev/null +++ b/themes/photo/components/BlogListPage.js @@ -0,0 +1,31 @@ +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import CONFIG from '../config' +import BlogPostCard from './BlogPostCard' +import PaginationNumber from './PaginationNumber' + +export const BlogListPage = props => { + const { page = 1, posts, postCount } = props + const { NOTION_CONFIG } = useGlobal() + const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG) + const totalPage = Math.ceil(postCount / POSTS_PER_PAGE) + + const showPageCover = siteConfig('MOVIE_POST_LIST_COVER', null, CONFIG) + if (!posts || posts.length === 0) { + return null + } + + return ( +
+
+ {posts?.map(post => ( + + ))} +
+ + +
+ ) +} diff --git a/themes/photo/components/BlogListScroll.js b/themes/photo/components/BlogListScroll.js new file mode 100644 index 00000000..aa1538c7 --- /dev/null +++ b/themes/photo/components/BlogListScroll.js @@ -0,0 +1,76 @@ +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import throttle from 'lodash.throttle' +import { useCallback, useEffect, useRef, useState } from 'react' +import CONFIG from '../config' +import BlogPostCard from './BlogPostCard' + +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', 12, props?.NOTION_CONFIG)) * page + ) + : [] + + if (posts) { + const totalCount = posts.length + hasMore = + page * parseInt(siteConfig('POSTS_PER_PAGE', 12, props?.NOTION_CONFIG)) < + 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('MOVIE_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/photo/components/BlogPostCard.js b/themes/photo/components/BlogPostCard.js new file mode 100644 index 00000000..771f31b5 --- /dev/null +++ b/themes/photo/components/BlogPostCard.js @@ -0,0 +1,62 @@ +import LazyImage from '@/components/LazyImage' +import NotionIcon from '@/components/NotionIcon' +import { siteConfig } from '@/lib/config' +import Link from 'next/link' +import TagItemMini from './TagItemMini' + +const BlogPostCard = ({ index, post, showSummary, siteInfo }) => { + // 主题默认强制显示图片 + if (post && !post.pageCoverThumbnail) { + post.pageCoverThumbnail = + siteInfo?.pageCover || siteConfig('RANDOM_IMAGE_URL') + } + + return ( +
+ + {/* 固定高度 ,空白用图片拉升填充 */} +
+ {/* 图片 填充卡片 */} +
+ +
+ +
+ {post?.tagItems && post?.tagItems.length > 0 && ( + <> +
+ {post.tagItems.map(tag => ( + + ))} +
+ + )} +
+ {/* 阴影遮罩 */} +

+ {siteConfig('POST_TITLE_ICON') && ( + + )} + {post.title} +

+ +

+ {post?.summary} +

+ +
+
+
+
+ +
+ ) +} + +export default BlogPostCard diff --git a/themes/photo/components/BlogRecommend.js b/themes/photo/components/BlogRecommend.js new file mode 100644 index 00000000..ef87030e --- /dev/null +++ b/themes/photo/components/BlogRecommend.js @@ -0,0 +1,66 @@ +import LazyImage from '@/components/LazyImage' +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import CONFIG from '../config' + +/** + * 关联推荐文章 + * @param {prev,next} param0 + * @returns + */ +export default function BlogRecommend(props) { + const { recommendPosts, siteInfo } = props + const { locale } = useGlobal() + if ( + !siteConfig('MOVIE_ARTICLE_RECOMMEND', null, CONFIG) || + !recommendPosts || + recommendPosts.length === 0 + ) { + return <> + } + + return ( +
+
+
+ + {locale.COMMON.RELATE_POSTS} +
+
+
+ {recommendPosts.map(post => { + const headerImage = post?.pageCoverThumbnail + ? post.pageCoverThumbnail + : siteInfo?.pageCover + + return ( + +
+
+
+ {post.title} +
+
+ {/* 卡片的阴影遮罩,为了凸显图片上的文字 */} +
+
+
+ + +
+ + ) + })} +
+
+ ) +} diff --git a/themes/photo/components/CategoryGroup.js b/themes/photo/components/CategoryGroup.js new file mode 100644 index 00000000..63c3917c --- /dev/null +++ b/themes/photo/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/photo/components/CategoryItem.js b/themes/photo/components/CategoryItem.js new file mode 100644 index 00000000..1190965b --- /dev/null +++ b/themes/photo/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/photo/components/ExampleRecentComments.js b/themes/photo/components/ExampleRecentComments.js new file mode 100644 index 00000000..9dbdfa7f --- /dev/null +++ b/themes/photo/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/photo/components/Footer.js b/themes/photo/components/Footer.js new file mode 100644 index 00000000..dc361e96 --- /dev/null +++ b/themes/photo/components/Footer.js @@ -0,0 +1,48 @@ +import { BeiAnGongAn } from '@/components/BeiAnGongAn' +import DarkModeButton from '@/components/DarkModeButton' +import { siteConfig } from '@/lib/config' +/** + * 页脚 + * @param {*} props + * @returns + */ +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/photo/components/Header.js b/themes/photo/components/Header.js new file mode 100644 index 00000000..a6856db4 --- /dev/null +++ b/themes/photo/components/Header.js @@ -0,0 +1,27 @@ +import { siteConfig } from '@/lib/config' +import Link from 'next/link' +import MenuHierarchical from './MenuHierarchical' + +/** + * 网站顶部 + * @returns + */ +export const Header = props => { + return ( + <> +
+ {/* 左侧Logo */} + + {siteConfig('TITLE')} + + + {/* 右侧使用一个三级菜单 */} +
+ +
+
+ + ) +} diff --git a/themes/photo/components/HomeBackgroundImage.js b/themes/photo/components/HomeBackgroundImage.js new file mode 100644 index 00000000..4cdb5056 --- /dev/null +++ b/themes/photo/components/HomeBackgroundImage.js @@ -0,0 +1,21 @@ +import LazyImage from '@/components/LazyImage' +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +/** + * 封面图 + * @param {*} props + * @returns + */ +export const HomeBackgroundImage = props => { + const { siteInfo } = useGlobal() + const background = siteConfig('MOVIE_HOME_BACKGROUND') + if (!background) { + return null + } + return ( + + ) +} diff --git a/themes/photo/components/JumpToTopButton.js b/themes/photo/components/JumpToTopButton.js new file mode 100644 index 00000000..f5e22b61 --- /dev/null +++ b/themes/photo/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/photo/components/LatestPostsGroup.js b/themes/photo/components/LatestPostsGroup.js new file mode 100644 index 00000000..40366e37 --- /dev/null +++ b/themes/photo/components/LatestPostsGroup.js @@ -0,0 +1,57 @@ +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import { useRouter } from 'next/router' + +/** + * 最新文章列表 + * @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}` + + return ( + +
+
  • {post.title}
  • +
    + + ) + })} +
    + ) +} +export default LatestPostsGroup diff --git a/themes/photo/components/LoadingCover.js b/themes/photo/components/LoadingCover.js new file mode 100644 index 00000000..75976180 --- /dev/null +++ b/themes/photo/components/LoadingCover.js @@ -0,0 +1,8 @@ + +export default function LoadingCover() { + return
    +
    + +
    +
    +} diff --git a/themes/photo/components/MenuHierarchical.js b/themes/photo/components/MenuHierarchical.js new file mode 100644 index 00000000..978e262e --- /dev/null +++ b/themes/photo/components/MenuHierarchical.js @@ -0,0 +1,92 @@ +import Collapse from '@/components/Collapse' +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import { useRouter } from 'next/router' +import { useState } from 'react' +import { usePhotoGlobal } from '..' +import CONFIG from '../config' +import { MenuItemCollapse } from './MenuItemCollapse' + +/** + * 三级菜单 + */ +export default function MenuHierarchical(props) { + const router = useRouter() + const { customNav, customMenu } = props + const { locale } = useGlobal() + const [isOpen, setIsOpen] = useState(false) + const { collapseRef } = usePhotoGlobal() + + const toggleMenuOpen = () => { + setIsOpen(!isOpen) + } + let links = [ + { + id: 1, + icon: 'fa-solid fa-house', + name: locale.NAV.INDEX, + href: '/', + show: siteConfig('MOVIE_MENU_INDEX', null, CONFIG) + }, + { + id: 2, + icon: 'fas fa-search', + name: locale.NAV.SEARCH, + href: '/search', + show: siteConfig('MOVIE_MENU_SEARCH', null, CONFIG) + }, + { + id: 3, + icon: 'fas fa-archive', + name: locale.NAV.ARCHIVE, + href: '/archive', + show: siteConfig('MOVIE_MENU_ARCHIVE', 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 + } + + return ( +
    + {/* 菜单按钮 */} +
    + {isOpen ? ( + + ) : ( + + )} +
    + + {/* 移动端菜单 */} + + {links?.map( + (link, index) => + link && + link.show && ( + + collapseRef.current?.updateCollapseHeight(param) + } + key={index} + link={link} + /> + ) + )} + + +
    + ) +} diff --git a/themes/photo/components/MenuItemCollapse.js b/themes/photo/components/MenuItemCollapse.js new file mode 100644 index 00000000..b11dbd57 --- /dev/null +++ b/themes/photo/components/MenuItemCollapse.js @@ -0,0 +1,83 @@ +import Collapse from '@/components/Collapse' +import Link from 'next/link' +import { useState } from 'react' + +/** + * 折叠菜单 + * @param {*} param0 + * @returns + */ +export const MenuItemCollapse = props => { + const { link } = props + const [show, changeShow] = useState(false) + const hasSubMenu = link?.subMenus?.length > 0 + + const [isOpen, changeIsOpen] = useState(false) + + const toggleShow = () => { + changeShow(!show) + } + + const toggleOpenSubMenu = () => { + changeIsOpen(!isOpen) + } + + if (!link || !link.show) { + return null + } + + return ( + <> +
    + {!hasSubMenu && ( + + + {link?.icon && } + {link?.name} + + + )} + {hasSubMenu && ( +
    + + {link?.icon && } + {link?.name} + + +
    + )} +
    + + {/* 折叠子菜单 */} + {hasSubMenu && ( + + {link.subMenus.map((sLink, index) => { + return ( +
    + + + {link?.icon && }{' '} + {sLink.title} + + +
    + ) + })} +
    + )} + + ) +} diff --git a/themes/photo/components/MenuItemDrop.js b/themes/photo/components/MenuItemDrop.js new file mode 100644 index 00000000..3b5cf813 --- /dev/null +++ b/themes/photo/components/MenuItemDrop.js @@ -0,0 +1,59 @@ +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/photo/components/NormalMenuItem.js b/themes/photo/components/NormalMenuItem.js new file mode 100644 index 00000000..2ed6f2e9 --- /dev/null +++ b/themes/photo/components/NormalMenuItem.js @@ -0,0 +1,20 @@ +import Link from 'next/link' + +/** + * 旧的普通菜单 + * @param {*} props + * @returns + */ +export const NormalMenuItem = props => { + const { link } = props + return ( + link?.show && ( + + {link.name} + + ) + ) +} diff --git a/themes/photo/components/PaginationNumber.js b/themes/photo/components/PaginationNumber.js new file mode 100644 index 00000000..64323b9c --- /dev/null +++ b/themes/photo/components/PaginationNumber.js @@ -0,0 +1,214 @@ +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 [value, setValue] = useState('') + 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(/\/$/, '') + .replace('.html', '') + const pages = generatePages(pagePrefix, page, currentPage, totalPage) + if (pages?.length <= 1) { + return <> + } + + 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/photo/components/PostItemCard.js b/themes/photo/components/PostItemCard.js new file mode 100644 index 00000000..c43854e1 --- /dev/null +++ b/themes/photo/components/PostItemCard.js @@ -0,0 +1,57 @@ +import LazyImage from '@/components/LazyImage' +import NotionIcon from '@/components/NotionIcon' +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import { formatDateFmt } from '@/lib/utils/formatDate' + +/** + * 普通的博客卡牌 + * 带封面图 + */ +const PostItemCard = ({ post }) => { + const { siteInfo } = useGlobal() + const cover = post?.pageCoverThumbnail || siteInfo?.pageCover + return ( +
    +
    + {/* */} +
    + +
    + {/* */} + +
    + {/* */} +

    + {siteConfig('POST_TITLE_ICON') && ( + + )} + {post.title} +

    + {/* */} + +
    + {formatDateFmt(post.publishDate, 'yyyy-MM')} +
    +
    +
    +
    + ) +} + +export default PostItemCard diff --git a/themes/photo/components/SearchInput.js b/themes/photo/components/SearchInput.js new file mode 100644 index 00000000..4f375d4e --- /dev/null +++ b/themes/photo/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/photo/components/SideBar.js b/themes/photo/components/SideBar.js new file mode 100644 index 00000000..ea0531b7 --- /dev/null +++ b/themes/photo/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/photo/components/SlotBar.js b/themes/photo/components/SlotBar.js new file mode 100644 index 00000000..e3bed727 --- /dev/null +++ b/themes/photo/components/SlotBar.js @@ -0,0 +1,34 @@ +import { useGlobal } from '@/lib/global' + +/** + * 博客列表上方嵌入条 + * @param {*} props + * @returns + */ +export default function SlotBar(props) { + const { tag, category } = props + const { locale } = useGlobal() + + if (tag) { + return ( +
    +
    +
    + {locale.COMMON.TAGS} : {tag}{' '} +
    +
    +
    +
    + ) + } else if (category) { + return ( +
    +
    + {locale.COMMON.CATEGORY} : {category} +
    +
    +
    + ) + } + return <> +} diff --git a/themes/photo/components/Swiper.js b/themes/photo/components/Swiper.js new file mode 100644 index 00000000..02b61da9 --- /dev/null +++ b/themes/photo/components/Swiper.js @@ -0,0 +1,111 @@ +import { useRef, useState } from 'react' +import PostItemCard from './PostItemCard' + +const Swiper = ({ posts }) => { + const [currentIndex, setCurrentIndex] = useState(0) + const containerRef = useRef(null) + + const touchStartPos = useRef({ x: 0, y: 0 }) + const isDragging = useRef(false) + const scrollStartLeft = useRef(0) // 记录拖拽开始时的滚动位置 + + // 处理鼠标和触摸开始事件 + const handleDragStart = e => { + const x = e.touches ? e.touches[0].clientX : e.clientX + touchStartPos.current = { x } + isDragging.current = true + scrollStartLeft.current = containerRef.current.scrollLeft + + // 更新鼠标样式 + containerRef.current.style.cursor = 'grabbing' + } + + // 处理鼠标和触摸移动事件 + const handleDragMove = e => { + if (!isDragging.current) return + + const x = e.touches ? e.touches[0].clientX : e.clientX + const deltaX = touchStartPos.current.x - x + + // 根据拖动的距离更新滚动位置 + containerRef.current.scrollLeft = scrollStartLeft.current + deltaX + } + + // 处理鼠标和触摸结束事件 + const handleDragEnd = () => { + isDragging.current = false + containerRef.current.style.cursor = 'grab' + } + + // 处理指示器点击事件 + const handleIndicatorClick = index => { + setCurrentIndex(index) + scrollToCard(index) + } + + // 滚动到特定卡片 + const scrollToCard = index => { + const container = containerRef.current + if (!container) return + const cardWidth = container.scrollWidth / posts.length + container.scrollTo({ + left: index * cardWidth - cardWidth / 6, // 调整位置以居中 + behavior: 'smooth' + }) + } + + return ( +
    +
    + handleIndicatorClick( + currentIndex === 0 ? posts.length - 1 : currentIndex - 1 + ) + }>
    + +
    + handleIndicatorClick( + currentIndex === posts.length - 1 ? 0 : currentIndex + 1 + ) + }>
    + +
    +
    + {posts.map((item, index) => ( +
    + +
    + ))} +
    +
    + +
    + {posts.map((_, index) => ( + + ))} +
    +
    + ) +} + +export default Swiper diff --git a/themes/photo/components/TagGroups.js b/themes/photo/components/TagGroups.js new file mode 100644 index 00000000..597cddc1 --- /dev/null +++ b/themes/photo/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/photo/components/TagItem.js b/themes/photo/components/TagItem.js new file mode 100644 index 00000000..c608c8cb --- /dev/null +++ b/themes/photo/components/TagItem.js @@ -0,0 +1,23 @@ +import Link from 'next/link' + +/** + * 标签 + * @param {*} param0 + * @returns + */ +export default function TagItem({ tag }) { + return ( +
    + +
    + {tag.name + (tag.count ? `(${tag.count})` : '')}{' '} +
    + +
    + ) +} diff --git a/themes/photo/components/TagItemMini.js b/themes/photo/components/TagItemMini.js new file mode 100644 index 00000000..a4b7da1c --- /dev/null +++ b/themes/photo/components/TagItemMini.js @@ -0,0 +1,19 @@ +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/photo/components/Title.js b/themes/photo/components/Title.js new file mode 100644 index 00000000..a073f1cc --- /dev/null +++ b/themes/photo/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
    +

    {siteConfig('POST_TITLE_ICON') && }{title}

    +

    + {description} +

    +
    +} diff --git a/themes/photo/config.js b/themes/photo/config.js new file mode 100644 index 00000000..b6b0bd9c --- /dev/null +++ b/themes/photo/config.js @@ -0,0 +1,18 @@ +/** + * 主题配置文件 + */ +const CONFIG = { + // 菜单配置 + MOVIE_MENU_CATEGORY: true, // 显示分类 + MOVIE_MENU_TAG: true, // 显示标签 + MOVIE_MENU_ARCHIVE: true, // 显示归档 + MOVIE_MENU_SEARCH: true, // 显示搜索 + MOVIE_HOME_BACKGROUND: false, // 首页是否显示背景图, 默认关闭 + + MOVIE_ARTICLE_RECOMMEND: true, // 推荐关联内容在文章底部 + MOVIE_VIDEO_COMBINE: true, // 聚合视频,开启后一篇文章内的多个含caption的视频会被合并到文章开头,并展示分集按钮 + MOVIE_VIDEO_COMBINE_SHOW_PAGE_FORCE: false, // 即使只有一集也显示集数切换按钮 + + MOVIE_POST_LIST_COVER: true // 列表显示文章封面 +} +export default CONFIG diff --git a/themes/photo/index.js b/themes/photo/index.js new file mode 100644 index 00000000..ea74aeb0 --- /dev/null +++ b/themes/photo/index.js @@ -0,0 +1,494 @@ +'use client' + +import AlgoliaSearchModal from '@/components/AlgoliaSearchModal' +import Comment from '@/components/Comment' +import replaceSearchResult from '@/components/Mark' +import NotionPage from '@/components/NotionPage' +import ShareBar from '@/components/ShareBar' +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import { loadWowJS } from '@/lib/plugins/wow' +import { isBrowser } from '@/lib/utils' +import { Transition } from '@headlessui/react' +import { useRouter } from 'next/router' +import { createContext, useContext, useEffect, useRef, useState } from 'react' +import ArchiveDateList from './components/ArchiveDateList' +import ArticleFooter from './components/ArticleFooter' +import { ArticleHeader } from './components/ArticleInfo' +import { ArticleLock } from './components/ArticleLock' +import BlogListGroupByDate from './components/BlogListGroupByDate' +import BlogRecommend from './components/BlogRecommend' +import CategoryGroup from './components/CategoryGroup' +import CategoryItem from './components/CategoryItem' +import { Footer } from './components/Footer' +import { Header } from './components/Header' +import { HomeBackgroundImage } from './components/HomeBackgroundImage' +import JumpToTopButton from './components/JumpToTopButton' +import LatestPostsGroup from './components/LatestPostsGroup' +import SlotBar from './components/SlotBar' +import Swiper from './components/Swiper' +import TagGroups from './components/TagGroups' +import TagItem from './components/TagItem' +import CONFIG from './config' +import { Style } from './style' + +// 主题全局状态 +const ThemeGlobalPhoto = createContext() +export const usePhotoGlobal = () => useContext(ThemeGlobalPhoto) + +/** + * 基础布局框架 + * 1.其它页面都嵌入在LayoutBase中 + * 2.采用左右两侧布局,移动端使用顶部导航栏 + * @returns {JSX.Element} + * @constructor + */ +const LayoutBase = props => { + const { children, slotTop } = props + const { onLoading, fullWidth } = useGlobal() + const collapseRef = useRef(null) + const router = useRouter() + const searchModal = useRef(null) + const [expandMenu, updateExpandMenu] = useState(false) + useEffect(() => { + loadWowJS() + }, []) + + // 首页背景图 + const headerSlot = + router.route === '/' && + siteConfig('MOVIE_HOME_BACKGROUND', null, CONFIG) ? ( + + ) : null + + return ( + +
    + + ) +} + +export { Style }