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 # 环境变量 @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的操作步骤
**期望的正常结果** **期望的正常结果**
【此项必填】希望按这个步骤,正常操作结果是什么 【此项必填】按这个步骤,预期出现的现象应该是什么
**截图** **复现步骤**
可选】相关的页面,应该的结果 此项必填】你的操作步骤按此步骤理应在我的开发环境出现一样的bug。
**环境** **环境**
@@ -32,4 +29,4 @@ assignees: tangly1024
- 【可选】浏览器 [例如. chrome, safari, firefox] - 【可选】浏览器 [例如. chrome, safari, firefox]
**补充说明** **补充说明**
【可选】与问题相关的其它说明 【可选】与问题相关的其它说明

View File

@@ -1,7 +1,9 @@
// 注: process.env.XX是Vercel的环境变量配置方式见https://docs.tangly1024.com/article/how-to-config-notion-next#c4768010ae7d44609b744e79e2f9959a // 注: process.env.XX是Vercel的环境变量配置方式见https://docs.tangly1024.com/article/how-to-config-notion-next#c4768010ae7d44609b744e79e2f9959a
const BLOG = { const BLOG = {
// Important page_idDuplicate Template from https://www.notion.so/tanghh/02ab3b8678004aa69e9e415905ef32a5 // 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 结尾。 PSEUDO_STATIC: process.env.NEXT_PUBLIC_PSEUDO_STATIC || false, // 伪静态路径开启后所有文章URL都以 .html 结尾。
NEXT_REVALIDATE_SECOND: process.env.NEXT_PUBLIC_REVALIDATE_SECOND || 5, // 更新内容缓存间隔 单位(秒)即每个页面有5秒的纯静态期、此期间无论多少次访问都不会抓取notion数据调大该值有助于节省Vercel资源、同时提升访问速率但也会使文章更新有延迟。 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 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: 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时关闭根据时间自动切换夜间模式 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版本后,欢迎语在此配置,英文逗号隔开 , 即可支持多个欢迎语打字效果。 // 3.14.1版本后,欢迎语在此配置,英文逗号隔开 , 即可支持多个欢迎语打字效果。
GREETING_WORDS: GREETING_WORDS:
@@ -43,7 +46,9 @@ const BLOG = {
IMAGE_COMPRESS_WIDTH: process.env.NEXT_PUBLIC_IMAGE_COMPRESS_WIDTH || 800, // 图片压缩宽度默认值,作用于博客封面和文章内容 越小加载图片越快 IMAGE_COMPRESS_WIDTH: process.env.NEXT_PUBLIC_IMAGE_COMPRESS_WIDTH || 800, // 图片压缩宽度默认值,作用于博客封面和文章内容 越小加载图片越快
IMAGE_ZOOM_IN_WIDTH: process.env.NEXT_PUBLIC_IMAGE_ZOOM_IN_WIDTH || 1200, // 文章图片点击放大后的画质宽度,不代表在网页中的实际展示宽度 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_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 配置在这里 // eg: images.unsplash.com(notion图床的所有图片都会替换),如果你在 notion 里已经添加了一个随机图片 url恰巧那个服务跑路或者挂掉想一键切换所有配图可以将该 url 配置在这里
// 默认下会将你上传到 notion的主页封面图和头像也给替换建议将主页封面图和头像放在其他图床在 notion 里配置 link 即可。 // 默认下会将你上传到 notion的主页封面图和头像也给替换建议将主页封面图和头像放在其他图床在 notion 里配置 link 即可。
@@ -122,23 +127,29 @@ const BLOG = {
CAN_COPY: process.env.NEXT_PUBLIC_CAN_COPY || true, // 是否允许复制页面内容 默认允许如果设置为false、则全栈禁止复制内容。 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: CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH:
process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH || true, // 是否显示切换主题 process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH ||
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_DARK_MODE: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_DARK_MODE || true, // 是否显示深色模式 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: CUSTOM_RIGHT_CLICK_CONTEXT_MENU_SHARE_LINK:
process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_SHARE_LINK || true, // 是否显示分享链接 process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_SHARE_LINK || true, // 是否显示分享链接
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_RANDOM_POST: CUSTOM_RIGHT_CLICK_CONTEXT_MENU_RANDOM_POST:
process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_RANDOM_POST || true, // 是否显示随机博客 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_CATEGORY:
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_TAG: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_TAG || true, // 是否显示标签 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_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'] CUSTOM_EXTERNAL_CSS: [''], // e.g. ['http://xx.com/style.css','http://xx.com/style.css']
// 侧栏布局 是否反转(左变右,右变左) 已支持主题: hexo next medium fukasawa example // 侧栏布局 是否反转(左变右,右变左) 已支持主题: 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 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 粉絲團' FACEBOOK_PAGE_TITLE: process.env.NEXT_PUBLIC_FACEBOOK_PAGE_TITLE || null, // 邊欄 Facebook Page widget 的標題欄,填''則無標題欄 e.g FACEBOOK 粉絲團'
@@ -151,7 +162,8 @@ const BLOG = {
// START********代码相关******** // START********代码相关********
// PrismJs 代码相关 // PrismJs 代码相关
PRISM_JS_PATH: 'https://npm.elemecdn.com/prismjs@1.29.0/components/', 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 // 代码主题 @see https://github.com/PrismJS/prism-themes
PRISM_THEME_PREFIX_PATH: PRISM_THEME_PREFIX_PATH:
@@ -168,16 +180,19 @@ const BLOG = {
CODE_MAC_BAR: process.env.NEXT_PUBLIC_CODE_MAC_BAR || true, // 代码左上角显示mac的红黄绿图标 CODE_MAC_BAR: process.env.NEXT_PUBLIC_CODE_MAC_BAR || true, // 代码左上角显示mac的红黄绿图标
CODE_LINE_NUMBERS: process.env.NEXT_PUBLIC_CODE_LINE_NUMBERS || false, // 是否显示行号 CODE_LINE_NUMBERS: process.env.NEXT_PUBLIC_CODE_LINE_NUMBERS || false, // 是否显示行号
CODE_COLLAPSE: process.env.NEXT_PUBLIC_CODE_COLLAPSE || true, // 是否支持折叠代码框 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********代码相关******** // END********代码相关********
// Mermaid 图表CDN // Mermaid 图表CDN
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 // QRCodeCDN
QR_CODE_CDN: 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_LIGHT: '#eeeeee', // use hex value, don't forget '#' e.g #fffefc
BACKGROUND_DARK: '#000000', // use hex value, don't forget '#' 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_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控制 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_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_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_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时会构建 // ALGOLIA_RECREATE_DATA: process.env.ALGOLIA_RECREATE_DATA || process.env.npm_lifecycle_event === 'build', // 为true时重新构建索引数据; 默认在build时会构建
PREVIEW_CATEGORY_COUNT: 16, // 首页最多展示的分类数量0为不限制 PREVIEW_CATEGORY_COUNT: 16, // 首页最多展示的分类数量0为不限制
PREVIEW_TAG_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, // 开关 FIREWORKS: process.env.NEXT_PUBLIC_FIREWORKS || false, // 开关
// 烟花色彩,感谢 https://github.com/Vixcity 提交的色彩 // 烟花色彩,感谢 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, // 开关 SAKURA: process.env.NEXT_PUBLIC_SAKURA || false, // 开关
@@ -238,14 +261,16 @@ const BLOG = {
process.env.NEXT_PUBLIC_TIANLI_GPT_CSS || process.env.NEXT_PUBLIC_TIANLI_GPT_CSS ||
'https://cdn1.tianli0.top/gh/zhheo/Post-Abstract-AI@0.15.2/tianli_gpt.css', 'https://cdn1.tianli0.top/gh/zhheo/Post-Abstract-AI@0.15.2/tianli_gpt.css',
TianliGPT_JS: 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 || '', TianliGPT_KEY: process.env.NEXT_PUBLIC_TIANLI_GPT_KEY || '',
// Chatbase 是否显示chatbase机器人 https://www.chatbase.co/ // Chatbase 是否显示chatbase机器人 https://www.chatbase.co/
CHATBASE_ID: process.env.NEXT_PUBLIC_CHATBASE_ID || null, CHATBASE_ID: process.env.NEXT_PUBLIC_CHATBASE_ID || null,
// WebwhizAI 机器人 @see https://github.com/webwhiz-ai/webwhiz // WebwhizAI 机器人 @see https://github.com/webwhiz-ai/webwhiz
WEB_WHIZ_ENABLED: process.env.NEXT_PUBLIC_WEB_WHIZ_ENABLED || false, // 是否显示 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 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_ENABLED: process.env.NEXT_PUBLIC_DIFY_CHATBOT_ENABLED || false,
DIFY_CHATBOT_BASE_URL: process.env.NEXT_PUBLIC_DIFY_CHATBOT_BASE_URL || '', DIFY_CHATBOT_BASE_URL: process.env.NEXT_PUBLIC_DIFY_CHATBOT_BASE_URL || '',
@@ -255,12 +280,14 @@ const BLOG = {
WIDGET_PET_LINK: WIDGET_PET_LINK:
process.env.NEXT_PUBLIC_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 '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: process.env.NEXT_PUBLIC_MUSIC_PLAYER || false, // 是否使用音乐播放插件
MUSIC_PLAYER_VISIBLE: process.env.NEXT_PUBLIC_MUSIC_PLAYER_VISIBLE || true, // 是否在左下角显示播放和切换,如果使用播放器,打开自动播放再隐藏,就会以类似背景音乐的方式播放,无法取消和暂停 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_LRC_TYPE: process.env.NEXT_PUBLIC_MUSIC_PLAYER_LRC_TYPE || '0', // 歌词显示类型,可选值: 3 | 1 | 00禁用 lrc 歌词1lrc 格式的字符串3lrc 文件 url前提是有配置歌词路径对 meting 无效)
MUSIC_PLAYER_CDN_URL: MUSIC_PLAYER_CDN_URL:
process.env.NEXT_PUBLIC_MUSIC_PLAYER_CDN_URL || process.env.NEXT_PUBLIC_MUSIC_PLAYER_CDN_URL ||
@@ -272,78 +299,105 @@ const BLOG = {
name: '风を共に舞う気持ち', name: '风を共に舞う気持ち',
artist: 'Falcom Sound Team jdk', artist: 'Falcom Sound Team jdk',
url: 'https://music.163.com/song/media/outer/url?id=731419.mp3', 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: '王都グランセル', name: '王都グランセル',
artist: 'Falcom Sound Team jdk', artist: 'Falcom Sound Team jdk',
url: 'https://music.163.com/song/media/outer/url?id=731355.mp3', 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: 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_SERVER:
MUSIC_PLAYER_METING_ID: process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_ID || '60198', // 对应歌单的 id process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_SERVER || 'netease', // 音乐平台,[netease, tencent, kugou, xiami, baidu]
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_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 // ----> 评论互动 可同时开启多个支持 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 评论插件 // artalk 评论插件
COMMENT_ARTALK_SERVER: process.env.NEXT_PUBLIC_COMMENT_ARTALK_SERVER || '', // ArtalkServert后端地址 https://artalk.js.org/guide/deploy.html COMMENT_ARTALK_SERVER: process.env.NEXT_PUBLIC_COMMENT_ARTALK_SERVER || '', // ArtalkServert后端地址 https://artalk.js.org/guide/deploy.html
COMMENT_ARTALK_JS: 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: 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 // twikoo
COMMENT_TWIKOO_ENV_ID: process.env.NEXT_PUBLIC_COMMENT_ENV_ID || '', // TWIKOO后端地址 腾讯云环境填envIdVercel环境填域名教程https://tangly1024.com/article/notionnext-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: 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 // 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/ // giscus @see https://giscus.app/
COMMENT_GISCUS_REPO: process.env.NEXT_PUBLIC_COMMENT_GISCUS_REPO || '', // 你的Github仓库名 e.g 'tangly1024/NotionNext' 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_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_CATEGORY_ID:
COMMENT_GISCUS_MAPPING: process.env.NEXT_PUBLIC_COMMENT_GISCUS_MAPPING || 'pathname', // 你的Github Discussions 使用哪種方式來標定文章, 預設 'pathname' process.env.NEXT_PUBLIC_COMMENT_GISCUS_CATEGORY_ID || '', // 你的Github Discussions 內的 Category ID ( 設定完 giscus 即可看到 )
COMMENT_GISCUS_REACTIONS_ENABLED: process.env.NEXT_PUBLIC_COMMENT_GISCUS_REACTIONS_ENABLED || '1', // 你的 Giscus 是否開啟文章表情符號 '1' 開啟 "0" 關閉 預設開啟 COMMENT_GISCUS_MAPPING:
COMMENT_GISCUS_EMIT_METADATA: process.env.NEXT_PUBLIC_COMMENT_GISCUS_EMIT_METADATA || '0', // 你的 Giscus 是否提取 Metadata '1' 開啟 '0' 關閉 預設關閉 process.env.NEXT_PUBLIC_COMMENT_GISCUS_MAPPING || 'pathname', // 你的Github Discussions 使用哪種方式來標定文章, 預設 'pathname'
COMMENT_GISCUS_INPUT_POSITION: process.env.NEXT_PUBLIC_COMMENT_GISCUS_INPUT_POSITION || 'bottom', // 你的 Giscus 發表留言位置 'bottom' 尾部 'top' 頂部, 預設 'bottom' 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_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_LOADING:
COMMENT_GISCUS_CROSSORIGIN: process.env.NEXT_PUBLIC_COMMENT_GISCUS_CROSSORIGIN || 'anonymous', // 你的 Giscus 可以跨網域, 預設 'anonymous' 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_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_HOST:
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 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/ // gitalk评论插件 更多参考 https://gitalk.github.io/
COMMENT_GITALK_REPO: process.env.NEXT_PUBLIC_COMMENT_GITALK_REPO || '', // 你的Github仓库名例如 'NotionNext' 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_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_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_ID:
COMMENT_GITALK_CLIENT_SECRET: process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_SECRET || '', // e.g 40位ID 在gitalk后台获取 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_DISTRACTION_FREE_MODE: false, // 类似facebook的无干扰模式
COMMENT_GITALK_JS_CDN_URL: 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: 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_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_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_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_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_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_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_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, // 最新评论 COMMENT_WALINE_RECENT: process.env.NEXT_PUBLIC_WALINE_RECENT || false, // 最新评论
@@ -357,15 +411,18 @@ const BLOG = {
// TOKEN: Webmention的API token // TOKEN: Webmention的API token
COMMENT_WEBMENTION_ENABLE: process.env.NEXT_PUBLIC_WEBMENTION_ENABLE || false, COMMENT_WEBMENTION_ENABLE: process.env.NEXT_PUBLIC_WEBMENTION_ENABLE || false,
COMMENT_WEBMENTION_AUTH: process.env.NEXT_PUBLIC_WEBMENTION_AUTH || '', COMMENT_WEBMENTION_AUTH: process.env.NEXT_PUBLIC_WEBMENTION_AUTH || '',
COMMENT_WEBMENTION_HOSTNAME: process.env.NEXT_PUBLIC_WEBMENTION_HOSTNAME || '', COMMENT_WEBMENTION_HOSTNAME:
COMMENT_WEBMENTION_TWITTER_USERNAME: process.env.NEXT_PUBLIC_TWITTER_USERNAME || '', 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 || '', 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_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_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_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 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_HOST_URL: process.env.NEXT_PUBLIC_MATOMO_HOST_URL || '', // Matomo服务器地址不带斜杠
MATOMO_SITE_ID: process.env.NEXT_PUBLIC_MATOMO_SITE_ID || '', // Matomo网站ID MATOMO_SITE_ID: process.env.NEXT_PUBLIC_MATOMO_SITE_ID || '', // Matomo网站ID
// ACKEE网站访客统计工具 // ACKEE网站访客统计工具
ANALYTICS_ACKEE_TRACKER: process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_TRACKER || '', // e.g 'https://ackee.tangly1024.com/tracker.js' ANALYTICS_ACKEE_TRACKER:
ANALYTICS_ACKEE_DATA_SERVER: process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_DATA_SERVER || '', // e.g https://ackee.tangly1024.com , don't end with a slash process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_TRACKER || '', // e.g 'https://ackee.tangly1024.com/tracker.js'
ANALYTICS_ACKEE_DOMAIN_ID: process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_DOMAIN_ID || '', // e.g '82e51db6-dec2-423a-b7c9-b4ff7ebb3302' 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 站点分析
CLARITY_ID: process.env.NEXT_PUBLIC_CLARITY_ID || null, // 只需要复制Clarity脚本中的ID部分ID是一个十位的英文数字组合 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_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_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_IN_ARTICLE:
ADSENSE_GOOGLE_SLOT_FLOW: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_FLOW || '1510444138', // Google AdScene>广告>按单元广告>新建信息流广告 process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_IN_ARTICLE || '3806269138', // Google AdScene>广告>按单元广告>新建文章内嵌广告 粘贴html代码中的data-ad-slot值
ADSENSE_GOOGLE_SLOT_NATIVE: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_NATIVE || '4980048999', // Google AdScene>广告>按单元广告>新建原生广告 ADSENSE_GOOGLE_SLOT_FLOW:
ADSENSE_GOOGLE_SLOT_AUTO: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_AUTO || '8807314373', // Google AdScene>广告>按单元广告>新建展示广告 (自动广告 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 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: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE || 'type', // 文章类型,
type_post: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_POST || 'Post', // 当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_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_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', // 文章标题 title: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TITLE || 'title', // 文章标题
status: process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS || 'status', status: process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS || 'status',
status_publish: process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS_PUBLISH || 'Published', // 当status状态值与此相同时为发布可以为中文 status_publish:
status_invisible: process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS_INVISIBLE || 'Invisible', // 当status状态值与此相同时为隐藏发布,可以为中文 除此之外其他页面状态不会显示在博客上 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', summary: process.env.NEXT_PUBLIC_NOTION_PROPERTY_SUMMARY || 'summary',
slug: process.env.NEXT_PUBLIC_NOTION_PROPERTY_SLUG || 'slug', slug: process.env.NEXT_PUBLIC_NOTION_PROPERTY_SLUG || 'slug',
category: process.env.NEXT_PUBLIC_NOTION_PROPERTY_CATEGORY || 'category', 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 AVATAR: process.env.NEXT_PUBLIC_AVATAR || '/avatar.svg', // 作者头像被notion中的ICON覆盖。若无ICON则取public目录下的avatar.png
TITLE: process.env.NEXT_PUBLIC_TITLE || 'NotionNext BLOG', // 站点标题 被notion中的页面标题覆盖此处请勿留空白否则服务器无法编译 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 文件 HOME_BANNER_IMAGE:
DESCRIPTION: process.env.NEXT_PUBLIC_DESCRIPTION || '这是一个由NotionNext生成的站点', // 站点描述被notion中的页面描述覆盖 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 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 { loadExternalResource } from '@/lib/utils'
import { isBrowser } from 'react-notion-x' import { useEffect } from 'react'
// import AOS from 'aos'
/** /**
* 加载滚动动画 * 加载滚动动画
* 改从外部CDN读取
* https://michalsnik.github.io/aos/ * https://michalsnik.github.io/aos/
*/ */
export default function AOSAnimation() { export default function AOSAnimation() {
if (isBrowser) { const initAOS = async () => {
AOS.init() 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 { siteConfig } from '@/lib/config'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import LA51 from './LA51'
import WebWhiz from './Webwhiz'
import TianLiGPT from './TianliGPT'
import { GlobalStyle } from './GlobalStyle' 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 { CUSTOM_EXTERNAL_CSS, CUSTOM_EXTERNAL_JS } from '@/blog.config'
import { isBrowser, loadExternalResource } from '@/lib/utils' import { isBrowser, loadExternalResource } from '@/lib/utils'
import { useEffect } from 'react'
import { initGoogleAdsense } from './GoogleAdsense'
const TwikooCommentCounter = dynamic(() => import('@/components/TwikooCommentCounter'), { ssr: false }) const TwikooCommentCounter = dynamic(
const DebugPanel = dynamic(() => import('@/components/DebugPanel'), { ssr: false }) () => import('@/components/TwikooCommentCounter'),
const ThemeSwitch = dynamic(() => import('@/components/ThemeSwitch'), { ssr: false }) { ssr: false }
const Fireworks = dynamic(() => import('@/components/Fireworks'), { 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 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 Ribbon = dynamic(() => import('@/components/Ribbon'), { ssr: false })
const Sakura = dynamic(() => import('@/components/Sakura'), { ssr: false }) const Sakura = dynamic(() => import('@/components/Sakura'), { ssr: false })
const StarrySky = dynamic(() => import('@/components/StarrySky'), { ssr: false }) const StarrySky = dynamic(() => import('@/components/StarrySky'), {
const DifyChatbot = dynamic(() => import('@/components/DifyChatbot'), { ssr: false }); ssr: false
const Analytics = dynamic(() => import('@vercel/analytics/react').then(async (m) => { return m.Analytics }), { 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 MusicPlayer = dynamic(() => import('@/components/Player'), { ssr: false })
const Ackee = dynamic(() => import('@/components/Ackee'), { ssr: false }) const Ackee = dynamic(() => import('@/components/Ackee'), { ssr: false })
const Gtag = dynamic(() => import('@/components/Gtag'), { ssr: false }) const Gtag = dynamic(() => import('@/components/Gtag'), { ssr: false })
const Busuanzi = dynamic(() => import('@/components/Busuanzi'), { ssr: false }) const Busuanzi = dynamic(() => import('@/components/Busuanzi'), { ssr: false })
const GoogleAdsense = dynamic(() => import('@/components/GoogleAdsense'), { ssr: false }) const Messenger = dynamic(() => import('@/components/FacebookMessenger'), {
const Messenger = dynamic(() => import('@/components/FacebookMessenger'), { ssr: false }) ssr: false
})
const VConsole = dynamic(() => import('@/components/VConsole'), { ssr: false }) const VConsole = dynamic(() => import('@/components/VConsole'), { ssr: false })
const CustomContextMenu = dynamic(() => import('@/components/CustomContextMenu'), { ssr: false }) const CustomContextMenu = dynamic(
const DisableCopy = dynamic(() => import('@/components/DisableCopy'), { ssr: false }) () => import('@/components/CustomContextMenu'),
const AdBlockDetect = dynamic(() => import('@/components/AdBlockDetect'), { ssr: false }) { ssr: false }
const LoadingProgress = dynamic(() => import('@/components/LoadingProgress'), { ssr: false }) )
const AosAnimation = dynamic(() => import('@/components/AOSAnimation'), { 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 * @param {*} props
* @returns * @returns
*/ */
const ExternalPlugin = (props) => { const ExternalPlugin = props => {
const DISABLE_PLUGIN = siteConfig('DISABLE_PLUGIN') const DISABLE_PLUGIN = siteConfig('DISABLE_PLUGIN')
const THEME_SWITCH = siteConfig('THEME_SWITCH') const THEME_SWITCH = siteConfig('THEME_SWITCH')
const DEBUG = siteConfig('DEBUG') const DEBUG = siteConfig('DEBUG')
@@ -55,7 +91,9 @@ const ExternalPlugin = (props) => {
const FLUTTERINGRIBBON = siteConfig('FLUTTERINGRIBBON') const FLUTTERINGRIBBON = siteConfig('FLUTTERINGRIBBON')
const COMMENT_TWIKOO_COUNT_ENABLE = siteConfig('COMMENT_TWIKOO_COUNT_ENABLE') const COMMENT_TWIKOO_COUNT_ENABLE = siteConfig('COMMENT_TWIKOO_COUNT_ENABLE')
const RIBBON = siteConfig('RIBBON') 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 CAN_COPY = siteConfig('CAN_COPY')
const WEB_WHIZ_ENABLED = siteConfig('WEB_WHIZ_ENABLED') const WEB_WHIZ_ENABLED = siteConfig('WEB_WHIZ_ENABLED')
const AD_WWADS_BLOCK_DETECT = siteConfig('AD_WWADS_BLOCK_DETECT') 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) { if (DISABLE_PLUGIN) {
return null return null
} }
return <> return (
<>
{/* 全局样式嵌入 */}
<GlobalStyle />
{/* 全局样式嵌入 */} {THEME_SWITCH && <ThemeSwitch />}
<GlobalStyle/> {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 />} {ANALYTICS_51LA_ID && ANALYTICS_51LA_CK && (
{DEBUG && <DebugPanel />} <>
{ANALYTICS_ACKEE_TRACKER && <Ackee />} <script id='LA_COLLECT' src='//sdk.51.la/js-sdk-pro.min.js' defer />
{ANALYTICS_GOOGLE_ID && <Gtag />} {/* <script async dangerouslySetInnerHTML={{
{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={{
__html: ` __html: `
LA.init({id:"${ANALYTICS_51LA_ID}",ck:"${ANALYTICS_51LA_CK}",hashMode:true,autoTrack:true}) LA.init({id:"${ANALYTICS_51LA_ID}",ck:"${ANALYTICS_51LA_CK}",hashMode:true,autoTrack:true})
` `
}} /> */} }} /> */}
</>)} </>
)}
{/* 注入JS脚本 */} {/* 注入JS脚本 */}
{GLOBAL_JS && <script async dangerouslySetInnerHTML={{ {GLOBAL_JS && (
__html: GLOBAL_JS <script
}} />} async
dangerouslySetInnerHTML={{
__html: GLOBAL_JS
}}
/>
)}
{CHATBASE_ID && (<> {CHATBASE_ID && (
<script id={CHATBASE_ID} src="https://www.chatbase.co/embed.min.js" defer /> <>
<script async dangerouslySetInnerHTML={{ <script
id={CHATBASE_ID}
src='https://www.chatbase.co/embed.min.js'
defer
/>
<script
async
dangerouslySetInnerHTML={{
__html: ` __html: `
window.chatbaseConfig = { window.chatbaseConfig = {
chatbotId: "${CHATBASE_ID}", chatbotId: "${CHATBASE_ID}",
} }
` `
}} /> }}
</>)} />
</>
)}
{CLARITY_ID && (<> {CLARITY_ID && (
<script async dangerouslySetInnerHTML={{ <>
__html: ` <script
async
dangerouslySetInnerHTML={{
__html: `
(function(c,l,a,r,i,t,y){ (function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)}; 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; 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); y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "${CLARITY_ID}"); })(window, document, "clarity", "script", "${CLARITY_ID}");
` `
}} /> }}
</>)} />
</>
)}
{COMMENT_DAO_VOICE_ID && (<> {COMMENT_DAO_VOICE_ID && (
{/* DaoVoice 反馈 */} <>
<script async dangerouslySetInnerHTML={{ {/* DaoVoice 反馈 */}
<script
async
dangerouslySetInnerHTML={{
__html: ` __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") (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: ` __html: `
daovoice('init', { daovoice('init', {
app_id: "${COMMENT_DAO_VOICE_ID}" app_id: "${COMMENT_DAO_VOICE_ID}"
@@ -202,34 +274,52 @@ const ExternalPlugin = (props) => {
daovoice('update'); 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聊天室 */} {/* gitter聊天室 */}
{COMMENT_GITTER_ROOM && (<> {COMMENT_GITTER_ROOM && (
<script src="https://sidecar.gitter.im/dist/sidecar.v1.js" async defer /> <>
<script async dangerouslySetInnerHTML={{ <script
src='https://sidecar.gitter.im/dist/sidecar.v1.js'
async
defer
/>
<script
async
dangerouslySetInnerHTML={{
__html: ` __html: `
((window.gitter = {}).chat = {}).options = { ((window.gitter = {}).chat = {}).options = {
room: '${COMMENT_GITTER_ROOM}' room: '${COMMENT_GITTER_ROOM}'
}; };
` `
}} /> }}
</>)} />
</>
)}
{/* 百度统计 */} {/* 百度统计 */}
{ANALYTICS_BAIDU_ID && ( {ANALYTICS_BAIDU_ID && (
<script async <script
dangerouslySetInnerHTML={{ async
__html: ` dangerouslySetInnerHTML={{
__html: `
var _hmt = _hmt || []; var _hmt = _hmt || [];
(function() { (function() {
var hm = document.createElement("script"); var hm = document.createElement("script");
@@ -238,29 +328,33 @@ const ExternalPlugin = (props) => {
s.parentNode.insertBefore(hm, s); s.parentNode.insertBefore(hm, s);
})(); })();
` `
}} }}
/> />
)} )}
{/* 站长统计 */} {/* 站长统计 */}
{ANALYTICS_CNZZ_ID && ( {ANALYTICS_CNZZ_ID && (
<script async <script
dangerouslySetInnerHTML={{ async
__html: ` 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")); 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 && (<> {ANALYTICS_GOOGLE_ID && (
<script async <>
src={`https://www.googletagmanager.com/gtag/js?id=${ANALYTICS_GOOGLE_ID}`} <script
/> async
<script async src={`https://www.googletagmanager.com/gtag/js?id=${ANALYTICS_GOOGLE_ID}`}
dangerouslySetInnerHTML={{ />
__html: ` <script
async
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);} function gtag(){dataLayer.push(arguments);}
gtag('js', new Date()); gtag('js', new Date());
@@ -268,14 +362,17 @@ const ExternalPlugin = (props) => {
page_path: window.location.pathname, page_path: window.location.pathname,
}); });
` `
}} }}
/> />
</>)} </>
)}
{/* Matomo 统计 */} {/* Matomo 统计 */}
{MATOMO_HOST_URL && MATOMO_SITE_ID && ( {MATOMO_HOST_URL && MATOMO_SITE_ID && (
<script async dangerouslySetInnerHTML={{ <script
__html: ` async
dangerouslySetInnerHTML={{
__html: `
var _paq = window._paq = window._paq || []; var _paq = window._paq = window._paq || [];
_paq.push(['trackPageView']); _paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']); _paq.push(['enableLinkTracking']);
@@ -287,10 +384,11 @@ const ExternalPlugin = (props) => {
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s); g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})(); })();
` `
}} /> }}
)} />
)}
</> </>
)
} }
export default ExternalPlugin export default ExternalPlugin

View File

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

View File

@@ -1,6 +1,8 @@
import { siteConfig } from '@/lib/config' import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global' 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 * Giscus评论 @see https://giscus.app/zh-CN
@@ -12,21 +14,34 @@ import Giscus from '@giscus/react'
const GiscusComponent = () => { const GiscusComponent = () => {
const { isDarkMode } = useGlobal() const { isDarkMode } = useGlobal()
const theme = isDarkMode ? 'dark' : 'light' 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 ( return (
<Giscus <div
repo={siteConfig('COMMENT_GISCUS_REPO')} id='giscus'
repoId={siteConfig('COMMENT_GISCUS_REPO_ID')} data-repo={siteConfig('COMMENT_GISCUS_REPO')}
categoryId={siteConfig('COMMENT_GISCUS_CATEGORY_ID')} data-repo-id={siteConfig('COMMENT_GISCUS_REPO_ID')}
mapping={siteConfig('COMMENT_GISCUS_MAPPING')} // data-category='{{ $.Site.Params.giscus.dataCategory }}'
reactionsEnabled={siteConfig('COMMENT_GISCUS_REACTIONS_ENABLED')} data-category-id={siteConfig('COMMENT_GISCUS_CATEGORY_ID')}
emitMetadata={siteConfig('COMMENT_GISCUS_EMIT_METADATA')} data-mapping={siteConfig('COMMENT_GISCUS_MAPPING')}
theme={theme} // data-strict='0'
inputPosition={siteConfig('COMMENT_GISCUS_INPUT_POSITION')} data-reactions-enabled={siteConfig('COMMENT_GISCUS_REACTIONS_ENABLED')}
lang={siteConfig('COMMENT_GISCUS_LANG')} data-emit-metadata={siteConfig('COMMENT_GISCUS_EMIT_METADATA')}
loading={siteConfig('COMMENT_GISCUS_LOADING')} data-input-position={siteConfig('COMMENT_GISCUS_INPUT_POSITION')}
crossorigin={siteConfig('COMMENT_GISCUS_CROSSORIGIN')} 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 { siteConfig } from '@/lib/config'
import { loadExternalResource } from '@/lib/utils' import { loadExternalResource } from '@/lib/utils'
import { useRouter } from 'next/router'
import { useEffect } from 'react' import { useEffect } from 'react'
function requestAd() { /**
const ads = document.getElementsByClassName('adsbygoogle') * 请求广告元素
* 调用后,实际只有当广告单元在页面中可见时才会真正获取
*/
function requestAd(ads) {
if (!ads || ads.length === 0) {
return
}
const adsbygoogle = window.adsbygoogle const adsbygoogle = window.adsbygoogle
if (adsbygoogle && ads.length > 0) { if (adsbygoogle && ads.length > 0) {
for (let i = 0; i <= ads.length; i++) { const observerOptions = {
try { root: null, // use the viewport as the root
const adStatus = ads[i].getAttribute('data-adsbygoogle-status') threshold: 0.5 // element is considered visible when 50% visible
if (!adStatus || adStatus !== 'done') { }
adsbygoogle.push(ads[i])
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 * @returns
*/ */
export default function GoogleAdsense() { export const initGoogleAdsense = async () => {
const initGoogleAdsense = () => { console.log('Load Adsense')
loadExternalResource( loadExternalResource(
`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${siteConfig('ADSENSE_GOOGLE_ID')}`, `https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${siteConfig('ADSENSE_GOOGLE_ID')}`,
'js' 'js'
).then(url => { ).then(url => {
setTimeout(() => {
requestAd()
}, 100)
})
}
const router = useRouter()
useEffect(() => {
// 延迟3秒加载
setTimeout(() => { setTimeout(() => {
initGoogleAdsense() // 页面加载完成后加载一次广告
}, 3000) const ads = document.getElementsByClassName('adsbygoogle')
}, [router]) 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') const newInsElement = document.createElement('ins')
newInsElement.className = 'adsbygoogle w-full py-1' newInsElement.className = 'adsbygoogle w-full py-1'
newInsElement.style.display = 'block' newInsElement.style.display = 'block'
newInsElement.setAttribute('data-ad-client', siteConfig('ADSENSE_GOOGLE_ID')) newInsElement.setAttribute(
newInsElement.setAttribute('data-adtest', siteConfig('ADSENSE_GOOGLE_TEST') ? 'on' : 'off') 'data-ad-client',
newInsElement.setAttribute('data-ad-slot', siteConfig('ADSENSE_GOOGLE_SLOT_AUTO')) 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-ad-format', 'auto')
newInsElement.setAttribute('data-full-width-responsive', 'true') newInsElement.setAttribute('data-full-width-responsive', 'true')

View File

@@ -12,12 +12,16 @@ export default function Live2D() {
const { theme, switchTheme } = useGlobal() const { theme, switchTheme } = useGlobal()
const showPet = JSON.parse(siteConfig('WIDGET_PET')) const showPet = JSON.parse(siteConfig('WIDGET_PET'))
const petLink = siteConfig('WIDGET_PET_LINK') const petLink = siteConfig('WIDGET_PET_LINK')
const petSwitchTheme = siteConfig('WIDGET_PET_SWITCH_THEME')
useEffect(() => { useEffect(() => {
if (showPet && !isMobile()) { if (showPet && !isMobile()) {
Promise.all([ Promise.all([
loadExternalResource('https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget@latest/live2d.min.js', 'js') loadExternalResource(
]).then((e) => { 'https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget@latest/live2d.min.js',
'js'
)
]).then(e => {
if (typeof window?.loadlive2d !== 'undefined') { if (typeof window?.loadlive2d !== 'undefined') {
// https://github.com/xiazeyu/live2d-widget-models // https://github.com/xiazeyu/live2d-widget-models
try { try {
@@ -31,7 +35,7 @@ export default function Live2D() {
}, [theme]) }, [theme])
function handleClick() { function handleClick() {
if (JSON.parse(siteConfig('WIDGET_PET_SWITCH_THEME'))) { if (petSwitchTheme) {
switchTheme() switchTheme()
} }
} }
@@ -40,9 +44,15 @@ export default function Live2D() {
return <></> return <></>
} }
return <canvas id="live2d" width="280" height="250" onClick={handleClick} return (
className="cursor-grab" <canvas
onMouseDown={(e) => e.target.classList.add('cursor-grabbing')} id='live2d'
onMouseUp={(e) => e.target.classList.remove('cursor-grabbing')} 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 { useRouter } from 'next/router'
import NProgress from 'nprogress' import { useEffect, useState } from 'react'
import { useEffect } from 'react'
/** /**
* 出现页面加载进度条 * 出现页面加载进度条
*/ */
export default function LoadingProgress() { export default function LoadingProgress() {
const router = useRouter() const router = useRouter()
const [NProgress, setNProgress] = useState(null)
// 加载进度条 // 加载进度条
useEffect(() => { useEffect(() => {
const handleStart = (url) => { loadExternalResource(
NProgress.start() '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 = () => { const handleStop = () => {
NProgress.done() NProgress?.done()
} }
router.events.on('routeChangeStart', handleStart) router.events.on('routeChangeStart', handleStart)

View File

@@ -1,53 +1,52 @@
import { siteConfig } from '@/lib/config' import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import copy from 'copy-to-clipboard'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useState } from 'react' import { useEffect, useState } from 'react'
import { import {
FacebookShareButton,
FacebookIcon,
FacebookMessengerShareButton,
FacebookMessengerIcon,
RedditShareButton,
RedditIcon,
LineShareButton,
LineIcon,
EmailShareButton,
EmailIcon, EmailIcon,
TwitterShareButton, EmailShareButton,
TwitterIcon, FacebookIcon,
TelegramShareButton, FacebookMessengerIcon,
TelegramIcon, FacebookMessengerShareButton,
WhatsappShareButton, FacebookShareButton,
WhatsappIcon, HatenaIcon,
LinkedinShareButton, HatenaShareButton,
InstapaperIcon,
InstapaperShareButton,
LineIcon,
LineShareButton,
LinkedinIcon, LinkedinIcon,
PinterestShareButton, LinkedinShareButton,
PinterestIcon,
VKIcon,
VKShareButton,
OKShareButton,
OKIcon,
TumblrShareButton,
TumblrIcon,
LivejournalIcon, LivejournalIcon,
LivejournalShareButton, LivejournalShareButton,
MailruShareButton,
MailruIcon, MailruIcon,
MailruShareButton,
OKIcon,
OKShareButton,
PinterestIcon,
PinterestShareButton,
PocketIcon,
PocketShareButton,
RedditIcon,
RedditShareButton,
TelegramIcon,
TelegramShareButton,
TumblrIcon,
TumblrShareButton,
TwitterIcon,
TwitterShareButton,
VKIcon,
VKShareButton,
ViberIcon, ViberIcon,
ViberShareButton, ViberShareButton,
WorkplaceShareButton,
WorkplaceIcon,
WeiboShareButton,
WeiboIcon, WeiboIcon,
PocketShareButton, WeiboShareButton,
PocketIcon, WhatsappIcon,
InstapaperShareButton, WhatsappShareButton,
InstapaperIcon, WorkplaceIcon,
HatenaShareButton, WorkplaceShareButton
HatenaIcon
} from 'react-share' } from 'react-share'
const QrCode = dynamic(() => import('@/components/QrCode'), { ssr: false }) const QrCode = dynamic(() => import('@/components/QrCode'), { ssr: false })
@@ -59,10 +58,11 @@ const QrCode = dynamic(() => import('@/components/QrCode'), { ssr: false })
*/ */
const ShareButtons = ({ post }) => { const ShareButtons = ({ post }) => {
const router = useRouter() const router = useRouter()
const shareUrl = siteConfig('LINK') + router.asPath const [shareUrl, setShareUrl] = useState(siteConfig('LINK') + router.asPath)
const title = post.title || siteConfig('TITLE') const title = post.title || siteConfig('TITLE')
const image = post.pageCover 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 services = siteConfig('POSTS_SHARE_SERVICES').split(',')
const titleWithSiteInfo = title + ' | ' + siteConfig('TITLE') const titleWithSiteInfo = title + ' | ' + siteConfig('TITLE')
@@ -70,8 +70,8 @@ const ShareButtons = ({ post }) => {
const [qrCodeShow, setQrCodeShow] = useState(false) const [qrCodeShow, setQrCodeShow] = useState(false)
const copyUrl = () => { const copyUrl = () => {
copy(shareUrl) navigator?.clipboard?.writeText(shareUrl)
alert(locale.COMMON.URL_COPIED) alert(locale.COMMON.URL_COPIED + ' \n' + shareUrl)
} }
const openPopover = () => { const openPopover = () => {
@@ -81,300 +81,308 @@ const ShareButtons = ({ post }) => {
setQrCodeShow(false) setQrCodeShow(false)
} }
useEffect(() => {
setShareUrl(window.location.href)
}, [])
return ( return (
<> <>
{services.map(singleService => { {services.map(singleService => {
if (singleService === 'facebook') { if (singleService === 'facebook') {
return ( return (
<FacebookShareButton <FacebookShareButton
key={singleService} key={singleService}
url={shareUrl} url={shareUrl}
className="mx-1" className='mx-1'>
> <FacebookIcon size={32} round />
<FacebookIcon size={32} round /> </FacebookShareButton>
</FacebookShareButton> )
) }
} if (singleService === 'messenger') {
if (singleService === 'messenger') { return (
return ( <FacebookMessengerShareButton
<FacebookMessengerShareButton key={singleService}
key={singleService} url={shareUrl}
url={shareUrl} appId={siteConfig('FACEBOOK_APP_ID')}
appId={siteConfig('FACEBOOK_APP_ID')} className='mx-1'>
className="mx-1" <FacebookMessengerIcon size={32} round />
> </FacebookMessengerShareButton>
<FacebookMessengerIcon size={32} round /> )
</FacebookMessengerShareButton> }
) if (singleService === 'line') {
} return (
if (singleService === 'line') { <LineShareButton
return ( key={singleService}
<LineShareButton url={shareUrl}
key={singleService} className='mx-1'>
url={shareUrl} <LineIcon size={32} round />
className="mx-1" </LineShareButton>
> )
<LineIcon size={32} round /> }
</LineShareButton> if (singleService === 'reddit') {
) return (
} <RedditShareButton
if (singleService === 'reddit') { key={singleService}
return ( url={shareUrl}
<RedditShareButton title={titleWithSiteInfo}
key={singleService} windowWidth={660}
url={shareUrl} windowHeight={460}
title={titleWithSiteInfo} className='mx-1'>
windowWidth={660} <RedditIcon size={32} round />
windowHeight={460} </RedditShareButton>
className="mx-1" )
> }
<RedditIcon size={32} round /> if (singleService === 'email') {
</RedditShareButton> return (
) <EmailShareButton
} key={singleService}
if (singleService === 'email') { url={shareUrl}
return ( subject={titleWithSiteInfo}
<EmailShareButton body={body}
key={singleService} className='mx-1'>
url={shareUrl} <EmailIcon size={32} round />
subject={titleWithSiteInfo} </EmailShareButton>
body={body} )
className="mx-1" }
> if (singleService === 'twitter') {
<EmailIcon size={32} round /> return (
</EmailShareButton> <TwitterShareButton
) key={singleService}
} url={shareUrl}
if (singleService === 'twitter') { title={titleWithSiteInfo}
return ( className='mx-1'>
<TwitterShareButton <TwitterIcon size={32} round />
key={singleService} </TwitterShareButton>
url={shareUrl} )
title={titleWithSiteInfo} }
className="mx-1" if (singleService === 'telegram') {
> return (
<TwitterIcon size={32} round /> <TelegramShareButton
</TwitterShareButton> key={singleService}
) url={shareUrl}
} title={titleWithSiteInfo}
if (singleService === 'telegram') { className='mx-1'>
return ( <TelegramIcon size={32} round />
<TelegramShareButton </TelegramShareButton>
key={singleService} )
url={shareUrl} }
title={titleWithSiteInfo} if (singleService === 'whatsapp') {
className="mx-1" return (
> <WhatsappShareButton
<TelegramIcon size={32} round /> key={singleService}
</TelegramShareButton> url={shareUrl}
) title={titleWithSiteInfo}
} separator=':: '
if (singleService === 'whatsapp') { className='mx-1'>
return ( <WhatsappIcon size={32} round />
<WhatsappShareButton </WhatsappShareButton>
key={singleService} )
url={shareUrl} }
title={titleWithSiteInfo} if (singleService === 'linkedin') {
separator=":: " return (
className="mx-1" <LinkedinShareButton
> key={singleService}
<WhatsappIcon size={32} round /> url={shareUrl}
</WhatsappShareButton> className='mx-1'>
) <LinkedinIcon size={32} round />
} </LinkedinShareButton>
if (singleService === 'linkedin') { )
return ( }
<LinkedinShareButton if (singleService === 'pinterest') {
key={singleService} return (
url={shareUrl} <PinterestShareButton
className="mx-1" key={singleService}
> url={shareUrl}
<LinkedinIcon size={32} round /> media={image}
</LinkedinShareButton> className='mx-1'>
) <PinterestIcon size={32} round />
} </PinterestShareButton>
if (singleService === 'pinterest') { )
return ( }
<PinterestShareButton if (singleService === 'vkshare') {
key={singleService} return (
url={shareUrl} <VKShareButton
media={image} key={singleService}
className="mx-1" url={shareUrl}
> image={image}
<PinterestIcon size={32} round /> className='mx-1'>
</PinterestShareButton> <VKIcon size={32} round />
) </VKShareButton>
} )
if (singleService === 'vkshare') { }
return ( if (singleService === 'okshare') {
<VKShareButton return (
key={singleService} <OKShareButton
url={shareUrl} key={singleService}
image={image} url={shareUrl}
className="mx-1" image={image}
> className='mx-1'>
<VKIcon size={32} round /> <OKIcon size={32} round />
</VKShareButton> </OKShareButton>
) )
} }
if (singleService === 'okshare') { if (singleService === 'tumblr') {
return ( return (
<OKShareButton <TumblrShareButton
key={singleService} key={singleService}
url={shareUrl} url={shareUrl}
image={image} title={titleWithSiteInfo}
className="mx-1" className='mx-1'>
> <TumblrIcon size={32} round />
<OKIcon size={32} round /> </TumblrShareButton>
</OKShareButton> )
) }
} if (singleService === 'livejournal') {
if (singleService === 'tumblr') { return (
return ( <LivejournalShareButton
<TumblrShareButton key={singleService}
key={singleService} url={shareUrl}
url={shareUrl} title={titleWithSiteInfo}
title={titleWithSiteInfo} description={shareUrl}
className="mx-1" className='mx-1'>
> <LivejournalIcon size={32} round />
<TumblrIcon size={32} round /> </LivejournalShareButton>
</TumblrShareButton> )
) }
} if (singleService === 'mailru') {
if (singleService === 'livejournal') { return (
return ( <MailruShareButton
<LivejournalShareButton key={singleService}
key={singleService} url={shareUrl}
url={shareUrl} title={titleWithSiteInfo}
title={titleWithSiteInfo} className='mx-1'>
description={shareUrl} <MailruIcon size={32} round />
className="mx-1" </MailruShareButton>
> )
<LivejournalIcon size={32} round /> }
</LivejournalShareButton> if (singleService === 'viber') {
) return (
} <ViberShareButton
if (singleService === 'mailru') { key={singleService}
return ( url={shareUrl}
<MailruShareButton title={titleWithSiteInfo}
key={singleService} className='mx-1'>
url={shareUrl} <ViberIcon size={32} round />
title={titleWithSiteInfo} </ViberShareButton>
className="mx-1" )
> }
<MailruIcon size={32} round /> if (singleService === 'workplace') {
</MailruShareButton> return (
) <WorkplaceShareButton
} key={singleService}
if (singleService === 'viber') { url={shareUrl}
return ( quote={titleWithSiteInfo}
<ViberShareButton className='mx-1'>
key={singleService} <WorkplaceIcon size={32} round />
url={shareUrl} </WorkplaceShareButton>
title={titleWithSiteInfo} )
className="mx-1" }
> if (singleService === 'weibo') {
<ViberIcon size={32} round /> return (
</ViberShareButton> <WeiboShareButton
) key={singleService}
} url={shareUrl}
if (singleService === 'workplace') { title={titleWithSiteInfo}
return ( image={image}
<WorkplaceShareButton className='mx-1'>
key={singleService} <WeiboIcon size={32} round />
url={shareUrl} </WeiboShareButton>
quote={titleWithSiteInfo} )
className="mx-1" }
> if (singleService === 'pocket') {
<WorkplaceIcon size={32} round /> return (
</WorkplaceShareButton> <PocketShareButton
) key={singleService}
} url={shareUrl}
if (singleService === 'weibo') { title={titleWithSiteInfo}
return ( className='mx-1'>
<WeiboShareButton <PocketIcon size={32} round />
key={singleService} </PocketShareButton>
url={shareUrl} )
title={titleWithSiteInfo} }
image={image} if (singleService === 'instapaper') {
className="mx-1" return (
> <InstapaperShareButton
<WeiboIcon size={32} round /> key={singleService}
</WeiboShareButton> url={shareUrl}
) title={titleWithSiteInfo}
} className='mx-1'>
if (singleService === 'pocket') { <InstapaperIcon size={32} round />
return ( </InstapaperShareButton>
<PocketShareButton )
key={singleService} }
url={shareUrl} if (singleService === 'hatena') {
title={titleWithSiteInfo} return (
className="mx-1" <HatenaShareButton
> key={singleService}
<PocketIcon size={32} round /> url={shareUrl}
</PocketShareButton> title={titleWithSiteInfo}
) windowWidth={660}
} windowHeight={460}
if (singleService === 'instapaper') { className='mx-1'>
return ( <HatenaIcon size={32} round />
<InstapaperShareButton </HatenaShareButton>
key={singleService} )
url={shareUrl} }
title={titleWithSiteInfo} if (singleService === 'qq') {
className="mx-1" return (
> <button
<InstapaperIcon size={32} round /> key={singleService}
</InstapaperShareButton> className='cursor-pointer bg-blue-600 text-white rounded-full mx-1'>
) <a
} target='_blank'
if (singleService === 'hatena') { rel='noreferrer'
return ( aria-label='Share by QQ'
<HatenaShareButton href={`http://connect.qq.com/widget/shareqq/index.html?url=${shareUrl}&sharesource=qzone&title=${title}&desc=${body}`}>
key={singleService} <i className='fab fa-qq w-8' />
url={shareUrl} </a>
title={titleWithSiteInfo} </button>
windowWidth={660} )
windowHeight={460} }
className="mx-1" if (singleService === 'wechat') {
> return (
<HatenaIcon size={32} round /> <button
</HatenaShareButton> onMouseEnter={openPopover}
) onMouseLeave={closePopover}
} aria-label={singleService}
if (singleService === 'qq') { key={singleService}
return <button key={singleService} className='cursor-pointer bg-blue-600 text-white rounded-full mx-1'> className='cursor-pointer bg-green-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}`} > <div id='wechat-button'>
<i className='fab fa-qq w-8' /> <i className='fab fa-weixin w-8' />
</a> </div>
</button> <div className='absolute'>
} <div
if (singleService === 'wechat') { id='pop'
return <button onMouseEnter={openPopover} onMouseLeave={closePopover} aria-label={singleService} key={singleService} className='cursor-pointer bg-green-600 text-white rounded-full mx-1'> className={
<div id='wechat-button'> (qrCodeShow ? 'opacity-100 ' : ' invisible opacity-0') +
<i className='fab fa-weixin w-8' /> ' z-40 absolute bottom-10 -left-10 bg-white shadow-xl transition-all duration-200 text-center'
</div> }>
<div className='absolute'> <div className='p-2 mt-1 w-28 h-28'>
<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'}> {qrCodeShow && <QrCode value={shareUrl} />}
<div className='p-2 mt-1 w-28 h-28'> </div>
{ qrCodeShow && <QrCode value={shareUrl}/> } <span className='text-black font-semibold p-1 rounded-t-lg text-sm mx-auto mb-1'>
</div> {locale.COMMON.SCAN_QR_CODE}
<span className='text-black font-semibold p-1 rounded-t-lg text-sm mx-auto mb-1'> </span>
{locale.COMMON.SCAN_QR_CODE} </div>
</span> </div>
</div> </button>
</div> )
</button> }
} if (singleService === 'link') {
if (singleService === 'link') { return (
return <button aria-label={singleService} key={singleService} className='cursor-pointer bg-yellow-500 text-white rounded-full mx-1'> <button
<div alt={locale.COMMON.URL_COPIED} onClick={copyUrl} > aria-label={singleService}
<i className='fas fa-link w-8' /> key={singleService}
</div> className='cursor-pointer bg-yellow-500 text-white rounded-full mx-1'>
</button> <div alt={locale.COMMON.URL_COPIED} onClick={copyUrl}>
} <i className='fas fa-link w-8' />
return <></> </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 BLOG from '@/blog.config'
import FileCache from './local_file_cache'
import MemoryCache from './memory_cache'
/** /**
* 为减少频繁接口请求notion数据将被缓存 * 为减少频繁接口请求notion数据将被缓存
@@ -39,9 +38,7 @@ export async function delCacheData(key) {
* @returns * @returns
*/ */
function getApi() { function getApi() {
if (process.env.MONGO_DB_URL && process.env.MONGO_DB_NAME) { if (process.env.ENABLE_FILE_CACHE) {
return MongoCache
} else if (process.env.ENABLE_FILE_CACHE) {
return FileCache return FileCache
} else { } else {
return MemoryCache 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) { if (!val && extendConfig) {
val = extendConfig[key] val = extendConfig[key]
} }
@@ -64,24 +64,37 @@ export const siteConfig = (key, defaultVal = null, extendConfig = null) => {
if (!val) { if (!val) {
return defaultVal return defaultVal
} else { }
if (typeof val === 'string') {
if (val === 'true' || val === 'false') { // 从Notion_CONFIG读取的配置通常都是字符串适当转义
return JSON.parse(val) if (typeof val === 'string') {
} // 解析布尔
if (/^\d+$/.test(val)) { if (val === 'true' || val === 'false') {
// 如果是数字使用parseFloat或者parseInt将字符串转换为数字 return JSON.parse(val)
return parseInt(val)
}
return val
} else {
try {
return JSON.parse(val)
} catch (error) {
// 如果值是一个字符串但不是有效的 JSON 格式,直接返回字符串
return 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 { compressImage, mapImgUrl } from '@/lib/notion/mapImage'
import { deepClone } from '@/lib/utils' import { deepClone } from '@/lib/utils'
import { idToUuid } from 'notion-utils' import { idToUuid } from 'notion-utils'
import { extractLangId, extractLangPrefix } from '../utils/pageId'
export { getAllTags } from '../notion/getAllTags' export { getAllTags } from '../notion/getAllTags'
export { getPost } from '../notion/getNotionPost' export { getPost } from '../notion/getNotionPost'
@@ -18,18 +19,62 @@ export { getPostBlocks } from '../notion/getPostBlocks'
* 获取博客数据; 基于Notion实现 * 获取博客数据; 基于Notion实现
* @param {*} pageId * @param {*} pageId
* @param {*} from * @param {*} from
* @param latestPostCount 截取最新文章数量 * @param {*} locale 语言 zh|en|jp 等等
* @param categoryCount
* @param tagsCount 截取标签数量
* @param pageType 过滤的文章类型,数组格式 ['Page','Post']
* @returns * @returns
* *
*/ */
export async function getGlobalData({ pageId = BLOG.NOTION_PAGE_ID, from }) { export async function getGlobalData({
// 从notion获取 pageId = BLOG.NOTION_PAGE_ID,
const data = await getNotionPageData({ pageId, from }) 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) const db = deepClone(data)
// 减少返回给前端的数据,减少流量损耗
delete db.block delete db.block
delete db.schema delete db.schema
delete db.rawMetadata delete db.rawMetadata
@@ -47,10 +92,12 @@ export async function getGlobalData({ pageId = BLOG.NOTION_PAGE_ID, from }) {
if (db?.post) { if (db?.post) {
db.post = cleanBlock(db?.post) db.post = cleanBlock(db?.post)
} }
return db return db
} }
/**
* 清理block数据
*/
function cleanBlock(post) { function cleanBlock(post) {
const pageBlock = post?.blockMap?.block const pageBlock = post?.blockMap?.block
for (const i in pageBlock) { for (const i in pageBlock) {
@@ -98,28 +145,6 @@ function getLatestPosts({ allPages, from, latestPostCount }) {
return latestPosts.slice(0, 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生成菜单 * 旧版本不读取Menu菜单而是读取type=Page生成菜单
@@ -167,7 +192,7 @@ function getCustomMenu({ collectionData }) {
if (menuPages && menuPages.length > 0) { if (menuPages && menuPages.length > 0) {
menuPages.forEach(e => { menuPages.forEach(e => {
e.show = true 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.target = '_blank'
e.to = e.slug e.to = e.slug
} else { } else {
@@ -225,13 +250,24 @@ function getCategoryOptions(schema) {
* @param from * @param from
* @returns {Promise<{title,description,pageCover,icon}>} * @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 title = collection?.name?.[0][0] || BLOG.TITLE
const description = collection?.description const description = collection?.description
? Object.assign(collection).description[0][0] ? Object.assign(collection).description[0][0]
: BLOG.DESCRIPTION : BLOG.DESCRIPTION
const pageCover = collection?.cover const pageCover = collection?.cover
? mapImgUrl(collection?.cover, block[idToUuid(BLOG.NOTION_PAGE_ID)]?.value) ? mapImgUrl(collection?.cover, block[pageId]?.value)
: BLOG.HOME_BANNER_IMAGE : BLOG.HOME_BANNER_IMAGE
// 用户头像压缩一下 // 用户头像压缩一下
let icon = compressImage( let icon = compressImage(
@@ -308,6 +344,7 @@ const EmptyData = pageId => {
status: 'Published', status: 'Published',
type: 'Post', type: 'Post',
slug: '13a171332816461db29d50e9f575b00d', slug: '13a171332816461db29d50e9f575b00d',
pageCoverThumbnail: BLOG.HOME_BANNER_IMAGE,
date: { date: {
start_date: '2023-04-24', start_date: '2023-04-24',
lastEditedDay: '2023-04-24', lastEditedDay: '2023-04-24',
@@ -426,7 +463,7 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
// 站点配置优先读取配置表格否则读取blog.config.js 文件 // 站点配置优先读取配置表格否则读取blog.config.js 文件
const NOTION_CONFIG = (await getConfigMapFromConfigPage(collectionData)) || {} const NOTION_CONFIG = (await getConfigMapFromConfigPage(collectionData)) || {}
const siteInfo = getSiteInfo({ collection, block }) const siteInfo = getSiteInfo({ collection, block, pageId })
// 查找所有的Post和Page // 查找所有的Post和Page
const allPages = collectionData.filter(post => { const allPages = collectionData.filter(post => {

View File

@@ -1,8 +1,17 @@
import { generateLocaleDict, initLocale, saveLangToLocalStorage } from './lang' import {
import { createContext, useContext, useEffect, useState } from 'react' THEMES,
initDarkMode,
saveDarkModeToLocalStorage
} from '@/themes/theme'
import { APPEARANCE, LANG, NOTION_PAGE_ID, THEME } from 'blog.config'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { THEMES, initDarkMode, saveDarkModeToLocalStorage } from '@/themes/theme' import { createContext, useContext, useEffect, useState } from 'react'
import { APPEARANCE, LANG, THEME } from 'blog.config' import {
generateLocaleDict,
initLocale,
redirectUserLang,
saveLangToLocalStorage
} from './lang'
const GlobalContext = createContext() const GlobalContext = createContext()
/** /**
@@ -12,9 +21,18 @@ const GlobalContext = createContext()
* @constructor * @constructor
*/ */
export function GlobalContextProvider(props) { 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 [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 [theme, setTheme] = useState(NOTION_CONFIG?.THEME || THEME) // 默认博客主题
const defaultDarkMode = NOTION_CONFIG?.APPEARANCE || APPEARANCE const defaultDarkMode = NOTION_CONFIG?.APPEARANCE || APPEARANCE
const [isDarkMode, updateDarkMode] = useState(defaultDarkMode === 'dark') // 默认深色模式 const [isDarkMode, updateDarkMode] = useState(defaultDarkMode === 'dark') // 默认深色模式
@@ -48,6 +66,7 @@ export function GlobalContextProvider(props) {
/** /**
* 更新语言 * 更新语言
* 这里是代码级别的多语言,整个站点和文章内容的多语言不在此处理
*/ */
function changeLang(lang) { function changeLang(lang) {
if (lang) { if (lang) {
@@ -60,6 +79,7 @@ export function GlobalContextProvider(props) {
useEffect(() => { useEffect(() => {
initDarkMode(updateDarkMode, defaultDarkMode) initDarkMode(updateDarkMode, defaultDarkMode)
initLocale(lang, locale, updateLang, updateLocale) 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 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 zhHK from './lang/zh-HK'
import zhTW from './lang/zh-TW' import zhTW from './lang/zh-TW'
import frFR from './lang/fr-FR' import { extractLangPrefix } from './utils/pageId'
import trTR from './lang/tr-TR'
import jaJP from './lang/ja-JP'
import { getQueryVariable, isBrowser, mergeDeep } from '@/lib/utils'
/** /**
* 在这里配置所有支持的语言 * 在这里配置所有支持的语言
@@ -43,7 +44,9 @@ export function generateLocaleDict(langString) {
// 然后尝试匹配只有语言匹配的情况 // 然后尝试匹配只有语言匹配的情况
if (!userLocale) { if (!userLocale) {
const languageOnlyLocales = supportedLocales.filter(locale => locale.startsWith(language)) const languageOnlyLocales = supportedLocales.filter(locale =>
locale.startsWith(language)
)
if (languageOnlyLocales.length > 0) { if (languageOnlyLocales.length > 0) {
userLocale = LANGS[languageOnlyLocales[0]] userLocale = LANGS[languageOnlyLocales[0]]
} }
@@ -51,7 +54,9 @@ export function generateLocaleDict(langString) {
// 如果还没匹配到,则返回最接近的语言包 // 如果还没匹配到,则返回最接近的语言包
if (!userLocale) { if (!userLocale) {
const fallbackLocale = supportedLocales.find(locale => locale.startsWith('en')) const fallbackLocale = supportedLocales.find(locale =>
locale.startsWith('en')
)
userLocale = LANGS[fallbackLocale] userLocale = LANGS[fallbackLocale]
} }
@@ -64,7 +69,11 @@ export function generateLocaleDict(langString) {
*/ */
export function initLocale(lang, locale, changeLang, changeLocale) { export function initLocale(lang, locale, changeLang, changeLocale) {
if (isBrowser) { if (isBrowser) {
const queryLang = getQueryVariable('lang') || loadLangFromLocalStorage() // 用户请求的预研
const queryLang =
getQueryVariable('locale') ||
getQueryVariable('lang') ||
loadLangFromLocalStorage()
let currentLang = lang let currentLang = lang
if (queryLang && queryLang !== 'undefined' && queryLang !== lang) { if (queryLang && queryLang !== 'undefined' && queryLang !== lang) {
currentLang = queryLang currentLang = queryLang
@@ -91,6 +100,39 @@ export const loadLangFromLocalStorage = () => {
* 保存语言 * 保存语言
* @param newTheme * @param newTheme
*/ */
export const saveLangToLocalStorage = (lang) => { export const saveLangToLocalStorage = lang => {
localStorage.setItem('lang', 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 { getDateValue, getTextContent } from 'notion-utils'
import { getPostBlocks } from './getPostBlocks' import { deepClone } from '../utils'
import getAllPageIds from './getAllPageIds' import getAllPageIds from './getAllPageIds'
import { getPostBlocks } from './getPostBlocks'
/** /**
* 从Notion中读取Config配置表 * 从Notion中读取Config配置表
@@ -23,8 +24,15 @@ export async function getConfigMapFromConfigPage(allPages) {
console.warn('[Notion配置] 忽略的配置') console.warn('[Notion配置] 忽略的配置')
return null return null
} }
// 找到Config类
const configPage = allPages?.find(post => { 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) { if (!configPage) {
@@ -43,21 +51,26 @@ export async function getConfigMapFromConfigPage(allPages) {
} }
if (!content) { if (!content) {
console.warn('[Notion配置] 未找到配置表格', pageRecordMap.block[configPageId], pageRecordMap.block[configPageId].value) console.warn(
'[Notion配置] 未找到配置表格',
pageRecordMap.block[configPageId],
pageRecordMap.block[configPageId].value
)
return null return null
} }
// 找到配置文件中的database // 找到PAGE文件中的database
// for (const contentId of content) {
// console.log('内容', contentId, configPageRecordMap.block[contentId].value.type === 'collection_view')
// }
const configTableId = content?.find(contentId => { const configTableId = content?.find(contentId => {
return pageRecordMap.block[contentId].value.type === 'collection_view' return pageRecordMap.block[contentId].value.type === 'collection_view'
}) })
// eslint-disable-next-line no-constant-condition, no-self-compare // eslint-disable-next-line no-constant-condition, no-self-compare
if (!configTableId) { if (!configTableId) {
console.warn('[Notion配置]未找到配置表格数据', pageRecordMap.block[configPageId], pageRecordMap.block[configPageId].value) console.warn(
'[Notion配置]未找到配置表格数据',
pageRecordMap.block[configPageId],
pageRecordMap.block[configPageId].value
)
return null return null
} }
@@ -67,7 +80,8 @@ export async function getConfigMapFromConfigPage(allPages) {
const rawMetadata = databaseRecordMap.value const rawMetadata = databaseRecordMap.value
// Check Type Page-Database和Inline-Database // Check Type Page-Database和Inline-Database
if ( 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`) console.error(`pageId "${configTableId}" is not a database`)
return null return null
@@ -79,9 +93,21 @@ export async function getConfigMapFromConfigPage(allPages) {
const collectionView = pageRecordMap.collection_view const collectionView = pageRecordMap.collection_view
const schema = collection?.schema const schema = collection?.schema
const viewIds = rawMetadata?.view_ids const viewIds = rawMetadata?.view_ids
const pageIds = getAllPageIds(collectionQuery, collectionId, collectionView, viewIds) const pageIds = getAllPageIds(
collectionQuery,
collectionId,
collectionView,
viewIds
)
if (pageIds?.length === 0) { 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++) { 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 图床转换为永久地址 // 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' const isImgBlock = BLOG.IMG_URL_TYPE === 'Notion' || type !== 'block'
if (isNotionSignImg && isImgBlock) { 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 (!isEmoji(ret) && ret.indexOf('notion.so/images/page-cover') < 0) {
if (BLOG.RANDOM_IMAGE_URL) { if (BLOG.RANDOM_IMAGE_URL) {
// 只有配置了随机图片接口,才会替换图片 // 只有配置了随机图片接口,才会替换图片
const texts = BLOG.RANDOM_IMAGE_REPLACE_TEXT const texts = BLOG.RANDOM_IMAGE_REPLACE_TEXT
let isReplace = false; let isReplace = false
if (texts) { if (texts) {
const textArr = texts.split(',') 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 const width = block?.format?.block_width
ret = compressImage(ret, width) ret = compressImage(ret, width)
} }
@@ -72,8 +86,9 @@ const mapImgUrl = (img, block, type = 'block', from = 'post') => {
* @returns * @returns
*/ */
function isEmoji(str) { 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; const emojiRegex =
return emojiRegex.test(str); /[\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) const params = new URLSearchParams(urlObj.search)
// Notion图床 // 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('width', width)
params.set('cache', 'v2') params.set('cache', 'v2')
// 生成新的URL // 生成新的URL
@@ -117,11 +135,11 @@ const compressImage = (image, width, quality = 50, fmt = 'webp') => {
return urlObj.toString() return urlObj.toString()
} else if (image.indexOf('https://your_picture_bed') === 0) { } else if (image.indexOf('https://your_picture_bed') === 0) {
// 此处还可以添加您的自定义图传的封面图压缩参数。 // 此处还可以添加您的自定义图传的封面图压缩参数。
// .e.g // .e.g
return 'do_somethin_here' return 'do_somethin_here'
} }
return image 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使用很方便 * WOWjs动画结合animate.css使用很方便
* 是data-aos的平替 aos ≈ wowjs + animate * 是data-aos的平替 aos ≈ wowjs + animate
*/ */
export const loadWowJS = async () => { export const loadWowJS = async () => {
await loadExternalResource('/css/wow/animate.css', 'css'); 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(
'https://cdnjs.cloudflare.com/ajax/libs/wow/1.1.2/wow.min.js',
'js'
)
// 配合animatecss 实现延时滚动动画和AOS动画相似 // 配合animatecss 实现延时滚动动画和AOS动画相似
const WOW = window.WOW; const WOW = window.WOW
console.log('加载WOW动画', WOW)
if (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 BLOG from '@/blog.config'
import ReactDOMServer from 'react-dom/server'
import { getPostBlocks } from '@/lib/db/getSiteData'
import NotionPage from '@/components/NotionPage' 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内容 * 生成RSS内容
* @param {*} post * @param {*} post
* @returns * @returns
*/ */
const createFeedContent = async post => { const createFeedContent = async post => {
// 加密的文章内容只返回摘要 // 加密的文章内容只返回摘要
if (post.password && post.password !== '') { if (post.password && post.password !== '') {
@@ -20,30 +21,35 @@ const createFeedContent = async post => {
post.blockMap = blockMap post.blockMap = blockMap
const content = ReactDOMServer.renderToString(<NotionPage post={post} />) const content = ReactDOMServer.renderToString(<NotionPage post={post} />)
const regexExp = 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, '') 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 year = new Date().getFullYear()
const feed = new Feed({ const feed = new Feed({
title: BLOG.TITLE, title: BLOG.TITLE,
description: BLOG.DESCRIPTION, description: BLOG.DESCRIPTION,
link: `${BLOG.LINK}/${BLOG.SUB_PATH}`, link: `${link}/${subPath}`,
language: BLOG.LANG, language: lang,
favicon: `${BLOG.LINK}/favicon.png`, favicon: `${link}/favicon.png`,
copyright: `All rights reserved ${year}, ${BLOG.AUTHOR}`, copyright: `All rights reserved ${year}, ${author}`,
author: { author: {
name: BLOG.AUTHOR, name: author,
email: BLOG.CONTACT_EMAIL, email: BLOG.CONTACT_EMAIL,
link: BLOG.LINK link: link
} }
}) })
for (const post of posts) { for (const post of posts) {
feed.addItem({ feed.addItem({
title: post.title, title: post.title,
link: `${BLOG.LINK}/${post.slug}`, link: `${link}/${post.slug}`,
description: post.summary, description: post.summary,
content: await createFeedContent(post), content: await createFeedContent(post),
date: new Date(post?.publishDay) 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 fs = require('fs')
const path = require('path') const path = require('path')
const BLOG = require('./blog.config') const BLOG = require('./blog.config')
const { extractLangPrefix } = require('./lib/utils/pageId')
// 打包时是否分析代码
const withBundleAnalyzer = require('@next/bundle-analyzer')({ const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: BLOG.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 * @param {*} directory
@@ -27,8 +52,7 @@ function scanSubdirectories(directory) {
return subdirectories return subdirectories
} }
// 扫描项目 /themes下的目录名
const themes = scanSubdirectories(path.resolve(__dirname, 'themes'))
module.exports = withBundleAnalyzer({ module.exports = withBundleAnalyzer({
images: { images: {
// 图片压缩 // 图片压缩
@@ -55,8 +79,52 @@ module.exports = withBundleAnalyzer({
} }
] ]
}, },
// 多语言
i18n: {
defaultLocale: BLOG.LANG.slice(0, 2),
// 支持的所有多语言,按需填写即可
locales
},
// 重写url
async rewrites() { 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 [ return [
...langsRewrites,
// 伪静态重写
{ {
source: '/:path*.html', source: '/:path*.html',
destination: '/:path*' destination: '/:path*'
@@ -84,17 +152,9 @@ module.exports = withBundleAnalyzer({
] ]
}, },
webpack: (config, { dev, isServer }) => { 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 配置,将动态路径映射到实际路径 // 动态主题:添加 resolve.alias 配置,将动态路径映射到实际路径
if (!isServer) { if (!isServer) {
console.log('[加载主题]', path.resolve(__dirname, 'themes', THEME)) console.log('[默认主题]', path.resolve(__dirname, 'themes', THEME))
} }
config.resolve.alias['@theme-components'] = path.resolve( config.resolve.alias['@theme-components'] = path.resolve(
__dirname, __dirname,

View File

@@ -1,81 +1,70 @@
{ {
"name": "notion-next", "name": "notion-next",
"version": "4.4.2", "version": "4.4.3",
"homepage": "https://github.com/tangly1024/NotionNext.git", "homepage": "https://github.com/tangly1024/NotionNext.git",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/tangly1024/NotionNext.git" "url": "https://github.com/tangly1024/NotionNext.git"
}, },
"author": { "author": {
"name": "tangly", "name": "tangly",
"email": "mail@tangly1024.com", "email": "mail@tangly1024.com",
"url": "http://tangly1024.com" "url": "http://tangly1024.com"
}, },
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"post-build": "next-sitemap --config next-sitemap.config.js", "post-build": "next-sitemap --config next-sitemap.config.js",
"export": "next build && next-sitemap --config next-sitemap.config.js && next export", "export": "next build && next-sitemap --config next-sitemap.config.js && next export",
"bundle-report": "cross-env ANALYZE=true yarn build" "bundle-report": "cross-env ANALYZE=true yarn build"
}, },
"dependencies": { "dependencies": {
"@giscus/react": "^2.2.6", "@headlessui/react": "^1.7.15",
"@headlessui/react": "^1.7.15", "@next/bundle-analyzer": "^12.1.1",
"@next/bundle-analyzer": "^12.1.1", "@vercel/analytics": "^1.0.0",
"@vercel/analytics": "^1.0.0", "algoliasearch": "^4.18.0",
"algoliasearch": "^4.18.0", "feed": "^4.2.2",
"animejs": "^3.2.1", "js-md5": "^0.7.3",
"aos": "^2.3.4", "lodash.throttle": "^4.1.1",
"axios": ">=0.21.1", "memory-cache": "^0.2.0",
"copy-to-clipboard": "^3.3.1", "next": "13.3.1",
"feed": "^4.2.2", "notion-client": "6.15.6",
"js-md5": "^0.7.3", "notion-utils": "6.15.6",
"localStorage": "^1.0.4", "react": "^18.2.0",
"lodash.throttle": "^4.1.1", "react-dom": "^18.2.0",
"memory-cache": "^0.2.0", "react-facebook": "^8.1.4",
"mongodb": "^4.6.0", "react-hotkeys-hook": "^4.5.0",
"next": "13.3.1", "react-notion-x": "6.16.0",
"notion-client": "6.15.6", "react-share": "^4.4.1",
"notion-utils": "6.15.6", "react-tweet-embed": "~2.0.0"
"nprogress": "^0.2.0", },
"preact": "^10.5.15", "devDependencies": {
"prism-themes": "1.9.0", "@waline/client": "^2.5.1",
"react": "^18.2.0", "autoprefixer": "^10.4.13",
"react-dom": "^18.2.0", "cross-env": "^7.0.3",
"react-facebook": "^8.1.4", "eslint": "^7.26.0",
"react-hotkeys-hook": "^4.5.0", "eslint-config-next": "^13.1.1",
"react-notion-x": "6.16.0", "eslint-config-prettier": "^9.1.0",
"react-share": "^4.4.1", "eslint-config-standard": "^16.0.2",
"react-tweet-embed": "~2.0.0", "eslint-plugin-import": "^2.23.0",
"typed.js": "^2.0.12" "eslint-plugin-node": "^11.1.0",
}, "eslint-plugin-prettier": "^5.1.3",
"devDependencies": { "eslint-plugin-promise": "^5.1.0",
"@waline/client": "^2.5.1", "eslint-plugin-react": "^7.23.2",
"autoprefixer": "^10.4.13", "eslint-plugin-react-hooks": "^4.6.0",
"cross-env": "^7.0.3", "next-sitemap": "^1.6.203",
"eslint": "^7.26.0", "postcss": "^8.4.31",
"eslint-config-next": "^13.1.1", "prettier": "3.2.5",
"eslint-config-prettier": "^9.1.0", "tailwindcss": "^3.3.2",
"eslint-config-standard": "^16.0.2", "webpack-bundle-analyzer": "^4.5.0"
"eslint-plugin-import": "^2.23.0", },
"eslint-plugin-node": "^11.1.0", "resolutions": {
"eslint-plugin-prettier": "^5.1.3", "axios": ">=0.21.1"
"eslint-plugin-promise": "^5.1.0", },
"eslint-plugin-react": "^7.23.2", "bugs": {
"eslint-plugin-react-hooks": "^4.6.0", "url": "https://github.com/tangly/NotionNext/issues",
"next-sitemap": "^1.6.203", "email": "tlyong1992@hotmail.com"
"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 { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { useRouter } from 'next/router'
/** /**
* 404 * 404
@@ -10,12 +10,17 @@ import { siteConfig } from '@/lib/config'
*/ */
const NoFound = props => { const NoFound = props => {
// 根据页面路径加载不同Layout文件 // 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() }) const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} /> return <Layout {...props} />
} }
export async function getStaticProps () { export async function getStaticProps(req) {
const props = (await getGlobalData({ from: '404' })) || {} const { locale } = req
const props = (await getGlobalData({ from: '404', locale })) || {}
return { props } return { props }
} }

View File

@@ -35,7 +35,11 @@ export async function getStaticPaths() {
paths: allPages paths: allPages
?.filter(row => checkSlug(row)) ?.filter(row => checkSlug(row))
.map(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 fallback: true
} }
@@ -46,18 +50,25 @@ export async function getStaticPaths() {
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
export async function getStaticProps({ params: { prefix, slug, suffix } }) { export async function getStaticProps({
params: { prefix, slug, suffix },
locale
}) {
let fullSlug = prefix + '/' + slug + '/' + suffix.join('/') 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')) { if (!fullSlug.endsWith('.html')) {
fullSlug += '.html' fullSlug += '.html'
} }
} }
const from = `slug-props-${fullSlug}`
const props = await getGlobalData({ from })
// 在列表内查找文章 // 在列表内查找文章
props.post = props?.allPages?.find(p => { 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) { if (!props?.post) {
props.post = null 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) { if (allPosts && allPosts.length > 0) {
const index = allPosts.indexOf(props.post) const index = allPosts.indexOf(props.post)
props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0] props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
props.next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[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 { } else {
props.prev = null props.prev = null
props.next = null props.next = null
@@ -100,7 +124,11 @@ export async function getStaticProps({ params: { prefix, slug, suffix } }) {
delete props.allPages delete props.allPages
return { return {
props, 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('/')) { if (slug.startsWith('/')) {
slug = slug.substring(1) 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 export default PrefixSlug

View File

@@ -28,25 +28,31 @@ export async function getStaticPaths() {
const { allPages } = await getGlobalData({ from }) const { allPages } = await getGlobalData({ from })
const paths = allPages const paths = allPages
?.filter(row => checkSlug(row)) ?.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 { return {
paths: paths, paths: paths,
fallback: true fallback: true
} }
} }
export async function getStaticProps({ params: { prefix, slug } }) { export async function getStaticProps({ params: { prefix, slug }, locale }) {
let fullSlug = prefix + '/' + slug 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')) { if (!fullSlug.endsWith('.html')) {
fullSlug += '.html' fullSlug += '.html'
} }
} }
const from = `slug-props-${fullSlug}`
const props = await getGlobalData({ from })
// 在列表内查找文章 // 在列表内查找文章
props.post = props?.allPages?.find(p => { 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) { if (!props?.post) {
props.post = null 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) { if (allPosts && allPosts.length > 0) {
const index = allPosts.indexOf(props.post) const index = allPosts.indexOf(props.post)
props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0] props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
props.next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[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 { } else {
props.prev = null props.prev = null
props.next = null props.next = null
@@ -89,7 +108,11 @@ export async function getStaticProps({ params: { prefix, slug } }) {
delete props.allPages delete props.allPages
return { return {
props, props,
revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND) revalidate: siteConfig(
'NEXT_REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
} }
} }
function checkSlug(row) { function checkSlug(row) {
@@ -97,6 +120,10 @@ function checkSlug(row) {
if (slug.startsWith('/')) { if (slug.startsWith('/')) {
slug = slug.substring(1) 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 export default PrefixSlug

View File

@@ -53,7 +53,10 @@ const Slug = props => {
props = { ...props, lock, setLock, validPassword } props = { ...props, lock, setLock, validPassword }
// 根据页面路径加载不同Layout文件 // 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() }) const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} /> return <Layout {...props} />
} }
@@ -67,25 +70,31 @@ export async function getStaticPaths() {
const from = 'slug-paths' const from = 'slug-paths'
const { allPages } = await getGlobalData({ from }) 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 { return {
paths: paths, paths: paths,
fallback: true fallback: true
} }
} }
export async function getStaticProps({ params: { prefix } }) { export async function getStaticProps({ params: { prefix }, locale }) {
let fullSlug = prefix 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')) { if (!fullSlug.endsWith('.html')) {
fullSlug += '.html' fullSlug += '.html'
} }
} }
const from = `slug-props-${fullSlug}`
const props = await getGlobalData({ from })
// 在列表内查找文章 // 在列表内查找文章
props.post = props?.allPages?.find(p => { 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) { if (!props?.post) {
props.post = null 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) { if (allPosts && allPosts.length > 0) {
const index = allPosts.indexOf(props.post) const index = allPosts.indexOf(props.post)
props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0] props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
props.next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[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 { } else {
props.prev = null props.prev = null
props.next = null props.next = null
@@ -128,7 +150,11 @@ export async function getStaticProps({ params: { prefix } }) {
delete props.allPages delete props.allPages
return { return {
props, 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('/')) { if (slug.startsWith('/')) {
slug = slug.substring(1) 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 export default Slug

View File

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

View File

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

View File

@@ -1,15 +1,18 @@
import { getGlobalData } from '@/lib/db/getSiteData'
import { useEffect } from 'react'
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
import { useRouter } from 'next/router' import { siteConfig } from '@/lib/config'
import { getLayoutByTheme } from '@/themes/theme' import { getGlobalData } from '@/lib/db/getSiteData'
import { isBrowser } from '@/lib/utils' import { isBrowser } from '@/lib/utils'
import { formatDateFmt } from '@/lib/utils/formatDate' 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 => { const ArchiveIndex = props => {
// 根据页面路径加载不同Layout文件 // 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() }) const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
useEffect(() => { useEffect(() => {
if (isBrowser) { if (isBrowser) {
@@ -28,10 +31,12 @@ const ArchiveIndex = props => {
return <Layout {...props} /> return <Layout {...props} />
} }
export async function getStaticProps() { export async function getStaticProps({ locale }) {
const props = await getGlobalData({ from: 'archive-index' }) 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 delete props.allPages
const postsSortByDate = Object.create(props.posts) const postsSortByDate = Object.create(props.posts)
@@ -56,7 +61,11 @@ export async function getStaticProps() {
return { return {
props, 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) { export default function Category(props) {
// 根据页面路径加载不同Layout文件 // 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() }) const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} /> return <Layout {...props} />
} }
export async function getStaticProps({ params: { category } }) { export async function getStaticProps({ params: { category }, locale }) {
const from = 'category-props' 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 props.postCount = props.posts.length
// 处理分页 // 处理分页
@@ -39,7 +46,11 @@ export async function getStaticProps({ params: { category } }) {
return { return {
props, 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) { export default function Category(props) {
// 根据页面路径加载不同Layout文件 // 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() }) const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} /> return <Layout {...props} />
} }
@@ -28,7 +31,10 @@ export async function getStaticProps({ params: { category, page } }) {
// 处理文章页数 // 处理文章页数
props.postCount = props.posts.length 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 delete props.allPages
props.page = page props.page = page
@@ -37,7 +43,11 @@ export async function getStaticProps({ params: { category, page } }) {
return { return {
props, 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 const categoryPosts = allPages
?.filter(page => page.type === 'Post' && page.status === 'Published') ?.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 postCount = categoryPosts.length
const totalPages = Math.ceil(postCount / siteConfig('POSTS_PER_PAGE')) 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 BLOG from '@/blog.config'
import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config' 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) { export default function Category(props) {
// 根据页面路径加载不同Layout文件 // 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() }) const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} /> return <Layout {...props} />
} }
export async function getStaticProps() { export async function getStaticProps({ locale }) {
const props = await getGlobalData({ from: 'category-index-props' }) const props = await getGlobalData({ from: 'category-index-props', locale })
delete props.allPages delete props.allPages
return { return {
props, 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 获取数据 * SSG 获取数据
* @returns * @returns
*/ */
export async function getStaticProps() { export async function getStaticProps(req) {
const { locale } = req
const from = 'index' const from = 'index'
const props = await getGlobalData({ from }) const props = await getGlobalData({ from, locale })
props.posts = props.allPages?.filter( props.posts = props.allPages?.filter(
page => page.type === 'Post' && page.status === 'Published' page => page.type === 'Post' && page.status === 'Published'
@@ -58,7 +59,7 @@ export async function getStaticProps() {
generateRobotsTxt() generateRobotsTxt()
// 生成Feed订阅 // 生成Feed订阅
if (JSON.parse(BLOG.ENABLE_RSS)) { if (JSON.parse(BLOG.ENABLE_RSS)) {
generateRss(props?.latestPosts || []) generateRss(props?.NOTION_CONFIG, props?.latestPosts || [])
} }
// 生成全文索引 - 仅在 yarn build 时执行 && process.env.npm_lifecycle_event === 'build' // 生成全文索引 - 仅在 yarn build 时执行 && process.env.npm_lifecycle_event === 'build'
@@ -67,7 +68,11 @@ export async function getStaticProps() {
return { return {
props, 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 => { const Page = props => {
// 根据页面路径加载不同Layout文件 // 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() }) const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} /> return <Layout {...props} />
} }
export async function getStaticPaths() { export async function getStaticPaths({ locale }) {
const from = 'page-paths' const from = 'page-paths'
const { postCount } = await getGlobalData({ from }) const { postCount } = await getGlobalData({ from, locale })
const totalPages = Math.ceil(postCount / siteConfig('POSTS_PER_PAGE')) const totalPages = Math.ceil(postCount / siteConfig('POSTS_PER_PAGE'))
return { return {
// remove first page, we 're not gonna handle that. // remove first page, we 're not gonna handle that.
@@ -33,9 +36,14 @@ export async function getStaticProps({ params: { page } }) {
const from = `page-${page}` const from = `page-${page}`
const props = await getGlobalData({ from }) const props = await getGlobalData({ from })
const { allPages } = props 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 props.page = page
// 处理预览 // 处理预览
@@ -45,14 +53,22 @@ export async function getStaticProps({ params: { page } }) {
if (post.password && post.password !== '') { if (post.password && post.password !== '') {
continue 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 delete props.allPages
return { return {
props, 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 => { const Index = props => {
// 根据页面路径加载不同Layout文件 // 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() }) const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} /> return <Layout {...props} />
} }
@@ -17,13 +20,15 @@ const Index = props => {
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
export async function getStaticProps({ params: { keyword } }) { export async function getStaticProps({ params: { keyword }, locale }) {
const props = await getGlobalData({ const props = await getGlobalData({
from: 'search-props', from: 'search-props',
pageType: ['Post'] locale
}) })
const { allPages } = props 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.posts = await filterByMemCache(allPosts, keyword)
props.postCount = props.posts.length props.postCount = props.posts.length
// 处理分页 // 处理分页
@@ -35,7 +40,11 @@ export async function getStaticProps({ params: { keyword } }) {
props.keyword = keyword props.keyword = keyword
return { return {
props, 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 * @param {*} obj
* @returns * @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) { for (const post of allPosts) {
const cacheKey = 'page_block_' + post.id const cacheKey = 'page_block_' + post.id
const page = await getDataFromCache(cacheKey, true) const page = await getDataFromCache(cacheKey, true)
const tagContent = post?.tags && Array.isArray(post?.tags) ? post?.tags.join(' ') : '' const tagContent =
const categoryContent = post.category && Array.isArray(post.category) ? post.category.join(' ') : '' 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 const articleInfo = post.title + post.summary + tagContent + categoryContent
let hit = articleInfo.toLowerCase().indexOf(keyword) > -1 let hit = articleInfo.toLowerCase().indexOf(keyword) > -1
const indexContent = getPageContentText(post, page) const indexContent = getPageContentText(post, page)

View File

@@ -8,7 +8,10 @@ import { useRouter } from 'next/router'
const Index = props => { const Index = props => {
const { keyword } = props const { keyword } = props
// 根据页面路径加载不同Layout文件 // 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() }) const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
props = { ...props, currentSearch: keyword } props = { ...props, currentSearch: keyword }
return <Layout {...props} /> return <Layout {...props} />
@@ -19,23 +22,33 @@ const Index = props => {
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
export async function getStaticProps({ params: { keyword, page } }) { export async function getStaticProps({ params: { keyword, page }, locale }) {
const props = await getGlobalData({ const props = await getGlobalData({
from: 'search-props', from: 'search-props',
pageType: ['Post'] pageType: ['Post'],
locale
}) })
const { allPages } = props 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.posts = await filterByMemCache(allPosts, keyword)
props.postCount = props.posts.length 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.keyword = keyword
props.page = page props.page = page
delete props.allPages delete props.allPages
return { return {
props, 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 * @param {*} obj
* @returns * @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) { for (const post of allPosts) {
const cacheKey = 'page_block_' + post.id const cacheKey = 'page_block_' + post.id
const page = await getDataFromCache(cacheKey, true) const page = await getDataFromCache(cacheKey, true)
const tagContent = post?.tags && Array.isArray(post?.tags) ? post?.tags.join(' ') : '' const tagContent =
const categoryContent = post.category && Array.isArray(post.category) ? post.category.join(' ') : '' 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 const articleInfo = post.title + post.summary + tagContent + categoryContent
let hit = articleInfo.indexOf(keyword) > -1 let hit = articleInfo.indexOf(keyword) > -1
let indexContent = [post.summary] 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 BLOG from '@/blog.config'
import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config' 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 const { posts } = props
// 根据页面路径加载不同Layout文件 // 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() }) const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
const router = useRouter() const router = useRouter()
const keyword = router?.query?.s const keyword = router?.query?.s
@@ -25,7 +28,7 @@ const Search = props => {
const tagContent = post?.tags ? post?.tags.join(' ') : '' const tagContent = post?.tags ? post?.tags.join(' ') : ''
const categoryContent = post.category ? post.category.join(' ') : '' const categoryContent = post.category ? post.category.join(' ') : ''
const searchContent = const searchContent =
post.title + post.summary + tagContent + categoryContent post.title + post.summary + tagContent + categoryContent
return searchContent.toLowerCase().includes(keyword.toLowerCase()) return searchContent.toLowerCase().includes(keyword.toLowerCase())
}) })
} else { } else {
@@ -40,16 +43,22 @@ const Search = props => {
/** /**
* 浏览器前端搜索 * 浏览器前端搜索
*/ */
export async function getStaticProps() { export async function getStaticProps({ locale }) {
const props = await getGlobalData({ const props = await getGlobalData({
from: 'search-props', from: 'search-props',
pageType: ['Post'] locale
}) })
const { allPages } = props 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 { return {
props, 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 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 { 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 => { const SignIn = props => {
// 根据页面路径加载不同Layout文件 // 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() }) const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} /> return <Layout {...props} />
} }
export async function getStaticProps() { export async function getStaticProps(req) {
const { locale } = req
const from = 'SignIn' const from = 'SignIn'
const props = await getGlobalData({ from }) const props = await getGlobalData({ from, locale })
delete props.allPages delete props.allPages
return { return {
props, 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 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 { 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 => { const SignUp = props => {
// 根据页面路径加载不同Layout文件 // 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() }) const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} /> return <Layout {...props} />
} }
export async function getStaticProps() { export async function getStaticProps(req) {
const { locale } = req
const from = 'SignIn' const from = 'SignIn'
const props = await getGlobalData({ from }) const props = await getGlobalData({ from, locale })
delete props.allPages delete props.allPages
return { return {
props, 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 // pages/sitemap.xml.js
import { getServerSideSitemap } from 'next-sitemap'
import { getGlobalData } from '@/lib/db/getSiteData'
import BLOG from '@/blog.config' 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) => { export const getServerSideProps = async ctx => {
const { allPages } = await getGlobalData({ from: 'rss' }) let fields = []
const defaultFields = [ const siteIds = BLOG.NOTION_PAGE_ID.split(',')
{ for (let index = 0; index < siteIds.length; index++) {
loc: `${BLOG.LINK}`, const siteId = siteIds[index]
lastmod: new Date().toISOString().split('T')[0], const id = extractLangId(siteId)
changefreq: 'daily', const locale = extractLangPrefix(siteId)
priority: '0.7' // 第一个id站点默认语言
}, { const siteData = await getNotionPageData({
loc: `${BLOG.LINK}/archive`, pageId: id,
lastmod: new Date().toISOString().split('T')[0], from: 'sitemap.xml'
changefreq: 'daily', })
priority: '0.7' const link = siteConfig('LINK', BLOG.LINK, siteData.NOTION_CONFIG)
}, { const localeFields = generateLocalesSitemap(link, siteData.allPages, locale)
loc: `${BLOG.LINK}/category`, fields = fields.concat(localeFields)
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)
// 缓存 // 缓存
ctx.res.setHeader( ctx.res.setHeader(
'Cache-Control', 'Cache-Control',
'public, max-age=3600, stale-while-revalidate=59' 'public, max-age=3600, stale-while-revalidate=59'
) )
console.log('fff', fields)
return getServerSideSitemap(ctx, 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 => { const Tag = props => {
// 根据页面路径加载不同Layout文件 // 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() }) const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} /> return <Layout {...props} />
} }
export async function getStaticProps({ params: { tag } }) { export async function getStaticProps({ params: { tag }, locale }) {
const from = 'tag-props' const from = 'tag-props'
const props = await getGlobalData({ from }) const props = await getGlobalData({ from, locale })
// 过滤状态 // 过滤状态
props.posts = props.allPages props.posts = props.allPages
@@ -39,7 +42,11 @@ export async function getStaticProps({ params: { tag } }) {
delete props.allPages delete props.allPages
return { return {
props, 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 => { const Tag = props => {
// 根据页面路径加载不同Layout文件 // 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() }) const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} /> return <Layout {...props} />
} }
export async function getStaticProps({ params: { tag, page } }) { export async function getStaticProps({ params: { tag, page }, locale }) {
const from = 'tag-page-props' const from = 'tag-page-props'
const props = await getGlobalData({ from }) const props = await getGlobalData({ from, locale })
// 过滤状态、标签 // 过滤状态、标签
props.posts = props.allPages props.posts = props.allPages
?.filter(page => page.type === 'Post' && page.status === 'Published') ?.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.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.tag = tag
props.page = page props.page = page
delete props.allPages delete props.allPages
return { return {
props, 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 BLOG from '@/blog.config'
import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config' 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 => { const TagIndex = props => {
// 根据页面路径加载不同Layout文件 // 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() }) const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} /> return <Layout {...props} />
} }
export async function getStaticProps() { export async function getStaticProps(req) {
const { locale } = req
const from = 'tag-index-props' const from = 'tag-index-props'
const props = await getGlobalData({ from }) const props = await getGlobalData({ from, locale })
delete props.allPages delete props.allPages
return { return {
props, 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"> <html lang="en">
<head>
<head> <meta charset="UTF-8" />
<meta charset="UTF-8"> <meta name="robots" content="noindex, nofollow" />
<meta name="robots" content="noindex, nofollow"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Full Screen iFrame</title> <title>Full Screen iFrame</title>
<style> <style>
html, html,
body { body {
height: 100%; height: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
} }
#myIframe { #myIframe {
width: 100%; width: 100%;
height: 100%; height: 100%;
border: none; border: none;
/* 可选:移除边框 */ /* 可选:移除边框 */
} }
</style> </style>
</head> </head>
<body> <body>
<!-- <div style="position: absolute; <!-- <div style="position: absolute;
right: 0px; right: 0px;
bottom: 0px; bottom: 0px;
@@ -32,17 +31,18 @@
<button onclick="toggleFullScreen()">Toggle Full Screen</button> <button onclick="toggleFullScreen()">Toggle Full Screen</button>
</div> --> </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 --> <!-- https://letsplay247.github.io/cz.html?n=space-wars-battleground -->
<script> <script>
var myParam = location.search.split('n=')[1] var myParam = location.search.split('n=')[1]
document.getElementById("myIframe").src = myParam; document.getElementById('myIframe').src = myParam
</script> </script>
<script src="/js/fullscreen.js" type="text/javascript"></script> <script src="/js/fullscreen.js" type="text/javascript"></script>
</body>
</body> </html>
</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 { useGlobal } from '@/lib/global'
import Link from 'next/link'
import CONFIG from '../config' import CONFIG from '../config'
const MenuGroupCard = (props) => { const MenuGroupCard = props => {
const { postCount, categoryOptions, tagOptions } = props const { postCount, categoryOptions, tagOptions } = props
const { locale } = useGlobal() const { locale } = useGlobal()
const archiveSlot = <div className='text-center'>{postCount}</div> 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 tagSlot = <div className='text-center'>{tagOptions?.length}</div>
const links = [ 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.ARTICLE,
{ name: locale.COMMON.TAGS, to: '/tag', slot: tagSlot, show: CONFIG.MENU_TAG } 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++) { for (let i = 0; i < links.length; i++) {
@@ -22,29 +39,31 @@ const MenuGroupCard = (props) => {
} }
return ( return (
<nav id='nav' className='leading-8 flex justify-center dark:text-gray-200 w-full'> <nav
{links.map(link => { id='nav'
if (link.show) { className='leading-8 flex justify-center dark:text-gray-200 w-full'>
return ( {links.map(link => {
<Link if (link.show) {
key={`${link.to}`} return (
title={link.to} <Link
href={link.to} key={`${link.to}`}
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} title={link.to}
className={'py-1.5 my-1 px-2 duration-300 text-base justify-center items-center cursor-pointer'}> href={link.to}
target={link?.target}
<div className='w-full items-center justify-center hover:scale-105 duration-200 transform dark:hover:text-red-400 hover:text-red-600'> className={
<div className='text-center'>{link.name}</div> 'py-1.5 my-1 px-2 duration-300 text-base justify-center items-center cursor-pointer'
<div className='text-center font-semibold'>{link.slot}</div> }>
</div> <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>
</Link> <div className='text-center font-semibold'>{link.slot}</div>
) </div>
} else { </Link>
return null )
} } else {
})} return null
</nav> }
})}
</nav>
) )
} }
export default MenuGroupCard export default MenuGroupCard

View File

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

View File

@@ -5,34 +5,48 @@ export const MenuItemDrop = ({ link }) => {
const [show, changeShow] = useState(false) const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0 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 && {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"> <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}
{link?.icon && <i className={link?.icon} />} {link?.name} <i
{hasSubMenu && <i className='px-2 fa fa-angle-down'></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> </Link>
</div> </li>
} )
})}
{hasSubMenu && </ul>
<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> </li>
)
} }

View File

@@ -8,7 +8,7 @@ import { useState } from 'react'
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
export const MenuItemCollapse = (props) => { export const MenuItemCollapse = props => {
const { link } = props const { link } = props
const [show, changeShow] = useState(false) const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0 const hasSubMenu = link?.subMenus?.length > 0
@@ -21,7 +21,7 @@ export const MenuItemCollapse = (props) => {
return null return null
} }
const selected = (router.pathname === link.to) || (router.asPath === link.to) const selected = router.pathname === link.to || router.asPath === link.to
const toggleShow = () => { const toggleShow = () => {
changeShow(!show) changeShow(!show)
@@ -31,32 +31,67 @@ export const MenuItemCollapse = (props) => {
changeIsOpen(!isOpen) changeIsOpen(!isOpen)
} }
return <> 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} > <>
<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 '> {hasSubMenu && (
<div><div className={`${link.icon} text-center w-4 mr-4`} />{link.name}</div> <div
</Link>} 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} {hasSubMenu && (
className="py-2 font-extralight flex justify-between cursor-pointer dark:text-gray-200 no-underline tracking-widest"> <Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
<div><div className={`${link.icon} text-center w-4 mr-4`} />{link.name}</div> {link.subMenus.map((sLink, index) => {
<div className='inline-flex items-center '><i className={`px-2 fas fa-chevron-right transition-all duration-200 ${isOpen ? 'rotate-90' : ''}`}></i></div> return (
</div>} <div
</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 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'> 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'}> <Link href={sLink.to} target={link?.target}>
<div><div className={`${sLink.icon} text-center w-3 mr-3 text-xs`} />{sLink.title}</div> <div>
</Link> <div
</div> className={`${sLink.icon} text-center w-3 mr-3 text-xs`}
})} />
</Collapse>} {sLink.title}
</div>
</Link>
</div>
)
})}
</Collapse>
)}
</> </>
)
} }

View File

@@ -5,34 +5,62 @@ export const MenuItemDrop = ({ link }) => {
const [show, changeShow] = useState(false) const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0 const hasSubMenu = link?.subMenus?.length > 0
return <li onMouseOver={() => changeShow(true)} onMouseOut={() => changeShow(false)} return (
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 '> <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 && {hasSubMenu && (
<Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} className='w-full my-auto items-center justify-between flex ' > <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> <div>
{link.slot} <div className={`${link.icon} text-center w-4 mr-2`} />
</Link>} {link.name}
</div>
{hasSubMenu && {link.slot}
<div className='w-full my-auto items-center justify-between flex '> {hasSubMenu && (
<div><div className={`${link.icon} text-center w-4 mr-2`} />{link.name}</div> <div className='text-right'>
{link.slot} <i
{hasSubMenu && <div className='text-right'><i className={`px-2 fas fa-chevron-right duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i></div>} className={`px-2 fas fa-chevron-right duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>
</div>} </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 && (
<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> </li>
)
} }

View File

@@ -9,8 +9,8 @@ export const Footer = props => {
return ( return (
<footer <footer
className={`z-10 relative mt-6 flex-shrink-0 m-auto w-full dark:text-gray-200 `}> 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' /> {/* <hr className='my-2 border-black dark:border-gray-100' /> */}
{/* 页面底部 */} {/* 页面底部 */}
<div className='w-full flex justify-between p-4 '> <div className='w-full flex justify-between p-4 '>
<p> <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 */ /* eslint-disable @next/next/no-img-element */
import { AdSlot } from '@/components/GoogleAdsense' import { AdSlot } from '@/components/GoogleAdsense'
import LazyImage from '@/components/LazyImage'
import { siteConfig } from '@/lib/config' import { siteConfig } from '@/lib/config'
import { checkContainHttp, deepClone, sliceUrlFromHttp } from '@/lib/utils' import { checkContainHttp, deepClone, sliceUrlFromHttp } from '@/lib/utils'
import Link from 'next/link' import Link from 'next/link'
@@ -88,8 +89,8 @@ export const GameListIndexCombine = ({ posts }) => {
} }
return ( return (
<div className='game-list-wrapper flex justify-center w-full px-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-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-3 px-2 md:p-0'>
{components?.map((ItemComponent, index) => { {components?.map((ItemComponent, index) => {
return ItemComponent return ItemComponent
})} })}
@@ -104,20 +105,24 @@ export const GameListIndexCombine = ({ posts }) => {
*/ */
const GameAd = () => { const GameAd = () => {
return ( 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' /> <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> </div>
) )
} }
/** /**
* 4卡组成一个大卡 * 大卡由2行2列小卡构成
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
const GameItemGroup = ({ items }) => { const GameItemGroup = ({ items }) => {
return ( 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) => ( {items.map((item, index) => (
<GameItem key={index} item={item} /> <GameItem key={index} item={item} />
))} ))}
@@ -140,38 +145,39 @@ const GameItem = ({ item, isLargeCard }) => {
const video = item?.ext?.video const video = item?.ext?.video
return ( return (
<Link <Link
title={title}
href={`${url}`} 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={() => { onMouseOver={() => {
setShowType('video') setShowType('video')
}} }}
onMouseOut={() => { onMouseOut={() => {
setShowType('img') 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'> <div className='text-center absolute bottom-0 invisible group-hover:bottom-2 group-hover:visible transition-all duration-200 text-white z-30'>
{title} {title}
</div> </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 className='h-full w-full absolute bg-gradient-to-b from-transparent to-black'></div>
</div> </div>
{showType === 'video' && ( {showType === 'video' && (
<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' loop='true'
autoPlay autoPlay
preload='none'> preload='none'>
<source src={video} type='video/mp4' /> <source src={video} type='video/mp4' />
</video> </video>
)} )}
<img <LazyImage
className='w-full h-full absolute object-cover group-hover:scale-105 duration-100 transition-all' className='w-full h-full absolute object-cover group-hover:scale-105 duration-100 transition-all'
src={img} src={img}
priority
alt={title} alt={title}
fill='full'
/> />
</Link> </Link>
) )

View File

@@ -43,7 +43,9 @@ export const GameListRelate = ({ posts }) => {
const GameItem = ({ item }) => { const GameItem = ({ item }) => {
const { title } = item const { title } = item
const [showType, setShowType] = useState('img') // img or video 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 img = item?.pageCoverThumbnail
const video = item?.ext?.video const video = item?.ext?.video
@@ -60,15 +62,19 @@ const GameItem = ({ item }) => {
title={title} title={title}
className={`card-single w-24 h-24 relative shadow rounded-md overflow-hidden flex justify-center items-center className={`card-single w-24 h-24 relative shadow rounded-md overflow-hidden flex justify-center items-center
group hover:border-purple-400`}> 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} {title}
</div> </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 className='h-full w-full absolute bg-gradient-to-b from-transparent to-black'></div>
</div> </div>
{showType === 'video' && ( {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' /> <source src={video} type='video/mp4' />
</video> </video>
)} )}

View File

@@ -1,7 +1,7 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import { siteConfig } from '@/lib/config' import { siteConfig } from '@/lib/config'
import { checkContainHttp, deepClone, sliceUrlFromHttp } from '@/lib/utils' import { checkContainHttp, deepClone, sliceUrlFromHttp } from '@/lib/utils'
import Link from 'next/link' import { useRouter } from 'next/router'
import { useState } from 'react' import { useState } from 'react'
import { useGameGlobal } from '..' import { useGameGlobal } from '..'
@@ -32,7 +32,7 @@ export const GameListRecent = ({ maxCount = 14 }) => {
return ( 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'> <div className='game-grid md:flex grid grid-flow-col gap-2'>
{components?.map((ItemComponent, index) => { {components?.map((ItemComponent, index) => {
return ItemComponent return ItemComponent
@@ -49,17 +49,43 @@ export const GameListRecent = ({ maxCount = 14 }) => {
* @returns * @returns
*/ */
const GameItem = ({ item }) => { const GameItem = ({ item }) => {
const router = useRouter()
const { recentGames, setRecentGames } = useGameGlobal()
const { title } = item || {} const { title } = item || {}
const [showType, setShowType] = useState('img') // img or video const [showType, setShowType] = useState('img') // img or video
const url = checkContainHttp(item.slug) const url = checkContainHttp(item.slug)
? sliceUrlFromHttp(item.slug) ? sliceUrlFromHttp(item.slug)
: `${siteConfig('SUB_PATH', '')}/${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 img = item?.pageCoverThumbnail
const video = item?.ext?.video const video = item?.ext?.video
return ( return (
<Link <div
href={`${url}`} onClick={handleButtonClick}
onMouseOver={() => { onMouseOver={() => {
setShowType('video') setShowType('video')
}} }}
@@ -67,11 +93,23 @@ const GameItem = ({ item }) => {
setShowType('img') setShowType('img')
}} }}
title={title} title={title}
className={`card-single h-28 w-28 relative shadow rounded-md overflow-hidden flex justify-center items-center 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`}> group hover:border-purple-400`}>
<div className='absolute right-0.5 top-1 z-20'> <button
<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' /> className='absolute right-0.5 top-1 z-20'
</div> 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'> <div className='absolute text-sm bottom-2 transition-all duration-200 text-white z-30'>
{title} {title}
</div> </div>
@@ -93,6 +131,6 @@ const GameItem = ({ item }) => {
src={img} src={img}
alt={title} alt={title}
/> />
</Link> </div>
) )
} }

View File

@@ -6,11 +6,13 @@ function GroupCategory({ currentCategory, categoryOptions }) {
} }
return ( return (
<> <div className='flex items-center'>
<Link className='mx-2' href='/category'> <Link className='mx-2' href='/category'>
<i className='fas fa-bars' /> <i className='fas fa-bars' />
</Link> </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 => { {categoryOptions.map(category => {
const selected = currentCategory === category.name const selected = currentCategory === category.name
return ( return (
@@ -34,7 +36,7 @@ function GroupCategory({ currentCategory, categoryOptions }) {
) )
})} })}
</div> </div>
</> </div>
) )
} }

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ import { useState } from 'react'
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
export const MenuItemCollapse = (props) => { export const MenuItemCollapse = props => {
const { link } = props const { link } = props
const [show, changeShow] = useState(false) const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0 const hasSubMenu = link?.subMenus?.length > 0
@@ -26,30 +26,59 @@ export const MenuItemCollapse = (props) => {
return null return null
} }
return <> return (
<div className='w-full px-4 py-2 text-left dark:bg-hexo-black-gray dark:border-black' onClick={toggleShow} > <>
{!hasSubMenu && <Link <div
href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} className='w-full px-4 py-2 text-left dark:bg-hexo-black-gray dark:border-black'
className="font-extralight flex justify-between pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest pb-1"> onClick={toggleShow}>
<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> {!hasSubMenu && (
</Link>} <Link
{hasSubMenu && <div href={link?.to}
onClick={hasSubMenu ? toggleOpenSubMenu : null} target={link?.target}
className="font-extralight flex justify-between pl-2 pr-4 cursor-pointer dark:text-gray-200 no-underline tracking-widest pb-1"> 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> <span className=' hover:text-red-400 transition-all items-center duration-200'>
<i className='px-2 fa fa-plus text-gray-400'></i> {link?.icon && (
</div>} <span className='mr-2'>
</div> <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}> {hasSubMenu && (
{link.subMenus.map((sLink, index) => { <Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
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.subMenus.map((sLink, index) => {
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}> return (
<span className='text-xs'>{sLink.title}</span> <div
</Link> key={index}
</div> 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}>
</Collapse>} <span className='text-xs'>{sLink.title}</span>
</Link>
</div>
)
})}
</Collapse>
)}
</> </>
)
} }

View File

@@ -20,7 +20,7 @@ export const MenuItemDrop = ({ link }) => {
<Link <Link
href={link?.to} href={link?.to}
className='flex flex-nowrap' className='flex flex-nowrap'
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}> target={link?.target}>
<div className='w-6 mr-2 text-center'> <div className='w-6 mr-2 text-center'>
{link?.icon && <i className={link?.icon} />} {link?.icon && <i className={link?.icon} />}
</div> </div>
@@ -47,11 +47,7 @@ export const MenuItemDrop = ({ link }) => {
<div <div
key={index} key={index}
className='text-gray-700 dark:text-gray-200 tracking-widest transition-all duration-200 '> className='text-gray-700 dark:text-gray-200 tracking-widest transition-all duration-200 '>
<Link <Link href={sLink.to} target={link?.target}>
href={sLink.to}
target={
link?.to?.indexOf('http') === 0 ? '_blank' : '_self'
}>
<span className='text-sm text-nowrap font-extralight'> <span className='text-sm text-nowrap font-extralight'>
{link?.icon && <i className={sLink?.icon}> &nbsp; </i>} {link?.icon && <i className={sLink?.icon}> &nbsp; </i>}
{sLink.title} {sLink.title}

View File

@@ -9,7 +9,7 @@ export default function PostInfo(props) {
const { post } = props const { post } = props
return ( 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>
<div> <div>
{post?.type !== 'Page' && ( {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 } = const { sideBarVisible, setSideBarVisible, filterGames, setFilterGames } =
useGameGlobal() useGameGlobal()
const inputRef = useRef(null) // 创建对输入框的引用 const inputRef = useRef(null) // 创建对输入框的引用
@@ -50,7 +50,7 @@ export default function SideBarContent({ allNavPages }) {
return ( return (
<div className='px-3'> <div className='px-3'>
<div className='py-2 flex justify-between'> <div className='py-2 flex justify-between'>
<Logo /> <Logo siteInfo={siteInfo} />
<button <button
onClick={() => { onClick={() => {
setSideBarVisible(false) setSideBarVisible(false)

View File

@@ -1,6 +1,5 @@
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
import Comment from '@/components/Comment' import Comment from '@/components/Comment'
import { Draggable } from '@/components/Draggable'
import { AdSlot } from '@/components/GoogleAdsense' import { AdSlot } from '@/components/GoogleAdsense'
import replaceSearchResult from '@/components/Mark' import replaceSearchResult from '@/components/Mark'
import NotionPage from '@/components/NotionPage' import NotionPage from '@/components/NotionPage'
@@ -17,9 +16,8 @@ import BlogArchiveItem from './components/BlogArchiveItem'
import { BlogListPage } from './components/BlogListPage' import { BlogListPage } from './components/BlogListPage'
import { BlogListScroll } from './components/BlogListScroll' import { BlogListScroll } from './components/BlogListScroll'
import BlogPostBar from './components/BlogPostBar' import BlogPostBar from './components/BlogPostBar'
import DownloadButton from './components/DownloadButton'
import { Footer } from './components/Footer' import { Footer } from './components/Footer'
import FullScreenButton from './components/FullScreenButton' import GameEmbed from './components/GameEmbed'
import { GameListIndexCombine } from './components/GameListIndexCombine' import { GameListIndexCombine } from './components/GameListIndexCombine'
import { GameListRelate } from './components/GameListRealate' import { GameListRelate } from './components/GameListRealate'
import { GameListRecent } from './components/GameListRecent' import { GameListRecent } from './components/GameListRecent'
@@ -46,7 +44,7 @@ export const useGameGlobal = () => useContext(ThemeGlobalGame)
* @constructor * @constructor
*/ */
const LayoutBase = props => { const LayoutBase = props => {
const { allNavPages, children } = props const { allNavPages, children, siteInfo } = props
const searchModal = useRef(null) const searchModal = useRef(null)
// 在列表中进行实时过滤 // 在列表中进行实时过滤
const [filterKey, setFilterKey] = useState('') const [filterKey, setFilterKey] = useState('')
@@ -64,11 +62,6 @@ const LayoutBase = props => {
const [sideBarVisible, setSideBarVisible] = useState(false) const [sideBarVisible, setSideBarVisible] = useState(false)
useEffect(() => { useEffect(() => {
setRecentGames(
localStorage.getItem('recent_games')
? JSON.parse(localStorage.getItem('recent_games'))
: []
)
loadWowJS() loadWowJS()
}, []) }, [])
@@ -87,7 +80,7 @@ const LayoutBase = props => {
}}> }}>
<div <div
id='theme-game' 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 /> <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='py-4 px-2 sticky top-0 h-screen flex flex-col justify-between'>
<div className='select-none'> <div className='select-none'>
{/* 抬头logo等 */} {/* 抬头logo等 */}
<Header /> <Header siteInfo={siteInfo} />
{/* 菜单栏 */} {/* 菜单栏 */}
<MenuList {...props} /> <MenuList {...props} />
</div> </div>
@@ -112,9 +105,8 @@ const LayoutBase = props => {
</div> </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> <div className='flex-grow h-full'>{children}</div>
<Footer /> <Footer />
</main> </main>
</div> </div>
@@ -124,7 +116,7 @@ const LayoutBase = props => {
onClose={() => { onClose={() => {
setSideBarVisible(false) setSideBarVisible(false)
}}> }}>
<SideBarContent {...props} /> <SideBarContent siteInfo={siteInfo} {...props} />
</SideBarDrawer> </SideBarDrawer>
</div> </div>
</ThemeGlobalGame.Provider> </ThemeGlobalGame.Provider>
@@ -138,19 +130,25 @@ const LayoutBase = props => {
* @returns * @returns
*/ */
const LayoutIndex = props => { const LayoutIndex = props => {
const { tagOptions, currentTag, categoryOptions, currentCategory } = props const { tagOptions, currentTag, categoryOptions, currentCategory, siteInfo } =
props
return ( return (
<> <>
{/* 首页移动端顶部导航 */} {/* 首页移动端顶部导航 */}
<div className='p-2 xl:hidden'> <div className='p-2 xl:hidden'>
<Header /> <Header siteInfo={siteInfo} />
</div> </div>
{/* 最近游戏 */} {/* 最近游戏 */}
<GameListRecent /> <GameListRecent />
{/* 游戏列表 */} {/* 游戏列表 */}
<LayoutPostList {...props} /> <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'> <div className='w-full bg-white dark:bg-hexo-black-gray rounded-lg p-2'>
{/* 标签汇总 */} {/* 标签汇总 */}
<GroupCategory <GroupCategory
@@ -159,16 +157,8 @@ const LayoutIndex = props => {
/> />
<hr /> <hr />
<GroupTag tagOptions={tagOptions} currentTag={currentTag} /> <GroupTag tagOptions={tagOptions} currentTag={currentTag} />
</div> {/* 站点公告信息 */}
<Announcement {...props} className='p-2' />
{/* 广告 */}
<div className='w-full'>
<AdSlot type='in-article' />
</div>
{/* 站点公告信息 */}
<div className='w-full bg-white dark:bg-hexo-black-gray rounded-lg p-2'>
<Announcement {...props} />
</div> </div>
</> </>
) )
@@ -278,27 +268,25 @@ const LayoutArchive = props => {
* @returns * @returns
*/ */
const LayoutSlug = props => { const LayoutSlug = props => {
const { setRecentGames } = useGameGlobal()
const { post, siteInfo, allNavPages, recommendPosts, lock, validPassword } = const { post, siteInfo, allNavPages, recommendPosts, lock, validPassword } =
props props
const game = deepClone(post)
const [loading, setLoading] = useState(true)
// const [url, setUrl] = useState(game?.ext?.href)
const relateGames = recommendPosts const relateGames = recommendPosts
const randomGames = shuffleArray(deepClone(allNavPages)) const randomGames = shuffleArray(deepClone(allNavPages))
// 初始化可安装应用 // 初始化可安装应用
initialPWA(game, siteInfo) initialPWA(post, siteInfo)
// 将当前游戏加入到最近游玩
useEffect(() => { useEffect(() => {
// 更新最新游戏 // 更新最新游戏
const recentGames = localStorage.getItem('recent_games') const recentGames = localStorage.getItem('recent_games')
? JSON.parse(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) { if (existedIndex === -1) {
recentGames.unshift(game) // 将游戏插入到数组头部 recentGames.unshift(post) // 将游戏插入到数组头部
} else { } else {
// 如果游戏已存在于数组中,将其移至数组头部 // 如果游戏已存在于数组中,将其移至数组头部
const existingGame = recentGames.splice(existedIndex, 1)[0] const existingGame = recentGames.splice(existedIndex, 1)[0]
@@ -306,126 +294,37 @@ const LayoutSlug = props => {
} }
localStorage.setItem('recent_games', JSON.stringify(recentGames)) localStorage.setItem('recent_games', JSON.stringify(recentGames))
const iframe = document.getElementById('game-wrapper') setRecentGames(recentGames)
}, [post])
// 定义一个函数来处理iframe加载成功事件
function iframeLoaded() {
if (game) {
setLoading(false)
}
}
// 绑定加载事件
if (iframe?.attachEvent) {
iframe?.attachEvent('onload', iframeLoaded)
} else {
if (iframe) iframe.onload = iframeLoaded
}
// 更改iFrame的title
if (
document
?.getElementById('game-wrapper')
?.contentDocument.querySelector('title')?.textContent
) {
document
.getElementById('game-wrapper')
.contentDocument.querySelector('title').textContent = `${
game?.title || ''
} - Play ${game?.title || ''} on ${siteConfig('TITLE')}`
}
}, [game])
return ( return (
<> <>
{lock && <ArticleLock validPassword={validPassword} />} {lock && <ArticleLock validPassword={validPassword} />}
{!lock && ( {!lock && (
<div id='article-wrapper' className='md:px-2'> <div id='article-wrapper'>
{/* 游戏区域 */} <div className='game-detail-wrapper w-full grow flex'>
<div className='game-detail-wrapper w-full grow flex md:px-2'> <div className={`w-full md:py-2`}>
{/* 移动端返回主页按钮 */} {/* 游戏窗口 */}
<Draggable stick='left'> <GameEmbed post={post} siteInfo={siteInfo} />
<div
style={{ left: '0px', top: '1rem' }}
className='fixed xl:hidden group space-x-1 flex items-center z-20 pr-3 pl-1 bg-[#202030] rounded-r-2xl shadow-lg '>
<Link
href='/'
className='px-1 py-3 hover:scale-125 duration-200 transition-all'
passHref>
<i className='fas fa-arrow-left' />
</Link>{' '}
<span
className='text-white font-serif'
onClick={() => {
document.querySelector('.game-info').scrollIntoView({
behavior: 'smooth',
block: 'end',
inline: 'nearest'
})
}}>
G
</span>
</div>
</Draggable>
<div className='w-full py-1 md:py-4'> {/* 资讯 */}
{/* 游戏区 */} <div className='game-info dark:text-white py-2 px-2 md:px-0 mt-14 md:mt-0'>
<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='w-full'> <div className='w-full'>
<GameListRelate posts={relateGames} /> <GameListRelate posts={relateGames} />
</div> </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} /> <PostInfo post={post} />
<NotionPage post={post} /> <NotionPage post={post} />
{/* 广告嵌入 */} {/* 广告嵌入 */}
<AdSlot /> <AdSlot />
{/* 分享栏目 */}
<ShareBar post={post} /> <ShareBar post={post} />
{/* 评论区 */}
<Comment frontMatter={post} /> <Comment frontMatter={post} />
</div> </div>
)} )}

View File

@@ -5,14 +5,21 @@
* @returns * @returns
*/ */
const Style = () => { const Style = () => {
return <style jsx global>{` return (
<style jsx global>{`
// 底色 // 底色
.dark body{ .dark body {
background-color: black; 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 } export { Style }

View File

@@ -8,7 +8,7 @@ import { useState } from 'react'
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
export const MenuItemCollapse = (props) => { export const MenuItemCollapse = props => {
const { link } = props const { link } = props
const [show, changeShow] = useState(false) const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0 const hasSubMenu = link?.subMenus?.length > 0
@@ -21,7 +21,7 @@ export const MenuItemCollapse = (props) => {
return null return null
} }
const selected = (router.pathname === link.to) || (router.asPath === link.to) const selected = router.pathname === link.to || router.asPath === link.to
const toggleShow = () => { const toggleShow = () => {
changeShow(!show) changeShow(!show)
@@ -31,31 +31,67 @@ export const MenuItemCollapse = (props) => {
changeIsOpen(!isOpen) changeIsOpen(!isOpen)
} }
return <> 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} > <>
<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 '> {hasSubMenu && (
<div><div className={`${link.icon} text-center w-4 mr-4`} />{link.name}</div> <div
</Link>} 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"> {hasSubMenu && (
<div><div className={`${link.icon} text-center w-4 mr-4`} />{link.name}</div> <Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
<div className='inline-flex items-center '><i className={`px-2 fas fa-chevron-right transition-all duration-200 ${isOpen ? 'rotate-90' : ''}`}></i></div> {link?.subMenus?.map((sLink, index) => {
</div>} return (
</div> <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 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'> 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'}> <Link href={sLink.to} target={link?.target}>
<div><div className={`${sLink.icon} text-center w-3 mr-3 text-xs`} />{sLink.title}</div> <div>
</Link> <div
</div> className={`${sLink.icon} text-center w-3 mr-3 text-xs`}
})} />
</Collapse>} {sLink.title}
</div>
</Link>
</div>
)
})}
</Collapse>
)}
</> </>
)
} }

View File

@@ -1,6 +1,6 @@
import Link from 'next/link' import Link from 'next/link'
import { useState } from 'react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useState } from 'react'
export const MenuItemDrop = ({ link }) => { export const MenuItemDrop = ({ link }) => {
const [show, changeShow] = useState(false) const [show, changeShow] = useState(false)
@@ -10,38 +10,64 @@ export const MenuItemDrop = ({ link }) => {
return null return null
} }
const hasSubMenu = link?.subMenus?.length > 0 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 && {!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 ' + <div
(selected ? 'bg-green-600 text-white hover:text-white' : 'hover:text-green-600')}> className={
<div> 'px-2 h-full whitespace-nowrap duration-300 text-sm justify-between dark:text-gray-300 cursor-pointer flex flex-nowrap items-center ' +
{link?.icon && <i className={link?.icon} />} {link?.name} (selected
{hasSubMenu && <i className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>} ? 'bg-green-600 text-white hover:text-white'
</div> : 'hover:text-green-600')
</div> }>
} <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 ' + {hasSubMenu && (
(selected ? 'bg-green-600 text-white hover:text-white' : 'hover:text-green-600')}> <ul
<Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}> 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?.icon && <i className={link?.icon} />} {link?.name} {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> </Link>
</div> </li>
} )
})}
{/* 子菜单 */} </ul>
{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> </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 { 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 { postCount, categoryOptions, tagOptions } = props
const { locale } = useGlobal() const { locale } = useGlobal()
const archiveSlot = <div className='text-center'>{postCount}</div> 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 tagSlot = <div className='text-center'>{tagOptions?.length}</div>
const links = [ 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.ARTICLE,
{ name: locale.COMMON.TAGS, to: '/tag', slot: tagSlot, show: siteConfig('HEO_MENU_TAG', null, CONFIG) } 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 ( return (
<nav id='nav' className='dark:text-gray-200 w-full px-5'> <nav id='nav' className='dark:text-gray-200 w-full px-5'>
{links.map((link, index) => { {links.map((link, index) => {
if (link.show) { if (link.show) {
return ( return (
<div key={index} className=''> <div key={index} className=''>
<Link title={link.to} <Link
href={link.to} title={link.to}
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} href={link.to}
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'}> target={link?.target}
<> className={
<div>{link.name} :</div> '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 className='font-semibold'>{link.slot}</div> }>
</> <>
</Link> <div>{link.name} :</div>
</div> <div className='font-semibold'>{link.slot}</div>
</>
) </Link>
} else { </div>
return null )
} } else {
})} return null
</nav> }
})}
</nav>
) )
} }
export default MenuGroupCard export default MenuGroupCard

View File

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

View File

@@ -1,11 +1,11 @@
// import Image from 'next/image' // 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 LazyImage from '@/components/LazyImage'
import { siteConfig } from '@/lib/config' 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 let wrapperTop = 0
@@ -20,21 +20,29 @@ const Hero = props => {
const scrollToWrapper = () => { const scrollToWrapper = () => {
window.scrollTo({ top: wrapperTop, behavior: 'smooth' }) window.scrollTo({ top: wrapperTop, behavior: 'smooth' })
} }
const GREETING_WORDS = siteConfig('GREETING_WORDS').split(',') const GREETING_WORDS = siteConfig('GREETING_WORDS').split(',')
useEffect(() => { useEffect(() => {
updateHeaderHeight() updateHeaderHeight()
if (!typed && window && document.getElementById('typed')) { if (!typed && window && document.getElementById('typed')) {
changeType( loadExternalResource(
new Typed('#typed', { 'https://cdn.jsdelivr.net/npm/typed.js@2.0.12',
strings: GREETING_WORDS, 'js'
typeSpeed: 200, ).then(() => {
backSpeed: 100, if (window.Typed) {
backDelay: 400, changeType(
showCursor: true, new window.Typed('#typed', {
smartBackspace: true strings: GREETING_WORDS,
}) typeSpeed: 200,
) backSpeed: 100,
backDelay: 400,
showCursor: true,
smartBackspace: true
})
)
}
})
} }
window.addEventListener('resize', updateHeaderHeight) window.addEventListener('resize', updateHeaderHeight)
@@ -51,33 +59,43 @@ const Hero = props => {
} }
return ( return (
<header <header
id="header" style={{ zIndex: 1 }} id='header'
className="w-full h-screen relative bg-black" 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 "> {/* 首页导航大按钮 */}
{/* 站点标题 */} {siteConfig('HEXO_HOME_NAV_BUTTONS', null, CONFIG) && (
<div className='font-black text-4xl md:text-5xl shadow-text'>{siteConfig('TITLE')}</div> <NavButtonGroup {...props} />
{/* 站点欢迎语 */} )}
<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} />} <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
<div onClick={scrollToWrapper} className="z-10 cursor-pointer w-full text-center py-4 text-3xl absolute bottom-10 text-white"> id='header-cover'
<div className="opacity-70 animate-bounce text-xs">{siteConfig('HEXO_SHOW_START_READING', null, CONFIG) && locale.COMMON.START_READING}</div> src={siteInfo?.pageCover}
<i className='opacity-70 animate-bounce fas fa-angle-down' /> className={`header-cover w-full h-screen object-cover object-center ${siteConfig('HEXO_HOME_NAV_BACKGROUND_IMG_FIXED', null, CONFIG) ? 'fixed' : ''}`}
</div> />
</div> </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 { 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 { postCount, categoryOptions, tagOptions } = props
const { locale } = useGlobal() const { locale } = useGlobal()
const archiveSlot = <div className='text-center'>{postCount}</div> 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 tagSlot = <div className='text-center'>{tagOptions?.length}</div>
const links = [ 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.ARTICLE,
{ name: locale.COMMON.TAGS, to: '/tag', slot: tagSlot, show: siteConfig('HEXO_MENU_TAG', null, CONFIG) } 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++) { for (let i = 0; i < links.length; i++) {
@@ -23,29 +40,31 @@ const MenuGroupCard = (props) => {
} }
return ( return (
<nav id='nav' className='leading-8 flex justify-center dark:text-gray-200 w-full'> <nav
{links.map(link => { id='nav'
if (link.show) { className='leading-8 flex justify-center dark:text-gray-200 w-full'>
return ( {links.map(link => {
<Link if (link.show) {
key={`${link.to}`} return (
title={link.to} <Link
href={link.to} key={`${link.to}`}
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} title={link.to}
className={'py-1.5 my-1 px-2 duration-300 text-base justify-center items-center cursor-pointer'}> href={link.to}
target={link?.target}
<div className='w-full items-center justify-center hover:scale-105 duration-200 transform dark:hover:text-indigo-400 hover:text-indigo-600'> className={
<div className='text-center'>{link.name}</div> 'py-1.5 my-1 px-2 duration-300 text-base justify-center items-center cursor-pointer'
<div className='text-center font-semibold'>{link.slot}</div> }>
</div> <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>
</Link> <div className='text-center font-semibold'>{link.slot}</div>
) </div>
} else { </Link>
return null )
} } else {
})} return null
</nav> }
})}
</nav>
) )
} }
export default MenuGroupCard export default MenuGroupCard

View File

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

View File

@@ -1,39 +1,42 @@
import CONFIG from './config' import Comment from '@/components/Comment'
import { createContext, useContext, useEffect, useRef } from 'react' import replaceSearchResult from '@/components/Mark'
import Footer from './components/Footer' import NotionPage from '@/components/NotionPage'
import SideRight from './components/SideRight' import ShareBar from '@/components/ShareBar'
import TopNav from './components/TopNav' import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import { isBrowser } from '@/lib/utils' import { isBrowser } from '@/lib/utils'
import BlogPostListPage from './components/BlogPostListPage' import { Transition } from '@headlessui/react'
import BlogPostListScroll from './components/BlogPostListScroll' import dynamic from 'next/dynamic'
import Hero from './components/Hero' import Link from 'next/link'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import Card from './components/Card' import { createContext, useContext, useEffect, useRef } from 'react'
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 ArticleAdjacent from './components/ArticleAdjacent' import ArticleAdjacent from './components/ArticleAdjacent'
import ArticleCopyright from './components/ArticleCopyright' import ArticleCopyright from './components/ArticleCopyright'
import { ArticleLock } from './components/ArticleLock'
import ArticleRecommend from './components/ArticleRecommend' import ArticleRecommend from './components/ArticleRecommend'
import ShareBar from '@/components/ShareBar' import BlogPostArchive from './components/BlogPostArchive'
import TagItemMini from './components/TagItemMini' import BlogPostListPage from './components/BlogPostListPage'
import Link from 'next/link' 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 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 { 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() const ThemeGlobalHexo = createContext()
@@ -50,93 +53,110 @@ const LayoutBase = props => {
const { onLoading, fullWidth } = useGlobal() const { onLoading, fullWidth } = useGlobal()
const router = useRouter() const router = useRouter()
const headerSlot = post const headerSlot = post ? (
? <PostHeader {...props} /> <PostHeader {...props} />
: (router.route === '/' && siteConfig('HEXO_HOME_BANNER_ENABLE', null, CONFIG) ) : router.route === '/' &&
? <Hero {...props} /> siteConfig('HEXO_HOME_BANNER_ENABLE', null, CONFIG) ? (
: null) <Hero {...props} />
) : null
const drawerRight = useRef(null) const drawerRight = useRef(null)
const tocRef = isBrowser ? document.getElementById('article-wrapper') : null const tocRef = isBrowser ? document.getElementById('article-wrapper') : null
const floatSlot = <> const floatSlot = (
{post?.toc?.length > 1 && <div className="block lg:hidden"> <>
<TocDrawerButton {post?.toc?.length > 1 && (
onClick={() => { <div className='block lg:hidden'>
drawerRight?.current?.handleSwitchVisible() <TocDrawerButton
}} onClick={() => {
/> drawerRight?.current?.handleSwitchVisible()
</div>} }}
<JumpToCommentButton /> />
</div>
)}
{post && <JumpToCommentButton />}
</> </>
)
// Algolia搜索框 // Algolia搜索框
const searchModal = useRef(null) const searchModal = useRef(null)
return ( return (
<ThemeGlobalHexo.Provider value={{ searchModal }}> <ThemeGlobalHexo.Provider value={{ searchModal }}>
<div id='theme-hexo' className={`${siteConfig('FONT_STYLE')} dark:bg-black scroll-smooth`}> <div
<Style/> 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} show={!onLoading}
appear={true} appear={true}
enter="transition ease-in-out duration-700 transform order-first" enter='transition ease-in-out duration-700 transform order-first'
enterFrom="opacity-0 -translate-y-16" enterFrom='opacity-0 translate-y-16'
enterTo="opacity-100" enterTo='opacity-100'
leave="transition ease-in-out duration-300 transform" leave='transition ease-in-out duration-300 transform'
leaveFrom="opacity-100" leaveFrom='opacity-100 translate-y-0'
leaveTo="opacity-0 translate-y-16" leaveTo='opacity-0 -translate-y-16'
unmount={false} unmount={false}>
> {/* 主区上部嵌入 */}
{headerSlot} {slotTop}
</Transition>
{/* 主区块 */} {children}
<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`}> </Transition>
<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} />
</div> </div>
{/* 悬浮菜单 */} {/* 右侧栏 */}
<RightFloatArea floatSlot={floatSlot} /> <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'>
<AlgoliaSearchModal cRef={searchModal} {...props}/> <TocDrawer post={post} cRef={drawerRight} targetRef={tocRef} />
{/* 页脚 */}
<Footer title={siteConfig('TITLE') } />
</div> </div>
{/* 悬浮菜单 */}
<RightFloatArea floatSlot={floatSlot} />
{/* 全文搜索 */}
<AlgoliaSearchModal cRef={searchModal} {...props} />
{/* 页脚 */}
<Footer title={siteConfig('TITLE')} />
</div>
</ThemeGlobalHexo.Provider> </ThemeGlobalHexo.Provider>
) )
} }
@@ -147,7 +167,7 @@ const LayoutBase = props => {
* @param {*} props * @param {*} props
* @returns * @returns
*/ */
const LayoutIndex = (props) => { const LayoutIndex = props => {
return <LayoutPostList {...props} className='pt-8' /> return <LayoutPostList {...props} className='pt-8' />
} }
@@ -156,11 +176,17 @@ const LayoutIndex = (props) => {
* @param {*} props * @param {*} props
* @returns * @returns
*/ */
const LayoutPostList = (props) => { const LayoutPostList = props => {
return <div className='pt-8'> return (
<SlotBar {...props} /> <div className='pt-8'>
{siteConfig('POST_LIST_STYLE') === 'page' ? <BlogPostListPage {...props} /> : <BlogPostListScroll {...props} />} <SlotBar {...props} />
{siteConfig('POST_LIST_STYLE') === 'page' ? (
<BlogPostListPage {...props} />
) : (
<BlogPostListScroll {...props} />
)}
</div> </div>
)
} }
/** /**
@@ -187,11 +213,20 @@ const LayoutSearch = props => {
}) })
return ( return (
<div className='pt-8'> <div className='pt-8'>
{!currentSearch {!currentSearch ? (
? <SearchNav {...props} /> <SearchNav {...props} />
: <div id="posts-wrapper"> {siteConfig('POST_LIST_STYLE') === 'page' ? <BlogPostListPage {...props} /> : <BlogPostListScroll {...props} />} </div>} ) : (
<div id='posts-wrapper'>
{' '}
{siteConfig('POST_LIST_STYLE') === 'page' ? (
<BlogPostListPage {...props} />
) : (
<BlogPostListScroll {...props} />
)}{' '}
</div> </div>
)}
</div>
) )
} }
@@ -200,21 +235,23 @@ const LayoutSearch = props => {
* @param {*} props * @param {*} props
* @returns * @returns
*/ */
const LayoutArchive = (props) => { const LayoutArchive = props => {
const { archivePosts } = props const { archivePosts } = props
return <div className='pt-8'> return (
<Card className='w-full'> <div className='pt-8'>
<div className="mb-10 pb-20 bg-white md:p-12 p-3 min-h-full dark:bg-hexo-black-gray"> <Card className='w-full'>
{Object.keys(archivePosts).map(archiveTitle => ( <div className='mb-10 pb-20 bg-white md:p-12 p-3 min-h-full dark:bg-hexo-black-gray'>
<BlogPostArchive {Object.keys(archivePosts).map(archiveTitle => (
key={archiveTitle} <BlogPostArchive
posts={archivePosts[archiveTitle]} key={archiveTitle}
archiveTitle={archiveTitle} posts={archivePosts[archiveTitle]}
/> archiveTitle={archiveTitle}
))} />
</div> ))}
</Card> </div>
</Card>
</div> </div>
)
} }
/** /**
@@ -228,51 +265,60 @@ const LayoutSlug = props => {
useEffect(() => { useEffect(() => {
// 404 // 404
if (!post) { if (!post) {
setTimeout(() => { setTimeout(
if (isBrowser) { () => {
const article = document.getElementById('notion-article') if (isBrowser) {
if (!article) { const article = document.getElementById('notion-article')
router.push('/404').then(() => { if (!article) {
console.warn('找不到页面', router.asPath) router.push('/404').then(() => {
}) console.warn('找不到页面', router.asPath)
})
}
} }
} },
}, siteConfig('POST_WAITING_TIME_FOR_404') * 1000) siteConfig('POST_WAITING_TIME_FOR_404') * 1000
)
} }
}, [post]) }, [post])
return ( 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"> <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 && <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文章主体 */} <ShareBar post={post} />
<section className='px-5 justify-center mx-auto max-w-2xl lg:max-w-full'> {post?.type === 'Post' && (
{post && <NotionPage post={post} />} <>
</section> <ArticleCopyright {...props} />
<ArticleRecommend {...props} />
<ArticleAdjacent {...props} />
</>
)}
</article>
{/* 分享 */} <div className='pt-4 border-dashed'></div>
<ShareBar post={post} />
{post?.type === 'Post' && <>
<ArticleCopyright {...props} />
<ArticleRecommend {...props} />
<ArticleAdjacent {...props} />
</>}
</article> {/* 评论互动 */}
<div className='duration-200 overflow-x-auto bg-white dark:bg-hexo-black-gray px-3'>
<div className='pt-4 border-dashed'></div> <Comment frontMatter={post} />
{/* 评论互动 */}
<div className="duration-200 overflow-x-auto bg-white dark:bg-hexo-black-gray px-3">
<Comment frontMatter={post} />
</div>
</div>}
</div> </div>
</div>
</> )}
</div>
</>
) )
} }
@@ -297,18 +343,18 @@ const Layout404 = props => {
}, 3000) }, 3000)
}) })
return ( return (
<> <>
<div className="text-black w-full h-screen text-center justify-center content-center items-center flex flex-col"> <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"> <div className='dark:text-gray-200'>
<h2 className="inline-block border-r-2 border-gray-600 mr-2 px-3 py-2 align-top"> <h2 className='inline-block border-r-2 border-gray-600 mr-2 px-3 py-2 align-top'>
404 404
</h2> </h2>
<div className="inline-block text-left h-32 leading-10 items-center"> <div className='inline-block text-left h-32 leading-10 items-center'>
<h2 className="m-0 p-0">页面未找到</h2> <h2 className='m-0 p-0'>页面未找到</h2>
</div> </div>
</div> </div>
</div> </div>
</> </>
) )
} }
@@ -321,24 +367,32 @@ const LayoutCategoryIndex = props => {
const { categoryOptions } = props const { categoryOptions } = props
const { locale } = useGlobal() const { locale } = useGlobal()
return ( return (
<div className='mt-8'> <div className='mt-8'>
<Card className="w-full min-h-screen"> <Card className='w-full min-h-screen'>
<div className="dark:text-gray-200 mb-5 mx-3"> <div className='dark:text-gray-200 mb-5 mx-3'>
<i className="mr-4 fas fa-th" /> {locale.COMMON.CATEGORY}: <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> </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 { tagOptions } = props
const { locale } = useGlobal() const { locale } = useGlobal()
return ( return (
<div className='mt-8'> <div className='mt-8'>
<Card className='w-full'> <Card className='w-full'>
<div className="dark:text-gray-200 mb-5 ml-4"> <div className='dark:text-gray-200 mb-5 ml-4'>
<i className="mr-4 fas fa-tag" /> {locale.COMMON.TAGS}: <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> </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 { export {
CONFIG as THEME_CONFIG,
LayoutBase,
LayoutIndex,
LayoutSearch,
LayoutArchive,
LayoutSlug,
Layout404, Layout404,
LayoutArchive,
LayoutBase,
LayoutCategoryIndex, LayoutCategoryIndex,
LayoutIndex,
LayoutPostList, LayoutPostList,
LayoutTagIndex LayoutSearch,
LayoutSlug,
LayoutTagIndex,
CONFIG as THEME_CONFIG
} }

View File

@@ -1,10 +1,10 @@
// import Image from 'next/image' // 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 LazyImage from '@/components/LazyImage'
import { siteConfig } from '@/lib/config' 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 let wrapperTop = 0
@@ -21,16 +21,23 @@ const Hero = props => {
useEffect(() => { useEffect(() => {
updateHeaderHeight() updateHeaderHeight()
if (!typed && window && document.getElementById('typed')) { if (!typed && window && document.getElementById('typed')) {
changeType( loadExternalResource(
new Typed('#typed', { 'https://cdn.jsdelivr.net/npm/typed.js@2.0.12',
strings: GREETING_WORDS, 'js'
typeSpeed: 200, ).then(() => {
backSpeed: 100, if (window.Typed) {
backDelay: 400, changeType(
showCursor: true, new window.Typed('#typed', {
smartBackspace: true strings: GREETING_WORDS,
}) typeSpeed: 200,
) backSpeed: 100,
backDelay: 400,
showCursor: true,
smartBackspace: true
})
)
}
})
} }
window.addEventListener('resize', updateHeaderHeight) window.addEventListener('resize', updateHeaderHeight)
@@ -47,29 +54,40 @@ const Hero = props => {
} }
return ( return (
<header <header
id="header" style={{ zIndex: 1 }} id='header'
className=" w-full h-screen relative bg-black" 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 "> <LazyImage
{/* 站点标题 */} priority={true}
<div className='text-4xl md:text-5xl shadow-text'>{siteConfig('TITLE')}</div> id='header-cover'
{/* 站点欢迎语 */} src={siteInfo?.pageCover}
<div className='mt-2 h-12 items-center text-center shadow-text text-white text-lg'> className={`header-cover object-center w-full h-screen object-cover ${siteConfig('MATERY_HOME_NAV_BACKGROUND_IMG_FIXED', null, CONFIG) ? 'fixed' : ''}`}
<span id='typed' /> />
</div> </header>
{/* 滚动按钮 */}
<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>
) )
} }

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 { 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 { postCount, categories, tags } = props
const { locale } = useGlobal() const { locale } = useGlobal()
const archiveSlot = <div className='text-center'>{postCount}</div> 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 tagSlot = <div className='text-center'>{tags?.length}</div>
const links = [ 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.ARTICLE,
{ name: locale.COMMON.TAGS, to: '/tag', slot: tagSlot, show: siteConfig('MATERY_MENU_TAG', null, CONFIG) } 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 ( return (
<nav id='nav' className='leading-8 flex justify-center w-full'> <nav id='nav' className='leading-8 flex justify-center w-full'>
{links.map(link => { {links.map(link => {
if (link.show) { if (link.show) {
return ( return (
<Link <Link
key={`${link.to}`} key={`${link.to}`}
title={link.to} title={link.to}
href={link.to} href={link.to}
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} target={link?.target}
className={'py-1.5 my-1 px-2 duration-300 text-base justify-center items-center cursor-pointer'}> 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='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 font-semibold'>{link.slot}</div> <div className='text-center'>{link.name}</div>
</div> <div className='text-center font-semibold'>{link.slot}</div>
</div>
</Link> </Link>
) )
} else { } else {
return null return null
} }
})} })}
</nav> </nav>
) )
} }
export default MenuGroupCard export default MenuGroupCard

View File

@@ -27,35 +27,63 @@ export const MenuItemCollapse = ({ link }) => {
return null return null
} }
const selected = (router.pathname === link.to) || (router.asPath === link.to) const selected = router.pathname === link.to || router.asPath === link.to
return <> 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 ')}> <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'}> {hasSubMenu && (
<div className='my-auto items-center justify-between flex '> <div
{link.icon && <i className={`${link.icon} w-4 mr-6 text-center`} />} onClick={hasSubMenu ? toggleOpenSubMenu : null}
<div >{link.name}</div> className='my-auto items-center w-full justify-between flex '>
</div> <div className=''>
{link.slot} <i className={`${link.icon} w-4 mr-6 text-center`} />
</Link>} {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> {hasSubMenu && (
<i className={`px-2 fas fa-chevron-left transition-all duration-200 ${isOpen ? '-rotate-90' : ''}`}></i> <Collapse isOpen={isOpen}>
</div>} {link.subMenus.map((sLink, index) => {
</div> return (
<div
{/* 折叠子菜单 */} key={index}
{hasSubMenu && <Collapse isOpen={isOpen}> 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.subMenus.map((sLink, index) => { <Link href={sLink.to} target={link?.target}>
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'> <span className='text-sm'>
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}> <i className={`${sLink.icon} w-4 mr-3 text-center`} />
<span className='text-sm'><i className={`${sLink.icon} w-4 mr-3 text-center`} />{sLink.title}</span> {sLink.title}
</Link> </span>
</div> </Link>
})} </div>
</Collapse>} )
})}
</Collapse>
)}
</> </>
)
} }

View File

@@ -9,33 +9,51 @@ export const MenuItemDrop = ({ link }) => {
return null 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 && {hasSubMenu && (
<Link <>
href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} <div className='cursor-pointer menu-link pl-2 pr-4 no-underline tracking-widest pb-1'>
className=" menu-link pl-2 pr-4 no-underline tracking-widest pb-1"> {link?.icon && <i className={link?.icon} />} {link?.name}
{link?.icon && <i className={link?.icon} />} {link?.name} <i
{hasSubMenu && <i className='px-2 fa fa-angle-down'></i>} className={`px-2 fa fa-angle-down duration-300 ${show ? 'rotate-180' : 'rotate-0'}`}></i>
</Link>} </div>
</>
{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 && (
<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> </div>
)
} }

View File

@@ -8,7 +8,7 @@ import { useState } from 'react'
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
export const MenuItemCollapse = (props) => { export const MenuItemCollapse = props => {
const { link } = props const { link } = props
const [show, changeShow] = useState(false) const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0 const hasSubMenu = link?.subMenus?.length > 0
@@ -21,7 +21,7 @@ export const MenuItemCollapse = (props) => {
return null return null
} }
const selected = (router.pathname === link.to) || (router.asPath === link.to) const selected = router.pathname === link.to || router.asPath === link.to
const toggleShow = () => { const toggleShow = () => {
changeShow(!show) changeShow(!show)
@@ -31,32 +31,67 @@ export const MenuItemCollapse = (props) => {
changeIsOpen(!isOpen) changeIsOpen(!isOpen)
} }
return <> 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} > <>
<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 '> {hasSubMenu && (
<div><div className={`${link.icon} text-center w-4 mr-4`} />{link.name}</div> <div
</Link>} 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} {hasSubMenu && (
className="py-2 font-extralight flex justify-between cursor-pointer dark:text-gray-200 no-underline tracking-widest"> <Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
<div><div className={`${link.icon} text-center w-4 mr-4`} />{link.name}</div> {link?.subMenus?.map(sLink => {
<div className='inline-flex items-center '><i className={`px-2 fas fa-chevron-right transition-all duration-200 ${isOpen ? 'rotate-90' : ''}`}></i></div> return (
</div>} <div
</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 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'> 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'}> <Link href={sLink.to} target={link?.target}>
<div><div className={`${sLink.icon} text-center w-3 mr-3 text-xs`} />{sLink.title}</div> <div>
</Link> <div
</div> className={`${sLink.icon} text-center w-3 mr-3 text-xs`}
})} />
</Collapse>} {sLink.title}
</div>
</Link>
</div>
)
})}
</Collapse>
)}
</> </>
)
} }

View File

@@ -1,6 +1,6 @@
import Link from 'next/link' import Link from 'next/link'
import { useState } from 'react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useState } from 'react'
export const MenuItemDrop = ({ link }) => { export const MenuItemDrop = ({ link }) => {
const [show, changeShow] = useState(false) const [show, changeShow] = useState(false)
@@ -12,39 +12,65 @@ export const MenuItemDrop = ({ link }) => {
return null return null
} }
const hasSubMenu = link?.subMenus?.length > 0 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 && {!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 ' + <div
(selected ? 'bg-green-600 text-white hover:text-white' : 'hover:text-green-600')}> className={
<div> 'px-3 h-full whitespace-nowrap duration-300 text-sm justify-between dark:text-gray-300 cursor-pointer flex flex-nowrap items-center ' +
{link?.icon && <i className={link?.icon} />} {link?.name} (selected
{hasSubMenu && <i className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>} ? 'bg-green-600 text-white hover:text-white'
</div> : 'hover:text-green-600')
</div> }>
} <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 ' + {hasSubMenu && (
(selected ? 'bg-green-600 text-white hover:text-white' : 'hover:text-green-600')}> <ul
<Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}> 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?.icon && <i className={link?.icon} />} {link?.name} {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> </Link>
</div> </li>
} )
})}
{/* 子菜单 */} </ul>
{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> </li>
)
} }

View File

@@ -28,11 +28,13 @@ export const MenuItemCollapse = props => {
return ( 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 && ( {!hasSubMenu && (
<Link <Link
href={link?.to} 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'> className='flex justify-between pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest'>
<span className=' transition-all items-center duration-200'> <span className=' transition-all items-center duration-200'>
{link?.icon && <i className={link.icon + ' mr-4'} />} {link?.icon && <i className={link.icon + ' mr-4'} />}
@@ -56,15 +58,19 @@ export const MenuItemCollapse = props => {
{/* 折叠子菜单 */} {/* 折叠子菜单 */}
{hasSubMenu && ( {hasSubMenu && (
<Collapse isOpen={isOpen} onHeightChange={props.onHeightChange} className='rounded-xl'> <Collapse
isOpen={isOpen}
onHeightChange={props.onHeightChange}
className='rounded-xl'>
{link.subMenus.map((sLink, index) => { {link.subMenus.map((sLink, index) => {
return ( return (
<div <div
key={index} key={index}
className='dark:text-gray-200 text-left px-3 justify-start tracking-widest transition-all duration-200 py-3 pr-6'> 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'> <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> </span>
</Link> </Link>
</div> </div>

View File

@@ -12,26 +12,23 @@ export const MenuItemDrop = ({ link }) => {
return ( return (
<div <div
onMouseOver={() => changeShow(true)} onMouseOver={() => changeShow(true)}
onMouseOut={() => changeShow(false)} onMouseOut={() => changeShow(false)}>
>
{!hasSubMenu && ( {!hasSubMenu && (
<Link <Link
href={link?.to} href={link?.to}
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} target={link?.target}
className="select-none menu-link pl-2 pr-4 no-underline tracking-widest pb-1" className='select-none menu-link pl-2 pr-4 no-underline tracking-widest pb-1'>
>
{link?.icon && <i className={link?.icon} />} {link?.name} {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> </Link>
)} )}
{hasSubMenu && ( {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} {link?.icon && <i className={link?.icon} />} {link?.name}
<i <i
className={`px-2 fa fa-angle-down duration-300 ${show ? 'rotate-180' : 'rotate-0'}`} className={`px-2 fa fa-angle-down duration-300 ${show ? 'rotate-180' : 'rotate-0'}`}></i>
></i>
</div> </div>
</> </>
)} )}
@@ -40,19 +37,14 @@ export const MenuItemDrop = ({ link }) => {
{hasSubMenu && ( {hasSubMenu && (
<ul <ul
style={{ backdropFilter: 'blur(3px)' }} 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) => { {link.subMenus.map((sLink, index) => {
return ( return (
<li <li
key={index} 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" 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}>
<Link <span className='text-sm'>
href={sLink.to}
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
>
<span className="text-sm">
{link?.icon && <i className={sLink?.icon}> &nbsp; </i>} {link?.icon && <i className={sLink?.icon}> &nbsp; </i>}
{sLink.title} {sLink.title}
</span> </span>

View File

@@ -8,7 +8,7 @@ import { useState } from 'react'
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
export const MenuItemCollapse = (props) => { export const MenuItemCollapse = props => {
const { link } = props const { link } = props
const [show, changeShow] = useState(false) const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0 const hasSubMenu = link?.subMenus?.length > 0
@@ -21,7 +21,7 @@ export const MenuItemCollapse = (props) => {
return null return null
} }
const selected = (router.pathname === link.to) || (router.asPath === link.to) const selected = router.pathname === link.to || router.asPath === link.to
const toggleShow = () => { const toggleShow = () => {
changeShow(!show) changeShow(!show)
@@ -31,32 +31,67 @@ export const MenuItemCollapse = (props) => {
changeIsOpen(!isOpen) changeIsOpen(!isOpen)
} }
return <> 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} > <>
<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 '> {hasSubMenu && (
<div><div className={`${link.icon} text-center w-4 mr-4`} />{link.name}</div> <div
</Link>} 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"> {hasSubMenu && (
<div><div className={`${link.icon} text-center w-4 mr-2`} />{link.name}</div> <Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
<div className='inline-flex items-center '><i className={`px-2 fas fa-chevron-right transition-all duration-200 ${isOpen ? 'rotate-90' : ''}`}></i></div> {link?.subMenus?.map((sLink, index) => {
</div>} return (
</div> <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 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'> 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'}> */} {/* <Link href={sLink.to} target={link?.target}> */}
<a href={`/#${sLink.title}`} target={'_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> <div>
</a> <div
</div> className={`${sLink?.icon ? sLink?.icon : 'fas fa-hashtag'} text-center w-3 mr-2 text-xs`}
})} />
</Collapse>} {sLink.title}
</div>
</a>
</div>
)
})}
</Collapse>
)}
</> </>
)
} }

View File

@@ -1,6 +1,6 @@
import Link from 'next/link' import Link from 'next/link'
import { useState } from 'react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useState } from 'react'
export const MenuItemDrop = ({ link }) => { export const MenuItemDrop = ({ link }) => {
const [show, changeShow] = useState(false) const [show, changeShow] = useState(false)
@@ -12,39 +12,65 @@ export const MenuItemDrop = ({ link }) => {
return null return null
} }
const hasSubMenu = link?.subMenus?.length > 0 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 && {!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 ' + <div
(selected ? 'bg-green-600 text-white hover:text-white' : 'hover:text-green-600')}> className={
<div> 'px-2 h-full whitespace-nowrap duration-300 text-sm justify-between dark:text-gray-300 cursor-pointer flex flex-nowrap items-center ' +
{link?.icon && <i className={link?.icon} />} {link?.name} (selected
{hasSubMenu && <i className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>} ? 'bg-green-600 text-white hover:text-white'
</div> : 'hover:text-green-600')
</div> }>
} <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 ' + {hasSubMenu && (
(selected ? 'bg-green-600 text-white hover:text-white' : 'hover:text-green-600')}> <ul
<Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}> 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?.icon && <i className={link?.icon} />} {link?.name} {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> </Link>
</div> </li>
} )
})}
{/* 子菜单 */} </ul>
{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> </li>
)
} }

View File

@@ -7,7 +7,7 @@ import { useState } from 'react'
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
export const MenuItemCollapse = (props) => { export const MenuItemCollapse = props => {
const { link } = props const { link } = props
const [show, changeShow] = useState(false) const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0 const hasSubMenu = link?.subMenus?.length > 0
@@ -22,32 +22,64 @@ export const MenuItemCollapse = (props) => {
changeIsOpen(!isOpen) changeIsOpen(!isOpen)
} }
return <> 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'} <div
className='w-full my-auto items-center justify-between flex dark:text-gray-200 '> 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'
<div><div className={`${link.icon} text-center w-4 mr-4`} />{link.name}</div> onClick={toggleShow}>
</Link>} {!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 {hasSubMenu && (
onClick={hasSubMenu ? toggleOpenSubMenu : null} <div
className="font-extralight flex justify-between cursor-pointer dark:text-gray-200 no-underline tracking-widest"> onClick={hasSubMenu ? toggleOpenSubMenu : null}
<div><div className={`${link.icon} text-center w-4 mr-4`} />{link.name}</div> className='font-extralight flex justify-between cursor-pointer dark:text-gray-200 no-underline tracking-widest'>
<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>} <div className={`${link.icon} text-center w-4 mr-4`} />
</div> {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}> {hasSubMenu && (
{link.subMenus.map((sLink, index) => { <Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
return <div key={index} className='whitespace-nowrap dark:text-gray-200 {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 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'> 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'}> <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> <div>
</Link> {sLink.icon && (
</div> <div
})} className={`${sLink.icon} text-center w-3 mr-3 text-xs`}
</Collapse>} />
)}
{sLink.title}
</div>
</Link>
</div>
)
})}
</Collapse>
)}
</> </>
)
} }

View File

@@ -5,34 +5,62 @@ export const MenuItemDrop = ({ link }) => {
const [show, changeShow] = useState(false) const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0 const hasSubMenu = link?.subMenus?.length > 0
return <li onMouseOver={() => changeShow(true)} onMouseOut={() => changeShow(false)} return (
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 '> <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 && {hasSubMenu && (
<Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} className='w-full my-auto items-center justify-between flex ' > <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> <div>
{link.slot} <div className={`${link.icon} text-center w-4 mr-4`} />
</Link>} {link.name}
</div>
{hasSubMenu && {link.slot}
<div className='w-full my-auto items-center justify-between flex '> {hasSubMenu && (
<div><div className={`${link.icon} text-center w-4 mr-4`} />{link.name}</div> <div className='text-right'>
{link.slot} <i
{hasSubMenu && <div className='text-right'><i className={`px-2 fas fa-chevron-right duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i></div>} className={`px-2 fas fa-chevron-right duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>
</div>} </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 && (
<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> </li>
)
} }

View File

@@ -7,7 +7,7 @@ import { useState } from 'react'
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
export const MenuItemCollapse = (props) => { export const MenuItemCollapse = props => {
const { link } = props const { link } = props
const [show, changeShow] = useState(false) const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0 const hasSubMenu = link?.subMenus?.length > 0
@@ -26,30 +26,59 @@ export const MenuItemCollapse = (props) => {
return null return null
} }
return <> return (
<div className='w-full px-4 py-2 text-left dark:bg-hexo-black-gray dark:border-black' onClick={toggleShow} > <>
{!hasSubMenu && <Link <div
href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} className='w-full px-4 py-2 text-left dark:bg-hexo-black-gray dark:border-black'
className="font-extralight flex justify-between pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest pb-1"> onClick={toggleShow}>
<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> {!hasSubMenu && (
</Link>} <Link
{hasSubMenu && <div href={link?.to}
onClick={hasSubMenu ? toggleOpenSubMenu : null} target={link?.target}
className="font-extralight flex justify-between pl-2 pr-4 cursor-pointer dark:text-gray-200 no-underline tracking-widest pb-1"> 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> <span className=' hover:text-red-400 transition-all items-center duration-200'>
<i className='px-2 fa fa-plus text-gray-400'></i> {link?.icon && (
</div>} <span className='mr-2'>
</div> <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}> {hasSubMenu && (
{link.subMenus.map((sLink, index) => { <Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
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.subMenus.map((sLink, index) => {
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}> return (
<span className='text-xs'>{sLink.title}</span> <div
</Link> key={index}
</div> 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}>
</Collapse>} <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 const hasSubMenu = link?.subMenus?.length > 0
return <li className='mx-3 my-2' > return (
<div className='cursor-pointer ' onMouseOver={() => changeShow(true)} onMouseOut={() => changeShow(false)}> <li className='mx-3 my-2'>
{!hasSubMenu && <div
<div className="block text-black dark:text-gray-50 nav" > className='cursor-pointer '
<Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} > onMouseOver={() => changeShow(true)}
{link?.icon && <i className={link?.icon} />} {link?.name} onMouseOut={() => changeShow(false)}>
</Link> {!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> </div>
} )
})}
{hasSubMenu && </ul>
<div className='block text-black dark:text-gray-50 nav'> )}
{link?.icon && <i className={link?.icon} />} {link?.name} </div>
<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>
</li> </li>
)
} }

View File

@@ -7,7 +7,7 @@ import { useState } from 'react'
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
export const MenuItemCollapse = (props) => { export const MenuItemCollapse = props => {
const { link } = props const { link } = props
const [show, changeShow] = useState(false) const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0 const hasSubMenu = link?.subMenus?.length > 0
@@ -26,30 +26,59 @@ export const MenuItemCollapse = (props) => {
return null return null
} }
return <> return (
<div className='w-full px-4 py-2 text-left dark:bg-hexo-black-gray dark:border-black' onClick={toggleShow} > <>
{!hasSubMenu && <Link <div
href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} className='w-full px-4 py-2 text-left dark:bg-hexo-black-gray dark:border-black'
className="font-extralight flex justify-between pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest pb-1"> onClick={toggleShow}>
<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> {!hasSubMenu && (
</Link>} <Link
{hasSubMenu && <div href={link?.to}
onClick={hasSubMenu ? toggleOpenSubMenu : null} target={link?.target}
className="font-extralight flex justify-between pl-2 pr-4 cursor-pointer dark:text-gray-200 no-underline tracking-widest pb-1"> 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> <span className=' hover:text-red-400 transition-all items-center duration-200'>
<i className='px-2 fa fa-plus text-gray-400'></i> {link?.icon && (
</div>} <span className='mr-2'>
</div> <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}> {hasSubMenu && (
{link.subMenus.map((sLink, index) => { <Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
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.subMenus.map((sLink, index) => {
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}> return (
<span className='text-xs'>{sLink.title}</span> <div
</Link> key={index}
</div> 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}>
</Collapse>} <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 const hasSubMenu = link?.subMenus?.length > 0
return <li className='cursor-pointer py-2 px-3' onMouseEnter={() => changeShow(true)} onMouseLeave={() => changeShow(false)}> return (
{!hasSubMenu && <li
<div className="block text-black dark:text-gray-50 nav" > className='cursor-pointer py-2 px-3'
<Link href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} > onMouseEnter={() => changeShow(true)}
{link?.icon && <i className={link?.icon} />} {link?.name} onMouseLeave={() => changeShow(false)}>
</Link> {!hasSubMenu && (
</div> <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 && {hasSubMenu && (
<div className='block text-black dark:text-gray-50 nav'> <div className='block text-black dark:text-gray-50 nav'>
{link?.icon && <i className={link?.icon} />} {link?.name} {link?.icon && <i className={link?.icon} />} {link?.name}
<i className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i> <i
</div> 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 `}> {hasSubMenu && (
{link.subMenus.map((sLink, index) => { <ul
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'> 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 href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}> {link.subMenus.map((sLink, index) => {
<span className='text-sm text-nowrap font-extralight'>{link?.icon && <i className={sLink?.icon} > &nbsp; </i>}{sLink.title}</span> return (
</Link> <li
</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'>
</ul>} <Link href={sLink.to} target={link?.target}>
<span className='text-sm text-nowrap font-extralight'>
</li> {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 * @param {*} param0
* @returns * @returns
*/ */
export const MenuItemCollapse = (props) => { export const MenuItemCollapse = props => {
const { link } = props const { link } = props
const [show, changeShow] = useState(false) const [show, changeShow] = useState(false)
const hasSubMenu = link?.subMenus?.length > 0 const hasSubMenu = link?.subMenus?.length > 0
@@ -26,30 +26,67 @@ export const MenuItemCollapse = (props) => {
return null return null
} }
return <> 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 <div
href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} className='w-full px-8 py-3 text-left border-b dark:bg-hexo-black-gray dark:border-black'
className="items-center flex justify-between pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest pb-1"> onClick={toggleShow}>
<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> {!hasSubMenu && (
</Link>} <Link
{hasSubMenu && <div href={link?.to}
onClick={hasSubMenu ? toggleOpenSubMenu : null} target={link?.target}
className="items-center flex justify-between pl-2 pr-4 cursor-pointer dark:text-gray-200 no-underline tracking-widest pb-1"> 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> <span className='text-blue-600 dark:text-blue-300 hover:text-red-400 transition-all items-center duration-200'>
<i className={`px-2 fa fa-plus transition-all duration-200 ${isOpen && 'rotate-45'} text-gray-400`}></i> {link?.icon && (
</div>} <span className='mr-2'>
</div> <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}> {hasSubMenu && (
{link.subMenus.map((sLink, index) => { <Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
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.subMenus.map((sLink, index) => {
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}> return (
<span className='ml-4 text-sm'>{sLink?.icon && <span className='mr-2 w-4'><i className={sLink.icon}/></span>}{sLink.title}</span> <div
</Link> key={index}
</div> 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}>
</Collapse>} <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 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 && {hasSubMenu && (
<Link <>
href={link?.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'} <div className='cursor-pointer menu-link pl-2 pr-4 text-gray-700 dark:text-gray-200 no-underline tracking-widest pb-1'>
className=" menu-link pl-2 pr-4 text-gray-700 dark:text-gray-200 no-underline tracking-widest pb-1"> {link?.icon && (
{link?.icon && <span className='mr-2'><i className={link.icon} /></span>}{link?.name} <span className='mr-2'>
{hasSubMenu && <i className='px-2 fa fa-angle-down'></i>} <i className={link.icon} />
</Link>} </span>
)}{' '}
{hasSubMenu && <> {link?.name}
<div className='cursor-pointer menu-link pl-2 pr-4 text-gray-700 dark:text-gray-200 no-underline tracking-widest pb-1'> <i
{link?.icon && <span className='mr-2'><i className={link.icon} /></span>} {link?.name} className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i>
<i className={`px-2 fas fa-chevron-down duration-500 transition-all ${show ? ' rotate-180' : ''}`}></i> </div>
</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 && (
<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> </div>
)
} }

View File

@@ -1,90 +1,99 @@
import { siteConfig } from '@/lib/config' import { siteConfig } from '@/lib/config'
import CONFIG from '../config' import CONFIG from '../config'
import { SVGLocation } from './svg/SVGLocation'
import { SVGEmail } from './svg/SVGEmail' import { SVGEmail } from './svg/SVGEmail'
import { SVGLocation } from './svg/SVGLocation'
/* eslint-disable react/no-unescaped-entities */ /* eslint-disable react/no-unescaped-entities */
export const Contact = () => { export const Contact = () => {
return <> return (
{/* <!-- ====== Contact Start ====== --> */} <>
<section id="contact" className="relative py-20 md:py-[120px]"> {/* <!-- ====== Contact Start ====== --> */}
<div <section id='contact' className='relative py-20 md:py-[120px]'>
className="absolute left-0 top-0 -z-[1] h-full w-full dark:bg-dark" <div className='absolute left-0 top-0 -z-[1] h-full w-full dark:bg-dark'></div>
></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 <div className='container px-4'>
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 className='-mx-4 flex flex-wrap items-center'>
></div> {/* 联系方式左侧文字 */}
<div className="container px-4"> <div className='w-full px-4 lg:w-7/12 xl:w-8/12'>
<div className="-mx-4 flex flex-wrap items-center"> <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'>
<div className="w-full px-4 lg:w-7/12 xl:w-8/12"> {siteConfig('STARTER_CONTACT_TITLE', null, CONFIG)}
<div className="ud-contact-content-wrapper"> </span>
<div className="ud-contact-title mb-12 lg:mb-[150px]"> <h2 className='max-w-[260px] text-[35px] font-semibold leading-[1.14] text-dark dark:text-white'>
<span {siteConfig('STARTER_CONTACT_TEXT', null, CONFIG)}
className="mb-6 block text-base font-medium text-dark dark:text-white" </h2>
>
{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>
</div> </div>
<div className="mb-8 flex w-[330px] max-w-full"> <div className='mb-12 flex flex-wrap justify-between lg:mb-0'>
<div className="mr-6 text-[32px] text-primary"> <div className='mb-8 flex w-[330px] max-w-full'>
<SVGEmail/> <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>
<div> <div className='mb-8 flex w-[330px] max-w-full'>
<h5 <div className='mr-6 text-[32px] text-primary'>
className="mb-[18px] text-lg font-semibold text-dark dark:text-white" <SVGEmail />
> </div>
{siteConfig('STARTER_CONTACT_EMAIL_TITLE', null, CONFIG)} <div>
</h5> <h5 className='mb-[18px] text-lg font-semibold text-dark dark:text-white'>
<p className="text-base text-body-color dark:text-dark-6"> {siteConfig(
{siteConfig('STARTER_CONTACT_EMAIL_TEXT', null, CONFIG)} 'STARTER_CONTACT_EMAIL_TITLE',
</p> 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>
</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>
{/* 联系方式右侧留言 */}
<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>
</div> </section>
</section> {/* <!-- ====== Contact End ====== --> */}
{/* <!-- ====== Contact End ====== --> */} </>
)
</>
} }

View File

@@ -1,164 +1,187 @@
import { siteConfig } from '@/lib/config' import { siteConfig } from '@/lib/config'
import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils'
import SocialButton from '@/themes/fukasawa/components/SocialButton'
import CONFIG from '../config' import CONFIG from '../config'
import { Logo } from './Logo' import { Logo } from './Logo'
import SocialButton from '@/themes/fukasawa/components/SocialButton'
import { SVGFooterCircleBG } from './svg/SVGFooterCircleBG' import { SVGFooterCircleBG } from './svg/SVGFooterCircleBG'
import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils'
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
export const Footer = (props) => { export const Footer = props => {
const latestPosts = props?.latestPosts ? props?.latestPosts.slice(0, 2) : [] const latestPosts = props?.latestPosts ? props?.latestPosts.slice(0, 2) : []
const STARTER_FOOTER_LINK_GROUP = siteConfig(
return <> 'STARTER_FOOTER_LINK_GROUP',
{/* <!-- ====== Footer Section Start --> */} [],
<footer CONFIG
className="wow fadeInUp relative z-10 bg-[#090E34] pt-20 lg:pt-[100px]" )
data-wow-delay=".15s" return (
> <>
<div className="container"> {/* <!-- ====== Footer Section Start --> */}
<div className="-mx-4 flex flex-wrap"> <footer
<div className="w-full px-4 sm:w-1/2 md:w-1/2 lg:w-4/12 xl:w-3/12"> className='wow fadeInUp relative z-10 bg-[#090E34] pt-20 lg:pt-[100px]'
<div className="mb-10 w-full"> data-wow-delay='.15s'>
<a <div className='container'>
className="-mx-4 mb-6 inline-block max-w-[160px]" <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'>
<Logo white={true}/> <div className='mb-10 w-full'>
</a> <a className='-mx-4 mb-6 inline-block max-w-[160px]'>
<p className="mb-8 max-w-[270px] text-base text-gray-7"> <Logo white={true} />
{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>
</a> </a>
})} <p className='mb-8 max-w-[270px] text-base text-gray-7'>
{siteConfig('STARTER_FOOTER_SLOGAN', null, CONFIG)}
</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> </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> </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]"> <div className='mt-12 border-t border-[#8890A4] border-opacity-40 py-8 lg:mt-[60px]'>
<img src="/images/starter/footer/shape-3.svg" alt="" /> <div className='container'>
</span> <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]"> {/* Footer 背景 */}
<SVGFooterCircleBG/> <div>
</span> <span className='absolute left-0 top-0 z-[-1]'>
</div> <img src='/images/starter/footer/shape-1.svg' alt='' />
</footer> </span>
{/* <!-- ====== Footer Section End --> */}
<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 */ /* eslint-disable @next/next/no-html-link-for-pages */
import { siteConfig } from '@/lib/config'; import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'; import { useGlobal } from '@/lib/global'
import throttle from 'lodash.throttle'; import throttle from 'lodash.throttle'
import { useRouter } from 'next/router'; import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react'
import CONFIG from '../config'; import CONFIG from '../config'
/** /**
* 站点图标 * 站点图标
@@ -21,10 +22,9 @@ export const Logo = ({ white }) => {
// 滚动监听 // 滚动监听
const throttleMs = 200 const throttleMs = 200
const navBarScrollListener = throttle(() => { const navBarScrollListener = throttle(() => {
const scrollY = window.scrollY; const scrollY = window.scrollY
// 何时显示浅色或白底的logo // 何时显示浅色或白底的logo
const homePageNavBar = router.route === '/' && scrollY < 10 // 在首页并且视窗在页面顶部 const homePageNavBar = router.route === '/' && scrollY < 10 // 在首页并且视窗在页面顶部
console.log('白色', homePageNavBar, router.route, scrollY < 10)
if (white || isDarkMode || homePageNavBar) { if (white || isDarkMode || homePageNavBar) {
setLogo(siteConfig('STARTER_LOGO_WHITE', null, CONFIG)) setLogo(siteConfig('STARTER_LOGO_WHITE', null, CONFIG))
setLogoTextColor('text-white') setLogoTextColor('text-white')
@@ -41,21 +41,29 @@ export const Logo = ({ white }) => {
} }
}, [isDarkMode, router]) }, [isDarkMode, router])
return <div className="w-60 max-w-full px-4"> return (
<div className="navbar-logo flex items-center w-full py-5 cursor-pointer"> <div className='w-60 max-w-full px-4'>
{/* eslint-disable-next-line @next/next/no-img-element */} <div className='navbar-logo flex items-center w-full py-5 cursor-pointer'>
{logo && <img {/* eslint-disable-next-line @next/next/no-img-element */}
onClick={() => { {logo && (
router.push('/') <img
}} onClick={() => {
src={logo} router.push('/')
alt="logo" }}
className="header-logo w-full" src={logo}
/>} alt='logo'
{/* logo文字 */} className='header-logo w-full'
<span onClick={() => { router.push('/') }} className={`${logoTextColor} dark:text-white py-1.5 header-logo-text whitespace-nowrap text-2xl font-semibold`}> />
{siteConfig('TITLE')} )}
</span> {/* logo文字 */}
</div> <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> </div>
)
} }

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