diff --git a/.env.local b/.env.local
index b8274ef0..c3d93adb 100644
--- a/.env.local
+++ b/.env.local
@@ -1,5 +1,5 @@
# 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables
-NEXT_PUBLIC_VERSION=4.4.0
+NEXT_PUBLIC_VERSION=4.4.1
# 可在此添加环境变量,去掉最左边的(# )注释即可
diff --git a/.prettierrc.json b/.prettierrc.json
index f0bc11c8..213aca02 100644
--- a/.prettierrc.json
+++ b/.prettierrc.json
@@ -1,10 +1,10 @@
{
- "singleQuote": true,
- "semi": false,
- "trailingComma": "none",
- "arrowParens": "avoid",
- "printWidth": 120,
- "bracketSpacing": true,
- "jsxSingleQuote": true,
- "jsxBracketSameLine": true
-}
\ No newline at end of file
+ "singleQuote": true,
+ "semi": false,
+ "trailingComma": "none",
+ "arrowParens": "avoid",
+ "printWidth": 80,
+ "bracketSpacing": true,
+ "jsxSingleQuote": true,
+ "jsxBracketSameLine": true
+}
diff --git a/blog.config.js b/blog.config.js
index 2af5e64e..df30298b 100644
--- a/blog.config.js
+++ b/blog.config.js
@@ -1,8 +1,7 @@
// 注: process.env.XX是Vercel的环境变量,配置方式见:https://docs.tangly1024.com/article/how-to-config-notion-next#c4768010ae7d44609b744e79e2f9959a
const BLOG = {
// Important page_id!!!Duplicate Template from https://www.notion.so/tanghh/02ab3b8678004aa69e9e415905ef32a5
- NOTION_PAGE_ID:
- process.env.NOTION_PAGE_ID || '02ab3b8678004aa69e9e415905ef32a5',
+ NOTION_PAGE_ID: process.env.NOTION_PAGE_ID || '02ab3b8678004aa69e9e415905ef32a5',
PSEUDO_STATIC: process.env.NEXT_PUBLIC_PSEUDO_STATIC || false, // 伪静态路径,开启后所有文章URL都以 .html 结尾。
NEXT_REVALIDATE_SECOND: process.env.NEXT_PUBLIC_REVALIDATE_SECOND || 5, // 更新内容缓存间隔 单位(秒);即每个页面有5秒的纯静态期、此期间无论多少次访问都不会抓取notion数据;调大该值有助于节省Vercel资源、同时提升访问速率,但也会使文章更新有延迟。
THEME: process.env.NEXT_PUBLIC_THEME || 'simple', // 当前主题,在themes文件夹下可找到所有支持的主题;主题名称就是文件夹名,例如 example,fukasawa,gitbook,heo,hexo,landing,matery,medium,next,nobelium,plog,simple
@@ -15,7 +14,9 @@ const BLOG = {
IS_TAG_COLOR_DISTINGUISHED: process.env.NEXT_PUBLIC_IS_TAG_COLOR_DISTINGUISHED === 'true' || true, // 对于名称相同的tag是否区分tag的颜色
// 3.14.1版本后,欢迎语在此配置,英文逗号隔开 , 即可支持多个欢迎语打字效果。
- GREETING_WORDS: process.env.NEXT_PUBLIC_GREETING_WORDS || 'Hi,我是一个程序员, Hi,我是一个打工人,Hi,我是一个干饭人,欢迎来到我的博客🎉',
+ GREETING_WORDS:
+ process.env.NEXT_PUBLIC_GREETING_WORDS ||
+ 'Hi,我是一个程序员, Hi,我是一个打工人,Hi,我是一个干饭人,欢迎来到我的博客🎉',
CUSTOM_MENU: process.env.NEXT_PUBLIC_CUSTOM_MENU || false, // 支持Menu 类型,从3.12.0版本起,各主题将逐步支持灵活的二级菜单配置,替代了原来的Page类型,此配置是试验功能、默认关闭。
@@ -90,7 +91,9 @@ const BLOG = {
'"Segoe UI Symbol"',
'"Apple Color Emoji"'
],
- FONT_AWESOME: process.env.NEXT_PUBLIC_FONT_AWESOME_PATH || 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css', // font-awesome 字体图标地址; 可选 /css/all.min.css , https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/font-awesome/6.0.0/css/all.min.css
+ FONT_AWESOME:
+ process.env.NEXT_PUBLIC_FONT_AWESOME_PATH ||
+ 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css', // font-awesome 字体图标地址; 可选 /css/all.min.css , https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/font-awesome/6.0.0/css/all.min.css
// END ************网站字体*****************
@@ -120,10 +123,13 @@ const BLOG = {
CAN_COPY: process.env.NEXT_PUBLIC_CAN_COPY || true, // 是否允许复制页面内容 默认允许,如果设置为false、则全栈禁止复制内容。
// 自定义右键菜单
CUSTOM_RIGHT_CLICK_CONTEXT_MENU: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU || true, // 自定义右键菜单,覆盖系统菜单
- CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH || true, // 是否显示切换主题
+ CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH:
+ process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH || true, // 是否显示切换主题
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_DARK_MODE: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_DARK_MODE || true, // 是否显示深色模式
- CUSTOM_RIGHT_CLICK_CONTEXT_MENU_SHARE_LINK: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_SHARE_LINK || true, // 是否显示分享链接
- CUSTOM_RIGHT_CLICK_CONTEXT_MENU_RANDOM_POST: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_RANDOM_POST || true, // 是否显示随机博客
+ CUSTOM_RIGHT_CLICK_CONTEXT_MENU_SHARE_LINK:
+ process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_SHARE_LINK || true, // 是否显示分享链接
+ CUSTOM_RIGHT_CLICK_CONTEXT_MENU_RANDOM_POST:
+ process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_RANDOM_POST || true, // 是否显示随机博客
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_CATEGORY: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_CATEGORY || true, // 是否显示分类
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_TAG: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_TAG || true, // 是否显示标签
@@ -148,10 +154,16 @@ const BLOG = {
PRISM_JS_AUTO_LOADER: 'https://npm.elemecdn.com/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js',
// 代码主题 @see https://github.com/PrismJS/prism-themes
- PRISM_THEME_PREFIX_PATH: process.env.NEXT_PUBLIC_PRISM_THEME_PREFIX_PATH || 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-okaidia.css', // 代码块默认主题
+ PRISM_THEME_PREFIX_PATH:
+ process.env.NEXT_PUBLIC_PRISM_THEME_PREFIX_PATH ||
+ 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-okaidia.css', // 代码块默认主题
PRISM_THEME_SWITCH: process.env.NEXT_PUBLIC_PRISM_THEME_SWITCH || true, // 是否开启浅色/深色模式代码主题切换; 开启后将显示以下两个主题
- PRISM_THEME_LIGHT_PATH: process.env.NEXT_PUBLIC_PRISM_THEME_LIGHT_PATH || 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-solarizedlight.css', // 浅色模式主题
- PRISM_THEME_DARK_PATH: process.env.NEXT_PUBLIC_PRISM_THEME_DARK_PATH || 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-okaidia.min.css', // 深色模式主题
+ PRISM_THEME_LIGHT_PATH:
+ process.env.NEXT_PUBLIC_PRISM_THEME_LIGHT_PATH ||
+ 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-solarizedlight.css', // 浅色模式主题
+ PRISM_THEME_DARK_PATH:
+ process.env.NEXT_PUBLIC_PRISM_THEME_DARK_PATH ||
+ 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-okaidia.min.css', // 深色模式主题
CODE_MAC_BAR: process.env.NEXT_PUBLIC_CODE_MAC_BAR || true, // 代码左上角显示mac的红黄绿图标
CODE_LINE_NUMBERS: process.env.NEXT_PUBLIC_CODE_LINE_NUMBERS || false, // 是否显示行号
@@ -161,16 +173,20 @@ const BLOG = {
// END********代码相关********
// Mermaid 图表CDN
- MERMAID_CDN: process.env.NEXT_PUBLIC_MERMAID_CDN || 'https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.2.4/mermaid.min.js', // CDN
+ MERMAID_CDN:
+ process.env.NEXT_PUBLIC_MERMAID_CDN || 'https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.2.4/mermaid.min.js', // CDN
// QRCodeCDN
- QR_CODE_CDN: process.env.NEXT_PUBLIC_QR_CODE_CDN || 'https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js',
+ QR_CODE_CDN:
+ process.env.NEXT_PUBLIC_QR_CODE_CDN || 'https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js',
BACKGROUND_LIGHT: '#eeeeee', // use hex value, don't forget '#' e.g #fffefc
BACKGROUND_DARK: '#000000', // use hex value, don't forget '#'
SUB_PATH: '', // leave this empty unless you want to deploy in a folder
POST_SHARE_BAR_ENABLE: process.env.NEXT_PUBLIC_POST_SHARE_BAR || 'true', // 文章分享功能 ,将在底部显示一个分享条
- POSTS_SHARE_SERVICES: process.env.NEXT_PUBLIC_POST_SHARE_SERVICES || 'link,wechat,qq,weibo,email,facebook,twitter,telegram,messenger,line,reddit,whatsapp,linkedin', // 分享的服務,按顺序显示,逗号隔开
+ POSTS_SHARE_SERVICES:
+ process.env.NEXT_PUBLIC_POST_SHARE_SERVICES ||
+ 'link,wechat,qq,weibo,email,facebook,twitter,telegram,messenger,line,reddit,whatsapp,linkedin', // 分享的服務,按顺序显示,逗号隔开
// 所有支持的分享服务:link(复制链接),wechat(微信),qq,weibo(微博),email(邮件),facebook,twitter,telegram,messenger,line,reddit,whatsapp,linkedin,vkshare,okshare,tumblr,livejournal,mailru,viber,workplace,pocket,instapaper,hatena
POST_URL_PREFIX: process.env.NEXT_PUBLIC_POST_URL_PREFIX || 'article',
@@ -181,9 +197,9 @@ const BLOG = {
POST_LIST_STYLE: process.env.NEXT_PUBLIC_POST_LIST_STYLE || 'page', // ['page','scroll] 文章列表样式:页码分页、单页滚动加载
POST_LIST_PREVIEW: process.env.NEXT_PUBLIC_POST_PREVIEW || 'false', // 是否在列表加载文章预览
- POST_PREVIEW_LINES: 12, // 预览博客行数
- POST_RECOMMEND_COUNT: 6, // 推荐文章数量
- POSTS_PER_PAGE: 12, // post counts per page
+ POST_PREVIEW_LINES: process.env.NEXT_PUBLIC_POST_POST_PREVIEW_LINES || 12, // 预览博客行数
+ POST_RECOMMEND_COUNT: process.env.NEXT_PUBLIC_POST_RECOMMEND_COUNT || 6, // 推荐文章数量
+ POSTS_PER_PAGE: process.env.NEXT_PUBLIC_POST_PER_PAGE || 12, // post counts per page
POSTS_SORT_BY: process.env.NEXT_PUBLIC_POST_SORT_BY || 'notion', // 排序方式 'date'按时间,'notion'由notion控制
POST_WAITING_TIME_FOR_404: process.env.NEXT_PUBLIC_POST_WAITING_TIME_FOR_404 || '8', // 文章加载超时时间,单位秒;超时后跳转到404页面
@@ -203,12 +219,7 @@ const BLOG = {
// 鼠标点击烟花特效
FIREWORKS: process.env.NEXT_PUBLIC_FIREWORKS || false, // 开关
// 烟花色彩,感谢 https://github.com/Vixcity 提交的色彩
- FIREWORKS_COLOR: [
- '255, 20, 97',
- '24, 255, 146',
- '90, 135, 255',
- '251, 243, 140'
- ],
+ FIREWORKS_COLOR: ['255, 20, 97', '24, 255, 146', '90, 135, 255', '251, 243, 140'],
// 樱花飘落特效
SAKURA: process.env.NEXT_PUBLIC_SAKURA || false, // 开关
@@ -223,8 +234,11 @@ const BLOG = {
// ********挂件组件相关********
// AI 文章摘要生成 @see https://docs_s.tianli0.top/
- TianliGPT_CSS: process.env.NEXT_PUBLIC_TIANLI_GPT_CSS || 'https://cdn1.tianli0.top/gh/zhheo/Post-Abstract-AI@0.15.2/tianli_gpt.css',
- TianliGPT_JS: process.env.NEXT_PUBLIC_TIANLI_GPT_JS || 'https://cdn1.tianli0.top/gh/zhheo/Post-Abstract-AI@0.15.2/tianli_gpt.js',
+ TianliGPT_CSS:
+ process.env.NEXT_PUBLIC_TIANLI_GPT_CSS ||
+ 'https://cdn1.tianli0.top/gh/zhheo/Post-Abstract-AI@0.15.2/tianli_gpt.css',
+ TianliGPT_JS:
+ process.env.NEXT_PUBLIC_TIANLI_GPT_JS || 'https://cdn1.tianli0.top/gh/zhheo/Post-Abstract-AI@0.15.2/tianli_gpt.js',
TianliGPT_KEY: process.env.NEXT_PUBLIC_TIANLI_GPT_KEY || '',
// Chatbase 是否显示chatbase机器人 https://www.chatbase.co/
@@ -239,19 +253,18 @@ const BLOG = {
// 悬浮挂件
WIDGET_PET: process.env.NEXT_PUBLIC_WIDGET_PET || true, // 是否显示宠物挂件
WIDGET_PET_LINK:
- process.env.NEXT_PUBLIC_WIDGET_PET_LINK ||
- 'https://cdn.jsdelivr.net/npm/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json', // 挂件模型地址 @see https://github.com/xiazeyu/live2d-widget-models
+ process.env.NEXT_PUBLIC_WIDGET_PET_LINK ||
+ 'https://cdn.jsdelivr.net/npm/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json', // 挂件模型地址 @see https://github.com/xiazeyu/live2d-widget-models
WIDGET_PET_SWITCH_THEME: process.env.NEXT_PUBLIC_WIDGET_PET_SWITCH_THEME || true, // 点击宠物挂件切换博客主题
// 音乐播放插件
MUSIC_PLAYER: process.env.NEXT_PUBLIC_MUSIC_PLAYER || false, // 是否使用音乐播放插件
MUSIC_PLAYER_VISIBLE: process.env.NEXT_PUBLIC_MUSIC_PLAYER_VISIBLE || true, // 是否在左下角显示播放和切换,如果使用播放器,打开自动播放再隐藏,就会以类似背景音乐的方式播放,无法取消和暂停
- MUSIC_PLAYER_AUTO_PLAY:
- process.env.NEXT_PUBLIC_MUSIC_PLAYER_AUTO_PLAY || true, // 是否自动播放,不过自动播放时常不生效(移动设备不支持自动播放)
+ MUSIC_PLAYER_AUTO_PLAY: process.env.NEXT_PUBLIC_MUSIC_PLAYER_AUTO_PLAY || true, // 是否自动播放,不过自动播放时常不生效(移动设备不支持自动播放)
MUSIC_PLAYER_LRC_TYPE: process.env.NEXT_PUBLIC_MUSIC_PLAYER_LRC_TYPE || '0', // 歌词显示类型,可选值: 3 | 1 | 0(0:禁用 lrc 歌词,1:lrc 格式的字符串,3:lrc 文件 url)(前提是有配置歌词路径,对 meting 无效)
MUSIC_PLAYER_CDN_URL:
- process.env.NEXT_PUBLIC_MUSIC_PLAYER_CDN_URL ||
- 'https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/aplayer/1.10.1/APlayer.min.js',
+ process.env.NEXT_PUBLIC_MUSIC_PLAYER_CDN_URL ||
+ 'https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/aplayer/1.10.1/APlayer.min.js',
MUSIC_PLAYER_ORDER: process.env.NEXT_PUBLIC_MUSIC_PLAYER_ORDER || 'list', // 默认播放方式,顺序 list,随机 random
MUSIC_PLAYER_AUDIO_LIST: [
// 示例音乐列表。除了以下配置外,还可配置歌词,具体配置项看此文档 https://aplayer.js.org/#/zh-Hans/
@@ -259,24 +272,19 @@ const BLOG = {
name: '风を共に舞う気持ち',
artist: 'Falcom Sound Team jdk',
url: 'https://music.163.com/song/media/outer/url?id=731419.mp3',
- cover:
- 'https://p2.music.126.net/kn6ugISTonvqJh3LHLaPtQ==/599233837187278.jpg'
+ cover: 'https://p2.music.126.net/kn6ugISTonvqJh3LHLaPtQ==/599233837187278.jpg'
},
{
name: '王都グランセル',
artist: 'Falcom Sound Team jdk',
url: 'https://music.163.com/song/media/outer/url?id=731355.mp3',
- cover:
- 'https://p1.music.126.net/kn6ugISTonvqJh3LHLaPtQ==/599233837187278.jpg'
+ cover: 'https://p1.music.126.net/kn6ugISTonvqJh3LHLaPtQ==/599233837187278.jpg'
}
],
MUSIC_PLAYER_METING: process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING || false, // 是否要开启 MetingJS,从平台获取歌单。会覆盖自定义的 MUSIC_PLAYER_AUDIO_LIST,更多配置信息:https://github.com/metowolf/MetingJS
- MUSIC_PLAYER_METING_SERVER:
- process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_SERVER || 'netease', // 音乐平台,[netease, tencent, kugou, xiami, baidu]
- MUSIC_PLAYER_METING_ID:
- process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_ID || '60198', // 对应歌单的 id
- MUSIC_PLAYER_METING_LRC_TYPE:
- process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_LRC_TYPE || '1', // 可选值: 3 | 1 | 0(0:禁用 lrc 歌词,1:lrc 格式的字符串,3:lrc 文件 url)
+ MUSIC_PLAYER_METING_SERVER: process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_SERVER || 'netease', // 音乐平台,[netease, tencent, kugou, xiami, baidu]
+ MUSIC_PLAYER_METING_ID: process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_ID || '60198', // 对应歌单的 id
+ MUSIC_PLAYER_METING_LRC_TYPE: process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_LRC_TYPE || '1', // 可选值: 3 | 1 | 0(0:禁用 lrc 歌词,1:lrc 格式的字符串,3:lrc 文件 url)
// ********挂件组件相关********
// ----> 评论互动 可同时开启多个支持 WALINE VALINE GISCUS CUSDIS UTTERRANCES GITALK
@@ -285,55 +293,47 @@ const BLOG = {
// artalk 评论插件
COMMENT_ARTALK_SERVER: process.env.NEXT_PUBLIC_COMMENT_ARTALK_SERVER || '', // ArtalkServert后端地址 https://artalk.js.org/guide/deploy.html
- COMMENT_ARTALK_JS: process.env.NEXT_PUBLIC_COMMENT_ARTALK_JS || 'https://cdnjs.cloudflare.com/ajax/libs/artalk/2.5.5/Artalk.js', // ArtalkServert js cdn
- COMMENT_ARTALK_CSS: process.env.NEXT_PUBLIC_COMMENT_ARTALK_CSS || 'https://cdnjs.cloudflare.com/ajax/libs/artalk/2.5.5/Artalk.css', // ArtalkServert css cdn
+ COMMENT_ARTALK_JS:
+ process.env.NEXT_PUBLIC_COMMENT_ARTALK_JS || 'https://cdnjs.cloudflare.com/ajax/libs/artalk/2.5.5/Artalk.js', // ArtalkServert js cdn
+ COMMENT_ARTALK_CSS:
+ process.env.NEXT_PUBLIC_COMMENT_ARTALK_CSS || 'https://cdnjs.cloudflare.com/ajax/libs/artalk/2.5.5/Artalk.css', // ArtalkServert css cdn
// twikoo
COMMENT_TWIKOO_ENV_ID: process.env.NEXT_PUBLIC_COMMENT_ENV_ID || '', // TWIKOO后端地址 腾讯云环境填envId;Vercel环境填域名,教程:https://tangly1024.com/article/notionnext-twikoo
COMMENT_TWIKOO_COUNT_ENABLE: process.env.NEXT_PUBLIC_COMMENT_TWIKOO_COUNT_ENABLE || false, // 博客列表是否显示评论数
- COMMENT_TWIKOO_CDN_URL: process.env.NEXT_PUBLIC_COMMENT_TWIKOO_CDN_URL || 'https://cdn.staticfile.org/twikoo/1.6.17/twikoo.min.js', // twikoo客户端cdn
+ COMMENT_TWIKOO_CDN_URL:
+ process.env.NEXT_PUBLIC_COMMENT_TWIKOO_CDN_URL || 'https://cdn.staticfile.org/twikoo/1.6.17/twikoo.min.js', // twikoo客户端cdn
// utterance
- COMMENT_UTTERRANCES_REPO:
- process.env.NEXT_PUBLIC_COMMENT_UTTERRANCES_REPO || '', // 你的代码仓库名, 例如我是 'tangly1024/NotionNext'; 更多文档参考 https://utteranc.es/
+ COMMENT_UTTERRANCES_REPO: process.env.NEXT_PUBLIC_COMMENT_UTTERRANCES_REPO || '', // 你的代码仓库名, 例如我是 'tangly1024/NotionNext'; 更多文档参考 https://utteranc.es/
// giscus @see https://giscus.app/
COMMENT_GISCUS_REPO: process.env.NEXT_PUBLIC_COMMENT_GISCUS_REPO || '', // 你的Github仓库名 e.g 'tangly1024/NotionNext'
COMMENT_GISCUS_REPO_ID: process.env.NEXT_PUBLIC_COMMENT_GISCUS_REPO_ID || '', // 你的Github Repo ID e.g ( 設定完 giscus 即可看到 )
- COMMENT_GISCUS_CATEGORY_ID:
- process.env.NEXT_PUBLIC_COMMENT_GISCUS_CATEGORY_ID || '', // 你的Github Discussions 內的 Category ID ( 設定完 giscus 即可看到 )
- COMMENT_GISCUS_MAPPING:
- process.env.NEXT_PUBLIC_COMMENT_GISCUS_MAPPING || 'pathname', // 你的Github Discussions 使用哪種方式來標定文章, 預設 'pathname'
- COMMENT_GISCUS_REACTIONS_ENABLED:
- process.env.NEXT_PUBLIC_COMMENT_GISCUS_REACTIONS_ENABLED || '1', // 你的 Giscus 是否開啟文章表情符號 '1' 開啟 "0" 關閉 預設開啟
- COMMENT_GISCUS_EMIT_METADATA:
- process.env.NEXT_PUBLIC_COMMENT_GISCUS_EMIT_METADATA || '0', // 你的 Giscus 是否提取 Metadata '1' 開啟 '0' 關閉 預設關閉
- COMMENT_GISCUS_INPUT_POSITION:
- process.env.NEXT_PUBLIC_COMMENT_GISCUS_INPUT_POSITION || 'bottom', // 你的 Giscus 發表留言位置 'bottom' 尾部 'top' 頂部, 預設 'bottom'
+ COMMENT_GISCUS_CATEGORY_ID: process.env.NEXT_PUBLIC_COMMENT_GISCUS_CATEGORY_ID || '', // 你的Github Discussions 內的 Category ID ( 設定完 giscus 即可看到 )
+ COMMENT_GISCUS_MAPPING: process.env.NEXT_PUBLIC_COMMENT_GISCUS_MAPPING || 'pathname', // 你的Github Discussions 使用哪種方式來標定文章, 預設 'pathname'
+ COMMENT_GISCUS_REACTIONS_ENABLED: process.env.NEXT_PUBLIC_COMMENT_GISCUS_REACTIONS_ENABLED || '1', // 你的 Giscus 是否開啟文章表情符號 '1' 開啟 "0" 關閉 預設開啟
+ COMMENT_GISCUS_EMIT_METADATA: process.env.NEXT_PUBLIC_COMMENT_GISCUS_EMIT_METADATA || '0', // 你的 Giscus 是否提取 Metadata '1' 開啟 '0' 關閉 預設關閉
+ COMMENT_GISCUS_INPUT_POSITION: process.env.NEXT_PUBLIC_COMMENT_GISCUS_INPUT_POSITION || 'bottom', // 你的 Giscus 發表留言位置 'bottom' 尾部 'top' 頂部, 預設 'bottom'
COMMENT_GISCUS_LANG: process.env.NEXT_PUBLIC_COMMENT_GISCUS_LANG || 'zh-CN', // 你的 Giscus 語言 e.g 'en', 'zh-TW', 'zh-CN', 預設 'en'
- COMMENT_GISCUS_LOADING:
- process.env.NEXT_PUBLIC_COMMENT_GISCUS_LOADING || 'lazy', // 你的 Giscus 載入是否漸進式載入, 預設 'lazy'
- COMMENT_GISCUS_CROSSORIGIN:
- process.env.NEXT_PUBLIC_COMMENT_GISCUS_CROSSORIGIN || 'anonymous', // 你的 Giscus 可以跨網域, 預設 'anonymous'
+ COMMENT_GISCUS_LOADING: process.env.NEXT_PUBLIC_COMMENT_GISCUS_LOADING || 'lazy', // 你的 Giscus 載入是否漸進式載入, 預設 'lazy'
+ COMMENT_GISCUS_CROSSORIGIN: process.env.NEXT_PUBLIC_COMMENT_GISCUS_CROSSORIGIN || 'anonymous', // 你的 Giscus 可以跨網域, 預設 'anonymous'
COMMENT_CUSDIS_APP_ID: process.env.NEXT_PUBLIC_COMMENT_CUSDIS_APP_ID || '', // data-app-id 36位 see https://cusdis.com/
- COMMENT_CUSDIS_HOST:
- process.env.NEXT_PUBLIC_COMMENT_CUSDIS_HOST || 'https://cusdis.com', // data-host, change this if you're using self-hosted version
- COMMENT_CUSDIS_SCRIPT_SRC:
- process.env.NEXT_PUBLIC_COMMENT_CUSDIS_SCRIPT_SRC ||
- '/js/cusdis.es.js', // change this if you're using self-hosted version
+ COMMENT_CUSDIS_HOST: process.env.NEXT_PUBLIC_COMMENT_CUSDIS_HOST || 'https://cusdis.com', // data-host, change this if you're using self-hosted version
+ COMMENT_CUSDIS_SCRIPT_SRC: process.env.NEXT_PUBLIC_COMMENT_CUSDIS_SCRIPT_SRC || '/js/cusdis.es.js', // change this if you're using self-hosted version
// gitalk评论插件 更多参考 https://gitalk.github.io/
COMMENT_GITALK_REPO: process.env.NEXT_PUBLIC_COMMENT_GITALK_REPO || '', // 你的Github仓库名,例如 'NotionNext'
COMMENT_GITALK_OWNER: process.env.NEXT_PUBLIC_COMMENT_GITALK_OWNER || '', // 你的用户名 e.g tangly1024
COMMENT_GITALK_ADMIN: process.env.NEXT_PUBLIC_COMMENT_GITALK_ADMIN || '', // 管理员用户名、一般是自己 e.g 'tangly1024'
- COMMENT_GITALK_CLIENT_ID:
- process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_ID || '', // e.g 20位ID , 在gitalk后台获取
- COMMENT_GITALK_CLIENT_SECRET:
- process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_SECRET || '', // e.g 40位ID, 在gitalk后台获取
+ COMMENT_GITALK_CLIENT_ID: process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_ID || '', // e.g 20位ID , 在gitalk后台获取
+ COMMENT_GITALK_CLIENT_SECRET: process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_SECRET || '', // e.g 40位ID, 在gitalk后台获取
COMMENT_GITALK_DISTRACTION_FREE_MODE: false, // 类似facebook的无干扰模式
- COMMENT_GITALK_JS_CDN_URL: process.env.NEXT_PUBLIC_COMMENT_GITALK_JS_CDN_URL || 'https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.min.js', // gitalk客户端 js cdn
- COMMENT_GITALK_CSS_CDN_URL: process.env.NEXT_PUBLIC_COMMENT_GITALK_CSS_CDN_URL || 'https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css', // gitalk客户端 css cdn
+ COMMENT_GITALK_JS_CDN_URL:
+ process.env.NEXT_PUBLIC_COMMENT_GITALK_JS_CDN_URL || 'https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.min.js', // gitalk客户端 js cdn
+ COMMENT_GITALK_CSS_CDN_URL:
+ process.env.NEXT_PUBLIC_COMMENT_GITALK_CSS_CDN_URL || 'https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css', // gitalk客户端 css cdn
COMMENT_GITTER_ROOM: process.env.NEXT_PUBLIC_COMMENT_GITTER_ROOM || '', // gitter聊天室 see https://gitter.im/ 不需要则留空
COMMENT_DAO_VOICE_ID: process.env.NEXT_PUBLIC_COMMENT_DAO_VOICE_ID || '', // DaoVoice http://dashboard.daovoice.io/get-started
@@ -343,8 +343,7 @@ const BLOG = {
COMMENT_VALINE_APP_ID: process.env.NEXT_PUBLIC_VALINE_ID || '', // Valine @see https://valine.js.org/quickstart.html 或 https://github.com/stonehank/react-valine#%E8%8E%B7%E5%8F%96app-id-%E5%92%8C-app-key
COMMENT_VALINE_APP_KEY: process.env.NEXT_PUBLIC_VALINE_KEY || '',
COMMENT_VALINE_SERVER_URLS: process.env.NEXT_PUBLIC_VALINE_SERVER_URLS || '', // 该配置适用于国内自定义域名用户, 海外版本会自动检测(无需手动填写) @see https://valine.js.org/configuration.html#serverURLs
- COMMENT_VALINE_PLACEHOLDER:
- process.env.NEXT_PUBLIC_VALINE_PLACEHOLDER || '抢个沙发吧~', // 可以搭配后台管理评论 https://github.com/DesertsP/Valine-Admin 便于查看评论,以及邮件通知,垃圾评论过滤等功能
+ COMMENT_VALINE_PLACEHOLDER: process.env.NEXT_PUBLIC_VALINE_PLACEHOLDER || '抢个沙发吧~', // 可以搭配后台管理评论 https://github.com/DesertsP/Valine-Admin 便于查看评论,以及邮件通知,垃圾评论过滤等功能
COMMENT_WALINE_SERVER_URL: process.env.NEXT_PUBLIC_WALINE_SERVER_URL || '', // 请配置完整的Waline评论地址 例如 hhttps://preview-waline.tangly1024.com @see https://waline.js.org/guide/get-started.html
COMMENT_WALINE_RECENT: process.env.NEXT_PUBLIC_WALINE_RECENT || false, // 最新评论
@@ -383,11 +382,9 @@ const BLOG = {
ANALYTICS_ACKEE_DATA_SERVER: process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_DATA_SERVER || '', // e.g https://ackee.tangly1024.com , don't end with a slash
ANALYTICS_ACKEE_DOMAIN_ID: process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_DOMAIN_ID || '', // e.g '82e51db6-dec2-423a-b7c9-b4ff7ebb3302'
- SEO_GOOGLE_SITE_VERIFICATION:
- process.env.NEXT_PUBLIC_SEO_GOOGLE_SITE_VERIFICATION || '', // Remove the value or replace it with your own google site verification code
+ SEO_GOOGLE_SITE_VERIFICATION: process.env.NEXT_PUBLIC_SEO_GOOGLE_SITE_VERIFICATION || '', // Remove the value or replace it with your own google site verification code
- SEO_BAIDU_SITE_VERIFICATION:
- process.env.NEXT_PUBLIC_SEO_BAIDU_SITE_VERIFICATION || '', // Remove the value or replace it with your own google site verification code
+ SEO_BAIDU_SITE_VERIFICATION: process.env.NEXT_PUBLIC_SEO_BAIDU_SITE_VERIFICATION || '', // Remove the value or replace it with your own google site verification code
// 微软 Clarity 站点分析
CLARITY_ID: process.env.NEXT_PUBLIC_CLARITY_ID || null, // 只需要复制Clarity脚本中的ID部分,ID是一个十位的英文数字组合
@@ -416,23 +413,20 @@ const BLOG = {
type: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE || 'type', // 文章类型,
type_post: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_POST || 'Post', // 当type文章类型与此值相同时,为博文。
type_page: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_PAGE || 'Page', // 当type文章类型与此值相同时,为单页。
- type_notice:
- process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_NOTICE || 'Notice', // 当type文章类型与此值相同时,为公告。
+ type_notice: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_NOTICE || 'Notice', // 当type文章类型与此值相同时,为公告。
type_menu: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_MENU || 'Menu', // 当type文章类型与此值相同时,为菜单。
- type_sub_menu:
- process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_SUB_MENU || 'SubMenu', // 当type文章类型与此值相同时,为子菜单。
+ type_sub_menu: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_SUB_MENU || 'SubMenu', // 当type文章类型与此值相同时,为子菜单。
title: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TITLE || 'title', // 文章标题
status: process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS || 'status',
- status_publish:
- process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS_PUBLISH || 'Published', // 当status状态值与此相同时为发布,可以为中文
- status_invisible:
- process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS_INVISIBLE || 'Invisible', // 当status状态值与此相同时为隐藏发布,可以为中文 , 除此之外其他页面状态不会显示在博客上
+ status_publish: process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS_PUBLISH || 'Published', // 当status状态值与此相同时为发布,可以为中文
+ status_invisible: process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS_INVISIBLE || 'Invisible', // 当status状态值与此相同时为隐藏发布,可以为中文 , 除此之外其他页面状态不会显示在博客上
summary: process.env.NEXT_PUBLIC_NOTION_PROPERTY_SUMMARY || 'summary',
slug: process.env.NEXT_PUBLIC_NOTION_PROPERTY_SLUG || 'slug',
category: process.env.NEXT_PUBLIC_NOTION_PROPERTY_CATEGORY || 'category',
date: process.env.NEXT_PUBLIC_NOTION_PROPERTY_DATE || 'date',
tags: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TAGS || 'tags',
- icon: process.env.NEXT_PUBLIC_NOTION_PROPERTY_ICON || 'icon'
+ icon: process.env.NEXT_PUBLIC_NOTION_PROPERTY_ICON || 'icon',
+ ext: process.env.NEXT_PUBLIC_NOTION_PROPERTY_EXT || 'ext' // 扩展字段,存放json-string,用于复杂业务
},
// RSS订阅
@@ -441,10 +435,14 @@ const BLOG = {
MAILCHIMP_API_KEY: process.env.MAILCHIMP_API_KEY || null, // 开启mailichimp邮件订阅 APIkey
// ANIMATE.css 动画
- ANIMATE_CSS_URL: process.env.NEXT_PUBLIC_ANIMATE_CSS_URL || 'https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css', // 动画CDN
+ ANIMATE_CSS_URL:
+ process.env.NEXT_PUBLIC_ANIMATE_CSS_URL ||
+ 'https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css', // 动画CDN
// 网站图片
- IMG_LAZY_LOAD_PLACEHOLDER: process.env.NEXT_PUBLIC_IMG_LAZY_LOAD_PLACEHOLDER || 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==', // 懒加载占位图片地址,支持base64或url
+ IMG_LAZY_LOAD_PLACEHOLDER:
+ process.env.NEXT_PUBLIC_IMG_LAZY_LOAD_PLACEHOLDER ||
+ 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==', // 懒加载占位图片地址,支持base64或url
IMG_URL_TYPE: process.env.NEXT_PUBLIC_IMG_TYPE || 'Notion', // 此配置已失效,请勿使用;AMAZON方案不再支持,仅支持Notion方案。 ['Notion','AMAZON'] 站点图片前缀 默认 Notion:(https://notion.so/images/xx) , AMAZON(https://s3.us-west-2.amazonaws.com/xxx)
IMG_SHADOW: process.env.NEXT_PUBLIC_IMG_SHADOW || false, // 文章图片是否自动添加阴影
IMG_COMPRESS_WIDTH: process.env.NEXT_PUBLIC_IMG_COMPRESS_WIDTH || 800, // Notion图片压缩宽度
@@ -452,15 +450,16 @@ const BLOG = {
// 作废配置
AVATAR: process.env.NEXT_PUBLIC_AVATAR || '/avatar.svg', // 作者头像,被notion中的ICON覆盖。若无ICON则取public目录下的avatar.png
TITLE: process.env.NEXT_PUBLIC_TITLE || 'NotionNext BLOG', // 站点标题 ,被notion中的页面标题覆盖;此处请勿留空白,否则服务器无法编译
- HOME_BANNER_IMAGE:
- process.env.NEXT_PUBLIC_HOME_BANNER_IMAGE || '/bg_image.jpg', // 首页背景大图, 会被notion中的封面图覆盖,若无封面图则会使用代码中的 /public/bg_image.jpg 文件
- DESCRIPTION:
- process.env.NEXT_PUBLIC_DESCRIPTION || '这是一个由NotionNext生成的站点', // 站点描述,被notion中的页面描述覆盖
+ HOME_BANNER_IMAGE: process.env.NEXT_PUBLIC_HOME_BANNER_IMAGE || '/bg_image.jpg', // 首页背景大图, 会被notion中的封面图覆盖,若无封面图则会使用代码中的 /public/bg_image.jpg 文件
+ DESCRIPTION: process.env.NEXT_PUBLIC_DESCRIPTION || '这是一个由NotionNext生成的站点', // 站点描述,被notion中的页面描述覆盖
// 开发相关
NOTION_ACCESS_TOKEN: process.env.NOTION_ACCESS_TOKEN || '', // Useful if you prefer not to make your database public
DEBUG: process.env.NEXT_PUBLIC_DEBUG || false, // 是否显示调试按钮
- ENABLE_CACHE: process.env.ENABLE_CACHE || process.env.npm_lifecycle_event === 'build' || process.env.npm_lifecycle_event === 'export', // 在打包过程中默认开启缓存,开发或运行时开启此功能意义不大。
+ ENABLE_CACHE:
+ process.env.ENABLE_CACHE ||
+ process.env.npm_lifecycle_event === 'build' ||
+ process.env.npm_lifecycle_event === 'export', // 在打包过程中默认开启缓存,开发或运行时开启此功能意义不大。
isProd: process.env.VERCEL_ENV === 'production', // distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables) isProd: process.env.VERCEL_ENV === 'production' // distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables)
BUNDLE_ANALYZER: process.env.ANALYZE === 'true' || false, // 是否展示编译依赖内容与大小
VERSION: process.env.NEXT_PUBLIC_VERSION // 版本号
diff --git a/components/Draggable.js b/components/Draggable.js
index 44d565b8..cf7be63a 100644
--- a/components/Draggable.js
+++ b/components/Draggable.js
@@ -1,10 +1,10 @@
-import { useRef, useEffect, useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
+
/**
* 可拖拽组件
*/
-
-export const Draggable = (props) => {
- const { children } = props
+export const Draggable = props => {
+ const { children, stick } = props
const draggableRef = useRef(null)
const rafRef = useRef(null)
const [moving, setMoving] = useState(false)
@@ -14,8 +14,10 @@ export const Draggable = (props) => {
const draggableElements = document.getElementsByClassName('draggable')
// 标准化鼠标事件对象
- function e(event) { // 定义事件对象标准化函数
- if (!event) { // 兼容IE浏览器
+ function e(event) {
+ // 定义事件对象标准化函数
+ if (!event) {
+ // 兼容IE浏览器
event = window.event
event.target = event.srcElement
event.layerX = event.offsetX
@@ -40,9 +42,10 @@ export const Draggable = (props) => {
document.onmousedown = start
document.ontouchstart = start
- function start (event) { // 按下鼠标时,初始化处理
+ function start(event) {
+ // 按下鼠标时,初始化处理
if (!draggableElements) return
- event = e(event)// 获取标准事件对象
+ event = e(event) // 获取标准事件对象
for (const drag of draggableElements) {
// 判断鼠标点击的区域是否是拖拽框内
@@ -60,19 +63,20 @@ export const Draggable = (props) => {
offsetX = event.mx - currentObj.offsetLeft
offsetY = event.my - currentObj.offsetTop
- document.onmousemove = move// 注册鼠标移动事件处理函数
+ document.onmousemove = move // 注册鼠标移动事件处理函数
document.ontouchmove = move
- document.onmouseup = stop// 注册松开鼠标事件处理函数
+ document.onmouseup = stop // 注册松开鼠标事件处理函数
document.ontouchend = stop
}
}
- function move(event) { // 鼠标移动处理函数
+ function move(event) {
+ // 鼠标移动处理函数
event = e(event)
rafRef.current = requestAnimationFrame(() => updatePosition(event))
}
- const stop = (event) => {
+ const stop = event => {
event = e(event)
document.documentElement.style.overflow = 'auto' // 恢复默认的滚动行为
cancelAnimationFrame(rafRef.current)
@@ -80,7 +84,7 @@ export const Draggable = (props) => {
currentObj = document.ontouchmove = document.ontouchend = document.onmousemove = document.onmouseup = null
}
- const updatePosition = (event) => {
+ const updatePosition = event => {
if (currentObj) {
const left = event.mx - offsetX
const top = event.my - offsetY
@@ -120,15 +124,18 @@ export const Draggable = (props) => {
if (offsetTop < 0) {
drag.firstElementChild.style.top = 0
}
- if (offsetTop > (clientHeight - offsetHeight)) {
+ if (offsetTop > clientHeight - offsetHeight) {
drag.firstElementChild.style.top = clientHeight - offsetHeight + 'px'
}
if (offsetLeft < 0) {
drag.firstElementChild.style.left = 0
}
- if (offsetLeft > (clientWidth - offsetWidth)) {
+ if (offsetLeft > clientWidth - offsetWidth) {
drag.firstElementChild.style.left = clientWidth - offsetWidth + 'px'
}
+ if (stick === 'left') {
+ drag.firstElementChild.style.left = 0 + 'px'
+ }
}
}
@@ -142,9 +149,11 @@ export const Draggable = (props) => {
}
}, [])
- return
- {children}
-
+ return (
+
+ {children}
+
+ )
}
Draggable.defaultProps = { left: 0, top: 0 }
diff --git a/lib/config.js b/lib/config.js
index 25211f30..546597c2 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -17,7 +17,7 @@ import { deepClone } from './utils'
export const siteConfig = (key, defaultVal = null, extendConfig = null) => {
let global = null
try {
- const isClient = typeof window !== 'undefined';
+ const isClient = typeof window !== 'undefined'
// eslint-disable-next-line react-hooks/rules-of-hooks
global = isClient ? useGlobal() : {}
// eslint-disable-next-line react-hooks/rules-of-hooks
@@ -67,15 +67,19 @@ export const siteConfig = (key, defaultVal = null, extendConfig = null) => {
} else {
if (typeof val === 'string') {
if (val === 'true' || val === 'false') {
- return JSON.parse(val);
+ return JSON.parse(val)
}
- return val;
+ if (/^\d+$/.test(val)) {
+ // 如果是数字,使用parseFloat或者parseInt将字符串转换为数字
+ return parseInt(val)
+ }
+ return val
} else {
try {
- return JSON.parse(val);
+ return JSON.parse(val)
} catch (error) {
// 如果值是一个字符串但不是有效的 JSON 格式,直接返回字符串
- return val;
+ return val
}
}
}
diff --git a/lib/db/getSiteData.js b/lib/db/getSiteData.js
index 997a97c6..0abe35ef 100755
--- a/lib/db/getSiteData.js
+++ b/lib/db/getSiteData.js
@@ -1,18 +1,18 @@
import BLOG from '@/blog.config'
import { getDataFromCache, setDataToCache } from '@/lib/cache/cache_manager'
-import { getPostBlocks, getSingleBlock } from '@/lib/notion/getPostBlocks'
-import { idToUuid } from 'notion-utils'
-import { deepClone } from '@/lib/utils'
import { getAllCategories } from '@/lib/notion/getAllCategories'
import getAllPageIds from '@/lib/notion/getAllPageIds'
import { getAllTags } from '@/lib/notion/getAllTags'
-import getPageProperties from '@/lib/notion/getPageProperties'
-import { compressImage, mapImgUrl } from '@/lib/notion/mapImage'
import { getConfigMapFromConfigPage } from '@/lib/notion/getNotionConfig'
+import getPageProperties from '@/lib/notion/getPageProperties'
+import { getPostBlocks, getSingleBlock } from '@/lib/notion/getPostBlocks'
+import { compressImage, mapImgUrl } from '@/lib/notion/mapImage'
+import { deepClone } from '@/lib/utils'
+import { idToUuid } from 'notion-utils'
export { getAllTags } from '../notion/getAllTags'
-export { getPostBlocks } from '../notion/getPostBlocks'
export { getPost } from '../notion/getNotionPost'
+export { getPostBlocks } from '../notion/getPostBlocks'
/**
* 获取博客数据; 基于Notion实现
@@ -275,7 +275,8 @@ export function getNavPages({ allPages }) {
slug: item.slug,
pageIcon: item.pageIcon || '',
lastEditedDate: item.lastEditedDate,
- publishDate: item.publishDate
+ publishDate: item.publishDate,
+ ext: item.ext || {}
}))
}
diff --git a/lib/notion/getPageProperties.js b/lib/notion/getPageProperties.js
index 0a96ce1d..fe93146a 100644
--- a/lib/notion/getPageProperties.js
+++ b/lib/notion/getPageProperties.js
@@ -1,6 +1,6 @@
-import { getTextContent, getDateValue } from 'notion-utils'
-import { NotionAPI } from 'notion-client'
import BLOG from '@/blog.config'
+import { NotionAPI } from 'notion-client'
+import { getDateValue, getTextContent } from 'notion-utils'
import formatDate from '../utils/formatDate'
// import { createHash } from 'crypto'
import md5 from 'js-md5'
@@ -49,8 +49,7 @@ export default async function getPageProperties(id, value, schema, authToken, ta
if (rawUsers[i][0][1]) {
const userId = rawUsers[i][0]
const res = await api.getUsers(userId)
- const resValue =
- res?.recordMapWithRoles?.notion_user?.[userId[1]]?.value
+ const resValue = res?.recordMapWithRoles?.notion_user?.[userId[1]]?.value
const user = {
id: resValue?.id,
first_name: resValue?.given_name,
@@ -93,16 +92,17 @@ export default async function getPageProperties(id, value, schema, authToken, ta
properties.pageIcon = mapImgUrl(value?.format?.page_icon, value) ?? ''
properties.pageCover = mapImgUrl(value?.format?.page_cover, value) ?? ''
properties.pageCoverThumbnail = mapImgUrl(value?.format?.page_cover, value, 'block', 'pageCoverThumbnail') ?? ''
-
+ properties.ext = converToJSON(properties?.ext)
properties.content = value.content ?? []
- properties.tagItems = properties?.tags?.map(tag => {
- return { name: tag, color: tagOptions?.find(t => t.value === tag)?.color || 'gray' }
- }) || []
+ properties.tagItems =
+ properties?.tags?.map(tag => {
+ return { name: tag, color: tagOptions?.find(t => t.value === tag)?.color || 'gray' }
+ }) || []
delete properties.content
// 处理URL
if (properties.type === 'Post') {
- properties.slug = (BLOG.POST_URL_PREFIX) ? generateCustomizeUrl(properties) : (properties.slug ?? properties.id)
+ properties.slug = BLOG.POST_URL_PREFIX ? generateCustomizeUrl(properties) : properties.slug ?? properties.id
} else if (properties.type === 'Page') {
properties.slug = properties.slug ?? properties.id
} else if (properties.type === 'Menu' || properties.type === 'SubMenu') {
@@ -122,6 +122,24 @@ export default async function getPageProperties(id, value, schema, authToken, ta
return properties
}
+/**
+ * 字符串转json
+ * @param {*} str
+ * @returns
+ */
+function converToJSON(str) {
+ if (!str) {
+ return {}
+ }
+ // 使用正则表达式去除空格和换行符
+ try {
+ return JSON.parse(str.replace(/\s/g, ''))
+ } catch (error) {
+ console.warn('无效JSON', str)
+ return {}
+ }
+}
+
/**
* 映射用户自定义表头
*/
@@ -164,7 +182,7 @@ function generateCustomizeUrl(postProperties) {
const formatPostCreatedDate = new Date(postProperties?.publishDay)
fullPrefix += String(formatPostCreatedDate.getUTCDate()).padStart(2, 0)
} else if (pattern === '%slug%') {
- fullPrefix += (postProperties.slug ?? postProperties.id)
+ fullPrefix += postProperties.slug ?? postProperties.id
} else if (!pattern.includes('%')) {
fullPrefix += pattern
} else {
@@ -180,5 +198,5 @@ function generateCustomizeUrl(postProperties) {
if (fullPrefix.endsWith('/')) {
fullPrefix = fullPrefix.substring(0, fullPrefix.length - 1) // 去掉尾部部的"/"
}
- return `${fullPrefix}/${(postProperties.slug ?? postProperties.id)}`
+ return `${fullPrefix}/${postProperties.slug ?? postProperties.id}`
}
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 9f5891e2..48592fc1 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -1,5 +1,5 @@
// 封装异步加载资源的方法
-import { memo } from 'react'
+import { memo } from 'react';
/**
* 判断是否客户端
@@ -7,6 +7,22 @@ import { memo } from 'react'
*/
export const isBrowser = typeof window !== 'undefined'
+/**
+ * 打乱数组
+ * @param {*} array
+ * @returns
+ */
+export const shuffleArray = (array) => {
+ if (!array) {
+ return []
+ }
+ for (let i = array.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [array[i], array[j]] = [array[j], array[i]];
+ }
+ return array;
+}
+
/**
* google机器人
* @returns
diff --git a/package.json b/package.json
index 9dc7983b..c8affb1a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "notion-next",
- "version": "4.4.0",
+ "version": "4.4.1",
"homepage": "https://github.com/tangly1024/NotionNext.git",
"license": "MIT",
"repository": {
diff --git a/pages/[prefix]/[slug]/[...suffix].js b/pages/[prefix]/[slug]/[...suffix].js
index d34b296a..3ce0b105 100644
--- a/pages/[prefix]/[slug]/[...suffix].js
+++ b/pages/[prefix]/[slug]/[...suffix].js
@@ -1,9 +1,10 @@
import BLOG from '@/blog.config'
-import { getGlobalData, getPostBlocks, getPost } from '@/lib/db/getSiteData'
-import { idToUuid } from 'notion-utils'
-import Slug, { getRecommendPost } from '..'
+import { siteConfig } from '@/lib/config'
+import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData'
import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
import { checkContainHttp } from '@/lib/utils'
+import { idToUuid } from 'notion-utils'
+import Slug, { getRecommendPost } from '..'
/**
* 根据notion的slug访问页面
@@ -12,7 +13,7 @@ import { checkContainHttp } from '@/lib/utils'
* @returns
*/
const PrefixSlug = props => {
- return
+ return
}
/**
@@ -31,8 +32,11 @@ export async function getStaticPaths() {
const { allPages } = await getGlobalData({ from })
return {
- paths: allPages?.filter(row => checkSlug(row))
- .map(row => ({ params: { prefix: row.slug.split('/')[0], slug: row.slug.split('/')[1], suffix: row.slug.split('/').slice(1) } })),
+ paths: allPages
+ ?.filter(row => checkSlug(row))
+ .map(row => ({
+ params: { prefix: row.slug.split('/')[0], slug: row.slug.split('/')[1], suffix: row.slug.split('/').slice(1) }
+ })),
fallback: true
}
}
@@ -52,8 +56,8 @@ export async function getStaticProps({ params: { prefix, slug, suffix } }) {
const from = `slug-props-${fullSlug}`
const props = await getGlobalData({ from })
// 在列表内查找文章
- props.post = props?.allPages?.find((p) => {
- return (p.type.indexOf('Menu') < 0) && (p.slug === fullSlug || p.id === idToUuid(fullSlug))
+ props.post = props?.allPages?.find(p => {
+ return p.type.indexOf('Menu') < 0 && (p.slug === fullSlug || p.id === idToUuid(fullSlug))
})
// 处理非列表内文章的内信息
@@ -86,7 +90,7 @@ export async function getStaticProps({ params: { prefix, slug, suffix } }) {
const index = allPosts.indexOf(props.post)
props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
props.next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0]
- props.recommendPosts = getRecommendPost(props.post, allPosts, BLOG.POST_RECOMMEND_COUNT)
+ props.recommendPosts = getRecommendPost(props.post, allPosts, siteConfig('POST_RECOMMEND_COUNT'))
} else {
props.prev = null
props.next = null
diff --git a/pages/[prefix]/[slug]/index.js b/pages/[prefix]/[slug]/index.js
index 1cbfd0d9..147d9ecb 100644
--- a/pages/[prefix]/[slug]/index.js
+++ b/pages/[prefix]/[slug]/index.js
@@ -1,9 +1,10 @@
import BLOG from '@/blog.config'
-import { getGlobalData, getPostBlocks, getPost } from '@/lib/db/getSiteData'
-import { idToUuid } from 'notion-utils'
-import Slug, { getRecommendPost } from '..'
+import { siteConfig } from '@/lib/config'
+import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData'
import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
import { checkContainHttp } from '@/lib/utils'
+import { idToUuid } from 'notion-utils'
+import Slug, { getRecommendPost } from '..'
/**
* 根据notion的slug访问页面
@@ -12,7 +13,7 @@ import { checkContainHttp } from '@/lib/utils'
* @returns
*/
const PrefixSlug = props => {
- return
+ return
}
export async function getStaticPaths() {
@@ -25,7 +26,8 @@ export async function getStaticPaths() {
const from = 'slug-paths'
const { allPages } = await getGlobalData({ from })
- const paths = allPages?.filter(row => checkSlug(row))
+ const paths = allPages
+ ?.filter(row => checkSlug(row))
.map(row => ({ params: { prefix: row.slug.split('/')[0], slug: row.slug.split('/')[1] } }))
return {
paths: paths,
@@ -43,8 +45,8 @@ export async function getStaticProps({ params: { prefix, slug } }) {
const from = `slug-props-${fullSlug}`
const props = await getGlobalData({ from })
// 在列表内查找文章
- props.post = props?.allPages?.find((p) => {
- return (p.type.indexOf('Menu') < 0) && (p.slug === fullSlug || p.id === idToUuid(fullSlug))
+ props.post = props?.allPages?.find(p => {
+ return p.type.indexOf('Menu') < 0 && (p.slug === fullSlug || p.id === idToUuid(fullSlug))
})
// 处理非列表内文章的内信息
@@ -77,7 +79,7 @@ export async function getStaticProps({ params: { prefix, slug } }) {
const index = allPosts.indexOf(props.post)
props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
props.next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0]
- props.recommendPosts = getRecommendPost(props.post, allPosts, BLOG.POST_RECOMMEND_COUNT)
+ props.recommendPosts = getRecommendPost(props.post, allPosts, siteConfig('POST_RECOMMEND_COUNT'))
} else {
props.prev = null
props.next = null
diff --git a/pages/[prefix]/index.js b/pages/[prefix]/index.js
index 5fc25e18..567f0244 100644
--- a/pages/[prefix]/index.js
+++ b/pages/[prefix]/index.js
@@ -1,14 +1,14 @@
import BLOG from '@/blog.config'
-import { getGlobalData, getPostBlocks, getPost } from '@/lib/db/getSiteData'
-import { useEffect, useState } from 'react'
-import { idToUuid } from 'notion-utils'
-import { useRouter } from 'next/router'
+import { siteConfig } from '@/lib/config'
+import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData'
import { getPageTableOfContents } from '@/lib/notion/getPageTableOfContents'
+import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
+import { checkContainHttp } from '@/lib/utils'
import { getLayoutByTheme } from '@/themes/theme'
import md5 from 'js-md5'
-import { checkContainHttp } from '@/lib/utils'
-import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
-import { siteConfig } from '@/lib/config'
+import { useRouter } from 'next/router'
+import { idToUuid } from 'notion-utils'
+import { useEffect, useState } from 'react'
/**
* 根据notion的slug访问页面
@@ -25,7 +25,7 @@ const Slug = props => {
/**
* 验证文章密码
* @param {*} result
- */
+ */
const validPassword = passInput => {
const encrypt = md5(post.slug + passInput)
if (passInput && encrypt === post.password) {
@@ -43,7 +43,9 @@ const Slug = props => {
} else {
setLock(false)
if (!lock && post?.blockMap?.block) {
- post.content = Object.keys(post.blockMap.block).filter(key => post.blockMap.block[key]?.value?.parent_id === post.id)
+ post.content = Object.keys(post.blockMap.block).filter(
+ key => post.blockMap.block[key]?.value?.parent_id === post.id
+ )
post.toc = getPageTableOfContents(post, post.blockMap)
}
}
@@ -65,8 +67,7 @@ export async function getStaticPaths() {
const from = 'slug-paths'
const { allPages } = await getGlobalData({ from })
- const paths = allPages?.filter(row => checkSlug(row))
- .map(row => ({ params: { prefix: row.slug } }))
+ const paths = allPages?.filter(row => checkSlug(row)).map(row => ({ params: { prefix: row.slug } }))
return {
paths: paths,
fallback: true
@@ -83,8 +84,8 @@ export async function getStaticProps({ params: { prefix } }) {
const from = `slug-props-${fullSlug}`
const props = await getGlobalData({ from })
// 在列表内查找文章
- props.post = props?.allPages?.find((p) => {
- return (p.type.indexOf('Menu') < 0) && (p.slug === fullSlug || p.id === idToUuid(fullSlug))
+ props.post = props?.allPages?.find(p => {
+ return p.type.indexOf('Menu') < 0 && (p.slug === fullSlug || p.id === idToUuid(fullSlug))
})
// 处理非列表内文章的内信息
@@ -117,7 +118,7 @@ export async function getStaticProps({ params: { prefix } }) {
const index = allPosts.indexOf(props.post)
props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
props.next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0]
- props.recommendPosts = getRecommendPost(props.post, allPosts, BLOG.POST_RECOMMEND_COUNT)
+ props.recommendPosts = getRecommendPost(props.post, allPosts, siteConfig('POST_RECOMMEND_COUNT'))
} else {
props.prev = null
props.next = null
@@ -171,7 +172,7 @@ function checkSlug(row) {
if (slug.startsWith('/')) {
slug = slug.substring(1)
}
- return ((slug.match(/\//g) || []).length === 0 && !checkContainHttp(slug)) && row.type.indexOf('Menu') < 0
+ return (slug.match(/\//g) || []).length === 0 && !checkContainHttp(slug) && row.type.indexOf('Menu') < 0
}
export default Slug
diff --git a/pages/category/[category]/index.js b/pages/category/[category]/index.js
index 203d429a..718d2b42 100644
--- a/pages/category/[category]/index.js
+++ b/pages/category/[category]/index.js
@@ -1,9 +1,8 @@
-import { getGlobalData } from '@/lib/db/getSiteData'
-import React from 'react'
import BLOG from '@/blog.config'
-import { useRouter } from 'next/router'
-import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config'
+import { getGlobalData } from '@/lib/db/getSiteData'
+import { getLayoutByTheme } from '@/themes/theme'
+import { useRouter } from 'next/router'
/**
* 分类页
@@ -28,10 +27,10 @@ export async function getStaticProps({ params: { category } }) {
// 处理文章页数
props.postCount = props.posts.length
// 处理分页
- if (BLOG.POST_LIST_STYLE === 'scroll') {
+ if (siteConfig('POST_LIST_STYLE') === 'scroll') {
// 滚动列表 给前端返回所有数据
- } else if (BLOG.POST_LIST_STYLE === 'page') {
- props.posts = props.posts?.slice(0, BLOG.POSTS_PER_PAGE)
+ } else if (siteConfig('POST_LIST_STYLE') === 'page') {
+ props.posts = props.posts?.slice(0, siteConfig('POSTS_PER_PAGE'))
}
delete props.allPages
diff --git a/pages/category/[category]/page/[page].js b/pages/category/[category]/page/[page].js
index 97ddf764..212fa8fa 100644
--- a/pages/category/[category]/page/[page].js
+++ b/pages/category/[category]/page/[page].js
@@ -1,9 +1,8 @@
-import { getGlobalData } from '@/lib/db/getSiteData'
-import React from 'react'
import BLOG from '@/blog.config'
-import { useRouter } from 'next/router'
-import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config'
+import { getGlobalData } from '@/lib/db/getSiteData'
+import { getLayoutByTheme } from '@/themes/theme'
+import { useRouter } from 'next/router'
/**
* 分类页
@@ -23,11 +22,13 @@ export async function getStaticProps({ params: { category, page } }) {
let props = await getGlobalData({ from })
// 过滤状态类型
- props.posts = props.allPages?.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post.category && post.category.includes(category))
+ props.posts = props.allPages
+ ?.filter(page => page.type === 'Post' && page.status === 'Published')
+ .filter(post => post && post.category && post.category.includes(category))
// 处理文章页数
props.postCount = props.posts.length
// 处理分页
- props.posts = props.posts.slice(BLOG.POSTS_PER_PAGE * (page - 1), BLOG.POSTS_PER_PAGE * page)
+ props.posts = props.posts.slice(siteConfig('POSTS_PER_PAGE') * (page - 1), siteConfig('POSTS_PER_PAGE') * page)
delete props.allPages
props.page = page
@@ -47,10 +48,12 @@ export async function getStaticPaths() {
categoryOptions?.forEach(category => {
// 过滤状态类型
- const categoryPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post.category && post.category.includes(category.name))
+ const categoryPosts = allPages
+ ?.filter(page => page.type === 'Post' && page.status === 'Published')
+ .filter(post => post && post.category && post.category.includes(category.name))
// 处理文章页数
const postCount = categoryPosts.length
- const totalPages = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
+ const totalPages = Math.ceil(postCount / siteConfig('POSTS_PER_PAGE'))
if (totalPages > 1) {
for (let i = 1; i <= totalPages; i++) {
paths.push({ params: { category: category.name, page: '' + i } })
diff --git a/pages/index.js b/pages/index.js
index 451d8026..286b1b12 100644
--- a/pages/index.js
+++ b/pages/index.js
@@ -1,9 +1,9 @@
import BLOG from '@/blog.config'
-import { getGlobalData, getPostBlocks } from '@/lib/db/getSiteData'
-import { generateRss } from '@/lib/rss'
-import { generateRobotsTxt } from '@/lib/robots.txt'
-import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config'
+import { getGlobalData, getPostBlocks } from '@/lib/db/getSiteData'
+import { generateRobotsTxt } from '@/lib/robots.txt'
+import { generateRss } from '@/lib/rss'
+import { getLayoutByTheme } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
@@ -28,20 +28,20 @@ export async function getStaticProps() {
props.posts = props.allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
// 处理分页
- if (BLOG.POST_LIST_STYLE === 'scroll') {
+ if (siteConfig('POST_LIST_STYLE') === 'scroll') {
// 滚动列表默认给前端返回所有数据
- } else if (BLOG.POST_LIST_STYLE === 'page') {
- props.posts = props.posts?.slice(0, BLOG.POSTS_PER_PAGE)
+ } else if (siteConfig('POST_LIST_STYLE') === 'page') {
+ props.posts = props.posts?.slice(0, siteConfig('POSTS_PER_PAGE'))
}
// 预览文章内容
- if (BLOG.POST_LIST_PREVIEW === 'true') {
+ if (siteConfig('POST_LIST_PREVIEW')) {
for (const i in props.posts) {
const post = props.posts[i]
if (post.password && post.password !== '') {
continue
}
- post.blockMap = await getPostBlocks(post.id, 'slug', BLOG.POST_PREVIEW_LINES)
+ post.blockMap = await getPostBlocks(post.id, 'slug', siteConfig('POST_PREVIEW_LINES'))
}
}
diff --git a/pages/page/[page].js b/pages/page/[page].js
index 6033655f..9b1278b6 100644
--- a/pages/page/[page].js
+++ b/pages/page/[page].js
@@ -1,8 +1,8 @@
import BLOG from '@/blog.config'
-import { getGlobalData, getPostBlocks } from '@/lib/db/getSiteData'
-import { useRouter } from 'next/router'
-import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config'
+import { getGlobalData, getPostBlocks } from '@/lib/db/getSiteData'
+import { getLayoutByTheme } from '@/themes/theme'
+import { useRouter } from 'next/router'
/**
* 文章列表分页
@@ -19,7 +19,7 @@ const Page = props => {
export async function getStaticPaths() {
const from = 'page-paths'
const { postCount } = await getGlobalData({ from })
- const totalPages = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
+ const totalPages = Math.ceil(postCount / siteConfig('POSTS_PER_PAGE'))
return {
// remove first page, we 're not gonna handle that.
paths: Array.from({ length: totalPages - 1 }, (_, i) => ({
@@ -35,17 +35,17 @@ export async function getStaticProps({ params: { page } }) {
const { allPages } = props
const allPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
// 处理分页
- props.posts = allPosts.slice(BLOG.POSTS_PER_PAGE * (page - 1), BLOG.POSTS_PER_PAGE * page)
+ props.posts = allPosts.slice(siteConfig('POSTS_PER_PAGE') * (page - 1), siteConfig('POSTS_PER_PAGE') * page)
props.page = page
// 处理预览
- if (BLOG.POST_LIST_PREVIEW === 'true') {
+ if (siteConfig('POST_LIST_PREVIEW')) {
for (const i in props.posts) {
const post = props.posts[i]
if (post.password && post.password !== '') {
continue
}
- post.blockMap = await getPostBlocks(post.id, 'slug', BLOG.POST_PREVIEW_LINES)
+ post.blockMap = await getPostBlocks(post.id, 'slug', siteConfig('POST_PREVIEW_LINES'))
}
}
diff --git a/pages/search/[keyword]/index.js b/pages/search/[keyword]/index.js
index cfbedefe..0038c9f8 100644
--- a/pages/search/[keyword]/index.js
+++ b/pages/search/[keyword]/index.js
@@ -1,9 +1,9 @@
-import { getGlobalData } from '@/lib/db/getSiteData'
-import { getDataFromCache } from '@/lib/cache/cache_manager'
import BLOG from '@/blog.config'
-import { useRouter } from 'next/router'
-import { getLayoutByTheme } from '@/themes/theme'
+import { getDataFromCache } from '@/lib/cache/cache_manager'
import { siteConfig } from '@/lib/config'
+import { getGlobalData } from '@/lib/db/getSiteData'
+import { getLayoutByTheme } from '@/themes/theme'
+import { useRouter } from 'next/router'
const Index = props => {
// 根据页面路径加载不同Layout文件
@@ -27,10 +27,10 @@ export async function getStaticProps({ params: { keyword } }) {
props.posts = await filterByMemCache(allPosts, keyword)
props.postCount = props.posts.length
// 处理分页
- if (BLOG.POST_LIST_STYLE === 'scroll') {
+ if (siteConfig('POST_LIST_STYLE') === 'scroll') {
// 滚动列表 给前端返回所有数据
- } else if (BLOG.POST_LIST_STYLE === 'page') {
- props.posts = props.posts?.slice(0, BLOG.POSTS_PER_PAGE)
+ } else if (siteConfig('POST_LIST_STYLE') === 'page') {
+ props.posts = props.posts?.slice(0, siteConfig('POSTS_PER_PAGE'))
}
props.keyword = keyword
return {
@@ -87,8 +87,7 @@ function getTextContent(textArray) {
* @param {*} obj
* @returns
*/
-const isIterable = obj =>
- obj != null && typeof obj[Symbol.iterator] === 'function'
+const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function'
/**
* 在内存缓存中进行全文索引
diff --git a/pages/search/[keyword]/page/[page].js b/pages/search/[keyword]/page/[page].js
index 68cfca62..43291b0b 100644
--- a/pages/search/[keyword]/page/[page].js
+++ b/pages/search/[keyword]/page/[page].js
@@ -1,9 +1,9 @@
-import { getGlobalData } from '@/lib/db/getSiteData'
-import { getDataFromCache } from '@/lib/cache/cache_manager'
import BLOG from '@/blog.config'
-import { useRouter } from 'next/router'
-import { getLayoutByTheme } from '@/themes/theme'
+import { getDataFromCache } from '@/lib/cache/cache_manager'
import { siteConfig } from '@/lib/config'
+import { getGlobalData } from '@/lib/db/getSiteData'
+import { getLayoutByTheme } from '@/themes/theme'
+import { useRouter } from 'next/router'
const Index = props => {
const { keyword } = props
@@ -29,7 +29,7 @@ export async function getStaticProps({ params: { keyword, page } }) {
props.posts = await filterByMemCache(allPosts, keyword)
props.postCount = props.posts.length
// 处理分页
- props.posts = props.posts.slice(BLOG.POSTS_PER_PAGE * (page - 1), BLOG.POSTS_PER_PAGE * page)
+ props.posts = props.posts.slice(siteConfig('POSTS_PER_PAGE') * (page - 1), siteConfig('POSTS_PER_PAGE') * page)
props.keyword = keyword
props.page = page
delete props.allPages
@@ -87,8 +87,7 @@ function getTextContent(textArray) {
* @param {*} obj
* @returns
*/
-const isIterable = obj =>
- obj != null && typeof obj[Symbol.iterator] === 'function'
+const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function'
/**
* 在内存缓存中进行全文索引
diff --git a/pages/tag/[tag]/index.js b/pages/tag/[tag]/index.js
index 8c65dbd9..ea4ffe6a 100644
--- a/pages/tag/[tag]/index.js
+++ b/pages/tag/[tag]/index.js
@@ -1,8 +1,8 @@
-import { getGlobalData } from '@/lib/db/getSiteData'
import BLOG from '@/blog.config'
-import { useRouter } from 'next/router'
-import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config'
+import { getGlobalData } from '@/lib/db/getSiteData'
+import { getLayoutByTheme } from '@/themes/theme'
+import { useRouter } from 'next/router'
/**
* 标签下的文章列表
@@ -21,16 +21,18 @@ export async function getStaticProps({ params: { tag } }) {
const props = await getGlobalData({ from })
// 过滤状态
- props.posts = props.allPages?.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post?.tags && post?.tags.includes(tag))
+ props.posts = props.allPages
+ ?.filter(page => page.type === 'Post' && page.status === 'Published')
+ .filter(post => post && post?.tags && post?.tags.includes(tag))
// 处理文章页数
props.postCount = props.posts.length
// 处理分页
- if (BLOG.POST_LIST_STYLE === 'scroll') {
+ if (siteConfig('POST_LIST_STYLE') === 'scroll') {
// 滚动列表 给前端返回所有数据
- } else if (BLOG.POST_LIST_STYLE === 'page') {
- props.posts = props.posts?.slice(0, BLOG.POSTS_PER_PAGE)
+ } else if (siteConfig('POST_LIST_STYLE') === 'page') {
+ props.posts = props.posts?.slice(0, siteConfig('POSTS_PER_PAGE'))
}
props.tag = tag
diff --git a/pages/tag/[tag]/page/[page].js b/pages/tag/[tag]/page/[page].js
index ebe85718..4566b059 100644
--- a/pages/tag/[tag]/page/[page].js
+++ b/pages/tag/[tag]/page/[page].js
@@ -1,8 +1,8 @@
-import { getGlobalData } from '@/lib/db/getSiteData'
import BLOG from '@/blog.config'
-import { useRouter } from 'next/router'
-import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config'
+import { getGlobalData } from '@/lib/db/getSiteData'
+import { getLayoutByTheme } from '@/themes/theme'
+import { useRouter } from 'next/router'
const Tag = props => {
// 根据页面路径加载不同Layout文件
@@ -14,11 +14,13 @@ export async function getStaticProps({ params: { tag, page } }) {
const from = 'tag-page-props'
const props = await getGlobalData({ from })
// 过滤状态、标签
- props.posts = props.allPages?.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post?.tags && post?.tags.includes(tag))
+ props.posts = props.allPages
+ ?.filter(page => page.type === 'Post' && page.status === 'Published')
+ .filter(post => post && post?.tags && post?.tags.includes(tag))
// 处理文章数
props.postCount = props.posts.length
// 处理分页
- props.posts = props.posts.slice(BLOG.POSTS_PER_PAGE * (page - 1), BLOG.POSTS_PER_PAGE * page)
+ props.posts = props.posts.slice(siteConfig('POSTS_PER_PAGE') * (page - 1), siteConfig('POSTS_PER_PAGE') * page)
props.tag = tag
props.page = page
@@ -35,10 +37,12 @@ export async function getStaticPaths() {
const paths = []
tagOptions?.forEach(tag => {
// 过滤状态类型
- const tagPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post?.tags && post?.tags.includes(tag.name))
+ const tagPosts = allPages
+ ?.filter(page => page.type === 'Post' && page.status === 'Published')
+ .filter(post => post && post?.tags && post?.tags.includes(tag.name))
// 处理文章页数
const postCount = tagPosts.length
- const totalPages = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
+ const totalPages = Math.ceil(postCount / siteConfig('POSTS_PER_PAGE'))
if (totalPages > 1) {
for (let i = 1; i <= totalPages; i++) {
paths.push({ params: { tag: tag.name, page: '' + i } })
diff --git a/public/games-external/common/index.htm b/public/games-external/common/index.htm
new file mode 100644
index 00000000..0862930f
--- /dev/null
+++ b/public/games-external/common/index.htm
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+ Full Screen iFrame
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/js/fullscreen.js b/public/js/fullscreen.js
new file mode 100644
index 00000000..bc4767f7
--- /dev/null
+++ b/public/js/fullscreen.js
@@ -0,0 +1,32 @@
+window.toggleFullScreen = toggleFullScreen
+function toggleFullScreen() {
+ var iframe = document.getElementById('myIframe')
+
+ if (!document.fullscreenElement) {
+ if (iframe.requestFullscreen) {
+ iframe.requestFullscreen()
+ } else if (iframe.mozRequestFullScreen) {
+ /* Firefox */
+ iframe.mozRequestFullScreen()
+ } else if (iframe.webkitRequestFullscreen) {
+ /* Chrome, Safari and Opera */
+ iframe.webkitRequestFullscreen()
+ } else if (iframe.msRequestFullscreen) {
+ /* IE/Edge */
+ iframe.msRequestFullscreen()
+ }
+ } else {
+ if (document.exitFullscreen) {
+ document.exitFullscreen()
+ } else if (document.mozCancelFullScreen) {
+ /* Firefox */
+ document.mozCancelFullScreen()
+ } else if (document.webkitExitFullscreen) {
+ /* Chrome, Safari and Opera */
+ document.webkitExitFullscreen()
+ } else if (document.msExitFullscreen) {
+ /* IE/Edge */
+ document.msExitFullscreen()
+ }
+ }
+}
diff --git a/styles/globals.css b/styles/globals.css
index 65057e3e..2b8f568f 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -76,9 +76,13 @@ nav {
}
.shadow-card {
- box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, rgba(0, 0, 0, 0.07) 0px 2px 4px,
- rgba(0, 0, 0, 0.07) 0px 4px 8px, rgba(0, 0, 0, 0.07) 0px 8px 16px,
- rgba(0, 0, 0, 0.07) 0px 16px 32px, rgba(0, 0, 0, 0.07) 0px 32px 64px;
+ box-shadow:
+ rgba(0, 0, 0, 0.07) 0px 1px 2px,
+ rgba(0, 0, 0, 0.07) 0px 2px 4px,
+ rgba(0, 0, 0, 0.07) 0px 4px 8px,
+ rgba(0, 0, 0, 0.07) 0px 8px 16px,
+ rgba(0, 0, 0, 0.07) 0px 16px 32px,
+ rgba(0, 0, 0, 0.07) 0px 32px 64px;
}
.gt-meta {
@@ -106,7 +110,6 @@ nav {
backdrop-filter: blur(10px);
}
-
.medium-zoom-overlay {
background: none !important;
/* background: rgba(0, 0, 0, 0.01) none repeat scroll 0% 0% !important; */
@@ -157,7 +160,7 @@ nav {
/* twikoo 评论区超链接样式 */
.tk-main a {
- @apply text-blue-700
+ @apply text-blue-700;
}
/* twikoo 内置的 element-ui 加载样式 */
@@ -167,7 +170,7 @@ nav {
/* Webmention style */
.webmention-block {
- background: rgba(0, 116, 222, .2);
+ background: rgba(0, 116, 222, 0.2);
padding: 1rem 2rem;
border-radius: 5px;
}
@@ -176,11 +179,11 @@ nav {
font-style: italic;
font-weight: 700;
font-size: 16px;
- margin-bottom: .5rem;
+ margin-bottom: 0.5rem;
}
.webmention-block-intro a {
- color: #0000EE;
+ color: #0000ee;
text-decoration: underline;
}
@@ -197,14 +200,14 @@ nav {
.webmention-counts .count {
font-weight: bold;
- margin-right: .2rem;
+ margin-right: 0.2rem;
}
/* .webmention-counts .counts > span {
margin-right: .8rem;
} */
-.webmention-counts .counts>span:not(:last-child):after {
- content: " • ";
+.webmention-counts .counts > span:not(:last-child):after {
+ content: ' • ';
}
a.avatar-wrapper {
@@ -221,7 +224,7 @@ a.avatar-wrapper {
.avatar {
border-radius: 50%;
margin: 0;
- border: 3px solid rgba(0, 116, 222, .5);
+ border: 3px solid rgba(0, 116, 222, 0.5);
}
.replies {
@@ -235,7 +238,7 @@ a.avatar-wrapper {
position: relative;
padding: 0;
align-items: flex-start;
- margin-top: .6rem;
+ margin-top: 0.6rem;
}
.reply p {
@@ -255,4 +258,9 @@ a.avatar-wrapper {
user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
-}
\ No newline at end of file
+}
+
+.writing-vertical {
+ writing-mode: vertical-rl; /* 竖向排列从右向左 */
+ text-orientation: upright; /* 文字方向正常 */
+}
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 (
+ <>
+
+
+
+
+ Please allow ads on our site
+
+
+
+
+
+ {
+ "Looks like you're using an ad blocker. We rely on advertising to help fund our site."
+ }
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/themes/game/components/Announcement.js b/themes/game/components/Announcement.js
new file mode 100644
index 00000000..0e849fc5
--- /dev/null
+++ b/themes/game/components/Announcement.js
@@ -0,0 +1,27 @@
+import dynamic from 'next/dynamic'
+
+const NotionPage = dynamic(() => import('@/components/NotionPage'))
+
+/**
+ * 公告
+ * @param {*} param0
+ * @returns
+ */
+const Announcement = ({ notice, className }) => {
+ if (notice?.blockMap) {
+ return (
+
+
+ {notice && (
+
+
+
+ )}
+
+
+ )
+ } else {
+ return null
+ }
+}
+export default Announcement
diff --git a/themes/game/components/ArticleLock.js b/themes/game/components/ArticleLock.js
new file mode 100644
index 00000000..3a5f4b4c
--- /dev/null
+++ b/themes/game/components/ArticleLock.js
@@ -0,0 +1,53 @@
+import { useGlobal } from '@/lib/global'
+import { useEffect, useRef } from 'react'
+
+/**
+ * 加密文章校验组件
+ * @param {password, validPassword} props
+ * @param password 正确的密码
+ * @param validPassword(bool) 回调函数,校验正确回调入参为true
+ * @returns
+ */
+export const ArticleLock = props => {
+ const { validPassword } = props
+ const { locale } = useGlobal()
+
+ const submitPassword = () => {
+ const p = document.getElementById('password')
+ if (!validPassword(p?.value)) {
+ const tips = document.getElementById('tips')
+ if (tips) {
+ tips.innerHTML = ''
+ tips.innerHTML = `${locale.COMMON.PASSWORD_ERROR}
`
+ }
+ }
+ }
+
+ const passwordInputRef = useRef(null)
+ useEffect(() => {
+ // 选中密码输入框并将其聚焦
+ passwordInputRef.current.focus()
+ }, [])
+
+ return
+
+
{locale.COMMON.ARTICLE_LOCK_TIPS}
+
+
{
+ if (e.key === 'Enter') {
+ submitPassword()
+ }
+ }}
+ ref={passwordInputRef} // 绑定ref到passwordInputRef变量
+ className='outline-none w-full text-sm pl-5 rounded-l transition focus:shadow-lg font-light leading-10 text-black dark:bg-gray-500 bg-gray-50'
+ >
+
+ {locale.COMMON.SUBMIT}
+
+
+
+
+
+
+}
diff --git a/themes/game/components/BlogArchiveItem.js b/themes/game/components/BlogArchiveItem.js
new file mode 100644
index 00000000..ac1c2522
--- /dev/null
+++ b/themes/game/components/BlogArchiveItem.js
@@ -0,0 +1,44 @@
+import { siteConfig } from '@/lib/config'
+import Link from 'next/link'
+import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils'
+
+/**
+ * 归档分组文章
+ * @param {*} param0
+ * @returns
+ */
+export default function BlogArchiveItem({ archiveTitle, archivePosts }) {
+ return (
+
+
+ {archiveTitle}
+
+
+
+
+ )
+}
diff --git a/themes/game/components/BlogListBar.js b/themes/game/components/BlogListBar.js
new file mode 100644
index 00000000..74511a89
--- /dev/null
+++ b/themes/game/components/BlogListBar.js
@@ -0,0 +1,38 @@
+import { useGameGlobal } from '..'
+import Tags from './Tags'
+
+export default function BlogListBar(props) {
+ const { tag, setFilterKey } = useGameGlobal()
+ const handleSearchChange = val => {
+ setFilterKey(val)
+ }
+ if (tag) {
+ return (
+
+
+
handleSearchChange(e.target.value)}
+ />
+
+
+
+
+ )
+ } else {
+ return <>>
+ }
+}
diff --git a/themes/game/components/BlogListPage.js b/themes/game/components/BlogListPage.js
new file mode 100644
index 00000000..8d821bd1
--- /dev/null
+++ b/themes/game/components/BlogListPage.js
@@ -0,0 +1,25 @@
+import { siteConfig } from '@/lib/config'
+import { GameListIndexCombine } from './GameListIndexCombine'
+import PaginationSimple from './PaginationSimple'
+/**
+ * 分页博客列表
+ * @param {*} props
+ * @returns
+ */
+export const BlogListPage = props => {
+ const { page = 1, postCount } = props
+ const totalPage = Math.ceil(
+ postCount / parseInt(siteConfig('POSTS_PER_PAGE'))
+ )
+ const showNext = page < totalPage
+
+ return (
+ <>
+
+
+
+
+
+ >
+ )
+}
diff --git a/themes/game/components/BlogListScroll.js b/themes/game/components/BlogListScroll.js
new file mode 100644
index 00000000..77d4d761
--- /dev/null
+++ b/themes/game/components/BlogListScroll.js
@@ -0,0 +1,59 @@
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import { deepClone } from '@/lib/utils'
+import throttle from 'lodash.throttle'
+import { useCallback, useEffect, useRef, useState } from 'react'
+import { GameListIndexCombine } from './GameListIndexCombine'
+
+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 (
+ <>
+
+
+
+
+
+ {' '}
+ {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 (
+ (
+
+
+
+
+ {post.title}
+
+
+
+
+ {!showPreview &&
+ {post.summary}
+
}
+ {showPreview && post?.blockMap && (
+
+
+
+
+ )}
+
+
+
+ )
+ )
+}
+
+export default BlogPost
diff --git a/themes/game/components/BlogPostBar.js b/themes/game/components/BlogPostBar.js
new file mode 100644
index 00000000..bf761253
--- /dev/null
+++ b/themes/game/components/BlogPostBar.js
@@ -0,0 +1,29 @@
+import { useGlobal } from '@/lib/global'
+
+/**
+ * 文章列表上方嵌入
+ * @param {*} props
+ * @returns
+ */
+export default function BlogPostBar(props) {
+ const { tag, category } = props
+ const { locale } = useGlobal()
+
+ if (tag) {
+ return (
+
+
+ {locale.COMMON.TAGS}:{tag}
+
+ )
+ } else if (category) {
+ return (
+
+
+ {locale.COMMON.CATEGORY}:{category}
+
+ )
+ } else {
+ return <>>
+ }
+}
diff --git a/themes/game/components/DarkModeButton.js b/themes/game/components/DarkModeButton.js
new file mode 100644
index 00000000..b0edaf72
--- /dev/null
+++ b/themes/game/components/DarkModeButton.js
@@ -0,0 +1,33 @@
+import { useGlobal } from '@/lib/global'
+import { useImperativeHandle } from 'react'
+
+/**
+ * 深色模式按钮
+ */
+const DarkModeButton = props => {
+ const { cRef, className } = props
+ const { isDarkMode, toggleDarkMode } = useGlobal()
+
+ /**
+ * 对外暴露方法
+ */
+ useImperativeHandle(cRef, () => {
+ return {
+ handleChangeDarkMode: () => {
+ toggleDarkMode()
+ }
+ }
+ })
+
+ return (
+
+
+ {isDarkMode ? 'Dark Mode' : 'Light Mode'}{' '}
+
+ )
+}
+export default DarkModeButton
diff --git a/themes/game/components/ExampleRecentComments.js b/themes/game/components/ExampleRecentComments.js
new file mode 100644
index 00000000..9dbdfa7f
--- /dev/null
+++ b/themes/game/components/ExampleRecentComments.js
@@ -0,0 +1,35 @@
+import { useEffect, useState } from 'react'
+import { siteConfig } from '@/lib/config'
+import Link from 'next/link'
+import { RecentComments } from '@waline/client'
+
+/**
+ * @see https://waline.js.org/guide/get-started.html
+ * @param {*} props
+ * @returns
+ */
+const ExampleRecentComments = (props) => {
+ const [comments, updateComments] = useState([])
+ const [onLoading, changeLoading] = useState(true)
+ useEffect(() => {
+ RecentComments({
+ serverURL: siteConfig('COMMENT_WALINE_SERVER_URL'),
+ count: 5
+ }).then(({ comments }) => {
+ changeLoading(false)
+ updateComments(comments)
+ })
+ }, [])
+
+ return <>
+ {onLoading && Loading...
}
+ {!onLoading && comments && comments.length === 0 && No Comments
}
+ {!onLoading && comments && comments.length > 0 && comments.map((comment) => )}
+
+ >
+}
+
+export default ExampleRecentComments
diff --git a/themes/game/components/Footer.js b/themes/game/components/Footer.js
new file mode 100644
index 00000000..3f90a843
--- /dev/null
+++ b/themes/game/components/Footer.js
@@ -0,0 +1,33 @@
+import { siteConfig } from '@/lib/config'
+
+export const Footer = props => {
+ const d = new Date()
+ const currentYear = d.getFullYear()
+ const since = siteConfig('SINCE')
+ const copyrightDate =
+ parseInt(since) < currentYear ? since + '-' + currentYear : currentYear
+
+ return (
+
+ )
+}
diff --git a/themes/game/components/FullScreen.js b/themes/game/components/FullScreen.js
new file mode 100644
index 00000000..223f740d
--- /dev/null
+++ b/themes/game/components/FullScreen.js
@@ -0,0 +1,31 @@
+/* eslint-disable @next/next/no-img-element */
+
+/**
+ * 全屏按钮
+ * @returns
+ */
+export default function FullScreen() {
+ function toggleFullScreen() {
+ // window.scrollTo(0, 2)
+ document?.querySelector('#game-wrapper')?.scrollIntoView({
+ behavior: 'smooth',
+ block: 'end',
+ inline: 'nearest'
+ })
+ document?.getElementById('game-wrapper')?.contentWindow?.toggleFullScreen &&
+ document?.getElementById('game-wrapper')?.contentWindow?.toggleFullScreen()
+ }
+
+ return (
+
+
+ FullScreen
+
+ )
+}
diff --git a/themes/game/components/GameListIndexCombine.js b/themes/game/components/GameListIndexCombine.js
new file mode 100644
index 00000000..2de16df6
--- /dev/null
+++ b/themes/game/components/GameListIndexCombine.js
@@ -0,0 +1,166 @@
+/* eslint-disable @next/next/no-img-element */
+import { AdSlot } from '@/components/GoogleAdsense'
+import { siteConfig } from '@/lib/config'
+import { checkContainHttp, deepClone, sliceUrlFromHttp } from '@/lib/utils'
+import Link from 'next/link'
+import { useState } from 'react'
+import CONFIG from '../config'
+
+/**
+ * 游戏列表
+ * @returns
+ */
+export const GameListIndexCombine = ({ posts }) => {
+ const gamesClone = deepClone(posts)
+
+ // 构造一个List
+ const components = []
+
+ // 根据序号随机大小;或根据game.recommend 决定
+ const recommend = siteConfig('GAME_INDEX_EXPAND_RECOMMEND', true, CONFIG)
+
+ let index = 0
+ // 无限循环
+ if (recommend) {
+ // 4合一卡组
+ let groupItems = []
+
+ while (gamesClone?.length > 0) {
+ index++
+
+ // 广告位
+ if (index % 9 === 0) {
+ components.push()
+ continue
+ }
+
+ // 试图将4合一卡组塞满
+ while (gamesClone?.length > 0 && groupItems.length < 4) {
+ const item = gamesClone.shift()
+ if (item.tags?.some(t => t === siteConfig('GAME_RECOMMEND_TAG', 'Recommend', CONFIG))) {
+ components.push()
+ continue
+ } else {
+ groupItems.push(item)
+ }
+ }
+
+ if (groupItems.length === 4) {
+ components.push()
+ // 清空4合一卡片
+ groupItems = []
+ } else {
+ // 剩余的4合一不满4个的给他放大卡
+ while (groupItems.length > 0) {
+ const item = groupItems.shift()
+ components.push()
+ }
+ }
+ }
+ } else {
+ while (gamesClone?.length > 0) {
+ index++
+
+ if (index % 6 === 0) {
+ components.push()
+ } else if (index % 2 === 0 && gamesClone?.length >= 4) {
+ // 如果是偶数,则从游戏列表中退出4个组成大卡牌
+ const groupItems = []
+ for (let i = 1; i <= 4; i++) {
+ groupItems.push(gamesClone.shift())
+ }
+ components.push()
+ } else {
+ const item = gamesClone.shift()
+ components.push()
+ }
+ }
+ }
+
+ return (
+
+
+ {components?.map((ItemComponent, index) => {
+ return ItemComponent
+ })}
+
+
+ )
+}
+
+/**
+ * 一个广告游戏大卡
+ * @returns
+ */
+const GameAd = () => {
+ return (
+
+ )
+}
+
+/**
+ * 4卡组成一个大卡
+ * @param {*} param0
+ * @returns
+ */
+const GameItemGroup = ({ items }) => {
+ return (
+
+ {items.map((item, index) => (
+
+ ))}
+
+ )
+}
+
+/**
+ * 游戏=单卡
+ * @param {*} param0
+ * @returns
+ */
+const GameItem = ({ item, isLargeCard }) => {
+ const { title } = item
+ const img = item.pageCoverThumbnail
+ const [showType, setShowType] = useState('img') // img or video
+ const url = checkContainHttp(item.slug) ? sliceUrlFromHttp(item.slug) : `${siteConfig('SUB_PATH', '')}/${item.slug}`
+ const video = item?.ext?.video
+ return (
+ {
+ setShowType('video')
+ }}
+ onMouseOut={() => {
+ setShowType('img')
+ }}
+ title={title}
+ className={`card-single ${
+ isLargeCard ? 'h-[20rem]' : 'h-full'
+ } w-full relative shadow rounded-md overflow-hidden flex justify-center items-center
+ group hover:border-purple-400`}>
+
+ {title}
+
+
+
+ {showType === 'video' && (
+
+ )}
+
+
+ )
+}
diff --git a/themes/game/components/GameListNormal.js b/themes/game/components/GameListNormal.js
new file mode 100644
index 00000000..b9bc9603
--- /dev/null
+++ b/themes/game/components/GameListNormal.js
@@ -0,0 +1,74 @@
+/* eslint-disable @next/next/no-img-element */
+import { siteConfig } from '@/lib/config'
+import { checkContainHttp, deepClone, sliceUrlFromHttp } from '@/lib/utils'
+import Link from 'next/link'
+import { useState } from 'react'
+
+/**
+ * 游戏列表- 关联游戏,在详情页展示
+ * @returns
+ */
+export const GameListNormal = ({ games, maxCount = 18 }) => {
+ const gamesClone = deepClone(games)
+
+ // 构造一个List
+ const components = []
+
+ let index = 0
+ // 无限循环
+ while (gamesClone?.length > 0 && index < maxCount) {
+ const item = gamesClone.shift()
+ components.push()
+ index++
+ continue
+ }
+
+ return (
+
+
+ {components?.map((ItemComponent, index) => {
+ return ItemComponent
+ })}
+
+
+ )
+}
+
+/**
+ * 游戏=单卡
+ * @param {*} param0
+ * @returns
+ */
+const GameItem = ({ item }) => {
+ const { title } = item
+ const img = item.pageCoverThumbnail
+ const [showType, setShowType] = useState('img') // img or video
+ const url = checkContainHttp(item.slug) ? sliceUrlFromHttp(item.slug) : `${siteConfig('SUB_PATH', '')}/${item.slug}`
+ const video = item?.ext?.video
+
+ return (
+ {
+ setShowType('video')
+ }}
+ onMouseOut={() => {
+ setShowType('img')
+ }}
+ title={title}
+ className={`card-single h-28 w-28 relative shadow rounded-md overflow-hidden flex justify-center items-center
+ group hover:border-purple-400`}>
+ {title}
+
+
+ {showType === 'video' && (
+
+ )}
+
+
+ )
+}
diff --git a/themes/game/components/GameListRealate.js b/themes/game/components/GameListRealate.js
new file mode 100644
index 00000000..685cb1b0
--- /dev/null
+++ b/themes/game/components/GameListRealate.js
@@ -0,0 +1,82 @@
+/* eslint-disable @next/next/no-img-element */
+import { siteConfig } from '@/lib/config'
+import { checkContainHttp, deepClone, sliceUrlFromHttp } from '@/lib/utils'
+import Link from 'next/link'
+import { useState } from 'react'
+
+/**
+ * 游戏列表- 关联游戏,在详情页展示
+ * @returns
+ */
+export const GameListRelate = ({ posts }) => {
+ const gamesClone = deepClone(posts)
+
+ // 构造一个List
+ const components = []
+ const maxCount = 24
+
+ let index = 0
+ // 无限循环
+ while (gamesClone?.length > 0 && index < maxCount) {
+ const item = gamesClone.shift()
+ components.push()
+ index++
+ continue
+ }
+
+ return (
+
+
+ {components?.map((ItemComponent, index) => {
+ return ItemComponent
+ })}
+
+
+ )
+}
+
+/**
+ * 游戏=单卡
+ * @param {*} param0
+ * @returns
+ */
+const GameItem = ({ item }) => {
+ const { title } = item
+ const [showType, setShowType] = useState('img') // img or video
+ const url = checkContainHttp(item.slug) ? sliceUrlFromHttp(item.slug) : `${siteConfig('SUB_PATH', '')}/${item.slug}`
+
+ const img = item?.pageCoverThumbnail
+ const video = item?.ext?.video
+
+ return (
+ {
+ setShowType('video')
+ }}
+ onMouseOut={() => {
+ setShowType('img')
+ }}
+ title={title}
+ className={`card-single w-24 h-24 relative shadow rounded-md overflow-hidden flex justify-center items-center
+ group hover:border-purple-400`}>
+
+ {title}
+
+
+
+ {showType === 'video' && (
+
+ )}
+
+
+ )
+}
diff --git a/themes/game/components/GameListRecent.js b/themes/game/components/GameListRecent.js
new file mode 100644
index 00000000..f390adc2
--- /dev/null
+++ b/themes/game/components/GameListRecent.js
@@ -0,0 +1,89 @@
+/* eslint-disable @next/next/no-img-element */
+import { siteConfig } from '@/lib/config'
+import { checkContainHttp, deepClone, sliceUrlFromHttp } from '@/lib/utils'
+import { useState } from 'react'
+import { useGameGlobal } from '..'
+
+/**
+ * 游戏列表- 最近游戏
+ * @returns
+ */
+export const GameListRecent = ({ maxCount = 14 }) => {
+ const { recentGames } = useGameGlobal()
+ const gamesClone = deepClone(recentGames)
+ // 构造一个List
+ const components = []
+
+ let index = 0
+ // 无限循环
+ while (gamesClone?.length > 0 && index < maxCount) {
+ const item = gamesClone?.shift()
+ if (item) {
+ components.push()
+ index++
+ }
+ continue
+ }
+
+ if (components.length === 0) {
+ return <>>
+ }
+
+ return (
+ <>
+
+
+ {components?.map((ItemComponent, index) => {
+ return ItemComponent
+ })}
+
+
+ >
+ )
+}
+
+/**
+ * 游戏=单卡
+ * @param {*} param0
+ * @returns
+ */
+const GameItem = ({ item }) => {
+ const { title } = item || {}
+ const [showType, setShowType] = useState('img') // img or video
+ const url = checkContainHttp(item.slug) ? sliceUrlFromHttp(item.slug) : `${siteConfig('SUB_PATH', '')}/${item.slug}`
+
+ const img = item?.pageCoverThumbnail
+ const video = item?.ext?.video
+ return (
+ {
+ setShowType('video')
+ }}
+ onMouseOut={() => {
+ setShowType('img')
+ }}
+ title={title}
+ className={`card-single h-28 w-28 relative shadow rounded-md overflow-hidden flex justify-center items-center
+ group hover:border-purple-400`}>
+
+
+
+ {title}
+
+
+ {showType === 'video' && (
+
+ )}
+
+
+ )
+}
diff --git a/themes/game/components/GroupCategory.js b/themes/game/components/GroupCategory.js
new file mode 100644
index 00000000..095755a1
--- /dev/null
+++ b/themes/game/components/GroupCategory.js
@@ -0,0 +1,41 @@
+import Link from 'next/link'
+
+function GroupCategory({ currentCategory, categoryOptions }) {
+ if (!categoryOptions) {
+ return <>>
+ }
+
+ return (
+ <>
+
+
+
+
+ {categoryOptions.map(category => {
+ const selected = currentCategory === category.name
+ return (
+
+ {/* */}
+ {category.name}
+ {/*
+ {category.count}
+ */}
+
+ )
+ })}
+
+ >
+ )
+}
+
+export default GroupCategory
diff --git a/themes/game/components/GroupTag.js b/themes/game/components/GroupTag.js
new file mode 100644
index 00000000..383705b2
--- /dev/null
+++ b/themes/game/components/GroupTag.js
@@ -0,0 +1,28 @@
+import Link from 'next/link'
+import TagItemMini from './TagItemMini'
+
+/**
+ * 标签组
+ * @param tags
+ * @param currentTag
+ * @returns {JSX.Element}
+ * @constructor
+ */
+function GroupTag({ tagOptions, currentTag }) {
+ if (!tagOptions) return <>>
+ return (
+ <>
+
+
+
+
+ {tagOptions?.slice(0, 20)?.map(tag => {
+ const selected = tag.name === currentTag
+ return
+ })}
+
+ >
+ )
+}
+
+export default GroupTag
diff --git a/themes/game/components/Header.js b/themes/game/components/Header.js
new file mode 100644
index 00000000..02873cb0
--- /dev/null
+++ b/themes/game/components/Header.js
@@ -0,0 +1,25 @@
+import { useGameGlobal } from '..'
+import Logo from './Logo'
+
+/**
+ * 顶栏
+ * @returns
+ */
+export default function Header() {
+ const { setSideBarVisible } = useGameGlobal()
+ return (
+
+ )
+}
diff --git a/themes/game/components/JumpToTopButton.js b/themes/game/components/JumpToTopButton.js
new file mode 100644
index 00000000..f5e22b61
--- /dev/null
+++ b/themes/game/components/JumpToTopButton.js
@@ -0,0 +1,18 @@
+import { useGlobal } from '@/lib/global'
+
+/**
+ * 跳转到网页顶部
+ * 当屏幕下滑500像素后会出现该控件
+ * @param targetRef 关联高度的目标html标签
+ * @param showPercent 是否显示百分比
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const JumpToTopButton = () => {
+ const { locale } = useGlobal()
+ return window.scrollTo({ top: 0, behavior: 'smooth' })}
+ >
+
+}
+
+export default JumpToTopButton
diff --git a/themes/game/components/Logo.js b/themes/game/components/Logo.js
new file mode 100644
index 00000000..251a833f
--- /dev/null
+++ b/themes/game/components/Logo.js
@@ -0,0 +1,14 @@
+import { siteConfig } from '@/lib/config'
+import Link from 'next/link'
+
+/* eslint-disable @next/next/no-html-link-for-pages */
+export default function Logo() {
+ return (
+
+
+
{siteConfig('TITLE')}
+ {siteConfig('BIO')}
+
+
+ )
+}
diff --git a/themes/game/components/LogoMini.js b/themes/game/components/LogoMini.js
new file mode 100644
index 00000000..b540e6e6
--- /dev/null
+++ b/themes/game/components/LogoMini.js
@@ -0,0 +1,11 @@
+import { siteConfig } from '@/lib/config'
+import Link from 'next/link'
+
+/* eslint-disable @next/next/no-html-link-for-pages */
+export default function LogoMini() {
+ return (
+
+ {siteConfig('TITLE')?.charAt(0)}
+
+ )
+}
diff --git a/themes/game/components/MenuItemCollapse.js b/themes/game/components/MenuItemCollapse.js
new file mode 100644
index 00000000..8f53f622
--- /dev/null
+++ b/themes/game/components/MenuItemCollapse.js
@@ -0,0 +1,55 @@
+import Collapse from '@/components/Collapse'
+import Link from 'next/link'
+import { useState } from 'react'
+
+/**
+ * 折叠菜单
+ * @param {*} param0
+ * @returns
+ */
+export const MenuItemCollapse = (props) => {
+ const { link } = props
+ const [show, changeShow] = useState(false)
+ const hasSubMenu = link?.subMenus?.length > 0
+
+ const [isOpen, changeIsOpen] = useState(false)
+
+ const toggleShow = () => {
+ changeShow(!show)
+ }
+
+ const toggleOpenSubMenu = () => {
+ changeIsOpen(!isOpen)
+ }
+
+ if (!link || !link.show) {
+ return null
+ }
+
+ return <>
+
+ {!hasSubMenu &&
+
{link?.icon && }{link?.name}
+ }
+ {hasSubMenu &&
+ {link?.icon && }{link?.name}
+
+
}
+
+
+ {/* 折叠子菜单 */}
+ {hasSubMenu &&
+ {link.subMenus.map((sLink, index) => {
+ return
+
+ {sLink.title}
+
+
+ })}
+ }
+ >
+}
diff --git a/themes/game/components/MenuItemDrop.js b/themes/game/components/MenuItemDrop.js
new file mode 100644
index 00000000..2289f3bc
--- /dev/null
+++ b/themes/game/components/MenuItemDrop.js
@@ -0,0 +1,68 @@
+import Link from 'next/link'
+import { useState } from 'react'
+
+export const MenuItemDrop = ({ link }) => {
+ const [show, changeShow] = useState(false)
+ if (!link || !link.show) {
+ return null
+ }
+
+ const hasSubMenu = link?.subMenus?.length > 0
+
+ return (
+
+ changeShow(true)}
+ onMouseOut={() => changeShow(false)}>
+ {!hasSubMenu && (
+
+
+
+ {link?.icon && }
+
+ {link?.name}
+
+
+ )}
+
+ {hasSubMenu && (
+
+ {link?.icon && }{' '}
+ {link?.name}
+
+
+ )}
+
+ {/* 子菜单 */}
+ {hasSubMenu && (
+
+ )}
+
+
+ )
+}
diff --git a/themes/game/components/MenuList.js b/themes/game/components/MenuList.js
new file mode 100644
index 00000000..b036bb86
--- /dev/null
+++ b/themes/game/components/MenuList.js
@@ -0,0 +1,76 @@
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import { useGameGlobal } from '..'
+import CONFIG from '../config'
+import DarkModeButton from './DarkModeButton'
+import { MenuItemDrop } from './MenuItemDrop'
+
+/**
+ * 导航菜单
+ */
+export const MenuList = props => {
+ const { setSideBarVisible } = useGameGlobal()
+ const { customNav, customMenu } = props
+ const { locale } = useGlobal()
+ const defaultLinks = [
+ {
+ id: 1,
+ icon: 'fas fa-home',
+ name: locale.NAV.INDEX,
+ to: '/' || '/',
+ show: true
+ },
+ {
+ id: 2,
+ icon: 'fas fa-th',
+ name: locale.COMMON.CATEGORY,
+ to: '/category',
+ show: siteConfig('GAME_MENU_CATEGORY', null, CONFIG)
+ },
+ {
+ id: 3,
+ icon: 'fas fa-tag',
+ name: locale.COMMON.TAGS,
+ to: '/tag',
+ show: siteConfig('GAME_MENU_TAG', null, CONFIG)
+ }
+ ]
+
+ let links = [].concat(defaultLinks)
+ if (customNav) {
+ links = defaultLinks.concat(customNav)
+ }
+
+ // 如果 开启自定义菜单,则覆盖Page生成的菜单
+ if (siteConfig('CUSTOM_MENU')) {
+ links = customMenu
+ }
+
+ return (
+
+ -
+
+
+ -
+
+
+ {links?.length > 0 &&
}
+
+ {links?.map(
+ (link, index) =>
+ link && link.show &&
+ )}
+
+ )
+}
diff --git a/themes/game/components/PaginationSimple.js b/themes/game/components/PaginationSimple.js
new file mode 100644
index 00000000..3f5a2760
--- /dev/null
+++ b/themes/game/components/PaginationSimple.js
@@ -0,0 +1,54 @@
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+/**
+ * 简易翻页插件
+ * @param page 当前页码
+ * @param showNext 是否有下一页
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const PaginationSimple = ({ page, showNext }) => {
+ const { locale } = useGlobal()
+ const router = useRouter()
+ const currentPage = +page
+ const pagePrefix = router.asPath
+ .split('?')[0]
+ .replace(/\/page\/[1-9]\d*/, '')
+ .replace(/\/$/, '')
+
+ return (
+
+
+ ←{locale.PAGINATION.PREV}
+
+
+ {locale.PAGINATION.NEXT}→
+
+
+ )
+}
+
+export default PaginationSimple
diff --git a/themes/game/components/PostInfo.js b/themes/game/components/PostInfo.js
new file mode 100644
index 00000000..8471d9db
--- /dev/null
+++ b/themes/game/components/PostInfo.js
@@ -0,0 +1,53 @@
+import NotionIcon from '@/components/NotionIcon'
+import Link from 'next/link'
+import TagItem from './TagItem'
+
+/**
+ * 文章详情页说明信息
+ */
+export default function PostInfo(props) {
+ const { post } = props
+
+ return (
+
+
+
+ {post?.type !== 'Page' && (
+ <>
+
+ {post?.category}
+
+ >
+ )}
+
+
+
+
+ {post?.title}
+
+
+ {post?.type !== 'Page' && (
+ <>
+
+ >
+ )}
+
+
+ )
+}
diff --git a/themes/game/components/RandomPostButton.js b/themes/game/components/RandomPostButton.js
new file mode 100644
index 00000000..a791f0f8
--- /dev/null
+++ b/themes/game/components/RandomPostButton.js
@@ -0,0 +1,26 @@
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import { useRouter } from 'next/router'
+
+/**
+ * 随机跳转到一个文章
+ */
+export default function RandomPostButton(props) {
+ const { latestPosts } = props
+ const router = useRouter()
+ const { locale } = useGlobal()
+ /**
+ * 随机跳转文章
+ */
+ function handleClick() {
+ const randomIndex = Math.floor(Math.random() * latestPosts.length)
+ const randomPost = latestPosts[randomIndex]
+ router.push(`${siteConfig('SUB_PATH', '')}/${randomPost?.slug}`)
+ }
+
+ return (
+
+
+
+ )
+}
diff --git a/themes/game/components/SearchButton.js b/themes/game/components/SearchButton.js
new file mode 100644
index 00000000..ea8fa508
--- /dev/null
+++ b/themes/game/components/SearchButton.js
@@ -0,0 +1,34 @@
+import { siteConfig } from '@/lib/config'
+import { useGlobal } from '@/lib/global'
+import { useRouter } from 'next/router'
+import { useGameGlobal } from '..'
+
+/**
+ * 搜索按钮
+ * @returns
+ */
+export default function SearchButton(props) {
+ const { locale } = useGlobal()
+ const { searchModal } = useGameGlobal()
+ const router = useRouter()
+
+ function handleSearch() {
+ if (siteConfig('ALGOLIA_APP_ID')) {
+ searchModal.current.openSearch()
+ } else {
+ router.push('/search')
+ }
+ }
+
+ return (
+ <>
+
+
+
+ >
+ )
+}
diff --git a/themes/game/components/SearchInput.js b/themes/game/components/SearchInput.js
new file mode 100644
index 00000000..9f1ffdbe
--- /dev/null
+++ b/themes/game/components/SearchInput.js
@@ -0,0 +1,88 @@
+import { useRouter } from 'next/router'
+import { useGlobal } from '@/lib/global'
+import { useImperativeHandle, useRef, useState } from 'react'
+
+let lock = false
+
+const SearchInput = props => {
+ const { tag, keyword, cRef } = props
+ const { locale } = useGlobal()
+ const router = useRouter()
+ const searchInputRef = useRef(null)
+ useImperativeHandle(cRef, () => {
+ return {
+ focus: () => {
+ searchInputRef?.current?.focus()
+ }
+ }
+ })
+ const handleSearch = () => {
+ const key = searchInputRef.current.value
+ if (key && key !== '') {
+ router.push({ pathname: '/search/' + key }).then(r => {
+ // console.log('搜索', key)
+ })
+ } else {
+ router.push({ pathname: '/' }).then(r => {
+ })
+ }
+ }
+ const handleKeyUp = (e) => {
+ if (e.keyCode === 13) { // 回车
+ handleSearch(searchInputRef.current.value)
+ } else if (e.keyCode === 27) { // ESC
+ cleanSearch()
+ }
+ }
+ const cleanSearch = () => {
+ searchInputRef.current.value = ''
+ setShowClean(false)
+ }
+ function lockSearchInput () {
+ lock = true
+ }
+
+ function unLockSearchInput () {
+ lock = false
+ }
+ const [showClean, setShowClean] = useState(false)
+ const updateSearchKey = (val) => {
+ if (lock) {
+ return
+ }
+ searchInputRef.current.value = val
+ if (val) {
+ setShowClean(true)
+ } else {
+ setShowClean(false)
+ }
+ }
+
+ return
+}
+
+export default SearchInput
diff --git a/themes/game/components/SideBar.js b/themes/game/components/SideBar.js
new file mode 100644
index 00000000..a4d1e060
--- /dev/null
+++ b/themes/game/components/SideBar.js
@@ -0,0 +1,65 @@
+import { siteConfig } from '@/lib/config'
+import Live2D from '@/components/Live2D'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import dynamic from 'next/dynamic'
+const ExampleRecentComments = dynamic(() => import('./ExampleRecentComments'))
+
+export const SideBar = (props) => {
+ const { locale } = useGlobal()
+ const { latestPosts, categories } = props
+ return (
+
+
+
+
+
+
+ {siteConfig('COMMENT_WALINE_SERVER_URL') && JSON.parse(siteConfig('COMMENT_WALINE_RECENT')) &&
}
+
+
+
+
+ );
+}
diff --git a/themes/game/components/SideBarContent.js b/themes/game/components/SideBarContent.js
new file mode 100644
index 00000000..e2efbeb3
--- /dev/null
+++ b/themes/game/components/SideBarContent.js
@@ -0,0 +1,65 @@
+import { siteConfig } from '@/lib/config'
+import { deepClone } from '@/lib/utils'
+import { useEffect, useRef } from 'react'
+import { useGameGlobal } from '..'
+import CONFIG from '../config'
+import { GameListNormal } from './GameListNormal'
+import Logo from './Logo'
+
+/**
+ * 侧拉抽屉的内容
+ */
+export default function SideBarContent() {
+ const { allNavPages, sideBarVisible, setSideBarVisible, filterGames, setFilterGames } = useGameGlobal()
+ const inputRef = useRef(null) // 创建对输入框的引用
+ const allGames = deepClone(allNavPages)
+ useEffect(() => {
+ if (sideBarVisible) {
+ setTimeout(() => {
+ inputRef.current.focus() // 在组件渲染后聚焦输入框
+ }, 100)
+ }
+ }, [sideBarVisible, inputRef])
+
+ const handleSearch = e => {
+ const search = e.target.value
+ if (!search || search === '') {
+ setFilterGames(
+ allGames?.filter(item => item.tags?.some(t => t === siteConfig('GAME_RECOMMEND_TAG', 'Recommend', CONFIG)))
+ )
+ return
+ }
+ setFilterGames(
+ allGames?.filter(item => {
+ return (
+ item.title.toLowerCase().includes(search.toLowerCase()) ||
+ item.id.toLowerCase().includes(search.toLowerCase()) ||
+ item.id.toLowerCase().replace('-', '').includes(search.toLowerCase().replace('-', ''))
+ )
+ })
+ )
+ }
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/themes/game/components/SideBarDrawer.js b/themes/game/components/SideBarDrawer.js
new file mode 100644
index 00000000..1766744b
--- /dev/null
+++ b/themes/game/components/SideBarDrawer.js
@@ -0,0 +1,61 @@
+import { useRouter } from 'next/router'
+import { useEffect } from 'react'
+
+/**
+ * 侧边栏抽屉面板,可以从侧面拉出
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const SideBarDrawer = ({ children, isOpen, onOpen, onClose, className }) => {
+ const router = useRouter()
+ useEffect(() => {
+ const sideBarDrawerRouteListener = () => {
+ switchSideDrawerVisible(false)
+ }
+ router.events.on('routeChangeComplete', sideBarDrawerRouteListener)
+ return () => {
+ router.events.off('routeChangeComplete', sideBarDrawerRouteListener)
+ }
+ }, [router.events])
+
+ // 点击按钮更改侧边抽屉状态
+ 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'
+ )
+
+ if (showStatus) {
+ sideBarDrawer?.classList.replace('-ml-96', 'ml-0')
+ sideBarDrawerBackground?.classList.replace('hidden', 'block')
+ } else {
+ sideBarDrawer?.classList.replace('ml-0', '-ml-96')
+ sideBarDrawerBackground?.classList.replace('block', 'hidden')
+ }
+ }
+
+ return (
+