diff --git a/.env.local b/.env.local index af3724bc..6952a616 100644 --- a/.env.local +++ b/.env.local @@ -1,5 +1,5 @@ # 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables -NEXT_PUBLIC_VERSION=4.7.6 +NEXT_PUBLIC_VERSION=4.7.7 # 可在此添加环境变量,去掉最左边的(# )注释即可 diff --git a/lib/db/getSiteData.js b/lib/db/getSiteData.js index 951bdaac..4d615824 100755 --- a/lib/db/getSiteData.js +++ b/lib/db/getSiteData.js @@ -65,8 +65,8 @@ export async function getNotionPageData({ pageId, from }) { const cacheKey = 'page_block_' + pageId let data = await getDataFromCache(cacheKey) if (data && data.pageIds?.length > 0) { - // console.log('[API<<--缓存]', `from:${from}`, `root-page-id:${pageId}`) - // return data + console.debug('[API<<--缓存]', `from:${from}`, `root-page-id:${pageId}`) + return data } else { // 从接口读取 data = await getDataBaseInfoByNotionAPI({ pageId, from }) diff --git a/package.json b/package.json index 76f6bf87..dff2f46a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "notion-next", - "version": "4.7.6", + "version": "4.7.7", "homepage": "https://github.com/tangly1024/NotionNext.git", "license": "MIT", "repository": { diff --git a/public/images/themes-preview/commerce.png b/public/images/themes-preview/commerce.png index a82980e3..a019ee3f 100644 Binary files a/public/images/themes-preview/commerce.png and b/public/images/themes-preview/commerce.png differ diff --git a/public/images/themes-preview/example.png b/public/images/themes-preview/example.png index 213e9d0b..ee8a8d78 100644 Binary files a/public/images/themes-preview/example.png and b/public/images/themes-preview/example.png differ diff --git a/public/images/themes-preview/fukasawa.png b/public/images/themes-preview/fukasawa.png index 155e7480..c892d1eb 100644 Binary files a/public/images/themes-preview/fukasawa.png and b/public/images/themes-preview/fukasawa.png differ diff --git a/public/images/themes-preview/game.png b/public/images/themes-preview/game.png index 66ff7efd..5359450d 100644 Binary files a/public/images/themes-preview/game.png and b/public/images/themes-preview/game.png differ diff --git a/public/images/themes-preview/heo.png b/public/images/themes-preview/heo.png index 19fcb1fa..73dfc1bb 100644 Binary files a/public/images/themes-preview/heo.png and b/public/images/themes-preview/heo.png differ diff --git a/public/images/themes-preview/hexo.png b/public/images/themes-preview/hexo.png index fa3e86f8..89d3aa0a 100644 Binary files a/public/images/themes-preview/hexo.png and b/public/images/themes-preview/hexo.png differ diff --git a/public/images/themes-preview/landing.png b/public/images/themes-preview/landing.png index cdbec697..e39f9afa 100644 Binary files a/public/images/themes-preview/landing.png and b/public/images/themes-preview/landing.png differ diff --git a/public/images/themes-preview/magzine.png b/public/images/themes-preview/magzine.png index cdbec697..e39f9afa 100644 Binary files a/public/images/themes-preview/magzine.png and b/public/images/themes-preview/magzine.png differ diff --git a/public/images/themes-preview/matery.png b/public/images/themes-preview/matery.png index 82c433b3..d0724280 100644 Binary files a/public/images/themes-preview/matery.png and b/public/images/themes-preview/matery.png differ diff --git a/public/images/themes-preview/medium.png b/public/images/themes-preview/medium.png index a3bf7468..c8cd22b9 100644 Binary files a/public/images/themes-preview/medium.png and b/public/images/themes-preview/medium.png differ diff --git a/public/images/themes-preview/movie.png b/public/images/themes-preview/movie.png index 6eea5fd5..3af8a76e 100644 Binary files a/public/images/themes-preview/movie.png and b/public/images/themes-preview/movie.png differ diff --git a/public/images/themes-preview/photo.png b/public/images/themes-preview/photo.png new file mode 100644 index 00000000..f2e074ae Binary files /dev/null and b/public/images/themes-preview/photo.png differ diff --git a/public/images/themes-preview/plog.png b/public/images/themes-preview/plog.png index 833bd76a..ddaccca7 100644 Binary files a/public/images/themes-preview/plog.png and b/public/images/themes-preview/plog.png differ diff --git a/public/images/themes-preview/starter.png b/public/images/themes-preview/starter.png index d3a110ea..c4ec9487 100644 Binary files a/public/images/themes-preview/starter.png and b/public/images/themes-preview/starter.png differ diff --git a/styles/globals.css b/styles/globals.css index 7c215c46..5c86cecf 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -2,6 +2,10 @@ @tailwind components; @tailwind utilities; +html { + overflow-x: hidden; +} + .wrapper { min-height: 100vh; display: flex; diff --git a/themes/photo/components/Announcement.js b/themes/photo/components/Announcement.js new file mode 100644 index 00000000..38b701a5 --- /dev/null +++ b/themes/photo/components/Announcement.js @@ -0,0 +1,23 @@ +import dynamic from 'next/dynamic' + +const NotionPage = dynamic(() => import('@/components/NotionPage')) +/** + * 公告 + * @param {*} param0 + * @returns + */ +const Announcement = ({ notice, className }) => { + if (!notice || Object.keys(notice).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} +
+ + +
+ ) +} 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..29b7bee0 --- /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..979a6ca3 --- /dev/null +++ b/themes/photo/components/MenuHierarchical.js @@ -0,0 +1,114 @@ +import Collapse from '@/components/Collapse' +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import { useRouter } from 'next/router' +import { useEffect, 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) + } + const closeModal = () => { + setIsOpen(false) + } + 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 + } + + const [title, setTitle] = useState(siteConfig('BIO')) + + useEffect(() => { + const currentLink = links.find(link => link.href === router.pathname) + if (currentLink) { + setTitle(currentLink.name) + } + closeModal() + }, [router]) + + return ( +
    + {/* 菜单按钮 */} +
    + {title} +
    + + {/* 移动端菜单 */} + + {links?.map( + (link, index) => + link && + link.show && ( + + collapseRef.current?.updateCollapseHeight(param) + } + key={index} + link={link} + /> + ) + )} + + + {/* 遮罩 */} + {isOpen && ( +
    + )} +
    + ) +} diff --git a/themes/photo/components/MenuItemCollapse.js b/themes/photo/components/MenuItemCollapse.js new file mode 100644 index 00000000..73458f13 --- /dev/null +++ b/themes/photo/components/MenuItemCollapse.js @@ -0,0 +1,81 @@ +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..41b57f5b --- /dev/null +++ b/themes/photo/components/PostItemCard.js @@ -0,0 +1,55 @@ +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' +import Link from 'next/link' + +/** + * 普通的博客卡牌 + * 带封面图 + */ +const PostItemCard = ({ post, className }) => { + 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..074f915c --- /dev/null +++ b/themes/photo/components/Swiper.js @@ -0,0 +1,106 @@ +import { useEffect, useRef, useState } from 'react' +import PostItemCard from './PostItemCard' + +/** + * 滑动走马灯 + * @param {*} param0 + * @returns + */ +const InertiaCarousel = ({ posts }) => { + const carouselRef = useRef(null) + const [isDragging, setIsDragging] = useState(false) + const [startX, setStartX] = useState(0) + const [scrollLeft, setScrollLeft] = useState(0) + const [lastX, setLastX] = useState(0) // 上一次的位置 + const [velocity, setVelocity] = useState(0) + const animationRef = useRef(null) + + // 开始拖拽事件 + const startDrag = e => { + e.preventDefault() + setIsDragging(true) + const startPosition = e.pageX || e.touches?.[0].pageX + setStartX(startPosition - carouselRef.current.offsetLeft) + setScrollLeft(carouselRef.current.scrollLeft) + setLastX(startPosition) // 初始化上一次的位置 + cancelInertiaScroll() // 停止任何正在进行的惯性动画 + } + + // 拖拽中事件 + const duringDrag = e => { + if (!isDragging) return + e.preventDefault() + const currentPosition = e.pageX || e.touches[0].pageX + const distance = currentPosition - startX + carouselRef.current.scrollLeft = scrollLeft - distance + + // 计算当前速度 + const deltaX = currentPosition - lastX + setVelocity(deltaX) // 更新速度 + setLastX(currentPosition) // 更新 lastX 为当前位置 + } + + // 结束拖拽事件,启动惯性滚动 + const endDrag = () => { + setIsDragging(false) + startInertiaScroll(velocity) // 根据最终速度启动惯性滚动 + } + + // 惯性滚动函数 + const startInertiaScroll = initialVelocity => { + let currentVelocity = initialVelocity + const decay = 0.95 // 惯性衰减系数 + const animate = () => { + if (Math.abs(currentVelocity) > 0.5) { + // 仅当速度足够大时继续滚动 + carouselRef.current.scrollLeft -= currentVelocity + currentVelocity *= decay // 速度衰减 + animationRef.current = requestAnimationFrame(animate) + } else { + cancelAnimationFrame(animationRef.current) + } + } + animate() + } + + // 取消惯性滚动 + const cancelInertiaScroll = () => { + if (animationRef.current) { + cancelAnimationFrame(animationRef.current) + } + } + + useEffect(() => { + return () => cancelInertiaScroll() // 清除动画 + }, []) + + return ( +
    + {/* Carousel items */} + +
    + {posts && + posts?.map((post, index) => ( + + ))} +
    + ) +} + +export default InertiaCarousel 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..f369e6fb --- /dev/null +++ b/themes/photo/index.js @@ -0,0 +1,496 @@ +'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 Announcement from './components/Announcement' +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 }