Merge branch 'main' into pr/jxpeng98/1257

This commit is contained in:
tangly1024.com
2023-07-26 18:43:34 +08:00
238 changed files with 7817 additions and 1027 deletions

View 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
View 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>
}

View File

@@ -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>
)

View File

@@ -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'>

View File

@@ -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>
)
}

View File

@@ -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`} />}

View 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 >
)
}

View File

@@ -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>
}

View File

@@ -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/>
</>
}

View File

@@ -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
View 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>
)
}

View 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

View File

@@ -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>
}

View File

@@ -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
View 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>}
</>)
}

View File

@@ -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
View 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)
}
}

View File

@@ -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>

View File

@@ -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}

View File

@@ -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)

View File

@@ -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

View File

@@ -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
View 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>&nbsp;{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标签和&nbsp;之类的特殊符合
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
}