mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-17 23:16:50 +00:00
498 lines
15 KiB
JavaScript
498 lines
15 KiB
JavaScript
'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
|
||
}
|