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
+ { + firstTenTags?.map((tag, index) => { + return +
+
{tag.name}
{tag.count ? {tag.count} : <>} +
+ + + }) + } +
+} + +/** + * 分页 + * @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 ( +
+ + {COMMENT_ARTALK_SERVER && (
+ +
)} + + {COMMENT_TWIKOO_ENV_ID && (
+ +
)} + + {COMMENT_WALINE_SERVER_URL && (
+ +
)} + + {COMMENT_VALINE_APP_ID && (
+ +
)} + + {COMMENT_GISCUS_REPO && ( +
+ +
+ )} + + {COMMENT_CUSDIS_APP_ID && (
+ +
)} + + {COMMENT_UTTERRANCES_REPO && (
+ +
)} + + {COMMENT_GITALK_CLIENT_ID && (
+ +
)} + + {COMMENT_WEBMENTION_ENABLE && (
+ +
)} +
+
+ ) +} + +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.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
+
{isDarkMode ? : }
+
+} +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}} +
+
+ + {/* 调试侧拉抽屉 */} +
+
+
+