Merge branch 'main' into deploy/preview.tangly1024.com

This commit is contained in:
tangly1024.com
2024-04-12 15:09:04 +08:00
106 changed files with 5195 additions and 4230 deletions

View File

@@ -1,5 +1,5 @@
# 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables
NEXT_PUBLIC_VERSION=4.4.2
NEXT_PUBLIC_VERSION=4.4.3
# 可在此添加环境变量,去掉最左边的(# )注释即可

View File

@@ -12,16 +12,13 @@ assignees: tangly1024
-->
**描述bug**
【此项必填】简单说明bug的现象、相关的错误提示、日志等
**复现步骤**
【此项必填】出现这个bug的操作步骤
【此项必填】简单说明目前出现的现象、相关的错误提示、日志等、截图
**期望的正常结果**
【此项必填】希望按这个步骤,正常操作结果是什么
【此项必填】按这个步骤,预期出现的现象应该是什么
**截图**
可选】相关的页面,应该的结果
**复现步骤**
此项必填】你的操作步骤按此步骤理应在我的开发环境出现一样的bug。
**环境**
@@ -32,4 +29,4 @@ assignees: tangly1024
- 【可选】浏览器 [例如. chrome, safari, firefox]
**补充说明**
【可选】与问题相关的其它说明
【可选】与问题相关的其它说明

View File

