diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..d8f037245635525a82493e8f2844c90b76f234e7
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
+.next*
\ No newline at end of file
diff --git a/.env.local b/.env.local
new file mode 100644
index 0000000000000000000000000000000000000000..c2daaaed4022230601aa29926118173364d0e7c8
--- /dev/null
+++ b/.env.local
@@ -0,0 +1,174 @@
+# 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables
+NEXT_PUBLIC_VERSION=4.2.4
+
+
+# 可在此添加环境变量,去掉最左边的(# )注释即可
+# Notion页面ID,必须
+# NOTION_PAGE_ID=097e5f674880459d8e1b4407758dc4fb
+
+# 非必须
+# NEXT_PUBLIC_PSEUDO_STATIC=
+# NEXT_PUBLIC_REVALIDATE_SECOND=
+# NEXT_PUBLIC_THEME=matery
+# NEXT_PUBLIC_THEME_SWITCH=
+# NEXT_PUBLIC_LANG=
+# NEXT_PUBLIC_APPEARANCE=
+# NEXT_PUBLIC_APPEARANCE_DARK_TIME=
+# NEXT_PUBLIC_GREETING_WORDS=
+# NEXT_PUBLIC_CUSTOM_MENU=
+# NEXT_PUBLIC_AUTHOR=
+# NEXT_PUBLIC_BIO=
+# NEXT_PUBLIC_LINK=
+# NEXT_PUBLIC_KEYWORD=
+# NEXT_PUBLIC_CONTACT_EMAIL=
+# NEXT_PUBLIC_CONTACT_WEIBO=
+# NEXT_PUBLIC_CONTACT_TWITTER=
+# NEXT_PUBLIC_CONTACT_GITHUB=
+# NEXT_PUBLIC_CONTACT_TELEGRAM=
+# NEXT_PUBLIC_CONTACT_LINKEDIN=
+# NEXT_PUBLIC_CONTACT_INSTAGRAM=
+# NEXT_PUBLIC_CONTACT_BILIBILI=
+# NEXT_PUBLIC_CONTACT_YOUTUBE=
+# NEXT_PUBLIC_FAVICON=
+# NEXT_PUBLIC_FONT_STYLE=
+# NEXT_PUBLIC_FONT_URL=
+# NEXT_PUBLIC_FONT_SANS=
+# NEXT_PUBLIC_FONT_SERIF=
+# NEXT_PUBLIC_FONT_AWESOME_PATH=
+# NEXT_PUBLIC_PRISM_THEME_PREFIX_PATH=
+# NEXT_PUBLIC_PRISM_THEME_SWITCH=
+# NEXT_PUBLIC_PRISM_THEME_LIGHT_PATH=
+# NEXT_PUBLIC_PRISM_THEME_DARK_PATH=
+# NEXT_PUBLIC_CODE_MAC_BAR=
+# NEXT_PUBLIC_CODE_LINE_NUMBERS=
+# NEXT_PUBLIC_CODE_COLLAPSE=
+# NEXT_PUBLIC_CODE_COLLAPSE_EXPAND_DEFAULT=
+# NEXT_PUBLIC_MERMAID_CDN=
+# NEXT_PUBLIC_QR_CODE_CDN=
+# NEXT_PUBLIC_BACKGROUND_LIGHT=
+# NEXT_PUBLIC_BACKGROUND_DARK=
+# NEXT_PUBLIC_SUB_PATH=
+# NEXT_PUBLIC_POST_SHARE_BAR=
+# NEXT_PUBLIC_POST_SHARE_SERVICES=
+# NEXT_PUBLIC_POST_URL_PREFIX=
+# NEXT_PUBLIC_POST_LIST_STYLE=
+# NEXT_PUBLIC_POST_PREVIEW=
+# NEXT_PUBLIC_POST_RECOMMEND_COUNT=
+# NEXT_PUBLIC_POSTS_PER_PAGE=
+# NEXT_PUBLIC_POST_SORT_BY=
+# NEXT_PUBLIC_ALGOLIA_APP_ID=
+# ALGOLIA_ADMIN_APP_KEY=
+# NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_APP_KEY=
+# NEXT_PUBLIC_ALGOLIA_INDEX=
+# NEXT_PUBLIC_PREVIEW_CATEGORY_COUNT=
+# NEXT_PUBLIC_PREVIEW_TAG_COUNT=
+# NEXT_PUBLIC_POST_DISABLE_GALLERY_CLICK=
+# NEXT_PUBLIC_FIREWORKS=
+# NEXT_PUBLIC_FIREWORKS_COLOR=
+# NEXT_PUBLIC_SAKURA=
+# NEXT_PUBLIC_NEST=
+# NEXT_PUBLIC_FLUTTERINGRIBBON=
+# NEXT_PUBLIC_RIBBON=
+# NEXT_PUBLIC_STARRY_SKY=
+# NEXT_PUBLIC_CHATBASE_ID=
+# NEXT_PUBLIC_WEB_WHIZ_ENABLED=
+# NEXT_PUBLIC_WEB_WHIZ_BASE_URL=
+# NEXT_PUBLIC_WEB_WHIZ_CHAT_BOT_ID=
+# NEXT_PUBLIC_WIDGET_PET=
+# NEXT_PUBLIC_WIDGET_PET_LINK=
+# NEXT_PUBLIC_WIDGET_PET_SWITCH_THEME=
+# NEXT_PUBLIC_MUSIC_PLAYER=
+# NEXT_PUBLIC_MUSIC_PLAYER_VISIBLE=
+# NEXT_PUBLIC_MUSIC_PLAYER_AUTO_PLAY=
+# NEXT_PUBLIC_MUSIC_PLAYER_LRC_TYPE=
+# NEXT_PUBLIC_MUSIC_PLAYER_CDN_URL=
+# NEXT_PUBLIC_MUSIC_PLAYER_ORDER=
+# NEXT_PUBLIC_MUSIC_PLAYER_AUDIO_LIST=
+# NEXT_PUBLIC_MUSIC_PLAYER_METING=
+# NEXT_PUBLIC_MUSIC_PLAYER_METING_SERVER=
+# NEXT_PUBLIC_MUSIC_PLAYER_METING_ID=
+# NEXT_PUBLIC_MUSIC_PLAYER_METING_LRC_TYPE=
+# NEXT_PUBLIC_COMMENT_ARTALK_SERVER=
+# NEXT_PUBLIC_COMMENT_ARTALK_JS=
+# NEXT_PUBLIC_COMMENT_ARTALK_CSS=
+# NEXT_PUBLIC_COMMENT_ENV_ID=
+# NEXT_PUBLIC_COMMENT_TWIKOO_COUNT_ENABLE=
+# NEXT_PUBLIC_COMMENT_TWIKOO_CDN_URL=
+# NEXT_PUBLIC_COMMENT_UTTERRANCES_REPO=
+# NEXT_PUBLIC_COMMENT_GISCUS_REPO=
+# NEXT_PUBLIC_COMMENT_GISCUS_REPO_ID=
+# NEXT_PUBLIC_COMMENT_GISCUS_CATEGORY_ID=
+# NEXT_PUBLIC_COMMENT_GISCUS_MAPPING=
+# NEXT_PUBLIC_COMMENT_GISCUS_REACTIONS_ENABLED=
+# NEXT_PUBLIC_COMMENT_GISCUS_EMIT_METADATA=
+# NEXT_PUBLIC_COMMENT_GISCUS_INPUT_POSITION=
+# NEXT_PUBLIC_COMMENT_GISCUS_LANG=
+# NEXT_PUBLIC_COMMENT_GISCUS_LOADING=
+# NEXT_PUBLIC_COMMENT_GISCUS_CROSSORIGIN=
+# NEXT_PUBLIC_COMMENT_CUSDIS_APP_ID=
+# NEXT_PUBLIC_COMMENT_CUSDIS_HOST=
+# NEXT_PUBLIC_COMMENT_CUSDIS_SCRIPT_SRC=
+# NEXT_PUBLIC_COMMENT_GITALK_REPO=
+# NEXT_PUBLIC_COMMENT_GITALK_OWNER=
+# NEXT_PUBLIC_COMMENT_GITALK_ADMIN=
+# NEXT_PUBLIC_COMMENT_GITALK_CLIENT_ID=
+# NEXT_PUBLIC_COMMENT_GITALK_CLIENT_SECRET=
+# NEXT_PUBLIC_COMMENT_GITALK_JS_CDN_URL=
+# NEXT_PUBLIC_COMMENT_GITALK_CSS_CDN_URL=
+# NEXT_PUBLIC_COMMENT_GITTER_ROOM=
+# NEXT_PUBLIC_COMMENT_DAO_VOICE_ID=
+# NEXT_PUBLIC_COMMENT_TIDIO_ID=
+# NEXT_PUBLIC_VALINE_CDN=
+# NEXT_PUBLIC_VALINE_ID=
+# NEXT_PUBLIC_VALINE_KEY=
+# NEXT_PUBLIC_VALINE_SERVER_URLS=
+# NEXT_PUBLIC_VALINE_PLACEHOLDER=
+# NEXT_PUBLIC_WALINE_SERVER_URL=
+# NEXT_PUBLIC_WALINE_RECENT=
+# NEXT_PUBLIC_WEBMENTION_ENABLE=
+# NEXT_PUBLIC_WEBMENTION_AUTH=
+# NEXT_PUBLIC_WEBMENTION_HOSTNAME=
+# NEXT_PUBLIC_TWITTER_USERNAME=
+# NEXT_PUBLIC_WEBMENTION_TOKEN=
+# NEXT_PUBLIC_ANALYTICS_VERCEL=
+# NEXT_PUBLIC_ANALYTICS_BUSUANZI_ENABLE=
+# NEXT_PUBLIC_ANALYTICS_BAIDU_ID=
+# NEXT_PUBLIC_ANALYTICS_CNZZ_ID=
+# NEXT_PUBLIC_ANALYTICS_GOOGLE_ID=
+# NEXT_PUBLIC_ANALYTICS_ACKEE_TRACKER=
+# NEXT_PUBLIC_ANALYTICS_ACKEE_DATA_SERVER=
+# NEXT_PUBLIC_ANALYTICS_ACKEE_DOMAIN_ID=
+# NEXT_PUBLIC_SEO_GOOGLE_SITE_VERIFICATION=
+# NEXT_PUBLIC_SEO_BAIDU_SITE_VERIFICATION=
+# NEXT_PUBLIC_ADSENSE_GOOGLE_ID=
+# NEXT_PUBLIC_ADSENSE_GOOGLE_TEST=
+# NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_IN_ARTICLE=
+# NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_FLOW=
+# NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_NATIVE=
+# NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_AUTO=
+# NEXT_PUBLIC_WWAD_ID=
+# NEXT_PUBLIC_WWADS_AD_BLOCK_DETECT=
+# NEXT_PUBLIC_NOTION_PROPERTY_PASSWORD=
+# NEXT_PUBLIC_NOTION_PROPERTY_TYPE=
+# NEXT_PUBLIC_NOTION_PROPERTY_TYPE_POST=
+# NEXT_PUBLIC_NOTION_PROPERTY_TYPE_PAGE=
+# NEXT_PUBLIC_NOTION_PROPERTY_TYPE_NOTICE=
+# NEXT_PUBLIC_NOTION_PROPERTY_TYPE_MENU=
+# NEXT_PUBLIC_NOTION_PROPERTY_TYPE_SUB_MENU=
+# NEXT_PUBLIC_NOTION_PROPERTY_TITLE=
+# NEXT_PUBLIC_NOTION_PROPERTY_STATUS=
+# NEXT_PUBLIC_NOTION_PROPERTY_STATUS_PUBLISH=
+# NEXT_PUBLIC_NOTION_PROPERTY_STATUS_INVISIBLE=
+# NEXT_PUBLIC_NOTION_PROPERTY_SUMMARY=
+# NEXT_PUBLIC_NOTION_PROPERTY_SLUG=
+# NEXT_PUBLIC_NOTION_PROPERTY_CATEGORY=
+# NEXT_PUBLIC_NOTION_PROPERTY_DATE=
+# NEXT_PUBLIC_NOTION_PROPERTY_TAGS=
+# NEXT_PUBLIC_NOTION_PROPERTY_ICON=
+# NEXT_PUBLIC_ENABLE_RSS=
+# MAILCHIMP_LIST_ID=
+# MAILCHIMP_API_KEY=
+# NEXT_PUBLIC_DEBUG=
+# ENABLE_CACHE=
+# VERCEL_ENV=
+# NEXT_PUBLIC_VERSION=
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000000000000000000000000000000000000..9836da770d83b61ad2d50e2205ba1becfa26d9df
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,38 @@
+module.exports = {
+ env: {
+ browser: true,
+ es2021: true,
+ node: true
+ },
+ extends: [
+ 'plugin:react/recommended',
+ 'plugin:@next/next/recommended',
+ 'standard'
+ ],
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true
+ },
+ ecmaVersion: 12,
+ sourceType: 'module'
+ },
+ plugins: [
+ 'react',
+ 'react-hooks'
+ ],
+ settings: {
+ react: {
+ version: 'detect'
+ }
+ },
+ rules: {
+ semi: 0,
+ 'react/no-unknown-property': 'off', // 
")
+ }
+ }
+ };
+
+ // check document ready
+ function docReady(t) {
+ document.readyState === 'complete' ||
+ document.readyState === 'interactive'
+ ? setTimeout(t, 1)
+ : document.addEventListener('DOMContentLoaded', t)
+ }
+
+ // check if wwads' fire function was blocked after document is ready with 3s timeout (waiting the ad loading)
+ docReady(function () {
+ setTimeout(function () {
+ if (window._AdBlockInit === undefined) {
+ ABDetected()
+ }
+ }, 3000)
+ })
+ }, [])
+ return null
+}
diff --git a/components/AlgoliaSearchModal.js b/components/AlgoliaSearchModal.js
new file mode 100644
index 0000000000000000000000000000000000000000..7c6bfdcb33aa81f8c99bcb598f584cebae0d3b39
--- /dev/null
+++ b/components/AlgoliaSearchModal.js
@@ -0,0 +1,230 @@
+import { useState, useImperativeHandle, useRef } from 'react'
+import algoliasearch from 'algoliasearch'
+import replaceSearchResult from '@/components/Mark'
+import Link from 'next/link'
+import { useGlobal } from '@/lib/global'
+import throttle from 'lodash/throttle'
+import { siteConfig } from '@/lib/config'
+
+/**
+ * 结合 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)
+ const [page, setPage] = useState(0)
+ const [keyword, setKeyword] = useState(null)
+ const [totalPage, setTotalPage] = useState(0)
+ const [totalHit, setTotalHit] = useState(0)
+ const [useTime, setUseTime] = useState(0)
+
+ /**
+ * 对外暴露方法
+ */
+ useImperativeHandle(cRef, () => {
+ return {
+ openSearch: () => {
+ setIsModalOpen(true)
+ }
+ }
+ })
+
+ const client = algoliasearch(siteConfig('ALGOLIA_APP_ID'), siteConfig('ALGOLIA_SEARCH_ONLY_APP_KEY'))
+ const index = client.initIndex(siteConfig('ALGOLIA_INDEX'))
+
+ /**
+ * 搜索
+ * @param {*} query
+ */
+ const handleSearch = async (query, page) => {
+ setKeyword(query)
+ setPage(page)
+ setSearchResults([])
+ setUseTime(0)
+ setTotalPage(0)
+ setTotalHit(0)
+ if (!query || query === '') {
+ return
+ }
+
+ try {
+ const res = await index.search(query, { page, hitsPerPage: 10 })
+ const { hits, nbHits, nbPages, processingTimeMS } = res
+ setUseTime(processingTimeMS)
+ setTotalPage(nbPages)
+ setTotalHit(nbHits)
+ setSearchResults(hits)
+
+ const doms = document.getElementById('search-wrapper').getElementsByClassName('replace')
+
+ setTimeout(() => {
+ replaceSearchResult({
+ doms,
+ search: query,
+ target: {
+ element: 'span',
+ className: 'text-blue-600 border-b border-dashed'
+ }
+ })
+ }, 150)
+ } catch (error) {
+ console.error('Algolia search error:', error)
+ }
+ }
+
+ const throttledHandleSearch = useRef(throttle(handleSearch, 300)) // 设置节流延迟时间
+
+ // 修改input的onChange事件处理函数
+ const handleInputChange = (e) => {
+ const query = e.target.value
+ throttledHandleSearch.current(query, 0)
+ }
+
+ /**
+ * 切换页码
+ * @param {*} page
+ */
+ const switchPage = (page) => {
+ throttledHandleSearch.current(keyword, page)
+ }
+
+ /**
+ * 关闭弹窗
+ */
+ const closeModal = () => {
+ setIsModalOpen(false)
+ }
+
+ if (!siteConfig('ALGOLIA_APP_ID')) {
+ return <>>
+ }
+
+ return (
+
+ {/* 模态框 */}
+
+
+
+
handleInputChange(e)}
+ className="text-black dark:text-gray-200 bg-gray-50 dark:bg-gray-600 outline-blue-500 w-full px-4 my-2 py-1 mb-4 border rounded-md"
+ />
+
+ {/* 标签组 */}
+
+
+
+
+
+
+
+
+ {totalHit > 0 && (
+
+ 共搜索到 {totalHit} 条结果,用时 {useTime} 毫秒
+
+ )}
+
+
+
+ Algolia 提供搜索服务
+ {' '}
+
+
+
+ {/* 遮罩 */}
+
+
+ )
+}
+
+/**
+ * 标签组
+ */
+function TagGroups(props) {
+ const { tagOptions } = useGlobal()
+ // 获取tagOptions数组前十个
+ const firstTenTags = tagOptions?.slice(0, 10)
+
+ return
+}
+
+/**
+ * 分页
+ * @param {*} param0
+ */
+function Pagination(props) {
+ const { totalPage, page, switchPage } = props
+ if (totalPage <= 0) {
+ return <>>
+ }
+ const pagesElement = []
+
+ for (let i = 0; i < totalPage; i++) {
+ const selected = page === i
+ pagesElement.push(getPageElement(i, selected, switchPage))
+ }
+ return
+ {pagesElement.map(p => p)}
+
+}
+
+/**
+ * 获取分页按钮
+ * @param {*} i
+ * @param {*} selected
+ */
+function getPageElement(i, selected, switchPage) {
+ return switchPage(i)} className={`${selected ? 'font-bold text-white bg-blue-600 rounded' : 'hover:text-blue-600 hover:font-bold'} text-center cursor-pointer w-6 h-6 `}>
+ {i + 1}
+
+}
diff --git a/components/Artalk.js b/components/Artalk.js
new file mode 100644
index 0000000000000000000000000000000000000000..e44edaabd3fda1aa7e8a3b9d296984aa9042a3df
--- /dev/null
+++ b/components/Artalk.js
@@ -0,0 +1,37 @@
+import { siteConfig } from '@/lib/config'
+import { loadExternalResource } from '@/lib/utils'
+import { useEffect } from 'react'
+
+/**
+ * Artalk 自托管评论系统 @see https://artalk.js.org/
+ * @returns {JSX.Element}
+ * @constructor
+ */
+
+const Artalk = ({ siteInfo }) => {
+ const artalkCss = siteConfig('COMMENT_ARTALK_CSS')
+ const artalkServer = siteConfig('COMMENT_ARTALK_SERVER')
+ const artalkLocale = siteConfig('LANG')
+ const site = siteConfig('TITLE')
+
+ useEffect(() => {
+ initArtalk()
+ }, [])
+
+ const initArtalk = async () => {
+ await loadExternalResource(artalkCss, 'css')
+ window?.Artalk?.init({
+ server: artalkServer, // 后端地址
+ el: '#artalk', // 容器元素
+ locale: artalkLocale,
+ // pageKey: '/post/1', // 固定链接 (留空自动获取)
+ // pageTitle: '关于引入 Artalk 的这档子事', // 页面标题 (留空自动获取)
+ site: site // 你的站点名
+ })
+ }
+ return (
+
+ )
+}
+
+export default Artalk
diff --git a/components/Busuanzi.js b/components/Busuanzi.js
new file mode 100644
index 0000000000000000000000000000000000000000..cbeb54da3f710c258cd69a1ef2e8a41ea449ba78
--- /dev/null
+++ b/components/Busuanzi.js
@@ -0,0 +1,26 @@
+import busuanzi from '@/lib/busuanzi'
+import { useRouter } from 'next/router'
+import { useGlobal } from '@/lib/global'
+// import { useRouter } from 'next/router'
+import { useEffect } from 'react'
+
+let path = ''
+
+export default function Busuanzi () {
+ const { theme } = useGlobal()
+ const router = useRouter()
+ router.events.on('routeChangeComplete', (url, option) => {
+ if (url !== path) {
+ path = url
+ busuanzi.fetch()
+ }
+ })
+
+ // 更换主题时更新
+ useEffect(() => {
+ if (theme) {
+ busuanzi.fetch()
+ }
+ }, [theme])
+ return null
+}
diff --git a/components/ChatBase.js b/components/ChatBase.js
new file mode 100644
index 0000000000000000000000000000000000000000..b114fdf6a13c921ce02fa642d947d8dd04f96a26
--- /dev/null
+++ b/components/ChatBase.js
@@ -0,0 +1,19 @@
+import { siteConfig } from '@/lib/config'
+
+/**
+ * 这是一个嵌入组件,可以在任意位置全屏显示您的chat-base对话框
+ * 暂时没有页面引用
+ * 因为您可以直接用内嵌网页的方式放入您的notion中 https://www.chatbase.co/chatbot-iframe/${siteConfig('CHATBASE_ID')}
+ */
+export default function ChatBase() {
+ if (!siteConfig('CHATBASE_ID')) {
+ return <>>
+ }
+
+ return
+}
diff --git a/components/Collapse.js b/components/Collapse.js
new file mode 100644
index 0000000000000000000000000000000000000000..69ef59ebca79c345230483442227585d186ac2b9
--- /dev/null
+++ b/components/Collapse.js
@@ -0,0 +1,96 @@
+import { useEffect, useImperativeHandle, useRef } from 'react'
+
+/**
+ * 折叠面板组件,支持水平折叠、垂直折叠
+ * @param {type:['horizontal','vertical'],isOpen} props
+ * @returns
+ */
+const Collapse = props => {
+ const { collapseRef } = props
+ const ref = useRef(null)
+ const type = props.type || 'vertical'
+
+ useImperativeHandle(collapseRef, () => {
+ return {
+ /**
+ * 当子元素高度变化时,可调用此方法更新折叠组件的高度
+ * @param {*} param0
+ */
+ updateCollapseHeight: ({ height, increase }) => {
+ if (props.isOpen) {
+ ref.current.style.height = ref.current.scrollHeight
+ ref.current.style.height = 'auto'
+ }
+ }
+ }
+ })
+
+ /**
+ * 折叠
+ * @param {*} element
+ */
+ const collapseSection = element => {
+ const sectionHeight = element.scrollHeight
+ const sectionWidth = element.scrollWidth
+
+ requestAnimationFrame(function () {
+ switch (type) {
+ case 'horizontal':
+ element.style.width = sectionWidth + 'px'
+ requestAnimationFrame(function () {
+ element.style.width = 0 + 'px'
+ })
+ break
+ case 'vertical':
+ element.style.height = sectionHeight + 'px'
+ requestAnimationFrame(function () {
+ element.style.height = 0 + 'px'
+ })
+ }
+ })
+ }
+
+ /**
+ * 展开
+ * @param {*} element
+ */
+ const expandSection = element => {
+ const sectionHeight = element.scrollHeight
+ const sectionWidth = element.scrollWidth
+ let clearTime = 0
+ switch (type) {
+ case 'horizontal':
+ element.style.width = sectionWidth + 'px'
+ clearTime = setTimeout(() => {
+ element.style.width = 'auto'
+ }, 400)
+ break
+ case 'vertical':
+ element.style.height = sectionHeight + 'px'
+ clearTime = setTimeout(() => {
+ element.style.height = 'auto'
+ }, 400)
+ }
+
+ clearTimeout(clearTime)
+ }
+
+ useEffect(() => {
+ if (props.isOpen) {
+ expandSection(ref.current)
+ } else {
+ collapseSection(ref.current)
+ }
+ // 通知父组件高度变化
+ props?.onHeightChange && props.onHeightChange({ height: ref.current.scrollHeight, increase: props.isOpen })
+ }, [props.isOpen])
+
+ return (
+
+ {props.children}
+
+ )
+}
+Collapse.defaultProps = { isOpen: false }
+
+export default Collapse
diff --git a/components/Comment.js b/components/Comment.js
new file mode 100644
index 0000000000000000000000000000000000000000..4f4107b85979cad9a9c6d403a61defc9275fd1ad
--- /dev/null
+++ b/components/Comment.js
@@ -0,0 +1,138 @@
+import dynamic from 'next/dynamic'
+import Tabs from '@/components/Tabs'
+import { isBrowser, isSearchEngineBot } from '@/lib/utils'
+import { useRouter } from 'next/router'
+import Artalk from './Artalk'
+import { siteConfig } from '@/lib/config'
+
+const WalineComponent = dynamic(
+ () => {
+ return import('@/components/WalineComponent')
+ },
+ { ssr: false }
+)
+
+const CusdisComponent = dynamic(
+ () => {
+ return import('@/components/CusdisComponent')
+ },
+ { ssr: false }
+)
+
+const TwikooCompenent = dynamic(
+ () => {
+ return import('@/components/Twikoo')
+ },
+ { ssr: false }
+)
+
+const GitalkComponent = dynamic(
+ () => {
+ return import('@/components/Gitalk')
+ },
+ { ssr: false }
+)
+const UtterancesComponent = dynamic(
+ () => {
+ return import('@/components/Utterances')
+ },
+ { ssr: false }
+)
+const GiscusComponent = dynamic(
+ () => {
+ return import('@/components/Giscus')
+ },
+ { ssr: false }
+)
+const WebMentionComponent = dynamic(
+ () => {
+ return import('@/components/WebMention')
+ },
+ { ssr: false }
+)
+
+const ValineComponent = dynamic(() => import('@/components/ValineComponent'), {
+ ssr: false
+})
+
+/**
+ * 评论组件
+ * @param {*} param0
+ * @returns
+ */
+const Comment = ({ siteInfo, frontMatter, className }) => {
+ const router = useRouter()
+
+ const COMMENT_ARTALK_SERVER = siteConfig('COMMENT_ARTALK_SERVER')
+ const COMMENT_TWIKOO_ENV_ID = siteConfig('COMMENT_TWIKOO_ENV_ID')
+ const COMMENT_WALINE_SERVER_URL = siteConfig('COMMENT_WALINE_SERVER_URL')
+ const COMMENT_VALINE_APP_ID = siteConfig('COMMENT_VALINE_APP_ID')
+ const COMMENT_GISCUS_REPO = siteConfig('COMMENT_GISCUS_REPO')
+ const COMMENT_CUSDIS_APP_ID = siteConfig('COMMENT_CUSDIS_APP_ID')
+ const COMMENT_UTTERRANCES_REPO = siteConfig('COMMENT_UTTERRANCES_REPO')
+ const COMMENT_GITALK_CLIENT_ID = siteConfig('COMMENT_GITALK_CLIENT_ID')
+ const COMMENT_WEBMENTION_ENABLE = siteConfig('COMMENT_WEBMENTION_ENABLE')
+
+ if (isSearchEngineBot()) {
+ return null
+ }
+
+ // 当连接中有特殊参数时跳转到评论区
+ if (isBrowser && ('giscus' in router.query || router.query.target === 'comment')) {
+ setTimeout(() => {
+ const url = router.asPath.replace('?target=comment', '')
+ history.replaceState({}, '', url)
+ document?.getElementById('comment')?.scrollIntoView({ block: 'start', behavior: 'smooth' })
+ }, 1000)
+ }
+
+ if (!frontMatter) {
+ return <>Loading...>
+ }
+
+ return (
+
+ )
+}
+
+export default Comment
diff --git a/components/CusdisComponent.js b/components/CusdisComponent.js
new file mode 100644
index 0000000000000000000000000000000000000000..148ee0c7d5c9cbb9887594a118782015a2273419
--- /dev/null
+++ b/components/CusdisComponent.js
@@ -0,0 +1,37 @@
+import { useGlobal } from '@/lib/global'
+import { useRouter } from 'next/router'
+import { useEffect } from 'react'
+import { loadExternalResource } from '@/lib/utils'
+import { siteConfig } from '@/lib/config'
+
+const CusdisComponent = ({ frontMatter }) => {
+ const router = useRouter()
+ const { isDarkMode, lang } = useGlobal()
+ const src = siteConfig('COMMENT_CUSDIS_SCRIPT_SRC')
+ const i18nForCusdis = siteConfig('LANG').toLowerCase().indexOf('zh') === 0 ? siteConfig('LANG').toLowerCase() : siteConfig('LANG').toLowerCase().substring(0, 2)
+ const langCDN = siteConfig('COMMENT_CUSDIS_LANG_SRC', `https://cusdis.com/js/widget/lang/${i18nForCusdis}.js`)
+
+ // 处理cusdis主题
+ useEffect(() => {
+ loadCusdis()
+ }, [isDarkMode, lang])
+
+ const loadCusdis = async () => {
+ await loadExternalResource(langCDN, 'js')
+ await loadExternalResource(src, 'js')
+
+ window?.CUSDIS?.initial()
+ }
+
+ return
+}
+
+export default CusdisComponent
diff --git a/components/CustomContextMenu.js b/components/CustomContextMenu.js
new file mode 100644
index 0000000000000000000000000000000000000000..ff0dc31342ed3ceae14a52fb58728bc1679cdeb3
--- /dev/null
+++ b/components/CustomContextMenu.js
@@ -0,0 +1,204 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useEffect, useState, useRef, useLayoutEffect } from 'react'
+import { useGlobal } from '@/lib/global'
+import { saveDarkModeToCookies, THEMES } from '@/themes/theme'
+import useWindowSize from '@/hooks/useWindowSize'
+import { siteConfig } from '@/lib/config'
+
+/**
+ * 自定义右键菜单
+ * @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 windowSize = useWindowSize()
+ const [width, setWidth] = useState(0)
+ const [height, setHeight] = useState(0)
+
+ const { latestPosts } = props
+ const router = useRouter()
+ /**
+ * 随机跳转文章
+ */
+ function handleJumpToRandomPost() {
+ const randomIndex = Math.floor(Math.random() * latestPosts.length)
+ const randomPost = latestPosts[randomIndex]
+ router.push(`${siteConfig('SUB_PATH', '')}/${randomPost?.slug}`)
+ }
+
+ useLayoutEffect(() => {
+ setWidth(menuRef.current.offsetWidth)
+ setHeight(menuRef.current.offsetHeight)
+ }, [])
+
+ useEffect(() => {
+ const handleContextMenu = (event) => {
+ event.preventDefault()
+ // 计算点击位置加菜单宽高是否超出屏幕,如果超出则贴边弹出
+ const x = (event.clientX < windowSize.width - width) ? event.clientX : windowSize.width - width
+ const y = (event.clientY < windowSize.height - height) ? event.clientY : windowSize.height - height
+ setPosition({ y: `${y}px`, x: `${x}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)
+ }
+ }, [windowSize])
+
+ 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 handleChangeTheme() {
+ const randomTheme = THEMES[Math.floor(Math.random() * THEMES.length)] // 从THEMES数组中 随机取一个主题
+ const query = router.query
+ query.theme = randomTheme
+ router.push({ pathname: router.pathname, query })
+ }
+
+ /**
+ * 复制内容
+ */
+ function handleCopy() {
+ const selectedText = document.getSelection().toString();
+ if (selectedText) {
+ const tempInput = document.createElement('input');
+ tempInput.value = selectedText;
+ document.body.appendChild(tempInput);
+ tempInput.select();
+ document.execCommand('copy');
+ document.body.removeChild(tempInput);
+ // alert("Text copied: " + selectedText);
+ } else {
+ // alert("Please select some text first.");
+ }
+
+ setShow(false)
+ }
+
+ 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 (
+
+
+ {/* 菜单内容 */}
+
+ {/* 顶部导航按钮 */}
+
+
+
+
+
+
+
+
+
+ {/* 跳转导航按钮 */}
+
+
+
+
+
{locale.MENU.WALK_AROUND}
+
+
+
+
+
{locale.MENU.CATEGORY}
+
+
+
+
+
{locale.MENU.TAGS}
+
+
+
+
+
+
+ {/* 功能按钮 */}
+
+
+ {siteConfig('CAN_COPY') && (
+
+ )}
+
+
+
+
{locale.MENU.SHARE_URL}
+
+
+
+ {isDarkMode ?
:
}
+
{isDarkMode ? locale.MENU.LIGHT_MODE : locale.MENU.DARK_MODE}
+
+ {siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH') && (
+
+
+
{locale.MENU.THEME_SWITCH}
+
+ )}
+
+
+
+
+
+ )
+}
diff --git a/components/DarkModeButton.js b/components/DarkModeButton.js
new file mode 100644
index 0000000000000000000000000000000000000000..0d875fa7963153badb09e2c8378d104dd627c31d
--- /dev/null
+++ b/components/DarkModeButton.js
@@ -0,0 +1,27 @@
+import { useGlobal } from '@/lib/global'
+import { Moon, Sun } from './HeroIcons'
+import { useImperativeHandle } from 'react'
+
+/**
+ * 深色模式按钮
+ */
+const DarkModeButton = (props) => {
+ const { cRef, className } = props
+ const { isDarkMode, toggleDarkMode } = useGlobal()
+
+ /**
+ * 对外暴露方法
+ */
+ useImperativeHandle(cRef, () => {
+ return {
+ handleChangeDarkMode: () => {
+ toggleDarkMode()
+ }
+ }
+ })
+
+ return
+}
+export default DarkModeButton
diff --git a/components/DebugPanel.js b/components/DebugPanel.js
new file mode 100644
index 0000000000000000000000000000000000000000..845da50203758e595956c40e98814961822e5bf6
--- /dev/null
+++ b/components/DebugPanel.js
@@ -0,0 +1,128 @@
+import { useEffect, useState } from 'react'
+import Select from './Select'
+import { useGlobal } from '@/lib/global'
+import { THEMES } from '@/themes/theme'
+import { useRouter } from 'next/router'
+import { siteConfigMap } from '@/lib/config'
+import { getQueryParam } from '@/lib/utils'
+
+/**
+ *
+ * @returns 调试面板
+ */
+const DebugPanel = () => {
+ const [show, setShow] = useState(false)
+ const { theme, switchTheme, locale } = useGlobal()
+ const router = useRouter()
+ const currentTheme = getQueryParam(router.asPath, 'theme') || theme
+ const [siteConfig, updateSiteConfig] = useState({})
+
+ // 主题下拉框
+ const themeOptions = THEMES?.map(t => ({ value: t, text: t }))
+
+ useEffect(() => {
+ updateSiteConfig(Object.assign({}, siteConfigMap()))
+ }, [])
+
+ function toggleShow() {
+ setShow(!show)
+ }
+
+ function handleChangeDebugTheme() {
+ switchTheme()
+ }
+
+ function handleUpdateDebugTheme(newTheme) {
+ const query = { ...router.query, theme: newTheme }
+ router.push({ pathname: router.pathname, query })
+ }
+
+ function filterResult(text) {
+ switch (text) {
+ case 'true':
+ return true
+ case 'false':
+ return false
+ case '':
+ return '-'
+ }
+ return text
+ }
+
+ return (
+ <>
+ {/* 调试按钮 */}
+
+
+ {show
+ ? {locale.COMMON.DEBUG_CLOSE}
+ : {locale.COMMON.DEBUG_OPEN}}
+
+
+
+ {/* 调试侧拉抽屉 */}
+
+
+
+
+ {/*
+
+ 主题配置{`config_${debugTheme}.js`}:
+
+
+ {Object.keys(themeConfig).map(k => (
+
+
+ {k}
+
+
+ {filterResult(themeConfig[k] + '')}
+
+
+ ))}
+
+
*/}
+
+ 站点配置[blog.config.js]
+
+
+ {siteConfig && Object.keys(siteConfig).map(k => (
+
+
+ {k}
+
+
+ {filterResult(siteConfig[k] + '')}
+
+
+ ))}
+
+
+
+
+ >
+ )
+}
+export default DebugPanel
diff --git a/components/DifyChatbot.js b/components/DifyChatbot.js
new file mode 100644
index 0000000000000000000000000000000000000000..c954a8723ff879ed6d00702266ba598c43c810ff
--- /dev/null
+++ b/components/DifyChatbot.js
@@ -0,0 +1,32 @@
+import { useEffect } from 'react';
+import { siteConfig } from '@/lib/config';
+
+export default function DifyChatbot() {
+ useEffect(() => {
+ // 这里使用 siteConfig() 函数调用来获取配置值
+ if (!siteConfig('DIFY_CHATBOT_ENABLED')) {
+ return;
+ }
+
+ // 配置 DifyChatbot,同样需要调用 siteConfig() 获取相应的配置值
+ window.difyChatbotConfig = {
+ token: siteConfig('DIFY_CHATBOT_TOKEN'),
+ baseUrl: siteConfig('DIFY_CHATBOT_BASE_URL')
+ };
+
+ // 加载 DifyChatbot 脚本
+ const script = document.createElement('script');
+ script.src = `${siteConfig('DIFY_CHATBOT_BASE_URL')}/embed.min.js`; // 注意调用 siteConfig()
+ script.id = siteConfig('DIFY_CHATBOT_TOKEN'); // 注意调用 siteConfig()
+ script.defer = true;
+ document.body.appendChild(script);
+
+ return () => {
+ // 在组件卸载时清理 script 标签
+ const existingScript = document.getElementById(siteConfig('DIFY_CHATBOT_TOKEN')); // 注意调用 siteConfig()
+ if (existingScript) document.body.removeChild(existingScript);
+ };
+ }, []); // 注意依赖数组为空,意味着脚本将仅在加载页面时执行一次
+
+ return null;
+}
diff --git a/components/DisableCopy.js b/components/DisableCopy.js
new file mode 100644
index 0000000000000000000000000000000000000000..9bc2cb9ea34bc1ed6f59b9b3b8f54eeb7a62f1cf
--- /dev/null
+++ b/components/DisableCopy.js
@@ -0,0 +1,21 @@
+import { siteConfig } from '@/lib/config'
+import { useEffect } from 'react'
+
+/**
+ * 禁止用户拷贝文章的插件
+ */
+export default function DisableCopy() {
+ useEffect(() => {
+ if (!JSON.parse(siteConfig('CAN_COPY'))) {
+ // 全栈添加禁止复制的样式
+ document.getElementsByTagName('html')[0].classList.add('forbid-copy')
+ // 监听复制事件
+ document.addEventListener('copy', function (event) {
+ event.preventDefault() // 阻止默认复制行为
+ alert('抱歉,本网页内容不可复制!')
+ })
+ }
+ }, [])
+
+ return null
+}
diff --git a/components/Draggable.js b/components/Draggable.js
new file mode 100644
index 0000000000000000000000000000000000000000..44d565b83754f625c1b12e060089a71943999557
--- /dev/null
+++ b/components/Draggable.js
@@ -0,0 +1,150 @@
+import { useRef, useEffect, useState } from 'react'
+/**
+ * 可拖拽组件
+ */
+
+export const Draggable = (props) => {
+ const { children } = props
+ const draggableRef = useRef(null)
+ const rafRef = useRef(null)
+ const [moving, setMoving] = useState(false)
+ let currentObj, offsetX, offsetY
+
+ useEffect(() => {
+ const draggableElements = document.getElementsByClassName('draggable')
+
+ // 标准化鼠标事件对象
+ function e(event) { // 定义事件对象标准化函数
+ if (!event) { // 兼容IE浏览器
+ event = window.event
+ event.target = event.srcElement
+ event.layerX = event.offsetX
+ event.layerY = event.offsetY
+ }
+ // 移动端
+ if (event.type === 'touchstart' || event.type === 'touchmove') {
+ event.clientX = event.touches[0].clientX
+ event.clientY = event.touches[0].clientY
+ }
+
+ event.mx = event.pageX || event.clientX + document.body.scrollLeft
+ // 计算鼠标指针的x轴距离
+ event.my = event.pageY || event.clientY + document.body.scrollTop
+ // 计算鼠标指针的y轴距离
+
+ return event // 返回标准化的事件对象
+ }
+
+ // 定义鼠标事件处理函数
+ // document.pointerdown = start
+ document.onmousedown = start
+ document.ontouchstart = start
+
+ function start (event) { // 按下鼠标时,初始化处理
+ if (!draggableElements) return
+ event = e(event)// 获取标准事件对象
+
+ for (const drag of draggableElements) {
+ // 判断鼠标点击的区域是否是拖拽框内
+ if (inDragBox(event, drag)) {
+ currentObj = drag.firstElementChild
+ }
+ }
+ if (currentObj) {
+ if (event.type === 'touchstart') {
+ event.preventDefault() // 阻止默认的滚动行为
+ document.documentElement.style.overflow = 'hidden' // 防止页面一起滚动
+ }
+
+ setMoving(true)
+ offsetX = event.mx - currentObj.offsetLeft
+ offsetY = event.my - currentObj.offsetTop
+
+ document.onmousemove = move// 注册鼠标移动事件处理函数
+ document.ontouchmove = move
+ document.onmouseup = stop// 注册松开鼠标事件处理函数
+ document.ontouchend = stop
+ }
+ }
+
+ function move(event) { // 鼠标移动处理函数
+ event = e(event)
+ rafRef.current = requestAnimationFrame(() => updatePosition(event))
+ }
+
+ const stop = (event) => {
+ event = e(event)
+ document.documentElement.style.overflow = 'auto' // 恢复默认的滚动行为
+ cancelAnimationFrame(rafRef.current)
+ setMoving(false)
+ currentObj = document.ontouchmove = document.ontouchend = document.onmousemove = document.onmouseup = null
+ }
+
+ const updatePosition = (event) => {
+ if (currentObj) {
+ const left = event.mx - offsetX
+ const top = event.my - offsetY
+ currentObj.style.left = left + 'px'
+ currentObj.style.top = top + 'px'
+ checkInWindow()
+ }
+ }
+
+ /**
+ * 鼠标是否在可拖拽区域内
+ * @param {*} event
+ * @returns
+ */
+ function inDragBox(event, drag) {
+ const { clientX, clientY } = event // 鼠标位置
+ const { offsetHeight, offsetWidth, offsetTop, offsetLeft } = drag.firstElementChild // 窗口位置
+ const horizontal = clientX > offsetLeft && clientX < offsetLeft + offsetWidth
+ const vertical = clientY > offsetTop && clientY < offsetTop + offsetHeight
+
+ if (horizontal && vertical) {
+ return true
+ }
+
+ return false
+ }
+
+ /**
+ * 若超出窗口则吸附。
+ */
+ function checkInWindow() {
+ // 检查是否悬浮在窗口内
+ for (const drag of draggableElements) {
+ // 判断鼠标点击的区域是否是拖拽框内
+ const { offsetHeight, offsetWidth, offsetTop, offsetLeft } = drag.firstElementChild
+ const { clientHeight, clientWidth } = document.documentElement
+ if (offsetTop < 0) {
+ drag.firstElementChild.style.top = 0
+ }
+ if (offsetTop > (clientHeight - offsetHeight)) {
+ drag.firstElementChild.style.top = clientHeight - offsetHeight + 'px'
+ }
+ if (offsetLeft < 0) {
+ drag.firstElementChild.style.left = 0
+ }
+ if (offsetLeft > (clientWidth - offsetWidth)) {
+ drag.firstElementChild.style.left = clientWidth - offsetWidth + 'px'
+ }
+ }
+ }
+
+ window.addEventListener('resize', checkInWindow)
+
+ return () => {
+ return () => {
+ window.removeEventListener('resize', checkInWindow)
+ cancelAnimationFrame(rafRef.current)
+ }
+ }
+ }, [])
+
+ return
+ {children}
+
+}
+
+Draggable.defaultProps = { left: 0, top: 0 }
diff --git a/components/Equation.js b/components/Equation.js
new file mode 100644
index 0000000000000000000000000000000000000000..86685d625f6b6dfa676572ef42e1ab1ff29d0abc
--- /dev/null
+++ b/components/Equation.js
@@ -0,0 +1,29 @@
+import * as React from 'react'
+
+import Katex from '@/components/KatexReact'
+import { getBlockTitle } from 'notion-utils'
+
+const katexSettings = {
+ throwOnError: false,
+ strict: false
+}
+
+/**
+ * 数学公式
+ * @param {} param0
+ * @returns
+ */
+export const Equation = ({ block, math, inline = false, className, ...rest }) => {
+ math = math || getBlockTitle(block, null)
+ if (!math) return null
+
+ return (
+
+
+
+ )
+}
diff --git a/components/ExternalPlugins.js b/components/ExternalPlugins.js
new file mode 100644
index 0000000000000000000000000000000000000000..768cc384639d4d66b71c01db828b6eca47b1855b
--- /dev/null
+++ b/components/ExternalPlugins.js
@@ -0,0 +1,290 @@
+import { siteConfig } from '@/lib/config'
+import dynamic from 'next/dynamic'
+import LA51 from './LA51'
+import WebWhiz from './Webwhiz'
+import TianLiGPT from './TianliGPT'
+import { GlobalStyle } from './GlobalStyle'
+
+import { CUSTOM_EXTERNAL_CSS, CUSTOM_EXTERNAL_JS, IMG_SHADOW } from '@/blog.config'
+import { isBrowser, loadExternalResource } from '@/lib/utils'
+
+const TwikooCommentCounter = dynamic(() => import('@/components/TwikooCommentCounter'), { ssr: false })
+const DebugPanel = dynamic(() => import('@/components/DebugPanel'), { ssr: false })
+const ThemeSwitch = dynamic(() => import('@/components/ThemeSwitch'), { ssr: false })
+const Fireworks = dynamic(() => import('@/components/Fireworks'), { ssr: false })
+const Nest = dynamic(() => import('@/components/Nest'), { ssr: false })
+const FlutteringRibbon = dynamic(() => import('@/components/FlutteringRibbon'), { ssr: false })
+const Ribbon = dynamic(() => import('@/components/Ribbon'), { ssr: false })
+const Sakura = dynamic(() => import('@/components/Sakura'), { ssr: false })
+const StarrySky = dynamic(() => import('@/components/StarrySky'), { ssr: false })
+const DifyChatbot = dynamic(() => import('@/components/DifyChatbot'), { ssr: false });
+const Analytics = dynamic(() => import('@vercel/analytics/react').then(async (m) => { return m.Analytics }), { ssr: false })
+const MusicPlayer = dynamic(() => import('@/components/Player'), { ssr: false })
+const Ackee = dynamic(() => import('@/components/Ackee'), { ssr: false })
+const Gtag = dynamic(() => import('@/components/Gtag'), { ssr: false })
+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 })
+const DisableCopy = dynamic(() => import('@/components/DisableCopy'), { ssr: false })
+const AdBlockDetect = dynamic(() => import('@/components/AdBlockDetect'), { ssr: false })
+const LoadingProgress = dynamic(() => import('@/components/LoadingProgress'), { ssr: false })
+const AosAnimation = dynamic(() => import('@/components/AOSAnimation'), { ssr: false })
+
+/**
+ * 各种插件脚本
+ * @param {*} props
+ * @returns
+ */
+const ExternalPlugin = (props) => {
+ const DISABLE_PLUGIN = siteConfig('DISABLE_PLUGIN')
+ const THEME_SWITCH = siteConfig('THEME_SWITCH')
+ const DEBUG = siteConfig('DEBUG')
+ const ANALYTICS_ACKEE_TRACKER = siteConfig('ANALYTICS_ACKEE_TRACKER')
+ const ANALYTICS_VERCEL = siteConfig('ANALYTICS_VERCEL')
+ const ANALYTICS_BUSUANZI_ENABLE = siteConfig('ANALYTICS_BUSUANZI_ENABLE')
+ const ADSENSE_GOOGLE_ID = siteConfig('ADSENSE_GOOGLE_ID')
+ const FACEBOOK_APP_ID = siteConfig('FACEBOOK_APP_ID')
+ const FACEBOOK_PAGE_ID = siteConfig('FACEBOOK_PAGE_ID')
+ const FIREWORKS = siteConfig('FIREWORKS')
+ const SAKURA = siteConfig('SAKURA')
+ const STARRY_SKY = siteConfig('STARRY_SKY')
+ const MUSIC_PLAYER = siteConfig('MUSIC_PLAYER')
+ const NEST = siteConfig('NEST')
+ const FLUTTERINGRIBBON = siteConfig('FLUTTERINGRIBBON')
+ const COMMENT_TWIKOO_COUNT_ENABLE = siteConfig('COMMENT_TWIKOO_COUNT_ENABLE')
+ const RIBBON = siteConfig('RIBBON')
+ const CUSTOM_RIGHT_CLICK_CONTEXT_MENU = siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU')
+ const CAN_COPY = siteConfig('CAN_COPY')
+ const WEB_WHIZ_ENABLED = siteConfig('WEB_WHIZ_ENABLED')
+ const AD_WWADS_BLOCK_DETECT = siteConfig('AD_WWADS_BLOCK_DETECT')
+ const CHATBASE_ID = siteConfig('CHATBASE_ID')
+ const COMMENT_DAO_VOICE_ID = siteConfig('COMMENT_DAO_VOICE_ID')
+ const AD_WWADS_ID = siteConfig('AD_WWADS_ID')
+ const COMMENT_TWIKOO_ENV_ID = siteConfig('COMMENT_TWIKOO_ENV_ID')
+ const COMMENT_TWIKOO_CDN_URL = siteConfig('COMMENT_TWIKOO_CDN_URL')
+ const COMMENT_ARTALK_SERVER = siteConfig('COMMENT_ARTALK_SERVER')
+ const COMMENT_ARTALK_JS = siteConfig('COMMENT_ARTALK_JS')
+ const COMMENT_TIDIO_ID = siteConfig('COMMENT_TIDIO_ID')
+ const COMMENT_GITTER_ROOM = siteConfig('COMMENT_GITTER_ROOM')
+ const ANALYTICS_BAIDU_ID = siteConfig('ANALYTICS_BAIDU_ID')
+ const ANALYTICS_CNZZ_ID = siteConfig('ANALYTICS_CNZZ_ID')
+ const ANALYTICS_GOOGLE_ID = siteConfig('ANALYTICS_GOOGLE_ID')
+ const MATOMO_HOST_URL = siteConfig('MATOMO_HOST_URL')
+ const MATOMO_SITE_ID = siteConfig('MATOMO_SITE_ID')
+ const ANALYTICS_51LA_ID = siteConfig('ANALYTICS_51LA_ID')
+ const ANALYTICS_51LA_CK = siteConfig('ANALYTICS_51LA_CK')
+ const DIFY_CHATBOT_ENABLED = siteConfig('DIFY_CHATBOT_ENABLED')
+ const TIANLI_KEY = siteConfig('TianliGPT_KEY')
+ const GLOBAL_JS = siteConfig('GLOBAL_JS')
+ const CLARITY_ID = siteConfig('CLARITY_ID')
+
+ // 自定义样式css和js引入
+ if (isBrowser) {
+ // 初始化AOS动画
+ // 静态导入本地自定义样式
+ loadExternalResource('/css/custom.css', 'css')
+ loadExternalResource('/js/custom.js', 'js')
+
+ // 自动添加图片阴影
+ if (IMG_SHADOW) {
+ loadExternalResource('/css/img-shadow.css', 'css')
+ }
+
+ // 导入外部自定义脚本
+ if (CUSTOM_EXTERNAL_JS && CUSTOM_EXTERNAL_JS.length > 0) {
+ for (const url of CUSTOM_EXTERNAL_JS) {
+ loadExternalResource(url, 'js')
+ }
+ }
+
+ // 导入外部自定义样式
+ if (CUSTOM_EXTERNAL_CSS && CUSTOM_EXTERNAL_CSS.length > 0) {
+ for (const url of CUSTOM_EXTERNAL_CSS) {
+ loadExternalResource(url, 'css')
+ }
+ }
+ }
+
+ if (DISABLE_PLUGIN) {
+ return null
+ }
+
+ return <>
+
+ {/* 全局样式嵌入 */}
+
+
+ {THEME_SWITCH && }
+ {DEBUG && }
+ {ANALYTICS_ACKEE_TRACKER && }
+ {ANALYTICS_GOOGLE_ID && }
+ {ANALYTICS_VERCEL && }
+ {ANALYTICS_BUSUANZI_ENABLE && }
+ {ADSENSE_GOOGLE_ID && }
+ {FACEBOOK_APP_ID && FACEBOOK_PAGE_ID && }
+ {FIREWORKS && }
+ {SAKURA && }
+ {STARRY_SKY && }
+ {MUSIC_PLAYER && }
+ {NEST && }
+ {FLUTTERINGRIBBON && }
+ {COMMENT_TWIKOO_COUNT_ENABLE && }
+ {RIBBON && }
+ {DIFY_CHATBOT_ENABLED && }
+ {CUSTOM_RIGHT_CLICK_CONTEXT_MENU && }
+ {!CAN_COPY && }
+ {WEB_WHIZ_ENABLED && }
+ {AD_WWADS_BLOCK_DETECT && }
+ {TIANLI_KEY && }
+
+
+
+ {ANALYTICS_51LA_ID && ANALYTICS_51LA_CK && }
+
+ {ANALYTICS_51LA_ID && ANALYTICS_51LA_CK && (<>
+
+ {/* */}
+ >)}
+
+ {/* 注入JS脚本 */}
+ {GLOBAL_JS && }
+
+ {CHATBASE_ID && (<>
+
+
+ >)}
+
+ {CLARITY_ID && (<>
+
+ >)}
+
+ {COMMENT_DAO_VOICE_ID && (<>
+ {/* DaoVoice 反馈 */}
+
+
+ >)}
+
+ {AD_WWADS_ID && }
+
+ {COMMENT_TWIKOO_ENV_ID && }
+
+ {COMMENT_ARTALK_SERVER && }
+
+ {COMMENT_TIDIO_ID && }
+
+ {/* gitter聊天室 */}
+ {COMMENT_GITTER_ROOM && (<>
+
+
+ >)}
+
+ {/* 百度统计 */}
+ {ANALYTICS_BAIDU_ID && (
+
+ )}
+
+ {/* 站长统计 */}
+ {ANALYTICS_CNZZ_ID && (
+
+ )}
+
+ {/* 谷歌统计 */}
+ {ANALYTICS_GOOGLE_ID && (<>
+
+
+ >)}
+
+ {/* Matomo 统计 */}
+ {MATOMO_HOST_URL && MATOMO_SITE_ID && (
+
+ )}
+
+ >
+}
+
+export default ExternalPlugin
diff --git a/components/ExternalScript.js b/components/ExternalScript.js
new file mode 100644
index 0000000000000000000000000000000000000000..00bd852180bfd305773c1652626aff6950e3d3cf
--- /dev/null
+++ b/components/ExternalScript.js
@@ -0,0 +1,29 @@
+'use client'
+
+import { isBrowser } from '@/lib/utils'
+
+/**
+ * 自定义外部 script
+ * 传入参数将转为