Merge branch 'main' into original-main-fix-DOMException

This commit is contained in:
tangly1024
2024-08-28 12:18:53 +08:00
committed by GitHub
192 changed files with 7978 additions and 3287 deletions

View File

@@ -1,5 +1,5 @@
# 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables # 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables
NEXT_PUBLIC_VERSION=4.5.3 NEXT_PUBLIC_VERSION=4.6.1
# 可在此添加环境变量,去掉最左边的(# )注释即可 # 可在此添加环境变量,去掉最左边的(# )注释即可

View File

@@ -35,15 +35,15 @@ Live Demo[https://preview.tangly1024.com/](https://preview.tangly1024.com/)
It only takes a few minutes to set up your personal site: It only takes a few minutes to set up your personal site:
- [Quick Deployment Tutorial - Multiple Options Available](https://tangly1024.com/article/notion-next) - [Quick Deployment Tutorial - Multiple Options Available](https://docs.tangly1024.com/article/vercel-deploy-notion-next)
- [Customization Guide - How to Configure Feature Plugins](https://tangly1024.com/article/notion-next-guide) - [Customization Guide - How to Configure Feature Plugins](https://docs.tangly1024.com/article/how-to-config-notion-next)
- [Development Guide - How to Conduct Local Development](https://tangly1024.com/article/how-to-develop-with-notion-next) - [Development Guide - How to Conduct Local Development](https://docs.tangly1024.com/article/how-to-develop-with-notion-next)
- [Update Guide - How to Get the Latest Upgrade Patch](https://tangly1024.com/article/how-to-update-notionnext) - [Update Guide - How to Get the Latest Upgrade Patch](https://docs.tangly1024.com/article/how-to-update-notionnext)
- [Version History - Check Feature Highlights for Each Version](https://tangly1024.com/article/notion-next-changelogs) - [Version History - Check Feature Highlights for Each Version](https://docs.tangly1024.com/article/latest)
## Acknowledgements ## Acknowledgements

View File

@@ -13,6 +13,7 @@ const BLOG = {
APPEARANCE: process.env.NEXT_PUBLIC_APPEARANCE || 'light', // ['light', 'dark', 'auto'], // light 日间模式 dark夜间模式 auto根据时间和主题自动夜间模式 APPEARANCE: process.env.NEXT_PUBLIC_APPEARANCE || 'light', // ['light', 'dark', 'auto'], // light 日间模式 dark夜间模式 auto根据时间和主题自动夜间模式
APPEARANCE_DARK_TIME: process.env.NEXT_PUBLIC_APPEARANCE_DARK_TIME || [18, 6], // 夜间模式起至时间false时关闭根据时间自动切换夜间模式 APPEARANCE_DARK_TIME: process.env.NEXT_PUBLIC_APPEARANCE_DARK_TIME || [18, 6], // 夜间模式起至时间false时关闭根据时间自动切换夜间模式
TAG_SORT_BY_COUNT: true, // 标签是否按照文章数量倒序排列,文章多的标签排在前。
IS_TAG_COLOR_DISTINGUISHED: IS_TAG_COLOR_DISTINGUISHED:
process.env.NEXT_PUBLIC_IS_TAG_COLOR_DISTINGUISHED === 'true' || true, // 对于名称相同的tag是否区分tag的颜色 process.env.NEXT_PUBLIC_IS_TAG_COLOR_DISTINGUISHED === 'true' || true, // 对于名称相同的tag是否区分tag的颜色
@@ -38,6 +39,9 @@ const BLOG = {
CONTACT_INSTAGRAM: process.env.NEXT_PUBLIC_CONTACT_INSTAGRAM || '', // 您的instagram地址 CONTACT_INSTAGRAM: process.env.NEXT_PUBLIC_CONTACT_INSTAGRAM || '', // 您的instagram地址
CONTACT_BILIBILI: process.env.NEXT_PUBLIC_CONTACT_BILIBILI || '', // B站主页 CONTACT_BILIBILI: process.env.NEXT_PUBLIC_CONTACT_BILIBILI || '', // B站主页
CONTACT_YOUTUBE: process.env.NEXT_PUBLIC_CONTACT_YOUTUBE || '', // Youtube主页 CONTACT_YOUTUBE: process.env.NEXT_PUBLIC_CONTACT_YOUTUBE || '', // Youtube主页
CONTACT_XIAOHONGSHU: process.env.NEXT_PUBLIC_CONTACT_XIAOHONGSHU || '', // 小红书主页
CONTACT_ZHISHIXINGQIU: process.env.NEXT_PUBLIC_CONTACT_ZHISHIXINGQIU || '', // 知识星球
CONTACT_WEHCHAT_PUBLIC: process.env.NEXT_PUBLIC_CONTACT_WEHCHAT_PUBLIC || '', // 微信公众号 格式https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=【xxxxxx】==#wechat_redirect
NOTION_HOST: process.env.NEXT_PUBLIC_NOTION_HOST || 'https://www.notion.so', // Notion域名您可以选择用自己的域名进行反向代理如果不懂得什么是反向代理请勿修改此项 NOTION_HOST: process.env.NEXT_PUBLIC_NOTION_HOST || 'https://www.notion.so', // Notion域名您可以选择用自己的域名进行反向代理如果不懂得什么是反向代理请勿修改此项
@@ -204,9 +208,10 @@ const BLOG = {
'link,wechat,qq,weibo,email,facebook,twitter,telegram,messenger,line,reddit,whatsapp,linkedin', // 分享的服務,按顺序显示,逗号隔开 'link,wechat,qq,weibo,email,facebook,twitter,telegram,messenger,line,reddit,whatsapp,linkedin', // 分享的服務,按顺序显示,逗号隔开
// 所有支持的分享服务link(复制链接),wechat(微信),qq,weibo(微博),email(邮件),facebook,twitter,telegram,messenger,line,reddit,whatsapp,linkedin,vkshare,okshare,tumblr,livejournal,mailru,viber,workplace,pocket,instapaper,hatena // 所有支持的分享服务link(复制链接),wechat(微信),qq,weibo(微博),email(邮件),facebook,twitter,telegram,messenger,line,reddit,whatsapp,linkedin,vkshare,okshare,tumblr,livejournal,mailru,viber,workplace,pocket,instapaper,hatena
POST_URL_PREFIX: process.env.NEXT_PUBLIC_POST_URL_PREFIX || 'article', // 文章URL前缀
POST_URL_PREFIX: process.env.NEXT_PUBLIC_POST_URL_PREFIX ?? 'article',
// POST类型文章的默认路径前缀例如默认POST类型的路径是 /article/[slug] // POST类型文章的默认路径前缀例如默认POST类型的路径是 /article/[slug]
// 如果此项配置为 '' 空, 则文章将没有前缀路径,使用场景: 希望文章前缀路径为 /post 的情况 支持多级 // 如果此项配置为 '' 空, 则文章将没有前缀路径
// 支援類似 WP 可自訂文章連結格式的功能https://wordpress.org/documentation/article/customize-permalinks/,目前只先實作 %year%/%month%/%day% // 支援類似 WP 可自訂文章連結格式的功能https://wordpress.org/documentation/article/customize-permalinks/,目前只先實作 %year%/%month%/%day%
// 例:如想連結改成前綴 article + 時間戳記,可變更為: 'article/%year%/%month%/%day%' // 例:如想連結改成前綴 article + 時間戳記,可變更為: 'article/%year%/%month%/%day%'
@@ -540,7 +545,7 @@ const BLOG = {
process.env.ENABLE_CACHE || process.env.ENABLE_CACHE ||
process.env.npm_lifecycle_event === 'build' || process.env.npm_lifecycle_event === 'build' ||
process.env.npm_lifecycle_event === 'export', // 在打包过程中默认开启缓存,开发或运行时开启此功能意义不大。 process.env.npm_lifecycle_event === 'export', // 在打包过程中默认开启缓存,开发或运行时开启此功能意义不大。
isProd: process.env.VERCEL_ENV === 'production', // distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables) isProd: process.env.VERCEL_ENV === 'production' || process.env.EXPORT, // distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables)
BUNDLE_ANALYZER: process.env.ANALYZE === 'true' || false, // 是否展示编译依赖内容与大小 BUNDLE_ANALYZER: process.env.ANALYZE === 'true' || false, // 是否展示编译依赖内容与大小
VERSION: process.env.NEXT_PUBLIC_VERSION // 版本号 VERSION: process.env.NEXT_PUBLIC_VERSION // 版本号
} }

View File

@@ -10,14 +10,8 @@ import { useEffect } from 'react'
export default function AOSAnimation() { export default function AOSAnimation() {
const initAOS = async () => { const initAOS = async () => {
Promise.all([ Promise.all([
loadExternalResource( loadExternalResource('/js/aos.js', 'js'),
'https://cdn.bootcdn.net/ajax/libs/aos/2.3.4/aos.js', loadExternalResource('/css/aos.css', 'css')
'js'
),
loadExternalResource(
'https://cdn.bootcdn.net/ajax/libs/aos/2.3.4/aos.css',
'css'
)
]).then(() => { ]).then(() => {
if (window.AOS) { if (window.AOS) {
window.AOS.init() window.AOS.init()

View File

@@ -1,11 +1,18 @@
import { useState, useImperativeHandle, useRef, useEffect, Fragment } from 'react'
import algoliasearch from 'algoliasearch'
import replaceSearchResult from '@/components/Mark' import replaceSearchResult from '@/components/Mark'
import Link from 'next/link'
import { useGlobal } from '@/lib/global'
import throttle from 'lodash/throttle'
import { siteConfig } from '@/lib/config' import { siteConfig } from '@/lib/config'
import { useHotkeys } from 'react-hotkeys-hook'; import { useGlobal } from '@/lib/global'
import algoliasearch from 'algoliasearch'
import throttle from 'lodash/throttle'
import Link from 'next/link'
import { useRouter } from 'next/router'
import {
Fragment,
useEffect,
useImperativeHandle,
useRef,
useState
} from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
const ShortCutActions = [ const ShortCutActions = [
{ {
@@ -20,7 +27,6 @@ const ShortCutActions = [
key: 'Esc', key: 'Esc',
action: '关闭' action: '关闭'
} }
] ]
/** /**
@@ -36,31 +42,49 @@ export default function AlgoliaSearchModal({ cRef }) {
const [totalPage, setTotalPage] = useState(0) const [totalPage, setTotalPage] = useState(0)
const [totalHit, setTotalHit] = useState(0) const [totalHit, setTotalHit] = useState(0)
const [useTime, setUseTime] = useState(0) const [useTime, setUseTime] = useState(0)
const inputRef = useRef(null)
const [activeIndex, setActiveIndex] = useState(0) const [activeIndex, setActiveIndex] = useState(0)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
useHotkeys('ctrl+k', (e) => { const inputRef = useRef(null)
const router = useRouter()
/**
* 快捷键设置
*/
useHotkeys('ctrl+k', e => {
e.preventDefault() e.preventDefault()
setIsModalOpen(true) setIsModalOpen(true)
}) })
// 方向键调整选中 // 方向键调整选中
useHotkeys('down', (e) => { useHotkeys(
e.preventDefault() 'down',
if (activeIndex < searchResults.length - 1) { e => {
setActiveIndex(activeIndex + 1) e.preventDefault()
} if (activeIndex < searchResults.length - 1) {
}, { enableOnFormTags: true }) setActiveIndex(activeIndex + 1)
useHotkeys('up', (e) => { }
e.preventDefault() },
if (activeIndex > 0) { { enableOnFormTags: true }
setActiveIndex(activeIndex - 1) )
} useHotkeys(
}, { enableOnFormTags: true }) 'up',
e => {
e.preventDefault()
if (activeIndex > 0) {
setActiveIndex(activeIndex - 1)
}
},
{ enableOnFormTags: true }
)
// esc关闭 // esc关闭
useHotkeys('esc', (e) => { useHotkeys(
e.preventDefault() 'esc',
setIsModalOpen(false) e => {
}, { enableOnFormTags: true }) e.preventDefault()
setIsModalOpen(false)
},
{ enableOnFormTags: true }
)
// 跳转Search结果 // 跳转Search结果
const onJumpSearchResult = () => { const onJumpSearchResult = () => {
if (searchResults.length > 0) { if (searchResults.length > 0) {
@@ -68,11 +92,15 @@ export default function AlgoliaSearchModal({ cRef }) {
} }
} }
// enter跳转 // enter跳转
useHotkeys('enter', (e) => { useHotkeys(
if (searchResults.length > 0) { 'enter',
onJumpSearchResult(index) e => {
} if (searchResults.length > 0) {
}, { enableOnFormTags: true }) onJumpSearchResult(index)
}
},
{ enableOnFormTags: true }
)
const resetSearch = () => { const resetSearch = () => {
setActiveIndex(0) setActiveIndex(0)
@@ -84,6 +112,16 @@ export default function AlgoliaSearchModal({ cRef }) {
if (inputRef.current) inputRef.current.value = '' if (inputRef.current) inputRef.current.value = ''
} }
/**
* 页面路径变化后自动关闭此modal
*/
useEffect(() => {
setIsModalOpen(false)
}, [router])
/**
* 自动聚焦搜索框
*/
useEffect(() => { useEffect(() => {
if (isModalOpen) { if (isModalOpen) {
setTimeout(() => { setTimeout(() => {
@@ -93,9 +131,10 @@ export default function AlgoliaSearchModal({ cRef }) {
resetSearch() resetSearch()
} }
}, [isModalOpen]) }, [isModalOpen])
/** /**
* 对外暴露方法 * 对外暴露方法
*/ **/
useImperativeHandle(cRef, () => { useImperativeHandle(cRef, () => {
return { return {
openSearch: () => { openSearch: () => {
@@ -104,7 +143,10 @@ export default function AlgoliaSearchModal({ cRef }) {
} }
}) })
const client = algoliasearch(siteConfig('ALGOLIA_APP_ID'), siteConfig('ALGOLIA_SEARCH_ONLY_APP_KEY')) const client = algoliasearch(
siteConfig('ALGOLIA_APP_ID'),
siteConfig('ALGOLIA_SEARCH_ONLY_APP_KEY')
)
const index = client.initIndex(siteConfig('ALGOLIA_INDEX')) const index = client.initIndex(siteConfig('ALGOLIA_INDEX'))
/** /**
@@ -131,7 +173,9 @@ export default function AlgoliaSearchModal({ cRef }) {
setTotalHit(nbHits) setTotalHit(nbHits)
setSearchResults(hits) setSearchResults(hits)
setIsLoading(false) setIsLoading(false)
const doms = document.getElementById('search-wrapper').getElementsByClassName('replace') const doms = document
.getElementById('search-wrapper')
.getElementsByClassName('replace')
setTimeout(() => { setTimeout(() => {
replaceSearchResult({ replaceSearchResult({
@@ -149,33 +193,35 @@ export default function AlgoliaSearchModal({ cRef }) {
} }
// 定义节流函数,确保在用户停止输入一段时间后才会调用处理搜索的方法 // 定义节流函数,确保在用户停止输入一段时间后才会调用处理搜索的方法
const throttledHandleInputChange = useRef(throttle((query, page = 0) => { const throttledHandleInputChange = useRef(
handleSearch(query, page); throttle((query, page = 0) => {
}, 1000)); handleSearch(query, page)
}, 1000)
)
// 用于存储搜索延迟的计时器 // 用于存储搜索延迟的计时器
const searchTimer = useRef(null); const searchTimer = useRef(null)
// 修改input的onChange事件处理函数 // 修改input的onChange事件处理函数
const handleInputChange = (e) => { const handleInputChange = e => {
const query = e.target.value; const query = e.target.value
// 如果已经有计时器在等待搜索,先清除之前的计时器 // 如果已经有计时器在等待搜索,先清除之前的计时器
if (searchTimer.current) { if (searchTimer.current) {
clearTimeout(searchTimer.current); clearTimeout(searchTimer.current)
} }
// 设置新的计时器,在用户停止输入一段时间后触发搜索 // 设置新的计时器,在用户停止输入一段时间后触发搜索
searchTimer.current = setTimeout(() => { searchTimer.current = setTimeout(() => {
throttledHandleInputChange.current(query); throttledHandleInputChange.current(query)
}, 800); }, 800)
}; }
/** /**
* 切换页码 * 切换页码
* @param {*} page * @param {*} page
*/ */
const switchPage = (page) => { const switchPage = page => {
throttledHandleInputChange.current(keyword, page) throttledHandleInputChange.current(keyword, page)
} }
@@ -191,58 +237,58 @@ export default function AlgoliaSearchModal({ cRef }) {
} }
return ( return (
<div <div
id="search-wrapper" id='search-wrapper'
className={`${isModalOpen ? 'opacity-100' : 'invisible opacity-0 pointer-events-none' className={`${
} z-30 fixed h-screen w-screen left-0 top-0 sm:mt-12 flex items-start justify-center mt-0`} isModalOpen ? 'opacity-100' : 'invisible opacity-0 pointer-events-none'
> } z-30 fixed h-screen w-screen left-0 top-0 sm:mt-12 flex items-start justify-center mt-0`}>
{/* 模态框 */} {/* 模态框 */}
<div <div
className={`${isModalOpen ? 'opacity-100' : 'invisible opacity-0 translate-y-10' className={`${
} flex flex-col justify-between w-full min-h-[10rem] h-full md:h-fit max-w-xl dark:bg-hexo-black-gray dark:border-gray-800 bg-white dark:bg- p-5 rounded-lg z-50 shadow border hover:border-blue-600 duration-300 transition-all `} isModalOpen ? 'opacity-100' : 'invisible opacity-0 translate-y-10'
> } flex flex-col justify-between w-full min-h-[10rem] h-full md:h-fit max-w-xl dark:bg-hexo-black-gray dark:border-gray-800 bg-white dark:bg- p-5 rounded-lg z-50 shadow border hover:border-blue-600 duration-300 transition-all `}>
<div className="flex justify-between items-center"> <div className='flex justify-between items-center'>
<div className="text-2xl text-blue-600 dark:text-yellow-600 font-bold">搜索</div> <div className='text-2xl text-blue-600 dark:text-yellow-600 font-bold'>
搜索
</div>
<div> <div>
<i <i
className="text-gray-600 fa-solid fa-xmark p-1 cursor-pointer hover:text-blue-600" className='text-gray-600 fa-solid fa-xmark p-1 cursor-pointer hover:text-blue-600'
onClick={closeModal} onClick={closeModal}></i>
></i>
</div> </div>
</div> </div>
<input <input
type="text" type='text'
placeholder="在这里输入搜索关键词..." placeholder='在这里输入搜索关键词...'
onChange={e => handleInputChange(e)} onChange={e => handleInputChange(e)}
className="text-black dark:text-gray-200 bg-gray-50 dark:bg-gray-600 outline-blue-500 w-full px-4 my-2 py-1 mb-4 border rounded-md" className='text-black dark:text-gray-200 bg-gray-50 dark:bg-gray-600 outline-blue-500 w-full px-4 my-2 py-1 mb-4 border rounded-md'
ref={inputRef} ref={inputRef}
/> />
{/* 标签组 */} {/* 标签组 */}
<div className="mb-4"> <div className='mb-4'>
<TagGroups /> <TagGroups />
</div> </div>
{ {searchResults.length === 0 && keyword && !isLoading && (
searchResults.length === 0 && keyword && !isLoading && ( <div>
<div> <p className=' text-slate-600 text-center my-4 text-base'>
<p className=" text-slate-600 text-center my-4 text-base"> 无法找到相关结果 {' '}
<span 无法找到相关结果
className='font-semibold' <span className='font-semibold'>&quot;{keyword}&quot;</span>
>&quot;{keyword}&quot;</span></p> </p>
</div> </div>
) )}
}
<ul className='flex-1 overflow-auto'> <ul className='flex-1 overflow-auto'>
{searchResults.map((result, index) => ( {searchResults.map((result, index) => (
<li key={result.objectID} <li
key={result.objectID}
onMouseEnter={() => setActiveIndex(index)} onMouseEnter={() => setActiveIndex(index)}
onClick={() => onJumpSearchResult(index)} onClick={() => onJumpSearchResult(index)}
className={`cursor-pointer replace my-2 p-2 duration-100 className={`cursor-pointer replace my-2 p-2 duration-100
rounded-lg rounded-lg
${activeIndex === index ? 'bg-blue-600 dark:bg-yellow-600' : ''}`}> ${activeIndex === index ? 'bg-blue-600 dark:bg-yellow-600' : ''}`}>
<a <a
className={`${activeIndex === index ? ' text-white' : ' text-black dark:text-gray-300 '}`} className={`${activeIndex === index ? ' text-white' : ' text-black dark:text-gray-300 '}`}>
>
{result.title} {result.title}
</a> </a>
</li> </li>
@@ -250,15 +296,22 @@ export default function AlgoliaSearchModal({ cRef }) {
</ul> </ul>
<Pagination totalPage={totalPage} page={page} switchPage={switchPage} /> <Pagination totalPage={totalPage} page={page} switchPage={switchPage} />
<div className='flex items-center justify-between mt-2 sm:text-sm text-xs dark:text-gray-300'> <div className='flex items-center justify-between mt-2 sm:text-sm text-xs dark:text-gray-300'>
{totalHit === 0 && (<div className='flex items-center'> {totalHit === 0 && (
{ <div className='flex items-center'>
ShortCutActions.map((action, index) => { {ShortCutActions.map((action, index) => {
return <Fragment key={index}><div className='border-gray-300 dark:text-gray-300 text-gray-600 px-2 rounded border inline-block'>{action.key}</div> return (
<span className='ml-2 mr-4 text-gray-600 dark:text-gray-300'>{action.action}</span></Fragment> <Fragment key={index}>
}) <div className='border-gray-300 dark:text-gray-300 text-gray-600 px-2 rounded border inline-block'>
} {action.key}
</div>) </div>
} <span className='ml-2 mr-4 text-gray-600 dark:text-gray-300'>
{action.action}
</span>
</Fragment>
)
})}
</div>
)}
<div> <div>
{totalHit > 0 && ( {totalHit > 0 && (
<p> <p>
@@ -266,19 +319,18 @@ export default function AlgoliaSearchModal({ cRef }) {
</p> </p>
)} )}
</div> </div>
<div className="text-gray-600 dark:text-gray-300 text-right"> <div className='text-gray-600 dark:text-gray-300 text-right'>
<span > <span>
<i className="fa-brands fa-algolia"></i> Algolia <i className='fa-brands fa-algolia'></i> Algolia
</span> </span>
</div> </div>
</div> </div>
</div> </div>
{/* 遮罩 */} {/* 遮罩 */}
<div <div
onClick={closeModal} onClick={closeModal}
className="z-30 fixed top-0 left-0 w-full h-full flex items-center justify-center glassmorphism" className='z-30 fixed top-0 left-0 w-full h-full flex items-center justify-center glassmorphism'
/> />
</div> </div>
) )
@@ -292,21 +344,31 @@ function TagGroups() {
// 获取tagOptions数组前十个 // 获取tagOptions数组前十个
const firstTenTags = tagOptions?.slice(0, 10) const firstTenTags = tagOptions?.slice(0, 10)
return <div id='tags-group' className='dark:border-gray-700 space-y-2'> return (
{ <div id='tags-group' className='dark:border-gray-700 space-y-2'>
firstTenTags?.map((tag, index) => { {firstTenTags?.map((tag, index) => {
return <Link passHref return (
key={index} <Link
href={`/tag/${encodeURIComponent(tag.name)}`} passHref
className={'cursor-pointer inline-block whitespace-nowrap'}> key={index}
<div className={'flex items-center text-black dark:text-gray-300 hover:bg-blue-600 dark:hover:bg-yellow-600 hover:scale-110 hover:text-white rounded-lg px-2 py-0.5 duration-150 transition-all'}> href={`/tag/${encodeURIComponent(tag.name)}`}
<div className='text-lg'>{tag.name} </div>{tag.count ? <sup className='relative ml-1'>{tag.count}</sup> : <></>} className={'cursor-pointer inline-block whitespace-nowrap'}>
</div> <div
className={
</Link> 'flex items-center text-black dark:text-gray-300 hover:bg-blue-600 dark:hover:bg-yellow-600 hover:scale-110 hover:text-white rounded-lg px-2 py-0.5 duration-150 transition-all'
}) }>
} <div className='text-lg'>{tag.name} </div>
</div> {tag.count ? (
<sup className='relative ml-1'>{tag.count}</sup>
) : (
<></>
)}
</div>
</Link>
)
})}
</div>
)
} }
/** /**
@@ -321,16 +383,16 @@ function Pagination(props) {
return ( return (
<div className='flex space-x-1 w-full justify-center py-1'> <div className='flex space-x-1 w-full justify-center py-1'>
{Array.from({ length: totalPage }, (_, i) => { {Array.from({ length: totalPage }, (_, i) => {
const classNames = page === i const classNames =
? 'font-bold text-white bg-blue-600 dark:bg-yellow-600 rounded' page === i
: 'hover:text-blue-600 hover:font-bold dark:text-gray-300' ? 'font-bold text-white bg-blue-600 dark:bg-yellow-600 rounded'
: 'hover:text-blue-600 hover:font-bold dark:text-gray-300'
return ( return (
<div <div
onClick={() => switchPage(i)} onClick={() => switchPage(i)}
className={`text-center cursor-pointer w-6 h-6 ${classNames}`} className={`text-center cursor-pointer w-6 h-6 ${classNames}`}
key={i} key={i}>
>
{i + 1} {i + 1}
</div> </div>
) )

View File

@@ -121,6 +121,11 @@ const Comment = ({ frontMatter, className }) => {
return null return null
} }
// 特定文章关闭评论区
if (frontMatter?.comment === 'Hide') {
return null
}
return ( return (
<div <div
key={frontMatter?.id} key={frontMatter?.id}

View File

@@ -1,10 +1,10 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect, useState, useRef, useLayoutEffect } from 'react'
import { useGlobal } from '@/lib/global'
import { saveDarkModeToLocalStorage, THEMES } from '@/themes/theme'
import useWindowSize from '@/hooks/useWindowSize' import useWindowSize from '@/hooks/useWindowSize'
import { siteConfig } from '@/lib/config' import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import { THEMES, saveDarkModeToLocalStorage } from '@/themes/theme'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
/** /**
* 自定义右键菜单 * 自定义右键菜单
@@ -20,14 +20,14 @@ export default function CustomContextMenu(props) {
const [width, setWidth] = useState(0) const [width, setWidth] = useState(0)
const [height, setHeight] = useState(0) const [height, setHeight] = useState(0)
const { latestPosts } = props const { allNavPages } = props
const router = useRouter() const router = useRouter()
/** /**
* 随机跳转文章 * 随机跳转文章
*/ */
function handleJumpToRandomPost() { function handleJumpToRandomPost() {
const randomIndex = Math.floor(Math.random() * latestPosts.length) const randomIndex = Math.floor(Math.random() * allNavPages.length)
const randomPost = latestPosts[randomIndex] const randomPost = allNavPages[randomIndex]
router.push(`${siteConfig('SUB_PATH', '')}/${randomPost?.slug}`) router.push(`${siteConfig('SUB_PATH', '')}/${randomPost?.slug}`)
} }
@@ -37,19 +37,30 @@ export default function CustomContextMenu(props) {
}, []) }, [])
useEffect(() => { useEffect(() => {
const handleContextMenu = (event) => { setShow(false)
}, [router])
useEffect(() => {
const handleContextMenu = event => {
event.preventDefault() event.preventDefault()
// 计算点击位置加菜单宽高是否超出屏幕,如果超出则贴边弹出 // 计算点击位置加菜单宽高是否超出屏幕,如果超出则贴边弹出
const x = (event.clientX < windowSize.width - width) ? event.clientX : windowSize.width - width const x =
const y = (event.clientY < windowSize.height - height) ? event.clientY : windowSize.height - height event.clientX < windowSize.width - width
? event.clientX
: windowSize.width - width
const y =
event.clientY < windowSize.height - height
? event.clientY
: windowSize.height - height
setPosition({ y: `${y}px`, x: `${x}px` }) setPosition({ y: `${y}px`, x: `${x}px` })
setShow(true) setShow(true)
} }
const handleClick = (event) => { /**
if (menuRef.current && !menuRef.current.contains(event.target)) { * 鼠标点击即关闭菜单
setShow(false) */
} const handleClick = event => {
setShow(false)
} }
window.addEventListener('contextmenu', handleContextMenu) window.addEventListener('contextmenu', handleContextMenu)
@@ -75,24 +86,24 @@ export default function CustomContextMenu(props) {
function handleScrollTop() { function handleScrollTop() {
window.scrollTo({ top: 0, behavior: 'smooth' }) window.scrollTo({ top: 0, behavior: 'smooth' })
setShow(false)
} }
function handleCopyLink() { function handleCopyLink() {
const url = window.location.href const url = window.location.href
navigator.clipboard.writeText(url) navigator.clipboard
.writeText(url)
.then(() => { .then(() => {
console.log('页面地址已复制') // console.log('页面地址已复制')
alert(`${locale.COMMON.PAGE_URL_COPIED} : ${url}`)
}) })
.catch((error) => { .catch(error => {
console.error('复制页面地址失败:', error) console.error('复制页面地址失败:', error)
}) })
setShow(false)
} }
/** /**
* 切换主题 * 切换主题
*/ */
function handleChangeTheme() { function handleChangeTheme() {
const randomTheme = THEMES[Math.floor(Math.random() * THEMES.length)] // 从THEMES数组中 随机取一个主题 const randomTheme = THEMES[Math.floor(Math.random() * THEMES.length)] // 从THEMES数组中 随机取一个主题
const query = router.query const query = router.query
@@ -104,7 +115,7 @@ export default function CustomContextMenu(props) {
* 复制内容 * 复制内容
*/ */
function handleCopy() { function handleCopy() {
const selectedText = document.getSelection().toString(); const selectedText = document.getSelection().toString()
if (selectedText) { if (selectedText) {
const tempInput = document.createElement('input'); const tempInput = document.createElement('input');
tempInput.value = selectedText; tempInput.value = selectedText;
@@ -118,8 +129,6 @@ export default function CustomContextMenu(props) {
} else { } else {
// alert("Please select some text first."); // alert("Please select some text first.");
} }
setShow(false)
} }
function handleChangeDarkMode() { function handleChangeDarkMode() {
@@ -131,77 +140,140 @@ export default function CustomContextMenu(props) {
htmlElement.classList?.add(newStatus ? 'dark' : 'light') htmlElement.classList?.add(newStatus ? 'dark' : 'light')
} }
// 一些配置变量
const CUSTOM_RIGHT_CLICK_CONTEXT_MENU_RANDOM_POST = siteConfig(
'CUSTOM_RIGHT_CLICK_CONTEXT_MENU_RANDOM_POST'
)
const CUSTOM_RIGHT_CLICK_CONTEXT_MENU_CATEGORY = siteConfig(
'CUSTOM_RIGHT_CLICK_CONTEXT_MENU_CATEGORY'
)
const CUSTOM_RIGHT_CLICK_CONTEXT_MENU_TAG = siteConfig(
'CUSTOM_RIGHT_CLICK_CONTEXT_MENU_TAG'
)
const CAN_COPY = siteConfig('CAN_COPY')
const CUSTOM_RIGHT_CLICK_CONTEXT_MENU_SHARE_LINK = siteConfig(
'CUSTOM_RIGHT_CLICK_CONTEXT_MENU_SHARE_LINK'
)
const CUSTOM_RIGHT_CLICK_CONTEXT_MENU_DARK_MODE = siteConfig(
'CUSTOM_RIGHT_CLICK_CONTEXT_MENU_DARK_MODE'
)
const CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH = siteConfig(
'CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH'
)
return ( return (
<div <div
ref={menuRef} ref={menuRef}
style={{ top: position.y, left: position.x }} style={{ top: position.y, left: position.x }}
className={`${show ? '' : 'invisible opacity-0'} select-none transition-opacity duration-200 fixed z-50`} className={`${show ? '' : 'invisible opacity-0'} select-none transition-opacity duration-200 fixed z-50`}>
> {/* 菜单内容 */}
<div className='rounded-xl w-52 dark:hover:border-yellow-600 bg-white dark:bg-[#040404] dark:text-gray-200 dark:border-gray-600 p-3 border drop-shadow-lg flex-col duration-300 transition-colors'>
{/* 顶部导航按钮 */}
<div className='flex justify-between'>
<i
onClick={handleBack}
className='hover:bg-blue-600 hover:text-white px-2 py-2 text-center w-8 rounded cursor-pointer fa-solid fa-arrow-left'></i>
<i
onClick={handleForward}
className='hover:bg-blue-600 hover:text-white px-2 py-2 text-center w-8 rounded cursor-pointer fa-solid fa-arrow-right'></i>
<i
onClick={handleRefresh}
className='hover:bg-blue-600 hover:text-white px-2 py-2 text-center w-8 rounded cursor-pointer fa-solid fa-rotate-right'></i>
<i
onClick={handleScrollTop}
className='hover:bg-blue-600 hover:text-white px-2 py-2 text-center w-8 rounded cursor-pointer fa-solid fa-arrow-up'></i>
</div>
{/* 菜单内容 */} <hr className='my-2 border-dashed' />
<div className='rounded-xl w-52 dark:hover:border-yellow-600 bg-white dark:bg-[#040404] dark:text-gray-200 dark:border-gray-600 p-3 border drop-shadow-lg flex-col duration-300 transition-colors'>
{/* 顶部导航按钮 */}
<div className='flex justify-between'>
<i onClick={handleBack} className="hover:bg-blue-600 hover:text-white px-2 py-2 text-center w-8 rounded cursor-pointer fa-solid fa-arrow-left"></i>
<i onClick={handleForward} className="hover:bg-blue-600 hover:text-white px-2 py-2 text-center w-8 rounded cursor-pointer fa-solid fa-arrow-right"></i>
<i onClick={handleRefresh} className="hover:bg-blue-600 hover:text-white px-2 py-2 text-center w-8 rounded cursor-pointer fa-solid fa-rotate-right"></i>
<i onClick={handleScrollTop} className="hover:bg-blue-600 hover:text-white px-2 py-2 text-center w-8 rounded cursor-pointer fa-solid fa-arrow-up"></i>
</div>
<hr className='my-2 border-dashed' />
{/* 跳转导航按钮 */}
<div className='w-full px-2'>
{siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_RANDOM_POST') && <div onClick={handleJumpToRandomPost} title={locale.MENU.WALK_AROUND} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className="fa-solid fa-podcast mr-2" />
<div className='whitespace-nowrap'>{locale.MENU.WALK_AROUND}</div>
</div>}
{siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_CATEGORY') && <Link href='/category' title={locale.MENU.CATEGORY} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className="fa-solid fa-square-minus mr-2" />
<div className='whitespace-nowrap'>{locale.MENU.CATEGORY}</div>
</Link>}
{siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_TAG') && <Link href='/tag' title={locale.MENU.TAGS} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className="fa-solid fa-tag mr-2" />
<div className='whitespace-nowrap'>{locale.MENU.TAGS}</div>
</Link>}
</div>
<hr className='my-2 border-dashed' />
{/* 功能按钮 */}
<div className='w-full px-2'>
{siteConfig('CAN_COPY') && (
<div onClick={handleCopy} title={locale.MENU.COPY} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className="fa-solid fa-copy mr-2" />
<div className='whitespace-nowrap'>{locale.MENU.COPY}</div>
</div>
)}
{siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_SHARE_LINK') && <div onClick={handleCopyLink} title={locale.MENU.SHARE_URL} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className="fa-solid fa-arrow-up-right-from-square mr-2" />
<div className='whitespace-nowrap'>{locale.MENU.SHARE_URL}</div>
</div>}
{siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_DARK_MODE') && <div onClick={handleChangeDarkMode} title={isDarkMode ? locale.MENU.LIGHT_MODE : locale.MENU.DARK_MODE} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
{isDarkMode ? <i className="fa-regular fa-sun mr-2" /> : <i className="fa-regular fa-moon mr-2" />}
<div className='whitespace-nowrap'> {isDarkMode ? locale.MENU.LIGHT_MODE : locale.MENU.DARK_MODE}</div>
</div>}
{siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH') && (
<div onClick={handleChangeTheme} title={locale.MENU.THEME_SWITCH} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className="fa-solid fa-palette mr-2" />
<div className='whitespace-nowrap'>{locale.MENU.THEME_SWITCH}</div>
</div>
)}
</div>
{/* 跳转导航按钮 */}
<div className='w-full px-2'>
{CUSTOM_RIGHT_CLICK_CONTEXT_MENU_RANDOM_POST && (
<div
onClick={handleJumpToRandomPost}
title={locale.MENU.WALK_AROUND}
className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className='fa-solid fa-podcast mr-2' />
<div className='whitespace-nowrap'>{locale.MENU.WALK_AROUND}</div>
</div> </div>
</div > )}
{CUSTOM_RIGHT_CLICK_CONTEXT_MENU_CATEGORY && (
<Link
href='/category'
title={locale.MENU.CATEGORY}
className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className='fa-solid fa-square-minus mr-2' />
<div className='whitespace-nowrap'>{locale.MENU.CATEGORY}</div>
</Link>
)}
{CUSTOM_RIGHT_CLICK_CONTEXT_MENU_TAG && (
<Link
href='/tag'
title={locale.MENU.TAGS}
className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className='fa-solid fa-tag mr-2' />
<div className='whitespace-nowrap'>{locale.MENU.TAGS}</div>
</Link>
)}
</div>
<hr className='my-2 border-dashed' />
{/* 功能按钮 */}
<div className='w-full px-2'>
{CAN_COPY && (
<div
onClick={handleCopy}
title={locale.MENU.COPY}
className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className='fa-solid fa-copy mr-2' />
<div className='whitespace-nowrap'>{locale.MENU.COPY}</div>
</div>
)}
{CUSTOM_RIGHT_CLICK_CONTEXT_MENU_SHARE_LINK && (
<div
onClick={handleCopyLink}
title={locale.MENU.SHARE_URL}
className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className='fa-solid fa-arrow-up-right-from-square mr-2' />
<div className='whitespace-nowrap'>{locale.MENU.SHARE_URL}</div>
</div>
)}
{CUSTOM_RIGHT_CLICK_CONTEXT_MENU_DARK_MODE && (
<div
onClick={handleChangeDarkMode}
title={
isDarkMode ? locale.MENU.LIGHT_MODE : locale.MENU.DARK_MODE
}
className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
{isDarkMode ? (
<i className='fa-regular fa-sun mr-2' />
) : (
<i className='fa-regular fa-moon mr-2' />
)}
<div className='whitespace-nowrap'>
{' '}
{isDarkMode ? locale.MENU.LIGHT_MODE : locale.MENU.DARK_MODE}
</div>
</div>
)}
{CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH && (
<div
onClick={handleChangeTheme}
title={locale.MENU.THEME_SWITCH}
className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className='fa-solid fa-palette mr-2' />
<div className='whitespace-nowrap'>
{locale.MENU.THEME_SWITCH}
</div>
</div>
)}
</div>
</div>
</div>
) )
} }

View File

@@ -5,8 +5,7 @@ import LA51 from './LA51'
import TianLiGPT from './TianliGPT' import TianLiGPT from './TianliGPT'
import WebWhiz from './Webwhiz' import WebWhiz from './Webwhiz'
import { CUSTOM_EXTERNAL_CSS, CUSTOM_EXTERNAL_JS } from '@/blog.config' import { convertInnerUrl } from '@/lib/notion/convertInnerUrl'
import { mapPageUrl } from '@/lib/notion/mapPageUrl'
import { isBrowser, loadExternalResource } from '@/lib/utils' import { isBrowser, loadExternalResource } from '@/lib/utils'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useEffect } from 'react' import { useEffect } from 'react'
@@ -44,8 +43,6 @@ const ExternalPlugin = props => {
const CHATBASE_ID = siteConfig('CHATBASE_ID') const CHATBASE_ID = siteConfig('CHATBASE_ID')
const COMMENT_DAO_VOICE_ID = siteConfig('COMMENT_DAO_VOICE_ID') const COMMENT_DAO_VOICE_ID = siteConfig('COMMENT_DAO_VOICE_ID')
const AD_WWADS_ID = siteConfig('AD_WWADS_ID') const AD_WWADS_ID = siteConfig('AD_WWADS_ID')
// const COMMENT_TWIKOO_ENV_ID = siteConfig('COMMENT_TWIKOO_ENV_ID')
// const COMMENT_TWIKOO_CDN_URL = siteConfig('COMMENT_TWIKOO_CDN_URL')
const COMMENT_ARTALK_SERVER = siteConfig('COMMENT_ARTALK_SERVER') const COMMENT_ARTALK_SERVER = siteConfig('COMMENT_ARTALK_SERVER')
const COMMENT_ARTALK_JS = siteConfig('COMMENT_ARTALK_JS') const COMMENT_ARTALK_JS = siteConfig('COMMENT_ARTALK_JS')
const COMMENT_TIDIO_ID = siteConfig('COMMENT_TIDIO_ID') const COMMENT_TIDIO_ID = siteConfig('COMMENT_TIDIO_ID')
@@ -64,6 +61,8 @@ const ExternalPlugin = props => {
const IMG_SHADOW = siteConfig('IMG_SHADOW') const IMG_SHADOW = siteConfig('IMG_SHADOW')
const ANIMATE_CSS_URL = siteConfig('ANIMATE_CSS_URL') const ANIMATE_CSS_URL = siteConfig('ANIMATE_CSS_URL')
const MOUSE_FOLLOW = siteConfig('MOUSE_FOLLOW') const MOUSE_FOLLOW = siteConfig('MOUSE_FOLLOW')
const CUSTOM_EXTERNAL_CSS = siteConfig('CUSTOM_EXTERNAL_CSS')
const CUSTOM_EXTERNAL_JS = siteConfig('CUSTOM_EXTERNAL_JS')
// 自定义样式css和js引入 // 自定义样式css和js引入
if (isBrowser) { if (isBrowser) {
@@ -106,7 +105,7 @@ const ExternalPlugin = props => {
} }
// 映射url // 映射url
mapPageUrl(props?.allNavPages) convertInnerUrl(props?.allNavPages)
}, [router]) }, [router])
useEffect(() => { useEffect(() => {

View File

@@ -18,7 +18,7 @@ const Fireworks = () => {
// 异步加载 // 异步加载
async function loadFireworks() { async function loadFireworks() {
loadExternalResource( loadExternalResource(
'https://cdn.bootcdn.net/ajax/libs/animejs/3.2.1/anime.min.js', 'https://cdnjs.snrat.com/ajax/libs/animejs/3.2.1/anime.min.js',
'js' 'js'
).then(() => { ).then(() => {
loadExternalResource('/js/fireworks.js', 'js').then(() => { loadExternalResource('/js/fireworks.js', 'js').then(() => {
@@ -35,7 +35,11 @@ const Fireworks = () => {
loadFireworks() loadFireworks()
return () => { return () => {
// 在组件卸载时清理资源(如果需要) // 在组件卸载时清理资源
const fireworksElements = document.getElementsByClassName('fireworks')
while (fireworksElements.length > 0) {
fireworksElements[0].parentNode.removeChild(fireworksElements[0])
}
} }
}, []) }, [])

View File

@@ -1,6 +1,7 @@
import { siteConfig } from '@/lib/config' import { siteConfig } from '@/lib/config'
import Head from 'next/head' import Head from 'next/head'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
/** /**
* 图片懒加载 * 图片懒加载
* @param {*} param0 * @param {*} param0
@@ -20,27 +21,52 @@ export default function LazyImage({
style style
}) { }) {
const maxWidth = siteConfig('IMAGE_COMPRESS_WIDTH') const maxWidth = siteConfig('IMAGE_COMPRESS_WIDTH')
const defaultPlaceholderSrc = siteConfig('IMG_LAZY_LOAD_PLACEHOLDER')
const imageRef = useRef(null) const imageRef = useRef(null)
const [adjustedSrc, setAdjustedSrc] = useState( const [currentSrc, setCurrentSrc] = useState(
placeholderSrc || siteConfig('IMG_LAZY_LOAD_PLACEHOLDER') placeholderSrc || defaultPlaceholderSrc
) )
if (!placeholderSrc) {
placeholderSrc = siteConfig('IMG_LAZY_LOAD_PLACEHOLDER')
}
/** /**
* 图加载成功回调 * 占位图加载成功
*/ */
const handleImageLoad = () => { const handleThumbnailLoaded = () => {
if (typeof onLoad === 'function') {
// onLoad() // 触发传递的onLoad回调函数
}
}
// 原图加载完成
const handleImageLoaded = img => {
if (typeof onLoad === 'function') { if (typeof onLoad === 'function') {
onLoad() // 触发传递的onLoad回调函数 onLoad() // 触发传递的onLoad回调函数
} }
} }
/**
* 图片加载失败回调
*/
const handleImageError = () => {
if (imageRef.current) {
// 尝试加载 placeholderSrc如果失败则加载 defaultPlaceholderSrc
if (imageRef.current.src !== placeholderSrc && placeholderSrc) {
imageRef.current.src = placeholderSrc
} else {
imageRef.current.src = defaultPlaceholderSrc
}
}
}
useEffect(() => { useEffect(() => {
const adjustedImageSrc = adjustImgSize(src, maxWidth) const adjustedImageSrc =
setAdjustedSrc(adjustedImageSrc) adjustImgSize(src, maxWidth) || defaultPlaceholderSrc
// 加载原图
const img = new Image()
img.src = adjustedImageSrc
img.onload = () => {
setCurrentSrc(adjustedImageSrc)
handleImageLoaded(adjustedImageSrc)
}
img.onerror = handleImageError
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
entries => { entries => {
@@ -69,9 +95,10 @@ export default function LazyImage({
// 动态添加width、height和className属性仅在它们为有效值时添加 // 动态添加width、height和className属性仅在它们为有效值时添加
const imgProps = { const imgProps = {
ref: imageRef, ref: imageRef,
src: priority ? adjustedSrc : placeholderSrc, src: currentSrc,
alt: alt, alt: alt,
onLoad: handleImageLoad onLoad: handleThumbnailLoaded, // 缩略图加载完成
onError: handleImageError // 添加onError处理函数
} }
if (id) { if (id) {
@@ -95,6 +122,7 @@ export default function LazyImage({
if (style) { if (style) {
imgProps.style = style imgProps.style = style
} }
return ( return (
<> <>
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
@@ -102,24 +130,25 @@ export default function LazyImage({
{/* 预加载 */} {/* 预加载 */}
{priority && ( {priority && (
<Head> <Head>
<link rel='preload' as='image' href={adjustedSrc} /> <link rel='preload' as='image' href={adjustImgSize(src, maxWidth)} />
</Head> </Head>
)} )}
</> </>
) )
} }
/** /**
* 根据窗口尺寸决定压缩图片宽度 * 根据窗口尺寸决定压缩图片宽度
* @param {*} src * @param {*} src
* @param {*} maxWidth * @param {*} maxWidth
* @returns * @returns
*/ */
const adjustImgSize = (src, maxWidth) => { const adjustImgSize = (src, maxWidth) => {
if (!src) { if (!src) {
return siteConfig('IMG_LAZY_LOAD_PLACEHOLDER') return null
} }
const screenWidth = window.screen.width const screenWidth =
(typeof window !== 'undefined' && window?.screen?.width) || maxWidth
// 屏幕尺寸大于默认图片尺寸,没必要再压缩 // 屏幕尺寸大于默认图片尺寸,没必要再压缩
if (screenWidth > maxWidth) { if (screenWidth > maxWidth) {

View File

@@ -11,7 +11,7 @@ export default function LoadingProgress() {
// 加载进度条 // 加载进度条
useEffect(() => { useEffect(() => {
loadExternalResource( loadExternalResource(
'https://cdn.bootcdn.net/ajax/libs/nprogress/0.2.0/nprogress.min.js', 'https://cdnjs.snrat.com/ajax/libs/nprogress/0.2.0/nprogress.min.js',
'js' 'js'
).then(() => { ).then(() => {
if (window.NProgress) { if (window.NProgress) {
@@ -19,7 +19,7 @@ export default function LoadingProgress() {
// 调速 // 调速
window.NProgress.settings.minimun = 0.1 window.NProgress.settings.minimun = 0.1
loadExternalResource( loadExternalResource(
'https://cdn.bootcdn.net/ajax/libs/nprogress/0.2.0/nprogress.min.css', 'https://cdnjs.snrat.com/ajax/libs/nprogress/0.2.0/nprogress.min.css',
'css' 'css'
) )
} }

View File

@@ -15,8 +15,26 @@ const MOUSE_FOLLOW = () => {
loadExternalResource('/js/mouse-follow.js', 'js').then(url => { loadExternalResource('/js/mouse-follow.js', 'js').then(url => {
window.createMouseCanvas && window.createMouseCanvas()({ type, color }) window.createMouseCanvas && window.createMouseCanvas()({ type, color })
}) })
return () => {
// 在组件卸载时清理资源
const mouseFollowElement = document.getElementById('vixcityCanvas')
mouseFollowElement?.parentNode?.removeChild(mouseFollowElement)
}
}, []) }, [])
return <></> return (
<>
<style global jsx>
{`
@media (max-width: 600px) {
#vixcityCanvas {
display: none;
}
}
`}
</style>
</>
)
} }
export default MOUSE_FOLLOW export default MOUSE_FOLLOW

View File

@@ -0,0 +1,63 @@
import { useState } from 'react'
/**
* 弹框通知
* @returns
*/
const useNotification = () => {
const [message, setMessage] = useState('')
const [isVisible, setIsVisible] = useState(false)
const showNotification = msg => {
setMessage(msg)
setIsVisible(true)
setTimeout(() => {
closeNotification()
}, 3000)
}
const closeNotification = () => {
setIsVisible(false)
setMessage('')
}
// 测试通知效果
// const toggleVisible = () => {
// setIsVisible(prev => !prev) // 使用函数式更新
// }
// useEffect(() => {
// document?.addEventListener('click', toggleVisible)
// return () => {
// document?.removeEventListener('click', toggleVisible)
// }
// }, [])
/**
* 通知组件
* @returns
*/
const Notification = () => {
return (
<div className={`notification fixed left-0 w-full px-2 z-20 bottom-14`}>
<div
className={` ${isVisible && message ? 'opacity-100 ' : 'invisible opacity-0 bottom-0'} transition-opacity duration-200
max-w-3xl mx-auto bg-green-500 flex items-center justify-between px-4 py-2 text-white rounded-lg shadow-lg`}>
{message}
<button
onClick={closeNotification}
className='ml-4 p-2 cursor-pointer bg-transparent text-white border-none'>
<i className='fas fa-times' />
</button>
</div>
</div>
)
}
return {
showNotification,
closeNotification,
Notification
}
}
export default useNotification

View File

@@ -31,7 +31,7 @@ const NotionPage = ({ post, className }) => {
// 页面首次打开时执行的勾子 // 页面首次打开时执行的勾子
useEffect(() => { useEffect(() => {
// 检测当前的url并自动滚动到对应目标 // 检测当前的url并自动滚动到对应目标
autoScrollToTarget() autoScrollToHash()
}, []) }, [])
// 页面文章发生变化时会执行的勾子 // 页面文章发生变化时会执行的勾子
@@ -144,9 +144,9 @@ const processGalleryImg = zoom => {
} }
/** /**
* 根据url参数自动滚动到指定区域 * 根据url参数自动滚动到锚位置
*/ */
const autoScrollToTarget = () => { const autoScrollToHash = () => {
setTimeout(() => { setTimeout(() => {
// 跳转到指定标题 // 跳转到指定标题
const needToJumpToTitle = window.location.hash const needToJumpToTitle = window.location.hash

View File

@@ -1,17 +1,18 @@
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import { useState } from 'react' import LANGS from '@/lib/lang'
import { Draggable } from './Draggable' import { getQueryParam } from '@/lib/utils'
import { THEMES } from '@/themes/theme' import { THEMES } from '@/themes/theme'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useState } from 'react'
import DarkModeButton from './DarkModeButton' import DarkModeButton from './DarkModeButton'
import { getQueryParam } from '@/lib/utils' import { Draggable } from './Draggable'
import LANGS from '@/lib/lang'
/** /**
* *
* @returns 主题切换 * @returns 主题切换
*/ */
const ThemeSwitch = () => { const ThemeSwitch = () => {
const { theme, lang, changeLang, locale, isDarkMode, toggleDarkMode } = useGlobal() const { theme, lang, changeLang, locale, isDarkMode, toggleDarkMode } =
useGlobal()
const router = useRouter() const router = useRouter()
const currentTheme = getQueryParam(router.asPath, 'theme') || theme const currentTheme = getQueryParam(router.asPath, 'theme') || theme
// const currentLang = getQueryParam(router.asPath, 'lang') || lang // const currentLang = getQueryParam(router.asPath, 'lang') || lang
@@ -19,8 +20,12 @@ const ThemeSwitch = () => {
// 修改当前路径url中的 theme 参数 // 修改当前路径url中的 theme 参数
// 例如 http://localhost?theme=hexo 跳转到 http://localhost?theme=newTheme // 例如 http://localhost?theme=hexo 跳转到 http://localhost?theme=newTheme
const onThemeSelectChange = (e) => { const onThemeSelectChange = e => {
document.ontouchmove = document.ontouchend = document.onmousemove = document.onmouseup = null document.ontouchmove =
document.ontouchend =
document.onmousemove =
document.onmouseup =
null
setIsLoading(true) setIsLoading(true)
const newTheme = e.target.value const newTheme = e.target.value
const query = router.query const query = router.query
@@ -28,59 +33,96 @@ const ThemeSwitch = () => {
router.push({ pathname: router.pathname, query }).then(() => { router.push({ pathname: router.pathname, query }).then(() => {
setTimeout(() => { setTimeout(() => {
setIsLoading(false) setIsLoading(false)
}, 500); }, 500)
}) })
} }
const onLangSelectChange = (e) => { const onLangSelectChange = e => {
document.ontouchmove = document.ontouchend = document.onmousemove = document.onmouseup = null document.ontouchmove =
document.ontouchend =
document.onmousemove =
document.onmouseup =
null
const newLang = e.target.value const newLang = e.target.value
changeLang(newLang) changeLang(newLang)
} }
return (<> return (
<Draggable> <>
<div id="draggableBox" style={{ left: '0px', top: '80vh' }} className="fixed group space-y-2 overflow-hidden z-50 p-3 flex flex-col items-start dark:text-white bg-white dark:bg-black rounded-xl shadow-lg "> <Draggable>
{/* 深色按钮 */} <div
<div className="text-sm flex items-center w-0 group-hover:w-32 transition-all duration-200"> id='draggableBox'
<DarkModeButton /> style={{ left: '0px', top: '80vh' }}
<div onClick={toggleDarkMode} className='cursor-pointer w-0 group-hover:w-24 transition-all duration-200 overflow-hidden whitespace-nowrap pl-1 h-auto'>{isDarkMode ? locale.MENU.DARK_MODE : locale.MENU.LIGHT_MODE}</div> className='fixed group flex flex-col items-start space-y-2 overflow-hidden z-50 p-3
</div> dark:text-white bg-white dark:bg-black
rounded-xl shadow-lg hover:scale-105 hover:shadow-2xl '>
{/* 翻译按钮 */} {/* 主题切换按钮 */}
<div className="text-sm flex items-center group-hover:w-32 transition-all duration-200"> <div className='text-sm flex items-center group-hover:w-32 duration-200'>
<i className="fa-solid fa-language w-5" /> <i className='fa-solid fa-palette w-5' />
<div className='w-0 group-hover:w-24 transition-all duration-200 overflow-hidden'> <div className='w-0 group-hover:w-24 duration-200 overflow-hidden'>
<label htmlFor="langSelect" className="sr-only">选择语言</label> <label htmlFor='themeSelect' className='sr-only'>
<select id="langSelect" value={lang} onChange={onLangSelectChange} name="themes" className='pl-1 bg-gray-50 dark:bg-black appearance-none outline-none dark:text-white uppercase cursor-pointer'> {locale.COMMON.THEME}
{Object.keys(LANGS)?.map(t => { </label>
return <option key={t} value={t}>{LANGS[t].LOCALE}</option> <select
})} id='themeSelect'
</select> value={currentTheme}
</div> onChange={onThemeSelectChange}
</div> name='themes'
className='pl-1 bg-gray-50 dark:bg-black appearance-none outline-none dark:text-white uppercase cursor-pointer'>
{/* 主题切换按钮 */} {THEMES?.map(t => {
<div className="text-sm flex items-center group-hover:w-32 transition-all duration-200"> return (
<i className="fa-solid fa-palette w-5" /> <option key={t} value={t}>
<div className='w-0 group-hover:w-24 transition-all duration-200 overflow-hidden'> {t}
<label htmlFor="themeSelect" className="sr-only">选择主题</label> </option>
<select id="themeSelect" value={currentTheme} onChange={onThemeSelectChange} name="themes" className='pl-1 bg-gray-50 dark:bg-black appearance-none outline-none dark:text-white uppercase cursor-pointer'> )
{THEMES?.map(t => { })}
return <option key={t} value={t}>{t}</option> </select>
})}
</select>
</div>
</div>
</div> </div>
</Draggable> </div>
{/* 切换主题加载时的全屏遮罩 */} {/* 深色按钮 */}
<div className={`${isLoading ? 'opacity-90 ' : 'opacity-0'} <div className='text-sm flex items-center w-0 group-hover:w-32 duration-200'>
w-screen h-screen glassmorphism bg-black text-white shadow-text flex justify-center items-center <DarkModeButton />
transition-all fixed top-0 left-0 pointer-events-none duration-1000 z-50 shadow-inner`}> <div
<i className='text-3xl mr-5 fas fa-spinner animate-spin' /> onClick={toggleDarkMode}
className='cursor-pointer w-0 group-hover:w-24 duration-200 overflow-hidden whitespace-nowrap pl-1 h-auto'>
{isDarkMode ? locale.MENU.DARK_MODE : locale.MENU.LIGHT_MODE}
</div>
</div>
{/* 翻译按钮 */}
<div className='text-sm flex items-center group-hover:w-32 duration-200'>
<i className='fa-solid fa-language w-5' />
<div className='w-0 group-hover:w-24 duration-200 overflow-hidden'>
<label htmlFor='langSelect' className='sr-only'>
Language Select
</label>
<select
id='langSelect'
value={lang}
onChange={onLangSelectChange}
name='themes'
className='pl-1 bg-gray-50 dark:bg-black appearance-none outline-none dark:text-white uppercase cursor-pointer'>
{Object.keys(LANGS)?.map(t => {
return (
<option key={t} value={t}>
{LANGS[t].LOCALE}
</option>
)
})}
</select>
</div>
</div>
</div> </div>
</Draggable>
{/* 切换主题加载时的全屏遮罩 */}
<div
className={`${isLoading ? 'opacity-90 ' : 'opacity-0'}
w-screen h-screen glassmorphism bg-black text-white shadow-text flex justify-center items-center
fixed top-0 left-0 pointer-events-none duration-1000 z-50 shadow-inner`}>
<i className='text-3xl mr-5 fas fa-spinner animate-spin' />
</div>
</> </>
) )
} }

View File

@@ -2,7 +2,7 @@
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
import { useGlobal } from './global' import { useGlobal } from './global'
import { deepClone } from './utils' import { deepClone, isUrl } from './utils'
/** /**
* 读取配置顺序 * 读取配置顺序
@@ -19,7 +19,7 @@ export const siteConfig = (key, defaultVal = null, extendConfig = {}) => {
return null return null
} }
// 特殊配置处理;某些配置只在服务端生效而Global的NOTION_CONFIG仅限前端组件使用因此需要从extendConfig中读取 // 特殊配置处理;以下配置只在服务端生效而Global的NOTION_CONFIG仅限前端组件使用因此需要从extendConfig中读取
switch (key) { switch (key) {
case 'NEXT_REVALIDATE_SECOND': case 'NEXT_REVALIDATE_SECOND':
case 'POST_RECOMMEND_COUNT': case 'POST_RECOMMEND_COUNT':
@@ -32,6 +32,8 @@ export const siteConfig = (key, defaultVal = null, extendConfig = {}) => {
case 'POST_LIST_STYLE': case 'POST_LIST_STYLE':
case 'POST_LIST_PREVIEW': case 'POST_LIST_PREVIEW':
case 'POST_URL_PREFIX_MAPPING_CATEGORY': case 'POST_URL_PREFIX_MAPPING_CATEGORY':
case 'IS_TAG_COLOR_DISTINGUISHED':
case 'TAG_SORT_BY_COUNT':
return convertVal(extendConfig[key] || defaultVal || BLOG[key]) return convertVal(extendConfig[key] || defaultVal || BLOG[key])
default: default:
} }
@@ -53,8 +55,8 @@ export const siteConfig = (key, defaultVal = null, extendConfig = {}) => {
let siteInfo = null let siteInfo = null
if (global) { if (global) {
val = global.NOTION_CONFIG?.[key]
siteInfo = global.siteInfo siteInfo = global.siteInfo
val = global.NOTION_CONFIG?.[key] || global.THEME_CONFIG?.[key]
} }
if (!val) { if (!val) {
@@ -89,46 +91,62 @@ export const siteConfig = (key, defaultVal = null, extendConfig = {}) => {
return defaultVal return defaultVal
} }
// 从Notion_CONFIG读取的配置通常都是字符串适当转义
return convertVal(val) return convertVal(val)
} }
/** /**
* 配置默认都是string类型 * 从环境变量和NotionConfig读取的配置都是string类型
* 识别配置的值是否数字、布尔、[]数组,若是则转成对应类型 * 这里识别配置的字符值若为否 数字、布尔、[]数组,{}对象,若是则转成对应类型
* 使用JSON和eval两个函数
* @param {*} val * @param {*} val
* @returns * @returns
*/ */
export const convertVal = val => { export const convertVal = val => {
if (typeof val === 'string') { // 如果传入参数本身就是obj、数组、boolean 就无需处理
// 解析布尔 if (typeof val !== 'string' || !val) {
if (val === 'true' || val === 'false') {
return JSON.parse(val)
}
// 解析数字parseInt将字符串转换为数字
if (/^\d+$/.test(val)) {
return parseInt(val)
}
// 转移 [] , {} 这种json串为json对象
try {
const parsedJson = JSON.parse(val)
// 检查解析后的结果是否是对象或数组
if (typeof parsedJson === 'object' && parsedJson !== null) {
return parsedJson
}
} catch (error) {
// JSON 解析失败,返回原始字符串值
return val
}
}
try {
return JSON.parse(val)
} catch (error) {
// 如果值是一个字符串但不是有效的 JSON 格式,直接返回字符串
return val return val
} }
// 解析数字parseInt将字符串转换为数字
if (/^\d+$/.test(val)) {
return parseInt(val)
}
// 检测是否url
if (isUrl(val)) {
return val
}
// 检测是否url
if (val === 'true' || val === 'false') {
return JSON.parse(val)
}
// 配置值前可能有污染的空格
if (val.indexOf('[') < 0 && val.indexOf('{') < 0) {
return val
}
// 转换 [] , {} , true/false 这类字符串为对象
try {
// 尝试解析json
const parsedJson = JSON.parse(val)
if (parsedJson !== null) {
return parsedJson
}
} catch (error) {
// try {
// // 尝试解析对象,对象解析能力不如上一步的json
// const evalObj = eval('(' + val + ')')
// if (evalObj !== null) {
// return evalObj
// }
// } catch (error) {
// // Ojbject 解析失败,返回原始字符串值
// return val
// }
return val
}
return val
} }
/** /**

View File

@@ -5,18 +5,18 @@ import getAllPageIds from '@/lib/notion/getAllPageIds'
import { getAllTags } from '@/lib/notion/getAllTags' import { getAllTags } from '@/lib/notion/getAllTags'
import { getConfigMapFromConfigPage } from '@/lib/notion/getNotionConfig' import { getConfigMapFromConfigPage } from '@/lib/notion/getNotionConfig'
import getPageProperties, { import getPageProperties, {
adjustPageProperties adjustPageProperties
} from '@/lib/notion/getPageProperties' } from '@/lib/notion/getPageProperties'
import { fetchInBatches, getPostBlocks } from '@/lib/notion/getPostBlocks' import { fetchInBatches, getPage } from '@/lib/notion/getPostBlocks'
import { compressImage, mapImgUrl } from '@/lib/notion/mapImage' import { compressImage, mapImgUrl } from '@/lib/notion/mapImage'
import { deepClone } from '@/lib/utils' import { deepClone } from '@/lib/utils'
import { idToUuid } from 'notion-utils' import { idToUuid } from 'notion-utils'
import { siteConfig } from '../config' import { siteConfig } from '../config'
import { extractLangId, extractLangPrefix } from '../utils/pageId' import { extractLangId, extractLangPrefix, getShortId } from '../utils/pageId'
export { getAllTags } from '../notion/getAllTags' export { getAllTags } from '../notion/getAllTags'
export { getPost } from '../notion/getNotionPost' export { getPost } from '../notion/getNotionPost'
export { getPostBlocks } from '../notion/getPostBlocks' export { getPage as getPostBlocks } from '../notion/getPostBlocks'
/** /**
* 获取博客数据; 基于Notion实现 * 获取博客数据; 基于Notion实现
@@ -77,7 +77,18 @@ export async function getNotionPageData({ pageId, from }) {
} }
// 返回给前端的数据做处理 // 返回给前端的数据做处理
const db = deepClone(data) return handleDataBeforeReturn(deepClone(data))
}
/**
* 返回给浏览器前端的数据处理
* 适当脱敏
* 减少体积
* 其它处理
* @param {*} db
*/
function handleDataBeforeReturn(db) {
// 清理多余数据
delete db.block delete db.block
delete db.schema delete db.schema
delete db.rawMetadata delete db.rawMetadata
@@ -91,42 +102,88 @@ export async function getNotionPageData({ pageId, from }) {
// 清理多余的块 // 清理多余的块
if (db?.notice) { if (db?.notice) {
db.notice = cleanBlock(db?.notice) db.notice = cleanBlock(db?.notice)
delete db.notice?.id
} }
if (db?.post) {
db.post = cleanBlock(db?.post) db.tagOptions = cleanIds(db?.tagOptions)
} db.categoryOptions = cleanIds(db?.categoryOptions)
db.customMenu = cleanIds(db?.customMenu)
// db.latestPosts = shortenIds(db?.latestPosts)
db.allNavPages = shortenIds(db?.allNavPages)
// db.allPages = cleanBlocks(db?.allPages)
return db return db
} }
/**
* 清理一组数据的id
* @param {*} items
* @returns
*/
function shortenIds(items) {
if (items && Array.isArray(items)) {
return deepClone(
items.map(item => {
item.short_id = getShortId(item.id)
delete item.id
return item
})
)
}
return items
}
/**
* 清理一组数据的id
* @param {*} items
* @returns
*/
function cleanIds(items) {
if (items && Array.isArray(items)) {
return deepClone(
items.map(item => {
delete item.id
return item
})
)
}
return items
}
/** /**
* 清理block数据 * 清理block数据
*/ */
function cleanBlock(post) { function cleanBlock(item) {
const post = deepClone(item)
const pageBlock = post?.blockMap?.block const pageBlock = post?.blockMap?.block
for (const i in pageBlock) { // delete post?.id
pageBlock[i] = cleanBlock(pageBlock[i])
delete pageBlock[i]?.role
delete pageBlock[i]?.value?.version
delete pageBlock[i]?.value?.created_by_table
delete pageBlock[i]?.value?.created_by_id
delete pageBlock[i]?.value?.last_edited_by_table
delete pageBlock[i]?.value?.last_edited_by_id
delete pageBlock[i]?.value?.space_id
delete pageBlock[i]?.value?.version
delete pageBlock[i]?.value?.format?.copied_from_pointer
delete pageBlock[i]?.value?.format?.block_locked_by
delete pageBlock[i]?.value?.parent_table
delete pageBlock[i]?.value?.copied_from_pointer
delete pageBlock[i]?.value?.copied_from
delete pageBlock[i]?.value?.created_by_table
delete pageBlock[i]?.value?.created_by_id
delete pageBlock[i]?.value?.last_edited_by_table
delete pageBlock[i]?.value?.last_edited_by_id
delete pageBlock[i]?.value?.permissions
delete pageBlock[i]?.value?.alive
}
// delete post?.blockMap?.collection // delete post?.blockMap?.collection
if (pageBlock) {
for (const i in pageBlock) {
pageBlock[i] = cleanBlock(pageBlock[i])
delete pageBlock[i]?.role
delete pageBlock[i]?.value?.version
delete pageBlock[i]?.value?.created_by_table
delete pageBlock[i]?.value?.created_by_id
delete pageBlock[i]?.value?.last_edited_by_table
delete pageBlock[i]?.value?.last_edited_by_id
delete pageBlock[i]?.value?.space_id
delete pageBlock[i]?.value?.version
delete pageBlock[i]?.value?.format?.copied_from_pointer
delete pageBlock[i]?.value?.format?.block_locked_by
delete pageBlock[i]?.value?.parent_table
delete pageBlock[i]?.value?.copied_from_pointer
delete pageBlock[i]?.value?.copied_from
delete pageBlock[i]?.value?.created_by_table
delete pageBlock[i]?.value?.created_by_id
delete pageBlock[i]?.value?.last_edited_by_table
delete pageBlock[i]?.value?.last_edited_by_id
delete pageBlock[i]?.value?.permissions
delete pageBlock[i]?.value?.alive
}
}
return post return post
} }
@@ -290,7 +347,6 @@ export function getNavPages({ allPages }) {
return ( return (
post && post &&
post?.slug && post?.slug &&
!post?.slug?.startsWith('http') &&
post?.type === 'Post' && post?.type === 'Post' &&
post?.status === 'Published' post?.status === 'Published'
) )
@@ -320,7 +376,7 @@ async function getNotice(post) {
return null return null
} }
post.blockMap = await getPostBlocks(post.id, 'data-notice') post.blockMap = await getPage(post.id, 'data-notice')
return post return post
} }
@@ -372,7 +428,7 @@ const EmptyData = pageId => {
*/ */
async function getDataBaseInfoByNotionAPI({ pageId, from }) { async function getDataBaseInfoByNotionAPI({ pageId, from }) {
console.log('[Fetching Data]', pageId, from) console.log('[Fetching Data]', pageId, from)
const pageRecordMap = await getPostBlocks(pageId, from) const pageRecordMap = await getPage(pageId, from)
if (!pageRecordMap) { if (!pageRecordMap) {
console.error('can`t get Notion Data ; Which id is: ', pageId) console.error('can`t get Notion Data ; Which id is: ', pageId)
return {} return {}
@@ -455,6 +511,7 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
adjustPageProperties(element, NOTION_CONFIG) adjustPageProperties(element, NOTION_CONFIG)
}) })
// 站点基础信息
const siteInfo = getSiteInfo({ collection, block, pageId }) const siteInfo = getSiteInfo({ collection, block, pageId })
// 文章计数 // 文章计数
@@ -468,7 +525,7 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
return ( return (
post && post &&
post?.slug && post?.slug &&
!post?.slug?.startsWith('http') && // !post?.slug?.startsWith('http') &&
(post?.status === 'Invisible' || post?.status === 'Published') (post?.status === 'Invisible' || post?.status === 'Published')
) )
}) })
@@ -490,11 +547,17 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
) )
})?.[0] })?.[0]
) )
// 所有分类
const categoryOptions = getAllCategories({ const categoryOptions = getAllCategories({
allPages, allPages,
categoryOptions: getCategoryOptions(schema) categoryOptions: getCategoryOptions(schema)
}) })
const tagOptions = getAllTags({ allPages, tagOptions: getTagOptions(schema) }) // 所有标签
const tagOptions = getAllTags({
allPages,
tagOptions: getTagOptions(schema),
NOTION_CONFIG
})
// 旧的菜单 // 旧的菜单
const customNav = getCustomNav({ const customNav = getCustomNav({
allPages: collectionData.filter( allPages: collectionData.filter(

View File

@@ -1,5 +1,6 @@
import { import {
THEMES, THEMES,
getThemeConfig,
initDarkMode, initDarkMode,
saveDarkModeToLocalStorage saveDarkModeToLocalStorage
} from '@/themes/theme' } from '@/themes/theme'
@@ -29,11 +30,14 @@ export function GlobalContextProvider(props) {
tagOptions, tagOptions,
NOTION_CONFIG NOTION_CONFIG
} = props } = props
const [lang, updateLang] = useState(NOTION_CONFIG?.LANG || LANG) // 默认语言 const [lang, updateLang] = useState(NOTION_CONFIG?.LANG || LANG) // 默认语言
const [locale, updateLocale] = useState( const [locale, updateLocale] = useState(
generateLocaleDict(NOTION_CONFIG?.LANG || LANG) generateLocaleDict(NOTION_CONFIG?.LANG || LANG)
) // 默认语言 ) // 默认语言
const [theme, setTheme] = useState(NOTION_CONFIG?.THEME || THEME) // 默认博客主题 const [theme, setTheme] = useState(NOTION_CONFIG?.THEME || THEME) // 默认博客主题
const [THEME_CONFIG, SET_THEME_CONFIG] = useState(null) // 主题配置
const defaultDarkMode = NOTION_CONFIG?.APPEARANCE || APPEARANCE const defaultDarkMode = NOTION_CONFIG?.APPEARANCE || APPEARANCE
const [isDarkMode, updateDarkMode] = useState(defaultDarkMode === 'dark') // 默认深色模式 const [isDarkMode, updateDarkMode] = useState(defaultDarkMode === 'dark') // 默认深色模式
const [onLoading, setOnLoading] = useState(false) // 抓取文章数据 const [onLoading, setOnLoading] = useState(false) // 抓取文章数据
@@ -54,6 +58,12 @@ export function GlobalContextProvider(props) {
return newTheme return newTheme
} }
// 抓取主题配置
const updateThemeConfig = async theme => {
const config = await getThemeConfig(theme)
SET_THEME_CONFIG(config)
}
// 切换深色模式 // 切换深色模式
const toggleDarkMode = () => { const toggleDarkMode = () => {
const newStatus = !isDarkMode const newStatus = !isDarkMode
@@ -79,7 +89,10 @@ export function GlobalContextProvider(props) {
useEffect(() => { useEffect(() => {
initDarkMode(updateDarkMode, defaultDarkMode) initDarkMode(updateDarkMode, defaultDarkMode)
initLocale(lang, locale, updateLang, updateLocale) initLocale(lang, locale, updateLang, updateLocale)
redirectUserLang(NOTION_PAGE_ID) // 可以
if (NOTION_CONFIG?.REDIRECT_LANG) {
redirectUserLang(NOTION_PAGE_ID)
}
}, []) }, [])
// 加载进度条 // 加载进度条
@@ -96,6 +109,9 @@ export function GlobalContextProvider(props) {
setOnLoading(false) setOnLoading(false)
} }
const currentTheme = router?.query?.theme || theme
updateThemeConfig(currentTheme)
router.events.on('routeChangeStart', handleStart) router.events.on('routeChangeStart', handleStart)
router.events.on('routeChangeError', handleStop) router.events.on('routeChangeError', handleStop)
router.events.on('routeChangeComplete', handleStop) router.events.on('routeChangeComplete', handleStop)
@@ -111,6 +127,7 @@ export function GlobalContextProvider(props) {
value={{ value={{
fullWidth, fullWidth,
NOTION_CONFIG, NOTION_CONFIG,
THEME_CONFIG,
toggleDarkMode, toggleDarkMode,
onLoading, onLoading,
setOnLoading, setOnLoading,

View File

@@ -14,11 +14,15 @@ export default {
INDEX: 'Home', INDEX: 'Home',
RSS: 'RSS', RSS: 'RSS',
SEARCH: 'Search', SEARCH: 'Search',
NAVIGATOR: 'NAV',
ABOUT: 'About', ABOUT: 'About',
MAIL: 'E-Mail', MAIL: 'E-Mail',
ARCHIVE: 'Archive' ARCHIVE: 'Archive'
}, },
COMMON: { COMMON: {
THEME: 'Theme',
ARTICLE_LIST: 'Article List',
RECOMMEND_POSTS: 'Recommend Posts',
MORE: 'More', MORE: 'More',
NO_MORE: 'No More', NO_MORE: 'No More',
LATEST_POSTS: 'Latest posts', LATEST_POSTS: 'Latest posts',
@@ -33,15 +37,21 @@ export default {
COPYRIGHT: 'Copyright', COPYRIGHT: 'Copyright',
AUTHOR: 'Author', AUTHOR: 'Author',
URL: 'URL', URL: 'URL',
NOW: 'NOW',
RECOMMEND_BADGES: 'Recommend',
BLOG: 'Blog',
POSTS: 'Posts', POSTS: 'Posts',
ARTICLE: 'Article', ARTICLE: 'Article',
VISITORS: 'Visitors', VISITORS: 'Visitors',
VIEWS: 'Views', VIEWS: 'Views',
COPYRIGHT_NOTICE: 'All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!', PAGE_URL_COPIED: 'Page URL copied',
COPYRIGHT_NOTICE:
'All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!',
RESULT_OF_SEARCH: 'Results Found', RESULT_OF_SEARCH: 'Results Found',
ARTICLE_DETAIL: 'Article Details', ARTICLE_DETAIL: 'Article Details',
PASSWORD_ERROR: 'Password Error!', PASSWORD_ERROR: 'Password Error!',
ARTICLE_LOCK_TIPS: 'Please Enter the password:', ARTICLE_LOCK_TIPS: 'Please Enter the password:',
ARTICLE_UNLOCK_TIPS: 'Article Unlocked',
NO_RESULTS_FOUND: 'No results found.', NO_RESULTS_FOUND: 'No results found.',
SUBMIT: 'Submit', SUBMIT: 'Submit',
POST_TIME: 'Post on', POST_TIME: 'Post on',
@@ -55,7 +65,8 @@ export default {
START_READING: 'Start Reading', START_READING: 'Start Reading',
MINUTE: 'min', MINUTE: 'min',
WORD_COUNT: 'Words', WORD_COUNT: 'Words',
READ_TIME: 'Read Time' READ_TIME: 'Read Time',
NEXT_POST: '下一篇'
}, },
PAGINATION: { PAGINATION: {
PREV: 'Prev', PREV: 'Prev',

View File

@@ -29,11 +29,14 @@ export default {
ARTICLE: '記事', ARTICLE: '記事',
VISITORS: '人の訪問者', VISITORS: '人の訪問者',
VIEWS: '回の閲覧', VIEWS: '回の閲覧',
COPYRIGHT_NOTICE: 'この記事はCC BY-NC-SA 4.0 ライセンスの下でライセンスされています。転載する場合には出典を明らかにしてください。', COPYRIGHT_NOTICE:
'この記事はCC BY-NC-SA 4.0 ライセンスの下でライセンスされています。転載する場合には出典を明らかにしてください。',
RESULT_OF_SEARCH: '個の検索結果', RESULT_OF_SEARCH: '個の検索結果',
ARTICLE_DETAIL: '記事の詳細', ARTICLE_DETAIL: '記事の詳細',
PASSWORD_ERROR: 'パスワードが違います!', PASSWORD_ERROR: 'パスワードが違います!',
ARTICLE_LOCK_TIPS: 'この記事はロックされています。アクセスパスワードを入力してください。', ARTICLE_LOCK_TIPS:
'この記事はロックされています。アクセスパスワードを入力してください。',
ARTICLE_UNLOCK_TIPS: '記事がロック解除されました',
SUBMIT: '送信', SUBMIT: '送信',
POST_TIME: '公開日', POST_TIME: '公開日',
LAST_EDITED_TIME: '最終更新日', LAST_EDITED_TIME: '最終更新日',

View File

@@ -20,6 +20,9 @@ export default {
ARCHIVE: '归档' ARCHIVE: '归档'
}, },
COMMON: { COMMON: {
THEME: 'Theme',
ARTICLE_LIST: '文章列表',
RECOMMEND_POSTS: '推荐文章',
MORE: '更多', MORE: '更多',
NO_MORE: '没有更多了', NO_MORE: '没有更多了',
LATEST_POSTS: '最新发布', LATEST_POSTS: '最新发布',
@@ -35,16 +38,21 @@ export default {
AUTHOR: '作者', AUTHOR: '作者',
URL: '链接', URL: '链接',
ANALYTICS: '统计', ANALYTICS: '统计',
RECOMMEND_BADGES: '荐',
BLOG: '博客',
NOW: '此刻',
POSTS: '篇文章', POSTS: '篇文章',
ARTICLE: '文章', ARTICLE: '文章',
VISITORS: '位访客', VISITORS: '位访客',
VIEWS: '次查看', VIEWS: '次查看',
PAGE_URL_COPIED: '页面地址已复制',
COPYRIGHT_NOTICE: '本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。', COPYRIGHT_NOTICE: '本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。',
RESULT_OF_SEARCH: '篇搜索到的结果', RESULT_OF_SEARCH: '篇搜索到的结果',
NO_RESULTS_FOUND: '没有找到文章', NO_RESULTS_FOUND: '没有找到文章',
ARTICLE_DETAIL: '文章详情', ARTICLE_DETAIL: '文章详情',
PASSWORD_ERROR: '密码错误!', PASSWORD_ERROR: '密码错误!',
ARTICLE_LOCK_TIPS: '文章已上锁,请输入访问密码', ARTICLE_LOCK_TIPS: '文章已上锁,请输入访问密码',
ARTICLE_UNLOCK_TIPS: '文章已解锁',
SUBMIT: '提交', SUBMIT: '提交',
POST_TIME: '发布于', POST_TIME: '发布于',
LAST_EDITED_TIME: '最后更新', LAST_EDITED_TIME: '最后更新',
@@ -57,7 +65,8 @@ export default {
START_READING: '开始阅读', START_READING: '开始阅读',
MINUTE: '分钟', MINUTE: '分钟',
WORD_COUNT: '字数', WORD_COUNT: '字数',
READ_TIME: '阅读时长' READ_TIME: '阅读时长',
NEXT_POST: '下一篇'
}, },
PAGINATION: { PAGINATION: {
PREV: '上页', PREV: '上页',

View File

@@ -5,7 +5,39 @@ export default {
RSS: '訂閱', RSS: '訂閱',
SEARCH: '搜尋', SEARCH: '搜尋',
ABOUT: '關於', ABOUT: '關於',
MAIL: '電郵' MAIL: '電郵',
NAVIGATOR: '導航',
ARCHIVE: '封存'
},
COMMON: {
ARTICLE_LIST: '文章列表',
MORE: '更多',
NO_MORE: '沒有更多了',
LATEST_POSTS: '最新文章',
TAGS: '標籤',
NO_TAG: '無標籤',
CATEGORY: '分類',
SHARE: '分享',
SCAN_QR_CODE: 'QRCode',
URL_COPIED: '連結已複製!',
TABLE_OF_CONTENTS: '目錄',
RELATE_POSTS: '相關文章',
COPYRIGHT: '著作權',
AUTHOR: '作者',
URL: '連結',
ANALYTICS: '分析',
POSTS: '篇文章',
ARTICLE: '文章',
VISITORS: '位訪客',
VIEWS: '次查看',
COPYRIGHT_NOTICE: '本文採用 CC BY-NC-SA 4.0 許可協議,轉載請註明出處。',
RESULT_OF_SEARCH: '篇搜尋到的结果',
ARTICLE_DETAIL: '完整文章',
PASSWORD_ERROR: '密碼錯誤!',
ARTICLE_LOCK_TIPS: '文章已上鎖,請輸入訪問密碼',
SUBMIT: '提交',
POST_TIME: '发布于',
LAST_EDITED_TIME: '最后更新'
}, },
PAGINATION: { PAGINATION: {
PREV: '上一頁', PREV: '上一頁',
@@ -13,7 +45,7 @@ export default {
}, },
SEARCH: { SEARCH: {
ARTICLES: '搜尋文章', ARTICLES: '搜尋文章',
TAGS: '搜尋標' TAGS: '搜尋標'
}, },
POST: { POST: {
BACK: '返回', BACK: '返回',

View File

@@ -10,6 +10,7 @@ export default {
ARCHIVE: '封存' ARCHIVE: '封存'
}, },
COMMON: { COMMON: {
ARTICLE_LIST: '文章列表',
MORE: '更多', MORE: '更多',
NO_MORE: '沒有更多了', NO_MORE: '沒有更多了',
LATEST_POSTS: '最新文章', LATEST_POSTS: '最新文章',

View File

@@ -1,33 +1,44 @@
import { uuidToId } from 'notion-utils' import { idToUuid } from 'notion-utils'
import { checkStrIsNotionId, getLastPartOfUrl, isBrowser } from '../utils' import { checkStrIsNotionId, getLastPartOfUrl, isBrowser } from '../utils'
/** /**
* 处理页面内连接跳转: * 处理页面内连接跳转:
* 1. 若是本站域名则在当前窗口打开不开新窗口 * 1.若是本站域名则在当前窗口打开不开新窗口
* 2. 若是Notion笔记中的内链尝试转换成博客中现有的文章地址 * 2.url是notion-id转成站内文章链接
*/ */
export const mapPageUrl = allPages => { export const convertInnerUrl = allPages => {
if (isBrowser) { if (isBrowser) {
const allAnchorTags = document
?.getElementById('notion-article')
?.getElementsByTagName('a')
if (!allAnchorTags) {
return
}
const currentURL = window.location.origin + window.location.pathname const currentURL = window.location.origin + window.location.pathname
const allAnchorTags = document.getElementsByTagName('a') // 或者使用 document.querySelectorAll('a') 获取 NodeList // url替换成slug
for (const anchorTag of allAnchorTags) { for (const anchorTag of allAnchorTags) {
// 检查url // 检查url
if (anchorTag?.href) { if (anchorTag?.href) {
// 如果url是一个Notion_id尝试匹配成博客的文章内链 // 如果url是一个Notion_id尝试匹配成博客的文章内链
const slug = getLastPartOfUrl(anchorTag.href) const slug = getLastPartOfUrl(anchorTag.href)
if (checkStrIsNotionId(slug)) { if (checkStrIsNotionId(slug)) {
const slugPage = allPages?.find(page => uuidToId(page.id) === slug) const slugPage = allPages?.find(page => {
return idToUuid(slug).indexOf(page.short_id) === 0
})
if (slugPage) { if (slugPage) {
anchorTag.href = slugPage?.href anchorTag.href = slugPage?.href
} }
} }
} }
}
// 链接在当前页面打开
for (const anchorTag of allAnchorTags) {
if (anchorTag?.target === '_blank') { if (anchorTag?.target === '_blank') {
const hrefWithoutQueryHash = anchorTag.href.split('?')[0].split('#')[0] const hrefWithoutQueryHash = anchorTag.href.split('?')[0].split('#')[0]
const hrefWithRelativeHash = const hrefWithRelativeHash =
currentURL.split('#')[0] + anchorTag.href.split('#')[1] currentURL.split('#')[0] || '' + anchorTag.href.split('#')[1] || ''
if ( if (
currentURL === hrefWithoutQueryHash || currentURL === hrefWithoutQueryHash ||
currentURL === hrefWithRelativeHash currentURL === hrefWithRelativeHash

View File

@@ -4,7 +4,7 @@ import { isIterable } from '../utils'
* 获取所有文章的标签 * 获取所有文章的标签
* @param allPosts * @param allPosts
* @param sliceCount 默认截取数量为12若为0则返回全部 * @param sliceCount 默认截取数量为12若为0则返回全部
* @param tagOptions tags的下拉选项 * @param categoryOptions categories的下拉选项
* @returns {Promise<{}|*[]>} * @returns {Promise<{}|*[]>}
*/ */
@@ -13,8 +13,14 @@ import { isIterable } from '../utils'
* @param allPosts * @param allPosts
* @returns {Promise<{}|*[]>} * @returns {Promise<{}|*[]>}
*/ */
export function getAllCategories({ allPages, categoryOptions, sliceCount = 0 }) { export function getAllCategories({
const allPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published') allPages,
categoryOptions,
sliceCount = 0
}) {
const allPosts = allPages?.filter(
page => page.type === 'Post' && page.status === 'Published'
)
if (!allPosts || !categoryOptions) { if (!allPosts || !categoryOptions) {
return [] return []
} }

View File

@@ -1,5 +1,5 @@
import { siteConfig } from '../config'
import { isIterable } from '../utils' import { isIterable } from '../utils'
import BLOG from '@/blog.config'
/** /**
* 获取所有文章的标签 * 获取所有文章的标签
@@ -8,8 +8,15 @@ import BLOG from '@/blog.config'
* @param tagOptions tags的下拉选项 * @param tagOptions tags的下拉选项
* @returns {Promise<{}|*[]>} * @returns {Promise<{}|*[]>}
*/ */
export function getAllTags({ allPages, sliceCount = 0, tagOptions }) { export function getAllTags({
const allPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published') allPages,
sliceCount = 0,
tagOptions,
NOTION_CONFIG
}) {
const allPosts = allPages?.filter(
page => page.type === 'Post' && page.status === 'Published'
)
if (!allPosts || !tagOptions) { if (!allPosts || !tagOptions) {
return [] return []
@@ -27,7 +34,12 @@ export function getAllTags({ allPages, sliceCount = 0, tagOptions }) {
}) })
const list = [] const list = []
const { IS_TAG_COLOR_DISTINGUISHED } = BLOG const IS_TAG_COLOR_DISTINGUISHED = siteConfig(
'IS_TAG_COLOR_DISTINGUISHED',
false,
NOTION_CONFIG
)
const TAG_SORT_BY_COUNT = siteConfig('TAG_SORT_BY_COUNT', true, NOTION_CONFIG)
if (isIterable(tagOptions)) { if (isIterable(tagOptions)) {
if (!IS_TAG_COLOR_DISTINGUISHED) { if (!IS_TAG_COLOR_DISTINGUISHED) {
// 如果不区分颜色, 那么不同颜色相同名称的tag当做同一种tag // 如果不区分颜色, 那么不同颜色相同名称的tag当做同一种tag
@@ -52,7 +64,10 @@ export function getAllTags({ allPages, sliceCount = 0, tagOptions }) {
} }
// 按照数量排序 // 按照数量排序
// list.sort((a, b) => b.count - a.count) if (TAG_SORT_BY_COUNT) {
list.sort((a, b) => b.count - a.count)
}
if (sliceCount && sliceCount > 0) { if (sliceCount && sliceCount > 0) {
return list.slice(0, sliceCount) return list.slice(0, sliceCount)
} else { } else {

View File

@@ -9,7 +9,7 @@
import { getDateValue, getTextContent } from 'notion-utils' import { getDateValue, getTextContent } from 'notion-utils'
import { deepClone } from '../utils' import { deepClone } from '../utils'
import getAllPageIds from './getAllPageIds' import getAllPageIds from './getAllPageIds'
import { getPostBlocks } from './getPostBlocks' import { getPage } from './getPostBlocks'
/** /**
* 从Notion中读取Config配置表 * 从Notion中读取Config配置表
@@ -41,12 +41,12 @@ export async function getConfigMapFromConfigPage(allPages) {
} }
const configPageId = configPage.id const configPageId = configPage.id
// console.log('[Notion配置]请求配置数据 ', configPage.id) // console.log('[Notion配置]请求配置数据 ', configPage.id)
let pageRecordMap = await getPostBlocks(configPageId, 'config-table') let pageRecordMap = await getPage(configPageId, 'config-table')
// console.log('配置中心Page', configPageId, pageRecordMap) // console.log('配置中心Page', configPageId, pageRecordMap)
let content = pageRecordMap.block[configPageId].value.content let content = pageRecordMap.block[configPageId].value.content
for (const table of ['Config-Table', 'CONFIG-TABLE']) { for (const table of ['Config-Table', 'CONFIG-TABLE']) {
if (content) break if (content) break
pageRecordMap = await getPostBlocks(configPageId, table) pageRecordMap = await getPage(configPageId, table)
content = pageRecordMap.block[configPageId].value.content content = pageRecordMap.block[configPageId].value.content
} }

View File

@@ -2,7 +2,7 @@ import BLOG from '@/blog.config'
import { idToUuid } from 'notion-utils' import { idToUuid } from 'notion-utils'
import { defaultMapImageUrl } from 'react-notion-x' import { defaultMapImageUrl } from 'react-notion-x'
import formatDate from '../utils/formatDate' import formatDate from '../utils/formatDate'
import { getPostBlocks } from './getPostBlocks' import { getPage } from './getPostBlocks'
/** /**
* 根据页面ID获取内容 * 根据页面ID获取内容
@@ -10,7 +10,7 @@ import { getPostBlocks } from './getPostBlocks'
* @returns * @returns
*/ */
export async function getPost(pageId) { export async function getPost(pageId) {
const blockMap = await getPostBlocks(pageId, 'slug') const blockMap = await getPage(pageId, 'slug')
if (!blockMap) { if (!blockMap) {
return null return null
} }

View File

@@ -6,10 +6,11 @@ import formatDate from '../utils/formatDate'
import md5 from 'js-md5' import md5 from 'js-md5'
import { siteConfig } from '../config' import { siteConfig } from '../config'
import { import {
checkContainHttp, checkStartWithHttp,
convertUrlStartWithOneSlash, convertUrlStartWithOneSlash,
sliceUrlFromHttp getLastSegmentFromUrl
} from '../utils' } from '../utils'
import { extractLangPrefix } from '../utils/pageId'
import { mapImgUrl } from './mapImage' import { mapImgUrl } from './mapImage'
/** /**
@@ -94,6 +95,7 @@ export default async function getPageProperties(
properties.type = properties.type?.[0] || '' properties.type = properties.type?.[0] || ''
properties.status = properties.status?.[0] || '' properties.status = properties.status?.[0] || ''
properties.category = properties.category?.[0] || '' properties.category = properties.category?.[0] || ''
properties.comment = properties.comment?.[0] || ''
// 映射值用户个性化type和status字段的下拉框选项在此映射回代码的英文标识 // 映射值用户个性化type和status字段的下拉框选项在此映射回代码的英文标识
mapProperties(properties) mapProperties(properties)
@@ -185,26 +187,38 @@ export function adjustPageProperties(properties, NOTION_CONFIG) {
properties.name = properties.title ?? '' properties.name = properties.title ?? ''
} }
// 开启伪静态路径 // http or https 开头的视为外链
if (JSON.parse(NOTION_CONFIG?.PSEUDO_STATIC || BLOG.PSEUDO_STATIC)) { if (checkStartWithHttp(properties?.href)) {
if ( properties.href = properties?.slug
!properties?.href?.endsWith('.html') &&
!properties?.href?.startsWith('http')
) {
properties.href += '.html'
}
}
// 检查处理外链
properties.href = checkContainHttp(properties?.href)
? sliceUrlFromHttp(properties?.href)
: convertUrlStartWithOneSlash(properties?.href)
// 设置链接在页内或新页面打开
if (properties.href?.indexOf('http') === 0) {
properties.target = '_blank' properties.target = '_blank'
} else { } else {
properties.target = '_self' properties.target = '_self'
// 伪静态路径右侧拼接.html
if (siteConfig('PSEUDO_STATIC', false, NOTION_CONFIG)) {
if (
!properties?.href?.endsWith('.html') &&
properties?.href !== '' &&
properties?.href !== '#' &&
properties?.href !== '/'
) {
properties.href += '.html'
}
}
// 相对路径转绝对路径url左侧拼接 /
properties.href = convertUrlStartWithOneSlash(properties?.href)
}
// 如果跳转链接是多语言,则在新窗口打开
if (BLOG.NOTION_PAGE_ID.indexOf(',') > 0) {
const siteIds = BLOG.NOTION_PAGE_ID.split(',')
for (let index = 0; index < siteIds.length; index++) {
const siteId = siteIds[index]
const prefix = extractLangPrefix(siteId)
if (getLastSegmentFromUrl(properties.href) === prefix) {
properties.target = '_blank'
}
}
} }
// 密码字段md5 // 密码字段md5
@@ -221,6 +235,10 @@ export function adjustPageProperties(properties, NOTION_CONFIG) {
* @returns * @returns
*/ */
function generateCustomizeSlug(postProperties, NOTION_CONFIG) { function generateCustomizeSlug(postProperties, NOTION_CONFIG) {
// 外链不处理
if (checkStartWithHttp(postProperties.slug)) {
return postProperties.slug
}
let fullPrefix = '' let fullPrefix = ''
const allSlugPatterns = siteConfig( const allSlugPatterns = siteConfig(
'POST_URL_PREFIX', 'POST_URL_PREFIX',
@@ -272,5 +290,10 @@ function generateCustomizeSlug(postProperties, NOTION_CONFIG) {
if (fullPrefix.endsWith('/')) { if (fullPrefix.endsWith('/')) {
fullPrefix = fullPrefix.substring(0, fullPrefix.length - 1) // 去掉尾部部的"/" fullPrefix = fullPrefix.substring(0, fullPrefix.length - 1) // 去掉尾部部的"/"
} }
return `${fullPrefix}/${postProperties.slug ?? postProperties.id}`
if(fullPrefix){
return `${fullPrefix}/${postProperties.slug ?? postProperties.id}`
}else{
return `${postProperties.slug ?? postProperties.id}`
}
} }

View File

@@ -10,7 +10,7 @@ import { deepClone, delay } from '../utils'
* @param {*} slice * @param {*} slice
* @returns * @returns
*/ */
export async function getPostBlocks(id, from, slice) { export async function getPage(id, from, slice) {
const cacheKey = 'page_block_' + id const cacheKey = 'page_block_' + id
let pageBlock = await getDataFromCache(cacheKey) let pageBlock = await getDataFromCache(cacheKey)
if (pageBlock) { if (pageBlock) {
@@ -27,28 +27,6 @@ export async function getPostBlocks(id, from, slice) {
return pageBlock return pageBlock
} }
/**
* 针对在getDataBaseInfoByNotionAPI=>getPostBlocks中获取不到的溢出的block-id用此方法另外抓取
* @param {*} id
* @param {*} from
* @returns
*/
export async function getSingleBlock(id, from) {
const cacheKey = 'single_block_' + id
let pageBlock = await getDataFromCache(cacheKey)
if (pageBlock) {
// console.log('[API<<--缓存]', `from:${from}`, cacheKey)
return pageBlock
}
pageBlock = await getPageWithRetry(id, 'single_' + from)
if (pageBlock) {
await setDataToCache(cacheKey, pageBlock)
}
return pageBlock
}
/** /**
* 调用接口,失败会重试 * 调用接口,失败会重试
* @param {*} id * @param {*} id
@@ -103,50 +81,70 @@ export async function getPageWithRetry(id, from, retryAttempts = 3) {
function filterPostBlocks(id, blockMap, slice) { function filterPostBlocks(id, blockMap, slice) {
const clonePageBlock = deepClone(blockMap) const clonePageBlock = deepClone(blockMap)
let count = 0 let count = 0
const blocksToProcess = Object.keys(clonePageBlock?.block || {})
// 循环遍历文档的每个block // 循环遍历文档的每个block
for (const i in clonePageBlock?.block) { for (let i = 0; i < blocksToProcess.length; i++) {
const b = clonePageBlock?.block[i] const blockId = blocksToProcess[i]
if (slice && slice > 0 && count > slice) { const b = clonePageBlock?.block[blockId]
delete clonePageBlock?.block[i]
continue
}
// 当BlockId等于PageId时移除
if (b?.value?.id === id) {
// 此block含有敏感信息
delete b?.value?.properties
continue
}
count++ if (slice && slice > 0 && count > slice) {
// 处理 c++、c#、汇编等语言名字映射 delete clonePageBlock?.block[blockId]
if (b?.value?.type === 'code') { continue
if (b?.value?.properties?.language?.[0][0] === 'C++') { }
b.value.properties.language[0][0] = 'cpp'
}
if (b?.value?.properties?.language?.[0][0] === 'C#') {
b.value.properties.language[0][0] = 'csharp'
}
if (b?.value?.properties?.language?.[0][0] === 'Assembly') {
b.value.properties.language[0][0] = 'asm6502'
}
}
// 如果是文件或嵌入式PDF需要重新加密签名 // 当BlockId等于PageId时移除
if ( if (b?.value?.id === id) {
(b?.value?.type === 'file' || // 此block含有敏感信息
b?.value?.type === 'pdf' || delete b?.value?.properties
b?.value?.type === 'video' || continue
b?.value?.type === 'audio') && }
b?.value?.properties?.source?.[0][0] &&
b?.value?.properties?.source?.[0][0].indexOf('amazonaws.com') > 0 count++
) {
const oldUrl = b?.value?.properties?.source?.[0][0] if (b?.value?.type === 'sync_block' && b?.value?.children) {
const newUrl = `https://notion.so/signed/${encodeURIComponent(oldUrl)}?table=block&id=${b?.value?.id}` const childBlocks = b.value.children
b.value.properties.source[0][0] = newUrl // 移除同步块
delete clonePageBlock.block[blockId]
// 用子块替代同步块
childBlocks.forEach((childBlock, index) => {
const newBlockId = `${blockId}_child_${index}`
clonePageBlock.block[newBlockId] = childBlock
blocksToProcess.splice(i + index + 1, 0, newBlockId)
})
// 重新处理新加入的子块
i--
continue
}
// 处理 c++、c#、汇编等语言名字映射
if (b?.value?.type === 'code') {
if (b?.value?.properties?.language?.[0][0] === 'C++') {
b.value.properties.language[0][0] = 'cpp'
}
if (b?.value?.properties?.language?.[0][0] === 'C#') {
b.value.properties.language[0][0] = 'csharp'
}
if (b?.value?.properties?.language?.[0][0] === 'Assembly') {
b.value.properties.language[0][0] = 'asm6502'
} }
} }
// 如果是文件或嵌入式PDF需要重新加密签名
if (
(b?.value?.type === 'file' ||
b?.value?.type === 'pdf' ||
b?.value?.type === 'video' ||
b?.value?.type === 'audio') &&
b?.value?.properties?.source?.[0][0] &&
b?.value?.properties?.source?.[0][0].indexOf('amazonaws.com') > 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
}
}
// 去掉不用的字段 // 去掉不用的字段
if (id === BLOG.NOTION_PAGE_ID) { if (id === BLOG.NOTION_PAGE_ID) {
return clonePageBlock return clonePageBlock
@@ -162,6 +160,11 @@ function filterPostBlocks(id, blockMap, slice) {
* @returns * @returns
*/ */
export const fetchInBatches = async (ids, batchSize = 100) => { export const fetchInBatches = async (ids, batchSize = 100) => {
// 如果 ids 不是数组,则将其转换为数组
if (!Array.isArray(ids)) {
ids = [ids]
}
const authToken = BLOG.NOTION_ACCESS_TOKEN || null const authToken = BLOG.NOTION_ACCESS_TOKEN || null
const api = new NotionAPI({ const api = new NotionAPI({
authToken, authToken,
@@ -171,7 +174,7 @@ export const fetchInBatches = async (ids, batchSize = 100) => {
let fetchedBlocks = {} let fetchedBlocks = {}
for (let i = 0; i < ids.length; i += batchSize) { for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize) const batch = ids.slice(i, i + batchSize)
console.log('[API-->>请求] Fetching missing blocks', ids.length) console.log('[API-->>请求] Fetching missing blocks', batch, ids.length)
const start = new Date().getTime() const start = new Date().getTime()
const pageChunk = await api.getBlocks(batch) const pageChunk = await api.getBlocks(batch)
const end = new Date().getTime() const end = new Date().getTime()

View File

@@ -110,6 +110,8 @@ const compressImage = (image, width, quality = 50, fmt = 'webp') => {
return image return image
} }
if (image.includes(".svg")) return image
if (!width || width === 0) { if (!width || width === 0) {
width = siteConfig('IMAGE_COMPRESS_WIDTH') width = siteConfig('IMAGE_COMPRESS_WIDTH')
} }

37
lib/password.js Normal file
View File

@@ -0,0 +1,37 @@
import { isBrowser } from './utils'
/**
* 获取默认密码
* 用户可以通过url中拼接参数输入密码
* 输入过一次的密码会被存储在浏览器中,便于下一次免密访问
* 返回的是一组历史密码,便于客户端多次尝试
*/
export const getPasswordQuery = path => {
// 使用 URL 对象解析 URL
const url = new URL(path, isBrowser ? window.location.origin : '')
// 获取查询参数
const queryParams = Object.fromEntries(url.searchParams.entries())
// 请求中带着密码
if (queryParams.password) {
// 将已输入密码作为默认密码存放在 localStorage便于下次读取并自动尝试
localStorage.setItem('password_default', queryParams.password)
}
// 获取路径部分
const cleanedPath = url.pathname
// 从 localStorage 中获取相关密码
const storedPassword = localStorage.getItem('password_' + cleanedPath)
const defaultPassword = localStorage.getItem('password_default')
// 将所有密码存储在一个数组中,并过滤掉无效值
const passwords = [
queryParams.password,
storedPassword,
defaultPassword
].filter(Boolean)
return passwords
}

View File

@@ -6,7 +6,7 @@ import algoliasearch from 'algoliasearch'
* 生成全文索引 * 生成全文索引
* @param {*} allPages * @param {*} allPages
*/ */
const generateAlgoliaSearch = async({ allPages, force = false }) => { const generateAlgoliaSearch = async ({ allPages, force = false }) => {
allPages?.forEach(p => { allPages?.forEach(p => {
// 判断这篇文章是否需要重新创建索引 // 判断这篇文章是否需要重新创建索引
if (p && !p.password) { if (p && !p.password) {
@@ -19,7 +19,7 @@ const generateAlgoliaSearch = async({ allPages, force = false }) => {
* 上传数据 * 上传数据
* 根据上次修改文章日期和上次更新索引数据判断是否需要更新algolia索引 * 根据上次修改文章日期和上次更新索引数据判断是否需要更新algolia索引
*/ */
const uploadDataToAlgolia = async(post) => { const uploadDataToAlgolia = async post => {
// Connect and authenticate with your Algolia app // Connect and authenticate with your Algolia app
const client = algoliasearch(BLOG.ALGOLIA_APP_ID, BLOG.ALGOLIA_ADMIN_APP_KEY) const client = algoliasearch(BLOG.ALGOLIA_APP_ID, BLOG.ALGOLIA_ADMIN_APP_KEY)
@@ -61,14 +61,18 @@ const uploadDataToAlgolia = async(post) => {
summary: post.summary, summary: post.summary,
lastEditedDate: post.lastEditedDate, // 更新文章时间 lastEditedDate: post.lastEditedDate, // 更新文章时间
lastIndexDate: new Date(), // 更新索引时间 lastIndexDate: new Date(), // 更新索引时间
content: truncate(getPageContentText(post, post.blockMap), 9000) // 索引9000个字节因为api限制总请求内容上限1万个字节 content: truncate(getPageContentText(post, post.blockMap), 8192) // 索引8192个字符API限制总请求内容上限1万个字节
} }
// console.log('更新Algolia索引', record) // console.log('更新Algolia索引', record)
index.saveObject(record).wait().then(r => { index
console.log('Algolia索引更新', r) .saveObject(record)
}).catch(err => { .wait()
console.log('Algolia异常', err) .then(r => {
}) console.log('Algolia索引更新', r)
})
.catch(err => {
console.log('Algolia异常', err)
})
} }
} }

View File

@@ -47,7 +47,12 @@ export const memorize = Component => {
return memo(MemoizedComponent) return memo(MemoizedComponent)
} }
// 转换外链 /**
* 诸如 article/https://test.com 等被错误拼接前缀的slug进行处理
* 转换为 https://test.com
* @param {*} str
* @returns
*/
export function sliceUrlFromHttp(str) { export function sliceUrlFromHttp(str) {
// 检查字符串是否包含http // 检查字符串是否包含http
if (str?.includes('http:') || str?.includes('https:')) { if (str?.includes('http:') || str?.includes('https:')) {
@@ -81,12 +86,25 @@ export function convertUrlStartWithOneSlash(str) {
return str return str
} }
/**
* 是否是一个相对或绝对路径的ur类
* @param {*} str
* @returns
*/
export function isUrl(str) {
if (!str) {
return false
}
return str?.indexOf('/') === 0 || checkStartWithHttp(str)
}
// 检查是否外链 // 检查是否外链
export function checkContainHttp(str) { export function checkStartWithHttp(str) {
// 检查字符串是否包含http // 检查字符串是否包含http
if (str?.includes('http:') || str?.includes('https:')) { if (str?.indexOf('http:') === 0 || str?.indexOf('https:') === 0) {
// 如果包含找到http的位置 // 如果包含找到http的位置
return str?.indexOf('http') > -1 return true
} else { } else {
// 不包含 // 不包含
return false return false
@@ -354,3 +372,19 @@ export const scanAndConvertToLinks = node => {
} }
} }
} }
/**
* 获取url最后一个斜杆后面的内容
* @param {*} url
* @returns
*/
export function getLastSegmentFromUrl(url) {
if (!url) {
return ''
}
// 去掉 URL 中的查询参数部分
const trimmedUrl = url.split('?')[0]
// 获取最后一个斜杠后面的内容
const segments = trimmedUrl.split('/')
return segments[segments.length - 1]
}

View File

@@ -30,4 +30,18 @@ function extractLangId(str) {
} }
} }
module.exports = { extractLangPrefix, extractLangId } /**
* 列表中用过来区分page只需要端的id足够
*/
function getShortId(uuid) {
if (!uuid || uuid.indexOf('-') < 0) {
return uuid
}
// 找到第一个 '-' 的位置
const index = uuid.indexOf('-')
// 截取从开始到第一个 '-' 之前的部分
return uuid.substring(0, index)
}
module.exports = { extractLangPrefix, extractLangId, getShortId }

View File

@@ -1,7 +1,7 @@
/** /**
* 文章相关工具 * 文章相关工具
*/ */
import { checkContainHttp } from '.' import { checkStartWithHttp } from '.'
/** /**
* 获取文章的关联推荐文章列表,目前根据标签关联性筛选 * 获取文章的关联推荐文章列表,目前根据标签关联性筛选
@@ -50,7 +50,7 @@ export function checkSlugHasNoSlash(row) {
} }
return ( return (
(slug.match(/\//g) || []).length === 0 && (slug.match(/\//g) || []).length === 0 &&
!checkContainHttp(slug) && !checkStartWithHttp(slug) &&
row.type.indexOf('Menu') < 0 row.type.indexOf('Menu') < 0
) )
} }
@@ -67,7 +67,7 @@ export function checkSlugHasOneSlash(row) {
} }
return ( return (
(slug.match(/\//g) || []).length === 1 && (slug.match(/\//g) || []).length === 1 &&
!checkContainHttp(slug) && !checkStartWithHttp(slug) &&
row.type.indexOf('Menu') < 0 row.type.indexOf('Menu') < 0
) )
} }
@@ -85,6 +85,6 @@ export function checkSlugHasMorThanTwoSlash(row) {
return ( return (
(slug.match(/\//g) || []).length >= 2 && (slug.match(/\//g) || []).length >= 2 &&
row.type.indexOf('Menu') < 0 && row.type.indexOf('Menu') < 0 &&
!checkContainHttp(slug) !checkStartWithHttp(slug)
) )
} }

View File

@@ -32,6 +32,16 @@ const locales = (function () {
return langs return langs
})() })()
// 编译前执行
// const preBuild = (function () {
// // 删除 public/sitemap.xml 文件 否则会和/pages/sitemap.xml.js 冲突。
// const sitemapPath = path.resolve(__dirname, 'public', 'sitemap.xml')
// if (fs.existsSync(sitemapPath)) {
// fs.unlinkSync(sitemapPath)
// console.log('Deleted existing sitemap.xml from public directory')
// }
// })()
/** /**
* 扫描指定目录下的文件夹名,用于获取所有主题 * 扫描指定目录下的文件夹名,用于获取所有主题
* @param {*} directory * @param {*} directory
@@ -53,7 +63,20 @@ function scanSubdirectories(directory) {
return subdirectories return subdirectories
} }
/**
* @type {import('next').NextConfig}
*/
const nextConfig = { const nextConfig = {
output: process.env.EXPORT ? 'export' : undefined,
// 多语言, 在export时禁用
i18n: process.env.EXPORT
? undefined
: {
defaultLocale: BLOG.LANG.slice(0, 2),
// 支持的所有多语言,按需填写即可
locales
},
images: { images: {
// 图片压缩 // 图片压缩
formats: ['image/avif', 'image/webp'], formats: ['image/avif', 'image/webp'],
@@ -71,90 +94,87 @@ const nextConfig = {
}, },
// 默认将feed重定向至 /public/rss/feed.xml // 默认将feed重定向至 /public/rss/feed.xml
async redirects() { redirects: process.env.EXPORT
return [ ? undefined
{ : async () => {
source: '/feed', return [
destination: '/rss/feed.xml',
permanent: true
}
]
},
// 多语言, 在export时禁用
i18n:
process.env.npm_lifecycle_event === 'export'
? undefined
: {
defaultLocale: BLOG.LANG.slice(0, 2),
// 支持的所有多语言,按需填写即可
locales
},
// 重写url
async rewrites() {
// 处理多语言重定向
const langsRewrites = []
if (BLOG.NOTION_PAGE_ID.indexOf(',') > 0) {
const siteIds = BLOG.NOTION_PAGE_ID.split(',')
const langs = []
for (let index = 0; index < siteIds.length; index++) {
const siteId = siteIds[index]
const prefix = extractLangPrefix(siteId)
// 如果包含前缀 例如 zh , en 等
if (prefix) {
langs.push(prefix)
}
console.log('[Locales]', siteId)
}
// 映射多语言
// 示例: source: '/:locale(zh|en)/:path*' ; :locale() 会将语言放入重写后的 `?locale=` 中。
langsRewrites.push(
{
source: `/:locale(${langs.join('|')})/:path*`,
destination: '/:path*'
},
// 匹配没有路径的情况,例如 [domain]/zh 或 [domain]/en
{
source: `/:locale(${langs.join('|')})`,
destination: '/'
},
// 匹配没有路径的情况,例如 [domain]/zh/ 或 [domain]/en/
{
source: `/:locale(${langs.join('|')})/`,
destination: '/'
}
)
}
return [
...langsRewrites,
// 伪静态重写
{
source: '/:path*.html',
destination: '/:path*'
}
]
},
async headers() {
return [
{
source: '/:path*{/}?',
headers: [
{ key: 'Access-Control-Allow-Credentials', value: 'true' },
{ key: 'Access-Control-Allow-Origin', value: '*' },
{ {
key: 'Access-Control-Allow-Methods', source: '/feed',
value: 'GET,OPTIONS,PATCH,DELETE,POST,PUT' destination: '/rss/feed.xml',
}, permanent: true
{
key: 'Access-Control-Allow-Headers',
value:
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version'
} }
] ]
} },
] // 重写url
}, rewrites: process.env.EXPORT
? undefined
: async () => {
// 处理多语言重定向
const langsRewrites = []
if (BLOG.NOTION_PAGE_ID.indexOf(',') > 0) {
const siteIds = BLOG.NOTION_PAGE_ID.split(',')
const langs = []
for (let index = 0; index < siteIds.length; index++) {
const siteId = siteIds[index]
const prefix = extractLangPrefix(siteId)
// 如果包含前缀 例如 zh , en 等
if (prefix) {
langs.push(prefix)
}
console.log('[Locales]', siteId)
}
// 映射多语言
// 示例: source: '/:locale(zh|en)/:path*' ; :locale() 会将语言放入重写后的 `?locale=` 中。
langsRewrites.push(
{
source: `/:locale(${langs.join('|')})/:path*`,
destination: '/:path*'
},
// 匹配没有路径的情况,例如 [domain]/zh 或 [domain]/en
{
source: `/:locale(${langs.join('|')})`,
destination: '/'
},
// 匹配没有路径的情况,例如 [domain]/zh/ 或 [domain]/en/
{
source: `/:locale(${langs.join('|')})/`,
destination: '/'
}
)
}
return [
...langsRewrites,
// 伪静态重写
{
source: '/:path*.html',
destination: '/:path*'
}
]
},
headers: process.env.EXPORT
? undefined
: async () => {
return [
{
source: '/:path*{/}?',
headers: [
{ key: 'Access-Control-Allow-Credentials', value: 'true' },
{ key: 'Access-Control-Allow-Origin', value: '*' },
{
key: 'Access-Control-Allow-Methods',
value: 'GET,OPTIONS,PATCH,DELETE,POST,PUT'
},
{
key: 'Access-Control-Allow-Headers',
value:
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version'
}
]
}
]
},
webpack: (config, { dev, isServer }) => { webpack: (config, { dev, isServer }) => {
// 动态主题:添加 resolve.alias 配置,将动态路径映射到实际路径 // 动态主题:添加 resolve.alias 配置,将动态路径映射到实际路径
if (!isServer) { if (!isServer) {

View File

@@ -1,6 +1,6 @@
{ {
"name": "notion-next", "name": "notion-next",
"version": "4.5.3", "version": "4.6.1",
"homepage": "https://github.com/tangly1024/NotionNext.git", "homepage": "https://github.com/tangly1024/NotionNext.git",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
@@ -17,8 +17,9 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"post-build": "next-sitemap --config next-sitemap.config.js", "post-build": "next-sitemap --config next-sitemap.config.js",
"export": "next build && next-sitemap --config next-sitemap.config.js && next export", "export": "cross-env EXPORT=true next build && next-sitemap --config next-sitemap.config.js",
"bundle-report": "cross-env ANALYZE=true yarn build" "bundle-report": "cross-env ANALYZE=true next build",
"build-all-in-dev": "cross-env VERCEL_ENV=production next build"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^1.7.15", "@headlessui/react": "^1.7.15",
@@ -29,7 +30,7 @@
"js-md5": "^0.7.3", "js-md5": "^0.7.3",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"memory-cache": "^0.2.0", "memory-cache": "^0.2.0",
"next": "13.3.1", "next": "14.2.4",
"notion-client": "6.15.6", "notion-client": "6.15.6",
"notion-utils": "6.15.6", "notion-utils": "6.15.6",
"react": "^18.2.0", "react": "^18.2.0",

View File

@@ -30,17 +30,17 @@ export async function getStaticPaths() {
const from = 'slug-paths' const from = 'slug-paths'
const { allPages } = await getGlobalData({ from }) const { allPages } = await getGlobalData({ from })
const paths = allPages
?.filter(row => checkSlugHasMorThanTwoSlash(row))
.map(row => ({
params: {
prefix: row.slug.split('/')[0],
slug: row.slug.split('/')[1],
suffix: row.slug.split('/').slice(2)
}
}))
return { return {
paths: allPages paths: paths,
?.filter(row => checkSlugHasMorThanTwoSlash(row))
.map(row => ({
params: {
prefix: row.slug.split('/')[0],
slug: row.slug.split('/')[1],
suffix: row.slug.split('/').slice(1)
}
})),
fallback: true fallback: true
} }
} }
@@ -83,16 +83,18 @@ export async function getStaticProps({
props.post = null props.post = null
return { return {
props, props,
revalidate: siteConfig( revalidate: process.env.EXPORT
'REVALIDATE_SECOND', ? undefined
BLOG.NEXT_REVALIDATE_SECOND, : siteConfig(
props.NOTION_CONFIG 'NEXT_REVALIDATE_SECOND',
) BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
} }
} }
// 文章内容加载 // 文章内容加载
if (!props?.posts?.blockMap) { if (!props?.post?.blockMap) {
props.post.blockMap = await getPostBlocks(props.post.id, from) props.post.blockMap = await getPostBlocks(props.post.id, from)
} }
// 生成全文索引 && JSON.parse(BLOG.ALGOLIA_RECREATE_DATA) // 生成全文索引 && JSON.parse(BLOG.ALGOLIA_RECREATE_DATA)
@@ -122,11 +124,13 @@ export async function getStaticProps({
delete props.allPages delete props.allPages
return { return {
props, props,
revalidate: siteConfig( revalidate: process.env.EXPORT
'NEXT_REVALIDATE_SECOND', ? undefined
BLOG.NEXT_REVALIDATE_SECOND, : siteConfig(
props.NOTION_CONFIG 'NEXT_REVALIDATE_SECOND',
) BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
} }
} }

View File

@@ -72,16 +72,18 @@ export async function getStaticProps({ params: { prefix, slug }, locale }) {
props.post = null props.post = null
return { return {
props, props,
revalidate: siteConfig( revalidate: process.env.EXPORT
'REVALIDATE_SECOND', ? undefined
BLOG.NEXT_REVALIDATE_SECOND, : siteConfig(
props.NOTION_CONFIG 'NEXT_REVALIDATE_SECOND',
) BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
} }
} }
// 文章内容加载 // 文章内容加载
if (!props?.posts?.blockMap) { if (!props?.post?.blockMap) {
props.post.blockMap = await getPostBlocks(props.post.id, from) props.post.blockMap = await getPostBlocks(props.post.id, from)
} }
// 生成全文索引 && JSON.parse(BLOG.ALGOLIA_RECREATE_DATA) // 生成全文索引 && JSON.parse(BLOG.ALGOLIA_RECREATE_DATA)
@@ -111,11 +113,13 @@ export async function getStaticProps({ params: { prefix, slug }, locale }) {
delete props.allPages delete props.allPages
return { return {
props, props,
revalidate: siteConfig( revalidate: process.env.EXPORT
'NEXT_REVALIDATE_SECOND', ? undefined
BLOG.NEXT_REVALIDATE_SECOND, : siteConfig(
props.NOTION_CONFIG 'NEXT_REVALIDATE_SECOND',
) BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
} }
} }

View File

@@ -1,7 +1,10 @@
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
import useNotification from '@/components/Notification'
import { siteConfig } from '@/lib/config' import { siteConfig } from '@/lib/config'
import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData' import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData'
import { useGlobal } from '@/lib/global'
import { getPageTableOfContents } from '@/lib/notion/getPageTableOfContents' import { getPageTableOfContents } from '@/lib/notion/getPageTableOfContents'
import { getPasswordQuery } from '@/lib/password'
import { uploadDataToAlgolia } from '@/lib/plugins/algolia' import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
import { checkSlugHasNoSlash, getRecommendPost } from '@/lib/utils/post' import { checkSlugHasNoSlash, getRecommendPost } from '@/lib/utils/post'
import { getLayoutByTheme } from '@/themes/theme' import { getLayoutByTheme } from '@/themes/theme'
@@ -19,9 +22,11 @@ import { useEffect, useState } from 'react'
const Slug = props => { const Slug = props => {
const { post } = props const { post } = props
const router = useRouter() const router = useRouter()
const { locale } = useGlobal()
// 文章锁🔐 // 文章锁🔐
const [lock, setLock] = useState(post?.password && post?.password !== '') const [lock, setLock] = useState(post?.password && post?.password !== '')
const { showNotification, Notification } = useNotification()
/** /**
* 验证文章密码 * 验证文章密码
@@ -36,6 +41,7 @@ const Slug = props => {
setLock(false) setLock(false)
// 输入密码存入localStorage下次自动提交 // 输入密码存入localStorage下次自动提交
localStorage.setItem('password_' + router.asPath, passInput) localStorage.setItem('password_' + router.asPath, passInput)
showNotification(locale.COMMON.ARTICLE_UNLOCK_TIPS) // 设置解锁成功提示显示
return true return true
} }
return false return false
@@ -56,10 +62,14 @@ const Slug = props => {
} }
} }
// 从localStorage中读取上次记录 自动提交密码 // 读取上次记录 自动提交密码
const passInput = localStorage.getItem('password_' + router.asPath) const passInputs = getPasswordQuery(router.asPath)
if (passInput) { if (passInputs.length > 0) {
validPassword(passInput) for (const passInput of passInputs) {
if (validPassword(passInput)) {
break // 密码验证成功,停止尝试
}
}
} }
}, [post]) }, [post])
@@ -83,7 +93,12 @@ const Slug = props => {
theme: siteConfig('THEME'), theme: siteConfig('THEME'),
router: useRouter() router: useRouter()
}) })
return <Layout {...props} /> return (
<>
<Layout {...props} />
{post?.password && post?.password !== '' && !lock && <Notification />}
</>
)
} }
export async function getStaticPaths() { export async function getStaticPaths() {
@@ -109,7 +124,7 @@ export async function getStaticProps({ params: { prefix }, locale }) {
let fullSlug = prefix let fullSlug = prefix
const from = `slug-props-${fullSlug}` const from = `slug-props-${fullSlug}`
const props = await getGlobalData({ from, locale }) const props = await getGlobalData({ from, locale })
if (siteConfig('PSEUDO_STATIC', BLOG.PSEUDO_STATIC, props.NOTION_CONFIG)) { if (siteConfig('PSEUDO_STATIC', false, props.NOTION_CONFIG)) {
if (!fullSlug.endsWith('.html')) { if (!fullSlug.endsWith('.html')) {
fullSlug += '.html' fullSlug += '.html'
} }
@@ -119,7 +134,7 @@ export async function getStaticProps({ params: { prefix }, locale }) {
props.post = props?.allPages?.find(p => { props.post = props?.allPages?.find(p => {
return ( return (
p.type.indexOf('Menu') < 0 && p.type.indexOf('Menu') < 0 &&
(p.slug === fullSlug || p.id === idToUuid(fullSlug)) (p.slug === prefix || p.id === idToUuid(prefix))
) )
}) })
@@ -136,16 +151,18 @@ export async function getStaticProps({ params: { prefix }, locale }) {
props.post = null props.post = null
return { return {
props, props,
revalidate: siteConfig( revalidate: process.env.EXPORT
'REVALIDATE_SECOND', ? undefined
BLOG.NEXT_REVALIDATE_SECOND, : siteConfig(
props.NOTION_CONFIG 'NEXT_REVALIDATE_SECOND',
) BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
} }
} }
// 文章内容加载 // 文章内容加载
if (!props?.posts?.blockMap) { if (!props?.post?.blockMap) {
props.post.blockMap = await getPostBlocks(props.post.id, from) props.post.blockMap = await getPostBlocks(props.post.id, from)
} }
@@ -176,11 +193,13 @@ export async function getStaticProps({ params: { prefix }, locale }) {
delete props.allPages delete props.allPages
return { return {
props, props,
revalidate: siteConfig( revalidate: process.env.EXPORT
'NEXT_REVALIDATE_SECOND', ? undefined
BLOG.NEXT_REVALIDATE_SECOND, : siteConfig(
props.NOTION_CONFIG 'NEXT_REVALIDATE_SECOND',
) BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
} }
} }

View File

@@ -61,11 +61,13 @@ export async function getStaticProps({ locale }) {
return { return {
props, props,
revalidate: siteConfig( revalidate: process.env.EXPORT
'NEXT_REVALIDATE_SECOND', ? undefined
BLOG.NEXT_REVALIDATE_SECOND, : siteConfig(
props.NOTION_CONFIG 'NEXT_REVALIDATE_SECOND',
) BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
} }
} }

View File

@@ -49,11 +49,13 @@ export async function getStaticProps({ params: { category }, locale }) {
return { return {
props, props,
revalidate: siteConfig( revalidate: process.env.EXPORT
'NEXT_REVALIDATE_SECOND', ? undefined
BLOG.NEXT_REVALIDATE_SECOND, : siteConfig(
props.NOTION_CONFIG 'NEXT_REVALIDATE_SECOND',
) BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
} }
} }

View File

@@ -44,11 +44,13 @@ export async function getStaticProps({ params: { category, page } }) {
return { return {
props, props,
revalidate: siteConfig( revalidate: process.env.EXPORT
'NEXT_REVALIDATE_SECOND', ? undefined
BLOG.NEXT_REVALIDATE_SECOND, : siteConfig(
props.NOTION_CONFIG 'NEXT_REVALIDATE_SECOND',
) BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
} }
} }
@@ -69,7 +71,7 @@ export async function getStaticPaths() {
// 处理文章页数 // 处理文章页数
const postCount = categoryPosts.length const postCount = categoryPosts.length
const totalPages = Math.ceil( const totalPages = Math.ceil(
postCount / siteConfig('POSTS_PER_PAGE', 12, NOTION_CONFIG) postCount / siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG)
) )
if (totalPages > 1) { if (totalPages > 1) {
for (let i = 1; i <= totalPages; i++) { for (let i = 1; i <= totalPages; i++) {

View File

@@ -24,10 +24,12 @@ export async function getStaticProps({ locale }) {
delete props.allPages delete props.allPages
return { return {
props, props,
revalidate: siteConfig( revalidate: process.env.EXPORT
'NEXT_REVALIDATE_SECOND', ? undefined
BLOG.NEXT_REVALIDATE_SECOND, : siteConfig(
props.NOTION_CONFIG 'NEXT_REVALIDATE_SECOND',
) BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
} }
} }

View File

@@ -69,11 +69,13 @@ export async function getStaticProps(req) {
return { return {
props, props,
revalidate: siteConfig( revalidate: process.env.EXPORT
'NEXT_REVALIDATE_SECOND', ? undefined
BLOG.NEXT_REVALIDATE_SECOND, : siteConfig(
props.NOTION_CONFIG 'NEXT_REVALIDATE_SECOND',
) BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
} }
} }

View File

@@ -23,7 +23,7 @@ export async function getStaticPaths({ locale }) {
const from = 'page-paths' const from = 'page-paths'
const { postCount, NOTION_CONFIG } = await getGlobalData({ from, locale }) const { postCount, NOTION_CONFIG } = await getGlobalData({ from, locale })
const totalPages = Math.ceil( const totalPages = Math.ceil(
postCount / siteConfig('POSTS_PER_PAGE', 12, NOTION_CONFIG) postCount / siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG)
) )
return { return {
// remove first page, we 're not gonna handle that. // remove first page, we 're not gonna handle that.
@@ -69,11 +69,13 @@ export async function getStaticProps({ params: { page } }) {
delete props.allPages delete props.allPages
return { return {
props, props,
revalidate: siteConfig( revalidate: process.env.EXPORT
'NEXT_REVALIDATE_SECOND', ? undefined
BLOG.NEXT_REVALIDATE_SECOND, : siteConfig(
props.NOTION_CONFIG 'NEXT_REVALIDATE_SECOND',
) BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
} }
} }

View File

@@ -47,11 +47,13 @@ export async function getStaticProps({ params: { keyword }, locale }) {
props.keyword = keyword props.keyword = keyword
return { return {
props, props,
revalidate: siteConfig( revalidate: process.env.EXPORT
'NEXT_REVALIDATE_SECOND', ? undefined
BLOG.NEXT_REVALIDATE_SECOND, : siteConfig(
props.NOTION_CONFIG 'NEXT_REVALIDATE_SECOND',
) BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
} }
} }

View File

@@ -45,11 +45,13 @@ export async function getStaticProps({ params: { keyword, page }, locale }) {
delete props.allPages delete props.allPages
return { return {
props, props,
revalidate: siteConfig( revalidate: process.env.EXPORT
'NEXT_REVALIDATE_SECOND', ? undefined
BLOG.NEXT_REVALIDATE_SECOND, : siteConfig(
props.NOTION_CONFIG 'NEXT_REVALIDATE_SECOND',
) BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
} }
} }

View File

@@ -54,11 +54,13 @@ export async function getStaticProps({ locale }) {
) )
return { return {
props, props,
revalidate: siteConfig( revalidate: process.env.EXPORT
'NEXT_REVALIDATE_SECOND', ? undefined
BLOG.NEXT_REVALIDATE_SECOND, : siteConfig(
props.NOTION_CONFIG 'NEXT_REVALIDATE_SECOND',
) BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
} }
} }

View File

@@ -27,11 +27,13 @@ export async function getStaticProps(req) {
delete props.allPages delete props.allPages
return { return {
props, props,
revalidate: siteConfig( revalidate: process.env.EXPORT
'NEXT_REVALIDATE_SECOND', ? undefined
BLOG.NEXT_REVALIDATE_SECOND, : siteConfig(
props.NOTION_CONFIG 'NEXT_REVALIDATE_SECOND',
) BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
} }
} }

View File

@@ -27,11 +27,13 @@ export async function getStaticProps(req) {
delete props.allPages delete props.allPages
return { return {
props, props,
revalidate: siteConfig( revalidate: process.env.EXPORT
'NEXT_REVALIDATE_SECOND', ? undefined
BLOG.NEXT_REVALIDATE_SECOND, : siteConfig(
props.NOTION_CONFIG 'NEXT_REVALIDATE_SECOND',
) BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
} }
} }

View File

@@ -27,7 +27,6 @@ export const getServerSideProps = async ctx => {
'Cache-Control', 'Cache-Control',
'public, max-age=3600, stale-while-revalidate=59' 'public, max-age=3600, stale-while-revalidate=59'
) )
console.log('fff', fields)
return getServerSideSitemap(ctx, fields) return getServerSideSitemap(ctx, fields)
} }
@@ -55,7 +54,7 @@ function generateLocalesSitemap(link, allPages, locale) {
priority: '0.7' priority: '0.7'
}, },
{ {
loc: `${link}${locale}/feed`, loc: `${link}${locale}/rss/feed.xml`,
lastmod: new Date().toISOString().split('T')[0], lastmod: new Date().toISOString().split('T')[0],
changefreq: 'daily', changefreq: 'daily',
priority: '0.7' priority: '0.7'

View File

@@ -45,11 +45,13 @@ export async function getStaticProps({ params: { tag }, locale }) {
delete props.allPages delete props.allPages
return { return {
props, props,
revalidate: siteConfig( revalidate: process.env.EXPORT
'NEXT_REVALIDATE_SECOND', ? undefined
BLOG.NEXT_REVALIDATE_SECOND, : siteConfig(
props.NOTION_CONFIG 'NEXT_REVALIDATE_SECOND',
) BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
} }
} }

View File

@@ -34,11 +34,13 @@ export async function getStaticProps({ params: { tag, page }, locale }) {
delete props.allPages delete props.allPages
return { return {
props, props,
revalidate: siteConfig( revalidate: process.env.EXPORT
'NEXT_REVALIDATE_SECOND', ? undefined
BLOG.NEXT_REVALIDATE_SECOND, : siteConfig(
props.NOTION_CONFIG 'NEXT_REVALIDATE_SECOND',
) BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
} }
} }
@@ -54,7 +56,7 @@ export async function getStaticPaths() {
// 处理文章页数 // 处理文章页数
const postCount = tagPosts.length const postCount = tagPosts.length
const totalPages = Math.ceil( const totalPages = Math.ceil(
postCount / siteConfig('POSTS_PER_PAGE', 12, NOTION_CONFIG) postCount / siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG)
) )
if (totalPages > 1) { if (totalPages > 1) {
for (let i = 1; i <= totalPages; i++) { for (let i = 1; i <= totalPages; i++) {

View File

@@ -26,11 +26,13 @@ export async function getStaticProps(req) {
delete props.allPages delete props.allPages
return { return {
props, props,
revalidate: siteConfig( revalidate: process.env.EXPORT
'NEXT_REVALIDATE_SECOND', ? undefined
BLOG.NEXT_REVALIDATE_SECOND, : siteConfig(
props.NOTION_CONFIG 'NEXT_REVALIDATE_SECOND',
) BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
} }
} }

916
public/css/aos.css Normal file
View File

@@ -0,0 +1,916 @@
[data-aos][data-aos][data-aos-duration='50'],
body[data-aos-duration='50'] [data-aos] {
transition-duration: 50ms;
}
[data-aos][data-aos][data-aos-delay='50'],
body[data-aos-delay='50'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='50'].aos-animate,
body[data-aos-delay='50'] [data-aos].aos-animate {
transition-delay: 50ms;
}
[data-aos][data-aos][data-aos-duration='100'],
body[data-aos-duration='100'] [data-aos] {
transition-duration: 0.1s;
}
[data-aos][data-aos][data-aos-delay='100'],
body[data-aos-delay='100'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='100'].aos-animate,
body[data-aos-delay='100'] [data-aos].aos-animate {
transition-delay: 0.1s;
}
[data-aos][data-aos][data-aos-duration='150'],
body[data-aos-duration='150'] [data-aos] {
transition-duration: 0.15s;
}
[data-aos][data-aos][data-aos-delay='150'],
body[data-aos-delay='150'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='150'].aos-animate,
body[data-aos-delay='150'] [data-aos].aos-animate {
transition-delay: 0.15s;
}
[data-aos][data-aos][data-aos-duration='200'],
body[data-aos-duration='200'] [data-aos] {
transition-duration: 0.2s;
}
[data-aos][data-aos][data-aos-delay='200'],
body[data-aos-delay='200'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='200'].aos-animate,
body[data-aos-delay='200'] [data-aos].aos-animate {
transition-delay: 0.2s;
}
[data-aos][data-aos][data-aos-duration='250'],
body[data-aos-duration='250'] [data-aos] {
transition-duration: 0.25s;
}
[data-aos][data-aos][data-aos-delay='250'],
body[data-aos-delay='250'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='250'].aos-animate,
body[data-aos-delay='250'] [data-aos].aos-animate {
transition-delay: 0.25s;
}
[data-aos][data-aos][data-aos-duration='300'],
body[data-aos-duration='300'] [data-aos] {
transition-duration: 0.3s;
}
[data-aos][data-aos][data-aos-delay='300'],
body[data-aos-delay='300'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='300'].aos-animate,
body[data-aos-delay='300'] [data-aos].aos-animate {
transition-delay: 0.3s;
}
[data-aos][data-aos][data-aos-duration='350'],
body[data-aos-duration='350'] [data-aos] {
transition-duration: 0.35s;
}
[data-aos][data-aos][data-aos-delay='350'],
body[data-aos-delay='350'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='350'].aos-animate,
body[data-aos-delay='350'] [data-aos].aos-animate {
transition-delay: 0.35s;
}
[data-aos][data-aos][data-aos-duration='400'],
body[data-aos-duration='400'] [data-aos] {
transition-duration: 0.4s;
}
[data-aos][data-aos][data-aos-delay='400'],
body[data-aos-delay='400'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='400'].aos-animate,
body[data-aos-delay='400'] [data-aos].aos-animate {
transition-delay: 0.4s;
}
[data-aos][data-aos][data-aos-duration='450'],
body[data-aos-duration='450'] [data-aos] {
transition-duration: 0.45s;
}
[data-aos][data-aos][data-aos-delay='450'],
body[data-aos-delay='450'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='450'].aos-animate,
body[data-aos-delay='450'] [data-aos].aos-animate {
transition-delay: 0.45s;
}
[data-aos][data-aos][data-aos-duration='500'],
body[data-aos-duration='500'] [data-aos] {
transition-duration: 0.5s;
}
[data-aos][data-aos][data-aos-delay='500'],
body[data-aos-delay='500'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='500'].aos-animate,
body[data-aos-delay='500'] [data-aos].aos-animate {
transition-delay: 0.5s;
}
[data-aos][data-aos][data-aos-duration='550'],
body[data-aos-duration='550'] [data-aos] {
transition-duration: 0.55s;
}
[data-aos][data-aos][data-aos-delay='550'],
body[data-aos-delay='550'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='550'].aos-animate,
body[data-aos-delay='550'] [data-aos].aos-animate {
transition-delay: 0.55s;
}
[data-aos][data-aos][data-aos-duration='600'],
body[data-aos-duration='600'] [data-aos] {
transition-duration: 0.6s;
}
[data-aos][data-aos][data-aos-delay='600'],
body[data-aos-delay='600'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='600'].aos-animate,
body[data-aos-delay='600'] [data-aos].aos-animate {
transition-delay: 0.6s;
}
[data-aos][data-aos][data-aos-duration='650'],
body[data-aos-duration='650'] [data-aos] {
transition-duration: 0.65s;
}
[data-aos][data-aos][data-aos-delay='650'],
body[data-aos-delay='650'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='650'].aos-animate,
body[data-aos-delay='650'] [data-aos].aos-animate {
transition-delay: 0.65s;
}
[data-aos][data-aos][data-aos-duration='700'],
body[data-aos-duration='700'] [data-aos] {
transition-duration: 0.7s;
}
[data-aos][data-aos][data-aos-delay='700'],
body[data-aos-delay='700'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='700'].aos-animate,
body[data-aos-delay='700'] [data-aos].aos-animate {
transition-delay: 0.7s;
}
[data-aos][data-aos][data-aos-duration='750'],
body[data-aos-duration='750'] [data-aos] {
transition-duration: 0.75s;
}
[data-aos][data-aos][data-aos-delay='750'],
body[data-aos-delay='750'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='750'].aos-animate,
body[data-aos-delay='750'] [data-aos].aos-animate {
transition-delay: 0.75s;
}
[data-aos][data-aos][data-aos-duration='800'],
body[data-aos-duration='800'] [data-aos] {
transition-duration: 0.8s;
}
[data-aos][data-aos][data-aos-delay='800'],
body[data-aos-delay='800'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='800'].aos-animate,
body[data-aos-delay='800'] [data-aos].aos-animate {
transition-delay: 0.8s;
}
[data-aos][data-aos][data-aos-duration='850'],
body[data-aos-duration='850'] [data-aos] {
transition-duration: 0.85s;
}
[data-aos][data-aos][data-aos-delay='850'],
body[data-aos-delay='850'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='850'].aos-animate,
body[data-aos-delay='850'] [data-aos].aos-animate {
transition-delay: 0.85s;
}
[data-aos][data-aos][data-aos-duration='900'],
body[data-aos-duration='900'] [data-aos] {
transition-duration: 0.9s;
}
[data-aos][data-aos][data-aos-delay='900'],
body[data-aos-delay='900'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='900'].aos-animate,
body[data-aos-delay='900'] [data-aos].aos-animate {
transition-delay: 0.9s;
}
[data-aos][data-aos][data-aos-duration='950'],
body[data-aos-duration='950'] [data-aos] {
transition-duration: 0.95s;
}
[data-aos][data-aos][data-aos-delay='950'],
body[data-aos-delay='950'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='950'].aos-animate,
body[data-aos-delay='950'] [data-aos].aos-animate {
transition-delay: 0.95s;
}
[data-aos][data-aos][data-aos-duration='1000'],
body[data-aos-duration='1000'] [data-aos] {
transition-duration: 1s;
}
[data-aos][data-aos][data-aos-delay='1000'],
body[data-aos-delay='1000'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='1000'].aos-animate,
body[data-aos-delay='1000'] [data-aos].aos-animate {
transition-delay: 1s;
}
[data-aos][data-aos][data-aos-duration='1050'],
body[data-aos-duration='1050'] [data-aos] {
transition-duration: 1.05s;
}
[data-aos][data-aos][data-aos-delay='1050'],
body[data-aos-delay='1050'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='1050'].aos-animate,
body[data-aos-delay='1050'] [data-aos].aos-animate {
transition-delay: 1.05s;
}
[data-aos][data-aos][data-aos-duration='1100'],
body[data-aos-duration='1100'] [data-aos] {
transition-duration: 1.1s;
}
[data-aos][data-aos][data-aos-delay='1100'],
body[data-aos-delay='1100'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='1100'].aos-animate,
body[data-aos-delay='1100'] [data-aos].aos-animate {
transition-delay: 1.1s;
}
[data-aos][data-aos][data-aos-duration='1150'],
body[data-aos-duration='1150'] [data-aos] {
transition-duration: 1.15s;
}
[data-aos][data-aos][data-aos-delay='1150'],
body[data-aos-delay='1150'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='1150'].aos-animate,
body[data-aos-delay='1150'] [data-aos].aos-animate {
transition-delay: 1.15s;
}
[data-aos][data-aos][data-aos-duration='1200'],
body[data-aos-duration='1200'] [data-aos] {
transition-duration: 1.2s;
}
[data-aos][data-aos][data-aos-delay='1200'],
body[data-aos-delay='1200'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='1200'].aos-animate,
body[data-aos-delay='1200'] [data-aos].aos-animate {
transition-delay: 1.2s;
}
[data-aos][data-aos][data-aos-duration='1250'],
body[data-aos-duration='1250'] [data-aos] {
transition-duration: 1.25s;
}
[data-aos][data-aos][data-aos-delay='1250'],
body[data-aos-delay='1250'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='1250'].aos-animate,
body[data-aos-delay='1250'] [data-aos].aos-animate {
transition-delay: 1.25s;
}
[data-aos][data-aos][data-aos-duration='1300'],
body[data-aos-duration='1300'] [data-aos] {
transition-duration: 1.3s;
}
[data-aos][data-aos][data-aos-delay='1300'],
body[data-aos-delay='1300'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='1300'].aos-animate,
body[data-aos-delay='1300'] [data-aos].aos-animate {
transition-delay: 1.3s;
}
[data-aos][data-aos][data-aos-duration='1350'],
body[data-aos-duration='1350'] [data-aos] {
transition-duration: 1.35s;
}
[data-aos][data-aos][data-aos-delay='1350'],
body[data-aos-delay='1350'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='1350'].aos-animate,
body[data-aos-delay='1350'] [data-aos].aos-animate {
transition-delay: 1.35s;
}
[data-aos][data-aos][data-aos-duration='1400'],
body[data-aos-duration='1400'] [data-aos] {
transition-duration: 1.4s;
}
[data-aos][data-aos][data-aos-delay='1400'],
body[data-aos-delay='1400'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='1400'].aos-animate,
body[data-aos-delay='1400'] [data-aos].aos-animate {
transition-delay: 1.4s;
}
[data-aos][data-aos][data-aos-duration='1450'],
body[data-aos-duration='1450'] [data-aos] {
transition-duration: 1.45s;
}
[data-aos][data-aos][data-aos-delay='1450'],
body[data-aos-delay='1450'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='1450'].aos-animate,
body[data-aos-delay='1450'] [data-aos].aos-animate {
transition-delay: 1.45s;
}
[data-aos][data-aos][data-aos-duration='1500'],
body[data-aos-duration='1500'] [data-aos] {
transition-duration: 1.5s;
}
[data-aos][data-aos][data-aos-delay='1500'],
body[data-aos-delay='1500'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='1500'].aos-animate,
body[data-aos-delay='1500'] [data-aos].aos-animate {
transition-delay: 1.5s;
}
[data-aos][data-aos][data-aos-duration='1550'],
body[data-aos-duration='1550'] [data-aos] {
transition-duration: 1.55s;
}
[data-aos][data-aos][data-aos-delay='1550'],
body[data-aos-delay='1550'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='1550'].aos-animate,
body[data-aos-delay='1550'] [data-aos].aos-animate {
transition-delay: 1.55s;
}
[data-aos][data-aos][data-aos-duration='1600'],
body[data-aos-duration='1600'] [data-aos] {
transition-duration: 1.6s;
}
[data-aos][data-aos][data-aos-delay='1600'],
body[data-aos-delay='1600'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='1600'].aos-animate,
body[data-aos-delay='1600'] [data-aos].aos-animate {
transition-delay: 1.6s;
}
[data-aos][data-aos][data-aos-duration='1650'],
body[data-aos-duration='1650'] [data-aos] {
transition-duration: 1.65s;
}
[data-aos][data-aos][data-aos-delay='1650'],
body[data-aos-delay='1650'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='1650'].aos-animate,
body[data-aos-delay='1650'] [data-aos].aos-animate {
transition-delay: 1.65s;
}
[data-aos][data-aos][data-aos-duration='1700'],
body[data-aos-duration='1700'] [data-aos] {
transition-duration: 1.7s;
}
[data-aos][data-aos][data-aos-delay='1700'],
body[data-aos-delay='1700'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='1700'].aos-animate,
body[data-aos-delay='1700'] [data-aos].aos-animate {
transition-delay: 1.7s;
}
[data-aos][data-aos][data-aos-duration='1750'],
body[data-aos-duration='1750'] [data-aos] {
transition-duration: 1.75s;
}
[data-aos][data-aos][data-aos-delay='1750'],
body[data-aos-delay='1750'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='1750'].aos-animate,
body[data-aos-delay='1750'] [data-aos].aos-animate {
transition-delay: 1.75s;
}
[data-aos][data-aos][data-aos-duration='1800'],
body[data-aos-duration='1800'] [data-aos] {
transition-duration: 1.8s;
}
[data-aos][data-aos][data-aos-delay='1800'],
body[data-aos-delay='1800'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='1800'].aos-animate,
body[data-aos-delay='1800'] [data-aos].aos-animate {
transition-delay: 1.8s;
}
[data-aos][data-aos][data-aos-duration='1850'],
body[data-aos-duration='1850'] [data-aos] {
transition-duration: 1.85s;
}
[data-aos][data-aos][data-aos-delay='1850'],
body[data-aos-delay='1850'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='1850'].aos-animate,
body[data-aos-delay='1850'] [data-aos].aos-animate {
transition-delay: 1.85s;
}
[data-aos][data-aos][data-aos-duration='1900'],
body[data-aos-duration='1900'] [data-aos] {
transition-duration: 1.9s;
}
[data-aos][data-aos][data-aos-delay='1900'],
body[data-aos-delay='1900'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='1900'].aos-animate,
body[data-aos-delay='1900'] [data-aos].aos-animate {
transition-delay: 1.9s;
}
[data-aos][data-aos][data-aos-duration='1950'],
body[data-aos-duration='1950'] [data-aos] {
transition-duration: 1.95s;
}
[data-aos][data-aos][data-aos-delay='1950'],
body[data-aos-delay='1950'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='1950'].aos-animate,
body[data-aos-delay='1950'] [data-aos].aos-animate {
transition-delay: 1.95s;
}
[data-aos][data-aos][data-aos-duration='2000'],
body[data-aos-duration='2000'] [data-aos] {
transition-duration: 2s;
}
[data-aos][data-aos][data-aos-delay='2000'],
body[data-aos-delay='2000'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='2000'].aos-animate,
body[data-aos-delay='2000'] [data-aos].aos-animate {
transition-delay: 2s;
}
[data-aos][data-aos][data-aos-duration='2050'],
body[data-aos-duration='2050'] [data-aos] {
transition-duration: 2.05s;
}
[data-aos][data-aos][data-aos-delay='2050'],
body[data-aos-delay='2050'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='2050'].aos-animate,
body[data-aos-delay='2050'] [data-aos].aos-animate {
transition-delay: 2.05s;
}
[data-aos][data-aos][data-aos-duration='2100'],
body[data-aos-duration='2100'] [data-aos] {
transition-duration: 2.1s;
}
[data-aos][data-aos][data-aos-delay='2100'],
body[data-aos-delay='2100'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='2100'].aos-animate,
body[data-aos-delay='2100'] [data-aos].aos-animate {
transition-delay: 2.1s;
}
[data-aos][data-aos][data-aos-duration='2150'],
body[data-aos-duration='2150'] [data-aos] {
transition-duration: 2.15s;
}
[data-aos][data-aos][data-aos-delay='2150'],
body[data-aos-delay='2150'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='2150'].aos-animate,
body[data-aos-delay='2150'] [data-aos].aos-animate {
transition-delay: 2.15s;
}
[data-aos][data-aos][data-aos-duration='2200'],
body[data-aos-duration='2200'] [data-aos] {
transition-duration: 2.2s;
}
[data-aos][data-aos][data-aos-delay='2200'],
body[data-aos-delay='2200'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='2200'].aos-animate,
body[data-aos-delay='2200'] [data-aos].aos-animate {
transition-delay: 2.2s;
}
[data-aos][data-aos][data-aos-duration='2250'],
body[data-aos-duration='2250'] [data-aos] {
transition-duration: 2.25s;
}
[data-aos][data-aos][data-aos-delay='2250'],
body[data-aos-delay='2250'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='2250'].aos-animate,
body[data-aos-delay='2250'] [data-aos].aos-animate {
transition-delay: 2.25s;
}
[data-aos][data-aos][data-aos-duration='2300'],
body[data-aos-duration='2300'] [data-aos] {
transition-duration: 2.3s;
}
[data-aos][data-aos][data-aos-delay='2300'],
body[data-aos-delay='2300'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='2300'].aos-animate,
body[data-aos-delay='2300'] [data-aos].aos-animate {
transition-delay: 2.3s;
}
[data-aos][data-aos][data-aos-duration='2350'],
body[data-aos-duration='2350'] [data-aos] {
transition-duration: 2.35s;
}
[data-aos][data-aos][data-aos-delay='2350'],
body[data-aos-delay='2350'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='2350'].aos-animate,
body[data-aos-delay='2350'] [data-aos].aos-animate {
transition-delay: 2.35s;
}
[data-aos][data-aos][data-aos-duration='2400'],
body[data-aos-duration='2400'] [data-aos] {
transition-duration: 2.4s;
}
[data-aos][data-aos][data-aos-delay='2400'],
body[data-aos-delay='2400'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='2400'].aos-animate,
body[data-aos-delay='2400'] [data-aos].aos-animate {
transition-delay: 2.4s;
}
[data-aos][data-aos][data-aos-duration='2450'],
body[data-aos-duration='2450'] [data-aos] {
transition-duration: 2.45s;
}
[data-aos][data-aos][data-aos-delay='2450'],
body[data-aos-delay='2450'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='2450'].aos-animate,
body[data-aos-delay='2450'] [data-aos].aos-animate {
transition-delay: 2.45s;
}
[data-aos][data-aos][data-aos-duration='2500'],
body[data-aos-duration='2500'] [data-aos] {
transition-duration: 2.5s;
}
[data-aos][data-aos][data-aos-delay='2500'],
body[data-aos-delay='2500'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='2500'].aos-animate,
body[data-aos-delay='2500'] [data-aos].aos-animate {
transition-delay: 2.5s;
}
[data-aos][data-aos][data-aos-duration='2550'],
body[data-aos-duration='2550'] [data-aos] {
transition-duration: 2.55s;
}
[data-aos][data-aos][data-aos-delay='2550'],
body[data-aos-delay='2550'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='2550'].aos-animate,
body[data-aos-delay='2550'] [data-aos].aos-animate {
transition-delay: 2.55s;
}
[data-aos][data-aos][data-aos-duration='2600'],
body[data-aos-duration='2600'] [data-aos] {
transition-duration: 2.6s;
}
[data-aos][data-aos][data-aos-delay='2600'],
body[data-aos-delay='2600'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='2600'].aos-animate,
body[data-aos-delay='2600'] [data-aos].aos-animate {
transition-delay: 2.6s;
}
[data-aos][data-aos][data-aos-duration='2650'],
body[data-aos-duration='2650'] [data-aos] {
transition-duration: 2.65s;
}
[data-aos][data-aos][data-aos-delay='2650'],
body[data-aos-delay='2650'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='2650'].aos-animate,
body[data-aos-delay='2650'] [data-aos].aos-animate {
transition-delay: 2.65s;
}
[data-aos][data-aos][data-aos-duration='2700'],
body[data-aos-duration='2700'] [data-aos] {
transition-duration: 2.7s;
}
[data-aos][data-aos][data-aos-delay='2700'],
body[data-aos-delay='2700'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='2700'].aos-animate,
body[data-aos-delay='2700'] [data-aos].aos-animate {
transition-delay: 2.7s;
}
[data-aos][data-aos][data-aos-duration='2750'],
body[data-aos-duration='2750'] [data-aos] {
transition-duration: 2.75s;
}
[data-aos][data-aos][data-aos-delay='2750'],
body[data-aos-delay='2750'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='2750'].aos-animate,
body[data-aos-delay='2750'] [data-aos].aos-animate {
transition-delay: 2.75s;
}
[data-aos][data-aos][data-aos-duration='2800'],
body[data-aos-duration='2800'] [data-aos] {
transition-duration: 2.8s;
}
[data-aos][data-aos][data-aos-delay='2800'],
body[data-aos-delay='2800'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='2800'].aos-animate,
body[data-aos-delay='2800'] [data-aos].aos-animate {
transition-delay: 2.8s;
}
[data-aos][data-aos][data-aos-duration='2850'],
body[data-aos-duration='2850'] [data-aos] {
transition-duration: 2.85s;
}
[data-aos][data-aos][data-aos-delay='2850'],
body[data-aos-delay='2850'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='2850'].aos-animate,
body[data-aos-delay='2850'] [data-aos].aos-animate {
transition-delay: 2.85s;
}
[data-aos][data-aos][data-aos-duration='2900'],
body[data-aos-duration='2900'] [data-aos] {
transition-duration: 2.9s;
}
[data-aos][data-aos][data-aos-delay='2900'],
body[data-aos-delay='2900'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='2900'].aos-animate,
body[data-aos-delay='2900'] [data-aos].aos-animate {
transition-delay: 2.9s;
}
[data-aos][data-aos][data-aos-duration='2950'],
body[data-aos-duration='2950'] [data-aos] {
transition-duration: 2.95s;
}
[data-aos][data-aos][data-aos-delay='2950'],
body[data-aos-delay='2950'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='2950'].aos-animate,
body[data-aos-delay='2950'] [data-aos].aos-animate {
transition-delay: 2.95s;
}
[data-aos][data-aos][data-aos-duration='3000'],
body[data-aos-duration='3000'] [data-aos] {
transition-duration: 3s;
}
[data-aos][data-aos][data-aos-delay='3000'],
body[data-aos-delay='3000'] [data-aos] {
transition-delay: 0;
}
[data-aos][data-aos][data-aos-delay='3000'].aos-animate,
body[data-aos-delay='3000'] [data-aos].aos-animate {
transition-delay: 3s;
}
[data-aos][data-aos][data-aos-easing='linear'],
body[data-aos-easing='linear'] [data-aos] {
transition-timing-function: cubic-bezier(0.25, 0.25, 0.75, 0.75);
}
[data-aos][data-aos][data-aos-easing='ease'],
body[data-aos-easing='ease'] [data-aos] {
transition-timing-function: ease;
}
[data-aos][data-aos][data-aos-easing='ease-in'],
body[data-aos-easing='ease-in'] [data-aos] {
transition-timing-function: ease-in;
}
[data-aos][data-aos][data-aos-easing='ease-out'],
body[data-aos-easing='ease-out'] [data-aos] {
transition-timing-function: ease-out;
}
[data-aos][data-aos][data-aos-easing='ease-in-out'],
body[data-aos-easing='ease-in-out'] [data-aos] {
transition-timing-function: ease-in-out;
}
[data-aos][data-aos][data-aos-easing='ease-in-back'],
body[data-aos-easing='ease-in-back'] [data-aos] {
transition-timing-function: cubic-bezier(0.6, -0.28, 0.735, 0.045);
}
[data-aos][data-aos][data-aos-easing='ease-out-back'],
body[data-aos-easing='ease-out-back'] [data-aos] {
transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
[data-aos][data-aos][data-aos-easing='ease-in-out-back'],
body[data-aos-easing='ease-in-out-back'] [data-aos] {
transition-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
[data-aos][data-aos][data-aos-easing='ease-in-sine'],
body[data-aos-easing='ease-in-sine'] [data-aos] {
transition-timing-function: cubic-bezier(0.47, 0, 0.745, 0.715);
}
[data-aos][data-aos][data-aos-easing='ease-out-sine'],
body[data-aos-easing='ease-out-sine'] [data-aos] {
transition-timing-function: cubic-bezier(0.39, 0.575, 0.565, 1);
}
[data-aos][data-aos][data-aos-easing='ease-in-out-sine'],
body[data-aos-easing='ease-in-out-sine'] [data-aos] {
transition-timing-function: cubic-bezier(0.445, 0.05, 0.55, 0.95);
}
[data-aos][data-aos][data-aos-easing='ease-in-quad'],
body[data-aos-easing='ease-in-quad'] [data-aos] {
transition-timing-function: cubic-bezier(0.55, 0.085, 0.68, 0.53);
}
[data-aos][data-aos][data-aos-easing='ease-out-quad'],
body[data-aos-easing='ease-out-quad'] [data-aos] {
transition-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
[data-aos][data-aos][data-aos-easing='ease-in-out-quad'],
body[data-aos-easing='ease-in-out-quad'] [data-aos] {
transition-timing-function: cubic-bezier(0.455, 0.03, 0.515, 0.955);
}
[data-aos][data-aos][data-aos-easing='ease-in-cubic'],
body[data-aos-easing='ease-in-cubic'] [data-aos] {
transition-timing-function: cubic-bezier(0.55, 0.085, 0.68, 0.53);
}
[data-aos][data-aos][data-aos-easing='ease-out-cubic'],
body[data-aos-easing='ease-out-cubic'] [data-aos] {
transition-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
[data-aos][data-aos][data-aos-easing='ease-in-out-cubic'],
body[data-aos-easing='ease-in-out-cubic'] [data-aos] {
transition-timing-function: cubic-bezier(0.455, 0.03, 0.515, 0.955);
}
[data-aos][data-aos][data-aos-easing='ease-in-quart'],
body[data-aos-easing='ease-in-quart'] [data-aos] {
transition-timing-function: cubic-bezier(0.55, 0.085, 0.68, 0.53);
}
[data-aos][data-aos][data-aos-easing='ease-out-quart'],
body[data-aos-easing='ease-out-quart'] [data-aos] {
transition-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
[data-aos][data-aos][data-aos-easing='ease-in-out-quart'],
body[data-aos-easing='ease-in-out-quart'] [data-aos] {
transition-timing-function: cubic-bezier(0.455, 0.03, 0.515, 0.955);
}
[data-aos^='fade'][data-aos^='fade'] {
opacity: 0;
transition-property: opacity, transform;
}
[data-aos^='fade'][data-aos^='fade'].aos-animate {
opacity: 1;
transform: translateZ(0);
}
[data-aos='fade-up'] {
transform: translate3d(0, 100px, 0);
}
[data-aos='fade-down'] {
transform: translate3d(0, -100px, 0);
}
[data-aos='fade-right'] {
transform: translate3d(-100px, 0, 0);
}
[data-aos='fade-left'] {
transform: translate3d(100px, 0, 0);
}
[data-aos='fade-up-right'] {
transform: translate3d(-100px, 100px, 0);
}
[data-aos='fade-up-left'] {
transform: translate3d(100px, 100px, 0);
}
[data-aos='fade-down-right'] {
transform: translate3d(-100px, -100px, 0);
}
[data-aos='fade-down-left'] {
transform: translate3d(100px, -100px, 0);
}
[data-aos^='zoom'][data-aos^='zoom'] {
opacity: 0;
transition-property: opacity, transform;
}
[data-aos^='zoom'][data-aos^='zoom'].aos-animate {
opacity: 1;
transform: translateZ(0) scale(1);
}
[data-aos='zoom-in'] {
transform: scale(0.6);
}
[data-aos='zoom-in-up'] {
transform: translate3d(0, 100px, 0) scale(0.6);
}
[data-aos='zoom-in-down'] {
transform: translate3d(0, -100px, 0) scale(0.6);
}
[data-aos='zoom-in-right'] {
transform: translate3d(-100px, 0, 0) scale(0.6);
}
[data-aos='zoom-in-left'] {
transform: translate3d(100px, 0, 0) scale(0.6);
}
[data-aos='zoom-out'] {
transform: scale(1.2);
}
[data-aos='zoom-out-up'] {
transform: translate3d(0, 100px, 0) scale(1.2);
}
[data-aos='zoom-out-down'] {
transform: translate3d(0, -100px, 0) scale(1.2);
}
[data-aos='zoom-out-right'] {
transform: translate3d(-100px, 0, 0) scale(1.2);
}
[data-aos='zoom-out-left'] {
transform: translate3d(100px, 0, 0) scale(1.2);
}
[data-aos^='slide'][data-aos^='slide'] {
transition-property: transform;
}
[data-aos^='slide'][data-aos^='slide'].aos-animate {
transform: translateZ(0);
}
[data-aos='slide-up'] {
transform: translate3d(0, 100%, 0);
}
[data-aos='slide-down'] {
transform: translate3d(0, -100%, 0);
}
[data-aos='slide-right'] {
transform: translate3d(-100%, 0, 0);
}
[data-aos='slide-left'] {
transform: translate3d(100%, 0, 0);
}
[data-aos^='flip'][data-aos^='flip'] {
backface-visibility: hidden;
transition-property: transform;
}
[data-aos='flip-left'] {
transform: perspective(2500px) rotateY(-100deg);
}
[data-aos='flip-left'].aos-animate {
transform: perspective(2500px) rotateY(0);
}
[data-aos='flip-right'] {
transform: perspective(2500px) rotateY(100deg);
}
[data-aos='flip-right'].aos-animate {
transform: perspective(2500px) rotateY(0);
}
[data-aos='flip-up'] {
transform: perspective(2500px) rotateX(-100deg);
}
[data-aos='flip-up'].aos-animate {
transform: perspective(2500px) rotateX(0);
}
[data-aos='flip-down'] {
transform: perspective(2500px) rotateX(100deg);
}
[data-aos='flip-down'].aos-animate {
transform: perspective(2500px) rotateX(0);
}

672
public/js/aos.js Normal file
View File

@@ -0,0 +1,672 @@
!(function (e, t) {
'object' == typeof exports && 'object' == typeof module
? (module.exports = t())
: 'function' == typeof define && define.amd
? define([], t)
: 'object' == typeof exports
? (exports.AOS = t())
: (e.AOS = t())
})(this, function () {
return (function (e) {
function t(o) {
if (n[o]) return n[o].exports
var i = (n[o] = { exports: {}, id: o, loaded: !1 })
return e[o].call(i.exports, i, i.exports, t), (i.loaded = !0), i.exports
}
var n = {}
return (t.m = e), (t.c = n), (t.p = 'dist/'), t(0)
})([
function (e, t, n) {
'use strict'
function o(e) {
return e && e.__esModule ? e : { default: e }
}
var i =
Object.assign ||
function (e) {
for (var t = 1; t < arguments.length; t++) {
var n = arguments[t]
for (var o in n)
Object.prototype.hasOwnProperty.call(n, o) && (e[o] = n[o])
}
return e
},
r = n(1),
a = (o(r), n(6)),
u = o(a),
c = n(7),
s = o(c),
f = n(8),
d = o(f),
l = n(9),
p = o(l),
m = n(10),
b = o(m),
v = n(11),
y = o(v),
g = n(14),
h = o(g),
w = [],
k = !1,
x = {
offset: 120,
delay: 0,
easing: 'ease',
duration: 400,
disable: !1,
once: !1,
startEvent: 'DOMContentLoaded',
throttleDelay: 99,
debounceDelay: 50,
disableMutationObserver: !1
},
j = function () {
var e =
arguments.length > 0 && void 0 !== arguments[0] && arguments[0]
if ((e && (k = !0), k))
return (w = (0, y.default)(w, x)), (0, b.default)(w, x.once), w
},
O = function () {
;(w = (0, h.default)()), j()
},
M = function () {
w.forEach(function (e, t) {
e.node.removeAttribute('data-aos'),
e.node.removeAttribute('data-aos-easing'),
e.node.removeAttribute('data-aos-duration'),
e.node.removeAttribute('data-aos-delay')
})
},
S = function (e) {
return (
e === !0 ||
('mobile' === e && p.default.mobile()) ||
('phone' === e && p.default.phone()) ||
('tablet' === e && p.default.tablet()) ||
('function' == typeof e && e() === !0)
)
},
_ = function (e) {
;(x = i(x, e)), (w = (0, h.default)())
var t = document.all && !window.atob
return S(x.disable) || t
? M()
: (x.disableMutationObserver ||
d.default.isSupported() ||
(console.info(
'\n aos: MutationObserver is not supported on this browser,\n code mutations observing has been disabled.\n You may have to call "refreshHard()" by yourself.\n '
),
(x.disableMutationObserver = !0)),
document
.querySelector('body')
.setAttribute('data-aos-easing', x.easing),
document
.querySelector('body')
.setAttribute('data-aos-duration', x.duration),
document
.querySelector('body')
.setAttribute('data-aos-delay', x.delay),
'DOMContentLoaded' === x.startEvent &&
['complete', 'interactive'].indexOf(document.readyState) > -1
? j(!0)
: 'load' === x.startEvent
? window.addEventListener(x.startEvent, function () {
j(!0)
})
: document.addEventListener(x.startEvent, function () {
j(!0)
}),
window.addEventListener(
'resize',
(0, s.default)(j, x.debounceDelay, !0)
),
window.addEventListener(
'orientationchange',
(0, s.default)(j, x.debounceDelay, !0)
),
window.addEventListener(
'scroll',
(0, u.default)(function () {
;(0, b.default)(w, x.once)
}, x.throttleDelay)
),
x.disableMutationObserver || d.default.ready('[data-aos]', O),
w)
}
e.exports = { init: _, refresh: j, refreshHard: O }
},
function (e, t) {},
,
,
,
,
function (e, t) {
;(function (t) {
'use strict'
function n(e, t, n) {
function o(t) {
var n = b,
o = v
return (b = v = void 0), (k = t), (g = e.apply(o, n))
}
function r(e) {
return (k = e), (h = setTimeout(f, t)), M ? o(e) : g
}
function a(e) {
var n = e - w,
o = e - k,
i = t - n
return S ? j(i, y - o) : i
}
function c(e) {
var n = e - w,
o = e - k
return void 0 === w || n >= t || n < 0 || (S && o >= y)
}
function f() {
var e = O()
return c(e) ? d(e) : void (h = setTimeout(f, a(e)))
}
function d(e) {
return (h = void 0), _ && b ? o(e) : ((b = v = void 0), g)
}
function l() {
void 0 !== h && clearTimeout(h), (k = 0), (b = w = v = h = void 0)
}
function p() {
return void 0 === h ? g : d(O())
}
function m() {
var e = O(),
n = c(e)
if (((b = arguments), (v = this), (w = e), n)) {
if (void 0 === h) return r(w)
if (S) return (h = setTimeout(f, t)), o(w)
}
return void 0 === h && (h = setTimeout(f, t)), g
}
var b,
v,
y,
g,
h,
w,
k = 0,
M = !1,
S = !1,
_ = !0
if ('function' != typeof e) throw new TypeError(s)
return (
(t = u(t) || 0),
i(n) &&
((M = !!n.leading),
(S = 'maxWait' in n),
(y = S ? x(u(n.maxWait) || 0, t) : y),
(_ = 'trailing' in n ? !!n.trailing : _)),
(m.cancel = l),
(m.flush = p),
m
)
}
function o(e, t, o) {
var r = !0,
a = !0
if ('function' != typeof e) throw new TypeError(s)
return (
i(o) &&
((r = 'leading' in o ? !!o.leading : r),
(a = 'trailing' in o ? !!o.trailing : a)),
n(e, t, { leading: r, maxWait: t, trailing: a })
)
}
function i(e) {
var t = 'undefined' == typeof e ? 'undefined' : c(e)
return !!e && ('object' == t || 'function' == t)
}
function r(e) {
return (
!!e && 'object' == ('undefined' == typeof e ? 'undefined' : c(e))
)
}
function a(e) {
return (
'symbol' == ('undefined' == typeof e ? 'undefined' : c(e)) ||
(r(e) && k.call(e) == d)
)
}
function u(e) {
if ('number' == typeof e) return e
if (a(e)) return f
if (i(e)) {
var t = 'function' == typeof e.valueOf ? e.valueOf() : e
e = i(t) ? t + '' : t
}
if ('string' != typeof e) return 0 === e ? e : +e
e = e.replace(l, '')
var n = m.test(e)
return n || b.test(e) ? v(e.slice(2), n ? 2 : 8) : p.test(e) ? f : +e
}
var c =
'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator
? function (e) {
return typeof e
}
: function (e) {
return e &&
'function' == typeof Symbol &&
e.constructor === Symbol &&
e !== Symbol.prototype
? 'symbol'
: typeof e
},
s = 'Expected a function',
f = NaN,
d = '[object Symbol]',
l = /^\s+|\s+$/g,
p = /^[-+]0x[0-9a-f]+$/i,
m = /^0b[01]+$/i,
b = /^0o[0-7]+$/i,
v = parseInt,
y =
'object' == ('undefined' == typeof t ? 'undefined' : c(t)) &&
t &&
t.Object === Object &&
t,
g =
'object' == ('undefined' == typeof self ? 'undefined' : c(self)) &&
self &&
self.Object === Object &&
self,
h = y || g || Function('return this')(),
w = Object.prototype,
k = w.toString,
x = Math.max,
j = Math.min,
O = function () {
return h.Date.now()
}
e.exports = o
}).call(
t,
(function () {
return this
})()
)
},
function (e, t) {
;(function (t) {
'use strict'
function n(e, t, n) {
function i(t) {
var n = b,
o = v
return (b = v = void 0), (O = t), (g = e.apply(o, n))
}
function r(e) {
return (O = e), (h = setTimeout(f, t)), M ? i(e) : g
}
function u(e) {
var n = e - w,
o = e - O,
i = t - n
return S ? x(i, y - o) : i
}
function s(e) {
var n = e - w,
o = e - O
return void 0 === w || n >= t || n < 0 || (S && o >= y)
}
function f() {
var e = j()
return s(e) ? d(e) : void (h = setTimeout(f, u(e)))
}
function d(e) {
return (h = void 0), _ && b ? i(e) : ((b = v = void 0), g)
}
function l() {
void 0 !== h && clearTimeout(h), (O = 0), (b = w = v = h = void 0)
}
function p() {
return void 0 === h ? g : d(j())
}
function m() {
var e = j(),
n = s(e)
if (((b = arguments), (v = this), (w = e), n)) {
if (void 0 === h) return r(w)
if (S) return (h = setTimeout(f, t)), i(w)
}
return void 0 === h && (h = setTimeout(f, t)), g
}
var b,
v,
y,
g,
h,
w,
O = 0,
M = !1,
S = !1,
_ = !0
if ('function' != typeof e) throw new TypeError(c)
return (
(t = a(t) || 0),
o(n) &&
((M = !!n.leading),
(S = 'maxWait' in n),
(y = S ? k(a(n.maxWait) || 0, t) : y),
(_ = 'trailing' in n ? !!n.trailing : _)),
(m.cancel = l),
(m.flush = p),
m
)
}
function o(e) {
var t = 'undefined' == typeof e ? 'undefined' : u(e)
return !!e && ('object' == t || 'function' == t)
}
function i(e) {
return (
!!e && 'object' == ('undefined' == typeof e ? 'undefined' : u(e))
)
}
function r(e) {
return (
'symbol' == ('undefined' == typeof e ? 'undefined' : u(e)) ||
(i(e) && w.call(e) == f)
)
}
function a(e) {
if ('number' == typeof e) return e
if (r(e)) return s
if (o(e)) {
var t = 'function' == typeof e.valueOf ? e.valueOf() : e
e = o(t) ? t + '' : t
}
if ('string' != typeof e) return 0 === e ? e : +e
e = e.replace(d, '')
var n = p.test(e)
return n || m.test(e) ? b(e.slice(2), n ? 2 : 8) : l.test(e) ? s : +e
}
var u =
'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator
? function (e) {
return typeof e
}
: function (e) {
return e &&
'function' == typeof Symbol &&
e.constructor === Symbol &&
e !== Symbol.prototype
? 'symbol'
: typeof e
},
c = 'Expected a function',
s = NaN,
f = '[object Symbol]',
d = /^\s+|\s+$/g,
l = /^[-+]0x[0-9a-f]+$/i,
p = /^0b[01]+$/i,
m = /^0o[0-7]+$/i,
b = parseInt,
v =
'object' == ('undefined' == typeof t ? 'undefined' : u(t)) &&
t &&
t.Object === Object &&
t,
y =
'object' == ('undefined' == typeof self ? 'undefined' : u(self)) &&
self &&
self.Object === Object &&
self,
g = v || y || Function('return this')(),
h = Object.prototype,
w = h.toString,
k = Math.max,
x = Math.min,
j = function () {
return g.Date.now()
}
e.exports = n
}).call(
t,
(function () {
return this
})()
)
},
function (e, t) {
'use strict'
function n(e) {
var t = void 0,
o = void 0,
i = void 0
for (t = 0; t < e.length; t += 1) {
if (((o = e[t]), o.dataset && o.dataset.aos)) return !0
if ((i = o.children && n(o.children))) return !0
}
return !1
}
function o() {
return (
window.MutationObserver ||
window.WebKitMutationObserver ||
window.MozMutationObserver
)
}
function i() {
return !!o()
}
function r(e, t) {
var n = window.document,
i = o(),
r = new i(a)
;(u = t),
r.observe(n.documentElement, {
childList: !0,
subtree: !0,
removedNodes: !0
})
}
function a(e) {
e &&
e.forEach(function (e) {
var t = Array.prototype.slice.call(e.addedNodes),
o = Array.prototype.slice.call(e.removedNodes),
i = t.concat(o)
if (n(i)) return u()
})
}
Object.defineProperty(t, '__esModule', { value: !0 })
var u = function () {}
t.default = { isSupported: i, ready: r }
},
function (e, t) {
'use strict'
function n(e, t) {
if (!(e instanceof t))
throw new TypeError('Cannot call a class as a function')
}
function o() {
return navigator.userAgent || navigator.vendor || window.opera || ''
}
Object.defineProperty(t, '__esModule', { value: !0 })
var i = (function () {
function e(e, t) {
for (var n = 0; n < t.length; n++) {
var o = t[n]
;(o.enumerable = o.enumerable || !1),
(o.configurable = !0),
'value' in o && (o.writable = !0),
Object.defineProperty(e, o.key, o)
}
}
return function (t, n, o) {
return n && e(t.prototype, n), o && e(t, o), t
}
})(),
r =
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i,
a =
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i,
u =
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i,
c =
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i,
s = (function () {
function e() {
n(this, e)
}
return (
i(e, [
{
key: 'phone',
value: function () {
var e = o()
return !(!r.test(e) && !a.test(e.substr(0, 4)))
}
},
{
key: 'mobile',
value: function () {
var e = o()
return !(!u.test(e) && !c.test(e.substr(0, 4)))
}
},
{
key: 'tablet',
value: function () {
return this.mobile() && !this.phone()
}
}
]),
e
)
})()
t.default = new s()
},
function (e, t) {
'use strict'
Object.defineProperty(t, '__esModule', { value: !0 })
var n = function (e, t, n) {
var o = e.node.getAttribute('data-aos-once')
t > e.position
? e.node.classList.add('aos-animate')
: 'undefined' != typeof o &&
('false' === o || (!n && 'true' !== o)) &&
e.node.classList.remove('aos-animate')
},
o = function (e, t) {
var o = window.pageYOffset,
i = window.innerHeight
e.forEach(function (e, r) {
n(e, i + o, t)
})
}
t.default = o
},
function (e, t, n) {
'use strict'
function o(e) {
return e && e.__esModule ? e : { default: e }
}
Object.defineProperty(t, '__esModule', { value: !0 })
var i = n(12),
r = o(i),
a = function (e, t) {
return (
e.forEach(function (e, n) {
e.node.classList.add('aos-init'),
(e.position = (0, r.default)(e.node, t.offset))
}),
e
)
}
t.default = a
},
function (e, t, n) {
'use strict'
function o(e) {
return e && e.__esModule ? e : { default: e }
}
Object.defineProperty(t, '__esModule', { value: !0 })
var i = n(13),
r = o(i),
a = function (e, t) {
var n = 0,
o = 0,
i = window.innerHeight,
a = {
offset: e.getAttribute('data-aos-offset'),
anchor: e.getAttribute('data-aos-anchor'),
anchorPlacement: e.getAttribute('data-aos-anchor-placement')
}
switch (
(a.offset && !isNaN(a.offset) && (o = parseInt(a.offset)),
a.anchor &&
document.querySelectorAll(a.anchor) &&
(e = document.querySelectorAll(a.anchor)[0]),
(n = (0, r.default)(e).top),
a.anchorPlacement)
) {
case 'top-bottom':
break
case 'center-bottom':
n += e.offsetHeight / 2
break
case 'bottom-bottom':
n += e.offsetHeight
break
case 'top-center':
n += i / 2
break
case 'bottom-center':
n += i / 2 + e.offsetHeight
break
case 'center-center':
n += i / 2 + e.offsetHeight / 2
break
case 'top-top':
n += i
break
case 'bottom-top':
n += e.offsetHeight + i
break
case 'center-top':
n += e.offsetHeight / 2 + i
}
return a.anchorPlacement || a.offset || isNaN(t) || (o = t), n + o
}
t.default = a
},
function (e, t) {
'use strict'
Object.defineProperty(t, '__esModule', { value: !0 })
var n = function (e) {
for (
var t = 0, n = 0;
e && !isNaN(e.offsetLeft) && !isNaN(e.offsetTop);
)
(t += e.offsetLeft - ('BODY' != e.tagName ? e.scrollLeft : 0)),
(n += e.offsetTop - ('BODY' != e.tagName ? e.scrollTop : 0)),
(e = e.offsetParent)
return { top: n, left: t }
}
t.default = n
},
function (e, t) {
'use strict'
Object.defineProperty(t, '__esModule', { value: !0 })
var n = function (e) {
return (
(e = e || document.querySelectorAll('[data-aos]')),
Array.prototype.map.call(e, function (e) {
return { node: e }
})
)
}
t.default = n
}
])
})

View File

@@ -57,10 +57,10 @@ function createFireworks({ config, anime }) {
function updateCoords(e) { function updateCoords(e) {
pointerX = pointerX =
e.clientX || e.clientX ||
(e.touches[0] ? e.touches[0].clientX : e.changedTouches[0].clientX) (e?.touches[0] ? e.touches[0].clientX : e.changedTouches[0].clientX)
pointerY = pointerY =
e.clientY || e.clientY ||
(e.touches[0] ? e.touches[0].clientY : e.changedTouches[0].clientY) (e?.touches[0] ? e.touches[0].clientY : e.changedTouches[0].clientY)
} }
function setParticuleDirection(p) { function setParticuleDirection(p) {

View File

@@ -123,7 +123,7 @@ function createSakura() {
'style', 'style',
'position: fixed;left: 0;top: 0;pointer-events: none;' 'position: fixed;left: 0;top: 0;pointer-events: none;'
) )
canvas.setAttribute('id', id) canvas.setAttribute('id', 'sakura')
document.getElementsByTagName('body')[0].appendChild(canvas) document.getElementsByTagName('body')[0].appendChild(canvas)
cxt = canvas.getContext('2d') cxt = canvas.getContext('2d')
var sakuraList = new SakuraList() var sakuraList = new SakuraList()

626
public/js/typed.min.js vendored Normal file
View File

@@ -0,0 +1,626 @@
/*!
*
* typed.js - A JavaScript Typing Animation Library
* Author: Matt Boldt <me@mattboldt.com>
* Version: v2.0.12
* Url: https://github.com/mattboldt/typed.js
* License(s): MIT
*
*/
;(function (t, e) {
'object' == typeof exports && 'object' == typeof module
? (module.exports = e())
: 'function' == typeof define && define.amd
? define([], e)
: 'object' == typeof exports
? (exports.Typed = e())
: (t.Typed = e())
})(this, function () {
return (function (t) {
function e(n) {
if (s[n]) return s[n].exports
var i = (s[n] = { exports: {}, id: n, loaded: !1 })
return t[n].call(i.exports, i, i.exports, e), (i.loaded = !0), i.exports
}
var s = {}
return (e.m = t), (e.c = s), (e.p = ''), e(0)
})([
function (t, e, s) {
'use strict'
function n(t, e) {
if (!(t instanceof e))
throw new TypeError('Cannot call a class as a function')
}
Object.defineProperty(e, '__esModule', { value: !0 })
var i = (function () {
function t(t, e) {
for (var s = 0; s < e.length; s++) {
var n = e[s]
;(n.enumerable = n.enumerable || !1),
(n.configurable = !0),
'value' in n && (n.writable = !0),
Object.defineProperty(t, n.key, n)
}
}
return function (e, s, n) {
return s && t(e.prototype, s), n && t(e, n), e
}
})(),
r = s(1),
o = s(3),
a = (function () {
function t(e, s) {
n(this, t), r.initializer.load(this, s, e), this.begin()
}
return (
i(t, [
{
key: 'toggle',
value: function () {
this.pause.status ? this.start() : this.stop()
}
},
{
key: 'stop',
value: function () {
this.typingComplete ||
this.pause.status ||
(this.toggleBlinking(!0),
(this.pause.status = !0),
this.options.onStop(this.arrayPos, this))
}
},
{
key: 'start',
value: function () {
this.typingComplete ||
(this.pause.status &&
((this.pause.status = !1),
this.pause.typewrite
? this.typewrite(
this.pause.curString,
this.pause.curStrPos
)
: this.backspace(
this.pause.curString,
this.pause.curStrPos
),
this.options.onStart(this.arrayPos, this)))
}
},
{
key: 'destroy',
value: function () {
this.reset(!1), this.options.onDestroy(this)
}
},
{
key: 'reset',
value: function () {
var t =
arguments.length <= 0 ||
void 0 === arguments[0] ||
arguments[0]
clearInterval(this.timeout),
this.replaceText(''),
this.cursor &&
this.cursor.parentNode &&
(this.cursor.parentNode.removeChild(this.cursor),
(this.cursor = null)),
(this.strPos = 0),
(this.arrayPos = 0),
(this.curLoop = 0),
t &&
(this.insertCursor(),
this.options.onReset(this),
this.begin())
}
},
{
key: 'begin',
value: function () {
var t = this
this.options.onBegin(this),
(this.typingComplete = !1),
this.shuffleStringsIfNeeded(this),
this.insertCursor(),
this.bindInputFocusEvents && this.bindFocusEvents(),
(this.timeout = setTimeout(function () {
t.currentElContent && 0 !== t.currentElContent.length
? t.backspace(
t.currentElContent,
t.currentElContent.length
)
: t.typewrite(
t.strings[t.sequence[t.arrayPos]],
t.strPos
)
}, this.startDelay))
}
},
{
key: 'typewrite',
value: function (t, e) {
var s = this
this.fadeOut &&
this.el.classList.contains(this.fadeOutClass) &&
(this.el.classList.remove(this.fadeOutClass),
this.cursor &&
this.cursor.classList.remove(this.fadeOutClass))
var n = this.humanizer(this.typeSpeed),
i = 1
return this.pause.status === !0
? void this.setPauseStatus(t, e, !0)
: void (this.timeout = setTimeout(function () {
e = o.htmlParser.typeHtmlChars(t, e, s)
var n = 0,
r = t.substr(e)
if ('^' === r.charAt(0) && /^\^\d+/.test(r)) {
var a = 1
;(r = /\d+/.exec(r)[0]),
(a += r.length),
(n = parseInt(r)),
(s.temporaryPause = !0),
s.options.onTypingPaused(s.arrayPos, s),
(t = t.substring(0, e) + t.substring(e + a)),
s.toggleBlinking(!0)
}
if ('`' === r.charAt(0)) {
for (
;
'`' !== t.substr(e + i).charAt(0) &&
(i++, !(e + i > t.length));
);
var u = t.substring(0, e),
l = t.substring(u.length + 1, e + i),
c = t.substring(e + i + 1)
;(t = u + l + c), i--
}
s.timeout = setTimeout(function () {
s.toggleBlinking(!1),
e >= t.length
? s.doneTyping(t, e)
: s.keepTyping(t, e, i),
s.temporaryPause &&
((s.temporaryPause = !1),
s.options.onTypingResumed(s.arrayPos, s))
}, n)
}, n))
}
},
{
key: 'keepTyping',
value: function (t, e, s) {
0 === e &&
(this.toggleBlinking(!1),
this.options.preStringTyped(this.arrayPos, this)),
(e += s)
var n = t.substr(0, e)
this.replaceText(n), this.typewrite(t, e)
}
},
{
key: 'doneTyping',
value: function (t, e) {
var s = this
this.options.onStringTyped(this.arrayPos, this),
this.toggleBlinking(!0),
(this.arrayPos === this.strings.length - 1 &&
(this.complete(),
this.loop === !1 || this.curLoop === this.loopCount)) ||
(this.timeout = setTimeout(function () {
s.backspace(t, e)
}, this.backDelay))
}
},
{
key: 'backspace',
value: function (t, e) {
var s = this
if (this.pause.status === !0)
return void this.setPauseStatus(t, e, !1)
if (this.fadeOut) return this.initFadeOut()
this.toggleBlinking(!1)
var n = this.humanizer(this.backSpeed)
this.timeout = setTimeout(function () {
e = o.htmlParser.backSpaceHtmlChars(t, e, s)
var n = t.substr(0, e)
if ((s.replaceText(n), s.smartBackspace)) {
var i = s.strings[s.arrayPos + 1]
i && n === i.substr(0, e)
? (s.stopNum = e)
: (s.stopNum = 0)
}
e > s.stopNum
? (e--, s.backspace(t, e))
: e <= s.stopNum &&
(s.arrayPos++,
s.arrayPos === s.strings.length
? ((s.arrayPos = 0),
s.options.onLastStringBackspaced(),
s.shuffleStringsIfNeeded(),
s.begin())
: s.typewrite(s.strings[s.sequence[s.arrayPos]], e))
}, n)
}
},
{
key: 'complete',
value: function () {
this.options.onComplete(this),
this.loop ? this.curLoop++ : (this.typingComplete = !0)
}
},
{
key: 'setPauseStatus',
value: function (t, e, s) {
;(this.pause.typewrite = s),
(this.pause.curString = t),
(this.pause.curStrPos = e)
}
},
{
key: 'toggleBlinking',
value: function (t) {
this.cursor &&
(this.pause.status ||
(this.cursorBlinking !== t &&
((this.cursorBlinking = t),
t
? this.cursor.classList.add('typed-cursor--blink')
: this.cursor.classList.remove(
'typed-cursor--blink'
))))
}
},
{
key: 'humanizer',
value: function (t) {
return Math.round((Math.random() * t) / 2) + t
}
},
{
key: 'shuffleStringsIfNeeded',
value: function () {
this.shuffle &&
(this.sequence = this.sequence.sort(function () {
return Math.random() - 0.5
}))
}
},
{
key: 'initFadeOut',
value: function () {
var t = this
return (
(this.el.className += ' ' + this.fadeOutClass),
this.cursor &&
(this.cursor.className += ' ' + this.fadeOutClass),
setTimeout(function () {
t.arrayPos++,
t.replaceText(''),
t.strings.length > t.arrayPos
? t.typewrite(t.strings[t.sequence[t.arrayPos]], 0)
: (t.typewrite(t.strings[0], 0), (t.arrayPos = 0))
}, this.fadeOutDelay)
)
}
},
{
key: 'replaceText',
value: function (t) {
this.attr
? this.el.setAttribute(this.attr, t)
: this.isInput
? (this.el.value = t)
: 'html' === this.contentType
? (this.el.innerHTML = t)
: (this.el.textContent = t)
}
},
{
key: 'bindFocusEvents',
value: function () {
var t = this
this.isInput &&
(this.el.addEventListener('focus', function (e) {
t.stop()
}),
this.el.addEventListener('blur', function (e) {
;(t.el.value && 0 !== t.el.value.length) || t.start()
}))
}
},
{
key: 'insertCursor',
value: function () {
this.showCursor &&
(this.cursor ||
((this.cursor = document.createElement('span')),
(this.cursor.className = 'typed-cursor'),
this.cursor.setAttribute('aria-hidden', !0),
(this.cursor.innerHTML = this.cursorChar),
this.el.parentNode &&
this.el.parentNode.insertBefore(
this.cursor,
this.el.nextSibling
)))
}
}
]),
t
)
})()
;(e['default'] = a), (t.exports = e['default'])
},
function (t, e, s) {
'use strict'
function n(t) {
return t && t.__esModule ? t : { default: t }
}
function i(t, e) {
if (!(t instanceof e))
throw new TypeError('Cannot call a class as a function')
}
Object.defineProperty(e, '__esModule', { value: !0 })
var r =
Object.assign ||
function (t) {
for (var e = 1; e < arguments.length; e++) {
var s = arguments[e]
for (var n in s)
Object.prototype.hasOwnProperty.call(s, n) && (t[n] = s[n])
}
return t
},
o = (function () {
function t(t, e) {
for (var s = 0; s < e.length; s++) {
var n = e[s]
;(n.enumerable = n.enumerable || !1),
(n.configurable = !0),
'value' in n && (n.writable = !0),
Object.defineProperty(t, n.key, n)
}
}
return function (e, s, n) {
return s && t(e.prototype, s), n && t(e, n), e
}
})(),
a = s(2),
u = n(a),
l = (function () {
function t() {
i(this, t)
}
return (
o(t, [
{
key: 'load',
value: function (t, e, s) {
if (
('string' == typeof s
? (t.el = document.querySelector(s))
: (t.el = s),
(t.options = r({}, u['default'], e)),
(t.isInput = 'input' === t.el.tagName.toLowerCase()),
(t.attr = t.options.attr),
(t.bindInputFocusEvents = t.options.bindInputFocusEvents),
(t.showCursor = !t.isInput && t.options.showCursor),
(t.cursorChar = t.options.cursorChar),
(t.cursorBlinking = !0),
(t.elContent = t.attr
? t.el.getAttribute(t.attr)
: t.el.textContent),
(t.contentType = t.options.contentType),
(t.typeSpeed = t.options.typeSpeed),
(t.startDelay = t.options.startDelay),
(t.backSpeed = t.options.backSpeed),
(t.smartBackspace = t.options.smartBackspace),
(t.backDelay = t.options.backDelay),
(t.fadeOut = t.options.fadeOut),
(t.fadeOutClass = t.options.fadeOutClass),
(t.fadeOutDelay = t.options.fadeOutDelay),
(t.isPaused = !1),
(t.strings = t.options.strings.map(function (t) {
return t.trim()
})),
'string' == typeof t.options.stringsElement
? (t.stringsElement = document.querySelector(
t.options.stringsElement
))
: (t.stringsElement = t.options.stringsElement),
t.stringsElement)
) {
;(t.strings = []), (t.stringsElement.style.display = 'none')
var n = Array.prototype.slice.apply(
t.stringsElement.children
),
i = n.length
if (i)
for (var o = 0; o < i; o += 1) {
var a = n[o]
t.strings.push(a.innerHTML.trim())
}
}
;(t.strPos = 0),
(t.arrayPos = 0),
(t.stopNum = 0),
(t.loop = t.options.loop),
(t.loopCount = t.options.loopCount),
(t.curLoop = 0),
(t.shuffle = t.options.shuffle),
(t.sequence = []),
(t.pause = {
status: !1,
typewrite: !0,
curString: '',
curStrPos: 0
}),
(t.typingComplete = !1)
for (var o in t.strings) t.sequence[o] = o
;(t.currentElContent = this.getCurrentElContent(t)),
(t.autoInsertCss = t.options.autoInsertCss),
this.appendAnimationCss(t)
}
},
{
key: 'getCurrentElContent',
value: function (t) {
var e = ''
return (e = t.attr
? t.el.getAttribute(t.attr)
: t.isInput
? t.el.value
: 'html' === t.contentType
? t.el.innerHTML
: t.el.textContent)
}
},
{
key: 'appendAnimationCss',
value: function (t) {
var e = 'data-typed-js-css'
if (
t.autoInsertCss &&
(t.showCursor || t.fadeOut) &&
!document.querySelector('[' + e + ']')
) {
var s = document.createElement('style')
;(s.type = 'text/css'), s.setAttribute(e, !0)
var n = ''
t.showCursor &&
(n +=
'\n .typed-cursor{\n opacity: 1;\n }\n .typed-cursor.typed-cursor--blink{\n animation: typedjsBlink 0.7s infinite;\n -webkit-animation: typedjsBlink 0.7s infinite;\n animation: typedjsBlink 0.7s infinite;\n }\n @keyframes typedjsBlink{\n 50% { opacity: 0.0; }\n }\n @-webkit-keyframes typedjsBlink{\n 0% { opacity: 1; }\n 50% { opacity: 0.0; }\n 100% { opacity: 1; }\n }\n '),
t.fadeOut &&
(n +=
'\n .typed-fade-out{\n opacity: 0;\n transition: opacity .25s;\n }\n .typed-cursor.typed-cursor--blink.typed-fade-out{\n -webkit-animation: 0;\n animation: 0;\n }\n '),
0 !== s.length &&
((s.innerHTML = n), document.body.appendChild(s))
}
}
}
]),
t
)
})()
e['default'] = l
var c = new l()
e.initializer = c
},
function (t, e) {
'use strict'
Object.defineProperty(e, '__esModule', { value: !0 })
var s = {
strings: [
'These are the default values...',
'You know what you should do?',
'Use your own!',
'Have a great day!'
],
stringsElement: null,
typeSpeed: 0,
startDelay: 0,
backSpeed: 0,
smartBackspace: !0,
shuffle: !1,
backDelay: 700,
fadeOut: !1,
fadeOutClass: 'typed-fade-out',
fadeOutDelay: 500,
loop: !1,
loopCount: 1 / 0,
showCursor: !0,
cursorChar: '|',
autoInsertCss: !0,
attr: null,
bindInputFocusEvents: !1,
contentType: 'html',
onBegin: function (t) {},
onComplete: function (t) {},
preStringTyped: function (t, e) {},
onStringTyped: function (t, e) {},
onLastStringBackspaced: function (t) {},
onTypingPaused: function (t, e) {},
onTypingResumed: function (t, e) {},
onReset: function (t) {},
onStop: function (t, e) {},
onStart: function (t, e) {},
onDestroy: function (t) {}
}
;(e['default'] = s), (t.exports = e['default'])
},
function (t, e) {
'use strict'
function s(t, e) {
if (!(t instanceof e))
throw new TypeError('Cannot call a class as a function')
}
Object.defineProperty(e, '__esModule', { value: !0 })
var n = (function () {
function t(t, e) {
for (var s = 0; s < e.length; s++) {
var n = e[s]
;(n.enumerable = n.enumerable || !1),
(n.configurable = !0),
'value' in n && (n.writable = !0),
Object.defineProperty(t, n.key, n)
}
}
return function (e, s, n) {
return s && t(e.prototype, s), n && t(e, n), e
}
})(),
i = (function () {
function t() {
s(this, t)
}
return (
n(t, [
{
key: 'typeHtmlChars',
value: function (t, e, s) {
if ('html' !== s.contentType) return e
var n = t.substr(e).charAt(0)
if ('<' === n || '&' === n) {
var i = ''
for (
i = '<' === n ? '>' : ';';
t.substr(e + 1).charAt(0) !== i &&
(e++, !(e + 1 > t.length));
);
e++
}
return e
}
},
{
key: 'backSpaceHtmlChars',
value: function (t, e, s) {
if ('html' !== s.contentType) return e
var n = t.substr(e).charAt(0)
if ('>' === n || ';' === n) {
var i = ''
for (
i = '>' === n ? '<' : '&';
t.substr(e - 1).charAt(0) !== i && (e--, !(e < 0));
);
e--
}
return e
}
}
]),
t
)
})()
e['default'] = i
var r = new i()
e.htmlParser = r
}
])
})
//# sourceMappingURL=typed.min.js.map

View File

@@ -0,0 +1 @@
<svg t="1717576439356" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6277" width="200" height="200"><path d="M512 512m-512 0a512 512 0 1 0 1024 0 512 512 0 1 0-1024 0Z" fill="#333333" p-id="6278"></path><path d="M199.936 439.04H147.456c4.608 68.352-12.288 120.064-12.288 120.064l28.16 51.456c37.376-15.872 36.608-171.52 36.608-171.52m169.984 0H317.44s-0.768 155.648 36.352 171.52l28.16-51.456c0.256 0.256-16.64-51.712-12.032-120.064M232.96 595.712H199.936c0 38.656 32.512 41.984 32.512 41.984h16.64c33.536 0 34.816-38.4 34.816-38.4v-228.352H232.96v224.768z m176.128-109.568h34.048L417.28 539.392s-15.104 29.952 9.216 30.976h56.576l22.528-41.216h-28.416c-2.56 0-4.864-2.048-4.864-4.864 0-0.768 0.256-1.536 0.512-2.048l42.24-82.944H465.92l-2.816 5.888h-3.328c-2.56 0-4.864-2.048-4.864-4.864 0-0.768 0.256-1.536 0.512-2.048l35.328-67.328h-49.152l-41.984 84.48c0.512 0-14.592 29.696 9.472 30.72m7.68 100.608s-6.4 0.512-10.496-3.584l-25.344 48.896s4.096 5.632 12.8 5.632h62.976l26.88-50.944h-66.816z" fill="#FFFFFF" p-id="6279"></path><path d="M606.976 439.04h32.512v-52.224h-115.968v52.224h32.768v149.248h-48.64l-24.32 49.152h172.8v-49.152h-49.152v-149.248z m232.192 38.656h-5.888v-33.792c0-31.488-25.6-57.088-57.088-57.088h-19.712v-15.872h-50.176v15.872h-31.488v51.2h31.488v39.68h-50.176v51.2h50.176v109.056h50.176v-108.8h68.864c6.656 0 12.288 5.376 12.288 12.288v53.76h-47.872c0 23.808 19.2 43.008 43.008 43.008h13.056c23.808 0 43.008-19.2 43.008-43.008v-67.584c-0.256-27.904-22.272-49.92-49.664-49.92M756.48 437.76h17.92c4.352 0 7.68 3.584 7.68 7.68v32h-25.856V437.76z" fill="#FFFFFF" p-id="6280"></path><path d="M888.064 411.904c0-13.824-11.008-24.832-24.832-24.832-13.824 0-24.832 11.008-24.832 24.832v24.832h24.832c13.568 0 24.832-11.264 24.832-24.832" fill="#FFFFFF" p-id="6281"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1 @@
<svg t="1717576376677" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5034" width="200" height="200"><path d="M511.061333 801.92a291.157333 291.157333 0 0 1-290.858666-290.858667 291.157333 291.157333 0 0 1 338.517333-286.933333 26.752 26.752 0 0 1-8.704 52.736 237.653333 237.653333 0 0 0-276.309333 234.24 237.653333 237.653333 0 0 0 237.354666 237.354667 237.653333 237.653333 0 0 0 233.258667-281.770667 26.709333 26.709333 0 1 1 52.48-9.941333 291.157333 291.157333 0 0 1-285.781333 345.173333M694.613333 275.328a53.333333 53.333333 0 1 1 0 106.666667 53.333333 53.333333 0 0 1 0-106.666667M511.061333 0C228.821333 0 0 228.821333 0 511.061333c0 282.24 228.821333 511.061333 511.061333 511.061334 282.24 0 511.061333-228.821333 511.061334-511.061334C1022.122667 228.821333 793.301333 0 511.061333 0" fill="#556377" p-id="5035"></path></svg>

After

Width:  |  Height:  |  Size: 889 B

View File

@@ -264,3 +264,11 @@ a.avatar-wrapper {
writing-mode: vertical-rl; /* 竖向排列从右向左 */ writing-mode: vertical-rl; /* 竖向排列从右向左 */
text-orientation: upright; /* 文字方向正常 */ text-orientation: upright; /* 文字方向正常 */
} }
/* Chatbase 在移动端禁止遮挡 */
@media (max-width: 700px) {
button#chatbase-bubble-button {
margin-bottom: 42px;
margin-right: 20px;
}
}

View File

@@ -523,8 +523,10 @@ summary > .notion-h {
.notion-page { .notion-page {
/* width: var(--notion-max-width); */ /* width: var(--notion-max-width); */
width: 100% !important; width: 100% !important;
padding-left: calc(min(12px, 8vw)); padding-left: 0px !important;
padding-right: calc(min(12px, 8vw)); padding-right: 0px !important;
/* padding-left: calc(min(12px, 8vw)); */
/* padding-right: calc(min(12px, 8vw)); */
} }
.notion-full-width { .notion-full-width {
@@ -1449,6 +1451,7 @@ code[class*='language-'] {
.notion-collection-card-property .notion-page-title-text { .notion-collection-card-property .notion-page-title-text {
border-bottom: 0 none; border-bottom: 0 none;
@apply dark:text-gray-200;
} }
.notion-collection-card-property .notion-collection-card-property

View File

@@ -14,7 +14,7 @@ import ProductCard from './ProductCard'
*/ */
const BlogPostListPage = ({ page = 1, posts = [], postCount, siteInfo }) => { const BlogPostListPage = ({ page = 1, posts = [], postCount, siteInfo }) => {
const { NOTION_CONFIG } = useGlobal() const { NOTION_CONFIG } = useGlobal()
const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', 12, NOTION_CONFIG) const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG)
const totalPage = Math.ceil(postCount / POSTS_PER_PAGE) const totalPage = Math.ceil(postCount / POSTS_PER_PAGE)
const showPagination = postCount >= POSTS_PER_PAGE const showPagination = postCount >= POSTS_PER_PAGE
if (!posts || posts.length === 0 || page > totalPage) { if (!posts || posts.length === 0 || page > totalPage) {

View File

@@ -20,7 +20,7 @@ const BlogPostListScroll = ({
siteInfo siteInfo
}) => { }) => {
const { NOTION_CONFIG } = useGlobal() const { NOTION_CONFIG } = useGlobal()
const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', 12, NOTION_CONFIG) const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG)
const [page, updatePage] = useState(1) const [page, updatePage] = useState(1)
const postsToShow = getListByPage(posts, page, POSTS_PER_PAGE) const postsToShow = getListByPage(posts, page, POSTS_PER_PAGE)

View File

@@ -5,39 +5,101 @@ import { siteConfig } from '@/lib/config'
* @constructor * @constructor
*/ */
const SocialButton = () => { const SocialButton = () => {
return <div className='w-full justify-center flex-wrap flex'> return (
<div className='space-x-3 text-xl text-gray-600 dark:text-gray-300 '> <div className='w-full justify-center flex-wrap flex'>
{siteConfig('CONTACT_GITHUB') && <a target='_blank' rel='noreferrer' title={'github'} href={siteConfig('CONTACT_GITHUB')} > <div className='space-x-3 text-xl text-gray-600 dark:text-gray-300 '>
<i className='transform hover:scale-125 duration-150 fab fa-github dark:hover:text-red-400 hover:text-red-600'/> {siteConfig('CONTACT_GITHUB') && (
</a>} <a
{siteConfig('CONTACT_TWITTER') && <a target='_blank' rel='noreferrer' title={'twitter'} href={siteConfig('CONTACT_TWITTER')} > target='_blank'
<i className='transform hover:scale-125 duration-150 fab fa-twitter dark:hover:text-red-400 hover:text-red-600'/> rel='noreferrer'
</a>} title={'github'}
{siteConfig('CONTACT_TELEGRAM') && <a target='_blank' rel='noreferrer' href={siteConfig('CONTACT_TELEGRAM')} title={'telegram'} > href={siteConfig('CONTACT_GITHUB')}>
<i className='transform hover:scale-125 duration-150 fab fa-telegram dark:hover:text-red-400 hover:text-red-600'/> <i className='transform hover:scale-125 duration-150 fab fa-github dark:hover:text-red-400 hover:text-red-600' />
</a>} </a>
{siteConfig('CONTACT_LINKEDIN') && <a target='_blank' rel='noreferrer' href={siteConfig('CONTACT_LINKEDIN')} title={'linkIn'} > )}
<i className='transform hover:scale-125 duration-150 fab fa-linkedin dark:hover:text-red-400 hover:text-red-600'/> {siteConfig('CONTACT_TWITTER') && (
</a>} <a
{siteConfig('CONTACT_WEIBO') && <a target='_blank' rel='noreferrer' title={'weibo'} href={siteConfig('CONTACT_WEIBO')} > target='_blank'
<i className='transform hover:scale-125 duration-150 fab fa-weibo dark:hover:text-red-400 hover:text-red-600'/> rel='noreferrer'
</a>} title={'twitter'}
{siteConfig('CONTACT_INSTAGRAM') && <a target='_blank' rel='noreferrer' title={'instagram'} href={siteConfig('CONTACT_INSTAGRAM')} > href={siteConfig('CONTACT_TWITTER')}>
<i className='transform hover:scale-125 duration-150 fab fa-instagram dark:hover:text-red-400 hover:text-red-600'/> <i className='transform hover:scale-125 duration-150 fab fa-twitter dark:hover:text-red-400 hover:text-red-600' />
</a>} </a>
{siteConfig('CONTACT_EMAIL') && <a target='_blank' rel='noreferrer' title={'email'} href={`mailto:${siteConfig('CONTACT_EMAIL')}`} > )}
<i className='transform hover:scale-125 duration-150 fas fa-envelope dark:hover:text-red-400 hover:text-red-600'/> {siteConfig('CONTACT_TELEGRAM') && (
</a>} <a
{JSON.parse(siteConfig('ENABLE_RSS')) && <a target='_blank' rel='noreferrer' title={'RSS'} href={'/feed'} > target='_blank'
<i className='transform hover:scale-125 duration-150 fas fa-rss dark:hover:text-red-400 hover:text-red-600'/> rel='noreferrer'
</a>} href={siteConfig('CONTACT_TELEGRAM')}
{siteConfig('CONTACT_BILIBILI') && <a target='_blank' rel='noreferrer' title={'bilibili'} href={siteConfig('CONTACT_BILIBILI')} > title={'telegram'}>
<i className='transform hover:scale-125 duration-150 fab fa-bilibili dark:hover:text-red-400 hover:text-red-600'/> <i className='transform hover:scale-125 duration-150 fab fa-telegram dark:hover:text-red-400 hover:text-red-600' />
</a>} </a>
{siteConfig('CONTACT_YOUTUBE') && <a target='_blank' rel='noreferrer' title={'youtube'} href={siteConfig('CONTACT_YOUTUBE')} > )}
<i className='transform hover:scale-125 duration-150 fab fa-youtube dark:hover:text-red-400 hover:text-red-600'/> {siteConfig('CONTACT_LINKEDIN') && (
</a>} <a
target='_blank'
rel='noreferrer'
href={siteConfig('CONTACT_LINKEDIN')}
title={'linkIn'}>
<i className='transform hover:scale-125 duration-150 fab fa-linkedin dark:hover:text-red-400 hover:text-red-600' />
</a>
)}
{siteConfig('CONTACT_WEIBO') && (
<a
target='_blank'
rel='noreferrer'
title={'weibo'}
href={siteConfig('CONTACT_WEIBO')}>
<i className='transform hover:scale-125 duration-150 fab fa-weibo dark:hover:text-red-400 hover:text-red-600' />
</a>
)}
{siteConfig('CONTACT_INSTAGRAM') && (
<a
target='_blank'
rel='noreferrer'
title={'instagram'}
href={siteConfig('CONTACT_INSTAGRAM')}>
<i className='transform hover:scale-125 duration-150 fab fa-instagram dark:hover:text-red-400 hover:text-red-600' />
</a>
)}
{siteConfig('CONTACT_EMAIL') && (
<a
target='_blank'
rel='noreferrer'
title={'email'}
href={`mailto:${siteConfig('CONTACT_EMAIL')}`}>
<i className='transform hover:scale-125 duration-150 fas fa-envelope dark:hover:text-red-400 hover:text-red-600' />
</a>
)}
{JSON.parse(siteConfig('ENABLE_RSS')) && (
<a
target='_blank'
rel='noreferrer'
title={'RSS'}
href={'/rss/feed.xml'}>
<i className='transform hover:scale-125 duration-150 fas fa-rss dark:hover:text-red-400 hover:text-red-600' />
</a>
)}
{siteConfig('CONTACT_BILIBILI') && (
<a
target='_blank'
rel='noreferrer'
title={'bilibili'}
href={siteConfig('CONTACT_BILIBILI')}>
<i className='transform hover:scale-125 duration-150 fab fa-bilibili dark:hover:text-red-400 hover:text-red-600' />
</a>
)}
{siteConfig('CONTACT_YOUTUBE') && (
<a
target='_blank'
rel='noreferrer'
title={'youtube'}
href={siteConfig('CONTACT_YOUTUBE')}>
<i className='transform hover:scale-125 duration-150 fab fa-youtube dark:hover:text-red-400 hover:text-red-600' />
</a>
)}
</div>
</div> </div>
</div> )
} }
export default SocialButton export default SocialButton

View File

@@ -14,7 +14,7 @@ export const BlogListPage = props => {
const { locale, NOTION_CONFIG } = useGlobal() const { locale, NOTION_CONFIG } = useGlobal()
const router = useRouter() const router = useRouter()
const totalPage = Math.ceil( const totalPage = Math.ceil(
postCount / siteConfig('POSTS_PER_PAGE', 12, NOTION_CONFIG) postCount / siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG)
) )
const currentPage = +page const currentPage = +page
@@ -24,6 +24,7 @@ export const BlogListPage = props => {
.split('?')[0] .split('?')[0]
.replace(/\/page\/[1-9]\d*/, '') .replace(/\/page\/[1-9]\d*/, '')
.replace(/\/$/, '') .replace(/\/$/, '')
.replace('.html', '')
const showPageCover = siteConfig('EXAMPLE_POST_LIST_COVER', null, CONFIG) const showPageCover = siteConfig('EXAMPLE_POST_LIST_COVER', null, CONFIG)
@@ -44,7 +45,7 @@ export const BlogListPage = props => {
: `${pagePrefix}/page/${currentPage - 1}`, : `${pagePrefix}/page/${currentPage - 1}`,
query: router.query.s ? { s: router.query.s } : {} query: router.query.s ? { s: router.query.s } : {}
}} }}
className={`${showPrev ? 'bg-black ' : 'bg-gray pointer-events-none '} text-white no-underline py-2 px-3 rounded`}> className={`${showPrev ? 'bg-black dark:bg-hexo-black-gray' : 'bg-gray pointer-events-none invisible'} text-white no-underline py-2 px-3 rounded`}>
{locale.PAGINATION.PREV} {locale.PAGINATION.PREV}
</Link> </Link>
<Link <Link
@@ -52,7 +53,7 @@ export const BlogListPage = props => {
pathname: `${pagePrefix}/page/${currentPage + 1}`, pathname: `${pagePrefix}/page/${currentPage + 1}`,
query: router.query.s ? { s: router.query.s } : {} query: router.query.s ? { s: router.query.s } : {}
}} }}
className={`${showNext ? 'bg-black ' : 'bg-gray pointer-events-none '} text-white no-underline py-2 px-3 rounded`}> className={`${showNext ? 'bg-black dark:bg-hexo-black-gray ' : 'bg-gray pointer-events-none invisible'} text-white no-underline py-2 px-3 rounded`}>
{locale.PAGINATION.NEXT} {locale.PAGINATION.NEXT}
</Link> </Link>
</div> </div>

View File

@@ -13,7 +13,7 @@ export const BlogListScroll = props => {
const { posts } = props const { posts } = props
const { locale, NOTION_CONFIG } = useGlobal() const { locale, NOTION_CONFIG } = useGlobal()
const [page, updatePage] = useState(1) const [page, updatePage] = useState(1)
const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', 12, NOTION_CONFIG) const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG)
let hasMore = false let hasMore = false
const postsToShow = posts const postsToShow = posts

View File

@@ -0,0 +1,94 @@
import throttle from 'lodash.throttle'
import { uuidToId } from 'notion-utils'
import { useCallback, useEffect, useRef, useState } from 'react'
/**
* 目录导航组件
* @param toc
* @returns {JSX.Element}
* @constructor
*/
const Catalog = ({ toc }) => {
// 监听滚动事件
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 (
<div className='px-3'>
<div
className='overflow-y-auto max-h-96 overscroll-none scroll-hidden'
ref={tRef}>
<nav className='h-full text-black dark:text-gray-300'>
{toc.map(tocItem => {
const id = uuidToId(tocItem.id)
tocIds.push(id)
return (
<a
key={id}
href={`#${id}`}
className={`notion-table-of-contents-item duration-300 transform font-light
notion-table-of-contents-item-indent-level-${tocItem.indentLevel} `}>
<span
style={{
display: 'inline-block',
marginLeft: tocItem.indentLevel * 16
}}
className={`truncate ${activeSection === id ? ' font-bold text-red-400 underline' : ''}`}>
{tocItem.text}
</span>
</a>
)
})}
</nav>
</div>
</div>
)
}
export default Catalog

View File

@@ -4,12 +4,13 @@ import { MenuList } from './MenuList'
/** /**
* 网站顶部 * 网站顶部
* LOGO 和 菜单
* @returns * @returns
*/ */
export const Header = props => { export const Header = props => {
return ( return (
<header className='w-full px-6 bg-white dark:bg-black relative z-20'> <header className='w-full px-6 bg-white dark:bg-black relative z-20'>
<div className='container mx-auto max-w-4xl md:flex justify-between items-center'> <div className='mx-auto max-w-4xl md:flex justify-between items-center'>
<Link <Link
href='/' href='/'
className='py-6 w-full text-center md:text-left md:w-auto text-gray-dark no-underline flex justify-center items-center'> className='py-6 w-full text-center md:text-left md:w-auto text-gray-dark no-underline flex justify-center items-center'>

View File

@@ -58,7 +58,7 @@ export const MenuList = props => {
return ( return (
<nav className='w-full bg-white md:pt-0 px-6 relative z-20 border-t border-b border-gray-light dark:border-hexo-black-gray dark:bg-black'> <nav className='w-full bg-white md:pt-0 px-6 relative z-20 border-t border-b border-gray-light dark:border-hexo-black-gray dark:bg-black'>
<div className='container mx-auto max-w-4xl md:flex justify-between items-center text-sm md:text-md md:justify-start'> <div className='mx-auto max-w-4xl md:flex justify-between items-center text-sm md:text-md md:justify-start'>
<ul className='w-full text-center md:text-left flex flex-wrap justify-center items-stretch md:justify-start md:items-start'> <ul className='w-full text-center md:text-left flex flex-wrap justify-center items-stretch md:justify-start md:items-start'>
{links.map((link, index) => ( {links.map((link, index) => (
<MenuItemDrop key={index} link={link} /> <MenuItemDrop key={index} link={link} />

View File

@@ -24,7 +24,6 @@ const SearchInput = ({ currentTag, keyword, cRef }) => {
const key = searchInputRef.current.value const key = searchInputRef.current.value
if (key && key !== '') { if (key && key !== '') {
router.push({ pathname: '/search/' + key }).then(r => { router.push({ pathname: '/search/' + key }).then(r => {
console.log('搜索', key)
}) })
} else { } else {
router.push({ pathname: '/' }).then(r => { router.push({ pathname: '/' }).then(r => {

View File

@@ -3,7 +3,9 @@ import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import Link from 'next/link' import Link from 'next/link'
import CONFIG from '../config'
import Announcement from './Announcement' import Announcement from './Announcement'
import Catalog from './Catalog'
const ExampleRecentComments = dynamic( const ExampleRecentComments = dynamic(
() => import('./RecentCommentListForExample') () => import('./RecentCommentListForExample')
) )
@@ -13,10 +15,36 @@ const ExampleRecentComments = dynamic(
*/ */
export const SideBar = props => { export const SideBar = props => {
const { locale } = useGlobal() const { locale } = useGlobal()
const { latestPosts, categoryOptions, notice } = props const { latestPosts, categoryOptions, notice, post } = props
// 评论相关
const COMMENT_WALINE_SERVER_URL = siteConfig(
'COMMENT_WALINE_SERVER_URL',
false
)
const COMMENT_WALINE_RECENT = siteConfig('COMMENT_WALINE_RECENT', false)
// 文章详情页特殊布局
const HIDDEN_NOTIFICATION =
post && siteConfig('EXAMPLE_ARTICLE_HIDDEN_NOTIFICATION', false, CONFIG)
// 文章详情页左右布局改为上下布局
const LAYOUT_VERTICAL =
post && siteConfig('EXAMPLE_ARTICLE_LAYOUT_VERTICAL', false, CONFIG)
return ( return (
<div className='w-full md:w-64 sticky top-8'> <>
<aside className='rounded shadow overflow-hidden mb-6'> {/* 目录 */}
{post?.toc && post?.toc.length > 2 && (
<aside className='w-full rounded shadow overflow-hidden mb-6 pb-4'>
<h3 className='text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b'>
{locale.COMMON.TABLE_OF_CONTENTS}
</h3>
<Catalog toc={post?.toc} />
</aside>
)}
{/* 分类 */}
<aside className='w-full rounded shadow overflow-hidden mb-6'>
<h3 className='text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b'> <h3 className='text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b'>
{locale.COMMON.CATEGORY} {locale.COMMON.CATEGORY}
</h3> </h3>
@@ -34,7 +62,7 @@ export const SideBar = props => {
{' '} {' '}
<a <a
href={`/category/${category.name}`} href={`/category/${category.name}`}
className='text-gray-darkest text-sm'> className='text-gray-darkest text-sm hover:underline'>
{category.name}({category.count}) {category.name}({category.count})
</a> </a>
</li> </li>
@@ -44,7 +72,9 @@ export const SideBar = props => {
</ul> </ul>
</div> </div>
</aside> </aside>
<aside className='rounded shadow overflow-hidden mb-6'>
{/* 最新文章 */}
<aside className='w-full rounded shadow overflow-hidden mb-6'>
<h3 className='text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b'> <h3 className='text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b'>
{locale.COMMON.LATEST_POSTS} {locale.COMMON.LATEST_POSTS}
</h3> </h3>
@@ -58,7 +88,7 @@ export const SideBar = props => {
{' '} {' '}
<a <a
href={`/${p.slug}`} href={`/${p.slug}`}
className='text-gray-darkest text-sm'> className='text-gray-darkest text-sm hover:underline'>
{p.title} {p.title}
</a> </a>
</li> </li>
@@ -68,25 +98,29 @@ export const SideBar = props => {
</ul> </ul>
</div> </div>
</aside> </aside>
{/* 公告 */}
{/* 公告栏 */} {/* 公告栏 */}
<Announcement post={notice} /> {!HIDDEN_NOTIFICATION && <Announcement post={notice} />}
{/* 最近评论 */} {/* 最近评论 */}
{siteConfig('COMMENT_WALINE_SERVER_URL') && {COMMENT_WALINE_SERVER_URL && COMMENT_WALINE_RECENT && (
siteConfig('COMMENT_WALINE_RECENT') && ( <aside className='w-full rounded shadow overflow-hidden mb-6'>
<aside className='rounded shadow overflow-hidden mb-6'> <h3 className='text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b'>
<h3 className='text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b'> {locale.COMMON.RECENT_COMMENTS}
{locale.COMMON.RECENT_COMMENTS} </h3>
</h3>
<div className='p-4'> <div className='p-4'>
<ExampleRecentComments /> <ExampleRecentComments />
</div> </div>
</aside> </aside>
)} )}
<aside className='rounded overflow-hidden mb-6'>
{/* 宠物挂件 */}
<aside
className={`rounded overflow-hidden mb-6 ${LAYOUT_VERTICAL ? 'hidden md:fixed right-4 bottom-20' : ''}`}>
<Live2D /> <Live2D />
</aside> </aside>
</div> </>
) )
} }

View File

@@ -0,0 +1,48 @@
import NotionIcon from '@/components/NotionIcon'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import CONFIG from '../config'
/**
* 标题栏
*/
export default function TitleBar(props) {
const { post } = props
const { fullWidth, siteInfo } = useGlobal()
const title = post?.title || siteConfig('TITLE')
const description = post?.description || siteConfig('AUTHOR')
const headerImage = post?.pageCoverThumbnail
? post.pageCoverThumbnail
: siteInfo?.pageCover
const TITLE_BG = siteConfig('EXAMPLE_TITLE_IMAGE', false, CONFIG)
return (
<>
{/* 标题栏 */}
{!fullWidth && (
<div className='relative overflow-hidden text-center px-6 py-12 mb-6 bg-gray-100 dark:bg-hexo-black-gray dark:border-hexo-black-gray border-b'>
<h1 className='title-1 relative text-xl md:text-4xl pb-4 z-10'>
{siteConfig('POST_TITLE_ICON') && (
<NotionIcon icon={post?.pageIcon} />
)}
{title}
</h1>
<p className='title-2 relative leading-loose text-gray-dark z-10'>
{description}
</p>
{TITLE_BG && (
<>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={headerImage}
className='absolute object-cover top-0 left-0 w-full h-full select-none opacity-70 z-0'
/>
</>
)}
</div>
)}
</>
)
}

View File

@@ -8,7 +8,12 @@ const CONFIG = {
EXAMPLE_MENU_ARCHIVE: true, // 显示归档 EXAMPLE_MENU_ARCHIVE: true, // 显示归档
EXAMPLE_MENU_SEARCH: true, // 显示搜索 EXAMPLE_MENU_SEARCH: true, // 显示搜索
EXAMPLE_POST_LIST_COVER: true // 列表显示文章封面 EXAMPLE_POST_LIST_COVER: true, // 列表显示文章封面
EXAMPLE_TITLE_IMAGE: false, // 标题栏,是否背景图片
// 文章页面布局
EXAMPLE_ARTICLE_LAYOUT_VERTICAL: false, // 文章详情,左右布局改为上下布局
EXAMPLE_ARTICLE_HIDDEN_NOTIFICATION: false // 文章详情隐藏公告
} }
export default CONFIG export default CONFIG

View File

@@ -2,7 +2,6 @@
import Comment from '@/components/Comment' import Comment from '@/components/Comment'
import replaceSearchResult from '@/components/Mark' import replaceSearchResult from '@/components/Mark'
import NotionIcon from '@/components/NotionIcon'
import NotionPage from '@/components/NotionPage' import NotionPage from '@/components/NotionPage'
import ShareBar from '@/components/ShareBar' import ShareBar from '@/components/ShareBar'
import { siteConfig } from '@/lib/config' import { siteConfig } from '@/lib/config'
@@ -21,6 +20,7 @@ import { PostLock } from './components/PostLock'
import { PostMeta } from './components/PostMeta' import { PostMeta } from './components/PostMeta'
import SearchInput from './components/SearchInput' import SearchInput from './components/SearchInput'
import { SideBar } from './components/SideBar' import { SideBar } from './components/SideBar'
import TitleBar from './components/TitleBar'
import CONFIG from './config' import CONFIG from './config'
import { Style } from './style' import { Style } from './style'
@@ -32,36 +32,15 @@ import { Style } from './style'
* @constructor * @constructor
*/ */
const LayoutBase = props => { const LayoutBase = props => {
const { children } = props const { children, post } = props
const { onLoading, fullWidth, locale } = useGlobal() const { onLoading, fullWidth, locale } = useGlobal()
const router = useRouter()
const { post, category, tag } = props
const title = post?.title || siteConfig('TITLE') // 文章详情页左右布局改为上下布局
const description = post?.description || siteConfig('AUTHOR') const LAYOUT_VERTICAL =
post && siteConfig('EXAMPLE_ARTICLE_LAYOUT_VERTICAL', false, CONFIG)
// 顶部如果是按照分类或标签查看文章列表,列表顶部嵌入一个横幅 // 网站左右布局颠倒
// 如果是搜索,则列表顶部嵌入 搜索框 const LAYOUT_SIDEBAR_REVERSE = siteConfig('LAYOUT_SIDEBAR_REVERSE', false)
let slotTop = null
if (category) {
slotTop = (
<div className='pb-12'>
<i className='mr-1 fas fa-folder-open' />
{category}
</div>
)
} else if (tag) {
slotTop = <div className='pb-12'>#{tag}</div>
} else if (props.slotTop) {
slotTop = props.slotTop
} else if (router.route === '/search') {
// 嵌入一个搜索框在顶部
slotTop = (
<div className='pb-12'>
<SearchInput {...props} />
</div>
)
}
return ( return (
<div <div
@@ -71,33 +50,20 @@ const LayoutBase = props => {
{/* 页头 */} {/* 页头 */}
<Header {...props} /> <Header {...props} />
{/* 标题栏 */}
<TitleBar {...props} />
{/* 主体 */} {/* 主体 */}
<div id='container-inner' className='w-full relative z-10'> <div id='container-inner' className='w-full relative z-10'>
{/* 标题栏 */}
{!fullWidth && (
<div className='text-center px-6 py-12 mb-6 bg-gray-100 dark:bg-hexo-black-gray dark:border-hexo-black-gray border-b'>
<h1 className='text-xl md:text-4xl pb-4'>
{siteConfig('POST_TITLE_ICON') && (
<NotionIcon icon={post?.pageIcon} />
)}
{title}
</h1>
<p className='leading-loose text-gray-dark'>{description}</p>
</div>
)}
<div <div
id='container-wrapper' id='container-wrapper'
className={ className={`relative mx-auto justify-center md:flex py-8 px-2
(JSON.parse(siteConfig('LAYOUT_SIDEBAR_REVERSE')) ${LAYOUT_SIDEBAR_REVERSE ? 'flex-row-reverse' : ''}
? 'flex-row-reverse' ${LAYOUT_VERTICAL ? 'items-center flex-col' : 'items-start'}
: '') + `}>
'relative container mx-auto justify-center md:flex items-start py-8 px-2'
}>
{/* 内容 */} {/* 内容 */}
<div <div
className={`w-full ${fullWidth ? '' : 'max-w-3xl'} xl:px-14 lg:px-4`}> className={`${fullWidth ? '' : LAYOUT_VERTICAL ? 'max-w-5xl' : 'max-w-3xl'} w-full xl:px-14 lg:px-4`}>
<Transition <Transition
show={!onLoading} show={!onLoading}
appear={true} appear={true}
@@ -109,13 +75,22 @@ const LayoutBase = props => {
leaveTo='opacity-0 -translate-y-16' leaveTo='opacity-0 -translate-y-16'
unmount={false}> unmount={false}>
{/* 嵌入模块 */} {/* 嵌入模块 */}
{slotTop} {props.slotTop}
{children} {children}
</Transition> </Transition>
</div> </div>
{/* 侧边栏 */} {/* 侧边栏 */}
{!fullWidth && <SideBar {...props} />} {!fullWidth && (
<div
className={`${
LAYOUT_VERTICAL
? 'flex space-x-0 md:space-x-2 md:flex-row flex-col w-full max-w-5xl justify-center xl:px-14 lg:px-4'
: 'md:w-64 sticky top-8'
}`}>
<SideBar {...props} />
</div>
)}
</div> </div>
</div> </div>
@@ -150,8 +125,20 @@ const LayoutIndex = props => {
* @returns * @returns
*/ */
const LayoutPostList = props => { const LayoutPostList = props => {
const { category, tag } = props
return ( return (
<> <>
{/* 显示分类 */}
{category && (
<div className='pb-12'>
<i className='mr-1 fas fa-folder-open' />
{category}
</div>
)}
{/* 显示标签 */}
{tag && <div className='pb-12'>#{tag}</div>}
{siteConfig('POST_LIST_STYLE') === 'page' ? ( {siteConfig('POST_LIST_STYLE') === 'page' ? (
<BlogListPage {...props} /> <BlogListPage {...props} />
) : ( ) : (
@@ -192,7 +179,7 @@ const LayoutSlug = props => {
{lock ? ( {lock ? (
<PostLock validPassword={validPassword} /> <PostLock validPassword={validPassword} />
) : ( ) : (
<div id='article-wrapper' className='px-2'> <div id='article-wrapper'>
<PostMeta post={post} /> <PostMeta post={post} />
<NotionPage post={post} /> <NotionPage post={post} />
<ShareBar post={post} /> <ShareBar post={post} />
@@ -237,7 +224,14 @@ const LayoutSearch = props => {
} }
}, [router]) }, [router])
return <LayoutPostList {...props} /> return (
<>
<div className='pb-12'>
<SearchInput {...props} />
</div>
<LayoutPostList {...props} />
</>
)
} }
/** /**

View File

@@ -1,22 +1,22 @@
import Logo from './Logo'
import GroupCategory from './GroupCategory'
import { MenuList } from './MenuList'
import GroupTag from './GroupTag'
import SearchInput from './SearchInput'
import SiteInfo from './SiteInfo'
import Catalog from './Catalog'
import Announcement from './Announcement'
import { useRouter } from 'next/router'
import DarkModeButton from '@/components/DarkModeButton' import DarkModeButton from '@/components/DarkModeButton'
import SocialButton from './SocialButton'
import CONFIG from '@/themes/fukasawa/config'
import { AdSlot } from '@/components/GoogleAdsense' import { AdSlot } from '@/components/GoogleAdsense'
import { siteConfig } from '@/lib/config' import { siteConfig } from '@/lib/config'
import MailChimpForm from './MailChimpForm'
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import { useEffect, useMemo, useState } from 'react'
import { isBrowser } from '@/lib/utils' import { isBrowser } from '@/lib/utils'
import CONFIG from '@/themes/fukasawa/config'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import { useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
import Announcement from './Announcement'
import Catalog from './Catalog'
import GroupCategory from './GroupCategory'
import GroupTag from './GroupTag'
import Logo from './Logo'
import MailChimpForm from './MailChimpForm'
import { MenuList } from './MenuList'
import SearchInput from './SearchInput'
import SiteInfo from './SiteInfo'
import SocialButton from './SocialButton'
/** /**
* 侧边栏 * 侧边栏
@@ -24,16 +24,41 @@ import { debounce } from 'lodash'
* @returns * @returns
*/ */
function AsideLeft(props) { function AsideLeft(props) {
const { tagOptions, currentTag, categoryOptions, currentCategory, post, slot, notice } = props const {
tagOptions,
currentTag,
categoryOptions,
currentCategory,
post,
slot,
notice
} = props
const router = useRouter() const router = useRouter()
const { fullWidth } = useGlobal() const { fullWidth } = useGlobal()
const FUKASAWA_SIDEBAR_COLLAPSE_SATUS_DEFAULT = fullWidth || siteConfig('FUKASAWA_SIDEBAR_COLLAPSE_SATUS_DEFAULT', null, CONFIG) const FUKASAWA_SIDEBAR_COLLAPSE_SATUS_DEFAULT =
fullWidth ||
siteConfig('FUKASAWA_SIDEBAR_COLLAPSE_SATUS_DEFAULT', null, CONFIG)
const FUKASAWA_SIDEBAR_COLLAPSE_ON_SCROLL = siteConfig(
'FUKASAWA_SIDEBAR_COLLAPSE_ON_SCROLL',
false,
CONFIG
)
const FUKASAWA_SIDEBAR_COLLAPSE_BUTTON = siteConfig(
'FUKASAWA_SIDEBAR_COLLAPSE_BUTTON',
null,
CONFIG
)
// 侧边栏折叠从 本地存储中获取 open 状态的初始值 // 侧边栏折叠从 本地存储中获取 open 状态的初始值
const [isCollapsed, setIsCollapse] = useState(() => { const [isCollapsed, setIsCollapse] = useState(() => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
return localStorage.getItem('fukasawa-sidebar-collapse') === 'true' || FUKASAWA_SIDEBAR_COLLAPSE_SATUS_DEFAULT return (
localStorage.getItem('fukasawa-sidebar-collapse') === 'true' ||
FUKASAWA_SIDEBAR_COLLAPSE_SATUS_DEFAULT
)
} }
return FUKASAWA_SIDEBAR_COLLAPSE_SATUS_DEFAULT return FUKASAWA_SIDEBAR_COLLAPSE_SATUS_DEFAULT
}) })
@@ -45,8 +70,8 @@ function AsideLeft(props) {
} }
}, [isCollapsed]) }, [isCollapsed])
const isReverse = siteConfig('LAYOUT_SIDEBAR_REVERSE')
const position = useMemo(() => { const position = useMemo(() => {
const isReverse = JSON.parse(siteConfig('LAYOUT_SIDEBAR_REVERSE'))
if (isCollapsed) { if (isCollapsed) {
if (isReverse) { if (isReverse) {
return 'right-2' return 'right-2'
@@ -69,7 +94,7 @@ function AsideLeft(props) {
// 自动折叠侧边栏 onResize 窗口宽度小于1366 || 滚动条滚动至页面的300px时 ; 将open设置为false // 自动折叠侧边栏 onResize 窗口宽度小于1366 || 滚动条滚动至页面的300px时 ; 将open设置为false
useEffect(() => { useEffect(() => {
if (!siteConfig('FUKASAWA_SIDEBAR_COLLAPSE_ON_SCROLL', false, CONFIG)) { if (!FUKASAWA_SIDEBAR_COLLAPSE_ON_SCROLL) {
return return
} }
const handleResize = debounce(() => { const handleResize = debounce(() => {
@@ -93,72 +118,88 @@ function AsideLeft(props) {
} }
}, []) }, [])
return (
return <div className={`sideLeft relative ${isCollapsed ? 'w-0' : 'w-80'} duration-300 transition-all bg-white dark:bg-hexo-black-gray min-h-screen hidden lg:block z-20`}> <div
{/* 折叠按钮 */} className={`sideLeft relative ${isCollapsed ? 'w-0' : 'w-80'} duration-300 transition-all bg-white dark:bg-hexo-black-gray min-h-screen hidden lg:block z-20`}>
{siteConfig('FUKASAWA_SIDEBAR_COLLAPSE_BUTTON', null, CONFIG) && <div className={`${position} hidden lg:block fixed top-0 cursor-pointer hover:scale-110 duration-300 px-3 py-2 dark:text-white`} onClick={toggleOpen}> {/* 悬浮的折叠按钮 */}
{isCollapsed ? <i className="fa-solid fa-indent text-xl"></i> : <i className='fas fa-bars text-xl'></i>} {FUKASAWA_SIDEBAR_COLLAPSE_BUTTON && (
</div>} <div
className={`${position} hidden lg:block fixed top-0 cursor-pointer hover:scale-110 duration-300 px-3 py-2 dark:text-white`}
<div className={`h-full ${isCollapsed ? 'hidden' : 'p-8'}`}> onClick={toggleOpen}>
{isCollapsed ? (
<Logo {...props} /> <i className='fa-solid fa-indent text-xl'></i>
) : (
<section className='siteInfo flex flex-col dark:text-gray-300 pt-8'> <i className='fas fa-bars text-xl'></i>
{siteConfig('DESCRIPTION')} )}
</section>
<section className='flex flex-col text-gray-600'>
<div className='w-12 my-4' />
<MenuList {...props} />
</section>
<section className='flex flex-col text-gray-600'>
<div className='w-12 my-4' />
<SearchInput {...props} />
</section>
<section className='flex flex-col dark:text-gray-300'>
<div className='w-12 my-4' />
<Announcement post={notice} />
</section>
<section>
<MailChimpForm />
</section>
<section>
<AdSlot type='in-article' />
</section>
{router.asPath !== '/tag' && <section className='flex flex-col'>
<div className='w-12 my-4' />
<GroupTag tags={tagOptions} currentTag={currentTag} />
</section>}
{router.asPath !== '/category' && <section className='flex flex-col'>
<div className='w-12 my-4' />
<GroupCategory categories={categoryOptions} currentCategory={currentCategory} />
</section>}
<section className='flex flex-col'>
<div className='w-12 my-4' />
<SocialButton />
<SiteInfo />
</section>
<section className='flex justify-center dark:text-gray-200 pt-4'>
<DarkModeButton />
</section>
<section className='sticky top-0 pt-12 flex flex-col max-h-screen '>
<Catalog toc={post?.toc} />
<div className='flex justify-center'>
<div>{slot}</div>
</div>
</section>
</div> </div>
)}
<div className={`h-full ${isCollapsed ? 'hidden' : 'p-8'}`}>
<Logo {...props} />
<section className='siteInfo flex flex-col dark:text-gray-300 pt-8'>
{siteConfig('DESCRIPTION')}
</section>
<section className='flex flex-col text-gray-600'>
<div className='w-12 my-4' />
<MenuList {...props} />
</section>
<section className='flex flex-col text-gray-600'>
<div className='w-12 my-4' />
<SearchInput {...props} />
</section>
<section className='flex flex-col dark:text-gray-300'>
<div className='w-12 my-4' />
<Announcement post={notice} />
</section>
<section>
<MailChimpForm />
</section>
<section>
<AdSlot type='in-article' />
</section>
{router.asPath !== '/tag' && (
<section className='flex flex-col'>
<div className='w-12 my-4' />
<GroupTag tags={tagOptions} currentTag={currentTag} />
</section>
)}
{router.asPath !== '/category' && (
<section className='flex flex-col'>
<div className='w-12 my-4' />
<GroupCategory
categories={categoryOptions}
currentCategory={currentCategory}
/>
</section>
)}
<section className='flex flex-col'>
<div className='w-12 my-4' />
<SocialButton />
<SiteInfo />
</section>
<section className='flex justify-center dark:text-gray-200 pt-4'>
<DarkModeButton />
</section>
<section className='sticky top-0 pt-12 flex flex-col max-h-screen '>
<Catalog toc={post?.toc} />
<div className='flex justify-center'>
<div>{slot}</div>
</div>
</section>
</div>
</div> </div>
)
} }
export default AsideLeft export default AsideLeft

View File

@@ -1,6 +1,7 @@
import LazyImage from '@/components/LazyImage' import LazyImage from '@/components/LazyImage'
import NotionIcon from '@/components/NotionIcon' import NotionIcon from '@/components/NotionIcon'
import { siteConfig } from '@/lib/config' import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import Link from 'next/link' import Link from 'next/link'
import CONFIG from '../config' import CONFIG from '../config'
import TagItemMini from './TagItemMini' import TagItemMini from './TagItemMini'
@@ -10,7 +11,8 @@ import TagItemMini from './TagItemMini'
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
const BlogCard = ({ index, post, showSummary, siteInfo }) => { const BlogCard = ({ showAnimate, post, showSummary }) => {
const {siteInfo} =useGlobal()
const showPreview = const showPreview =
siteConfig('FUKASAWA_POST_LIST_PREVIEW', null, CONFIG) && post.blockMap siteConfig('FUKASAWA_POST_LIST_PREVIEW', null, CONFIG) && post.blockMap
// fukasawa 强制显示图片 // fukasawa 强制显示图片
@@ -24,11 +26,12 @@ const BlogCard = ({ index, post, showSummary, siteInfo }) => {
const showPageCover = const showPageCover =
siteConfig('FUKASAWA_POST_LIST_COVER', null, CONFIG) && siteConfig('FUKASAWA_POST_LIST_COVER', null, CONFIG) &&
post?.pageCoverThumbnail post?.pageCoverThumbnail
const FUKASAWA_POST_LIST_ANIMATION = siteConfig( const FUKASAWA_POST_LIST_ANIMATION = siteConfig(
'FUKASAWA_POST_LIST_ANIMATION', 'FUKASAWA_POST_LIST_ANIMATION',
null, null,
CONFIG CONFIG
) ) || showAnimate
// 动画样式 首屏卡片不用,后面翻出来的加动画 // 动画样式 首屏卡片不用,后面翻出来的加动画
const aosProps = FUKASAWA_POST_LIST_ANIMATION const aosProps = FUKASAWA_POST_LIST_ANIMATION

View File

@@ -17,8 +17,9 @@ import PaginationSimple from './PaginationSimple'
*/ */
const BlogListPage = ({ page = 1, posts = [], postCount, siteInfo }) => { const BlogListPage = ({ page = 1, posts = [], postCount, siteInfo }) => {
const { NOTION_CONFIG } = useGlobal() const { NOTION_CONFIG } = useGlobal()
const postsPerPage = siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG)
const totalPage = Math.ceil( const totalPage = Math.ceil(
postCount / parseInt(siteConfig('POSTS_PER_PAGE', 12, NOTION_CONFIG)) postCount / postsPerPage
) )
const showNext = page < totalPage const showNext = page < totalPage
@@ -33,23 +34,27 @@ const BlogListPage = ({ page = 1, posts = [], postCount, siteInfo }) => {
return () => window.removeEventListener('resize', handleResize) return () => window.removeEventListener('resize', handleResize)
}, []) }, [])
/** /**
* 文章重新布局,使纵向排列看起来是横向排列 * 文章重新布局,使纵向排列看起来是横向排列
*/ */
useEffect(() => { useEffect(() => {
const count = posts?.length || 0 const count = posts?.length || 0;
const rows = Math.ceil(count / columns) const rows = Math.ceil(count / columns);
const newFilterPosts = [] const newFilterPosts = new Array(count);
for (let i = 0; i < columns; i++) {
for (let j = 0; j < rows; j++) { let index = 0;
const index = j * columns + i for (let col = 0; col < columns; col++) {
if (index < count) { for (let row = 0; row < rows; row++) {
newFilterPosts.push(deepClone(posts[index])) const sourceIndex = row * columns + col;
if (sourceIndex < count) {
newFilterPosts[index] = deepClone(posts[sourceIndex]);
index++;
}
} }
}
} }
setFilterPosts(newFilterPosts)
}, [columns, posts]) setFilterPosts(newFilterPosts);
}, [columns, posts]);
if (!filterPosts || filterPosts.length === 0) { if (!filterPosts || filterPosts.length === 0) {
return <BlogPostListEmpty /> return <BlogPostListEmpty />

View File

@@ -1,41 +1,36 @@
import { siteConfig } from '@/lib/config' import { siteConfig } from '@/lib/config';
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global';
import { useEffect, useRef, useState } from 'react' import throttle from 'lodash.throttle';
import BlogCard from './BlogCard' import { useCallback, useEffect, useRef, useState } from 'react';
import BlogPostListEmpty from './BlogListEmpty' import BlogCard from './BlogCard';
import BlogPostListEmpty from './BlogListEmpty';
const BlogListScroll = ({ posts }) => {
const { locale, NOTION_CONFIG } = useGlobal();
const [page, setPage] = useState(1);
const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG);
const [filterPostsGroups, setFilterPostsGroups] = useState([]);
// 每页显示的文章数量
const postsPerPage = POSTS_PER_PAGE;
// 计算总页数
const totalPages = Math.ceil(posts.length / postsPerPage);
// 加载更多文章
const loadMorePosts = () => {
if (page < totalPages) {
setPage(page + 1);
}
};
/**
* 文章列表分页表格
* @param page 当前页
* @param posts 所有文章
* @param tags 所有标签
* @returns {JSX.Element}
* @constructor
*/
const BlogListScroll = props => {
const { posts = [], siteInfo } = props
const { locale, NOTION_CONFIG } = useGlobal()
const targetRef = useRef(null) const targetRef = useRef(null)
const [page, updatePage] = useState(1)
const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', 12, NOTION_CONFIG)
let hasMore = false
const postsToShow = posts
? Object.assign(posts).slice(0, POSTS_PER_PAGE * page)
: []
if (posts) {
const totalCount = posts.length
hasMore = page * POSTS_PER_PAGE < totalCount
}
const handleGetMore = () => {
if (!hasMore) return
updatePage(page + 1)
}
// 监听滚动自动分页加载 // 监听滚动自动分页加载
const scrollTrigger = () => { const scrollTrigger = useCallback(
requestAnimationFrame(() => { throttle(() => {
const scrollS = window.scrollY + window.outerHeight const scrollS = window.scrollY + window.outerHeight
const clientHeight = targetRef const clientHeight = targetRef
? targetRef.current ? targetRef.current
@@ -43,47 +38,64 @@ const BlogListScroll = props => {
: 0 : 0
: 0 : 0
if (scrollS > clientHeight + 100) { if (scrollS > clientHeight + 100) {
handleGetMore() loadMorePosts()
} }
}) }, 500)
} )
useEffect(() => { useEffect(() => {
window.addEventListener('scroll', scrollTrigger) window.addEventListener('scroll', scrollTrigger)
return () => { return () => {
window.removeEventListener('scroll', scrollTrigger) window.removeEventListener('scroll', scrollTrigger)
} }
}, []) })
// 根据当前页和每页文章数截取应该显示的文章
useEffect(() => {
const startIndex = (page - 1) * postsPerPage;
const endIndex = startIndex + postsPerPage;
const postsToShow = posts.slice(startIndex, endIndex);
const columns = 3; // 假设有3列
// 重新排列文章,保证列优先顺序
const newFilterPosts = [];
for (let col = 0; col < columns; col++) {
for (let i = col; i < postsToShow.length; i += columns) {
newFilterPosts.push(postsToShow[i]);
}
}
setFilterPostsGroups((prev) => [...prev, newFilterPosts]);
}, [posts, page]);
if (!posts || posts.length === 0) { if (!posts || posts.length === 0) {
return <BlogPostListEmpty /> return <BlogPostListEmpty />;
} else { } else {
return ( return (
<div id='posts-wrapper' ref={targetRef} className='grid-container'> <div ref={targetRef}>
{/* 文章列表 */} {filterPostsGroups.map((group, groupIndex) => (
{postsToShow?.map(post => ( <div key={groupIndex} id="posts-wrapper" className="grid-container mb-10">
<div {group.map((post) => (
key={post.id} <div
className='grid-item justify-center flex' key={post.id}
style={{ breakInside: 'avoid' }}> className="grid-item justify-center flex"
<BlogCard style={{ breakInside: 'avoid' }}
index={posts.indexOf(post)} >
key={post.id} <BlogCard key={post.id} post={post} showAnimate={groupIndex > 0}/>
post={post} </div>
siteInfo={siteInfo} ))}
/>
</div> </div>
))} ))}
<div <div
className='w-full my-4 py-4 text-center cursor-pointer ' className="w-full my-4 py-4 text-center cursor-pointer"
onClick={handleGetMore}> onClick={loadMorePosts}
{' '} >
{hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '} {page < totalPages ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}
</div> </div>
</div> </div>
) );
} }
} };
export default BlogListScroll;
export default BlogListScroll

View File

@@ -0,0 +1,64 @@
import Collapse from '@/components/Collapse'
import { useRef, useState } from 'react'
import Logo from './Logo'
import { MenuList } from './MenuList'
import SearchInput from './SearchInput'
/**
* 顶部导航
* @param {*} param0
* @returns
*/
const Header = props => {
const [isOpen, changeShow] = useState(false)
const collapseRef = useRef(null)
const toggleMenuOpen = () => {
changeShow(!isOpen)
}
return (
<div id='top-nav' className='z-40 block lg:hidden'>
{/* 导航栏 */}
<div
id='sticky-nav'
className={
'relative w-full top-0 z-20 transform duration-500 bg-white dark:bg-black'
}>
<Collapse type='vertical' isOpen={isOpen} collapseRef={collapseRef}>
<div className='py-1 px-5'>
<MenuList
{...props}
onHeightChange={param =>
collapseRef.current?.updateCollapseHeight(param)
}
/>
<SearchInput {...props} />
</div>
</Collapse>
<div className='w-full flex justify-between items-center p-4 '>
{/* 左侧LOGO 标题 */}
<div className='flex flex-none flex-grow-0'>
<Logo {...props} />
</div>
<div className='flex'></div>
{/* 右侧功能 */}
<div className='mr-1 flex justify-end items-center text-sm space-x-4 font-serif dark:text-gray-200'>
<div
onClick={toggleMenuOpen}
className='cursor-pointer text-lg p-2'>
{isOpen ? (
<i className='fas fa-times' />
) : (
<i className='fas fa-bars' />
)}
</div>
</div>
</div>
</div>
</div>
)
}
export default Header

View File

@@ -7,9 +7,11 @@ export const MenuItemDrop = ({ link }) => {
return ( return (
<li <li
onMouseOver={() => changeShow(true)} onMouseOver={() => {
changeShow(true)
}}
onMouseOut={() => changeShow(false)} onMouseOut={() => changeShow(false)}
className='relative py-1 duration-500 justify-between text-gray-500 dark:text-gray-300 hover:text-black hover:underline cursor-pointer flex flex-nowrap items-center '> className='relative py-1 mb-1.5 duration-500 justify-between text-gray-500 dark:text-gray-300 hover:text-black hover:underline cursor-pointer flex flex-nowrap items-center '>
{!hasSubMenu && ( {!hasSubMenu && (
<Link <Link
href={link?.href} href={link?.href}
@@ -33,7 +35,7 @@ export const MenuItemDrop = ({ link }) => {
{hasSubMenu && ( {hasSubMenu && (
<div className='text-right'> <div className='text-right'>
<i <i
className={`px-2 fas fa-chevron-right duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i> className={`px-2 fas fa-chevron-left duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>
</div> </div>
)} )}
</div> </div>
@@ -42,7 +44,7 @@ export const MenuItemDrop = ({ link }) => {
{/* 子菜单 */} {/* 子菜单 */}
{hasSubMenu && ( {hasSubMenu && (
<ul <ul
className={`${show ? 'visible opacity-100 left-52' : 'invisible opacity-0 left-40'} z-20 py-1 absolute right-0 top-0 w-full border-gray-100 bg-white dark:bg-black dark:border-gray-800 transition-all duration-300 drop-shadow-lg `}> className={`${show ? 'visible opacity-100 left-72' : 'invisible opacity-0 left-80'} z-20 p-2 absolute right-0 top-0 w-full border-gray-100 bg-white dark:bg-black dark:border-gray-800 transition-all duration-300 drop-shadow-lg `}>
{link?.subMenus?.map((sLink, index) => { {link?.subMenus?.map((sLink, index) => {
return ( return (
<li key={index}> <li key={index}>

View File

@@ -28,11 +28,6 @@ export const MenuList = props => {
name: locale.NAV.ARCHIVE, name: locale.NAV.ARCHIVE,
href: '/archive', href: '/archive',
show: siteConfig('FUKASAWA_MENU_ARCHIVE', null, CONFIG) show: siteConfig('FUKASAWA_MENU_ARCHIVE', null, CONFIG)
},
{
name: locale.NAV.SEARCH,
href: '/search',
show: siteConfig('FUKASAWA_MENU_SEARCH', null, CONFIG)
} }
] ]

View File

@@ -6,39 +6,101 @@ import { siteConfig } from '@/lib/config'
* @constructor * @constructor
*/ */
const SocialButton = () => { const SocialButton = () => {
return <div className='w-52 flex-wrap flex'> return (
<div className='space-x-3 text-lg text-gray-500 dark:text-gray-400'> <div className='w-52 flex-wrap flex'>
{siteConfig('CONTACT_GITHUB') && <a target='_blank' rel='noreferrer' title={'github'} href={siteConfig('CONTACT_GITHUB')} > <div className='space-x-3 text-lg text-gray-500 dark:text-gray-400'>
<i className='fab fa-github transform hover:scale-125 duration-150'/> {siteConfig('CONTACT_GITHUB') && (
</a>} <a
{siteConfig('CONTACT_TWITTER') && <a target='_blank' rel='noreferrer' title={'twitter'} href={siteConfig('CONTACT_TWITTER')} > target='_blank'
<i className='fab fa-twitter transform hover:scale-125 duration-150'/> rel='noreferrer'
</a>} title={'github'}
{siteConfig('CONTACT_TELEGRAM') && <a target='_blank' rel='noreferrer' href={siteConfig('CONTACT_TELEGRAM')} title={'telegram'} > href={siteConfig('CONTACT_GITHUB')}>
<i className='fab fa-telegram transform hover:scale-125 duration-150'/> <i className='fab fa-github transform hover:scale-125 duration-150' />
</a>} </a>
{siteConfig('CONTACT_LINKEDIN') && <a target='_blank' rel='noreferrer' href={siteConfig('CONTACT_LINKEDIN')} title={'linkedIn'} > )}
<i className='fab fa-linkedin transform hover:scale-125 duration-150'/> {siteConfig('CONTACT_TWITTER') && (
</a>} <a
{siteConfig('CONTACT_WEIBO') && <a target='_blank' rel='noreferrer' title={'weibo'} href={siteConfig('CONTACT_WEIBO')} > target='_blank'
<i className='fab fa-weibo transform hover:scale-125 duration-150'/> rel='noreferrer'
</a>} title={'twitter'}
{siteConfig('CONTACT_INSTAGRAM') && <a target='_blank' rel='noreferrer' title={'instagram'} href={siteConfig('CONTACT_INSTAGRAM')} > href={siteConfig('CONTACT_TWITTER')}>
<i className='fab fa-instagram transform hover:scale-125 duration-150'/> <i className='fab fa-twitter transform hover:scale-125 duration-150' />
</a>} </a>
{siteConfig('CONTACT_EMAIL') && <a target='_blank' rel='noreferrer' title={'email'} href={`mailto:${siteConfig('CONTACT_EMAIL')}`} > )}
<i className='fas fa-envelope transform hover:scale-125 duration-150'/> {siteConfig('CONTACT_TELEGRAM') && (
</a>} <a
{JSON.parse(siteConfig('ENABLE_RSS')) && <a target='_blank' rel='noreferrer' title={'RSS'} href={'/feed'} > target='_blank'
<i className='fas fa-rss transform hover:scale-125 duration-150'/> rel='noreferrer'
</a>} href={siteConfig('CONTACT_TELEGRAM')}
{siteConfig('CONTACT_BILIBILI') && <a target='_blank' rel='noreferrer' title={'bilibili'} href={siteConfig('CONTACT_BILIBILI')} > title={'telegram'}>
<i className='fab fa-bilibili transform hover:scale-125 duration-150'/> <i className='fab fa-telegram transform hover:scale-125 duration-150' />
</a>} </a>
{siteConfig('CONTACT_YOUTUBE') && <a target='_blank' rel='noreferrer' title={'youtube'} href={siteConfig('CONTACT_YOUTUBE')} > )}
<i className='fab fa-youtube transform hover:scale-125 duration-150'/> {siteConfig('CONTACT_LINKEDIN') && (
</a>} <a
target='_blank'
rel='noreferrer'
href={siteConfig('CONTACT_LINKEDIN')}
title={'linkedIn'}>
<i className='fab fa-linkedin transform hover:scale-125 duration-150' />
</a>
)}
{siteConfig('CONTACT_WEIBO') && (
<a
target='_blank'
rel='noreferrer'
title={'weibo'}
href={siteConfig('CONTACT_WEIBO')}>
<i className='fab fa-weibo transform hover:scale-125 duration-150' />
</a>
)}
{siteConfig('CONTACT_INSTAGRAM') && (
<a
target='_blank'
rel='noreferrer'
title={'instagram'}
href={siteConfig('CONTACT_INSTAGRAM')}>
<i className='fab fa-instagram transform hover:scale-125 duration-150' />
</a>
)}
{siteConfig('CONTACT_EMAIL') && (
<a
target='_blank'
rel='noreferrer'
title={'email'}
href={`mailto:${siteConfig('CONTACT_EMAIL')}`}>
<i className='fas fa-envelope transform hover:scale-125 duration-150' />
</a>
)}
{JSON.parse(siteConfig('ENABLE_RSS')) && (
<a
target='_blank'
rel='noreferrer'
title={'RSS'}
href={'/rss/feed.xml'}>
<i className='fas fa-rss transform hover:scale-125 duration-150' />
</a>
)}
{siteConfig('CONTACT_BILIBILI') && (
<a
target='_blank'
rel='noreferrer'
title={'bilibili'}
href={siteConfig('CONTACT_BILIBILI')}>
<i className='fab fa-bilibili transform hover:scale-125 duration-150' />
</a>
)}
{siteConfig('CONTACT_YOUTUBE') && (
<a
target='_blank'
rel='noreferrer'
title={'youtube'}
href={siteConfig('CONTACT_YOUTUBE')}>
<i className='fab fa-youtube transform hover:scale-125 duration-150' />
</a>
)}
</div>
</div> </div>
</div> )
} }
export default SocialButton export default SocialButton

View File

@@ -1,50 +0,0 @@
import { useState, useRef } from 'react'
import Collapse from '@/components/Collapse'
import { MenuList } from './MenuList'
import Logo from './Logo'
import SearchInput from './SearchInput'
/**
* 顶部导航
* @param {*} param0
* @returns
*/
const TopNav = props => {
const [isOpen, changeShow] = useState(false)
const collapseRef = useRef(null)
const toggleMenuOpen = () => {
changeShow(!isOpen)
}
return (<div id='top-nav' className='z-40 block lg:hidden'>
{/* 导航栏 */}
<div id='sticky-nav' className={'relative w-full top-0 z-20 transform duration-500 bg-white dark:bg-black'}>
<Collapse type='vertical' isOpen={isOpen} collapseRef={collapseRef}>
<div className='py-1 px-5'>
<MenuList {...props} onHeightChange={(param) => collapseRef.current?.updateCollapseHeight(param)} />
<SearchInput {...props} />
</div>
</Collapse>
<div className='w-full flex justify-between items-center p-4 '>
{/* 左侧LOGO 标题 */}
<div className='flex flex-none flex-grow-0'>
<Logo {...props} />
</div>
<div className='flex'>
</div>
{/* 右侧功能 */}
<div className='mr-1 flex justify-end items-center text-sm space-x-4 font-serif dark:text-gray-200'>
<div onClick={toggleMenuOpen} className='cursor-pointer text-lg p-2'>
{isOpen ? <i className='fas fa-times' /> : <i className='fas fa-bars' />}
</div>
</div>
</div>
</div>
</div>)
}
export default TopNav

View File

@@ -10,11 +10,9 @@ const CONFIG = {
FUKASAWA_MENU_CATEGORY: true, // 显示分类 FUKASAWA_MENU_CATEGORY: true, // 显示分类
FUKASAWA_MENU_TAG: true, // 显示标签 FUKASAWA_MENU_TAG: true, // 显示标签
FUKASAWA_MENU_ARCHIVE: true, // 显示归档 FUKASAWA_MENU_ARCHIVE: true, // 显示归档
FUKASAWA_MENU_SEARCH: false, // 显示搜索
FUKASAWA_SIDEBAR_COLLAPSE_BUTTON: true, // 侧边栏折叠按钮 FUKASAWA_SIDEBAR_COLLAPSE_BUTTON: true, // 侧边栏折叠按钮
FUKASAWA_SIDEBAR_COLLAPSE_SATUS_DEFAULT: false, // 侧边栏默认折叠收起 FUKASAWA_SIDEBAR_COLLAPSE_SATUS_DEFAULT: false, // 侧边栏默认折叠收起
FUKASAWA_SIDEBAR_COLLAPSE_ON_SCROLL: false // 侧边栏滚动时折叠 仅文章阅读页有效 FUKASAWA_SIDEBAR_COLLAPSE_ON_SCROLL: false // 侧边栏滚动时折叠 仅文章阅读页有效
} }
export default CONFIG export default CONFIG

View File

@@ -1,27 +1,27 @@
'use client' 'use client'
import CONFIG from './config' import AlgoliaSearchModal from '@/components/AlgoliaSearchModal'
import TopNav from './components/TopNav' import { AdSlot } from '@/components/GoogleAdsense'
import AsideLeft from './components/AsideLeft' import replaceSearchResult from '@/components/Mark'
import { isBrowser } from '@/lib/utils' import WWAds from '@/components/WWAds'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import { isBrowser } from '@/lib/utils'
import { Transition } from '@headlessui/react'
import dynamic from 'next/dynamic'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { createContext, useContext, useEffect, useRef } from 'react'
import ArticleDetail from './components/ArticleDetail'
import ArticleLock from './components/ArticleLock'
import AsideLeft from './components/AsideLeft'
import BlogListPage from './components/BlogListPage' import BlogListPage from './components/BlogListPage'
import BlogListScroll from './components/BlogListScroll' import BlogListScroll from './components/BlogListScroll'
import BlogArchiveItem from './components/BlogPostArchive' import BlogArchiveItem from './components/BlogPostArchive'
import ArticleDetail from './components/ArticleDetail' import Header from './components/Header'
import ArticleLock from './components/ArticleLock'
import TagItemMini from './components/TagItemMini' import TagItemMini from './components/TagItemMini'
import { useRouter } from 'next/router' import CONFIG from './config'
import { createContext, useContext, useEffect, useRef } from 'react'
import Link from 'next/link'
import { Transition } from '@headlessui/react'
import dynamic from 'next/dynamic'
import { AdSlot } from '@/components/GoogleAdsense'
import { Style } from './style' import { Style } from './style'
import replaceSearchResult from '@/components/Mark'
import { siteConfig } from '@/lib/config'
import WWAds from '@/components/WWAds'
import AlgoliaSearchModal from '@/components/AlgoliaSearchModal'
const Live2D = dynamic(() => import('@/components/Live2D')) const Live2D = dynamic(() => import('@/components/Live2D'))
@@ -43,51 +43,58 @@ export const useFukasawaGlobal = () => useContext(ThemeGlobalFukasawa)
* @returns {JSX.Element} * @returns {JSX.Element}
* @constructor * @constructor
*/ */
const LayoutBase = (props) => { const LayoutBase = props => {
const { children, headerSlot } = props const { children, headerSlot } = props
const leftAreaSlot = <Live2D /> const leftAreaSlot = <Live2D />
const { onLoading, fullWidth } = useGlobal() const { onLoading, fullWidth } = useGlobal()
const searchModal = useRef(null) const searchModal = useRef(null)
return ( return (
<ThemeGlobalFukasawa.Provider value={{ searchModal }}> <ThemeGlobalFukasawa.Provider value={{ searchModal }}>
<div id='theme-fukasawa' className={`${siteConfig('FONT_STYLE')} dark:bg-black scroll-smooth`}> <div
<AlgoliaSearchModal cRef={searchModal} {...props} /> id='theme-fukasawa'
className={`${siteConfig('FONT_STYLE')} dark:bg-black scroll-smooth`}>
<Style /> <Style />
{/* 页头导航,此主题只在移动端生效 */}
<Header {...props} />
<TopNav {...props} /> <div
className={
<div className={(JSON.parse(siteConfig('LAYOUT_SIDEBAR_REVERSE')) ? 'flex-row-reverse' : '') + ' flex'}> (JSON.parse(siteConfig('LAYOUT_SIDEBAR_REVERSE'))
? 'flex-row-reverse'
: '') + ' flex'
}>
{/* 侧边抽屉 */} {/* 侧边抽屉 */}
<AsideLeft {...props} slot={leftAreaSlot} /> <AsideLeft {...props} slot={leftAreaSlot} />
<main id='wrapper' className='relative flex w-full py-8 justify-center bg-day dark:bg-night'> <main
<div id='container-inner' className={`${fullWidth ? '' : '2xl:max-w-6xl md:max-w-4xl'} w-full relative z-10`}> id='wrapper'
className='relative flex w-full py-8 justify-center bg-day dark:bg-night'>
<div
id='container-inner'
className={`${fullWidth ? '' : '2xl:max-w-6xl md:max-w-4xl'} w-full relative z-10`}>
<Transition <Transition
show={!onLoading} show={!onLoading}
appear={true} appear={true}
className="w-full" className='w-full'
enter="transition ease-in-out duration-700 transform order-first" enter='transition ease-in-out duration-700 transform order-first'
enterFrom="opacity-0 translate-y-16" enterFrom='opacity-0 translate-y-16'
enterTo="opacity-100" enterTo='opacity-100'
leave="transition ease-in-out duration-300 transform" leave='transition ease-in-out duration-300 transform'
leaveFrom="opacity-100 translate-y-0" leaveFrom='opacity-100 translate-y-0'
leaveTo="opacity-0 -translate-y-16" leaveTo='opacity-0 -translate-y-16'
unmount={false} unmount={false}>
>
<div> {headerSlot} </div> <div> {headerSlot} </div>
<div> {children} </div> <div> {children} </div>
</Transition> </Transition>
<div className='mt-2'> <div className='mt-2'>
<AdSlot type='native' /> <AdSlot type='native' />
</div> </div>
</div> </div>
</main> </main>
</div> </div>
<AlgoliaSearchModal cRef={searchModal} {...props} />
</div> </div>
</ThemeGlobalFukasawa.Provider> </ThemeGlobalFukasawa.Provider>
) )
@@ -96,49 +103,65 @@ const LayoutBase = (props) => {
/** /**
* 首页 * 首页
* @param {*} props notion数据 * @param {*} props notion数据
* @returns 首页就是一个博客列表 * @returns 首页就是一个博客列表
*/ */
const LayoutIndex = (props) => { const LayoutIndex = props => {
return <LayoutPostList {...props} /> return <LayoutPostList {...props} />
} }
/** /**
* 博客列表 * 博客列表
* @param {*} props * @param {*} props
*/ */
const LayoutPostList = (props) => { const LayoutPostList = props => {
return <> const POST_LIST_STYLE = siteConfig('POST_LIST_STYLE')
<div className='w-full p-2'><WWAds className='w-full' orientation='horizontal' /></div> return (
{siteConfig('POST_LIST_STYLE') === 'page' ? <BlogListPage {...props} /> : <BlogListScroll {...props} />} <>
</> <div className='w-full p-2'>
<WWAds className='w-full' orientation='horizontal' />
</div>
{ POST_LIST_STYLE=== 'page' ? (
<BlogListPage {...props} />
) : (
<BlogListScroll {...props} />
)}
</>
)
} }
/** /**
* 文章详情 * 文章详情
* @param {*} props * @param {*} props
* @returns * @returns
*/ */
const LayoutSlug = (props) => { const LayoutSlug = props => {
const { post, lock, validPassword } = props const { post, lock, validPassword } = props
const router = useRouter() const router = useRouter()
useEffect(() => { useEffect(() => {
// 404 // 404
if (!post) { if (!post) {
setTimeout(() => { setTimeout(
if (isBrowser) { () => {
const article = document.getElementById('notion-article') if (isBrowser) {
if (!article) { const article = document.getElementById('notion-article')
router.push('/404').then(() => { if (!article) {
console.warn('找不到页面', router.asPath) router.push('/404').then(() => {
}) console.warn('找不到页面', router.asPath)
})
}
} }
} },
}, siteConfig('POST_WAITING_TIME_FOR_404') * 1000) siteConfig('POST_WAITING_TIME_FOR_404') * 1000
)
} }
}, [post]) }, [post])
return ( return (
<> <>
{lock ? <ArticleLock validPassword={validPassword} /> : <ArticleDetail {...props} />} {lock ? (
<ArticleLock validPassword={validPassword} />
) : (
<ArticleDetail {...props} />
)}
</> </>
) )
} }
@@ -167,26 +190,28 @@ const LayoutSearch = props => {
/** /**
* 归档页面 * 归档页面
*/ */
const LayoutArchive = (props) => { const LayoutArchive = props => {
const { archivePosts } = props const { archivePosts } = props
return <> return (
<div className="mb-10 pb-20 bg-white md:p-12 p-3 dark:bg-gray-800 shadow-md min-h-full"> <>
{Object.keys(archivePosts).map(archiveTitle => ( <div className='mb-10 pb-20 bg-white md:p-12 p-3 dark:bg-gray-800 shadow-md min-h-full'>
<BlogArchiveItem {Object.keys(archivePosts).map(archiveTitle => (
key={archiveTitle} <BlogArchiveItem
posts={archivePosts[archiveTitle]} key={archiveTitle}
archiveTitle={archiveTitle} posts={archivePosts[archiveTitle]}
/> archiveTitle={archiveTitle}
))} />
</div> ))}
</> </div>
</>
)
} }
/** /**
* 404 * 404
* @param {*} props * @param {*} props
* @returns * @returns
*/ */
const Layout404 = props => { const Layout404 = props => {
return <>404</> return <>404</>
} }
@@ -194,16 +219,17 @@ const Layout404 = props => {
/** /**
* 分类列表 * 分类列表
* @param {*} props * @param {*} props
* @returns * @returns
*/ */
const LayoutCategoryIndex = (props) => { const LayoutCategoryIndex = props => {
const { locale } = useGlobal() const { locale } = useGlobal()
const { categoryOptions } = props const { categoryOptions } = props
return ( return (
<> <>
<div className='bg-white dark:bg-gray-700 px-10 py-10 shadow'> <div className='bg-white dark:bg-gray-700 px-10 py-10 shadow'>
<div className='dark:text-gray-200 mb-5'> <div className='dark:text-gray-200 mb-5'>
<i className='mr-4 fas fa-th' />{locale.COMMON.CATEGORY}: <i className='mr-4 fas fa-th' />
{locale.COMMON.CATEGORY}:
</div> </div>
<div id='category-list' className='duration-200 flex flex-wrap'> <div id='category-list' className='duration-200 flex flex-wrap'>
{categoryOptions?.map(category => { {categoryOptions?.map(category => {
@@ -214,8 +240,11 @@ const LayoutCategoryIndex = (props) => {
passHref passHref
legacyBehavior> legacyBehavior>
<div <div
className={'hover:text-black dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600 px-5 cursor-pointer py-2 hover:bg-gray-100'}> className={
<i className='mr-4 fas fa-folder' />{category.name}({category.count}) 'hover:text-black dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600 px-5 cursor-pointer py-2 hover:bg-gray-100'
}>
<i className='mr-4 fas fa-folder' />
{category.name}({category.count})
</div> </div>
</Link> </Link>
) )
@@ -229,36 +258,41 @@ const LayoutCategoryIndex = (props) => {
/** /**
* 标签列表 * 标签列表
* @param {*} props * @param {*} props
* @returns * @returns
*/ */
const LayoutTagIndex = (props) => { const LayoutTagIndex = props => {
const { locale } = useGlobal() const { locale } = useGlobal()
const { tagOptions } = props const { tagOptions } = props
return <> return (
<div className='bg-white dark:bg-gray-700 px-10 py-10 shadow'> <>
<div className='dark:text-gray-200 mb-5'><i className='mr-4 fas fa-tag' />{locale.COMMON.TAGS}:</div> <div className='bg-white dark:bg-gray-700 px-10 py-10 shadow'>
<div id="tags-list" className="duration-200 flex flex-wrap ml-8"> <div className='dark:text-gray-200 mb-5'>
{tagOptions.map(tag => { <i className='mr-4 fas fa-tag' />
return ( {locale.COMMON.TAGS}:
<div key={tag.name} className="p-2"> </div>
<TagItemMini key={tag.name} tag={tag} /> <div id='tags-list' className='duration-200 flex flex-wrap ml-8'>
</div> {tagOptions.map(tag => {
) return (
})} <div key={tag.name} className='p-2'>
<TagItemMini key={tag.name} tag={tag} />
</div>
)
})}
</div>
</div> </div>
</div> </>
</> )
} }
export { export {
CONFIG as THEME_CONFIG,
LayoutBase,
LayoutIndex,
LayoutSearch,
LayoutArchive,
LayoutSlug,
Layout404, Layout404,
LayoutPostList, LayoutArchive,
LayoutBase,
LayoutCategoryIndex, LayoutCategoryIndex,
LayoutTagIndex LayoutIndex,
LayoutPostList,
LayoutSearch,
LayoutSlug,
LayoutTagIndex,
CONFIG as THEME_CONFIG
} }

View File

@@ -44,7 +44,16 @@ const Style = () => {
column-gap: .5rem; column-gap: .5rem;
} }
} }
.container {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 10px;
padding: 10px;
}
`}</style> `}</style>
} }
export { Style } export { Style }

View File

@@ -1,3 +1,4 @@
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import { GameListIndexCombine } from './GameListIndexCombine' import { GameListIndexCombine } from './GameListIndexCombine'
import PaginationSimple from './PaginationSimple' import PaginationSimple from './PaginationSimple'
@@ -9,7 +10,8 @@ import PaginationSimple from './PaginationSimple'
export const BlogListPage = props => { export const BlogListPage = props => {
const { page = 1, postCount } = props const { page = 1, postCount } = props
const { NOTION_CONFIG } = useGlobal() const { NOTION_CONFIG } = useGlobal()
const totalPage = Math.ceil(postCount / NOTION_CONFIG) const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG)
const totalPage = Math.ceil(postCount / POSTS_PER_PAGE)
const showNext = page < totalPage const showNext = page < totalPage
return ( return (

View File

@@ -9,7 +9,7 @@ export const BlogListScroll = props => {
const { posts } = props const { posts } = props
const { locale, NOTION_CONFIG } = useGlobal() const { locale, NOTION_CONFIG } = useGlobal()
const [page, updatePage] = useState(1) const [page, updatePage] = useState(1)
const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', 12, NOTION_CONFIG) const POSTS_PER_PAGE = siteConfig('POSTS_PER_PAGE', null, NOTION_CONFIG)
let hasMore = false let hasMore = false
const postsToShow = const postsToShow =

View File

@@ -17,6 +17,7 @@ const PaginationSimple = ({ page, showNext }) => {
.split('?')[0] .split('?')[0]
.replace(/\/page\/[1-9]\d*/, '') .replace(/\/page\/[1-9]\d*/, '')
.replace(/\/$/, '') .replace(/\/$/, '')
.replace('.html', '')
return ( return (
<div className='my-10 flex justify-between font-medium text-black dark:text-gray-100 space-x-2'> <div className='my-10 flex justify-between font-medium text-black dark:text-gray-100 space-x-2'>

View File

@@ -1,23 +1,27 @@
import { useGitBookGlobal } from '@/themes/gitbook' import MobileButtonCatalog from './MobileButtonCatalog'
import JumpToTopButton from './JumpToTopButton' import MobileButtonPageNav from './MobileButtonPageNav'
/**
* 移动端底部导航
* @param {*} param0
* @returns
*/
export default function BottomMenuBar({ post, className }) { export default function BottomMenuBar({ post, className }) {
const { pageNavVisible, changePageNavVisible } = useGitBookGlobal() const showTocButton = post?.toc?.length > 1
const togglePageNavVisible = () => {
changePageNavVisible(!pageNavVisible)
}
return ( return (
<div className={'sticky z-10 bottom-0 w-full h-12 bg-white dark:bg-hexo-black-gray ' + className}> <>
<div className='flex justify-between h-full shadow-card'> {/* 移动端底部导航按钮 */}
<div onClick={togglePageNavVisible} className='flex w-full items-center justify-center cursor-pointer'> <div className='bottom-button-group md:hidden w-screen h-14 px-4 fixed flex items-center justify-between right-left bottom-0 z-30 bg-white border-t dark:border-gray-800'>
<i className="fa-solid fa-book"></i> <div className='w-full'>
</div> <MobileButtonPageNav />
<div className='flex w-full items-center justify-center cursor-pointer'>
<JumpToTopButton />
</div>
</div>
</div> </div>
{showTocButton && (
<div className='w-full'>
<MobileButtonCatalog />
</div>
)}
</div>
</>
) )
} }

View File

@@ -1,3 +1,4 @@
import { useGlobal } from '@/lib/global'
import { isBrowser } from '@/lib/utils' import { isBrowser } from '@/lib/utils'
import throttle from 'lodash.throttle' import throttle from 'lodash.throttle'
import { uuidToId } from 'notion-utils' import { uuidToId } from 'notion-utils'
@@ -13,6 +14,7 @@ const Catalog = ({ post }) => {
const toc = post?.toc const toc = post?.toc
// 同步选中目录事件 // 同步选中目录事件
const [activeSection, setActiveSection] = useState(null) const [activeSection, setActiveSection] = useState(null)
const {locale}= useGlobal()
// 监听滚动事件 // 监听滚动事件
useEffect(() => { useEffect(() => {
@@ -67,6 +69,7 @@ const Catalog = ({ post }) => {
return ( return (
<> <>
<div className='w-full hidden md:block'><i className='mr-1 fas fa-stream' />{locale.COMMON.TABLE_OF_CONTENTS}</div>
<div <div
id='toc-wrapper' id='toc-wrapper'
className='toc-wrapper overflow-y-auto my-2 max-h-80 overscroll-none scroll-hidden'> className='toc-wrapper overflow-y-auto my-2 max-h-80 overscroll-none scroll-hidden'>

View File

@@ -0,0 +1,66 @@
import { useGlobal } from '@/lib/global'
import { useGitBookGlobal } from '@/themes/gitbook'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import Catalog from './Catalog'
/**
* 悬浮抽屉目录
* @param toc
* @param post
* @returns {JSX.Element}
* @constructor
*/
const CatalogDrawerWrapper = ({ post, cRef }) => {
const { tocVisible, changeTocVisible } = useGitBookGlobal()
const { locale } = useGlobal()
const router = useRouter()
const switchVisible = () => {
changeTocVisible(!tocVisible)
}
useEffect(() => {
changeTocVisible(false)
}, [router])
return (
<>
<div
id='gitbook-toc-float'
className='fixed top-0 right-0 z-40 md:hidden'>
{/* 侧边菜单 */}
<div
className={
(tocVisible
? 'animate__slideInRight '
: ' -mr-72 animate__slideOutRight') +
' overflow-y-hidden shadow-card w-60 duration-200 fixed right-1 bottom-16 rounded py-2 bg-white dark:bg-hexo-black-gray'
}>
{post && (
<>
<div className='px-4 pb-2 flex justify-between items-center border-b font-bold'>
<span>{locale.COMMON.TABLE_OF_CONTENTS}</span>
<i
className='fas fa-times p-1 cursor-pointer'
onClick={() => {
changeTocVisible(false)
}}></i>
</div>
<div className='dark:text-gray-400 text-gray-600 px-3'>
<Catalog post={post} />
</div>
</>
)}
</div>
</div>
{/* 背景蒙版 */}
<div
id='right-drawer-background'
className={
(tocVisible ? 'block' : 'hidden') +
' fixed top-0 left-0 z-30 w-full h-full'
}
onClick={switchVisible}
/>
</>
)
}
export default CatalogDrawerWrapper

View File

@@ -1,25 +0,0 @@
import { useGitBookGlobal } from '@/themes/gitbook'
/**
* 移动端悬浮目录按钮
*/
export default function FloatTocButton () {
const { tocVisible, changeTocVisible } = useGitBookGlobal()
const toggleToc = () => {
changeTocVisible(!tocVisible)
}
return (
<div
onClick={toggleToc}
className={ 'text-black flex justify-center items-center dark:text-gray-200 dark:bg-hexo-black-gray py-2 px-2'
}
>
<a
id="toc-button"
className={'fa-list-ol cursor-pointer fas hover:scale-150 transform duration-200'}
/>
</div>
)
}

View File

@@ -1,33 +1,66 @@
import { siteConfig } from '@/lib/config' import { siteConfig } from '@/lib/config'
import SocialButton from './SocialButton'
/**
* 站点也叫
* @param {*} param0
* @returns
*/
const Footer = ({ siteInfo }) => { const Footer = ({ siteInfo }) => {
const d = new Date() const d = new Date()
const currentYear = d.getFullYear() const currentYear = d.getFullYear()
const since = siteConfig('SINCE') const since = siteConfig('SINCE')
const copyrightDate = parseInt(since) < currentYear ? since + '-' + currentYear : currentYear const copyrightDate =
parseInt(since) < currentYear ? since + '-' + currentYear : currentYear
return ( return (
<footer <footer className='z-20 bg:white dark:bg-hexo-black-gray justify-center text-center w-full text-sm relative'>
className='z-20 py-2 bg:white dark:bg-hexo-black-gray justify-center text-center w-full text-sm relative' <hr className='pb-2' />
>
<hr className='pb-2' />
<div className='flex justify-center'> <SocialButton />
<div><i className='mx-1 animate-pulse fas fa-heart' /> <a href={siteConfig('LINK')} className='underline font-bold text-gray-500 dark:text-gray-300 '>{siteConfig('AUTHOR')}</a>.<br /></div>
© {`${copyrightDate}`}
</div>
<div className='text-xs font-serif'>Powered By <a href='https://github.com/tangly1024/NotionNext' className='underline text-gray-500 dark:text-gray-300'>NotionNext {siteConfig('VERSION')}</a></div> <div className='flex justify-center pt-1'>
<div>
<i className='mx-1 animate-pulse fas fa-heart' />{' '}
<a
href={siteConfig('LINK')}
className='underline font-bold text-gray-500 dark:text-gray-300 '>
{siteConfig('AUTHOR')}
</a>
.<br />
</div>
© {`${copyrightDate}`}
</div>
{siteConfig('BEI_AN') && <><i className='fas fa-shield-alt' /> <a href='https://beian.miit.gov.cn/' className='mr-2'>{siteConfig('BEI_AN')}</a><br /></>} <div className='text-xs font-serif'>
Powered By{' '}
<a
href='https://github.com/tangly1024/NotionNext'
className='underline text-gray-500 dark:text-gray-300'>
NotionNext {siteConfig('VERSION')}
</a>
</div>
<span className='hidden busuanzi_container_site_pv'> {siteConfig('BEI_AN') && (
<i className='fas fa-eye' /><span className='px-1 busuanzi_value_site_pv'> </span> </span> <>
<span className='pl-2 hidden busuanzi_container_site_uv'> <i className='fas fa-shield-alt' />{' '}
<i className='fas fa-users' /> <span className='px-1 busuanzi_value_site_uv'> </span> </span> <a href='https://beian.miit.gov.cn/' className='mr-2'>
<h1 className='pt-1'>{siteConfig('TITLE')}</h1> {siteConfig('BEI_AN')}
</a>
<br />
</>
)}
</footer> <span className='hidden busuanzi_container_site_pv'>
<i className='fas fa-eye' />
<span className='px-1 busuanzi_value_site_pv'> </span>{' '}
</span>
<span className='pl-2 hidden busuanzi_container_site_uv'>
<i className='fas fa-users' />{' '}
<span className='px-1 busuanzi_value_site_uv'> </span>{' '}
</span>
{/* SEO title */}
<h1 className='pt-1 hidden'>{siteConfig('TITLE')}</h1>
</footer>
) )
} }

View File

@@ -90,7 +90,7 @@ export default function Header(props) {
{isOpen ? ( {isOpen ? (
<i className='fas fa-times' /> <i className='fas fa-times' />
) : ( ) : (
<i className='fa-solid fa-ellipsis-vertical' /> <i className='fa-solid fa-bars' />
)} )}
</div> </div>
</div> </div>

View File

@@ -1,4 +1,3 @@
/** /**
* 跳转到网页顶部 * 跳转到网页顶部
* 当屏幕下滑500像素后会出现该控件 * 当屏幕下滑500像素后会出现该控件
@@ -9,15 +8,20 @@
*/ */
const JumpToTopButton = ({ showPercent = false, percent, className }) => { const JumpToTopButton = ({ showPercent = false, percent, className }) => {
return ( return (
<div <div
id="jump-to-top" id='jump-to-top'
data-aos="fade-up" data-aos='fade-up'
data-aos-duration="300" data-aos-duration='300'
data-aos-once="false" data-aos-once='false'
data-aos-anchor-placement="top-center" data-aos-anchor-placement='top-center'
className='fixed xl:right-80 right-2 mr-10 bottom-24 z-20'> className='fixed xl:right-80 right-2 bottom-24 z-20'>
<i className='fas fa-chevron-up cursor-pointer p-2 rounded-full border bg-white dark:bg-hexo-black-gray' onClick={() => { window.scrollTo({ top: 0, behavior: 'smooth' }) }} /> <i
</div> className='shadow fas fa-chevron-up cursor-pointer p-2 rounded-full border bg-white dark:bg-hexo-black-gray'
onClick={() => {
window.scrollTo({ top: 0, behavior: 'smooth' })
}}
/>
</div>
) )
} }

Some files were not shown because too many files have changed in this diff Show More