diff --git a/.eslintrc.js b/.eslintrc.js index 9836da77..c6fbb20a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,11 +4,7 @@ module.exports = { es2021: true, node: true }, - extends: [ - 'plugin:react/recommended', - 'plugin:@next/next/recommended', - 'standard' - ], + extends: ['plugin:react/recommended', 'plugin:@next/next/recommended', 'standard', 'prettier'], parserOptions: { ecmaFeatures: { jsx: true @@ -16,10 +12,7 @@ module.exports = { ecmaVersion: 12, sourceType: 'module' }, - plugins: [ - 'react', - 'react-hooks' - ], + plugins: ['react', 'react-hooks', 'prettier'], settings: { react: { version: 'detect' diff --git a/.prettierrc.json b/.prettierrc.json index 5f5ab6df..f0bc11c8 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,6 +1,10 @@ { - "singleQuote": true, - "semi": false, - "trailingComma": "none", - "arrowParens": "avoid" -} + "singleQuote": true, + "semi": false, + "trailingComma": "none", + "arrowParens": "avoid", + "printWidth": 120, + "bracketSpacing": true, + "jsxSingleQuote": true, + "jsxBracketSameLine": true +} \ No newline at end of file diff --git a/components/WalineComponent.js b/components/WalineComponent.js index 37247d48..cd2d78f5 100644 --- a/components/WalineComponent.js +++ b/components/WalineComponent.js @@ -1,4 +1,4 @@ -import React, { createRef } from 'react' +import { createRef } from 'react' import { init } from '@waline/client' import { useRouter } from 'next/router' import '@waline/client/dist/waline.css' @@ -21,7 +21,7 @@ const WalineComponent = (props) => { } } - React.useEffect(() => { + useEffect(() => { if (!waline) { waline = init({ ...props, diff --git a/lib/db/getSiteData.js b/lib/db/getSiteData.js index 85dd0c7a..997a97c6 100755 --- a/lib/db/getSiteData.js +++ b/lib/db/getSiteData.js @@ -25,10 +25,7 @@ export { getPost } from '../notion/getNotionPost' * @returns * */ -export async function getGlobalData({ - pageId = BLOG.NOTION_PAGE_ID, - from -}) { +export async function getGlobalData({ pageId = BLOG.NOTION_PAGE_ID, from }) { // 从notion获取 const data = await getNotionPageData({ pageId, from }) const db = deepClone(data) @@ -89,7 +86,9 @@ function cleanBlock(post) { * @returns */ function getLatestPosts({ allPages, from, latestPostCount }) { - const allPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published') + const allPosts = allPages?.filter( + page => page.type === 'Post' && page.status === 'Published' + ) const latestPosts = Object.create(allPosts).sort((a, b) => { const dateA = new Date(a?.lastEditedDate || a?.publishDate) @@ -140,7 +139,13 @@ function getCustomNav({ allPages }) { p.to = '/' + p.slug } } - customNav.push({ icon: p.icon || null, name: p.title, to: p.slug, target: '_blank', show: true }) + customNav.push({ + icon: p.icon || null, + name: p.title, + to: p.slug, + target: '_blank', + show: true + }) }) } return customNav @@ -152,7 +157,12 @@ function getCustomNav({ allPages }) { * @returns */ function getCustomMenu({ collectionData }) { - const menuPages = collectionData.filter(post => post.status === 'Published' && (post?.type === BLOG.NOTION_PROPERTY_NAME.type_menu || post?.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu)) + const menuPages = collectionData.filter( + post => + post.status === 'Published' && + (post?.type === BLOG.NOTION_PROPERTY_NAME.type_menu || + post?.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu) + ) const menus = [] if (menuPages && menuPages.length > 0) { menuPages.forEach(e => { @@ -190,7 +200,9 @@ function getCustomMenu({ collectionData }) { */ function getTagOptions(schema) { if (!schema) return {} - const tagSchema = Object.values(schema).find(e => e.name === BLOG.NOTION_PROPERTY_NAME.tags) + const tagSchema = Object.values(schema).find( + e => e.name === BLOG.NOTION_PROPERTY_NAME.tags + ) return tagSchema?.options || [] } @@ -201,7 +213,9 @@ function getTagOptions(schema) { */ function getCategoryOptions(schema) { if (!schema) return {} - const categorySchema = Object.values(schema).find(e => e.name === BLOG.NOTION_PROPERTY_NAME.category) + const categorySchema = Object.values(schema).find( + e => e.name === BLOG.NOTION_PROPERTY_NAME.category + ) return categorySchema?.options || [] } @@ -213,9 +227,15 @@ function getCategoryOptions(schema) { */ function getSiteInfo({ collection, block }) { const title = collection?.name?.[0][0] || BLOG.TITLE - const description = collection?.description ? Object.assign(collection).description[0][0] : BLOG.DESCRIPTION - const pageCover = collection?.cover ? mapImgUrl(collection?.cover, block[idToUuid(BLOG.NOTION_PAGE_ID)]?.value) : BLOG.HOME_BANNER_IMAGE - let icon = collection?.icon ? mapImgUrl(collection?.icon, collection, 'collection') : BLOG.AVATAR + const description = collection?.description + ? Object.assign(collection).description[0][0] + : BLOG.DESCRIPTION + const pageCover = collection?.cover + ? mapImgUrl(collection?.cover, block[idToUuid(BLOG.NOTION_PAGE_ID)]?.value) + : BLOG.HOME_BANNER_IMAGE + let icon = collection?.icon + ? mapImgUrl(collection?.icon, collection, 'collection') + : BLOG.AVATAR // 用户头像压缩一下 icon = compressImage(icon) @@ -236,7 +256,13 @@ function getSiteInfo({ collection, block }) { */ export function getNavPages({ allPages }) { const allNavPages = allPages?.filter(post => { - return post && post?.slug && (!post?.slug?.startsWith('http')) && post?.type === 'Post' && post?.status === 'Published' + return ( + post && + post?.slug && + !post?.slug?.startsWith('http') && + post?.type === 'Post' && + post?.status === 'Published' + ) }) return allNavPages.map(item => ({ @@ -248,7 +274,8 @@ export function getNavPages({ allPages }) { summary: item.summary || null, slug: item.slug, pageIcon: item.pageIcon || '', - lastEditedDate: item.lastEditedDate + lastEditedDate: item.lastEditedDate, + publishDate: item.publishDate })) } @@ -265,19 +292,26 @@ async function getNotice(post) { } // 没有数据时返回 -const EmptyData = (pageId) => { +const EmptyData = pageId => { const empty = { notice: null, siteInfo: getSiteInfo({}), - allPages: [{ - id: 1, - title: `无法获取Notion数据,请检查Notion_ID: \n 当前 ${pageId}`, - summary: '访问文档获取帮助→ https://tangly1024.com/article/vercel-deploy-notion-next', - status: 'Published', - type: 'Post', - slug: '13a171332816461db29d50e9f575b00d', - date: { start_date: '2023-04-24', lastEditedDay: '2023-04-24', tagItems: [] } - }], + allPages: [ + { + id: 1, + title: `无法获取Notion数据,请检查Notion_ID: \n 当前 ${pageId}`, + summary: + '访问文档获取帮助→ https://tangly1024.com/article/vercel-deploy-notion-next', + status: 'Published', + type: 'Post', + slug: '13a171332816461db29d50e9f575b00d', + date: { + start_date: '2023-04-24', + lastEditedDay: '2023-04-24', + tagItems: [] + } + } + ], allNavPages: [], collection: [], collectionQuery: {}, @@ -313,7 +347,8 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { const rawMetadata = block[pageId]?.value // Check Type Page-Database和Inline-Database if ( - rawMetadata?.type !== 'collection_view_page' && rawMetadata?.type !== 'collection_view' + rawMetadata?.type !== 'collection_view_page' && + rawMetadata?.type !== 'collection_view' ) { console.error(`pageId "${pageId}" is not a database`) return EmptyData(pageId) @@ -328,9 +363,21 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { const viewIds = rawMetadata?.view_ids const collectionData = [] - const pageIds = getAllPageIds(collectionQuery, collectionId, collectionView, viewIds) + const pageIds = getAllPageIds( + collectionQuery, + collectionId, + collectionView, + viewIds + ) if (pageIds?.length === 0) { - console.error('获取到的文章列表为空,请检查notion模板', collectionQuery, collection, collectionView, viewIds, pageRecordMap) + console.error( + '获取到的文章列表为空,请检查notion模板', + collectionQuery, + collection, + collectionView, + viewIds, + pageRecordMap + ) } else { // console.log('有效Page数量', pageIds?.length) } @@ -343,7 +390,14 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { // 如果找不到文章对应的block,说明发生了溢出,使用pageID再去请求 const pageBlock = await getSingleBlock(id, from) if (pageBlock.block[id].value) { - const properties = (await getPageProperties(id, pageBlock.block[id].value, schema, null, getTagOptions(schema))) || null + const properties = + (await getPageProperties( + id, + pageBlock.block[id].value, + schema, + null, + getTagOptions(schema) + )) || null if (properties) { collectionData.push(properties) } @@ -351,7 +405,14 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { continue } - const properties = (await getPageProperties(id, value, schema, null, getTagOptions(schema))) || null + const properties = + (await getPageProperties( + id, + value, + schema, + null, + getTagOptions(schema) + )) || null if (properties) { collectionData.push(properties) } @@ -365,13 +426,16 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { if (post?.type === 'Post' && post.status === 'Published') { postCount++ } - return post && post?.slug && - (!post?.slug?.startsWith('http')) && + return ( + post && + post?.slug && + !post?.slug?.startsWith('http') && (post?.status === 'Invisible' || post?.status === 'Published') + ) }) // 站点配置优先读取配置表格,否则读取blog.config.js 文件 - const NOTION_CONFIG = await getConfigMapFromConfigPage(collectionData) || {} + const NOTION_CONFIG = (await getConfigMapFromConfigPage(collectionData)) || {} // Sort by date if (BLOG.POSTS_SORT_BY === 'date') { @@ -380,13 +444,27 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { }) } - const notice = await getNotice(collectionData.filter(post => { - return post && post?.type && post?.type === 'Notice' && post.status === 'Published' - })?.[0]) - const categoryOptions = getAllCategories({ allPages, categoryOptions: getCategoryOptions(schema) }) + const notice = await getNotice( + collectionData.filter(post => { + return ( + post && + post?.type && + post?.type === 'Notice' && + post.status === 'Published' + ) + })?.[0] + ) + const categoryOptions = getAllCategories({ + allPages, + categoryOptions: getCategoryOptions(schema) + }) const tagOptions = getAllTags({ allPages, tagOptions: getTagOptions(schema) }) // 旧的菜单 - const customNav = getCustomNav({ allPages: collectionData.filter(post => post?.type === 'Page' && post.status === 'Published') }) + const customNav = getCustomNav({ + allPages: collectionData.filter( + post => post?.type === 'Page' && post.status === 'Published' + ) + }) // 新的菜单 const customMenu = await getCustomMenu({ collectionData }) const latestPosts = getLatestPosts({ allPages, from, latestPostCount: 6 }) diff --git a/package.json b/package.json index 0ccd17e1..13b60baf 100644 --- a/package.json +++ b/package.json @@ -57,9 +57,11 @@ "cross-env": "^7.0.3", "eslint": "^7.26.0", "eslint-config-next": "^13.1.1", + "eslint-config-prettier": "^9.1.0", "eslint-config-standard": "^16.0.2", "eslint-plugin-import": "^2.23.0", "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-promise": "^5.1.0", "eslint-plugin-react": "^7.23.2", "eslint-plugin-react-hooks": "^4.6.0", @@ -76,4 +78,4 @@ "url": "https://github.com/tangly/NotionNext/issues", "email": "tlyong1992@hotmail.com" } -} \ No newline at end of file +} diff --git a/public/videos/dplayer.html b/public/videos/dplayer.html new file mode 100644 index 00000000..66573c2b --- /dev/null +++ b/public/videos/dplayer.html @@ -0,0 +1,50 @@ + + + + + + + DPlayer Video Player + + + + + + + +
+ + + + + + + + + + \ No newline at end of file diff --git a/themes/heo/components/PaginationNumber.js b/themes/heo/components/PaginationNumber.js index 4a1022ee..54c1bf34 100644 --- a/themes/heo/components/PaginationNumber.js +++ b/themes/heo/components/PaginationNumber.js @@ -17,105 +17,126 @@ const PaginationNumber = ({ page, totalPage }) => { const currentPage = +page const showNext = page < totalPage const showPrev = currentPage !== 1 - const pagePrefix = router.asPath.split('?')[0].replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '') + const pagePrefix = router.asPath + .split('?')[0] + .replace(/\/page\/[1-9]\d*/, '') + .replace(/\/$/, '') const pages = generatePages(pagePrefix, page, currentPage, totalPage) const [value, setValue] = useState('') - const handleInputChange = (event) => { + const handleInputChange = event => { const newValue = event.target.value.replace(/[^0-9]/g, '') setValue(newValue) } /** - * 调到指定页 - */ + * 调到指定页 + */ const jumpToPage = () => { if (value) { - router.push(value === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${value}`) + router.push( + value === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${value}` + ) } } - return (<> - - {/* pc端分页按钮 */} -
- {/* 上一页 */} - -
- -
- {locale.PAGINATION.PREV} -
-
- - - - {/* 分页 */} -
- {pages} - - {/* 跳转页码 */} -
- -
- -
-
+ return ( + <> + {/* pc端分页按钮 */} +
+ {/* 上一页 */} + +
+ +
+ {locale.PAGINATION.PREV}
+
+ - {/* 下一页 */} - + {/* 分页 */} +
+ {pages} -
- -
- {locale.PAGINATION.NEXT} -
-
- + {/* 跳转页码 */} +
+ +
+ +
+
- {/* 移动端分页 */} + {/* 下一页 */} + +
+ +
+ {locale.PAGINATION.NEXT} +
+
+ +
-
- {/* 上一页 */} - - {locale.PAGINATION.PREV} - + {/* 移动端分页 */} - {showPrev && showNext &&
} +
+ {/* 上一页 */} + + {locale.PAGINATION.PREV} + - {/* 下一页 */} - - {locale.PAGINATION.NEXT} - -
- ) + {showPrev && showNext &&
} + + {/* 下一页 */} + + {locale.PAGINATION.NEXT} + +
+ + ) } /** @@ -127,21 +148,23 @@ const PaginationNumber = ({ page, totalPage }) => { */ function getPageElement(page, currentPage, pagePrefix) { const selected = page + '' === currentPage + '' + if (!page) { + return <> + } return ( - ( - - {page} - - ) + + {page} + ) } @@ -171,7 +194,11 @@ function generatePages(pagePrefix, page, currentPage, totalPage) { startPage = totalPage - dynamicGroupCount } if (startPage > 2) { - pages.push(
...
) + pages.push( +
+ ...{' '} +
+ ) } for (let i = 0; i < dynamicGroupCount; i++) { diff --git a/themes/hexo/components/ArticleRecommend.js b/themes/hexo/components/ArticleRecommend.js index a3a40b7d..0bbcd937 100644 --- a/themes/hexo/components/ArticleRecommend.js +++ b/themes/hexo/components/ArticleRecommend.js @@ -1,9 +1,9 @@ -import Link from 'next/link' -import CONFIG from '../config' +import LazyImage from '@/components/LazyImage' import { siteConfig } from '@/lib/config' import { useGlobal } from '@/lib/global' -import LazyImage from '@/components/LazyImage' import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils' +import Link from 'next/link' +import CONFIG from '../config' /** * 关联推荐文章 @@ -13,50 +13,52 @@ import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils' export default function ArticleRecommend({ recommendPosts, siteInfo }) { const { locale } = useGlobal() - if ( - !siteConfig('HEXO_ARTICLE_RECOMMEND', null, CONFIG) || - !recommendPosts || - recommendPosts.length === 0 - ) { + if (!siteConfig('HEXO_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 - const url = checkContainHttp(post.slug) ? sliceUrlFromHttp(post.slug) : `${siteConfig('SUB_PATH', '')}/${post.slug}` - - return ( - ( - -
-
-
- {post.title} -
-
- -
- - ) - ) - })} -
+
+
+
+ + {locale.COMMON.RELATE_POSTS}
+
+
+ {recommendPosts.map(post => { + const headerImage = post?.pageCoverThumbnail ? post.pageCoverThumbnail : siteInfo?.pageCover + const url = checkContainHttp(post.slug) + ? sliceUrlFromHttp(post.slug) + : `${siteConfig('SUB_PATH', '')}/${post.slug}` + + return ( + +
+
+
+ {post.title} +
+
+ + + {/* 卡片的阴影遮罩,为了凸显图片上的文字 */} +
+
+
+
+ + ) + })} +
+
) } diff --git a/themes/movie/components/Announcement.js b/themes/movie/components/Announcement.js new file mode 100644 index 00000000..1f265417 --- /dev/null +++ b/themes/movie/components/Announcement.js @@ -0,0 +1,22 @@ +import { useGlobal } from '@/lib/global' +import dynamic from 'next/dynamic' + +const NotionPage = dynamic(() => import('@/components/NotionPage')) + +const Announcement = ({ post, className }) => { + const { locale } = useGlobal() + if (!post || Object.keys(post).length === 0) { + return <> + } + return +} +export default Announcement diff --git a/themes/movie/components/ArchiveDateList.js b/themes/movie/components/ArchiveDateList.js new file mode 100644 index 00000000..cd389028 --- /dev/null +++ b/themes/movie/components/ArchiveDateList.js @@ -0,0 +1,38 @@ +import { useGlobal } from '@/lib/global' +import { formatDateFmt } from '@/lib/utils/formatDate' +import Link from 'next/link' + +export default function ArchiveDateList(props) { + const postsSortByDate = Object.create(props.allNavPages) + const { locale } = useGlobal() + + postsSortByDate.sort((a, b) => { + return b?.publishDate - a?.publishDate + }) + + let dates = [] + postsSortByDate.forEach(post => { + const date = formatDateFmt(post.publishDate, 'yyyy-MM') + if (!dates[date]) { + dates.push(date) + } + }) + dates = dates.slice(0, 5) + return ( +
+
{locale.NAV.ARCHIVE}
+ {dates?.map((date, index) => { + return ( +
+ + {date} + +
+ ) + })} +
+ ) +} diff --git a/themes/movie/components/ArticleInfo.js b/themes/movie/components/ArticleInfo.js new file mode 100644 index 00000000..9997579a --- /dev/null +++ b/themes/movie/components/ArticleInfo.js @@ -0,0 +1,46 @@ +import { useGlobal } from '@/lib/global' +import { formatDateFmt } from '@/lib/utils/formatDate' +import Link from 'next/link' + +export const ArticleInfo = props => { + const { post } = props + const { locale } = useGlobal() + + return ( +
+

{post?.title}

+ +
+ + {post?.publishDay} + + + {post?.type !== 'Page' && ( + <> + + {post?.category} + + + )} + +
+ {post?.tags?.length > 0 && ( + <> + {locale.COMMON.TAGS} : + + )} + {post?.tags?.map(tag => { + return ( + + {tag} + + ) + })} +
+
+
+ ) +} diff --git a/themes/movie/components/ArticleLock.js b/themes/movie/components/ArticleLock.js new file mode 100644 index 00000000..3744c183 --- /dev/null +++ b/themes/movie/components/ArticleLock.js @@ -0,0 +1,52 @@ +import { useGlobal } from '@/lib/global' +import { useEffect, useRef } from 'react' + +/** + * 加密文章校验组件 + * @param {password, validPassword} props + * @param password 正确的密码 + * @param validPassword(bool) 回调函数,校验正确回调入参为true + * @returns + */ +export const ArticleLock = props => { + const { validPassword } = props + const { locale } = useGlobal() + + const submitPassword = () => { + const p = document.getElementById('password') + if (!validPassword(p?.value)) { + const tips = document.getElementById('tips') + if (tips) { + tips.innerHTML = '' + tips.innerHTML = `
${locale.COMMON.PASSWORD_ERROR}
` + } + } + } + const passwordInputRef = useRef(null) + useEffect(() => { + // 选中密码输入框并将其聚焦 + passwordInputRef.current.focus() + }, []) + + return
+
+
{locale.COMMON.ARTICLE_LOCK_TIPS}
+
+ { + if (e.key === 'Enter') { + submitPassword() + } + }} + ref={passwordInputRef} // 绑定ref到passwordInputRef变量 + className='outline-none w-full text-sm pl-5 rounded-l transition focus:shadow-lg font-light leading-10 text-black dark:bg-gray-500 bg-gray-50' + > +
+  {locale.COMMON.SUBMIT} +
+
+
+
+
+
+} diff --git a/themes/movie/components/BlogListGroupByDate.js b/themes/movie/components/BlogListGroupByDate.js new file mode 100644 index 00000000..7a8fe083 --- /dev/null +++ b/themes/movie/components/BlogListGroupByDate.js @@ -0,0 +1,38 @@ +import { siteConfig } from '@/lib/config' +import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils' +import Link from 'next/link' + +/** + * 按照日期将文章分组 + * 归档页面用到 + * @param {*} param0 + * @returns + */ +export default function BlogListGroupByDate({ archiveTitle, archivePosts }) { + return
+
+ {archiveTitle} +
+ +
    + {archivePosts[archiveTitle].map(post => { + const url = checkContainHttp(post.slug) ? sliceUrlFromHttp(post.slug) : `${siteConfig('SUB_PATH', '')}/${post.slug}` + + return
  • +
    + + {post?.publishDay} + {' '} +   + + {post.title} + +
    +
  • + })} +
+
+} diff --git a/themes/movie/components/BlogListPage.js b/themes/movie/components/BlogListPage.js new file mode 100644 index 00000000..648f2ee2 --- /dev/null +++ b/themes/movie/components/BlogListPage.js @@ -0,0 +1,25 @@ +import { siteConfig } from '@/lib/config' +import CONFIG from '../config' +import BlogPostCard from './BlogPostCard' +import PaginationNumber from './PaginationNumber' + +export const BlogListPage = props => { + const { page = 1, posts, postCount } = props + const totalPage = Math.ceil(postCount / parseInt(siteConfig('POSTS_PER_PAGE'))) + + const showPageCover = siteConfig('MOVIE_POST_LIST_COVER', null, CONFIG) + + return ( +
+
+ {posts?.map(post => ( + + ))} +
+ + +
+ ) +} diff --git a/themes/movie/components/BlogListScroll.js b/themes/movie/components/BlogListScroll.js new file mode 100644 index 00000000..597ba7dc --- /dev/null +++ b/themes/movie/components/BlogListScroll.js @@ -0,0 +1,60 @@ +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')) * page) : [] + + if (posts) { + const totalCount = posts.length + hasMore = page * parseInt(siteConfig('POSTS_PER_PAGE')) < totalCount + } + const handleGetMore = () => { + if (!hasMore) return + updatePage(page + 1) + } + + const targetRef = useRef(null) + + // 监听滚动自动分页加载 + const scrollTrigger = useCallback( + throttle(() => { + const scrollS = window.scrollY + window.outerHeight + const clientHeight = targetRef ? (targetRef.current ? targetRef.current.clientHeight : 0) : 0 + if (scrollS > clientHeight + 100) { + handleGetMore() + } + }, 500) + ) + const showPageCover = siteConfig('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/movie/components/BlogPostCard.js b/themes/movie/components/BlogPostCard.js new file mode 100644 index 00000000..61954610 --- /dev/null +++ b/themes/movie/components/BlogPostCard.js @@ -0,0 +1,60 @@ +import LazyImage from '@/components/LazyImage' +import NotionIcon from '@/components/NotionIcon' +import { siteConfig } from '@/lib/config' +import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils' +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') + } + + const url = checkContainHttp(post.slug) ? sliceUrlFromHttp(post.slug) : `${siteConfig('SUB_PATH', '')}/${post.slug}` + + return ( +
+ + {/* 固定高度 ,空白用图片拉升填充 */} +
+ {/* 图片 填充卡片 */} +
+ +
+ +
+ {post?.tagItems && post?.tagItems.length > 0 && ( + <> +
+ {post.tagItems.map(tag => ( + + ))} +
+ + )} +
+ {/* 阴影遮罩 */} +

+ + {post.title} +

+ +

+ {post?.summary} +

+ +
+
+
+
+ +
+ ) +} + +export default BlogPostCard diff --git a/themes/movie/components/BlogRecommend.js b/themes/movie/components/BlogRecommend.js new file mode 100644 index 00000000..9e434558 --- /dev/null +++ b/themes/movie/components/BlogRecommend.js @@ -0,0 +1,62 @@ +import LazyImage from '@/components/LazyImage' +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils' +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 + const url = checkContainHttp(post.slug) + ? sliceUrlFromHttp(post.slug) + : `${siteConfig('SUB_PATH', '')}/${post.slug}` + + return ( + +
+
+
{post.title}
+
+ {/* 卡片的阴影遮罩,为了凸显图片上的文字 */} +
+
+
+ + +
+ + ) + })} +
+
+ ) +} diff --git a/themes/movie/components/CategoryGroup.js b/themes/movie/components/CategoryGroup.js new file mode 100644 index 00000000..63c3917c --- /dev/null +++ b/themes/movie/components/CategoryGroup.js @@ -0,0 +1,43 @@ +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' + +const CategoryGroup = props => { + const { currentCategory, categoryOptions } = props + const { locale } = useGlobal() + if (!categoryOptions || categoryOptions.length === 0) return <> + const categoryCount = siteConfig('PREVIEW_CATEGORY_COUNT') + const categories = categoryOptions.slice(0, categoryCount) + return ( + <> +
+

