From 9d6e6d76d367f35e8b7707d6e05e6167d7ddf195 Mon Sep 17 00:00:00 2001 From: Femoon <839242981@qq.com> Date: Sat, 21 Oct 2023 15:55:50 +0800 Subject: [PATCH 01/72] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Medium=20?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E9=A6=96=E9=A1=B5=E6=96=87=E7=AB=A0=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=97=A0=E5=9B=BE=E7=89=87=20AOS=20=E5=8A=A8=E7=94=BB?= =?UTF-8?q?=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/LazyImage.js | 1 + 1 file changed, 1 insertion(+) diff --git a/components/LazyImage.js b/components/LazyImage.js index 4556bd0d..d878ddb1 100644 --- a/components/LazyImage.js +++ b/components/LazyImage.js @@ -60,6 +60,7 @@ export default function LazyImage({ ref: imageRef, src: imageLoaded ? src : placeholderSrc, alt: alt, + style: src ? style : { height: '0px', ...style }, onLoad: handleImageLoad } From 000077ca1ac4a2b27d9c6f7c8780ed3acb743c04 Mon Sep 17 00:00:00 2001 From: Femoon <839242981@qq.com> Date: Mon, 23 Oct 2023 10:03:40 +0800 Subject: [PATCH 02/72] =?UTF-8?q?fix:=20=E6=94=B9=E7=94=A8=20style=20?= =?UTF-8?q?=E4=BC=A0=20height?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/LazyImage.js | 1 - themes/medium/components/BlogPostCard.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/components/LazyImage.js b/components/LazyImage.js index d878ddb1..4556bd0d 100644 --- a/components/LazyImage.js +++ b/components/LazyImage.js @@ -60,7 +60,6 @@ export default function LazyImage({ ref: imageRef, src: imageLoaded ? src : placeholderSrc, alt: alt, - style: src ? style : { height: '0px', ...style }, onLoad: handleImageLoad } diff --git a/themes/medium/components/BlogPostCard.js b/themes/medium/components/BlogPostCard.js index 273660b0..16a963e6 100644 --- a/themes/medium/components/BlogPostCard.js +++ b/themes/medium/components/BlogPostCard.js @@ -31,7 +31,7 @@ const BlogPostCard = ({ post, showSummary }) => { }>
{CONFIG.POST_LIST_COVER &&
- +
} {post.title}
From 608cc8991b1e80f24685e52d9ed616b728a1bf3b Mon Sep 17 00:00:00 2001 From: tangly1024 Date: Sun, 29 Oct 2023 11:21:16 +0800 Subject: [PATCH 03/72] =?UTF-8?q?=E5=A4=9A=E7=BA=A7slug-bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/notion/getPageProperties.js | 2 + pages/[prefix]/[slug]/[...suffix].js | 113 ++++++++++++++++++ pages/[prefix]/{[slug].js => [slug]/index.js} | 3 +- pages/[prefix]/index.js | 1 + 4 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 pages/[prefix]/[slug]/[...suffix].js rename pages/[prefix]/{[slug].js => [slug]/index.js} (96%) diff --git a/lib/notion/getPageProperties.js b/lib/notion/getPageProperties.js index 8d3add33..ddf47f3e 100644 --- a/lib/notion/getPageProperties.js +++ b/lib/notion/getPageProperties.js @@ -135,6 +135,8 @@ function mapProperties(properties) { /** * 获取自定义URL + * 可以根据变量生成URL + * 支持:%year%/%month%/%day%/%slug% * @param {*} postProperties * @returns */ diff --git a/pages/[prefix]/[slug]/[...suffix].js b/pages/[prefix]/[slug]/[...suffix].js new file mode 100644 index 00000000..67f099d7 --- /dev/null +++ b/pages/[prefix]/[slug]/[...suffix].js @@ -0,0 +1,113 @@ +import BLOG from '@/blog.config' +import { getPostBlocks } from '@/lib/notion' +import { getGlobalData } from '@/lib/notion/getNotionData' +import { idToUuid } from 'notion-utils' +import { getNotion } from '@/lib/notion/getNotion' +import Slug, { getRecommendPost } from '..' +import { uploadDataToAlgolia } from '@/lib/algolia' + +/** + * 根据notion的slug访问页面 + * 解析三级以上目录 /article/2023/10/29/test + * @param {*} props + * @returns + */ +const PrefixSlug = props => { + return +} + +/** + * 编译渲染页面路径 + * @returns + */ +export async function getStaticPaths() { + if (!BLOG.isProd) { + return { + paths: [], + fallback: true + } + } + + const from = 'slug-paths' + const { allPages } = await getGlobalData({ from }) + return { + paths: allPages?.filter(row => hasMultipleSlashes(row.slug) && row.type.indexOf('Menu') < 0).map(row => ({ params: { prefix: row.slug.split('/')[0], slug: row.slug.split('/')[1], suffix: row.slug.split('/').slice(1) } })), + fallback: true + } +} + +/** + * 抓取页面数据 + * @param {*} param0 + * @returns + */ +export async function getStaticProps({ params: { prefix, slug, suffix } }) { + let fullSlug = prefix + '/' + slug + '/' + suffix.join('/') + if (JSON.parse(BLOG.PSEUDO_STATIC)) { + if (!fullSlug.endsWith('.html')) { + fullSlug += '.html' + } + } + const from = `slug-props-${fullSlug}` + const props = await getGlobalData({ from }) + // 在列表内查找文章 + props.post = props?.allPages?.find((p) => { + return p.slug === fullSlug || p.id === idToUuid(fullSlug) + }) + + // 处理非列表内文章的内信息 + if (!props?.post) { + const pageId = fullSlug.slice(-1)[0] + if (pageId.length >= 32) { + const post = await getNotion(pageId) + props.post = post + } + } + + // 无法获取文章 + if (!props?.post) { + props.post = null + return { props, revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND) } + } + + // 文章内容加载 + if (!props?.posts?.blockMap) { + props.post.blockMap = await getPostBlocks(props.post.id, from) + } + // 生成全文索引 && JSON.parse(BLOG.ALGOLIA_RECREATE_DATA) + if (BLOG.ALGOLIA_APP_ID) { + uploadDataToAlgolia(props?.post) + } + + // 推荐关联文章处理 + const allPosts = props.allPages?.filter(page => page.type === 'Post' && page.status === 'Published') + if (allPosts && allPosts.length > 0) { + const index = allPosts.indexOf(props.post) + props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0] + props.next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0] + props.recommendPosts = getRecommendPost(props.post, allPosts, BLOG.POST_RECOMMEND_COUNT) + } else { + props.prev = null + props.next = null + props.recommendPosts = [] + } + + delete props.allPages + return { + props, + revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND) + } +} + +/** + * 判断是否包含两个以上的 / + * @param {*} str + * @returns + */ +function hasMultipleSlashes(str) { + const regex = /\/+/g; // 创建正则表达式,匹配所有的斜杠符号 + const matches = str.match(regex); // 在字符串中找到所有匹配的斜杠符号 + return matches && matches.length >= 2; // 判断匹配的斜杠符号数量是否大于等于2 +} + +export default PrefixSlug diff --git a/pages/[prefix]/[slug].js b/pages/[prefix]/[slug]/index.js similarity index 96% rename from pages/[prefix]/[slug].js rename to pages/[prefix]/[slug]/index.js index 2beae09c..4571b791 100644 --- a/pages/[prefix]/[slug].js +++ b/pages/[prefix]/[slug]/index.js @@ -3,11 +3,12 @@ import { getPostBlocks } from '@/lib/notion' import { getGlobalData } from '@/lib/notion/getNotionData' import { idToUuid } from 'notion-utils' import { getNotion } from '@/lib/notion/getNotion' -import Slug, { getRecommendPost } from '.' +import Slug, { getRecommendPost } from '..' import { uploadDataToAlgolia } from '@/lib/algolia' /** * 根据notion的slug访问页面 + * 解析二级目录 /article/about * @param {*} props * @returns */ diff --git a/pages/[prefix]/index.js b/pages/[prefix]/index.js index ecfea4e1..6fa5d47b 100644 --- a/pages/[prefix]/index.js +++ b/pages/[prefix]/index.js @@ -13,6 +13,7 @@ import { uploadDataToAlgolia } from '@/lib/algolia' /** * 根据notion的slug访问页面 + * 只解析一级目录例如 /about * @param {*} props * @returns */ From 0d0c1ac9ea7265eeaee5f2b80343fcc85a859c71 Mon Sep 17 00:00:00 2001 From: tangly1024 Date: Sun, 29 Oct 2023 13:25:17 +0800 Subject: [PATCH 04/72] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E6=92=AD=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/notion/getPostBlocks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/notion/getPostBlocks.js b/lib/notion/getPostBlocks.js index a9e3a757..809dfe7a 100644 --- a/lib/notion/getPostBlocks.js +++ b/lib/notion/getPostBlocks.js @@ -102,7 +102,7 @@ function filterPostBlocks(id, pageBlock, slice) { } // 如果是文件,或嵌入式PDF,需要重新加密签名 - if ((b?.value?.type === 'file' || b?.value?.type === 'pdf') && b?.value?.properties?.source?.[0][0]) { + if ((b?.value?.type === 'file' || b?.value?.type === 'pdf' || b?.value?.type === 'video') && b?.value?.properties?.source?.[0][0]) { const oldUrl = b?.value?.properties?.source?.[0][0] const newUrl = `https://notion.so/signed/${encodeURIComponent(oldUrl)}?table=block&id=${b?.value?.id}` b.value.properties.source[0][0] = newUrl From fd29df3b26211aad04a9e6462cae038d7006e826 Mon Sep 17 00:00:00 2001 From: tangly1024 Date: Sun, 29 Oct 2023 13:26:43 +0800 Subject: [PATCH 05/72] 4.0.17 --- .env.local | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.local b/.env.local index 5df7065b..8fb4ae71 100644 --- a/.env.local +++ b/.env.local @@ -1,2 +1,2 @@ # 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables -NEXT_PUBLIC_VERSION=4.0.16 \ No newline at end of file +NEXT_PUBLIC_VERSION=4.0.17 \ No newline at end of file diff --git a/package.json b/package.json index 1c24ce6f..586f98cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "notion-next", - "version": "4.0.16", + "version": "4.0.17", "homepage": "https://github.com/tangly1024/NotionNext.git", "license": "MIT", "repository": { From 61f014e60ff60ab395caa3eb6be2000110918280 Mon Sep 17 00:00:00 2001 From: expoli <31023767+expoli@users.noreply.github.com> Date: Sun, 29 Oct 2023 19:42:38 +0800 Subject: [PATCH 06/72] [fix] key not exist error --- themes/hexo/components/MenuGroupCard.js | 6 ++++++ themes/hexo/components/MenuListSide.js | 6 ++++++ themes/hexo/components/MenuListTop.js | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/themes/hexo/components/MenuGroupCard.js b/themes/hexo/components/MenuGroupCard.js index 574a8e5b..5be22e2b 100644 --- a/themes/hexo/components/MenuGroupCard.js +++ b/themes/hexo/components/MenuGroupCard.js @@ -16,6 +16,12 @@ const MenuGroupCard = (props) => { { name: locale.COMMON.TAGS, to: '/tag', slot: tagSlot, show: CONFIG.MENU_TAG } ] + for (let i = 0; i < links.length; i++) { + if (links[i].id !== i) { + links[i].id = i + } + } + return ( {/* 移动端菜单 */} ) From ce68786c00e0f2a3070da33411600648ab59efbd Mon Sep 17 00:00:00 2001 From: "tangly1024.com" Date: Thu, 2 Nov 2023 17:06:41 +0800 Subject: [PATCH 23/72] log --- components/ThemeSwitch.js | 2 +- lib/notion/getNotionConfig.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/ThemeSwitch.js b/components/ThemeSwitch.js index 0c74a63e..d3913d30 100644 --- a/components/ThemeSwitch.js +++ b/components/ThemeSwitch.js @@ -45,7 +45,7 @@ const ThemeSwitch = () => { {/* 翻译按钮 */}
- +
{ + 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 bg-gray-100 dark:bg-gray-500'> + +
+  {locale.COMMON.SUBMIT} +
+
+
+
+
+ +} diff --git a/themes/commerce/components/ArticleRecommend.js b/themes/commerce/components/ArticleRecommend.js new file mode 100644 index 00000000..dc5ef58e --- /dev/null +++ b/themes/commerce/components/ArticleRecommend.js @@ -0,0 +1,60 @@ +import Link from 'next/link' +import CONFIG from '../config' +import BLOG from '@/blog.config' +import { useGlobal } from '@/lib/global' +import LazyImage from '@/components/LazyImage' + +/** + * 关联推荐文章 + * @param {prev,next} param0 + * @returns + */ +export default function ArticleRecommend({ recommendPosts, siteInfo }) { + const { locale } = useGlobal() + + if ( + !CONFIG.ARTICLE_RECOMMEND || + !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/commerce/components/BlogPostArchive.js b/themes/commerce/components/BlogPostArchive.js new file mode 100644 index 00000000..85fe02d1 --- /dev/null +++ b/themes/commerce/components/BlogPostArchive.js @@ -0,0 +1,49 @@ +import React from 'react' +import Link from 'next/link' +import BLOG from '@/blog.config' +/** + * 博客归档列表 + * @param posts 所有文章 + * @param archiveTitle 归档标题 + * @returns {JSX.Element} + * @constructor + */ +const BlogPostArchive = ({ posts = [], archiveTitle }) => { + if (!posts || posts.length === 0) { + return <> + } else { + return ( +
+
+ {archiveTitle} +
+
    + {posts?.map(post => ( +
  • +
    + {post.date?.start_date}{' '} +   + + + {post.title} + + +
    +
  • + ))} +
+
+ ) + } +} + +export default BlogPostArchive diff --git a/themes/commerce/components/BlogPostCard.js b/themes/commerce/components/BlogPostCard.js new file mode 100644 index 00000000..f1febbdd --- /dev/null +++ b/themes/commerce/components/BlogPostCard.js @@ -0,0 +1,49 @@ +import Link from 'next/link' +import React from 'react' +import CONFIG from '../config' +import { BlogPostCardInfo } from './BlogPostCardInfo' +import BLOG from '@/blog.config' +import LazyImage from '@/components/LazyImage' +// import Image from 'next/image' + +const BlogPostCard = ({ index, post, showSummary, siteInfo }) => { + const showPreview = CONFIG.POST_LIST_PREVIEW && post.blockMap + if (post && !post.pageCoverThumbnail && CONFIG.POST_LIST_COVER_DEFAULT) { + post.pageCoverThumbnail = siteInfo?.pageCover + } + const showPageCover = CONFIG.POST_LIST_COVER && post?.pageCoverThumbnail && !showPreview + // const delay = (index % 2) * 200 + + return ( + +
+
+ + {/* 文字内容 */} + + + {/* 图片封面 */} + {showPageCover && ( +
+ + + +
+ )} + +
+ +
+ + ) +} + +export default BlogPostCard diff --git a/themes/commerce/components/BlogPostCardInfo.js b/themes/commerce/components/BlogPostCardInfo.js new file mode 100644 index 00000000..eb2b5676 --- /dev/null +++ b/themes/commerce/components/BlogPostCardInfo.js @@ -0,0 +1,94 @@ +import NotionPage from '@/components/NotionPage' +import Link from 'next/link' +import TagItemMini from './TagItemMini' +import TwikooCommentCount from '@/components/TwikooCommentCount' +import BLOG from '@/blog.config' +import { formatDateFmt } from '@/lib/formatDate' + +/** + * 博客列表的文字内容 + * @param {*} param0 + * @returns + */ +export const BlogPostCardInfo = ({ post, showPreview, showPageCover, showSummary }) => { + return
+
+ {/* 标题 */} + + + {post.title} + + + + {/* 分类 */} + { post?.category &&
+ + + + {post.category} + + + + +
} + + {/* 摘要 */} + {(!showPreview || showSummary) && !post.results && ( +

+ {post.summary} +

+ )} + + {/* 搜索结果 */} + {post.results && ( +

+ {post.results.map((r, index) => ( + {r} + ))} +

+ )} + {/* 预览 */} + {showPreview && ( +
+ +
+ )} + +
+ +
+ {/* 日期标签 */} +
+ {/* 日期 */} + + + + {post?.publishDay || post.lastEditedDay} + + + +
+
+ {' '} + {post.tagItems?.map(tag => ( + + ))} +
+
+
+
+
+} diff --git a/themes/commerce/components/BlogPostListEmpty.js b/themes/commerce/components/BlogPostListEmpty.js new file mode 100644 index 00000000..5f75c3e7 --- /dev/null +++ b/themes/commerce/components/BlogPostListEmpty.js @@ -0,0 +1,14 @@ +import { useGlobal } from '@/lib/global' + +/** + * 空白博客 列表 + * @returns {JSX.Element} + * @constructor + */ +const BlogPostListEmpty = ({ currentSearch }) => { + const { locale } = useGlobal() + return
+
{locale.COMMON.NO_MORE} {(currentSearch &&
{currentSearch}
)}
+
+} +export default BlogPostListEmpty diff --git a/themes/commerce/components/BlogPostListPage.js b/themes/commerce/components/BlogPostListPage.js new file mode 100644 index 00000000..92008f83 --- /dev/null +++ b/themes/commerce/components/BlogPostListPage.js @@ -0,0 +1,34 @@ +import BlogPostCard from './BlogPostCard' +import PaginationNumber from './PaginationNumber' +import BLOG from '@/blog.config' +import BlogPostListEmpty from './BlogPostListEmpty' + +/** + * 文章列表分页表格 + * @param page 当前页 + * @param posts 所有文章 + * @param tags 所有标签 + * @returns {JSX.Element} + * @constructor + */ +const BlogPostListPage = ({ page = 1, posts = [], postCount, siteInfo }) => { + const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE) + const showPagination = postCount >= BLOG.POSTS_PER_PAGE + if (!posts || posts.length === 0 || page > totalPage) { + return + } else { + return ( +
+ {/* 文章列表 */} +
+ {posts?.map(post => ( + + ))} +
+ {showPagination && } +
+ ) + } +} + +export default BlogPostListPage diff --git a/themes/commerce/components/BlogPostListScroll.js b/themes/commerce/components/BlogPostListScroll.js new file mode 100644 index 00000000..7646b056 --- /dev/null +++ b/themes/commerce/components/BlogPostListScroll.js @@ -0,0 +1,75 @@ +import BLOG from '@/blog.config' +import BlogPostCard from './BlogPostCard' +import BlogPostListEmpty from './BlogPostListEmpty' +import { useGlobal } from '@/lib/global' +import React from 'react' +import CONFIG from '../config' +import { getListByPage } from '@/lib/utils' + +/** + * 博客列表滚动分页 + * @param posts 所有文章 + * @param tags 所有标签 + * @returns {JSX.Element} + * @constructor + */ +const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = CONFIG.POST_LIST_SUMMARY, siteInfo }) => { + const postsPerPage = BLOG.POSTS_PER_PAGE + const [page, updatePage] = React.useState(1) + const postsToShow = getListByPage(posts, page, postsPerPage) + + let hasMore = false + if (posts) { + const totalCount = posts.length + hasMore = page * postsPerPage < totalCount + } + + const handleGetMore = () => { + if (!hasMore) return + updatePage(page + 1) + } + + // 监听滚动自动分页加载 + const scrollTrigger = () => { + requestAnimationFrame(() => { + const scrollS = window.scrollY + window.outerHeight + const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0 + if (scrollS > clientHeight + 100) { + handleGetMore() + } + }) + } + + // 监听滚动 + React.useEffect(() => { + window.addEventListener('scroll', scrollTrigger) + return () => { + window.removeEventListener('scroll', scrollTrigger) + } + }) + + const targetRef = React.useRef(null) + const { locale } = useGlobal() + + if (!postsToShow || postsToShow.length === 0) { + return + } else { + return
+ + {/* 文章列表 */} +
+ {postsToShow.map(post => ( + + ))} +
+ +
+
{ handleGetMore() }} + className='w-full my-4 py-4 text-center cursor-pointer rounded-xl dark:text-gray-200' + > {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE}`}
+
+
+ } +} + +export default BlogPostListScroll diff --git a/themes/commerce/components/Card.js b/themes/commerce/components/Card.js new file mode 100644 index 00000000..c2db0e49 --- /dev/null +++ b/themes/commerce/components/Card.js @@ -0,0 +1,9 @@ +const Card = ({ children, headerSlot, className }) => { + return
+ <>{headerSlot} +
+ {children} +
+
+} +export default Card diff --git a/themes/commerce/components/Catalog.js b/themes/commerce/components/Catalog.js new file mode 100644 index 00000000..5cf1e4ad --- /dev/null +++ b/themes/commerce/components/Catalog.js @@ -0,0 +1,95 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import throttle from 'lodash.throttle' +import { uuidToId } from 'notion-utils' +import Progress from './Progress' +import { useGlobal } from '@/lib/global' + +/** + * 目录导航组件 + * @param toc + * @returns {JSX.Element} + * @constructor + */ +const Catalog = ({ toc }) => { + const { locale } = useGlobal() + // 监听滚动事件 + useEffect(() => { + window.addEventListener('scroll', actionSectionScrollSpy) + actionSectionScrollSpy() + return () => { + window.removeEventListener('scroll', actionSectionScrollSpy) + } + }, []) + + // 目录自动滚动 + const tRef = useRef(null) + const tocIds = [] + + // 同步选中目录事件 + const [activeSection, setActiveSection] = useState(null) + + const throttleMs = 200 + const actionSectionScrollSpy = useCallback(throttle(() => { + const sections = document.getElementsByClassName('notion-h') + let prevBBox = null + let currentSectionId = activeSection + for (let i = 0; i < sections.length; ++i) { + const section = sections[i] + if (!section || !(section instanceof Element)) continue + if (!currentSectionId) { + currentSectionId = section.getAttribute('data-id') + } + const bbox = section.getBoundingClientRect() + const prevHeight = prevBBox ? bbox.top - prevBBox.bottom : 0 + const offset = Math.max(150, prevHeight / 4) + // GetBoundingClientRect returns values relative to viewport + if (bbox.top - offset < 0) { + currentSectionId = section.getAttribute('data-id') + prevBBox = bbox + continue + } + // No need to continue loop, if last element has been detected + break + } + setActiveSection(currentSectionId) + const index = tocIds.indexOf(currentSectionId) || 0 + tRef?.current?.scrollTo({ top: 28 * index, behavior: 'smooth' }) + }, throttleMs)) + + // 无目录就直接返回空 + if (!toc || toc.length < 1) { + return <> + } + + return
+
{locale.COMMON.TABLE_OF_CONTENTS}
+
+ +
+
+ + +
+
+} + +export default Catalog diff --git a/themes/commerce/components/CategoryGroup.js b/themes/commerce/components/CategoryGroup.js new file mode 100644 index 00000000..3e8ff7d0 --- /dev/null +++ b/themes/commerce/components/CategoryGroup.js @@ -0,0 +1,31 @@ +import Link from 'next/link' +import React from 'react' + +const CategoryGroup = ({ currentCategory, categories }) => { + if (!categories) { + return <> + } + return <> +
+ {categories.map(category => { + const selected = currentCategory === category.name + return ( + + +
{category.name}({category.count})
+ + + ); + })} +
+ ; +} + +export default CategoryGroup diff --git a/themes/commerce/components/FloatDarkModeButton.js b/themes/commerce/components/FloatDarkModeButton.js new file mode 100644 index 00000000..f693d1f0 --- /dev/null +++ b/themes/commerce/components/FloatDarkModeButton.js @@ -0,0 +1,31 @@ +import { useGlobal } from '@/lib/global' +import { saveDarkModeToCookies } from '@/themes/theme' +import CONFIG from '../config' + +export default function FloatDarkModeButton () { + const { isDarkMode, updateDarkMode } = useGlobal() + + if (!CONFIG.WIDGET_DARK_MODE) { + return <> + } + + // 用户手动设置主题 + const handleChangeDarkMode = () => { + const newStatus = !isDarkMode + saveDarkModeToCookies(newStatus) + updateDarkMode(newStatus) + const htmlElement = document.getElementsByTagName('html')[0] + htmlElement.classList?.remove(newStatus ? 'light' : 'dark') + htmlElement.classList?.add(newStatus ? 'dark' : 'light') + } + + return ( +
+ +
+ ) +} diff --git a/themes/commerce/components/Footer.js b/themes/commerce/components/Footer.js new file mode 100644 index 00000000..ced637e9 --- /dev/null +++ b/themes/commerce/components/Footer.js @@ -0,0 +1,36 @@ +import React from 'react' +import BLOG from '@/blog.config' +import { siteConfig } from '@/lib/config' + +const Footer = ({ title }) => { + const d = new Date() + const currentYear = d.getFullYear() + const copyrightDate = (function() { + if (Number.isInteger(BLOG.SINCE) && BLOG.SINCE < currentYear) { + return BLOG.SINCE + '-' + currentYear + } + return currentYear + })() + + return ( + + ) +} + +export default Footer diff --git a/themes/commerce/components/Hero.js b/themes/commerce/components/Hero.js new file mode 100644 index 00000000..ef50465f --- /dev/null +++ b/themes/commerce/components/Hero.js @@ -0,0 +1,68 @@ +// import Image from 'next/image' +import { useEffect, useState } from 'react' +import Typed from 'typed.js' +import CONFIG from '../config' +import NavButtonGroup from './NavButtonGroup' +import { useGlobal } from '@/lib/global' +import LazyImage from '@/components/LazyImage' +import { siteConfig } from '@/lib/config' + +let wrapperTop = 0 + +/** + * 顶部全屏大图 + * @returns + */ +const Hero = props => { + const [typed, changeType] = useState() + const { siteInfo } = props + const { locale } = useGlobal() + const scrollToWrapper = () => { + window.scrollTo({ top: wrapperTop, behavior: 'smooth' }) + } + const GREETING_WORDS = siteConfig('GREETING_WORDS').split(',') + useEffect(() => { + updateHeaderHeight() + + if (!typed && window && document.getElementById('typed')) { + changeType( + new Typed('#typed', { + strings: GREETING_WORDS, + typeSpeed: 200, + backSpeed: 100, + backDelay: 400, + showCursor: true, + smartBackspace: true + }) + ) + } + + window.addEventListener('resize', updateHeaderHeight) + return () => { + window.removeEventListener('resize', updateHeaderHeight) + } + }) + + function updateHeaderHeight() { + requestAnimationFrame(() => { + const wrapperElement = document.getElementById('wrapper') + wrapperTop = wrapperElement?.offsetTop + }) + } + + return ( + + ) +} + +export default Hero diff --git a/themes/commerce/components/HexoRecentComments.js b/themes/commerce/components/HexoRecentComments.js new file mode 100644 index 00000000..2ebf00c8 --- /dev/null +++ b/themes/commerce/components/HexoRecentComments.js @@ -0,0 +1,47 @@ +import React from 'react' +import BLOG from '@/blog.config' +import Card from '@/themes/hexo/components/Card' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import { RecentComments } from '@waline/client' + +/** + * @see https://waline.js.org/guide/get-started.html + * @param {*} props + * @returns + */ +const HexoRecentComments = (props) => { + const [comments, updateComments] = React.useState([]) + const { locale } = useGlobal() + const [onLoading, changeLoading] = React.useState(true) + React.useEffect(() => { + RecentComments({ + serverURL: BLOG.COMMENT_WALINE_SERVER_URL, + count: 5 + }).then(({ comments }) => { + changeLoading(false) + updateComments(comments) + }) + }, []) + + return ( + +
+ + {locale.COMMON.RECENT_COMMENTS} +
+ + {onLoading &&
Loading...
} + {!onLoading && comments && comments.length === 0 &&
No Comments
} + {!onLoading && comments && comments.length > 0 && comments.map((comment) =>
+
+
+ --{comment.nick} +
+
)} + + + ) +} + +export default HexoRecentComments diff --git a/themes/commerce/components/InfoCard.js b/themes/commerce/components/InfoCard.js new file mode 100644 index 00000000..6b33034e --- /dev/null +++ b/themes/commerce/components/InfoCard.js @@ -0,0 +1,33 @@ +import { useRouter } from 'next/router' +import Card from './Card' +import SocialButton from './SocialButton' +import MenuGroupCard from './MenuGroupCard' +import LazyImage from '@/components/LazyImage' +import { siteConfig } from '@/lib/config' + +/** + * 社交信息卡 + * @param {*} props + * @returns + */ +export function InfoCard(props) { + const { className, siteInfo } = props + const router = useRouter() + return ( + +
{ + router.push('/') + }} + > + {/* eslint-disable-next-line @next/next/no-img-element */} + +
+
{siteConfig('AUTHOR')}
+
{siteConfig('BIO')}
+ + +
+ ) +} diff --git a/themes/commerce/components/JumpToCommentButton.js b/themes/commerce/components/JumpToCommentButton.js new file mode 100644 index 00000000..fb007712 --- /dev/null +++ b/themes/commerce/components/JumpToCommentButton.js @@ -0,0 +1,29 @@ +import React from 'react' +import CONFIG from '../config' + +/** + * 跳转到评论区 + * @returns {JSX.Element} + * @constructor + */ +const JumpToCommentButton = () => { + if (!CONFIG.WIDGET_TO_COMMENT) { + return <> + } + + function navToComment() { + if (document.getElementById('comment')) { + window.scrollTo({ top: document.getElementById('comment').offsetTop, behavior: 'smooth' }) + } + // 兼容性不好 + // const commentElement = document.getElementById('comment') + // if (commentElement) { + // commentElement?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' }) + } + + return (
+ +
) +} + +export default JumpToCommentButton diff --git a/themes/commerce/components/JumpToTopButton.js b/themes/commerce/components/JumpToTopButton.js new file mode 100644 index 00000000..77313f46 --- /dev/null +++ b/themes/commerce/components/JumpToTopButton.js @@ -0,0 +1,25 @@ +import { useGlobal } from '@/lib/global' +import React from 'react' +import CONFIG from '../config' + +/** + * 跳转到网页顶部 + * 当屏幕下滑500像素后会出现该控件 + * @param targetRef 关联高度的目标html标签 + * @param showPercent 是否显示百分比 + * @returns {JSX.Element} + * @constructor + */ +const JumpToTopButton = ({ showPercent = true, percent }) => { + const { locale } = useGlobal() + + if (!CONFIG.WIDGET_TO_TOP) { + return <> + } + return (
window.scrollTo({ top: 0, behavior: 'smooth' })} > +
+ {showPercent && (
{percent}
)} +
) +} + +export default JumpToTopButton diff --git a/themes/commerce/components/LatestPostsGroup.js b/themes/commerce/components/LatestPostsGroup.js new file mode 100644 index 00000000..a6d9ace2 --- /dev/null +++ b/themes/commerce/components/LatestPostsGroup.js @@ -0,0 +1,64 @@ +import BLOG from '@/blog.config' +import LazyImage from '@/components/LazyImage' +import { useGlobal } from '@/lib/global' +// import Image from 'next/image' +import Link from 'next/link' +import { useRouter } from 'next/router' + +/** + * 最新文章列表 + * @param posts 所有文章数据 + * @param sliceCount 截取展示的数量 默认6 + * @constructor + */ +const LatestPostsGroup = ({ latestPosts, siteInfo }) => { + // 获取当前路径 + const currentPath = useRouter().asPath + const { locale } = useGlobal() + + if (!latestPosts) { + return <> + } + + return <> +
+
+ + {locale.COMMON.LATEST_POSTS} +
+
+ {latestPosts.map(post => { + const selected = currentPath === `${BLOG.SUB_PATH}/${post.slug}` + + const headerImage = post?.pageCoverThumbnail ? post.pageCoverThumbnail : siteInfo?.pageCover + + return ( + ( + +
+ +
+
+
+
{post.title}
+
{post.lastEditedDay}
+
+
+ + ) + ) + })} + +} +export default LatestPostsGroup diff --git a/themes/commerce/components/LoadingCover.js b/themes/commerce/components/LoadingCover.js new file mode 100644 index 00000000..c6418fad --- /dev/null +++ b/themes/commerce/components/LoadingCover.js @@ -0,0 +1,8 @@ +export default function LoadingCover () { + return (
+
+ +
+
+ ) +} diff --git a/themes/commerce/components/LogoBar.js b/themes/commerce/components/LogoBar.js new file mode 100644 index 00000000..925f1063 --- /dev/null +++ b/themes/commerce/components/LogoBar.js @@ -0,0 +1,20 @@ +import Link from 'next/link' +import { siteConfig } from '@/lib/config' +import LazyImage from '@/components/LazyImage'; + +/** + * Logo图标 + * @param {*} props + * @returns + */ +export default function LogoBar (props) { + const { siteInfo } = props + return ( +
+ + + +
{siteConfig('TITLE')}
+
+ ); +} diff --git a/themes/commerce/components/MenuBarMobile.js b/themes/commerce/components/MenuBarMobile.js new file mode 100644 index 00000000..d21fbfe5 --- /dev/null +++ b/themes/commerce/components/MenuBarMobile.js @@ -0,0 +1,39 @@ +import React from 'react' +import { useGlobal } from '@/lib/global' +import CONFIG from '../config' +import BLOG from '@/blog.config' +import { MenuItemCollapse } from './MenuItemCollapse' + +export const MenuBarMobile = (props) => { + const { customMenu, customNav } = props + const { locale } = useGlobal() + + let links = [ + // { name: locale.NAV.INDEX, to: '/' || '/', show: true }, + { name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY }, + { name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG }, + { name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE } + // { name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH } + ] + + if (customNav) { + links = links.concat(customNav) + } + + // 如果 开启自定义菜单,则不再使用 Page生成菜单。 + if (BLOG.CUSTOM_MENU) { + links = customMenu + } + + if (!links || links.length === 0) { + return null + } + + return ( + + ) +} diff --git a/themes/commerce/components/MenuGroupCard.js b/themes/commerce/components/MenuGroupCard.js new file mode 100644 index 00000000..3b5ac595 --- /dev/null +++ b/themes/commerce/components/MenuGroupCard.js @@ -0,0 +1,51 @@ +import React from 'react' +import Link from 'next/link' +import { useGlobal } from '@/lib/global' +import CONFIG from '../config' + +const MenuGroupCard = (props) => { + const { postCount, categoryOptions, tagOptions } = props + const { locale } = useGlobal() + const archiveSlot =
{postCount}
+ const categorySlot =
{categoryOptions?.length}
+ const tagSlot =
{tagOptions?.length}
+ + const links = [ + { name: locale.COMMON.ARTICLE, to: '/archive', slot: archiveSlot, show: CONFIG.MENU_ARCHIVE }, + { name: locale.COMMON.CATEGORY, to: '/category', slot: categorySlot, show: CONFIG.MENU_CATEGORY }, + { name: locale.COMMON.TAGS, to: '/tag', slot: tagSlot, show: CONFIG.MENU_TAG } + ] + + for (let i = 0; i < links.length; i++) { + if (links[i].id !== i) { + links[i].id = i + } + } + + return ( + + ) +} +export default MenuGroupCard diff --git a/themes/commerce/components/MenuItemCollapse.js b/themes/commerce/components/MenuItemCollapse.js new file mode 100644 index 00000000..ccd9d6bc --- /dev/null +++ b/themes/commerce/components/MenuItemCollapse.js @@ -0,0 +1,54 @@ +import Collapse from '@/components/Collapse' +import Link from 'next/link' +import { useState } from 'react' + +/** + * 折叠菜单 + * @param {*} param0 + * @returns + */ +export const MenuItemCollapse = ({ link }) => { + 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/commerce/components/MenuItemDrop.js b/themes/commerce/components/MenuItemDrop.js new file mode 100644 index 00000000..05a6b4e6 --- /dev/null +++ b/themes/commerce/components/MenuItemDrop.js @@ -0,0 +1,43 @@ +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useState } from 'react' + +export const MenuItemDrop = ({ link }) => { + const [show, changeShow] = useState(false) + const hasSubMenu = link?.subMenus?.length > 0 + const selected = useRouter().asPath === link.to + + if (!link || !link.show) { + return null + } + + return
changeShow(true)} onMouseOut={() => changeShow(false)} className='h-full'> + + {!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/commerce/components/MenuListSide.js b/themes/commerce/components/MenuListSide.js new file mode 100644 index 00000000..789d07d2 --- /dev/null +++ b/themes/commerce/components/MenuListSide.js @@ -0,0 +1,43 @@ +import React from 'react' +import { useGlobal } from '@/lib/global' +import BLOG from '@/blog.config' +import { MenuItemCollapse } from './MenuItemCollapse' +import CONFIG from '../config' + +export const MenuListSide = (props) => { + const { customNav, customMenu } = props + const { locale } = useGlobal() + + let links = [ + { icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE }, + { icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH }, + { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY }, + { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG } + ] + + if (customNav) { + links = customNav.concat(links) + } + + for (let i = 0; i < links.length; i++) { + if (links[i].id !== i) { + links[i].id = i + } + } + + // 如果 开启自定义菜单,则覆盖Page生成的菜单 + if (BLOG.CUSTOM_MENU) { + links = customMenu + } + + if (!links || links.length === 0) { + return null + } + + return ( + + ) +} diff --git a/themes/commerce/components/MenuListTop.js b/themes/commerce/components/MenuListTop.js new file mode 100644 index 00000000..4a07b562 --- /dev/null +++ b/themes/commerce/components/MenuListTop.js @@ -0,0 +1,43 @@ +import React from 'react' +import { useGlobal } from '@/lib/global' +import CONFIG from '../config' +import { MenuItemDrop } from './MenuItemDrop' +import { siteConfig } from '@/lib/config' + +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: CONFIG.MENU_INDEX }, + { id: 2, icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH }, + { id: 3, icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE } + // { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY }, + // { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG } + ] + + 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/commerce/components/NavButtonGroup.js b/themes/commerce/components/NavButtonGroup.js new file mode 100644 index 00000000..38709213 --- /dev/null +++ b/themes/commerce/components/NavButtonGroup.js @@ -0,0 +1,33 @@ + +import React from 'react' +import Link from 'next/link' + +/** + * 首页导航大按钮组件 + * @param {*} props + * @returns + */ +const NavButtonGroup = (props) => { + const { categoryOptions } = props + if (!categoryOptions || categoryOptions.length === 0) { + return <> + } + + return ( + + ) +} +export default NavButtonGroup diff --git a/themes/commerce/components/PaginationNumber.js b/themes/commerce/components/PaginationNumber.js new file mode 100644 index 00000000..d041fe6c --- /dev/null +++ b/themes/commerce/components/PaginationNumber.js @@ -0,0 +1,107 @@ +import Link from 'next/link' +import { useRouter } from 'next/router' + +/** + * 数字翻页插件 + * @param page 当前页码 + * @param showNext 是否有下一页 + * @returns {JSX.Element} + * @constructor + */ +const PaginationNumber = ({ page, totalPage }) => { + const router = useRouter() + const currentPage = +page + const showNext = page < totalPage + const pagePrefix = router.asPath.split('?')[0].replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '') + const pages = generatePages(pagePrefix, page, currentPage, totalPage) + + return ( +
+ {/* 上一页 */} + + + + + + + {pages} + + {/* 下一页 */} + + + + + +
+ ) +} + +function getPageElement(page, currentPage, pagePrefix) { + return ( + ( + + {page} + + ) + ) +} + +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/commerce/components/PostHeader.js b/themes/commerce/components/PostHeader.js new file mode 100644 index 00000000..acafaf0f --- /dev/null +++ b/themes/commerce/components/PostHeader.js @@ -0,0 +1,79 @@ +import Link from 'next/link' +import TagItemMini from './TagItemMini' +import { useGlobal } from '@/lib/global' +import BLOG from '@/blog.config' +import NotionIcon from '@/components/NotionIcon' +import LazyImage from '@/components/LazyImage' +import { formatDateFmt } from '@/lib/formatDate' + +export default function PostHeader({ post, siteInfo }) { + const { locale } = useGlobal() + + if (!post) { + return <> + } + const headerImage = post?.pageCover ? post.pageCover : siteInfo?.pageCover + + return ( + + ) +} diff --git a/themes/commerce/components/Progress.js b/themes/commerce/components/Progress.js new file mode 100644 index 00000000..8cb9fe0e --- /dev/null +++ b/themes/commerce/components/Progress.js @@ -0,0 +1,44 @@ +import React, { useEffect, useState } from 'react' +import { isBrowser } from '@/lib/utils' + +/** + * 顶部页面阅读进度条 + * @returns {JSX.Element} + * @constructor + */ +const Progress = ({ targetRef, showPercent = true }) => { + const currentRef = targetRef?.current || targetRef + const [percent, changePercent] = useState(0) + const scrollListener = () => { + const target = currentRef || (isBrowser && document.getElementById('article-wrapper')) + if (target) { + const clientHeight = target.clientHeight + const scrollY = window.pageYOffset + const fullHeight = clientHeight - window.outerHeight + let per = parseFloat(((scrollY / fullHeight) * 100).toFixed(0)) + if (per > 100) per = 100 + if (per < 0) per = 0 + changePercent(per) + } + } + + useEffect(() => { + document.addEventListener('scroll', scrollListener) + return () => document.removeEventListener('scroll', scrollListener) + }, []) + + return ( +
+
+ {showPercent && ( +
{percent}%
+ )} +
+
+ ) +} + +export default Progress diff --git a/themes/commerce/components/RightFloatArea.js b/themes/commerce/components/RightFloatArea.js new file mode 100644 index 00000000..06fef20e --- /dev/null +++ b/themes/commerce/components/RightFloatArea.js @@ -0,0 +1,42 @@ +import throttle from 'lodash.throttle' +import { useCallback, useEffect, useState } from 'react' +import FloatDarkModeButton from './FloatDarkModeButton' +import JumpToTopButton from './JumpToTopButton' + +/** + * 悬浮在右下角的按钮,当页面向下滚动100px时会出现 + * @param {*} param0 + * @returns + */ +export default function RightFloatArea({ floatSlot }) { + const [showFloatButton, switchShow] = useState(false) + const scrollListener = useCallback(throttle(() => { + const targetRef = document.getElementById('wrapper') + const clientHeight = targetRef?.clientHeight + const scrollY = window.pageYOffset + const fullHeight = clientHeight - window.outerHeight + let per = parseFloat(((scrollY / fullHeight) * 100).toFixed(0)) + if (per > 100) per = 100 + const shouldShow = scrollY > 100 && per > 0 + + // 右下角显示悬浮按钮 + if (shouldShow !== showFloatButton) { + switchShow(shouldShow) + } + }, 200)) + + useEffect(() => { + document.addEventListener('scroll', scrollListener) + return () => document.removeEventListener('scroll', scrollListener) + }, []) + + return ( +
+
+ + {floatSlot} + +
+
+ ) +} diff --git a/themes/commerce/components/SearchDrawer.js b/themes/commerce/components/SearchDrawer.js new file mode 100644 index 00000000..c7ec88a7 --- /dev/null +++ b/themes/commerce/components/SearchDrawer.js @@ -0,0 +1,36 @@ +import { Router } from 'next/router' +import { useImperativeHandle, useRef } from 'react' +import SearchInput from './SearchInput' +const SearchDrawer = ({ cRef, slot }) => { + const searchDrawer = useRef() + const searchInputRef = useRef() + useImperativeHandle(cRef, () => { + return { + show: () => { + searchDrawer?.current?.classList?.remove('hidden') + searchInputRef?.current?.focus() + } + } + }) + const hidden = () => { + searchDrawer?.current?.classList?.add('hidden') + } + Router.events.on('routeChangeComplete', (...args) => { + hidden() + }) + return ( +
+
+
+ + {slot} +
+
+ + {/* 背景蒙版 */} +
+
+ ) +} + +export default SearchDrawer diff --git a/themes/commerce/components/SearchInput.js b/themes/commerce/components/SearchInput.js new file mode 100644 index 00000000..462c58b3 --- /dev/null +++ b/themes/commerce/components/SearchInput.js @@ -0,0 +1,106 @@ +import { useRouter } from 'next/router' +import { useImperativeHandle, useRef, useState } from 'react' +import { useGlobal } from '@/lib/global' +let lock = false + +const SearchInput = props => { + const { currentSearch, cRef, className } = props + const [onLoading, setLoadingState] = useState(false) + const router = useRouter() + const searchInputRef = useRef() + const { locale } = useGlobal() + useImperativeHandle(cRef, () => { + return { + focus: () => { + searchInputRef?.current?.focus() + } + } + }) + + const handleSearch = () => { + const key = searchInputRef.current.value + if (key && key !== '') { + setLoadingState(true) + router.push({ pathname: '/search/' + key }).then(r => { + setLoadingState(false) + }) + // location.href = '/search/' + 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 = '' + } + + const [showClean, setShowClean] = useState(false) + const updateSearchKey = val => { + if (lock) { + return + } + searchInputRef.current.value = val + + if (val) { + setShowClean(true) + } else { + setShowClean(false) + } + } + function lockSearchInput () { + lock = true + } + + function unLockSearchInput () { + lock = false + } + + return ( +
+ updateSearchKey(e.target.value)} + defaultValue={currentSearch || ''} + /> + +
+ +
+ + {showClean && ( +
+ +
+ )} +
+ ) +} + +export default SearchInput diff --git a/themes/commerce/components/SearchNav.js b/themes/commerce/components/SearchNav.js new file mode 100644 index 00000000..fc393c1b --- /dev/null +++ b/themes/commerce/components/SearchNav.js @@ -0,0 +1,70 @@ +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import { useEffect, useRef } from 'react' +import Card from './Card' +import SearchInput from './SearchInput' +import TagItemMini from './TagItemMini' + +/** + * 搜索页面的导航 + * @param {*} props + * @returns + */ +export default function SearchNav(props) { + const { tagOptions, categoryOptions } = props + const cRef = useRef(null) + const { locale } = useGlobal() + useEffect(() => { + // 自动聚焦到搜索框 + cRef?.current?.focus() + }, []) + + return <> +
+ + {/* 分类 */} + +
+ + {locale.COMMON.CATEGORY}: +
+
+ {categoryOptions?.map(category => { + return ( + +
+ + {category.name}({category.count}) +
+ + ) + })} +
+
+ {/* 标签 */} + +
+ + {locale.COMMON.TAGS}: +
+
+ {tagOptions?.map(tag => { + return ( +
+ +
+ ) + })} +
+
+
+ +} diff --git a/themes/commerce/components/SideBar.js b/themes/commerce/components/SideBar.js new file mode 100644 index 00000000..913ce0a9 --- /dev/null +++ b/themes/commerce/components/SideBar.js @@ -0,0 +1,33 @@ +import { siteConfig } from '@/lib/config' +import LazyImage from '@/components/LazyImage' +import { useRouter } from 'next/router' +import MenuGroupCard from './MenuGroupCard' +import { MenuListSide } from './MenuListSide' + +/** + * 侧边抽屉 + * @param tags + * @param currentTag + * @returns {JSX.Element} + * @constructor + */ +const SideBar = (props) => { + const { siteInfo } = props + const router = useRouter() + return ( + + ) +} + +export default SideBar diff --git a/themes/commerce/components/SideBarDrawer.js b/themes/commerce/components/SideBarDrawer.js new file mode 100644 index 00000000..f457d4a3 --- /dev/null +++ b/themes/commerce/components/SideBarDrawer.js @@ -0,0 +1,52 @@ +import { useRouter } from 'next/router' +import { useEffect } from 'react' + +/** + * 侧边栏抽屉面板,可以从侧面拉出 + * @returns {JSX.Element} + * @constructor + */ +const SideBarDrawer = ({ children, isOpen, onOpen, onClose, className }) => { + const router = useRouter() + useEffect(() => { + const sideBarDrawerRouteListener = () => { + switchSideDrawerVisible(false) + } + router.events.on('routeChangeComplete', sideBarDrawerRouteListener) + return () => { + router.events.off('routeChangeComplete', sideBarDrawerRouteListener) + } + }, [router.events]) + + // 点击按钮更改侧边抽屉状态 + const switchSideDrawerVisible = (showStatus) => { + if (showStatus) { + onOpen && onOpen() + } else { + onClose && onClose() + } + const sideBarDrawer = window.document.getElementById('sidebar-drawer') + const sideBarDrawerBackground = window.document.getElementById('sidebar-drawer-background') + + if (showStatus) { + sideBarDrawer?.classList.replace('-mr-72', 'mr-0') + sideBarDrawerBackground?.classList.replace('hidden', 'block') + } else { + sideBarDrawer?.classList.replace('mr-0', '-mr-72') + sideBarDrawerBackground?.classList.replace('block', 'hidden') + } + } + + return + + } export default Footer diff --git a/themes/commerce/components/Hero.js b/themes/commerce/components/Hero.js index ef50465f..34ddbd57 100644 --- a/themes/commerce/components/Hero.js +++ b/themes/commerce/components/Hero.js @@ -1,65 +1,24 @@ // import Image from 'next/image' -import { useEffect, useState } from 'react' -import Typed from 'typed.js' import CONFIG from '../config' -import NavButtonGroup from './NavButtonGroup' -import { useGlobal } from '@/lib/global' import LazyImage from '@/components/LazyImage' -import { siteConfig } from '@/lib/config' - -let wrapperTop = 0 /** * 顶部全屏大图 * @returns */ const Hero = props => { - const [typed, changeType] = useState() const { siteInfo } = props - const { locale } = useGlobal() - const scrollToWrapper = () => { - window.scrollTo({ top: wrapperTop, behavior: 'smooth' }) - } - const GREETING_WORDS = siteConfig('GREETING_WORDS').split(',') - useEffect(() => { - updateHeaderHeight() - - if (!typed && window && document.getElementById('typed')) { - changeType( - new Typed('#typed', { - strings: GREETING_WORDS, - typeSpeed: 200, - backSpeed: 100, - backDelay: 400, - showCursor: true, - smartBackspace: true - }) - ) - } - - window.addEventListener('resize', updateHeaderHeight) - return () => { - window.removeEventListener('resize', updateHeaderHeight) - } - }) - - function updateHeaderHeight() { - requestAnimationFrame(() => { - const wrapperElement = document.getElementById('wrapper') - wrapperTop = wrapperElement?.offsetTop - }) - } return ( ) diff --git a/themes/commerce/components/LogoBar.js b/themes/commerce/components/LogoBar.js index 925f1063..de5a5bfe 100644 --- a/themes/commerce/components/LogoBar.js +++ b/themes/commerce/components/LogoBar.js @@ -1,5 +1,5 @@ import Link from 'next/link' -import { siteConfig } from '@/lib/config' +// import { siteConfig } from '@/lib/config' import LazyImage from '@/components/LazyImage'; /** @@ -14,7 +14,7 @@ export default function LogoBar (props) { -
{siteConfig('TITLE')}
+ {/*
{siteConfig('TITLE')}
*/}
); } diff --git a/themes/commerce/components/MenuItemDrop.js b/themes/commerce/components/MenuItemDrop.js index 05a6b4e6..cec4b854 100644 --- a/themes/commerce/components/MenuItemDrop.js +++ b/themes/commerce/components/MenuItemDrop.js @@ -16,14 +16,14 @@ export const MenuItemDrop = ({ link }) => { {!hasSubMenu && - {link?.icon && } {link?.name} + className={`${selected && 'border-b-2 border-[#D2232A]'} h-full flex space-x-1 whitespace-nowrap items-center font-sans menu-link pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest pb-1`}> + {link?.icon && }
{link?.name}
{/* {hasSubMenu && } */} } {hasSubMenu && <> -
- {link?.icon && } {link?.name} +
+ {link?.icon && }
{link?.name}
{/* */}
} diff --git a/themes/commerce/components/MenuListSide.js b/themes/commerce/components/MenuListSide.js index 789d07d2..a60ba77e 100644 --- a/themes/commerce/components/MenuListSide.js +++ b/themes/commerce/components/MenuListSide.js @@ -18,7 +18,7 @@ export const MenuListSide = (props) => { if (customNav) { links = customNav.concat(links) } - + for (let i = 0; i < links.length; i++) { if (links[i].id !== i) { links[i].id = i diff --git a/themes/commerce/components/TopNavBar.js b/themes/commerce/components/TopNavBar.js index 00a232fa..c2bf6065 100644 --- a/themes/commerce/components/TopNavBar.js +++ b/themes/commerce/components/TopNavBar.js @@ -17,7 +17,6 @@ export default function TopNavBar(props) { const { customNav, customMenu } = props const [isOpen, changeShow] = useState(false) const collapseRef = useRef(null) - let windowTop = 0 const { locale } = useGlobal() @@ -43,19 +42,17 @@ export default function TopNavBar(props) { } }, []) - const throttleMs = 200 + const throttleMs = 150 const scrollTrigger = throttle(() => { const scrollS = window.scrollY - const nav = document.querySelector('#navbar') + const nav = document.querySelector('#top-navbar') - const narrowNav = scrollS >= windowTop || scrollS > 200 + const narrowNav = scrollS > 50 if (narrowNav) { - nav && nav.classList.replace('h-24', 'h-16') - windowTop = scrollS + nav && nav.classList.replace('h-24', 'h-14') } else { - nav && nav.classList.replace('h-16', 'h-24') - windowTop = scrollS + nav && nav.classList.replace('h-14', 'h-24') } }, throttleMs) @@ -68,34 +65,32 @@ export default function TopNavBar(props) { return null } - return ( -
+ return
- {/* 移动端折叠菜单 */} - -
- collapseRef.current?.updateCollapseHeight(param)} /> -
-
+ {/* 移动端折叠菜单 */} + +
+ collapseRef.current?.updateCollapseHeight(param)} /> +
+
- {/* 导航栏菜单 */} -