mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-14 15:09:22 +00:00
Merge pull request #2214 from tangly1024/feat/theme-game
Feat/theme game
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
# 可在此添加环境变量,去掉最左边的(# )注释即可
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"trailingComma": "none",
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 120,
|
||||
"bracketSpacing": true,
|
||||
"jsxSingleQuote": true,
|
||||
"jsxBracketSameLine": true
|
||||
}
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"trailingComma": "none",
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 80,
|
||||
"bracketSpacing": true,
|
||||
"jsxSingleQuote": true,
|
||||
"jsxBracketSameLine": true
|
||||
}
|
||||
|
||||
183
blog.config.js
183
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 // 版本号
|
||||
|
||||
@@ -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 <div className={`draggable ${moving ? 'cursor-grabbing' : 'cursor-grab'} select-none`} ref={draggableRef}>
|
||||
{children}
|
||||
</div>
|
||||
return (
|
||||
<div className={`draggable ${moving ? 'cursor-grabbing' : 'cursor-grab'} select-none`} ref={draggableRef}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Draggable.defaultProps = { left: 0, top: 0 }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 || {}
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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 <Slug {...props}/>
|
||||
return <Slug {...props} />
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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
|
||||
|
||||
@@ -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 <Slug {...props}/>
|
||||
return <Slug {...props} />
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 } })
|
||||
|
||||
@@ -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'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
/**
|
||||
* 在内存缓存中进行全文索引
|
||||
|
||||
@@ -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'
|
||||
|
||||
/**
|
||||
* 在内存缓存中进行全文索引
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 } })
|
||||
|
||||
48
public/games-external/common/index.htm
Normal file
48
public/games-external/common/index.htm
Normal file
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Full Screen iFrame</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#myIframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
/* 可选:移除边框 */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- <div style="position: absolute;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
background: white;">
|
||||
<button onclick="toggleFullScreen()">Toggle Full Screen</button>
|
||||
</div> -->
|
||||
|
||||
<iframe id="myIframe" allowfullscreen="allowfullscreen" allow="autoplay" scrolling="no"></iframe>
|
||||
|
||||
|
||||
<!-- https://letsplay247.github.io/cz.html?n=space-wars-battleground -->
|
||||
<script>
|
||||
var myParam = location.search.split('n=')[1]
|
||||
document.getElementById("myIframe").src = myParam;
|
||||
</script>
|
||||
<script src="/js/fullscreen.js" type="text/javascript"></script>
|
||||
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
||||
32
public/js/fullscreen.js
Normal file
32
public/js/fullscreen.js
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
.writing-vertical {
|
||||
writing-mode: vertical-rl; /* 竖向排列从右向左 */
|
||||
text-orientation: upright; /* 文字方向正常 */
|
||||
}
|
||||
|
||||
107
themes/game/components/AdBlockerDetect.js
Normal file
107
themes/game/components/AdBlockerDetect.js
Normal file
@@ -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 (
|
||||
<>
|
||||
<div className="fixed w-screen h-screen z-40 flex justify-center items-center bg-black bg-opacity-75 top-0 left-0">
|
||||
<div className="fc-dialog-content z-50 bg-white rounded-md p-4 max-w-md">
|
||||
<div className="fc-dialog-headline">
|
||||
<h1 className="fc-dialog-headline-text text-3xl">
|
||||
Please allow ads on our site
|
||||
</h1>
|
||||
</div>
|
||||
<hr className="my-4" />
|
||||
<div className="fc-dialog-body">
|
||||
<p className="fc-dialog-body-text text-xl">
|
||||
{
|
||||
"Looks like you're using an ad blocker. We rely on advertising to help fund our site."
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-center mt-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsAdBlocker(false)
|
||||
}}
|
||||
className="px-12 py-2 gap-2 bg-green-600 rounded text-white "
|
||||
>
|
||||
OK ({noticeCountdown})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
27
themes/game/components/Announcement.js
Normal file
27
themes/game/components/Announcement.js
Normal file
@@ -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 (
|
||||
<div className={className}>
|
||||
<section id='announcement-wrapper' className='mb-10'>
|
||||
{notice && (
|
||||
<div id='announcement-content'>
|
||||
<NotionPage post={notice} />
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
export default Announcement
|
||||
53
themes/game/components/ArticleLock.js
Normal file
53
themes/game/components/ArticleLock.js
Normal file
@@ -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 = `<div class='text-red-500 animate__shakeX animate__animated'>${locale.COMMON.PASSWORD_ERROR}</div>`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const passwordInputRef = useRef(null)
|
||||
useEffect(() => {
|
||||
// 选中密码输入框并将其聚焦
|
||||
passwordInputRef.current.focus()
|
||||
}, [])
|
||||
|
||||
return <div id='container' className='w-full flex justify-center items-center h-96 '>
|
||||
<div className='text-center space-y-3'>
|
||||
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
|
||||
<div className='flex'>
|
||||
<input id="password" type='password'
|
||||
onKeyDown={(e) => {
|
||||
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'
|
||||
></input>
|
||||
<div onClick={submitPassword} className="px-3 whitespace-nowrap cursor-pointer items-center justify-center py-2 rounded-r duration-300 bg-gray-300" >
|
||||
<i className={'duration-200 cursor-pointer fas fa-key dark:text-black'} > {locale.COMMON.SUBMIT}</i>
|
||||
</div>
|
||||
</div>
|
||||
<div id='tips'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
44
themes/game/components/BlogArchiveItem.js
Normal file
44
themes/game/components/BlogArchiveItem.js
Normal file
@@ -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 (
|
||||
<div key={archiveTitle}>
|
||||
<div id={archiveTitle} className="pt-16 pb-4 text-3xl dark:text-gray-300" >
|
||||
{archiveTitle}
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{archivePosts[archiveTitle].map(post => {
|
||||
const url = checkContainHttp(post.slug) ? sliceUrlFromHttp(post.slug) : `${siteConfig('SUB_PATH', '')}/${post.slug}`
|
||||
|
||||
return <li
|
||||
key={post.id}
|
||||
className="border-l-2 p-1 text-xs md:text-base items-center hover:scale-x-105 hover:border-gray-500 dark:hover:border-gray-300 dark:border-gray-400 transform duration-500"
|
||||
>
|
||||
<div id={post?.publishDay}>
|
||||
<span className="text-gray-400">
|
||||
{post.date?.start_date}
|
||||
</span>{' '}
|
||||
|
||||
<Link
|
||||
href={url}
|
||||
passHref
|
||||
className="dark:text-gray-400 dark:hover:text-gray-300 overflow-x-hidden hover:underline cursor-pointer text-gray-600">
|
||||
|
||||
{post.title}
|
||||
|
||||
</Link>
|
||||
</div>
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
38
themes/game/components/BlogListBar.js
Normal file
38
themes/game/components/BlogListBar.js
Normal file
@@ -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 (
|
||||
<div className='mb-4'>
|
||||
<div className='relative'>
|
||||
<input
|
||||
type='text'
|
||||
placeholder={tag ? `Search in #${tag}` : 'Search Articles'}
|
||||
className='outline-none block w-full border px-4 py-2 border-black bg-white text-black dark:bg-night dark:border-white dark:text-white'
|
||||
onChange={e => handleSearchChange(e.target.value)}
|
||||
/>
|
||||
<svg
|
||||
className='absolute right-3 top-3 h-5 w-5 text-black dark:text-white'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
fill='none'
|
||||
viewBox='0 0 24 24'
|
||||
stroke='currentColor'>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth='2'
|
||||
d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'></path>
|
||||
</svg>
|
||||
</div>
|
||||
<Tags {...props} />
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
25
themes/game/components/BlogListPage.js
Normal file
25
themes/game/components/BlogListPage.js
Normal file
@@ -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 (
|
||||
<>
|
||||
<div id='posts-wrapper' className='my-4 select-none'>
|
||||
<GameListIndexCombine {...props} />
|
||||
</div>
|
||||
|
||||
<PaginationSimple page={page} showNext={showNext} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
59
themes/game/components/BlogListScroll.js
Normal file
59
themes/game/components/BlogListScroll.js
Normal file
@@ -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 (
|
||||
<>
|
||||
<div id='posts-wrapper' className='my-4' ref={targetRef}>
|
||||
<GameListIndexCombine posts={postsToShow} />
|
||||
</div>
|
||||
|
||||
<div onClick={handleGetMore} className='w-full my-4 py-4 text-center cursor-pointer '>
|
||||
{' '}
|
||||
{hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
41
themes/game/components/BlogPost.js
Normal file
41
themes/game/components/BlogPost.js
Normal file
@@ -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 (
|
||||
(<Link href={url}>
|
||||
|
||||
<article key={post.id} className="mb-6 md:mb-8">
|
||||
<header className="flex flex-col justify-between md:flex-row md:items-baseline">
|
||||
<h2 className="text-lg md:text-xl font-medium mb-2 cursor-pointer text-black dark:text-gray-100">
|
||||
<NotionIcon icon={post.pageIcon} />{post.title}
|
||||
</h2>
|
||||
<time className="flex-shrink-0 text-gray-600 dark:text-gray-400">
|
||||
{post?.publishDay}
|
||||
</time>
|
||||
</header>
|
||||
<main>
|
||||
{!showPreview && <p className="hidden md:block leading-8 text-gray-700 dark:text-gray-300">
|
||||
{post.summary}
|
||||
</p>}
|
||||
{showPreview && post?.blockMap && (
|
||||
<div className="overflow-ellipsis truncate">
|
||||
<NotionPage post={post} />
|
||||
<hr className='border-dashed py-4'/>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</article>
|
||||
|
||||
</Link>)
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogPost
|
||||
29
themes/game/components/BlogPostBar.js
Normal file
29
themes/game/components/BlogPostBar.js
Normal file
@@ -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 (
|
||||
<div className='flex items-center text-xl mt-4 px-2'>
|
||||
<i className='mr-2 fas fa-tag' />
|
||||
{locale.COMMON.TAGS}:{tag}
|
||||
</div>
|
||||
)
|
||||
} else if (category) {
|
||||
return (
|
||||
<div className='flex items-center text-xl mt-4 px-2'>
|
||||
<i className='mr-2 fas fa-th' />
|
||||
{locale.COMMON.CATEGORY}:{category}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
33
themes/game/components/DarkModeButton.js
Normal file
33
themes/game/components/DarkModeButton.js
Normal file
@@ -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 (
|
||||
<div
|
||||
onClick={toggleDarkMode}
|
||||
className={`${className || ''} flex items-center`}>
|
||||
<i
|
||||
className={`w-6 mr-2 fas ${isDarkMode ? 'fa-sun' : 'fa-moon px-0.5'}`}
|
||||
/>
|
||||
{isDarkMode ? 'Dark Mode' : 'Light Mode'}{' '}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default DarkModeButton
|
||||
35
themes/game/components/ExampleRecentComments.js
Normal file
35
themes/game/components/ExampleRecentComments.js
Normal file
@@ -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 && <div>Loading...<i className='ml-2 fas fa-spinner animate-spin' /></div>}
|
||||
{!onLoading && comments && comments.length === 0 && <div>No Comments</div>}
|
||||
{!onLoading && comments && comments.length > 0 && comments.map((comment) => <div key={comment.objectId} className='pb-2'>
|
||||
<div className='dark:text-gray-300 text-gray-600 text-xs waline-recent-content wl-content' dangerouslySetInnerHTML={{ __html: comment.comment }} />
|
||||
<div className='dark:text-gray-400 text-gray-400 text-sm text-right cursor-pointer hover:text-red-500 hover:underline pt-1'><Link href={{ pathname: comment.url, hash: comment.objectId, query: { target: 'comment' } }}>--{comment.nick}</Link></div>
|
||||
</div>)}
|
||||
|
||||
</>
|
||||
}
|
||||
|
||||
export default ExampleRecentComments
|
||||
33
themes/game/components/Footer.js
Normal file
33
themes/game/components/Footer.js
Normal file
@@ -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 (
|
||||
<footer
|
||||
className={`z-10 relative mt-6 flex-shrink-0 m-auto w-full dark:text-gray-200 `}>
|
||||
<hr className='my-2 border-black dark:border-gray-100' />
|
||||
{/* 页面底部 */}
|
||||
<div className='w-full flex justify-between p-4 '>
|
||||
<p>
|
||||
© {siteConfig('TITLE')} {copyrightDate}
|
||||
</p>
|
||||
<p>{siteConfig('DESCRIPTION')}</p>
|
||||
|
||||
<span className='dark:text-gray-200 no-underline ml-4'>
|
||||
Powered by
|
||||
<a
|
||||
href='https://github.com/tangly1024/NotionNext'
|
||||
className=' hover:underline'>
|
||||
{' '}
|
||||
NotionNext {siteConfig('VERSION')}{' '}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
31
themes/game/components/FullScreen.js
Normal file
31
themes/game/components/FullScreen.js
Normal file
@@ -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 (
|
||||
<div
|
||||
className='group text-white w-full justify-center items-center flex rounded-lg m-2 md:m-0 p-2 hover:bg-gray-700 bg-[#1F2030] md:rounded-none md:bg-none'
|
||||
onClick={toggleFullScreen}>
|
||||
<i
|
||||
alt='full screen'
|
||||
title='full screen'
|
||||
className='cursor-pointer fas fa-expand group-hover:scale-125 transition-all duration-150 '
|
||||
/>
|
||||
<span className='h-full flex mx-2 md:hidden items-center select-none'>FullScreen</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
166
themes/game/components/GameListIndexCombine.js
Normal file
166
themes/game/components/GameListIndexCombine.js
Normal file
@@ -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<Component>
|
||||
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(<GameAd key={index} />)
|
||||
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(<GameItem key={index} item={item} isLargeCard={true} />)
|
||||
continue
|
||||
} else {
|
||||
groupItems.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
if (groupItems.length === 4) {
|
||||
components.push(<GameItemGroup key={index} items={groupItems} />)
|
||||
// 清空4合一卡片
|
||||
groupItems = []
|
||||
} else {
|
||||
// 剩余的4合一不满4个的给他放大卡
|
||||
while (groupItems.length > 0) {
|
||||
const item = groupItems.shift()
|
||||
components.push(<GameItem key={index++} item={item} isLargeCard={true} />)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while (gamesClone?.length > 0) {
|
||||
index++
|
||||
|
||||
if (index % 6 === 0) {
|
||||
components.push(<GameAd key={index} />)
|
||||
} else if (index % 2 === 0 && gamesClone?.length >= 4) {
|
||||
// 如果是偶数,则从游戏列表中退出4个组成大卡牌
|
||||
const groupItems = []
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
groupItems.push(gamesClone.shift())
|
||||
}
|
||||
components.push(<GameItemGroup key={index} items={groupItems} />)
|
||||
} else {
|
||||
const item = gamesClone.shift()
|
||||
components.push(<GameItem key={index} item={item} isLargeCard={true} />)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='game-list-wrapper flex justify-center w-full px-2'>
|
||||
<div className='game-grid mx-auto w-full h-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2'>
|
||||
{components?.map((ItemComponent, index) => {
|
||||
return ItemComponent
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 一个广告游戏大卡
|
||||
* @returns
|
||||
*/
|
||||
const GameAd = () => {
|
||||
return (
|
||||
<div className='card-group border border-gray-600 rounded game-ad h-[20rem] w-full overflow-hidden'>
|
||||
<AdSlot type='flow' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 4卡组成一个大卡
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const GameItemGroup = ({ items }) => {
|
||||
return (
|
||||
<div className='card-group h-[20rem] w-full grid grid-cols-2 grid-rows-2 gap-2'>
|
||||
{items.map((item, index) => (
|
||||
<GameItem key={index} item={item} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏=单卡
|
||||
* @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 (
|
||||
<Link
|
||||
href={`${url}`}
|
||||
onMouseOver={() => {
|
||||
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`}>
|
||||
<div className='text-center absolute bottom-0 invisible group-hover:bottom-2 group-hover:visible transition-all duration-200 text-white z-30'>
|
||||
{title}
|
||||
</div>
|
||||
<div className='h-1/2 w-full absolute left-0 bottom-0 z-20 opacity-0 group-hover:opacity-75 transition-all duration-200'>
|
||||
<div className='h-full w-full absolute bg-gradient-to-b from-transparent to-black'></div>
|
||||
</div>
|
||||
|
||||
{showType === 'video' && (
|
||||
<video
|
||||
className={`z-10 object-cover w-full ${isLargeCard ? 'h-[20rem]' : 'h-full'} absolute overflow-hidden`}
|
||||
loop='true'
|
||||
autoPlay
|
||||
preload='none'>
|
||||
<source src={video} type='video/mp4' />
|
||||
</video>
|
||||
)}
|
||||
<img
|
||||
className='w-full h-full absolute object-cover group-hover:scale-105 duration-100 transition-all'
|
||||
src={img}
|
||||
alt={title}
|
||||
/>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
74
themes/game/components/GameListNormal.js
Normal file
74
themes/game/components/GameListNormal.js
Normal file
@@ -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<Component>
|
||||
const components = []
|
||||
|
||||
let index = 0
|
||||
// 无限循环
|
||||
while (gamesClone?.length > 0 && index < maxCount) {
|
||||
const item = gamesClone.shift()
|
||||
components.push(<GameItem key={index} item={item} isLargeCard={true} />)
|
||||
index++
|
||||
continue
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='game-list-wrapper w-full'>
|
||||
<div className='game-grid mx-auto w-full h-full grid grid-cols-3 gap-2'>
|
||||
{components?.map((ItemComponent, index) => {
|
||||
return ItemComponent
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏=单卡
|
||||
* @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 (
|
||||
<Link
|
||||
href={`${url}`}
|
||||
onMouseOver={() => {
|
||||
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`}>
|
||||
<div className='absolute text-sm bottom-2 transition-all duration-200 text-white z-30'>{title}</div>
|
||||
<div className='h-1/2 w-full absolute left-0 bottom-0 z-20 opacity-75 transition-all duration-200'>
|
||||
<div className='h-full w-full absolute bg-gradient-to-b from-transparent to-black'></div>
|
||||
</div>
|
||||
|
||||
{showType === 'video' && (
|
||||
<video className='z-10 object-cover w-auto h-28 absolute overflow-hidden' loop='true' autoPlay preload='none'>
|
||||
<source src={video} type='video/mp4' />
|
||||
</video>
|
||||
)}
|
||||
<img className='w-full h-full absolute object-cover' src={img} alt={title} />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
82
themes/game/components/GameListRealate.js
Normal file
82
themes/game/components/GameListRealate.js
Normal file
@@ -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<Component>
|
||||
const components = []
|
||||
const maxCount = 24
|
||||
|
||||
let index = 0
|
||||
// 无限循环
|
||||
while (gamesClone?.length > 0 && index < maxCount) {
|
||||
const item = gamesClone.shift()
|
||||
components.push(<GameItem key={index} item={item} isLargeCard={true} />)
|
||||
index++
|
||||
continue
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='game-list-wrapper w-full max-w-full overflow-x-auto'>
|
||||
<div className='game-grid grid grid-flow-col justify-start gap-2'>
|
||||
{components?.map((ItemComponent, index) => {
|
||||
return ItemComponent
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏=单卡
|
||||
* @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 (
|
||||
<Link
|
||||
href={`${url}`}
|
||||
onMouseOver={() => {
|
||||
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`}>
|
||||
<div className='text-sm text-center absolute bottom-0 invisible group-hover:bottom-2 group-hover:visible transition-all duration-200 text-white z-30'>
|
||||
{title}
|
||||
</div>
|
||||
<div className='h-1/2 w-full absolute left-0 bottom-0 z-20 opacity-0 group-hover:opacity-75 transition-all duration-200'>
|
||||
<div className='h-full w-full absolute bg-gradient-to-b from-transparent to-black'></div>
|
||||
</div>
|
||||
|
||||
{showType === 'video' && (
|
||||
<video className={`z-10 object-cover w-full h-24 absolute overflow-hidden`} loop='true' autoPlay preload='none'>
|
||||
<source src={video} type='video/mp4' />
|
||||
</video>
|
||||
)}
|
||||
<img
|
||||
className='w-24 h-24 absolute object-cover group-hover:scale-105 duration-100 transition-all'
|
||||
src={img}
|
||||
alt={title}
|
||||
/>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
89
themes/game/components/GameListRecent.js
Normal file
89
themes/game/components/GameListRecent.js
Normal file
@@ -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<Component>
|
||||
const components = []
|
||||
|
||||
let index = 0
|
||||
// 无限循环
|
||||
while (gamesClone?.length > 0 && index < maxCount) {
|
||||
const item = gamesClone?.shift()
|
||||
if (item) {
|
||||
components.push(<GameItem key={index} item={item} isLargeCard={true} />)
|
||||
index++
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (components.length === 0) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='game-list-recent-wrapper w-full max-w-full overflow-x-auto pt-4 px-2'>
|
||||
<div className='game-grid md:flex grid grid-flow-col gap-2'>
|
||||
{components?.map((ItemComponent, index) => {
|
||||
return ItemComponent
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏=单卡
|
||||
* @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 (
|
||||
<a
|
||||
href={`${url}`}
|
||||
onMouseOver={() => {
|
||||
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`}>
|
||||
<div className='absolute right-0.5 top-1 z-20'>
|
||||
<i className='fas fa-clock-rotate-left w-6 h-6 flex items-center justify-center shadow rounded-full bg-white text-blue-500 text-sm' />
|
||||
</div>
|
||||
<div className='absolute text-sm bottom-2 transition-all duration-200 text-white z-30'>{title}</div>
|
||||
<div className='h-1/2 w-full absolute left-0 bottom-0 z-20 opacity-75 transition-all duration-200'>
|
||||
<div className='h-full w-full absolute bg-gradient-to-b from-transparent to-black'></div>
|
||||
</div>
|
||||
|
||||
{showType === 'video' && (
|
||||
<video className='z-10 object-cover w-auto h-28 absolute overflow-hidden' loop='true' autoPlay preload='none'>
|
||||
<source src={video} type='video/mp4' />
|
||||
</video>
|
||||
)}
|
||||
<img
|
||||
className='w-full h-full absolute object-cover group-hover:scale-105 duration-100 transition-all'
|
||||
src={img}
|
||||
alt={title}
|
||||
/>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
41
themes/game/components/GroupCategory.js
Normal file
41
themes/game/components/GroupCategory.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
function GroupCategory({ currentCategory, categoryOptions }) {
|
||||
if (!categoryOptions) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link className='mx-2' href='/category'>
|
||||
<i className='fas fa-bars' />
|
||||
</Link>
|
||||
<div id='category-list' className='dark:border-gray-600 flex py-1'>
|
||||
{categoryOptions.map(category => {
|
||||
const selected = currentCategory === category.name
|
||||
return (
|
||||
<Link
|
||||
key={category.name}
|
||||
href={`/category/${category.name}`}
|
||||
passHref
|
||||
className={` ${
|
||||
selected
|
||||
? 'bg-green-500 text-white '
|
||||
: 'dark:text-gray-300 hover:bg-green-500 rounded-lg hover:text-white'
|
||||
} whitespace-nowrap overflow-ellipsis w-full items-center px-2 cursor-pointer py-1 font-bold`}>
|
||||
{/* <i
|
||||
className={`${selected ? 'text-white fa-folder-open' : 'fa-folder text-gray-400'} fas mr-2`}
|
||||
/> */}
|
||||
{category.name}
|
||||
{/* <span className='text-xs flex items-start pl-2 h-full'>
|
||||
{category.count}
|
||||
</span> */}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GroupCategory
|
||||
28
themes/game/components/GroupTag.js
Normal file
28
themes/game/components/GroupTag.js
Normal file
@@ -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 (
|
||||
<>
|
||||
<Link href='/tag'>
|
||||
<i className='fas fa-tags p-2' />
|
||||
</Link>
|
||||
<div id='tags-group' className='flex flex-wrap p-1 gap-2'>
|
||||
{tagOptions?.slice(0, 20)?.map(tag => {
|
||||
const selected = tag.name === currentTag
|
||||
return <TagItemMini key={tag.name} tag={tag} selected={selected} />
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GroupTag
|
||||
25
themes/game/components/Header.js
Normal file
25
themes/game/components/Header.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useGameGlobal } from '..'
|
||||
import Logo from './Logo'
|
||||
|
||||
/**
|
||||
* 顶栏
|
||||
* @returns
|
||||
*/
|
||||
export default function Header() {
|
||||
const { setSideBarVisible } = useGameGlobal()
|
||||
return (
|
||||
<header className='z-20'>
|
||||
<div className='w-full h-16 rounded-md bg-white shadow-md hover:shadow-xl transition-shadow duration-200 dark:bg-[#1F2030] flex justify-between items-center px-4'>
|
||||
<Logo />
|
||||
|
||||
<button
|
||||
className='flex xl:hidden'
|
||||
onClick={() => {
|
||||
setSideBarVisible(true)
|
||||
}}>
|
||||
<i className='fas fa-search' />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
18
themes/game/components/JumpToTopButton.js
Normal file
18
themes/game/components/JumpToTopButton.js
Normal file
@@ -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 <div title={locale.POST.TOP} className='cursor-pointer p-2 text-center' onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
||||
><i className='fas fa-angle-up text-2xl' />
|
||||
</div>
|
||||
}
|
||||
|
||||
export default JumpToTopButton
|
||||
14
themes/game/components/Logo.js
Normal file
14
themes/game/components/Logo.js
Normal file
@@ -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 (
|
||||
<Link passHref href='/' className='logo rounded cursor-pointer flex flex-col items-center'>
|
||||
<div className='w-full'>
|
||||
<h1 className='text-2xl dark:text-white font-bold font-serif'>{siteConfig('TITLE')}</h1>
|
||||
<h2 className='text-xs text-gray-400 whitespace-nowrap'>{siteConfig('BIO')}</h2>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
11
themes/game/components/LogoMini.js
Normal file
11
themes/game/components/LogoMini.js
Normal file
@@ -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 (
|
||||
<Link href='/' className='logo rounded cursor-pointer flex items-center text-xl text-white font-bold font-serif'>
|
||||
{siteConfig('TITLE')?.charAt(0)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
55
themes/game/components/MenuItemCollapse.js
Normal file
55
themes/game/components/MenuItemCollapse.js
Normal file
@@ -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 <>
|
||||
<div className='w-full px-4 py-2 text-left dark:bg-hexo-black-gray dark:border-black' onClick={toggleShow} >
|
||||
{!hasSubMenu && <Link
|
||||
href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
|
||||
className="font-extralight flex justify-between pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest pb-1">
|
||||
<span className=' hover:text-red-400 transition-all items-center duration-200'>{link?.icon && <span className='mr-2'><i className={link.icon}/></span>}{link?.name}</span>
|
||||
</Link>}
|
||||
{hasSubMenu && <div
|
||||
onClick={hasSubMenu ? toggleOpenSubMenu : null}
|
||||
className="font-extralight flex justify-between pl-2 pr-4 cursor-pointer dark:text-gray-200 no-underline tracking-widest pb-1">
|
||||
<span className=' hover:text-red-400 transition-all items-center duration-200'>{link?.icon && <span className='mr-2'><i className={link.icon}/></span>}{link?.name}</span>
|
||||
<i className='px-2 fa fa-plus text-gray-400'></i>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
{/* 折叠子菜单 */}
|
||||
{hasSubMenu && <Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
|
||||
{link.subMenus.map((sLink, index) => {
|
||||
return <div key={index} className='font-extralight dark:bg-black text-left px-10 justify-start bg-gray-50 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 border-b dark:border-gray-800 py-3 pr-6'>
|
||||
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
|
||||
<span className='text-xs'>{sLink.title}</span>
|
||||
</Link>
|
||||
</div>
|
||||
})}
|
||||
</Collapse>}
|
||||
</>
|
||||
}
|
||||
68
themes/game/components/MenuItemDrop.js
Normal file
68
themes/game/components/MenuItemDrop.js
Normal file
@@ -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 (
|
||||
<li>
|
||||
<div
|
||||
className='cursor-pointer relative'
|
||||
onMouseOver={() => changeShow(true)}
|
||||
onMouseOut={() => changeShow(false)}>
|
||||
{!hasSubMenu && (
|
||||
<div className='dark:text-gray-50 nav hover:scale-105 transition-transform duration-200'>
|
||||
<Link
|
||||
href={link?.to}
|
||||
className='flex flex-nowrap'
|
||||
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
|
||||
<div className='w-6 mr-2 text-center'>
|
||||
{link?.icon && <i className={link?.icon} />}
|
||||
</div>
|
||||
{link?.name}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasSubMenu && (
|
||||
<div className='dark:text-gray-50 nav'>
|
||||
{link?.icon && <i className={`${link?.icon} w-6 text-center`} />}{' '}
|
||||
{link?.name}
|
||||
<i
|
||||
className={`absolute right-0 top-0 px-2 h-full flex items-center fas fa-chevron-left duration-500 transition-all ${show ? ' rotate-180' : ''} `}></i>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 子菜单 */}
|
||||
{hasSubMenu && (
|
||||
<ul
|
||||
className={`${show ? 'visible opacity-100 -left-5 ml-40' : 'invisible opacity-0 -left-4 '} rounded shadow-md z-30 -mt-2 py-2 px-4 absolute top-0 hover:scale-105 transition-all duration-200 border-gray-100 bg-white dark:bg-black`}>
|
||||
{link.subMenus.map((sLink, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className='text-gray-700 dark:text-gray-200 tracking-widest transition-all duration-200 '>
|
||||
<Link
|
||||
href={sLink.to}
|
||||
target={
|
||||
link?.to?.indexOf('http') === 0 ? '_blank' : '_self'
|
||||
}>
|
||||
<span className='text-sm text-nowrap font-extralight'>
|
||||
{link?.icon && <i className={sLink?.icon}> </i>}
|
||||
{sLink.title}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
76
themes/game/components/MenuList.js
Normal file
76
themes/game/components/MenuList.js
Normal file
@@ -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 (
|
||||
<ul
|
||||
className={`dark:text-white p-4 space-y-4 shadow-md hover:shadow-xl transition-shadow duration-200 bg-white dark:bg-hexo-black-gray my-4 rounded-md`}>
|
||||
<li>
|
||||
<button
|
||||
className='flex items-center hover:scale-105 transition-transform duration-200'
|
||||
onClick={() => {
|
||||
setSideBarVisible(true)
|
||||
}}>
|
||||
<i className='fas fa-search w-6 mr-2' />
|
||||
<span>Search</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button className='flex items-center hover:scale-105 transition-transform duration-200'>
|
||||
{/* 切换深色模式 */}
|
||||
<DarkModeButton className='text-center' />
|
||||
</button>
|
||||
</li>
|
||||
{links?.length > 0 && <hr />}
|
||||
|
||||
{links?.map(
|
||||
(link, index) =>
|
||||
link && link.show && <MenuItemDrop key={index} link={link} />
|
||||
)}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
54
themes/game/components/PaginationSimple.js
Normal file
54
themes/game/components/PaginationSimple.js
Normal file
@@ -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 (
|
||||
<div className='my-10 flex justify-between font-medium text-black dark:text-gray-100 space-x-2'>
|
||||
<Link
|
||||
href={{
|
||||
pathname:
|
||||
currentPage === 2
|
||||
? `${pagePrefix}/`
|
||||
: `${pagePrefix}/page/${currentPage - 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
passHref
|
||||
rel='prev'
|
||||
className={`${
|
||||
currentPage === 1 ? 'invisible' : 'visible'
|
||||
} text-center w-full duration-200 px-4 py-2 hover:border-black dark:border-hexo-black-gray border-b-2 hover:font-bold`}>
|
||||
←{locale.PAGINATION.PREV}
|
||||
</Link>
|
||||
<Link
|
||||
href={{
|
||||
pathname: `${pagePrefix}/page/${currentPage + 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
passHref
|
||||
rel='next'
|
||||
className={`${
|
||||
showNext ? 'visible' : 'invisible'
|
||||
} text-center w-full duration-200 px-4 py-2 hover:border-black dark:border-hexo-black-gray border-b-2 hover:font-bold`}>
|
||||
{locale.PAGINATION.NEXT}→
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaginationSimple
|
||||
53
themes/game/components/PostInfo.js
Normal file
53
themes/game/components/PostInfo.js
Normal file
@@ -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 (
|
||||
<section className='flex-wrap flex mt-2 text-gray--600 dark:text-gray-400 font-light leading-8'>
|
||||
<div>
|
||||
<div>
|
||||
{post?.type !== 'Page' && (
|
||||
<>
|
||||
<Link
|
||||
href={`/category/${post?.category}`}
|
||||
passHref
|
||||
className='cursor-pointer text-xs font-bold hover:underline mr-2'>
|
||||
{post?.category}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h1 className='font-bold text-3xl text-black dark:text-white'>
|
||||
<NotionIcon icon={post?.pageIcon} />
|
||||
{post?.title}
|
||||
</h1>
|
||||
|
||||
{post?.type !== 'Page' && (
|
||||
<>
|
||||
<nav className='flex my-2 items-start text-gray-500 dark:text-gray-400'>
|
||||
{post?.tags && (
|
||||
<div className='flex flex-wrap max-w-full overflow-x-auto article-tags'>
|
||||
{post?.tags.map(tag => (
|
||||
<TagItem key={tag} tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<span className='hidden busuanzi_container_page_pv mr-2'>
|
||||
<i className='mr-1 fas fa-fire' />
|
||||
|
||||
<span className='mr-2 busuanzi_value_page_pv' />
|
||||
</span>
|
||||
</nav>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
26
themes/game/components/RandomPostButton.js
Normal file
26
themes/game/components/RandomPostButton.js
Normal file
@@ -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 (
|
||||
<div title={locale.MENU.WALK_AROUND} className='cursor-pointer hover:bg-black hover:bg-opacity-10 rounded-full w-10 h-10 flex justify-center items-center duration-200 transition-all' onClick={handleClick}>
|
||||
<i className="fa-solid fa-podcast"></i>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
34
themes/game/components/SearchButton.js
Normal file
34
themes/game/components/SearchButton.js
Normal file
@@ -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 (
|
||||
<>
|
||||
<div
|
||||
onClick={handleSearch}
|
||||
title={locale.NAV.SEARCH}
|
||||
alt={locale.NAV.SEARCH}
|
||||
className='cursor-pointer hover:bg-black hover:bg-opacity-10 rounded-full w-10 h-10 flex justify-center items-center duration-200 transition-all'>
|
||||
<i title={locale.NAV.SEARCH} className='fa-solid fa-magnifying-glass' />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
88
themes/game/components/SearchInput.js
Normal file
88
themes/game/components/SearchInput.js
Normal file
@@ -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 <section className='flex w-full bg-gray-100'>
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type='text'
|
||||
placeholder={tag ? `${locale.SEARCH.TAGS} #${tag}` : `${locale.SEARCH.ARTICLES}`}
|
||||
className={'outline-none w-full text-sm pl-4 transition focus:shadow-lg font-light leading-10 text-black bg-gray-100 dark:bg-gray-900 dark:text-white'}
|
||||
onKeyUp={handleKeyUp}
|
||||
onCompositionStart={lockSearchInput}
|
||||
onCompositionUpdate={lockSearchInput}
|
||||
onCompositionEnd={unLockSearchInput}
|
||||
onChange={e => updateSearchKey(e.target.value)}
|
||||
defaultValue={keyword || ''}
|
||||
/>
|
||||
|
||||
<div className='-ml-8 cursor-pointer float-right items-center justify-center py-2'
|
||||
onClick={handleSearch}>
|
||||
<i className={'hover:text-black transform duration-200 text-gray-500 cursor-pointer fas fa-search'} />
|
||||
</div>
|
||||
|
||||
{(showClean &&
|
||||
<div className='-ml-12 cursor-pointer dark:bg-gray-600 dark:hover:bg-gray-800 float-right items-center justify-center py-2'>
|
||||
<i className='hover:text-black transform duration-200 text-gray-400 cursor-pointer fas fa-times' onClick={cleanSearch} />
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
}
|
||||
|
||||
export default SearchInput
|
||||
65
themes/game/components/SideBar.js
Normal file
65
themes/game/components/SideBar.js
Normal file
@@ -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 (
|
||||
<div className="w-full md:w-64 sticky top-8">
|
||||
|
||||
<aside className="rounded shadow overflow-hidden mb-6">
|
||||
<h3 className="text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b">{locale.COMMON.CATEGORY}</h3>
|
||||
|
||||
<div className="p-4">
|
||||
<ul className="list-reset leading-normal">
|
||||
{categories?.map(category => {
|
||||
return (
|
||||
<Link
|
||||
key={category.name}
|
||||
href={`/category/${category.name}`}
|
||||
passHref
|
||||
legacyBehavior>
|
||||
<li> <a href="#" className="text-gray-darkest text-sm">{category.name}({category.count})</a></li>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
<aside className="rounded shadow overflow-hidden mb-6">
|
||||
<h3 className="text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b">{locale.COMMON.LATEST_POSTS}</h3>
|
||||
|
||||
<div className="p-4">
|
||||
<ul className="list-reset leading-normal">
|
||||
{latestPosts?.map(p => {
|
||||
return (
|
||||
<Link key={p.id} href={`/${p.slug}`} passHref legacyBehavior>
|
||||
<li> <a href="#" className="text-gray-darkest text-sm">{p.title}</a></li>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{siteConfig('COMMENT_WALINE_SERVER_URL') && JSON.parse(siteConfig('COMMENT_WALINE_RECENT')) && <aside className="rounded shadow overflow-hidden mb-6">
|
||||
<h3 className="text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b">{locale.COMMON.RECENT_COMMENTS}</h3>
|
||||
|
||||
<div className="p-4">
|
||||
<ExampleRecentComments/>
|
||||
</div>
|
||||
</aside>}
|
||||
|
||||
<aside className="rounded overflow-hidden mb-6">
|
||||
<Live2D />
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
65
themes/game/components/SideBarContent.js
Normal file
65
themes/game/components/SideBarContent.js
Normal file
@@ -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 (
|
||||
<div className='px-3'>
|
||||
<div className='py-2 flex justify-between'>
|
||||
<Logo />
|
||||
<button
|
||||
onClick={() => {
|
||||
setSideBarVisible(false)
|
||||
}}>
|
||||
<i className='fas fa-times' />
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
autoFocus
|
||||
id='search-input'
|
||||
ref={inputRef} // 将引用绑定到输入框
|
||||
className='w-full h-12 rounded px-3 text-black'
|
||||
onChange={handleSearch}
|
||||
placeholder='Input the name of game'></input>
|
||||
<div className='py-4'>
|
||||
<GameListNormal games={filterGames} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
61
themes/game/components/SideBarDrawer.js
Normal file
61
themes/game/components/SideBarDrawer.js
Normal file
@@ -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 (
|
||||
<div id='sidebar-wrapper' className={`top-0 ${className}`}>
|
||||
<div
|
||||
id='sidebar-drawer'
|
||||
className={`${isOpen ? 'ml-0 visible opacity-100' : '-ml-96 invisible opacity-0'} w-96 bg-[#83FFE7] dark:bg-black shadow-black shadow-lg flex flex-col duration-300 fixed h-full left-0 overflow-y-scroll scroll-hidden top-0 z-30`}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* 背景蒙版 */}
|
||||
<div
|
||||
id='sidebar-drawer-background'
|
||||
onClick={() => {
|
||||
switchSideDrawerVisible(false)
|
||||
}}
|
||||
className={`${isOpen ? 'visible opacity-100' : 'invisible opacity-0 '} animate__animated animate__fadeIn fixed top-0 duration-300 left-0 z-20 w-full h-full bg-black/70`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default SideBarDrawer
|
||||
29
themes/game/components/SvgIcon.js
Normal file
29
themes/game/components/SvgIcon.js
Normal file
@@ -0,0 +1,29 @@
|
||||
export const SvgIcon = () => {
|
||||
return <svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
width="24"
|
||||
height="24"
|
||||
className="fill-current text-black dark:text-white"
|
||||
/>
|
||||
<rect width="24" height="24" fill="url(#paint0_radial)" />
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="paint0_radial"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="rotate(45) scale(39.598)"
|
||||
>
|
||||
<stop stopColor="#CFCFCF" stopOpacity="0.6" />
|
||||
<stop offset="1" stopColor="#E9E9E9" stopOpacity="0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
}
|
||||
11
themes/game/components/TagItem.js
Normal file
11
themes/game/components/TagItem.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
const TagItem = ({ tag }) => (
|
||||
<Link href={`/tag/${encodeURIComponent(tag)}`}>
|
||||
<p className='cursor-pointer hover:bg-gray-50 dark:hover:bg-hexo-black-gray mr-1 rounded-full px-2 py-1 border leading-none text-sm dark:border-gray-600'>
|
||||
{tag}
|
||||
</p>
|
||||
</Link>
|
||||
)
|
||||
|
||||
export default TagItem
|
||||
21
themes/game/components/TagItemMini.js
Normal file
21
themes/game/components/TagItemMini.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
const TagItemMini = ({ tag, selected = false }) => {
|
||||
return (
|
||||
<Link
|
||||
key={tag}
|
||||
href={selected ? '/' : `/tag/${encodeURIComponent(tag.name)}`}
|
||||
className={` rounded hover:text-white hover:bg-green-500 text-black dark:text-white dark:bg-gray-800 py-0.5 px-1 `}
|
||||
passHref>
|
||||
{/* # {tag.name} */}
|
||||
<span className='flex flex-nowrap cursor-pointer'>
|
||||
# <span>{tag.name}</span>{' '}
|
||||
<span className='h-full flex items-start text-xs ml-1'>
|
||||
{tag.count ? `${tag.count}` : ''}
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagItemMini
|
||||
38
themes/game/components/Tags.js
Normal file
38
themes/game/components/Tags.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
const Tags = props => {
|
||||
const { tagOptions, tag } = props
|
||||
const currentTag = tag
|
||||
if (!tagOptions) return null
|
||||
return (
|
||||
<div className="tag-container">
|
||||
<ul className="flex max-w-full mt-4 overflow-x-auto">
|
||||
{Object.keys(tagOptions).map(key => {
|
||||
const tag = tagOptions[key]
|
||||
const selected = tag.name === currentTag
|
||||
return (
|
||||
<li
|
||||
key={tag.id}
|
||||
className={`mr-3 font-medium border whitespace-nowrap dark:text-gray-300 ${
|
||||
selected
|
||||
? 'text-white bg-black border-black dark:bg-gray-600 dark:border-gray-600'
|
||||
: 'bg-gray-100 border-gray-100 text-gray-400 dark:bg-night dark:border-gray-800'
|
||||
}`}
|
||||
>
|
||||
<Link
|
||||
key={tag.id}
|
||||
href={selected ? '/search' : `/tag/${encodeURIComponent(tag.name)}`}
|
||||
className="px-4 py-2 block">
|
||||
|
||||
{`${tag.name} (${tag.count})`}
|
||||
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tags
|
||||
19
themes/game/components/Title.js
Normal file
19
themes/game/components/Title.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
/**
|
||||
* 标题栏
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
export const Title = (props) => {
|
||||
const { post } = props
|
||||
const title = post?.title || siteConfig('DESCRIPTION')
|
||||
const description = post?.description || siteConfig('AUTHOR')
|
||||
|
||||
return <div className="text-center px-6 py-12 mb-6 bg-gray-100 dark:bg-hexo-black-gray dark:border-hexo-black-gray border-b">
|
||||
<h1 className=" text-xl md:text-4xl pb-4">{title}</h1>
|
||||
<p className="leading-loose text-gray-dark">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
18
themes/game/config.js
Normal file
18
themes/game/config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const CONFIG = {
|
||||
GAME_NAV_NOTION_ICON: true, // 是否读取Notion图标作为站点头像 ; 否则默认显示黑色SVG方块
|
||||
|
||||
GAME_RECOMMEND_TAG: 'Recommend', // 打了此Tag,被视为推荐
|
||||
GAME_INDEX_EXPAND_RECOMMEND: true, // 首页列表是否将推荐游戏放大,否则随机放大。
|
||||
|
||||
// 特殊菜单
|
||||
GAME_MENU_RANDOM_POST: true, // 是否显示随机跳转文章按钮
|
||||
GAME_MENU_SEARCH_BUTTON: true, // 是否显示搜索按钮,该按钮支持Algolia搜索
|
||||
|
||||
// 默认菜单配置 (开启自定义菜单后,以下配置则失效,请在Notion中自行配置菜单)
|
||||
GAME_MENU_CATEGORY: false, // 显示分类
|
||||
GAME_MENU_TAG: true, // 显示标签
|
||||
GAME_MENU_ARCHIVE: false, // 显示归档
|
||||
GAME_MENU_SEARCH: true, // 显示搜索
|
||||
GAME_MENU_RSS: false // 显示订阅
|
||||
}
|
||||
export default CONFIG
|
||||
523
themes/game/index.js
Normal file
523
themes/game/index.js
Normal file
@@ -0,0 +1,523 @@
|
||||
import Comment from '@/components/Comment'
|
||||
import { Draggable } from '@/components/Draggable'
|
||||
import { AdSlot } from '@/components/GoogleAdsense'
|
||||
import replaceSearchResult from '@/components/Mark'
|
||||
import NotionPage from '@/components/NotionPage'
|
||||
import ShareBar from '@/components/ShareBar'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { deepClone, isBrowser, shuffleArray } from '@/lib/utils'
|
||||
import Link from 'next/link'
|
||||
import { createContext, useContext, useEffect, useRef, useState } from 'react'
|
||||
import Announcement from './components/Announcement'
|
||||
import { ArticleLock } from './components/ArticleLock'
|
||||
import BlogArchiveItem from './components/BlogArchiveItem'
|
||||
import { BlogListPage } from './components/BlogListPage'
|
||||
import { BlogListScroll } from './components/BlogListScroll'
|
||||
import BlogPostBar from './components/BlogPostBar'
|
||||
import { Footer } from './components/Footer'
|
||||
import FullScreen from './components/FullScreen'
|
||||
import { GameListIndexCombine } from './components/GameListIndexCombine'
|
||||
import { GameListRelate } from './components/GameListRealate'
|
||||
import { GameListRecent } from './components/GameListRecent'
|
||||
import GroupCategory from './components/GroupCategory'
|
||||
import GroupTag from './components/GroupTag'
|
||||
import Header from './components/Header'
|
||||
import { MenuList } from './components/MenuList'
|
||||
import PostInfo from './components/PostInfo'
|
||||
import SideBarContent from './components/SideBarContent'
|
||||
import SideBarDrawer from './components/SideBarDrawer'
|
||||
import CONFIG from './config'
|
||||
import { Style } from './style'
|
||||
|
||||
// const AlgoliaSearchModal = dynamic(() => import('@/components/AlgoliaSearchModal'), { ssr: false })
|
||||
|
||||
// 主题全局状态
|
||||
const ThemeGlobalGame = createContext()
|
||||
export const useGameGlobal = () => useContext(ThemeGlobalGame)
|
||||
|
||||
/**
|
||||
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
|
||||
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const LayoutBase = props => {
|
||||
const { allNavPages, children } = props
|
||||
|
||||
// const fullWidth = post?.fullWidth ?? false
|
||||
// const { onLoading } = useGlobal()
|
||||
const searchModal = useRef(null)
|
||||
// 在列表中进行实时过滤
|
||||
const [filterKey, setFilterKey] = useState('')
|
||||
|
||||
const [filterGames, setFilterGames] = useState(
|
||||
deepClone(
|
||||
allNavPages?.filter(item =>
|
||||
item.tags?.some(
|
||||
t => t === siteConfig('GAME_RECOMMEND_TAG', 'Recommend', CONFIG)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
const [recentGames, setRecentGames] = useState([])
|
||||
const [sideBarVisible, setSideBarVisible] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setRecentGames(
|
||||
localStorage.getItem('recent_games')
|
||||
? JSON.parse(localStorage.getItem('recent_games'))
|
||||
: []
|
||||
)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ThemeGlobalGame.Provider
|
||||
value={{
|
||||
searchModal,
|
||||
filterKey,
|
||||
setFilterKey,
|
||||
recentGames,
|
||||
setRecentGames,
|
||||
filterGames,
|
||||
setFilterGames,
|
||||
sideBarVisible,
|
||||
setSideBarVisible
|
||||
}}>
|
||||
<div
|
||||
id='theme-game'
|
||||
className={`${siteConfig('FONT_STYLE')} w-full h-full min-h-screen justify-center bg-[#83FFE7] dark:bg-black dark:text-gray-300 scroll-smooth`}>
|
||||
<Style />
|
||||
|
||||
{/* 左右布局 */}
|
||||
<div
|
||||
id='wrapper'
|
||||
className={'relative flex justify-between w-full h-full mx-auto'}>
|
||||
{/* PC端左侧 */}
|
||||
<div className='w-52 hidden xl:block relative z-10'>
|
||||
<div className='py-4 px-2 sticky top-0 h-screen flex flex-col justify-between'>
|
||||
<div className='select-none'>
|
||||
{/* 抬头logo等 */}
|
||||
<Header />
|
||||
{/* 菜单栏 */}
|
||||
<MenuList {...props} />
|
||||
</div>
|
||||
|
||||
{/* 左侧广告栏目 */}
|
||||
<div className='w-full'>
|
||||
<AdSlot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧 */}
|
||||
<main className='flex-grow w-full h-full flex flex-col min-h-screen overflow-x-auto'>
|
||||
<div className='flex-grow h-full'>{children}</div>
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<SideBarDrawer
|
||||
isOpen={sideBarVisible}
|
||||
onClose={() => {
|
||||
setSideBarVisible(false)
|
||||
}}>
|
||||
<SideBarContent />
|
||||
</SideBarDrawer>
|
||||
</div>
|
||||
</ThemeGlobalGame.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页
|
||||
* 首页是个博客列表,加上顶部嵌入一个公告
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutIndex = props => {
|
||||
const { tagOptions, currentTag, categoryOptions, currentCategory } = props
|
||||
return (
|
||||
<>
|
||||
{/* 首页移动端顶部导航 */}
|
||||
<div className='p-2 xl:hidden'>
|
||||
<Header />
|
||||
</div>
|
||||
{/* 最近游戏 */}
|
||||
<GameListRecent />
|
||||
{/* 游戏列表 */}
|
||||
<LayoutPostList {...props} />
|
||||
|
||||
{/* 主区域下方 */}
|
||||
<div className='w-full bg-white dark:bg-hexo-black-gray rounded-lg p-2'>
|
||||
{/* 标签汇总 */}
|
||||
<GroupCategory
|
||||
categoryOptions={categoryOptions}
|
||||
currentCategory={currentCategory}
|
||||
/>
|
||||
<hr />
|
||||
<GroupTag tagOptions={tagOptions} currentTag={currentTag} />
|
||||
</div>
|
||||
|
||||
{/* 广告 */}
|
||||
<div className='w-full'>
|
||||
<AdSlot type='in-article' />
|
||||
</div>
|
||||
|
||||
{/* 站点公告信息 */}
|
||||
<div className='w-full bg-white dark:bg-hexo-black-gray rounded-lg p-2'>
|
||||
<Announcement {...props} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 博客列表
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutPostList = props => {
|
||||
const { posts } = props
|
||||
const { filterKey } = useGameGlobal()
|
||||
let filteredBlogPosts = []
|
||||
if (filterKey && posts) {
|
||||
filteredBlogPosts = posts.filter(post => {
|
||||
const tagContent = post?.tags ? post?.tags.join(' ') : ''
|
||||
const searchContent = post.title + post.summary + tagContent
|
||||
return searchContent.toLowerCase().includes(filterKey.toLowerCase())
|
||||
})
|
||||
} else {
|
||||
filteredBlogPosts = deepClone(posts)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<BlogPostBar {...props} />
|
||||
{siteConfig('POST_LIST_STYLE') === 'page' ? (
|
||||
<BlogListPage posts={filteredBlogPosts} {...props} />
|
||||
) : (
|
||||
<BlogListScroll posts={filteredBlogPosts} {...props} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
* 页面是博客列表,上方嵌入一个搜索引导条
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutSearch = props => {
|
||||
const { keyword, posts } = props
|
||||
useEffect(() => {
|
||||
if (isBrowser) {
|
||||
replaceSearchResult({
|
||||
doms: document.getElementById('posts-wrapper'),
|
||||
search: keyword,
|
||||
target: {
|
||||
element: 'span',
|
||||
className: 'text-red-500 border-b border-dashed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 在列表中进行实时过滤
|
||||
const { filterKey } = useGameGlobal()
|
||||
let filteredBlogPosts = []
|
||||
if (filterKey && posts) {
|
||||
filteredBlogPosts = posts.filter(post => {
|
||||
const tagContent = post?.tags ? post?.tags.join(' ') : ''
|
||||
const searchContent = post.title + post.summary + tagContent
|
||||
return searchContent.toLowerCase().includes(filterKey.toLowerCase())
|
||||
})
|
||||
} else {
|
||||
filteredBlogPosts = deepClone(posts)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{siteConfig('POST_LIST_STYLE') === 'page' ? (
|
||||
<BlogListPage {...props} posts={filteredBlogPosts} />
|
||||
) : (
|
||||
<BlogListScroll {...props} posts={filteredBlogPosts} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 归档
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutArchive = props => {
|
||||
const { archivePosts } = props
|
||||
return (
|
||||
<>
|
||||
<div className='mb-10 pb-20 md:py-12 p-3 min-h-screen w-full'>
|
||||
{Object.keys(archivePosts).map(archiveTitle => (
|
||||
<BlogArchiveItem
|
||||
key={archiveTitle}
|
||||
archiveTitle={archiveTitle}
|
||||
archivePosts={archivePosts}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章详情
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutSlug = props => {
|
||||
const { post, allNavPages, recommendPosts, lock, validPassword } = props
|
||||
const game = deepClone(post)
|
||||
const [loading, setLoading] = useState(false)
|
||||
// const [url, setUrl] = useState(game?.ext?.href)
|
||||
const relateGames = recommendPosts
|
||||
const randomGames = shuffleArray(deepClone(allNavPages))
|
||||
|
||||
// 将当前游戏加入到最近游玩
|
||||
useEffect(() => {
|
||||
// 更新最新游戏
|
||||
const recentGames = localStorage.getItem('recent_games')
|
||||
? JSON.parse(localStorage.getItem('recent_games'))
|
||||
: []
|
||||
|
||||
const existedIndex = recentGames.findIndex(item => item?.id === game?.id)
|
||||
if (existedIndex === -1) {
|
||||
recentGames.unshift(game) // 将游戏插入到数组头部
|
||||
} else {
|
||||
// 如果游戏已存在于数组中,将其移至数组头部
|
||||
const existingGame = recentGames.splice(existedIndex, 1)[0]
|
||||
recentGames.unshift(existingGame)
|
||||
}
|
||||
localStorage.setItem('recent_games', JSON.stringify(recentGames))
|
||||
|
||||
const iframe = document.getElementById('game-wrapper')
|
||||
|
||||
// 定义一个函数来处理iframe加载成功事件
|
||||
function iframeLoaded() {
|
||||
if (game) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定加载事件
|
||||
if (iframe?.attachEvent) {
|
||||
iframe?.attachEvent('onload', iframeLoaded)
|
||||
} else {
|
||||
if (iframe) iframe.onload = iframeLoaded
|
||||
}
|
||||
|
||||
// 更改iFrame的title
|
||||
if (
|
||||
document
|
||||
?.getElementById('game-wrapper')
|
||||
?.contentDocument.querySelector('title')?.textContent
|
||||
) {
|
||||
document
|
||||
.getElementById('game-wrapper')
|
||||
.contentDocument.querySelector('title').textContent = `${
|
||||
game?.title || ''
|
||||
} - Play ${game?.title || ''} on ${siteConfig('TITLE')}`
|
||||
}
|
||||
}, [game])
|
||||
|
||||
return (
|
||||
<>
|
||||
{lock && <ArticleLock validPassword={validPassword} />}
|
||||
|
||||
{!lock && (
|
||||
<div id='article-wrapper' className='md:px-2'>
|
||||
{/* 游戏区域 */}
|
||||
<div className='game-detail-wrapper w-full grow flex md:px-2'>
|
||||
{/* 移动端返回主页按钮 */}
|
||||
<Draggable stick='left'>
|
||||
<div
|
||||
style={{ left: '0px', top: '1rem' }}
|
||||
className='fixed xl:hidden group space-x-1 flex items-center z-20 pr-3 pl-1 bg-[#202030] rounded-r-2xl shadow-lg '>
|
||||
<Link
|
||||
href='/'
|
||||
className='px-1 py-3 hover:scale-125 duration-200 transition-all'
|
||||
passHref>
|
||||
<i className='fas fa-arrow-left' />
|
||||
</Link>{' '}
|
||||
<span
|
||||
className='text-white font-serif'
|
||||
onClick={() => {
|
||||
document.querySelector('.game-info').scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'end',
|
||||
inline: 'nearest'
|
||||
})
|
||||
}}>
|
||||
G
|
||||
</span>
|
||||
</div>
|
||||
</Draggable>
|
||||
|
||||
<div className='w-full py-1 md:py-4'>
|
||||
{/* 游戏区 */}
|
||||
<div className='bg-black w-full xl:h-[calc(100vh-8rem)] h-screen rounded-md relative'>
|
||||
{/* Loading遮罩 */}
|
||||
{loading && (
|
||||
<div className='absolute z-20 w-full xl:h-[calc(100vh-8rem)] h-screen rounded-md overflow-hidden '>
|
||||
<div className='z-20 absolute bg-black bg-opacity-75 w-full h-full flex flex-col gap-4 justify-center items-center'>
|
||||
<h2 className='text-3xl text-white flex gap-2'>
|
||||
<i className='fas fa-spinner animate-spin'></i>
|
||||
{siteConfig('TITLE')}
|
||||
</h2>
|
||||
<h3 className='text-xl text-white'>
|
||||
{siteConfig('DESCRIPTION')}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* 游戏封面图 */}
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
{game?.img && (
|
||||
<img
|
||||
src={game?.img}
|
||||
className='w-full h-full blur-md absolute top-0 left-0 z-0'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<iframe
|
||||
id='game-wrapper'
|
||||
className={`w-full xl:h-[calc(100vh-8rem)] h-screen md:rounded-md overflow-hidden ${game?.ext?.href ? '' : 'hidden'}`}
|
||||
style={{
|
||||
position: 'relative'
|
||||
}}
|
||||
src={game?.ext?.href}></iframe>
|
||||
|
||||
{/* 游戏窗口装饰器 */}
|
||||
{game && !loading && (
|
||||
<div className='game-decorator bg-[#0B0D14] right-0 bottom-0 flex justify-center h-12 md:w-12 z-10 md:absolute'>
|
||||
{/* 加入全屏按钮 */}
|
||||
<FullScreen />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 游戏资讯 */}
|
||||
<div className='game-info dark:text-white py-4 px-2 md:px-0 mt-8 md:mt-0'>
|
||||
{/* 关联游戏 */}
|
||||
<div className='w-full'>
|
||||
<GameListRelate posts={relateGames} />
|
||||
</div>
|
||||
|
||||
{game && (
|
||||
<div className='bg-white shadow-md my-2 p-2 rounded-md dark:bg-black'>
|
||||
<PostInfo post={post} />
|
||||
<NotionPage post={post} />
|
||||
{/* 广告嵌入 */}
|
||||
<AdSlot />
|
||||
<ShareBar post={post} />
|
||||
<Comment frontMatter={post} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 其它游戏列表 */}
|
||||
<GameListIndexCombine posts={randomGames} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 404 页面
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const Layout404 = props => {
|
||||
return <>404 Not found.</>
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章分类列表
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutCategoryIndex = props => {
|
||||
const { categoryOptions } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
id='category-list'
|
||||
className='duration-200 flex flex-wrap my-4 gap-2'>
|
||||
{categoryOptions?.map(category => {
|
||||
return (
|
||||
<Link
|
||||
key={category.name}
|
||||
href={`/category/${category.name}`}
|
||||
passHref
|
||||
legacyBehavior>
|
||||
<div
|
||||
className={
|
||||
'bg-white rounded-lg hover:text-black dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600 px-5 cursor-pointer py-2 hover:bg-gray-100'
|
||||
}>
|
||||
{/* <i className='mr-4 fas fa-folder' /> */}
|
||||
{category.name}({category.count})
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章标签列表
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutTagIndex = props => {
|
||||
const { tagOptions } = props
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div id='tags-list' className='duration-200 flex flex-wrap my-4 gap-2'>
|
||||
{tagOptions.map(tag => {
|
||||
return (
|
||||
<Link
|
||||
key={tag.name}
|
||||
href={`/tag/${encodeURIComponent(tag.name)}`}
|
||||
passHref
|
||||
className={` select-none cursor-pointer flex bg-white rounded-lg hover:bg-gray-500 hover:text-white duration-200 mr-2 py-1 px-2 text-xs whitespace-nowrap dark:hover:text-white hover:shadow-xl dark:bg-gray-800`}>
|
||||
<i className='mr-1 fas fa-tag' />{' '}
|
||||
{tag.name + (tag.count ? `(${tag.count})` : '')}{' '}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Layout404,
|
||||
LayoutArchive,
|
||||
LayoutBase,
|
||||
LayoutCategoryIndex,
|
||||
LayoutIndex,
|
||||
LayoutPostList,
|
||||
LayoutSearch,
|
||||
LayoutSlug,
|
||||
LayoutTagIndex,
|
||||
CONFIG as THEME_CONFIG
|
||||
}
|
||||
18
themes/game/style.js
Normal file
18
themes/game/style.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable react/no-unknown-property */
|
||||
/**
|
||||
* 此处样式只对当前主题生效
|
||||
* 此处不支持tailwindCSS的 @apply 语法
|
||||
* @returns
|
||||
*/
|
||||
const Style = () => {
|
||||
return <style jsx global>{`
|
||||
|
||||
// 底色
|
||||
.dark body{
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
`}</style>
|
||||
}
|
||||
|
||||
export { Style }
|
||||
@@ -10,9 +10,19 @@ export default function BlogPostBar(props) {
|
||||
const { locale } = useGlobal()
|
||||
|
||||
if (tag) {
|
||||
return <div className='flex items-center py-8'><div className='text-xl'><i className='mr-2 fas fa-tag' />{locale.COMMON.TAGS}:</div>{tag}</div>
|
||||
return (
|
||||
<div className='flex items-center text-xl py-8'>
|
||||
<i className='mr-2 fas fa-tag' />
|
||||
{locale.COMMON.TAGS}:{tag}
|
||||
</div>
|
||||
)
|
||||
} else if (category) {
|
||||
<div className='flex items-center py-8'><div className='text-xl'><i className='mr-2 fas fa-th' />{locale.COMMON.CATEGORY}:</div>{category}</div>
|
||||
return (
|
||||
<div className='flex items-center text-xl py-8'>
|
||||
<i className='mr-2 fas fa-th' />
|
||||
{locale.COMMON.CATEGORY}:{category}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return <></>
|
||||
}
|
||||
|
||||
29
themes/simple/components/BlogPostBar.js
Normal file
29
themes/simple/components/BlogPostBar.js
Normal file
@@ -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 (
|
||||
<div className='flex items-center text-xl py-2'>
|
||||
<i className='mr-2 fas fa-tag' />
|
||||
{locale.COMMON.TAGS}: {tag}
|
||||
</div>
|
||||
)
|
||||
} else if (category) {
|
||||
return (
|
||||
<div className='flex items-center text-xl py-2'>
|
||||
<i className='mr-2 fas fa-th' />
|
||||
{locale.COMMON.CATEGORY}: {category}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,59 @@
|
||||
import CONFIG from './config'
|
||||
import { createContext, useContext, useEffect, useRef } from 'react'
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { AdSlot } from '@/components/GoogleAdsense'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { Transition } from '@headlessui/react'
|
||||
import Link from 'next/link'
|
||||
import { Style } from './style'
|
||||
import replaceSearchResult from '@/components/Mark'
|
||||
import dynamic from 'next/dynamic'
|
||||
import NotionPage from '@/components/NotionPage'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
import { Transition } from '@headlessui/react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { createContext, useContext, useEffect, useRef } from 'react'
|
||||
import BlogPostBar from './components/BlogPostBar'
|
||||
import CONFIG from './config'
|
||||
import { Style } from './style'
|
||||
|
||||
const AlgoliaSearchModal = dynamic(() => import('@/components/AlgoliaSearchModal'), { ssr: false })
|
||||
const AlgoliaSearchModal = dynamic(
|
||||
() => import('@/components/AlgoliaSearchModal'),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
// 主题组件
|
||||
const BlogListScroll = dynamic(() => import('./components/BlogListScroll'), { ssr: false });
|
||||
const BlogArchiveItem = dynamic(() => import('./components/BlogArchiveItem'), { ssr: false });
|
||||
const ArticleLock = dynamic(() => import('./components/ArticleLock'), { ssr: false });
|
||||
const ArticleInfo = dynamic(() => import('./components/ArticleInfo'), { ssr: false });
|
||||
const Comment = dynamic(() => import('@/components/Comment'), { ssr: false });
|
||||
const ArticleAround = dynamic(() => import('./components/ArticleAround'), { ssr: false });
|
||||
const ShareBar = dynamic(() => import('@/components/ShareBar'), { ssr: false });
|
||||
const TopBar = dynamic(() => import('./components/TopBar'), { ssr: false });
|
||||
const Header = dynamic(() => import('./components/Header'), { ssr: false });
|
||||
const NavBar = dynamic(() => import('./components/NavBar'), { ssr: false });
|
||||
const SideBar = dynamic(() => import('./components/SideBar'), { ssr: false });
|
||||
const JumpToTopButton = dynamic(() => import('./components/JumpToTopButton'), { ssr: false });
|
||||
const Footer = dynamic(() => import('./components/Footer'), { ssr: false });
|
||||
const SearchInput = dynamic(() => import('./components/SearchInput'), { ssr: false });
|
||||
const WWAds = dynamic(() => import('@/components/WWAds'), { ssr: false });
|
||||
const BlogListPage = dynamic(() => import('./components/BlogListPage'), { ssr: false })
|
||||
const RecommendPosts = dynamic(() => import('./components/RecommendPosts'), { ssr: false })
|
||||
const BlogListScroll = dynamic(() => import('./components/BlogListScroll'), {
|
||||
ssr: false
|
||||
})
|
||||
const BlogArchiveItem = dynamic(() => import('./components/BlogArchiveItem'), {
|
||||
ssr: false
|
||||
})
|
||||
const ArticleLock = dynamic(() => import('./components/ArticleLock'), {
|
||||
ssr: false
|
||||
})
|
||||
const ArticleInfo = dynamic(() => import('./components/ArticleInfo'), {
|
||||
ssr: false
|
||||
})
|
||||
const Comment = dynamic(() => import('@/components/Comment'), { ssr: false })
|
||||
const ArticleAround = dynamic(() => import('./components/ArticleAround'), {
|
||||
ssr: false
|
||||
})
|
||||
const ShareBar = dynamic(() => import('@/components/ShareBar'), { ssr: false })
|
||||
const TopBar = dynamic(() => import('./components/TopBar'), { ssr: false })
|
||||
const Header = dynamic(() => import('./components/Header'), { ssr: false })
|
||||
const NavBar = dynamic(() => import('./components/NavBar'), { ssr: false })
|
||||
const SideBar = dynamic(() => import('./components/SideBar'), { ssr: false })
|
||||
const JumpToTopButton = dynamic(() => import('./components/JumpToTopButton'), {
|
||||
ssr: false
|
||||
})
|
||||
const Footer = dynamic(() => import('./components/Footer'), { ssr: false })
|
||||
const SearchInput = dynamic(() => import('./components/SearchInput'), {
|
||||
ssr: false
|
||||
})
|
||||
const WWAds = dynamic(() => import('@/components/WWAds'), { ssr: false })
|
||||
const BlogListPage = dynamic(() => import('./components/BlogListPage'), {
|
||||
ssr: false
|
||||
})
|
||||
const RecommendPosts = dynamic(() => import('./components/RecommendPosts'), {
|
||||
ssr: false
|
||||
})
|
||||
|
||||
// 主题全局状态
|
||||
const ThemeGlobalSimple = createContext()
|
||||
@@ -50,57 +72,63 @@ const LayoutBase = props => {
|
||||
|
||||
return (
|
||||
<ThemeGlobalSimple.Provider value={{ searchModal }}>
|
||||
<div id='theme-simple' className={`${siteConfig('FONT_STYLE')} min-h-screen flex flex-col dark:text-gray-300 bg-white dark:bg-black scroll-smooth`}>
|
||||
<div
|
||||
id='theme-simple'
|
||||
className={`${siteConfig('FONT_STYLE')} min-h-screen flex flex-col dark:text-gray-300 bg-white dark:bg-black scroll-smooth`}>
|
||||
<Style />
|
||||
|
||||
<Style/>
|
||||
{siteConfig('SIMPLE_TOP_BAR', null, CONFIG) && <TopBar {...props} />}
|
||||
|
||||
{siteConfig('SIMPLE_TOP_BAR', null, CONFIG) && <TopBar {...props} />}
|
||||
{/* 顶部LOGO */}
|
||||
<Header {...props} />
|
||||
|
||||
{/* 顶部LOGO */}
|
||||
<Header {...props} />
|
||||
{/* 导航栏 */}
|
||||
<NavBar {...props} />
|
||||
|
||||
{/* 导航栏 */}
|
||||
<NavBar {...props} />
|
||||
{/* 主体 */}
|
||||
<div
|
||||
id='container-wrapper'
|
||||
className={
|
||||
(JSON.parse(siteConfig('LAYOUT_SIDEBAR_REVERSE'))
|
||||
? 'flex-row-reverse'
|
||||
: '') + ' w-full flex-1 flex items-start max-w-9/10 mx-auto pt-12'
|
||||
}>
|
||||
<div id='container-inner ' className='w-full flex-grow min-h-fit'>
|
||||
<Transition
|
||||
show={!onLoading}
|
||||
appear={true}
|
||||
enter='transition ease-in-out duration-700 transform order-first'
|
||||
enterFrom='opacity-0 translate-y-16'
|
||||
enterTo='opacity-100'
|
||||
leave='transition ease-in-out duration-300 transform'
|
||||
leaveFrom='opacity-100 translate-y-0'
|
||||
leaveTo='opacity-0 -translate-y-16'
|
||||
unmount={false}>
|
||||
{slotTop}
|
||||
|
||||
{/* 主体 */}
|
||||
<div id='container-wrapper' className={(JSON.parse(siteConfig('LAYOUT_SIDEBAR_REVERSE')) ? 'flex-row-reverse' : '') + ' w-full flex-1 flex items-start max-w-9/10 mx-auto pt-12'}>
|
||||
<div id='container-inner ' className='w-full flex-grow min-h-fit'>
|
||||
<Transition
|
||||
show={!onLoading}
|
||||
appear={true}
|
||||
enter="transition ease-in-out duration-700 transform order-first"
|
||||
enterFrom="opacity-0 translate-y-16"
|
||||
enterTo="opacity-100"
|
||||
leave="transition ease-in-out duration-300 transform"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 -translate-y-16"
|
||||
unmount={false}
|
||||
>
|
||||
{slotTop}
|
||||
{children}
|
||||
</Transition>
|
||||
<AdSlot type='native' />
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</Transition>
|
||||
<AdSlot type='native' />
|
||||
</div>
|
||||
|
||||
{fullWidth
|
||||
? null
|
||||
: <div id='right-sidebar' className="hidden xl:block flex-none sticky top-8 w-96 border-l dark:border-gray-800 pl-12 border-gray-100">
|
||||
{fullWidth ? null : (
|
||||
<div
|
||||
id='right-sidebar'
|
||||
className='hidden xl:block flex-none sticky top-8 w-96 border-l dark:border-gray-800 pl-12 border-gray-100'>
|
||||
<SideBar {...props} />
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
|
||||
<div className='fixed right-4 bottom-4 z-20'>
|
||||
<JumpToTopButton />
|
||||
</div>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<AlgoliaSearchModal cRef={searchModal} {...props}/>
|
||||
|
||||
<Footer {...props} />
|
||||
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='fixed right-4 bottom-4 z-20'>
|
||||
<JumpToTopButton />
|
||||
</div>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<AlgoliaSearchModal cRef={searchModal} {...props} />
|
||||
|
||||
<Footer {...props} />
|
||||
</div>
|
||||
</ThemeGlobalSimple.Provider>
|
||||
)
|
||||
}
|
||||
@@ -121,9 +149,14 @@ const LayoutIndex = props => {
|
||||
*/
|
||||
const LayoutPostList = props => {
|
||||
return (
|
||||
<>
|
||||
{siteConfig('POST_LIST_STYLE') === 'page' ? <BlogListPage {...props} /> : <BlogListScroll {...props} />}
|
||||
</>
|
||||
<>
|
||||
<BlogPostBar {...props} />
|
||||
{siteConfig('POST_LIST_STYLE') === 'page' ? (
|
||||
<BlogListPage {...props} />
|
||||
) : (
|
||||
<BlogListScroll {...props} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -149,7 +182,9 @@ const LayoutSearch = props => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const slotTop = siteConfig('ALGOLIA_APP_ID') ? null : <SearchInput {...props} />
|
||||
const slotTop = siteConfig('ALGOLIA_APP_ID') ? null : (
|
||||
<SearchInput {...props} />
|
||||
)
|
||||
|
||||
return <LayoutPostList {...props} slotTop={slotTop} />
|
||||
}
|
||||
@@ -162,11 +197,17 @@ const LayoutSearch = props => {
|
||||
const LayoutArchive = props => {
|
||||
const { archivePosts } = props
|
||||
return (
|
||||
<>
|
||||
<div className="mb-10 pb-20 md:py-12 p-3 min-h-screen w-full">
|
||||
{Object.keys(archivePosts).map(archiveTitle => <BlogArchiveItem key={archiveTitle} archiveTitle={archiveTitle} archivePosts={archivePosts} />)}
|
||||
</div>
|
||||
</>
|
||||
<>
|
||||
<div className='mb-10 pb-20 md:py-12 p-3 min-h-screen w-full'>
|
||||
{Object.keys(archivePosts).map(archiveTitle => (
|
||||
<BlogArchiveItem
|
||||
key={archiveTitle}
|
||||
archiveTitle={archiveTitle}
|
||||
archivePosts={archivePosts}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -180,39 +221,39 @@ const LayoutSlug = props => {
|
||||
const { fullWidth } = useGlobal()
|
||||
|
||||
return (
|
||||
<>
|
||||
<>
|
||||
{lock && <ArticleLock validPassword={validPassword} />}
|
||||
|
||||
{lock && <ArticleLock validPassword={validPassword} />}
|
||||
<div
|
||||
id='article-wrapper'
|
||||
className={`px-2 ${fullWidth ? '' : 'xl:max-w-4xl 2xl:max-w-6xl'}`}>
|
||||
{/* 文章信息 */}
|
||||
<ArticleInfo post={post} />
|
||||
|
||||
<div id="article-wrapper" className={`px-2 ${fullWidth ? '' : 'xl:max-w-4xl 2xl:max-w-6xl'}`}>
|
||||
{/* 广告嵌入 */}
|
||||
{/* <AdSlot type={'in-article'} /> */}
|
||||
<WWAds orientation='horizontal' className='w-full' />
|
||||
|
||||
{/* 文章信息 */}
|
||||
<ArticleInfo post={post} />
|
||||
{/* Notion文章主体 */}
|
||||
{!lock && <NotionPage post={post} />}
|
||||
|
||||
{/* 广告嵌入 */}
|
||||
{/* <AdSlot type={'in-article'} /> */}
|
||||
<WWAds orientation="horizontal" className="w-full" />
|
||||
{/* 分享 */}
|
||||
<ShareBar post={post} />
|
||||
|
||||
{/* Notion文章主体 */}
|
||||
{!lock && <NotionPage post={post} />}
|
||||
{/* 广告嵌入 */}
|
||||
<AdSlot type={'in-article'} />
|
||||
|
||||
{/* 分享 */}
|
||||
<ShareBar post={post} />
|
||||
{post?.type === 'Post' && (
|
||||
<>
|
||||
<ArticleAround prev={prev} next={next} />
|
||||
<RecommendPosts recommendPosts={recommendPosts} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 广告嵌入 */}
|
||||
<AdSlot type={'in-article'} />
|
||||
|
||||
{post?.type === 'Post' && <>
|
||||
<ArticleAround prev={prev} next={next} />
|
||||
<RecommendPosts recommendPosts={recommendPosts}/>
|
||||
</>}
|
||||
|
||||
{/* 评论区 */}
|
||||
<Comment frontMatter={post} />
|
||||
|
||||
</div>
|
||||
|
||||
</>
|
||||
{/* 评论区 */}
|
||||
<Comment frontMatter={post} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -221,27 +262,28 @@ const LayoutSlug = props => {
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const Layout404 = (props) => {
|
||||
const Layout404 = props => {
|
||||
const { post } = props
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
// 404
|
||||
if (!post) {
|
||||
setTimeout(() => {
|
||||
if (isBrowser) {
|
||||
const article = document.getElementById('notion-article')
|
||||
if (!article) {
|
||||
router.push('/404').then(() => {
|
||||
console.warn('找不到页面', router.asPath)
|
||||
})
|
||||
setTimeout(
|
||||
() => {
|
||||
if (isBrowser) {
|
||||
const article = document.getElementById('notion-article')
|
||||
if (!article) {
|
||||
router.push('/404').then(() => {
|
||||
console.warn('找不到页面', router.asPath)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}, siteConfig('POST_WAITING_TIME_FOR_404') * 1000)
|
||||
},
|
||||
siteConfig('POST_WAITING_TIME_FOR_404') * 1000
|
||||
)
|
||||
}
|
||||
}, [post])
|
||||
return <>
|
||||
404 Not found.
|
||||
</>
|
||||
return <>404 Not found.</>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -252,24 +294,27 @@ const Layout404 = (props) => {
|
||||
const LayoutCategoryIndex = props => {
|
||||
const { categoryOptions } = props
|
||||
return (
|
||||
<>
|
||||
<div id='category-list' className='duration-200 flex flex-wrap'>
|
||||
{categoryOptions?.map(category => {
|
||||
return (
|
||||
<Link
|
||||
key={category.name}
|
||||
href={`/category/${category.name}`}
|
||||
passHref
|
||||
legacyBehavior>
|
||||
<div
|
||||
className={'hover:text-black dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600 px-5 cursor-pointer py-2 hover:bg-gray-100'}>
|
||||
<i className='mr-4 fas fa-folder' />{category.name}({category.count})
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
<>
|
||||
<div id='category-list' className='duration-200 flex flex-wrap'>
|
||||
{categoryOptions?.map(category => {
|
||||
return (
|
||||
<Link
|
||||
key={category.name}
|
||||
href={`/category/${category.name}`}
|
||||
passHref
|
||||
legacyBehavior>
|
||||
<div
|
||||
className={
|
||||
'hover:text-black dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600 px-5 cursor-pointer py-2 hover:bg-gray-100'
|
||||
}>
|
||||
<i className='mr-4 fas fa-folder' />
|
||||
{category.name}({category.count})
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -278,38 +323,41 @@ const LayoutCategoryIndex = props => {
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutTagIndex = (props) => {
|
||||
const LayoutTagIndex = props => {
|
||||
const { tagOptions } = props
|
||||
return (
|
||||
<>
|
||||
<div id='tags-list' className='duration-200 flex flex-wrap'>
|
||||
{tagOptions.map(tag => {
|
||||
return (
|
||||
<div key={tag.name} className='p-2'>
|
||||
<Link
|
||||
key={tag}
|
||||
href={`/tag/${encodeURIComponent(tag.name)}`}
|
||||
passHref
|
||||
className={`cursor-pointer inline-block rounded hover:bg-gray-500 hover:text-white duration-200 mr-2 py-1 px-2 text-xs whitespace-nowrap dark:hover:text-white text-gray-600 hover:shadow-xl dark:border-gray-400 notion-${tag.color}_background dark:bg-gray-800`}>
|
||||
<div className='font-light dark:text-gray-400'><i className='mr-1 fas fa-tag' /> {tag.name + (tag.count ? `(${tag.count})` : '')} </div>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<>
|
||||
<div id='tags-list' className='duration-200 flex flex-wrap'>
|
||||
{tagOptions.map(tag => {
|
||||
return (
|
||||
<div key={tag.name} className='p-2'>
|
||||
<Link
|
||||
key={tag}
|
||||
href={`/tag/${encodeURIComponent(tag.name)}`}
|
||||
passHref
|
||||
className={`cursor-pointer inline-block rounded hover:bg-gray-500 hover:text-white duration-200 mr-2 py-1 px-2 text-xs whitespace-nowrap dark:hover:text-white text-gray-600 hover:shadow-xl dark:border-gray-400 notion-${tag.color}_background dark:bg-gray-800`}>
|
||||
<div className='font-light dark:text-gray-400'>
|
||||
<i className='mr-1 fas fa-tag' />{' '}
|
||||
{tag.name + (tag.count ? `(${tag.count})` : '')}{' '}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
CONFIG as THEME_CONFIG,
|
||||
LayoutBase,
|
||||
LayoutIndex,
|
||||
LayoutSearch,
|
||||
LayoutArchive,
|
||||
LayoutSlug,
|
||||
Layout404,
|
||||
LayoutArchive,
|
||||
LayoutBase,
|
||||
LayoutCategoryIndex,
|
||||
LayoutIndex,
|
||||
LayoutPostList,
|
||||
LayoutTagIndex
|
||||
LayoutSearch,
|
||||
LayoutSlug,
|
||||
LayoutTagIndex,
|
||||
CONFIG as THEME_CONFIG
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import BLOG, { LAYOUT_MAPPINGS } from '@/blog.config'
|
||||
import { getQueryParam, getQueryVariable, isBrowser } from '../lib/utils'
|
||||
import dynamic from 'next/dynamic'
|
||||
import getConfig from 'next/config'
|
||||
import * as ThemeComponents from '@theme-components'
|
||||
import getConfig from 'next/config'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { getQueryParam, getQueryVariable, isBrowser } from '../lib/utils'
|
||||
|
||||
// 在next.config.js中扫描所有主题
|
||||
export const { THEMES = [] } = getConfig().publicRuntimeConfig
|
||||
@@ -100,7 +100,6 @@ export const initDarkMode = (updateDarkMode, defaultDarkMode) => {
|
||||
|
||||
// 查看localStorage中用户记录的是否深色模式
|
||||
const userDarkMode = loadDarkModeFromLocalStorage()
|
||||
console.log('深色模式',userDarkMode)
|
||||
if (userDarkMode) {
|
||||
newDarkMode = userDarkMode === 'dark' || userDarkMode === 'true'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user