{locale.COMMON.CATEGORY}

+
+ {categories.map(category => { + const selected = currentCategory === category.name + return ( + + + {category.name}({category.count}) + + ) + })} +
+
+ + ) +} + +export default CategoryGroup diff --git a/themes/movie/components/CategoryItem.js b/themes/movie/components/CategoryItem.js new file mode 100644 index 00000000..1190965b --- /dev/null +++ b/themes/movie/components/CategoryItem.js @@ -0,0 +1,20 @@ +import Link from 'next/link' + +/** + * 文章分类 + * @param {*} param0 + * @returns + */ +export default function CategoryItem({ category }) { + return ( + +
+ {category.name}({category.count}) +
+ + ) +} diff --git a/themes/movie/components/ExampleRecentComments.js b/themes/movie/components/ExampleRecentComments.js new file mode 100644 index 00000000..9dbdfa7f --- /dev/null +++ b/themes/movie/components/ExampleRecentComments.js @@ -0,0 +1,35 @@ +import { useEffect, useState } from 'react' +import { siteConfig } from '@/lib/config' +import Link from 'next/link' +import { RecentComments } from '@waline/client' + +/** + * @see https://waline.js.org/guide/get-started.html + * @param {*} props + * @returns + */ +const ExampleRecentComments = (props) => { + const [comments, updateComments] = useState([]) + const [onLoading, changeLoading] = useState(true) + useEffect(() => { + RecentComments({ + serverURL: siteConfig('COMMENT_WALINE_SERVER_URL'), + count: 5 + }).then(({ comments }) => { + changeLoading(false) + updateComments(comments) + }) + }, []) + + return <> + {onLoading &&
Loading...
} + {!onLoading && comments && comments.length === 0 &&
No Comments
} + {!onLoading && comments && comments.length > 0 && comments.map((comment) =>
+
+
--{comment.nick}
+
)} + + +} + +export default ExampleRecentComments diff --git a/themes/movie/components/Footer.js b/themes/movie/components/Footer.js new file mode 100644 index 00000000..77c8bad6 --- /dev/null +++ b/themes/movie/components/Footer.js @@ -0,0 +1,26 @@ +import DarkModeButton from '@/components/DarkModeButton' +import { siteConfig } from '@/lib/config' + +export const Footer = (props) => { + const d = new Date() + const currentYear = d.getFullYear() + const since = siteConfig('SINCE') + const copyrightDate = parseInt(since) < currentYear ? since + '-' + currentYear : currentYear + + return +} diff --git a/themes/movie/components/Header.js b/themes/movie/components/Header.js new file mode 100644 index 00000000..a0f2d78c --- /dev/null +++ b/themes/movie/components/Header.js @@ -0,0 +1,166 @@ +import Collapse from '@/components/Collapse' +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' +import { useMovieGlobal } from '..' +import CONFIG from '../config' +import { MenuItemCollapse } from './MenuItemCollapse' +import { MenuItemDrop } from './MenuItemDrop' + +/** + * 网站顶部 + * @returns + */ +export const Header = props => { + const { collapseRef, searchModal } = useMovieGlobal() + const router = useRouter() + const { customNav, customMenu } = props + const { locale } = useGlobal() + const [isOpen, setIsOpen] = useState(false) + const [showSearch, setShowSearch] = useState(false) + const toggleMenuOpen = () => { + setIsOpen(!isOpen) + } + let links = [ + { + id: 1, + icon: 'fa-solid fa-house', + name: locale.NAV.INDEX, + to: '/', + show: siteConfig('MOVIE_MENU_INDEX', null, CONFIG) + }, + { + id: 2, + icon: 'fas fa-search', + name: locale.NAV.SEARCH, + to: '/search', + show: siteConfig('MOVIE_MENU_SEARCH', null, CONFIG) + }, + { + id: 3, + icon: 'fas fa-archive', + name: locale.NAV.ARCHIVE, + to: '/archive', + show: siteConfig('MOVIE_MENU_ARCHIVE', null, CONFIG) + } + // { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: siteConfig('MENU_CATEGORY', null, CONFIG) }, + // { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: siteConfig('MENU_TAG', null, CONFIG) } + ] + + if (customNav) { + links = links.concat(customNav) + } + + for (let i = 0; i < links.length; i++) { + if (links[i].id !== i) { + links[i].id = i + } + } + + // 如果 开启自定义菜单,则覆盖Page生成的菜单 + if (siteConfig('CUSTOM_MENU')) { + links = customMenu + } + + // 展示搜索框 + const toggleShowSearchInput = () => { + if (siteConfig('ALGOLIA_APP_ID')) { + searchModal.current.openSearch() + } else { + setShowSearch(!showSearch) + } + } + + useEffect(() => { + if (showSearch) { + setTimeout(() => { + document.getElementById('search').focus() + }, 100) + } + }, [showSearch]) + + const onKeyUp = e => { + if (e.keyCode === 13) { + const search = document.getElementById('search').value + if (search) { + router.push({ pathname: '/search/' + search }) + } + } + } + + const handleSearch = () => { + const search = document.getElementById('search').value + if (search) { + router.push({ pathname: '/search/' + search }) + } + } + + return ( + <> +
+ + {siteConfig('TITLE')} + + +
+ {/* 右侧菜单 */} + <> + + +
+ +
+
+ + +
+ + {/* 移动端按钮 */} +
+
+ {isOpen ? : } +
+
+ +
+
+ + + {/* 移动端菜单 */} + + {links?.map( + (link, index) => + link && + link.show && ( + collapseRef.current?.updateCollapseHeight(param)} + key={index} + link={link} + /> + ) + )} + + + + ) +} diff --git a/themes/movie/components/JumpToTopButton.js b/themes/movie/components/JumpToTopButton.js new file mode 100644 index 00000000..f5e22b61 --- /dev/null +++ b/themes/movie/components/JumpToTopButton.js @@ -0,0 +1,18 @@ +import { useGlobal } from '@/lib/global' + +/** + * 跳转到网页顶部 + * 当屏幕下滑500像素后会出现该控件 + * @param targetRef 关联高度的目标html标签 + * @param showPercent 是否显示百分比 + * @returns {JSX.Element} + * @constructor + */ +const JumpToTopButton = () => { + const { locale } = useGlobal() + return
window.scrollTo({ top: 0, behavior: 'smooth' })} + > +
+} + +export default JumpToTopButton diff --git a/themes/movie/components/LatestPostsGroup.js b/themes/movie/components/LatestPostsGroup.js new file mode 100644 index 00000000..b7ada599 --- /dev/null +++ b/themes/movie/components/LatestPostsGroup.js @@ -0,0 +1,62 @@ +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils' + +/** + * 最新文章列表 + * @param posts 所有文章数据 + * @param sliceCount 截取展示的数量 默认6 + * @constructor + */ +const LatestPostsGroup = ({ latestPosts }) => { + // 获取当前路径 + const currentPath = useRouter().asPath + const { locale } = useGlobal() + + if (!latestPosts) { + return <> + } + + return ( +
+
+
+ + {locale.COMMON.LATEST_POSTS} +
+
+ + {latestPosts.map(post => { + const selected = + currentPath === `${siteConfig('SUB_PATH', '')}/${post.slug}` + const url = checkContainHttp(post.slug) + ? sliceUrlFromHttp(post.slug) + : `${siteConfig('SUB_PATH', '')}/${post.slug}` + return ( + +
+
  • {post.title}
  • +
    + + ) + })} +
    + ) +} +export default LatestPostsGroup diff --git a/themes/movie/components/LoadingCover.js b/themes/movie/components/LoadingCover.js new file mode 100644 index 00000000..75976180 --- /dev/null +++ b/themes/movie/components/LoadingCover.js @@ -0,0 +1,8 @@ + +export default function LoadingCover() { + return
    +
    + +
    +
    +} diff --git a/themes/movie/components/MenuItemCollapse.js b/themes/movie/components/MenuItemCollapse.js new file mode 100644 index 00000000..0f91f1d4 --- /dev/null +++ b/themes/movie/components/MenuItemCollapse.js @@ -0,0 +1,77 @@ +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/movie/components/MenuItemDrop.js b/themes/movie/components/MenuItemDrop.js new file mode 100644 index 00000000..93f27f44 --- /dev/null +++ b/themes/movie/components/MenuItemDrop.js @@ -0,0 +1,67 @@ +import Link from 'next/link' +import { useState } from 'react' + +export const MenuItemDrop = ({ link }) => { + const [show, changeShow] = useState(false) + const hasSubMenu = link?.subMenus?.length > 0 + + if (!link || !link.show) { + return null + } + + return ( +
    changeShow(true)} + onMouseOut={() => changeShow(false)} + > + {!hasSubMenu && ( + + {link?.icon && } {link?.name} + {hasSubMenu && } + + )} + + {hasSubMenu && ( + <> +
    + {link?.icon && } {link?.name} + +
    + + )} + + {/* 子菜单 */} + {hasSubMenu && ( +
      + {link.subMenus.map((sLink, index) => { + return ( +
    • + + + {link?.icon &&   } + {sLink.title} + + +
    • + ) + })} +
    + )} +
    + ) +} diff --git a/themes/movie/components/NormalMenuItem.js b/themes/movie/components/NormalMenuItem.js new file mode 100644 index 00000000..aec74837 --- /dev/null +++ b/themes/movie/components/NormalMenuItem.js @@ -0,0 +1,14 @@ +import Link from 'next/link' + +/** + * 旧的普通菜单 + * @param {*} props + * @returns + */ +export const NormalMenuItem = (props) => { + const { link } = props + return link?.show && + {link.name} + +} diff --git a/themes/movie/components/PaginationNumber.js b/themes/movie/components/PaginationNumber.js new file mode 100644 index 00000000..effc1835 --- /dev/null +++ b/themes/movie/components/PaginationNumber.js @@ -0,0 +1,220 @@ +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(/\/$/, '') + 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/movie/components/SearchInput.js b/themes/movie/components/SearchInput.js new file mode 100644 index 00000000..4f375d4e --- /dev/null +++ b/themes/movie/components/SearchInput.js @@ -0,0 +1,87 @@ +import { useRouter } from 'next/router' +import { useGlobal } from '@/lib/global' +import { useImperativeHandle, useRef, useState } from 'react' + +let lock = false + +const SearchInput = ({ currentTag, keyword, cRef }) => { + const { locale } = useGlobal() + const router = useRouter() + const searchInputRef = useRef(null) + useImperativeHandle(cRef, () => { + return { + focus: () => { + searchInputRef?.current?.focus() + } + } + }) + const handleSearch = () => { + const key = searchInputRef.current.value + if (key && key !== '') { + router.push({ pathname: '/search/' + key }).then(r => { + console.log('搜索', key) + }) + } else { + router.push({ pathname: '/' }).then(r => { + }) + } + } + const handleKeyUp = (e) => { + if (e.keyCode === 13) { // 回车 + handleSearch(searchInputRef.current.value) + } else if (e.keyCode === 27) { // ESC + cleanSearch() + } + } + const cleanSearch = () => { + searchInputRef.current.value = '' + setShowClean(false) + } + function lockSearchInput () { + lock = true + } + + function unLockSearchInput () { + lock = false + } + const [showClean, setShowClean] = useState(false) + const updateSearchKey = (val) => { + if (lock) { + return + } + searchInputRef.current.value = val + if (val) { + setShowClean(true) + } else { + setShowClean(false) + } + } + + return
    + updateSearchKey(e.target.value)} + defaultValue={keyword || ''} + /> + +
    + +
    + + {(showClean && +
    + +
    + )} +
    +} + +export default SearchInput diff --git a/themes/movie/components/SideBar.js b/themes/movie/components/SideBar.js new file mode 100644 index 00000000..ea0531b7 --- /dev/null +++ b/themes/movie/components/SideBar.js @@ -0,0 +1,68 @@ +import { siteConfig } from '@/lib/config' +import Live2D from '@/components/Live2D' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import dynamic from 'next/dynamic' +import Announcement from './Announcement' +const ExampleRecentComments = dynamic(() => import('./ExampleRecentComments')) + +export const SideBar = (props) => { + const { locale } = useGlobal() + const { latestPosts, categoryOptions, notice } = props + return ( +
    + + + + + + + + {siteConfig('COMMENT_WALINE_SERVER_URL') && siteConfig('COMMENT_WALINE_RECENT') && } + + + +
    + ) +} diff --git a/themes/movie/components/SlotBar.js b/themes/movie/components/SlotBar.js new file mode 100644 index 00000000..e3bed727 --- /dev/null +++ b/themes/movie/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/movie/components/TagGroups.js b/themes/movie/components/TagGroups.js new file mode 100644 index 00000000..597cddc1 --- /dev/null +++ b/themes/movie/components/TagGroups.js @@ -0,0 +1,51 @@ +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import { useRouter } from 'next/router' + +/** + * 标签组 + * @param tags + * @param currentTag + * @returns {JSX.Element} + * @constructor + */ +const TagGroups = ({ tagOptions, className }) => { + const router = useRouter() + const { locale } = useGlobal() + const { tag: currentTag } = router.query + if (!tagOptions) return <> + + return ( +
    +
    {locale.COMMON.TAGS}
    +
    + {tagOptions.map((tag, index) => { + const selected = currentTag === tag.name + return ( + +
    +
    {tag.name}
    + {tag.count ? ( + {tag.count} + ) : ( + <> + )} +
    + + ) + })} +
    +
    + ) +} + +export default TagGroups diff --git a/themes/movie/components/TagItem.js b/themes/movie/components/TagItem.js new file mode 100644 index 00000000..c608c8cb --- /dev/null +++ b/themes/movie/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/movie/components/TagItemMini.js b/themes/movie/components/TagItemMini.js new file mode 100644 index 00000000..a4b7da1c --- /dev/null +++ b/themes/movie/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/movie/components/Title.js b/themes/movie/components/Title.js new file mode 100644 index 00000000..73ca9d7f --- /dev/null +++ b/themes/movie/components/Title.js @@ -0,0 +1,20 @@ +import NotionIcon from '@/components/NotionIcon' +import { siteConfig } from '@/lib/config' + +/** + * 标题栏 + * @param {*} props + * @returns + */ +export const Title = (props) => { + const { post } = props + const title = post?.title || siteConfig('TITLE') + const description = post?.description || siteConfig('AUTHOR') + + return
    +

    {title}

    +

    + {description} +

    +
    +} diff --git a/themes/movie/config.js b/themes/movie/config.js new file mode 100644 index 00000000..a2c5fd4d --- /dev/null +++ b/themes/movie/config.js @@ -0,0 +1,16 @@ +/** + * 主题配置文件 + */ +const CONFIG = { + // 菜单配置 + MOVIE_MENU_CATEGORY: true, // 显示分类 + MOVIE_MENU_TAG: true, // 显示标签 + MOVIE_MENU_ARCHIVE: true, // 显示归档 + MOVIE_MENU_SEARCH: true, // 显示搜索 + + MOVIE_ARTICLE_RECOMMEND: true, // 推荐关联内容在文章底部 + MOVIE_VIDEO_COMBINE: true, // 聚合视频,开启后一篇文章内的多个含caption的视频会被合并到文章开头,并展示分集按钮 + + MOVIE_POST_LIST_COVER: true // 列表显示文章封面 +} +export default CONFIG diff --git a/themes/movie/index.js b/themes/movie/index.js new file mode 100644 index 00000000..5b7f9184 --- /dev/null +++ b/themes/movie/index.js @@ -0,0 +1,438 @@ +'use client' + +import AlgoliaSearchModal from '@/components/AlgoliaSearchModal' +import Comment from '@/components/Comment' +import replaceSearchResult from '@/components/Mark' +import NotionPage from '@/components/NotionPage' +import ShareBar from '@/components/ShareBar' +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import { loadWowJS } from '@/lib/plugins/wow' +import { isBrowser } from '@/lib/utils' +import { Transition } from '@headlessui/react' +import { useRouter } from 'next/router' +import { createContext, useContext, useEffect, useRef, useState } from 'react' +import ArchiveDateList from './components/ArchiveDateList' +import { ArticleInfo } from './components/ArticleInfo' +import { ArticleLock } from './components/ArticleLock' +import BlogListGroupByDate from './components/BlogListGroupByDate' +import { BlogListPage } from './components/BlogListPage' +import { BlogListScroll } from './components/BlogListScroll' +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 JumpToTopButton from './components/JumpToTopButton' +import LatestPostsGroup from './components/LatestPostsGroup' +import SlotBar from './components/SlotBar' +import TagGroups from './components/TagGroups' +import TagItem from './components/TagItem' +import CONFIG from './config' +import { Style } from './style' + +// 主题全局状态 +const ThemeGlobalMovie = createContext() +export const useMovieGlobal = () => useContext(ThemeGlobalMovie) + +/** + * 基础布局框架 + * 1.其它页面都嵌入在LayoutBase中 + * 2.采用左右两侧布局,移动端使用顶部导航栏 + * @returns {JSX.Element} + * @constructor + */ +const LayoutBase = props => { + const { children, slotTop } = props + const { onLoading, fullWidth } = useGlobal() + const collapseRef = useRef(null) + + const searchModal = useRef(null) + const [expandMenu, updateExpandMenu] = useState(false) + useEffect(() => { + loadWowJS() + }, []) + + return ( + +
    + + ) +} + +export { Style } diff --git a/themes/simple/components/NavBar.js b/themes/simple/components/NavBar.js index 38df0736..0392c051 100644 --- a/themes/simple/components/NavBar.js +++ b/themes/simple/components/NavBar.js @@ -9,7 +9,7 @@ import { MenuList } from './MenuList' * @param {*} props * @returns */ -export default function NavBar (props) { +export default function NavBar(props) { const [showSearchInput, changeShowSearchInput] = useState(false) const router = useRouter() const { searchModal } = useSimpleGlobal() @@ -23,7 +23,7 @@ export default function NavBar (props) { } } - const onKeyUp = (e) => { + const onKeyUp = e => { if (e.keyCode === 13) { const search = document.getElementById('theme-simple-search').value if (search) { @@ -33,19 +33,41 @@ export default function NavBar (props) { } return ( - ) } diff --git a/yarn.lock b/yarn.lock index 7935bfbc..8e4cfd52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -869,6 +869,11 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@pkgr/core@^0.1.0": + version "0.1.1" + resolved "https://r.cnpmjs.org/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" + integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== + "@polka/url@^1.0.0-next.20", "@polka/url@^1.0.0-next.24": version "1.0.0-next.24" resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.24.tgz#58601079e11784d20f82d0585865bb42305c4df3" @@ -2415,6 +2420,11 @@ eslint-config-next@^13.1.1: eslint-plugin-react "^7.33.2" eslint-plugin-react-hooks "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" +eslint-config-prettier@^9.1.0: + version "9.1.0" + resolved "https://r.cnpmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f" + integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== + eslint-config-standard@^16.0.2: version "16.0.3" resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz#6c8761e544e96c531ff92642eeb87842b8488516" @@ -2514,6 +2524,14 @@ eslint-plugin-node@^11.1.0: resolve "^1.10.1" semver "^6.1.0" +eslint-plugin-prettier@^5.1.3: + version "5.1.3" + resolved "https://r.cnpmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz#17cfade9e732cef32b5f5be53bd4e07afd8e67e1" + integrity sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw== + dependencies: + prettier-linter-helpers "^1.0.0" + synckit "^0.8.6" + eslint-plugin-promise@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-5.2.0.tgz#a596acc32981627eb36d9d75f9666ac1a4564971" @@ -2685,6 +2703,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-diff@^1.1.2: + version "1.3.0" + resolved "https://r.cnpmjs.org/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.1: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" @@ -4133,6 +4156,13 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://r2.cnpmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + prettier@3.2.5: version "3.2.5" resolved "https://r.cnpmjs.org/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" @@ -4831,6 +4861,14 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +synckit@^0.8.6: + version "0.8.8" + resolved "https://r.cnpmjs.org/synckit/-/synckit-0.8.8.tgz#fe7fe446518e3d3d49f5e429f443cf08b6edfcd7" + integrity sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ== + dependencies: + "@pkgr/core" "^0.1.0" + tslib "^2.6.2" + table@^6.0.9: version "6.8.1" resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" @@ -4968,7 +5006,7 @@ tslib@^1.11.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.5.0: +tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.5.0, tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==