mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-14 07:26:52 +00:00
Merge branch 'main' into original-main-fix-DOMException
This commit is contained in:
@@ -10,14 +10,8 @@ import { useEffect } from 'react'
|
||||
export default function AOSAnimation() {
|
||||
const initAOS = async () => {
|
||||
Promise.all([
|
||||
loadExternalResource(
|
||||
'https://cdn.bootcdn.net/ajax/libs/aos/2.3.4/aos.js',
|
||||
'js'
|
||||
),
|
||||
loadExternalResource(
|
||||
'https://cdn.bootcdn.net/ajax/libs/aos/2.3.4/aos.css',
|
||||
'css'
|
||||
)
|
||||
loadExternalResource('/js/aos.js', 'js'),
|
||||
loadExternalResource('/css/aos.css', 'css')
|
||||
]).then(() => {
|
||||
if (window.AOS) {
|
||||
window.AOS.init()
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { useState, useImperativeHandle, useRef, useEffect, Fragment } from 'react'
|
||||
import algoliasearch from 'algoliasearch'
|
||||
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 { 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 = [
|
||||
{
|
||||
@@ -20,7 +27,6 @@ const ShortCutActions = [
|
||||
key: 'Esc',
|
||||
action: '关闭'
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
/**
|
||||
@@ -36,31 +42,49 @@ export default function AlgoliaSearchModal({ cRef }) {
|
||||
const [totalPage, setTotalPage] = useState(0)
|
||||
const [totalHit, setTotalHit] = useState(0)
|
||||
const [useTime, setUseTime] = useState(0)
|
||||
const inputRef = useRef(null)
|
||||
const [activeIndex, setActiveIndex] = useState(0)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
useHotkeys('ctrl+k', (e) => {
|
||||
const inputRef = useRef(null)
|
||||
const router = useRouter()
|
||||
|
||||
/**
|
||||
* 快捷键设置
|
||||
*/
|
||||
useHotkeys('ctrl+k', e => {
|
||||
e.preventDefault()
|
||||
setIsModalOpen(true)
|
||||
})
|
||||
// 方向键调整选中
|
||||
useHotkeys('down', (e) => {
|
||||
e.preventDefault()
|
||||
if (activeIndex < searchResults.length - 1) {
|
||||
setActiveIndex(activeIndex + 1)
|
||||
}
|
||||
}, { enableOnFormTags: true })
|
||||
useHotkeys('up', (e) => {
|
||||
e.preventDefault()
|
||||
if (activeIndex > 0) {
|
||||
setActiveIndex(activeIndex - 1)
|
||||
}
|
||||
}, { enableOnFormTags: true })
|
||||
useHotkeys(
|
||||
'down',
|
||||
e => {
|
||||
e.preventDefault()
|
||||
if (activeIndex < searchResults.length - 1) {
|
||||
setActiveIndex(activeIndex + 1)
|
||||
}
|
||||
},
|
||||
{ enableOnFormTags: true }
|
||||
)
|
||||
useHotkeys(
|
||||
'up',
|
||||
e => {
|
||||
e.preventDefault()
|
||||
if (activeIndex > 0) {
|
||||
setActiveIndex(activeIndex - 1)
|
||||
}
|
||||
},
|
||||
{ enableOnFormTags: true }
|
||||
)
|
||||
// esc关闭
|
||||
useHotkeys('esc', (e) => {
|
||||
e.preventDefault()
|
||||
setIsModalOpen(false)
|
||||
}, { enableOnFormTags: true })
|
||||
useHotkeys(
|
||||
'esc',
|
||||
e => {
|
||||
e.preventDefault()
|
||||
setIsModalOpen(false)
|
||||
},
|
||||
{ enableOnFormTags: true }
|
||||
)
|
||||
|
||||
// 跳转Search结果
|
||||
const onJumpSearchResult = () => {
|
||||
if (searchResults.length > 0) {
|
||||
@@ -68,11 +92,15 @@ export default function AlgoliaSearchModal({ cRef }) {
|
||||
}
|
||||
}
|
||||
// enter跳转
|
||||
useHotkeys('enter', (e) => {
|
||||
if (searchResults.length > 0) {
|
||||
onJumpSearchResult(index)
|
||||
}
|
||||
}, { enableOnFormTags: true })
|
||||
useHotkeys(
|
||||
'enter',
|
||||
e => {
|
||||
if (searchResults.length > 0) {
|
||||
onJumpSearchResult(index)
|
||||
}
|
||||
},
|
||||
{ enableOnFormTags: true }
|
||||
)
|
||||
|
||||
const resetSearch = () => {
|
||||
setActiveIndex(0)
|
||||
@@ -84,6 +112,16 @@ export default function AlgoliaSearchModal({ cRef }) {
|
||||
if (inputRef.current) inputRef.current.value = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面路径变化后,自动关闭此modal
|
||||
*/
|
||||
useEffect(() => {
|
||||
setIsModalOpen(false)
|
||||
}, [router])
|
||||
|
||||
/**
|
||||
* 自动聚焦搜索框
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (isModalOpen) {
|
||||
setTimeout(() => {
|
||||
@@ -93,9 +131,10 @@ export default function AlgoliaSearchModal({ cRef }) {
|
||||
resetSearch()
|
||||
}
|
||||
}, [isModalOpen])
|
||||
|
||||
/**
|
||||
* 对外暴露方法
|
||||
*/
|
||||
* 对外暴露方法
|
||||
**/
|
||||
useImperativeHandle(cRef, () => {
|
||||
return {
|
||||
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'))
|
||||
|
||||
/**
|
||||
@@ -131,7 +173,9 @@ export default function AlgoliaSearchModal({ cRef }) {
|
||||
setTotalHit(nbHits)
|
||||
setSearchResults(hits)
|
||||
setIsLoading(false)
|
||||
const doms = document.getElementById('search-wrapper').getElementsByClassName('replace')
|
||||
const doms = document
|
||||
.getElementById('search-wrapper')
|
||||
.getElementsByClassName('replace')
|
||||
|
||||
setTimeout(() => {
|
||||
replaceSearchResult({
|
||||
@@ -149,33 +193,35 @@ export default function AlgoliaSearchModal({ cRef }) {
|
||||
}
|
||||
|
||||
// 定义节流函数,确保在用户停止输入一段时间后才会调用处理搜索的方法
|
||||
const throttledHandleInputChange = useRef(throttle((query, page = 0) => {
|
||||
handleSearch(query, page);
|
||||
}, 1000));
|
||||
const throttledHandleInputChange = useRef(
|
||||
throttle((query, page = 0) => {
|
||||
handleSearch(query, page)
|
||||
}, 1000)
|
||||
)
|
||||
|
||||
// 用于存储搜索延迟的计时器
|
||||
const searchTimer = useRef(null);
|
||||
const searchTimer = useRef(null)
|
||||
|
||||
// 修改input的onChange事件处理函数
|
||||
const handleInputChange = (e) => {
|
||||
const query = e.target.value;
|
||||
const handleInputChange = e => {
|
||||
const query = e.target.value
|
||||
|
||||
// 如果已经有计时器在等待搜索,先清除之前的计时器
|
||||
if (searchTimer.current) {
|
||||
clearTimeout(searchTimer.current);
|
||||
clearTimeout(searchTimer.current)
|
||||
}
|
||||
|
||||
// 设置新的计时器,在用户停止输入一段时间后触发搜索
|
||||
searchTimer.current = setTimeout(() => {
|
||||
throttledHandleInputChange.current(query);
|
||||
}, 800);
|
||||
};
|
||||
throttledHandleInputChange.current(query)
|
||||
}, 800)
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换页码
|
||||
* @param {*} page
|
||||
*/
|
||||
const switchPage = (page) => {
|
||||
const switchPage = page => {
|
||||
throttledHandleInputChange.current(keyword, page)
|
||||
}
|
||||
|
||||
@@ -191,58 +237,58 @@ export default function AlgoliaSearchModal({ cRef }) {
|
||||
}
|
||||
return (
|
||||
<div
|
||||
id="search-wrapper"
|
||||
className={`${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`}
|
||||
>
|
||||
id='search-wrapper'
|
||||
className={`${
|
||||
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
|
||||
className={`${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="text-2xl text-blue-600 dark:text-yellow-600 font-bold">搜索</div>
|
||||
className={`${
|
||||
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='text-2xl text-blue-600 dark:text-yellow-600 font-bold'>
|
||||
搜索
|
||||
</div>
|
||||
<div>
|
||||
<i
|
||||
className="text-gray-600 fa-solid fa-xmark p-1 cursor-pointer hover:text-blue-600"
|
||||
onClick={closeModal}
|
||||
></i>
|
||||
className='text-gray-600 fa-solid fa-xmark p-1 cursor-pointer hover:text-blue-600'
|
||||
onClick={closeModal}></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="在这里输入搜索关键词..."
|
||||
type='text'
|
||||
placeholder='在这里输入搜索关键词...'
|
||||
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}
|
||||
/>
|
||||
|
||||
{/* 标签组 */}
|
||||
<div className="mb-4">
|
||||
<div className='mb-4'>
|
||||
<TagGroups />
|
||||
</div>
|
||||
{
|
||||
searchResults.length === 0 && keyword && !isLoading && (
|
||||
<div>
|
||||
<p className=" text-slate-600 text-center my-4 text-base"> 无法找到相关结果
|
||||
<span
|
||||
className='font-semibold'
|
||||
>"{keyword}"</span></p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{searchResults.length === 0 && keyword && !isLoading && (
|
||||
<div>
|
||||
<p className=' text-slate-600 text-center my-4 text-base'>
|
||||
{' '}
|
||||
无法找到相关结果
|
||||
<span className='font-semibold'>"{keyword}"</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<ul className='flex-1 overflow-auto'>
|
||||
{searchResults.map((result, index) => (
|
||||
<li key={result.objectID}
|
||||
<li
|
||||
key={result.objectID}
|
||||
onMouseEnter={() => setActiveIndex(index)}
|
||||
onClick={() => onJumpSearchResult(index)}
|
||||
className={`cursor-pointer replace my-2 p-2 duration-100
|
||||
rounded-lg
|
||||
${activeIndex === index ? 'bg-blue-600 dark:bg-yellow-600' : ''}`}>
|
||||
<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}
|
||||
</a>
|
||||
</li>
|
||||
@@ -250,15 +296,22 @@ export default function AlgoliaSearchModal({ cRef }) {
|
||||
</ul>
|
||||
<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'>
|
||||
{totalHit === 0 && (<div className='flex items-center'>
|
||||
{
|
||||
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>
|
||||
<span className='ml-2 mr-4 text-gray-600 dark:text-gray-300'>{action.action}</span></Fragment>
|
||||
})
|
||||
}
|
||||
</div>)
|
||||
}
|
||||
{totalHit === 0 && (
|
||||
<div className='flex items-center'>
|
||||
{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>
|
||||
<span className='ml-2 mr-4 text-gray-600 dark:text-gray-300'>
|
||||
{action.action}
|
||||
</span>
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{totalHit > 0 && (
|
||||
<p>
|
||||
@@ -266,19 +319,18 @@ export default function AlgoliaSearchModal({ cRef }) {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-gray-600 dark:text-gray-300 text-right">
|
||||
<span >
|
||||
<i className="fa-brands fa-algolia"></i> Algolia 提供搜索服务
|
||||
<div className='text-gray-600 dark:text-gray-300 text-right'>
|
||||
<span>
|
||||
<i className='fa-brands fa-algolia'></i> Algolia 提供搜索服务
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* 遮罩 */}
|
||||
<div
|
||||
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>
|
||||
)
|
||||
@@ -292,21 +344,31 @@ function TagGroups() {
|
||||
// 获取tagOptions数组前十个
|
||||
const firstTenTags = tagOptions?.slice(0, 10)
|
||||
|
||||
return <div id='tags-group' className='dark:border-gray-700 space-y-2'>
|
||||
{
|
||||
firstTenTags?.map((tag, index) => {
|
||||
return <Link passHref
|
||||
key={index}
|
||||
href={`/tag/${encodeURIComponent(tag.name)}`}
|
||||
className={'cursor-pointer inline-block whitespace-nowrap'}>
|
||||
<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'}>
|
||||
<div className='text-lg'>{tag.name} </div>{tag.count ? <sup className='relative ml-1'>{tag.count}</sup> : <></>}
|
||||
</div>
|
||||
|
||||
</Link>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
return (
|
||||
<div id='tags-group' className='dark:border-gray-700 space-y-2'>
|
||||
{firstTenTags?.map((tag, index) => {
|
||||
return (
|
||||
<Link
|
||||
passHref
|
||||
key={index}
|
||||
href={`/tag/${encodeURIComponent(tag.name)}`}
|
||||
className={'cursor-pointer inline-block whitespace-nowrap'}>
|
||||
<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'
|
||||
}>
|
||||
<div className='text-lg'>{tag.name} </div>
|
||||
{tag.count ? (
|
||||
<sup className='relative ml-1'>{tag.count}</sup>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -321,16 +383,16 @@ function Pagination(props) {
|
||||
return (
|
||||
<div className='flex space-x-1 w-full justify-center py-1'>
|
||||
{Array.from({ length: totalPage }, (_, i) => {
|
||||
const classNames = page === i
|
||||
? 'font-bold text-white bg-blue-600 dark:bg-yellow-600 rounded'
|
||||
: 'hover:text-blue-600 hover:font-bold dark:text-gray-300'
|
||||
const classNames =
|
||||
page === i
|
||||
? 'font-bold text-white bg-blue-600 dark:bg-yellow-600 rounded'
|
||||
: 'hover:text-blue-600 hover:font-bold dark:text-gray-300'
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => switchPage(i)}
|
||||
className={`text-center cursor-pointer w-6 h-6 ${classNames}`}
|
||||
key={i}
|
||||
>
|
||||
key={i}>
|
||||
{i + 1}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -121,6 +121,11 @@ const Comment = ({ frontMatter, className }) => {
|
||||
return null
|
||||
}
|
||||
|
||||
// 特定文章关闭评论区
|
||||
if (frontMatter?.comment === 'Hide') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={frontMatter?.id}
|
||||
|
||||
@@ -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 { 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 [height, setHeight] = useState(0)
|
||||
|
||||
const { latestPosts } = props
|
||||
const { allNavPages } = props
|
||||
const router = useRouter()
|
||||
/**
|
||||
* 随机跳转文章
|
||||
*/
|
||||
function handleJumpToRandomPost() {
|
||||
const randomIndex = Math.floor(Math.random() * latestPosts.length)
|
||||
const randomPost = latestPosts[randomIndex]
|
||||
const randomIndex = Math.floor(Math.random() * allNavPages.length)
|
||||
const randomPost = allNavPages[randomIndex]
|
||||
router.push(`${siteConfig('SUB_PATH', '')}/${randomPost?.slug}`)
|
||||
}
|
||||
|
||||
@@ -37,19 +37,30 @@ export default function CustomContextMenu(props) {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleContextMenu = (event) => {
|
||||
setShow(false)
|
||||
}, [router])
|
||||
|
||||
useEffect(() => {
|
||||
const handleContextMenu = event => {
|
||||
event.preventDefault()
|
||||
// 计算点击位置加菜单宽高是否超出屏幕,如果超出则贴边弹出
|
||||
const x = (event.clientX < windowSize.width - width) ? event.clientX : windowSize.width - width
|
||||
const y = (event.clientY < windowSize.height - height) ? event.clientY : windowSize.height - height
|
||||
const x =
|
||||
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` })
|
||||
setShow(true)
|
||||
}
|
||||
|
||||
const handleClick = (event) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
||||
setShow(false)
|
||||
}
|
||||
/**
|
||||
* 鼠标点击即关闭菜单
|
||||
*/
|
||||
const handleClick = event => {
|
||||
setShow(false)
|
||||
}
|
||||
|
||||
window.addEventListener('contextmenu', handleContextMenu)
|
||||
@@ -75,24 +86,24 @@ export default function CustomContextMenu(props) {
|
||||
|
||||
function handleScrollTop() {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
setShow(false)
|
||||
}
|
||||
|
||||
function handleCopyLink() {
|
||||
const url = window.location.href
|
||||
navigator.clipboard.writeText(url)
|
||||
navigator.clipboard
|
||||
.writeText(url)
|
||||
.then(() => {
|
||||
console.log('页面地址已复制')
|
||||
// console.log('页面地址已复制')
|
||||
alert(`${locale.COMMON.PAGE_URL_COPIED} : ${url}`)
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
console.error('复制页面地址失败:', error)
|
||||
})
|
||||
setShow(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换主题
|
||||
*/
|
||||
* 切换主题
|
||||
*/
|
||||
function handleChangeTheme() {
|
||||
const randomTheme = THEMES[Math.floor(Math.random() * THEMES.length)] // 从THEMES数组中 随机取一个主题
|
||||
const query = router.query
|
||||
@@ -104,7 +115,7 @@ export default function CustomContextMenu(props) {
|
||||
* 复制内容
|
||||
*/
|
||||
function handleCopy() {
|
||||
const selectedText = document.getSelection().toString();
|
||||
const selectedText = document.getSelection().toString()
|
||||
if (selectedText) {
|
||||
const tempInput = document.createElement('input');
|
||||
tempInput.value = selectedText;
|
||||
@@ -118,8 +129,6 @@ export default function CustomContextMenu(props) {
|
||||
} else {
|
||||
// alert("Please select some text first.");
|
||||
}
|
||||
|
||||
setShow(false)
|
||||
}
|
||||
|
||||
function handleChangeDarkMode() {
|
||||
@@ -131,77 +140,140 @@ export default function CustomContextMenu(props) {
|
||||
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 (
|
||||
<div
|
||||
ref={menuRef}
|
||||
style={{ top: position.y, left: position.x }}
|
||||
className={`${show ? '' : 'invisible opacity-0'} select-none transition-opacity duration-200 fixed z-50`}
|
||||
>
|
||||
<div
|
||||
ref={menuRef}
|
||||
style={{ top: position.y, left: position.x }}
|
||||
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>
|
||||
|
||||
{/* 菜单内容 */}
|
||||
<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>
|
||||
<hr className='my-2 border-dashed' />
|
||||
|
||||
{/* 跳转导航按钮 */}
|
||||
<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 >
|
||||
)}
|
||||
|
||||
{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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@ import LA51 from './LA51'
|
||||
import TianLiGPT from './TianliGPT'
|
||||
import WebWhiz from './Webwhiz'
|
||||
|
||||
import { CUSTOM_EXTERNAL_CSS, CUSTOM_EXTERNAL_JS } from '@/blog.config'
|
||||
import { mapPageUrl } from '@/lib/notion/mapPageUrl'
|
||||
import { convertInnerUrl } from '@/lib/notion/convertInnerUrl'
|
||||
import { isBrowser, loadExternalResource } from '@/lib/utils'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
@@ -44,8 +43,6 @@ const ExternalPlugin = props => {
|
||||
const CHATBASE_ID = siteConfig('CHATBASE_ID')
|
||||
const COMMENT_DAO_VOICE_ID = siteConfig('COMMENT_DAO_VOICE_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_JS = siteConfig('COMMENT_ARTALK_JS')
|
||||
const COMMENT_TIDIO_ID = siteConfig('COMMENT_TIDIO_ID')
|
||||
@@ -64,6 +61,8 @@ const ExternalPlugin = props => {
|
||||
const IMG_SHADOW = siteConfig('IMG_SHADOW')
|
||||
const ANIMATE_CSS_URL = siteConfig('ANIMATE_CSS_URL')
|
||||
const MOUSE_FOLLOW = siteConfig('MOUSE_FOLLOW')
|
||||
const CUSTOM_EXTERNAL_CSS = siteConfig('CUSTOM_EXTERNAL_CSS')
|
||||
const CUSTOM_EXTERNAL_JS = siteConfig('CUSTOM_EXTERNAL_JS')
|
||||
|
||||
// 自定义样式css和js引入
|
||||
if (isBrowser) {
|
||||
@@ -106,7 +105,7 @@ const ExternalPlugin = props => {
|
||||
}
|
||||
|
||||
// 映射url
|
||||
mapPageUrl(props?.allNavPages)
|
||||
convertInnerUrl(props?.allNavPages)
|
||||
}, [router])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -18,7 +18,7 @@ const Fireworks = () => {
|
||||
// 异步加载
|
||||
async function loadFireworks() {
|
||||
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'
|
||||
).then(() => {
|
||||
loadExternalResource('/js/fireworks.js', 'js').then(() => {
|
||||
@@ -35,7 +35,11 @@ const Fireworks = () => {
|
||||
loadFireworks()
|
||||
|
||||
return () => {
|
||||
// 在组件卸载时清理资源(如果需要)
|
||||
// 在组件卸载时清理资源
|
||||
const fireworksElements = document.getElementsByClassName('fireworks')
|
||||
while (fireworksElements.length > 0) {
|
||||
fireworksElements[0].parentNode.removeChild(fireworksElements[0])
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import Head from 'next/head'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
/**
|
||||
* 图片懒加载
|
||||
* @param {*} param0
|
||||
@@ -20,27 +21,52 @@ export default function LazyImage({
|
||||
style
|
||||
}) {
|
||||
const maxWidth = siteConfig('IMAGE_COMPRESS_WIDTH')
|
||||
const defaultPlaceholderSrc = siteConfig('IMG_LAZY_LOAD_PLACEHOLDER')
|
||||
const imageRef = useRef(null)
|
||||
const [adjustedSrc, setAdjustedSrc] = useState(
|
||||
placeholderSrc || siteConfig('IMG_LAZY_LOAD_PLACEHOLDER')
|
||||
const [currentSrc, setCurrentSrc] = useState(
|
||||
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') {
|
||||
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(() => {
|
||||
const adjustedImageSrc = adjustImgSize(src, maxWidth)
|
||||
setAdjustedSrc(adjustedImageSrc)
|
||||
const 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(
|
||||
entries => {
|
||||
@@ -69,9 +95,10 @@ export default function LazyImage({
|
||||
// 动态添加width、height和className属性,仅在它们为有效值时添加
|
||||
const imgProps = {
|
||||
ref: imageRef,
|
||||
src: priority ? adjustedSrc : placeholderSrc,
|
||||
src: currentSrc,
|
||||
alt: alt,
|
||||
onLoad: handleImageLoad
|
||||
onLoad: handleThumbnailLoaded, // 缩略图加载完成
|
||||
onError: handleImageError // 添加onError处理函数
|
||||
}
|
||||
|
||||
if (id) {
|
||||
@@ -95,6 +122,7 @@ export default function LazyImage({
|
||||
if (style) {
|
||||
imgProps.style = style
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
@@ -102,24 +130,25 @@ export default function LazyImage({
|
||||
{/* 预加载 */}
|
||||
{priority && (
|
||||
<Head>
|
||||
<link rel='preload' as='image' href={adjustedSrc} />
|
||||
<link rel='preload' as='image' href={adjustImgSize(src, maxWidth)} />
|
||||
</Head>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据窗口尺寸决定压缩图片宽度
|
||||
* @param {*} src
|
||||
* @param {*} maxWidth
|
||||
* @returns
|
||||
*/
|
||||
|
||||
const adjustImgSize = (src, maxWidth) => {
|
||||
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) {
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function LoadingProgress() {
|
||||
// 加载进度条
|
||||
useEffect(() => {
|
||||
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'
|
||||
).then(() => {
|
||||
if (window.NProgress) {
|
||||
@@ -19,7 +19,7 @@ export default function LoadingProgress() {
|
||||
// 调速
|
||||
window.NProgress.settings.minimun = 0.1
|
||||
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'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,8 +15,26 @@ const MOUSE_FOLLOW = () => {
|
||||
loadExternalResource('/js/mouse-follow.js', 'js').then(url => {
|
||||
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
|
||||
|
||||
63
components/Notification.js
Normal file
63
components/Notification.js
Normal 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
|
||||
@@ -31,7 +31,7 @@ const NotionPage = ({ post, className }) => {
|
||||
// 页面首次打开时执行的勾子
|
||||
useEffect(() => {
|
||||
// 检测当前的url并自动滚动到对应目标
|
||||
autoScrollToTarget()
|
||||
autoScrollToHash()
|
||||
}, [])
|
||||
|
||||
// 页面文章发生变化时会执行的勾子
|
||||
@@ -144,9 +144,9 @@ const processGalleryImg = zoom => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据url参数自动滚动到指定区域
|
||||
* 根据url参数自动滚动到锚位置
|
||||
*/
|
||||
const autoScrollToTarget = () => {
|
||||
const autoScrollToHash = () => {
|
||||
setTimeout(() => {
|
||||
// 跳转到指定标题
|
||||
const needToJumpToTitle = window.location.hash
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useState } from 'react'
|
||||
import { Draggable } from './Draggable'
|
||||
import LANGS from '@/lib/lang'
|
||||
import { getQueryParam } from '@/lib/utils'
|
||||
import { THEMES } from '@/themes/theme'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
import DarkModeButton from './DarkModeButton'
|
||||
import { getQueryParam } from '@/lib/utils'
|
||||
import LANGS from '@/lib/lang'
|
||||
import { Draggable } from './Draggable'
|
||||
/**
|
||||
*
|
||||
* @returns 主题切换
|
||||
*/
|
||||
const ThemeSwitch = () => {
|
||||
const { theme, lang, changeLang, locale, isDarkMode, toggleDarkMode } = useGlobal()
|
||||
const { theme, lang, changeLang, locale, isDarkMode, toggleDarkMode } =
|
||||
useGlobal()
|
||||
const router = useRouter()
|
||||
const currentTheme = getQueryParam(router.asPath, 'theme') || theme
|
||||
// const currentLang = getQueryParam(router.asPath, 'lang') || lang
|
||||
@@ -19,8 +20,12 @@ const ThemeSwitch = () => {
|
||||
|
||||
// 修改当前路径url中的 theme 参数
|
||||
// 例如 http://localhost?theme=hexo 跳转到 http://localhost?theme=newTheme
|
||||
const onThemeSelectChange = (e) => {
|
||||
document.ontouchmove = document.ontouchend = document.onmousemove = document.onmouseup = null
|
||||
const onThemeSelectChange = e => {
|
||||
document.ontouchmove =
|
||||
document.ontouchend =
|
||||
document.onmousemove =
|
||||
document.onmouseup =
|
||||
null
|
||||
setIsLoading(true)
|
||||
const newTheme = e.target.value
|
||||
const query = router.query
|
||||
@@ -28,59 +33,96 @@ const ThemeSwitch = () => {
|
||||
router.push({ pathname: router.pathname, query }).then(() => {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
}, 500);
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
const onLangSelectChange = (e) => {
|
||||
document.ontouchmove = document.ontouchend = document.onmousemove = document.onmouseup = null
|
||||
const onLangSelectChange = e => {
|
||||
document.ontouchmove =
|
||||
document.ontouchend =
|
||||
document.onmousemove =
|
||||
document.onmouseup =
|
||||
null
|
||||
const newLang = e.target.value
|
||||
changeLang(newLang)
|
||||
}
|
||||
|
||||
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 ">
|
||||
{/* 深色按钮 */}
|
||||
<div className="text-sm flex items-center w-0 group-hover:w-32 transition-all duration-200">
|
||||
<DarkModeButton />
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* 翻译按钮 */}
|
||||
<div className="text-sm flex items-center group-hover:w-32 transition-all duration-200">
|
||||
<i className="fa-solid fa-language w-5" />
|
||||
<div className='w-0 group-hover:w-24 transition-all duration-200 overflow-hidden'>
|
||||
<label htmlFor="langSelect" className="sr-only">选择语言:</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 className="text-sm flex items-center group-hover:w-32 transition-all duration-200">
|
||||
<i className="fa-solid fa-palette w-5" />
|
||||
<div className='w-0 group-hover:w-24 transition-all duration-200 overflow-hidden'>
|
||||
<label htmlFor="themeSelect" className="sr-only">选择主题:</label>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
<Draggable>
|
||||
<div
|
||||
id='draggableBox'
|
||||
style={{ left: '0px', top: '80vh' }}
|
||||
className='fixed group flex flex-col items-start space-y-2 overflow-hidden z-50 p-3
|
||||
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 duration-200'>
|
||||
<i className='fa-solid fa-palette w-5' />
|
||||
<div className='w-0 group-hover:w-24 duration-200 overflow-hidden'>
|
||||
<label htmlFor='themeSelect' className='sr-only'>
|
||||
{locale.COMMON.THEME}
|
||||
</label>
|
||||
<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>
|
||||
</div>
|
||||
</Draggable>
|
||||
</div>
|
||||
|
||||
{/* 切换主题加载时的全屏遮罩 */}
|
||||
<div className={`${isLoading ? 'opacity-90 ' : 'opacity-0'}
|
||||
w-screen h-screen glassmorphism bg-black text-white shadow-text flex justify-center items-center
|
||||
transition-all 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 className='text-sm flex items-center w-0 group-hover:w-32 duration-200'>
|
||||
<DarkModeButton />
|
||||
<div
|
||||
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>
|
||||
</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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user