mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-18 15:09:34 +00:00
Merge branch 'main' into pr/jxpeng98/1257
This commit is contained in:
92
components/AlgoliaSearchModal.js
Normal file
92
components/AlgoliaSearchModal.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useState, useImperativeHandle } from 'react'
|
||||
import BLOG from '@/blog.config'
|
||||
import algoliasearch from 'algoliasearch'
|
||||
import replaceSearchResult from '@/components/Mark'
|
||||
|
||||
/**
|
||||
* 结合 Algolia 实现的弹出式搜索框
|
||||
* 打开方式 cRef.current.openSearch()
|
||||
* https://www.algolia.com/doc/api-reference/search-api-parameters/
|
||||
*/
|
||||
export default function AlgoliaSearchModal({ cRef }) {
|
||||
const [searchResults, setSearchResults] = useState([])
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
/**
|
||||
* 对外暴露方法
|
||||
*/
|
||||
useImperativeHandle(cRef, () => {
|
||||
return {
|
||||
openSearch: () => {
|
||||
setIsModalOpen(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!BLOG.ALGOLIA_APP_ID) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const client = algoliasearch(BLOG.ALGOLIA_APP_ID, BLOG.ALGOLIA_SEARCH_ONLY_APP_KEY)
|
||||
const index = client.initIndex(BLOG.ALGOLIA_INDEX)
|
||||
|
||||
const handleSearch = async (query) => {
|
||||
try {
|
||||
const res = await index.search(query)
|
||||
console.log(res)
|
||||
const { hits } = res
|
||||
setSearchResults(hits)
|
||||
const doms = document.getElementById('search-wrapper').getElementsByClassName('replace')
|
||||
replaceSearchResult({
|
||||
doms,
|
||||
search: query,
|
||||
target: {
|
||||
element: 'span',
|
||||
className: 'text-blue-600 border-b border-dashed'
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Algolia search error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
setIsModalOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div id='search-wrapper' className={`${isModalOpen ? 'opacity-100' : 'invisible opacity-0 pointer-events-none'} fixed h-screen w-screen left-0 top-0 flex items-center justify-center`}>
|
||||
{/* 内容 */}
|
||||
<div className={`${isModalOpen ? 'opacity-100' : 'invisible opacity-0 translate-y-10'} flex flex-col justify-between w-full min-h-[10rem] 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 font-bold'>搜索</div>
|
||||
<div><i class="text-gray-600 fa-solid fa-xmark p-1 cursor-pointer hover:text-blue-600" onClick={closeModal} ></i></div>
|
||||
</div>
|
||||
|
||||
<input type="text" placeholder="在这里输入搜索关键词..." onChange={(e) => handleSearch(e.target.value)}
|
||||
className="bg-gray-50 dark:bg-gray-600 outline-blue-500 w-full px-4 my-2 py-1 mb-4 border rounded-md" />
|
||||
|
||||
{/* 标签组 */}
|
||||
<div>
|
||||
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{searchResults.map((result) => (
|
||||
<li key={result.objectID} className="replace my-2">
|
||||
<a href={`${BLOG.SUB_PATH}/${result.slug}`} className="font-bold hover:text-blue-600 ">
|
||||
{result.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className='text-gray-600'><i class="fa-brands fa-algolia"></i> Algolia 提供搜索服务</div>
|
||||
</div>
|
||||
|
||||
{/* 遮罩 */}
|
||||
<div onClick={closeModal} className="z-30 fixed top-0 left-0 w-full h-full flex items-center justify-center glassmorphism" />
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
19
components/ChatBase.js
Normal file
19
components/ChatBase.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
/**
|
||||
* 这是一个嵌入组件,可以在任意位置全屏显示您的chat-base对话框
|
||||
* 暂时没有页面引用
|
||||
* 因为您可以直接用内嵌网页的方式放入您的notion中 https://www.chatbase.co/chatbot-iframe/${BLOG.CHATBASE_ID}
|
||||
*/
|
||||
export default function ChatBase() {
|
||||
if (!BLOG.CHATBASE_ID) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return <iframe
|
||||
src={`https://www.chatbase.co/chatbot-iframe/${BLOG.CHATBASE_ID}`}
|
||||
width="100%"
|
||||
style={{ height: '100%', minHeight: '700px' }}
|
||||
frameborder="0"
|
||||
></iframe>
|
||||
}
|
||||
@@ -84,7 +84,7 @@ const Collapse = props => {
|
||||
}, [props.isOpen])
|
||||
|
||||
return (
|
||||
<div ref={ref} style={type === 'vertical' ? { height: '0px', willChange: 'height' } : { width: '0px', willChange: 'width' }} className={`${props.className} overflow-hidden duration-200 `}>
|
||||
<div ref={ref} style={type === 'vertical' ? { height: '0px', willChange: 'height' } : { width: '0px', willChange: 'width' }} className={`${props.className || ''} overflow-hidden duration-200 `}>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -54,7 +54,12 @@ const ValineComponent = dynamic(() => import('@/components/ValineComponent'), {
|
||||
ssr: false
|
||||
})
|
||||
|
||||
const Comment = ({ frontMatter }) => {
|
||||
/**
|
||||
* 评论组件
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const Comment = ({ frontMatter, className }) => {
|
||||
const router = useRouter()
|
||||
|
||||
if (isBrowser() && ('giscus' in router.query || router.query.target === 'comment')) {
|
||||
@@ -70,7 +75,7 @@ const Comment = ({ frontMatter }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div id='comment' className='comment mt-5 text-gray-800 dark:text-gray-300'>
|
||||
<div key={frontMatter?.id} id='comment' className={`comment mt-5 text-gray-800 dark:text-gray-300 ${className || ''}`}>
|
||||
<Tabs>
|
||||
|
||||
{BLOG.COMMENT_TWIKOO_ENV_ID && (<div key='Twikoo'>
|
||||
|
||||
@@ -16,55 +16,57 @@ const CommonHead = ({ meta, children }) => {
|
||||
const category = meta?.category || BLOG.KEYWORDS || '軟體科技' // section 主要是像是 category 這樣的分類,Facebook 用這個來抓連結的分類
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
<meta name="theme-color" content={BLOG.BACKGROUND_DARK} />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, minimum-scale=1.0"/>
|
||||
<meta name="robots" content="follow, index" />
|
||||
<meta charSet="UTF-8" />
|
||||
{BLOG.SEO_GOOGLE_SITE_VERIFICATION && (
|
||||
<meta
|
||||
name="google-site-verification"
|
||||
content={BLOG.SEO_GOOGLE_SITE_VERIFICATION}
|
||||
/>
|
||||
)}
|
||||
<meta name="keywords" content={keywords} />
|
||||
<meta name="description" content={description} />
|
||||
<meta property="og:locale" content={lang} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={url} />
|
||||
<meta property="og:image" content={image} />
|
||||
<meta property="og:site_name" content={BLOG.TITLE} />
|
||||
<meta property="og:type" content={type} />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
<meta name="theme-color" content={BLOG.BACKGROUND_DARK} />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, minimum-scale=1.0" />
|
||||
<meta name="robots" content="follow, index" />
|
||||
<meta charSet="UTF-8" />
|
||||
{BLOG.SEO_GOOGLE_SITE_VERIFICATION && (
|
||||
<meta
|
||||
name="google-site-verification"
|
||||
content={BLOG.SEO_GOOGLE_SITE_VERIFICATION}
|
||||
/>
|
||||
)}
|
||||
{BLOG.SEO_BAIDU_SITE_VERIFICATION && (<meta name="baidu-site-verification" content={BLOG.SEO_BAIDU_SITE_VERIFICATION} />)}
|
||||
<meta name="keywords" content={keywords} />
|
||||
<meta name="description" content={description} />
|
||||
<meta property="og:locale" content={lang} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={url} />
|
||||
<meta property="og:image" content={image} />
|
||||
<meta property="og:site_name" content={BLOG.TITLE} />
|
||||
<meta property="og:type" content={type} />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:title" content={title} />
|
||||
|
||||
{BLOG.COMMENT_WEBMENTION.ENABLE && (
|
||||
<>
|
||||
<link rel="webmention" href={`https://webmention.io/${BLOG.COMMENT_WEBMENTION.HOSTNAME}/webmention`} />
|
||||
<link rel="pingback" href={`https://webmention.io/${BLOG.COMMENT_WEBMENTION.HOSTNAME}/xmlrpc`} />
|
||||
</>
|
||||
)}
|
||||
{BLOG.COMMENT_WEBMENTION.ENABLE && BLOG.COMMENT_WEBMENTION.AUTH !== '' && (
|
||||
<link href={BLOG.COMMENT_WEBMENTION.AUTH} rel="me" />
|
||||
)}
|
||||
{BLOG.COMMENT_WEBMENTION.ENABLE && (
|
||||
<>
|
||||
<link rel="webmention" href={`https://webmention.io/${BLOG.COMMENT_WEBMENTION.HOSTNAME}/webmention`} />
|
||||
<link rel="pingback" href={`https://webmention.io/${BLOG.COMMENT_WEBMENTION.HOSTNAME}/xmlrpc`} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{JSON.parse(BLOG.ANALYTICS_BUSUANZI_ENABLE) && <meta name="referrer" content="no-referrer-when-downgrade" />}
|
||||
{meta?.type === 'Post' && (
|
||||
<>
|
||||
<meta
|
||||
property="article:published_time"
|
||||
content={meta.publishTime}
|
||||
/>
|
||||
<meta property="article:author" content={BLOG.AUTHOR} />
|
||||
<meta property="article:section" content={category} />
|
||||
<meta property="article:publisher" content={BLOG.FACEBOOK_PAGE} />
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</Head>
|
||||
{BLOG.COMMENT_WEBMENTION.ENABLE && BLOG.COMMENT_WEBMENTION.AUTH !== '' && (
|
||||
<link href={BLOG.COMMENT_WEBMENTION.AUTH} rel="me" />
|
||||
)}
|
||||
|
||||
{JSON.parse(BLOG.ANALYTICS_BUSUANZI_ENABLE) && <meta name="referrer" content="no-referrer-when-downgrade" />}
|
||||
{meta?.type === 'Post' && (
|
||||
<>
|
||||
<meta
|
||||
property="article:published_time"
|
||||
content={meta.publishTime}
|
||||
/>
|
||||
<meta property="article:author" content={BLOG.AUTHOR} />
|
||||
<meta property="article:section" content={category} />
|
||||
<meta property="article:publisher" content={BLOG.FACEBOOK_PAGE} />
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</Head>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,18 @@ import BLOG from '@/blog.config'
|
||||
*/
|
||||
const CommonScript = () => {
|
||||
return (<>
|
||||
|
||||
{BLOG.CHATBASE_ID && (<>
|
||||
<script id={BLOG.CHATBASE_ID} src="https://www.chatbase.co/embed.min.js" defer/>
|
||||
<script async dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.chatbaseConfig = {
|
||||
chatbotId: "${BLOG.CHATBASE_ID}",
|
||||
}
|
||||
`
|
||||
}}/>
|
||||
</>)}
|
||||
|
||||
{BLOG.COMMENT_DAO_VOICE_ID && (<>
|
||||
{/* DaoVoice 反馈 */}
|
||||
<script async dangerouslySetInnerHTML={{
|
||||
@@ -26,10 +38,6 @@ const CommonScript = () => {
|
||||
/>
|
||||
</>)}
|
||||
|
||||
{/* GoogleAdsense 本地开发请加入 data-adbreak-test="on" */}
|
||||
{BLOG.ADSENSE_GOOGLE_ID && <script async src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${BLOG.ADSENSE_GOOGLE_ID}`}
|
||||
crossOrigin="anonymous" />}
|
||||
|
||||
{BLOG.COMMENT_CUSDIS_APP_ID && <script defer src='https://cusdis.com/js/widget/lang/zh-cn.js' />}
|
||||
|
||||
{BLOG.COMMENT_TIDIO_ID && <script async src={`//code.tidio.co/${BLOG.COMMENT_TIDIO_ID}.js`} />}
|
||||
|
||||
158
components/CustomContextMenu.js
Normal file
158
components/CustomContextMenu.js
Normal file
@@ -0,0 +1,158 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { saveDarkModeToCookies, THEMES } from '@/themes/theme'
|
||||
|
||||
/**
|
||||
* 自定义右键菜单
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
export default function CustomContextMenu(props) {
|
||||
const [position, setPosition] = useState({ x: '0px', y: '0px' })
|
||||
const [show, setShow] = useState(false)
|
||||
const { isDarkMode, updateDarkMode, locale } = useGlobal()
|
||||
const menuRef = useRef(null)
|
||||
|
||||
const { latestPosts } = props
|
||||
const router = useRouter()
|
||||
function handleJumpToRandomPost() {
|
||||
const randomIndex = Math.floor(Math.random() * latestPosts.length)
|
||||
const randomPost = latestPosts[randomIndex]
|
||||
router.push(randomPost.slug)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleContextMenu = (event) => {
|
||||
event.preventDefault()
|
||||
setPosition({ y: `${event.clientY}px`, x: `${event.clientX}px` })
|
||||
setShow(true)
|
||||
}
|
||||
|
||||
const handleClick = (event) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
||||
setShow(false)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('contextmenu', handleContextMenu)
|
||||
window.addEventListener('click', handleClick)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('contextmenu', handleContextMenu)
|
||||
window.removeEventListener('click', handleClick)
|
||||
}
|
||||
}, [])
|
||||
|
||||
function handleBack() {
|
||||
window.history.back()
|
||||
}
|
||||
|
||||
function handleForward() {
|
||||
window.history.forward()
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
function handleScrollTop() {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
setShow(false)
|
||||
}
|
||||
|
||||
function handleCopyLink() {
|
||||
const url = window.location.href
|
||||
navigator.clipboard.writeText(url)
|
||||
.then(() => {
|
||||
console.log('页面地址已复制')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('复制页面地址失败:', error)
|
||||
})
|
||||
setShow(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换主题
|
||||
*/
|
||||
function handeChangeTheme() {
|
||||
const randomTheme = THEMES[Math.floor(Math.random() * THEMES.length)] // 从THEMES数组中 随机取一个主题
|
||||
const query = router.query
|
||||
query.theme = randomTheme
|
||||
router.push({ pathname: router.pathname, query })
|
||||
}
|
||||
|
||||
function handleChangeDarkMode() {
|
||||
const newStatus = !isDarkMode
|
||||
saveDarkModeToCookies(newStatus)
|
||||
updateDarkMode(newStatus)
|
||||
const htmlElement = document.getElementsByTagName('html')[0]
|
||||
htmlElement.classList?.remove(newStatus ? 'light' : 'dark')
|
||||
htmlElement.classList?.add(newStatus ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
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 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'>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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'>
|
||||
|
||||
<div onClick={handleCopyLink} title={locale.MENU.COPY_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.COPY_URL}</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<div onClick={handeChangeTheme} 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 >
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,26 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { saveDarkModeToCookies } from '@/themes/theme'
|
||||
import { Moon, Sun } from './HeroIcons'
|
||||
import { useImperativeHandle } from 'react'
|
||||
|
||||
/**
|
||||
* 深色模式按钮
|
||||
*/
|
||||
const DarkModeButton = (props) => {
|
||||
const { cRef, className } = props
|
||||
const { isDarkMode, updateDarkMode } = useGlobal()
|
||||
|
||||
/**
|
||||
* 对外暴露方法
|
||||
*/
|
||||
useImperativeHandle(cRef, () => {
|
||||
return {
|
||||
handleChangeDarkMode: () => {
|
||||
handleChangeDarkMode()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 用户手动设置主题
|
||||
const handleChangeDarkMode = () => {
|
||||
const newStatus = !isDarkMode
|
||||
@@ -14,7 +31,7 @@ const DarkModeButton = (props) => {
|
||||
htmlElement.classList?.add(newStatus ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
return <div onClick={handleChangeDarkMode} className={`${props.className ? props.className : ''} flex justify-center dark:text-gray-200 text-gray-800`}>
|
||||
return <div onClick={handleChangeDarkMode} className={`${className || ''} flex justify-center dark:text-gray-200 text-gray-800`}>
|
||||
<div id='darkModeButton' className=' hover:scale-110 cursor-pointer transform duration-200 w-5 h-5'> {isDarkMode ? <Sun /> : <Moon />}</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ const Busuanzi = dynamic(() => import('@/components/Busuanzi'), { ssr: false })
|
||||
const GoogleAdsense = dynamic(() => import('@/components/GoogleAdsense'), { ssr: false })
|
||||
const Messenger = dynamic(() => import('@/components/FacebookMessenger'), { ssr: false })
|
||||
const VConsole = dynamic(() => import('@/components/VConsole'), { ssr: false })
|
||||
const CustomContextMenu = dynamic(() => import('@/components/CustomContextMenu'), { ssr: false })
|
||||
|
||||
/**
|
||||
* 各种第三方组件
|
||||
@@ -53,6 +54,7 @@ const ExternalPlugin = (props) => {
|
||||
{JSON.parse(BLOG.FLUTTERINGRIBBON) && <FlutteringRibbon />}
|
||||
{JSON.parse(BLOG.COMMENT_TWIKOO_COUNT_ENABLE) && <TwikooCommentCounter {...props}/>}
|
||||
{JSON.parse(BLOG.RIBBON) && <Ribbon />}
|
||||
{JSON.parse(BLOG.CUSTOM_RIGHT_CLICK_CONTEXT_MENU) && <CustomContextMenu {...props} />}
|
||||
<VConsole/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import BLOG from '@/blog.config'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useEffect } from 'react'
|
||||
import { isBrowser, loadExternalResource } from '@/lib/utils'
|
||||
|
||||
/**
|
||||
* 自定义引入外部JS 和 CSS
|
||||
* @returns
|
||||
*/
|
||||
const ExternalScript = () => {
|
||||
useEffect(() => {
|
||||
if (isBrowser()) {
|
||||
// 静态导入本地自定义样式
|
||||
loadExternalResource(BLOG.FONT_AWESOME, 'css')
|
||||
loadExternalResource('/css/custom.css', 'css')
|
||||
loadExternalResource('/js/custom.js', 'js')
|
||||
|
||||
@@ -28,12 +28,7 @@ const ExternalScript = () => {
|
||||
loadExternalResource(url, 'css')
|
||||
}
|
||||
}
|
||||
// 渲染所有字体
|
||||
BLOG.FONT_URL?.forEach(e => {
|
||||
loadExternalResource(e, 'css')
|
||||
})
|
||||
}, [])
|
||||
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
56
components/FlipCard.js
Normal file
56
components/FlipCard.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
/**
|
||||
* 翻转组件
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
export default function FlipCard(props) {
|
||||
const [isFlipped, setIsFlipped] = useState(false)
|
||||
|
||||
function handleCardFlip() {
|
||||
setIsFlipped(!isFlipped)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flip-card ${isFlipped ? 'flipped' : ''}`} >
|
||||
<div className={`flip-card-front ${props.className || ''}`} onMouseEnter={handleCardFlip}>
|
||||
{props.frontContent}
|
||||
</div>
|
||||
<div className={`flip-card-back ${props.className || ''}`} onMouseLeave={handleCardFlip}>
|
||||
{props.backContent}
|
||||
</div>
|
||||
<style jsx>{`
|
||||
.flip-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.flip-card-front,
|
||||
.flip-card-back {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.flip-card-front {
|
||||
z-index: 2;
|
||||
transform: rotateY(0);
|
||||
}
|
||||
|
||||
.flip-card-back {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.flip-card.flipped {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
48
components/FullScreenButton.js
Normal file
48
components/FullScreenButton.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
/**
|
||||
* 全屏按钮
|
||||
* @returns
|
||||
*/
|
||||
const FullScreenButton = () => {
|
||||
const [isFullScreen, setIsFullScreen] = useState(false)
|
||||
|
||||
const handleFullScreenClick = () => {
|
||||
if (!isBrowser()) {
|
||||
return
|
||||
}
|
||||
const element = document.documentElement
|
||||
if (!isFullScreen) {
|
||||
if (element.requestFullscreen) {
|
||||
element.requestFullscreen()
|
||||
} else if (element.webkitRequestFullscreen) {
|
||||
element.webkitRequestFullscreen()
|
||||
} else if (element.mozRequestFullScreen) {
|
||||
element.mozRequestFullScreen()
|
||||
} else if (element.msRequestFullscreen) {
|
||||
element.msRequestFullscreen()
|
||||
}
|
||||
setIsFullScreen(true)
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen()
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen()
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen()
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen()
|
||||
}
|
||||
setIsFullScreen(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={handleFullScreenClick} className='dark:text-gray-300'>
|
||||
{isFullScreen ? '退出全屏' : <i className="fa-solid fa-expand"></i>}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default FullScreenButton
|
||||
@@ -1,4 +1,5 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
@@ -8,25 +9,35 @@ import { useEffect } from 'react'
|
||||
*/
|
||||
export default function GoogleAdsense() {
|
||||
const initGoogleAdsense = () => {
|
||||
setTimeout(() => {
|
||||
const ads = document.getElementsByClassName('adsbygoogle')
|
||||
const adsbygoogle = window.adsbygoogle
|
||||
if (ads.length > 0) {
|
||||
for (let i = 0; i <= ads.length; i++) {
|
||||
try {
|
||||
adsbygoogle.push(ads[i])
|
||||
console.log('adsbygoogle', i, ads[i], adsbygoogle)
|
||||
} catch (e) {
|
||||
// GoogleAdsense 本地开发请加入 data-adbreak-test="on"
|
||||
// {BLOG.ADSENSE_GOOGLE_ID && <script async src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${BLOG.ADSENSE_GOOGLE_ID}`}
|
||||
// crossOrigin="anonymous" />}
|
||||
|
||||
loadExternalResource(`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${BLOG.ADSENSE_GOOGLE_ID}`, 'js').then(url => {
|
||||
setTimeout(() => {
|
||||
const ads = document.getElementsByClassName('adsbygoogle')
|
||||
const adsbygoogle = window.adsbygoogle
|
||||
console.log('google-ads', adsbygoogle)
|
||||
if (ads.length > 0) {
|
||||
for (let i = 0; i <= ads.length; i++) {
|
||||
try {
|
||||
adsbygoogle.push(ads[i])
|
||||
console.log('adsbygoogle', i, ads[i], adsbygoogle)
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 100)
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
initGoogleAdsense()
|
||||
// 延迟3秒加载
|
||||
setTimeout(() => {
|
||||
initGoogleAdsense()
|
||||
}, 3000)
|
||||
}, [router])
|
||||
|
||||
return null
|
||||
@@ -50,7 +61,7 @@ const AdSlot = ({ type = 'show' }) => {
|
||||
data-ad-format="fluid"
|
||||
data-adtest={BLOG.ADSENSE_GOOGLE_TEST ? 'on' : 'off'}
|
||||
data-ad-client={BLOG.ADSENSE_GOOGLE_ID}
|
||||
data-ad-slot="3806269138"></ins>
|
||||
data-ad-slot={BLOG.ADSENSE_GOOGLE_SLOT_IN_ARTICLE}></ins>
|
||||
}
|
||||
|
||||
// 信息流广告
|
||||
@@ -61,7 +72,7 @@ const AdSlot = ({ type = 'show' }) => {
|
||||
style={{ display: 'block' }}
|
||||
data-adtest={BLOG.ADSENSE_GOOGLE_TEST ? 'on' : 'off'}
|
||||
data-ad-client={BLOG.ADSENSE_GOOGLE_ID}
|
||||
data-ad-slot="1510444138"></ins>
|
||||
data-ad-slot={BLOG.ADSENSE_GOOGLE_SLOT_FLOW}></ins>
|
||||
}
|
||||
|
||||
// 原生广告
|
||||
@@ -71,7 +82,7 @@ const AdSlot = ({ type = 'show' }) => {
|
||||
data-ad-format="autorelaxed"
|
||||
data-adtest={BLOG.ADSENSE_GOOGLE_TEST ? 'on' : 'off'}
|
||||
data-ad-client={BLOG.ADSENSE_GOOGLE_ID}
|
||||
data-ad-slot="4980048999"></ins>
|
||||
data-ad-slot={BLOG.ADSENSE_GOOGLE_SLOT_NATIVE}></ins>
|
||||
}
|
||||
|
||||
// 展示广告
|
||||
@@ -79,7 +90,7 @@ const AdSlot = ({ type = 'show' }) => {
|
||||
style={{ display: 'block' }}
|
||||
data-ad-client={BLOG.ADSENSE_GOOGLE_ID}
|
||||
data-adtest={BLOG.ADSENSE_GOOGLE_TEST ? 'on' : 'off'}
|
||||
data-ad-slot="8807314373"
|
||||
data-ad-slot={BLOG.ADSENSE_GOOGLE_SLOT_AUTO}
|
||||
data-ad-format="auto"
|
||||
data-full-width-responsive="true"></ins>
|
||||
}
|
||||
|
||||
@@ -3,27 +3,98 @@
|
||||
* @returns
|
||||
*/
|
||||
|
||||
const Moon = () => {
|
||||
export const Moon = () => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||
</svg>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
const Sun = () => {
|
||||
export const Sun = () => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const Home = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const User = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const ArrowPath = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const ChevronLeft = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const ChevronRight = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const ChevronDoubleLeft = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M18.75 19.5l-7.5-7.5 7.5-7.5m-6 15L5.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
const Home = ({ className }) => {
|
||||
export const ChevronDoubleRight = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M11.25 4.5l7.5 7.5-7.5 7.5m-6-15l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
const User = ({ className }) => {
|
||||
export const InformationCircle = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const HashTag = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5.25 8.25h15m-16.5 7.5h15m-1.8-13.5l-3.9 19.5m-2.1-19.5l-3.9 19.5" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const GlobeAlt = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const ArrowRightCircle = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12.75 15l3-3m0 0l-3-3m3 3h-7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const PlusSmall = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v12m6-6H6" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const ArrowSmallRight = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const ArrowSmallUp = ({ className }) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 19.5v-15m0 0l-6.75 6.75M12 4.5l6.75 6.75" />
|
||||
</svg>
|
||||
}
|
||||
export { Moon, Sun, Home, User }
|
||||
|
||||
95
components/LazyImage.js
Normal file
95
components/LazyImage.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import Head from 'next/head'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
|
||||
/**
|
||||
* 图片懒加载
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function LazyImage({
|
||||
priority,
|
||||
id,
|
||||
src,
|
||||
alt,
|
||||
placeholderSrc = BLOG.IMG_LAZY_LOAD_PLACEHOLDER,
|
||||
className,
|
||||
width,
|
||||
height,
|
||||
title,
|
||||
onLoad,
|
||||
style
|
||||
}) {
|
||||
const imageRef = useRef(null)
|
||||
const [imageLoaded, setImageLoaded] = useState(false)
|
||||
|
||||
const handleImageLoad = () => {
|
||||
setImageLoaded(true)
|
||||
if (typeof onLoad === 'function') {
|
||||
onLoad() // 触发传递的onLoad回调函数
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const lazyImage = entry.target
|
||||
lazyImage.src = src
|
||||
observer.unobserve(lazyImage)
|
||||
}
|
||||
})
|
||||
},
|
||||
{ rootMargin: '50px 0px' } // Adjust the rootMargin as needed to trigger the loading earlier or later
|
||||
)
|
||||
|
||||
if (imageRef.current) {
|
||||
observer.observe(imageRef.current)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (imageRef.current) {
|
||||
observer.unobserve(imageRef.current)
|
||||
}
|
||||
}
|
||||
}, [src])
|
||||
|
||||
// 动态添加width、height和className属性,仅在它们为有效值时添加
|
||||
const imgProps = {
|
||||
ref: imageRef,
|
||||
src: imageLoaded ? src : placeholderSrc,
|
||||
alt: alt,
|
||||
onLoad: handleImageLoad
|
||||
}
|
||||
|
||||
if (id) {
|
||||
imgProps.id = id
|
||||
}
|
||||
|
||||
if (title) {
|
||||
imgProps.title = title
|
||||
}
|
||||
|
||||
if (width && width !== 'auto') {
|
||||
imgProps.width = width
|
||||
}
|
||||
|
||||
if (height && height !== 'auto') {
|
||||
imgProps.height = height
|
||||
}
|
||||
if (className) {
|
||||
imgProps.className = className
|
||||
}
|
||||
if (style) {
|
||||
imgProps.style = style
|
||||
}
|
||||
return (<>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img {...imgProps} />
|
||||
{/* 预加载 */}
|
||||
{priority && <Head>
|
||||
<link rel='preload' as='image' src={src} />
|
||||
</Head>}
|
||||
</>)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
|
||||
/**
|
||||
* 加载文件时的全局遮罩
|
||||
* @returns
|
||||
*/
|
||||
const LoadingCover = (props) => {
|
||||
const { onReading } = props
|
||||
|
||||
return <div className={`${onReading ? 'opacity-30' : 'opacity-0'} bg-black text-white shadow-text w-screen h-screen 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>
|
||||
}
|
||||
export default LoadingCover
|
||||
31
components/Mark.js
Normal file
31
components/Mark.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
|
||||
/**
|
||||
* 将搜索结果的关键词高亮
|
||||
*/
|
||||
export default async function replaceSearchResult({ doms, search, target }) {
|
||||
if (!doms || !search || !target) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const url = await loadExternalResource('https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/mark.min.js', 'js')
|
||||
console.log('markjs 加载成功', url, window.Mark)
|
||||
console.log('------', doms)
|
||||
|
||||
const Mark = window.Mark
|
||||
if (doms instanceof HTMLCollection) {
|
||||
for (const container of doms) {
|
||||
const re = new RegExp(search, 'gim')
|
||||
const instance = new Mark(container)
|
||||
instance.markRegExp(re, target)
|
||||
}
|
||||
} else {
|
||||
const re = new RegExp(search, 'gim')
|
||||
const instance = new Mark(doms)
|
||||
instance.markRegExp(re, target)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('markjs 加载失败', error)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import LazyImage from './LazyImage'
|
||||
|
||||
/**
|
||||
* notion的图标icon
|
||||
* 可能是emoji 可能是 svg 也可能是 图片
|
||||
@@ -9,9 +11,7 @@ const NotionIcon = ({ icon }) => {
|
||||
}
|
||||
|
||||
if (icon.startsWith('http') || icon.startsWith('data:')) {
|
||||
// return <Image src={icon} width={30} height={30}/>
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
return <img src={icon} className='w-8 h-8 my-auto inline mr-1'/>
|
||||
return <LazyImage src={icon} className='w-8 h-8 my-auto inline mr-1'/>
|
||||
}
|
||||
|
||||
return <span className='mr-1'>{icon}</span>
|
||||
|
||||
@@ -5,9 +5,9 @@ import React, { useEffect, useRef } from 'react'
|
||||
// import { Code } from 'react-notion-x/build/third-party/code'
|
||||
import TweetEmbed from 'react-tweet-embed'
|
||||
|
||||
import BLOG from '@/blog.config'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import { mapImgUrl } from '@/lib/notion/mapImage'
|
||||
import BLOG from '@/blog.config'
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
|
||||
const Code = dynamic(() =>
|
||||
@@ -86,7 +86,7 @@ const NotionPage = ({ post, className }) => {
|
||||
return <>{post?.summary || ''}</>
|
||||
}
|
||||
|
||||
return <div id='notion-article' className={`mx-auto ${className || ''}`}>
|
||||
return <div id='notion-article' className={`mx-auto overflow-x-hidden ${className || ''}`}>
|
||||
<NotionRenderer
|
||||
recordMap={post.blockMap}
|
||||
mapPageUrl={mapPageUrl}
|
||||
|
||||
@@ -13,68 +13,55 @@ import 'prismjs/plugins/line-numbers/prism-line-numbers.css'
|
||||
import BLOG from '@/blog.config'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
/**
|
||||
* 代码美化相关
|
||||
* @author https://github.com/txs/
|
||||
* @returns
|
||||
*/
|
||||
const PrismMac = () => {
|
||||
const router = useRouter()
|
||||
const { isDarkMode } = useGlobal()
|
||||
|
||||
useEffect(() => {
|
||||
const handleDarkModeChange = () => {
|
||||
// 加载prism样式
|
||||
loadPrismThemeCSS()
|
||||
if (JSON.parse(BLOG.CODE_MAC_BAR)) {
|
||||
loadExternalResource('/css/prism-mac-style.css', 'css')
|
||||
if (JSON.parse(BLOG.CODE_MAC_BAR)) {
|
||||
loadExternalResource('/css/prism-mac-style.css', 'css')
|
||||
}
|
||||
// 加载prism样式
|
||||
loadPrismThemeCSS(isDarkMode)
|
||||
// 折叠代码
|
||||
loadExternalResource(BLOG.PRISM_JS_AUTO_LOADER, 'js').then((url) => {
|
||||
if (window?.Prism?.plugins?.autoloader) {
|
||||
window.Prism.plugins.autoloader.languages_path = BLOG.PRISM_JS_PATH
|
||||
}
|
||||
loadExternalResource(BLOG.PRISM_JS_AUTO_LOADER, 'js').then((url) => {
|
||||
if (window?.Prism?.plugins?.autoloader) {
|
||||
window.Prism.plugins.autoloader.languages_path = BLOG.PRISM_JS_PATH
|
||||
}
|
||||
renderPrismMac()
|
||||
renderMermaid()
|
||||
})
|
||||
}
|
||||
handleDarkModeChange()
|
||||
|
||||
const handleDarkModeToggle = () => {
|
||||
const currentTheme = document.documentElement.className
|
||||
handleDarkModeChange()
|
||||
document.documentElement.className = currentTheme === 'light' ? 'dark' : 'light'
|
||||
}
|
||||
renderPrismMac()
|
||||
renderMermaid()
|
||||
renderCollapseCode()
|
||||
})
|
||||
}, [router, isDarkMode])
|
||||
|
||||
const darkModeSwitchButton = document.getElementById('darkModeButton')
|
||||
darkModeSwitchButton.addEventListener('click', handleDarkModeToggle)
|
||||
|
||||
return () => {
|
||||
darkModeSwitchButton.removeEventListener('click', handleDarkModeToggle)
|
||||
}
|
||||
}, [router])
|
||||
return <></>
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载样式
|
||||
*/
|
||||
const loadPrismThemeCSS = () => {
|
||||
const loadPrismThemeCSS = (isDarkMode) => {
|
||||
let PRISM_THEME
|
||||
let PRISM_PREVIOUS
|
||||
const themeClass = document.documentElement.className
|
||||
if (JSON.parse(BLOG.PRISM_THEME_SWITCH)) {
|
||||
if (themeClass === 'dark') {
|
||||
if (isDarkMode) {
|
||||
PRISM_THEME = BLOG.PRISM_THEME_DARK_PATH
|
||||
PRISM_PREVIOUS = BLOG.PRISM_THEME_LIGHT_PATH
|
||||
const previousTheme = document.querySelector(`link[href="${PRISM_PREVIOUS}"]`)
|
||||
if (previousTheme) {
|
||||
previousTheme.parentNode.removeChild(previousTheme)
|
||||
}
|
||||
} else {
|
||||
PRISM_THEME = BLOG.PRISM_THEME_LIGHT_PATH
|
||||
PRISM_PREVIOUS = BLOG.PRISM_THEME_DARK_PATH
|
||||
const previousTheme = document.querySelector(`link[href="${PRISM_PREVIOUS}"]`)
|
||||
if (previousTheme) {
|
||||
previousTheme.parentNode.removeChild(previousTheme)
|
||||
}
|
||||
}
|
||||
const previousTheme = document.querySelector(`link[href="${PRISM_PREVIOUS}"]`)
|
||||
if (previousTheme) {
|
||||
previousTheme.parentNode.removeChild(previousTheme)
|
||||
}
|
||||
loadExternalResource(PRISM_THEME, 'css')
|
||||
} else {
|
||||
@@ -82,6 +69,52 @@ const loadPrismThemeCSS = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 将代码块转为可折叠对象
|
||||
*/
|
||||
const renderCollapseCode = () => {
|
||||
if (!JSON.parse(BLOG.CODE_COLLAPSE)) {
|
||||
return
|
||||
}
|
||||
const codeBlocks = document.querySelectorAll('.code-toolbar')
|
||||
for (const codeBlock of codeBlocks) {
|
||||
// 判断当前元素是否被包裹
|
||||
if (codeBlock.closest('.collapse-wrapper')) {
|
||||
continue // 如果被包裹了,跳过当前循环
|
||||
}
|
||||
|
||||
const code = codeBlock.querySelector('code')
|
||||
const language = code.getAttribute('class').match(/language-(\w+)/)[1]
|
||||
|
||||
const collapseWrapper = document.createElement('div')
|
||||
collapseWrapper.className = 'collapse-wrapper w-full py-2'
|
||||
const panelWrapper = document.createElement('div')
|
||||
panelWrapper.className = 'border dark:border-gray-600 rounded-md hover:border-indigo-500 duration-200 transition-colors'
|
||||
|
||||
const header = document.createElement('div')
|
||||
header.className = 'flex justify-between items-center px-4 py-2 cursor-pointer select-none'
|
||||
header.innerHTML = `<h3 class="text-lg font-medium">${language}</h3><svg class="transition-all duration-200 w-5 h-5 transform rotate-0" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M6.293 6.293a1 1 0 0 1 1.414 0L10 8.586l2.293-2.293a1 1 0 0 1 1.414 1.414l-3 3a1 1 0 0 1-1.414 0l-3-3a1 1 0 0 1 0-1.414z" clip-rule="evenodd"/></svg>`
|
||||
|
||||
const panel = document.createElement('div')
|
||||
panel.className = 'invisible h-0 transition-transform duration-200 border-t border-gray-300'
|
||||
|
||||
panelWrapper.appendChild(header)
|
||||
panelWrapper.appendChild(panel)
|
||||
collapseWrapper.appendChild(panelWrapper)
|
||||
|
||||
codeBlock.parentNode.insertBefore(collapseWrapper, codeBlock)
|
||||
panel.appendChild(codeBlock)
|
||||
|
||||
header.addEventListener('click', () => {
|
||||
panel.classList.toggle('invisible')
|
||||
panel.classList.toggle('h-0')
|
||||
panel.classList.toggle('h-auto')
|
||||
header.querySelector('svg').classList.toggle('rotate-180')
|
||||
panelWrapper.classList.toggle('border-gray-300')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将mermaid语言 渲染成图片
|
||||
*/
|
||||
@@ -116,13 +149,13 @@ const renderMermaid = async() => {
|
||||
}
|
||||
}
|
||||
})
|
||||
if (document.querySelector('#container-inner')) {
|
||||
observer.observe(document.querySelector('#container-inner'), { attributes: true, subtree: true })
|
||||
if (document.querySelector('#notion-article')) {
|
||||
observer.observe(document.querySelector('#notion-article'), { attributes: true, subtree: true })
|
||||
}
|
||||
}
|
||||
|
||||
function renderPrismMac() {
|
||||
const container = document?.getElementById('container-inner')
|
||||
const container = document?.getElementById('notion-article')
|
||||
|
||||
// Add line numbers
|
||||
if (BLOG.CODE_LINE_NUMBERS === 'true') {
|
||||
@@ -179,11 +212,11 @@ const fixCodeLineStyle = () => {
|
||||
}
|
||||
}
|
||||
})
|
||||
observer.observe(document.querySelector('#container'), { attributes: true, subtree: true })
|
||||
observer.observe(document.querySelector('#notion-article'), { attributes: true, subtree: true })
|
||||
setTimeout(() => {
|
||||
const preCodes = document.querySelectorAll('pre.notion-code')
|
||||
for (const preCode of preCodes) {
|
||||
console.log('code', preCode)
|
||||
// console.log('code', preCode)
|
||||
Prism.plugins.lineNumbers.resize(preCode)
|
||||
}
|
||||
}, 10)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import QRCode from 'qrcode.react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useState } from 'react'
|
||||
|
||||
import {
|
||||
@@ -49,6 +49,13 @@ import {
|
||||
HatenaIcon
|
||||
} from 'react-share'
|
||||
|
||||
const QRCode = dynamic(
|
||||
() => {
|
||||
return import('qrcode.react')
|
||||
},
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
/**
|
||||
* @author https://github.com/txs
|
||||
* @param {*} param0
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { Draggable } from './Draggable'
|
||||
import { THEMES } from '@/themes/theme'
|
||||
import { useRouter } from 'next/router'
|
||||
import DarkModeButton from './DarkModeButton'
|
||||
/**
|
||||
*
|
||||
* @returns 主题切换
|
||||
@@ -10,28 +11,40 @@ import { useRouter } from 'next/router'
|
||||
const ThemeSwitch = () => {
|
||||
const { theme } = useGlobal()
|
||||
const router = useRouter()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
// 修改当前路径url中的 theme 参数
|
||||
// 例如 http://localhost?theme=hexo 跳转到 http://localhost?theme=newTheme
|
||||
const onSelectChange = (e) => {
|
||||
setIsLoading(true)
|
||||
const newTheme = e.target.value
|
||||
const query = router.query
|
||||
query.theme = newTheme
|
||||
router.push({ pathname: router.pathname, query })
|
||||
router.push({ pathname: router.pathname, query }).then(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
return (<>
|
||||
<Draggable>
|
||||
<div id="draggableBox" style={{ left: '10px', top: '85vh' }} className="fixed text-white bg-black z-50 rounded-lg shadow-card">
|
||||
<div className="py-2 flex items-center text-sm px-2">
|
||||
<select value={theme} onChange={onSelectChange} name="cars" className='text-white bg-black uppercase cursor-pointer'>
|
||||
{THEMES?.map(t => {
|
||||
return <option key={t} value={t}>{t}</option>
|
||||
})}
|
||||
</select>
|
||||
<i className='fas fa-palette pl-1' />
|
||||
<div id="draggableBox" style={{ left: '10px', top: '80vh' }} className="fixed z-50 dark:text-white bg-gray-50 dark:bg-black rounded-2xl drop-shadow-lg">
|
||||
<div className="p-3 w-full flex items-center text-sm group duration-200 transition-all">
|
||||
<DarkModeButton className='mr-2' />
|
||||
<div className='w-0 group-hover:w-20 transition-all duration-200 overflow-hidden'>
|
||||
<select value={theme} onChange={onSelectChange} name="themes" className='appearance-none outline-none dark:text-white bg-gray-50 dark:bg-black uppercase cursor-pointer'>
|
||||
{THEMES?.map(t => {
|
||||
return <option key={t} value={t}>{t}</option>
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<i className="fa-solid fa-palette pl-2"></i>
|
||||
</div>
|
||||
</div>
|
||||
{/* 切换主题加载时的全屏遮罩 */}
|
||||
<div className={`${isLoading ? 'opacity-50 ' : 'opacity-0'} w-screen h-screen 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>
|
||||
</Draggable>
|
||||
</>
|
||||
)
|
||||
|
||||
67
components/WordCount.js
Normal file
67
components/WordCount.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* 字数统计
|
||||
* @returns
|
||||
*/
|
||||
export default function WordCount() {
|
||||
const { locale } = useGlobal()
|
||||
useEffect(() => {
|
||||
countWords()
|
||||
})
|
||||
|
||||
return <span id='wordCountWrapper' className='flex gap-3 font-light'>
|
||||
<span className='flex whitespace-nowrap items-center'>
|
||||
<i className='pl-1 pr-2 fas fa-file-word' />
|
||||
<span id='wordCount'>0</span>
|
||||
</span>
|
||||
<span className='flex whitespace-nowrap items-center'>
|
||||
<i className='mr-1 fas fa-clock' />
|
||||
<span></span>
|
||||
<span id='readTime'>0</span> {locale.COMMON.MINUTE}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新字数统计和阅读时间
|
||||
*/
|
||||
function countWords() {
|
||||
const articleText = deleteHtmlTag(document.getElementById('notion-article')?.innerHTML)
|
||||
const wordCount = fnGetCpmisWords(articleText)
|
||||
// 阅读速度 300-500每分钟
|
||||
document.getElementById('wordCount').innerHTML = wordCount
|
||||
document.getElementById('readTime').innerHTML = Math.floor(wordCount / 400) + 1
|
||||
const wordCountWrapper = document.getElementById('wordCountWrapper')
|
||||
wordCountWrapper.classList.remove('hidden')
|
||||
}
|
||||
|
||||
// 去除html标签
|
||||
function deleteHtmlTag(str) {
|
||||
if (!str) {
|
||||
return ''
|
||||
}
|
||||
str = str.replace(/<[^>]+>|&[^>]+;/g, '').trim()// 去掉所有的html标签和 之类的特殊符合
|
||||
return str
|
||||
}
|
||||
|
||||
// 用word方式计算正文字数
|
||||
function fnGetCpmisWords(str) {
|
||||
if (!str) {
|
||||
return 0
|
||||
}
|
||||
let sLen = 0
|
||||
try {
|
||||
// eslint-disable-next-line no-irregular-whitespace
|
||||
str = str.replace(/(\r\n+|\s+| +)/g, '龘')
|
||||
// eslint-disable-next-line no-control-regex
|
||||
str = str.replace(/[\x00-\xff]/g, 'm')
|
||||
str = str.replace(/m+/g, '*')
|
||||
str = str.replace(/龘+/g, '')
|
||||
sLen = str.length
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
return sLen
|
||||
}
|
||||
Reference in New Issue
Block a user