@@ -1,7 +1,9 @@
// 注: process.env.XX是Vercel的环境变量配置方式见https://docs.tangly1024.com/article/how-to-config-notion-next#c4768010ae7d44609b744e79e2f9959a
const BLOG = {
// Important page_idDuplicate 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,en:7c1d570661754c8fbc568e00a01fd70e',
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
@@ -11,7 +13,8 @@ const BLOG = {
APPEARANCE: process.env.NEXT_PUBLIC_APPEARANCE || 'light', // ['light', 'dark', 'auto'], // light 日间模式 dark夜间模式 auto根据时间和主题自动夜间模式
APPEARANCE_DARK_TIME: process.env.NEXT_PUBLIC_APPEARANCE_DARK_TIME || [18, 6], // 夜间模式起至时间false时关闭根据时间自动切换夜间模式
IS_TAG_COLOR_DISTINGUISHED: process.env.NEXT_PUBLIC_IS_TAG_COLOR_DISTINGUISHED === 'true' || true, // 对于名称相同的tag是否区分tag的颜色
IS_TAG_COLOR_DISTINGUISHED:
process.env.NEXT_PUBLIC_IS_TAG_COLOR_DISTINGUISHED === 'true' || true, // 对于名称相同的tag是否区分tag的颜色
// 3.14.1版本后,欢迎语在此配置,英文逗号隔开 , 即可支持多个欢迎语打字效果。
GREETING_WORDS:
@@ -43,7 +46,9 @@ const BLOG = {
IMAGE_COMPRESS_WIDTH: process.env.NEXT_PUBLIC_IMAGE_COMPRESS_WIDTH || 800, // 图片压缩宽度默认值,作用于博客封面和文章内容 越小加载图片越快
IMAGE_ZOOM_IN_WIDTH: process.env.NEXT_PUBLIC_IMAGE_ZOOM_IN_WIDTH || 1200, // 文章图片点击放大后的画质宽度,不代表在网页中的实际展示宽度
RANDOM_IMAGE_URL: process.env.NEXT_PUBLIC_RANDOM_IMAGE_URL || '', // 随机图片API,如果未配置下面的关键字,主页封面,头像,文章封面图都会被替换为随机图片
RANDOM_IMAGE_REPLACE_TEXT: process.env.NEXT_PUBLIC_RANDOM_IMAGE_NOT_REPLACE_TEXT || 'images.unsplash.com', // 触发替换图片的 url 关键字(多个支持用英文逗号分开)只有图片地址中包含此关键字才会替换为上方随机图片url
RANDOM_IMAGE_REPLACE_TEXT:
process.env.NEXT_PUBLIC_RANDOM_IMAGE_NOT_REPLACE_TEXT ||
'images.unsplash.com', // 触发替换图片的 url 关键字(多个支持用英文逗号分开)只有图片地址中包含此关键字才会替换为上方随机图片url
// eg: images.unsplash.com(notion图床的所有图片都会替换),如果你在 notion 里已经添加了一个随机图片 url恰巧那个服务跑路或者挂掉想一键切换所有配图可以将该 url 配置在这里
// 默认下会将你上传到 notion的主页封面图和头像也给替换建议将主页封面图和头像放在其他图床在 notion 里配置 link 即可。
@@ -122,23 +127,29 @@ 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:
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_DARK_MODE: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_DARK_MODE || true, // 是否显示深色模式
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_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, // 是否显示标签
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, // 是否显示标签
// 自定义外部脚本,外部样式
CUSTOM_EXTERNAL_JS: [''], // e.g. ['http://xx.com/script.js','http://xx.com/script.js']
CUSTOM_EXTERNAL_CSS: [''], // e.g. ['http://xx.com/style.css','http://xx.com/style.css']
// 侧栏布局 是否反转(左变右,右变左) 已支持主题: hexo next medium fukasawa example
LAYOUT_SIDEBAR_REVERSE: process.env.NEXT_PUBLIC_LAYOUT_SIDEBAR_REVERSE || false,
LAYOUT_SIDEBAR_REVERSE:
process.env.NEXT_PUBLIC_LAYOUT_SIDEBAR_REVERSE || false,
// 一个小插件展示你的facebook fan page~ @see https://tw.andys.pro/article/add-facebook-fanpage-notionnext
FACEBOOK_PAGE_TITLE: process.env.NEXT_PUBLIC_FACEBOOK_PAGE_TITLE || null, // 邊欄 Facebook Page widget 的標題欄,填''則無標題欄 e.g FACEBOOK 粉絲團'
@@ -151,7 +162,8 @@ const BLOG = {
// START********代码相关********
// PrismJs 代码相关
PRISM_JS_PATH: 'https://npm.elemecdn.com/prismjs@1.29.0/components/',
PRISM_JS_AUTO_LOADER: 'https://npm.elemecdn.com/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js',
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:
@@ -168,16 +180,19 @@ const BLOG = {
CODE_MAC_BAR: process.env.NEXT_PUBLIC_CODE_MAC_BAR || true, // 代码左上角显示mac的红黄绿图标
CODE_LINE_NUMBERS: process.env.NEXT_PUBLIC_CODE_LINE_NUMBERS || false, // 是否显示行号
CODE_COLLAPSE: process.env.NEXT_PUBLIC_CODE_COLLAPSE || true, // 是否支持折叠代码框
CODE_COLLAPSE_EXPAND_DEFAULT: process.env.NEXT_PUBLIC_CODE_COLLAPSE_EXPAND_DEFAULT || true, // 折叠代码默认是展开状态
CODE_COLLAPSE_EXPAND_DEFAULT:
process.env.NEXT_PUBLIC_CODE_COLLAPSE_EXPAND_DEFAULT || true, // 折叠代码默认是展开状态
// 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
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',
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 '#'
@@ -202,24 +217,32 @@ const BLOG = {
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页面
POST_WAITING_TIME_FOR_404:
process.env.NEXT_PUBLIC_POST_WAITING_TIME_FOR_404 || '8', // 文章加载超时时间单位秒超时后跳转到404页面
ALGOLIA_APP_ID: process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || null, // 在这里查看 https://dashboard.algolia.com/account/api-keys/
ALGOLIA_ADMIN_APP_KEY: process.env.ALGOLIA_ADMIN_APP_KEY || null, // 管理后台的KEY不要暴露在代码中在这里查看 https://dashboard.algolia.com/account/api-keys/
ALGOLIA_SEARCH_ONLY_APP_KEY: process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_APP_KEY || null, // 客户端搜索用的KEY
ALGOLIA_SEARCH_ONLY_APP_KEY:
process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_APP_KEY || null, // 客户端搜索用的KEY
ALGOLIA_INDEX: process.env.NEXT_PUBLIC_ALGOLIA_INDEX || null, // 在Algolia中创建一个index用作数据库
// ALGOLIA_RECREATE_DATA: process.env.ALGOLIA_RECREATE_DATA || process.env.npm_lifecycle_event === 'build', // 为true时重新构建索引数据; 默认在build时会构建
PREVIEW_CATEGORY_COUNT: 16, // 首页最多展示的分类数量0为不限制
PREVIEW_TAG_COUNT: 16, // 首页最多展示的标签数量0为不限制
POST_DISABLE_GALLERY_CLICK: process.env.NEXT_PUBLIC_POST_DISABLE_GALLERY_CLICK || false, // 画册视图禁止点击,方便在友链页面的画册插入链接
POST_DISABLE_GALLERY_CLICK:
process.env.NEXT_PUBLIC_POST_DISABLE_GALLERY_CLICK || false, // 画册视图禁止点击,方便在友链页面的画册插入链接
// ********动态特效相关********
// 鼠标点击烟花特效
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, // 开关
@@ -238,14 +261,16 @@ const BLOG = {
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',
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/
CHATBASE_ID: process.env.NEXT_PUBLIC_CHATBASE_ID || null,
// WebwhizAI 机器人 @see https://github.com/webwhiz-ai/webwhiz
WEB_WHIZ_ENABLED: process.env.NEXT_PUBLIC_WEB_WHIZ_ENABLED || false, // 是否显示
WEB_WHIZ_BASE_URL: process.env.NEXT_PUBLIC_WEB_WHIZ_BASE_URL || 'https://api.webwhiz.ai', // 可以自建服务器
WEB_WHIZ_BASE_URL:
process.env.NEXT_PUBLIC_WEB_WHIZ_BASE_URL || 'https://api.webwhiz.ai', // 可以自建服务器
WEB_WHIZ_CHAT_BOT_ID: process.env.NEXT_PUBLIC_WEB_WHIZ_CHAT_BOT_ID || null, // 在后台获取ID
DIFY_CHATBOT_ENABLED: process.env.NEXT_PUBLIC_DIFY_CHATBOT_ENABLED || false,
DIFY_CHATBOT_BASE_URL: process.env.NEXT_PUBLIC_DIFY_CHATBOT_BASE_URL || '',
@@ -255,12 +280,14 @@ const BLOG = {
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
WIDGET_PET_SWITCH_THEME: process.env.NEXT_PUBLIC_WIDGET_PET_SWITCH_THEME || true, // 点击宠物挂件切换博客主题
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 | 00禁用 lrc 歌词1lrc 格式的字符串3lrc 文件 url前提是有配置歌词路径对 meting 无效)
MUSIC_PLAYER_CDN_URL:
process.env.NEXT_PUBLIC_MUSIC_PLAYER_CDN_URL ||
@@ -272,78 +299,105 @@ 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 | 00禁用 lrc 歌词1lrc 格式的字符串3lrc 文件 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 | 00禁用 lrc 歌词1lrc 格式的字符串3lrc 文件 url
// ********挂件组件相关********
// ----> 评论互动 可同时开启多个支持 WALINE VALINE GISCUS CUSDIS UTTERRANCES GITALK
COMMENT_HIDE_SINGLE_TAB: process.env.NEXT_PUBLIC_COMMENT_HIDE_SINGLE_TAB || false, // Whether hide the tab when there's no tabs. 只有一个评论组件时是否隐藏切换组件的标签页
COMMENT_HIDE_SINGLE_TAB:
process.env.NEXT_PUBLIC_COMMENT_HIDE_SINGLE_TAB || false, // Whether hide the tab when there's no tabs. 只有一个评论组件时是否隐藏切换组件的标签页
// 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
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
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后端地址 腾讯云环境填envIdVercel环境填域名教程https://tangly1024.com/article/notionnext-twikoo
COMMENT_TWIKOO_COUNT_ENABLE: process.env.NEXT_PUBLIC_COMMENT_TWIKOO_COUNT_ENABLE || false, // 博客列表是否显示评论数
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
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
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
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
COMMENT_TIDIO_ID: process.env.NEXT_PUBLIC_COMMENT_TIDIO_ID || '', // [tidio_id] -> //code.tidio.co/[tidio_id].js
COMMENT_VALINE_CDN: process.env.NEXT_PUBLIC_VALINE_CDN || 'https://unpkg.com/valine@1.5.1/dist/Valine.min.js',
COMMENT_VALINE_CDN:
process.env.NEXT_PUBLIC_VALINE_CDN ||
'https://unpkg.com/valine@1.5.1/dist/Valine.min.js',
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, // 最新评论
@@ -357,15 +411,18 @@ const BLOG = {
// TOKEN: Webmention的API token
COMMENT_WEBMENTION_ENABLE: process.env.NEXT_PUBLIC_WEBMENTION_ENABLE || false,
COMMENT_WEBMENTION_AUTH: process.env.NEXT_PUBLIC_WEBMENTION_AUTH || '',
COMMENT_WEBMENTION_HOSTNAME: process.env.NEXT_PUBLIC_WEBMENTION_HOSTNAME || '',
COMMENT_WEBMENTION_TWITTER_USERNAME: process.env.NEXT_PUBLIC_TWITTER_USERNAME || '',
COMMENT_WEBMENTION_HOSTNAME:
process.env.NEXT_PUBLIC_WEBMENTION_HOSTNAME || '',
COMMENT_WEBMENTION_TWITTER_USERNAME:
process.env.NEXT_PUBLIC_TWITTER_USERNAME || '',
COMMENT_WEBMENTION_TOKEN: process.env.NEXT_PUBLIC_WEBMENTION_TOKEN || '',
// <---- 评论插件
// ----> 站点统计
ANALYTICS_VERCEL: process.env.NEXT_PUBLIC_ANALYTICS_VERCEL || false, // vercel自带的统计 https://vercel.com/docs/concepts/analytics/quickstart https://github.com/tangly1024/NotionNext/issues/897
ANALYTICS_BUSUANZI_ENABLE: process.env.NEXT_PUBLIC_ANALYTICS_BUSUANZI_ENABLE || true, // 展示网站阅读量、访问数 see http://busuanzi.ibruce.info/
ANALYTICS_BUSUANZI_ENABLE:
process.env.NEXT_PUBLIC_ANALYTICS_BUSUANZI_ENABLE || true, // 展示网站阅读量、访问数 see http://busuanzi.ibruce.info/
ANALYTICS_BAIDU_ID: process.env.NEXT_PUBLIC_ANALYTICS_BAIDU_ID || '', // e.g 只需要填写百度统计的id[baidu_id] -> https://hm.baidu.com/hm.js?[baidu_id]
ANALYTICS_CNZZ_ID: process.env.NEXT_PUBLIC_ANALYTICS_CNZZ_ID || '', // 只需要填写站长统计的id, [cnzz_id] -> https://s9.cnzz.com/z_stat.php?id=[cnzz_id]&web_id=[cnzz_id]
ANALYTICS_GOOGLE_ID: process.env.NEXT_PUBLIC_ANALYTICS_GOOGLE_ID || '', // 谷歌Analytics的id e.g: G-XXXXXXXXXX
@@ -378,13 +435,18 @@ const BLOG = {
MATOMO_HOST_URL: process.env.NEXT_PUBLIC_MATOMO_HOST_URL || '', // Matomo服务器地址不带斜杠
MATOMO_SITE_ID: process.env.NEXT_PUBLIC_MATOMO_SITE_ID || '', // Matomo网站ID
// ACKEE网站访客统计工具
ANALYTICS_ACKEE_TRACKER: process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_TRACKER || '', // e.g 'https://ackee.tangly1024.com/tracker.js'
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'
ANALYTICS_ACKEE_TRACKER:
process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_TRACKER || '', // e.g 'https://ackee.tangly1024.com/tracker.js'
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是一个十位的英文数字组合
@@ -396,10 +458,14 @@ const BLOG = {
// 谷歌广告
ADSENSE_GOOGLE_ID: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_ID || '', // 谷歌广告ID e.g ca-pub-xxxxxxxxxxxxxxxx
ADSENSE_GOOGLE_TEST: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_TEST || false, // 谷歌广告ID测试模式这种模式获取假的测试广告用于开发 https://www.tangly1024.com/article/local-dev-google-adsense
ADSENSE_GOOGLE_SLOT_IN_ARTICLE: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_IN_ARTICLE || '3806269138', // Google AdScene>广告>按单元广告>新建文章内嵌广告 粘贴html代码中的data-ad-slot值
ADSENSE_GOOGLE_SLOT_FLOW: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_FLOW || '1510444138', // Google AdScene>广告>按单元广告>新建信息流广告
ADSENSE_GOOGLE_SLOT_NATIVE: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_NATIVE || '4980048999', // Google AdScene>广告>按单元广告>新建原生广告
ADSENSE_GOOGLE_SLOT_AUTO: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_AUTO || '8807314373', // Google AdScene>广告>按单元广告>新建展示广告 (自动广告
ADSENSE_GOOGLE_SLOT_IN_ARTICLE:
process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_IN_ARTICLE || '3806269138', // Google AdScene>广告>按单元广告>新建文章内嵌广告 粘贴html代码中的data-ad-slot值
ADSENSE_GOOGLE_SLOT_FLOW:
process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_FLOW || '1510444138', // Google AdScene>广告>按单元广告>新建信息流广告
ADSENSE_GOOGLE_SLOT_NATIVE:
process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_NATIVE || '4980048999', // Google AdScene>广告>按单元广告>新建原生广告
ADSENSE_GOOGLE_SLOT_AUTO:
process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_AUTO || '8807314373', // Google AdScene>广告>按单元广告>新建展示广告 (自动广告)
// 万维广告
AD_WWADS_ID: process.env.NEXT_PUBLIC_WWAD_ID || null, // https://wwads.cn/ 创建您的万维广告单元ID
@@ -413,13 +479,17 @@ 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',
@@ -450,8 +520,10 @@ 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

View File

@@ -1,12 +1,30 @@
import AOS from 'aos'
import { isBrowser } from 'react-notion-x'
import { loadExternalResource } from '@/lib/utils'
import { useEffect } from 'react'
// import AOS from 'aos'
/**
* 加载滚动动画
* 改从外部CDN读取
* https://michalsnik.github.io/aos/
*/
export default function AOSAnimation() {
if (isBrowser) {
AOS.init()
const initAOS = async () => {
Promise.all([
loadExternalResource(
'https://cdn.bootcdn.net/ajax/libs/aos/2.3.4/aos.js',
'js'
),
loadExternalResource(
'https://cdn.bootcdn.net/ajax/libs/aos/2.3.4/aos.css',
'css'
)
]).then(() => {
if (window.AOS) {
window.AOS.init()
}
})
}
useEffect(() => {
initAOS()
}, [])
}

View File

@@ -1,43 +1,79 @@
import { siteConfig } from '@/lib/config'
import dynamic from 'next/dynamic'
import LA51 from './LA51'
import WebWhiz from './Webwhiz'
import TianLiGPT from './TianliGPT'
import { GlobalStyle } from './GlobalStyle'
import LA51 from './LA51'
import TianLiGPT from './TianliGPT'
import WebWhiz from './Webwhiz'
import { CUSTOM_EXTERNAL_CSS, CUSTOM_EXTERNAL_JS } from '@/blog.config'
import { isBrowser, loadExternalResource } from '@/lib/utils'
import { useEffect } from 'react'
import { initGoogleAdsense } from './GoogleAdsense'
const TwikooCommentCounter = dynamic(() => import('@/components/TwikooCommentCounter'), { ssr: false })
const DebugPanel = dynamic(() => import('@/components/DebugPanel'), { ssr: false })
const ThemeSwitch = dynamic(() => import('@/components/ThemeSwitch'), { ssr: false })
const Fireworks = dynamic(() => import('@/components/Fireworks'), { ssr: false })
const TwikooCommentCounter = dynamic(
() => import('@/components/TwikooCommentCounter'),
{ ssr: false }
)
const DebugPanel = dynamic(() => import('@/components/DebugPanel'), {
ssr: false
})
const ThemeSwitch = dynamic(() => import('@/components/ThemeSwitch'), {
ssr: false
})
const Fireworks = dynamic(() => import('@/components/Fireworks'), {
ssr: false
})
const Nest = dynamic(() => import('@/components/Nest'), { ssr: false })
const FlutteringRibbon = dynamic(() => import('@/components/FlutteringRibbon'), { ssr: false })
const FlutteringRibbon = dynamic(
() => import('@/components/FlutteringRibbon'),
{ ssr: false }
)
const Ribbon = dynamic(() => import('@/components/Ribbon'), { ssr: false })
const Sakura = dynamic(() => import('@/components/Sakura'), { ssr: false })
const StarrySky = dynamic(() => import('@/components/StarrySky'), { ssr: false })
const DifyChatbot = dynamic(() => import('@/components/DifyChatbot'), { ssr: false });
const Analytics = dynamic(() => import('@vercel/analytics/react').then(async (m) => { return m.Analytics }), { ssr: false })
const StarrySky = dynamic(() => import('@/components/StarrySky'), {
ssr: false
})
const DifyChatbot = dynamic(() => import('@/components/DifyChatbot'), {
ssr: false
})
const Analytics = dynamic(
() =>
import('@vercel/analytics/react').then(async m => {
return m.Analytics
}),
{ ssr: false }
)
const MusicPlayer = dynamic(() => import('@/components/Player'), { ssr: false })
const Ackee = dynamic(() => import('@/components/Ackee'), { ssr: false })
const Gtag = dynamic(() => import('@/components/Gtag'), { ssr: false })
const Busuanzi = dynamic(() => import('@/components/Busuanzi'), { ssr: false })
const GoogleAdsense = dynamic(() => import('@/components/GoogleAdsense'), { ssr: false })
const Messenger = dynamic(() => import('@/components/FacebookMessenger'), { ssr: false })
const Messenger = dynamic(() => import('@/components/FacebookMessenger'), {
ssr: false
})
const VConsole = dynamic(() => import('@/components/VConsole'), { ssr: false })
const CustomContextMenu = dynamic(() => import('@/components/CustomContextMenu'), { ssr: false })
const DisableCopy = dynamic(() => import('@/components/DisableCopy'), { ssr: false })
const AdBlockDetect = dynamic(() => import('@/components/AdBlockDetect'), { ssr: false })
const LoadingProgress = dynamic(() => import('@/components/LoadingProgress'), { ssr: false })
const AosAnimation = dynamic(() => import('@/components/AOSAnimation'), { ssr: false })
const CustomContextMenu = dynamic(
() => import('@/components/CustomContextMenu'),
{ ssr: false }
)
const DisableCopy = dynamic(() => import('@/components/DisableCopy'), {
ssr: false
})
const AdBlockDetect = dynamic(() => import('@/components/AdBlockDetect'), {
ssr: false
})
const LoadingProgress = dynamic(() => import('@/components/LoadingProgress'), {
ssr: false
})
const AosAnimation = dynamic(() => import('@/components/AOSAnimation'), {
ssr: false
})
/**
* 各种插件脚本
* @param {*} props
* @returns
*/
const ExternalPlugin = (props) => {
const ExternalPlugin = props => {
const DISABLE_PLUGIN = siteConfig('DISABLE_PLUGIN')
const THEME_SWITCH = siteConfig('THEME_SWITCH')
const DEBUG = siteConfig('DEBUG')
@@ -55,7 +91,9 @@ const ExternalPlugin = (props) => {
const FLUTTERINGRIBBON = siteConfig('FLUTTERINGRIBBON')
const COMMENT_TWIKOO_COUNT_ENABLE = siteConfig('COMMENT_TWIKOO_COUNT_ENABLE')
const RIBBON = siteConfig('RIBBON')
const CUSTOM_RIGHT_CLICK_CONTEXT_MENU = siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU')
const CUSTOM_RIGHT_CLICK_CONTEXT_MENU = siteConfig(
'CUSTOM_RIGHT_CLICK_CONTEXT_MENU'
)
const CAN_COPY = siteConfig('CAN_COPY')
const WEB_WHIZ_ENABLED = siteConfig('WEB_WHIZ_ENABLED')
const AD_WWADS_BLOCK_DETECT = siteConfig('AD_WWADS_BLOCK_DETECT')
@@ -113,88 +151,122 @@ const ExternalPlugin = (props) => {
}
}
useEffect(() => {
if (ADSENSE_GOOGLE_ID) {
setTimeout(() => {
// 异步渲染谷歌广告
initGoogleAdsense()
}, 1000)
}
}, [])
if (DISABLE_PLUGIN) {
return null
}
return <>
return (
<>
{/* 全局样式嵌入 */}
<GlobalStyle />
{/* 全局样式嵌入 */}
<GlobalStyle/>
{THEME_SWITCH && <ThemeSwitch />}
{DEBUG && <DebugPanel />}
{ANALYTICS_ACKEE_TRACKER && <Ackee />}
{ANALYTICS_GOOGLE_ID && <Gtag />}
{ANALYTICS_VERCEL && <Analytics />}
{ANALYTICS_BUSUANZI_ENABLE && <Busuanzi />}
{FACEBOOK_APP_ID && FACEBOOK_PAGE_ID && <Messenger />}
{FIREWORKS && <Fireworks />}
{SAKURA && <Sakura />}
{STARRY_SKY && <StarrySky />}
{MUSIC_PLAYER && <MusicPlayer />}
{NEST && <Nest />}
{FLUTTERINGRIBBON && <FlutteringRibbon />}
{COMMENT_TWIKOO_COUNT_ENABLE && <TwikooCommentCounter {...props} />}
{RIBBON && <Ribbon />}
{DIFY_CHATBOT_ENABLED && <DifyChatbot />}
{CUSTOM_RIGHT_CLICK_CONTEXT_MENU && <CustomContextMenu {...props} />}
{!CAN_COPY && <DisableCopy />}
{WEB_WHIZ_ENABLED && <WebWhiz />}
{AD_WWADS_BLOCK_DETECT && <AdBlockDetect />}
{TIANLI_KEY && <TianLiGPT />}
<VConsole />
<LoadingProgress />
<AosAnimation />
{ANALYTICS_51LA_ID && ANALYTICS_51LA_CK && <LA51 />}
{THEME_SWITCH && <ThemeSwitch />}
{DEBUG && <DebugPanel />}
{ANALYTICS_ACKEE_TRACKER && <Ackee />}
{ANALYTICS_GOOGLE_ID && <Gtag />}
{ANALYTICS_VERCEL && <Analytics />}
{ANALYTICS_BUSUANZI_ENABLE && <Busuanzi />}
{ADSENSE_GOOGLE_ID && <GoogleAdsense />}
{FACEBOOK_APP_ID && FACEBOOK_PAGE_ID && <Messenger />}
{FIREWORKS && <Fireworks />}
{SAKURA && <Sakura />}
{STARRY_SKY && <StarrySky />}
{MUSIC_PLAYER && <MusicPlayer />}
{NEST && <Nest />}
{FLUTTERINGRIBBON && <FlutteringRibbon />}
{COMMENT_TWIKOO_COUNT_ENABLE && <TwikooCommentCounter {...props} />}
{RIBBON && <Ribbon />}
{DIFY_CHATBOT_ENABLED && <DifyChatbot />}
{CUSTOM_RIGHT_CLICK_CONTEXT_MENU && <CustomContextMenu {...props} />}
{!CAN_COPY && <DisableCopy />}
{WEB_WHIZ_ENABLED && <WebWhiz />}
{AD_WWADS_BLOCK_DETECT && <AdBlockDetect />}
{TIANLI_KEY && <TianLiGPT/>}
<VConsole />
<LoadingProgress />
<AosAnimation />
{ANALYTICS_51LA_ID && ANALYTICS_51LA_CK && <LA51/>}
{ANALYTICS_51LA_ID && ANALYTICS_51LA_CK && (<>
<script id="LA_COLLECT" src="//sdk.51.la/js-sdk-pro.min.js" defer/>
{/* <script async dangerouslySetInnerHTML={{
{ANALYTICS_51LA_ID && ANALYTICS_51LA_CK && (
<>
<script id='LA_COLLECT' src='//sdk.51.la/js-sdk-pro.min.js' defer />
{/* <script async dangerouslySetInnerHTML={{
__html: `
LA.init({id:"${ANALYTICS_51LA_ID}",ck:"${ANALYTICS_51LA_CK}",hashMode:true,autoTrack:true})
`
}} /> */}
</>)}
</>
)}
{/* 注入JS脚本 */}
{GLOBAL_JS && <script async dangerouslySetInnerHTML={{
__html: GLOBAL_JS
}} />}
{/* 注入JS脚本 */}
{GLOBAL_JS && (
<script
async
dangerouslySetInnerHTML={{
__html: GLOBAL_JS
}}
/>
)}
{CHATBASE_ID && (<>
<script id={CHATBASE_ID} src="https://www.chatbase.co/embed.min.js" defer />
<script async dangerouslySetInnerHTML={{
{CHATBASE_ID && (
<>
<script
id={CHATBASE_ID}
src='https://www.chatbase.co/embed.min.js'
defer
/>
<script
async
dangerouslySetInnerHTML={{
__html: `
window.chatbaseConfig = {
chatbotId: "${CHATBASE_ID}",
}
`
}} />
</>)}
}}
/>
</>
)}
{CLARITY_ID && (<>
<script async dangerouslySetInnerHTML={{
__html: `
{CLARITY_ID && (
<>
<script
async
dangerouslySetInnerHTML={{
__html: `
(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "${CLARITY_ID}");
`
}} />
</>)}
}}
/>
</>
)}
{COMMENT_DAO_VOICE_ID && (<>
{/* DaoVoice 反馈 */}
<script async dangerouslySetInnerHTML={{
{COMMENT_DAO_VOICE_ID && (
<>
{/* DaoVoice 反馈 */}
<script
async
dangerouslySetInnerHTML={{
__html: `
(function(i,s,o,g,r,a,m){i["DaoVoiceObject"]=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;a.charset="utf-8";m.parentNode.insertBefore(a,m)})(window,document,"script",('https:' == document.location.protocol ? 'https:' : 'http:') + "//widget.daovoice.io/widget/daf1a94b.js","daovoice")
`
}}
/>
<script async dangerouslySetInnerHTML={{
/>
<script
async
dangerouslySetInnerHTML={{
__html: `
daovoice('init', {
app_id: "${COMMENT_DAO_VOICE_ID}"
@@ -202,34 +274,52 @@ const ExternalPlugin = (props) => {
daovoice('update');
`
}}
/>
</>)}
/>
</>
)}
{AD_WWADS_ID && <script type="text/javascript" src="https://cdn.wwads.cn/js/makemoney.js" async></script>}
{AD_WWADS_ID && (
<script
type='text/javascript'
src='https://cdn.wwads.cn/js/makemoney.js'
async></script>
)}
{COMMENT_TWIKOO_ENV_ID && <script defer src={COMMENT_TWIKOO_CDN_URL} />}
{COMMENT_TWIKOO_ENV_ID && <script defer src={COMMENT_TWIKOO_CDN_URL} />}
{COMMENT_ARTALK_SERVER && <script defer src={COMMENT_ARTALK_JS} />}
{COMMENT_ARTALK_SERVER && <script defer src={COMMENT_ARTALK_JS} />}
{COMMENT_TIDIO_ID && <script async src={`//code.tidio.co/${COMMENT_TIDIO_ID}.js`} />}
{COMMENT_TIDIO_ID && (
<script async src={`//code.tidio.co/${COMMENT_TIDIO_ID}.js`} />
)}
{/* gitter聊天室 */}
{COMMENT_GITTER_ROOM && (<>
<script src="https://sidecar.gitter.im/dist/sidecar.v1.js" async defer />
<script async dangerouslySetInnerHTML={{
{/* gitter聊天室 */}
{COMMENT_GITTER_ROOM && (
<>
<script
src='https://sidecar.gitter.im/dist/sidecar.v1.js'
async
defer
/>
<script
async
dangerouslySetInnerHTML={{
__html: `
((window.gitter = {}).chat = {}).options = {
room: '${COMMENT_GITTER_ROOM}'
};
`
}} />
</>)}
}}
/>
</>
)}
{/* 百度统计 */}
{ANALYTICS_BAIDU_ID && (
<script async
dangerouslySetInnerHTML={{
__html: `
{/* 百度统计 */}
{ANALYTICS_BAIDU_ID && (
<script
async
dangerouslySetInnerHTML={{
__html: `
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
@@ -238,29 +328,33 @@ const ExternalPlugin = (props) => {
s.parentNode.insertBefore(hm, s);
})();
`
}}
/>
)}
}}
/>
)}
{/* 站长统计 */}
{ANALYTICS_CNZZ_ID && (
<script async
dangerouslySetInnerHTML={{
__html: `
{/* 站长统计 */}
{ANALYTICS_CNZZ_ID && (
<script
async
dangerouslySetInnerHTML={{
__html: `
document.write(unescape("%3Cspan style='display:none' id='cnzz_stat_icon_${ANALYTICS_CNZZ_ID}'%3E%3C/span%3E%3Cscript src='https://s9.cnzz.com/z_stat.php%3Fid%3D${ANALYTICS_CNZZ_ID}' type='text/javascript'%3E%3C/script%3E"));
`
}}
/>
)}
}}
/>
)}
{/* 谷歌统计 */}
{ANALYTICS_GOOGLE_ID && (<>
<script async
src={`https://www.googletagmanager.com/gtag/js?id=${ANALYTICS_GOOGLE_ID}`}
/>
<script async
dangerouslySetInnerHTML={{
__html: `
{/* 谷歌统计 */}
{ANALYTICS_GOOGLE_ID && (
<>
<script
async
src={`https://www.googletagmanager.com/gtag/js?id=${ANALYTICS_GOOGLE_ID}`}
/>
<script
async
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
@@ -268,14 +362,17 @@ const ExternalPlugin = (props) => {
page_path: window.location.pathname,
});
`
}}
/>
</>)}
}}
/>
</>
)}
{/* Matomo 统计 */}
{MATOMO_HOST_URL && MATOMO_SITE_ID && (
<script async dangerouslySetInnerHTML={{
__html: `
{/* Matomo 统计 */}
{MATOMO_HOST_URL && MATOMO_SITE_ID && (
<script
async
dangerouslySetInnerHTML={{
__html: `
var _paq = window._paq = window._paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
@@ -287,10 +384,11 @@ const ExternalPlugin = (props) => {
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
`
}} />
)}
}}
/>
)}
</>
)
}
export default ExternalPlugin

View File

@@ -3,8 +3,9 @@
* custom by hexo-theme-yun @YunYouJun
*/
import { useEffect } from 'react'
import anime from 'animejs'
// import anime from 'animejs'
import { siteConfig } from '@/lib/config'
import { loadExternalResource } from '@/lib/utils'
/**
* 鼠标点击烟花特效
@@ -14,17 +15,37 @@ const Fireworks = () => {
const fireworksColor = siteConfig('FIREWORKS_COLOR')
useEffect(() => {
createFireworks({ colors: fireworksColor })
// 异步加载
async function loadFireworks() {
loadExternalResource(
'https://cdn.bootcdn.net/ajax/libs/animejs/3.2.1/anime.min.js',
'js'
).then(() => {
if (window.anime) {
createFireworks({
config: { colors: fireworksColor },
anime: window.anime
})
}
})
}
loadFireworks()
return () => {
// 在组件卸载时清理资源(如果需要)
}
}, [])
return <canvas id='fireworks' className='fireworks'></canvas>
}
export default Fireworks
/**
* 创建烟花
* @param config
*/
function createFireworks(config) {
* 创建烟花
* @param config
*/
function createFireworks({ config, anime }) {
const defaultConfig = {
colors: config?.colors,
numberOfParticules: 20,
@@ -57,8 +78,8 @@ function createFireworks(config) {
const ctx = canvasEl.getContext('2d')
/**
* 设置画布尺寸
*/
* 设置画布尺寸
*/
function setCanvasSize(canvasEl) {
canvasEl.width = window.innerWidth
canvasEl.height = window.innerHeight
@@ -67,16 +88,16 @@ function createFireworks(config) {
}
/**
* update pointer
* @param {TouchEvent} e
*/
* update pointer
* @param {TouchEvent} e
*/
function updateCoords(e) {
pointerX =
e.clientX ||
(e.touches[0] ? e.touches[0].clientX : e.changedTouches[0].clientX)
e.clientX ||
(e.touches[0] ? e.touches[0].clientX : e.changedTouches[0].clientX)
pointerY =
e.clientY ||
(e.touches[0] ? e.touches[0].clientY : e.changedTouches[0].clientY)
e.clientY ||
(e.touches[0] ? e.touches[0].clientY : e.changedTouches[0].clientY)
}
function setParticuleDirection(p) {
@@ -93,26 +114,25 @@ function createFireworks(config) {
}
/**
* 在指定位置创建粒子
* @param {number} x
* @param {number} y
* @returns
*/
* 在指定位置创建粒子
* @param {number} x
* @param {number} y
* @returns
*/
function createParticule(x, y) {
const p = {
x,
y,
color: `rgba(${
colors[anime.random(0, colors.length - 1)]
},${
anime.random(0.2, 0.8)
})`,
color: `rgba(${colors[anime.random(0, colors.length - 1)]},${anime.random(
0.2,
0.8
)})`,
radius: anime.random(config.circleRadius.min, config.circleRadius.max),
endPos: null,
draw() {}
}
p.endPos = setParticuleDirection(p)
p.draw = function() {
p.draw = function () {
ctx.beginPath()
ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI, true)
ctx.fillStyle = p.color
@@ -131,7 +151,7 @@ function createFireworks(config) {
lineWidth: 6,
draw() {}
}
p.draw = function() {
p.draw = function () {
ctx.globalAlpha = p.alpha
ctx.beginPath()
ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI, true)
@@ -144,13 +164,17 @@ function createFireworks(config) {
}
function renderParticule(anim) {
for (let i = 0; i < anim.animatables.length; i++) { anim.animatables[i].target.draw() }
for (let i = 0; i < anim.animatables.length; i++) {
anim.animatables[i].target.draw()
}
}
function animateParticules(x, y) {
const circle = createCircle(x, y)
const particules = []
for (let i = 0; i < config.numberOfParticules; i++) { particules.push(createParticule(x, y)) }
for (let i = 0; i < config.numberOfParticules; i++) {
particules.push(createParticule(x, y))
}
anime
.timeline()
@@ -197,7 +221,7 @@ function createFireworks(config) {
document.addEventListener(
'mousedown',
(e) => {
e => {
render.play()
updateCoords(e)
animateParticules(pointerX, pointerY)

View File

@@ -1,6 +1,8 @@
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import Giscus from '@giscus/react'
import { loadExternalResource } from '@/lib/utils'
import { useEffect } from 'react'
// import Giscus from '@giscus/react'
/**
* Giscus评论 @see https://giscus.app/zh-CN
@@ -12,21 +14,34 @@ import Giscus from '@giscus/react'
const GiscusComponent = () => {
const { isDarkMode } = useGlobal()
const theme = isDarkMode ? 'dark' : 'light'
useEffect(() => {
loadExternalResource('/js/giscus.js', 'js').then(() => {
if (window.Giscus) {
window.Giscus.init('#giscus')
}
})
return () => {
window.Giscus.destroy()
}
}, [isDarkMode])
return (
<Giscus
repo={siteConfig('COMMENT_GISCUS_REPO')}
repoId={siteConfig('COMMENT_GISCUS_REPO_ID')}
categoryId={siteConfig('COMMENT_GISCUS_CATEGORY_ID')}
mapping={siteConfig('COMMENT_GISCUS_MAPPING')}
reactionsEnabled={siteConfig('COMMENT_GISCUS_REACTIONS_ENABLED')}
emitMetadata={siteConfig('COMMENT_GISCUS_EMIT_METADATA')}
theme={theme}
inputPosition={siteConfig('COMMENT_GISCUS_INPUT_POSITION')}
lang={siteConfig('COMMENT_GISCUS_LANG')}
loading={siteConfig('COMMENT_GISCUS_LOADING')}
crossorigin={siteConfig('COMMENT_GISCUS_CROSSORIGIN')}
/>
<div
id='giscus'
data-repo={siteConfig('COMMENT_GISCUS_REPO')}
data-repo-id={siteConfig('COMMENT_GISCUS_REPO_ID')}
// data-category='{{ $.Site.Params.giscus.dataCategory }}'
data-category-id={siteConfig('COMMENT_GISCUS_CATEGORY_ID')}
data-mapping={siteConfig('COMMENT_GISCUS_MAPPING')}
// data-strict='0'
data-reactions-enabled={siteConfig('COMMENT_GISCUS_REACTIONS_ENABLED')}
data-emit-metadata={siteConfig('COMMENT_GISCUS_EMIT_METADATA')}
data-input-position={siteConfig('COMMENT_GISCUS_INPUT_POSITION')}
data-theme={theme}
data-lang={siteConfig('COMMENT_GISCUS_LANG')}
data-loading={siteConfig('COMMENT_GISCUS_LOADING')}
// crossorigin={siteConfig('COMMENT_GISCUS_CROSSORIGIN')}
></div>
)
}

View File

@@ -1,48 +1,105 @@
import { siteConfig } from '@/lib/config'
import { loadExternalResource } from '@/lib/utils'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
function requestAd() {
const ads = document.getElementsByClassName('adsbygoogle')
/**
* 请求广告元素
* 调用后,实际只有当广告单元在页面中可见时才会真正获取
*/
function requestAd(ads) {
if (!ads || ads.length === 0) {
return
}
const adsbygoogle = window.adsbygoogle
if (adsbygoogle && ads.length > 0) {
for (let i = 0; i <= ads.length; i++) {
try {
const adStatus = ads[i].getAttribute('data-adsbygoogle-status')
if (!adStatus || adStatus !== 'done') {
adsbygoogle.push(ads[i])
const observerOptions = {
root: null, // use the viewport as the root
threshold: 0.5 // element is considered visible when 50% visible
}
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const adStatus = entry.target.getAttribute('data-adsbygoogle-status')
if (!adStatus || adStatus !== 'done') {
adsbygoogle.push(entry.target)
observer.unobserve(entry.target) // stop observing once ad is loaded
}
}
} catch (e) {}
})
}, observerOptions)
ads.forEach(ad => {
observer.observe(ad)
})
}
}
// 获取节点或其子节点中包含 adsbygoogle 类的节点
function getNodesWithAdsByGoogleClass(node) {
const adsNodes = []
// 检查节点及其子节点是否包含 adsbygoogle 类
function checkNodeForAds(node) {
if (
node.nodeType === Node.ELEMENT_NODE &&
node.classList.contains('adsbygoogle')
) {
adsNodes.push(node)
} else {
// 递归检查子节点
for (let i = 0; i < node.childNodes.length; i++) {
checkNodeForAds(node.childNodes[i])
}
}
}
checkNodeForAds(node)
return adsNodes
}
/**
* 初始化谷歌广告
* @returns
*/
export default function GoogleAdsense() {
const initGoogleAdsense = () => {
loadExternalResource(
`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${siteConfig('ADSENSE_GOOGLE_ID')}`,
'js'
).then(url => {
setTimeout(() => {
requestAd()
}, 100)
})
}
const router = useRouter()
useEffect(() => {
// 延迟3秒加载
export const initGoogleAdsense = async () => {
console.log('Load Adsense')
loadExternalResource(
`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${siteConfig('ADSENSE_GOOGLE_ID')}`,
'js'
).then(url => {
setTimeout(() => {
initGoogleAdsense()
}, 3000)
}, [router])
// 页面加载完成后加载一次广告
const ads = document.getElementsByClassName('adsbygoogle')
if (window.adsbygoogle && ads.length > 0) {
requestAd(Array.from(ads))
}
return null
// 创建一个 MutationObserver 实例,监听页面上新出现的广告单元
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
// 检查每个添加到DOM中的节点
mutation.addedNodes.forEach(node => {
// 如果节点是adsbygoogle元素则请求广告
if (node.nodeType === Node.ELEMENT_NODE) {
const adsNodes = getNodesWithAdsByGoogleClass(node)
if (adsNodes.length > 0) {
requestAd(adsNodes)
}
}
})
})
})
// 配置 MutationObserver 监听特定类型的 DOM 变化
const observerConfig = {
childList: true, // 观察目标子节点的变化
subtree: true // 包括目标节点的所有后代节点
}
// 启动 MutationObserver
observer.observe(document.body, observerConfig)
}, 100)
})
}
/**
@@ -128,9 +185,18 @@ const AdEmbed = () => {
const newInsElement = document.createElement('ins')
newInsElement.className = 'adsbygoogle w-full py-1'
newInsElement.style.display = 'block'
newInsElement.setAttribute('data-ad-client', siteConfig('ADSENSE_GOOGLE_ID'))
newInsElement.setAttribute('data-adtest', siteConfig('ADSENSE_GOOGLE_TEST') ? 'on' : 'off')
newInsElement.setAttribute('data-ad-slot', siteConfig('ADSENSE_GOOGLE_SLOT_AUTO'))
newInsElement.setAttribute(
'data-ad-client',
siteConfig('ADSENSE_GOOGLE_ID')
)
newInsElement.setAttribute(
'data-adtest',
siteConfig('ADSENSE_GOOGLE_TEST') ? 'on' : 'off'
)
newInsElement.setAttribute(
'data-ad-slot',
siteConfig('ADSENSE_GOOGLE_SLOT_AUTO')
)
newInsElement.setAttribute('data-ad-format', 'auto')
newInsElement.setAttribute('data-full-width-responsive', 'true')

View File

@@ -12,12 +12,16 @@ export default function Live2D() {
const { theme, switchTheme } = useGlobal()
const showPet = JSON.parse(siteConfig('WIDGET_PET'))
const petLink = siteConfig('WIDGET_PET_LINK')
const petSwitchTheme = siteConfig('WIDGET_PET_SWITCH_THEME')
useEffect(() => {
if (showPet && !isMobile()) {
Promise.all([
loadExternalResource('https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget@latest/live2d.min.js', 'js')
]).then((e) => {
loadExternalResource(
'https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget@latest/live2d.min.js',
'js'
)
]).then(e => {
if (typeof window?.loadlive2d !== 'undefined') {
// https://github.com/xiazeyu/live2d-widget-models
try {
@@ -31,7 +35,7 @@ export default function Live2D() {
}, [theme])
function handleClick() {
if (JSON.parse(siteConfig('WIDGET_PET_SWITCH_THEME'))) {
if (petSwitchTheme) {
switchTheme()
}
}
@@ -40,9 +44,15 @@ export default function Live2D() {
return <></>
}
return <canvas id="live2d" width="280" height="250" onClick={handleClick}
className="cursor-grab"
onMouseDown={(e) => e.target.classList.add('cursor-grabbing')}
onMouseUp={(e) => e.target.classList.remove('cursor-grabbing')}
return (
<canvas
id='live2d'
width='280'
height='250'
onClick={handleClick}
className='cursor-grab'
onMouseDown={e => e.target.classList.add('cursor-grabbing')}
onMouseUp={e => e.target.classList.remove('cursor-grabbing')}
/>
)
}

View File

@@ -1,20 +1,36 @@
import { loadExternalResource } from '@/lib/utils'
import { useRouter } from 'next/router'
import NProgress from 'nprogress'
import { useEffect } from 'react'
import { useEffect, useState } from 'react'
/**
* 出现页面加载进度条
*/
export default function LoadingProgress() {
const router = useRouter()
const [NProgress, setNProgress] = useState(null)
// 加载进度条
useEffect(() => {
const handleStart = (url) => {
NProgress.start()
loadExternalResource(
'https://cdn.bootcdn.net/ajax/libs/nprogress/0.2.0/nprogress.min.js',
'js'
).then(() => {
if (window.NProgress) {
setNProgress(window.NProgress)
// 调速
window.NProgress.settings.minimun = 0.1
loadExternalResource(
'https://cdn.bootcdn.net/ajax/libs/nprogress/0.2.0/nprogress.min.css',
'css'
)
}
})
const handleStart = url => {
NProgress?.start()
}
const handleStop = () => {
NProgress.done()
NProgress?.done()
}
router.events.on('routeChangeStart', handleStart)

View File

@@ -1,53 +1,52 @@
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import copy from 'copy-to-clipboard'
import dynamic from 'next/dynamic'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import {
FacebookShareButton,
FacebookIcon,
FacebookMessengerShareButton,
FacebookMessengerIcon,
RedditShareButton,
RedditIcon,
LineShareButton,
LineIcon,
EmailShareButton,
EmailIcon,
TwitterShareButton,
TwitterIcon,
TelegramShareButton,
TelegramIcon,
WhatsappShareButton,
WhatsappIcon,
LinkedinShareButton,
EmailShareButton,
FacebookIcon,
FacebookMessengerIcon,
FacebookMessengerShareButton,
FacebookShareButton,
HatenaIcon,
HatenaShareButton,
InstapaperIcon,
InstapaperShareButton,
LineIcon,
LineShareButton,
LinkedinIcon,
PinterestShareButton,
PinterestIcon,
VKIcon,
VKShareButton,
OKShareButton,
OKIcon,
TumblrShareButton,
TumblrIcon,
LinkedinShareButton,
LivejournalIcon,
LivejournalShareButton,
MailruShareButton,
MailruIcon,
MailruShareButton,
OKIcon,
OKShareButton,
PinterestIcon,
PinterestShareButton,
PocketIcon,
PocketShareButton,
RedditIcon,
RedditShareButton,
TelegramIcon,
TelegramShareButton,
TumblrIcon,
TumblrShareButton,
TwitterIcon,
TwitterShareButton,
VKIcon,
VKShareButton,
ViberIcon,
ViberShareButton,
WorkplaceShareButton,
WorkplaceIcon,
WeiboShareButton,
WeiboIcon,
PocketShareButton,
PocketIcon,
InstapaperShareButton,
InstapaperIcon,
HatenaShareButton,
HatenaIcon
WeiboShareButton,
WhatsappIcon,
WhatsappShareButton,
WorkplaceIcon,
WorkplaceShareButton
} from 'react-share'
const QrCode = dynamic(() => import('@/components/QrCode'), { ssr: false })
@@ -59,10 +58,11 @@ const QrCode = dynamic(() => import('@/components/QrCode'), { ssr: false })
*/
const ShareButtons = ({ post }) => {
const router = useRouter()
const shareUrl = siteConfig('LINK') + router.asPath
const [shareUrl, setShareUrl] = useState(siteConfig('LINK') + router.asPath)
const title = post.title || siteConfig('TITLE')
const image = post.pageCover
const body = post?.title + ' | ' + title + ' ' + shareUrl + ' ' + post?.summary
const body =
post?.title + ' | ' + title + ' ' + shareUrl + ' ' + post?.summary
const services = siteConfig('POSTS_SHARE_SERVICES').split(',')
const titleWithSiteInfo = title + ' | ' + siteConfig('TITLE')
@@ -70,8 +70,8 @@ const ShareButtons = ({ post }) => {
const [qrCodeShow, setQrCodeShow] = useState(false)
const copyUrl = () => {
copy(shareUrl)
alert(locale.COMMON.URL_COPIED)
navigator?.clipboard?.writeText(shareUrl)
alert(locale.COMMON.URL_COPIED + ' \n' + shareUrl)
}
const openPopover = () => {
@@ -81,300 +81,308 @@ const ShareButtons = ({ post }) => {
setQrCodeShow(false)
}
useEffect(() => {
setShareUrl(window.location.href)
}, [])
return (
<>
{services.map(singleService => {
if (singleService === 'facebook') {
return (
<FacebookShareButton
key={singleService}
url={shareUrl}
className="mx-1"
>
<FacebookIcon size={32} round />
</FacebookShareButton>
)
}
if (singleService === 'messenger') {
return (
<FacebookMessengerShareButton
key={singleService}
url={shareUrl}
appId={siteConfig('FACEBOOK_APP_ID')}
className="mx-1"
>
<FacebookMessengerIcon size={32} round />
</FacebookMessengerShareButton>
)
}
if (singleService === 'line') {
return (
<LineShareButton
key={singleService}
url={shareUrl}
className="mx-1"
>
<LineIcon size={32} round />
</LineShareButton>
)
}
if (singleService === 'reddit') {
return (
<RedditShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
windowWidth={660}
windowHeight={460}
className="mx-1"
>
<RedditIcon size={32} round />
</RedditShareButton>
)
}
if (singleService === 'email') {
return (
<EmailShareButton
key={singleService}
url={shareUrl}
subject={titleWithSiteInfo}
body={body}
className="mx-1"
>
<EmailIcon size={32} round />
</EmailShareButton>
)
}
if (singleService === 'twitter') {
return (
<TwitterShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
className="mx-1"
>
<TwitterIcon size={32} round />
</TwitterShareButton>
)
}
if (singleService === 'telegram') {
return (
<TelegramShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
className="mx-1"
>
<TelegramIcon size={32} round />
</TelegramShareButton>
)
}
if (singleService === 'whatsapp') {
return (
<WhatsappShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
separator=":: "
className="mx-1"
>
<WhatsappIcon size={32} round />
</WhatsappShareButton>
)
}
if (singleService === 'linkedin') {
return (
<LinkedinShareButton
key={singleService}
url={shareUrl}
className="mx-1"
>
<LinkedinIcon size={32} round />
</LinkedinShareButton>
)
}
if (singleService === 'pinterest') {
return (
<PinterestShareButton
key={singleService}
url={shareUrl}
media={image}
className="mx-1"
>
<PinterestIcon size={32} round />
</PinterestShareButton>
)
}
if (singleService === 'vkshare') {
return (
<VKShareButton
key={singleService}
url={shareUrl}
image={image}
className="mx-1"
>
<VKIcon size={32} round />
</VKShareButton>
)
}
if (singleService === 'okshare') {
return (
<OKShareButton
key={singleService}
url={shareUrl}
image={image}
className="mx-1"
>
<OKIcon size={32} round />
</OKShareButton>
)
}
if (singleService === 'tumblr') {
return (
<TumblrShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
className="mx-1"
>
<TumblrIcon size={32} round />
</TumblrShareButton>
)
}
if (singleService === 'livejournal') {
return (
<LivejournalShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
description={shareUrl}
className="mx-1"
>
<LivejournalIcon size={32} round />
</LivejournalShareButton>
)
}
if (singleService === 'mailru') {
return (
<MailruShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
className="mx-1"
>
<MailruIcon size={32} round />
</MailruShareButton>
)
}
if (singleService === 'viber') {
return (
<ViberShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
className="mx-1"
>
<ViberIcon size={32} round />
</ViberShareButton>
)
}
if (singleService === 'workplace') {
return (
<WorkplaceShareButton
key={singleService}
url={shareUrl}
quote={titleWithSiteInfo}
className="mx-1"
>
<WorkplaceIcon size={32} round />
</WorkplaceShareButton>
)
}
if (singleService === 'weibo') {
return (
<WeiboShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
image={image}
className="mx-1"
>
<WeiboIcon size={32} round />
</WeiboShareButton>
)
}
if (singleService === 'pocket') {
return (
<PocketShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
className="mx-1"
>
<PocketIcon size={32} round />
</PocketShareButton>
)
}
if (singleService === 'instapaper') {
return (
<InstapaperShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
className="mx-1"
>
<InstapaperIcon size={32} round />
</InstapaperShareButton>
)
}
if (singleService === 'hatena') {
return (
<HatenaShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
windowWidth={660}
windowHeight={460}
className="mx-1"
>
<HatenaIcon size={32} round />
</HatenaShareButton>
)
}
if (singleService === 'qq') {
return <button key={singleService} className='cursor-pointer bg-blue-600 text-white rounded-full mx-1'>
<a target='_blank' rel='noreferrer' aria-label="Share by QQ" href={`http://connect.qq.com/widget/shareqq/index.html?url=${shareUrl}&sharesource=qzone&title=${title}&desc=${body}`} >
<i className='fab fa-qq w-8' />
</a>
</button>
}
if (singleService === 'wechat') {
return <button onMouseEnter={openPopover} onMouseLeave={closePopover} aria-label={singleService} key={singleService} className='cursor-pointer bg-green-600 text-white rounded-full mx-1'>
<div id='wechat-button'>
<i className='fab fa-weixin w-8' />
</div>
<div className='absolute'>
<div id='pop' className={(qrCodeShow ? 'opacity-100 ' : ' invisible opacity-0') + ' z-40 absolute bottom-10 -left-10 bg-white shadow-xl transition-all duration-200 text-center'}>
<div className='p-2 mt-1 w-28 h-28'>
{ qrCodeShow && <QrCode value={shareUrl}/> }
</div>
<span className='text-black font-semibold p-1 rounded-t-lg text-sm mx-auto mb-1'>
{locale.COMMON.SCAN_QR_CODE}
</span>
</div>
</div>
</button>
}
if (singleService === 'link') {
return <button aria-label={singleService} key={singleService} className='cursor-pointer bg-yellow-500 text-white rounded-full mx-1'>
<div alt={locale.COMMON.URL_COPIED} onClick={copyUrl} >
<i className='fas fa-link w-8' />
</div>
</button>
}
return <></>
})}
</>
<>
{services.map(singleService => {
if (singleService === 'facebook') {
return (
<FacebookShareButton
key={singleService}
url={shareUrl}
className='mx-1'>
<FacebookIcon size={32} round />
</FacebookShareButton>
)
}
if (singleService === 'messenger') {
return (
<FacebookMessengerShareButton
key={singleService}
url={shareUrl}
appId={siteConfig('FACEBOOK_APP_ID')}
className='mx-1'>
<FacebookMessengerIcon size={32} round />
</FacebookMessengerShareButton>
)
}
if (singleService === 'line') {
return (
<LineShareButton
key={singleService}
url={shareUrl}
className='mx-1'>
<LineIcon size={32} round />
</LineShareButton>
)
}
if (singleService === 'reddit') {
return (
<RedditShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
windowWidth={660}
windowHeight={460}
className='mx-1'>
<RedditIcon size={32} round />
</RedditShareButton>
)
}
if (singleService === 'email') {
return (
<EmailShareButton
key={singleService}
url={shareUrl}
subject={titleWithSiteInfo}
body={body}
className='mx-1'>
<EmailIcon size={32} round />
</EmailShareButton>
)
}
if (singleService === 'twitter') {
return (
<TwitterShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
className='mx-1'>
<TwitterIcon size={32} round />
</TwitterShareButton>
)
}
if (singleService === 'telegram') {
return (
<TelegramShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
className='mx-1'>
<TelegramIcon size={32} round />
</TelegramShareButton>
)
}
if (singleService === 'whatsapp') {
return (
<WhatsappShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
separator=':: '
className='mx-1'>
<WhatsappIcon size={32} round />
</WhatsappShareButton>
)
}
if (singleService === 'linkedin') {
return (
<LinkedinShareButton
key={singleService}
url={shareUrl}
className='mx-1'>
<LinkedinIcon size={32} round />
</LinkedinShareButton>
)
}
if (singleService === 'pinterest') {
return (
<PinterestShareButton
key={singleService}
url={shareUrl}
media={image}
className='mx-1'>
<PinterestIcon size={32} round />
</PinterestShareButton>
)
}
if (singleService === 'vkshare') {
return (
<VKShareButton
key={singleService}
url={shareUrl}
image={image}
className='mx-1'>
<VKIcon size={32} round />
</VKShareButton>
)
}
if (singleService === 'okshare') {
return (
<OKShareButton
key={singleService}
url={shareUrl}
image={image}
className='mx-1'>
<OKIcon size={32} round />
</OKShareButton>
)
}
if (singleService === 'tumblr') {
return (
<TumblrShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
className='mx-1'>
<TumblrIcon size={32} round />
</TumblrShareButton>
)
}
if (singleService === 'livejournal') {
return (
<LivejournalShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
description={shareUrl}
className='mx-1'>
<LivejournalIcon size={32} round />
</LivejournalShareButton>
)
}
if (singleService === 'mailru') {
return (
<MailruShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
className='mx-1'>
<MailruIcon size={32} round />
</MailruShareButton>
)
}
if (singleService === 'viber') {
return (
<ViberShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
className='mx-1'>
<ViberIcon size={32} round />
</ViberShareButton>
)
}
if (singleService === 'workplace') {
return (
<WorkplaceShareButton
key={singleService}
url={shareUrl}
quote={titleWithSiteInfo}
className='mx-1'>
<WorkplaceIcon size={32} round />
</WorkplaceShareButton>
)
}
if (singleService === 'weibo') {
return (
<WeiboShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
image={image}
className='mx-1'>
<WeiboIcon size={32} round />
</WeiboShareButton>
)
}
if (singleService === 'pocket') {
return (
<PocketShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
className='mx-1'>
<PocketIcon size={32} round />
</PocketShareButton>
)
}
if (singleService === 'instapaper') {
return (
<InstapaperShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
className='mx-1'>
<InstapaperIcon size={32} round />
</InstapaperShareButton>
)
}
if (singleService === 'hatena') {
return (
<HatenaShareButton
key={singleService}
url={shareUrl}
title={titleWithSiteInfo}
windowWidth={660}
windowHeight={460}
className='mx-1'>
<HatenaIcon size={32} round />
</HatenaShareButton>
)
}
if (singleService === 'qq') {
return (
<button
key={singleService}
className='cursor-pointer bg-blue-600 text-white rounded-full mx-1'>
<a
target='_blank'
rel='noreferrer'
aria-label='Share by QQ'
href={`http://connect.qq.com/widget/shareqq/index.html?url=${shareUrl}&sharesource=qzone&title=${title}&desc=${body}`}>
<i className='fab fa-qq w-8' />
</a>
</button>
)
}
if (singleService === 'wechat') {
return (
<button
onMouseEnter={openPopover}
onMouseLeave={closePopover}
aria-label={singleService}
key={singleService}
className='cursor-pointer bg-green-600 text-white rounded-full mx-1'>
<div id='wechat-button'>
<i className='fab fa-weixin w-8' />
</div>
<div className='absolute'>
<div
id='pop'
className={
(qrCodeShow ? 'opacity-100 ' : ' invisible opacity-0') +
' z-40 absolute bottom-10 -left-10 bg-white shadow-xl transition-all duration-200 text-center'
}>
<div className='p-2 mt-1 w-28 h-28'>
{qrCodeShow && <QrCode value={shareUrl} />}
</div>
<span className='text-black font-semibold p-1 rounded-t-lg text-sm mx-auto mb-1'>
{locale.COMMON.SCAN_QR_CODE}
</span>
</div>
</div>
</button>
)
}
if (singleService === 'link') {
return (
<button
aria-label={singleService}
key={singleService}
className='cursor-pointer bg-yellow-500 text-white rounded-full mx-1'>
<div alt={locale.COMMON.URL_COPIED} onClick={copyUrl}>
<i className='fas fa-link w-8' />
</div>
</button>
)
}
return <></>
})}
</>
)
}

View File

@@ -1,7 +1,6 @@
import MemoryCache from './memory_cache'
import FileCache from './local_file_cache'
import MongoCache from './mongo_db_cache'
import BLOG from '@/blog.config'
import FileCache from './local_file_cache'
import MemoryCache from './memory_cache'
/**
* 为减少频繁接口请求notion数据将被缓存
@@ -39,9 +38,7 @@ export async function delCacheData(key) {
* @returns
*/
function getApi() {
if (process.env.MONGO_DB_URL && process.env.MONGO_DB_NAME) {
return MongoCache
} else if (process.env.ENABLE_FILE_CACHE) {
if (process.env.ENABLE_FILE_CACHE) {
return FileCache
} else {
return MemoryCache

View File

@@ -1,49 +0,0 @@
const MongoClient = require('mongodb').MongoClient
const DB_URL = process.env.MONGO_DB_URL // e.g. mongodb+srv://mongo_user:[password]@xxx.mongodb.net//?retryWrites=true&w=majority
const DB_NAME = process.env.MONGO_DB_NAME // e.g. tangly1024
const DB_COLLECTION = 'posts'
export async function getCache (key) {
const client = await MongoClient.connect(DB_URL).catch(err => { console.error(err) })
const dbo = client.db(DB_NAME)
const query = { block_id: key }
const res = await dbo.collection('posts').findOne(query).catch(err => { console.error(err) })
await client.close()
return res
}
/**
* 并发请求写文件异常; Vercel生产环境不支持写文件。
* @param key
* @param data
* @returns {Promise<null>}
*/
export async function setCache (key, data) {
const client = await MongoClient.connect(DB_URL).catch(err => { console.error(err) })
const dbo = client.db(DB_NAME)
data.block_id = key
const query = { block_id: key }
const jsonObj = JSON.parse(JSON.stringify(data))
const updRes = await dbo.collection(DB_COLLECTION).updateOne(query, { $set: jsonObj }).catch(err => { console.error(err) })
console.log('更新结果', key, updRes)
if (updRes.matchedCount === 0) {
const insertRes = await dbo.collection(DB_COLLECTION).insertOne(jsonObj).catch(err => { console.error(err) })
console.log('插入结果', key, insertRes)
}
await client.close()
return data
}
export async function delCache (key, data) {
const client = await MongoClient.connect(DB_URL).catch(err => { console.error(err) })
const dbo = client.db(DB_NAME)
const query = { block_id: key }
const res = await dbo.collection('posts').deleteOne(query).catch(err => { console.error(err) })
console.log('删除结果', key, res)
await client.close()
return null
}
export default { getCache, setCache, delCache }

View File

@@ -52,7 +52,7 @@ export const siteConfig = (key, defaultVal = null, extendConfig = null) => {
}
}
// 其次 有传入的配置参考,则尝试读取
// 其次 有传入的extendConfig,则尝试读取
if (!val && extendConfig) {
val = extendConfig[key]
}
@@ -64,24 +64,37 @@ export const siteConfig = (key, defaultVal = null, extendConfig = null) => {
if (!val) {
return defaultVal
} else {
if (typeof val === 'string') {
if (val === 'true' || val === 'false') {
return JSON.parse(val)
}
if (/^\d+$/.test(val)) {
// 如果是数字使用parseFloat或者parseInt将字符串转换为数字
return parseInt(val)
}
return val
} else {
try {
return JSON.parse(val)
} catch (error) {
// 如果值是一个字符串但不是有效的 JSON 格式,直接返回字符串
return val
}
}
// 从Notion_CONFIG读取的配置通常都是字符串适当转义
if (typeof val === 'string') {
// 解析布尔
if (val === 'true' || val === 'false') {
return JSON.parse(val)
}
// 解析数字parseInt将字符串转换为数字
if (/^\d+$/.test(val)) {
return parseInt(val)
}
// 转移 [] , {} 这种json串为json对象
try {
const parsedJson = JSON.parse(val)
// 检查解析后的结果是否是对象或数组
if (typeof parsedJson === 'object' && parsedJson !== null) {
return parsedJson
}
} catch (error) {
// JSON 解析失败,返回原始字符串值
return val
}
}
try {
return JSON.parse(val)
} catch (error) {
// 如果值是一个字符串但不是有效的 JSON 格式,直接返回字符串
return val
}
}

View File

@@ -9,6 +9,7 @@ import { getPostBlocks, getSingleBlock } from '@/lib/notion/getPostBlocks'
import { compressImage, mapImgUrl } from '@/lib/notion/mapImage'
import { deepClone } from '@/lib/utils'
import { idToUuid } from 'notion-utils'
import { extractLangId, extractLangPrefix } from '../utils/pageId'
export { getAllTags } from '../notion/getAllTags'
export { getPost } from '../notion/getNotionPost'
@@ -18,18 +19,62 @@ export { getPostBlocks } from '../notion/getPostBlocks'
* 获取博客数据; 基于Notion实现
* @param {*} pageId
* @param {*} from
* @param latestPostCount 截取最新文章数量
* @param categoryCount
* @param tagsCount 截取标签数量
* @param pageType 过滤的文章类型,数组格式 ['Page','Post']
* @param {*} locale 语言 zh|en|jp 等等
* @returns
*
*/
export async function getGlobalData({ pageId = BLOG.NOTION_PAGE_ID, from }) {
// 从notion获取
const data = await getNotionPageData({ pageId, from })
export async function getGlobalData({
pageId = BLOG.NOTION_PAGE_ID,
from,
locale
}) {
// 获取站点数据 如果pageId有逗号隔开则分次取数据
const siteIds = pageId?.split(',') || []
let data = EmptyData(pageId)
try {
for (let index = 0; index < siteIds.length; index++) {
const siteId = siteIds[index]
const id = extractLangId(siteId)
const prefix = extractLangPrefix(siteId)
// 第一个id站点默认语言
if (index === 0 || locale === prefix) {
data = await getNotionPageData({
pageId: id,
from
})
}
}
} catch (error) {
console.error('异常', error)
}
return data
}
/**
* 获取指定notion的collection数据
* @param pageId
* @param from 请求来源
* @returns {Promise<JSX.Element|*|*[]>}
*/
export async function getNotionPageData({ pageId, from }) {
// 尝试从缓存获取
const cacheKey = 'page_block_' + pageId
let data = await getDataFromCache(cacheKey)
if (data && data.pageIds?.length > 0) {
// console.log('[API<<--缓存]', `from:${from}`, `root-page-id:${pageId}`)
// return data
} else {
// 从接口读取
data = await getDataBaseInfoByNotionAPI({ pageId, from })
// 存入缓存
if (data) {
await setDataToCache(cacheKey, data)
}
}
// 返回给前端的数据做处理
const db = deepClone(data)
// 减少返回给前端的数据,减少流量损耗
delete db.block
delete db.schema
delete db.rawMetadata
@@ -47,10 +92,12 @@ export async function getGlobalData({ pageId = BLOG.NOTION_PAGE_ID, from }) {
if (db?.post) {
db.post = cleanBlock(db?.post)
}
return db
}
/**
* 清理block数据
*/
function cleanBlock(post) {
const pageBlock = post?.blockMap?.block
for (const i in pageBlock) {
@@ -98,28 +145,6 @@ function getLatestPosts({ allPages, from, latestPostCount }) {
return latestPosts.slice(0, latestPostCount)
}
/**
* 获取指定notion的collection数据
* @param pageId
* @param from 请求来源
* @returns {Promise<JSX.Element|*|*[]>}
*/
export async function getNotionPageData({ pageId, from }) {
// 尝试从缓存获取
const cacheKey = 'page_block_' + pageId
const data = await getDataFromCache(cacheKey)
if (data && data.pageIds?.length > 0) {
// console.log('[API<<--缓存]', `from:${from}`, `root-page-id:${pageId}`)
return data
}
const db = await getDataBaseInfoByNotionAPI({ pageId, from })
// 存入缓存
if (db) {
await setDataToCache(cacheKey, db)
}
return db
}
/**
* 获取用户自定义单页菜单
* 旧版本不读取Menu菜单而是读取type=Page生成菜单
@@ -167,7 +192,7 @@ function getCustomMenu({ collectionData }) {
if (menuPages && menuPages.length > 0) {
menuPages.forEach(e => {
e.show = true
if (e?.slug?.indexOf('http') === 0) {
if (e?.slug?.indexOf('http') === 0 && e?.slug?.indexOf(BLOG.LINK) < 0) {
e.target = '_blank'
e.to = e.slug
} else {
@@ -225,13 +250,24 @@ function getCategoryOptions(schema) {
* @param from
* @returns {Promise<{title,description,pageCover,icon}>}
*/
function getSiteInfo({ collection, block, NOTION_CONFIG }) {
function getSiteInfo({ collection, block, NOTION_CONFIG, pageId }) {
if (!collection || !block || NOTION_CONFIG || pageId) {
return {
title: BLOG.TITLE,
description: BLOG.DESCRIPTION,
pageCover: BLOG.HOME_BANNER_IMAGE,
icon: BLOG.AVATAR,
link: BLOG.LINK
}
}
const title = collection?.name?.[0][0] || BLOG.TITLE
const description = collection?.description
? Object.assign(collection).description[0][0]
: BLOG.DESCRIPTION
const pageCover = collection?.cover
? mapImgUrl(collection?.cover, block[idToUuid(BLOG.NOTION_PAGE_ID)]?.value)
? mapImgUrl(collection?.cover, block[pageId]?.value)
: BLOG.HOME_BANNER_IMAGE
// 用户头像压缩一下
let icon = compressImage(
@@ -308,6 +344,7 @@ const EmptyData = pageId => {
status: 'Published',
type: 'Post',
slug: '13a171332816461db29d50e9f575b00d',
pageCoverThumbnail: BLOG.HOME_BANNER_IMAGE,
date: {
start_date: '2023-04-24',
lastEditedDay: '2023-04-24',
@@ -426,7 +463,7 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
// 站点配置优先读取配置表格否则读取blog.config.js 文件
const NOTION_CONFIG = (await getConfigMapFromConfigPage(collectionData)) || {}
const siteInfo = getSiteInfo({ collection, block })
const siteInfo = getSiteInfo({ collection, block, pageId })
// 查找所有的Post和Page
const allPages = collectionData.filter(post => {

View File

@@ -1,8 +1,17 @@
import { generateLocaleDict, initLocale, saveLangToLocalStorage } from './lang'
import { createContext, useContext, useEffect, useState } from 'react'
import {
THEMES,
initDarkMode,
saveDarkModeToLocalStorage
} from '@/themes/theme'
import { APPEARANCE, LANG, NOTION_PAGE_ID, THEME } from 'blog.config'
import { useRouter } from 'next/router'
import { THEMES, initDarkMode, saveDarkModeToLocalStorage } from '@/themes/theme'
import { APPEARANCE, LANG, THEME } from 'blog.config'
import { createContext, useContext, useEffect, useState } from 'react'
import {
generateLocaleDict,
initLocale,
redirectUserLang,
saveLangToLocalStorage
} from './lang'
const GlobalContext = createContext()
/**
@@ -12,9 +21,18 @@ const GlobalContext = createContext()
* @constructor
*/
export function GlobalContextProvider(props) {
const { post, children, siteInfo, categoryOptions, tagOptions, NOTION_CONFIG } = props
const {
post,
children,
siteInfo,
categoryOptions,
tagOptions,
NOTION_CONFIG
} = props
const [lang, updateLang] = useState(NOTION_CONFIG?.LANG || LANG) // 默认语言
const [locale, updateLocale] = useState(generateLocaleDict(NOTION_CONFIG?.LANG || LANG)) // 默认语言
const [locale, updateLocale] = useState(
generateLocaleDict(NOTION_CONFIG?.LANG || LANG)
) // 默认语言
const [theme, setTheme] = useState(NOTION_CONFIG?.THEME || THEME) // 默认博客主题
const defaultDarkMode = NOTION_CONFIG?.APPEARANCE || APPEARANCE
const [isDarkMode, updateDarkMode] = useState(defaultDarkMode === 'dark') // 默认深色模式
@@ -48,6 +66,7 @@ export function GlobalContextProvider(props) {
/**
* 更新语言
* 这里是代码级别的多语言,整个站点和文章内容的多语言不在此处理
*/
function changeLang(lang) {
if (lang) {
@@ -60,6 +79,7 @@ export function GlobalContextProvider(props) {
useEffect(() => {
initDarkMode(updateDarkMode, defaultDarkMode)
initLocale(lang, locale, updateLang, updateLocale)
redirectUserLang(NOTION_PAGE_ID)
}, [])
// 加载进度条

View File

@@ -1,11 +1,12 @@
import zhCN from './lang/zh-CN'
import { getQueryVariable, isBrowser, mergeDeep } from '@/lib/utils'
import enUS from './lang/en-US'
import frFR from './lang/fr-FR'
import jaJP from './lang/ja-JP'
import trTR from './lang/tr-TR'
import zhCN from './lang/zh-CN'
import zhHK from './lang/zh-HK'
import zhTW from './lang/zh-TW'
import frFR from './lang/fr-FR'
import trTR from './lang/tr-TR'
import jaJP from './lang/ja-JP'
import { getQueryVariable, isBrowser, mergeDeep } from '@/lib/utils'
import { extractLangPrefix } from './utils/pageId'
/**
* 在这里配置所有支持的语言
@@ -43,7 +44,9 @@ export function generateLocaleDict(langString) {
// 然后尝试匹配只有语言匹配的情况
if (!userLocale) {
const languageOnlyLocales = supportedLocales.filter(locale => locale.startsWith(language))
const languageOnlyLocales = supportedLocales.filter(locale =>
locale.startsWith(language)
)
if (languageOnlyLocales.length > 0) {
userLocale = LANGS[languageOnlyLocales[0]]
}
@@ -51,7 +54,9 @@ export function generateLocaleDict(langString) {
// 如果还没匹配到,则返回最接近的语言包
if (!userLocale) {
const fallbackLocale = supportedLocales.find(locale => locale.startsWith('en'))
const fallbackLocale = supportedLocales.find(locale =>
locale.startsWith('en')
)
userLocale = LANGS[fallbackLocale]
}
@@ -64,7 +69,11 @@ export function generateLocaleDict(langString) {
*/
export function initLocale(lang, locale, changeLang, changeLocale) {
if (isBrowser) {
const queryLang = getQueryVariable('lang') || loadLangFromLocalStorage()
// 用户请求的预研
const queryLang =
getQueryVariable('locale') ||
getQueryVariable('lang') ||
loadLangFromLocalStorage()
let currentLang = lang
if (queryLang && queryLang !== 'undefined' && queryLang !== lang) {
currentLang = queryLang
@@ -91,6 +100,39 @@ export const loadLangFromLocalStorage = () => {
* 保存语言
* @param newTheme
*/
export const saveLangToLocalStorage = (lang) => {
export const saveLangToLocalStorage = lang => {
localStorage.setItem('lang', lang)
}
/**
* 检测用户的预研偏好,跳转至对应的多语言网站
* @param {*} lang
* @param {*} pageId
*
*/
export const redirectUserLang = (lang, pageId) => {
if (!isBrowser) {
return
}
// 只在首页处理跳转
if (!window.location.pathname === '/') {
return
}
const userLang =
getQueryVariable('locale') ||
getQueryVariable('lang') ||
window?.navigator?.language
const siteIds = pageId?.split(',') || []
// 默认是进首页; 如果检测到有一个多语言匹配了用户浏览器,则自动跳转过去
for (let index = 0; index < siteIds.length; index++) {
const siteId = siteIds[index]
const prefix = extractLangPrefix(siteId)
if (prefix === userLang) {
if (window.location.pathname.indexOf(prefix) < 0) {
window.location.href = '/' + prefix
}
}
}
}

View File

@@ -7,8 +7,9 @@
*
*/
import { getDateValue, getTextContent } from 'notion-utils'
import { getPostBlocks } from './getPostBlocks'
import { deepClone } from '../utils'
import getAllPageIds from './getAllPageIds'
import { getPostBlocks } from './getPostBlocks'
/**
* 从Notion中读取Config配置表
@@ -23,8 +24,15 @@ export async function getConfigMapFromConfigPage(allPages) {
console.warn('[Notion配置] 忽略的配置')
return null
}
// 找到Config类
const configPage = allPages?.find(post => {
return post && post?.type && (post?.type === 'CONFIG' || post?.type === 'config' || post?.type === 'Config')
return (
post &&
post?.type &&
(post?.type === 'CONFIG' ||
post?.type === 'config' ||
post?.type === 'Config')
)
})
if (!configPage) {
@@ -43,21 +51,26 @@ export async function getConfigMapFromConfigPage(allPages) {
}
if (!content) {
console.warn('[Notion配置] 未找到配置表格', pageRecordMap.block[configPageId], pageRecordMap.block[configPageId].value)
console.warn(
'[Notion配置] 未找到配置表格',
pageRecordMap.block[configPageId],
pageRecordMap.block[configPageId].value
)
return null
}
// 找到配置文件中的database
// for (const contentId of content) {
// console.log('内容', contentId, configPageRecordMap.block[contentId].value.type === 'collection_view')
// }
// 找到PAGE文件中的database
const configTableId = content?.find(contentId => {
return pageRecordMap.block[contentId].value.type === 'collection_view'
})
// eslint-disable-next-line no-constant-condition, no-self-compare
if (!configTableId) {
console.warn('[Notion配置]未找到配置表格数据', pageRecordMap.block[configPageId], pageRecordMap.block[configPageId].value)
console.warn(
'[Notion配置]未找到配置表格数据',
pageRecordMap.block[configPageId],
pageRecordMap.block[configPageId].value
)
return null
}
@@ -67,7 +80,8 @@ export async function getConfigMapFromConfigPage(allPages) {
const rawMetadata = databaseRecordMap.value
// Check Type Page-Database和Inline-Database
if (
rawMetadata?.type !== 'collection_view_page' && rawMetadata?.type !== 'collection_view'
rawMetadata?.type !== 'collection_view_page' &&
rawMetadata?.type !== 'collection_view'
) {
console.error(`pageId "${configTableId}" is not a database`)
return null
@@ -79,9 +93,21 @@ export async function getConfigMapFromConfigPage(allPages) {
const collectionView = pageRecordMap.collection_view
const schema = collection?.schema
const viewIds = rawMetadata?.view_ids
const pageIds = getAllPageIds(collectionQuery, collectionId, collectionView, viewIds)
const pageIds = getAllPageIds(
collectionQuery,
collectionId,
collectionView,
viewIds
)
if (pageIds?.length === 0) {
console.error('[Notion配置]获取到的文章列表为空请检查notion模板', collectionQuery, collection, collectionView, viewIds, databaseRecordMap)
console.error(
'[Notion配置]获取到的文章列表为空请检查notion模板',
collectionQuery,
collection,
collectionView,
viewIds,
databaseRecordMap
)
}
// 遍历用户的表格
for (let i = 0; i < pageIds.length; i++) {
@@ -136,5 +162,31 @@ export async function getConfigMapFromConfigPage(allPages) {
}
}
return notionConfig
// 最后检查Notion_Config页面的INLINE_CONFIG是否是一个js对象
const combine = Object.assign(
{},
deepClone(notionConfig),
parseConfig(notionConfig?.INLINE_CONFIG)
)
return combine
}
/**
* 解析INLINE_CONFIG
* @param {*} configString
* @returns
*/
export function parseConfig(configString) {
if (!configString) {
return {}
}
// 解析对象
try {
// eslint-disable-next-line no-eval
const config = eval('(' + configString + ')')
return config
} catch (evalError) {
console.error('解析 eval(INLINE_CONFIG) 配置时出错:', evalError)
return {}
}
}

View File

@@ -22,17 +22,27 @@ const mapImgUrl = (img, block, type = 'block', from = 'post') => {
}
// Notion 图床转换为永久地址
const isNotionSignImg = ret.indexOf('https://www.notion.so/image') !== 0 && (ret.indexOf('secure.notion-static.com') > 0 || ret.indexOf('prod-files-secure') > 0)
const isNotionSignImg =
ret.indexOf('https://www.notion.so/image') !== 0 &&
(ret.indexOf('secure.notion-static.com') > 0 ||
ret.indexOf('prod-files-secure') > 0)
const isImgBlock = BLOG.IMG_URL_TYPE === 'Notion' || type !== 'block'
if (isNotionSignImg && isImgBlock) {
ret = BLOG.NOTION_HOST + '/image/' + encodeURIComponent(ret) + '?table=' + type + '&id=' + block.id
ret =
BLOG.NOTION_HOST +
'/image/' +
encodeURIComponent(ret) +
'?table=' +
type +
'&id=' +
block.id
}
if (!isEmoji(ret) && ret.indexOf('notion.so/images/page-cover') < 0) {
if (BLOG.RANDOM_IMAGE_URL) {
// 只有配置了随机图片接口,才会替换图片
const texts = BLOG.RANDOM_IMAGE_REPLACE_TEXT
let isReplace = false;
let isReplace = false
if (texts) {
const textArr = texts.split(',')
// 判断是否包含替换的文本
@@ -58,7 +68,11 @@ const mapImgUrl = (img, block, type = 'block', from = 'post') => {
}
// 统一压缩图片
if (from === 'pageCoverThumbnail' || block.type === 'image' || block.type === 'page') {
if (
from === 'pageCoverThumbnail' ||
block?.type === 'image' ||
block?.type === 'page'
) {
const width = block?.format?.block_width
ret = compressImage(ret, width)
}
@@ -72,8 +86,9 @@ const mapImgUrl = (img, block, type = 'block', from = 'post') => {
* @returns
*/
function isEmoji(str) {
const emojiRegex = /[\u{1F300}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F900}-\u{1F9FF}\u{1F018}-\u{1F270}\u{238C}\u{2B06}\u{2B07}\u{2B05}\u{27A1}\u{2194}-\u{2199}\u{2194}\u{21A9}\u{21AA}\u{2934}\u{2935}\u{25AA}\u{25AB}\u{25FE}\u{25FD}\u{25FB}\u{25FC}\u{25B6}\u{25C0}\u{1F200}-\u{1F251}]/u;
return emojiRegex.test(str);
const emojiRegex =
/[\u{1F300}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F900}-\u{1F9FF}\u{1F018}-\u{1F270}\u{238C}\u{2B06}\u{2B07}\u{2B05}\u{27A1}\u{2194}-\u{2199}\u{2194}\u{21A9}\u{21AA}\u{2934}\u{2935}\u{25AA}\u{25AB}\u{25FE}\u{25FD}\u{25FB}\u{25FC}\u{25B6}\u{25C0}\u{1F200}-\u{1F251}]/u
return emojiRegex.test(str)
}
/**
@@ -97,7 +112,10 @@ const compressImage = (image, width, quality = 50, fmt = 'webp') => {
const params = new URLSearchParams(urlObj.search)
// Notion图床
if (image.indexOf(BLOG.NOTION_HOST) === 0 && image.indexOf('amazonaws.com') > 0) {
if (
image.indexOf(BLOG.NOTION_HOST) === 0 &&
image.indexOf('amazonaws.com') > 0
) {
params.set('width', width)
params.set('cache', 'v2')
// 生成新的URL
@@ -117,11 +135,11 @@ const compressImage = (image, width, quality = 50, fmt = 'webp') => {
return urlObj.toString()
} else if (image.indexOf('https://your_picture_bed') === 0) {
// 此处还可以添加您的自定义图传的封面图压缩参数。
// .e.g
// .e.g
return 'do_somethin_here'
}
return image
}
export { mapImgUrl, compressImage }
export { compressImage, mapImgUrl }

View File

@@ -1,16 +1,18 @@
const { loadExternalResource } = require('../utils');
const { loadExternalResource } = require('../utils')
/**
* WOWjs动画结合animate.css使用很方便
* 是data-aos的平替 aos ≈ wowjs + animate
*/
export const loadWowJS = async () => {
await loadExternalResource('/css/wow/animate.css', 'css');
await loadExternalResource('https://cdnjs.cloudflare.com/ajax/libs/wow/1.1.2/wow.min.js', 'js');
await loadExternalResource('/css/wow/animate.css', 'css')
await loadExternalResource(
'https://cdnjs.cloudflare.com/ajax/libs/wow/1.1.2/wow.min.js',
'js'
)
// 配合animatecss 实现延时滚动动画和AOS动画相似
const WOW = window.WOW;
console.log('加载WOW动画', WOW)
const WOW = window.WOW
if (WOW) {
new WOW().init();
new WOW().init()
}
};
}

View File

@@ -1,15 +1,16 @@
import fs from 'fs'
import { Feed } from 'feed'
import BLOG from '@/blog.config'
import ReactDOMServer from 'react-dom/server'
import { getPostBlocks } from '@/lib/db/getSiteData'
import NotionPage from '@/components/NotionPage'
import { getPostBlocks } from '@/lib/db/getSiteData'
import { Feed } from 'feed'
import fs from 'fs'
import ReactDOMServer from 'react-dom/server'
import { siteConfig } from './config'
/**
* 生成RSS内容
* @param {*} post
* @returns
*/
* 生成RSS内容
* @param {*} post
* @returns
*/
const createFeedContent = async post => {
// 加密的文章内容只返回摘要
if (post.password && post.password !== '') {
@@ -20,30 +21,35 @@ const createFeedContent = async post => {
post.blockMap = blockMap
const content = ReactDOMServer.renderToString(<NotionPage post={post} />)
const regexExp =
/<div class="notion-collection-row"><div class="notion-collection-row-body"><div class="notion-collection-row-property"><div class="notion-collection-column-title"><svg.*?class="notion-collection-column-title-icon">.*?<\/svg><div class="notion-collection-column-title-body">.*?<\/div><\/div><div class="notion-collection-row-value">.*?<\/div><\/div><\/div><\/div>/g
/<div class="notion-collection-row"><div class="notion-collection-row-body"><div class="notion-collection-row-property"><div class="notion-collection-column-title"><svg.*?class="notion-collection-column-title-icon">.*?<\/svg><div class="notion-collection-column-title-body">.*?<\/div><\/div><div class="notion-collection-row-value">.*?<\/div><\/div><\/div><\/div>/g
return content.replace(regexExp, '')
}
}
export async function generateRss(posts) {
export async function generateRss(NOTION_CONFIG, posts) {
const link = siteConfig('LINK', BLOG.LINK, NOTION_CONFIG)
const author = siteConfig('AUTHOR', BLOG.AUTHOR, NOTION_CONFIG)
const lang = siteConfig('LANG', BLOG.LANG, NOTION_CONFIG)
const subPath = siteConfig('SUB_PATH', BLOG.SUB_PATH, NOTION_CONFIG)
const year = new Date().getFullYear()
const feed = new Feed({
title: BLOG.TITLE,
description: BLOG.DESCRIPTION,
link: `${BLOG.LINK}/${BLOG.SUB_PATH}`,
language: BLOG.LANG,
favicon: `${BLOG.LINK}/favicon.png`,
copyright: `All rights reserved ${year}, ${BLOG.AUTHOR}`,
link: `${link}/${subPath}`,
language: lang,
favicon: `${link}/favicon.png`,
copyright: `All rights reserved ${year}, ${author}`,
author: {
name: BLOG.AUTHOR,
name: author,
email: BLOG.CONTACT_EMAIL,
link: BLOG.LINK
link: link
}
})
for (const post of posts) {
feed.addItem({
title: post.title,
link: `${BLOG.LINK}/${post.slug}`,
link: `${link}/${post.slug}`,
description: post.summary,
content: await createFeedContent(post),
date: new Date(post?.publishDay)

33
lib/utils/pageId.js Normal file
View File

@@ -0,0 +1,33 @@
/**
* 截取page-id的语言前缀
* notionPageId的格式可以是 en:xxxxx
* @param {*} str
* @returns en|zh|xx
*/
function extractLangPrefix(str) {
const match = str.match(/^(.+?):/)
if (match && match[1]) {
return match[1]
} else {
return ''
}
}
/**
* 截取page-id的id
* notionPageId的格式可以是 en:xxxxx * @param {*} str
* @returns xxxxx
*/
function extractLangId(str) {
// 使用正则表达式匹配字符串
const match = str.match(/:\s*(.+)/)
// 如果匹配成功,则返回匹配到的内容
if (match && match[1]) {
return match[1]
} else {
// 如果没有匹配到,则返回空字符串或者其他你想要返回的值
return str
}
}
module.exports = { extractLangPrefix, extractLangId }

View File

@@ -2,11 +2,36 @@ const { THEME } = require('./blog.config')
const fs = require('fs')
const path = require('path')
const BLOG = require('./blog.config')
const { extractLangPrefix } = require('./lib/utils/pageId')
// 打包时是否分析代码
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: BLOG.BUNDLE_ANALYZER
})
// 扫描项目 /themes下的目录名
const themes = scanSubdirectories(path.resolve(__dirname, 'themes'))
// 检测用户开启的多语言
const locales = (function () {
// 根据BLOG_NOTION_PAGE_ID 检查支持多少种语言数据.
// 支持如下格式配置多个语言的页面id xxx,zh:xxx,en:xxx
const langs = ['zh', 'en']
if (BLOG.NOTION_PAGE_ID.indexOf(',') > 0) {
const siteIds = BLOG.NOTION_PAGE_ID.split(',')
for (let index = 0; index < siteIds.length; index++) {
const siteId = siteIds[index]
const prefix = extractLangPrefix(siteId)
// 如果包含前缀 例如 zh , en 等
if (prefix) {
if (!langs.includes(prefix)) {
langs.push(prefix)
}
}
}
}
return langs
})()
/**
* 扫描指定目录下的文件夹名,用于获取所有主题
* @param {*} directory
@@ -27,8 +52,7 @@ function scanSubdirectories(directory) {
return subdirectories
}
// 扫描项目 /themes下的目录名
const themes = scanSubdirectories(path.resolve(__dirname, 'themes'))
module.exports = withBundleAnalyzer({
images: {
// 图片压缩
@@ -55,8 +79,52 @@ module.exports = withBundleAnalyzer({
}
]
},
// 多语言
i18n: {
defaultLocale: BLOG.LANG.slice(0, 2),
// 支持的所有多语言,按需填写即可
locales
},
// 重写url
async rewrites() {
// 处理多语言重定向
const langsRewrites = []
if (BLOG.NOTION_PAGE_ID.indexOf(',') > 0) {
const siteIds = BLOG.NOTION_PAGE_ID.split(',')
const langs = []
for (let index = 0; index < siteIds.length; index++) {
const siteId = siteIds[index]
const prefix = extractLangPrefix(siteId)
// 如果包含前缀 例如 zh , en 等
if (prefix) {
langs.push(prefix)
}
console.log('[Locales]', siteId)
}
// 映射多语言
// 示例: source: '/:locale(zh|en)/:path*' ; :locale() 会将语言放入重写后的 `?locale=` 中。
langsRewrites.push(
{
source: `/:locale(${langs.join('|')})/:path*`,
destination: '/:path*'
},
// 匹配没有路径的情况,例如 [domain]/zh 或 [domain]/en
{
source: `/:locale(${langs.join('|')})`,
destination: '/'
},
// 匹配没有路径的情况,例如 [domain]/zh/ 或 [domain]/en/
{
source: `/:locale(${langs.join('|')})/`,
destination: '/'
}
)
}
return [
...langsRewrites,
// 伪静态重写
{
source: '/:path*.html',
destination: '/:path*'
@@ -84,17 +152,9 @@ module.exports = withBundleAnalyzer({
]
},
webpack: (config, { dev, isServer }) => {
// Replace React with Preact only in client production build
// if (!dev && !isServer) {
// Object.assign(config.resolve.alias, {
// react: 'preact/compat',
// 'react-dom/test-utils': 'preact/test-utils',
// 'react-dom': 'preact/compat'
// })
// }
// 动态主题:添加 resolve.alias 配置,将动态路径映射到实际路径
if (!isServer) {
console.log('[加载主题]', path.resolve(__dirname, 'themes', THEME))
console.log('[默认主题]', path.resolve(__dirname, 'themes', THEME))
}
config.resolve.alias['@theme-components'] = path.resolve(
__dirname,

View File

@@ -1,81 +1,70 @@
{
"name": "notion-next",
"version": "4.4.2",
"homepage": "https://github.com/tangly1024/NotionNext.git",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/tangly1024/NotionNext.git"
},
"author": {
"name": "tangly",
"email": "mail@tangly1024.com",
"url": "http://tangly1024.com"
},
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"post-build": "next-sitemap --config next-sitemap.config.js",
"export": "next build && next-sitemap --config next-sitemap.config.js && next export",
"bundle-report": "cross-env ANALYZE=true yarn build"
},
"dependencies": {
"@giscus/react": "^2.2.6",
"@headlessui/react": "^1.7.15",
"@next/bundle-analyzer": "^12.1.1",
"@vercel/analytics": "^1.0.0",
"algoliasearch": "^4.18.0",
"animejs": "^3.2.1",
"aos": "^2.3.4",
"axios": ">=0.21.1",
"copy-to-clipboard": "^3.3.1",
"feed": "^4.2.2",
"js-md5": "^0.7.3",
"localStorage": "^1.0.4",
"lodash.throttle": "^4.1.1",
"memory-cache": "^0.2.0",
"mongodb": "^4.6.0",
"next": "13.3.1",
"notion-client": "6.15.6",
"notion-utils": "6.15.6",
"nprogress": "^0.2.0",
"preact": "^10.5.15",
"prism-themes": "1.9.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-facebook": "^8.1.4",
"react-hotkeys-hook": "^4.5.0",
"react-notion-x": "6.16.0",
"react-share": "^4.4.1",
"react-tweet-embed": "~2.0.0",
"typed.js": "^2.0.12"
},
"devDependencies": {
"@waline/client": "^2.5.1",
"autoprefixer": "^10.4.13",
"cross-env": "^7.0.3",
"eslint": "^7.26.0",
"eslint-config-next": "^13.1.1",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.23.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.6.0",
"next-sitemap": "^1.6.203",
"postcss": "^8.4.31",
"prettier": "3.2.5",
"tailwindcss": "^3.3.2",
"webpack-bundle-analyzer": "^4.5.0"
},
"resolutions": {
"axios": ">=0.21.1"
},
"bugs": {
"url": "https://github.com/tangly/NotionNext/issues",
"email": "tlyong1992@hotmail.com"
}
"name": "notion-next",
"version": "4.4.3",
"homepage": "https://github.com/tangly1024/NotionNext.git",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/tangly1024/NotionNext.git"
},
"author": {
"name": "tangly",
"email": "mail@tangly1024.com",
"url": "http://tangly1024.com"
},
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"post-build": "next-sitemap --config next-sitemap.config.js",
"export": "next build && next-sitemap --config next-sitemap.config.js && next export",
"bundle-report": "cross-env ANALYZE=true yarn build"
},
"dependencies": {
"@headlessui/react": "^1.7.15",
"@next/bundle-analyzer": "^12.1.1",
"@vercel/analytics": "^1.0.0",
"algoliasearch": "^4.18.0",
"feed": "^4.2.2",
"js-md5": "^0.7.3",
"lodash.throttle": "^4.1.1",
"memory-cache": "^0.2.0",
"next": "13.3.1",
"notion-client": "6.15.6",
"notion-utils": "6.15.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-facebook": "^8.1.4",
"react-hotkeys-hook": "^4.5.0",
"react-notion-x": "6.16.0",
"react-share": "^4.4.1",
"react-tweet-embed": "~2.0.0"
},
"devDependencies": {
"@waline/client": "^2.5.1",
"autoprefixer": "^10.4.13",
"cross-env": "^7.0.3",
"eslint": "^7.26.0",
"eslint-config-next": "^13.1.1",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.23.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.6.0",
"next-sitemap": "^1.6.203",
"postcss": "^8.4.31",
"prettier": "3.2.5",
"tailwindcss": "^3.3.2",
"webpack-bundle-analyzer": "^4.5.0"
},
"resolutions": {
"axios": ">=0.21.1"
},
"bugs": {
"url": "https://github.com/tangly/NotionNext/issues",
"email": "tlyong1992@hotmail.com"
}
}

View File

@@ -1,7 +1,7 @@
import { getGlobalData } from '@/lib/db/getSiteData'
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'
/**
* 404
@@ -10,12 +10,17 @@ import { siteConfig } from '@/lib/config'
*/
const NoFound = props => {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
}
export async function getStaticProps () {
const props = (await getGlobalData({ from: '404' })) || {}
export async function getStaticProps(req) {
const { locale } = req
const props = (await getGlobalData({ from: '404', locale })) || {}
return { props }
}

View File

@@ -35,7 +35,11 @@ export async function getStaticPaths() {
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) }
params: {
prefix: row.slug.split('/')[0],
slug: row.slug.split('/')[1],
suffix: row.slug.split('/').slice(1)
}
})),
fallback: true
}
@@ -46,18 +50,25 @@ export async function getStaticPaths() {
* @param {*} param0
* @returns
*/
export async function getStaticProps({ params: { prefix, slug, suffix } }) {
export async function getStaticProps({
params: { prefix, slug, suffix },
locale
}) {
let fullSlug = prefix + '/' + slug + '/' + suffix.join('/')
if (JSON.parse(BLOG.PSEUDO_STATIC)) {
const from = `slug-props-${fullSlug}`
const props = await getGlobalData({ from, locale })
if (siteConfig('PSEUDO_STATIC', BLOG.PSEUDO_STATIC, props.NOTION_CONFIG)) {
if (!fullSlug.endsWith('.html')) {
fullSlug += '.html'
}
}
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))
return (
p.type.indexOf('Menu') < 0 &&
(p.slug === fullSlug || p.id === idToUuid(fullSlug))
)
})
// 处理非列表内文章的内信息
@@ -72,7 +83,14 @@ export async function getStaticProps({ params: { prefix, slug, suffix } }) {
// 无法获取文章
if (!props?.post) {
props.post = null
return { props, revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND) }
return {
props,
revalidate: siteConfig(
'REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}
// 文章内容加载
@@ -85,12 +103,18 @@ export async function getStaticProps({ params: { prefix, slug, suffix } }) {
}
// 推荐关联文章处理
const allPosts = props.allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
const allPosts = props.allPages?.filter(
page => page.type === 'Post' && page.status === 'Published'
)
if (allPosts && allPosts.length > 0) {
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, siteConfig('POST_RECOMMEND_COUNT'))
props.recommendPosts = getRecommendPost(
props.post,
allPosts,
siteConfig('POST_RECOMMEND_COUNT')
)
} else {
props.prev = null
props.next = null
@@ -100,7 +124,11 @@ export async function getStaticProps({ params: { prefix, slug, suffix } }) {
delete props.allPages
return {
props,
revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND)
revalidate: siteConfig(
'NEXT_REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}
@@ -109,7 +137,11 @@ function checkSlug(row) {
if (slug.startsWith('/')) {
slug = slug.substring(1)
}
return (slug.match(/\//g) || []).length >= 2 && row.type.indexOf('Menu') < 0 && !checkContainHttp(slug)
return (
(slug.match(/\//g) || []).length >= 2 &&
row.type.indexOf('Menu') < 0 &&
!checkContainHttp(slug)
)
}
export default PrefixSlug

View File

@@ -28,25 +28,31 @@ export async function getStaticPaths() {
const { allPages } = await getGlobalData({ from })
const paths = allPages
?.filter(row => checkSlug(row))
.map(row => ({ params: { prefix: row.slug.split('/')[0], slug: row.slug.split('/')[1] } }))
.map(row => ({
params: { prefix: row.slug.split('/')[0], slug: row.slug.split('/')[1] }
}))
return {
paths: paths,
fallback: true
}
}
export async function getStaticProps({ params: { prefix, slug } }) {
export async function getStaticProps({ params: { prefix, slug }, locale }) {
let fullSlug = prefix + '/' + slug
if (JSON.parse(BLOG.PSEUDO_STATIC)) {
const from = `slug-props-${fullSlug}`
const props = await getGlobalData({ from, locale })
if (siteConfig('PSEUDO_STATIC', BLOG.PSEUDO_STATIC, props.NOTION_CONFIG)) {
if (!fullSlug.endsWith('.html')) {
fullSlug += '.html'
}
}
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))
return (
p.type.indexOf('Menu') < 0 &&
(p.slug === fullSlug || p.id === idToUuid(fullSlug))
)
})
// 处理非列表内文章的内信息
@@ -61,7 +67,14 @@ export async function getStaticProps({ params: { prefix, slug } }) {
// 无法获取文章
if (!props?.post) {
props.post = null
return { props, revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND) }
return {
props,
revalidate: siteConfig(
'REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}
// 文章内容加载
@@ -74,12 +87,18 @@ export async function getStaticProps({ params: { prefix, slug } }) {
}
// 推荐关联文章处理
const allPosts = props.allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
const allPosts = props.allPages?.filter(
page => page.type === 'Post' && page.status === 'Published'
)
if (allPosts && allPosts.length > 0) {
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, siteConfig('POST_RECOMMEND_COUNT'))
props.recommendPosts = getRecommendPost(
props.post,
allPosts,
siteConfig('POST_RECOMMEND_COUNT')
)
} else {
props.prev = null
props.next = null
@@ -89,7 +108,11 @@ export async function getStaticProps({ params: { prefix, slug } }) {
delete props.allPages
return {
props,
revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND)
revalidate: siteConfig(
'NEXT_REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}
function checkSlug(row) {
@@ -97,6 +120,10 @@ function checkSlug(row) {
if (slug.startsWith('/')) {
slug = slug.substring(1)
}
return (slug.match(/\//g) || []).length === 1 && !checkContainHttp(slug) && row.type.indexOf('Menu') < 0
return (
(slug.match(/\//g) || []).length === 1 &&
!checkContainHttp(slug) &&
row.type.indexOf('Menu') < 0
)
}
export default PrefixSlug

View File

@@ -53,7 +53,10 @@ const Slug = props => {
props = { ...props, lock, setLock, validPassword }
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
}
@@ -67,25 +70,31 @@ 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
}
}
export async function getStaticProps({ params: { prefix } }) {
export async function getStaticProps({ params: { prefix }, locale }) {
let fullSlug = prefix
if (JSON.parse(BLOG.PSEUDO_STATIC)) {
const from = `slug-props-${fullSlug}`
const props = await getGlobalData({ from, locale })
if (siteConfig('PSEUDO_STATIC', BLOG.PSEUDO_STATIC, props.NOTION_CONFIG)) {
if (!fullSlug.endsWith('.html')) {
fullSlug += '.html'
}
}
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))
return (
p.type.indexOf('Menu') < 0 &&
(p.slug === fullSlug || p.id === idToUuid(fullSlug))
)
})
// 处理非列表内文章的内信息
@@ -99,7 +108,14 @@ export async function getStaticProps({ params: { prefix } }) {
// 无法获取文章
if (!props?.post) {
props.post = null
return { props, revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND) }
return {
props,
revalidate: siteConfig(
'REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}
// 文章内容加载
@@ -113,12 +129,18 @@ export async function getStaticProps({ params: { prefix } }) {
}
// 推荐关联文章处理
const allPosts = props.allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
const allPosts = props.allPages?.filter(
page => page.type === 'Post' && page.status === 'Published'
)
if (allPosts && allPosts.length > 0) {
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, siteConfig('POST_RECOMMEND_COUNT'))
props.recommendPosts = getRecommendPost(
props.post,
allPosts,
siteConfig('POST_RECOMMEND_COUNT')
)
} else {
props.prev = null
props.next = null
@@ -128,7 +150,11 @@ export async function getStaticProps({ params: { prefix } }) {
delete props.allPages
return {
props,
revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND)
revalidate: siteConfig(
'NEXT_REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}
@@ -172,7 +198,11 @@ 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

View File

@@ -1,24 +1,22 @@
// import '@/styles/animate.css' // @see https://animate.style/
import '@/styles/globals.css'
import '@/styles/nprogress.css'
import '@/styles/utility-patterns.css'
// core styles shared by all of react-notion-x (required)
import 'react-notion-x/src/styles.css'
import '@/styles/notion.css' // 重写部分样式
import 'aos/dist/aos.css' // You can also use <link> for styles
import 'react-notion-x/src/styles.css'
import useAdjustStyle from '@/hooks/useAdjustStyle'
import { GlobalContextProvider } from '@/lib/global'
import { getGlobalLayoutByTheme } from '@/themes/theme'
import { useRouter } from 'next/router'
import { useCallback, useMemo } from 'react'
import { getQueryParam } from '../lib/utils'
import useAdjustStyle from '@/hooks/useAdjustStyle'
// 各种扩展插件 这个要阻塞引入
import BLOG from '@/blog.config'
import ExternalPlugins from '@/components/ExternalPlugins'
import GlobalHead from '@/components/GlobalHead'
import BLOG from '@/blog.config'
/**
* App挂载DOM 入口文件
@@ -27,11 +25,15 @@ import BLOG from '@/blog.config'
*/
const MyApp = ({ Component, pageProps }) => {
// 一些可能出现 bug 的样式,可以统一放入该钩子进行调整
useAdjustStyle();
useAdjustStyle()
const route = useRouter()
const queryParam = useMemo(() => {
return getQueryParam(route.asPath, 'theme') || pageProps?.NOTION_CONFIG?.THEME || BLOG.THEME
return (
getQueryParam(route.asPath, 'theme') ||
pageProps?.NOTION_CONFIG?.THEME ||
BLOG.THEME
)
}, [route])
// 整体布局
@@ -47,7 +49,7 @@ const MyApp = ({ Component, pageProps }) => {
return (
<GlobalContextProvider {...pageProps}>
<GLayout {...pageProps}>
<GlobalHead {...pageProps}/>
<GlobalHead {...pageProps} />
<Component {...pageProps} />
</GLayout>
<ExternalPlugins {...pageProps} />

View File

@@ -1,6 +1,6 @@
// eslint-disable-next-line @next/next/no-document-import-in-page
import Document, { Html, Head, Main, NextScript } from 'next/document'
import BLOG from '@/blog.config'
import Document, { Head, Html, Main, NextScript } from 'next/document'
class MyDocument extends Document {
static async getInitialProps(ctx) {
@@ -10,29 +10,52 @@ class MyDocument extends Document {
render() {
return (
<Html lang={BLOG.LANG}>
<Head>
<link rel='icon' href= {`${BLOG.BLOG_FAVICON}`} />
{/* 预加载字体 */}
{BLOG.FONT_AWESOME && <>
<link rel='preload' href={BLOG.FONT_AWESOME} as="style" crossOrigin="anonymous" />
<link rel="stylesheet" href={BLOG.FONT_AWESOME} crossOrigin="anonymous" referrerPolicy="no-referrer" />
</>}
<Html lang={BLOG.LANG}>
<Head>
<link rel='icon' href={`${BLOG.BLOG_FAVICON}`} />
{/* 预加载字体 */}
{BLOG.FONT_AWESOME && (
<>
<link
rel='preload'
href={BLOG.FONT_AWESOME}
as='style'
crossOrigin='anonymous'
/>
<link
rel='stylesheet'
href={BLOG.FONT_AWESOME}
crossOrigin='anonymous'
referrerPolicy='no-referrer'
/>
</>
)}
{BLOG.FONT_URL?.map((fontUrl, index) => {
if (fontUrl.endsWith('.css') || fontUrl.includes('googleapis.com/css')) {
return <link key={index} rel="stylesheet" href={fontUrl} />
} else {
return <link key={index} rel="preload" href={fontUrl} as="font" type="font/woff2" />
}
})}
</Head>
{BLOG.FONT_URL?.map((fontUrl, index) => {
if (
fontUrl.endsWith('.css') ||
fontUrl.includes('googleapis.com/css')
) {
return <link key={index} rel='stylesheet' href={fontUrl} />
} else {
return (
<link
key={index}
rel='preload'
href={fontUrl}
as='font'
type='font/woff2'
/>
)
}
})}
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}

View File

@@ -1,15 +1,18 @@
import { getGlobalData } from '@/lib/db/getSiteData'
import { useEffect } 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 { isBrowser } from '@/lib/utils'
import { formatDateFmt } from '@/lib/utils/formatDate'
import { siteConfig } from '@/lib/config'
import { getLayoutByTheme } from '@/themes/theme'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
const ArchiveIndex = props => {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
useEffect(() => {
if (isBrowser) {
@@ -28,10 +31,12 @@ const ArchiveIndex = props => {
return <Layout {...props} />
}
export async function getStaticProps() {
const props = await getGlobalData({ from: 'archive-index' })
export async function getStaticProps({ locale }) {
const props = await getGlobalData({ from: 'archive-index', locale })
// 处理分页
props.posts = props.allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
props.posts = props.allPages?.filter(
page => page.type === 'Post' && page.status === 'Published'
)
delete props.allPages
const postsSortByDate = Object.create(props.posts)
@@ -56,7 +61,11 @@ export async function getStaticProps() {
return {
props,
revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND)
revalidate: siteConfig(
'NEXT_REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}

View File

@@ -11,19 +11,26 @@ import { useRouter } from 'next/router'
*/
export default function Category(props) {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
}
export async function getStaticProps({ params: { category } }) {
export async function getStaticProps({ params: { category }, locale }) {
const from = 'category-props'
let props = await getGlobalData({ from })
let props = await getGlobalData({ from, locale })
// 过滤状态
props.posts = props.allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
props.posts = props.allPages?.filter(
page => page.type === 'Post' && page.status === 'Published'
)
// 处理过滤
props.posts = props.posts.filter(post => post && post.category && post.category.includes(category))
props.posts = props.posts.filter(
post => post && post.category && post.category.includes(category)
)
// 处理文章页数
props.postCount = props.posts.length
// 处理分页
@@ -39,7 +46,11 @@ export async function getStaticProps({ params: { category } }) {
return {
props,
revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND)
revalidate: siteConfig(
'NEXT_REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}

View File

@@ -12,7 +12,10 @@ import { useRouter } from 'next/router'
export default function Category(props) {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
}
@@ -28,7 +31,10 @@ export async function getStaticProps({ params: { category, page } }) {
// 处理文章页数
props.postCount = props.posts.length
// 处理分页
props.posts = props.posts.slice(siteConfig('POSTS_PER_PAGE') * (page - 1), siteConfig('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
@@ -37,7 +43,11 @@ export async function getStaticProps({ params: { category, page } }) {
return {
props,
revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND)
revalidate: siteConfig(
'NEXT_REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}
@@ -50,7 +60,9 @@ export async function getStaticPaths() {
// 过滤状态类型
const categoryPosts = allPages
?.filter(page => page.type === 'Post' && page.status === 'Published')
.filter(post => post && post.category && post.category.includes(category.name))
.filter(
post => post && post.category && post.category.includes(category.name)
)
// 处理文章页数
const postCount = categoryPosts.length
const totalPages = Math.ceil(postCount / siteConfig('POSTS_PER_PAGE'))

View File

@@ -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'
/**
* 分类首页
@@ -12,16 +11,23 @@ import { siteConfig } from '@/lib/config'
*/
export default function Category(props) {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
}
export async function getStaticProps() {
const props = await getGlobalData({ from: 'category-index-props' })
export async function getStaticProps({ locale }) {
const props = await getGlobalData({ from: 'category-index-props', locale })
delete props.allPages
return {
props,
revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND)
revalidate: siteConfig(
'NEXT_REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}

View File

@@ -24,9 +24,10 @@ const Index = props => {
* SSG 获取数据
* @returns
*/
export async function getStaticProps() {
export async function getStaticProps(req) {
const { locale } = req
const from = 'index'
const props = await getGlobalData({ from })
const props = await getGlobalData({ from, locale })
props.posts = props.allPages?.filter(
page => page.type === 'Post' && page.status === 'Published'
@@ -58,7 +59,7 @@ export async function getStaticProps() {
generateRobotsTxt()
// 生成Feed订阅
if (JSON.parse(BLOG.ENABLE_RSS)) {
generateRss(props?.latestPosts || [])
generateRss(props?.NOTION_CONFIG, props?.latestPosts || [])
}
// 生成全文索引 - 仅在 yarn build 时执行 && process.env.npm_lifecycle_event === 'build'
@@ -67,7 +68,11 @@ export async function getStaticProps() {
return {
props,
revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND)
revalidate: siteConfig(
'NEXT_REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}

View File

@@ -11,14 +11,17 @@ import { useRouter } from 'next/router'
*/
const Page = props => {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
}
export async function getStaticPaths() {
export async function getStaticPaths({ locale }) {
const from = 'page-paths'
const { postCount } = await getGlobalData({ from })
const { postCount } = await getGlobalData({ from, locale })
const totalPages = Math.ceil(postCount / siteConfig('POSTS_PER_PAGE'))
return {
// remove first page, we 're not gonna handle that.
@@ -33,9 +36,14 @@ export async function getStaticProps({ params: { page } }) {
const from = `page-${page}`
const props = await getGlobalData({ from })
const { allPages } = props
const allPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
const allPosts = allPages?.filter(
page => page.type === 'Post' && page.status === 'Published'
)
// 处理分页
props.posts = allPosts.slice(siteConfig('POSTS_PER_PAGE') * (page - 1), siteConfig('POSTS_PER_PAGE') * page)
props.posts = allPosts.slice(
siteConfig('POSTS_PER_PAGE') * (page - 1),
siteConfig('POSTS_PER_PAGE') * page
)
props.page = page
// 处理预览
@@ -45,14 +53,22 @@ export async function getStaticProps({ params: { page } }) {
if (post.password && post.password !== '') {
continue
}
post.blockMap = await getPostBlocks(post.id, 'slug', siteConfig('POST_PREVIEW_LINES'))
post.blockMap = await getPostBlocks(
post.id,
'slug',
siteConfig('POST_PREVIEW_LINES')
)
}
}
delete props.allPages
return {
props,
revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND)
revalidate: siteConfig(
'NEXT_REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}

View File

@@ -7,7 +7,10 @@ import { useRouter } from 'next/router'
const Index = props => {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
}
@@ -17,13 +20,15 @@ const Index = props => {
* @param {*} param0
* @returns
*/
export async function getStaticProps({ params: { keyword } }) {
export async function getStaticProps({ params: { keyword }, locale }) {
const props = await getGlobalData({
from: 'search-props',
pageType: ['Post']
locale
})
const { allPages } = props
const allPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
const allPosts = allPages?.filter(
page => page.type === 'Post' && page.status === 'Published'
)
props.posts = await filterByMemCache(allPosts, keyword)
props.postCount = props.posts.length
// 处理分页
@@ -35,7 +40,11 @@ export async function getStaticProps({ params: { keyword } }) {
props.keyword = keyword
return {
props,
revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND)
revalidate: siteConfig(
'NEXT_REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}
@@ -87,7 +96,8 @@ 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'
/**
* 在内存缓存中进行全文索引
@@ -103,8 +113,12 @@ async function filterByMemCache(allPosts, keyword) {
for (const post of allPosts) {
const cacheKey = 'page_block_' + post.id
const page = await getDataFromCache(cacheKey, true)
const tagContent = post?.tags && Array.isArray(post?.tags) ? post?.tags.join(' ') : ''
const categoryContent = post.category && Array.isArray(post.category) ? post.category.join(' ') : ''
const tagContent =
post?.tags && Array.isArray(post?.tags) ? post?.tags.join(' ') : ''
const categoryContent =
post.category && Array.isArray(post.category)
? post.category.join(' ')
: ''
const articleInfo = post.title + post.summary + tagContent + categoryContent
let hit = articleInfo.toLowerCase().indexOf(keyword) > -1
const indexContent = getPageContentText(post, page)

View File

@@ -8,7 +8,10 @@ import { useRouter } from 'next/router'
const Index = props => {
const { keyword } = props
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
props = { ...props, currentSearch: keyword }
return <Layout {...props} />
@@ -19,23 +22,33 @@ const Index = props => {
* @param {*} param0
* @returns
*/
export async function getStaticProps({ params: { keyword, page } }) {
export async function getStaticProps({ params: { keyword, page }, locale }) {
const props = await getGlobalData({
from: 'search-props',
pageType: ['Post']
pageType: ['Post'],
locale
})
const { allPages } = props
const allPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
const allPosts = allPages?.filter(
page => page.type === 'Post' && page.status === 'Published'
)
props.posts = await filterByMemCache(allPosts, keyword)
props.postCount = props.posts.length
// 处理分页
props.posts = props.posts.slice(siteConfig('POSTS_PER_PAGE') * (page - 1), siteConfig('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
return {
props,
revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND)
revalidate: siteConfig(
'NEXT_REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}
@@ -87,7 +100,8 @@ 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'
/**
* 在内存缓存中进行全文索引
@@ -103,8 +117,12 @@ async function filterByMemCache(allPosts, keyword) {
for (const post of allPosts) {
const cacheKey = 'page_block_' + post.id
const page = await getDataFromCache(cacheKey, true)
const tagContent = post?.tags && Array.isArray(post?.tags) ? post?.tags.join(' ') : ''
const categoryContent = post.category && Array.isArray(post.category) ? post.category.join(' ') : ''
const tagContent =
post?.tags && Array.isArray(post?.tags) ? post?.tags.join(' ') : ''
const categoryContent =
post.category && Array.isArray(post.category)
? post.category.join(' ')
: ''
const articleInfo = post.title + post.summary + tagContent + categoryContent
let hit = articleInfo.indexOf(keyword) > -1
let indexContent = [post.summary]

View File

@@ -1,8 +1,8 @@
import { getGlobalData } from '@/lib/db/getSiteData'
import { useRouter } from 'next/router'
import BLOG from '@/blog.config'
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'
/**
* 搜索路由
@@ -13,7 +13,10 @@ const Search = props => {
const { posts } = props
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
const router = useRouter()
const keyword = router?.query?.s
@@ -25,7 +28,7 @@ const Search = props => {
const tagContent = post?.tags ? post?.tags.join(' ') : ''
const categoryContent = post.category ? post.category.join(' ') : ''
const searchContent =
post.title + post.summary + tagContent + categoryContent
post.title + post.summary + tagContent + categoryContent
return searchContent.toLowerCase().includes(keyword.toLowerCase())
})
} else {
@@ -40,16 +43,22 @@ const Search = props => {
/**
* 浏览器前端搜索
*/
export async function getStaticProps() {
export async function getStaticProps({ locale }) {
const props = await getGlobalData({
from: 'search-props',
pageType: ['Post']
locale
})
const { allPages } = props
props.posts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
props.posts = allPages?.filter(
page => page.type === 'Post' && page.status === 'Published'
)
return {
props,
revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND)
revalidate: siteConfig(
'NEXT_REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}

View File

@@ -1,8 +1,8 @@
import BLOG from '@/blog.config'
import { getGlobalData } from '@/lib/db/getSiteData'
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'
/**
* 登录
@@ -11,18 +11,27 @@ import { siteConfig } from '@/lib/config'
*/
const SignIn = props => {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
}
export async function getStaticProps() {
export async function getStaticProps(req) {
const { locale } = req
const from = 'SignIn'
const props = await getGlobalData({ from })
const props = await getGlobalData({ from, locale })
delete props.allPages
return {
props,
revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND)
revalidate: siteConfig(
'NEXT_REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}

View File

@@ -1,8 +1,8 @@
import BLOG from '@/blog.config'
import { getGlobalData } from '@/lib/db/getSiteData'
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'
/**
* 注册
@@ -11,18 +11,27 @@ import { siteConfig } from '@/lib/config'
*/
const SignUp = props => {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
}
export async function getStaticProps() {
export async function getStaticProps(req) {
const { locale } = req
const from = 'SignIn'
const props = await getGlobalData({ from })
const props = await getGlobalData({ from, locale })
delete props.allPages
return {
props,
revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND)
revalidate: siteConfig(
'NEXT_REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}

View File

@@ -1,61 +1,94 @@
// pages/sitemap.xml.js
import { getServerSideSitemap } from 'next-sitemap'
import { getGlobalData } from '@/lib/db/getSiteData'
import BLOG from '@/blog.config'
import { siteConfig } from '@/lib/config'
import { getNotionPageData } from '@/lib/db/getSiteData'
import { extractLangId, extractLangPrefix } from '@/lib/utils/pageId'
import { getServerSideSitemap } from 'next-sitemap'
export const getServerSideProps = async (ctx) => {
const { allPages } = await getGlobalData({ from: 'rss' })
const defaultFields = [
{
loc: `${BLOG.LINK}`,
lastmod: new Date().toISOString().split('T')[0],
changefreq: 'daily',
priority: '0.7'
}, {
loc: `${BLOG.LINK}/archive`,
lastmod: new Date().toISOString().split('T')[0],
changefreq: 'daily',
priority: '0.7'
}, {
loc: `${BLOG.LINK}/category`,
lastmod: new Date().toISOString().split('T')[0],
changefreq: 'daily',
priority: '0.7'
}, {
loc: `${BLOG.LINK}/feed`,
lastmod: new Date().toISOString().split('T')[0],
changefreq: 'daily',
priority: '0.7'
}, {
loc: `${BLOG.LINK}/search`,
lastmod: new Date().toISOString().split('T')[0],
changefreq: 'daily',
priority: '0.7'
}, {
loc: `${BLOG.LINK}/tag`,
lastmod: new Date().toISOString().split('T')[0],
changefreq: 'daily',
priority: '0.7'
}
]
const postFields = allPages?.filter(p => p.status === BLOG.NOTION_PROPERTY_NAME.status_publish)?.map(post => {
const slugWithoutLeadingSlash = post?.slug.startsWith('/') ? post?.slug?.slice(1) : post.slug
return {
loc: `${BLOG.LINK}/${slugWithoutLeadingSlash}`,
lastmod: new Date(post?.publishDay).toISOString().split('T')[0],
changefreq: 'daily',
priority: '0.7'
}
})
const fields = defaultFields.concat(postFields)
export const getServerSideProps = async ctx => {
let fields = []
const siteIds = BLOG.NOTION_PAGE_ID.split(',')
for (let index = 0; index < siteIds.length; index++) {
const siteId = siteIds[index]
const id = extractLangId(siteId)
const locale = extractLangPrefix(siteId)
// 第一个id站点默认语言
const siteData = await getNotionPageData({
pageId: id,
from: 'sitemap.xml'
})
const link = siteConfig('LINK', BLOG.LINK, siteData.NOTION_CONFIG)
const localeFields = generateLocalesSitemap(link, siteData.allPages, locale)
fields = fields.concat(localeFields)
}
// 缓存
ctx.res.setHeader(
'Cache-Control',
'public, max-age=3600, stale-while-revalidate=59'
)
console.log('fff', fields)
return getServerSideSitemap(ctx, fields)
}
export default () => { }
function generateLocalesSitemap(link, allPages, locale) {
if (locale && locale.length > 0 && locale.indexOf('/') !== 0) {
locale = '/' + locale
}
const defaultFields = [
{
loc: `${link}${locale}`,
lastmod: new Date().toISOString().split('T')[0],
changefreq: 'daily',
priority: '0.7'
},
{
loc: `${link}${locale}/archive`,
lastmod: new Date().toISOString().split('T')[0],
changefreq: 'daily',
priority: '0.7'
},
{
loc: `${link}${locale}/category`,
lastmod: new Date().toISOString().split('T')[0],
changefreq: 'daily',
priority: '0.7'
},
{
loc: `${link}${locale}/feed`,
lastmod: new Date().toISOString().split('T')[0],
changefreq: 'daily',
priority: '0.7'
},
{
loc: `${link}${locale}/search`,
lastmod: new Date().toISOString().split('T')[0],
changefreq: 'daily',
priority: '0.7'
},
{
loc: `${link}${locale}/tag`,
lastmod: new Date().toISOString().split('T')[0],
changefreq: 'daily',
priority: '0.7'
}
]
const postFields =
allPages
?.filter(p => p.status === BLOG.NOTION_PROPERTY_NAME.status_publish)
?.map(post => {
const slugWithoutLeadingSlash = post?.slug.startsWith('/')
? post?.slug?.slice(1)
: post.slug
return {
loc: `${link}${locale}/${slugWithoutLeadingSlash}`,
lastmod: new Date(post?.publishDay).toISOString().split('T')[0],
changefreq: 'daily',
priority: '0.7'
}
}) ?? []
return defaultFields.concat(postFields)
}
export default () => {}

View File

@@ -11,14 +11,17 @@ import { useRouter } from 'next/router'
*/
const Tag = props => {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
}
export async function getStaticProps({ params: { tag } }) {
export async function getStaticProps({ params: { tag }, locale }) {
const from = 'tag-props'
const props = await getGlobalData({ from })
const props = await getGlobalData({ from, locale })
// 过滤状态
props.posts = props.allPages
@@ -39,7 +42,11 @@ export async function getStaticProps({ params: { tag } }) {
delete props.allPages
return {
props,
revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND)
revalidate: siteConfig(
'NEXT_REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}

View File

@@ -6,13 +6,16 @@ import { useRouter } from 'next/router'
const Tag = props => {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
}
export async function getStaticProps({ params: { tag, page } }) {
export async function getStaticProps({ params: { tag, page }, locale }) {
const from = 'tag-page-props'
const props = await getGlobalData({ from })
const props = await getGlobalData({ from, locale })
// 过滤状态、标签
props.posts = props.allPages
?.filter(page => page.type === 'Post' && page.status === 'Published')
@@ -20,14 +23,21 @@ export async function getStaticProps({ params: { tag, page } }) {
// 处理文章数
props.postCount = props.posts.length
// 处理分页
props.posts = props.posts.slice(siteConfig('POSTS_PER_PAGE') * (page - 1), siteConfig('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
delete props.allPages
return {
props,
revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND)
revalidate: siteConfig(
'NEXT_REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}

View File

@@ -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'
/**
* 标签首页
@@ -11,17 +11,26 @@ import { siteConfig } from '@/lib/config'
*/
const TagIndex = props => {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
}
export async function getStaticProps() {
export async function getStaticProps(req) {
const { locale } = req
const from = 'tag-index-props'
const props = await getGlobalData({ from })
const props = await getGlobalData({ from, locale })
delete props.allPages
return {
props,
revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND)
revalidate: siteConfig(
'NEXT_REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}

View File

@@ -1,30 +1,29 @@
<!DOCTYPE html>
<!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">
<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;
}
html,
body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
#myIframe {
width: 100%;
height: 100%;
border: none;
/* 可选:移除边框 */
}
#myIframe {
width: 100%;
height: 100%;
border: none;
/* 可选:移除边框 */
}
</style>
</head>
</head>
<body>
<body>
<!-- <div style="position: absolute;
right: 0px;
bottom: 0px;
@@ -32,17 +31,18 @@
<button onclick="toggleFullScreen()">Toggle Full Screen</button>
</div> -->
<iframe id="myIframe" allowfullscreen="allowfullscreen" allow="autoplay" scrolling="no"></iframe>
<iframe
id="myIframe"
allowfullscreen="true"
allow="autoplay;fullscreen"
scrolling="auto"
></iframe>
<!-- https://letsplay247.github.io/cz.html?n=space-wars-battleground -->
<script>
var myParam = location.search.split('n=')[1]
document.getElementById("myIframe").src = myParam;
var myParam = location.search.split('n=')[1]
document.getElementById('myIframe').src = myParam
</script>
<script src="/js/fullscreen.js" type="text/javascript"></script>
</body>
</html>
</body>
</html>

209
public/js/giscus.js Normal file
View File

@@ -0,0 +1,209 @@
/* eslint-disable */
;(function () {
var baseUrl = 'https://giscus.app'
var giscusIframe = null
// 错误日志
function handleError(a) {
return '[giscus] An error occurred. Error message: "'.concat(a, '".')
}
// 站点元信息
function getMetaContent(name, includeProperty) {
void 0 === includeProperty && (includeProperty = !1)
includeProperty = includeProperty
? "meta[property='og:".concat(name, "'],")
: ''
return (name = document.querySelector(
includeProperty + "meta[name='".concat(name, "']")
))
? name.content
: ''
}
// 渲染
function render(querySelector) {
// const giscusContainer = document.currentScript
const giscusContainer = document.querySelector(querySelector)
// var k = new URL(m.src).origin
let dataset = new URL(location.href)
let paramsSession = dataset.searchParams.get('giscus') || ''
const localStorageSession = localStorage.getItem('giscus-session')
dataset.searchParams.delete('giscus')
dataset.hash = ''
let url = dataset.toString()
if (paramsSession)
localStorage.setItem('giscus-session', JSON.stringify(paramsSession)),
history.replaceState(void 0, document.title, url)
else if (localStorageSession) {
try {
paramsSession = JSON.parse(localStorageSession)
} catch (a) {
localStorage.removeItem('giscus-session'),
console.warn(
''.concat(
handleError(a === null || void 0 === a ? void 0 : a.message),
' Session has been cleared.'
)
)
}
}
dataset = giscusContainer.dataset
var params = {}
params.origin = url
params.session = paramsSession
params.theme = dataset.theme
params.reactionsEnabled = dataset.reactionsEnabled || '1'
params.emitMetadata = dataset.emitMetadata || '0'
params.inputPosition = dataset.inputPosition || 'bottom'
params.repo = dataset.repo
params.repoId = dataset.repoId
params.category = dataset.category || ''
params.categoryId = dataset.categoryId
params.strict = dataset.strict || '0'
params.description = getMetaContent('description', !0)
params.backLink = getMetaContent('giscus:backlink') || url
switch (dataset.mapping) {
case 'url':
params.term = url
break
case 'title':
params.term = document.title
break
case 'og:title':
params.term = getMetaContent('title', !0)
break
case 'specific':
params.term = dataset.term
break
case 'number':
params.number = dataset.term
break
default:
params.term =
location.pathname.length < 2
? 'index'
: location.pathname.substring(1).replace(/\.\w+$/, '')
}
const q =
(paramsSession = document.querySelector('.giscus')) && paramsSession.id
q && (params.origin = ''.concat(url, '#').concat(q))
url = dataset.lang ? '/'.concat(dataset.lang) : ''
url = ''
.concat(baseUrl)
.concat(url, '/widget?')
.concat(new URLSearchParams(params))
dataset = dataset.loading === 'lazy' ? 'lazy' : void 0
// 创建iframe
giscusIframe = document.createElement('iframe')
Object.entries({
class: 'giscus-frame giscus-frame--loading',
title: 'Comments',
scrolling: 'no',
allow: 'clipboard-write',
src: url,
loading: dataset
}).forEach(function (a) {
const g = a[0]
return (a = a[1]) && giscusIframe.setAttribute(g, a)
})
giscusIframe.style.opacity = '0'
giscusIframe.addEventListener('load', function () {
giscusIframe.style.removeProperty('opacity')
giscusIframe.classList.remove('giscus-frame--loading')
})
dataset =
document.getElementById('giscus-css') || document.createElement('link')
dataset.id = 'giscus-css'
dataset.rel = 'stylesheet'
dataset.href = ''.concat(baseUrl, '/default.css')
document.head.prepend(dataset)
if (paramsSession) {
for (; paramsSession.firstChild; ) paramsSession.firstChild.remove()
paramsSession.appendChild(giscusIframe)
} else
(paramsSession = document.createElement('div')),
paramsSession.setAttribute('class', 'giscus'),
paramsSession.appendChild(giscusIframe),
giscusContainer.insertAdjacentElement('afterend', paramsSession)
}
// 处理接收消息
function handdleMessage(event) {
if (!giscusIframe) {
return
}
event.origin === baseUrl &&
((event = event.data),
typeof event === 'object' &&
event.giscus &&
(event.giscus.resizeHeight &&
(giscusIframe.style.height = ''.concat(
event.giscus.resizeHeight,
'px'
)),
event.giscus.signOut
? (localStorage.removeItem('giscus-session'),
console.log(
'[giscus] User has logged out. Session has been cleared.'
),
p())
: event.giscus.error &&
((event = event.giscus.error),
event.includes('Bad credentials') ||
event.includes('Invalid state value') ||
event.includes('State has expired')
? localStorage.getItem('giscus-session') !== null
? (localStorage.removeItem('giscus-session'),
console.warn(
''.concat(handleError(event), ' Session has been cleared.')
),
p())
: localStorageSession ||
console.error(
''
.concat(
handleError(event),
' No session is stored initially. '
)
.concat(
'Please consider reporting this error at https://github.com/giscus/giscus/issues/new.'
)
)
: event.includes('Discussion not found')
? console.warn(
'[giscus] '.concat(
event,
'. A new discussion will be created if a comment/reaction is submitted.'
)
)
: event.includes('API rate limit exceeded')
? console.warn(handleError(event))
: console.error(
''
.concat(handleError(event), ' ')
.concat(
'Please consider reporting this error at https://github.com/giscus/giscus/issues/new.'
)
))))
}
// 初始化
function initializeGiscus(querySelector) {
render(querySelector)
window.addEventListener('message', handdleMessage)
}
// 销毁
function destroyGiscus() {
giscusIframe?.remove()
giscusIframe = null
}
// 暴露接口
window.Giscus = {
init: initializeGiscus,
destroy: destroyGiscus
}
})()

View File

@@ -1,84 +0,0 @@
/* Make clicks pass-through */
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: #29d;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
/* Fancy blur effect */
#nprogress .peg {
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
box-shadow: 0 0 10px #29d, 0 0 5px #29d;
opacity: 1;
-webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px);
}
/* Remove these to get rid of the spinner */
#nprogress .spinner {
display: block;
position: fixed;
z-index: 1031;
top: 15px;
right: 15px;
}
#nprogress .spinner-icon {
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: #29d;
border-left-color: #29d;
border-radius: 50%;
-webkit-animation: nprogress-spinner 400ms linear infinite;
animation: nprogress-spinner 400ms linear infinite;
}
.nprogress-custom-parent {
overflow: hidden;
position: relative;
}
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
@-webkit-keyframes nprogress-spinner {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes nprogress-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@@ -1,18 +1,35 @@
import Link from 'next/link'
import { useGlobal } from '@/lib/global'
import Link from 'next/link'
import CONFIG from '../config'
const MenuGroupCard = (props) => {
const MenuGroupCard = props => {
const { postCount, categoryOptions, tagOptions } = props
const { locale } = useGlobal()
const archiveSlot = <div className='text-center'>{postCount}</div>
const categorySlot = <div className='text-center'>{categoryOptions?.length}</div>
const categorySlot = (
<div className='text-center'>{categoryOptions?.length}</div>
)
const tagSlot = <div className='text-center'>{tagOptions?.length}</div>
const links = [
{ name: locale.COMMON.ARTICLE, to: '/archive', slot: archiveSlot, show: CONFIG.MENU_ARCHIVE },
{ name: locale.COMMON.CATEGORY, to: '/category', slot: categorySlot, show: CONFIG.MENU_CATEGORY },
{ name: locale.COMMON.TAGS, to: '/tag', slot: tagSlot, show: CONFIG.MENU_TAG }
{
name: locale.COMMON.ARTICLE,
to: '/archive',
slot: archiveSlot,
show: CONFIG.MENU_ARCHIVE
},
{
name: locale.COMMON.CATEGORY,
to: '/category',
slot: categorySlot,
show: CONFIG.MENU_CATEGORY
},
{
name: locale.COMMON.TAGS,
to: '/tag',
slot: tagSlot,
show: CONFIG.MENU_TAG
}
]
for (let i = 0; i < links.length; i++) {
@@ -22,29 +39,31 @@ const MenuGroupCard = (props) => {
}
return (
<nav id='nav' className='leading-8 flex justify-center dark:text-gray-200 w-full'>
{links.map(link => {
if (link.show) {
return (
<Link
key={`${link.to}`}
title={link.to}
href={link.to}
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
className={'py-1.5 my-1 px-2 duration-300 text-base justify-center items-center cursor-pointer'}>
<div className='w-full items-center justify-center hover:scale-105 duration-200 transform dark:hover:text-red-400 hover:text-red-600'>
<div className='text-center'>{link.name}</div>
<div className='text-center font-semibold'>{link.slot}</div>
</div>
</Link>
)
} else {
return null
}
})}
</nav>
<nav
id='nav'
className='leading-8 flex justify-center dark:text-gray-200 w-full'>
{links.map(link => {
if (link.show) {
return (
<Link
key={`${link.to}`}
title={link.to}
href={link.to}
target={link?.target}
className={
'py-1.5 my-1 px-2 duration-300 text-base justify-center items-center cursor-pointer'
}>
<div className='w-full items-center justify-center hover:scale-105 duration-200 transform dark:hover:text-red-400 hover:text-red-600'>
<div className='text-center'>{link.name}</div>
<div className='text-center font-semibold'>{link.slot}</div>
</div>
</Link>
)
} else {
return null
}
})}
</nav>
)
}
export default MenuGroupCard

View File

@@ -7,7 +7,7 @@ import { useState } from 'react'
* @param {*} param0
* @returns
*/
export const MenuItemCollapse = (props) => {
export const MenuItemCollapse = props => {
const { link } = props
const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0
@@ -26,30 +26,55 @@ export const MenuItemCollapse = (props) => {
return null
}
return <>
<div className='w-full px-8 py-3 text-left dark:bg-hexo-black-gray' onClick={toggleShow} >
{!hasSubMenu && <Link
href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
className="hover:text-[#D2232A] font-extralight flex justify-between pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest pb-1">
<span className=' transition-all items-center duration-200'>{link?.icon && <i className={link.icon + ' mr-4'} />}{link?.name}</span>
</Link>}
{hasSubMenu && <div
onClick={hasSubMenu ? toggleOpenSubMenu : null}
className="hover:text-[#D2232A] font-extralight flex items-center justify-between pl-2 pr-4 cursor-pointer dark:text-gray-200 no-underline tracking-widest pb-1">
<span className='transition-all items-center duration-200'>{link?.icon && <i className={link.icon + ' mr-4'} />}{link?.name}</span>
<i className={`px-2 fas fa-chevron-left transition-all duration-200 ${isOpen ? '-rotate-90' : ''} text-gray-400`}></i>
</div>}
</div>
return (
<>
<div
className='w-full px-8 py-3 text-left dark:bg-hexo-black-gray'
onClick={toggleShow}>
{!hasSubMenu && (
<Link
href={link?.to}
target={link?.target}
className='hover:text-[#D2232A] font-extralight flex justify-between pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest pb-1'>
<span className=' transition-all items-center duration-200'>
{link?.icon && <i className={link.icon + ' mr-4'} />}
{link?.name}
</span>
</Link>
)}
{hasSubMenu && (
<div
onClick={hasSubMenu ? toggleOpenSubMenu : null}
className='hover:text-[#D2232A] font-extralight flex items-center justify-between pl-2 pr-4 cursor-pointer dark:text-gray-200 no-underline tracking-widest pb-1'>
<span className='transition-all items-center duration-200'>
{link?.icon && <i className={link.icon + ' mr-4'} />}
{link?.name}
</span>
<i
className={`px-2 fas fa-chevron-left transition-all duration-200 ${isOpen ? '-rotate-90' : ''} text-gray-400`}></i>
</div>
)}
</div>
{/* 折叠子菜单 */}
{hasSubMenu && <Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
{link.subMenus.map((sLink, index) => {
return <div key={index} className='dark:bg-black dark:text-gray-200 text-left px-10 justify-start bg-gray-50 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 py-3 pr-6'>
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
<span className='text-sm ml-4 whitespace-nowrap'>{link?.icon && <i className={sLink.icon + ' mr-2'} />} {sLink.title}</span>
</Link>
</div>
})}
</Collapse>}
{/* 折叠子菜单 */}
{hasSubMenu && (
<Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
{link.subMenus.map((sLink, index) => {
return (
<div
key={index}
className='dark:bg-black dark:text-gray-200 text-left px-10 justify-start bg-gray-50 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 py-3 pr-6'>
<Link href={sLink.to} target={link?.target}>
<span className='text-sm ml-4 whitespace-nowrap'>
{link?.icon && <i className={sLink.icon + ' mr-2'} />}{' '}
{sLink.title}
</span>
</Link>
</div>
)
})}
</Collapse>
)}
</>
)
}

View File

@@ -11,33 +11,51 @@ export const MenuItemDrop = ({ link }) => {
return null
}
return <div onMouseOver={() => changeShow(true)} onMouseOut={() => changeShow(false)} className='h-full'>
return (
<div
onMouseOver={() => changeShow(true)}
onMouseOut={() => changeShow(false)}
className='h-full'>
{!hasSubMenu && (
<Link
href={link?.to}
target={link?.target}
className={`${selected && 'border-b-2 border-[#D2232A]'} h-full flex space-x-1 whitespace-nowrap items-center font-sans menu-link pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest pb-1`}>
{link?.icon && <i className={link?.icon} />} <div>{link?.name}</div>
{/* {hasSubMenu && <i className='px-2 fa fa-angle-down'></i>} */}
</Link>
)}
{!hasSubMenu &&
<Link
href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
className={`${selected && 'border-b-2 border-[#D2232A]'} h-full flex space-x-1 whitespace-nowrap items-center font-sans menu-link pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest pb-1`}>
{link?.icon && <i className={link?.icon}/>} <div>{link?.name}</div>
{/* {hasSubMenu && <i className='px-2 fa fa-angle-down'></i>} */}
</Link>}
{hasSubMenu && <>
<div className='h-full flex space-x-1 whitespace-nowrap items-center cursor-pointer font-sans menu-link pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest pb-1'>
{link?.icon && <i className={link?.icon}/>} <div>{link?.name}</div>
{/* <i className={`px-2 fa fa-angle-down duration-300 ${show ? 'rotate-180' : 'rotate-0'}`}></i> */}
</div>
</>}
{/* 子菜单 */}
{hasSubMenu && <ul style={{ backdropFilter: 'blur(3px)' }} className={`${show ? 'visible opacity-100 shadow-lg' : 'invisible opacity-0'} overflow-hidden bg-white transition-all duration-300 z-20 absolute block `}>
{link.subMenus.map((sLink, index) => {
return <li key={index} className='cursor-pointer hover:bg-red-300 text-gray-900 hover:text-black tracking-widest transition-all duration-200 dark:border-gray-800 py-1 pr-6 pl-3'>
<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} > &nbsp; </i>}{sLink.title}</span>
</Link>
</li>
})}
</ul>}
{hasSubMenu && (
<>
<div className='h-full flex space-x-1 whitespace-nowrap items-center cursor-pointer font-sans menu-link pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest pb-1'>
{link?.icon && <i className={link?.icon} />} <div>{link?.name}</div>
{/* <i className={`px-2 fa fa-angle-down duration-300 ${show ? 'rotate-180' : 'rotate-0'}`}></i> */}
</div>
</>
)}
{/* 子菜单 */}
{hasSubMenu && (
<ul
style={{ backdropFilter: 'blur(3px)' }}
className={`${show ? 'visible opacity-100 shadow-lg' : 'invisible opacity-0'} overflow-hidden bg-white transition-all duration-300 z-20 absolute block `}>
{link.subMenus.map((sLink, index) => {
return (
<li
key={index}
className='cursor-pointer hover:bg-red-300 text-gray-900 hover:text-black tracking-widest transition-all duration-200 dark:border-gray-800 py-1 pr-6 pl-3'>
<Link href={sLink.to} target={link?.target}>
<span className='text-sm text-nowrap font-extralight'>
{link?.icon && <i className={sLink?.icon}> &nbsp; </i>}
{sLink.title}
</span>
</Link>
</li>
)
})}
</ul>
)}
</div>
)
}

View File

@@ -5,34 +5,48 @@ export const MenuItemDrop = ({ link }) => {
const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0
return <li className='cursor-pointer' onMouseOver={() => changeShow(true)} onMouseOut={() => changeShow(false)} >
return (
<li
className='cursor-pointer'
onMouseOver={() => changeShow(true)}
onMouseOut={() => changeShow(false)}>
{!hasSubMenu && (
<div className='rounded px-2 md:pl-0 md:mr-3 my-4 md:pr-3 text-gray-700 dark:text-gray-200 no-underline md:border-r border-gray-light'>
<Link href={link?.to} target={link?.target}>
{link?.icon && <i className={link?.icon} />} {link?.name}
{hasSubMenu && <i className='px-2 fa fa-angle-down'></i>}
</Link>
</div>
)}
{!hasSubMenu &&
<div className="rounded px-2 md:pl-0 md:mr-3 my-4 md:pr-3 text-gray-700 dark:text-gray-200 no-underline md:border-r border-gray-light">
<Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
{link?.icon && <i className={link?.icon} />} {link?.name}
{hasSubMenu && <i className='px-2 fa fa-angle-down'></i>}
{hasSubMenu && (
<div className='rounded px-2 md:pl-0 md:mr-3 my-4 md:pr-3 text-gray-700 dark:text-gray-200 no-underline md:border-r border-gray-light'>
{link?.icon && <i className={link?.icon} />} {link?.name}
<i
className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>
</div>
)}
{/* 子菜单 */}
{hasSubMenu && (
<ul
className={`${show ? 'visible opacity-100 top-12' : 'invisible opacity-0 top-10'} border-gray-100 bg-white dark:bg-black dark:border-gray-800 transition-all duration-300 z-20 absolute block drop-shadow-lg `}>
{link.subMenus.map((sLink, index) => {
return (
<li
key={index}
className='not:last-child:border-b-0 border-b text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 dark:border-gray-800 py-3 pr-6 pl-3'>
<Link href={sLink.to} target={link?.target}>
<span className='text-sm text-nowrap font-extralight'>
{link?.icon && <i className={sLink?.icon}> &nbsp; </i>}
{sLink.title}
</span>
</Link>
</div>
}
{hasSubMenu &&
<div className='rounded px-2 md:pl-0 md:mr-3 my-4 md:pr-3 text-gray-700 dark:text-gray-200 no-underline md:border-r border-gray-light'>
{link?.icon && <i className={link?.icon} />} {link?.name}
<i className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>
</div>
}
{/* 子菜单 */}
{hasSubMenu && <ul className={`${show ? 'visible opacity-100 top-12' : 'invisible opacity-0 top-10'} border-gray-100 bg-white dark:bg-black dark:border-gray-800 transition-all duration-300 z-20 absolute block drop-shadow-lg `}>
{link.subMenus.map((sLink, index) => {
return <li key={index} className='not:last-child:border-b-0 border-b text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 dark:border-gray-800 py-3 pr-6 pl-3'>
<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} > &nbsp; </i>}{sLink.title}</span>
</Link>
</li>
})}
</ul>}
</li>
)
})}
</ul>
)}
</li>
)
}

View File

@@ -8,7 +8,7 @@ import { useState } from 'react'
* @param {*} param0
* @returns
*/
export const MenuItemCollapse = (props) => {
export const MenuItemCollapse = props => {
const { link } = props
const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0
@@ -21,7 +21,7 @@ export const MenuItemCollapse = (props) => {
return null
}
const selected = (router.pathname === link.to) || (router.asPath === link.to)
const selected = router.pathname === link.to || router.asPath === link.to
const toggleShow = () => {
changeShow(!show)
@@ -31,32 +31,67 @@ export const MenuItemCollapse = (props) => {
changeIsOpen(!isOpen)
}
return <>
<div className={ (selected ? 'bg-gray-600 text-white hover:text-white' : 'hover:text-gray-600') + ' px-5 w-full text-left duration-200 dark:bg-hexo-black-gray dark:border-black'} onClick={toggleShow} >
return (
<>
<div
className={
(selected
? 'bg-gray-600 text-white hover:text-white'
: 'hover:text-gray-600') +
' px-5 w-full text-left duration-200 dark:bg-hexo-black-gray dark:border-black'
}
onClick={toggleShow}>
{!hasSubMenu && (
<Link
href={link?.to}
target={link?.target}
className='dark:text-gray-200 py-2 w-full my-auto items-center justify-between flex '>
<div>
<div className={`${link.icon} text-center w-4 mr-4`} />
{link.name}
</div>
</Link>
)}
{!hasSubMenu && <Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} className='dark:text-gray-200 py-2 w-full my-auto items-center justify-between flex '>
<div><div className={`${link.icon} text-center w-4 mr-4`} />{link.name}</div>
</Link>}
{hasSubMenu && (
<div
onClick={hasSubMenu ? toggleOpenSubMenu : null}
className='py-2 font-extralight flex justify-between cursor-pointer dark:text-gray-200 no-underline tracking-widest'>
<div>
<div className={`${link.icon} text-center w-4 mr-4`} />
{link.name}
</div>
<div className='inline-flex items-center '>
<i
className={`px-2 fas fa-chevron-right transition-all duration-200 ${isOpen ? 'rotate-90' : ''}`}></i>
</div>
</div>
)}
</div>
{hasSubMenu && <div
onClick={hasSubMenu ? toggleOpenSubMenu : null}
className="py-2 font-extralight flex justify-between cursor-pointer dark:text-gray-200 no-underline tracking-widest">
<div><div className={`${link.icon} text-center w-4 mr-4`} />{link.name}</div>
<div className='inline-flex items-center '><i className={`px-2 fas fa-chevron-right transition-all duration-200 ${isOpen ? 'rotate-90' : ''}`}></i></div>
</div>}
</div>
{/* 折叠子菜单 */}
{hasSubMenu && <Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
{link.subMenus.map((sLink, index) => {
return <div key={index} className='whitespace-nowrap dark:text-gray-200
{/* 折叠子菜单 */}
{hasSubMenu && (
<Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
{link.subMenus.map((sLink, index) => {
return (
<div
key={index}
className='whitespace-nowrap dark:text-gray-200
not:last-child:border-b-0 border-b dark:border-gray-800 py-2 px-14 cursor-pointer hover:bg-gray-100
font-extralight dark:bg-black text-left justify-start text-gray-600 bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200'>
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
<div><div className={`${sLink.icon} text-center w-3 mr-3 text-xs`} />{sLink.title}</div>
</Link>
</div>
})}
</Collapse>}
<Link href={sLink.to} target={link?.target}>
<div>
<div
className={`${sLink.icon} text-center w-3 mr-3 text-xs`}
/>
{sLink.title}
</div>
</Link>
</div>
)
})}
</Collapse>
)}
</>
)
}

View File

@@ -5,34 +5,62 @@ export const MenuItemDrop = ({ link }) => {
const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0
return <li onMouseOver={() => changeShow(true)} onMouseOut={() => changeShow(false)}
className='relative py-1 duration-500 justify-between text-gray-500 dark:text-gray-300 hover:text-black hover:underline cursor-pointer flex flex-nowrap items-center '>
return (
<li
onMouseOver={() => changeShow(true)}
onMouseOut={() => changeShow(false)}
className='relative py-1 duration-500 justify-between text-gray-500 dark:text-gray-300 hover:text-black hover:underline cursor-pointer flex flex-nowrap items-center '>
{!hasSubMenu && (
<Link
href={link?.to}
target={link?.target}
className='w-full my-auto items-center justify-between flex '>
<div>
<div className={`${link.icon} text-center w-4 mr-2`} />
{link.name}
</div>
{link.slot}
</Link>
)}
{!hasSubMenu &&
<Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} className='w-full my-auto items-center justify-between flex ' >
<div><div className={`${link.icon} text-center w-4 mr-2`} />{link.name}</div>
{link.slot}
</Link>}
{hasSubMenu &&
<div className='w-full my-auto items-center justify-between flex '>
<div><div className={`${link.icon} text-center w-4 mr-2`} />{link.name}</div>
{link.slot}
{hasSubMenu && <div className='text-right'><i className={`px-2 fas fa-chevron-right duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i></div>}
</div>}
{/* 子菜单 */}
{hasSubMenu && <ul className={`${show ? 'visible opacity-100 left-52' : 'invisible opacity-0 left-40'} z-20 py-1 absolute right-0 top-0 w-full border-gray-100 bg-white dark:bg-black dark:border-gray-800 transition-all duration-300 drop-shadow-lg `}>
{link?.subMenus?.map((sLink, index) => {
return <li key={index}>
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} className='my-auto py-1 px-2 items-center justify-start flex text-gray-500 dark:text-gray-300 hover:text-black hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 dark:border-gray-800 '>
{sLink.icon && <i className={`${sLink.icon} w-4 text-center `} />}
<div className={'ml-2 whitespace-nowrap'}>{sLink.name}</div>
{sLink.slot}
</Link>
</li>
})}
</ul>}
{hasSubMenu && (
<div className='w-full my-auto items-center justify-between flex '>
<div>
<div className={`${link.icon} text-center w-4 mr-2`} />
{link.name}
</div>
{link.slot}
{hasSubMenu && (
<div className='text-right'>
<i
className={`px-2 fas fa-chevron-right duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>
</div>
)}
</div>
)}
{/* 子菜单 */}
{hasSubMenu && (
<ul
className={`${show ? 'visible opacity-100 left-52' : 'invisible opacity-0 left-40'} z-20 py-1 absolute right-0 top-0 w-full border-gray-100 bg-white dark:bg-black dark:border-gray-800 transition-all duration-300 drop-shadow-lg `}>
{link?.subMenus?.map((sLink, index) => {
return (
<li key={index}>
<Link
href={sLink.to}
target={link?.target}
className='my-auto py-1 px-2 items-center justify-start flex text-gray-500 dark:text-gray-300 hover:text-black hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 dark:border-gray-800 '>
{sLink.icon && (
<i className={`${sLink.icon} w-4 text-center `} />
)}
<div className={'ml-2 whitespace-nowrap'}>{sLink.name}</div>
{sLink.slot}
</Link>
</li>
)
})}
</ul>
)}
</li>
)
}

View File

@@ -9,8 +9,8 @@ export const Footer = props => {
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' />
className={`z-10 dark:bg-black bg-white p-2 rounded-lg relative mt-6 flex-shrink-0 mb-4 w-full shadow dark:text-gray-200 `}>
{/* <hr className='my-2 border-black dark:border-gray-100' /> */}
{/* 页面底部 */}
<div className='w-full flex justify-between p-4 '>
<p>

View File

@@ -0,0 +1,188 @@
/* eslint-disable @next/next/no-img-element */
import { Draggable } from '@/components/Draggable'
import { siteConfig } from '@/lib/config'
import { deepClone } from '@/lib/utils'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import DownloadButton from './DownloadButton'
import FullScreenButton from './FullScreenButton'
/**
* 嵌入游戏
* @param {*} param0
* @returns
*/
export default function GameEmbed({ post, siteInfo }) {
const game = deepClone(post)
const newWindow = game?.ext?.new_window || false
const originUrl = game?.ext?.href
// 提示用户在新窗口打开
const [tipNewWindow, setTipNewWindow] = useState(newWindow)
const [loading, setLoading] = useState(true)
/**
* 新窗口中打开游戏。
* 并且在回到此页面后后刷新iframe尝试重新加载iframe
*/
const openInNewWindow = () => {
// 关闭提示框
setTipNewWindow(false)
// 添加监听器
document.addEventListener('visibilitychange', handleVisibilityChange)
// 定义监听器函数
function handleVisibilityChange() {
if (document.visibilityState === 'hidden') {
// console.log("用户切换到了其他标签页");
} else {
// console.log("用户回到了当前页面");
setLoading(true)
// 刷新网页
reloadIframe()
// 移除监听器
document.removeEventListener('visibilitychange', handleVisibilityChange)
}
}
}
/**
* 隐藏提示框
*/
const hiddenTips = () => {
setTipNewWindow(false)
}
function reloadIframe() {
var iframe = document.getElementById('game-wrapper')
iframe.contentWindow.location.reload()
}
// 定义一个函数来处理iframe加载成功事件
function iframeLoaded() {
if (game) {
setLoading(false)
}
}
useEffect(() => {
// 是否弹窗提示新网页打开
setTipNewWindow(newWindow)
const iframe = document.getElementById('game-wrapper')
// 绑定加载事件
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')}`
}
}, [post])
return (
<div
className={`${originUrl ? '' : 'hidden'} bg-black w-full xl:h-[calc(100vh-8rem)] h-screen rounded-md relative`}>
{/* 移动端返回主页按钮 */}
<Draggable stick='left'>
<div
style={{ left: '0px', top: '1rem' }}
className='text-white 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-chevron-left' />
</Link>{' '}
<span
className='font-serif px-1'
onClick={() => {
document.querySelector('.game-info').scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'nearest'
})
}}>
{/* Title首字母 */}
<i className='fas fa-info' />
</span>
</div>
</Draggable>
{/* 提示框新窗口打开 */}
{tipNewWindow && (
<div
id='open-tips'
className={`animate__animated animate__fadeIn bottom-8 right-4 absolute z-20 flex items-end justify-end`}>
<div className='relative w-96 h-auto bg-white rounded-lg p-2'>
<div className='absolute right-2'>
<button className='text-xl p-2' onClick={hiddenTips}>
<i className='fas fa-times'></i>
</button>
</div>
<div className='p-2 text-lg'>
If the game fails to load, please try accessing the{' '}
<a
className='underline text-blue-500'
rel='noReferrer'
href={`${originUrl?.replace('/games-external/common/index.htm?n=', '')}`}
target='_blank'
onClick={openInNewWindow}>
source webpage
</a>
.
</div>
</div>
</div>
)}
{/* 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 items-center'>
<i className='fas fa-spinner animate-spin'></i>
{siteInfo?.title || siteConfig('TITLE')}
</h2>
<h3 className='text-xl text-white'>
{siteInfo?.description || siteConfig('DESCRIPTION')}
</h3>
</div>
{/* 游戏封面图 */}
{game?.pageCoverThumbnail && (
<img
src={game?.pageCoverThumbnail}
className='w-full h-full object-cover blur-md absolute top-0 left-0 z-0'
/>
)}
</div>
)}
<iframe
id='game-wrapper'
src={originUrl}
className={`relative w-full xl:h-[calc(100vh-8rem)] h-screen md:rounded-md overflow-hidden`}
/>
{/* 游戏窗口装饰器 */}
{originUrl && !loading && (
<div className='game-decorator bg-[#0B0D14] right-0 bottom-0 flex justify-center z-10 md:absolute'>
<DownloadButton />
<FullScreenButton />
</div>
)}
</div>
)
}

View File

@@ -1,5 +1,6 @@
/* eslint-disable @next/next/no-img-element */
import { AdSlot } from '@/components/GoogleAdsense'
import LazyImage from '@/components/LazyImage'
import { siteConfig } from '@/lib/config'
import { checkContainHttp, deepClone, sliceUrlFromHttp } from '@/lib/utils'
import Link from 'next/link'
@@ -88,8 +89,8 @@ export const GameListIndexCombine = ({ posts }) => {
}
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'>
<div className='game-list-wrapper flex justify-center w-full'>
<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-3 px-2 md:p-0'>
{components?.map((ItemComponent, index) => {
return ItemComponent
})}
@@ -104,20 +105,24 @@ export const GameListIndexCombine = ({ posts }) => {
*/
const GameAd = () => {
return (
<div className='card-group border border-gray-600 rounded game-ad h-[20rem] w-full overflow-hidden'>
<div className='card-group relative rounded-lg game-ad h-80 w-full overflow-hidden'>
<AdSlot type='flow' />
<div className='absolute left-0 right-0 w-full h-full flex flex-col justify-center items-center bg-white'>
<p className='text-2xl'>{siteConfig('TITLE')}</p>
<p>{siteConfig('DESCRIPTION')}</p>
</div>
</div>
)
}
/**
* 4卡组成一个大卡
* 大卡由2行2列小卡构成
* @param {*} param0
* @returns
*/
const GameItemGroup = ({ items }) => {
return (
<div className='card-group h-[20rem] w-full grid grid-cols-2 grid-rows-2 gap-2'>
<div className='card-group h-80 w-full grid grid-cols-2 grid-rows-2 gap-3'>
{items.map((item, index) => (
<GameItem key={index} item={item} />
))}
@@ -140,38 +145,39 @@ const GameItem = ({ item, isLargeCard }) => {
const video = item?.ext?.video
return (
<Link
title={title}
href={`${url}`}
className={`card-single ${isLargeCard ? 'h-80 ' : 'h-full text-xs'} w-full transition-all duration-200 hover:scale-105 shadow-md hover:shadow-lg relative rounded-lg overflow-hidden flex justify-center items-center
group hover:border-purple-400`}
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-2/3 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`}
className={`z-10 object-cover w-full ${isLargeCard ? 'h-80' : 'h-full'} absolute overflow-hidden`}
loop='true'
autoPlay
preload='none'>
<source src={video} type='video/mp4' />
</video>
)}
<img
<LazyImage
className='w-full h-full absolute object-cover group-hover:scale-105 duration-100 transition-all'
src={img}
priority
alt={title}
fill='full'
/>
</Link>
)

View File

@@ -43,7 +43,9 @@ export const GameListRelate = ({ posts }) => {
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 url = checkContainHttp(item.slug)
? sliceUrlFromHttp(item.slug)
: `${siteConfig('SUB_PATH', '')}/${item.slug}`
const img = item?.pageCoverThumbnail
const video = item?.ext?.video
@@ -60,15 +62,19 @@ const GameItem = ({ item }) => {
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'>
<div className='text-xs 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-2/3 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'>
<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>
)}

View File

@@ -1,7 +1,7 @@
/* 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 { useRouter } from 'next/router'
import { useState } from 'react'
import { useGameGlobal } from '..'
@@ -32,7 +32,7 @@ export const GameListRecent = ({ maxCount = 14 }) => {
return (
<>
<div className='game-list-recent-wrapper w-full max-w-full overflow-x-auto pt-4 px-2'>
<div className='game-list-recent-wrapper w-full max-w-full overflow-x-auto pt-4 px-2 md:px-0'>
<div className='game-grid md:flex grid grid-flow-col gap-2'>
{components?.map((ItemComponent, index) => {
return ItemComponent
@@ -49,17 +49,43 @@ export const GameListRecent = ({ maxCount = 14 }) => {
* @returns
*/
const GameItem = ({ item }) => {
const router = useRouter()
const { recentGames, setRecentGames } = useGameGlobal()
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 [isClockVisible, setClockVisible] = useState(true)
const toggleIcons = () => {
setClockVisible(!isClockVisible)
}
/**
* 移除最近
*/
const removeRecent = () => {
const updatedRecentGames = deepClone(recentGames) // 创建一个 recentGames 的副本
const indexToRemove = updatedRecentGames.findIndex(
game => game?.title === item.title
) // 找到要移除的项的索引
if (indexToRemove !== -1) {
updatedRecentGames.splice(indexToRemove, 1) // 使用 splice 方法删除项
setRecentGames(updatedRecentGames) // 更新 recentGames 状态
localStorage.setItem('recent_games', JSON.stringify(updatedRecentGames))
}
}
const handleButtonClick = () => {
router.push(url) // 如果是 Next.js
}
const img = item?.pageCoverThumbnail
const video = item?.ext?.video
return (
<Link
href={`${url}`}
<div
onClick={handleButtonClick}
onMouseOver={() => {
setShowType('video')
}}
@@ -67,11 +93,23 @@ const GameItem = ({ item }) => {
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>
className={`cursor-pointer card-single h-28 w-28 relative shadow rounded-md overflow-hidden flex justify-center items-center
group hover:border-purple-400`}>
<button
className='absolute right-0.5 top-1 z-20'
onClick={e => {
e.stopPropagation() // 阻止事件冒泡,防止触发父级元素的点击事件
removeRecent()
}}
onMouseEnter={toggleIcons}
onMouseLeave={toggleIcons}>
{isClockVisible ? (
<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'></i>
) : (
<i className='fas fa-trash-can w-6 h-6 flex items-center justify-center shadow rounded-full bg-white text-red-500 text-sm'></i>
)}
</button>
<div className='absolute text-sm bottom-2 transition-all duration-200 text-white z-30'>
{title}
</div>
@@ -93,6 +131,6 @@ const GameItem = ({ item }) => {
src={img}
alt={title}
/>
</Link>
</div>
)
}

View File

@@ -6,11 +6,13 @@ function GroupCategory({ currentCategory, categoryOptions }) {
}
return (
<>
<div className='flex items-center'>
<Link className='mx-2' href='/category'>
<i className='fas fa-bars' />
</Link>
<div id='category-list' className='dark:border-gray-600 flex flex-wrap py-1'>
<div
id='category-list'
className='dark:border-gray-600 flex flex-wrap py-1'>
{categoryOptions.map(category => {
const selected = currentCategory === category.name
return (
@@ -34,7 +36,7 @@ function GroupCategory({ currentCategory, categoryOptions }) {
)
})}
</div>
</>
</div>
)
}

View File

@@ -11,7 +11,7 @@ import TagItemMini from './TagItemMini'
function GroupTag({ tagOptions, currentTag }) {
if (!tagOptions) return <></>
return (
<>
<div className='flex items-center'>
<Link href='/tag'>
<i className='fas fa-tags p-2' />
</Link>
@@ -21,7 +21,7 @@ function GroupTag({ tagOptions, currentTag }) {
return <TagItemMini key={tag.name} tag={tag} selected={selected} />
})}
</div>
</>
</div>
)
}

View File

@@ -5,12 +5,13 @@ import Logo from './Logo'
* 顶栏
* @returns
*/
export default function Header() {
export default function Header(props) {
const { siteInfo } = props
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 />
<Logo siteInfo={siteInfo} />
<button
className='flex xl:hidden'

View File

@@ -2,12 +2,19 @@ import { siteConfig } from '@/lib/config'
import Link from 'next/link'
/* eslint-disable @next/next/no-html-link-for-pages */
export default function Logo() {
export default function Logo({ siteInfo }) {
return (
<Link passHref href='/' className='logo rounded cursor-pointer flex flex-col items-center'>
<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>
<h1 className='text-2xl dark:text-white font-bold font-serif'>
{siteInfo?.title}
</h1>
<h2 className='text-xs text-gray-400 whitespace-nowrap'>
{siteConfig('BIO')}
</h2>
</div>
</Link>
)

View File

@@ -7,7 +7,7 @@ import { useState } from 'react'
* @param {*} param0
* @returns
*/
export const MenuItemCollapse = (props) => {
export const MenuItemCollapse = props => {
const { link } = props
const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0
@@ -26,30 +26,59 @@ export const MenuItemCollapse = (props) => {
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>
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?.target}
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>}
{/* 折叠子菜单 */}
{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?.target}>
<span className='text-xs'>{sLink.title}</span>
</Link>
</div>
)
})}
</Collapse>
)}
</>
)
}

View File

@@ -20,7 +20,7 @@ export const MenuItemDrop = ({ link }) => {
<Link
href={link?.to}
className='flex flex-nowrap'
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
target={link?.target}>
<div className='w-6 mr-2 text-center'>
{link?.icon && <i className={link?.icon} />}
</div>
@@ -47,11 +47,7 @@ export const MenuItemDrop = ({ link }) => {
<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'
}>
<Link href={sLink.to} target={link?.target}>
<span className='text-sm text-nowrap font-extralight'>
{link?.icon && <i className={sLink?.icon}> &nbsp; </i>}
{sLink.title}

View File

@@ -9,7 +9,7 @@ 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'>
<section className='flex-wrap flex m-2 text-gray--600 dark:text-gray-400 font-light leading-8'>
<div>
<div>
{post?.type !== 'Page' && (

View File

@@ -9,7 +9,7 @@ import Logo from './Logo'
/**
* 侧拉抽屉的内容
*/
export default function SideBarContent({ allNavPages }) {
export default function SideBarContent({ allNavPages, siteInfo }) {
const { sideBarVisible, setSideBarVisible, filterGames, setFilterGames } =
useGameGlobal()
const inputRef = useRef(null) // 创建对输入框的引用
@@ -50,7 +50,7 @@ export default function SideBarContent({ allNavPages }) {
return (
<div className='px-3'>
<div className='py-2 flex justify-between'>
<Logo />
<Logo siteInfo={siteInfo} />
<button
onClick={() => {
setSideBarVisible(false)

View File

@@ -1,6 +1,5 @@
/* eslint-disable @next/next/no-img-element */
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'
@@ -17,9 +16,8 @@ import BlogArchiveItem from './components/BlogArchiveItem'
import { BlogListPage } from './components/BlogListPage'
import { BlogListScroll } from './components/BlogListScroll'
import BlogPostBar from './components/BlogPostBar'
import DownloadButton from './components/DownloadButton'
import { Footer } from './components/Footer'
import FullScreenButton from './components/FullScreenButton'
import GameEmbed from './components/GameEmbed'
import { GameListIndexCombine } from './components/GameListIndexCombine'
import { GameListRelate } from './components/GameListRealate'
import { GameListRecent } from './components/GameListRecent'
@@ -46,7 +44,7 @@ export const useGameGlobal = () => useContext(ThemeGlobalGame)
* @constructor
*/
const LayoutBase = props => {
const { allNavPages, children } = props
const { allNavPages, children, siteInfo } = props
const searchModal = useRef(null)
// 在列表中进行实时过滤
const [filterKey, setFilterKey] = useState('')
@@ -64,11 +62,6 @@ const LayoutBase = props => {
const [sideBarVisible, setSideBarVisible] = useState(false)
useEffect(() => {
setRecentGames(
localStorage.getItem('recent_games')
? JSON.parse(localStorage.getItem('recent_games'))
: []
)
loadWowJS()
}, [])
@@ -87,7 +80,7 @@ const LayoutBase = props => {
}}>
<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`}>
className={`${siteConfig('FONT_STYLE')} w-full h-full min-h-screen justify-center dark:bg-black dark:bg-opacity-50 dark:text-gray-300 scroll-smooth`}>
<Style />
{/* 左右布局 */}
@@ -99,7 +92,7 @@ const LayoutBase = props => {
<div className='py-4 px-2 sticky top-0 h-screen flex flex-col justify-between'>
<div className='select-none'>
{/* 抬头logo等 */}
<Header />
<Header siteInfo={siteInfo} />
{/* 菜单栏 */}
<MenuList {...props} />
</div>
@@ -112,9 +105,8 @@ const LayoutBase = props => {
</div>
{/* 右侧 */}
<main className='flex-grow w-full h-full flex flex-col min-h-screen overflow-x-auto'>
<main className='flex-grow w-full h-full flex flex-col min-h-screen overflow-x-auto md:p-2'>
<div className='flex-grow h-full'>{children}</div>
<Footer />
</main>
</div>
@@ -124,7 +116,7 @@ const LayoutBase = props => {
onClose={() => {
setSideBarVisible(false)
}}>
<SideBarContent {...props} />
<SideBarContent siteInfo={siteInfo} {...props} />
</SideBarDrawer>
</div>
</ThemeGlobalGame.Provider>
@@ -138,19 +130,25 @@ const LayoutBase = props => {
* @returns
*/
const LayoutIndex = props => {
const { tagOptions, currentTag, categoryOptions, currentCategory } = props
const { tagOptions, currentTag, categoryOptions, currentCategory, siteInfo } =
props
return (
<>
{/* 首页移动端顶部导航 */}
<div className='p-2 xl:hidden'>
<Header />
<Header siteInfo={siteInfo} />
</div>
{/* 最近游戏 */}
<GameListRecent />
{/* 游戏列表 */}
<LayoutPostList {...props} />
{/* 主区域下方 */}
{/* 广告 */}
<div className='w-full'>
<AdSlot type='in-article' />
</div>
{/* 主区域下方 导览 */}
<div className='w-full bg-white dark:bg-hexo-black-gray rounded-lg p-2'>
{/* 标签汇总 */}
<GroupCategory
@@ -159,16 +157,8 @@ const LayoutIndex = props => {
/>
<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} />
{/* 站点公告信息 */}
<Announcement {...props} className='p-2' />
</div>
</>
)
@@ -278,27 +268,25 @@ const LayoutArchive = props => {
* @returns
*/
const LayoutSlug = props => {
const { setRecentGames } = useGameGlobal()
const { post, siteInfo, allNavPages, recommendPosts, lock, validPassword } =
props
const game = deepClone(post)
const [loading, setLoading] = useState(true)
// const [url, setUrl] = useState(game?.ext?.href)
const relateGames = recommendPosts
const randomGames = shuffleArray(deepClone(allNavPages))
// 初始化可安装应用
initialPWA(game, siteInfo)
initialPWA(post, siteInfo)
// 将当前游戏加入到最近游玩
useEffect(() => {
// 更新最新游戏
const recentGames = localStorage.getItem('recent_games')
? JSON.parse(localStorage.getItem('recent_games'))
: []
const existedIndex = recentGames.findIndex(item => item?.id === game?.id)
const existedIndex = recentGames.findIndex(item => item?.id === post?.id)
if (existedIndex === -1) {
recentGames.unshift(game) // 将游戏插入到数组头部
recentGames.unshift(post) // 将游戏插入到数组头部
} else {
// 如果游戏已存在于数组中,将其移至数组头部
const existingGame = recentGames.splice(existedIndex, 1)[0]
@@ -306,126 +294,37 @@ const LayoutSlug = props => {
}
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])
setRecentGames(recentGames)
}, [post])
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 id='article-wrapper'>
<div className='game-detail-wrapper w-full grow flex'>
<div className={`w-full md:py-2`}>
{/* 游戏窗口 */}
<GameEmbed post={post} siteInfo={siteInfo} />
<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 items-center'>
<i className='fas fa-spinner animate-spin'></i>
{siteInfo?.title || siteConfig('TITLE')}
</h2>
<h3 className='text-xl text-white'>
{siteInfo?.description || siteConfig('DESCRIPTION')}
</h3>
</div>
{/* 游戏封面图 */}
{game?.pageCoverThumbnail && (
<img
src={game?.pageCoverThumbnail}
className='w-full h-full object-cover 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 z-10 md:absolute'>
<DownloadButton />
<FullScreenButton />
</div>
)}
</div>
{/* 游戏资讯 */}
<div className='game-info dark:text-white py-4 px-2 md:px-0 mt-14 md:mt-0'>
{/* 资讯 */}
<div className='game-info dark:text-white py-2 px-2 md:px-0 mt-14 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'>
{/* 详情描述 */}
{post && (
<div className='bg-white shadow-md my-2 p-4 rounded-md dark:bg-black'>
<PostInfo post={post} />
<NotionPage post={post} />
{/* 广告嵌入 */}
<AdSlot />
{/* 分享栏目 */}
<ShareBar post={post} />
{/* 评论区 */}
<Comment frontMatter={post} />
</div>
)}

View File

@@ -5,14 +5,21 @@
* @returns
*/
const Style = () => {
return <style jsx global>{`
// 底色
.dark body{
return (
<style jsx global>{`
// 底色
.dark body {
background-color: black;
}
}
`}</style>
body {
background-color: #ffffff;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 800 800'%3E%3Cdefs%3E%3CradialGradient id='a' cx='400' cy='400' r='50%25' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%23ffffff'/%3E%3Cstop offset='1' stop-color='%230EF'/%3E%3C/radialGradient%3E%3CradialGradient id='b' cx='400' cy='400' r='70%25' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%23ffffff'/%3E%3Cstop offset='1' stop-color='%230FF'/%3E%3C/radialGradient%3E%3C/defs%3E%3Crect fill='url(%23a)' width='800' height='800'/%3E%3Cg fill-opacity='.8'%3E%3Cpath fill='url(%23b)' d='M998.7 439.2c1.7-26.5 1.7-52.7 0.1-78.5L401 399.9c0 0 0-0.1 0-0.1l587.6-116.9c-5.1-25.9-11.9-51.2-20.3-75.8L400.9 399.7c0 0 0-0.1 0-0.1l537.3-265c-11.6-23.5-24.8-46.2-39.3-67.9L400.8 399.5c0 0 0-0.1-0.1-0.1l450.4-395c-17.3-19.7-35.8-38.2-55.5-55.5l-395 450.4c0 0-0.1 0-0.1-0.1L733.4-99c-21.7-14.5-44.4-27.6-68-39.3l-265 537.4c0 0-0.1 0-0.1 0l192.6-567.4c-24.6-8.3-49.9-15.1-75.8-20.2L400.2 399c0 0-0.1 0-0.1 0l39.2-597.7c-26.5-1.7-52.7-1.7-78.5-0.1L399.9 399c0 0-0.1 0-0.1 0L282.9-188.6c-25.9 5.1-51.2 11.9-75.8 20.3l192.6 567.4c0 0-0.1 0-0.1 0l-265-537.3c-23.5 11.6-46.2 24.8-67.9 39.3l332.8 498.1c0 0-0.1 0-0.1 0.1L4.4-51.1C-15.3-33.9-33.8-15.3-51.1 4.4l450.4 395c0 0 0 0.1-0.1 0.1L-99 66.6c-14.5 21.7-27.6 44.4-39.3 68l537.4 265c0 0 0 0.1 0 0.1l-567.4-192.6c-8.3 24.6-15.1 49.9-20.2 75.8L399 399.8c0 0 0 0.1 0 0.1l-597.7-39.2c-1.7 26.5-1.7 52.7-0.1 78.5L399 400.1c0 0 0 0.1 0 0.1l-587.6 116.9c5.1 25.9 11.9 51.2 20.3 75.8l567.4-192.6c0 0 0 0.1 0 0.1l-537.3 265c11.6 23.5 24.8 46.2 39.3 67.9l498.1-332.8c0 0 0 0.1 0.1 0.1l-450.4 395c17.3 19.7 35.8 38.2 55.5 55.5l395-450.4c0 0 0.1 0 0.1 0.1L66.6 899c21.7 14.5 44.4 27.6 68 39.3l265-537.4c0 0 0.1 0 0.1 0L207.1 968.3c24.6 8.3 49.9 15.1 75.8 20.2L399.8 401c0 0 0.1 0 0.1 0l-39.2 597.7c26.5 1.7 52.7 1.7 78.5 0.1L400.1 401c0 0 0.1 0 0.1 0l116.9 587.6c25.9-5.1 51.2-11.9 75.8-20.3L400.3 400.9c0 0 0.1 0 0.1 0l265 537.3c23.5-11.6 46.2-24.8 67.9-39.3L400.5 400.8c0 0 0.1 0 0.1-0.1l395 450.4c19.7-17.3 38.2-35.8 55.5-55.5l-450.4-395c0 0 0-0.1 0.1-0.1L899 733.4c14.5-21.7 27.6-44.4 39.3-68l-537.4-265c0 0 0-0.1 0-0.1l567.4 192.6c8.3-24.6 15.1-49.9 20.2-75.8L401 400.2c0 0 0-0.1 0-0.1L998.7 439.2z'/%3E%3C/g%3E%3C/svg%3E");
background-attachment: fixed;
background-size: cover;
}
`}</style>
)
}
export { Style }

View File

@@ -8,7 +8,7 @@ import { useState } from 'react'
* @param {*} param0
* @returns
*/
export const MenuItemCollapse = (props) => {
export const MenuItemCollapse = props => {
const { link } = props
const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0
@@ -21,7 +21,7 @@ export const MenuItemCollapse = (props) => {
return null
}
const selected = (router.pathname === link.to) || (router.asPath === link.to)
const selected = router.pathname === link.to || router.asPath === link.to
const toggleShow = () => {
changeShow(!show)
@@ -31,31 +31,67 @@ export const MenuItemCollapse = (props) => {
changeIsOpen(!isOpen)
}
return <>
<div className={ (selected ? 'bg-green-600 text-white hover:text-white' : 'hover:text-green-600') + ' px-7 w-full text-left duration-200 dark:bg-hexo-black-gray dark:border-black'} onClick={toggleShow} >
return (
<>
<div
className={
(selected
? 'bg-green-600 text-white hover:text-white'
: 'hover:text-green-600') +
' px-7 w-full text-left duration-200 dark:bg-hexo-black-gray dark:border-black'
}
onClick={toggleShow}>
{!hasSubMenu && (
<Link
href={link?.to}
target={link?.target}
className='py-2 w-full my-auto items-center justify-between flex '>
<div>
<div className={`${link.icon} text-center w-4 mr-4`} />
{link.name}
</div>
</Link>
)}
{!hasSubMenu && <Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} className='py-2 w-full my-auto items-center justify-between flex '>
<div><div className={`${link.icon} text-center w-4 mr-4`} />{link.name}</div>
</Link>}
{hasSubMenu && (
<div
onClick={hasSubMenu ? toggleOpenSubMenu : null}
className='py-2 font-extralight flex justify-between cursor-pointer dark:text-gray-200 no-underline tracking-widest'>
<div>
<div className={`${link.icon} text-center w-4 mr-4`} />
{link.name}
</div>
<div className='inline-flex items-center '>
<i
className={`px-2 fas fa-chevron-right transition-all duration-200 ${isOpen ? 'rotate-90' : ''}`}></i>
</div>
</div>
)}
</div>
{hasSubMenu && <div onClick={hasSubMenu ? toggleOpenSubMenu : null}
className="py-2 font-extralight flex justify-between cursor-pointer dark:text-gray-200 no-underline tracking-widest">
<div><div className={`${link.icon} text-center w-4 mr-4`} />{link.name}</div>
<div className='inline-flex items-center '><i className={`px-2 fas fa-chevron-right transition-all duration-200 ${isOpen ? 'rotate-90' : ''}`}></i></div>
</div>}
</div>
{/* 折叠子菜单 */}
{hasSubMenu && <Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
{link?.subMenus?.map((sLink, index) => {
return <div key={index} className='
{/* 折叠子菜单 */}
{hasSubMenu && (
<Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
{link?.subMenus?.map((sLink, index) => {
return (
<div
key={index}
className='
not:last-child:border-b-0 border-b dark:border-gray-800 py-2 px-14 cursor-pointer hover:bg-gray-100 dark:text-gray-200
font-extralight dark:bg-black text-left justify-start text-gray-600 bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200'>
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
<div><div className={`${sLink.icon} text-center w-3 mr-3 text-xs`} />{sLink.title}</div>
</Link>
</div>
})}
</Collapse>}
<Link href={sLink.to} target={link?.target}>
<div>
<div
className={`${sLink.icon} text-center w-3 mr-3 text-xs`}
/>
{sLink.title}
</div>
</Link>
</div>
)
})}
</Collapse>
)}
</>
)
}

View File

@@ -1,6 +1,6 @@
import Link from 'next/link'
import { useState } from 'react'
import { useRouter } from 'next/router'
import { useState } from 'react'
export const MenuItemDrop = ({ link }) => {
const [show, changeShow] = useState(false)
@@ -10,38 +10,64 @@ export const MenuItemDrop = ({ link }) => {
return null
}
const hasSubMenu = link?.subMenus?.length > 0
const selected = (router.pathname === link.to) || (router.asPath === link.to)
return <li className='cursor-pointer list-none items-center flex mx-2' onMouseOver={() => changeShow(true)} onMouseOut={() => changeShow(false)} >
const selected = router.pathname === link.to || router.asPath === link.to
return (
<li
className='cursor-pointer list-none items-center flex mx-2'
onMouseOver={() => changeShow(true)}
onMouseOut={() => changeShow(false)}>
{hasSubMenu && (
<div
className={
'px-2 h-full whitespace-nowrap duration-300 text-sm justify-between dark:text-gray-300 cursor-pointer flex flex-nowrap items-center ' +
(selected
? 'bg-green-600 text-white hover:text-white'
: 'hover:text-green-600')
}>
<div>
{link?.icon && <i className={link?.icon} />} {link?.name}
{hasSubMenu && (
<i
className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>
)}
</div>
</div>
)}
{hasSubMenu &&
<div className={'px-2 h-full whitespace-nowrap duration-300 text-sm justify-between dark:text-gray-300 cursor-pointer flex flex-nowrap items-center ' +
(selected ? 'bg-green-600 text-white hover:text-white' : 'hover:text-green-600')}>
<div>
{link?.icon && <i className={link?.icon} />} {link?.name}
{hasSubMenu && <i className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>}
</div>
</div>
}
{!hasSubMenu && (
<div
className={
'px-2 h-full whitespace-nowrap duration-300 text-sm justify-between dark:text-gray-300 cursor-pointer flex flex-nowrap items-center ' +
(selected
? 'bg-green-600 text-white hover:text-white'
: 'hover:text-green-600')
}>
<Link href={link?.to} target={link?.target}>
{link?.icon && <i className={link?.icon} />} {link?.name}
</Link>
</div>
)}
{!hasSubMenu &&
<div className={'px-2 h-full whitespace-nowrap duration-300 text-sm justify-between dark:text-gray-300 cursor-pointer flex flex-nowrap items-center ' +
(selected ? 'bg-green-600 text-white hover:text-white' : 'hover:text-green-600')}>
<Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
{link?.icon && <i className={link?.icon} />} {link?.name}
{/* 子菜单 */}
{hasSubMenu && (
<ul
className={`${show ? 'visible opacity-100 top-12 ' : 'invisible opacity-0 top-10 '} border-gray-100 bg-white dark:bg-black dark:border-gray-800 transition-all duration-300 z-20 absolute block drop-shadow-lg `}>
{link?.subMenus?.map((sLink, index) => {
return (
<li
key={index}
className='not:last-child:border-b-0 border-b text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 dark:border-gray-800 py-3 pr-6 pl-3'>
<Link href={sLink.to} target={link?.target}>
<span className='text-xs font-extralight'>
{link?.icon && <i className={sLink?.icon}> &nbsp; </i>}
{sLink.title}
</span>
</Link>
</div>
}
{/* 子菜单 */}
{hasSubMenu && <ul className={`${show ? 'visible opacity-100 top-12 ' : 'invisible opacity-0 top-10 '} border-gray-100 bg-white dark:bg-black dark:border-gray-800 transition-all duration-300 z-20 absolute block drop-shadow-lg `}>
{link?.subMenus?.map((sLink, index) => {
return <li key={index} className='not:last-child:border-b-0 border-b text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 dark:border-gray-800 py-3 pr-6 pl-3'>
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
<span className='text-xs font-extralight'>{link?.icon && <i className={sLink?.icon} > &nbsp; </i>}{sLink.title}</span>
</Link>
</li>
})}
</ul>}
</li>
)
})}
</ul>
)}
</li>
)
}

View File

@@ -1,44 +1,63 @@
import Link from 'next/link'
import { useGlobal } from '@/lib/global'
import CONFIG from '../config'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import Link from 'next/link'
import CONFIG from '../config'
const MenuGroupCard = (props) => {
const MenuGroupCard = props => {
const { postCount, categoryOptions, tagOptions } = props
const { locale } = useGlobal()
const archiveSlot = <div className='text-center'>{postCount}</div>
const categorySlot = <div className='text-center'>{categoryOptions?.length}</div>
const categorySlot = (
<div className='text-center'>{categoryOptions?.length}</div>
)
const tagSlot = <div className='text-center'>{tagOptions?.length}</div>
const links = [
{ name: locale.COMMON.ARTICLE, to: '/archive', slot: archiveSlot, show: siteConfig('HEO_MENU_ARCHIVE', null, CONFIG) },
{ name: locale.COMMON.CATEGORY, to: '/category', slot: categorySlot, show: siteConfig('HEO_MENU_CATEGORY', null, CONFIG) },
{ name: locale.COMMON.TAGS, to: '/tag', slot: tagSlot, show: siteConfig('HEO_MENU_TAG', null, CONFIG) }
{
name: locale.COMMON.ARTICLE,
to: '/archive',
slot: archiveSlot,
show: siteConfig('HEO_MENU_ARCHIVE', null, CONFIG)
},
{
name: locale.COMMON.CATEGORY,
to: '/category',
slot: categorySlot,
show: siteConfig('HEO_MENU_CATEGORY', null, CONFIG)
},
{
name: locale.COMMON.TAGS,
to: '/tag',
slot: tagSlot,
show: siteConfig('HEO_MENU_TAG', null, CONFIG)
}
]
return (
<nav id='nav' className='dark:text-gray-200 w-full px-5'>
{links.map((link, index) => {
if (link.show) {
return (
<div key={index} className=''>
<Link title={link.to}
href={link.to}
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
className={'w-full flex items-center justify-between py-1 hover:scale-105 duration-200 transform dark:hover:text-indigo-400 hover:text-indigo-600 px-2 cursor-pointer'}>
<>
<div>{link.name} :</div>
<div className='font-semibold'>{link.slot}</div>
</>
</Link>
</div>
)
} else {
return null
}
})}
</nav>
<nav id='nav' className='dark:text-gray-200 w-full px-5'>
{links.map((link, index) => {
if (link.show) {
return (
<div key={index} className=''>
<Link
title={link.to}
href={link.to}
target={link?.target}
className={
'w-full flex items-center justify-between py-1 hover:scale-105 duration-200 transform dark:hover:text-indigo-400 hover:text-indigo-600 px-2 cursor-pointer'
}>
<>
<div>{link.name} :</div>
<div className='font-semibold'>{link.slot}</div>
</>
</Link>
</div>
)
} else {
return null
}
})}
</nav>
)
}
export default MenuGroupCard

View File

@@ -25,30 +25,55 @@ export const MenuItemCollapse = ({ link }) => {
return null
}
return <>
<div className='select-none w-full px-2 py-2 border rounded-xl text-left dark:bg-hexo-black-gray' 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">
<span className=' transition-all items-center duration-200'>{link?.icon && <i className={link.icon + ' mr-4'} />}{link?.name}</span>
</Link>}
{hasSubMenu && <div
onClick={hasSubMenu ? toggleOpenSubMenu : null}
className="font-extralight flex items-center justify-between pl-2 pr-4 cursor-pointer dark:text-gray-200 no-underline tracking-widest">
<span className='transition-all items-center duration-200'>{link?.icon && <i className={link.icon + ' mr-4'} />}{link?.name}</span>
<i className={`select-none px-2 fas fa-chevron-left transition-all duration-200 ${isOpen ? '-rotate-90' : ''} text-gray-400`}></i>
</div>}
</div>
return (
<>
<div
className='select-none w-full px-2 py-2 border rounded-xl text-left dark:bg-hexo-black-gray'
onClick={toggleShow}>
{!hasSubMenu && (
<Link
href={link?.to}
target={link?.target}
className='font-extralight flex justify-between pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest'>
<span className=' transition-all items-center duration-200'>
{link?.icon && <i className={link.icon + ' mr-4'} />}
{link?.name}
</span>
</Link>
)}
{hasSubMenu && (
<div
onClick={hasSubMenu ? toggleOpenSubMenu : null}
className='font-extralight flex items-center justify-between pl-2 pr-4 cursor-pointer dark:text-gray-200 no-underline tracking-widest'>
<span className='transition-all items-center duration-200'>
{link?.icon && <i className={link.icon + ' mr-4'} />}
{link?.name}
</span>
<i
className={`select-none px-2 fas fa-chevron-left transition-all duration-200 ${isOpen ? '-rotate-90' : ''} text-gray-400`}></i>
</div>
)}
</div>
{/* 折叠子菜单 */}
{hasSubMenu && <Collapse isOpen={isOpen} className='rounded-xl'>
{link.subMenus.map((sLink, index) => {
return <div key={index} className='dark:bg-black dark:text-gray-200 text-left px-3 justify-start bg-gray-50 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 py-3 pr-6'>
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
<span className='text-sm ml-4 whitespace-nowrap'>{link?.icon && <i className={sLink.icon + ' mr-2'} />} {sLink.title}</span>
</Link>
</div>
})}
</Collapse>}
{/* 折叠子菜单 */}
{hasSubMenu && (
<Collapse isOpen={isOpen} className='rounded-xl'>
{link.subMenus.map((sLink, index) => {
return (
<div
key={index}
className='dark:bg-black dark:text-gray-200 text-left px-3 justify-start bg-gray-50 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 py-3 pr-6'>
<Link href={sLink.to} target={link?.target}>
<span className='text-sm ml-4 whitespace-nowrap'>
{link?.icon && <i className={sLink.icon + ' mr-2'} />}{' '}
{sLink.title}
</span>
</Link>
</div>
)
})}
</Collapse>
)}
</>
)
}

View File

@@ -9,34 +9,50 @@ export const MenuItemDrop = ({ link }) => {
return null
}
return <div onMouseOver={() => changeShow(true)} onMouseOut={() => changeShow(false)} >
return (
<div
onMouseOver={() => changeShow(true)}
onMouseOut={() => changeShow(false)}>
{/* 不含子菜单 */}
{!hasSubMenu && (
<Link
target={link?.target}
href={link?.to}
className=' hover:bg-black hover:bg-opacity-10 rounded-2xl flex justify-center items-center px-3 py-1 no-underline tracking-widest'>
{link?.icon && <i className={link?.icon} />} {link?.name}
</Link>
)}
{/* 含子菜单 */}
{!hasSubMenu &&
<Link
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
href={link?.to}
className=" hover:bg-black hover:bg-opacity-10 rounded-2xl flex justify-center items-center px-3 py-1 no-underline tracking-widest">
{link?.icon && <i className={link?.icon} />} {link?.name}
</Link>}
{/* 含子菜单的按钮 */}
{hasSubMenu && <>
<div className='cursor-pointer hover:bg-black hover:bg-opacity-10 rounded-2xl flex justify-center items-center px-3 py-1 no-underline tracking-widest'>
{link?.icon && <i className={link?.icon} />} {link?.name}
</div>
</>}
{/* 子菜单 */}
{hasSubMenu && <ul style={{ backdropFilter: 'blur(3px)' }} className={`${show ? 'visible opacity-100 top-14' : 'invisible opacity-0 top-20'} drop-shadow-md overflow-hidden rounded-xl bg-white transition-all duration-300 z-20 absolute`}>
{link.subMenus.map((sLink, index) => {
return <li key={index} className='cursor-pointer hover:bg-blue-600 hover:text-white text-gray-900 tracking-widest transition-all duration-200 dark:border-gray-700 py-1 pr-6 pl-3'>
<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} > &nbsp; </i>}{sLink.title}</span>
</Link>
</li>
})}
</ul>}
{/* 含子菜单的按钮 */}
{hasSubMenu && (
<>
<div className='cursor-pointer hover:bg-black hover:bg-opacity-10 rounded-2xl flex justify-center items-center px-3 py-1 no-underline tracking-widest'>
{link?.icon && <i className={link?.icon} />} {link?.name}
</div>
</>
)}
{/* 子菜单 */}
{hasSubMenu && (
<ul
style={{ backdropFilter: 'blur(3px)' }}
className={`${show ? 'visible opacity-100 top-14' : 'invisible opacity-0 top-20'} drop-shadow-md overflow-hidden rounded-xl bg-white transition-all duration-300 z-20 absolute`}>
{link.subMenus.map((sLink, index) => {
return (
<li
key={index}
className='cursor-pointer hover:bg-blue-600 hover:text-white text-gray-900 tracking-widest transition-all duration-200 dark:border-gray-700 py-1 pr-6 pl-3'>
<Link href={sLink.to} target={link?.target}>
<span className='text-sm text-nowrap font-extralight'>
{link?.icon && <i className={sLink?.icon}> &nbsp; </i>}
{sLink.title}
</span>
</Link>
</li>
)
})}
</ul>
)}
</div>
)
}

View File

@@ -1,11 +1,11 @@
// import Image from 'next/image'
import { useEffect, useState } from 'react'
import Typed from 'typed.js'
import CONFIG from '../config'
import NavButtonGroup from './NavButtonGroup'
import { useGlobal } from '@/lib/global'
import LazyImage from '@/components/LazyImage'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import { loadExternalResource } from '@/lib/utils'
import { useEffect, useState } from 'react'
import CONFIG from '../config'
import NavButtonGroup from './NavButtonGroup'
let wrapperTop = 0
@@ -20,21 +20,29 @@ const Hero = props => {
const scrollToWrapper = () => {
window.scrollTo({ top: wrapperTop, behavior: 'smooth' })
}
const GREETING_WORDS = siteConfig('GREETING_WORDS').split(',')
useEffect(() => {
updateHeaderHeight()
if (!typed && window && document.getElementById('typed')) {
changeType(
new Typed('#typed', {
strings: GREETING_WORDS,
typeSpeed: 200,
backSpeed: 100,
backDelay: 400,
showCursor: true,
smartBackspace: true
})
)
loadExternalResource(
'https://cdn.jsdelivr.net/npm/typed.js@2.0.12',
'js'
).then(() => {
if (window.Typed) {
changeType(
new window.Typed('#typed', {
strings: GREETING_WORDS,
typeSpeed: 200,
backSpeed: 100,
backDelay: 400,
showCursor: true,
smartBackspace: true
})
)
}
})
}
window.addEventListener('resize', updateHeaderHeight)
@@ -51,33 +59,43 @@ const Hero = props => {
}
return (
<header
id="header" style={{ zIndex: 1 }}
className="w-full h-screen relative bg-black"
>
<header
id='header'
style={{ zIndex: 1 }}
className='w-full h-screen relative bg-black'>
<div className='text-white absolute bottom-0 flex flex-col h-full items-center justify-center w-full '>
{/* 站点标题 */}
<div className='font-black text-4xl md:text-5xl shadow-text'>
{siteConfig('TITLE')}
</div>
{/* 站点欢迎语 */}
<div className='mt-2 h-12 items-center text-center font-medium shadow-text text-lg'>
<span id='typed' />
</div>
<div className="text-white absolute bottom-0 flex flex-col h-full items-center justify-center w-full ">
{/* 站点标题 */}
<div className='font-black text-4xl md:text-5xl shadow-text'>{siteConfig('TITLE')}</div>
{/* 站点欢迎语 */}
<div className='mt-2 h-12 items-center text-center font-medium shadow-text text-lg'>
<span id='typed' />
</div>
{/* 首页导航大按钮 */}
{siteConfig('HEXO_HOME_NAV_BUTTONS', null, CONFIG) && (
<NavButtonGroup {...props} />
)}
{/* 首页导航大按钮 */}
{siteConfig('HEXO_HOME_NAV_BUTTONS', null, CONFIG) && <NavButtonGroup {...props} />}
{/* 滚动按钮 */}
<div
onClick={scrollToWrapper}
className='z-10 cursor-pointer w-full text-center py-4 text-3xl absolute bottom-10 text-white'>
<div className='opacity-70 animate-bounce text-xs'>
{siteConfig('HEXO_SHOW_START_READING', null, CONFIG) &&
locale.COMMON.START_READING}
</div>
<i className='opacity-70 animate-bounce fas fa-angle-down' />
</div>
</div>
{/* 滚动按钮 */}
<div onClick={scrollToWrapper} className="z-10 cursor-pointer w-full text-center py-4 text-3xl absolute bottom-10 text-white">
<div className="opacity-70 animate-bounce text-xs">{siteConfig('HEXO_SHOW_START_READING', null, CONFIG) && locale.COMMON.START_READING}</div>
<i className='opacity-70 animate-bounce fas fa-angle-down' />
</div>
</div>
<LazyImage id='header-cover' src={siteInfo?.pageCover}
className={`header-cover w-full h-screen object-cover object-center ${siteConfig('HEXO_HOME_NAV_BACKGROUND_IMG_FIXED', null, CONFIG) ? 'fixed' : ''}`} />
</header>
<LazyImage
id='header-cover'
src={siteInfo?.pageCover}
className={`header-cover w-full h-screen object-cover object-center ${siteConfig('HEXO_HOME_NAV_BACKGROUND_IMG_FIXED', null, CONFIG) ? 'fixed' : ''}`}
/>
</header>
)
}

View File

@@ -1,19 +1,36 @@
import Link from 'next/link'
import { useGlobal } from '@/lib/global'
import CONFIG from '../config'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import Link from 'next/link'
import CONFIG from '../config'
const MenuGroupCard = (props) => {
const MenuGroupCard = props => {
const { postCount, categoryOptions, tagOptions } = props
const { locale } = useGlobal()
const archiveSlot = <div className='text-center'>{postCount}</div>
const categorySlot = <div className='text-center'>{categoryOptions?.length}</div>
const categorySlot = (
<div className='text-center'>{categoryOptions?.length}</div>
)
const tagSlot = <div className='text-center'>{tagOptions?.length}</div>
const links = [
{ name: locale.COMMON.ARTICLE, to: '/archive', slot: archiveSlot, show: siteConfig('HEXO_MENU_ARCHIVE', null, CONFIG) },
{ name: locale.COMMON.CATEGORY, to: '/category', slot: categorySlot, show: siteConfig('HEXO_MENU_CATEGORY', null, CONFIG) },
{ name: locale.COMMON.TAGS, to: '/tag', slot: tagSlot, show: siteConfig('HEXO_MENU_TAG', null, CONFIG) }
{
name: locale.COMMON.ARTICLE,
to: '/archive',
slot: archiveSlot,
show: siteConfig('HEXO_MENU_ARCHIVE', null, CONFIG)
},
{
name: locale.COMMON.CATEGORY,
to: '/category',
slot: categorySlot,
show: siteConfig('HEXO_MENU_CATEGORY', null, CONFIG)
},
{
name: locale.COMMON.TAGS,
to: '/tag',
slot: tagSlot,
show: siteConfig('HEXO_MENU_TAG', null, CONFIG)
}
]
for (let i = 0; i < links.length; i++) {
@@ -23,29 +40,31 @@ const MenuGroupCard = (props) => {
}
return (
<nav id='nav' className='leading-8 flex justify-center dark:text-gray-200 w-full'>
{links.map(link => {
if (link.show) {
return (
<Link
key={`${link.to}`}
title={link.to}
href={link.to}
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
className={'py-1.5 my-1 px-2 duration-300 text-base justify-center items-center cursor-pointer'}>
<div className='w-full items-center justify-center hover:scale-105 duration-200 transform dark:hover:text-indigo-400 hover:text-indigo-600'>
<div className='text-center'>{link.name}</div>
<div className='text-center font-semibold'>{link.slot}</div>
</div>
</Link>
)
} else {
return null
}
})}
</nav>
<nav
id='nav'
className='leading-8 flex justify-center dark:text-gray-200 w-full'>
{links.map(link => {
if (link.show) {
return (
<Link
key={`${link.to}`}
title={link.to}
href={link.to}
target={link?.target}
className={
'py-1.5 my-1 px-2 duration-300 text-base justify-center items-center cursor-pointer'
}>
<div className='w-full items-center justify-center hover:scale-105 duration-200 transform dark:hover:text-indigo-400 hover:text-indigo-600'>
<div className='text-center'>{link.name}</div>
<div className='text-center font-semibold'>{link.slot}</div>
</div>
</Link>
)
} else {
return null
}
})}
</nav>
)
}
export default MenuGroupCard

View File

@@ -7,7 +7,7 @@ import { useState } from 'react'
* @param {*} param0
* @returns
*/
export const MenuItemCollapse = (props) => {
export const MenuItemCollapse = props => {
const { link } = props
const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0
@@ -26,30 +26,55 @@ export const MenuItemCollapse = (props) => {
return null
}
return <>
<div className='w-full px-8 py-3 text-left dark:bg-hexo-black-gray' 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=' transition-all items-center duration-200'>{link?.icon && <i className={link.icon + ' mr-4'} />}{link?.name}</span>
</Link>}
{hasSubMenu && <div
onClick={hasSubMenu ? toggleOpenSubMenu : null}
className="font-extralight flex items-center justify-between pl-2 pr-4 cursor-pointer dark:text-gray-200 no-underline tracking-widest pb-1">
<span className='transition-all items-center duration-200'>{link?.icon && <i className={link.icon + ' mr-4'} />}{link?.name}</span>
<i className={`px-2 fas fa-chevron-left transition-all duration-200 ${isOpen ? '-rotate-90' : ''} text-gray-400`}></i>
</div>}
</div>
return (
<>
<div
className='w-full px-8 py-3 text-left dark:bg-hexo-black-gray'
onClick={toggleShow}>
{!hasSubMenu && (
<Link
href={link?.to}
target={link?.target}
className='font-extralight flex justify-between pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest pb-1'>
<span className=' transition-all items-center duration-200'>
{link?.icon && <i className={link.icon + ' mr-4'} />}
{link?.name}
</span>
</Link>
)}
{hasSubMenu && (
<div
onClick={hasSubMenu ? toggleOpenSubMenu : null}
className='font-extralight flex items-center justify-between pl-2 pr-4 cursor-pointer dark:text-gray-200 no-underline tracking-widest pb-1'>
<span className='transition-all items-center duration-200'>
{link?.icon && <i className={link.icon + ' mr-4'} />}
{link?.name}
</span>
<i
className={`px-2 fas fa-chevron-left transition-all duration-200 ${isOpen ? '-rotate-90' : ''} text-gray-400`}></i>
</div>
)}
</div>
{/* 折叠子菜单 */}
{hasSubMenu && <Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
{link.subMenus.map((sLink, index) => {
return <div key={index} className='dark:bg-black dark:text-gray-200 text-left px-10 justify-start bg-gray-50 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 py-3 pr-6'>
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
<span className='text-sm ml-4 whitespace-nowrap'>{link?.icon && <i className={sLink.icon + ' mr-2'} />} {sLink.title}</span>
</Link>
</div>
})}
</Collapse>}
{/* 折叠子菜单 */}
{hasSubMenu && (
<Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
{link.subMenus.map((sLink, index) => {
return (
<div
key={index}
className='dark:bg-black dark:text-gray-200 text-left px-10 justify-start bg-gray-50 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 py-3 pr-6'>
<Link href={sLink.to} target={link?.target}>
<span className='text-sm ml-4 whitespace-nowrap'>
{link?.icon && <i className={sLink.icon + ' mr-2'} />}{' '}
{sLink.title}
</span>
</Link>
</div>
)
})}
</Collapse>
)}
</>
)
}

View File

@@ -9,33 +9,51 @@ export const MenuItemDrop = ({ link }) => {
return null
}
return <div onMouseOver={() => changeShow(true)} onMouseOut={() => changeShow(false)} >
return (
<div
onMouseOver={() => changeShow(true)}
onMouseOut={() => changeShow(false)}>
{!hasSubMenu && (
<Link
href={link?.to}
target={link?.target}
className=' menu-link pl-2 pr-4 no-underline tracking-widest pb-1'>
{link?.icon && <i className={link?.icon} />} {link?.name}
{hasSubMenu && <i className='px-2 fa fa-angle-down'></i>}
</Link>
)}
{!hasSubMenu &&
<Link
href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
className=" menu-link pl-2 pr-4 no-underline tracking-widest pb-1">
{link?.icon && <i className={link?.icon}/>} {link?.name}
{hasSubMenu && <i className='px-2 fa fa-angle-down'></i>}
</Link>}
{hasSubMenu && <>
<div className='cursor-pointer menu-link pl-2 pr-4 no-underline tracking-widest pb-1'>
{link?.icon && <i className={link?.icon}/>} {link?.name}
<i className={`px-2 fa fa-angle-down duration-300 ${show ? 'rotate-180' : 'rotate-0'}`}></i>
</div>
</>}
{/* 子菜单 */}
{hasSubMenu && <ul style={{ backdropFilter: 'blur(3px)' }} className={`${show ? 'visible opacity-100 top-12' : 'invisible opacity-0 top-20'} drop-shadow-md overflow-hidden rounded-md text-black dark:text-white bg-white dark:bg-black transition-all duration-300 z-20 absolute block `}>
{link.subMenus.map((sLink, index) => {
return <li key={index} className='cursor-pointer hover:bg-indigo-300 hover:text-black tracking-widest transition-all duration-200 dark:border-gray-800 py-1 pr-6 pl-3'>
<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} > &nbsp; </i>}{sLink.title}</span>
</Link>
</li>
})}
</ul>}
{hasSubMenu && (
<>
<div className='cursor-pointer menu-link pl-2 pr-4 no-underline tracking-widest pb-1'>
{link?.icon && <i className={link?.icon} />} {link?.name}
<i
className={`px-2 fa fa-angle-down duration-300 ${show ? 'rotate-180' : 'rotate-0'}`}></i>
</div>
</>
)}
{/* 子菜单 */}
{hasSubMenu && (
<ul
style={{ backdropFilter: 'blur(3px)' }}
className={`${show ? 'visible opacity-100 top-12' : 'invisible opacity-0 top-20'} drop-shadow-md overflow-hidden rounded-md text-black dark:text-white bg-white dark:bg-black transition-all duration-300 z-20 absolute block `}>
{link.subMenus.map((sLink, index) => {
return (
<li
key={index}
className='cursor-pointer hover:bg-indigo-300 hover:text-black tracking-widest transition-all duration-200 dark:border-gray-800 py-1 pr-6 pl-3'>
<Link href={sLink.to} target={link?.target}>
<span className='text-sm text-nowrap font-extralight'>
{link?.icon && <i className={sLink?.icon}> &nbsp; </i>}
{sLink.title}
</span>
</Link>
</li>
)
})}
</ul>
)}
</div>
)
}

View File

@@ -1,39 +1,42 @@
import CONFIG from './config'
import { createContext, useContext, useEffect, useRef } from 'react'
import Footer from './components/Footer'
import SideRight from './components/SideRight'
import TopNav from './components/TopNav'
import Comment from '@/components/Comment'
import replaceSearchResult from '@/components/Mark'
import NotionPage from '@/components/NotionPage'
import ShareBar from '@/components/ShareBar'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import { isBrowser } from '@/lib/utils'
import BlogPostListPage from './components/BlogPostListPage'
import BlogPostListScroll from './components/BlogPostListScroll'
import Hero from './components/Hero'
import { Transition } from '@headlessui/react'
import dynamic from 'next/dynamic'
import Link from 'next/link'
import { useRouter } from 'next/router'
import Card from './components/Card'
import RightFloatArea from './components/RightFloatArea'
import SearchNav from './components/SearchNav'
import BlogPostArchive from './components/BlogPostArchive'
import { ArticleLock } from './components/ArticleLock'
import PostHeader from './components/PostHeader'
import JumpToCommentButton from './components/JumpToCommentButton'
import TocDrawer from './components/TocDrawer'
import TocDrawerButton from './components/TocDrawerButton'
import Comment from '@/components/Comment'
import NotionPage from '@/components/NotionPage'
import { createContext, useContext, useEffect, useRef } from 'react'
import ArticleAdjacent from './components/ArticleAdjacent'
import ArticleCopyright from './components/ArticleCopyright'
import { ArticleLock } from './components/ArticleLock'
import ArticleRecommend from './components/ArticleRecommend'
import ShareBar from '@/components/ShareBar'
import TagItemMini from './components/TagItemMini'
import Link from 'next/link'
import BlogPostArchive from './components/BlogPostArchive'
import BlogPostListPage from './components/BlogPostListPage'
import BlogPostListScroll from './components/BlogPostListScroll'
import Card from './components/Card'
import Footer from './components/Footer'
import Hero from './components/Hero'
import JumpToCommentButton from './components/JumpToCommentButton'
import PostHeader from './components/PostHeader'
import RightFloatArea from './components/RightFloatArea'
import SearchNav from './components/SearchNav'
import SideRight from './components/SideRight'
import SlotBar from './components/SlotBar'
import { Transition } from '@headlessui/react'
import TagItemMini from './components/TagItemMini'
import TocDrawer from './components/TocDrawer'
import TocDrawerButton from './components/TocDrawerButton'
import TopNav from './components/TopNav'
import CONFIG from './config'
import { Style } from './style'
import replaceSearchResult from '@/components/Mark'
import { siteConfig } from '@/lib/config'
import dynamic from 'next/dynamic'
const AlgoliaSearchModal = dynamic(() => import('@/components/AlgoliaSearchModal'), { ssr: false })
const AlgoliaSearchModal = dynamic(
() => import('@/components/AlgoliaSearchModal'),
{ ssr: false }
)
// 主题全局状态
const ThemeGlobalHexo = createContext()
@@ -50,93 +53,110 @@ const LayoutBase = props => {
const { onLoading, fullWidth } = useGlobal()
const router = useRouter()
const headerSlot = post
? <PostHeader {...props} />
: (router.route === '/' && siteConfig('HEXO_HOME_BANNER_ENABLE', null, CONFIG)
? <Hero {...props} />
: null)
const headerSlot = post ? (
<PostHeader {...props} />
) : router.route === '/' &&
siteConfig('HEXO_HOME_BANNER_ENABLE', null, CONFIG) ? (
<Hero {...props} />
) : null
const drawerRight = useRef(null)
const tocRef = isBrowser ? document.getElementById('article-wrapper') : null
const floatSlot = <>
{post?.toc?.length > 1 && <div className="block lg:hidden">
<TocDrawerButton
onClick={() => {
drawerRight?.current?.handleSwitchVisible()
}}
/>
</div>}
<JumpToCommentButton />
const floatSlot = (
<>
{post?.toc?.length > 1 && (
<div className='block lg:hidden'>
<TocDrawerButton
onClick={() => {
drawerRight?.current?.handleSwitchVisible()
}}
/>
</div>
)}
{post && <JumpToCommentButton />}
</>
)
// Algolia搜索框
const searchModal = useRef(null)
return (
<ThemeGlobalHexo.Provider value={{ searchModal }}>
<div id='theme-hexo' className={`${siteConfig('FONT_STYLE')} dark:bg-black scroll-smooth`}>
<Style/>
<div
id='theme-hexo'
className={`${siteConfig('FONT_STYLE')} dark:bg-black scroll-smooth`}>
<Style />
{/* 顶部导航 */}
<TopNav {...props} />
{/* 顶部导航 */}
<TopNav {...props} />
{/* 顶部嵌入 */}
<Transition
{/* 顶部嵌入 */}
<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'
leaveTo='opacity-0 translate-y-16'
unmount={false}>
{headerSlot}
</Transition>
{/* 主区块 */}
<main
id='wrapper'
className={`${siteConfig('HEXO_HOME_BANNER_ENABLE', null, CONFIG) ? '' : 'pt-16'} bg-hexo-background-gray dark:bg-black w-full py-8 md:px-8 lg:px-24 min-h-screen relative`}>
<div
id='container-inner'
className={
(JSON.parse(siteConfig('LAYOUT_SIDEBAR_REVERSE'))
? 'flex-row-reverse'
: '') +
' w-full mx-auto lg:flex lg:space-x-4 justify-center relative z-10'
}>
<div
className={`${className || ''} w-full ${fullWidth ? '' : 'max-w-4xl'} h-full overflow-hidden`}>
<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"
leaveTo="opacity-0 translate-y-16"
unmount={false}
>
{headerSlot}
</Transition>
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}
{/* 主区块 */}
<main id="wrapper" className={`${siteConfig('HEXO_HOME_BANNER_ENABLE', null, CONFIG) ? '' : 'pt-16'} bg-hexo-background-gray dark:bg-black w-full py-8 md:px-8 lg:px-24 min-h-screen relative`}>
<div id="container-inner" className={(JSON.parse(siteConfig('LAYOUT_SIDEBAR_REVERSE')) ? 'flex-row-reverse' : '') + ' w-full mx-auto lg:flex lg:space-x-4 justify-center relative z-10'} >
<div className={`${className || ''} w-full ${fullWidth ? '' : 'max-w-4xl'} h-full overflow-hidden`}>
<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>
</div>
{/* 右侧栏 */}
<SideRight {...props} className={`space-y-4 lg:w-80 pt-4 ${post ? 'lg:pt-0' : 'lg:pt-4'}`} />
</div>
</main>
<div className='block lg:hidden'>
<TocDrawer post={post} cRef={drawerRight} targetRef={tocRef} />
{children}
</Transition>
</div>
{/* 悬浮菜单 */}
<RightFloatArea floatSlot={floatSlot} />
{/* 右侧栏 */}
<SideRight
{...props}
className={`space-y-4 lg:w-80 pt-4 ${post ? 'lg:pt-0' : 'lg:pt-4'}`}
/>
</div>
</main>
{/* 全文搜索 */}
<AlgoliaSearchModal cRef={searchModal} {...props}/>
{/* 页脚 */}
<Footer title={siteConfig('TITLE') } />
<div className='block lg:hidden'>
<TocDrawer post={post} cRef={drawerRight} targetRef={tocRef} />
</div>
{/* 悬浮菜单 */}
<RightFloatArea floatSlot={floatSlot} />
{/* 全文搜索 */}
<AlgoliaSearchModal cRef={searchModal} {...props} />
{/* 页脚 */}
<Footer title={siteConfig('TITLE')} />
</div>
</ThemeGlobalHexo.Provider>
)
}
@@ -147,7 +167,7 @@ const LayoutBase = props => {
* @param {*} props
* @returns
*/
const LayoutIndex = (props) => {
const LayoutIndex = props => {
return <LayoutPostList {...props} className='pt-8' />
}
@@ -156,11 +176,17 @@ const LayoutIndex = (props) => {
* @param {*} props
* @returns
*/
const LayoutPostList = (props) => {
return <div className='pt-8'>
<SlotBar {...props} />
{siteConfig('POST_LIST_STYLE') === 'page' ? <BlogPostListPage {...props} /> : <BlogPostListScroll {...props} />}
const LayoutPostList = props => {
return (
<div className='pt-8'>
<SlotBar {...props} />
{siteConfig('POST_LIST_STYLE') === 'page' ? (
<BlogPostListPage {...props} />
) : (
<BlogPostListScroll {...props} />
)}
</div>
)
}
/**
@@ -187,11 +213,20 @@ const LayoutSearch = props => {
})
return (
<div className='pt-8'>
{!currentSearch
? <SearchNav {...props} />
: <div id="posts-wrapper"> {siteConfig('POST_LIST_STYLE') === 'page' ? <BlogPostListPage {...props} /> : <BlogPostListScroll {...props} />} </div>}
<div className='pt-8'>
{!currentSearch ? (
<SearchNav {...props} />
) : (
<div id='posts-wrapper'>
{' '}
{siteConfig('POST_LIST_STYLE') === 'page' ? (
<BlogPostListPage {...props} />
) : (
<BlogPostListScroll {...props} />
)}{' '}
</div>
)}
</div>
)
}
@@ -200,21 +235,23 @@ const LayoutSearch = props => {
* @param {*} props
* @returns
*/
const LayoutArchive = (props) => {
const LayoutArchive = props => {
const { archivePosts } = props
return <div className='pt-8'>
<Card className='w-full'>
<div className="mb-10 pb-20 bg-white md:p-12 p-3 min-h-full dark:bg-hexo-black-gray">
{Object.keys(archivePosts).map(archiveTitle => (
<BlogPostArchive
key={archiveTitle}
posts={archivePosts[archiveTitle]}
archiveTitle={archiveTitle}
/>
))}
</div>
</Card>
return (
<div className='pt-8'>
<Card className='w-full'>
<div className='mb-10 pb-20 bg-white md:p-12 p-3 min-h-full dark:bg-hexo-black-gray'>
{Object.keys(archivePosts).map(archiveTitle => (
<BlogPostArchive
key={archiveTitle}
posts={archivePosts[archiveTitle]}
archiveTitle={archiveTitle}
/>
))}
</div>
</Card>
</div>
)
}
/**
@@ -228,51 +265,60 @@ const LayoutSlug = props => {
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 (
<>
<div className="w-full lg:hover:shadow lg:border rounded-t-xl lg:rounded-xl lg:px-2 lg:py-4 bg-white dark:bg-hexo-black-gray dark:border-black article">
{lock && <ArticleLock validPassword={validPassword} />}
<>
<div className='w-full lg:hover:shadow lg:border rounded-t-xl lg:rounded-xl lg:px-2 lg:py-4 bg-white dark:bg-hexo-black-gray dark:border-black article'>
{lock && <ArticleLock validPassword={validPassword} />}
{!lock && <div id="article-wrapper" className="overflow-x-auto flex-grow mx-auto md:w-full md:px-5 ">
{!lock && (
<div
id='article-wrapper'
className='overflow-x-auto flex-grow mx-auto md:w-full md:px-5 '>
<article
itemScope
itemType='https://schema.org/Movie'
className='subpixel-antialiased overflow-y-hidden'>
{/* Notion文章主体 */}
<section className='px-5 justify-center mx-auto max-w-2xl lg:max-w-full'>
{post && <NotionPage post={post} />}
</section>
<article itemScope itemType="https://schema.org/Movie" className="subpixel-antialiased overflow-y-hidden" >
{/* Notion文章主体 */}
<section className='px-5 justify-center mx-auto max-w-2xl lg:max-w-full'>
{post && <NotionPage post={post} />}
</section>
{/* 分享 */}
<ShareBar post={post} />
{post?.type === 'Post' && (
<>
<ArticleCopyright {...props} />
<ArticleRecommend {...props} />
<ArticleAdjacent {...props} />
</>
)}
</article>
{/* 分享 */}
<ShareBar post={post} />
{post?.type === 'Post' && <>
<ArticleCopyright {...props} />
<ArticleRecommend {...props} />
<ArticleAdjacent {...props} />
</>}
<div className='pt-4 border-dashed'></div>
</article>
<div className='pt-4 border-dashed'></div>
{/* 评论互动 */}
<div className="duration-200 overflow-x-auto bg-white dark:bg-hexo-black-gray px-3">
<Comment frontMatter={post} />
</div>
</div>}
{/* 评论互动 */}
<div className='duration-200 overflow-x-auto bg-white dark:bg-hexo-black-gray px-3'>
<Comment frontMatter={post} />
</div>
</>
</div>
)}
</div>
</>
)
}
@@ -297,18 +343,18 @@ const Layout404 = props => {
}, 3000)
})
return (
<>
<div className="text-black w-full h-screen text-center justify-center content-center items-center flex flex-col">
<div className="dark:text-gray-200">
<h2 className="inline-block border-r-2 border-gray-600 mr-2 px-3 py-2 align-top">
404
</h2>
<div className="inline-block text-left h-32 leading-10 items-center">
<h2 className="m-0 p-0">页面未找到</h2>
</div>
</div>
</div>
</>
<>
<div className='text-black w-full h-screen text-center justify-center content-center items-center flex flex-col'>
<div className='dark:text-gray-200'>
<h2 className='inline-block border-r-2 border-gray-600 mr-2 px-3 py-2 align-top'>
404
</h2>
<div className='inline-block text-left h-32 leading-10 items-center'>
<h2 className='m-0 p-0'>页面未找到</h2>
</div>
</div>
</div>
</>
)
}
@@ -321,24 +367,32 @@ const LayoutCategoryIndex = props => {
const { categoryOptions } = props
const { locale } = useGlobal()
return (
<div className='mt-8'>
<Card className="w-full min-h-screen">
<div className="dark:text-gray-200 mb-5 mx-3">
<i className="mr-4 fas fa-th" /> {locale.COMMON.CATEGORY}:
</div>
<div id="category-list" className="duration-200 flex flex-wrap mx-8">
{categoryOptions?.map(category => {
return (
<Link key={category.name} href={`/category/${category.name}`} passHref legacyBehavior>
<div className={' duration-300 dark:hover:text-white px-5 cursor-pointer py-2 hover:text-indigo-400'}>
<i className="mr-4 fas fa-folder" /> {category.name}({category.count})
</div>
</Link>
)
})}
</div>
</Card>
<div className='mt-8'>
<Card className='w-full min-h-screen'>
<div className='dark:text-gray-200 mb-5 mx-3'>
<i className='mr-4 fas fa-th' /> {locale.COMMON.CATEGORY}:
</div>
<div id='category-list' className='duration-200 flex flex-wrap mx-8'>
{categoryOptions?.map(category => {
return (
<Link
key={category.name}
href={`/category/${category.name}`}
passHref
legacyBehavior>
<div
className={
' duration-300 dark:hover:text-white px-5 cursor-pointer py-2 hover:text-indigo-400'
}>
<i className='mr-4 fas fa-folder' /> {category.name}(
{category.count})
</div>
</Link>
)
})}
</div>
</Card>
</div>
)
}
@@ -351,30 +405,32 @@ const LayoutTagIndex = props => {
const { tagOptions } = props
const { locale } = useGlobal()
return (
<div className='mt-8'>
<Card className='w-full'>
<div className="dark:text-gray-200 mb-5 ml-4">
<i className="mr-4 fas fa-tag" /> {locale.COMMON.TAGS}:
</div>
<div id="tags-list" className="duration-200 flex flex-wrap ml-8">
{tagOptions.map(tag => <div key={tag.name} className="p-2">
<TagItemMini key={tag.name} tag={tag} />
</div>)}
</div>
</Card>
<div className='mt-8'>
<Card className='w-full'>
<div className='dark:text-gray-200 mb-5 ml-4'>
<i className='mr-4 fas fa-tag' /> {locale.COMMON.TAGS}:
</div>
<div id='tags-list' className='duration-200 flex flex-wrap ml-8'>
{tagOptions.map(tag => (
<div key={tag.name} className='p-2'>
<TagItemMini key={tag.name} tag={tag} />
</div>
))}
</div>
</Card>
</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
}

View File

@@ -1,10 +1,10 @@
// import Image from 'next/image'
import { useEffect, useState } from 'react'
import Typed from 'typed.js'
import CONFIG from '../config'
import { useGlobal } from '@/lib/global'
import LazyImage from '@/components/LazyImage'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import { loadExternalResource } from '@/lib/utils'
import { useEffect, useState } from 'react'
import CONFIG from '../config'
let wrapperTop = 0
@@ -21,16 +21,23 @@ const Hero = props => {
useEffect(() => {
updateHeaderHeight()
if (!typed && window && document.getElementById('typed')) {
changeType(
new Typed('#typed', {
strings: GREETING_WORDS,
typeSpeed: 200,
backSpeed: 100,
backDelay: 400,
showCursor: true,
smartBackspace: true
})
)
loadExternalResource(
'https://cdn.jsdelivr.net/npm/typed.js@2.0.12',
'js'
).then(() => {
if (window.Typed) {
changeType(
new window.Typed('#typed', {
strings: GREETING_WORDS,
typeSpeed: 200,
backSpeed: 100,
backDelay: 400,
showCursor: true,
smartBackspace: true
})
)
}
})
}
window.addEventListener('resize', updateHeaderHeight)
@@ -47,29 +54,40 @@ const Hero = props => {
}
return (
<header
id="header" style={{ zIndex: 1 }}
className=" w-full h-screen relative bg-black"
>
<header
id='header'
style={{ zIndex: 1 }}
className=' w-full h-screen relative bg-black'>
<div className='text-white absolute flex flex-col h-full items-center justify-center w-full '>
{/* 站点标题 */}
<div className='text-4xl md:text-5xl shadow-text'>
{siteConfig('TITLE')}
</div>
{/* 站点欢迎语 */}
<div className='mt-2 h-12 items-center text-center shadow-text text-white text-lg'>
<span id='typed' />
</div>
{/* 滚动按钮 */}
<div
onClick={() => {
window.scrollTo({ top: wrapperTop, behavior: 'smooth' })
}}
className='mt-12 border cursor-pointer w-40 text-center pt-4 pb-3 text-md text-white hover:bg-orange-600 duration-300 rounded-3xl z-40'>
<i className='animate-bounce fas fa-angle-double-down' />{' '}
<span>
{siteConfig('MATERY_SHOW_START_READING', null, CONFIG) &&
locale.COMMON.START_READING}
</span>
</div>
</div>
<div className="text-white absolute flex flex-col h-full items-center justify-center w-full ">
{/* 站点标题 */}
<div className='text-4xl md:text-5xl shadow-text'>{siteConfig('TITLE')}</div>
{/* 站点欢迎语 */}
<div className='mt-2 h-12 items-center text-center shadow-text text-white text-lg'>
<span id='typed' />
</div>
{/* 滚动按钮 */}
<div onClick={() => { window.scrollTo({ top: wrapperTop, behavior: 'smooth' }) }}
className="mt-12 border cursor-pointer w-40 text-center pt-4 pb-3 text-md text-white hover:bg-orange-600 duration-300 rounded-3xl z-40">
<i className='animate-bounce fas fa-angle-double-down' /> <span>{siteConfig('MATERY_SHOW_START_READING', null, CONFIG) && locale.COMMON.START_READING}</span>
</div>
</div>
<LazyImage priority={true} id='header-cover'src={siteInfo?.pageCover}
className={`header-cover object-center w-full h-screen object-cover ${siteConfig('MATERY_HOME_NAV_BACKGROUND_IMG_FIXED', null, CONFIG) ? 'fixed' : ''}`} />
</header>
<LazyImage
priority={true}
id='header-cover'
src={siteInfo?.pageCover}
className={`header-cover object-center w-full h-screen object-cover ${siteConfig('MATERY_HOME_NAV_BACKGROUND_IMG_FIXED', null, CONFIG) ? 'fixed' : ''}`}
/>
</header>
)
}

View File

@@ -1,9 +1,9 @@
import Link from 'next/link'
import { useGlobal } from '@/lib/global'
import CONFIG from '../config'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import Link from 'next/link'
import CONFIG from '../config'
const MenuGroupCard = (props) => {
const MenuGroupCard = props => {
const { postCount, categories, tags } = props
const { locale } = useGlobal()
const archiveSlot = <div className='text-center'>{postCount}</div>
@@ -11,35 +11,50 @@ const MenuGroupCard = (props) => {
const tagSlot = <div className='text-center'>{tags?.length}</div>
const links = [
{ name: locale.COMMON.ARTICLE, to: '/archive', slot: archiveSlot, show: siteConfig('MATERY_MENU_ARCHIVE', null, CONFIG) },
{ name: locale.COMMON.CATEGORY, to: '/category', slot: categorySlot, show: siteConfig('MATERY_MENU_CATEGORY', null, CONFIG) },
{ name: locale.COMMON.TAGS, to: '/tag', slot: tagSlot, show: siteConfig('MATERY_MENU_TAG', null, CONFIG) }
{
name: locale.COMMON.ARTICLE,
to: '/archive',
slot: archiveSlot,
show: siteConfig('MATERY_MENU_ARCHIVE', null, CONFIG)
},
{
name: locale.COMMON.CATEGORY,
to: '/category',
slot: categorySlot,
show: siteConfig('MATERY_MENU_CATEGORY', null, CONFIG)
},
{
name: locale.COMMON.TAGS,
to: '/tag',
slot: tagSlot,
show: siteConfig('MATERY_MENU_TAG', null, CONFIG)
}
]
return (
<nav id='nav' className='leading-8 flex justify-center w-full'>
{links.map(link => {
if (link.show) {
return (
<Link
key={`${link.to}`}
title={link.to}
href={link.to}
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
className={'py-1.5 my-1 px-2 duration-300 text-base justify-center items-center cursor-pointer'}>
<div className='w-full items-center justify-center hover:scale-105 duration-200 transform dark:hover:text-indigo-400 hover:text-indigo-600'>
<div className='text-center'>{link.name}</div>
<div className='text-center font-semibold'>{link.slot}</div>
</div>
</Link>
)
} else {
return null
}
})}
</nav>
{links.map(link => {
if (link.show) {
return (
<Link
key={`${link.to}`}
title={link.to}
href={link.to}
target={link?.target}
className={
'py-1.5 my-1 px-2 duration-300 text-base justify-center items-center cursor-pointer'
}>
<div className='w-full items-center justify-center hover:scale-105 duration-200 transform dark:hover:text-indigo-400 hover:text-indigo-600'>
<div className='text-center'>{link.name}</div>
<div className='text-center font-semibold'>{link.slot}</div>
</div>
</Link>
)
} else {
return null
}
})}
</nav>
)
}
export default MenuGroupCard

View File

@@ -27,35 +27,63 @@ export const MenuItemCollapse = ({ link }) => {
return null
}
const selected = (router.pathname === link.to) || (router.asPath === link.to)
const selected = router.pathname === link.to || router.asPath === link.to
return <>
<div onClick={toggleShow} className={'py-2 px-5 duration-300 text-base justify-between hover:bg-indigo-700 hover:text-white hover:shadow-lg cursor-pointer font-light flex flex-nowrap items-center ' +
(selected ? 'bg-indigo-500 text-white ' : ' text-black dark:text-white ')}>
return (
<>
<div
onClick={toggleShow}
className={
'py-2 px-5 duration-300 text-base justify-between hover:bg-indigo-700 hover:text-white hover:shadow-lg cursor-pointer font-light flex flex-nowrap items-center ' +
(selected
? 'bg-indigo-500 text-white '
: ' text-black dark:text-white ')
}>
{!hasSubMenu && (
<Link href={link?.to} target={link?.target}>
<div className='my-auto items-center justify-between flex '>
{link.icon && (
<i className={`${link.icon} w-4 mr-6 text-center`} />
)}
<div>{link.name}</div>
</div>
{link.slot}
</Link>
)}
{!hasSubMenu && <Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
<div className='my-auto items-center justify-between flex '>
{link.icon && <i className={`${link.icon} w-4 mr-6 text-center`} />}
<div >{link.name}</div>
</div>
{link.slot}
</Link>}
{hasSubMenu && (
<div
onClick={hasSubMenu ? toggleOpenSubMenu : null}
className='my-auto items-center w-full justify-between flex '>
<div className=''>
<i className={`${link.icon} w-4 mr-6 text-center`} />
{link?.name}
</div>
<i
className={`px-2 fas fa-chevron-left transition-all duration-200 ${isOpen ? '-rotate-90' : ''}`}></i>
</div>
)}
</div>
{hasSubMenu && <div onClick={hasSubMenu ? toggleOpenSubMenu : null} className='my-auto items-center w-full justify-between flex '>
<div className=''><i className={`${link.icon} w-4 mr-6 text-center`} />{link?.name}</div>
<i className={`px-2 fas fa-chevron-left transition-all duration-200 ${isOpen ? '-rotate-90' : ''}`}></i>
</div>}
</div>
{/* 折叠子菜单 */}
{hasSubMenu && <Collapse isOpen={isOpen}>
{link.subMenus.map((sLink, index) => {
return <div key={index} className='cursor-pointer whitespace-nowrap dark:text-gray-200 w-full font-extralight dark:bg-black text-left px-5 justify-start bg-gray-100 hover:bg-indigo-700 hover:text-white 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-sm'><i className={`${sLink.icon} w-4 mr-3 text-center`} />{sLink.title}</span>
</Link>
</div>
})}
</Collapse>}
{/* 折叠子菜单 */}
{hasSubMenu && (
<Collapse isOpen={isOpen}>
{link.subMenus.map((sLink, index) => {
return (
<div
key={index}
className='cursor-pointer whitespace-nowrap dark:text-gray-200 w-full font-extralight dark:bg-black text-left px-5 justify-start bg-gray-100 hover:bg-indigo-700 hover:text-white 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?.target}>
<span className='text-sm'>
<i className={`${sLink.icon} w-4 mr-3 text-center`} />
{sLink.title}
</span>
</Link>
</div>
)
})}
</Collapse>
)}
</>
)
}

View File

@@ -9,33 +9,51 @@ export const MenuItemDrop = ({ link }) => {
return null
}
return <div onMouseOver={() => changeShow(true)} onMouseOut={() => changeShow(false)} >
return (
<div
onMouseOver={() => changeShow(true)}
onMouseOut={() => changeShow(false)}>
{!hasSubMenu && (
<Link
href={link?.to}
target={link?.target}
className=' menu-link pl-2 pr-4 no-underline tracking-widest pb-1'>
{link?.icon && <i className={link?.icon} />} {link?.name}
{hasSubMenu && <i className='px-2 fa fa-angle-down'></i>}
</Link>
)}
{!hasSubMenu &&
<Link
href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
className=" menu-link pl-2 pr-4 no-underline tracking-widest pb-1">
{link?.icon && <i className={link?.icon} />} {link?.name}
{hasSubMenu && <i className='px-2 fa fa-angle-down'></i>}
</Link>}
{hasSubMenu && <>
<div className='cursor-pointer menu-link pl-2 pr-4 no-underline tracking-widest pb-1'>
{link?.icon && <i className={link?.icon} />} {link?.name}
<i className={`px-2 fa fa-angle-down duration-300 ${show ? 'rotate-180' : 'rotate-0'}`}></i>
</div>
</>}
{/* 子菜单 */}
{hasSubMenu && <ul style={{ backdropFilter: 'blur(3px)' }} className={`${show ? 'visible opacity-100 top-12' : 'invisible opacity-0 top-20'} drop-shadow-md overflow-hidden rounded-md bg-white transition-all duration-300 z-20 absolute block `}>
{link.subMenus.map((sLink, index) => {
return <li key={index} className='cursor-pointer hover:bg-indigo-300 text-gray-900 hover:text-black tracking-widest transition-all duration-200 dark:border-gray-800 py-1 pr-6 pl-3'>
<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} > &nbsp; </i>}{sLink.title}</span>
</Link>
</li>
})}
</ul>}
{hasSubMenu && (
<>
<div className='cursor-pointer menu-link pl-2 pr-4 no-underline tracking-widest pb-1'>
{link?.icon && <i className={link?.icon} />} {link?.name}
<i
className={`px-2 fa fa-angle-down duration-300 ${show ? 'rotate-180' : 'rotate-0'}`}></i>
</div>
</>
)}
{/* 子菜单 */}
{hasSubMenu && (
<ul
style={{ backdropFilter: 'blur(3px)' }}
className={`${show ? 'visible opacity-100 top-12' : 'invisible opacity-0 top-20'} drop-shadow-md overflow-hidden rounded-md bg-white transition-all duration-300 z-20 absolute block `}>
{link.subMenus.map((sLink, index) => {
return (
<li
key={index}
className='cursor-pointer hover:bg-indigo-300 text-gray-900 hover:text-black tracking-widest transition-all duration-200 dark:border-gray-800 py-1 pr-6 pl-3'>
<Link href={sLink.to} target={link?.target}>
<span className='text-sm text-nowrap font-extralight'>
{link?.icon && <i className={sLink?.icon}> &nbsp; </i>}
{sLink.title}
</span>
</Link>
</li>
)
})}
</ul>
)}
</div>
)
}

View File

@@ -8,7 +8,7 @@ import { useState } from 'react'
* @param {*} param0
* @returns
*/
export const MenuItemCollapse = (props) => {
export const MenuItemCollapse = props => {
const { link } = props
const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0
@@ -21,7 +21,7 @@ export const MenuItemCollapse = (props) => {
return null
}
const selected = (router.pathname === link.to) || (router.asPath === link.to)
const selected = router.pathname === link.to || router.asPath === link.to
const toggleShow = () => {
changeShow(!show)
@@ -31,32 +31,67 @@ export const MenuItemCollapse = (props) => {
changeIsOpen(!isOpen)
}
return <>
<div className={ (selected ? 'bg-green-600 text-white hover:text-white' : 'hover:text-green-600') + ' px-5 w-full text-left duration-200 dark:bg-hexo-black-gray dark:border-black'} onClick={toggleShow} >
return (
<>
<div
className={
(selected
? 'bg-green-600 text-white hover:text-white'
: 'hover:text-green-600') +
' px-5 w-full text-left duration-200 dark:bg-hexo-black-gray dark:border-black'
}
onClick={toggleShow}>
{!hasSubMenu && (
<Link
href={link?.to}
target={link?.target}
className='py-2 w-full my-auto items-center justify-between flex '>
<div>
<div className={`${link.icon} text-center w-4 mr-4`} />
{link.name}
</div>
</Link>
)}
{!hasSubMenu && <Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} className='py-2 w-full my-auto items-center justify-between flex '>
<div><div className={`${link.icon} text-center w-4 mr-4`} />{link.name}</div>
</Link>}
{hasSubMenu && (
<div
onClick={hasSubMenu ? toggleOpenSubMenu : null}
className='py-2 font-extralight flex justify-between cursor-pointer dark:text-gray-200 no-underline tracking-widest'>
<div>
<div className={`${link.icon} text-center w-4 mr-4`} />
{link.name}
</div>
<div className='inline-flex items-center '>
<i
className={`px-2 fas fa-chevron-right transition-all duration-200 ${isOpen ? 'rotate-90' : ''}`}></i>
</div>
</div>
)}
</div>
{hasSubMenu && <div
onClick={hasSubMenu ? toggleOpenSubMenu : null}
className="py-2 font-extralight flex justify-between cursor-pointer dark:text-gray-200 no-underline tracking-widest">
<div><div className={`${link.icon} text-center w-4 mr-4`} />{link.name}</div>
<div className='inline-flex items-center '><i className={`px-2 fas fa-chevron-right transition-all duration-200 ${isOpen ? 'rotate-90' : ''}`}></i></div>
</div>}
</div>
{/* 折叠子菜单 */}
{hasSubMenu && <Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
{link?.subMenus?.map(sLink => {
return <div key={sLink.id} className='
{/* 折叠子菜单 */}
{hasSubMenu && (
<Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
{link?.subMenus?.map(sLink => {
return (
<div
key={sLink.id}
className='
not:last-child:border-b-0 border-b dark:border-gray-800 py-2 px-14 cursor-pointer hover:bg-gray-100 dark:text-gray-200
font-extralight dark:bg-black text-left justify-start text-gray-600 bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200'>
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
<div><div className={`${sLink.icon} text-center w-3 mr-3 text-xs`} />{sLink.title}</div>
</Link>
</div>
})}
</Collapse>}
<Link href={sLink.to} target={link?.target}>
<div>
<div
className={`${sLink.icon} text-center w-3 mr-3 text-xs`}
/>
{sLink.title}
</div>
</Link>
</div>
)
})}
</Collapse>
)}
</>
)
}

View File

@@ -1,6 +1,6 @@
import Link from 'next/link'
import { useState } from 'react'
import { useRouter } from 'next/router'
import { useState } from 'react'
export const MenuItemDrop = ({ link }) => {
const [show, changeShow] = useState(false)
@@ -12,39 +12,65 @@ export const MenuItemDrop = ({ link }) => {
return null
}
const hasSubMenu = link?.subMenus?.length > 0
const selected = (router.pathname === link.to) || (router.asPath === link.to)
const selected = router.pathname === link.to || router.asPath === link.to
return <li className='cursor-pointer list-none items-center flex mx-2' onMouseOver={() => changeShow(true)} onMouseOut={() => changeShow(false)} >
return (
<li
className='cursor-pointer list-none items-center flex mx-2'
onMouseOver={() => changeShow(true)}
onMouseOut={() => changeShow(false)}>
{hasSubMenu && (
<div
className={
'px-3 h-full whitespace-nowrap duration-300 text-sm justify-between dark:text-gray-300 cursor-pointer flex flex-nowrap items-center ' +
(selected
? 'bg-green-600 text-white hover:text-white'
: 'hover:text-green-600')
}>
<div>
{link?.icon && <i className={link?.icon} />} {link?.name}
{hasSubMenu && (
<i
className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>
)}
</div>
</div>
)}
{hasSubMenu &&
<div className={'px-3 h-full whitespace-nowrap duration-300 text-sm justify-between dark:text-gray-300 cursor-pointer flex flex-nowrap items-center ' +
(selected ? 'bg-green-600 text-white hover:text-white' : 'hover:text-green-600')}>
<div>
{link?.icon && <i className={link?.icon} />} {link?.name}
{hasSubMenu && <i className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>}
</div>
</div>
}
{!hasSubMenu && (
<div
className={
'px-3 h-full whitespace-nowrap duration-300 text-sm justify-between dark:text-gray-300 cursor-pointer flex flex-nowrap items-center ' +
(selected
? 'bg-green-600 text-white hover:text-white'
: 'hover:text-green-600')
}>
<Link href={link?.to} target={link?.target}>
{link?.icon && <i className={link?.icon} />} {link?.name}
</Link>
</div>
)}
{!hasSubMenu &&
<div className={'px-3 h-full whitespace-nowrap duration-300 text-sm justify-between dark:text-gray-300 cursor-pointer flex flex-nowrap items-center ' +
(selected ? 'bg-green-600 text-white hover:text-white' : 'hover:text-green-600')}>
<Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
{link?.icon && <i className={link?.icon} />} {link?.name}
{/* 子菜单 */}
{hasSubMenu && (
<ul
className={`${show ? 'visible opacity-100 top-12 ' : 'invisible opacity-0 top-10 '} border-gray-100 bg-white dark:bg-black dark:border-gray-800 transition-all duration-300 z-20 absolute block drop-shadow-lg `}>
{link?.subMenus?.map(sLink => {
return (
<li
key={sLink.id}
className='not:last-child:border-b-0 border-b text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 dark:border-gray-800 py-3 pr-6 pl-3'>
<Link href={sLink.to} target={link?.target}>
<span className='text-xs font-extralight'>
{link?.icon && <i className={sLink?.icon}> &nbsp; </i>}
{sLink.title}
</span>
</Link>
</div>
}
{/* 子菜单 */}
{hasSubMenu && <ul className={`${show ? 'visible opacity-100 top-12 ' : 'invisible opacity-0 top-10 '} border-gray-100 bg-white dark:bg-black dark:border-gray-800 transition-all duration-300 z-20 absolute block drop-shadow-lg `}>
{link?.subMenus?.map(sLink => {
return <li key={sLink.id} className='not:last-child:border-b-0 border-b text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 dark:border-gray-800 py-3 pr-6 pl-3'>
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
<span className='text-xs font-extralight'>{link?.icon && <i className={sLink?.icon} > &nbsp; </i>}{sLink.title}</span>
</Link>
</li>
})}
</ul>}
</li>
)
})}
</ul>
)}
</li>
)
}

View File

@@ -28,11 +28,13 @@ export const MenuItemCollapse = props => {
return (
<>
<div className='select-none w-full px-6 py-2 text-left ' onClick={toggleShow}>
<div
className='select-none w-full px-6 py-2 text-left '
onClick={toggleShow}>
{!hasSubMenu && (
<Link
href={link?.to}
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
target={link?.target}
className='flex justify-between pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest'>
<span className=' transition-all items-center duration-200'>
{link?.icon && <i className={link.icon + ' mr-4'} />}
@@ -56,15 +58,19 @@ export const MenuItemCollapse = props => {
{/* 折叠子菜单 */}
{hasSubMenu && (
<Collapse isOpen={isOpen} onHeightChange={props.onHeightChange} className='rounded-xl'>
<Collapse
isOpen={isOpen}
onHeightChange={props.onHeightChange}
className='rounded-xl'>
{link.subMenus.map((sLink, index) => {
return (
<div
key={index}
className='dark:text-gray-200 text-left px-3 justify-start tracking-widest transition-all duration-200 py-3 pr-6'>
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
<Link href={sLink.to} target={link?.target}>
<span className='text-sm ml-4 whitespace-nowrap'>
{link?.icon && <i className={sLink.icon + ' mr-2'} />} {sLink.title}
{link?.icon && <i className={sLink.icon + ' mr-2'} />}{' '}
{sLink.title}
</span>
</Link>
</div>

View File

@@ -12,26 +12,23 @@ export const MenuItemDrop = ({ link }) => {
return (
<div
onMouseOver={() => changeShow(true)}
onMouseOut={() => changeShow(false)}
>
onMouseOut={() => changeShow(false)}>
{!hasSubMenu && (
<Link
href={link?.to}
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
className="select-none menu-link pl-2 pr-4 no-underline tracking-widest pb-1"
>
target={link?.target}
className='select-none menu-link pl-2 pr-4 no-underline tracking-widest pb-1'>
{link?.icon && <i className={link?.icon} />} {link?.name}
{hasSubMenu && <i className="px-2 fa fa-angle-down"></i>}
{hasSubMenu && <i className='px-2 fa fa-angle-down'></i>}
</Link>
)}
{hasSubMenu && (
<>
<div className="cursor-pointer menu-link pl-2 pr-4 no-underline tracking-widest pb-1">
<div className='cursor-pointer menu-link pl-2 pr-4 no-underline tracking-widest pb-1'>
{link?.icon && <i className={link?.icon} />} {link?.name}
<i
className={`px-2 fa fa-angle-down duration-300 ${show ? 'rotate-180' : 'rotate-0'}`}
></i>
className={`px-2 fa fa-angle-down duration-300 ${show ? 'rotate-180' : 'rotate-0'}`}></i>
</div>
</>
)}
@@ -40,19 +37,14 @@ export const MenuItemDrop = ({ link }) => {
{hasSubMenu && (
<ul
style={{ backdropFilter: 'blur(3px)' }}
className={`${show ? 'visible opacity-100 top-14' : 'invisible opacity-0 top-20'} drop-shadow-md overflow-hidden rounded-md text-black dark:text-white bg-white dark:bg-black transition-all duration-300 z-30 absolute block `}
>
className={`${show ? 'visible opacity-100 top-14' : 'invisible opacity-0 top-20'} drop-shadow-md overflow-hidden rounded-md text-black dark:text-white bg-white dark:bg-black transition-all duration-300 z-30 absolute block `}>
{link.subMenus.map((sLink, index) => {
return (
<li
key={index}
className="cursor-pointer text-start dark:bg-hexo-black-gray dark:hover:bg-gray-300 hover:bg-gray-300 hover:text-black tracking-widest transition-all duration-200 dark:border-gray-800 py-1 pr-6 pl-3"
>
<Link
href={sLink.to}
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
>
<span className="text-sm">
className='cursor-pointer text-start dark:bg-hexo-black-gray dark:hover:bg-gray-300 hover:bg-gray-300 hover:text-black tracking-widest transition-all duration-200 dark:border-gray-800 py-1 pr-6 pl-3'>
<Link href={sLink.to} target={link?.target}>
<span className='text-sm'>
{link?.icon && <i className={sLink?.icon}> &nbsp; </i>}
{sLink.title}
</span>

View File

@@ -8,7 +8,7 @@ import { useState } from 'react'
* @param {*} param0
* @returns
*/
export const MenuItemCollapse = (props) => {
export const MenuItemCollapse = props => {
const { link } = props
const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0
@@ -21,7 +21,7 @@ export const MenuItemCollapse = (props) => {
return null
}
const selected = (router.pathname === link.to) || (router.asPath === link.to)
const selected = router.pathname === link.to || router.asPath === link.to
const toggleShow = () => {
changeShow(!show)
@@ -31,32 +31,67 @@ export const MenuItemCollapse = (props) => {
changeIsOpen(!isOpen)
}
return <>
<div className={ 'text-black' + (selected ? 'text-white hover:text-white' : 'hover:text-gray-600') + ' px-7 w-full text-left duration-200 dark:border-black'} onClick={toggleShow} >
return (
<>
<div
className={
'text-black' +
(selected ? 'text-white hover:text-white' : 'hover:text-gray-600') +
' px-7 w-full text-left duration-200 dark:border-black'
}
onClick={toggleShow}>
{!hasSubMenu && (
<Link
href={link?.to}
target={link?.target}
className='py-2 w-full my-auto items-center justify-between flex '>
<div>
<div className={`${link.icon} text-center w-4 mr-4`} />
{link.name}
</div>
</Link>
)}
{!hasSubMenu && <Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} className='py-2 w-full my-auto items-center justify-between flex '>
<div><div className={`${link.icon} text-center w-4 mr-4`} />{link.name}</div>
</Link>}
{hasSubMenu && (
<div
onClick={hasSubMenu ? toggleOpenSubMenu : null}
className='py-2 flex justify-between cursor-pointer dark:text-gray-400 dark:hover:text-white font-bold no-underline tracking-widest'>
<div>
<div className={`${link.icon} text-center w-4 mr-2`} />
{link.name}
</div>
<div className='inline-flex items-center '>
<i
className={`px-2 fas fa-chevron-right transition-all duration-200 ${isOpen ? 'rotate-90' : ''}`}></i>
</div>
</div>
)}
</div>
{hasSubMenu && <div onClick={hasSubMenu ? toggleOpenSubMenu : null}
className="py-2 flex justify-between cursor-pointer dark:text-gray-400 dark:hover:text-white font-bold no-underline tracking-widest">
<div><div className={`${link.icon} text-center w-4 mr-2`} />{link.name}</div>
<div className='inline-flex items-center '><i className={`px-2 fas fa-chevron-right transition-all duration-200 ${isOpen ? 'rotate-90' : ''}`}></i></div>
</div>}
</div>
{/* 折叠子菜单 */}
{hasSubMenu && <Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
{link?.subMenus?.map((sLink, index) => {
return <div key={index} className='
{/* 折叠子菜单 */}
{hasSubMenu && (
<Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
{link?.subMenus?.map((sLink, index) => {
return (
<div
key={index}
className='
py-2 px-14 cursor-pointer hover:bg-gray-100 dark:text-gray-400 dark:hover:text-white font-bold
dark:bg-black text-left justify-start text-gray-600 bg-gray-50 bg-opacity-20 dark:hover:bg-gray-600 tracking-widest transition-all duration-200'>
{/* <Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}> */}
<a href={`/#${sLink.title}`} target={'_self'}>
<div><div className={`${sLink?.icon ? sLink?.icon : 'fas fa-hashtag'} text-center w-3 mr-2 text-xs`} />{sLink.title}</div>
</a>
</div>
})}
</Collapse>}
{/* <Link href={sLink.to} target={link?.target}> */}
<a href={`/#${sLink.title}`} target={'_self'}>
<div>
<div
className={`${sLink?.icon ? sLink?.icon : 'fas fa-hashtag'} text-center w-3 mr-2 text-xs`}
/>
{sLink.title}
</div>
</a>
</div>
)
})}
</Collapse>
)}
</>
)
}

View File

@@ -1,6 +1,6 @@
import Link from 'next/link'
import { useState } from 'react'
import { useRouter } from 'next/router'
import { useState } from 'react'
export const MenuItemDrop = ({ link }) => {
const [show, changeShow] = useState(false)
@@ -12,39 +12,65 @@ export const MenuItemDrop = ({ link }) => {
return null
}
const hasSubMenu = link?.subMenus?.length > 0
const selected = (router.pathname === link.to) || (router.asPath === link.to)
const selected = router.pathname === link.to || router.asPath === link.to
return <li className='cursor-pointer list-none items-center flex mx-2' onMouseOver={() => changeShow(true)} onMouseOut={() => changeShow(false)} >
return (
<li
className='cursor-pointer list-none items-center flex mx-2'
onMouseOver={() => changeShow(true)}
onMouseOut={() => changeShow(false)}>
{hasSubMenu && (
<div
className={
'px-2 h-full whitespace-nowrap duration-300 text-sm justify-between dark:text-gray-300 cursor-pointer flex flex-nowrap items-center ' +
(selected
? 'bg-green-600 text-white hover:text-white'
: 'hover:text-green-600')
}>
<div>
{link?.icon && <i className={link?.icon} />} {link?.name}
{hasSubMenu && (
<i
className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>
)}
</div>
</div>
)}
{hasSubMenu &&
<div className={'px-2 h-full whitespace-nowrap duration-300 text-sm justify-between dark:text-gray-300 cursor-pointer flex flex-nowrap items-center ' +
(selected ? 'bg-green-600 text-white hover:text-white' : 'hover:text-green-600')}>
<div>
{link?.icon && <i className={link?.icon} />} {link?.name}
{hasSubMenu && <i className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>}
</div>
</div>
}
{!hasSubMenu && (
<div
className={
'px-2 h-full whitespace-nowrap duration-300 text-sm justify-between dark:text-gray-300 cursor-pointer flex flex-nowrap items-center ' +
(selected
? 'bg-green-600 text-white hover:text-white'
: 'hover:text-green-600')
}>
<Link href={link?.to} target={link?.target}>
{link?.icon && <i className={link?.icon} />} {link?.name}
</Link>
</div>
)}
{!hasSubMenu &&
<div className={'px-2 h-full whitespace-nowrap duration-300 text-sm justify-between dark:text-gray-300 cursor-pointer flex flex-nowrap items-center ' +
(selected ? 'bg-green-600 text-white hover:text-white' : 'hover:text-green-600')}>
<Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
{link?.icon && <i className={link?.icon} />} {link?.name}
{/* 子菜单 */}
{hasSubMenu && (
<ul
className={`${show ? 'visible opacity-100 top-12 ' : 'invisible opacity-0 top-10 '} border-gray-100 bg-white dark:bg-black dark:border-gray-800 transition-all duration-300 z-20 absolute block drop-shadow-lg `}>
{link?.subMenus?.map((sLink, index) => {
return (
<li
key={index}
className='not:last-child:border-b-0 border-b text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 dark:border-gray-800 py-3 pr-6 pl-3'>
<Link href={sLink.to} target={link?.target}>
<span className='text-xs font-extralight'>
{link?.icon && <i className={sLink?.icon}> &nbsp; </i>}
{sLink.title}
</span>
</Link>
</div>
}
{/* 子菜单 */}
{hasSubMenu && <ul className={`${show ? 'visible opacity-100 top-12 ' : 'invisible opacity-0 top-10 '} border-gray-100 bg-white dark:bg-black dark:border-gray-800 transition-all duration-300 z-20 absolute block drop-shadow-lg `}>
{link?.subMenus?.map((sLink, index) => {
return <li key={index} className='not:last-child:border-b-0 border-b text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 dark:border-gray-800 py-3 pr-6 pl-3'>
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
<span className='text-xs font-extralight'>{link?.icon && <i className={sLink?.icon} > &nbsp; </i>}{sLink.title}</span>
</Link>
</li>
})}
</ul>}
</li>
)
})}
</ul>
)}
</li>
)
}

View File

@@ -7,7 +7,7 @@ import { useState } from 'react'
* @param {*} param0
* @returns
*/
export const MenuItemCollapse = (props) => {
export const MenuItemCollapse = props => {
const { link } = props
const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0
@@ -22,32 +22,64 @@ export const MenuItemCollapse = (props) => {
changeIsOpen(!isOpen)
}
return <>
<div className='px-5 py-2 w-full text-left duration-200 hover:bg-gray-700 hover:text-white not:last-child:border-b-0 border-b dark:bg-hexo-black-gray dark:border-black' onClick={toggleShow} >
{!hasSubMenu && <Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
className='w-full my-auto items-center justify-between flex dark:text-gray-200 '>
<div><div className={`${link.icon} text-center w-4 mr-4`} />{link.name}</div>
</Link>}
return (
<>
<div
className='px-5 py-2 w-full text-left duration-200 hover:bg-gray-700 hover:text-white not:last-child:border-b-0 border-b dark:bg-hexo-black-gray dark:border-black'
onClick={toggleShow}>
{!hasSubMenu && (
<Link
href={link?.to}
target={link?.target}
className='w-full my-auto items-center justify-between flex dark:text-gray-200 '>
<div>
<div className={`${link.icon} text-center w-4 mr-4`} />
{link.name}
</div>
</Link>
)}
{hasSubMenu && <div
onClick={hasSubMenu ? toggleOpenSubMenu : null}
className="font-extralight flex justify-between cursor-pointer dark:text-gray-200 no-underline tracking-widest">
<div><div className={`${link.icon} text-center w-4 mr-4`} />{link.name}</div>
<div className='inline-flex items-center '><i className={`px-2 fas fa-chevron-right transition-all duration-200 ${isOpen ? 'rotate-90' : ''}`}></i></div>
</div>}
</div>
{hasSubMenu && (
<div
onClick={hasSubMenu ? toggleOpenSubMenu : null}
className='font-extralight flex justify-between cursor-pointer dark:text-gray-200 no-underline tracking-widest'>
<div>
<div className={`${link.icon} text-center w-4 mr-4`} />
{link.name}
</div>
<div className='inline-flex items-center '>
<i
className={`px-2 fas fa-chevron-right transition-all duration-200 ${isOpen ? 'rotate-90' : ''}`}></i>
</div>
</div>
)}
</div>
{/* 折叠子菜单 */}
{hasSubMenu && <Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
{link.subMenus.map((sLink, index) => {
return <div key={index} className='whitespace-nowrap dark:text-gray-200
{/* 折叠子菜单 */}
{hasSubMenu && (
<Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
{link.subMenus.map((sLink, index) => {
return (
<div
key={index}
className='whitespace-nowrap dark:text-gray-200
not:last-child:border-b-0 border-b dark:border-gray-800 py-2 px-14 cursor-pointer hover:bg-gray-100
font-extralight dark:bg-black text-left justify-start text-gray-600 bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200'>
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
<div>{sLink.icon && <div className={`${sLink.icon} text-center w-3 mr-3 text-xs`} />}{sLink.title}</div>
</Link>
</div>
})}
</Collapse>}
<Link href={sLink.to} target={link?.target}>
<div>
{sLink.icon && (
<div
className={`${sLink.icon} text-center w-3 mr-3 text-xs`}
/>
)}
{sLink.title}
</div>
</Link>
</div>
)
})}
</Collapse>
)}
</>
)
}

View File

@@ -5,34 +5,62 @@ export const MenuItemDrop = ({ link }) => {
const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0
return <li onMouseOver={() => changeShow(true)} onMouseOut={() => changeShow(false)}
className='relative py-1.5 px-5 duration-300 text-base justify-between hover:bg-gray-700 hover:text-white hover:shadow-lg cursor-pointer font-light flex flex-nowrap items-center '>
return (
<li
onMouseOver={() => changeShow(true)}
onMouseOut={() => changeShow(false)}
className='relative py-1.5 px-5 duration-300 text-base justify-between hover:bg-gray-700 hover:text-white hover:shadow-lg cursor-pointer font-light flex flex-nowrap items-center '>
{!hasSubMenu && (
<Link
href={link?.to}
target={link?.target}
className='w-full my-auto items-center justify-between flex '>
<div>
<div className={`${link.icon} text-center w-4 mr-4`} />
{link.name}
</div>
{link.slot}
</Link>
)}
{!hasSubMenu &&
<Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} className='w-full my-auto items-center justify-between flex ' >
<div><div className={`${link.icon} text-center w-4 mr-4`} />{link.name}</div>
{link.slot}
</Link>}
{hasSubMenu &&
<div className='w-full my-auto items-center justify-between flex '>
<div><div className={`${link.icon} text-center w-4 mr-4`} />{link.name}</div>
{link.slot}
{hasSubMenu && <div className='text-right'><i className={`px-2 fas fa-chevron-right duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i></div>}
</div>}
{/* 子菜单 */}
{hasSubMenu && <ul className={`${show ? 'visible opacity-100 left-56' : 'invisible opacity-0 left-40'} whitespace-nowrap absolute right-0 top-0 w-full border-gray-100 bg-white dark:bg-black dark:border-gray-800 transition-all duration-300 drop-shadow-lg `}>
{link?.subMenus?.map(sLink => {
return <li key={sLink.id} >
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} className='my-auto h-9 px-2 items-center justify-start flex not:last-child:border-b-0 border-b text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 dark:border-gray-800 '>
{sLink.icon && <i className={`${sLink.icon} w-4 text-center`} />}
<div className={'ml-4'}>{sLink.name}</div>
{sLink.slot}
</Link>
</li>
})}
</ul>}
{hasSubMenu && (
<div className='w-full my-auto items-center justify-between flex '>
<div>
<div className={`${link.icon} text-center w-4 mr-4`} />
{link.name}
</div>
{link.slot}
{hasSubMenu && (
<div className='text-right'>
<i
className={`px-2 fas fa-chevron-right duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>
</div>
)}
</div>
)}
{/* 子菜单 */}
{hasSubMenu && (
<ul
className={`${show ? 'visible opacity-100 left-56' : 'invisible opacity-0 left-40'} whitespace-nowrap absolute right-0 top-0 w-full border-gray-100 bg-white dark:bg-black dark:border-gray-800 transition-all duration-300 drop-shadow-lg `}>
{link?.subMenus?.map(sLink => {
return (
<li key={sLink.id}>
<Link
href={sLink.to}
target={link?.target}
className='my-auto h-9 px-2 items-center justify-start flex not:last-child:border-b-0 border-b text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 dark:border-gray-800 '>
{sLink.icon && (
<i className={`${sLink.icon} w-4 text-center`} />
)}
<div className={'ml-4'}>{sLink.name}</div>
{sLink.slot}
</Link>
</li>
)
})}
</ul>
)}
</li>
)
}

View File

@@ -7,7 +7,7 @@ import { useState } from 'react'
* @param {*} param0
* @returns
*/
export const MenuItemCollapse = (props) => {
export const MenuItemCollapse = props => {
const { link } = props
const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0
@@ -26,30 +26,59 @@ export const MenuItemCollapse = (props) => {
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>
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?.target}
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>}
{/* 折叠子菜单 */}
{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?.target}>
<span className='text-xs'>{sLink.title}</span>
</Link>
</div>
)
})}
</Collapse>
)}
</>
)
}

View File

@@ -11,35 +11,49 @@ export const MenuItemDrop = ({ link }) => {
const hasSubMenu = link?.subMenus?.length > 0
return <li className='mx-3 my-2' >
<div className='cursor-pointer ' onMouseOver={() => changeShow(true)} onMouseOut={() => changeShow(false)}>
{!hasSubMenu &&
<div className="block text-black dark:text-gray-50 nav" >
<Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} >
{link?.icon && <i className={link?.icon} />} {link?.name}
</Link>
return (
<li className='mx-3 my-2'>
<div
className='cursor-pointer '
onMouseOver={() => changeShow(true)}
onMouseOut={() => changeShow(false)}>
{!hasSubMenu && (
<div className='block text-black dark:text-gray-50 nav'>
<Link href={link?.to} target={link?.target}>
{link?.icon && <i className={link?.icon} />} {link?.name}
</Link>
</div>
)}
{hasSubMenu && (
<div className='block text-black dark:text-gray-50 nav'>
{link?.icon && <i className={link?.icon} />} {link?.name}
<i
className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>
</div>
)}
{/* 子菜单 */}
{hasSubMenu && (
<ul
className={`${show ? 'visible opacity-100 top-12 ' : 'invisible opacity-0 top-10 '} border-gray-100 bg-white dark:bg-black dark:border-gray-800 transition-all duration-300 z-20 absolute block drop-shadow-lg `}>
{link.subMenus.map((sLink, index) => {
return (
<div
key={index}
className='not:last-child:border-b-0 border-b text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 dark:border-gray-800 py-3 pr-6 pl-3'>
<Link href={sLink.to} target={link?.target}>
<span className='text-sm text-nowrap font-extralight'>
{link?.icon && <i className={sLink?.icon}> &nbsp; </i>}
{sLink.title}
</span>
</Link>
</div>
}
{hasSubMenu &&
<div className='block text-black dark:text-gray-50 nav'>
{link?.icon && <i className={link?.icon} />} {link?.name}
<i className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>
</div>
}
{/* 子菜单 */}
{hasSubMenu && <ul className={`${show ? 'visible opacity-100 top-12 ' : 'invisible opacity-0 top-10 '} border-gray-100 bg-white dark:bg-black dark:border-gray-800 transition-all duration-300 z-20 absolute block drop-shadow-lg `}>
{link.subMenus.map((sLink, index) => {
return <div key={index} className='not:last-child:border-b-0 border-b text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 dark:border-gray-800 py-3 pr-6 pl-3'>
<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} > &nbsp; </i>}{sLink.title}</span>
</Link>
</div>
})}
</ul>}
</div>
)
})}
</ul>
)}
</div>
</li>
)
}

View File

@@ -7,7 +7,7 @@ import { useState } from 'react'
* @param {*} param0
* @returns
*/
export const MenuItemCollapse = (props) => {
export const MenuItemCollapse = props => {
const { link } = props
const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0
@@ -26,30 +26,59 @@ export const MenuItemCollapse = (props) => {
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>
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?.target}
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>}
{/* 折叠子菜单 */}
{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?.target}>
<span className='text-xs'>{sLink.title}</span>
</Link>
</div>
)
})}
</Collapse>
)}
</>
)
}

View File

@@ -9,32 +9,47 @@ export const MenuItemDrop = ({ link }) => {
const hasSubMenu = link?.subMenus?.length > 0
return <li className='cursor-pointer py-2 px-3' onMouseEnter={() => changeShow(true)} onMouseLeave={() => changeShow(false)}>
{!hasSubMenu &&
<div className="block text-black dark:text-gray-50 nav" >
<Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} >
{link?.icon && <i className={link?.icon} />} {link?.name}
</Link>
</div>
}
return (
<li
className='cursor-pointer py-2 px-3'
onMouseEnter={() => changeShow(true)}
onMouseLeave={() => changeShow(false)}>
{!hasSubMenu && (
<div className='block text-black dark:text-gray-50 nav'>
<Link href={link?.to} target={link?.target}>
{link?.icon && <i className={link?.icon} />} {link?.name}
</Link>
</div>
)}
{hasSubMenu &&
<div className='block text-black dark:text-gray-50 nav'>
{link?.icon && <i className={link?.icon} />} {link?.name}
<i className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>
</div>
}
{hasSubMenu && (
<div className='block text-black dark:text-gray-50 nav'>
{link?.icon && <i className={link?.icon} />} {link?.name}
<i
className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>
</div>
)}
{/* 子菜单 */}
{hasSubMenu && <ul className={`${show ? 'visible opacity-100 bottom-16 ' : 'invisible opacity-0 bottom-14'} border-gray-100 bg-white rounded-lg overflow-hidden dark:bg-black dark:border-gray-800 bg-opacity-60 transition-all duration-300 z-20 absolute block drop-shadow-lg `}>
{link.subMenus.map((sLink, index) => {
return <li key={index} className='text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 dark:border-gray-800 py-3 pr-6 pl-3'>
<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} > &nbsp; </i>}{sLink.title}</span>
</Link>
</li>
})}
</ul>}
</li>
{/* 子菜单 */}
{hasSubMenu && (
<ul
className={`${show ? 'visible opacity-100 bottom-16 ' : 'invisible opacity-0 bottom-14'} border-gray-100 bg-white rounded-lg overflow-hidden dark:bg-black dark:border-gray-800 bg-opacity-60 transition-all duration-300 z-20 absolute block drop-shadow-lg `}>
{link.subMenus.map((sLink, index) => {
return (
<li
key={index}
className='text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 dark:border-gray-800 py-3 pr-6 pl-3'>
<Link href={sLink.to} target={link?.target}>
<span className='text-sm text-nowrap font-extralight'>
{link?.icon && <i className={sLink?.icon}> &nbsp; </i>}
{sLink.title}
</span>
</Link>
</li>
)
})}
</ul>
)}
</li>
)
}

View File

@@ -7,7 +7,7 @@ import { useState } from 'react'
* @param {*} param0
* @returns
*/
export const MenuItemCollapse = (props) => {
export const MenuItemCollapse = props => {
const { link } = props
const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0
@@ -26,30 +26,67 @@ export const MenuItemCollapse = (props) => {
return null
}
return <>
<div className='w-full px-8 py-3 text-left border-b dark:bg-hexo-black-gray dark:border-black' onClick={toggleShow} >
{!hasSubMenu && <Link
href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
className="items-center flex justify-between pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest pb-1">
<span className='text-blue-600 dark:text-blue-300 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="items-center flex justify-between pl-2 pr-4 cursor-pointer dark:text-gray-200 no-underline tracking-widest pb-1">
<span className='text-blue-600 dark:text-blue-300 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 transition-all duration-200 ${isOpen && 'rotate-45'} text-gray-400`}></i>
</div>}
</div>
return (
<>
<div
className='w-full px-8 py-3 text-left border-b dark:bg-hexo-black-gray dark:border-black'
onClick={toggleShow}>
{!hasSubMenu && (
<Link
href={link?.to}
target={link?.target}
className='items-center flex justify-between pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest pb-1'>
<span className='text-blue-600 dark:text-blue-300 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='items-center flex justify-between pl-2 pr-4 cursor-pointer dark:text-gray-200 no-underline tracking-widest pb-1'>
<span className='text-blue-600 dark:text-blue-300 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 transition-all duration-200 ${isOpen && 'rotate-45'} text-gray-400`}></i>
</div>
)}
</div>
{/* 折叠子菜单 */}
{hasSubMenu && <Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
{link.subMenus.map((sLink, index) => {
return <div key={index} className='dark:bg-black text-left px-10 justify-start text-blue-600 dark:text-blue-300 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='ml-4 text-sm'>{sLink?.icon && <span className='mr-2 w-4'><i className={sLink.icon}/></span>}{sLink.title}</span>
</Link>
</div>
})}
</Collapse>}
{/* 折叠子菜单 */}
{hasSubMenu && (
<Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
{link.subMenus.map((sLink, index) => {
return (
<div
key={index}
className='dark:bg-black text-left px-10 justify-start text-blue-600 dark:text-blue-300 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?.target}>
<span className='ml-4 text-sm'>
{sLink?.icon && (
<span className='mr-2 w-4'>
<i className={sLink.icon} />
</span>
)}
{sLink.title}
</span>
</Link>
</div>
)
})}
</Collapse>
)}
</>
)
}

View File

@@ -9,33 +9,60 @@ export const MenuItemDrop = ({ link }) => {
return null
}
return <div onMouseOver={() => changeShow(true)} onMouseOut={() => changeShow(false)} >
return (
<div
onMouseOver={() => changeShow(true)}
onMouseOut={() => changeShow(false)}>
{!hasSubMenu && (
<Link
href={link?.to}
target={link?.target}
className=' menu-link pl-2 pr-4 text-gray-700 dark:text-gray-200 no-underline tracking-widest pb-1'>
{link?.icon && (
<span className='mr-2'>
<i className={link.icon} />
</span>
)}
{link?.name}
{hasSubMenu && <i className='px-2 fa fa-angle-down'></i>}
</Link>
)}
{!hasSubMenu &&
<Link
href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
className=" menu-link pl-2 pr-4 text-gray-700 dark:text-gray-200 no-underline tracking-widest pb-1">
{link?.icon && <span className='mr-2'><i className={link.icon} /></span>}{link?.name}
{hasSubMenu && <i className='px-2 fa fa-angle-down'></i>}
</Link>}
{hasSubMenu && <>
<div className='cursor-pointer menu-link pl-2 pr-4 text-gray-700 dark:text-gray-200 no-underline tracking-widest pb-1'>
{link?.icon && <span className='mr-2'><i className={link.icon} /></span>} {link?.name}
<i className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>
</div>
</>}
{/* 子菜单 */}
{hasSubMenu && <ul className={`${show ? 'visible opacity-100 top-12' : 'invisible opacity-0 top-10'} border-gray-100 bg-white dark:bg-black dark:border-gray-800 transition-all duration-300 z-20 absolute block drop-shadow-lg `}>
{link.subMenus.map((sLink, index) => {
return <li key={index} className='not:last-child:border-b-0 border-b text-blue-600 dark:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 dark:border-gray-800 py-3 pr-6 pl-2'>
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
<span className='text-sm text-nowrap'>{sLink?.icon && <i className={sLink?.icon} > &nbsp; </i>}{sLink.title}</span>
</Link>
</li>
})}
</ul>}
{hasSubMenu && (
<>
<div className='cursor-pointer menu-link pl-2 pr-4 text-gray-700 dark:text-gray-200 no-underline tracking-widest pb-1'>
{link?.icon && (
<span className='mr-2'>
<i className={link.icon} />
</span>
)}{' '}
{link?.name}
<i
className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>
</div>
</>
)}
{/* 子菜单 */}
{hasSubMenu && (
<ul
className={`${show ? 'visible opacity-100 top-12' : 'invisible opacity-0 top-10'} border-gray-100 bg-white dark:bg-black dark:border-gray-800 transition-all duration-300 z-20 absolute block drop-shadow-lg `}>
{link.subMenus.map((sLink, index) => {
return (
<li
key={index}
className='not:last-child:border-b-0 border-b text-blue-600 dark:text-blue-300 hover:bg-gray-50 dark:hover:bg-gray-900 tracking-widest transition-all duration-200 dark:border-gray-800 py-3 pr-6 pl-2'>
<Link href={sLink.to} target={link?.target}>
<span className='text-sm text-nowrap'>
{sLink?.icon && <i className={sLink?.icon}> &nbsp; </i>}
{sLink.title}
</span>
</Link>
</li>
)
})}
</ul>
)}
</div>
)
}

View File

@@ -1,90 +1,99 @@
import { siteConfig } from '@/lib/config'
import CONFIG from '../config'
import { SVGLocation } from './svg/SVGLocation'
import { SVGEmail } from './svg/SVGEmail'
import { SVGLocation } from './svg/SVGLocation'
/* eslint-disable react/no-unescaped-entities */
export const Contact = () => {
return <>
{/* <!-- ====== Contact Start ====== --> */}
<section id="contact" className="relative py-20 md:py-[120px]">
<div
className="absolute left-0 top-0 -z-[1] h-full w-full dark:bg-dark"
></div>
<div
className="absolute left-0 top-0 -z-[1] h-1/2 w-full bg-[#E9F9FF] dark:bg-dark-700 lg:h-[45%] xl:h-1/2"
></div>
<div className="container px-4">
<div className="-mx-4 flex flex-wrap items-center">
{/* 联系方式左侧文字 */}
<div className="w-full px-4 lg:w-7/12 xl:w-8/12">
<div className="ud-contact-content-wrapper">
<div className="ud-contact-title mb-12 lg:mb-[150px]">
<span
className="mb-6 block text-base font-medium text-dark dark:text-white"
>
{siteConfig('STARTER_CONTACT_TITLE', null, CONFIG)}
</span>
<h2
className="max-w-[260px] text-[35px] font-semibold leading-[1.14] text-dark dark:text-white"
>
{siteConfig('STARTER_CONTACT_TEXT', null, CONFIG)}
</h2>
</div>
<div className="mb-12 flex flex-wrap justify-between lg:mb-0">
<div className="mb-8 flex w-[330px] max-w-full">
<div className="mr-6 text-[32px] text-primary">
<SVGLocation/>
</div>
<div>
<h5
className="mb-[18px] text-lg font-semibold text-dark dark:text-white"
>
{siteConfig('STARTER_CONTACT_LOCATION_TITLE', null, CONFIG)}
</h5>
<p className="text-base text-body-color dark:text-dark-6">
{siteConfig('STARTER_CONTACT_LOCATION_TEXT', null, CONFIG)}
</p>
</div>
return (
<>
{/* <!-- ====== Contact Start ====== --> */}
<section id='contact' className='relative py-20 md:py-[120px]'>
<div className='absolute left-0 top-0 -z-[1] h-full w-full dark:bg-dark'></div>
<div className='absolute left-0 top-0 -z-[1] h-1/2 w-full bg-[#E9F9FF] dark:bg-dark-700 lg:h-[45%] xl:h-1/2'></div>
<div className='container px-4'>
<div className='-mx-4 flex flex-wrap items-center'>
{/* 联系方式左侧文字 */}
<div className='w-full px-4 lg:w-7/12 xl:w-8/12'>
<div className='ud-contact-content-wrapper'>
<div className='ud-contact-title mb-12 lg:mb-[150px]'>
<span className='mb-6 block text-base font-medium text-dark dark:text-white'>
{siteConfig('STARTER_CONTACT_TITLE', null, CONFIG)}
</span>
<h2 className='max-w-[260px] text-[35px] font-semibold leading-[1.14] text-dark dark:text-white'>
{siteConfig('STARTER_CONTACT_TEXT', null, CONFIG)}
</h2>
</div>
<div className="mb-8 flex w-[330px] max-w-full">
<div className="mr-6 text-[32px] text-primary">
<SVGEmail/>
<div className='mb-12 flex flex-wrap justify-between lg:mb-0'>
<div className='mb-8 flex w-[330px] max-w-full'>
<div className='mr-6 text-[32px] text-primary'>
<SVGLocation />
</div>
<div>
<h5 className='mb-[18px] text-lg font-semibold text-dark dark:text-white'>
{siteConfig(
'STARTER_CONTACT_LOCATION_TITLE',
null,
CONFIG
)}
</h5>
<p className='text-base text-body-color dark:text-dark-6'>
{siteConfig(
'STARTER_CONTACT_LOCATION_TEXT',
null,
CONFIG
)}
</p>
</div>
</div>
<div>
<h5
className="mb-[18px] text-lg font-semibold text-dark dark:text-white"
>
{siteConfig('STARTER_CONTACT_EMAIL_TITLE', null, CONFIG)}
</h5>
<p className="text-base text-body-color dark:text-dark-6">
{siteConfig('STARTER_CONTACT_EMAIL_TEXT', null, CONFIG)}
</p>
<div className='mb-8 flex w-[330px] max-w-full'>
<div className='mr-6 text-[32px] text-primary'>
<SVGEmail />
</div>
<div>
<h5 className='mb-[18px] text-lg font-semibold text-dark dark:text-white'>
{siteConfig(
'STARTER_CONTACT_EMAIL_TITLE',
null,
CONFIG
)}
</h5>
<p className='text-base text-body-color dark:text-dark-6'>
{siteConfig('STARTER_CONTACT_EMAIL_TEXT', null, CONFIG)}
</p>
</div>
</div>
</div>
</div>
</div>
{siteConfig('STARTER_CONTACT_MSG_EXTERNAL_URL', null, CONFIG) && (
<>
{/* 联系方式右侧留言 */}
<div className='w-full px-4 lg:w-5/12 xl:w-4/12'>
<div
className='wow fadeInUp rounded-lg bg-white px-8 py-10 shadow-testimonial dark:bg-dark-2 dark:shadow-none sm:px-10 sm:py-12 md:p-[60px] lg:p-10 lg:px-10 lg:py-12 2xl:p-[60px]'
data-wow-delay='.2s'>
{/* 自定义的留言表单 、 需要对接接口 */}
{/* <MessageForm/> */}
{/* 嵌入外部表单 */}
<iframe
src={siteConfig(
'STARTER_CONTACT_MSG_EXTERNAL_URL',
null,
CONFIG
)}
width='100%'
height='500px'
frameBorder='0'></iframe>
</div>
</div>
</>
)}
</div>
{/* 联系方式右侧留言 */}
<div className="w-full px-4 lg:w-5/12 xl:w-4/12">
<div
className="wow fadeInUp rounded-lg bg-white px-8 py-10 shadow-testimonial dark:bg-dark-2 dark:shadow-none sm:px-10 sm:py-12 md:p-[60px] lg:p-10 lg:px-10 lg:py-12 2xl:p-[60px]"
data-wow-delay=".2s"
>
{/* 自定义的留言表单 、 需要对接接口 */}
{/* <MessageForm/> */}
{/* 嵌入外部表单 */}
<iframe src="https://noteforms.com/forms/yfctc7" width="100%" height="500px" frameBorder="0"></iframe>
</div>
</div>
</div>
</div>
</section>
{/* <!-- ====== Contact End ====== --> */}
</>
</section>
{/* <!-- ====== Contact End ====== --> */}
</>
)
}

View File

@@ -1,164 +1,187 @@
import { siteConfig } from '@/lib/config'
import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils'
import SocialButton from '@/themes/fukasawa/components/SocialButton'
import CONFIG from '../config'
import { Logo } from './Logo'
import SocialButton from '@/themes/fukasawa/components/SocialButton'
import { SVGFooterCircleBG } from './svg/SVGFooterCircleBG'
import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils'
/* eslint-disable @next/next/no-img-element */
export const Footer = (props) => {
export const Footer = props => {
const latestPosts = props?.latestPosts ? props?.latestPosts.slice(0, 2) : []
return <>
{/* <!-- ====== Footer Section Start --> */}
<footer
className="wow fadeInUp relative z-10 bg-[#090E34] pt-20 lg:pt-[100px]"
data-wow-delay=".15s"
>
<div className="container">
<div className="-mx-4 flex flex-wrap">
<div className="w-full px-4 sm:w-1/2 md:w-1/2 lg:w-4/12 xl:w-3/12">
<div className="mb-10 w-full">
<a
className="-mx-4 mb-6 inline-block max-w-[160px]"
>
<Logo white={true}/>
</a>
<p className="mb-8 max-w-[270px] text-base text-gray-7">
{siteConfig('STARTER_FOOTER_SLOGAN', null, CONFIG)}
</p>
<div className="-mx-3 flex items-center">
<div className='mx-3'><SocialButton/></div>
</div>
</div>
</div>
{/* 中间三列菜单组 */}
{CONFIG.STARTER_FOOTER_LINK_GROUP?.map((item, index) => {
return <div key={index} className="w-full px-4 sm:w-1/2 md:w-1/2 lg:w-2/12 xl:w-2/12">
<div className="mb-10 w-full">
<h4 className="mb-9 text-lg font-semibold text-white">
{item.TITLE}
</h4>
<ul>
{item?.LINK_GROUP?.map((l, i) => {
return <li key={i}>
<a href={l.URL}
className="mb-3 inline-block text-base text-gray-7 hover:text-primary"
>
{l.TITLE}
</a>
</li>
})}
</ul>
</div>
</div>
})}
{/* 页脚右侧最新博文 */}
<div className="w-full px-4 md:w-2/3 lg:w-6/12 xl:w-3/12">
<div className="mb-10 w-full">
<h4 className="mb-9 text-lg font-semibold text-white">
{siteConfig('STARTER_FOOTER_BLOG_LATEST_TITLE', null, CONFIG)}
</h4>
{/* 展示两条最新博客文章 */}
<div className="flex flex-col gap-8">
{latestPosts?.map((item, index) => {
const url = checkContainHttp(item.slug) ? sliceUrlFromHttp(item.slug) : `${siteConfig('SUB_PATH', '')}/${item.slug}`
return <a key={index}
href={url}
className="group flex items-center gap-[22px]"
>
<div className="overflow-hidden rounded w-20 h-12">
<img
src={item.pageCoverThumbnail}
alt={item.title}
/>
</div>
<span
className="line-clamp-2 max-w-[180px] text-base text-gray-7 group-hover:text-white"
>
{item.title}
</span>
const STARTER_FOOTER_LINK_GROUP = siteConfig(
'STARTER_FOOTER_LINK_GROUP',
[],
CONFIG
)
return (
<>
{/* <!-- ====== Footer Section Start --> */}
<footer
className='wow fadeInUp relative z-10 bg-[#090E34] pt-20 lg:pt-[100px]'
data-wow-delay='.15s'>
<div className='container'>
<div className='-mx-4 flex flex-wrap'>
<div className='w-full px-4 sm:w-1/2 md:w-1/2 lg:w-4/12 xl:w-3/12'>
<div className='mb-10 w-full'>
<a className='-mx-4 mb-6 inline-block max-w-[160px]'>
<Logo white={true} />
</a>
})}
</div>
</div>
</div>
</div>
</div>
{/* 底部版权信息相关 */}
<div
className="mt-12 border-t border-[#8890A4] border-opacity-40 py-8 lg:mt-[60px]"
>
<div className="container">
<div className="-mx-4 flex flex-wrap">
<div className="w-full px-4 md:w-2/3 lg:w-1/2">
<div className="my-1">
<div
className="-mx-3 flex items-center justify-center md:justify-start"
>
<a
href= {siteConfig('STARTER_FOOTER_PRIVACY_POLICY_URL', null, CONFIG)}
className="px-3 text-base text-gray-7 hover:text-white hover:underline"
>
{siteConfig('STARTER_FOOTER_PRIVACY_POLICY_TEXT', null, CONFIG)}
</a>
<a
href= {siteConfig('STARTER_FOOTER_PRIVACY_LEGAL_NOTICE_URL', null, CONFIG)}
className="px-3 text-base text-gray-7 hover:text-white hover:underline"
>
{siteConfig('STARTER_FOOTER_PRIVACY_LEGAL_NOTICE_TEXT', null, CONFIG)}
</a>
<a
href= {siteConfig('STARTER_FOOTER_PRIVACY_TERMS_OF_SERVICE_URL', null, CONFIG)}
className="px-3 text-base text-gray-7 hover:text-white hover:underline"
>
{siteConfig('STARTER_FOOTER_PRIVACY_TERMS_OF_SERVICE_TEXT', null, CONFIG)}
</a>
</div>
</div>
</div>
<div className="w-full px-4 md:w-1/3 lg:w-1/2">
<div className="my-1 flex justify-center md:justify-end">
<p className="text-base text-gray-7">
Designed and Developed by
<a
href="https://github.com/tangly1024/NotionNext"
rel="nofollow noopner noreferrer"
target="_blank"
className="px-1 text-gray-1 hover:underline"
>
NotionNext {siteConfig('VERSION')}
</a>
<p className='mb-8 max-w-[270px] text-base text-gray-7'>
{siteConfig('STARTER_FOOTER_SLOGAN', null, CONFIG)}
</p>
<div className='-mx-3 flex items-center'>
<div className='mx-3'>
<SocialButton />
</div>
</div>
</div>
</div>
{/* 中间三列菜单组 */}
{STARTER_FOOTER_LINK_GROUP?.map((item, index) => {
return (
<div
key={index}
className='w-full px-4 sm:w-1/2 md:w-1/2 lg:w-2/12 xl:w-2/12'>
<div className='mb-10 w-full'>
<h4 className='mb-9 text-lg font-semibold text-white'>
{item.TITLE}
</h4>
<ul>
{item?.LINK_GROUP?.map((l, i) => {
return (
<li key={i}>
<a
href={l.URL}
className='mb-3 inline-block text-base text-gray-7 hover:text-primary'>
{l.TITLE}
</a>
</li>
)
})}
</ul>
</div>
</div>
)
})}
{/* 页脚右侧最新博文 */}
<div className='w-full px-4 md:w-2/3 lg:w-6/12 xl:w-3/12'>
<div className='mb-10 w-full'>
<h4 className='mb-9 text-lg font-semibold text-white'>
{siteConfig('STARTER_FOOTER_BLOG_LATEST_TITLE', null, CONFIG)}
</h4>
{/* 展示两条最新博客文章 */}
<div className='flex flex-col gap-8'>
{latestPosts?.map((item, index) => {
const url = checkContainHttp(item.slug)
? sliceUrlFromHttp(item.slug)
: `${siteConfig('SUB_PATH', '')}/${item.slug}`
return (
<a
key={index}
href={url}
className='group flex items-center gap-[22px]'>
<div className='overflow-hidden rounded w-20 h-12'>
<img src={item.pageCoverThumbnail} alt={item.title} />
</div>
<span className='line-clamp-2 max-w-[180px] text-base text-gray-7 group-hover:text-white'>
{item.title}
</span>
</a>
)
})}
</div>
</div>
</div>
</div>
</div>
</div>
{/* Footer 背景 */}
<div>
<span className="absolute left-0 top-0 z-[-1]">
<img src="/images/starter/footer/shape-1.svg" alt="" />
</span>
{/* 底部版权信息相关 */}
<span className="absolute bottom-0 right-0 z-[-1]">
<img src="/images/starter/footer/shape-3.svg" alt="" />
</span>
<div className='mt-12 border-t border-[#8890A4] border-opacity-40 py-8 lg:mt-[60px]'>
<div className='container'>
<div className='-mx-4 flex flex-wrap'>
<div className='w-full px-4 md:w-2/3 lg:w-1/2'>
<div className='my-1'>
<div className='-mx-3 flex items-center justify-center md:justify-start'>
<a
href={siteConfig(
'STARTER_FOOTER_PRIVACY_POLICY_URL',
null,
CONFIG
)}
className='px-3 text-base text-gray-7 hover:text-white hover:underline'>
{siteConfig(
'STARTER_FOOTER_PRIVACY_POLICY_TEXT',
null,
CONFIG
)}
</a>
<a
href={siteConfig(
'STARTER_FOOTER_PRIVACY_LEGAL_NOTICE_URL',
null,
CONFIG
)}
className='px-3 text-base text-gray-7 hover:text-white hover:underline'>
{siteConfig(
'STARTER_FOOTER_PRIVACY_LEGAL_NOTICE_TEXT',
null,
CONFIG
)}
</a>
<a
href={siteConfig(
'STARTER_FOOTER_PRIVACY_TERMS_OF_SERVICE_URL',
null,
CONFIG
)}
className='px-3 text-base text-gray-7 hover:text-white hover:underline'>
{siteConfig(
'STARTER_FOOTER_PRIVACY_TERMS_OF_SERVICE_TEXT',
null,
CONFIG
)}
</a>
</div>
</div>
</div>
<div className='w-full px-4 md:w-1/3 lg:w-1/2'>
<div className='my-1 flex justify-center md:justify-end'>
<p className='text-base text-gray-7'>
Designed and Developed by
<a
href='https://github.com/tangly1024/NotionNext'
rel='nofollow noopner noreferrer'
target='_blank'
className='px-1 text-gray-1 hover:underline'>
NotionNext {siteConfig('VERSION')}
</a>
</p>
</div>
</div>
</div>
</div>
</div>
<span className="absolute right-0 top-0 z-[-1]">
<SVGFooterCircleBG/>
</span>
</div>
</footer>
{/* <!-- ====== Footer Section End --> */}
{/* Footer 背景 */}
<div>
<span className='absolute left-0 top-0 z-[-1]'>
<img src='/images/starter/footer/shape-1.svg' alt='' />
</span>
<span className='absolute bottom-0 right-0 z-[-1]'>
<img src='/images/starter/footer/shape-3.svg' alt='' />
</span>
<span className='absolute right-0 top-0 z-[-1]'>
<SVGFooterCircleBG />
</span>
</div>
</footer>
{/* <!-- ====== Footer Section End --> */}
</>
)
}

View File

@@ -1,10 +1,11 @@
/* eslint-disable @next/next/no-img-element */
/* eslint-disable @next/next/no-html-link-for-pages */
import { siteConfig } from '@/lib/config';
import { useGlobal } from '@/lib/global';
import throttle from 'lodash.throttle';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import CONFIG from '../config';
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import throttle from 'lodash.throttle'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import CONFIG from '../config'
/**
* 站点图标
@@ -21,10 +22,9 @@ export const Logo = ({ white }) => {
// 滚动监听
const throttleMs = 200
const navBarScrollListener = throttle(() => {
const scrollY = window.scrollY;
const scrollY = window.scrollY
// 何时显示浅色或白底的logo
const homePageNavBar = router.route === '/' && scrollY < 10 // 在首页并且视窗在页面顶部
console.log('白色', homePageNavBar, router.route, scrollY < 10)
if (white || isDarkMode || homePageNavBar) {
setLogo(siteConfig('STARTER_LOGO_WHITE', null, CONFIG))
setLogoTextColor('text-white')
@@ -41,21 +41,29 @@ export const Logo = ({ white }) => {
}
}, [isDarkMode, router])
return <div className="w-60 max-w-full px-4">
<div className="navbar-logo flex items-center w-full py-5 cursor-pointer">
{/* eslint-disable-next-line @next/next/no-img-element */}
{logo && <img
onClick={() => {
router.push('/')
}}
src={logo}
alt="logo"
className="header-logo w-full"
/>}
{/* logo文字 */}
<span onClick={() => { router.push('/') }} className={`${logoTextColor} dark:text-white py-1.5 header-logo-text whitespace-nowrap text-2xl font-semibold`}>
{siteConfig('TITLE')}
</span>
</div>
return (
<div className='w-60 max-w-full px-4'>
<div className='navbar-logo flex items-center w-full py-5 cursor-pointer'>
{/* eslint-disable-next-line @next/next/no-img-element */}
{logo && (
<img
onClick={() => {
router.push('/')
}}
src={logo}
alt='logo'
className='header-logo w-full'
/>
)}
{/* logo文字 */}
<span
onClick={() => {
router.push('/')
}}
className={`${logoTextColor} dark:text-white py-1.5 header-logo-text whitespace-nowrap text-2xl font-semibold`}>
{siteConfig('TITLE')}
</span>
</div>
</div>
)
}

Some files were not shown because too many files have changed in this diff Show More