diff --git a/.env.local b/.env.example
similarity index 99%
rename from .env.local
rename to .env.example
index f2327cf8..58d0b56f 100644
--- a/.env.local
+++ b/.env.example
@@ -1,12 +1,11 @@
# 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables
-NEXT_PUBLIC_VERSION=4.6.2
-
# 可在此添加环境变量,去掉最左边的(# )注释即可
# Notion页面ID,必须
# NOTION_PAGE_ID=097e5f674880459d8e1b4407758dc4fb
# 非必须
+# NEXT_PUBLIC_VERSION=
# NEXT_PUBLIC_PSEUDO_STATIC=
# NEXT_PUBLIC_REVALIDATE_SECOND=
# NEXT_PUBLIC_THEME=matery
@@ -174,3 +173,4 @@ NEXT_PUBLIC_VERSION=4.6.2
# ENABLE_CACHE=
# VERCEL_ENV=
# NEXT_PUBLIC_VERSION=
+# NEXT_BUILD_STANDALONE=
diff --git a/.eslintrc.js b/.eslintrc.js
index c6fbb20a..f523d8f0 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -4,15 +4,30 @@ module.exports = {
es2021: true,
node: true
},
- extends: ['plugin:react/recommended', 'plugin:@next/next/recommended', 'standard', 'prettier'],
+ extends: [
+ 'plugin:react/jsx-runtime',
+ 'plugin:react/recommended',
+ 'plugin:@next/next/recommended',
+ 'next',
+ 'prettier',
+ 'plugin:@typescript-eslint/recommended', // 添加 TypeScript 推荐规则
+ 'plugin:@typescript-eslint/recommended-requiring-type-checking' // 添加需要类型检查的规则
+ ],
+ parser: '@typescript-eslint/parser', // 使用 TypeScript 解析器
parserOptions: {
ecmaFeatures: {
jsx: true
},
ecmaVersion: 12,
- sourceType: 'module'
+ sourceType: 'module',
+ project: './tsconfig.eslint.json' // 指向新的 ESLint 配置文件
},
- plugins: ['react', 'react-hooks', 'prettier'],
+ plugins: [
+ 'react',
+ 'react-hooks',
+ 'prettier',
+ '@typescript-eslint' // 添加 TypeScript 插件
+ ],
settings: {
react: {
version: 'detect'
@@ -23,8 +38,31 @@ module.exports = {
'react/no-unknown-property': 'off', //
>
)
}
diff --git a/components/LoadingCover.js b/components/LoadingCover.js
new file mode 100644
index 00000000..fc51083d
--- /dev/null
+++ b/components/LoadingCover.js
@@ -0,0 +1,77 @@
+'user client'
+import { useGlobal } from '@/lib/global'
+import { useEffect, useState } from 'react'
+/**
+ * @see https://css-loaders.com/
+ * @returns 加载动画
+ */
+export default function LoadingCover() {
+ const { onLoading, setOnLoading } = useGlobal()
+ const [isVisible, setIsVisible] = useState(false) // 初始状态设置为false,避免服务端渲染与客户端渲染不一致
+
+ useEffect(() => {
+ // 确保在客户端渲染时才设置可见性
+ if (onLoading) {
+ setIsVisible(true)
+ } else {
+ setIsVisible(false)
+ }
+ }, [onLoading])
+
+ const handleClick = () => {
+ setOnLoading(false) // 强行关闭 LoadingCover
+ }
+
+ if (typeof window === 'undefined') {
+ return null // 避免在服务端渲染时渲染出这个组件
+ }
+
+ return isVisible ? (
+
{
const autoScrollToHash = () => {
setTimeout(() => {
// 跳转到指定标题
- const needToJumpToTitle = window.location.hash
+ const hash = window?.location?.hash
+ const needToJumpToTitle = hash && hash.length > 0
if (needToJumpToTitle) {
- const tocNode = document.getElementById(window.location.hash.substring(1))
+ console.log('jump to hash', hash)
+ const tocNode = document.getElementById(hash.substring(1))
if (tocNode && tocNode?.className?.indexOf('notion') > -1) {
tocNode.scrollIntoView({ block: 'start', behavior: 'smooth' })
}
diff --git a/components/OpenWrite.js b/components/OpenWrite.js
new file mode 100644
index 00000000..a219830d
--- /dev/null
+++ b/components/OpenWrite.js
@@ -0,0 +1,154 @@
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import { isBrowser, loadExternalResource } from '@/lib/utils'
+import { useRouter } from 'next/router'
+import { useEffect } from 'react'
+/**
+ * OpenWrite公众号导流插件
+ * 使用介绍:https://openwrite.cn/guide/readmore/readmore.html#%E4%BA%8C%E3%80%81%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8
+ * 登录后台配置你的博客:https://readmore.openwrite.cn/
+ * @returns
+ */
+const OpenWrite = () => {
+ const router = useRouter()
+ const qrcode = siteConfig('OPEN_WRITE_QRCODE', '请配置公众号二维码')
+ const blogId = siteConfig('OPEN_WRITE_BLOG_ID')
+ const name = siteConfig('OPEN_WRITE_NAME', '请配置公众号名')
+ const id = 'article-wrapper'
+ const keyword = siteConfig('OPEN_WRITE_KEYWORD', '请配置公众号关键词')
+ const btnText = siteConfig(
+ 'OPEN_WRITE_BTN_TEXT',
+ '原创不易,完成人机检测,阅读全文'
+ )
+ // 验证一次后的有效时长,单位小时
+ const cookieAge = siteConfig('OPEN_WRITE_VALIDITY_DURATION', 1)
+ // 白名单
+ const whiteList = siteConfig('OPEN_WRITE_WHITE_LIST', '')
+
+ // 登录信息
+ const { isLoaded, isSignedIn } = useGlobal()
+
+ const loadOpenWrite = async () => {
+ try {
+ await loadExternalResource(
+ 'https://readmore.openwrite.cn/js/readmore-2.0.js',
+ 'js'
+ )
+ const BTWPlugin = window?.BTWPlugin
+
+ if (BTWPlugin) {
+ const btw = new BTWPlugin()
+ window.btw = btw
+ btw.init({
+ qrcode,
+ id,
+ name,
+ btnText,
+ keyword,
+ blogId,
+ cookieAge
+ })
+
+ // btw初始化后,开始监听read-more-wrap何时消失
+ const intervalId = setInterval(() => {
+ const readMoreWrapElement = document.getElementById('read-more-wrap')
+ const articleWrapElement = document.getElementById('article-wrapper')
+
+ if (!readMoreWrapElement && articleWrapElement) {
+ toggleTocItems(false) // 恢复目录项的点击
+ // 自动调整文章区域的高度
+ articleWrapElement.style.height = 'auto'
+ // 停止定时器
+ clearInterval(intervalId)
+ }
+ }, 1000) // 每秒检查一次
+
+ // Return cleanup function to clear the interval if the component unmounts
+ return () => clearInterval(intervalId)
+ }
+ } catch (error) {
+ console.error('OpenWrite 加载异常', error)
+ }
+ }
+ useEffect(() => {
+ const existWhite = existedWhiteList(router.asPath, whiteList)
+ // 白名单中,免检
+ if (existWhite) {
+ return
+ }
+ if (isSignedIn) {
+ // 用户已登录免检
+ console.log('用户已登录')
+ return
+ }
+
+ // 开发环境免检
+ if (process.env.NODE_ENV === 'development') {
+ console.log('开发环境:屏蔽OpenWrite')
+ return
+ }
+
+ if (isBrowser && blogId && !isSignedIn) {
+ toggleTocItems(true) // 禁止目录项的点击
+
+ // Check if the element with id 'read-more-wrap' already exists
+ const readMoreWrap = document.getElementById('read-more-wrap')
+
+ // Only load the script if the element doesn't exist
+ if (!readMoreWrap) {
+ loadOpenWrite()
+ }
+ }
+ }, [isLoaded, router])
+
+ // 启动一个监听器,当页面上存在#read-more-wrap对象时,所有的 a .catalog-item 对象都禁止点击
+
+ return <>>
+}
+
+// 定义禁用和恢复目录项点击的函数
+const toggleTocItems = disable => {
+ const tocItems = document.querySelectorAll('a.catalog-item')
+ tocItems.forEach(item => {
+ if (disable) {
+ item.style.pointerEvents = 'none'
+ item.style.opacity = '0.5'
+ } else {
+ item.style.pointerEvents = 'auto'
+ item.style.opacity = '1'
+ }
+ })
+}
+
+/**
+ * 检查白名单
+ * @param {*} path 当前url的字符串
+ * @param {*} whiteListStr 白名单字符串
+ */
+function existedWhiteList(path, whiteListStr) {
+ // 参数检查
+ if (!path || !whiteListStr) {
+ return true
+ }
+
+ // 提取 path 最后一个斜杠后的内容,去掉前面的斜杆
+ // 移除查询参数(从 '?' 开始的部分)和 `.html` 后缀
+ const processedPath = path
+ .replace(/\?.*$/, '') // 移除查询参数
+ .replace(/.*\/([^/]+)(?:\.html)?$/, '$1') // 去掉前面的路径和 .html
+
+ // 严格检查白名单字符串中是否包含处理后的 path
+ // const whiteListArray = whiteListStr.split(',')
+ // return whiteListArray.includes(processedPath)
+
+ // 放宽判断
+ const isWhite = whiteListStr.includes(processedPath)
+
+ if (isWhite) {
+ console.log('OpenWrite白名单', processedPath)
+ }
+
+ return isWhite
+}
+
+export default OpenWrite
diff --git a/components/Player.js b/components/Player.js
index 3c741f94..91fef339 100644
--- a/components/Player.js
+++ b/components/Player.js
@@ -71,10 +71,9 @@ const Player = () => {
fixed='true'
type='playlist'
preload='auto'
- lrc-type={siteConfig('MUSIC_PLAYER_METING_LRC_TYPE')}
api={siteConfig(
'MUSIC_PLAYER_METING_API',
- 'https://api.i-meto.com/meting/api'
+ 'https://api.i-meto.com/meting/api?server=:server&type=:type&id=:id&r=:r'
)}
autoplay={autoPlay}
order={siteConfig('MUSIC_PLAYER_ORDER')}
diff --git a/components/PoweredBy.js b/components/PoweredBy.js
new file mode 100644
index 00000000..87b8705f
--- /dev/null
+++ b/components/PoweredBy.js
@@ -0,0 +1,20 @@
+import { siteConfig } from '@/lib/config'
+
+/**
+ * 驱动版权
+ * @returns
+ */
+export default function PoweredBy(props) {
+ return (
+
+ )
+}
diff --git a/components/GlobalHead.js b/components/SEO.js
similarity index 91%
rename from components/GlobalHead.js
rename to components/SEO.js
index 0bd3dcca..b102ed11 100644
--- a/components/GlobalHead.js
+++ b/components/SEO.js
@@ -10,25 +10,55 @@ import { useEffect } from 'react'
* @param {*} param0
* @returns
*/
-const GlobalHead = props => {
+const SEO = props => {
const { children, siteInfo, post, NOTION_CONFIG } = props
- let url = siteConfig('PATH')?.length
- ? `${siteConfig('LINK')}/${siteConfig('SUB_PATH', '')}`
- : siteConfig('LINK')
+ const PATH = siteConfig('PATH')
+ const LINK = siteConfig('LINK')
+ const SUB_PATH = siteConfig('SUB_PATH', '')
+ let url = PATH?.length
+ ? `${LINK}/${SUB_PATH}`
+ : LINK
let image
const router = useRouter()
const meta = getSEOMeta(props, router, useGlobal()?.locale)
+ const webFontUrl = siteConfig('FONT_URL')
+
+ useEffect(() => {
+ // 使用WebFontLoader字体加载
+ loadExternalResource(
+ 'https://cdnjs.cloudflare.com/ajax/libs/webfont/1.6.28/webfontloader.js',
+ 'js'
+ ).then(url => {
+ const WebFont = window?.WebFont
+ if (WebFont) {
+ // console.log('LoadWebFont', webFontUrl)
+ WebFont.load({
+ custom: {
+ // families: ['"LXGW WenKai"'],
+ urls: webFontUrl
+ }
+ })
+ }
+ })
+ }, [])
+
+ // SEO关键词
+ const KEYWORDS = siteConfig('KEYWORDS')
+ let keywords = meta?.tags || KEYWORDS
+ if (post?.tags && post?.tags?.length > 0) {
+ keywords = post?.tags?.join(',')
+ }
if (meta) {
url = `${url}/${meta.slug}`
image = meta.image || '/bg_image.jpg'
}
- const title = meta?.title || siteConfig('TITLE')
+ const TITLE = siteConfig('TITLE')
+ const title = meta?.title || TITLE
const description = meta?.description || `${siteInfo?.description}`
const type = meta?.type || 'website'
const lang = siteConfig('LANG').replace('-', '_') // Facebook OpenGraph 要 zh_CN 這樣的格式才抓得到語言
- const category = meta?.category || siteConfig('KEYWORDS') // section 主要是像是 category 這樣的分類,Facebook 用這個來抓連結的分類
+ const category = meta?.category || KEYWORDS // section 主要是像是 category 這樣的分類,Facebook 用這個來抓連結的分類
const favicon = siteConfig('BLOG_FAVICON')
- const webFontUrl = siteConfig('FONT_URL')
const BACKGROUND_DARK = siteConfig('BACKGROUND_DARK', '', NOTION_CONFIG)
const SEO_BAIDU_SITE_VERIFICATION = siteConfig(
@@ -68,31 +98,8 @@ const GlobalHead = props => {
)
const FACEBOOK_PAGE = siteConfig('FACEBOOK_PAGE', null, NOTION_CONFIG)
- // SEO关键词
- let keywords = meta?.tags || siteConfig('KEYWORDS')
- if (post?.tags && post?.tags?.length > 0) {
- keywords = post?.tags?.join(',')
- }
-
- useEffect(() => {
- // 使用WebFontLoader字体加载
- loadExternalResource(
- 'https://cdnjs.cloudflare.com/ajax/libs/webfont/1.6.28/webfontloader.js',
- 'js'
- ).then(url => {
- const WebFont = window?.WebFont
- if (WebFont) {
- console.log('LoadWebFont', webFontUrl)
- WebFont.load({
- custom: {
- // families: ['"LXGW WenKai"'],
- urls: webFontUrl
- }
- })
- }
- })
- }, [])
+ const AUTHOR = siteConfig('AUTHOR')
return (
@@ -153,7 +160,7 @@ const GlobalHead = props => {
{meta?.type === 'Post' && (
<>
-
+
>
@@ -172,6 +179,7 @@ const getSEOMeta = (props, router, locale) => {
const { post, siteInfo, tag, category, page } = props
const keyword = router?.query?.s
+ const TITLE = siteConfig('TITLE')
switch (router.route) {
case '/':
return {
@@ -234,7 +242,7 @@ const getSEOMeta = (props, router, locale) => {
case '/search/[keyword]/page/[page]':
return {
title: `${keyword || ''}${keyword ? ' | ' : ''}${locale.NAV.SEARCH} | ${siteInfo?.title}`,
- description: siteConfig('TITLE'),
+ description: TITLE,
image: `${siteInfo?.pageCover}`,
slug: 'search/' + (keyword || ''),
type: 'website'
@@ -275,4 +283,4 @@ const getSEOMeta = (props, router, locale) => {
}
}
-export default GlobalHead
+export default SEO
diff --git a/components/ShareBar.js b/components/ShareBar.js
index 334a5f12..1acbef4e 100644
--- a/components/ShareBar.js
+++ b/components/ShareBar.js
@@ -1,7 +1,9 @@
import { siteConfig } from '@/lib/config'
import dynamic from 'next/dynamic'
-const ShareButtons = dynamic(() => import('@/components/ShareButtons'), { ssr: false })
+const ShareButtons = dynamic(() => import('@/components/ShareButtons'), {
+ ssr: false
+})
/**
* 分享栏
@@ -9,14 +11,20 @@ const ShareButtons = dynamic(() => import('@/components/ShareButtons'), { ssr: f
* @returns
*/
const ShareBar = ({ post }) => {
- if (!JSON.parse(siteConfig('POST_SHARE_BAR_ENABLE')) || !post || post?.type !== 'Post') {
+ if (
+ !JSON.parse(siteConfig('POST_SHARE_BAR_ENABLE')) ||
+ !post ||
+ post?.type !== 'Post'
+ ) {
return <>>
}
- return
-
-
-
+ return (
+
+ )
}
export default ShareBar
diff --git a/components/ShareButtons.js b/components/ShareButtons.js
index 853aa3a4..5ea54055 100644
--- a/components/ShareButtons.js
+++ b/components/ShareButtons.js
@@ -5,48 +5,48 @@ import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import {
- EmailIcon,
- EmailShareButton,
- FacebookIcon,
- FacebookMessengerIcon,
- FacebookMessengerShareButton,
- FacebookShareButton,
- HatenaIcon,
- HatenaShareButton,
- InstapaperIcon,
- InstapaperShareButton,
- LineIcon,
- LineShareButton,
- LinkedinIcon,
- LinkedinShareButton,
- LivejournalIcon,
- LivejournalShareButton,
- MailruIcon,
- MailruShareButton,
- OKIcon,
- OKShareButton,
- PinterestIcon,
- PinterestShareButton,
- PocketIcon,
- PocketShareButton,
- RedditIcon,
- RedditShareButton,
- TelegramIcon,
- TelegramShareButton,
- TumblrIcon,
- TumblrShareButton,
- TwitterIcon,
- TwitterShareButton,
- VKIcon,
- VKShareButton,
- ViberIcon,
- ViberShareButton,
- WeiboIcon,
- WeiboShareButton,
- WhatsappIcon,
- WhatsappShareButton,
- WorkplaceIcon,
- WorkplaceShareButton
+ EmailIcon,
+ EmailShareButton,
+ FacebookIcon,
+ FacebookMessengerIcon,
+ FacebookMessengerShareButton,
+ FacebookShareButton,
+ HatenaIcon,
+ HatenaShareButton,
+ InstapaperIcon,
+ InstapaperShareButton,
+ LineIcon,
+ LineShareButton,
+ LinkedinIcon,
+ LinkedinShareButton,
+ LivejournalIcon,
+ LivejournalShareButton,
+ MailruIcon,
+ MailruShareButton,
+ OKIcon,
+ OKShareButton,
+ PinterestIcon,
+ PinterestShareButton,
+ PocketIcon,
+ PocketShareButton,
+ RedditIcon,
+ RedditShareButton,
+ TelegramIcon,
+ TelegramShareButton,
+ TumblrIcon,
+ TumblrShareButton,
+ TwitterIcon,
+ TwitterShareButton,
+ VKIcon,
+ VKShareButton,
+ ViberIcon,
+ ViberShareButton,
+ WeiboIcon,
+ WeiboShareButton,
+ WhatsappIcon,
+ WhatsappShareButton,
+ WorkplaceIcon,
+ WorkplaceShareButton
} from 'react-share'
const QrCode = dynamic(() => import('@/components/QrCode'), { ssr: false })
@@ -59,8 +59,8 @@ const QrCode = dynamic(() => import('@/components/QrCode'), { ssr: false })
const ShareButtons = ({ post }) => {
const router = useRouter()
const [shareUrl, setShareUrl] = useState(siteConfig('LINK') + router.asPath)
- const title = post.title || siteConfig('TITLE')
- const image = post.pageCover
+ const title = post?.title || siteConfig('TITLE')
+ const image = post?.pageCover
const body =
post?.title + ' | ' + title + ' ' + shareUrl + ' ' + post?.summary
@@ -70,8 +70,10 @@ const ShareButtons = ({ post }) => {
const [qrCodeShow, setQrCodeShow] = useState(false)
const copyUrl = () => {
- navigator?.clipboard?.writeText(shareUrl)
- alert(locale.COMMON.URL_COPIED + ' \n' + shareUrl)
+ // 确保 shareUrl 是一个正确的字符串并进行解码
+ const decodedUrl = decodeURIComponent(shareUrl)
+ navigator?.clipboard?.writeText(decodedUrl)
+ alert(locale.COMMON.URL_COPIED + ' \n' + decodedUrl)
}
const openPopover = () => {
diff --git a/components/SideBarDrawer.js b/components/SideBarDrawer.js
index 31a29396..c8398a3e 100644
--- a/components/SideBarDrawer.js
+++ b/components/SideBarDrawer.js
@@ -6,8 +6,16 @@ import { useEffect } from 'react'
* @returns {JSX.Element}
* @constructor
*/
-const SideBarDrawer = ({ children, isOpen, onOpen, onClose, className }) => {
+const SideBarDrawer = ({
+ children,
+ isOpen,
+ onOpen,
+ onClose,
+ className,
+ showOnPC = false
+}) => {
const router = useRouter()
+
useEffect(() => {
const sideBarDrawerRouteListener = () => {
switchSideDrawerVisible(false)
@@ -19,32 +27,44 @@ const SideBarDrawer = ({ children, isOpen, onOpen, onClose, className }) => {
}, [router.events])
// 点击按钮更改侧边抽屉状态
- const switchSideDrawerVisible = (showStatus) => {
+ const switchSideDrawerVisible = showStatus => {
if (showStatus) {
onOpen && onOpen()
} else {
onClose && onClose()
}
const sideBarDrawer = window.document.getElementById('sidebar-drawer')
- const sideBarDrawerBackground = window.document.getElementById('sidebar-drawer-background')
+ const sideBarDrawerBackground = window.document.getElementById(
+ 'sidebar-drawer-background'
+ )
if (showStatus) {
- sideBarDrawer?.classList.replace('-ml-60', 'ml-0')
+ sideBarDrawer?.classList.replace('translate-x-[-100%]', 'translate-x-0')
sideBarDrawerBackground?.classList.replace('hidden', 'block')
} else {
- sideBarDrawer?.classList.replace('ml-0', '-ml-60')
+ sideBarDrawer?.classList.replace('translate-x-0', 'translate-x-[-100%]')
sideBarDrawerBackground?.classList.replace('block', 'hidden')
}
}
- return