Files
NotionNext/themes/photo/index.js
anime d500cff483 fix(修复错误调用React HOOK的问题):
(cherry picked from commit 03408d4d65a52a1388d9fc251c45a624ff056766)
2024-12-04 02:58:54 +08:00

498 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import AlgoliaSearchModal from '@/components/AlgoliaSearchModal'
import Comment from '@/components/Comment'
import replaceSearchResult from '@/components/Mark'
import NotionPage from '@/components/NotionPage'
import ShareBar from '@/components/ShareBar'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import { loadWowJS } from '@/lib/plugins/wow'
import { isBrowser } from '@/lib/utils'
import { Transition } from '@headlessui/react'
import { useRouter } from 'next/router'
import { createContext, useContext, useEffect, useRef, useState } from 'react'
import Announcement from './components/Announcement'
import ArchiveDateList from './components/ArchiveDateList'
import ArticleFooter from './components/ArticleFooter'
import { ArticleHeader } from './components/ArticleInfo'
import { ArticleLock } from './components/ArticleLock'
import BlogListGroupByDate from './components/BlogListGroupByDate'
import BlogRecommend from './components/BlogRecommend'
import CategoryGroup from './components/CategoryGroup'
import CategoryItem from './components/CategoryItem'
import { Footer } from './components/Footer'
import { Header } from './components/Header'
import { HomeBackgroundImage } from './components/HomeBackgroundImage'
import JumpToTopButton from './components/JumpToTopButton'
import LatestPostsGroup from './components/LatestPostsGroup'
import SlotBar from './components/SlotBar'
import Swiper from './components/Swiper'
import TagGroups from './components/TagGroups'
import TagItem from './components/TagItem'
import CONFIG from './config'
import { Style } from './style'
// 主题全局状态
const ThemeGlobalPhoto = createContext()
export const usePhotoGlobal = () => useContext(ThemeGlobalPhoto)
/**
* 基础布局框架
* 1.其它页面都嵌入在LayoutBase中
* 2.采用左右两侧布局,移动端使用顶部导航栏
* @returns {JSX.Element}
* @constructor
*/
const LayoutBase = props => {
const { children, slotTop } = props
const { onLoading, fullWidth } = useGlobal()
const collapseRef = useRef(null)
const router = useRouter()
const searchModal = useRef(null)
const [expandMenu, updateExpandMenu] = useState(false)
useEffect(() => {
loadWowJS()
}, [])
// 首页背景图
const headerSlot =
router.route === '/' &&
siteConfig('MOVIE_HOME_BACKGROUND', null, CONFIG) ? (
<HomeBackgroundImage />
) : null
return (
<ThemeGlobalPhoto.Provider
value={{ searchModal, expandMenu, updateExpandMenu, collapseRef }}>
<div
id='theme-photo'
className={`${siteConfig('FONT_STYLE')} dark:text-gray-300 duration-300 transition-all bg-white dark:bg-[#2A2A2A] scroll-smooth min-h-screen flex flex-col justify-between`}>
<Style />
{/* 页头 */}
<Header {...props} />
{headerSlot}
{/* 主体 */}
<div id='container-inner' className='w-full relative flex-grow z-10'>
<div
id='container-wrapper'
className={
(JSON.parse(siteConfig('LAYOUT_SIDEBAR_REVERSE'))
? 'flex-row-reverse'
: '') + 'relative mx-auto justify-center md:flex items-start'
}>
{/* 内容 */}
<div className={`w-full ${fullWidth ? '' : ''} px-0`}>
<Transition
show={!onLoading}
appear={true}
enter='transition ease-in-out duration-700 transform order-first'
enterFrom='opacity-0 translate-y-16'
enterTo='opacity-100'
leave='transition ease-in-out duration-300 transform'
leaveFrom='opacity-100 translate-y-0'
leaveTo='opacity-0 -translate-y-16'
unmount={false}>
{/* 嵌入模块 */}
{slotTop}
{children}
</Transition>
</div>
</div>
</div>
{/* 页脚 */}
<Footer {...props} />
{/* 搜索框 */}
<AlgoliaSearchModal cRef={searchModal} {...props} />
{/* 回顶按钮 */}
<div className='fixed right-4 bottom-4 z-10'>
<JumpToTopButton />
</div>
</div>
</ThemeGlobalPhoto.Provider>
)
}
/**
* 首页
* @param {*} props
* @returns 此主题首页就是列表
*/
const LayoutIndex = props => {
return <LayoutPostList {...props} />
}
/**
* 文章列表
* @param {*} props
* @returns
*/
const LayoutPostList = props => {
return (
<div className='mx-auto'>
<SlotBar {...props} />
{/* 滑动组件 */}
<Swiper {...props} />
{/* 公告 */}
<Announcement {...props} className='mx-auto w-full max-w-5xl my-12' />
</div>
)
}
/**
* 文章详情页
* @param {*} props
* @returns
*/
const LayoutSlug = props => {
const { post, lock, validPassword } = props
const router = useRouter()
const waiting404 = siteConfig('POST_WAITING_TIME_FOR_404') * 1000
useEffect(() => {
// 用js 实现将页面中的多个视频聚合为一个分集的视频
function combineVideo() {
// 找到 id 为 notion-article 的元素
const notionArticle = document.querySelector('#article-wrapper #notion-article')
if (!notionArticle) return // 如果找不到对应的元素,则退出函数
// 找到所有的 .notion-asset-wrapper 元素
const assetWrappers = document.querySelectorAll('.notion-asset-wrapper')
if (!assetWrappers || assetWrappers.length === 0) return // 如果找不到对应的元素,则退出函数
// 不要重复创建
const exists = document.querySelectorAll('.video-wrapper')
if (exists && exists.length > 0) return
// 创建视频区块容器元素
const videoWrapper = document.createElement('div')
videoWrapper.className =
'video-wrapper py-1 px-3 bg-gray-100 dark:bg-white dark:text-black mx-auto'
// 创建走马灯封装容器元素
const carouselWrapper = document.createElement('div')
carouselWrapper.classList.add('notion-carousel-wrapper')
// 创建分集按钮figcaption文本的数组
const figCaptionValues = []
// 遍历所有 .notion-asset-wrapper 元素
assetWrappers.forEach((wrapper, index) => {
// 检查 .notion-asset-wrapper 元素是否有子元素 figcaption
const figCaption = wrapper.querySelector('figcaption')
// 检查 .notion-asset-wrapper 元素是否有 notion-asset-wrapper-video 或 notion-asset-wrapper-embed 类
if (
!wrapper.classList.contains('notion-asset-wrapper-video') &&
!wrapper.classList.contains('notion-asset-wrapper-embed')
)
return
if (!figCaption) return // 如果没有子元素 figcaption则不处理该元素
// 获取 figcaption 的文本内容并添加到数组中
const figCaptionValue = figCaption
? figCaption?.textContent?.trim()
: `P-${index}`
figCaptionValues.push(figCaptionValue)
// 创建一个新的 div 元素用于包裹当前的 .notion-asset-wrapper 元素
const carouselItem = document.createElement('div')
carouselItem.classList.add('notion-carousel')
carouselItem.appendChild(wrapper)
// 如有外链、保存在data-src中
const iframe = wrapper.querySelector('iframe')
if (iframe) {
iframe?.setAttribute('data-src', iframe?.getAttribute('src'))
}
// 如果是第一个元素,设置为 active
if (index === 0) {
carouselItem.classList.add('active')
} else {
iframe?.setAttribute('src', '')
}
// 将元素添加到容器中
carouselWrapper.appendChild(carouselItem)
// 从 DOM 中移除原始的 .notion-asset-wrapper 元素
// wrapper.parentNode.removeChild(wrapper)
})
// 创建一个用于保存 figcaption 值的容器元素
const figCaptionWrapper = document.createElement('div')
figCaptionWrapper.className =
'notion-carousel-route py-2 max-h-36 overflow-y-auto'
// 遍历 figCaptionValues 数组,并将每个值添加到容器元素中
figCaptionValues.forEach(value => {
const div = document.createElement('div')
div.textContent = value
div.addEventListener('click', function () {
// 遍历所有的 carouselItem 元素
document.querySelectorAll('.notion-carousel').forEach(item => {
// 外链保存在data-src中
const iframe = item.querySelector('iframe')
// 判断当前元素是否包含该 figCaption 的文本内容,如果是则设置为 active否则取消 active
if (item.querySelector('figcaption').textContent.trim() === value) {
item.classList.add('active')
if (iframe) {
iframe.setAttribute('src', iframe.getAttribute('data-src'))
}
} else {
item.classList.remove('active')
// 不活跃窗口暂停播放仅支持notion上传视频、不支持外链
item.querySelectorAll('video')?.forEach(video => {
video.pause()
})
// 外链通过设置src来实现视频暂停播放
if (iframe) {
iframe.setAttribute('src', '')
}
}
})
})
figCaptionWrapper.appendChild(div)
})
if (carouselWrapper.children.length > 0) {
// 将包含 figcaption 值的容器元素添加到 notion-article 的第一个子元素插入
videoWrapper.appendChild(carouselWrapper)
// 显示分集按钮 大于1集才显示 ;或者用户 要求强制显示
if (
figCaptionWrapper.children.length > 1 ||
siteConfig('MOVIE_VIDEO_COMBINE_SHOW_PAGE_FORCE', false, CONFIG)
) {
videoWrapper.appendChild(figCaptionWrapper)
}
// 放入页面
if (
notionArticle.firstChild &&
notionArticle.contains(notionArticle.firstChild)
) {
notionArticle.insertBefore(videoWrapper, notionArticle.firstChild)
} else {
notionArticle.appendChild(videoWrapper)
}
}
}
setTimeout(() => {
combineVideo()
}, 1500)
// 404
if (!post) {
setTimeout(
() => {
if (isBrowser) {
const article = document.querySelector('#article-wrapper #notion-article')
if (!article) {
router.push('/404').then(() => {
console.warn('找不到页面', router.asPath)
})
}
}
},
waiting404
)
}
return () => {
// 获取所有 class="video-wrapper" 的元素
const videoWrappers = document.querySelectorAll('.video-wrapper')
// 遍历所有匹配的元素并移除它们
videoWrappers.forEach(wrapper => {
wrapper.parentNode.removeChild(wrapper) // 从 DOM 中移除元素
})
}
}, [post])
return (
<>
{!lock ? post && (
<div
id='article-wrapper'
className='px-2 max-w-5xl 2xl:max-w-[70%] mx-auto'>
{/* 标题 */}
<ArticleHeader post={post} />
{/* 页面元素 */}
<NotionPage post={post} />
{/* 文章页脚 */}
<ArticleFooter post={post} />
{/* 推荐 */}
<BlogRecommend {...props} />
{/* 分享栏目 */}
<ShareBar post={post} />
{/* 评论区 */}
<Comment frontMatter={post} />
</div>
) : (
<ArticleLock validPassword={validPassword} />
)}
</>
)
}
/**
* 404页
* @param {*} props
* @returns
*/
const Layout404 = props => {
const { locale } = useGlobal()
const { searchModal } = usePhotoGlobal()
const router = useRouter()
// 展示搜索框
const toggleShowSearchInput = () => {
if (siteConfig('ALGOLIA_APP_ID')) {
searchModal.current.openSearch()
}
}
const onKeyUp = e => {
if (e.keyCode === 13) {
const search = document.getElementById('search').value
if (search) {
router.push({ pathname: '/search/' + search })
}
}
}
return (
<>
<div className='h-52'>
<h2 className='text-4xl'>{locale.COMMON.NO_RESULTS_FOUND}</h2>
<hr className='my-4' />
<div className='max-w-md relative'>
<input
autoFocus
id='search'
onClick={toggleShowSearchInput}
onKeyUp={onKeyUp}
className='float-left w-full outline-none h-full p-2 rounded dark:bg-[#383838] bg-gray-100'
aria-label='Submit search'
type='search'
name='s'
autoComplete='off'
placeholder='Type then hit enter to search...'
/>
<i className='fas fa-search absolute right-0 my-auto p-2'></i>
</div>
</div>
{/* 底部导航 */}
<div className='h-full flex-grow grid grid-cols-4 gap-4'>
<LatestPostsGroup {...props} />
<CategoryGroup {...props} />
<ArchiveDateList {...props} />
<TagGroups {...props} />
</div>
</>
)
}
/**
* 搜索页
* @param {*} props
* @returns
*/
const LayoutSearch = props => {
const { keyword } = props
const router = useRouter()
useEffect(() => {
if (isBrowser) {
// 高亮搜索到的结果
const container = document.getElementById('posts-wrapper')
if (keyword && container) {
replaceSearchResult({
doms: container,
search: keyword,
target: {
element: 'span',
className: 'text-red-500 border-b border-dashed'
}
})
}
}
}, [router])
return <LayoutPostList {...props} />
}
/**
* 归档列表
* @param {*} props
* @returns 按照日期将文章分组排序
*/
const LayoutArchive = props => {
const { archivePosts } = props
return (
<>
<div className='mb-10 pb-20 md:py-12 p-3 min-h-screen w-full max-w-5xl 2xl:max-w-[70%] mx-auto'>
{Object.keys(archivePosts).map(archiveTitle => (
<BlogListGroupByDate
key={archiveTitle}
archiveTitle={archiveTitle}
archivePosts={archivePosts}
/>
))}
</div>
</>
)
}
/**
* 分类列表
* @param {*} props
* @returns
*/
const LayoutCategoryIndex = props => {
const { categoryOptions } = props
return (
<>
<div id='category-list' className='duration-200 flex flex-wrap'>
{categoryOptions?.map(category => (
<CategoryItem key={category.name} category={category} />
))}
</div>
</>
)
}
/**
* 标签列表
* @param {*} props
* @returns
*/
const LayoutTagIndex = props => {
const { tagOptions } = props
return (
<>
<div id='tags-list' className='duration-200 flex flex-wrap'>
{tagOptions.map(tag => (
<TagItem key={tag.name} tag={tag} />
))}
</div>
</>
)
}
export {
Layout404,
LayoutArchive,
LayoutBase,
LayoutCategoryIndex,
LayoutIndex,
LayoutPostList,
LayoutSearch,
LayoutSlug,
LayoutTagIndex,
CONFIG as THEME_CONFIG
}