diff --git a/themes/game/components/AdBlockerDetect.js b/themes/game/components/AdBlockerDetect.js
new file mode 100644
index 00000000..81f24d39
--- /dev/null
+++ b/themes/game/components/AdBlockerDetect.js
@@ -0,0 +1,107 @@
+import { useRouter } from 'next/router'
+import { useState, useEffect } from 'react'
+
+/**
+ * 检测是否用了任意一种广告屏蔽插件
+ * @returns {JSX.Element|null} 如果检测到广告屏蔽插件则返回提示信息,否则返回null
+ */
+export default function AdBlockerDetect() {
+ const [isAdBlocker, setIsAdBlocker] = useState(false)
+ const [noticeCountdown, setNoticeCountdown] = useState(10) // 广告拦截弹窗提示倒计时
+ const router = useRouter()
+
+ useEffect(() => {
+ let adsCheckCountdown = 10 // 广告拦截检测倒计时
+ // GoogleAds 是否被拦截
+ const adLoadTimer = setInterval(() => {
+ if (window.adsbygoogle) {
+ clearInterval(adLoadTimer)
+ checkAdBlocker()
+ } else {
+ if (adsCheckCountdown > 1) {
+ adsCheckCountdown--
+ } else {
+ clearInterval(adLoadTimer)
+ setIsAdBlocker(true)
+ }
+ }
+ }, 1000)
+
+ return () => clearInterval(adLoadTimer)
+ }, [router])
+
+ /**
+ * 检测广告单元可见度
+ */
+ const checkAdBlocker = () => {
+ const ads = document.querySelectorAll('.adsbygoogle')
+ if (ads.length === 0) {
+ setIsAdBlocker(true)
+ } else {
+ let adEffect = false
+ for (const ad of ads) {
+ const adStyle = getComputedStyle(ad)
+ if (adStyle.display !== 'none' && adStyle.visibility !== 'hidden') {
+ adEffect = true
+ break
+ }
+ }
+ if (!adEffect) {
+ setIsAdBlocker(true)
+ }
+ }
+ }
+
+ useEffect(() => {
+ if (isAdBlocker) {
+ const timer = setInterval(() => {
+ setNoticeCountdown(prevCountdown => {
+ if (prevCountdown <= 0) {
+ clearInterval(timer)
+ setIsAdBlocker(false)
+ return 0
+ } else {
+ return prevCountdown - 1
+ }
+ })
+ }, 1000)
+ return () => clearInterval(timer)
+ }
+ }, [isAdBlocker])
+
+ if (!isAdBlocker) {
+ return null
+ }
+
+ return (
+ <>
+
+
+
+ {posts?.map(post => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/themes/game/components/BlogListScroll.js b/themes/game/components/BlogListScroll.js
new file mode 100644
index 00000000..42a1f440
--- /dev/null
+++ b/themes/game/components/BlogListScroll.js
@@ -0,0 +1,83 @@
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import throttle from 'lodash.throttle'
+import { deepClone } from '@/lib/utils'
+import { siteConfig } from '@/lib/config'
+import { useCallback, useEffect, useRef, useState } from 'react'
+
+export const BlogListScroll = props => {
+ const { posts } = props
+ const { locale } = useGlobal()
+
+ const [page, updatePage] = useState(1)
+
+ let hasMore = false
+ const postsToShow = posts && Array.isArray(posts)
+ ? deepClone(posts).slice(0, parseInt(siteConfig('POSTS_PER_PAGE')) * page)
+ : []
+
+ if (posts) {
+ const totalCount = posts.length
+ hasMore = page * parseInt(siteConfig('POSTS_PER_PAGE')) < totalCount
+ }
+ const handleGetMore = () => {
+ if (!hasMore) return
+ updatePage(page + 1)
+ }
+
+ const targetRef = useRef(null)
+
+ // 监听滚动自动分页加载
+ const scrollTrigger = useCallback(throttle(() => {
+ const scrollS = window.scrollY + window.outerHeight
+ const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
+ if (scrollS > clientHeight + 100) {
+ handleGetMore()
+ }
+ }, 500))
+
+ useEffect(() => {
+ window.addEventListener('scroll', scrollTrigger)
+
+ return () => {
+ window.removeEventListener('scroll', scrollTrigger)
+ }
+ })
+
+ return (
+
+ {postsToShow.map(p => (
+
+
+
+ {p.title}
+
+
+
+
+
+
+ {p.summary}
+
+
+ ))}
+
+
+ {' '}
+ {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
+
+
+
+ )
+}
diff --git a/themes/game/components/BlogPost.js b/themes/game/components/BlogPost.js
new file mode 100644
index 00000000..b5845cf9
--- /dev/null
+++ b/themes/game/components/BlogPost.js
@@ -0,0 +1,41 @@
+import Link from 'next/link'
+import { siteConfig } from '@/lib/config'
+import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils'
+import NotionIcon from '@/components/NotionIcon'
+import NotionPage from '@/components/NotionPage'
+
+const BlogPost = ({ post }) => {
+ const url = checkContainHttp(post.slug) ? sliceUrlFromHttp(post.slug) : `${siteConfig('SUB_PATH', '')}/${post.slug}`
+
+ const showPreview = siteConfig('POST_LIST_PREVIEW') && post.blockMap
+
+ return (
+ (Loading...
}
+ {!onLoading && comments && comments.length === 0 &&