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

This commit is contained in:
tangly1024.com
2024-04-02 14:51:01 +08:00
237 changed files with 9661 additions and 1321 deletions

View File

@@ -1,5 +1,5 @@
# 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables
NEXT_PUBLIC_VERSION=4.3.1
NEXT_PUBLIC_VERSION=4.4.2
# 可在此添加环境变量,去掉最左边的(# )注释即可
@@ -166,6 +166,7 @@ NEXT_PUBLIC_VERSION=4.3.1
# NEXT_PUBLIC_NOTION_PROPERTY_TAGS=
# NEXT_PUBLIC_NOTION_PROPERTY_ICON=
# NEXT_PUBLIC_ENABLE_RSS=
# NEXT_PUBLIC_IS_TAG_COLOR_DISTINGUISHED=
# MAILCHIMP_LIST_ID=
# MAILCHIMP_API_KEY=
# NEXT_PUBLIC_DEBUG=

View File

@@ -4,11 +4,7 @@ module.exports = {
es2021: true,
node: true
},
extends: [
'plugin:react/recommended',
'plugin:@next/next/recommended',
'standard'
],
extends: ['plugin:react/recommended', 'plugin:@next/next/recommended', 'standard', 'prettier'],
parserOptions: {
ecmaFeatures: {
jsx: true
@@ -16,10 +12,7 @@ module.exports = {
ecmaVersion: 12,
sourceType: 'module'
},
plugins: [
'react',
'react-hooks'
],
plugins: ['react', 'react-hooks', 'prettier'],
settings: {
react: {
version: 'detect'

View File

@@ -13,55 +13,72 @@ name: "CodeQL"
on:
push:
branches: [ main ]
branches: [ "main" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
branches: [ "main" ]
schedule:
- cron: '21 5 * * 3'
- cron: '22 13 * * 1'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners
# Consider using larger runners for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
permissions:
# required for all workflows
security-events: write
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
language: [ 'javascript-typescript' ]
# CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ]
# Use only 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
#- run: |
# make bootstrap
# make release
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

39
.github/workflows/sync.yaml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: Upstream Sync
permissions:
contents: write
on:
schedule:
- cron: "0 0 * * *" # every day
workflow_dispatch:
jobs:
sync_latest_from_upstream:
name: Sync latest commits from upstream repo
runs-on: ubuntu-latest
if: ${{ github.event.repository.fork }}
steps:
# Step 1: run a standard checkout action
- name: Checkout target repo
uses: actions/checkout@v3
# Step 2: run the sync action
- name: Sync upstream changes
id: sync
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
with:
upstream_sync_repo: tangly1024/NotionNext
upstream_sync_branch: main
target_sync_branch: main
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
# Set test_mode true to run tests instead of the true action!!
test_mode: false
- name: Sync check
if: failure()
run: |
echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次。
exit 1

View File

@@ -2,5 +2,9 @@
"singleQuote": true,
"semi": false,
"trailingComma": "none",
"arrowParens": "avoid"
"arrowParens": "avoid",
"printWidth": 80,
"bracketSpacing": true,
"jsxSingleQuote": true,
"jsxBracketSameLine": true
}

View File

@@ -1,8 +1,7 @@
// 注: process.env.XX是Vercel的环境变量配置方式见https://docs.tangly1024.com/article/how-to-config-notion-next#c4768010ae7d44609b744e79e2f9959a
const BLOG = {
// Important page_idDuplicate Template from https://www.notion.so/tanghh/02ab3b8678004aa69e9e415905ef32a5
NOTION_PAGE_ID:
process.env.NOTION_PAGE_ID || '02ab3b8678004aa69e9e415905ef32a5',
NOTION_PAGE_ID: process.env.NOTION_PAGE_ID || '02ab3b8678004aa69e9e415905ef32a5',
PSEUDO_STATIC: process.env.NEXT_PUBLIC_PSEUDO_STATIC || false, // 伪静态路径开启后所有文章URL都以 .html 结尾。
NEXT_REVALIDATE_SECOND: process.env.NEXT_PUBLIC_REVALIDATE_SECOND || 5, // 更新内容缓存间隔 单位(秒)即每个页面有5秒的纯静态期、此期间无论多少次访问都不会抓取notion数据调大该值有助于节省Vercel资源、同时提升访问速率但也会使文章更新有延迟。
THEME: process.env.NEXT_PUBLIC_THEME || 'simple', // 当前主题在themes文件夹下可找到所有支持的主题主题名称就是文件夹名例如 example,fukasawa,gitbook,heo,hexo,landing,matery,medium,next,nobelium,plog,simple
@@ -12,8 +11,12 @@ const BLOG = {
APPEARANCE: process.env.NEXT_PUBLIC_APPEARANCE || 'light', // ['light', 'dark', 'auto'], // light 日间模式 dark夜间模式 auto根据时间和主题自动夜间模式
APPEARANCE_DARK_TIME: process.env.NEXT_PUBLIC_APPEARANCE_DARK_TIME || [18, 6], // 夜间模式起至时间false时关闭根据时间自动切换夜间模式
IS_TAG_COLOR_DISTINGUISHED: process.env.NEXT_PUBLIC_IS_TAG_COLOR_DISTINGUISHED === 'true' || true, // 对于名称相同的tag是否区分tag的颜色
// 3.14.1版本后,欢迎语在此配置,英文逗号隔开 , 即可支持多个欢迎语打字效果。
GREETING_WORDS: process.env.NEXT_PUBLIC_GREETING_WORDS || 'Hi我是一个程序员, Hi我是一个打工人,Hi我是一个干饭人,欢迎来到我的博客🎉',
GREETING_WORDS:
process.env.NEXT_PUBLIC_GREETING_WORDS ||
'Hi我是一个程序员, Hi我是一个打工人,Hi我是一个干饭人,欢迎来到我的博客🎉',
CUSTOM_MENU: process.env.NEXT_PUBLIC_CUSTOM_MENU || false, // 支持Menu 类型从3.12.0版本起各主题将逐步支持灵活的二级菜单配置替代了原来的Page类型此配置是试验功能、默认关闭。
@@ -88,7 +91,9 @@ const BLOG = {
'"Segoe UI Symbol"',
'"Apple Color Emoji"'
],
FONT_AWESOME: process.env.NEXT_PUBLIC_FONT_AWESOME_PATH || 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css', // font-awesome 字体图标地址; 可选 /css/all.min.css https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/font-awesome/6.0.0/css/all.min.css
FONT_AWESOME:
process.env.NEXT_PUBLIC_FONT_AWESOME_PATH ||
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css', // font-awesome 字体图标地址; 可选 /css/all.min.css https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/font-awesome/6.0.0/css/all.min.css
// END ************网站字体*****************
@@ -116,8 +121,17 @@ const BLOG = {
},
CAN_COPY: process.env.NEXT_PUBLIC_CAN_COPY || true, // 是否允许复制页面内容 默认允许如果设置为false、则全栈禁止复制内容。
// 自定义右键菜单
CUSTOM_RIGHT_CLICK_CONTEXT_MENU: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU || true, // 自定义右键菜单,覆盖系统菜单
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH || true, // 右键菜单是否允许切换主题
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH:
process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH || true, // 是否显示切换主题
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_DARK_MODE: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_DARK_MODE || true, // 是否显示深色模式
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_SHARE_LINK:
process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_SHARE_LINK || true, // 是否显示分享链接
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_RANDOM_POST:
process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_RANDOM_POST || true, // 是否显示随机博客
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_CATEGORY: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_CATEGORY || true, // 是否显示分类
CUSTOM_RIGHT_CLICK_CONTEXT_MENU_TAG: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_TAG || true, // 是否显示标签
// 自定义外部脚本,外部样式
CUSTOM_EXTERNAL_JS: [''], // e.g. ['http://xx.com/script.js','http://xx.com/script.js']
@@ -140,10 +154,16 @@ const BLOG = {
PRISM_JS_AUTO_LOADER: 'https://npm.elemecdn.com/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js',
// 代码主题 @see https://github.com/PrismJS/prism-themes
PRISM_THEME_PREFIX_PATH: process.env.NEXT_PUBLIC_PRISM_THEME_PREFIX_PATH || 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-okaidia.css', // 代码块默认主题
PRISM_THEME_PREFIX_PATH:
process.env.NEXT_PUBLIC_PRISM_THEME_PREFIX_PATH ||
'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-okaidia.css', // 代码块默认主题
PRISM_THEME_SWITCH: process.env.NEXT_PUBLIC_PRISM_THEME_SWITCH || true, // 是否开启浅色/深色模式代码主题切换; 开启后将显示以下两个主题
PRISM_THEME_LIGHT_PATH: process.env.NEXT_PUBLIC_PRISM_THEME_LIGHT_PATH || 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-solarizedlight.css', // 浅色模式主题
PRISM_THEME_DARK_PATH: process.env.NEXT_PUBLIC_PRISM_THEME_DARK_PATH || 'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-okaidia.min.css', // 深色模式主题
PRISM_THEME_LIGHT_PATH:
process.env.NEXT_PUBLIC_PRISM_THEME_LIGHT_PATH ||
'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-solarizedlight.css', // 浅色模式主题
PRISM_THEME_DARK_PATH:
process.env.NEXT_PUBLIC_PRISM_THEME_DARK_PATH ||
'https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-okaidia.min.css', // 深色模式主题
CODE_MAC_BAR: process.env.NEXT_PUBLIC_CODE_MAC_BAR || true, // 代码左上角显示mac的红黄绿图标
CODE_LINE_NUMBERS: process.env.NEXT_PUBLIC_CODE_LINE_NUMBERS || false, // 是否显示行号
@@ -153,16 +173,20 @@ const BLOG = {
// END********代码相关********
// Mermaid 图表CDN
MERMAID_CDN: process.env.NEXT_PUBLIC_MERMAID_CDN || 'https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.2.4/mermaid.min.js', // CDN
MERMAID_CDN:
process.env.NEXT_PUBLIC_MERMAID_CDN || 'https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.2.4/mermaid.min.js', // CDN
// QRCodeCDN
QR_CODE_CDN: process.env.NEXT_PUBLIC_QR_CODE_CDN || 'https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js',
QR_CODE_CDN:
process.env.NEXT_PUBLIC_QR_CODE_CDN || 'https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js',
BACKGROUND_LIGHT: '#eeeeee', // use hex value, don't forget '#' e.g #fffefc
BACKGROUND_DARK: '#000000', // use hex value, don't forget '#'
SUB_PATH: '', // leave this empty unless you want to deploy in a folder
POST_SHARE_BAR_ENABLE: process.env.NEXT_PUBLIC_POST_SHARE_BAR || 'true', // 文章分享功能 ,将在底部显示一个分享条
POSTS_SHARE_SERVICES: process.env.NEXT_PUBLIC_POST_SHARE_SERVICES || 'link,wechat,qq,weibo,email,facebook,twitter,telegram,messenger,line,reddit,whatsapp,linkedin', // 分享的服務,按顺序显示,逗号隔开
POSTS_SHARE_SERVICES:
process.env.NEXT_PUBLIC_POST_SHARE_SERVICES ||
'link,wechat,qq,weibo,email,facebook,twitter,telegram,messenger,line,reddit,whatsapp,linkedin', // 分享的服務,按顺序显示,逗号隔开
// 所有支持的分享服务link(复制链接),wechat(微信),qq,weibo(微博),email(邮件),facebook,twitter,telegram,messenger,line,reddit,whatsapp,linkedin,vkshare,okshare,tumblr,livejournal,mailru,viber,workplace,pocket,instapaper,hatena
POST_URL_PREFIX: process.env.NEXT_PUBLIC_POST_URL_PREFIX || 'article',
@@ -173,9 +197,9 @@ const BLOG = {
POST_LIST_STYLE: process.env.NEXT_PUBLIC_POST_LIST_STYLE || 'page', // ['page','scroll] 文章列表样式:页码分页、单页滚动加载
POST_LIST_PREVIEW: process.env.NEXT_PUBLIC_POST_PREVIEW || 'false', // 是否在列表加载文章预览
POST_PREVIEW_LINES: 12, // 预览博客行数
POST_RECOMMEND_COUNT: 6, // 推荐文章数量
POSTS_PER_PAGE: 12, // post counts per page
POST_PREVIEW_LINES: process.env.NEXT_PUBLIC_POST_POST_PREVIEW_LINES || 12, // 预览博客行数
POST_RECOMMEND_COUNT: process.env.NEXT_PUBLIC_POST_RECOMMEND_COUNT || 6, // 推荐文章数量
POSTS_PER_PAGE: process.env.NEXT_PUBLIC_POST_PER_PAGE || 12, // post counts per page
POSTS_SORT_BY: process.env.NEXT_PUBLIC_POST_SORT_BY || 'notion', // 排序方式 'date'按时间,'notion'由notion控制
POST_WAITING_TIME_FOR_404: process.env.NEXT_PUBLIC_POST_WAITING_TIME_FOR_404 || '8', // 文章加载超时时间单位秒超时后跳转到404页面
@@ -195,12 +219,7 @@ const BLOG = {
// 鼠标点击烟花特效
FIREWORKS: process.env.NEXT_PUBLIC_FIREWORKS || false, // 开关
// 烟花色彩,感谢 https://github.com/Vixcity 提交的色彩
FIREWORKS_COLOR: [
'255, 20, 97',
'24, 255, 146',
'90, 135, 255',
'251, 243, 140'
],
FIREWORKS_COLOR: ['255, 20, 97', '24, 255, 146', '90, 135, 255', '251, 243, 140'],
// 樱花飘落特效
SAKURA: process.env.NEXT_PUBLIC_SAKURA || false, // 开关
@@ -215,8 +234,11 @@ const BLOG = {
// ********挂件组件相关********
// AI 文章摘要生成 @see https://docs_s.tianli0.top/
TianliGPT_CSS: process.env.NEXT_PUBLIC_TIANLI_GPT_CSS || 'https://cdn1.tianli0.top/gh/zhheo/Post-Abstract-AI@0.15.2/tianli_gpt.css',
TianliGPT_JS: process.env.NEXT_PUBLIC_TIANLI_GPT_JS || 'https://cdn1.tianli0.top/gh/zhheo/Post-Abstract-AI@0.15.2/tianli_gpt.js',
TianliGPT_CSS:
process.env.NEXT_PUBLIC_TIANLI_GPT_CSS ||
'https://cdn1.tianli0.top/gh/zhheo/Post-Abstract-AI@0.15.2/tianli_gpt.css',
TianliGPT_JS:
process.env.NEXT_PUBLIC_TIANLI_GPT_JS || 'https://cdn1.tianli0.top/gh/zhheo/Post-Abstract-AI@0.15.2/tianli_gpt.js',
TianliGPT_KEY: process.env.NEXT_PUBLIC_TIANLI_GPT_KEY || '',
// Chatbase 是否显示chatbase机器人 https://www.chatbase.co/
@@ -231,19 +253,18 @@ const BLOG = {
// 悬浮挂件
WIDGET_PET: process.env.NEXT_PUBLIC_WIDGET_PET || true, // 是否显示宠物挂件
WIDGET_PET_LINK:
process.env.NEXT_PUBLIC_WIDGET_PET_LINK ||
'https://cdn.jsdelivr.net/npm/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json', // 挂件模型地址 @see https://github.com/xiazeyu/live2d-widget-models
process.env.NEXT_PUBLIC_WIDGET_PET_LINK ||
'https://cdn.jsdelivr.net/npm/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json', // 挂件模型地址 @see https://github.com/xiazeyu/live2d-widget-models
WIDGET_PET_SWITCH_THEME: process.env.NEXT_PUBLIC_WIDGET_PET_SWITCH_THEME || true, // 点击宠物挂件切换博客主题
// 音乐播放插件
MUSIC_PLAYER: process.env.NEXT_PUBLIC_MUSIC_PLAYER || false, // 是否使用音乐播放插件
MUSIC_PLAYER_VISIBLE: process.env.NEXT_PUBLIC_MUSIC_PLAYER_VISIBLE || true, // 是否在左下角显示播放和切换,如果使用播放器,打开自动播放再隐藏,就会以类似背景音乐的方式播放,无法取消和暂停
MUSIC_PLAYER_AUTO_PLAY:
process.env.NEXT_PUBLIC_MUSIC_PLAYER_AUTO_PLAY || true, // 是否自动播放,不过自动播放时常不生效(移动设备不支持自动播放)
MUSIC_PLAYER_AUTO_PLAY: process.env.NEXT_PUBLIC_MUSIC_PLAYER_AUTO_PLAY || true, // 是否自动播放,不过自动播放时常不生效(移动设备不支持自动播放)
MUSIC_PLAYER_LRC_TYPE: process.env.NEXT_PUBLIC_MUSIC_PLAYER_LRC_TYPE || '0', // 歌词显示类型,可选值: 3 | 1 | 00禁用 lrc 歌词1lrc 格式的字符串3lrc 文件 url前提是有配置歌词路径对 meting 无效)
MUSIC_PLAYER_CDN_URL:
process.env.NEXT_PUBLIC_MUSIC_PLAYER_CDN_URL ||
'https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/aplayer/1.10.1/APlayer.min.js',
process.env.NEXT_PUBLIC_MUSIC_PLAYER_CDN_URL ||
'https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/aplayer/1.10.1/APlayer.min.js',
MUSIC_PLAYER_ORDER: process.env.NEXT_PUBLIC_MUSIC_PLAYER_ORDER || 'list', // 默认播放方式,顺序 list随机 random
MUSIC_PLAYER_AUDIO_LIST: [
// 示例音乐列表。除了以下配置外,还可配置歌词,具体配置项看此文档 https://aplayer.js.org/#/zh-Hans/
@@ -251,24 +272,19 @@ const BLOG = {
name: '风を共に舞う気持ち',
artist: 'Falcom Sound Team jdk',
url: 'https://music.163.com/song/media/outer/url?id=731419.mp3',
cover:
'https://p2.music.126.net/kn6ugISTonvqJh3LHLaPtQ==/599233837187278.jpg'
cover: 'https://p2.music.126.net/kn6ugISTonvqJh3LHLaPtQ==/599233837187278.jpg'
},
{
name: '王都グランセル',
artist: 'Falcom Sound Team jdk',
url: 'https://music.163.com/song/media/outer/url?id=731355.mp3',
cover:
'https://p1.music.126.net/kn6ugISTonvqJh3LHLaPtQ==/599233837187278.jpg'
cover: 'https://p1.music.126.net/kn6ugISTonvqJh3LHLaPtQ==/599233837187278.jpg'
}
],
MUSIC_PLAYER_METING: process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING || false, // 是否要开启 MetingJS从平台获取歌单。会覆盖自定义的 MUSIC_PLAYER_AUDIO_LIST更多配置信息https://github.com/metowolf/MetingJS
MUSIC_PLAYER_METING_SERVER:
process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_SERVER || 'netease', // 音乐平台,[netease, tencent, kugou, xiami, baidu]
MUSIC_PLAYER_METING_ID:
process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_ID || '60198', // 对应歌单的 id
MUSIC_PLAYER_METING_LRC_TYPE:
process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_LRC_TYPE || '1', // 可选值: 3 | 1 | 00禁用 lrc 歌词1lrc 格式的字符串3lrc 文件 url
MUSIC_PLAYER_METING_SERVER: process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_SERVER || 'netease', // 音乐平台,[netease, tencent, kugou, xiami, baidu]
MUSIC_PLAYER_METING_ID: process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_ID || '60198', // 对应歌单的 id
MUSIC_PLAYER_METING_LRC_TYPE: process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_LRC_TYPE || '1', // 可选值: 3 | 1 | 00禁用 lrc 歌词1lrc 格式的字符串3lrc 文件 url
// ********挂件组件相关********
// ----> 评论互动 可同时开启多个支持 WALINE VALINE GISCUS CUSDIS UTTERRANCES GITALK
@@ -277,55 +293,47 @@ const BLOG = {
// artalk 评论插件
COMMENT_ARTALK_SERVER: process.env.NEXT_PUBLIC_COMMENT_ARTALK_SERVER || '', // ArtalkServert后端地址 https://artalk.js.org/guide/deploy.html
COMMENT_ARTALK_JS: process.env.NEXT_PUBLIC_COMMENT_ARTALK_JS || 'https://cdnjs.cloudflare.com/ajax/libs/artalk/2.5.5/Artalk.js', // ArtalkServert js cdn
COMMENT_ARTALK_CSS: process.env.NEXT_PUBLIC_COMMENT_ARTALK_CSS || 'https://cdnjs.cloudflare.com/ajax/libs/artalk/2.5.5/Artalk.css', // ArtalkServert css cdn
COMMENT_ARTALK_JS:
process.env.NEXT_PUBLIC_COMMENT_ARTALK_JS || 'https://cdnjs.cloudflare.com/ajax/libs/artalk/2.5.5/Artalk.js', // ArtalkServert js cdn
COMMENT_ARTALK_CSS:
process.env.NEXT_PUBLIC_COMMENT_ARTALK_CSS || 'https://cdnjs.cloudflare.com/ajax/libs/artalk/2.5.5/Artalk.css', // ArtalkServert css cdn
// twikoo
COMMENT_TWIKOO_ENV_ID: process.env.NEXT_PUBLIC_COMMENT_ENV_ID || '', // TWIKOO后端地址 腾讯云环境填envIdVercel环境填域名教程https://tangly1024.com/article/notionnext-twikoo
COMMENT_TWIKOO_COUNT_ENABLE: process.env.NEXT_PUBLIC_COMMENT_TWIKOO_COUNT_ENABLE || false, // 博客列表是否显示评论数
COMMENT_TWIKOO_CDN_URL: process.env.NEXT_PUBLIC_COMMENT_TWIKOO_CDN_URL || 'https://cdn.staticfile.org/twikoo/1.6.17/twikoo.min.js', // twikoo客户端cdn
COMMENT_TWIKOO_CDN_URL:
process.env.NEXT_PUBLIC_COMMENT_TWIKOO_CDN_URL || 'https://cdn.staticfile.org/twikoo/1.6.17/twikoo.min.js', // twikoo客户端cdn
// utterance
COMMENT_UTTERRANCES_REPO:
process.env.NEXT_PUBLIC_COMMENT_UTTERRANCES_REPO || '', // 你的代码仓库名, 例如我是 'tangly1024/NotionNext' 更多文档参考 https://utteranc.es/
COMMENT_UTTERRANCES_REPO: process.env.NEXT_PUBLIC_COMMENT_UTTERRANCES_REPO || '', // 你的代码仓库名, 例如我是 'tangly1024/NotionNext' 更多文档参考 https://utteranc.es/
// giscus @see https://giscus.app/
COMMENT_GISCUS_REPO: process.env.NEXT_PUBLIC_COMMENT_GISCUS_REPO || '', // 你的Github仓库名 e.g 'tangly1024/NotionNext'
COMMENT_GISCUS_REPO_ID: process.env.NEXT_PUBLIC_COMMENT_GISCUS_REPO_ID || '', // 你的Github Repo ID e.g ( 設定完 giscus 即可看到 )
COMMENT_GISCUS_CATEGORY_ID:
process.env.NEXT_PUBLIC_COMMENT_GISCUS_CATEGORY_ID || '', // 你的Github Discussions 內的 Category ID ( 設定完 giscus 即可看到 )
COMMENT_GISCUS_MAPPING:
process.env.NEXT_PUBLIC_COMMENT_GISCUS_MAPPING || 'pathname', // 你的Github Discussions 使用哪種方式來標定文章, 預設 'pathname'
COMMENT_GISCUS_REACTIONS_ENABLED:
process.env.NEXT_PUBLIC_COMMENT_GISCUS_REACTIONS_ENABLED || '1', // 你的 Giscus 是否開啟文章表情符號 '1' 開啟 "0" 關閉 預設開啟
COMMENT_GISCUS_EMIT_METADATA:
process.env.NEXT_PUBLIC_COMMENT_GISCUS_EMIT_METADATA || '0', // 你的 Giscus 是否提取 Metadata '1' 開啟 '0' 關閉 預設關閉
COMMENT_GISCUS_INPUT_POSITION:
process.env.NEXT_PUBLIC_COMMENT_GISCUS_INPUT_POSITION || 'bottom', // 你的 Giscus 發表留言位置 'bottom' 尾部 'top' 頂部, 預設 'bottom'
COMMENT_GISCUS_CATEGORY_ID: process.env.NEXT_PUBLIC_COMMENT_GISCUS_CATEGORY_ID || '', // 你的Github Discussions 內的 Category ID ( 設定完 giscus 即可看到 )
COMMENT_GISCUS_MAPPING: process.env.NEXT_PUBLIC_COMMENT_GISCUS_MAPPING || 'pathname', // 你的Github Discussions 使用哪種方式來標定文章, 預設 'pathname'
COMMENT_GISCUS_REACTIONS_ENABLED: process.env.NEXT_PUBLIC_COMMENT_GISCUS_REACTIONS_ENABLED || '1', // 你的 Giscus 是否開啟文章表情符號 '1' 開啟 "0" 關閉 預設開啟
COMMENT_GISCUS_EMIT_METADATA: process.env.NEXT_PUBLIC_COMMENT_GISCUS_EMIT_METADATA || '0', // 你的 Giscus 是否提取 Metadata '1' 開啟 '0' 關閉 預設關閉
COMMENT_GISCUS_INPUT_POSITION: process.env.NEXT_PUBLIC_COMMENT_GISCUS_INPUT_POSITION || 'bottom', // 你的 Giscus 發表留言位置 'bottom' 尾部 'top' 頂部, 預設 'bottom'
COMMENT_GISCUS_LANG: process.env.NEXT_PUBLIC_COMMENT_GISCUS_LANG || 'zh-CN', // 你的 Giscus 語言 e.g 'en', 'zh-TW', 'zh-CN', 預設 'en'
COMMENT_GISCUS_LOADING:
process.env.NEXT_PUBLIC_COMMENT_GISCUS_LOADING || 'lazy', // 你的 Giscus 載入是否漸進式載入, 預設 'lazy'
COMMENT_GISCUS_CROSSORIGIN:
process.env.NEXT_PUBLIC_COMMENT_GISCUS_CROSSORIGIN || 'anonymous', // 你的 Giscus 可以跨網域, 預設 'anonymous'
COMMENT_GISCUS_LOADING: process.env.NEXT_PUBLIC_COMMENT_GISCUS_LOADING || 'lazy', // 你的 Giscus 載入是否漸進式載入, 預設 'lazy'
COMMENT_GISCUS_CROSSORIGIN: process.env.NEXT_PUBLIC_COMMENT_GISCUS_CROSSORIGIN || 'anonymous', // 你的 Giscus 可以跨網域, 預設 'anonymous'
COMMENT_CUSDIS_APP_ID: process.env.NEXT_PUBLIC_COMMENT_CUSDIS_APP_ID || '', // data-app-id 36位 see https://cusdis.com/
COMMENT_CUSDIS_HOST:
process.env.NEXT_PUBLIC_COMMENT_CUSDIS_HOST || 'https://cusdis.com', // data-host, change this if you're using self-hosted version
COMMENT_CUSDIS_SCRIPT_SRC:
process.env.NEXT_PUBLIC_COMMENT_CUSDIS_SCRIPT_SRC ||
'/js/cusdis.es.js', // change this if you're using self-hosted version
COMMENT_CUSDIS_HOST: process.env.NEXT_PUBLIC_COMMENT_CUSDIS_HOST || 'https://cusdis.com', // data-host, change this if you're using self-hosted version
COMMENT_CUSDIS_SCRIPT_SRC: process.env.NEXT_PUBLIC_COMMENT_CUSDIS_SCRIPT_SRC || '/js/cusdis.es.js', // change this if you're using self-hosted version
// gitalk评论插件 更多参考 https://gitalk.github.io/
COMMENT_GITALK_REPO: process.env.NEXT_PUBLIC_COMMENT_GITALK_REPO || '', // 你的Github仓库名例如 'NotionNext'
COMMENT_GITALK_OWNER: process.env.NEXT_PUBLIC_COMMENT_GITALK_OWNER || '', // 你的用户名 e.g tangly1024
COMMENT_GITALK_ADMIN: process.env.NEXT_PUBLIC_COMMENT_GITALK_ADMIN || '', // 管理员用户名、一般是自己 e.g 'tangly1024'
COMMENT_GITALK_CLIENT_ID:
process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_ID || '', // e.g 20位ID 在gitalk后台获取
COMMENT_GITALK_CLIENT_SECRET:
process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_SECRET || '', // e.g 40位ID 在gitalk后台获取
COMMENT_GITALK_CLIENT_ID: process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_ID || '', // e.g 20位ID 在gitalk后台获取
COMMENT_GITALK_CLIENT_SECRET: process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_SECRET || '', // e.g 40位ID 在gitalk后台获取
COMMENT_GITALK_DISTRACTION_FREE_MODE: false, // 类似facebook的无干扰模式
COMMENT_GITALK_JS_CDN_URL: process.env.NEXT_PUBLIC_COMMENT_GITALK_JS_CDN_URL || 'https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.min.js', // gitalk客户端 js cdn
COMMENT_GITALK_CSS_CDN_URL: process.env.NEXT_PUBLIC_COMMENT_GITALK_CSS_CDN_URL || 'https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css', // gitalk客户端 css cdn
COMMENT_GITALK_JS_CDN_URL:
process.env.NEXT_PUBLIC_COMMENT_GITALK_JS_CDN_URL || 'https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.min.js', // gitalk客户端 js cdn
COMMENT_GITALK_CSS_CDN_URL:
process.env.NEXT_PUBLIC_COMMENT_GITALK_CSS_CDN_URL || 'https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css', // gitalk客户端 css cdn
COMMENT_GITTER_ROOM: process.env.NEXT_PUBLIC_COMMENT_GITTER_ROOM || '', // gitter聊天室 see https://gitter.im/ 不需要则留空
COMMENT_DAO_VOICE_ID: process.env.NEXT_PUBLIC_COMMENT_DAO_VOICE_ID || '', // DaoVoice http://dashboard.daovoice.io/get-started
@@ -335,8 +343,7 @@ const BLOG = {
COMMENT_VALINE_APP_ID: process.env.NEXT_PUBLIC_VALINE_ID || '', // Valine @see https://valine.js.org/quickstart.html 或 https://github.com/stonehank/react-valine#%E8%8E%B7%E5%8F%96app-id-%E5%92%8C-app-key
COMMENT_VALINE_APP_KEY: process.env.NEXT_PUBLIC_VALINE_KEY || '',
COMMENT_VALINE_SERVER_URLS: process.env.NEXT_PUBLIC_VALINE_SERVER_URLS || '', // 该配置适用于国内自定义域名用户, 海外版本会自动检测(无需手动填写) @see https://valine.js.org/configuration.html#serverURLs
COMMENT_VALINE_PLACEHOLDER:
process.env.NEXT_PUBLIC_VALINE_PLACEHOLDER || '抢个沙发吧~', // 可以搭配后台管理评论 https://github.com/DesertsP/Valine-Admin 便于查看评论,以及邮件通知,垃圾评论过滤等功能
COMMENT_VALINE_PLACEHOLDER: process.env.NEXT_PUBLIC_VALINE_PLACEHOLDER || '抢个沙发吧~', // 可以搭配后台管理评论 https://github.com/DesertsP/Valine-Admin 便于查看评论,以及邮件通知,垃圾评论过滤等功能
COMMENT_WALINE_SERVER_URL: process.env.NEXT_PUBLIC_WALINE_SERVER_URL || '', // 请配置完整的Waline评论地址 例如 hhttps://preview-waline.tangly1024.com @see https://waline.js.org/guide/get-started.html
COMMENT_WALINE_RECENT: process.env.NEXT_PUBLIC_WALINE_RECENT || false, // 最新评论
@@ -375,11 +382,9 @@ const BLOG = {
ANALYTICS_ACKEE_DATA_SERVER: process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_DATA_SERVER || '', // e.g https://ackee.tangly1024.com , don't end with a slash
ANALYTICS_ACKEE_DOMAIN_ID: process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_DOMAIN_ID || '', // e.g '82e51db6-dec2-423a-b7c9-b4ff7ebb3302'
SEO_GOOGLE_SITE_VERIFICATION:
process.env.NEXT_PUBLIC_SEO_GOOGLE_SITE_VERIFICATION || '', // Remove the value or replace it with your own google site verification code
SEO_GOOGLE_SITE_VERIFICATION: process.env.NEXT_PUBLIC_SEO_GOOGLE_SITE_VERIFICATION || '', // Remove the value or replace it with your own google site verification code
SEO_BAIDU_SITE_VERIFICATION:
process.env.NEXT_PUBLIC_SEO_BAIDU_SITE_VERIFICATION || '', // Remove the value or replace it with your own google site verification code
SEO_BAIDU_SITE_VERIFICATION: process.env.NEXT_PUBLIC_SEO_BAIDU_SITE_VERIFICATION || '', // Remove the value or replace it with your own google site verification code
// 微软 Clarity 站点分析
CLARITY_ID: process.env.NEXT_PUBLIC_CLARITY_ID || null, // 只需要复制Clarity脚本中的ID部分ID是一个十位的英文数字组合
@@ -408,23 +413,20 @@ const BLOG = {
type: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE || 'type', // 文章类型,
type_post: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_POST || 'Post', // 当type文章类型与此值相同时为博文。
type_page: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_PAGE || 'Page', // 当type文章类型与此值相同时为单页。
type_notice:
process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_NOTICE || 'Notice', // 当type文章类型与此值相同时为公告。
type_notice: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_NOTICE || 'Notice', // 当type文章类型与此值相同时为公告。
type_menu: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_MENU || 'Menu', // 当type文章类型与此值相同时为菜单。
type_sub_menu:
process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_SUB_MENU || 'SubMenu', // 当type文章类型与此值相同时为子菜单。
type_sub_menu: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_SUB_MENU || 'SubMenu', // 当type文章类型与此值相同时为子菜单。
title: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TITLE || 'title', // 文章标题
status: process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS || 'status',
status_publish:
process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS_PUBLISH || 'Published', // 当status状态值与此相同时为发布可以为中文
status_invisible:
process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS_INVISIBLE || 'Invisible', // 当status状态值与此相同时为隐藏发布可以为中文 除此之外其他页面状态不会显示在博客上
status_publish: process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS_PUBLISH || 'Published', // 当status状态值与此相同时为发布可以为中文
status_invisible: process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS_INVISIBLE || 'Invisible', // 当status状态值与此相同时为隐藏发布,可以为中文 除此之外其他页面状态不会显示在博客上
summary: process.env.NEXT_PUBLIC_NOTION_PROPERTY_SUMMARY || 'summary',
slug: process.env.NEXT_PUBLIC_NOTION_PROPERTY_SLUG || 'slug',
category: process.env.NEXT_PUBLIC_NOTION_PROPERTY_CATEGORY || 'category',
date: process.env.NEXT_PUBLIC_NOTION_PROPERTY_DATE || 'date',
tags: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TAGS || 'tags',
icon: process.env.NEXT_PUBLIC_NOTION_PROPERTY_ICON || 'icon'
icon: process.env.NEXT_PUBLIC_NOTION_PROPERTY_ICON || 'icon',
ext: process.env.NEXT_PUBLIC_NOTION_PROPERTY_EXT || 'ext' // 扩展字段存放json-string用于复杂业务
},
// RSS订阅
@@ -432,24 +434,32 @@ const BLOG = {
MAILCHIMP_LIST_ID: process.env.MAILCHIMP_LIST_ID || null, // 开启mailichimp邮件订阅 客户列表ID ,具体使用方法参阅文档
MAILCHIMP_API_KEY: process.env.MAILCHIMP_API_KEY || null, // 开启mailichimp邮件订阅 APIkey
// 作废配置
AVATAR: process.env.NEXT_PUBLIC_AVATAR || '/avatar.svg', // 作者头像被notion中的ICON覆盖。若无ICON则取public目录下的avatar.png
TITLE: process.env.NEXT_PUBLIC_TITLE || 'NotionNext BLOG', // 站点标题 被notion中的页面标题覆盖此处请勿留空白否则服务器无法编译
HOME_BANNER_IMAGE:
process.env.NEXT_PUBLIC_HOME_BANNER_IMAGE || '/bg_image.jpg', // 首页背景大图, 会被notion中的封面图覆盖若无封面图则会使用代码中的 /public/bg_image.jpg 文件
DESCRIPTION:
process.env.NEXT_PUBLIC_DESCRIPTION || '这是一个由NotionNext生成的站点', // 站点描述被notion中的页面描述覆盖
// ANIMATE.css 动画
ANIMATE_CSS_URL:
process.env.NEXT_PUBLIC_ANIMATE_CSS_URL ||
'https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css', // 动画CDN
// 网站图片
IMG_LAZY_LOAD_PLACEHOLDER: process.env.NEXT_PUBLIC_IMG_LAZY_LOAD_PLACEHOLDER || 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==', // 懒加载占位图片地址支持base64或url
IMG_LAZY_LOAD_PLACEHOLDER:
process.env.NEXT_PUBLIC_IMG_LAZY_LOAD_PLACEHOLDER ||
'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==', // 懒加载占位图片地址支持base64或url
IMG_URL_TYPE: process.env.NEXT_PUBLIC_IMG_TYPE || 'Notion', // 此配置已失效请勿使用AMAZON方案不再支持仅支持Notion方案。 ['Notion','AMAZON'] 站点图片前缀 默认 Notion:(https://notion.so/images/xx) AMAZON(https://s3.us-west-2.amazonaws.com/xxx)
IMG_SHADOW: process.env.NEXT_PUBLIC_IMG_SHADOW || false, // 文章图片是否自动添加阴影
IMG_COMPRESS_WIDTH: process.env.NEXT_PUBLIC_IMG_COMPRESS_WIDTH || 800, // Notion图片压缩宽度
// 作废配置
AVATAR: process.env.NEXT_PUBLIC_AVATAR || '/avatar.svg', // 作者头像被notion中的ICON覆盖。若无ICON则取public目录下的avatar.png
TITLE: process.env.NEXT_PUBLIC_TITLE || 'NotionNext BLOG', // 站点标题 被notion中的页面标题覆盖此处请勿留空白否则服务器无法编译
HOME_BANNER_IMAGE: process.env.NEXT_PUBLIC_HOME_BANNER_IMAGE || '/bg_image.jpg', // 首页背景大图, 会被notion中的封面图覆盖若无封面图则会使用代码中的 /public/bg_image.jpg 文件
DESCRIPTION: process.env.NEXT_PUBLIC_DESCRIPTION || '这是一个由NotionNext生成的站点', // 站点描述被notion中的页面描述覆盖
// 开发相关
NOTION_ACCESS_TOKEN: process.env.NOTION_ACCESS_TOKEN || '', // Useful if you prefer not to make your database public
DEBUG: process.env.NEXT_PUBLIC_DEBUG || false, // 是否显示调试按钮
ENABLE_CACHE: process.env.ENABLE_CACHE || process.env.npm_lifecycle_event === 'build' || process.env.npm_lifecycle_event === 'export', // 在打包过程中默认开启缓存,开发或运行时开启此功能意义不大。
ENABLE_CACHE:
process.env.ENABLE_CACHE ||
process.env.npm_lifecycle_event === 'build' ||
process.env.npm_lifecycle_event === 'export', // 在打包过程中默认开启缓存,开发或运行时开启此功能意义不大。
isProd: process.env.VERCEL_ENV === 'production', // distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables) isProd: process.env.VERCEL_ENV === 'production' // distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables)
BUNDLE_ANALYZER: process.env.ANALYZE === 'true' || false, // 是否展示编译依赖内容与大小
VERSION: process.env.NEXT_PUBLIC_VERSION // 版本号

View File

@@ -1,4 +1,4 @@
import { useState, useImperativeHandle, useRef, useEffect } from 'react'
import { useState, useImperativeHandle, useRef, useEffect, Fragment } from 'react'
import algoliasearch from 'algoliasearch'
import replaceSearchResult from '@/components/Mark'
import Link from 'next/link'
@@ -7,6 +7,22 @@ import throttle from 'lodash/throttle'
import { siteConfig } from '@/lib/config'
import { useHotkeys } from 'react-hotkeys-hook';
const ShortCutActions = [
{
key: '↑ ↓',
action: '选择'
},
{
key: 'Enter',
action: '跳转'
},
{
key: 'Esc',
action: '关闭'
}
]
/**
* 结合 Algolia 实现的弹出式搜索框
* 打开方式 cRef.current.openSearch()
@@ -22,6 +38,7 @@ export default function AlgoliaSearchModal({ cRef }) {
const [useTime, setUseTime] = useState(0)
const inputRef = useRef(null)
const [activeIndex, setActiveIndex] = useState(0)
const [isLoading, setIsLoading] = useState(false)
useHotkeys('ctrl+k', (e) => {
e.preventDefault()
setIsModalOpen(true)
@@ -105,7 +122,7 @@ export default function AlgoliaSearchModal({ cRef }) {
if (!query || query === '') {
return
}
setIsLoading(true)
try {
const res = await index.search(query, { page, hitsPerPage: 10 })
const { hits, nbHits, nbPages, processingTimeMS } = res
@@ -113,6 +130,7 @@ export default function AlgoliaSearchModal({ cRef }) {
setTotalPage(nbPages)
setTotalHit(nbHits)
setSearchResults(hits)
setIsLoading(false)
const doms = document.getElementById('search-wrapper').getElementsByClassName('replace')
setTimeout(() => {
@@ -175,12 +193,12 @@ export default function AlgoliaSearchModal({ cRef }) {
<div
id="search-wrapper"
className={`${isModalOpen ? 'opacity-100' : 'invisible opacity-0 pointer-events-none'
} z-30 fixed h-screen w-screen left-0 top-0 mt-12 flex items-start justify-center`}
} z-30 fixed h-screen w-screen left-0 top-0 sm:mt-12 flex items-start justify-center mt-0`}
>
{/* 模态框 */}
<div
className={`${isModalOpen ? 'opacity-100' : 'invisible opacity-0 translate-y-10'
} flex flex-col justify-between w-full min-h-[10rem] max-w-xl dark:bg-hexo-black-gray dark:border-gray-800 bg-white dark:bg- p-5 rounded-lg z-50 shadow border hover:border-blue-600 duration-300 transition-all `}
} flex flex-col justify-between w-full min-h-[10rem] h-full md:h-fit max-w-xl dark:bg-hexo-black-gray dark:border-gray-800 bg-white dark:bg- p-5 rounded-lg z-50 shadow border hover:border-blue-600 duration-300 transition-all `}
>
<div className="flex justify-between items-center">
<div className="text-2xl text-blue-600 dark:text-yellow-600 font-bold">搜索</div>
@@ -205,21 +223,23 @@ export default function AlgoliaSearchModal({ cRef }) {
<TagGroups />
</div>
{
searchResults.length === 0 && keyword && (
searchResults.length === 0 && keyword && !isLoading && (
<div>
<p className=" text-slate-600 text-center my-4 text-base"> 无法找到相关结果
<p className=" text-slate-600 text-center my-4 text-base"> 无法找到相关结果
<span
className='font-semibold'
>&quot;{keyword}&quot;</span></p>
</div>
)
}
<ul>
<ul className='flex-1 overflow-auto'>
{searchResults.map((result, index) => (
<li key={result.objectID}
onMouseEnter={() => setActiveIndex(index)}
onClick={() => onJumpSearchResult(index)}
className={`cursor-pointer replace my-2 p-2 duration-100 ${activeIndex === index ? 'bg-blue-600 dark:bg-yellow-600' : ''}`}>
className={`cursor-pointer replace my-2 p-2 duration-100
rounded-lg
${activeIndex === index ? 'bg-blue-600 dark:bg-yellow-600' : ''}`}>
<a
className={`${activeIndex === index ? ' text-white' : ' text-black dark:text-gray-300 '}`}
>
@@ -229,7 +249,16 @@ export default function AlgoliaSearchModal({ cRef }) {
))}
</ul>
<Pagination totalPage={totalPage} page={page} switchPage={switchPage} />
<div className='flex items-center justify-between mt-2 text-sm dark:text-gray-300'>
<div className='flex items-center justify-between mt-2 sm:text-sm text-xs dark:text-gray-300'>
{totalHit === 0 && (<div className='flex items-center'>
{
ShortCutActions.map((action, index) => {
return <Fragment key={index}><div className='border-gray-300 dark:text-gray-300 text-gray-600 px-2 rounded border inline-block'>{action.key}</div>
<span className='ml-2 mr-4 text-gray-600 dark:text-gray-300'>{action.action}</span></Fragment>
})
}
</div>)
}
<div>
{totalHit > 0 && (
<p>
@@ -237,7 +266,7 @@ export default function AlgoliaSearchModal({ cRef }) {
</p>
)}
</div>
<div className="text-gray-600 text-right">
<div className="text-gray-600 dark:text-gray-300 text-right">
<span >
<i className="fa-brands fa-algolia"></i> Algolia
</span>
@@ -294,7 +323,7 @@ function Pagination(props) {
{Array.from({ length: totalPage }, (_, i) => {
const classNames = page === i
? 'font-bold text-white bg-blue-600 dark:bg-yellow-600 rounded'
: 'hover:text-blue-600 hover:font-bold'
: 'hover:text-blue-600 hover:font-bold dark:text-gray-300'
return (
<div

11
components/Badge.js Normal file
View File

@@ -0,0 +1,11 @@
/**
* 红点
*/
export default function Badge() {
return <>
{/* 红点 */}
<span class="absolute right-1 top-1 flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
</span></>
}

View File

@@ -1,4 +1,4 @@
import busuanzi from '@/lib/busuanzi'
import busuanzi from '@/lib/plugins/busuanzi'
import { useRouter } from 'next/router'
import { useGlobal } from '@/lib/global'
// import { useRouter } from 'next/router'

View File

@@ -151,20 +151,20 @@ export default function CustomContextMenu(props) {
{/* 跳转导航按钮 */}
<div className='w-full px-2'>
<div onClick={handleJumpToRandomPost} title={locale.MENU.WALK_AROUND} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
{siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_RANDOM_POST') && <div onClick={handleJumpToRandomPost} title={locale.MENU.WALK_AROUND} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className="fa-solid fa-podcast mr-2" />
<div className='whitespace-nowrap'>{locale.MENU.WALK_AROUND}</div>
</div>
</div>}
<Link href='/category' title={locale.MENU.CATEGORY} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
{siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_CATEGORY') && <Link href='/category' title={locale.MENU.CATEGORY} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className="fa-solid fa-square-minus mr-2" />
<div className='whitespace-nowrap'>{locale.MENU.CATEGORY}</div>
</Link>
</Link>}
<Link href='/tag' title={locale.MENU.TAGS} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
{siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_TAG') && <Link href='/tag' title={locale.MENU.TAGS} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className="fa-solid fa-tag mr-2" />
<div className='whitespace-nowrap'>{locale.MENU.TAGS}</div>
</Link>
</Link>}
</div>
@@ -180,15 +180,16 @@ export default function CustomContextMenu(props) {
</div>
)}
<div onClick={handleCopyLink} title={locale.MENU.SHARE_URL} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
{siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_SHARE_LINK') && <div onClick={handleCopyLink} title={locale.MENU.SHARE_URL} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className="fa-solid fa-arrow-up-right-from-square mr-2" />
<div className='whitespace-nowrap'>{locale.MENU.SHARE_URL}</div>
</div>
</div>}
<div onClick={handleChangeDarkMode} title={isDarkMode ? locale.MENU.LIGHT_MODE : locale.MENU.DARK_MODE} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
{siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_DARK_MODE') && <div onClick={handleChangeDarkMode} title={isDarkMode ? locale.MENU.LIGHT_MODE : locale.MENU.DARK_MODE} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
{isDarkMode ? <i className="fa-regular fa-sun mr-2" /> : <i className="fa-regular fa-moon mr-2" />}
<div className='whitespace-nowrap'> {isDarkMode ? locale.MENU.LIGHT_MODE : locale.MENU.DARK_MODE}</div>
</div>
</div>}
{siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH') && (
<div onClick={handleChangeTheme} title={locale.MENU.THEME_SWITCH} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className="fa-solid fa-palette mr-2" />

View File

@@ -1,10 +1,10 @@
import { useRef, useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
/**
* 可拖拽组件
*/
export const Draggable = (props) => {
const { children } = props
export const Draggable = props => {
const { children, stick } = props
const draggableRef = useRef(null)
const rafRef = useRef(null)
const [moving, setMoving] = useState(false)
@@ -14,8 +14,10 @@ export const Draggable = (props) => {
const draggableElements = document.getElementsByClassName('draggable')
// 标准化鼠标事件对象
function e(event) { // 定义事件对象标准化函数
if (!event) { // 兼容IE浏览器
function e(event) {
// 定义事件对象标准化函数
if (!event) {
// 兼容IE浏览器
event = window.event
event.target = event.srcElement
event.layerX = event.offsetX
@@ -40,9 +42,10 @@ export const Draggable = (props) => {
document.onmousedown = start
document.ontouchstart = start
function start (event) { // 按下鼠标时,初始化处理
function start(event) {
// 按下鼠标时,初始化处理
if (!draggableElements) return
event = e(event)// 获取标准事件对象
event = e(event) // 获取标准事件对象
for (const drag of draggableElements) {
// 判断鼠标点击的区域是否是拖拽框内
@@ -60,19 +63,20 @@ export const Draggable = (props) => {
offsetX = event.mx - currentObj.offsetLeft
offsetY = event.my - currentObj.offsetTop
document.onmousemove = move// 注册鼠标移动事件处理函数
document.onmousemove = move // 注册鼠标移动事件处理函数
document.ontouchmove = move
document.onmouseup = stop// 注册松开鼠标事件处理函数
document.onmouseup = stop // 注册松开鼠标事件处理函数
document.ontouchend = stop
}
}
function move(event) { // 鼠标移动处理函数
function move(event) {
// 鼠标移动处理函数
event = e(event)
rafRef.current = requestAnimationFrame(() => updatePosition(event))
}
const stop = (event) => {
const stop = event => {
event = e(event)
document.documentElement.style.overflow = 'auto' // 恢复默认的滚动行为
cancelAnimationFrame(rafRef.current)
@@ -80,7 +84,7 @@ export const Draggable = (props) => {
currentObj = document.ontouchmove = document.ontouchend = document.onmousemove = document.onmouseup = null
}
const updatePosition = (event) => {
const updatePosition = event => {
if (currentObj) {
const left = event.mx - offsetX
const top = event.my - offsetY
@@ -120,15 +124,18 @@ export const Draggable = (props) => {
if (offsetTop < 0) {
drag.firstElementChild.style.top = 0
}
if (offsetTop > (clientHeight - offsetHeight)) {
if (offsetTop > clientHeight - offsetHeight) {
drag.firstElementChild.style.top = clientHeight - offsetHeight + 'px'
}
if (offsetLeft < 0) {
drag.firstElementChild.style.left = 0
}
if (offsetLeft > (clientWidth - offsetWidth)) {
if (offsetLeft > clientWidth - offsetWidth) {
drag.firstElementChild.style.left = clientWidth - offsetWidth + 'px'
}
if (stick === 'left') {
drag.firstElementChild.style.left = 0 + 'px'
}
}
}
@@ -142,9 +149,11 @@ export const Draggable = (props) => {
}
}, [])
return <div className={`draggable ${moving ? 'cursor-grabbing' : 'cursor-grab'} select-none`} ref={draggableRef}>
{children}
</div>
return (
<div className={`draggable ${moving ? 'cursor-grabbing' : 'cursor-grab'} select-none`} ref={draggableRef}>
{children}
</div>
)
}
Draggable.defaultProps = { left: 0, top: 0 }

View File

@@ -80,6 +80,7 @@ const ExternalPlugin = (props) => {
const GLOBAL_JS = siteConfig('GLOBAL_JS')
const CLARITY_ID = siteConfig('CLARITY_ID')
const IMG_SHADOW = siteConfig('IMG_SHADOW')
const ANIMATE_CSS_URL = siteConfig('ANIMATE_CSS_URL')
// 自定义样式css和js引入
if (isBrowser) {
@@ -93,6 +94,10 @@ const ExternalPlugin = (props) => {
loadExternalResource('/css/img-shadow.css', 'css')
}
if (ANIMATE_CSS_URL) {
loadExternalResource(ANIMATE_CSS_URL, 'css')
}
// 导入外部自定义脚本
if (CUSTOM_EXTERNAL_JS && CUSTOM_EXTERNAL_JS.length > 0) {
for (const url of CUSTOM_EXTERNAL_JS) {

View File

@@ -8,74 +8,76 @@ import { useRouter } from 'next/router'
* @param {*} param0
* @returns
*/
const GlobalHead = (props) => {
const { children } = props
const GlobalHead = props => {
const { children, siteInfo } = props
let url = siteConfig('PATH')?.length ? `${siteConfig('LINK')}/${siteConfig('SUB_PATH', '')}` : siteConfig('LINK')
let image
const meta = getSEOMeta(props, useRouter(), useGlobal())
const router = useRouter()
const meta = getSEOMeta(props, router, useGlobal())
if (meta) {
url = `${url}/${meta.slug}`
image = meta.image || '/bg_image.jpg'
}
const title = meta?.title || siteConfig('TITLE')
const description = meta?.description || siteConfig('DESCRIPTION')
const description = meta?.description || `${siteInfo?.description}`
const type = meta?.type || 'website'
const keywords = meta?.tags || siteConfig('KEYWORDS')
const lang = siteConfig('LANG').replace('-', '_') // Facebook OpenGraph 要 zh_CN 這樣的格式才抓得到語言
const category = meta?.category || siteConfig('KEYWORDS') // section 主要是像是 category 這樣的分類Facebook 用這個來抓連結的分類
return (
<Head>
<title>{title}</title>
<meta name="theme-color" content={siteConfig('BACKGROUND_DARK')} />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, minimum-scale=1.0" />
<meta name="robots" content="follow, index" />
<meta charSet="UTF-8" />
{siteConfig('SEO_GOOGLE_SITE_VERIFICATION') && (
<meta
name="google-site-verification"
content={siteConfig('SEO_GOOGLE_SITE_VERIFICATION')}
/>
)}
{siteConfig('SEO_BAIDU_SITE_VERIFICATION') && (<meta name="baidu-site-verification" content={siteConfig('SEO_BAIDU_SITE_VERIFICATION')} />)}
<meta name="keywords" content={keywords} />
<meta name="description" content={description} />
<meta property="og:locale" content={lang} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={url} />
<meta property="og:image" content={image} />
<meta property="og:site_name" content={siteConfig('TITLE')} />
<meta property="og:type" content={type} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:description" content={description} />
<meta name="twitter:title" content={title} />
<Head>
<title>{title}</title>
<meta name='theme-color' content={siteConfig('BACKGROUND_DARK')} />
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=5.0, minimum-scale=1.0' />
<meta name='robots' content='follow, index' />
<meta charSet='UTF-8' />
{siteConfig('SEO_GOOGLE_SITE_VERIFICATION') && (
<meta name='google-site-verification' content={siteConfig('SEO_GOOGLE_SITE_VERIFICATION')} />
)}
{siteConfig('SEO_BAIDU_SITE_VERIFICATION') && (
<meta name='baidu-site-verification' content={siteConfig('SEO_BAIDU_SITE_VERIFICATION')} />
)}
<meta name='keywords' content={keywords} />
<meta name='description' content={description} />
<meta property='og:locale' content={lang} />
<meta property='og:title' content={title} />
<meta property='og:description' content={description} />
<meta property='og:url' content={url} />
<meta property='og:image' content={image} />
<meta property='og:site_name' content={siteConfig('TITLE')} />
<meta property='og:type' content={type} />
<meta name='twitter:card' content='summary_large_image' />
<meta name='twitter:description' content={description} />
<meta name='twitter:title' content={title} />
{siteConfig('COMMENT_WEBMENTION_ENABLE') && (
<>
<link rel="webmention" href={`https://webmention.io/${siteConfig('COMMENT_WEBMENTION_HOSTNAME')}/webmention`} />
<link rel="pingback" href={`https://webmention.io/${siteConfig('COMMENT_WEBMENTION_HOSTNAME')}/xmlrpc`} />
</>
)}
{siteConfig('COMMENT_WEBMENTION_ENABLE') && (
<>
<link
rel='webmention'
href={`https://webmention.io/${siteConfig('COMMENT_WEBMENTION_HOSTNAME')}/webmention`}
/>
<link rel='pingback' href={`https://webmention.io/${siteConfig('COMMENT_WEBMENTION_HOSTNAME')}/xmlrpc`} />
</>
)}
{siteConfig('COMMENT_WEBMENTION_ENABLE') && siteConfig('COMMENT_WEBMENTION_AUTH') !== '' && (
<link href={siteConfig('COMMENT_WEBMENTION_AUTH')} rel="me" />
)}
{siteConfig('COMMENT_WEBMENTION_ENABLE') && siteConfig('COMMENT_WEBMENTION_AUTH') !== '' && (
<link href={siteConfig('COMMENT_WEBMENTION_AUTH')} rel='me' />
)}
{JSON.parse(siteConfig('ANALYTICS_BUSUANZI_ENABLE')) && <meta name="referrer" content="no-referrer-when-downgrade" />}
{meta?.type === 'Post' && (
<>
<meta
property="article:published_time"
content={meta.publishDay}
/>
<meta property="article:author" content={siteConfig('AUTHOR')} />
<meta property="article:section" content={category} />
<meta property="article:publisher" content={siteConfig('FACEBOOK_PAGE')} />
</>
)}
{children}
</Head>
{JSON.parse(siteConfig('ANALYTICS_BUSUANZI_ENABLE')) && (
<meta name='referrer' content='no-referrer-when-downgrade' />
)}
{meta?.type === 'Post' && (
<>
<meta property='article:published_time' content={meta.publishDay} />
<meta property='article:author' content={siteConfig('AUTHOR')} />
<meta property='article:section' content={category} />
<meta property='article:publisher' content={siteConfig('FACEBOOK_PAGE')} />
</>
)}
{children}
</Head>
)
}
@@ -86,105 +88,104 @@ const GlobalHead = (props) => {
*/
const getSEOMeta = (props, router, global) => {
const { locale } = global
const { post, tag, category, page } = props
const { post, siteInfo, tag, category, page } = props
const keyword = router?.query?.s
switch (router.route) {
case '/':
return {
title: `${siteConfig('TITLE')} | ${siteConfig('DESCRIPTION')}`,
description: siteConfig('DESCRIPTION'),
image: siteConfig('HOME_BANNER_IMAGE'),
title: `${siteInfo?.title} | ${siteInfo?.description}`,
description: `${siteInfo?.description}`,
image: `${siteInfo?.pageCover}`,
slug: '',
type: 'website'
}
case '/archive':
return {
title: `${locale.NAV.ARCHIVE} | ${siteConfig('TITLE')}`,
description: siteConfig('DESCRIPTION'),
image: siteConfig('HOME_BANNER_IMAGE'),
title: `${locale.NAV.ARCHIVE} | ${siteInfo?.title}`,
description: `${siteInfo?.description}`,
image: `${siteInfo?.pageCover}`,
slug: 'archive',
type: 'website'
}
case '/page/[page]':
return {
title: `${page} | Page | ${siteConfig('TITLE')}`,
description: siteConfig('DESCRIPTION'),
image: siteConfig('HOME_BANNER_IMAGE'),
title: `${page} | Page | ${siteInfo?.title}`,
description: `${siteInfo?.description}`,
image: `${siteInfo?.pageCover}`,
slug: 'page/' + page,
type: 'website'
}
case '/category/[category]':
return {
title: `${category} | ${locale.COMMON.CATEGORY} | ${
siteConfig('TITLE') || ''
}`,
description: siteConfig('DESCRIPTION'),
title: `${category} | ${locale.COMMON.CATEGORY} | ${siteInfo?.title}`,
description: `${siteInfo?.description}`,
slug: 'category/' + category,
image: siteConfig('HOME_BANNER_IMAGE'),
image: `${siteInfo?.pageCover}`,
type: 'website'
}
case '/category/[category]/page/[page]':
return {
title: `${category} | ${locale.COMMON.CATEGORY} | ${
siteConfig('TITLE') || ''
}`,
description: siteConfig('DESCRIPTION'),
title: `${category} | ${locale.COMMON.CATEGORY} | ${siteInfo?.title}`,
description: `${siteInfo?.description}`,
slug: 'category/' + category,
image: siteConfig('HOME_BANNER_IMAGE'),
image: `${siteInfo?.pageCover}`,
type: 'website'
}
case '/tag/[tag]':
case '/tag/[tag]/page/[page]':
return {
title: `${tag} | ${locale.COMMON.TAGS} | ${siteConfig('TITLE')}`,
description: siteConfig('DESCRIPTION'),
image: siteConfig('HOME_BANNER_IMAGE'),
title: `${tag} | ${locale.COMMON.TAGS} | ${siteInfo?.title}`,
description: `${siteInfo?.description}`,
image: `${siteInfo?.pageCover}`,
slug: 'tag/' + tag,
type: 'website'
}
case '/search':
return {
title: `${keyword || ''}${keyword ? ' | ' : ''}${locale.NAV.SEARCH} | ${siteConfig('TITLE')}`,
description: siteConfig('DESCRIPTION'),
image: siteConfig('HOME_BANNER_IMAGE'),
title: `${keyword || ''}${keyword ? ' | ' : ''}${locale.NAV.SEARCH} | ${siteInfo?.title}`,
description: `${siteInfo?.description}`,
image: `${siteInfo?.pageCover}`,
slug: 'search',
type: 'website'
}
case '/search/[keyword]':
case '/search/[keyword]/page/[page]':
return {
title: `${keyword || ''}${keyword ? ' | ' : ''}${locale.NAV.SEARCH} | ${siteConfig('TITLE')}`,
title: `${keyword || ''}${keyword ? ' | ' : ''}${locale.NAV.SEARCH} | ${siteInfo?.title}`,
description: siteConfig('TITLE'),
image: siteConfig('HOME_BANNER_IMAGE'),
image: `${siteInfo?.pageCover}`,
slug: 'search/' + (keyword || ''),
type: 'website'
}
case '/404':
return { title: `${siteConfig('TITLE')} | 页面找不到啦`, image: siteConfig('HOME_BANNER_IMAGE') }
return {
title: `${siteInfo?.title} | 页面找不到啦`,
image: `${siteInfo?.pageCover}`
}
case '/tag':
return {
title: `${locale.COMMON.TAGS} | ${siteConfig('TITLE')}`,
description: siteConfig('DESCRIPTION'),
image: siteConfig('HOME_BANNER_IMAGE'),
title: `${locale.COMMON.TAGS} | ${siteInfo?.title}`,
description: `${siteInfo?.description}`,
image: `${siteInfo?.pageCover}`,
slug: 'tag',
type: 'website'
}
case '/category':
return {
title: `${locale.COMMON.CATEGORY} | ${siteConfig('TITLE')}`,
description: siteConfig('DESCRIPTION'),
image: siteConfig('HOME_BANNER_IMAGE'),
title: `${locale.COMMON.CATEGORY} | ${siteInfo?.title}`,
description: `${siteInfo?.description}`,
image: `${siteInfo?.pageCover}`,
slug: 'category',
type: 'website'
}
default:
return {
title: post ? `${post?.title} | ${siteConfig('TITLE')}` : `${siteConfig('TITLE')} | loading`,
title: post ? `${post?.title} | ${siteInfo?.title}` : `${siteInfo?.title} | loading`,
description: post?.summary,
type: post?.type,
slug: post?.slug,
image: post?.pageCoverThumbnail || siteConfig('HOME_BANNER_IMAGE'),
image: post?.pageCoverThumbnail || `${siteInfo?.pageCover}`,
category: post?.category?.[0],
tags: post?.tags
}

View File

@@ -3,25 +3,33 @@ import { loadExternalResource } from '@/lib/utils'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
function requestAd() {
const ads = document.getElementsByClassName('adsbygoogle')
const adsbygoogle = window.adsbygoogle
if (adsbygoogle && ads.length > 0) {
for (let i = 0; i <= ads.length; i++) {
try {
const adStatus = ads[i].getAttribute('data-adsbygoogle-status')
if (!adStatus || adStatus !== 'done') {
adsbygoogle.push(ads[i])
}
} catch (e) {}
}
}
}
/**
* 初始化谷歌广告
* @returns
*/
export default function GoogleAdsense() {
const initGoogleAdsense = () => {
loadExternalResource(`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${siteConfig('ADSENSE_GOOGLE_ID')}`, 'js').then(url => {
loadExternalResource(
`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${siteConfig('ADSENSE_GOOGLE_ID')}`,
'js'
).then(url => {
setTimeout(() => {
const ads = document.getElementsByClassName('adsbygoogle')
const adsbygoogle = window.adsbygoogle
if (ads.length > 0) {
for (let i = 0; i <= ads.length; i++) {
try {
adsbygoogle.push(ads[i])
} catch (e) {
}
}
}
requestAd()
}, 100)
})
}
@@ -49,44 +57,92 @@ const AdSlot = ({ type = 'show' }) => {
}
// 文章内嵌广告
if (type === 'in-article') {
return <ins className="adsbygoogle"
style={{ display: 'block', textAlign: 'center' }}
data-ad-layout="in-article"
data-ad-format="fluid"
data-adtest={siteConfig('ADSENSE_GOOGLE_TEST') ? 'on' : 'off'}
data-ad-client={siteConfig('ADSENSE_GOOGLE_ID')}
data-ad-slot={siteConfig('ADSENSE_GOOGLE_SLOT_IN_ARTICLE')}></ins>
return (
<ins
className='adsbygoogle'
style={{ display: 'block', textAlign: 'center' }}
data-ad-layout='in-article'
data-ad-format='fluid'
data-adtest={siteConfig('ADSENSE_GOOGLE_TEST') ? 'on' : 'off'}
data-ad-client={siteConfig('ADSENSE_GOOGLE_ID')}
data-ad-slot={siteConfig('ADSENSE_GOOGLE_SLOT_IN_ARTICLE')}></ins>
)
}
// 信息流广告
if (type === 'flow') {
return <ins className="adsbygoogle"
data-ad-format="fluid"
data-ad-layout-key="-5j+cz+30-f7+bf"
style={{ display: 'block' }}
data-adtest={siteConfig('ADSENSE_GOOGLE_TEST') ? 'on' : 'off'}
data-ad-client={siteConfig('ADSENSE_GOOGLE_ID')}
data-ad-slot={siteConfig('ADSENSE_GOOGLE_SLOT_FLOW')}></ins>
return (
<ins
className='adsbygoogle'
data-ad-format='fluid'
data-ad-layout-key='-5j+cz+30-f7+bf'
style={{ display: 'block' }}
data-adtest={siteConfig('ADSENSE_GOOGLE_TEST') ? 'on' : 'off'}
data-ad-client={siteConfig('ADSENSE_GOOGLE_ID')}
data-ad-slot={siteConfig('ADSENSE_GOOGLE_SLOT_FLOW')}></ins>
)
}
// 原生广告
if (type === 'native') {
return <ins className="adsbygoogle"
style={{ display: 'block', textAlign: 'center' }}
data-ad-format="autorelaxed"
data-adtest={siteConfig('ADSENSE_GOOGLE_TEST') ? 'on' : 'off'}
data-ad-client={siteConfig('ADSENSE_GOOGLE_ID')}
data-ad-slot={siteConfig('ADSENSE_GOOGLE_SLOT_NATIVE')}></ins>
return (
<ins
className='adsbygoogle'
style={{ display: 'block', textAlign: 'center' }}
data-ad-format='autorelaxed'
data-adtest={siteConfig('ADSENSE_GOOGLE_TEST') ? 'on' : 'off'}
data-ad-client={siteConfig('ADSENSE_GOOGLE_ID')}
data-ad-slot={siteConfig('ADSENSE_GOOGLE_SLOT_NATIVE')}></ins>
)
}
// 展示广告
return <ins className="adsbygoogle"
style={{ display: 'block' }}
data-ad-client={siteConfig('ADSENSE_GOOGLE_ID')}
data-adtest={siteConfig('ADSENSE_GOOGLE_TEST') ? 'on' : 'off'}
data-ad-slot={siteConfig('ADSENSE_GOOGLE_SLOT_AUTO')}
data-ad-format="auto"
data-full-width-responsive="true"></ins>
return (
<ins
className='adsbygoogle'
style={{ display: 'block' }}
data-ad-client={siteConfig('ADSENSE_GOOGLE_ID')}
data-adtest={siteConfig('ADSENSE_GOOGLE_TEST') ? 'on' : 'off'}
data-ad-slot={siteConfig('ADSENSE_GOOGLE_SLOT_AUTO')}
data-ad-format='auto'
data-full-width-responsive='true'></ins>
)
}
export { AdSlot }
/**
* 嵌入到文章内部的广告单元
* 检测文本内容 出现<ins/> 关键词时自动替换为广告
* @param {*} props
*/
const AdEmbed = () => {
useEffect(() => {
setTimeout(() => {
// 找到所有 class 为 notion-text 且内容为 '<ins/>' 的 div 元素
const notionTextElements = document.querySelectorAll('div.notion-text')
// 遍历找到的元素
notionTextElements?.forEach(element => {
// 检查元素的内容是否为 '<ins/>'
if (element.innerHTML.trim() === '&lt;ins/&gt;') {
// 创建新的 <ins> 元素
const newInsElement = document.createElement('ins')
newInsElement.className = 'adsbygoogle w-full py-1'
newInsElement.style.display = 'block'
newInsElement.setAttribute('data-ad-client', siteConfig('ADSENSE_GOOGLE_ID'))
newInsElement.setAttribute('data-adtest', siteConfig('ADSENSE_GOOGLE_TEST') ? 'on' : 'off')
newInsElement.setAttribute('data-ad-slot', siteConfig('ADSENSE_GOOGLE_SLOT_AUTO'))
newInsElement.setAttribute('data-ad-format', 'auto')
newInsElement.setAttribute('data-full-width-responsive', 'true')
// 用新创建的 <ins> 元素替换掉原来的 div 元素
element?.parentNode?.replaceChild(newInsElement, element)
}
})
requestAd()
}, 1000)
}, [])
return <></>
}
export { AdEmbed, AdSlot }

View File

@@ -1,6 +1,6 @@
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import * as gtag from '@/lib/gtag'
import * as gtag from '@/lib/plugins/gtag'
const Gtag = () => {
const router = useRouter()

View File

@@ -1,33 +1,34 @@
import dynamic from 'next/dynamic'
import mediumZoom from '@fisch0920/medium-zoom'
import { useEffect, useRef } from 'react'
import 'katex/dist/katex.min.css'
import { siteConfig } from '@/lib/config'
import { compressImage, mapImgUrl } from '@/lib/notion/mapImage'
import { isBrowser } from '@/lib/utils'
import { siteConfig } from '@/lib/config'
import mediumZoom from '@fisch0920/medium-zoom'
import 'katex/dist/katex.min.css'
import dynamic from 'next/dynamic'
import { useEffect, useRef } from 'react'
import { NotionRenderer } from 'react-notion-x'
const Code = dynamic(() =>
import('react-notion-x/build/third-party/code').then(async (m) => {
return m.Code
}), { ssr: false }
const Code = dynamic(
() =>
import('react-notion-x/build/third-party/code').then(async m => {
return m.Code
}),
{ ssr: false }
)
// 公式
const Equation = dynamic(() =>
import('@/components/Equation').then(async (m) => {
// 化学方程式
await import('@/lib/mhchem')
return m.Equation
}), { ssr: false }
const Equation = dynamic(
() =>
import('@/components/Equation').then(async m => {
// 化学方程式
await import('@/lib/plugins/mhchem')
return m.Equation
}),
{ ssr: false }
)
const Pdf = dynamic(
() => import('react-notion-x/build/third-party/pdf').then((m) => m.Pdf),
{
ssr: false
}
)
const Pdf = dynamic(() => import('react-notion-x/build/third-party/pdf').then(m => m.Pdf), {
ssr: false
})
// https://github.com/txs
// import PrismMac from '@/components/PrismMac'
@@ -42,13 +43,16 @@ const TweetEmbed = dynamic(() => import('react-tweet-embed'), {
ssr: false
})
const Collection = dynamic(() =>
import('react-notion-x/build/third-party/collection').then((m) => m.Collection), { ssr: true }
)
/**
* 文内google广告
*/
const AdEmbed = dynamic(() => import('@/components/GoogleAdsense').then(m => m.AdEmbed), { ssr: true })
const Modal = dynamic(
() => import('react-notion-x/build/third-party/modal').then((m) => m.Modal), { ssr: false }
)
const Collection = dynamic(() => import('react-notion-x/build/third-party/collection').then(m => m.Collection), {
ssr: true
})
const Modal = dynamic(() => import('react-notion-x/build/third-party/modal').then(m => m.Modal), { ssr: false })
const Tweet = ({ id }) => {
return <TweetEmbed tweetId={id} />
@@ -64,15 +68,17 @@ const NotionPage = ({ post, className }) => {
autoScrollToTarget()
}, [])
const zoom = typeof window !== 'undefined' && mediumZoom({
container: '.notion-viewport',
background: 'rgba(0, 0, 0, 0.2)',
margin: getMediumZoomMargin()
})
const zoom =
typeof window !== 'undefined' &&
mediumZoom({
container: '.notion-viewport',
background: 'rgba(0, 0, 0, 0.2)',
margin: getMediumZoomMargin()
})
const zoomRef = useRef(zoom ? zoom.clone() : null)
useEffect(() => {
if (!isBrowser) return;
if (!isBrowser) return
// 将相册gallery下的图片加入放大功能
if (siteConfig('POST_DISABLE_GALLERY_CLICK')) {
@@ -81,7 +87,7 @@ const NotionPage = ({ post, className }) => {
const imgList = document?.querySelectorAll('.notion-collection-card-cover img')
if (imgList && zoomRef.current) {
for (let i = 0; i < imgList.length; i++) {
(zoomRef.current).attach(imgList[i])
zoomRef.current.attach(imgList[i])
}
}
@@ -119,45 +125,48 @@ const NotionPage = ({ post, className }) => {
if (mutation.target.classList.contains('medium-zoom-image--opened')) {
// 等待动画完成后替换为更高清的图像
setTimeout(() => {
// 获取该元素的 src 属性
const src = mutation?.target?.getAttribute('src');
// 获取该元素的 src 属性
const src = mutation?.target?.getAttribute('src')
// 替换为更高清的图像
mutation?.target?.setAttribute('src', compressImage(src, siteConfig('IMAGE_ZOOM_IN_WIDTH', 1200)));
}, 800);
mutation?.target?.setAttribute('src', compressImage(src, siteConfig('IMAGE_ZOOM_IN_WIDTH', 1200)))
}, 800)
}
}
});
});
})
})
// 监视整个文档中的元素和属性的变化
observer.observe(document.body, { attributes: true, subtree: true, attributeFilter: ['class'] });
observer.observe(document.body, { attributes: true, subtree: true, attributeFilter: ['class'] })
return () => {
observer.disconnect();
};
observer.disconnect()
}
}, [])
if (!post || !post.blockMap) {
return <>{post?.summary || ''}</>
}
return <div id='notion-article' className={`mx-auto overflow-hidden ${className || ''}`}>
<NotionRenderer
recordMap={post.blockMap}
mapPageUrl={mapPageUrl}
mapImageUrl={mapImgUrl}
components={{
Code,
Collection,
Equation,
Modal,
Pdf,
Tweet
}} />
return (
<div id='notion-article' className={`mx-auto overflow-hidden ${className || ''}`}>
<NotionRenderer
recordMap={post.blockMap}
mapPageUrl={mapPageUrl}
mapImageUrl={mapImgUrl}
components={{
Code,
Collection,
Equation,
Modal,
Pdf,
Tweet
}}
/>
<PrismMac/>
</div>
<AdEmbed />
<PrismMac />
</div>
)
}
/**

80
components/PWA.js Normal file
View File

@@ -0,0 +1,80 @@
import { compressImage } from '@/lib/notion/mapImage'
import { isBrowser } from '../lib/utils'
/**
* 初始化PWA
* @see https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps
* @param {*} props
* @returns
*/
export function PWA(post, siteInfo) {
if (!isBrowser || !post) {
return
}
// 将 manifest 数据转换为 JSON 字符串
const manifestData = {
id: post?.id,
name: post?.title + ' | ' + siteInfo.title,
short_name: post?.title,
description: post?.summary || siteInfo.description,
icons: [
{
src: compressImage(post?.pageCoverThumbnail, 192),
type: 'image/png',
sizes: '192x192'
}
],
form_factor: 'phone',
start_url: window.location.href,
scope: window.location.href,
display: 'standalone',
background_color: '#181818',
theme_color: '#181818'
}
// 删除已有的 manifest link 元素(如果存在)
const existingManifest = document.querySelector('link[rel="manifest"]')
if (existingManifest) {
existingManifest.parentNode.removeChild(existingManifest)
}
// 创建 manifest 元素
const manifest = document.createElement('link')
manifest.rel = 'manifest'
// 设置 manifest 的 href 为一个 Blob URL
const blobUrl = URL.createObjectURL(
new Blob([JSON.stringify(manifestData)], {
type: 'application/json'
})
)
// 这里会报错因为前端收到的事一个转义了双引号的字符串无法解析成json不知道怎么解决
manifest.href = blobUrl
// 将 manifest 添加到 head 中
document.head.appendChild(manifest)
// 不要忘记在适当的时候释放 Blob URL避免内存泄漏
// 例如,在页面卸载或不再需要该 Blob URL 时
window.addEventListener('unload', () => {
URL.revokeObjectURL(blobUrl)
})
}
/**
* 截去url结尾的 / 便于和slug拼接
* @param {*} str
* @returns
*/
// function getRootPath() {
// const protocol = window.location.protocol
// const hostname = window.location.hostname
// const port = window.location.port
// // 如果端口号存在且不是默认的80或443则包含端口号
// if (port && port !== '80' && port !== '443') {
// return protocol + '//' + hostname + ':' + port
// } else {
// return protocol + '//' + hostname
// }
// }

View File

@@ -19,7 +19,10 @@ const Player = () => {
const musicPlayerEnable = siteConfig('MUSIC_PLAYER')
const musicPlayerCDN = siteConfig('MUSIC_PLAYER_CDN_URL')
const musicMetingEnable = siteConfig('MUSIC_PLAYER_METING')
const musicMetingCDNUrl = siteConfig('MUSIC_PLAYER_METING_CDN_URL', 'https://cdnjs.cloudflare.com/ajax/libs/meting/2.0.1/Meting.min.js')
const musicMetingCDNUrl = siteConfig(
'MUSIC_PLAYER_METING_CDN_URL',
'https://cdnjs.cloudflare.com/ajax/libs/meting/2.0.1/Meting.min.js'
)
const initMusicPlayer = async () => {
if (!musicPlayerEnable) {
@@ -36,14 +39,16 @@ const Player = () => {
}
if (!meting && window.APlayer) {
setPlayer(new window.APlayer({
container: ref.current,
fixed: true,
lrcType: lrcType,
autoplay: autoPlay,
order: order,
audio: audio
}))
setPlayer(
new window.APlayer({
container: ref.current,
fixed: true,
lrcType: lrcType,
autoplay: autoPlay,
order: order,
audio: audio
})
)
}
}
@@ -57,23 +62,28 @@ const Player = () => {
return (
<div className={playerVisible ? 'visible' : 'invisible'}>
<link
rel="stylesheet"
type="text/css"
href="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/aplayer/1.10.1/APlayer.min.css"
rel='stylesheet'
type='text/css'
href='https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/aplayer/1.10.1/APlayer.min.css'
/>
{meting
? <meting-js
fixed="true"
type="playlist"
preload="auto"
lrc-type={siteConfig('MUSIC_PLAYER_METING_LRC_TYPE')}
autoplay={autoPlay}
order={siteConfig('MUSIC_PLAYER_ORDER')}
server={siteConfig('MUSIC_PLAYER_METING_SERVER')}
id={siteConfig('MUSIC_PLAYER_METING_ID')}
/>
: <div ref={ref} data-player={player} />
}
{meting ? (
<meting-js
fixed='true'
type='playlist'
preload='auto'
lrc-type={siteConfig('MUSIC_PLAYER_METING_LRC_TYPE')}
api={siteConfig(
'MUSIC_PLAYER_METING_API',
'https://api.i-meto.com/meting/api'
)}
autoplay={autoPlay}
order={siteConfig('MUSIC_PLAYER_ORDER')}
server={siteConfig('MUSIC_PLAYER_METING_SERVER')}
id={siteConfig('MUSIC_PLAYER_METING_ID')}
/>
) : (
<div ref={ref} data-player={player} />
)}
</div>
)
}

View File

@@ -1,4 +1,4 @@
import React, { createRef } from 'react'
import { createRef, useEffect } from 'react'
import { init } from '@waline/client'
import { useRouter } from 'next/router'
import '@waline/client/dist/waline.css'
@@ -21,7 +21,7 @@ const WalineComponent = (props) => {
}
}
React.useEffect(() => {
useEffect(() => {
if (!waline) {
waline = init({
...props,

View File

@@ -17,7 +17,7 @@ import { deepClone } from './utils'
export const siteConfig = (key, defaultVal = null, extendConfig = null) => {
let global = null
try {
const isClient = typeof window !== 'undefined';
const isClient = typeof window !== 'undefined'
// eslint-disable-next-line react-hooks/rules-of-hooks
global = isClient ? useGlobal() : {}
// eslint-disable-next-line react-hooks/rules-of-hooks
@@ -67,15 +67,19 @@ export const siteConfig = (key, defaultVal = null, extendConfig = null) => {
} else {
if (typeof val === 'string') {
if (val === 'true' || val === 'false') {
return JSON.parse(val);
return JSON.parse(val)
}
return val;
if (/^\d+$/.test(val)) {
// 如果是数字使用parseFloat或者parseInt将字符串转换为数字
return parseInt(val)
}
return val
} else {
try {
return JSON.parse(val);
return JSON.parse(val)
} catch (error) {
// 如果值是一个字符串但不是有效的 JSON 格式,直接返回字符串
return val;
return val
}
}
}

View File

@@ -1,17 +1,21 @@
import BLOG from '@/blog.config'
import { getDataFromCache, setDataToCache } from '@/lib/cache/cache_manager'
import { getAllCategories } from '@/lib/notion/getAllCategories'
import getAllPageIds from '@/lib/notion/getAllPageIds'
import { getAllTags } from '@/lib/notion/getAllTags'
import { getConfigMapFromConfigPage } from '@/lib/notion/getNotionConfig'
import getPageProperties from '@/lib/notion/getPageProperties'
import { getPostBlocks, getSingleBlock } from '@/lib/notion/getPostBlocks'
import { compressImage, mapImgUrl } from '@/lib/notion/mapImage'
import { deepClone } from '@/lib/utils'
import { idToUuid } from 'notion-utils'
import { deepClone } from '../utils'
import { getAllCategories } from './getAllCategories'
import getAllPageIds from './getAllPageIds'
import { getAllTags } from './getAllTags'
import getPageProperties from './getPageProperties'
import { compressImage, mapImgUrl } from './mapImage'
import { getConfigMapFromConfigPage } from './getNotionConfig'
export { getAllTags } from '../notion/getAllTags'
export { getPost } from '../notion/getNotionPost'
export { getPostBlocks } from '../notion/getPostBlocks'
/**
* 获取博客数据
* 获取博客数据; 基于Notion实现
* @param {*} pageId
* @param {*} from
* @param latestPostCount 截取最新文章数量
@@ -21,10 +25,7 @@ import { getConfigMapFromConfigPage } from './getNotionConfig'
* @returns
*
*/
export async function getGlobalData({
pageId = BLOG.NOTION_PAGE_ID,
from
}) {
export async function getGlobalData({ pageId = BLOG.NOTION_PAGE_ID, from }) {
// 从notion获取
const data = await getNotionPageData({ pageId, from })
const db = deepClone(data)
@@ -85,7 +86,9 @@ function cleanBlock(post) {
* @returns
*/
function getLatestPosts({ allPages, from, latestPostCount }) {
const allPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
const allPosts = allPages?.filter(
page => page.type === 'Post' && page.status === 'Published'
)
const latestPosts = Object.create(allPosts).sort((a, b) => {
const dateA = new Date(a?.lastEditedDate || a?.publishDate)
@@ -136,7 +139,13 @@ function getCustomNav({ allPages }) {
p.to = '/' + p.slug
}
}
customNav.push({ icon: p.icon || null, name: p.title, to: p.slug, target: '_blank', show: true })
customNav.push({
icon: p.icon || null,
name: p.title,
to: p.slug,
target: '_blank',
show: true
})
})
}
return customNav
@@ -148,7 +157,12 @@ function getCustomNav({ allPages }) {
* @returns
*/
function getCustomMenu({ collectionData }) {
const menuPages = collectionData.filter(post => post.status === 'Published' && (post?.type === BLOG.NOTION_PROPERTY_NAME.type_menu || post?.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu))
const menuPages = collectionData.filter(
post =>
post.status === 'Published' &&
(post?.type === BLOG.NOTION_PROPERTY_NAME.type_menu ||
post?.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu)
)
const menus = []
if (menuPages && menuPages.length > 0) {
menuPages.forEach(e => {
@@ -186,7 +200,9 @@ function getCustomMenu({ collectionData }) {
*/
function getTagOptions(schema) {
if (!schema) return {}
const tagSchema = Object.values(schema).find(e => e.name === BLOG.NOTION_PROPERTY_NAME.tags)
const tagSchema = Object.values(schema).find(
e => e.name === BLOG.NOTION_PROPERTY_NAME.tags
)
return tagSchema?.options || []
}
@@ -197,7 +213,9 @@ function getTagOptions(schema) {
*/
function getCategoryOptions(schema) {
if (!schema) return {}
const categorySchema = Object.values(schema).find(e => e.name === BLOG.NOTION_PROPERTY_NAME.category)
const categorySchema = Object.values(schema).find(
e => e.name === BLOG.NOTION_PROPERTY_NAME.category
)
return categorySchema?.options || []
}
@@ -207,21 +225,29 @@ function getCategoryOptions(schema) {
* @param from
* @returns {Promise<{title,description,pageCover,icon}>}
*/
function getSiteInfo({ collection, block }) {
function getSiteInfo({ collection, block, NOTION_CONFIG }) {
const title = collection?.name?.[0][0] || BLOG.TITLE
const description = collection?.description ? Object.assign(collection).description[0][0] : BLOG.DESCRIPTION
const pageCover = collection?.cover ? mapImgUrl(collection?.cover, block[idToUuid(BLOG.NOTION_PAGE_ID)]?.value) : BLOG.HOME_BANNER_IMAGE
let icon = collection?.icon ? mapImgUrl(collection?.icon, collection, 'collection') : BLOG.AVATAR
const description = collection?.description
? Object.assign(collection).description[0][0]
: BLOG.DESCRIPTION
const pageCover = collection?.cover
? mapImgUrl(collection?.cover, block[idToUuid(BLOG.NOTION_PAGE_ID)]?.value)
: BLOG.HOME_BANNER_IMAGE
// 用户头像压缩一下
icon = compressImage(icon)
let icon = compressImage(
collection?.icon
? mapImgUrl(collection?.icon, collection, 'collection')
: BLOG.AVATAR
)
// 站点网址
const link = NOTION_CONFIG?.LINK || BLOG.LINK
// 站点图标不能是emoji
// 站点图标不能是emoji
const emojiPattern = /\uD83C[\uDF00-\uDFFF]|\uD83D[\uDC00-\uDE4F]/g
if (!icon || emojiPattern.test(icon)) {
icon = BLOG.AVATAR
}
return { title, description, pageCover, icon }
return { title, description, pageCover, icon, link }
}
/**
@@ -232,7 +258,13 @@ function getSiteInfo({ collection, block }) {
*/
export function getNavPages({ allPages }) {
const allNavPages = allPages?.filter(post => {
return post && post?.slug && (!post?.slug?.startsWith('http')) && post?.type === 'Post' && post?.status === 'Published'
return (
post &&
post?.slug &&
!post?.slug?.startsWith('http') &&
post?.type === 'Post' &&
post?.status === 'Published'
)
})
return allNavPages.map(item => ({
@@ -244,7 +276,9 @@ export function getNavPages({ allPages }) {
summary: item.summary || null,
slug: item.slug,
pageIcon: item.pageIcon || '',
lastEditedDate: item.lastEditedDate
lastEditedDate: item.lastEditedDate,
publishDate: item.publishDate,
ext: item.ext || {}
}))
}
@@ -261,19 +295,26 @@ async function getNotice(post) {
}
// 没有数据时返回
const EmptyData = (pageId) => {
const EmptyData = pageId => {
const empty = {
notice: null,
siteInfo: getSiteInfo({}),
allPages: [{
id: 1,
title: `无法获取Notion数据请检查Notion_ID \n 当前 ${pageId}`,
summary: '访问文档获取帮助→ https://tangly1024.com/article/vercel-deploy-notion-next',
status: 'Published',
type: 'Post',
slug: '13a171332816461db29d50e9f575b00d',
date: { start_date: '2023-04-24', lastEditedDay: '2023-04-24', tagItems: [] }
}],
allPages: [
{
id: 1,
title: `无法获取Notion数据请检查Notion_ID \n 当前 ${pageId}`,
summary:
'访问文档获取帮助→ https://tangly1024.com/article/vercel-deploy-notion-next',
status: 'Published',
type: 'Post',
slug: '13a171332816461db29d50e9f575b00d',
date: {
start_date: '2023-04-24',
lastEditedDay: '2023-04-24',
tagItems: []
}
}
],
allNavPages: [],
collection: [],
collectionQuery: {},
@@ -309,13 +350,13 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
const rawMetadata = block[pageId]?.value
// Check Type Page-Database和Inline-Database
if (
rawMetadata?.type !== 'collection_view_page' && rawMetadata?.type !== 'collection_view'
rawMetadata?.type !== 'collection_view_page' &&
rawMetadata?.type !== 'collection_view'
) {
console.error(`pageId "${pageId}" is not a database`)
return EmptyData(pageId)
}
const collection = Object.values(pageRecordMap.collection)[0]?.value || {}
const siteInfo = getSiteInfo({ collection, block })
const collectionId = rawMetadata?.collection_id
const collectionQuery = pageRecordMap.collection_query
const collectionView = pageRecordMap.collection_view
@@ -324,9 +365,21 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
const viewIds = rawMetadata?.view_ids
const collectionData = []
const pageIds = getAllPageIds(collectionQuery, collectionId, collectionView, viewIds)
const pageIds = getAllPageIds(
collectionQuery,
collectionId,
collectionView,
viewIds
)
if (pageIds?.length === 0) {
console.error('获取到的文章列表为空请检查notion模板', collectionQuery, collection, collectionView, viewIds, pageRecordMap)
console.error(
'获取到的文章列表为空请检查notion模板',
collectionQuery,
collection,
collectionView,
viewIds,
pageRecordMap
)
} else {
// console.log('有效Page数量', pageIds?.length)
}
@@ -339,7 +392,14 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
// 如果找不到文章对应的block说明发生了溢出使用pageID再去请求
const pageBlock = await getSingleBlock(id, from)
if (pageBlock.block[id].value) {
const properties = (await getPageProperties(id, pageBlock.block[id].value, schema, null, getTagOptions(schema))) || null
const properties =
(await getPageProperties(
id,
pageBlock.block[id].value,
schema,
null,
getTagOptions(schema)
)) || null
if (properties) {
collectionData.push(properties)
}
@@ -347,7 +407,14 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
continue
}
const properties = (await getPageProperties(id, value, schema, null, getTagOptions(schema))) || null
const properties =
(await getPageProperties(
id,
value,
schema,
null,
getTagOptions(schema)
)) || null
if (properties) {
collectionData.push(properties)
}
@@ -356,19 +423,24 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
// 文章计数
let postCount = 0
// 站点配置优先读取配置表格否则读取blog.config.js 文件
const NOTION_CONFIG = (await getConfigMapFromConfigPage(collectionData)) || {}
const siteInfo = getSiteInfo({ collection, block })
// 查找所有的Post和Page
const allPages = collectionData.filter(post => {
if (post?.type === 'Post' && post.status === 'Published') {
postCount++
}
return post && post?.slug &&
(!post?.slug?.startsWith('http')) &&
return (
post &&
post?.slug &&
!post?.slug?.startsWith('http') &&
(post?.status === 'Invisible' || post?.status === 'Published')
)
})
// 站点配置优先读取配置表格否则读取blog.config.js 文件
const NOTION_CONFIG = await getConfigMapFromConfigPage(collectionData) || {}
// Sort by date
if (BLOG.POSTS_SORT_BY === 'date') {
allPages.sort((a, b) => {
@@ -376,13 +448,27 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
})
}
const notice = await getNotice(collectionData.filter(post => {
return post && post?.type && post?.type === 'Notice' && post.status === 'Published'
})?.[0])
const categoryOptions = getAllCategories({ allPages, categoryOptions: getCategoryOptions(schema) })
const notice = await getNotice(
collectionData.filter(post => {
return (
post &&
post?.type &&
post?.type === 'Notice' &&
post.status === 'Published'
)
})?.[0]
)
const categoryOptions = getAllCategories({
allPages,
categoryOptions: getCategoryOptions(schema)
})
const tagOptions = getAllTags({ allPages, tagOptions: getTagOptions(schema) })
// 旧的菜单
const customNav = getCustomNav({ allPages: collectionData.filter(post => post?.type === 'Page' && post.status === 'Published') })
const customNav = getCustomNav({
allPages: collectionData.filter(
post => post?.type === 'Page' && post.status === 'Published'
)
})
// 新的菜单
const customMenu = await getCustomMenu({ collectionData })
const latestPosts = getLatestPosts({ allPages, from, latestPostCount: 6 })

View File

@@ -16,7 +16,8 @@ export function GlobalContextProvider(props) {
const [lang, updateLang] = useState(NOTION_CONFIG?.LANG || LANG) // 默认语言
const [locale, updateLocale] = useState(generateLocaleDict(NOTION_CONFIG?.LANG || LANG)) // 默认语言
const [theme, setTheme] = useState(NOTION_CONFIG?.THEME || THEME) // 默认博客主题
const [isDarkMode, updateDarkMode] = useState(NOTION_CONFIG?.APPEARANCE || APPEARANCE === 'dark') // 默认深色模式
const defaultDarkMode = NOTION_CONFIG?.APPEARANCE || APPEARANCE
const [isDarkMode, updateDarkMode] = useState(defaultDarkMode === 'dark') // 默认深色模式
const [onLoading, setOnLoading] = useState(false) // 抓取文章数据
const router = useRouter()
@@ -57,13 +58,13 @@ export function GlobalContextProvider(props) {
}
useEffect(() => {
initDarkMode(updateDarkMode)
initDarkMode(updateDarkMode, defaultDarkMode)
initLocale(lang, locale, updateLang, updateLocale)
}, [])
// 加载进度条
useEffect(() => {
const handleStart = (url) => {
const handleStart = url => {
const { theme } = router.query
if (theme && !url.includes(`theme=${theme}`)) {
const newUrl = `${url}${url.includes('?') ? '&' : '?'}theme=${theme}`
@@ -86,27 +87,28 @@ export function GlobalContextProvider(props) {
}, [router])
return (
<GlobalContext.Provider value={{
fullWidth,
NOTION_CONFIG,
toggleDarkMode,
onLoading,
setOnLoading,
lang,
changeLang,
locale,
updateLocale,
isDarkMode,
updateDarkMode,
theme,
setTheme,
switchTheme,
siteInfo,
categoryOptions,
tagOptions
}}>
{children}
</GlobalContext.Provider>
<GlobalContext.Provider
value={{
fullWidth,
NOTION_CONFIG,
toggleDarkMode,
onLoading,
setOnLoading,
lang,
changeLang,
locale,
updateLocale,
isDarkMode,
updateDarkMode,
theme,
setTheme,
switchTheme,
siteInfo,
categoryOptions,
tagOptions
}}>
{children}
</GlobalContext.Provider>
)
}

View File

@@ -5,7 +5,7 @@ import zhTW from './lang/zh-TW'
import frFR from './lang/fr-FR'
import trTR from './lang/tr-TR'
import jaJP from './lang/ja-JP'
import { getQueryVariable, isBrowser, mergeDeep } from './utils'
import { getQueryVariable, isBrowser, mergeDeep } from '@/lib/utils'
/**
* 在这里配置所有支持的语言

View File

View File

@@ -1,2 +0,0 @@
export { getAllTags } from './notion/getAllTags'
export { getPostBlocks } from './notion/getPostBlocks'

View File

@@ -1,7 +1,7 @@
import BLOG from '@/blog.config'
import getAllPageIds from './getAllPageIds'
import getPageProperties from './getPageProperties'
import { getNotionPageData } from '@/lib/notion/getNotionData'
import { getNotionPageData } from '@/lib/db/getSiteData'
import { delCacheData } from '@/lib/cache/cache_manager'
/**

View File

@@ -1,4 +1,5 @@
import { isIterable } from '../utils'
import BLOG from '@/blog.config'
/**
* 获取所有文章的标签
@@ -24,14 +25,30 @@ export function getAllTags({ allPages, sliceCount = 0, tagOptions }) {
tagObj[tag] = 1
}
})
const list = []
const { IS_TAG_COLOR_DISTINGUISHED } = BLOG
if (isIterable(tagOptions)) {
tagOptions.forEach(c => {
const count = tagObj[c.value]
if (count) {
list.push({ id: c.id, name: c.value, color: c.color, count })
}
})
if (!IS_TAG_COLOR_DISTINGUISHED) {
// 如果不区分颜色, 那么不同颜色相同名称的tag当做同一种tag
const savedTagNames = new Set()
tagOptions.forEach(c => {
if (!savedTagNames.has(c.value)) {
const count = tagObj[c.value]
if (count) {
list.push({ id: c.id, name: c.value, color: c.color, count })
}
savedTagNames.add(c.value)
}
})
} else {
tagOptions.forEach(c => {
const count = tagObj[c.value]
if (count) {
list.push({ id: c.id, name: c.value, color: c.color, count })
}
})
}
}
// 按照数量排序

View File

@@ -1,6 +1,6 @@
import BLOG from '@/blog.config'
import { idToUuid } from 'notion-utils'
import formatDate from '../formatDate'
import formatDate from '../utils/formatDate'
import { getPostBlocks } from './getPostBlocks'
import { defaultMapImageUrl } from 'react-notion-x'
@@ -9,7 +9,7 @@ import { defaultMapImageUrl } from 'react-notion-x'
* @param {*} pageId
* @returns
*/
export async function getNotion(pageId) {
export async function getPost(pageId) {
const blockMap = await getPostBlocks(pageId, 'slug')
if (!blockMap) {
return null

View File

@@ -1,7 +1,7 @@
import { getTextContent, getDateValue } from 'notion-utils'
import { NotionAPI } from 'notion-client'
import BLOG from '@/blog.config'
import formatDate from '../formatDate'
import { NotionAPI } from 'notion-client'
import { getDateValue, getTextContent } from 'notion-utils'
import formatDate from '../utils/formatDate'
// import { createHash } from 'crypto'
import md5 from 'js-md5'
import { mapImgUrl } from './mapImage'
@@ -49,8 +49,7 @@ export default async function getPageProperties(id, value, schema, authToken, ta
if (rawUsers[i][0][1]) {
const userId = rawUsers[i][0]
const res = await api.getUsers(userId)
const resValue =
res?.recordMapWithRoles?.notion_user?.[userId[1]]?.value
const resValue = res?.recordMapWithRoles?.notion_user?.[userId[1]]?.value
const user = {
id: resValue?.id,
first_name: resValue?.given_name,
@@ -93,16 +92,17 @@ export default async function getPageProperties(id, value, schema, authToken, ta
properties.pageIcon = mapImgUrl(value?.format?.page_icon, value) ?? ''
properties.pageCover = mapImgUrl(value?.format?.page_cover, value) ?? ''
properties.pageCoverThumbnail = mapImgUrl(value?.format?.page_cover, value, 'block', 'pageCoverThumbnail') ?? ''
properties.ext = converToJSON(properties?.ext)
properties.content = value.content ?? []
properties.tagItems = properties?.tags?.map(tag => {
return { name: tag, color: tagOptions?.find(t => t.value === tag)?.color || 'gray' }
}) || []
properties.tagItems =
properties?.tags?.map(tag => {
return { name: tag, color: tagOptions?.find(t => t.value === tag)?.color || 'gray' }
}) || []
delete properties.content
// 处理URL
if (properties.type === 'Post') {
properties.slug = (BLOG.POST_URL_PREFIX) ? generateCustomizeUrl(properties) : (properties.slug ?? properties.id)
properties.slug = BLOG.POST_URL_PREFIX ? generateCustomizeUrl(properties) : properties.slug ?? properties.id
} else if (properties.type === 'Page') {
properties.slug = properties.slug ?? properties.id
} else if (properties.type === 'Menu' || properties.type === 'SubMenu') {
@@ -122,6 +122,24 @@ export default async function getPageProperties(id, value, schema, authToken, ta
return properties
}
/**
* 字符串转json
* @param {*} str
* @returns
*/
function converToJSON(str) {
if (!str) {
return {}
}
// 使用正则表达式去除空格和换行符
try {
return JSON.parse(str.replace(/\s/g, ''))
} catch (error) {
console.warn('无效JSON', str)
return {}
}
}
/**
* 映射用户自定义表头
*/
@@ -164,7 +182,7 @@ function generateCustomizeUrl(postProperties) {
const formatPostCreatedDate = new Date(postProperties?.publishDay)
fullPrefix += String(formatPostCreatedDate.getUTCDate()).padStart(2, 0)
} else if (pattern === '%slug%') {
fullPrefix += (postProperties.slug ?? postProperties.id)
fullPrefix += postProperties.slug ?? postProperties.id
} else if (!pattern.includes('%')) {
fullPrefix += pattern
} else {
@@ -180,5 +198,5 @@ function generateCustomizeUrl(postProperties) {
if (fullPrefix.endsWith('/')) {
fullPrefix = fullPrefix.substring(0, fullPrefix.length - 1) // 去掉尾部部的"/"
}
return `${fullPrefix}/${(postProperties.slug ?? postProperties.id)}`
return `${fullPrefix}/${postProperties.slug ?? postProperties.id}`
}

View File

@@ -27,6 +27,12 @@ export async function getPostBlocks(id, from, slice) {
return pageBlock
}
/**
* 针对在getDataBaseInfoByNotionAPI=>getPostBlocks中获取不到的溢出的block-id用此方法另外抓取
* @param {*} id
* @param {*} from
* @returns
*/
export async function getSingleBlock(id, from) {
const cacheKey = 'single_block_' + id
let pageBlock = await getDataFromCache(cacheKey)

View File

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

View File

@@ -2,7 +2,7 @@ import fs from 'fs'
import { Feed } from 'feed'
import BLOG from '@/blog.config'
import ReactDOMServer from 'react-dom/server'
import { getPostBlocks } from './notion'
import { getPostBlocks } from '@/lib/db/getSiteData'
import NotionPage from '@/components/NotionPage'
/**

View File

@@ -7,6 +7,22 @@ import { memo } from 'react'
*/
export const isBrowser = typeof window !== 'undefined'
/**
* 打乱数组
* @param {*} array
* @returns
*/
export const shuffleArray = array => {
if (!array) {
return []
}
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[array[i], array[j]] = [array[j], array[i]]
}
return array
}
/**
* google机器人
* @returns
@@ -16,7 +32,7 @@ export const isSearchEngineBot = () => {
return false
}
// 获取用户代理字符串
const userAgent = navigator.userAgent;
const userAgent = navigator.userAgent
// 使用正则表达式检测是否包含搜索引擎爬虫关键字
return /Googlebot|bingbot|Baidu/.test(userAgent)
}
@@ -24,8 +40,8 @@ export const isSearchEngineBot = () => {
/**
* 组件持久化
*/
export const memorize = (Component) => {
const MemoizedComponent = (props) => {
export const memorize = Component => {
const MemoizedComponent = props => {
return <Component {...props} />
}
return memo(MemoizedComponent)
@@ -34,26 +50,26 @@ export const memorize = (Component) => {
// 转换外链
export function sliceUrlFromHttp(str) {
// 检查字符串是否包含http
if (str.includes('http:') || str.includes('https:')) {
if (str?.includes('http:') || str?.includes('https:')) {
// 如果包含找到http的位置
const index = str.indexOf('http');
const index = str?.indexOf('http')
// 返回http之后的部分
return str.slice(index, str.length);
return str.slice(index, str.length)
} else {
// 如果不包含,返回原字符串
return str;
return str
}
}
// 检查是否外链
export function checkContainHttp(str) {
// 检查字符串是否包含http
if (str.includes('http:') || str.includes('https:')) {
if (str?.includes('http:') || str?.includes('https:')) {
// 如果包含找到http的位置
return str.indexOf('http') > -1
return str?.indexOf('http') > -1
} else {
// 不包含
return false;
return false
}
}
@@ -65,7 +81,10 @@ export function checkContainHttp(str) {
*/
export function loadExternalResource(url, type) {
// 检查是否已存在
const elements = type === 'js' ? document.querySelectorAll(`[src='${url}']`) : document.querySelectorAll(`[href='${url}']`)
const elements =
type === 'js'
? document.querySelectorAll(`[src='${url}']`)
: document.querySelectorAll(`[href='${url}']`)
return new Promise((resolve, reject) => {
if (elements.length > 0 || !url) {
@@ -112,9 +131,11 @@ export function getQueryVariable(key) {
const vars = query.split('&')
for (let i = 0; i < vars.length; i++) {
const pair = vars[i].split('=')
if (pair[0] === key) { return pair[1] }
if (pair[0] === key) {
return pair[1]
}
}
return (false)
return false
}
/**
* 获取 URL 中指定参数的值
@@ -124,9 +145,9 @@ export function getQueryVariable(key) {
*/
export function getQueryParam(url, param) {
// 移除哈希部分
const urlWithoutHash = url.split('#')[0];
const searchParams = new URLSearchParams(urlWithoutHash.split('?')[1]);
return searchParams.get(param);
const urlWithoutHash = url.split('#')[0]
const searchParams = new URLSearchParams(urlWithoutHash.split('?')[1])
return searchParams.get(param)
}
/**
@@ -157,7 +178,7 @@ export function mergeDeep(target, ...sources) {
* @returns {boolean}
*/
export function isObject(item) {
return (item && typeof item === 'object' && !Array.isArray(item))
return item && typeof item === 'object' && !Array.isArray(item)
}
/**
@@ -210,10 +231,7 @@ export const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
* @returns {*}
*/
export const getListByPage = function (list, pageIndex, pageSize) {
return list.slice(
0,
pageIndex * pageSize
)
return list.slice(0, pageIndex * pageSize)
}
/**
@@ -230,7 +248,7 @@ export const isMobile = () => {
// isMobile = true
// }
if (!isMobile && (/Mobi|Android|iPhone/i.test(navigator.userAgent))) {
if (!isMobile && /Mobi|Android|iPhone/i.test(navigator.userAgent)) {
isMobile = true
}
@@ -249,41 +267,41 @@ export const isMobile = () => {
* 扫描页面上的所有文本节点将url格式的文本转为可点击链接
* @param {*} node
*/
export const scanAndConvertToLinks = (node) => {
export const scanAndConvertToLinks = node => {
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent;
const urlRegex = /https?:\/\/[^\s]+/g;
let lastIndex = 0;
let match;
const text = node.textContent
const urlRegex = /https?:\/\/[^\s]+/g
let lastIndex = 0
let match
const newNode = document.createElement('span');
const newNode = document.createElement('span')
while ((match = urlRegex.exec(text)) !== null) {
const beforeText = text.substring(lastIndex, match.index);
const url = match[0];
const beforeText = text.substring(lastIndex, match.index)
const url = match[0]
if (beforeText) {
newNode.appendChild(document.createTextNode(beforeText));
newNode.appendChild(document.createTextNode(beforeText))
}
const link = document.createElement('a');
link.href = url;
const link = document.createElement('a')
link.href = url
link.target = '_blank'
link.textContent = url;
link.textContent = url
newNode.appendChild(link);
newNode.appendChild(link)
lastIndex = urlRegex.lastIndex;
lastIndex = urlRegex.lastIndex
}
if (lastIndex < text.length) {
newNode.appendChild(document.createTextNode(text.substring(lastIndex)));
newNode.appendChild(document.createTextNode(text.substring(lastIndex)))
}
node.parentNode.replaceChild(newNode, node);
node.parentNode.replaceChild(newNode, node)
} else if (node.nodeType === Node.ELEMENT_NODE) {
for (const childNode of node.childNodes) {
scanAndConvertToLinks(childNode);
scanAndConvertToLinks(childNode)
}
}
}

View File

@@ -96,19 +96,27 @@ module.exports = withBundleAnalyzer({
if (!isServer) {
console.log('[加载主题]', path.resolve(__dirname, 'themes', THEME))
}
config.resolve.alias['@theme-components'] = path.resolve(__dirname, 'themes', THEME)
config.resolve.alias['@theme-components'] = path.resolve(
__dirname,
'themes',
THEME
)
return config
},
experimental: {
scrollRestoration: true
},
exportPathMap: async function (defaultPathMap, { dev, dir, outDir, distDir, buildId }) {
// 导出时 忽略/pages/sitemap.xml.js 否则报错getServerSideProps
exportPathMap: async function (
defaultPathMap,
{ dev, dir, outDir, distDir, buildId }
) {
// export 静态导出时 忽略/pages/sitemap.xml.js 否则和getServerSideProps这个动态文件冲突
const pages = { ...defaultPathMap }
delete pages['/sitemap.xml']
return pages
},
publicRuntimeConfig: { // 这里的配置既可以服务端获取到,也可以在浏览器端获取到
publicRuntimeConfig: {
// 这里的配置既可以服务端获取到,也可以在浏览器端获取到
NODE_ENV_API: process.env.NODE_ENV_API || 'prod',
THEMES: themes
}

View File

@@ -1,6 +1,6 @@
{
"name": "notion-next",
"version": "4.3.1",
"version": "4.4.2",
"homepage": "https://github.com/tangly1024/NotionNext.git",
"license": "MIT",
"repository": {
@@ -57,9 +57,11 @@
"cross-env": "^7.0.3",
"eslint": "^7.26.0",
"eslint-config-next": "^13.1.1",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.23.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.6.0",
@@ -76,4 +78,4 @@
"url": "https://github.com/tangly/NotionNext/issues",
"email": "tlyong1992@hotmail.com"
}
}
}

View File

@@ -1,4 +1,4 @@
import { getGlobalData } from '@/lib/notion/getNotionData'
import { getGlobalData } from '@/lib/db/getSiteData'
import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config'

View File

@@ -1,11 +1,10 @@
import BLOG from '@/blog.config'
import { getPostBlocks } from '@/lib/notion'
import { getGlobalData } from '@/lib/notion/getNotionData'
import { idToUuid } from 'notion-utils'
import { getNotion } from '@/lib/notion/getNotion'
import Slug, { getRecommendPost } from '..'
import { uploadDataToAlgolia } from '@/lib/algolia'
import { siteConfig } from '@/lib/config'
import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData'
import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
import { checkContainHttp } from '@/lib/utils'
import { idToUuid } from 'notion-utils'
import Slug, { getRecommendPost } from '..'
/**
* 根据notion的slug访问页面
@@ -14,7 +13,7 @@ import { checkContainHttp } from '@/lib/utils'
* @returns
*/
const PrefixSlug = props => {
return <Slug {...props}/>
return <Slug {...props} />
}
/**
@@ -33,8 +32,11 @@ export async function getStaticPaths() {
const { allPages } = await getGlobalData({ from })
return {
paths: allPages?.filter(row => checkSlug(row))
.map(row => ({ params: { prefix: row.slug.split('/')[0], slug: row.slug.split('/')[1], suffix: row.slug.split('/').slice(1) } })),
paths: allPages
?.filter(row => checkSlug(row))
.map(row => ({
params: { prefix: row.slug.split('/')[0], slug: row.slug.split('/')[1], suffix: row.slug.split('/').slice(1) }
})),
fallback: true
}
}
@@ -54,15 +56,15 @@ export async function getStaticProps({ params: { prefix, slug, suffix } }) {
const from = `slug-props-${fullSlug}`
const props = await getGlobalData({ from })
// 在列表内查找文章
props.post = props?.allPages?.find((p) => {
return (p.type.indexOf('Menu') < 0) && (p.slug === fullSlug || p.id === idToUuid(fullSlug))
props.post = props?.allPages?.find(p => {
return p.type.indexOf('Menu') < 0 && (p.slug === fullSlug || p.id === idToUuid(fullSlug))
})
// 处理非列表内文章的内信息
if (!props?.post) {
const pageId = fullSlug.slice(-1)[0]
if (pageId.length >= 32) {
const post = await getNotion(pageId)
const post = await getPost(pageId)
props.post = post
}
}
@@ -88,7 +90,7 @@ export async function getStaticProps({ params: { prefix, slug, suffix } }) {
const index = allPosts.indexOf(props.post)
props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
props.next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0]
props.recommendPosts = getRecommendPost(props.post, allPosts, BLOG.POST_RECOMMEND_COUNT)
props.recommendPosts = getRecommendPost(props.post, allPosts, siteConfig('POST_RECOMMEND_COUNT'))
} else {
props.prev = null
props.next = null

View File

@@ -1,11 +1,10 @@
import BLOG from '@/blog.config'
import { getPostBlocks } from '@/lib/notion'
import { getGlobalData } from '@/lib/notion/getNotionData'
import { idToUuid } from 'notion-utils'
import { getNotion } from '@/lib/notion/getNotion'
import Slug, { getRecommendPost } from '..'
import { uploadDataToAlgolia } from '@/lib/algolia'
import { siteConfig } from '@/lib/config'
import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData'
import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
import { checkContainHttp } from '@/lib/utils'
import { idToUuid } from 'notion-utils'
import Slug, { getRecommendPost } from '..'
/**
* 根据notion的slug访问页面
@@ -14,7 +13,7 @@ import { checkContainHttp } from '@/lib/utils'
* @returns
*/
const PrefixSlug = props => {
return <Slug {...props}/>
return <Slug {...props} />
}
export async function getStaticPaths() {
@@ -27,7 +26,8 @@ export async function getStaticPaths() {
const from = 'slug-paths'
const { allPages } = await getGlobalData({ from })
const paths = allPages?.filter(row => checkSlug(row))
const paths = allPages
?.filter(row => checkSlug(row))
.map(row => ({ params: { prefix: row.slug.split('/')[0], slug: row.slug.split('/')[1] } }))
return {
paths: paths,
@@ -45,15 +45,15 @@ export async function getStaticProps({ params: { prefix, slug } }) {
const from = `slug-props-${fullSlug}`
const props = await getGlobalData({ from })
// 在列表内查找文章
props.post = props?.allPages?.find((p) => {
return (p.type.indexOf('Menu') < 0) && (p.slug === fullSlug || p.id === idToUuid(fullSlug))
props.post = props?.allPages?.find(p => {
return p.type.indexOf('Menu') < 0 && (p.slug === fullSlug || p.id === idToUuid(fullSlug))
})
// 处理非列表内文章的内信息
if (!props?.post) {
const pageId = slug.slice(-1)[0]
if (pageId.length >= 32) {
const post = await getNotion(pageId)
const post = await getPost(pageId)
props.post = post
}
}
@@ -79,7 +79,7 @@ export async function getStaticProps({ params: { prefix, slug } }) {
const index = allPosts.indexOf(props.post)
props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
props.next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0]
props.recommendPosts = getRecommendPost(props.post, allPosts, BLOG.POST_RECOMMEND_COUNT)
props.recommendPosts = getRecommendPost(props.post, allPosts, siteConfig('POST_RECOMMEND_COUNT'))
} else {
props.prev = null
props.next = null

View File

@@ -1,16 +1,14 @@
import BLOG from '@/blog.config'
import { getPostBlocks } from '@/lib/notion'
import { getGlobalData } from '@/lib/notion/getNotionData'
import { useEffect, useState } from 'react'
import { idToUuid } from 'notion-utils'
import { useRouter } from 'next/router'
import { getNotion } from '@/lib/notion/getNotion'
import { siteConfig } from '@/lib/config'
import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData'
import { getPageTableOfContents } from '@/lib/notion/getPageTableOfContents'
import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
import { checkContainHttp } from '@/lib/utils'
import { getLayoutByTheme } from '@/themes/theme'
import md5 from 'js-md5'
import { checkContainHttp } from '@/lib/utils'
import { uploadDataToAlgolia } from '@/lib/algolia'
import { siteConfig } from '@/lib/config'
import { useRouter } from 'next/router'
import { idToUuid } from 'notion-utils'
import { useEffect, useState } from 'react'
/**
* 根据notion的slug访问页面
@@ -27,7 +25,7 @@ const Slug = props => {
/**
* 验证文章密码
* @param {*} result
*/
*/
const validPassword = passInput => {
const encrypt = md5(post.slug + passInput)
if (passInput && encrypt === post.password) {
@@ -45,7 +43,9 @@ const Slug = props => {
} else {
setLock(false)
if (!lock && post?.blockMap?.block) {
post.content = Object.keys(post.blockMap.block).filter(key => post.blockMap.block[key]?.value?.parent_id === post.id)
post.content = Object.keys(post.blockMap.block).filter(
key => post.blockMap.block[key]?.value?.parent_id === post.id
)
post.toc = getPageTableOfContents(post, post.blockMap)
}
}
@@ -67,8 +67,7 @@ export async function getStaticPaths() {
const from = 'slug-paths'
const { allPages } = await getGlobalData({ from })
const paths = allPages?.filter(row => checkSlug(row))
.map(row => ({ params: { prefix: row.slug } }))
const paths = allPages?.filter(row => checkSlug(row)).map(row => ({ params: { prefix: row.slug } }))
return {
paths: paths,
fallback: true
@@ -85,15 +84,15 @@ export async function getStaticProps({ params: { prefix } }) {
const from = `slug-props-${fullSlug}`
const props = await getGlobalData({ from })
// 在列表内查找文章
props.post = props?.allPages?.find((p) => {
return (p.type.indexOf('Menu') < 0) && (p.slug === fullSlug || p.id === idToUuid(fullSlug))
props.post = props?.allPages?.find(p => {
return p.type.indexOf('Menu') < 0 && (p.slug === fullSlug || p.id === idToUuid(fullSlug))
})
// 处理非列表内文章的内信息
if (!props?.post) {
const pageId = prefix
if (pageId.length >= 32) {
const post = await getNotion(pageId)
const post = await getPost(pageId)
props.post = post
}
}
@@ -119,7 +118,7 @@ export async function getStaticProps({ params: { prefix } }) {
const index = allPosts.indexOf(props.post)
props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
props.next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0]
props.recommendPosts = getRecommendPost(props.post, allPosts, BLOG.POST_RECOMMEND_COUNT)
props.recommendPosts = getRecommendPost(props.post, allPosts, siteConfig('POST_RECOMMEND_COUNT'))
} else {
props.prev = null
props.next = null
@@ -173,7 +172,7 @@ function checkSlug(row) {
if (slug.startsWith('/')) {
slug = slug.substring(1)
}
return ((slug.match(/\//g) || []).length === 0 && !checkContainHttp(slug)) && row.type.indexOf('Menu') < 0
return (slug.match(/\//g) || []).length === 0 && !checkContainHttp(slug) && row.type.indexOf('Menu') < 0
}
export default Slug

View File

@@ -1,4 +1,4 @@
import '@/styles/animate.css' // @see https://animate.style/
// import '@/styles/animate.css' // @see https://animate.style/
import '@/styles/globals.css'
import '@/styles/nprogress.css'
import '@/styles/utility-patterns.css'

View File

@@ -1,4 +1,4 @@
import subscribeToMailchimpApi from '@/lib/mailchimp'
import subscribeToMailchimpApi from '@/lib/plugins/mailchimp'
/**
* 接受邮件订阅

View File

@@ -1,10 +1,10 @@
import { getGlobalData } from '@/lib/notion/getNotionData'
import { getGlobalData } from '@/lib/db/getSiteData'
import { useEffect } from 'react'
import BLOG from '@/blog.config'
import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'
import { isBrowser } from '@/lib/utils'
import { formatDateFmt } from '@/lib/formatDate'
import { formatDateFmt } from '@/lib/utils/formatDate'
import { siteConfig } from '@/lib/config'
const ArchiveIndex = props => {

View File

@@ -1,9 +1,8 @@
import { getGlobalData } from '@/lib/notion/getNotionData'
import React from 'react'
import BLOG from '@/blog.config'
import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
* 分类页
@@ -28,10 +27,10 @@ export async function getStaticProps({ params: { category } }) {
// 处理文章页数
props.postCount = props.posts.length
// 处理分页
if (BLOG.POST_LIST_STYLE === 'scroll') {
if (siteConfig('POST_LIST_STYLE') === 'scroll') {
// 滚动列表 给前端返回所有数据
} else if (BLOG.POST_LIST_STYLE === 'page') {
props.posts = props.posts?.slice(0, BLOG.POSTS_PER_PAGE)
} else if (siteConfig('POST_LIST_STYLE') === 'page') {
props.posts = props.posts?.slice(0, siteConfig('POSTS_PER_PAGE'))
}
delete props.allPages

View File

@@ -1,9 +1,8 @@
import { getGlobalData } from '@/lib/notion/getNotionData'
import React from 'react'
import BLOG from '@/blog.config'
import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
* 分类页
@@ -23,11 +22,13 @@ export async function getStaticProps({ params: { category, page } }) {
let props = await getGlobalData({ from })
// 过滤状态类型
props.posts = props.allPages?.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post.category && post.category.includes(category))
props.posts = props.allPages
?.filter(page => page.type === 'Post' && page.status === 'Published')
.filter(post => post && post.category && post.category.includes(category))
// 处理文章页数
props.postCount = props.posts.length
// 处理分页
props.posts = props.posts.slice(BLOG.POSTS_PER_PAGE * (page - 1), BLOG.POSTS_PER_PAGE * page)
props.posts = props.posts.slice(siteConfig('POSTS_PER_PAGE') * (page - 1), siteConfig('POSTS_PER_PAGE') * page)
delete props.allPages
props.page = page
@@ -47,10 +48,12 @@ export async function getStaticPaths() {
categoryOptions?.forEach(category => {
// 过滤状态类型
const categoryPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post.category && post.category.includes(category.name))
const categoryPosts = allPages
?.filter(page => page.type === 'Post' && page.status === 'Published')
.filter(post => post && post.category && post.category.includes(category.name))
// 处理文章页数
const postCount = categoryPosts.length
const totalPages = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
const totalPages = Math.ceil(postCount / siteConfig('POSTS_PER_PAGE'))
if (totalPages > 1) {
for (let i = 1; i <= totalPages; i++) {
paths.push({ params: { category: category.name, page: '' + i } })

View File

@@ -1,4 +1,4 @@
import { getGlobalData } from '@/lib/notion/getNotionData'
import { getGlobalData } from '@/lib/db/getSiteData'
import React from 'react'
import BLOG from '@/blog.config'
import { useRouter } from 'next/router'

View File

@@ -1,10 +1,9 @@
import BLOG from '@/blog.config'
import { getPostBlocks } from '@/lib/notion'
import { getGlobalData } from '@/lib/notion/getNotionData'
import { generateRss } from '@/lib/rss'
import { generateRobotsTxt } from '@/lib/robots.txt'
import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config'
import { getGlobalData, getPostBlocks } from '@/lib/db/getSiteData'
import { generateRobotsTxt } from '@/lib/robots.txt'
import { generateRss } from '@/lib/rss'
import { getLayoutByTheme } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
@@ -14,7 +13,10 @@ import { useRouter } from 'next/router'
*/
const Index = props => {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() })
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
}
@@ -26,23 +28,29 @@ export async function getStaticProps() {
const from = 'index'
const props = await getGlobalData({ from })
props.posts = props.allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
props.posts = props.allPages?.filter(
page => page.type === 'Post' && page.status === 'Published'
)
// 处理分页
if (BLOG.POST_LIST_STYLE === 'scroll') {
if (siteConfig('POST_LIST_STYLE') === 'scroll') {
// 滚动列表默认给前端返回所有数据
} else if (BLOG.POST_LIST_STYLE === 'page') {
props.posts = props.posts?.slice(0, BLOG.POSTS_PER_PAGE)
} else if (siteConfig('POST_LIST_STYLE') === 'page') {
props.posts = props.posts?.slice(0, siteConfig('POSTS_PER_PAGE'))
}
// 预览文章内容
if (BLOG.POST_LIST_PREVIEW === 'true') {
if (siteConfig('POST_LIST_PREVIEW')) {
for (const i in props.posts) {
const post = props.posts[i]
if (post.password && post.password !== '') {
continue
}
post.blockMap = await getPostBlocks(post.id, 'slug', BLOG.POST_PREVIEW_LINES)
post.blockMap = await getPostBlocks(
post.id,
'slug',
siteConfig('POST_PREVIEW_LINES')
)
}
}

View File

@@ -1,9 +1,8 @@
import BLOG from '@/blog.config'
import { getPostBlocks } from '@/lib/notion'
import { getGlobalData } from '@/lib/notion/getNotionData'
import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config'
import { getGlobalData, getPostBlocks } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
* 文章列表分页
@@ -20,7 +19,7 @@ const Page = props => {
export async function getStaticPaths() {
const from = 'page-paths'
const { postCount } = await getGlobalData({ from })
const totalPages = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
const totalPages = Math.ceil(postCount / siteConfig('POSTS_PER_PAGE'))
return {
// remove first page, we 're not gonna handle that.
paths: Array.from({ length: totalPages - 1 }, (_, i) => ({
@@ -36,17 +35,17 @@ export async function getStaticProps({ params: { page } }) {
const { allPages } = props
const allPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
// 处理分页
props.posts = allPosts.slice(BLOG.POSTS_PER_PAGE * (page - 1), BLOG.POSTS_PER_PAGE * page)
props.posts = allPosts.slice(siteConfig('POSTS_PER_PAGE') * (page - 1), siteConfig('POSTS_PER_PAGE') * page)
props.page = page
// 处理预览
if (BLOG.POST_LIST_PREVIEW === 'true') {
if (siteConfig('POST_LIST_PREVIEW')) {
for (const i in props.posts) {
const post = props.posts[i]
if (post.password && post.password !== '') {
continue
}
post.blockMap = await getPostBlocks(post.id, 'slug', BLOG.POST_PREVIEW_LINES)
post.blockMap = await getPostBlocks(post.id, 'slug', siteConfig('POST_PREVIEW_LINES'))
}
}

View File

@@ -1,9 +1,9 @@
import { getGlobalData } from '@/lib/notion/getNotionData'
import { getDataFromCache } from '@/lib/cache/cache_manager'
import BLOG from '@/blog.config'
import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'
import { getDataFromCache } from '@/lib/cache/cache_manager'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { useRouter } from 'next/router'
const Index = props => {
// 根据页面路径加载不同Layout文件
@@ -27,10 +27,10 @@ export async function getStaticProps({ params: { keyword } }) {
props.posts = await filterByMemCache(allPosts, keyword)
props.postCount = props.posts.length
// 处理分页
if (BLOG.POST_LIST_STYLE === 'scroll') {
if (siteConfig('POST_LIST_STYLE') === 'scroll') {
// 滚动列表 给前端返回所有数据
} else if (BLOG.POST_LIST_STYLE === 'page') {
props.posts = props.posts?.slice(0, BLOG.POSTS_PER_PAGE)
} else if (siteConfig('POST_LIST_STYLE') === 'page') {
props.posts = props.posts?.slice(0, siteConfig('POSTS_PER_PAGE'))
}
props.keyword = keyword
return {
@@ -87,8 +87,7 @@ function getTextContent(textArray) {
* @param {*} obj
* @returns
*/
const isIterable = obj =>
obj != null && typeof obj[Symbol.iterator] === 'function'
const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function'
/**
* 在内存缓存中进行全文索引

View File

@@ -1,9 +1,9 @@
import { getGlobalData } from '@/lib/notion/getNotionData'
import { getDataFromCache } from '@/lib/cache/cache_manager'
import BLOG from '@/blog.config'
import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'
import { getDataFromCache } from '@/lib/cache/cache_manager'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { useRouter } from 'next/router'
const Index = props => {
const { keyword } = props
@@ -29,7 +29,7 @@ export async function getStaticProps({ params: { keyword, page } }) {
props.posts = await filterByMemCache(allPosts, keyword)
props.postCount = props.posts.length
// 处理分页
props.posts = props.posts.slice(BLOG.POSTS_PER_PAGE * (page - 1), BLOG.POSTS_PER_PAGE * page)
props.posts = props.posts.slice(siteConfig('POSTS_PER_PAGE') * (page - 1), siteConfig('POSTS_PER_PAGE') * page)
props.keyword = keyword
props.page = page
delete props.allPages
@@ -87,8 +87,7 @@ function getTextContent(textArray) {
* @param {*} obj
* @returns
*/
const isIterable = obj =>
obj != null && typeof obj[Symbol.iterator] === 'function'
const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function'
/**
* 在内存缓存中进行全文索引

View File

@@ -1,4 +1,4 @@
import { getGlobalData } from '@/lib/notion/getNotionData'
import { getGlobalData } from '@/lib/db/getSiteData'
import { useRouter } from 'next/router'
import BLOG from '@/blog.config'
import { getLayoutByTheme } from '@/themes/theme'

View File

@@ -1,5 +1,5 @@
import BLOG from '@/blog.config'
import { getGlobalData } from '@/lib/notion/getNotionData'
import { getGlobalData } from '@/lib/db/getSiteData'
import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config'

View File

@@ -1,5 +1,5 @@
import BLOG from '@/blog.config'
import { getGlobalData } from '@/lib/notion/getNotionData'
import { getGlobalData } from '@/lib/db/getSiteData'
import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config'

View File

@@ -1,6 +1,6 @@
// pages/sitemap.xml.js
import { getServerSideSitemap } from 'next-sitemap'
import { getGlobalData } from '@/lib/notion/getNotionData'
import { getGlobalData } from '@/lib/db/getSiteData'
import BLOG from '@/blog.config'
export const getServerSideProps = async (ctx) => {

View File

@@ -1,8 +1,8 @@
import { getGlobalData } from '@/lib/notion/getNotionData'
import BLOG from '@/blog.config'
import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
* 标签下的文章列表
@@ -21,16 +21,18 @@ export async function getStaticProps({ params: { tag } }) {
const props = await getGlobalData({ from })
// 过滤状态
props.posts = props.allPages?.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post?.tags && post?.tags.includes(tag))
props.posts = props.allPages
?.filter(page => page.type === 'Post' && page.status === 'Published')
.filter(post => post && post?.tags && post?.tags.includes(tag))
// 处理文章页数
props.postCount = props.posts.length
// 处理分页
if (BLOG.POST_LIST_STYLE === 'scroll') {
if (siteConfig('POST_LIST_STYLE') === 'scroll') {
// 滚动列表 给前端返回所有数据
} else if (BLOG.POST_LIST_STYLE === 'page') {
props.posts = props.posts?.slice(0, BLOG.POSTS_PER_PAGE)
} else if (siteConfig('POST_LIST_STYLE') === 'page') {
props.posts = props.posts?.slice(0, siteConfig('POSTS_PER_PAGE'))
}
props.tag = tag

View File

@@ -1,8 +1,8 @@
import { getGlobalData } from '@/lib/notion/getNotionData'
import BLOG from '@/blog.config'
import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { useRouter } from 'next/router'
const Tag = props => {
// 根据页面路径加载不同Layout文件
@@ -14,11 +14,13 @@ export async function getStaticProps({ params: { tag, page } }) {
const from = 'tag-page-props'
const props = await getGlobalData({ from })
// 过滤状态、标签
props.posts = props.allPages?.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post?.tags && post?.tags.includes(tag))
props.posts = props.allPages
?.filter(page => page.type === 'Post' && page.status === 'Published')
.filter(post => post && post?.tags && post?.tags.includes(tag))
// 处理文章数
props.postCount = props.posts.length
// 处理分页
props.posts = props.posts.slice(BLOG.POSTS_PER_PAGE * (page - 1), BLOG.POSTS_PER_PAGE * page)
props.posts = props.posts.slice(siteConfig('POSTS_PER_PAGE') * (page - 1), siteConfig('POSTS_PER_PAGE') * page)
props.tag = tag
props.page = page
@@ -35,10 +37,12 @@ export async function getStaticPaths() {
const paths = []
tagOptions?.forEach(tag => {
// 过滤状态类型
const tagPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post?.tags && post?.tags.includes(tag.name))
const tagPosts = allPages
?.filter(page => page.type === 'Post' && page.status === 'Published')
.filter(post => post && post?.tags && post?.tags.includes(tag.name))
// 处理文章页数
const postCount = tagPosts.length
const totalPages = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
const totalPages = Math.ceil(postCount / siteConfig('POSTS_PER_PAGE'))
if (totalPages > 1) {
for (let i = 1; i <= totalPages; i++) {
paths.push({ params: { tag: tag.name, page: '' + i } })

View File

@@ -1,4 +1,4 @@
import { getGlobalData } from '@/lib/notion/getNotionData'
import { getGlobalData } from '@/lib/db/getSiteData'
import BLOG from '@/blog.config'
import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme'

View File

@@ -2,7 +2,7 @@
/*!
* animate.css -https://daneden.github.io/animate.css/
* Version - 3.7.2
* Version - 3.7.2 适配wowjs
* Licensed under the MIT license - http://opensource.org/licenses/MIT
*
* Copyright (c) 2019 Daniel Eden

60
public/dplayer.htm Normal file
View File

@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DPlayer Video Player</title>
<!-- 引入 DPlayer 样式文件 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/dplayer/dist/DPlayer.min.css">
<style>
html,
body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
#dplayer-container {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<!-- 创建一个容器用于放置视频播放器 -->
<div id="dplayer-container"></div>
<!-- 引入 Hls.js 库 -->
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<!-- 引入 DPlayer JavaScript 文件 -->
<script src="https://cdn.jsdelivr.net/npm/dplayer/dist/DPlayer.min.js"></script>
<script>
var myParam = decodeURIComponent(location.search.split('n=')[1]);
if (!myParam) {
alert('无效的视频地址')
}
// 创建 DPlayer 实例
var dp = new DPlayer({
// 定义容器
container: document.getElementById('dplayer-container'),
// 视频源地址
video: {
url: myParam
// 如果有多个清晰度,可以在这里添加更多的清晰度选项
// quality: [
// { name: 'HD', url: 'https://example.com/your-video-hd.mp4', type: 'normal' },
// { name: 'SD', url: 'https://example.com/your-video-sd.mp4', type: 'normal' }
// ],
},
autoplay: false, // 设置为手动点击播放
// 视频封面图片
poster: 'https://example.com/your-video-poster.jpg',
});
</script>
</body>
</html>

View File

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex, nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Full Screen iFrame</title>
<style>
html,
body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
#myIframe {
width: 100%;
height: 100%;
border: none;
/* 可选:移除边框 */
}
</style>
</head>
<body>
<!-- <div style="position: absolute;
right: 0px;
bottom: 0px;
background: white;">
<button onclick="toggleFullScreen()">Toggle Full Screen</button>
</div> -->
<iframe id="myIframe" allowfullscreen="allowfullscreen" allow="autoplay" scrolling="no"></iframe>
<!-- https://letsplay247.github.io/cz.html?n=space-wars-battleground -->
<script>
var myParam = location.search.split('n=')[1]
document.getElementById("myIframe").src = myParam;
</script>
<script src="/js/fullscreen.js" type="text/javascript"></script>
</body>
</html>

32
public/js/fullscreen.js Normal file
View File

@@ -0,0 +1,32 @@
window.toggleFullScreen = toggleFullScreen
function toggleFullScreen() {
var iframe = document.getElementById('myIframe')
if (!document.fullscreenElement) {
if (iframe.requestFullscreen) {
iframe.requestFullscreen()
} else if (iframe.mozRequestFullScreen) {
/* Firefox */
iframe.mozRequestFullScreen()
} else if (iframe.webkitRequestFullscreen) {
/* Chrome, Safari and Opera */
iframe.webkitRequestFullscreen()
} else if (iframe.msRequestFullscreen) {
/* IE/Edge */
iframe.msRequestFullscreen()
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.mozCancelFullScreen) {
/* Firefox */
document.mozCancelFullScreen()
} else if (document.webkitExitFullscreen) {
/* Chrome, Safari and Opera */
document.webkitExitFullscreen()
} else if (document.msExitFullscreen) {
/* IE/Edge */
document.msExitFullscreen()
}
}
}

View File

@@ -76,9 +76,13 @@ nav {
}
.shadow-card {
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, rgba(0, 0, 0, 0.07) 0px 2px 4px,
rgba(0, 0, 0, 0.07) 0px 4px 8px, rgba(0, 0, 0, 0.07) 0px 8px 16px,
rgba(0, 0, 0, 0.07) 0px 16px 32px, rgba(0, 0, 0, 0.07) 0px 32px 64px;
box-shadow:
rgba(0, 0, 0, 0.07) 0px 1px 2px,
rgba(0, 0, 0, 0.07) 0px 2px 4px,
rgba(0, 0, 0, 0.07) 0px 4px 8px,
rgba(0, 0, 0, 0.07) 0px 8px 16px,
rgba(0, 0, 0, 0.07) 0px 16px 32px,
rgba(0, 0, 0, 0.07) 0px 32px 64px;
}
.gt-meta {
@@ -106,7 +110,6 @@ nav {
backdrop-filter: blur(10px);
}
.medium-zoom-overlay {
background: none !important;
/* background: rgba(0, 0, 0, 0.01) none repeat scroll 0% 0% !important; */
@@ -157,7 +160,7 @@ nav {
/* twikoo 评论区超链接样式 */
.tk-main a {
@apply text-blue-700
@apply text-blue-700;
}
/* twikoo 内置的 element-ui 加载样式 */
@@ -167,7 +170,7 @@ nav {
/* Webmention style */
.webmention-block {
background: rgba(0, 116, 222, .2);
background: rgba(0, 116, 222, 0.2);
padding: 1rem 2rem;
border-radius: 5px;
}
@@ -176,11 +179,11 @@ nav {
font-style: italic;
font-weight: 700;
font-size: 16px;
margin-bottom: .5rem;
margin-bottom: 0.5rem;
}
.webmention-block-intro a {
color: #0000EE;
color: #0000ee;
text-decoration: underline;
}
@@ -197,14 +200,14 @@ nav {
.webmention-counts .count {
font-weight: bold;
margin-right: .2rem;
margin-right: 0.2rem;
}
/* .webmention-counts .counts > span {
margin-right: .8rem;
} */
.webmention-counts .counts>span:not(:last-child):after {
content: "";
.webmention-counts .counts > span:not(:last-child):after {
content: '';
}
a.avatar-wrapper {
@@ -221,7 +224,7 @@ a.avatar-wrapper {
.avatar {
border-radius: 50%;
margin: 0;
border: 3px solid rgba(0, 116, 222, .5);
border: 3px solid rgba(0, 116, 222, 0.5);
}
.replies {
@@ -235,7 +238,7 @@ a.avatar-wrapper {
position: relative;
padding: 0;
align-items: flex-start;
margin-top: .6rem;
margin-top: 0.6rem;
}
.reply p {
@@ -255,4 +258,9 @@ a.avatar-wrapper {
user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
}
.writing-vertical {
writing-mode: vertical-rl; /* 竖向排列从右向左 */
text-orientation: upright; /* 文字方向正常 */
}

View File

@@ -116,7 +116,6 @@
}
}
.notion-page-content-inner {
position: relative;
display: flex;
@@ -179,7 +178,6 @@
color: var(--select-color-2) !important;
}
.notion-app {
position: relative;
background: var(--bg-color);
@@ -211,17 +209,17 @@
/* width: auto !important; */
}
@media (max-width: 768px){
.medium-zoom-image--opened {
object-fit: fill !important;
height: auto !important;
}
@media (max-width: 768px) {
.medium-zoom-image--opened {
object-fit: fill !important;
height: auto !important;
}
}
@media (min-width: 768px){
.medium-zoom-image--opened {
object-fit: scale-down !important;
}
@media (min-width: 768px) {
.medium-zoom-image--opened {
object-fit: scale-down !important;
}
}
.notion-frame {
@@ -405,13 +403,13 @@ summary > .notion-h {
.notion-h1 {
font-size: 1.575em;
margin-top: 1.08em;
@apply border-b w-full
@apply border-b w-full;
}
.notion-h2 {
@apply w-full
@apply w-full;
}
.notion-h3 {
@apply w-full
@apply w-full;
}
.notion-header-anchor {
@@ -443,7 +441,7 @@ summary > .notion-h {
.notion-h:hover .notion-hash-link {
opacity: 1;
@apply dark:fill-gray-200
@apply dark:fill-gray-200;
}
.notion-hash-link {
@@ -560,10 +558,12 @@ summary > .notion-h {
color: inherit;
word-break: break-word;
text-decoration: inherit;
border-bottom: .05em solid !important;
border-bottom: 0.05em solid !important;
border-color: var(--fg-color-2);
opacity: 0.7;
transition: border-color 100ms ease-in, opacity 100ms ease-in;
transition:
border-color 100ms ease-in,
opacity 100ms ease-in;
}
.notion-link:hover {
@@ -601,7 +601,7 @@ summary > .notion-h {
margin: 2px 4px 0 2px;
fill: var(--fg-color-6);
color: var(--fg-color-icon);
@apply dark:fill-gray-200
@apply dark:fill-gray-200;
}
img.notion-page-icon,
@@ -667,12 +667,12 @@ svg.notion-page-icon {
}
.notion-list-numbered > .notion-list-numbered {
list-style-type: lower-alpha;
list-style-type: lower-alpha;
}
.notion-list-numbered > .notion-list-numbered > .notion-list-numbered {
list-style-type: lower-roman;
}
list-style-type: lower-roman;
}
.notion-list-disc li {
padding-left: 0.1em;
@@ -701,7 +701,7 @@ svg.notion-page-icon {
}
.notion-asset-wrapper-image > div {
height: auto !important;
height: auto !important;
}
.notion-asset-wrapper-full {
@@ -709,7 +709,7 @@ svg.notion-page-icon {
}
.notion-asset-wrapper img {
width: 90%;
/* width: 90%; */
/* height: 100%; */
height: auto !important;
max-height: 100%;
@@ -851,7 +851,7 @@ code[class*='language-'] {
.notion-bookmark-link {
display: flex;
margin-top: 6px;
@apply w-52 md:w-80
@apply w-52 md:w-80;
}
.notion-bookmark-link > img {
@@ -919,7 +919,7 @@ code[class*='language-'] {
font-size: 14px;
line-height: 1.4;
color: var(--fg-color-3);
@apply dark:text-gray-300
@apply dark:text-gray-300;
}
.notion-callout {
@@ -1122,7 +1122,7 @@ code[class*='language-'] {
.notion-table-of-contents {
width: 100%;
margin: 4px 0;
@apply bg-gray-50 dark:bg-gray-900 p-2
@apply bg-gray-50 dark:bg-gray-900 p-2;
}
.notion-table-of-contents-item {
@@ -1142,8 +1142,7 @@ code[class*='language-'] {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@apply dark:text-white
@apply dark:text-white;
}
.notion-table-of-contents-item:hover {
@@ -1296,8 +1295,8 @@ code[class*='language-'] {
transition: background 20ms ease-in 0s;
color: inherit;
text-decoration: none;
@apply dark:stroke-slate-200
@apply dark:stroke-slate-200;
}
.notion-file-link:hover {
@@ -1337,7 +1336,7 @@ code[class*='language-'] {
line-height: 16px;
margin-left: 6px;
@apply dark:text-gray-400 !important
@apply dark:text-gray-400 !important;
}
.notion-audio {
@@ -1396,8 +1395,8 @@ code[class*='language-'] {
white-space: normal;
}
.katex-display>.katex>.katex-html>.tag {
position: inherit !important;
.katex-display > .katex > .katex-html > .tag {
position: inherit !important;
}
.notion-page-title {
@@ -1424,13 +1423,13 @@ code[class*='language-'] {
@apply max-w-0;
}
.notion-collection-card{
.notion-collection-card {
/* cursor: default !important; */
}
.notion-collection-card-property .notion-link {
border-bottom: 0 none;
cursor: pointer
cursor: pointer;
}
.notion-collection-card-property .notion-page-title {
@@ -1469,7 +1468,7 @@ code[class*='language-'] {
}
.notion-collection-row {
@apply hidden
@apply hidden;
}
.notion-collection-row-body {
@@ -1612,7 +1611,8 @@ code[class*='language-'] {
}
.lazy-image-real.medium-zoom-image {
transition: transform 0.3s cubic-bezier(0.2, 0, 0.2, 1),
transition:
transform 0.3s cubic-bezier(0.2, 0, 0.2, 1),
opacity 400ms ease-out !important;
will-change: opacity, transform;
}
@@ -1700,12 +1700,16 @@ svg + .notion-page-title-text {
@apply text-gray-600 dark:text-gray-300;
}
.notion-gray_background,.notion-brown_background,
.notion-orange_background,.notion-yellow_background,
.notion-blue_background,.notion-purple_background,
.notion-teal_background,.notion-red_background,
.notion-pink_background{
@apply dark:text-black
.notion-gray_background,
.notion-brown_background,
.notion-orange_background,
.notion-yellow_background,
.notion-blue_background,
.notion-purple_background,
.notion-teal_background,
.notion-red_background,
.notion-pink_background {
@apply dark:text-black;
}
.notion-bookmark:hover {
@@ -1737,7 +1741,7 @@ svg + .notion-page-title-text {
padding: 4px 2px;
white-space: nowrap;
overflow: hidden;
@apply px-0 !important
@apply px-0 !important;
}
.notion-collection-header-title {
@@ -1791,7 +1795,7 @@ svg + .notion-page-title-text {
/* fill: var(--fg-color); */
fill: rgba(55, 53, 47);
margin-right: 6px;
@apply dark:fill-gray-200
@apply dark:fill-gray-200;
}
.notion-collection-view-type-title {
@@ -1799,13 +1803,13 @@ svg + .notion-page-title-text {
overflow: hidden;
text-overflow: ellipsis;
color: var(--fg-color);
@apply dark:text-gray-200
@apply dark:text-gray-200;
}
.notion-table {
align-self: center;
overflow: auto hidden;
@apply w-full !important
@apply w-full !important;
}
.notion-table-view {
@@ -1814,13 +1818,13 @@ svg + .notion-page-title-text {
min-width: var(--notion-max-width);
padding-left: 0;
transition: padding 200ms ease-out;
@apply px-0 !important
@apply px-0 !important;
}
.notion-table-header {
display: flex;
position: absolute;
z-index:30;
z-index: 30;
height: 33px;
color: var(--fg-color-3);
min-width: var(--notion-max-width);
@@ -1872,7 +1876,7 @@ svg + .notion-page-title-text {
line-height: 120%;
min-width: 0;
font-size: 14px;
@apply dark:text-gray-200
@apply dark:text-gray-200;
}
.notion-collection-column-title-icon {
@@ -1883,12 +1887,11 @@ svg + .notion-page-title-text {
min-height: 14px;
fill: var(--fg-color-2);
margin-right: 6px;
@apply dark:text-gray-200 dark:fill-gray-200
@apply dark:text-gray-200 dark:fill-gray-200;
}
.notion-collection-view-tabs-content-item-active {
@apply dark:border-gray-300
@apply dark:border-gray-300;
}
.notion-collection-column-title-body {
@@ -1947,12 +1950,12 @@ svg + .notion-page-title-text {
padding: 7px 8px 0;
}
.notion-simple-table {
.notion-simple-table {
@apply whitespace-nowrap overflow-x-auto block w-full border-0 !important;
}
.notion-asset-wrapper-pdf > div {
display: block !important;
display: block !important;
}
/* https://github.com/kchen0x */
@@ -1972,47 +1975,49 @@ svg + .notion-page-title-text {
/* color: var(--notion-gray); */
}
.notion-asset-wrapper-pdf>div{
width:unset!important
.notion-asset-wrapper-pdf > div {
width: unset !important;
}
/* pdf预览适配页面 */
.react-pdf__Page__canvas,.react-pdf__Page__textContent{
width: 100% !important;
height: auto !important;
.react-pdf__Page__canvas,
.react-pdf__Page__textContent {
width: 100% !important;
height: auto !important;
}
/* simple table设置 */
table,thead,tbody{
display:block
table,
thead,
tbody {
display: block;
}
thead, tbody tr {
display:table;
width:100%;
table-layout:fixed;
thead,
tbody tr {
display: table;
width: 100%;
table-layout: fixed;
}
.notion-collection-card{
@apply dark:text-gray-200 dark:bg-gray-800 dark:hover:bg-black
.notion-collection-card {
@apply dark:text-gray-200 dark:bg-gray-800 dark:hover:bg-black;
}
.notion-code-copy{
display: none;
.notion-code-copy {
display: none;
}
pre[class*="language-mermaid"] {
@apply bg-gray-50 dark:bg-gray-200 !important;
pre[class*='language-mermaid'] {
@apply bg-gray-50 dark:bg-gray-200 !important;
}
/* mermaid 原文隐藏 */
code.language-mermaid {
display:none
display: none;
}
.code-toolbar{
.code-toolbar {
@apply w-full shadow-md pb-0;
}
@@ -2036,7 +2041,7 @@ code.language-mermaid {
@apply dark:border-gray-200 !important;
}
.notion-external-image > svg > g > path{
.notion-external-image > svg > g > path {
@apply dark:fill-gray-200 !important;
}
@@ -2049,17 +2054,16 @@ code.language-mermaid {
}
/* 表格头 */
.notion-simple-table tr:first-child td{
.notion-simple-table tr:first-child td {
background-color: #f5f6f8;
@apply text-center font-bold dark:bg-gray-800 !important;
}
.notion-simple-table td{
border: 1px solid var(#eee) !important
.notion-simple-table td {
border: 1px solid var(#eee) !important;
}
/* 竖屏视频高度bug */
figure.notion-asset-wrapper.notion-asset-wrapper-video>div {
height: 100% !important;
figure.notion-asset-wrapper.notion-asset-wrapper-video > div {
height: 100% !important;
}

View File

@@ -0,0 +1,30 @@
import Card from './Card'
export function AnalyticsCard (props) {
const { postCount } = props
return <Card>
<div className='ml-2 mb-3 '>
<i className='fas fa-chart-area' /> 统计
</div>
<div className='text-xs font-light justify-center mx-7'>
<div className='inline'>
<div className='flex justify-between'>
<div>文章数:</div>
<div>{postCount}</div>
</div>
</div>
<div className='hidden busuanzi_container_page_pv ml-2'>
<div className='flex justify-between'>
<div>访问量:</div>
<div className='busuanzi_value_page_pv' />
</div>
</div>
<div className='hidden busuanzi_container_site_uv ml-2'>
<div className='flex justify-between'>
<div>访客数:</div>
<div className='busuanzi_value_site_uv' />
</div>
</div>
</div>
</Card>
}

View File

@@ -0,0 +1,21 @@
import { useGlobal } from '@/lib/global'
import dynamic from 'next/dynamic'
const NotionPage = dynamic(() => import('@/components/NotionPage'))
const Announcement = ({ post, className }) => {
const { locale } = useGlobal()
if (post?.blockMap) {
return <div className={className}>
<section id='announcement-wrapper' className="dark:text-gray-300 border dark:border-black rounded-xl lg:p-6 p-4 bg-white dark:bg-hexo-black-gray">
<div><i className='mr-2 fas fa-bullhorn' />{locale.COMMON.ANNOUNCEMENT}</div>
{post && (<div id="announcement-content">
<NotionPage post={post} className='text-center' />
</div>)}
</section>
</div>
} else {
return <></>
}
}
export default Announcement

View File

@@ -0,0 +1,33 @@
import Link from 'next/link'
import CONFIG from '../config'
/**
* 上一篇,下一篇文章
* @param {prev,next} param0
* @returns
*/
export default function ArticleAdjacent ({ prev, next }) {
if (!prev || !next || !CONFIG.ARTICLE_ADJACENT) {
return <></>
}
return (
<section className='pt-8 text-gray-800 items-center text-xs md:text-sm flex justify-between m-1 '>
<Link
href={`/${prev.slug}`}
passHref
className='py-1 cursor-pointer hover:underline justify-start items-center dark:text-white flex w-full h-full duration-200'>
<i className='mr-1 fas fa-angle-left' />{prev.title}
</Link>
<Link
href={`/${next.slug}`}
passHref
className='py-1 cursor-pointer hover:underline justify-end items-center dark:text-white flex w-full h-full duration-200'>
{next.title}
<i className='ml-1 my-1 fas fa-angle-right' />
</Link>
</section>
)
}

View File

@@ -0,0 +1,43 @@
import { useGlobal } from '@/lib/global'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import CONFIG from '../config'
import { siteConfig } from '@/lib/config'
export default function ArticleCopyright () {
const router = useRouter()
const [path, setPath] = useState(siteConfig('LINK') + router.asPath)
useEffect(() => {
setPath(window.location.href)
})
const { locale } = useGlobal()
if (!CONFIG.ARTICLE_COPYRIGHT) {
return <></>
}
return (
<section className="dark:text-gray-300 mt-6 mx-1 ">
<ul className="overflow-x-auto whitespace-nowrap text-sm dark:bg-gray-900 bg-gray-100 p-5 leading-8 border-l-2 border-red-500">
<li>
<strong className='mr-2'>{locale.COMMON.AUTHOR}:</strong>
<Link href={'/about'} className="hover:underline">
{siteConfig('AUTHOR')}
</Link>
</li>
<li>
<strong className='mr-2'>{locale.COMMON.URL}:</strong>
<a className="whitespace-normal break-words hover:underline" href={path}>
{path}
</a>
</li>
<li>
<strong className='mr-2'>{locale.COMMON.COPYRIGHT}:</strong>
{locale.COMMON.COPYRIGHT_NOTICE}
</li>
</ul>
</section>
);
}

View File

@@ -0,0 +1,51 @@
import { useGlobal } from '@/lib/global'
import { useEffect, useRef } from 'react'
/**
* 加密文章校验组件
* @param {password, validPassword} props
* @param password 正确的密码
* @param validPassword(bool) 回调函数校验正确回调入参为true
* @returns
*/
export const ArticleLock = props => {
const { validPassword } = props
const { locale } = useGlobal()
const submitPassword = () => {
const p = document.getElementById('password')
if (!validPassword(p?.value)) {
const tips = document.getElementById('tips')
if (tips) {
tips.innerHTML = ''
tips.innerHTML = `<div class='text-red-500 animate__shakeX animate__animated'>${locale.COMMON.PASSWORD_ERROR}</div>`
}
}
}
const passwordInputRef = useRef(null)
useEffect(() => {
// 选中密码输入框并将其聚焦
passwordInputRef.current.focus()
}, [])
return <div id='container' className='w-full flex justify-center items-center h-96 '>
<div className='text-center space-y-3'>
<div className='font-bold dark:text-gray-300 text-black'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
<div className='flex mx-4'>
<input id="password" type='password'
onKeyDown={(e) => {
if (e.key === 'Enter') {
submitPassword()
}
}}
ref={passwordInputRef} // 绑定ref到passwordInputRef变量
className='outline-none w-full text-sm pl-5 rounded-l transition focus:shadow-lg font-light leading-10 bg-gray-100 dark:bg-gray-500'>
</input>
<div onClick={submitPassword} className="px-3 whitespace-nowrap cursor-pointer items-center justify-center py-2 bg-red-500 hover:bg-red-400 text-white rounded-r duration-300" >
<i className={'duration-200 cursor-pointer fas fa-key'} >&nbsp;{locale.COMMON.SUBMIT}</i>
</div>
</div>
<div id='tips'>
</div>
</div>
</div>
}

View File

@@ -0,0 +1,60 @@
import Link from 'next/link'
import CONFIG from '../config'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import LazyImage from '@/components/LazyImage'
/**
* 关联推荐文章
* @param {prev,next} param0
* @returns
*/
export default function ArticleRecommend({ recommendPosts, siteInfo }) {
const { locale } = useGlobal()
if (
!CONFIG.ARTICLE_RECOMMEND ||
!recommendPosts ||
recommendPosts.length === 0
) {
return <></>
}
return (
<div className="pt-8">
<div className=" mb-2 px-1 flex flex-nowrap justify-between">
<div className='dark:text-gray-300'>
<i className="mr-2 fas fa-thumbs-up" />
{locale.COMMON.RELATE_POSTS}
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{recommendPosts.map(post => {
const headerImage = post?.pageCoverThumbnail
? post.pageCoverThumbnail
: siteInfo?.pageCover
return (
(<Link
key={post.id}
title={post.title}
href={`${siteConfig('SUB_PATH', '')}/${post.slug}`}
passHref
className="flex h-40 cursor-pointer overflow-hidden">
<div className="h-full w-full relative group">
<div className="flex items-center justify-center w-full h-full duration-300 ">
<div className="z-10 text-lg px-4 font-bold text-white text-center shadow-text select-none">
{post.title}
</div>
</div>
<LazyImage src={headerImage} className='absolute top-0 w-full h-full object-cover object-center group-hover:scale-110 group-hover:brightness-50 transform duration-200' />
</div>
</Link>)
)
})}
</div>
</div>
)
}

View File

@@ -0,0 +1,49 @@
import Link from 'next/link'
import { siteConfig } from '@/lib/config'
/**
* 博客归档列表
* @param posts 所有文章
* @param archiveTitle 归档标题
* @returns {JSX.Element}
* @constructor
*/
const BlogPostArchive = ({ posts = [], archiveTitle }) => {
if (!posts || posts.length === 0) {
return <></>
} else {
return (
<div>
<div
className="pt-16 pb-4 text-3xl dark:text-gray-300"
id={archiveTitle}
>
{archiveTitle}
</div>
<ul>
{posts?.map(post => (
<li
key={post.id}
className="border-l-2 p-1 text-xs md:text-base items-center hover:scale-x-105 hover:border-red-500 dark:hover:border-red-300 dark:border-red-400 transform duration-500"
>
<div id={post?.publishDay}>
<span className="text-gray-400">{post.date?.start_date}</span>{' '}
&nbsp;
<Link
href={`${siteConfig('SUB_PATH', '')}/${post.slug}`}
passHref
className="dark:text-gray-400 dark:hover:text-red-300 overflow-x-hidden hover:underline cursor-pointer text-gray-600">
{post.title}
</Link>
</div>
</li>
))}
</ul>
</div>
)
}
}
export default BlogPostArchive

View File

@@ -0,0 +1,94 @@
import NotionPage from '@/components/NotionPage'
import Link from 'next/link'
import TagItemMini from './TagItemMini'
import TwikooCommentCount from '@/components/TwikooCommentCount'
import { siteConfig } from '@/lib/config'
import formatDate from '@/lib/utils/formatDate'
/**
* 博客列表的文字内容
* @param {*} param0
* @returns
*/
export const BlogPostCardInfo = ({ post, showPreview, showPageCover, showSummary }) => {
return <div className={`flex flex-col justify-between lg:p-6 p-4 ${showPageCover && !showPreview ? 'md:w-7/12 w-full md:max-h-60' : 'w-full'}`}>
<div>
{/* 标题 */}
<Link
href={`${siteConfig('SUB_PATH', '')}/${post.slug}`}
passHref
className={`line-clamp-2 replace cursor-pointer text-2xl ${showPreview ? 'text-center' : ''
} leading-tight font-normal text-gray-600 dark:text-gray-100 hover:text-red-700 dark:hover:text-red-400`}>
<span className='menu-link '>{post.title}</span>
</Link>
{/* 分类 */}
{ post?.category && <div
className={`flex mt-2 items-center ${showPreview ? 'justify-center' : 'justify-start'
} flex-wrap dark:text-gray-500 text-gray-400 `}
>
<Link
href={`/category/${post.category}`}
passHref
className="cursor-pointer font-light text-sm menu-link hover:text-red-700 dark:hover:text-red-400 transform">
<i className="mr-1 far fa-folder" />
{post.category}
</Link>
<TwikooCommentCount className='text-sm hover:text-red-700 dark:hover:text-red-400' post={post}/>
</div>}
{/* 摘要 */}
{(!showPreview || showSummary) && !post.results && (
<p className="line-clamp-2 replace my-3 text-gray-700 dark:text-gray-300 text-sm font-light leading-7">
{post.summary}
</p>
)}
{/* 搜索结果 */}
{post.results && (
<p className="line-clamp-2 mt-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7">
{post.results.map((r, index) => (
<span key={index}>{r}</span>
))}
</p>
)}
{/* 预览 */}
{showPreview && (
<div className="overflow-ellipsis truncate">
<NotionPage post={post} />
</div>
)}
</div>
<div>
{/* 日期标签 */}
<div className="text-gray-400 justify-between flex">
{/* 日期 */}
<Link
href={`/archive#${formatDate(post?.publishDate, 'yyyy-MM')}`}
passHref
className="font-light menu-link cursor-pointer text-sm leading-4 mr-3">
<i className="far fa-calendar-alt mr-1" />
{post?.publishDay || post.lastEditedDay}
</Link>
<div className="md:flex-nowrap flex-wrap md:justify-start inline-block">
<div>
{' '}
{post.tagItems?.map(tag => (
<TagItemMini key={tag.name} tag={tag} />
))}
</div>
</div>
</div>
</div>
</div>
}

View File

@@ -0,0 +1,14 @@
import { useGlobal } from '@/lib/global'
/**
* 空白博客 列表
* @returns {JSX.Element}
* @constructor
*/
const BlogPostListEmpty = ({ currentSearch }) => {
const { locale } = useGlobal()
return <div className='flex w-full items-center justify-center min-h-screen mx-auto md:-mt-20'>
<div className='text-gray-500 dark:text-gray-300'>{locale.COMMON.NO_MORE} {(currentSearch && <div>{currentSearch}</div>)}</div>
</div>
}
export default BlogPostListEmpty

View File

@@ -0,0 +1,34 @@
import ProductCard from './ProductCard'
import PaginationNumber from './PaginationNumber'
import { siteConfig } from '@/lib/config'
import BlogPostListEmpty from './BlogPostListEmpty'
/**
* 文章列表分页表格
* @param page 当前页
* @param posts 所有文章
* @param tags 所有标签
* @returns {JSX.Element}
* @constructor
*/
const BlogPostListPage = ({ page = 1, posts = [], postCount, siteInfo }) => {
const totalPage = Math.ceil(postCount / parseInt(siteConfig('POSTS_PER_PAGE')))
const showPagination = postCount >= parseInt(siteConfig('POSTS_PER_PAGE'))
if (!posts || posts.length === 0 || page > totalPage) {
return <BlogPostListEmpty />
} else {
return (
<div id="container" className='w-full'>
{/* 文章列表 */}
<div className="py-4 gap-4 grid grid-cols-3">
{posts?.map(post => (
<ProductCard index={posts.indexOf(post)} key={post.id} post={post} siteInfo={siteInfo}/>
))}
</div>
{showPagination && <PaginationNumber page={page} totalPage={totalPage} />}
</div>
)
}
}
export default BlogPostListPage

View File

@@ -0,0 +1,75 @@
import { siteConfig } from '@/lib/config'
import ProductCard from './ProductCard'
import BlogPostListEmpty from './BlogPostListEmpty'
import { useGlobal } from '@/lib/global'
import CONFIG from '../config'
import { getListByPage } from '@/lib/utils'
import { useEffect, useRef, useState } from 'react'
/**
* 博客列表滚动分页
* @param posts 所有文章
* @param tags 所有标签
* @returns {JSX.Element}
* @constructor
*/
const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = CONFIG.POST_LIST_SUMMARY, siteInfo }) => {
const postsPerPage = parseInt(siteConfig('POSTS_PER_PAGE'))
const [page, updatePage] = useState(1)
const postsToShow = getListByPage(posts, page, postsPerPage)
let hasMore = false
if (posts) {
const totalCount = posts.length
hasMore = page * postsPerPage < totalCount
}
const handleGetMore = () => {
if (!hasMore) return
updatePage(page + 1)
}
// 监听滚动自动分页加载
const scrollTrigger = () => {
requestAnimationFrame(() => {
const scrollS = window.scrollY + window.outerHeight
const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
if (scrollS > clientHeight + 100) {
handleGetMore()
}
})
}
// 监听滚动
useEffect(() => {
window.addEventListener('scroll', scrollTrigger)
return () => {
window.removeEventListener('scroll', scrollTrigger)
}
})
const targetRef = useRef(null)
const { locale } = useGlobal()
if (!postsToShow || postsToShow.length === 0) {
return <BlogPostListEmpty currentSearch={currentSearch} />
} else {
return <div id='container' ref={targetRef} className='w-full'>
{/* 文章列表 */}
<div className="space-y-6 px-2">
{postsToShow.map(post => (
<ProductCard key={post.id} post={post} showSummary={showSummary} siteInfo={siteInfo}/>
))}
</div>
<div>
<div onClick={() => { handleGetMore() }}
className='w-full my-4 py-4 text-center cursor-pointer rounded-xl dark:text-gray-200'
> {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE}`} </div>
</div>
</div>
}
}
export default BlogPostListScroll

View File

@@ -0,0 +1,9 @@
const Card = ({ children, headerSlot, className }) => {
return <div className={className}>
<>{headerSlot}</>
<section className="card shadow-md hover:shadow-md dark:text-gray-300 border dark:border-black rounded-xl lg:p-6 p-4 bg-white dark:bg-hexo-black-gray lg:duration-100">
{children}
</section>
</div>
}
export default Card

View File

@@ -0,0 +1,95 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import throttle from 'lodash.throttle'
import { uuidToId } from 'notion-utils'
import Progress from './Progress'
import { useGlobal } from '@/lib/global'
/**
* 目录导航组件
* @param toc
* @returns {JSX.Element}
* @constructor
*/
const Catalog = ({ toc }) => {
const { locale } = useGlobal()
// 监听滚动事件
useEffect(() => {
window.addEventListener('scroll', actionSectionScrollSpy)
actionSectionScrollSpy()
return () => {
window.removeEventListener('scroll', actionSectionScrollSpy)
}
}, [])
// 目录自动滚动
const tRef = useRef(null)
const tocIds = []
// 同步选中目录事件
const [activeSection, setActiveSection] = useState(null)
const throttleMs = 200
const actionSectionScrollSpy = useCallback(throttle(() => {
const sections = document.getElementsByClassName('notion-h')
let prevBBox = null
let currentSectionId = activeSection
for (let i = 0; i < sections.length; ++i) {
const section = sections[i]
if (!section || !(section instanceof Element)) continue
if (!currentSectionId) {
currentSectionId = section.getAttribute('data-id')
}
const bbox = section.getBoundingClientRect()
const prevHeight = prevBBox ? bbox.top - prevBBox.bottom : 0
const offset = Math.max(150, prevHeight / 4)
// GetBoundingClientRect returns values relative to viewport
if (bbox.top - offset < 0) {
currentSectionId = section.getAttribute('data-id')
prevBBox = bbox
continue
}
// No need to continue loop, if last element has been detected
break
}
setActiveSection(currentSectionId)
const index = tocIds.indexOf(currentSectionId) || 0
tRef?.current?.scrollTo({ top: 28 * index, behavior: 'smooth' })
}, throttleMs))
// 无目录就直接返回空
if (!toc || toc.length < 1) {
return <></>
}
return <div className='px-3 py-1'>
<div className='w-full'><i className='mr-1 fas fa-stream' />{locale.COMMON.TABLE_OF_CONTENTS}</div>
<div className='w-full py-3'>
<Progress />
</div>
<div className='overflow-y-auto max-h-36 lg:max-h-96 overscroll-none scroll-hidden' ref={tRef}>
<nav className='h-full text-black'>
{toc.map((tocItem) => {
const id = uuidToId(tocItem.id)
tocIds.push(id)
return (
<a
key={id}
href={`#${id}`}
className={`notion-table-of-contents-item duration-300 transform font-light dark:text-gray-200
notion-table-of-contents-item-indent-level-${tocItem.indentLevel} `}
>
<span style={{ display: 'inline-block', marginLeft: tocItem.indentLevel * 16 }}
className={`${activeSection === id && ' font-bold text-red-600'}`}
>
{tocItem.text}
</span>
</a>
)
})}
</nav>
</div>
</div>
}
export default Catalog

View File

@@ -0,0 +1,30 @@
import Link from 'next/link'
const CategoryGroup = ({ currentCategory, categories }) => {
if (!categories) {
return <></>
}
return <>
<div id='category-list' className='dark:border-gray-600 flex flex-wrap mx-4'>
{categories.map(category => {
const selected = currentCategory === category.name
return (
<Link
key={category.name}
href={`/category/${category.name}`}
passHref
className={(selected
? 'hover:text-white dark:hover:text-white bg-red-600 text-white '
: 'dark:text-gray-400 text-gray-500 hover:text-white dark:hover:text-white hover:bg-red-600') +
' text-sm w-full items-center duration-300 px-2 cursor-pointer py-1 font-light'}>
<div> <i className={`mr-2 fas ${selected ? 'fa-folder-open' : 'fa-folder'}`} />{category.name}({category.count})</div>
</Link>
);
})}
</div>
</>;
}
export default CategoryGroup

View File

@@ -0,0 +1,34 @@
import { useGlobal } from '@/lib/global'
import { saveDarkModeToLocalStorage } from '@/themes/theme'
import CONFIG from '../config'
export default function FloatDarkModeButton() {
const { isDarkMode, updateDarkMode } = useGlobal()
if (!CONFIG.WIDGET_DARK_MODE) {
return <></>
}
// 用户手动设置主题
const handleChangeDarkMode = () => {
const newStatus = !isDarkMode
saveDarkModeToLocalStorage(newStatus)
updateDarkMode(newStatus)
const htmlElement = document.getElementsByTagName('html')[0]
htmlElement.classList?.remove(newStatus ? 'light' : 'dark')
htmlElement.classList?.add(newStatus ? 'dark' : 'light')
}
return (
<div
onClick={handleChangeDarkMode}
className={
'justify-center items-center w-7 h-7 text-center transform hover:scale-105 duration-200'
}>
<i
id='darkModeButton'
className={`${isDarkMode ? 'fa-sun' : 'fa-moon'} fas text-xs`}
/>
</div>
)
}

View File

@@ -0,0 +1,121 @@
import { siteConfig } from '@/lib/config'
import Link from 'next/link'
/**
* 页脚
* @param {*} param0
* @returns
*/
const Footer = (props) => {
const d = new Date()
const currentYear = d.getFullYear()
const since = siteConfig('SINCE')
const copyrightDate = parseInt(since) < currentYear ? since + '-' + currentYear : currentYear
const { categoryOptions, customMenu } = props
return <footer id="footer-wrapper" className='relative bg-[#2A2A2A] justify-center w-full leading-6 text-gray-300 text-sm p-10'>
<div id='footer-container' className='w-full mx-auto max-w-screen-xl'>
<div className='flex'>
<div className='hidden md:flex flex-grow my-6 space-x-20 text-lg '>
{/* 分类菜单 */}
<div>
<div className='font-bold mb-4 text-white'>{siteConfig('COMMERCE_TEXT_FOOTER_MENU_1', 'Product Center')}</div>
<nav id='home-nav-button' className={'flex flex-col space-y-2 text-start'}>
{categoryOptions?.map(category => {
return (
<Link
key={`${category.name}`}
title={`${category.name}`}
href={`/category/${category.name}`}
passHref>
{category.name}
</Link>
)
})}
</nav>
</div>
{/* 系统菜单 */}
<div>
<div className='font-bold mb-4 text-white'>{siteConfig('COMMERCE_TEXT_FOOTER_MENU_2', 'About US')}</div>
<nav id='home-nav-button' className={'flex flex-col space-y-2 text-start'}>
{customMenu?.map(menu => {
return (
<Link
key={`${menu.name}`}
title={`${menu.name}`}
href={`${menu.to}`}
passHref>
{menu.name}
</Link>
)
})}
</nav>
</div>
</div>
{<div className='md:border-l pl-4 border-gray-600 my-6 whitespace-pre-line text-left flex-grow'>
<div className='font-bold text-l text-white mb-6'>{siteConfig('COMMERCE_TEXT_FOOTER_TITLE', 'Contact US')}</div>
<div className='space-y-4'>
<div className='flex space-x-4 text-2xl'>
{JSON.parse(siteConfig('COMMERCE_CONTACT_WHATSAPP_SHOW'), true) && <div>
{<a target='_blank' rel='noreferrer' href={siteConfig('CONTACT_WHATSAPP', '#')} title={'telegram'} >
<i className='transform hover:scale-125 duration-150 fa-brands fa-whatsapp dark:hover:text-red-400 hover:text-red-600' />
</a>}
</div>
}
{JSON.parse(siteConfig('COMMERCE_CONTACT_TELEGRAM_SHOW', true)) && <div>
{<a target='_blank' rel='noreferrer' href={siteConfig('CONTACT_TELEGRAM', '#')} title={'telegram'} >
<i className='transform hover:scale-125 duration-150 fab fa-telegram dark:hover:text-red-400 hover:text-red-600' />
</a>}
</div>}
</div>
<div className='text-lg'> {siteConfig('CONTACT_EMAIL') && <a target='_blank' rel='noreferrer' title={'email'} href={`mailto:${siteConfig('CONTACT_EMAIL')}`} >
<i className='transform hover:scale-125 duration-150 fas fa-envelope dark:hover:text-red-400 hover:text-red-600' /> {siteConfig('CONTACT_EMAIL')}
</a>}</div>
<div className='text-lg'> {siteConfig('CONTACT_PHONE') && <div>
<i className='transform hover:scale-125 duration-150 fas fa-user dark:hover:text-red-400 hover:text-red-600' /> {siteConfig('CONTACT_PHONE')}
</div>}</div>
</div>
</div>}
</div>
{/* 底部版权相关 */}
<div id='footer-copyright-wrapper' className='flex flex-col md:flex-row justify-between border-t border-gray-600 pt-8'>
<div className='text-start space-y-1'>
{/* 网站所有者 */}
<div> Copyright <i className='fas fa-copyright' /> {`${copyrightDate}`} <a href={siteConfig('LINK')} className='underline font-bold dark:text-gray-300 '>{siteConfig('AUTHOR')}</a> All Rights Reserved.</div>
{/* 技术支持 */}
<div className='text-xs text-light-500 dark:text-gray-700'>Powered by <a href='https://github.com/tangly1024/NotionNext' className='dark:text-gray-300'>NotionNext {siteConfig('VERSION')}</a>.</div>
{/* 站点统计 */}
<div>
<span className='hidden busuanzi_container_site_pv'>
<i className='fas fa-eye' /><span className='px-1 busuanzi_value_site_pv'> </span> </span>
<span className='pl-2 hidden busuanzi_container_site_uv'>
<i className='fas fa-users' /> <span className='px-1 busuanzi_value_site_uv'> </span> </span>
</div>
</div>
{/* 右边公司名字 */}
<div className='md:text-right'>
<h1 className='text-xs pt-4 text-light-400 dark:text-gray-400'>{siteConfig('TITLE')} {siteConfig('BIO')}</h1>
<h2> {siteConfig('DESCRIPTION')}</h2>
{/* 可选备案信息 */}
{siteConfig('BEI_AN') && <><i className='fas fa-shield-alt' /> <a href='https://beian.miit.gov.cn/' className='mr-2'>{siteConfig('BEI_AN')}</a></>}
</div>
</div>
</div>
</footer >
}
export default Footer

View File

@@ -0,0 +1,96 @@
import LogoBar from './LogoBar'
import { useEffect, useRef, useState } from 'react'
import Collapse from '@/components/Collapse'
import { MenuBarMobile } from './MenuBarMobile'
import { useGlobal } from '@/lib/global'
import CONFIG from '../config'
import { MenuItemDrop } from './MenuItemDrop'
import { siteConfig } from '@/lib/config'
import throttle from 'lodash.throttle'
/**
* 顶部导航栏 + 菜单
* @param {} param0
* @returns
*/
export default function Header(props) {
const { customNav, customMenu } = props
const [isOpen, changeShow] = useState(false)
const collapseRef = useRef(null)
const { locale } = useGlobal()
const defaultLinks = [
{ icon: 'fas fa-th', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY },
{ icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG },
{ icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE },
{ icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH }
]
let links = defaultLinks.concat(customNav)
const toggleMenuOpen = () => {
changeShow(!isOpen)
}
// 向下滚动时,调整导航条高度
useEffect(() => {
scrollTrigger()
window.addEventListener('scroll', scrollTrigger)
return () => {
window.removeEventListener('scroll', scrollTrigger)
}
}, [])
const throttleMs = 150
const scrollTrigger = throttle(() => {
const scrollS = window.scrollY
const nav = document.querySelector('#top-navbar')
const narrowNav = scrollS > 50
if (narrowNav) {
nav && nav.classList.replace('h-24', 'h-14')
} else {
nav && nav.classList.replace('h-14', 'h-24')
}
}, throttleMs)
// 如果 开启自定义菜单则覆盖Page生成的菜单
if (siteConfig('CUSTOM_MENU')) {
links = customMenu
}
if (!links || links.length === 0) {
return null
}
return <div id='top-navbar-wrapper' className={'sticky top-0 w-full z-40 shadow bg-white dark:bg-hexo-black-gray '}>
{/* 导航栏菜单内容 */}
<div id="top-navbar" className='px-4 flex w-full mx-auto max-w-screen-xl h-24 transition-all duration-200 items-between'>
{/* 左侧图标Logo */}
<LogoBar {...props} />
{/* 移动端折叠按钮 */}
<div className='mr-1 flex md:hidden justify-end items-center text-lg space-x-4 font-serif dark:text-gray-200'>
<div onClick={toggleMenuOpen} className='cursor-pointer'>
{isOpen ? <i className='fas fa-times' /> : <i className='fas fa-bars' />}
</div>
</div>
{/* 桌面端顶部菜单 */}
<div className='hidden md:flex items-center'>
{links && links?.map(link => <MenuItemDrop key={link?.id} link={link} />)}
</div>
</div>
{/* 移动端折叠菜单 */}
<Collapse type='vertical' collapseRef={collapseRef} isOpen={isOpen} className='md:hidden'>
<div className='bg-white dark:bg-hexo-black-gray pt-1 py-2 lg:hidden '>
<MenuBarMobile {...props} onHeightChange={(param) => collapseRef.current?.updateCollapseHeight(param)} />
</div>
</Collapse>
</div>
}

View File

@@ -0,0 +1,24 @@
// import Image from 'next/image'
import CONFIG from '../config'
import LazyImage from '@/components/LazyImage'
/**
* 顶部全屏大图
* @returns
*/
const Hero = props => {
const { siteInfo } = props
return (
<header id="header" className="w-full h-auto aspect-[5/2] relative bg-black" >
<div className="text-white absolute bottom-0 flex flex-col h-full items-center justify-center w-full "></div>
<LazyImage id='header-cover' src={siteInfo?.pageCover}
className={`header-cover w-full h-auto aspect-[5/2] object-cover object-center ${CONFIG.HOME_NAV_BACKGROUND_IMG_FIXED ? 'fixed' : ''}`} />
</header>
)
}
export default Hero

View File

@@ -0,0 +1,47 @@
import { useEffect, useState } from 'react'
import { siteConfig } from '@/lib/config'
import Card from '@/themes/hexo/components/Card'
import { useGlobal } from '@/lib/global'
import Link from 'next/link'
import { RecentComments } from '@waline/client'
/**
* @see https://waline.js.org/guide/get-started.html
* @param {*} props
* @returns
*/
const HexoRecentComments = (props) => {
const [comments, updateComments] = useState([])
const { locale } = useGlobal()
const [onLoading, changeLoading] = useState(true)
useEffect(() => {
RecentComments({
serverURL: siteConfig('COMMENT_WALINE_SERVER_URL'),
count: 5
}).then(({ comments }) => {
changeLoading(false)
updateComments(comments)
})
}, [])
return (
<Card >
<div className=" mb-2 px-1 justify-between">
<i className="mr-2 fas fas fa-comment" />
{locale.COMMON.RECENT_COMMENTS}
</div>
{onLoading && <div>Loading...<i className='ml-2 fas fa-spinner animate-spin' /></div>}
{!onLoading && comments && comments.length === 0 && <div>No Comments</div>}
{!onLoading && comments && comments.length > 0 && comments.map((comment) => <div key={comment.objectId} className='pb-2 pl-1'>
<div className='dark:text-gray-200 text-sm waline-recent-content wl-content' dangerouslySetInnerHTML={{ __html: comment.comment }} />
<div className='dark:text-gray-400 text-gray-400 text-sm text-right cursor-pointer hover:text-red-500 hover:underline pt-1 pr-2'>
<Link href={{ pathname: comment.url, hash: comment.objectId, query: { target: 'comment' } }}>--{comment.nick}</Link>
</div>
</div>)}
</Card>
)
}
export default HexoRecentComments

View File

@@ -0,0 +1,33 @@
import { useRouter } from 'next/router'
import Card from './Card'
import SocialButton from './SocialButton'
import MenuGroupCard from './MenuGroupCard'
import LazyImage from '@/components/LazyImage'
import { siteConfig } from '@/lib/config'
/**
* 社交信息卡
* @param {*} props
* @returns
*/
export function InfoCard(props) {
const { className, siteInfo } = props
const router = useRouter()
return (
<Card className={className}>
<div
className='justify-center items-center flex py-6 dark:text-gray-100 transform duration-200 cursor-pointer'
onClick={() => {
router.push('/')
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<LazyImage src={siteInfo?.icon} className='rounded-full' width={120} alt={siteConfig('AUTHOR')} />
</div>
<div className='font-medium text-center text-xl pb-4'>{siteConfig('AUTHOR')}</div>
<div className='text-sm text-center'>{siteConfig('BIO')}</div>
<MenuGroupCard {...props} />
<SocialButton />
</Card>
)
}

View File

@@ -0,0 +1,28 @@
import CONFIG from '../config'
/**
* 跳转到评论区
* @returns {JSX.Element}
* @constructor
*/
const JumpToCommentButton = () => {
if (!CONFIG.WIDGET_TO_COMMENT) {
return <></>
}
function navToComment() {
if (document.getElementById('comment')) {
window.scrollTo({ top: document.getElementById('comment').offsetTop, behavior: 'smooth' })
}
// 兼容性不好
// const commentElement = document.getElementById('comment')
// if (commentElement) {
// commentElement?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' })
}
return (<div className='flex space-x-1 items-center justify-center transform hover:scale-105 duration-200 w-7 h-7 text-center' onClick={navToComment} >
<i className='fas fa-comment text-xs' />
</div>)
}
export default JumpToCommentButton

View File

@@ -0,0 +1,24 @@
import { useGlobal } from '@/lib/global'
import CONFIG from '../config'
/**
* 跳转到网页顶部
* 当屏幕下滑500像素后会出现该控件
* @param targetRef 关联高度的目标html标签
* @param showPercent 是否显示百分比
* @returns {JSX.Element}
* @constructor
*/
const JumpToTopButton = ({ showPercent = true, percent }) => {
const { locale } = useGlobal()
if (!CONFIG.WIDGET_TO_TOP) {
return <></>
}
return (<div className='space-x-1 items-center justify-center transform hover:scale-105 duration-200 w-7 h-auto pb-1 text-center' onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })} >
<div title={locale.POST.TOP} ><i className='fas fa-arrow-up text-xs' /></div>
{showPercent && (<div className='text-xs hidden lg:block'>{percent}</div>)}
</div>)
}
export default JumpToTopButton

View File

@@ -0,0 +1,64 @@
import { siteConfig } from '@/lib/config'
import LazyImage from '@/components/LazyImage'
import { useGlobal } from '@/lib/global'
// import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
/**
* 最新文章列表
* @param posts 所有文章数据
* @param sliceCount 截取展示的数量 默认6
* @constructor
*/
const LatestPostsGroup = ({ latestPosts, siteInfo }) => {
// 获取当前路径
const currentPath = useRouter().asPath
const { locale } = useGlobal()
if (!latestPosts) {
return <></>
}
return <>
<div className=" mb-2 px-1 flex flex-nowrap justify-between">
<div>
<i className="mr-2 fas fas fa-history" />
{locale.COMMON.LATEST_POSTS}
</div>
</div>
{latestPosts.map(post => {
const selected = currentPath === `${siteConfig('SUB_PATH', '')}/${post.slug}`
const headerImage = post?.pageCoverThumbnail ? post.pageCoverThumbnail : siteInfo?.pageCover
return (
(<Link
key={post.id}
title={post.title}
href={`${siteConfig('SUB_PATH', '')}/${post.slug}`}
passHref
className={'my-3 flex'}>
<div className="w-20 h-14 overflow-hidden relative">
<LazyImage src={`${headerImage}`} className='object-cover w-full h-full'/>
</div>
<div
className={
(selected ? ' text-red-400 ' : 'dark:text-gray-400 ') +
' text-sm overflow-x-hidden hover:text-red-600 px-2 duration-200 w-full rounded ' +
' hover:text-red-400 cursor-pointer items-center flex'
}
>
<div>
<div className='line-clamp-2 menu-link'>{post.title}</div>
<div className="text-gray-500">{post.lastEditedDay}</div>
</div>
</div>
</Link>)
)
})}
</>
}
export default LatestPostsGroup

View File

@@ -0,0 +1,8 @@
export default function LoadingCover () {
return (<div id="loading-cover" className={'md:-mt-20 flex-grow dark:text-white text-black animate__animated animate__fadeIn flex flex-col justify-center z-50 w-full h-screen container mx-auto'}>
<div className="mx-auto">
<i className="fas fa-spinner animate-spin"/>
</div>
</div>
)
}

View File

@@ -0,0 +1,20 @@
import Link from 'next/link'
// import { siteConfig } from '@/lib/config'
import LazyImage from '@/components/LazyImage';
/**
* Logo图标
* @param {*} props
* @returns
*/
export default function LogoBar (props) {
const { siteInfo } = props
return (
<div id='top-wrapper' className='w-full flex items-center'>
<Link href='/' className='text-md md:text-xl dark:text-gray-200 r'>
<LazyImage className='h-12 mr-3' src={siteInfo?.icon}/>
</Link>
{/* <div>{siteConfig('TITLE')}</div> */}
</div>
);
}

View File

@@ -0,0 +1,36 @@
import { useGlobal } from '@/lib/global'
import CONFIG from '../config'
import { MenuItemCollapse } from './MenuItemCollapse'
import { siteConfig } from '@/lib/config'
export const MenuBarMobile = (props) => {
const { customMenu, customNav } = props
const { locale } = useGlobal()
let links = [
// { name: locale.NAV.INDEX, to: '/' || '/', show: true },
{ name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY },
{ name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG },
{ name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE }
// { name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH }
]
if (customNav) {
links = links.concat(customNav)
}
// 如果 开启自定义菜单,则不再使用 Page生成菜单。
if (siteConfig('CUSTOM_MENU')) {
links = customMenu
}
if (!links || links.length === 0) {
return null
}
return (
<nav id='nav' className=' text-md'>
{links?.map(link => <MenuItemCollapse onHeightChange={props.onHeightChange} key={link?.id} link={link}/>)}
</nav>
)
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,42 @@
import { useGlobal } from '@/lib/global'
import { siteConfig } from '@/lib/config'
import { MenuItemCollapse } from './MenuItemCollapse'
import CONFIG from '../config'
export const MenuListSide = (props) => {
const { customNav, customMenu } = props
const { locale } = useGlobal()
let links = [
{ icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE },
{ icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH },
{ icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY },
{ icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG }
]
if (customNav) {
links = customNav.concat(links)
}
for (let i = 0; i < links.length; i++) {
if (links[i].id !== i) {
links[i].id = i
}
}
// 如果 开启自定义菜单则覆盖Page生成的菜单
if (siteConfig('CUSTOM_MENU')) {
links = customMenu
}
if (!links || links.length === 0) {
return null
}
return (
<nav>
{/* {links.map(link => <MenuItemNormal key={link?.id} link={link} />)} */}
{links?.map(link => <MenuItemCollapse key={link?.id} link={link} />)}
</nav>
)
}

View File

@@ -0,0 +1,42 @@
import { useGlobal } from '@/lib/global'
import CONFIG from '../config'
import { MenuItemDrop } from './MenuItemDrop'
import { siteConfig } from '@/lib/config'
export const MenuListTop = (props) => {
const { customNav, customMenu } = props
const { locale } = useGlobal()
let links = [
{ id: 1, icon: 'fa-solid fa-house', name: locale.NAV.INDEX, to: '/', show: CONFIG.MENU_INDEX },
{ id: 2, icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH },
{ id: 3, icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE }
// { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY },
// { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG }
]
if (customNav) {
links = links.concat(customNav)
}
for (let i = 0; i < links.length; i++) {
if (links[i].id !== i) {
links[i].id = i
}
}
// 如果 开启自定义菜单则覆盖Page生成的菜单
if (siteConfig('CUSTOM_MENU')) {
links = customMenu
}
if (!links || links.length === 0) {
return null
}
return (<>
<nav id='nav-mobile' className='leading-8 justify-center font-light w-full flex'>
{links?.map(link => link && link.show && <MenuItemDrop key={link?.id} link={link} />)}
</nav>
</>)
}

View File

@@ -0,0 +1,31 @@
import Link from 'next/link'
/**
* 首页导航大按钮组件
* @param {*} props
* @returns
*/
const NavButtonGroup = (props) => {
const { categoryOptions } = props
if (!categoryOptions || categoryOptions.length === 0) {
return <></>
}
return (
<nav id='home-nav-button' className={'w-full z-10 md:h-72 md:mt-6 xl:mt-32 px-5 py-2 mt-8 flex flex-wrap md:max-w-6xl space-y-2 md:space-y-0 md:flex justify-center max-h-80 overflow-auto'}>
{categoryOptions?.map(category => {
return (
<Link
key={`${category.name}`}
title={`${category.name}`}
href={`/category/${category.name}`}
passHref
className='text-center shadow-text w-full sm:w-4/5 md:mx-6 md:w-40 md:h-14 lg:h-20 h-14 justify-center items-center flex border-2 cursor-pointer rounded-lg glassmorphism hover:bg-white hover:text-black duration-200 hover:scale-105 transform'>
{category.name}
</Link>
)
})}
</nav>
)
}
export default NavButtonGroup

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