diff --git a/.env.local b/.env.local
index 2dacdbef..cfe09402 100644
--- a/.env.local
+++ b/.env.local
@@ -1,5 +1,5 @@
# 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables
-NEXT_PUBLIC_VERSION=4.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=
diff --git a/.eslintrc.js b/.eslintrc.js
index 9836da77..c6fbb20a 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -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'
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 56795ea7..abffa38d 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -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}}"
\ No newline at end of file
diff --git a/.github/workflows/sync.yaml b/.github/workflows/sync.yaml
new file mode 100644
index 00000000..038eff8d
--- /dev/null
+++ b/.github/workflows/sync.yaml
@@ -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
diff --git a/.prettierrc.json b/.prettierrc.json
index 5f5ab6df..213aca02 100644
--- a/.prettierrc.json
+++ b/.prettierrc.json
@@ -2,5 +2,9 @@
"singleQuote": true,
"semi": false,
"trailingComma": "none",
- "arrowParens": "avoid"
+ "arrowParens": "avoid",
+ "printWidth": 80,
+ "bracketSpacing": true,
+ "jsxSingleQuote": true,
+ "jsxBracketSameLine": true
}
diff --git a/blog.config.js b/blog.config.js
index 746829cb..df30298b 100644
--- a/blog.config.js
+++ b/blog.config.js
@@ -1,8 +1,7 @@
// 注: process.env.XX是Vercel的环境变量,配置方式见:https://docs.tangly1024.com/article/how-to-config-notion-next#c4768010ae7d44609b744e79e2f9959a
const BLOG = {
// Important page_id!!!Duplicate Template from https://www.notion.so/tanghh/02ab3b8678004aa69e9e415905ef32a5
- NOTION_PAGE_ID:
- process.env.NOTION_PAGE_ID || '02ab3b8678004aa69e9e415905ef32a5',
+ NOTION_PAGE_ID: process.env.NOTION_PAGE_ID || '02ab3b8678004aa69e9e415905ef32a5',
PSEUDO_STATIC: process.env.NEXT_PUBLIC_PSEUDO_STATIC || false, // 伪静态路径,开启后所有文章URL都以 .html 结尾。
NEXT_REVALIDATE_SECOND: process.env.NEXT_PUBLIC_REVALIDATE_SECOND || 5, // 更新内容缓存间隔 单位(秒);即每个页面有5秒的纯静态期、此期间无论多少次访问都不会抓取notion数据;调大该值有助于节省Vercel资源、同时提升访问速率,但也会使文章更新有延迟。
THEME: process.env.NEXT_PUBLIC_THEME || 'simple', // 当前主题,在themes文件夹下可找到所有支持的主题;主题名称就是文件夹名,例如 example,fukasawa,gitbook,heo,hexo,landing,matery,medium,next,nobelium,plog,simple
@@ -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 | 0(0:禁用 lrc 歌词,1:lrc 格式的字符串,3:lrc 文件 url)(前提是有配置歌词路径,对 meting 无效)
MUSIC_PLAYER_CDN_URL:
- process.env.NEXT_PUBLIC_MUSIC_PLAYER_CDN_URL ||
- 'https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/aplayer/1.10.1/APlayer.min.js',
+ process.env.NEXT_PUBLIC_MUSIC_PLAYER_CDN_URL ||
+ 'https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/aplayer/1.10.1/APlayer.min.js',
MUSIC_PLAYER_ORDER: process.env.NEXT_PUBLIC_MUSIC_PLAYER_ORDER || 'list', // 默认播放方式,顺序 list,随机 random
MUSIC_PLAYER_AUDIO_LIST: [
// 示例音乐列表。除了以下配置外,还可配置歌词,具体配置项看此文档 https://aplayer.js.org/#/zh-Hans/
@@ -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 | 0(0:禁用 lrc 歌词,1:lrc 格式的字符串,3:lrc 文件 url)
+ MUSIC_PLAYER_METING_SERVER: process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_SERVER || 'netease', // 音乐平台,[netease, tencent, kugou, xiami, baidu]
+ MUSIC_PLAYER_METING_ID: process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_ID || '60198', // 对应歌单的 id
+ MUSIC_PLAYER_METING_LRC_TYPE: process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_LRC_TYPE || '1', // 可选值: 3 | 1 | 0(0:禁用 lrc 歌词,1:lrc 格式的字符串,3:lrc 文件 url)
// ********挂件组件相关********
// ----> 评论互动 可同时开启多个支持 WALINE VALINE GISCUS CUSDIS UTTERRANCES GITALK
@@ -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后端地址 腾讯云环境填envId;Vercel环境填域名,教程:https://tangly1024.com/article/notionnext-twikoo
COMMENT_TWIKOO_COUNT_ENABLE: process.env.NEXT_PUBLIC_COMMENT_TWIKOO_COUNT_ENABLE || false, // 博客列表是否显示评论数
- COMMENT_TWIKOO_CDN_URL: process.env.NEXT_PUBLIC_COMMENT_TWIKOO_CDN_URL || 'https://cdn.staticfile.org/twikoo/1.6.17/twikoo.min.js', // twikoo客户端cdn
+ COMMENT_TWIKOO_CDN_URL:
+ process.env.NEXT_PUBLIC_COMMENT_TWIKOO_CDN_URL || 'https://cdn.staticfile.org/twikoo/1.6.17/twikoo.min.js', // twikoo客户端cdn
// utterance
- COMMENT_UTTERRANCES_REPO:
- process.env.NEXT_PUBLIC_COMMENT_UTTERRANCES_REPO || '', // 你的代码仓库名, 例如我是 'tangly1024/NotionNext'; 更多文档参考 https://utteranc.es/
+ COMMENT_UTTERRANCES_REPO: process.env.NEXT_PUBLIC_COMMENT_UTTERRANCES_REPO || '', // 你的代码仓库名, 例如我是 'tangly1024/NotionNext'; 更多文档参考 https://utteranc.es/
// giscus @see https://giscus.app/
COMMENT_GISCUS_REPO: process.env.NEXT_PUBLIC_COMMENT_GISCUS_REPO || '', // 你的Github仓库名 e.g 'tangly1024/NotionNext'
COMMENT_GISCUS_REPO_ID: process.env.NEXT_PUBLIC_COMMENT_GISCUS_REPO_ID || '', // 你的Github Repo ID e.g ( 設定完 giscus 即可看到 )
- COMMENT_GISCUS_CATEGORY_ID:
- process.env.NEXT_PUBLIC_COMMENT_GISCUS_CATEGORY_ID || '', // 你的Github Discussions 內的 Category ID ( 設定完 giscus 即可看到 )
- COMMENT_GISCUS_MAPPING:
- process.env.NEXT_PUBLIC_COMMENT_GISCUS_MAPPING || 'pathname', // 你的Github Discussions 使用哪種方式來標定文章, 預設 'pathname'
- COMMENT_GISCUS_REACTIONS_ENABLED:
- process.env.NEXT_PUBLIC_COMMENT_GISCUS_REACTIONS_ENABLED || '1', // 你的 Giscus 是否開啟文章表情符號 '1' 開啟 "0" 關閉 預設開啟
- COMMENT_GISCUS_EMIT_METADATA:
- process.env.NEXT_PUBLIC_COMMENT_GISCUS_EMIT_METADATA || '0', // 你的 Giscus 是否提取 Metadata '1' 開啟 '0' 關閉 預設關閉
- COMMENT_GISCUS_INPUT_POSITION:
- process.env.NEXT_PUBLIC_COMMENT_GISCUS_INPUT_POSITION || 'bottom', // 你的 Giscus 發表留言位置 'bottom' 尾部 'top' 頂部, 預設 'bottom'
+ COMMENT_GISCUS_CATEGORY_ID: process.env.NEXT_PUBLIC_COMMENT_GISCUS_CATEGORY_ID || '', // 你的Github Discussions 內的 Category ID ( 設定完 giscus 即可看到 )
+ COMMENT_GISCUS_MAPPING: process.env.NEXT_PUBLIC_COMMENT_GISCUS_MAPPING || 'pathname', // 你的Github Discussions 使用哪種方式來標定文章, 預設 'pathname'
+ COMMENT_GISCUS_REACTIONS_ENABLED: process.env.NEXT_PUBLIC_COMMENT_GISCUS_REACTIONS_ENABLED || '1', // 你的 Giscus 是否開啟文章表情符號 '1' 開啟 "0" 關閉 預設開啟
+ COMMENT_GISCUS_EMIT_METADATA: process.env.NEXT_PUBLIC_COMMENT_GISCUS_EMIT_METADATA || '0', // 你的 Giscus 是否提取 Metadata '1' 開啟 '0' 關閉 預設關閉
+ COMMENT_GISCUS_INPUT_POSITION: process.env.NEXT_PUBLIC_COMMENT_GISCUS_INPUT_POSITION || 'bottom', // 你的 Giscus 發表留言位置 'bottom' 尾部 'top' 頂部, 預設 'bottom'
COMMENT_GISCUS_LANG: process.env.NEXT_PUBLIC_COMMENT_GISCUS_LANG || 'zh-CN', // 你的 Giscus 語言 e.g 'en', 'zh-TW', 'zh-CN', 預設 'en'
- COMMENT_GISCUS_LOADING:
- process.env.NEXT_PUBLIC_COMMENT_GISCUS_LOADING || 'lazy', // 你的 Giscus 載入是否漸進式載入, 預設 'lazy'
- COMMENT_GISCUS_CROSSORIGIN:
- process.env.NEXT_PUBLIC_COMMENT_GISCUS_CROSSORIGIN || 'anonymous', // 你的 Giscus 可以跨網域, 預設 'anonymous'
+ COMMENT_GISCUS_LOADING: process.env.NEXT_PUBLIC_COMMENT_GISCUS_LOADING || 'lazy', // 你的 Giscus 載入是否漸進式載入, 預設 'lazy'
+ COMMENT_GISCUS_CROSSORIGIN: process.env.NEXT_PUBLIC_COMMENT_GISCUS_CROSSORIGIN || 'anonymous', // 你的 Giscus 可以跨網域, 預設 'anonymous'
COMMENT_CUSDIS_APP_ID: process.env.NEXT_PUBLIC_COMMENT_CUSDIS_APP_ID || '', // data-app-id 36位 see https://cusdis.com/
- COMMENT_CUSDIS_HOST:
- process.env.NEXT_PUBLIC_COMMENT_CUSDIS_HOST || 'https://cusdis.com', // data-host, change this if you're using self-hosted version
- COMMENT_CUSDIS_SCRIPT_SRC:
- process.env.NEXT_PUBLIC_COMMENT_CUSDIS_SCRIPT_SRC ||
- '/js/cusdis.es.js', // change this if you're using self-hosted version
+ COMMENT_CUSDIS_HOST: process.env.NEXT_PUBLIC_COMMENT_CUSDIS_HOST || 'https://cusdis.com', // data-host, change this if you're using self-hosted version
+ COMMENT_CUSDIS_SCRIPT_SRC: process.env.NEXT_PUBLIC_COMMENT_CUSDIS_SCRIPT_SRC || '/js/cusdis.es.js', // change this if you're using self-hosted version
// gitalk评论插件 更多参考 https://gitalk.github.io/
COMMENT_GITALK_REPO: process.env.NEXT_PUBLIC_COMMENT_GITALK_REPO || '', // 你的Github仓库名,例如 'NotionNext'
COMMENT_GITALK_OWNER: process.env.NEXT_PUBLIC_COMMENT_GITALK_OWNER || '', // 你的用户名 e.g tangly1024
COMMENT_GITALK_ADMIN: process.env.NEXT_PUBLIC_COMMENT_GITALK_ADMIN || '', // 管理员用户名、一般是自己 e.g 'tangly1024'
- COMMENT_GITALK_CLIENT_ID:
- process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_ID || '', // e.g 20位ID , 在gitalk后台获取
- COMMENT_GITALK_CLIENT_SECRET:
- process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_SECRET || '', // e.g 40位ID, 在gitalk后台获取
+ COMMENT_GITALK_CLIENT_ID: process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_ID || '', // e.g 20位ID , 在gitalk后台获取
+ COMMENT_GITALK_CLIENT_SECRET: process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_SECRET || '', // e.g 40位ID, 在gitalk后台获取
COMMENT_GITALK_DISTRACTION_FREE_MODE: false, // 类似facebook的无干扰模式
- COMMENT_GITALK_JS_CDN_URL: process.env.NEXT_PUBLIC_COMMENT_GITALK_JS_CDN_URL || 'https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.min.js', // gitalk客户端 js cdn
- COMMENT_GITALK_CSS_CDN_URL: process.env.NEXT_PUBLIC_COMMENT_GITALK_CSS_CDN_URL || 'https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css', // gitalk客户端 css cdn
+ COMMENT_GITALK_JS_CDN_URL:
+ process.env.NEXT_PUBLIC_COMMENT_GITALK_JS_CDN_URL || 'https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.min.js', // gitalk客户端 js cdn
+ COMMENT_GITALK_CSS_CDN_URL:
+ process.env.NEXT_PUBLIC_COMMENT_GITALK_CSS_CDN_URL || 'https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css', // gitalk客户端 css cdn
COMMENT_GITTER_ROOM: process.env.NEXT_PUBLIC_COMMENT_GITTER_ROOM || '', // gitter聊天室 see https://gitter.im/ 不需要则留空
COMMENT_DAO_VOICE_ID: process.env.NEXT_PUBLIC_COMMENT_DAO_VOICE_ID || '', // DaoVoice http://dashboard.daovoice.io/get-started
@@ -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 // 版本号
diff --git a/components/AlgoliaSearchModal.js b/components/AlgoliaSearchModal.js
index f7bcdeb9..ea727a48 100644
--- a/components/AlgoliaSearchModal.js
+++ b/components/AlgoliaSearchModal.js
@@ -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 }) {
{/* 模态框 */}
搜索
@@ -205,21 +223,23 @@ export default function AlgoliaSearchModal({ cRef }) {
{
- searchResults.length === 0 && keyword && (
+ searchResults.length === 0 && keyword && !isLoading && (
-
无法找到相关结果
+
无法找到相关结果
"{keyword}"
)
}
-
+
{searchResults.map((result, index) => (
- 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' : ''}`}>
@@ -229,7 +249,16 @@ export default function AlgoliaSearchModal({ cRef }) {
))}
-
+
+ {totalHit === 0 && (
+ {
+ ShortCutActions.map((action, index) => {
+ return
{action.key}
+ {action.action}
+ })
+ }
+
)
+ }
{totalHit > 0 && (
@@ -237,7 +266,7 @@ export default function AlgoliaSearchModal({ cRef }) {
)}
-
+
Algolia 提供搜索服务
@@ -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 (
+ {/* 红点 */}
+
+
+
+ >
+}
diff --git a/components/Busuanzi.js b/components/Busuanzi.js
index cbeb54da..ebdab36f 100644
--- a/components/Busuanzi.js
+++ b/components/Busuanzi.js
@@ -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'
diff --git a/components/CustomContextMenu.js b/components/CustomContextMenu.js
index 4398c331..46ffb2a0 100644
--- a/components/CustomContextMenu.js
+++ b/components/CustomContextMenu.js
@@ -151,20 +151,20 @@ export default function CustomContextMenu(props) {
{/* 跳转导航按钮 */}
-
+ {siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_RANDOM_POST') &&
{locale.MENU.WALK_AROUND}
-
+
}
-
+ {siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_CATEGORY') &&
{locale.MENU.CATEGORY}
-
+ }
-
+ {siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_TAG') &&
{locale.MENU.TAGS}
-
+ }
@@ -180,15 +180,16 @@ export default function CustomContextMenu(props) {
)}
-
+ {siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_SHARE_LINK') &&
{locale.MENU.SHARE_URL}
-
+
}
-
+ {siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_DARK_MODE') &&
{isDarkMode ?
:
}
{isDarkMode ? locale.MENU.LIGHT_MODE : locale.MENU.DARK_MODE}
-
+
}
+
{siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH') && (
diff --git a/components/Draggable.js b/components/Draggable.js
index 44d565b8..cf7be63a 100644
--- a/components/Draggable.js
+++ b/components/Draggable.js
@@ -1,10 +1,10 @@
-import { useRef, useEffect, useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
+
/**
* 可拖拽组件
*/
-
-export const Draggable = (props) => {
- const { children } = props
+export const Draggable = props => {
+ const { children, stick } = props
const draggableRef = useRef(null)
const rafRef = useRef(null)
const [moving, setMoving] = useState(false)
@@ -14,8 +14,10 @@ export const Draggable = (props) => {
const draggableElements = document.getElementsByClassName('draggable')
// 标准化鼠标事件对象
- function e(event) { // 定义事件对象标准化函数
- if (!event) { // 兼容IE浏览器
+ function e(event) {
+ // 定义事件对象标准化函数
+ if (!event) {
+ // 兼容IE浏览器
event = window.event
event.target = event.srcElement
event.layerX = event.offsetX
@@ -40,9 +42,10 @@ export const Draggable = (props) => {
document.onmousedown = start
document.ontouchstart = start
- function start (event) { // 按下鼠标时,初始化处理
+ function start(event) {
+ // 按下鼠标时,初始化处理
if (!draggableElements) return
- event = e(event)// 获取标准事件对象
+ event = e(event) // 获取标准事件对象
for (const drag of draggableElements) {
// 判断鼠标点击的区域是否是拖拽框内
@@ -60,19 +63,20 @@ export const Draggable = (props) => {
offsetX = event.mx - currentObj.offsetLeft
offsetY = event.my - currentObj.offsetTop
- document.onmousemove = move// 注册鼠标移动事件处理函数
+ document.onmousemove = move // 注册鼠标移动事件处理函数
document.ontouchmove = move
- document.onmouseup = stop// 注册松开鼠标事件处理函数
+ document.onmouseup = stop // 注册松开鼠标事件处理函数
document.ontouchend = stop
}
}
- function move(event) { // 鼠标移动处理函数
+ function move(event) {
+ // 鼠标移动处理函数
event = e(event)
rafRef.current = requestAnimationFrame(() => updatePosition(event))
}
- const stop = (event) => {
+ const stop = event => {
event = e(event)
document.documentElement.style.overflow = 'auto' // 恢复默认的滚动行为
cancelAnimationFrame(rafRef.current)
@@ -80,7 +84,7 @@ export const Draggable = (props) => {
currentObj = document.ontouchmove = document.ontouchend = document.onmousemove = document.onmouseup = null
}
- const updatePosition = (event) => {
+ const updatePosition = event => {
if (currentObj) {
const left = event.mx - offsetX
const top = event.my - offsetY
@@ -120,15 +124,18 @@ export const Draggable = (props) => {
if (offsetTop < 0) {
drag.firstElementChild.style.top = 0
}
- if (offsetTop > (clientHeight - offsetHeight)) {
+ if (offsetTop > clientHeight - offsetHeight) {
drag.firstElementChild.style.top = clientHeight - offsetHeight + 'px'
}
if (offsetLeft < 0) {
drag.firstElementChild.style.left = 0
}
- if (offsetLeft > (clientWidth - offsetWidth)) {
+ if (offsetLeft > clientWidth - offsetWidth) {
drag.firstElementChild.style.left = clientWidth - offsetWidth + 'px'
}
+ if (stick === 'left') {
+ drag.firstElementChild.style.left = 0 + 'px'
+ }
}
}
@@ -142,9 +149,11 @@ export const Draggable = (props) => {
}
}, [])
- return
- {children}
-
+ return (
+
+ {children}
+
+ )
}
Draggable.defaultProps = { left: 0, top: 0 }
diff --git a/components/ExternalPlugins.js b/components/ExternalPlugins.js
index 78da3094..3df09e6c 100644
--- a/components/ExternalPlugins.js
+++ b/components/ExternalPlugins.js
@@ -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) {
diff --git a/components/GlobalHead.js b/components/GlobalHead.js
index 4723c218..86368328 100644
--- a/components/GlobalHead.js
+++ b/components/GlobalHead.js
@@ -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 (
-
-
{title}
-
-
-
-
- {siteConfig('SEO_GOOGLE_SITE_VERIFICATION') && (
-
- )}
- {siteConfig('SEO_BAIDU_SITE_VERIFICATION') && (
)}
-
-
-
-
-
-
-
-
-
-
-
-
+
+
{title}
+
+
+
+
+ {siteConfig('SEO_GOOGLE_SITE_VERIFICATION') && (
+
+ )}
+ {siteConfig('SEO_BAIDU_SITE_VERIFICATION') && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
- {siteConfig('COMMENT_WEBMENTION_ENABLE') && (
- <>
-
-
- >
- )}
+ {siteConfig('COMMENT_WEBMENTION_ENABLE') && (
+ <>
+
+
+ >
+ )}
- {siteConfig('COMMENT_WEBMENTION_ENABLE') && siteConfig('COMMENT_WEBMENTION_AUTH') !== '' && (
-
- )}
+ {siteConfig('COMMENT_WEBMENTION_ENABLE') && siteConfig('COMMENT_WEBMENTION_AUTH') !== '' && (
+
+ )}
- {JSON.parse(siteConfig('ANALYTICS_BUSUANZI_ENABLE')) &&
}
- {meta?.type === 'Post' && (
- <>
-
-
-
-
- >
- )}
- {children}
-
+ {JSON.parse(siteConfig('ANALYTICS_BUSUANZI_ENABLE')) && (
+
+ )}
+ {meta?.type === 'Post' && (
+ <>
+
+
+
+
+ >
+ )}
+ {children}
+
)
}
@@ -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
}
diff --git a/components/GoogleAdsense.js b/components/GoogleAdsense.js
index 61606150..2add89e6 100644
--- a/components/GoogleAdsense.js
+++ b/components/GoogleAdsense.js
@@ -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
+ return (
+
+ )
}
// 信息流广告
if (type === 'flow') {
- return
+ return (
+
+ )
}
// 原生广告
if (type === 'native') {
- return
+ return (
+
+ )
}
// 展示广告
- return
+ return (
+
+ )
}
-export { AdSlot }
+/**
+ * 嵌入到文章内部的广告单元
+ * 检测文本内容 出现
关键词时自动替换为广告
+ * @param {*} props
+ */
+const AdEmbed = () => {
+ useEffect(() => {
+ setTimeout(() => {
+ // 找到所有 class 为 notion-text 且内容为 '
' 的 div 元素
+ const notionTextElements = document.querySelectorAll('div.notion-text')
+
+ // 遍历找到的元素
+ notionTextElements?.forEach(element => {
+ // 检查元素的内容是否为 '
'
+ if (element.innerHTML.trim() === '<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')
+
+ // 用新创建的 元素替换掉原来的 div 元素
+ element?.parentNode?.replaceChild(newInsElement, element)
+ }
+ })
+
+ requestAd()
+ }, 1000)
+ }, [])
+ return <>>
+}
+
+export { AdEmbed, AdSlot }
diff --git a/components/Gtag.js b/components/Gtag.js
index 0314b55f..445b6e1e 100644
--- a/components/Gtag.js
+++ b/components/Gtag.js
@@ -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()
diff --git a/components/NotionPage.js b/components/NotionPage.js
index b3be2174..9e0e9c24 100644
--- a/components/NotionPage.js
+++ b/components/NotionPage.js
@@ -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
@@ -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
+ )
}
/**
diff --git a/components/PWA.js b/components/PWA.js
new file mode 100644
index 00000000..118a7c18
--- /dev/null
+++ b/components/PWA.js
@@ -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
+// }
+// }
diff --git a/components/Player.js b/components/Player.js
index 8c252615..3c741f94 100644
--- a/components/Player.js
+++ b/components/Player.js
@@ -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 (
- {meting
- ?
- :
- }
+ {meting ? (
+
+ ) : (
+
+ )}
)
}
diff --git a/components/WalineComponent.js b/components/WalineComponent.js
index 37247d48..960f153f 100644
--- a/components/WalineComponent.js
+++ b/components/WalineComponent.js
@@ -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,
diff --git a/lib/config.js b/lib/config.js
index 25211f30..546597c2 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -17,7 +17,7 @@ import { deepClone } from './utils'
export const siteConfig = (key, defaultVal = null, extendConfig = null) => {
let global = null
try {
- const isClient = typeof window !== 'undefined';
+ const isClient = typeof window !== 'undefined'
// eslint-disable-next-line react-hooks/rules-of-hooks
global = isClient ? useGlobal() : {}
// eslint-disable-next-line react-hooks/rules-of-hooks
@@ -67,15 +67,19 @@ export const siteConfig = (key, defaultVal = null, extendConfig = null) => {
} else {
if (typeof val === 'string') {
if (val === 'true' || val === 'false') {
- return JSON.parse(val);
+ return JSON.parse(val)
}
- return val;
+ if (/^\d+$/.test(val)) {
+ // 如果是数字,使用parseFloat或者parseInt将字符串转换为数字
+ return parseInt(val)
+ }
+ return val
} else {
try {
- return JSON.parse(val);
+ return JSON.parse(val)
} catch (error) {
// 如果值是一个字符串但不是有效的 JSON 格式,直接返回字符串
- return val;
+ return val
}
}
}
diff --git a/lib/notion/getNotionData.js b/lib/db/getSiteData.js
similarity index 68%
rename from lib/notion/getNotionData.js
rename to lib/db/getSiteData.js
index 814f4076..e7771025 100755
--- a/lib/notion/getNotionData.js
+++ b/lib/db/getSiteData.js
@@ -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 })
diff --git a/lib/global.js b/lib/global.js
index 4eadfa19..77b94e2d 100644
--- a/lib/global.js
+++ b/lib/global.js
@@ -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 (
-
- {children}
-
+
+ {children}
+
)
}
diff --git a/lib/lang.js b/lib/lang.js
index eb6a8890..60b80882 100644
--- a/lib/lang.js
+++ b/lib/lang.js
@@ -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'
/**
* 在这里配置所有支持的语言
diff --git a/lib/memorize.js b/lib/memorize.js
deleted file mode 100644
index e69de29b..00000000
diff --git a/lib/notion.js b/lib/notion.js
deleted file mode 100644
index 0042487f..00000000
--- a/lib/notion.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export { getAllTags } from './notion/getAllTags'
-export { getPostBlocks } from './notion/getPostBlocks'
diff --git a/lib/notion/getAllPosts.js b/lib/notion/getAllPosts.js
index 5d63107a..dd221234 100644
--- a/lib/notion/getAllPosts.js
+++ b/lib/notion/getAllPosts.js
@@ -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'
/**
diff --git a/lib/notion/getAllTags.js b/lib/notion/getAllTags.js
index dfc92ac7..4c7559b9 100644
--- a/lib/notion/getAllTags.js
+++ b/lib/notion/getAllTags.js
@@ -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 })
+ }
+ })
+ }
}
// 按照数量排序
diff --git a/lib/notion/getNotion.js b/lib/notion/getNotionPost.js
similarity index 93%
rename from lib/notion/getNotion.js
rename to lib/notion/getNotionPost.js
index aad65379..beb3bf56 100644
--- a/lib/notion/getNotion.js
+++ b/lib/notion/getNotionPost.js
@@ -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
diff --git a/lib/notion/getPageProperties.js b/lib/notion/getPageProperties.js
index 5caf9368..fe93146a 100644
--- a/lib/notion/getPageProperties.js
+++ b/lib/notion/getPageProperties.js
@@ -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}`
}
diff --git a/lib/notion/getPostBlocks.js b/lib/notion/getPostBlocks.js
index 9210e0d1..c9652370 100644
--- a/lib/notion/getPostBlocks.js
+++ b/lib/notion/getPostBlocks.js
@@ -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)
diff --git a/lib/algolia.js b/lib/plugins/algolia.js
similarity index 100%
rename from lib/algolia.js
rename to lib/plugins/algolia.js
diff --git a/lib/busuanzi.js b/lib/plugins/busuanzi.js
similarity index 100%
rename from lib/busuanzi.js
rename to lib/plugins/busuanzi.js
diff --git a/lib/gtag.js b/lib/plugins/gtag.js
similarity index 100%
rename from lib/gtag.js
rename to lib/plugins/gtag.js
diff --git a/lib/mailchimp.js b/lib/plugins/mailchimp.js
similarity index 100%
rename from lib/mailchimp.js
rename to lib/plugins/mailchimp.js
diff --git a/lib/mhchem.js b/lib/plugins/mhchem.js
similarity index 100%
rename from lib/mhchem.js
rename to lib/plugins/mhchem.js
diff --git a/lib/wow.js b/lib/plugins/wow.js
similarity index 78%
rename from lib/wow.js
rename to lib/plugins/wow.js
index e4682e46..af0b4920 100644
--- a/lib/wow.js
+++ b/lib/plugins/wow.js
@@ -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;
diff --git a/lib/rss.js b/lib/rss.js
index fff81f7c..5757b0f8 100644
--- a/lib/rss.js
+++ b/lib/rss.js
@@ -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'
/**
diff --git a/lib/formatDate.js b/lib/utils/formatDate.js
similarity index 100%
rename from lib/formatDate.js
rename to lib/utils/formatDate.js
diff --git a/lib/utils.js b/lib/utils/index.js
similarity index 76%
rename from lib/utils.js
rename to lib/utils/index.js
index d632e1c3..b0652494 100644
--- a/lib/utils.js
+++ b/lib/utils/index.js
@@ -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
}
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)
}
}
}
diff --git a/next.config.js b/next.config.js
index fd0130ec..c4e9d8e4 100644
--- a/next.config.js
+++ b/next.config.js
@@ -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
}
diff --git a/package.json b/package.json
index 69a231e2..5a4a8cce 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
-}
\ No newline at end of file
+}
diff --git a/pages/404.js b/pages/404.js
index 49ec11df..a4746405 100644
--- a/pages/404.js
+++ b/pages/404.js
@@ -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'
diff --git a/pages/[prefix]/[slug]/[...suffix].js b/pages/[prefix]/[slug]/[...suffix].js
index 7fc2b0a1..3ce0b105 100644
--- a/pages/[prefix]/[slug]/[...suffix].js
+++ b/pages/[prefix]/[slug]/[...suffix].js
@@ -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
+ return
}
/**
@@ -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
diff --git a/pages/[prefix]/[slug]/index.js b/pages/[prefix]/[slug]/index.js
index 9eefb1a9..147d9ecb 100644
--- a/pages/[prefix]/[slug]/index.js
+++ b/pages/[prefix]/[slug]/index.js
@@ -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
+ return
}
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
diff --git a/pages/[prefix]/index.js b/pages/[prefix]/index.js
index 91f1ec76..567f0244 100644
--- a/pages/[prefix]/index.js
+++ b/pages/[prefix]/index.js
@@ -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
diff --git a/pages/_app.js b/pages/_app.js
index 9b6a74bc..cf71fe18 100644
--- a/pages/_app.js
+++ b/pages/_app.js
@@ -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'
diff --git a/pages/api/subscribe.js b/pages/api/subscribe.js
index 86adddf9..d2d77894 100644
--- a/pages/api/subscribe.js
+++ b/pages/api/subscribe.js
@@ -1,4 +1,4 @@
-import subscribeToMailchimpApi from '@/lib/mailchimp'
+import subscribeToMailchimpApi from '@/lib/plugins/mailchimp'
/**
* 接受邮件订阅
diff --git a/pages/archive/index.js b/pages/archive/index.js
index bbcc718c..13bc905b 100644
--- a/pages/archive/index.js
+++ b/pages/archive/index.js
@@ -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 => {
diff --git a/pages/category/[category]/index.js b/pages/category/[category]/index.js
index 4056e9a1..718d2b42 100644
--- a/pages/category/[category]/index.js
+++ b/pages/category/[category]/index.js
@@ -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
diff --git a/pages/category/[category]/page/[page].js b/pages/category/[category]/page/[page].js
index 85ee8e69..212fa8fa 100644
--- a/pages/category/[category]/page/[page].js
+++ b/pages/category/[category]/page/[page].js
@@ -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 } })
diff --git a/pages/category/index.js b/pages/category/index.js
index bd32e187..e4532cbc 100644
--- a/pages/category/index.js
+++ b/pages/category/index.js
@@ -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'
diff --git a/pages/index.js b/pages/index.js
index 83f13da0..9f2dfb91 100644
--- a/pages/index.js
+++ b/pages/index.js
@@ -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
}
@@ -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')
+ )
}
}
diff --git a/pages/page/[page].js b/pages/page/[page].js
index 57e61068..9b1278b6 100644
--- a/pages/page/[page].js
+++ b/pages/page/[page].js
@@ -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'))
}
}
diff --git a/pages/search/[keyword]/index.js b/pages/search/[keyword]/index.js
index 22a33eda..0038c9f8 100644
--- a/pages/search/[keyword]/index.js
+++ b/pages/search/[keyword]/index.js
@@ -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'
/**
* 在内存缓存中进行全文索引
diff --git a/pages/search/[keyword]/page/[page].js b/pages/search/[keyword]/page/[page].js
index 6fcd3be8..43291b0b 100644
--- a/pages/search/[keyword]/page/[page].js
+++ b/pages/search/[keyword]/page/[page].js
@@ -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'
/**
* 在内存缓存中进行全文索引
diff --git a/pages/search/index.js b/pages/search/index.js
index 43695ed5..e41da00b 100644
--- a/pages/search/index.js
+++ b/pages/search/index.js
@@ -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'
diff --git a/pages/signin.js b/pages/signin.js
index 9abfb1f8..0fc037bc 100644
--- a/pages/signin.js
+++ b/pages/signin.js
@@ -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'
diff --git a/pages/signup.js b/pages/signup.js
index d570d46b..1fccbe8b 100644
--- a/pages/signup.js
+++ b/pages/signup.js
@@ -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'
diff --git a/pages/sitemap.xml.js b/pages/sitemap.xml.js
index 58c9a846..a0967ede 100644
--- a/pages/sitemap.xml.js
+++ b/pages/sitemap.xml.js
@@ -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) => {
diff --git a/pages/tag/[tag]/index.js b/pages/tag/[tag]/index.js
index f1da323e..ea4ffe6a 100644
--- a/pages/tag/[tag]/index.js
+++ b/pages/tag/[tag]/index.js
@@ -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
diff --git a/pages/tag/[tag]/page/[page].js b/pages/tag/[tag]/page/[page].js
index ce2951ab..4566b059 100644
--- a/pages/tag/[tag]/page/[page].js
+++ b/pages/tag/[tag]/page/[page].js
@@ -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 } })
diff --git a/pages/tag/index.js b/pages/tag/index.js
index 5866b978..2141242d 100644
--- a/pages/tag/index.js
+++ b/pages/tag/index.js
@@ -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'
diff --git a/styles/animate.css b/public/css/wow/animate.css
similarity index 99%
rename from styles/animate.css
rename to public/css/wow/animate.css
index c83e40e1..ae252ed1 100644
--- a/styles/animate.css
+++ b/public/css/wow/animate.css
@@ -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
diff --git a/public/dplayer.htm b/public/dplayer.htm
new file mode 100644
index 00000000..8e19a1d5
--- /dev/null
+++ b/public/dplayer.htm
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+ DPlayer Video Player
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/games-external/common/index.htm b/public/games-external/common/index.htm
new file mode 100644
index 00000000..0862930f
--- /dev/null
+++ b/public/games-external/common/index.htm
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+ Full Screen iFrame
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/js/fullscreen.js b/public/js/fullscreen.js
new file mode 100644
index 00000000..bc4767f7
--- /dev/null
+++ b/public/js/fullscreen.js
@@ -0,0 +1,32 @@
+window.toggleFullScreen = toggleFullScreen
+function toggleFullScreen() {
+ var iframe = document.getElementById('myIframe')
+
+ if (!document.fullscreenElement) {
+ if (iframe.requestFullscreen) {
+ iframe.requestFullscreen()
+ } else if (iframe.mozRequestFullScreen) {
+ /* Firefox */
+ iframe.mozRequestFullScreen()
+ } else if (iframe.webkitRequestFullscreen) {
+ /* Chrome, Safari and Opera */
+ iframe.webkitRequestFullscreen()
+ } else if (iframe.msRequestFullscreen) {
+ /* IE/Edge */
+ iframe.msRequestFullscreen()
+ }
+ } else {
+ if (document.exitFullscreen) {
+ document.exitFullscreen()
+ } else if (document.mozCancelFullScreen) {
+ /* Firefox */
+ document.mozCancelFullScreen()
+ } else if (document.webkitExitFullscreen) {
+ /* Chrome, Safari and Opera */
+ document.webkitExitFullscreen()
+ } else if (document.msExitFullscreen) {
+ /* IE/Edge */
+ document.msExitFullscreen()
+ }
+ }
+}
diff --git a/styles/globals.css b/styles/globals.css
index 65057e3e..2b8f568f 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -76,9 +76,13 @@ nav {
}
.shadow-card {
- box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, rgba(0, 0, 0, 0.07) 0px 2px 4px,
- rgba(0, 0, 0, 0.07) 0px 4px 8px, rgba(0, 0, 0, 0.07) 0px 8px 16px,
- rgba(0, 0, 0, 0.07) 0px 16px 32px, rgba(0, 0, 0, 0.07) 0px 32px 64px;
+ box-shadow:
+ rgba(0, 0, 0, 0.07) 0px 1px 2px,
+ rgba(0, 0, 0, 0.07) 0px 2px 4px,
+ rgba(0, 0, 0, 0.07) 0px 4px 8px,
+ rgba(0, 0, 0, 0.07) 0px 8px 16px,
+ rgba(0, 0, 0, 0.07) 0px 16px 32px,
+ rgba(0, 0, 0, 0.07) 0px 32px 64px;
}
.gt-meta {
@@ -106,7 +110,6 @@ nav {
backdrop-filter: blur(10px);
}
-
.medium-zoom-overlay {
background: none !important;
/* background: rgba(0, 0, 0, 0.01) none repeat scroll 0% 0% !important; */
@@ -157,7 +160,7 @@ nav {
/* twikoo 评论区超链接样式 */
.tk-main a {
- @apply text-blue-700
+ @apply text-blue-700;
}
/* twikoo 内置的 element-ui 加载样式 */
@@ -167,7 +170,7 @@ nav {
/* Webmention style */
.webmention-block {
- background: rgba(0, 116, 222, .2);
+ background: rgba(0, 116, 222, 0.2);
padding: 1rem 2rem;
border-radius: 5px;
}
@@ -176,11 +179,11 @@ nav {
font-style: italic;
font-weight: 700;
font-size: 16px;
- margin-bottom: .5rem;
+ margin-bottom: 0.5rem;
}
.webmention-block-intro a {
- color: #0000EE;
+ color: #0000ee;
text-decoration: underline;
}
@@ -197,14 +200,14 @@ nav {
.webmention-counts .count {
font-weight: bold;
- margin-right: .2rem;
+ margin-right: 0.2rem;
}
/* .webmention-counts .counts > span {
margin-right: .8rem;
} */
-.webmention-counts .counts>span:not(:last-child):after {
- content: " • ";
+.webmention-counts .counts > span:not(:last-child):after {
+ content: ' • ';
}
a.avatar-wrapper {
@@ -221,7 +224,7 @@ a.avatar-wrapper {
.avatar {
border-radius: 50%;
margin: 0;
- border: 3px solid rgba(0, 116, 222, .5);
+ border: 3px solid rgba(0, 116, 222, 0.5);
}
.replies {
@@ -235,7 +238,7 @@ a.avatar-wrapper {
position: relative;
padding: 0;
align-items: flex-start;
- margin-top: .6rem;
+ margin-top: 0.6rem;
}
.reply p {
@@ -255,4 +258,9 @@ a.avatar-wrapper {
user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
-}
\ No newline at end of file
+}
+
+.writing-vertical {
+ writing-mode: vertical-rl; /* 竖向排列从右向左 */
+ text-orientation: upright; /* 文字方向正常 */
+}
diff --git a/styles/notion.css b/styles/notion.css
index b78ab06b..e231c45f 100644
--- a/styles/notion.css
+++ b/styles/notion.css
@@ -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;
}
diff --git a/themes/commerce/components/AnalyticsCard.js b/themes/commerce/components/AnalyticsCard.js
new file mode 100644
index 00000000..0ee1e1cd
--- /dev/null
+++ b/themes/commerce/components/AnalyticsCard.js
@@ -0,0 +1,30 @@
+import Card from './Card'
+
+export function AnalyticsCard (props) {
+ const { postCount } = props
+ return
+
+ 统计
+
+
+
+}
diff --git a/themes/commerce/components/Announcement.js b/themes/commerce/components/Announcement.js
new file mode 100644
index 00000000..695c26a4
--- /dev/null
+++ b/themes/commerce/components/Announcement.js
@@ -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
+
+ {locale.COMMON.ANNOUNCEMENT}
+ {post && (
+
+
)}
+
+
+ } else {
+ return <>>
+ }
+}
+export default Announcement
diff --git a/themes/commerce/components/ArticleAdjacent.js b/themes/commerce/components/ArticleAdjacent.js
new file mode 100644
index 00000000..21ca9e32
--- /dev/null
+++ b/themes/commerce/components/ArticleAdjacent.js
@@ -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 (
+
+
+
+ {prev.title}
+
+
+
+ {next.title}
+
+
+
+
+ )
+}
diff --git a/themes/commerce/components/ArticleCopyright.js b/themes/commerce/components/ArticleCopyright.js
new file mode 100644
index 00000000..67c65bab
--- /dev/null
+++ b/themes/commerce/components/ArticleCopyright.js
@@ -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 (
+
+
+ -
+ {locale.COMMON.AUTHOR}:
+
+ {siteConfig('AUTHOR')}
+
+
+ -
+ {locale.COMMON.URL}:
+
+ {path}
+
+
+ -
+ {locale.COMMON.COPYRIGHT}:
+ {locale.COMMON.COPYRIGHT_NOTICE}
+
+
+
+ );
+}
diff --git a/themes/commerce/components/ArticleLock.js b/themes/commerce/components/ArticleLock.js
new file mode 100644
index 00000000..9ef687e9
--- /dev/null
+++ b/themes/commerce/components/ArticleLock.js
@@ -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 = `${locale.COMMON.PASSWORD_ERROR}
`
+ }
+ }
+ }
+ const passwordInputRef = useRef(null)
+ useEffect(() => {
+ // 选中密码输入框并将其聚焦
+ passwordInputRef.current.focus()
+ }, [])
+
+ return
+
+
{locale.COMMON.ARTICLE_LOCK_TIPS}
+
+
{
+ if (e.key === 'Enter') {
+ submitPassword()
+ }
+ }}
+ ref={passwordInputRef} // 绑定ref到passwordInputRef变量
+ className='outline-none w-full text-sm pl-5 rounded-l transition focus:shadow-lg font-light leading-10 bg-gray-100 dark:bg-gray-500'>
+
+
+ {locale.COMMON.SUBMIT}
+
+
+
+
+
+
+}
diff --git a/themes/commerce/components/ArticleRecommend.js b/themes/commerce/components/ArticleRecommend.js
new file mode 100644
index 00000000..b26aace1
--- /dev/null
+++ b/themes/commerce/components/ArticleRecommend.js
@@ -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 (
+
+
+
+
+ {locale.COMMON.RELATE_POSTS}
+
+
+
+ {recommendPosts.map(post => {
+ const headerImage = post?.pageCoverThumbnail
+ ? post.pageCoverThumbnail
+ : siteInfo?.pageCover
+
+ return (
+ (
+
+
+
+ )
+ )
+ })}
+
+
+ )
+}
diff --git a/themes/commerce/components/BlogPostArchive.js b/themes/commerce/components/BlogPostArchive.js
new file mode 100644
index 00000000..8c3fbaaf
--- /dev/null
+++ b/themes/commerce/components/BlogPostArchive.js
@@ -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 (
+
+ )
+ }
+}
+
+export default BlogPostArchive
diff --git a/themes/commerce/components/BlogPostCardInfo.js b/themes/commerce/components/BlogPostCardInfo.js
new file mode 100644
index 00000000..8b2f4a48
--- /dev/null
+++ b/themes/commerce/components/BlogPostCardInfo.js
@@ -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
+
+ {/* 标题 */}
+
+
+
{post.title}
+
+
+
+ {/* 分类 */}
+ { post?.category &&
+
+
+
+ {post.category}
+
+
+
+
+
}
+
+ {/* 摘要 */}
+ {(!showPreview || showSummary) && !post.results && (
+
+ {post.summary}
+
+ )}
+
+ {/* 搜索结果 */}
+ {post.results && (
+
+ {post.results.map((r, index) => (
+ {r}
+ ))}
+
+ )}
+ {/* 预览 */}
+ {showPreview && (
+
+
+
+ )}
+
+
+
+
+ {/* 日期标签 */}
+
+ {/* 日期 */}
+
+
+
+ {post?.publishDay || post.lastEditedDay}
+
+
+
+
+
+ {' '}
+ {post.tagItems?.map(tag => (
+
+ ))}
+
+
+
+
+
+}
diff --git a/themes/commerce/components/BlogPostListEmpty.js b/themes/commerce/components/BlogPostListEmpty.js
new file mode 100644
index 00000000..5f75c3e7
--- /dev/null
+++ b/themes/commerce/components/BlogPostListEmpty.js
@@ -0,0 +1,14 @@
+import { useGlobal } from '@/lib/global'
+
+/**
+ * 空白博客 列表
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const BlogPostListEmpty = ({ currentSearch }) => {
+ const { locale } = useGlobal()
+ return
+
{locale.COMMON.NO_MORE} {(currentSearch &&
{currentSearch}
)}
+
+}
+export default BlogPostListEmpty
diff --git a/themes/commerce/components/BlogPostListPage.js b/themes/commerce/components/BlogPostListPage.js
new file mode 100644
index 00000000..17ca53bb
--- /dev/null
+++ b/themes/commerce/components/BlogPostListPage.js
@@ -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
+ } else {
+ return (
+
+ {/* 文章列表 */}
+
+ {posts?.map(post => (
+
+ ))}
+
+ {showPagination &&
}
+
+ )
+ }
+}
+
+export default BlogPostListPage
diff --git a/themes/commerce/components/BlogPostListScroll.js b/themes/commerce/components/BlogPostListScroll.js
new file mode 100644
index 00000000..91beb929
--- /dev/null
+++ b/themes/commerce/components/BlogPostListScroll.js
@@ -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
+ } else {
+ return
+
+ {/* 文章列表 */}
+
+ {postsToShow.map(post => (
+
+ ))}
+
+
+
+
{ 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}`}
+
+
+ }
+}
+
+export default BlogPostListScroll
diff --git a/themes/commerce/components/Card.js b/themes/commerce/components/Card.js
new file mode 100644
index 00000000..c2db0e49
--- /dev/null
+++ b/themes/commerce/components/Card.js
@@ -0,0 +1,9 @@
+const Card = ({ children, headerSlot, className }) => {
+ return
+ <>{headerSlot}>
+
+
+}
+export default Card
diff --git a/themes/commerce/components/Catalog.js b/themes/commerce/components/Catalog.js
new file mode 100644
index 00000000..5cf1e4ad
--- /dev/null
+++ b/themes/commerce/components/Catalog.js
@@ -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
+
{locale.COMMON.TABLE_OF_CONTENTS}
+
+
+
+
+
+
+}
+
+export default Catalog
diff --git a/themes/commerce/components/CategoryGroup.js b/themes/commerce/components/CategoryGroup.js
new file mode 100644
index 00000000..0b313088
--- /dev/null
+++ b/themes/commerce/components/CategoryGroup.js
@@ -0,0 +1,30 @@
+import Link from 'next/link'
+
+const CategoryGroup = ({ currentCategory, categories }) => {
+ if (!categories) {
+ return <>>
+ }
+ return <>
+
+ {categories.map(category => {
+ const selected = currentCategory === category.name
+ return (
+
+
+
{category.name}({category.count})
+
+
+ );
+ })}
+
+ >;
+}
+
+export default CategoryGroup
diff --git a/themes/commerce/components/FloatDarkModeButton.js b/themes/commerce/components/FloatDarkModeButton.js
new file mode 100644
index 00000000..eb62ca06
--- /dev/null
+++ b/themes/commerce/components/FloatDarkModeButton.js
@@ -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 (
+
+
+
+ )
+}
diff --git a/themes/commerce/components/Footer.js b/themes/commerce/components/Footer.js
new file mode 100644
index 00000000..a8efec25
--- /dev/null
+++ b/themes/commerce/components/Footer.js
@@ -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
+}
+
+export default Footer
diff --git a/themes/commerce/components/Header.js b/themes/commerce/components/Header.js
new file mode 100644
index 00000000..af963564
--- /dev/null
+++ b/themes/commerce/components/Header.js
@@ -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
+
+ {/* 导航栏菜单内容 */}
+
+
+ {/* 左侧图标Logo */}
+
+
+ {/* 移动端折叠按钮 */}
+
+
+ {/* 桌面端顶部菜单 */}
+
+ {links && links?.map(link => )}
+
+
+
+ {/* 移动端折叠菜单 */}
+
+
+ collapseRef.current?.updateCollapseHeight(param)} />
+
+
+
+}
diff --git a/themes/commerce/components/Hero.js b/themes/commerce/components/Hero.js
new file mode 100644
index 00000000..af3caae1
--- /dev/null
+++ b/themes/commerce/components/Hero.js
@@ -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 (
+
+ )
+}
+
+export default Hero
diff --git a/themes/commerce/components/HexoRecentComments.js b/themes/commerce/components/HexoRecentComments.js
new file mode 100644
index 00000000..db712bea
--- /dev/null
+++ b/themes/commerce/components/HexoRecentComments.js
@@ -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 (
+
+
+
+ {locale.COMMON.RECENT_COMMENTS}
+
+
+ {onLoading && Loading...
}
+ {!onLoading && comments && comments.length === 0 && No Comments
}
+ {!onLoading && comments && comments.length > 0 && comments.map((comment) =>
+
+
+ --{comment.nick}
+
+
)}
+
+
+ )
+}
+
+export default HexoRecentComments
diff --git a/themes/commerce/components/InfoCard.js b/themes/commerce/components/InfoCard.js
new file mode 100644
index 00000000..6b33034e
--- /dev/null
+++ b/themes/commerce/components/InfoCard.js
@@ -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 (
+
+ {
+ router.push('/')
+ }}
+ >
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
+
+ {siteConfig('AUTHOR')}
+ {siteConfig('BIO')}
+
+
+
+ )
+}
diff --git a/themes/commerce/components/JumpToCommentButton.js b/themes/commerce/components/JumpToCommentButton.js
new file mode 100644
index 00000000..ded36c9a
--- /dev/null
+++ b/themes/commerce/components/JumpToCommentButton.js
@@ -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 (
+
+
)
+}
+
+export default JumpToCommentButton
diff --git a/themes/commerce/components/JumpToTopButton.js b/themes/commerce/components/JumpToTopButton.js
new file mode 100644
index 00000000..a89899a9
--- /dev/null
+++ b/themes/commerce/components/JumpToTopButton.js
@@ -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 ( window.scrollTo({ top: 0, behavior: 'smooth' })} >
+
+ {showPercent && (
{percent}
)}
+
)
+}
+
+export default JumpToTopButton
diff --git a/themes/commerce/components/LatestPostsGroup.js b/themes/commerce/components/LatestPostsGroup.js
new file mode 100644
index 00000000..3c4fadbc
--- /dev/null
+++ b/themes/commerce/components/LatestPostsGroup.js
@@ -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 <>
+
+
+
+ {locale.COMMON.LATEST_POSTS}
+
+
+ {latestPosts.map(post => {
+ const selected = currentPath === `${siteConfig('SUB_PATH', '')}/${post.slug}`
+
+ const headerImage = post?.pageCoverThumbnail ? post.pageCoverThumbnail : siteInfo?.pageCover
+
+ return (
+ (
+
+
+
+
+
+
+
{post.title}
+
{post.lastEditedDay}
+
+
+
+ )
+ )
+ })}
+ >
+}
+export default LatestPostsGroup
diff --git a/themes/commerce/components/LoadingCover.js b/themes/commerce/components/LoadingCover.js
new file mode 100644
index 00000000..c6418fad
--- /dev/null
+++ b/themes/commerce/components/LoadingCover.js
@@ -0,0 +1,8 @@
+export default function LoadingCover () {
+ return (
+ )
+}
diff --git a/themes/commerce/components/LogoBar.js b/themes/commerce/components/LogoBar.js
new file mode 100644
index 00000000..de5a5bfe
--- /dev/null
+++ b/themes/commerce/components/LogoBar.js
@@ -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 (
+
+
+
+
+ {/* {siteConfig('TITLE')}
*/}
+
+ );
+}
diff --git a/themes/commerce/components/MenuBarMobile.js b/themes/commerce/components/MenuBarMobile.js
new file mode 100644
index 00000000..a8ba0f12
--- /dev/null
+++ b/themes/commerce/components/MenuBarMobile.js
@@ -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 (
+
+ )
+}
diff --git a/themes/commerce/components/MenuGroupCard.js b/themes/commerce/components/MenuGroupCard.js
new file mode 100644
index 00000000..5e4d84a4
--- /dev/null
+++ b/themes/commerce/components/MenuGroupCard.js
@@ -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 = {postCount}
+ const categorySlot = {categoryOptions?.length}
+ const tagSlot = {tagOptions?.length}
+
+ 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 (
+
+ )
+}
+export default MenuGroupCard
diff --git a/themes/commerce/components/MenuItemCollapse.js b/themes/commerce/components/MenuItemCollapse.js
new file mode 100644
index 00000000..1ce6d59d
--- /dev/null
+++ b/themes/commerce/components/MenuItemCollapse.js
@@ -0,0 +1,55 @@
+import Collapse from '@/components/Collapse'
+import Link from 'next/link'
+import { useState } from 'react'
+
+/**
+ * 折叠菜单
+ * @param {*} param0
+ * @returns
+ */
+export const MenuItemCollapse = (props) => {
+ const { link } = props
+ const [show, changeShow] = useState(false)
+ const hasSubMenu = link?.subMenus?.length > 0
+
+ const [isOpen, changeIsOpen] = useState(false)
+
+ const toggleShow = () => {
+ changeShow(!show)
+ }
+
+ const toggleOpenSubMenu = () => {
+ changeIsOpen(!isOpen)
+ }
+
+ if (!link || !link.show) {
+ return null
+ }
+
+ return <>
+
+ {!hasSubMenu &&
+
{link?.icon && }{link?.name}
+ }
+ {hasSubMenu &&
+ {link?.icon && }{link?.name}
+
+
}
+
+
+ {/* 折叠子菜单 */}
+ {hasSubMenu &&
+ {link.subMenus.map((sLink, index) => {
+ return
+
+ {link?.icon && } {sLink.title}
+
+
+ })}
+ }
+ >
+}
diff --git a/themes/commerce/components/MenuItemDrop.js b/themes/commerce/components/MenuItemDrop.js
new file mode 100644
index 00000000..e83f46f7
--- /dev/null
+++ b/themes/commerce/components/MenuItemDrop.js
@@ -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 changeShow(true)} onMouseOut={() => changeShow(false)} className='h-full'>
+
+ {!hasSubMenu &&
+
+ {link?.icon &&
} {link?.name}
+ {/* {hasSubMenu && } */}
+ }
+
+ {hasSubMenu && <>
+
+ {link?.icon &&
} {link?.name}
+ {/* */}
+
+ >}
+
+ {/* 子菜单 */}
+ {hasSubMenu &&
+ {link.subMenus.map((sLink, index) => {
+ return -
+
+ {link?.icon && }{sLink.title}
+
+
+ })}
+
}
+
+
+}
diff --git a/themes/commerce/components/MenuListSide.js b/themes/commerce/components/MenuListSide.js
new file mode 100644
index 00000000..89133256
--- /dev/null
+++ b/themes/commerce/components/MenuListSide.js
@@ -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 (
+
+ )
+}
diff --git a/themes/commerce/components/MenuListTop.js b/themes/commerce/components/MenuListTop.js
new file mode 100644
index 00000000..c4a10085
--- /dev/null
+++ b/themes/commerce/components/MenuListTop.js
@@ -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 (<>
+
+ >)
+}
diff --git a/themes/commerce/components/NavButtonGroup.js b/themes/commerce/components/NavButtonGroup.js
new file mode 100644
index 00000000..8539eb49
--- /dev/null
+++ b/themes/commerce/components/NavButtonGroup.js
@@ -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 (
+
+ )
+}
+export default NavButtonGroup
diff --git a/themes/commerce/components/PaginationNumber.js b/themes/commerce/components/PaginationNumber.js
new file mode 100644
index 00000000..d041fe6c
--- /dev/null
+++ b/themes/commerce/components/PaginationNumber.js
@@ -0,0 +1,107 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+/**
+ * 数字翻页插件
+ * @param page 当前页码
+ * @param showNext 是否有下一页
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const PaginationNumber = ({ page, totalPage }) => {
+ const router = useRouter()
+ const currentPage = +page
+ const showNext = page < totalPage
+ const pagePrefix = router.asPath.split('?')[0].replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '')
+ const pages = generatePages(pagePrefix, page, currentPage, totalPage)
+
+ return (
+
+ {/* 上一页 */}
+
+
+
+
+
+
+ {pages}
+
+ {/* 下一页 */}
+
+
+
+
+
+
+ )
+}
+
+function getPageElement(page, currentPage, pagePrefix) {
+ return (
+ (
+
+ {page}
+
+ )
+ )
+}
+
+function generatePages(pagePrefix, page, currentPage, totalPage) {
+ const pages = []
+ const groupCount = 7 // 最多显示页签数
+ if (totalPage <= groupCount) {
+ for (let i = 1; i <= totalPage; i++) {
+ pages.push(getPageElement(i, page, pagePrefix))
+ }
+ } else {
+ pages.push(getPageElement(1, page, pagePrefix))
+ const dynamicGroupCount = groupCount - 2
+ let startPage = currentPage - 2
+ if (startPage <= 1) {
+ startPage = 2
+ }
+ if (startPage + dynamicGroupCount > totalPage) {
+ startPage = totalPage - dynamicGroupCount
+ }
+ if (startPage > 2) {
+ pages.push(...
)
+ }
+
+ for (let i = 0; i < dynamicGroupCount; i++) {
+ if (startPage + i < totalPage) {
+ pages.push(getPageElement(startPage + i, page, pagePrefix))
+ }
+ }
+
+ if (startPage + dynamicGroupCount < totalPage) {
+ pages.push(...
)
+ }
+
+ pages.push(getPageElement(totalPage, page, pagePrefix))
+ }
+ return pages
+}
+export default PaginationNumber
diff --git a/themes/commerce/components/PostHeader.js b/themes/commerce/components/PostHeader.js
new file mode 100644
index 00000000..e48b1f0c
--- /dev/null
+++ b/themes/commerce/components/PostHeader.js
@@ -0,0 +1,39 @@
+import Link from 'next/link'
+import NotionIcon from '@/components/NotionIcon'
+import LazyImage from '@/components/LazyImage'
+import { siteConfig } from '@/lib/config'
+
+export default function PostHeader({ post }) {
+ if (!post) {
+ return <>>
+ }
+ const headerImage = post?.pageCover ? post.pageCover : siteConfig('HOME_BANNER_IMAGE')
+
+ return (
+
+ )
+}
diff --git a/themes/commerce/components/ProductCard.js b/themes/commerce/components/ProductCard.js
new file mode 100644
index 00000000..8ec25b6c
--- /dev/null
+++ b/themes/commerce/components/ProductCard.js
@@ -0,0 +1,37 @@
+import Link from 'next/link'
+import CONFIG from '../config'
+import { siteConfig } from '@/lib/config'
+import LazyImage from '@/components/LazyImage'
+// import Image from 'next/image'
+
+/**
+ * 商品卡
+ */
+const ProductCard = ({ index, post, siteInfo }) => {
+ if (post && !post.pageCoverThumbnail && CONFIG.POST_LIST_COVER_DEFAULT) {
+ post.pageCoverThumbnail = siteInfo?.pageCover
+ }
+
+ return (
+
+
+
+
+
+ {/* 图片封面 */}
+
+
+
+
+
+
+
{post.title}
+
+
+
+
+
+ )
+}
+
+export default ProductCard
diff --git a/themes/commerce/components/ProductCategories.js b/themes/commerce/components/ProductCategories.js
new file mode 100644
index 00000000..22ec36b9
--- /dev/null
+++ b/themes/commerce/components/ProductCategories.js
@@ -0,0 +1,27 @@
+import { siteConfig } from '@/lib/config'
+import Link from 'next/link'
+
+export default function ProductCategories(props) {
+ const { categoryOptions } = props
+
+ return
+ {/* 分类菜单 */}
+
+
{siteConfig('COMMERCE_TEXT_MENU_GROUP', 'Product Categories')}
+
+
+
+}
diff --git a/themes/commerce/components/ProductCenter.js b/themes/commerce/components/ProductCenter.js
new file mode 100644
index 00000000..ec82469d
--- /dev/null
+++ b/themes/commerce/components/ProductCenter.js
@@ -0,0 +1,33 @@
+import { siteConfig } from '@/lib/config'
+import ProductCard from './ProductCard'
+import ProductCategories from './ProductCategories'
+
+/**
+ * 产品中心
+ * @param {*} props
+ * @returns
+ */
+export default function ProductCenter(props) {
+ const { allNavPages } = props
+ const posts = allNavPages.slice(0, parseInt(siteConfig('COMMERCE_HOME_POSTS_COUNT', 9)))
+
+ return
+
{siteConfig('COMMERCE_TEXT_CENTER_TITLE', 'Product Center')}
+ {siteConfig('COMMERCE_TEXT_CENTER_DESCRIPTION') &&
{siteConfig('COMMERCE_TEXT_CENTER_DESCRIPTION')}
}
+
+
+
+
+
+
+ {/* 文章列表 */}
+
+ {posts?.map(post => (
+
+ ))}
+
+
+
+
+
+}
diff --git a/themes/commerce/components/Progress.js b/themes/commerce/components/Progress.js
new file mode 100644
index 00000000..585e1c21
--- /dev/null
+++ b/themes/commerce/components/Progress.js
@@ -0,0 +1,44 @@
+import { useEffect, useState } from 'react'
+import { isBrowser } from '@/lib/utils'
+
+/**
+ * 顶部页面阅读进度条
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const Progress = ({ targetRef, showPercent = true }) => {
+ const currentRef = targetRef?.current || targetRef
+ const [percent, changePercent] = useState(0)
+ const scrollListener = () => {
+ const target = currentRef || (isBrowser && document.getElementById('article-wrapper'))
+ if (target) {
+ const clientHeight = target.clientHeight
+ const scrollY = window.pageYOffset
+ const fullHeight = clientHeight - window.outerHeight
+ let per = parseFloat(((scrollY / fullHeight) * 100).toFixed(0))
+ if (per > 100) per = 100
+ if (per < 0) per = 0
+ changePercent(per)
+ }
+ }
+
+ useEffect(() => {
+ document.addEventListener('scroll', scrollListener)
+ return () => document.removeEventListener('scroll', scrollListener)
+ }, [])
+
+ return (
+
+
+ {showPercent && (
+
{percent}%
+ )}
+
+
+ )
+}
+
+export default Progress
diff --git a/themes/commerce/components/RightFloatArea.js b/themes/commerce/components/RightFloatArea.js
new file mode 100644
index 00000000..0d5015a1
--- /dev/null
+++ b/themes/commerce/components/RightFloatArea.js
@@ -0,0 +1,41 @@
+import throttle from 'lodash.throttle'
+import { useCallback, useEffect, useState } from 'react'
+import JumpToTopButton from './JumpToTopButton'
+
+/**
+ * 悬浮在右下角的按钮,当页面向下滚动100px时会出现
+ * @param {*} param0
+ * @returns
+ */
+export default function RightFloatArea({ floatSlot }) {
+ const [showFloatButton, switchShow] = useState(false)
+ const scrollListener = useCallback(throttle(() => {
+ const targetRef = document.getElementById('wrapper')
+ const clientHeight = targetRef?.clientHeight
+ const scrollY = window.pageYOffset
+ const fullHeight = clientHeight - window.outerHeight
+ let per = parseFloat(((scrollY / fullHeight) * 100).toFixed(0))
+ if (per > 100) per = 100
+ const shouldShow = scrollY > 100 && per > 0
+
+ // 右下角显示悬浮按钮
+ if (shouldShow !== showFloatButton) {
+ switchShow(shouldShow)
+ }
+ }, 200))
+
+ useEffect(() => {
+ document.addEventListener('scroll', scrollListener)
+ return () => document.removeEventListener('scroll', scrollListener)
+ }, [])
+
+ return (
+
+
+ {/* */}
+ {floatSlot}
+
+
+
+ )
+}
diff --git a/themes/commerce/components/SearchDrawer.js b/themes/commerce/components/SearchDrawer.js
new file mode 100644
index 00000000..c7ec88a7
--- /dev/null
+++ b/themes/commerce/components/SearchDrawer.js
@@ -0,0 +1,36 @@
+import { Router } from 'next/router'
+import { useImperativeHandle, useRef } from 'react'
+import SearchInput from './SearchInput'
+const SearchDrawer = ({ cRef, slot }) => {
+ const searchDrawer = useRef()
+ const searchInputRef = useRef()
+ useImperativeHandle(cRef, () => {
+ return {
+ show: () => {
+ searchDrawer?.current?.classList?.remove('hidden')
+ searchInputRef?.current?.focus()
+ }
+ }
+ })
+ const hidden = () => {
+ searchDrawer?.current?.classList?.add('hidden')
+ }
+ Router.events.on('routeChangeComplete', (...args) => {
+ hidden()
+ })
+ return (
+
+ )
+}
+
+export default SearchDrawer
diff --git a/themes/commerce/components/SearchInput.js b/themes/commerce/components/SearchInput.js
new file mode 100644
index 00000000..462c58b3
--- /dev/null
+++ b/themes/commerce/components/SearchInput.js
@@ -0,0 +1,106 @@
+import { useRouter } from 'next/router'
+import { useImperativeHandle, useRef, useState } from 'react'
+import { useGlobal } from '@/lib/global'
+let lock = false
+
+const SearchInput = props => {
+ const { currentSearch, cRef, className } = props
+ const [onLoading, setLoadingState] = useState(false)
+ const router = useRouter()
+ const searchInputRef = useRef()
+ const { locale } = useGlobal()
+ useImperativeHandle(cRef, () => {
+ return {
+ focus: () => {
+ searchInputRef?.current?.focus()
+ }
+ }
+ })
+
+ const handleSearch = () => {
+ const key = searchInputRef.current.value
+ if (key && key !== '') {
+ setLoadingState(true)
+ router.push({ pathname: '/search/' + key }).then(r => {
+ setLoadingState(false)
+ })
+ // location.href = '/search/' + key
+ } else {
+ router.push({ pathname: '/' }).then(r => {})
+ }
+ }
+ const handleKeyUp = e => {
+ if (e.keyCode === 13) {
+ // 回车
+ handleSearch(searchInputRef.current.value)
+ } else if (e.keyCode === 27) {
+ // ESC
+ cleanSearch()
+ }
+ }
+ const cleanSearch = () => {
+ searchInputRef.current.value = ''
+ }
+
+ const [showClean, setShowClean] = useState(false)
+ const updateSearchKey = val => {
+ if (lock) {
+ return
+ }
+ searchInputRef.current.value = val
+
+ if (val) {
+ setShowClean(true)
+ } else {
+ setShowClean(false)
+ }
+ }
+ function lockSearchInput () {
+ lock = true
+ }
+
+ function unLockSearchInput () {
+ lock = false
+ }
+
+ return (
+
+
updateSearchKey(e.target.value)}
+ defaultValue={currentSearch || ''}
+ />
+
+
+
+
+
+ {showClean && (
+
+
+
+ )}
+
+ )
+}
+
+export default SearchInput
diff --git a/themes/commerce/components/SearchNav.js b/themes/commerce/components/SearchNav.js
new file mode 100644
index 00000000..fc393c1b
--- /dev/null
+++ b/themes/commerce/components/SearchNav.js
@@ -0,0 +1,70 @@
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import { useEffect, useRef } from 'react'
+import Card from './Card'
+import SearchInput from './SearchInput'
+import TagItemMini from './TagItemMini'
+
+/**
+ * 搜索页面的导航
+ * @param {*} props
+ * @returns
+ */
+export default function SearchNav(props) {
+ const { tagOptions, categoryOptions } = props
+ const cRef = useRef(null)
+ const { locale } = useGlobal()
+ useEffect(() => {
+ // 自动聚焦到搜索框
+ cRef?.current?.focus()
+ }, [])
+
+ return <>
+
+
+ {/* 分类 */}
+
+
+
+ {locale.COMMON.CATEGORY}:
+
+
+ {categoryOptions?.map(category => {
+ return (
+
+
+
+ {category.name}({category.count})
+
+
+ )
+ })}
+
+
+ {/* 标签 */}
+
+
+
+ {locale.COMMON.TAGS}:
+
+
+
+
+>
+}
diff --git a/themes/commerce/components/SideBar.js b/themes/commerce/components/SideBar.js
new file mode 100644
index 00000000..913ce0a9
--- /dev/null
+++ b/themes/commerce/components/SideBar.js
@@ -0,0 +1,33 @@
+import { siteConfig } from '@/lib/config'
+import LazyImage from '@/components/LazyImage'
+import { useRouter } from 'next/router'
+import MenuGroupCard from './MenuGroupCard'
+import { MenuListSide } from './MenuListSide'
+
+/**
+ * 侧边抽屉
+ * @param tags
+ * @param currentTag
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const SideBar = (props) => {
+ const { siteInfo } = props
+ const router = useRouter()
+ return (
+
+
+
+
{ router.push('/') }}
+ className='justify-center items-center flex hover:rotate-45 py-6 hover:scale-105 dark:text-gray-100 transform duration-200 cursor-pointer'>
+
+
+
+
+
+
+
+ )
+}
+
+export default SideBar
diff --git a/themes/commerce/components/SideBarDrawer.js b/themes/commerce/components/SideBarDrawer.js
new file mode 100644
index 00000000..f457d4a3
--- /dev/null
+++ b/themes/commerce/components/SideBarDrawer.js
@@ -0,0 +1,52 @@
+import { useRouter } from 'next/router'
+import { useEffect } from 'react'
+
+/**
+ * 侧边栏抽屉面板,可以从侧面拉出
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const SideBarDrawer = ({ children, isOpen, onOpen, onClose, className }) => {
+ const router = useRouter()
+ useEffect(() => {
+ const sideBarDrawerRouteListener = () => {
+ switchSideDrawerVisible(false)
+ }
+ router.events.on('routeChangeComplete', sideBarDrawerRouteListener)
+ return () => {
+ router.events.off('routeChangeComplete', sideBarDrawerRouteListener)
+ }
+ }, [router.events])
+
+ // 点击按钮更改侧边抽屉状态
+ const switchSideDrawerVisible = (showStatus) => {
+ if (showStatus) {
+ onOpen && onOpen()
+ } else {
+ onClose && onClose()
+ }
+ const sideBarDrawer = window.document.getElementById('sidebar-drawer')
+ const sideBarDrawerBackground = window.document.getElementById('sidebar-drawer-background')
+
+ if (showStatus) {
+ sideBarDrawer?.classList.replace('-mr-72', 'mr-0')
+ sideBarDrawerBackground?.classList.replace('hidden', 'block')
+ } else {
+ sideBarDrawer?.classList.replace('mr-0', '-mr-72')
+ sideBarDrawerBackground?.classList.replace('block', 'hidden')
+ }
+ }
+
+ return