sandy-try commited on
Commit
1b72d7e
1 Parent(s): 85e8b2f

Upload 699 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +1 -0
  2. .env.local +174 -0
  3. .eslintrc.js +38 -0
  4. .gitattributes +1 -0
  5. .gitignore +52 -0
  6. .prettierrc.json +6 -0
  7. CONTRIBUTING.md +52 -0
  8. Dockerfile +27 -0
  9. blog.config.js +432 -0
  10. components/AOSAnimation.js +12 -0
  11. components/Ackee.js +85 -0
  12. components/AdBlockDetect.js +40 -0
  13. components/AlgoliaSearchModal.js +230 -0
  14. components/Artalk.js +37 -0
  15. components/Busuanzi.js +26 -0
  16. components/ChatBase.js +19 -0
  17. components/Collapse.js +96 -0
  18. components/Comment.js +138 -0
  19. components/CusdisComponent.js +37 -0
  20. components/CustomContextMenu.js +204 -0
  21. components/DarkModeButton.js +27 -0
  22. components/DebugPanel.js +128 -0
  23. components/DifyChatbot.js +32 -0
  24. components/DisableCopy.js +21 -0
  25. components/Draggable.js +150 -0
  26. components/Equation.js +29 -0
  27. components/ExternalPlugins.js +290 -0
  28. components/ExternalScript.js +29 -0
  29. components/FacebookMessenger.js +282 -0
  30. components/FacebookPage.js +34 -0
  31. components/Fireworks.js +216 -0
  32. components/FlipCard.js +56 -0
  33. components/FlutteringRibbon.js +322 -0
  34. components/FullScreenButton.js +48 -0
  35. components/Giscus.js +33 -0
  36. components/Gitalk.js +49 -0
  37. components/GlobalHead.js +194 -0
  38. components/GlobalStyle.js +20 -0
  39. components/GoogleAdsense.js +92 -0
  40. components/Gtag.js +18 -0
  41. components/HeroIcons.js +100 -0
  42. components/KatexReact.js +58 -0
  43. components/LA51.js +18 -0
  44. components/LazyImage.js +98 -0
  45. components/Live2D.js +48 -0
  46. components/Loading.js +13 -0
  47. components/LoadingProgress.js +29 -0
  48. components/Mark.js +28 -0
  49. components/Nest.js +124 -0
  50. components/NotionIcon.js +20 -0
.dockerignore ADDED
@@ -0,0 +1 @@
 
 
1
+ .next*
.env.local ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables
2
+ NEXT_PUBLIC_VERSION=4.2.4
3
+
4
+
5
+ # 可在此添加环境变量,去掉最左边的(# )注释即可
6
+ # Notion页面ID,必须
7
+ # NOTION_PAGE_ID=097e5f674880459d8e1b4407758dc4fb
8
+
9
+ # 非必须
10
+ # NEXT_PUBLIC_PSEUDO_STATIC=
11
+ # NEXT_PUBLIC_REVALIDATE_SECOND=
12
+ # NEXT_PUBLIC_THEME=matery
13
+ # NEXT_PUBLIC_THEME_SWITCH=
14
+ # NEXT_PUBLIC_LANG=
15
+ # NEXT_PUBLIC_APPEARANCE=
16
+ # NEXT_PUBLIC_APPEARANCE_DARK_TIME=
17
+ # NEXT_PUBLIC_GREETING_WORDS=
18
+ # NEXT_PUBLIC_CUSTOM_MENU=
19
+ # NEXT_PUBLIC_AUTHOR=
20
+ # NEXT_PUBLIC_BIO=
21
+ # NEXT_PUBLIC_LINK=
22
+ # NEXT_PUBLIC_KEYWORD=
23
+ # NEXT_PUBLIC_CONTACT_EMAIL=
24
+ # NEXT_PUBLIC_CONTACT_WEIBO=
25
+ # NEXT_PUBLIC_CONTACT_TWITTER=
26
+ # NEXT_PUBLIC_CONTACT_GITHUB=
27
+ # NEXT_PUBLIC_CONTACT_TELEGRAM=
28
+ # NEXT_PUBLIC_CONTACT_LINKEDIN=
29
+ # NEXT_PUBLIC_CONTACT_INSTAGRAM=
30
+ # NEXT_PUBLIC_CONTACT_BILIBILI=
31
+ # NEXT_PUBLIC_CONTACT_YOUTUBE=
32
+ # NEXT_PUBLIC_FAVICON=
33
+ # NEXT_PUBLIC_FONT_STYLE=
34
+ # NEXT_PUBLIC_FONT_URL=
35
+ # NEXT_PUBLIC_FONT_SANS=
36
+ # NEXT_PUBLIC_FONT_SERIF=
37
+ # NEXT_PUBLIC_FONT_AWESOME_PATH=
38
+ # NEXT_PUBLIC_PRISM_THEME_PREFIX_PATH=
39
+ # NEXT_PUBLIC_PRISM_THEME_SWITCH=
40
+ # NEXT_PUBLIC_PRISM_THEME_LIGHT_PATH=
41
+ # NEXT_PUBLIC_PRISM_THEME_DARK_PATH=
42
+ # NEXT_PUBLIC_CODE_MAC_BAR=
43
+ # NEXT_PUBLIC_CODE_LINE_NUMBERS=
44
+ # NEXT_PUBLIC_CODE_COLLAPSE=
45
+ # NEXT_PUBLIC_CODE_COLLAPSE_EXPAND_DEFAULT=
46
+ # NEXT_PUBLIC_MERMAID_CDN=
47
+ # NEXT_PUBLIC_QR_CODE_CDN=
48
+ # NEXT_PUBLIC_BACKGROUND_LIGHT=
49
+ # NEXT_PUBLIC_BACKGROUND_DARK=
50
+ # NEXT_PUBLIC_SUB_PATH=
51
+ # NEXT_PUBLIC_POST_SHARE_BAR=
52
+ # NEXT_PUBLIC_POST_SHARE_SERVICES=
53
+ # NEXT_PUBLIC_POST_URL_PREFIX=
54
+ # NEXT_PUBLIC_POST_LIST_STYLE=
55
+ # NEXT_PUBLIC_POST_PREVIEW=
56
+ # NEXT_PUBLIC_POST_RECOMMEND_COUNT=
57
+ # NEXT_PUBLIC_POSTS_PER_PAGE=
58
+ # NEXT_PUBLIC_POST_SORT_BY=
59
+ # NEXT_PUBLIC_ALGOLIA_APP_ID=
60
+ # ALGOLIA_ADMIN_APP_KEY=
61
+ # NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_APP_KEY=
62
+ # NEXT_PUBLIC_ALGOLIA_INDEX=
63
+ # NEXT_PUBLIC_PREVIEW_CATEGORY_COUNT=
64
+ # NEXT_PUBLIC_PREVIEW_TAG_COUNT=
65
+ # NEXT_PUBLIC_POST_DISABLE_GALLERY_CLICK=
66
+ # NEXT_PUBLIC_FIREWORKS=
67
+ # NEXT_PUBLIC_FIREWORKS_COLOR=
68
+ # NEXT_PUBLIC_SAKURA=
69
+ # NEXT_PUBLIC_NEST=
70
+ # NEXT_PUBLIC_FLUTTERINGRIBBON=
71
+ # NEXT_PUBLIC_RIBBON=
72
+ # NEXT_PUBLIC_STARRY_SKY=
73
+ # NEXT_PUBLIC_CHATBASE_ID=
74
+ # NEXT_PUBLIC_WEB_WHIZ_ENABLED=
75
+ # NEXT_PUBLIC_WEB_WHIZ_BASE_URL=
76
+ # NEXT_PUBLIC_WEB_WHIZ_CHAT_BOT_ID=
77
+ # NEXT_PUBLIC_WIDGET_PET=
78
+ # NEXT_PUBLIC_WIDGET_PET_LINK=
79
+ # NEXT_PUBLIC_WIDGET_PET_SWITCH_THEME=
80
+ # NEXT_PUBLIC_MUSIC_PLAYER=
81
+ # NEXT_PUBLIC_MUSIC_PLAYER_VISIBLE=
82
+ # NEXT_PUBLIC_MUSIC_PLAYER_AUTO_PLAY=
83
+ # NEXT_PUBLIC_MUSIC_PLAYER_LRC_TYPE=
84
+ # NEXT_PUBLIC_MUSIC_PLAYER_CDN_URL=
85
+ # NEXT_PUBLIC_MUSIC_PLAYER_ORDER=
86
+ # NEXT_PUBLIC_MUSIC_PLAYER_AUDIO_LIST=
87
+ # NEXT_PUBLIC_MUSIC_PLAYER_METING=
88
+ # NEXT_PUBLIC_MUSIC_PLAYER_METING_SERVER=
89
+ # NEXT_PUBLIC_MUSIC_PLAYER_METING_ID=
90
+ # NEXT_PUBLIC_MUSIC_PLAYER_METING_LRC_TYPE=
91
+ # NEXT_PUBLIC_COMMENT_ARTALK_SERVER=
92
+ # NEXT_PUBLIC_COMMENT_ARTALK_JS=
93
+ # NEXT_PUBLIC_COMMENT_ARTALK_CSS=
94
+ # NEXT_PUBLIC_COMMENT_ENV_ID=
95
+ # NEXT_PUBLIC_COMMENT_TWIKOO_COUNT_ENABLE=
96
+ # NEXT_PUBLIC_COMMENT_TWIKOO_CDN_URL=
97
+ # NEXT_PUBLIC_COMMENT_UTTERRANCES_REPO=
98
+ # NEXT_PUBLIC_COMMENT_GISCUS_REPO=
99
+ # NEXT_PUBLIC_COMMENT_GISCUS_REPO_ID=
100
+ # NEXT_PUBLIC_COMMENT_GISCUS_CATEGORY_ID=
101
+ # NEXT_PUBLIC_COMMENT_GISCUS_MAPPING=
102
+ # NEXT_PUBLIC_COMMENT_GISCUS_REACTIONS_ENABLED=
103
+ # NEXT_PUBLIC_COMMENT_GISCUS_EMIT_METADATA=
104
+ # NEXT_PUBLIC_COMMENT_GISCUS_INPUT_POSITION=
105
+ # NEXT_PUBLIC_COMMENT_GISCUS_LANG=
106
+ # NEXT_PUBLIC_COMMENT_GISCUS_LOADING=
107
+ # NEXT_PUBLIC_COMMENT_GISCUS_CROSSORIGIN=
108
+ # NEXT_PUBLIC_COMMENT_CUSDIS_APP_ID=
109
+ # NEXT_PUBLIC_COMMENT_CUSDIS_HOST=
110
+ # NEXT_PUBLIC_COMMENT_CUSDIS_SCRIPT_SRC=
111
+ # NEXT_PUBLIC_COMMENT_GITALK_REPO=
112
+ # NEXT_PUBLIC_COMMENT_GITALK_OWNER=
113
+ # NEXT_PUBLIC_COMMENT_GITALK_ADMIN=
114
+ # NEXT_PUBLIC_COMMENT_GITALK_CLIENT_ID=
115
+ # NEXT_PUBLIC_COMMENT_GITALK_CLIENT_SECRET=
116
+ # NEXT_PUBLIC_COMMENT_GITALK_JS_CDN_URL=
117
+ # NEXT_PUBLIC_COMMENT_GITALK_CSS_CDN_URL=
118
+ # NEXT_PUBLIC_COMMENT_GITTER_ROOM=
119
+ # NEXT_PUBLIC_COMMENT_DAO_VOICE_ID=
120
+ # NEXT_PUBLIC_COMMENT_TIDIO_ID=
121
+ # NEXT_PUBLIC_VALINE_CDN=
122
+ # NEXT_PUBLIC_VALINE_ID=
123
+ # NEXT_PUBLIC_VALINE_KEY=
124
+ # NEXT_PUBLIC_VALINE_SERVER_URLS=
125
+ # NEXT_PUBLIC_VALINE_PLACEHOLDER=
126
+ # NEXT_PUBLIC_WALINE_SERVER_URL=
127
+ # NEXT_PUBLIC_WALINE_RECENT=
128
+ # NEXT_PUBLIC_WEBMENTION_ENABLE=
129
+ # NEXT_PUBLIC_WEBMENTION_AUTH=
130
+ # NEXT_PUBLIC_WEBMENTION_HOSTNAME=
131
+ # NEXT_PUBLIC_TWITTER_USERNAME=
132
+ # NEXT_PUBLIC_WEBMENTION_TOKEN=
133
+ # NEXT_PUBLIC_ANALYTICS_VERCEL=
134
+ # NEXT_PUBLIC_ANALYTICS_BUSUANZI_ENABLE=
135
+ # NEXT_PUBLIC_ANALYTICS_BAIDU_ID=
136
+ # NEXT_PUBLIC_ANALYTICS_CNZZ_ID=
137
+ # NEXT_PUBLIC_ANALYTICS_GOOGLE_ID=
138
+ # NEXT_PUBLIC_ANALYTICS_ACKEE_TRACKER=
139
+ # NEXT_PUBLIC_ANALYTICS_ACKEE_DATA_SERVER=
140
+ # NEXT_PUBLIC_ANALYTICS_ACKEE_DOMAIN_ID=
141
+ # NEXT_PUBLIC_SEO_GOOGLE_SITE_VERIFICATION=
142
+ # NEXT_PUBLIC_SEO_BAIDU_SITE_VERIFICATION=
143
+ # NEXT_PUBLIC_ADSENSE_GOOGLE_ID=
144
+ # NEXT_PUBLIC_ADSENSE_GOOGLE_TEST=
145
+ # NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_IN_ARTICLE=
146
+ # NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_FLOW=
147
+ # NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_NATIVE=
148
+ # NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_AUTO=
149
+ # NEXT_PUBLIC_WWAD_ID=
150
+ # NEXT_PUBLIC_WWADS_AD_BLOCK_DETECT=
151
+ # NEXT_PUBLIC_NOTION_PROPERTY_PASSWORD=
152
+ # NEXT_PUBLIC_NOTION_PROPERTY_TYPE=
153
+ # NEXT_PUBLIC_NOTION_PROPERTY_TYPE_POST=
154
+ # NEXT_PUBLIC_NOTION_PROPERTY_TYPE_PAGE=
155
+ # NEXT_PUBLIC_NOTION_PROPERTY_TYPE_NOTICE=
156
+ # NEXT_PUBLIC_NOTION_PROPERTY_TYPE_MENU=
157
+ # NEXT_PUBLIC_NOTION_PROPERTY_TYPE_SUB_MENU=
158
+ # NEXT_PUBLIC_NOTION_PROPERTY_TITLE=
159
+ # NEXT_PUBLIC_NOTION_PROPERTY_STATUS=
160
+ # NEXT_PUBLIC_NOTION_PROPERTY_STATUS_PUBLISH=
161
+ # NEXT_PUBLIC_NOTION_PROPERTY_STATUS_INVISIBLE=
162
+ # NEXT_PUBLIC_NOTION_PROPERTY_SUMMARY=
163
+ # NEXT_PUBLIC_NOTION_PROPERTY_SLUG=
164
+ # NEXT_PUBLIC_NOTION_PROPERTY_CATEGORY=
165
+ # NEXT_PUBLIC_NOTION_PROPERTY_DATE=
166
+ # NEXT_PUBLIC_NOTION_PROPERTY_TAGS=
167
+ # NEXT_PUBLIC_NOTION_PROPERTY_ICON=
168
+ # NEXT_PUBLIC_ENABLE_RSS=
169
+ # MAILCHIMP_LIST_ID=
170
+ # MAILCHIMP_API_KEY=
171
+ # NEXT_PUBLIC_DEBUG=
172
+ # ENABLE_CACHE=
173
+ # VERCEL_ENV=
174
+ # NEXT_PUBLIC_VERSION=
.eslintrc.js ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ env: {
3
+ browser: true,
4
+ es2021: true,
5
+ node: true
6
+ },
7
+ extends: [
8
+ 'plugin:react/recommended',
9
+ 'plugin:@next/next/recommended',
10
+ 'standard'
11
+ ],
12
+ parserOptions: {
13
+ ecmaFeatures: {
14
+ jsx: true
15
+ },
16
+ ecmaVersion: 12,
17
+ sourceType: 'module'
18
+ },
19
+ plugins: [
20
+ 'react',
21
+ 'react-hooks'
22
+ ],
23
+ settings: {
24
+ react: {
25
+ version: 'detect'
26
+ }
27
+ },
28
+ rules: {
29
+ semi: 0,
30
+ 'react/no-unknown-property': 'off', // <style jsx>
31
+ 'react/prop-types': 'off',
32
+ 'space-before-function-paren': 0,
33
+ 'react-hooks/rules-of-hooks': 'error' // Checks rules of Hooks
34
+ },
35
+ globals: {
36
+ React: true
37
+ }
38
+ }
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ public/videos/video.mp4 filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.js
7
+
8
+ # testing
9
+ /coverage
10
+
11
+ # next.js
12
+ /.next/
13
+ /out/
14
+
15
+ # production
16
+ /build
17
+
18
+ # misc
19
+ .DS_Store
20
+ *.pem
21
+
22
+ # debug
23
+ npm-debug.log*
24
+ yarn-debug.log*
25
+ yarn-error.log*
26
+
27
+ # local env files
28
+ # .env.local # 版本号放在此环境变量中
29
+ .env.development.local
30
+ .env.test.local
31
+ .env.production.local
32
+ .env
33
+
34
+ # vercel
35
+ .vercel
36
+
37
+ # dev
38
+ /data.json
39
+ /pnpm-lock.yaml
40
+ .idea
41
+ .vscode
42
+
43
+
44
+ # sitemap
45
+ /public/robots.txt
46
+ /public/sitemap.xml
47
+ /public/rss/*
48
+
49
+
50
+ # yarn
51
+ package-lock.json
52
+ # yarn.lock
.prettierrc.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "singleQuote": true,
3
+ "semi": false,
4
+ "trailingComma": "none",
5
+ "arrowParens": "avoid"
6
+ }
CONTRIBUTING.md ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributing
2
+
3
+ - [Setup](#setup)
4
+ - [Creating new themes](#creating-new-themes)
5
+ - [Adding localizations](#adding-localizations)
6
+
7
+ Thanks for considering to contribute!
8
+
9
+ ## Setup
10
+
11
+ To contribute to NotionNext, follow these steps:
12
+
13
+ 1. [Fork][fork] the repository to your GitHub account.
14
+ 2. Clone the repository to your device (or use something like Codespaces).
15
+ 3. Create a new branch in the repository.
16
+ 4. Make your modifications.
17
+ 5. Commit your modifications and push the branch.
18
+ 6. [Create a PR][pr] from the branch in your fork to NotionNext' `main` branch.
19
+
20
+ This project is built with [Next.js][next.js] and `yarn` as the package manager.
21
+ Here are some commands that you can use:
22
+
23
+ - `yarn`: install dependencies
24
+ - `yarn dev`: compile and hot-reload for development
25
+ - `yarn build`: compile and minify for production
26
+ - `yarn start`: serve the compiled build in production mode
27
+
28
+ ## Creating new themes
29
+
30
+ If you want to submit your custom theme to NotionNext, copy a new folder in
31
+ [`themes`][themes-dir] from [`example`][example]. The folder name will be the
32
+ theme's key.
33
+
34
+ ## Adding localizations
35
+
36
+ If your language is not yet supported by NotionNext, please contribute a
37
+ localization! Follow these steps to add a new localization:
38
+
39
+ 1. Copy one of the [en-US.js][en-US.js] in [lang-dir][lang-dir] and rename the new
40
+ directory into your language's code ( e.g. `zh-CN.js`).
41
+ 2. Start translating the strings.
42
+ 3. Add your language config to [lang.js][lang.js].
43
+ 4. [Create a PR][pr] with your localization updates.
44
+
45
+ [fork]: https://github.com/tangly1024/NotionNext/fork
46
+ [pr]: https://github.com/tangly1024/NotionNext/compare
47
+ [next.js]: https://github.com/vercel/next.js
48
+ [themes-dir]: themes
49
+ [example]: themes/example
50
+ [lang-dir]: lib/lang
51
+ [en-US.js]: lib/lang/en-US.js
52
+ [lang.js]: lib/lang.js
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ARG NOTION_PAGE_ID
2
+ # Install dependencies only when needed
3
+ FROM node:18-alpine3.18 AS deps
4
+ # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
5
+ RUN apk add --no-cache libc6-compat
6
+ WORKDIR /app
7
+ COPY package.json ./
8
+ RUN yarn install --frozen-lockfile
9
+
10
+ # Rebuild the source code only when needed
11
+ FROM node:18-alpine3.18 AS builder
12
+ ARG NOTION_PAGE_ID
13
+ WORKDIR /app
14
+ COPY --from=deps /app/node_modules ./node_modules
15
+ COPY . .
16
+ RUN yarn build
17
+
18
+ ENV NODE_ENV production
19
+
20
+ EXPOSE 7860
21
+
22
+ # Next.js collects completely anonymous telemetry data about general usage.
23
+ # Learn more here: https://nextjs.org/telemetry
24
+ # Uncomment the following line in case you want to disable telemetry.
25
+ # ENV NEXT_TELEMETRY_DISABLED 1
26
+
27
+ CMD ["yarn", "start"]
blog.config.js ADDED
@@ -0,0 +1,432 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 注: process.env.XX是Vercel的环境变量,配置方式见:https://docs.tangly1024.com/article/how-to-config-notion-next#c4768010ae7d44609b744e79e2f9959a
2
+ const BLOG = {
3
+ // Important page_id!!!Duplicate Template from https://www.notion.so/tanghh/02ab3b8678004aa69e9e415905ef32a5
4
+ NOTION_PAGE_ID:
5
+ process.env.NOTION_PAGE_ID || '02ab3b8678004aa69e9e415905ef32a5',
6
+ PSEUDO_STATIC: process.env.NEXT_PUBLIC_PSEUDO_STATIC || false, // 伪静态路径,开启后所有文章URL都以 .html 结尾。
7
+ NEXT_REVALIDATE_SECOND: process.env.NEXT_PUBLIC_REVALIDATE_SECOND || 5, // 更新内容缓存间隔 单位(秒);即每个页面有5秒的纯静态期、此期间无论多少次访问都不会抓取notion数据;调大该值有助于节省Vercel资源、同时提升访问速率,但也会使文章更新有延迟。
8
+ THEME: process.env.NEXT_PUBLIC_THEME || 'hexo', // 当前主题,在themes文件夹下可找到所有支持的主题;主题名称就是文件夹名,例如 example,fukasawa,gitbook,heo,hexo,landing,matery,medium,next,nobelium,plog,simple
9
+ THEME_SWITCH: process.env.NEXT_PUBLIC_THEME_SWITCH || false, // 是否显示切换主题按钮
10
+ LANG: process.env.NEXT_PUBLIC_LANG || 'zh-CN', // e.g 'zh-CN','en-US' see /lib/lang.js for more.
11
+ SINCE: process.env.NEXT_SINCE || 2021, // e.g if leave this empty, current year will be used.
12
+ APPEARANCE: process.env.NEXT_PUBLIC_APPEARANCE || 'dark', // ['light', 'dark', 'auto'], // light 日间模式 , dark夜间模式, auto根据时间和主题自动夜间模式
13
+ APPEARANCE_DARK_TIME: process.env.NEXT_PUBLIC_APPEARANCE_DARK_TIME || 'false', // [18, 6], // 夜间模式起至时间,false时关闭根据时间自动切换夜间模式
14
+
15
+ // 3.14.1版本后,欢迎语在此配置,英文逗号隔开 , 即可支持多个欢迎语打字效果。
16
+ GREETING_WORDS: process.env.NEXT_PUBLIC_GREETING_WORDS || 'Hi,我是我, Hi,我就是我,Hi,我还是我,欢迎来到我的博客🎉',
17
+
18
+ CUSTOM_MENU: process.env.NEXT_PUBLIC_CUSTOM_MENU || false, // 支持Menu 类型,从3.12.0版本起,各主题将逐步支持灵活的二级菜单配置,替代了原来的Page类型,此配置是试验功能、默认关闭。
19
+
20
+ AUTHOR: process.env.NEXT_PUBLIC_AUTHOR || 'NotionNext', // 您的昵称 例如 tangly1024
21
+ BIO: process.env.NEXT_PUBLIC_BIO || '一个普通的干饭人🍚', // 作者简介
22
+ LINK: process.env.NEXT_PUBLIC_LINK || 'https://tangly1024.com', // 网站地址
23
+ KEYWORDS: process.env.NEXT_PUBLIC_KEYWORD || 'Notion, 博客', // 网站关键词 英文逗号隔开
24
+
25
+ // 社交链接,不需要可留空白,例如 CONTACT_WEIBO:''
26
+ CONTACT_EMAIL: process.env.NEXT_PUBLIC_CONTACT_EMAIL || '', // 邮箱地址 例如mail@tangly1024.com
27
+ CONTACT_WEIBO: process.env.NEXT_PUBLIC_CONTACT_WEIBO || '', // 你的微博个人主页
28
+ CONTACT_TWITTER: process.env.NEXT_PUBLIC_CONTACT_TWITTER || '', // 你的twitter个人主页
29
+ CONTACT_GITHUB: process.env.NEXT_PUBLIC_CONTACT_GITHUB || '', // 你的github个人主页 例如 https://github.com/tangly1024
30
+ CONTACT_TELEGRAM: process.env.NEXT_PUBLIC_CONTACT_TELEGRAM || '', // 你的telegram 地址 例如 https://t.me/tangly_1024
31
+ CONTACT_LINKEDIN: process.env.NEXT_PUBLIC_CONTACT_LINKEDIN || '', // 你的linkedIn 首页
32
+ CONTACT_INSTAGRAM: process.env.NEXT_PUBLIC_CONTACT_INSTAGRAM || '', // 您的instagram地址
33
+ CONTACT_BILIBILI: process.env.NEXT_PUBLIC_CONTACT_BILIBILI || '', // B站主页
34
+ CONTACT_YOUTUBE: process.env.NEXT_PUBLIC_CONTACT_YOUTUBE || '', // Youtube主页
35
+
36
+ NOTION_HOST: process.env.NEXT_PUBLIC_NOTION_HOST || 'https://www.notion.so', // Notion域名,您可以选择用自己的域名进行反向代理,如果不懂得什么是反向代理,请勿修改此项
37
+
38
+ BLOG_FAVICON: process.env.NEXT_PUBLIC_FAVICON || '/favicon.ico', // blog favicon 配置, 默认使用 /public/favicon.ico,支持在线图片,如 https://img.imesong.com/favicon.png
39
+
40
+ IMAGE_COMPRESS_WIDTH: process.env.NEXT_PUBLIC_IMAGE_COMPRESS_WIDTH || 800, // 图片压缩宽度默认值,作用于博客封面和文章内容 越小加载图片越快
41
+ IMAGE_ZOOM_IN_WIDTH: process.env.NEXT_PUBLIC_IMAGE_ZOOM_IN_WIDTH || 1200, // 文章图片点击放大后的画质宽度,不代表在网页中的实际展示宽度
42
+ RANDOM_IMAGE_URL: process.env.NEXT_PUBLIC_RANDOM_IMAGE_URL || '', // 随机图片API,如果未配置下面的关键字,主页封面,头像,文章封面图都会被替换为随机图片
43
+ RANDOM_IMAGE_REPLACE_TEXT: process.env.NEXT_PUBLIC_RANDOM_IMAGE_NOT_REPLACE_TEXT || 'images.unsplash.com', // 触发替换图片的 url 关键字(多个支持用英文逗号分开),只有图片地址中包含此关键字才会替换为上方随机图片url
44
+ // eg: images.unsplash.com(notion图床的所有图片都会替换),如果你在 notion 里已经添加了一个随机图片 url,恰巧那个服务跑路或者挂掉,想一键切换所有配图可以将该 url 配置在这里
45
+ // 默认下会将你上传到 notion的主页封面图和头像也给替换,建议将主页封面图和头像放在其他图床,在 notion 里配置 link 即可。
46
+
47
+ // START ************网站字体*****************
48
+ // ['font-serif','font-sans'] 两种可选,分别是衬线和无衬线: 参考 https://www.jianshu.com/p/55e410bd2115
49
+ // 后面空格隔开的font-light的字体粗细,留空是默认粗细;参考 https://www.tailwindcss.cn/docs/font-weight
50
+ FONT_STYLE: process.env.NEXT_PUBLIC_FONT_STYLE || 'font-sans font-light',
51
+ // 字体CSS 例如 https://npm.elemecdn.com/lxgw-wenkai-webfont@1.6.0/style.css
52
+ FONT_URL: [
53
+ // 'https://npm.elemecdn.com/lxgw-wenkai-webfont@1.6.0/style.css',
54
+ 'https://fonts.googleapis.com/css?family=Bitter&display=swap',
55
+ 'https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300&display=swap',
56
+ 'https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300&display=swap'
57
+ ],
58
+ // 无衬线字体 例如'"LXGW WenKai"'
59
+ FONT_SANS: [
60
+ // '"LXGW WenKai"',
61
+ '"PingFang SC"',
62
+ '-apple-system',
63
+ 'BlinkMacSystemFont',
64
+ '"Hiragino Sans GB"',
65
+ '"Microsoft YaHei"',
66
+ '"Segoe UI Emoji"',
67
+ '"Segoe UI Symbol"',
68
+ '"Segoe UI"',
69
+ '"Noto Sans SC"',
70
+ 'HarmonyOS_Regular',
71
+ '"Helvetica Neue"',
72
+ 'Helvetica',
73
+ '"Source Han Sans SC"',
74
+ 'Arial',
75
+ 'sans-serif',
76
+ '"Apple Color Emoji"'
77
+ ],
78
+ // 衬线字体 例如'"LXGW WenKai"'
79
+ FONT_SERIF: [
80
+ // '"LXGW WenKai"',
81
+ 'Bitter',
82
+ '"Noto Serif SC"',
83
+ 'SimSun',
84
+ '"Times New Roman"',
85
+ 'Times',
86
+ 'serif',
87
+ '"Segoe UI Emoji"',
88
+ '"Segoe UI Symbol"',
89
+ '"Apple Color Emoji"'
90
+ ],
91
+ 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
92
+
93
+ // END ************网站字体*****************
94
+ CAN_COPY: process.env.NEXT_PUBLIC_CAN_COPY || true, // 是否允许复制页面内容 默认允许,如果设置为false、则全栈禁止复制内容。
95
+ CUSTOM_RIGHT_CLICK_CONTEXT_MENU: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU || true, // 自定义右键菜单,覆盖系统菜单
96
+ CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH || true,
97
+
98
+ // 自定义外部脚本,外部样式
99
+ CUSTOM_EXTERNAL_JS: ['https://cdn.jsdelivr.net/gh/no2y/jslib@main/lanterns/lanterns.min.js','https://fastly.jsdelivr.net/gh/stevenjoezhang/live2d-widget@latest/autoload.js'], // https://proxy.api.030101.xyz/ e.g. ['http://xx.com/script.js','http://xx.com/script.js']
100
+ CUSTOM_EXTERNAL_CSS: [''], // e.g. ['http://xx.com/style.css','http://xx.com/style.css']
101
+
102
+ // 侧栏布局 是否反转(左变右,右变左) 已支持主题: hexo next medium fukasawa example
103
+ LAYOUT_SIDEBAR_REVERSE: process.env.NEXT_PUBLIC_LAYOUT_SIDEBAR_REVERSE || false,
104
+
105
+ // 一个小插件展示你的facebook fan page~ @see https://tw.andys.pro/article/add-facebook-fanpage-notionnext
106
+ FACEBOOK_PAGE_TITLE: process.env.NEXT_PUBLIC_FACEBOOK_PAGE_TITLE || null, // 邊欄 Facebook Page widget 的標題欄,填''則無標題欄 e.g FACEBOOK 粉絲團'
107
+ FACEBOOK_PAGE: process.env.NEXT_PUBLIC_FACEBOOK_PAGE || null, // Facebook Page 的連結 e.g https://www.facebook.com/tw.andys.pro
108
+ FACEBOOK_PAGE_ID: process.env.NEXT_PUBLIC_FACEBOOK_PAGE_ID || '', // Facebook Page ID 來啟用 messenger 聊天功能
109
+ FACEBOOK_APP_ID: process.env.NEXT_PUBLIC_FACEBOOK_APP_ID || '', // Facebook App ID 來啟用 messenger 聊天功能 获取: https://developers.facebook.com/
110
+
111
+ BEI_AN: process.env.NEXT_PUBLIC_BEI_AN || '', // 备案号 闽ICP备XXXXXXX
112
+
113
+ // START********代码相关********
114
+ // PrismJs 代码相关
115
+ PRISM_JS_PATH: 'https://npm.elemecdn.com/prismjs@1.29.0/components/',
116
+ PRISM_JS_AUTO_LOADER: 'https://npm.elemecdn.com/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js',
117
+
118
+ // 代码主题 @see https://github.com/PrismJS/prism-themes
119
+ 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', // 代码块默认主题
120
+ PRISM_THEME_SWITCH: process.env.NEXT_PUBLIC_PRISM_THEME_SWITCH || true, // 是否开启浅色/深色模式代码主题切换; 开启后将显示以下两个主题
121
+ 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', // 浅色模式主题
122
+ 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', // 深色模式主题
123
+
124
+ CODE_MAC_BAR: process.env.NEXT_PUBLIC_CODE_MAC_BAR || true, // 代码左上角显示mac的红黄绿图标
125
+ CODE_LINE_NUMBERS: process.env.NEXT_PUBLIC_CODE_LINE_NUMBERS || false, // 是否显示行号
126
+ CODE_COLLAPSE: process.env.NEXT_PUBLIC_CODE_COLLAPSE || true, // 是否支持折叠代码框
127
+ CODE_COLLAPSE_EXPAND_DEFAULT: process.env.NEXT_PUBLIC_CODE_COLLAPSE_EXPAND_DEFAULT || true, // 折叠代码默认是展开状态
128
+
129
+ // END********代码相关********
130
+
131
+ // Mermaid 图表CDN
132
+ MERMAID_CDN: process.env.NEXT_PUBLIC_MERMAID_CDN || 'https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.2.4/mermaid.min.js', // CDN
133
+ // QRCodeCDN
134
+ QR_CODE_CDN: process.env.NEXT_PUBLIC_QR_CODE_CDN || 'https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js',
135
+
136
+ BACKGROUND_LIGHT: '#eeeeee', // use hex value, don't forget '#' e.g #fffefc
137
+ BACKGROUND_DARK: '#000000', // use hex value, don't forget '#'
138
+ SUB_PATH: '', // leave this empty unless you want to deploy in a folder
139
+
140
+ POST_SHARE_BAR_ENABLE: process.env.NEXT_PUBLIC_POST_SHARE_BAR || 'true', // 文章分享功能 ,将在底部显示一个分享条
141
+ POSTS_SHARE_SERVICES: process.env.NEXT_PUBLIC_POST_SHARE_SERVICES || 'link,wechat,qq,weibo,email,facebook,twitter,telegram,messenger,line,reddit,whatsapp,linkedin', // 分享的服務,按顺序显示,逗号隔开
142
+ // 所有支持的分享服务:link(复制链接),wechat(微信),qq,weibo(微博),email(邮件),facebook,twitter,telegram,messenger,line,reddit,whatsapp,linkedin,vkshare,okshare,tumblr,livejournal,mailru,viber,workplace,pocket,instapaper,hatena
143
+
144
+ POST_URL_PREFIX: process.env.NEXT_PUBLIC_POST_URL_PREFIX || 'article',
145
+ // POST类型文章的默认路径前缀,例如默认POST类型的路径是 /article/[slug]
146
+ // 如果此项配置为 '' 空, 则文章将没有前缀路径,使用场景: 希望文章前缀路径为 /post 的情况 支持多级
147
+ // 支援類似 WP 可自訂文章連結格式的功能:https://wordpress.org/documentation/article/customize-permalinks/,目前只先實作 %year%/%month%/%day%
148
+ // 例:如想連結改成前綴 article + 時間戳記,可變更為: 'article/%year%/%month%/%day%'
149
+
150
+ POST_LIST_STYLE: process.env.NEXT_PUBLIC_POST_LIST_STYLE || 'page', // ['page','scroll] 文章列表样式:页码分页、单页滚动加载
151
+ POST_LIST_PREVIEW: process.env.NEXT_PUBLIC_POST_PREVIEW || 'false', // 是否在列表加载文章预览
152
+ POST_PREVIEW_LINES: 12, // 预览博客行数
153
+ POST_RECOMMEND_COUNT: 6, // 推荐文章数量
154
+ POSTS_PER_PAGE: 12, // post counts per page
155
+ POSTS_SORT_BY: process.env.NEXT_PUBLIC_POST_SORT_BY || 'notion', // 排序方式 'date'按时间,'notion'由notion控制
156
+
157
+ ALGOLIA_APP_ID: process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || null, // 在这里查看 https://dashboard.algolia.com/account/api-keys/
158
+ ALGOLIA_ADMIN_APP_KEY: process.env.ALGOLIA_ADMIN_APP_KEY || null, // 管理后台的KEY,不要暴露在代码中,在这里查看 https://dashboard.algolia.com/account/api-keys/
159
+ ALGOLIA_SEARCH_ONLY_APP_KEY: process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_APP_KEY || null, // 客户端搜索用的KEY
160
+ ALGOLIA_INDEX: process.env.NEXT_PUBLIC_ALGOLIA_INDEX || null, // 在Algolia中创建一个index用作数据库
161
+ // ALGOLIA_RECREATE_DATA: process.env.ALGOLIA_RECREATE_DATA || process.env.npm_lifecycle_event === 'build', // 为true时重新构建索引数据; 默认在build时会构建
162
+
163
+ PREVIEW_CATEGORY_COUNT: 16, // 首页最多展示的分类数量,0为不限制
164
+ PREVIEW_TAG_COUNT: 16, // 首页最多展示的标签数量,0为不限制
165
+
166
+ POST_DISABLE_GALLERY_CLICK: process.env.NEXT_PUBLIC_POST_DISABLE_GALLERY_CLICK || false, // 画册视图禁止点击,方便在友链页面的画册插入链接
167
+
168
+ // ********动态特效相关********
169
+ // 鼠标点击烟花特效
170
+ FIREWORKS: process.env.NEXT_PUBLIC_FIREWORKS || false, // 开关
171
+ // 烟花色彩,感谢 https://github.com/Vixcity 提交的色彩
172
+ FIREWORKS_COLOR: [
173
+ '255, 20, 97',
174
+ '24, 255, 146',
175
+ '90, 135, 255',
176
+ '251, 243, 140'
177
+ ],
178
+
179
+ // 樱花飘落特效
180
+ SAKURA: process.env.NEXT_PUBLIC_SAKURA || false, // 开关
181
+ // 漂浮线段特效
182
+ NEST: process.env.NEXT_PUBLIC_NEST || false, // 开关
183
+ // 动态彩带特效
184
+ FLUTTERINGRIBBON: process.env.NEXT_PUBLIC_FLUTTERINGRIBBON || false, // 开关
185
+ // 静态彩带特效
186
+ RIBBON: process.env.NEXT_PUBLIC_RIBBON || false, // 开关
187
+ // 星空雨特效 黑夜模式才会生效
188
+ STARRY_SKY: process.env.NEXT_PUBLIC_STARRY_SKY || false, // 开关
189
+
190
+ // ********挂件组件相关********
191
+ // AI 文章摘要生成 @see https://docs_s.tianli0.top/
192
+ TianliGPT_CSS: process.env.NEXT_PUBLIC_TIANLI_GPT_CSS || 'https://cdn1.tianli0.top/gh/zhheo/Post-Abstract-AI@0.15.2/tianli_gpt.css',
193
+ TianliGPT_JS: process.env.NEXT_PUBLIC_TIANLI_GPT_JS || 'https://cdn1.tianli0.top/gh/zhheo/Post-Abstract-AI@0.15.2/tianli_gpt.js',
194
+ TianliGPT_KEY: process.env.NEXT_PUBLIC_TIANLI_GPT_KEY || '',
195
+
196
+ // Chatbase 是否显示chatbase机器人 https://www.chatbase.co/
197
+ CHATBASE_ID: process.env.NEXT_PUBLIC_CHATBASE_ID || 'p2_j9YrCgq7Bmj810MQNj',
198
+ // WebwhizAI 机器人 @see https://github.com/webwhiz-ai/webwhiz
199
+ WEB_WHIZ_ENABLED: process.env.NEXT_PUBLIC_WEB_WHIZ_ENABLED || false, // 是否显示
200
+ WEB_WHIZ_BASE_URL: process.env.NEXT_PUBLIC_WEB_WHIZ_BASE_URL || 'https://api.webwhiz.ai', // 可以自建服务器
201
+ WEB_WHIZ_CHAT_BOT_ID: process.env.NEXT_PUBLIC_WEB_WHIZ_CHAT_BOT_ID || null, // 在后台获取ID
202
+ DIFY_CHATBOT_ENABLED: process.env.NEXT_PUBLIC_DIFY_CHATBOT_ENABLED || false,
203
+ DIFY_CHATBOT_BASE_URL: process.env.NEXT_PUBLIC_DIFY_CHATBOT_BASE_URL || '',
204
+ DIFY_CHATBOT_TOKEN: process.env.NEXT_PUBLIC_DIFY_CHATBOT_TOKEN || '',
205
+ // 悬浮挂件
206
+ WIDGET_PET: process.env.NEXT_PUBLIC_WIDGET_PET || true, // 是否显示宠物挂件
207
+ WIDGET_PET_LINK:
208
+ process.env.NEXT_PUBLIC_WIDGET_PET_LINK ||
209
+ 'https://cdn.jsdelivr.net/npm/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json', // 挂件模型地址 @see https://github.com/xiazeyu/live2d-widget-models
210
+ WIDGET_PET_SWITCH_THEME: process.env.NEXT_PUBLIC_WIDGET_PET_SWITCH_THEME || true, // 点击宠物挂件切换博客主题
211
+
212
+ // 音乐播放插件
213
+ MUSIC_PLAYER: process.env.NEXT_PUBLIC_MUSIC_PLAYER || false, // 是否使用音乐播放插件
214
+ MUSIC_PLAYER_VISIBLE: process.env.NEXT_PUBLIC_MUSIC_PLAYER_VISIBLE || true, // 是否在左下角显示播放和切换,如果使用播放器,打开自动播放再隐藏,就会以类似背景音乐的方式播放,无法取消和暂停
215
+ MUSIC_PLAYER_AUTO_PLAY:
216
+ process.env.NEXT_PUBLIC_MUSIC_PLAYER_AUTO_PLAY || true, // 是否自动播放,不过自动播放时常不生效(移动设备不支持自动播放)
217
+ MUSIC_PLAYER_LRC_TYPE: process.env.NEXT_PUBLIC_MUSIC_PLAYER_LRC_TYPE || '0', // 歌词显示类型,可选值: 3 | 1 | 0(0:禁用 lrc 歌词,1:lrc 格式的字符串,3:lrc 文件 url)(前提是有配置歌词路径,对 meting 无效)
218
+ MUSIC_PLAYER_CDN_URL:
219
+ process.env.NEXT_PUBLIC_MUSIC_PLAYER_CDN_URL ||
220
+ 'https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/aplayer/1.10.1/APlayer.min.js',
221
+ MUSIC_PLAYER_ORDER: process.env.NEXT_PUBLIC_MUSIC_PLAYER_ORDER || 'list', // 默认播放方式,顺序 list,随机 random
222
+ MUSIC_PLAYER_AUDIO_LIST: [
223
+ // 示例音乐列表。除了以下配置外,还可配置歌词,具体配置项看此文档 https://aplayer.js.org/#/zh-Hans/
224
+ {
225
+ name: '风を共に舞う気持ち',
226
+ artist: 'Falcom Sound Team jdk',
227
+ url: 'https://music.163.com/song/media/outer/url?id=731419.mp3',
228
+ cover:
229
+ 'https://p2.music.126.net/kn6ugISTonvqJh3LHLaPtQ==/599233837187278.jpg'
230
+ },
231
+ {
232
+ name: '王都グランセル',
233
+ artist: 'Falcom Sound Team jdk',
234
+ url: 'https://music.163.com/song/media/outer/url?id=731355.mp3',
235
+ cover:
236
+ 'https://p1.music.126.net/kn6ugISTonvqJh3LHLaPtQ==/599233837187278.jpg'
237
+ }
238
+ ],
239
+ MUSIC_PLAYER_METING: process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING || false, // 是否要开启 MetingJS,从平台获取歌单。会覆盖自定义的 MUSIC_PLAYER_AUDIO_LIST,更多配置信息:https://github.com/metowolf/MetingJS
240
+ MUSIC_PLAYER_METING_SERVER:
241
+ process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_SERVER || 'netease', // 音乐平台,[netease, tencent, kugou, xiami, baidu]
242
+ MUSIC_PLAYER_METING_ID:
243
+ process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_ID || '60198', // 对应歌单的 id
244
+ MUSIC_PLAYER_METING_LRC_TYPE:
245
+ process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_LRC_TYPE || '1', // 可选值: 3 | 1 | 0(0:禁用 lrc 歌词,1:lrc 格式的字符串,3:lrc 文件 url)
246
+
247
+ // ********挂件组件相关********
248
+ // ----> 评论互动 可同时开启多个支持 WALINE VALINE GISCUS CUSDIS UTTERRANCES GITALK
249
+
250
+ COMMENT_HIDE_SINGLE_TAB: process.env.NEXT_PUBLIC_COMMENT_HIDE_SINGLE_TAB || false, // Whether hide the tab when there's no tabs. 只有一个评论组件时是否隐藏切换组件的标签页
251
+
252
+ // artalk 评论插件
253
+ COMMENT_ARTALK_SERVER: process.env.NEXT_PUBLIC_COMMENT_ARTALK_SERVER || '', // ArtalkServert后端地址 https://artalk.js.org/guide/deploy.html
254
+ 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
255
+ 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
256
+
257
+ // twikoo
258
+ COMMENT_TWIKOO_ENV_ID: process.env.NEXT_PUBLIC_COMMENT_ENV_ID || '', // TWIKOO后端地址 腾讯云环境填envId;Vercel环境填域名,教程:https://tangly1024.com/article/notionnext-twikoo
259
+ COMMENT_TWIKOO_COUNT_ENABLE: process.env.NEXT_PUBLIC_COMMENT_TWIKOO_COUNT_ENABLE || false, // 博客列表是否显示评论数
260
+ 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
261
+
262
+ // utterance
263
+ COMMENT_UTTERRANCES_REPO:
264
+ process.env.NEXT_PUBLIC_COMMENT_UTTERRANCES_REPO || '', // 你的代码仓库名, 例如我是 'tangly1024/NotionNext'; 更多文档参考 https://utteranc.es/
265
+
266
+ // giscus @see https://giscus.app/
267
+ COMMENT_GISCUS_REPO: process.env.NEXT_PUBLIC_COMMENT_GISCUS_REPO || '', // 你的Github仓库名 e.g 'tangly1024/NotionNext'
268
+ COMMENT_GISCUS_REPO_ID: process.env.NEXT_PUBLIC_COMMENT_GISCUS_REPO_ID || '', // 你的Github Repo ID e.g ( 設定完 giscus 即可看到 )
269
+ COMMENT_GISCUS_CATEGORY_ID:
270
+ process.env.NEXT_PUBLIC_COMMENT_GISCUS_CATEGORY_ID || '', // 你的Github Discussions 內的 Category ID ( 設定完 giscus 即可看到 )
271
+ COMMENT_GISCUS_MAPPING:
272
+ process.env.NEXT_PUBLIC_COMMENT_GISCUS_MAPPING || 'pathname', // 你的Github Discussions 使用哪種方式來標定文章, 預設 'pathname'
273
+ COMMENT_GISCUS_REACTIONS_ENABLED:
274
+ process.env.NEXT_PUBLIC_COMMENT_GISCUS_REACTIONS_ENABLED || '1', // 你的 Giscus 是否開啟文章表情符號 '1' 開啟 "0" 關閉 預設開啟
275
+ COMMENT_GISCUS_EMIT_METADATA:
276
+ process.env.NEXT_PUBLIC_COMMENT_GISCUS_EMIT_METADATA || '0', // 你的 Giscus 是否提取 Metadata '1' 開啟 '0' 關閉 預設關閉
277
+ COMMENT_GISCUS_INPUT_POSITION:
278
+ process.env.NEXT_PUBLIC_COMMENT_GISCUS_INPUT_POSITION || 'bottom', // 你的 Giscus 發表留言位置 'bottom' 尾部 'top' 頂部, 預設 'bottom'
279
+ COMMENT_GISCUS_LANG: process.env.NEXT_PUBLIC_COMMENT_GISCUS_LANG || 'zh-CN', // 你的 Giscus 語言 e.g 'en', 'zh-TW', 'zh-CN', 預設 'en'
280
+ COMMENT_GISCUS_LOADING:
281
+ process.env.NEXT_PUBLIC_COMMENT_GISCUS_LOADING || 'lazy', // 你的 Giscus 載入是否漸進式載入, 預設 'lazy'
282
+ COMMENT_GISCUS_CROSSORIGIN:
283
+ process.env.NEXT_PUBLIC_COMMENT_GISCUS_CROSSORIGIN || 'anonymous', // 你的 Giscus 可以跨網域, 預設 'anonymous'
284
+
285
+ COMMENT_CUSDIS_APP_ID: process.env.NEXT_PUBLIC_COMMENT_CUSDIS_APP_ID || '', // data-app-id 36位 see https://cusdis.com/
286
+ COMMENT_CUSDIS_HOST:
287
+ process.env.NEXT_PUBLIC_COMMENT_CUSDIS_HOST || 'https://cusdis.com', // data-host, change this if you're using self-hosted version
288
+ COMMENT_CUSDIS_SCRIPT_SRC:
289
+ process.env.NEXT_PUBLIC_COMMENT_CUSDIS_SCRIPT_SRC ||
290
+ '/js/cusdis.es.js', // change this if you're using self-hosted version
291
+
292
+ // gitalk评论插件 更多参考 https://gitalk.github.io/
293
+ COMMENT_GITALK_REPO: process.env.NEXT_PUBLIC_COMMENT_GITALK_REPO || '', // 你的Github仓库名,例如 'NotionNext'
294
+ COMMENT_GITALK_OWNER: process.env.NEXT_PUBLIC_COMMENT_GITALK_OWNER || '', // 你的用户名 e.g tangly1024
295
+ COMMENT_GITALK_ADMIN: process.env.NEXT_PUBLIC_COMMENT_GITALK_ADMIN || '', // 管理员用户名、一般是自己 e.g 'tangly1024'
296
+ COMMENT_GITALK_CLIENT_ID:
297
+ process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_ID || '', // e.g 20位ID , 在gitalk后台获取
298
+ COMMENT_GITALK_CLIENT_SECRET:
299
+ process.env.NEXT_PUBLIC_COMMENT_GITALK_CLIENT_SECRET || '', // e.g 40位ID, 在gitalk后台获取
300
+ COMMENT_GITALK_DISTRACTION_FREE_MODE: false, // 类似facebook的无干扰模式
301
+ 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
302
+ 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
303
+
304
+ COMMENT_GITTER_ROOM: process.env.NEXT_PUBLIC_COMMENT_GITTER_ROOM || '', // gitter聊天室 see https://gitter.im/ 不需要则留空
305
+ COMMENT_DAO_VOICE_ID: process.env.NEXT_PUBLIC_COMMENT_DAO_VOICE_ID || '', // DaoVoice http://dashboard.daovoice.io/get-started
306
+ COMMENT_TIDIO_ID: process.env.NEXT_PUBLIC_COMMENT_TIDIO_ID || '', // [tidio_id] -> //code.tidio.co/[tidio_id].js
307
+
308
+ COMMENT_VALINE_CDN: process.env.NEXT_PUBLIC_VALINE_CDN || 'https://unpkg.com/valine@1.5.1/dist/Valine.min.js',
309
+ 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
310
+ COMMENT_VALINE_APP_KEY: process.env.NEXT_PUBLIC_VALINE_KEY || '',
311
+ COMMENT_VALINE_SERVER_URLS: process.env.NEXT_PUBLIC_VALINE_SERVER_URLS || '', // 该配置适用于国内自定义域名用户, 海外版本会自动检测(无需手动填写) @see https://valine.js.org/configuration.html#serverURLs
312
+ COMMENT_VALINE_PLACEHOLDER:
313
+ process.env.NEXT_PUBLIC_VALINE_PLACEHOLDER || '抢个沙发吧~', // 可以搭配后台管理评论 https://github.com/DesertsP/Valine-Admin 便于查看评论,以及邮件通知,垃圾评论过滤等功能
314
+
315
+ 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
316
+ COMMENT_WALINE_RECENT: process.env.NEXT_PUBLIC_WALINE_RECENT || false, // 最新评论
317
+
318
+ // 此评论系统基于WebMention,细节可参考https://webmention.io
319
+ // 它是一个基于IndieWeb理念的开放式评论系统,下方COMMENT_WEBMENTION包含的属性皆需配置:
320
+ // ENABLE: 是否开启
321
+ // AUTH: Webmention使用的IndieLogin,可使用Twitter或Github个人页面连结
322
+ // HOSTNAME: Webmention绑定之网域,通常即为本站网址
323
+ // TWITTER_USERNAME: 评论显示区域需要的资讯
324
+ // TOKEN: Webmention的API token
325
+ COMMENT_WEBMENTION_ENABLE: process.env.NEXT_PUBLIC_WEBMENTION_ENABLE || false,
326
+ COMMENT_WEBMENTION_AUTH: process.env.NEXT_PUBLIC_WEBMENTION_AUTH || '',
327
+ COMMENT_WEBMENTION_HOSTNAME: process.env.NEXT_PUBLIC_WEBMENTION_HOSTNAME || '',
328
+ COMMENT_WEBMENTION_TWITTER_USERNAME: process.env.NEXT_PUBLIC_TWITTER_USERNAME || '',
329
+ COMMENT_WEBMENTION_TOKEN: process.env.NEXT_PUBLIC_WEBMENTION_TOKEN || '',
330
+
331
+ // <---- 评论插件
332
+
333
+ // ----> 站点统计
334
+ ANALYTICS_VERCEL: process.env.NEXT_PUBLIC_ANALYTICS_VERCEL || false, // vercel自带的统计 https://vercel.com/docs/concepts/analytics/quickstart https://github.com/tangly1024/NotionNext/issues/897
335
+ ANALYTICS_BUSUANZI_ENABLE: process.env.NEXT_PUBLIC_ANALYTICS_BUSUANZI_ENABLE || true, // 展示网站阅读量、访问数 see http://busuanzi.ibruce.info/
336
+ ANALYTICS_BAIDU_ID: process.env.NEXT_PUBLIC_ANALYTICS_BAIDU_ID || '', // e.g 只需要填写百度统计的id,[baidu_id] -> https://hm.baidu.com/hm.js?[baidu_id]
337
+ ANALYTICS_CNZZ_ID: process.env.NEXT_PUBLIC_ANALYTICS_CNZZ_ID || '', // 只需要填写站长统计的id, [cnzz_id] -> https://s9.cnzz.com/z_stat.php?id=[cnzz_id]&web_id=[cnzz_id]
338
+ ANALYTICS_GOOGLE_ID: process.env.NEXT_PUBLIC_ANALYTICS_GOOGLE_ID || '', // 谷歌Analytics的id e.g: G-XXXXXXXXXX
339
+
340
+ // 51la 站点统计 https://www.51.la/
341
+ ANALYTICS_51LA_ID: process.env.NEXT_PUBLIC_ANALYTICS_51LA_ID || '', // id,在51la后台获取 参阅 https://docs.tangly1024.com/article/notion-next-51-la
342
+ ANALYTICS_51LA_CK: process.env.NEXT_PUBLIC_ANALYTICS_51LA_CK || '', // ck,在51la后台获取
343
+
344
+ // Matomo 网站统计
345
+ MATOMO_HOST_URL: process.env.NEXT_PUBLIC_MATOMO_HOST_URL || '', // Matomo服务器地址,不带斜杠
346
+ MATOMO_SITE_ID: process.env.NEXT_PUBLIC_MATOMO_SITE_ID || '', // Matomo网站ID
347
+ // ACKEE网站访客统计工具
348
+ ANALYTICS_ACKEE_TRACKER: process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_TRACKER || '', // e.g 'https://ackee.tangly1024.com/tracker.js'
349
+ ANALYTICS_ACKEE_DATA_SERVER: process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_DATA_SERVER || '', // e.g https://ackee.tangly1024.com , don't end with a slash
350
+ ANALYTICS_ACKEE_DOMAIN_ID: process.env.NEXT_PUBLIC_ANALYTICS_ACKEE_DOMAIN_ID || '', // e.g '82e51db6-dec2-423a-b7c9-b4ff7ebb3302'
351
+
352
+ SEO_GOOGLE_SITE_VERIFICATION:
353
+ process.env.NEXT_PUBLIC_SEO_GOOGLE_SITE_VERIFICATION || '', // Remove the value or replace it with your own google site verification code
354
+
355
+ SEO_BAIDU_SITE_VERIFICATION:
356
+ process.env.NEXT_PUBLIC_SEO_BAIDU_SITE_VERIFICATION || '', // Remove the value or replace it with your own google site verification code
357
+
358
+ // 微软 Clarity 站点分析
359
+ CLARITY_ID: process.env.NEXT_PUBLIC_CLARITY_ID || null, // 只需要复制Clarity脚本中的ID部分,ID是一个十位的英文数字组合
360
+
361
+ // <---- 站点统计
362
+
363
+ // START---->营收相关
364
+
365
+ // 谷歌广告
366
+ ADSENSE_GOOGLE_ID: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_ID || '', // 谷歌广告ID e.g ca-pub-xxxxxxxxxxxxxxxx
367
+ ADSENSE_GOOGLE_TEST: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_TEST || false, // 谷歌广告ID测试模式,这种模式获取假的测试广告,用于开发 https://www.tangly1024.com/article/local-dev-google-adsense
368
+ ADSENSE_GOOGLE_SLOT_IN_ARTICLE: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_IN_ARTICLE || '3806269138', // Google AdScene>广告>按单元广告>新建文章内嵌广告 粘贴html代码中的data-ad-slot值
369
+ ADSENSE_GOOGLE_SLOT_FLOW: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_FLOW || '1510444138', // Google AdScene>广告>按单元广告>新建信息流广告
370
+ ADSENSE_GOOGLE_SLOT_NATIVE: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_NATIVE || '4980048999', // Google AdScene>广告>按单元广告>新建原生广告
371
+ ADSENSE_GOOGLE_SLOT_AUTO: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_AUTO || '8807314373', // Google AdScene>广告>按单元广告>新建展示广告 (自动广告)
372
+
373
+ // 万维广告
374
+ AD_WWADS_ID: process.env.NEXT_PUBLIC_WWAD_ID || null, // https://wwads.cn/ 创建您的万维广告单元ID
375
+ AD_WWADS_BLOCK_DETECT: process.env.NEXT_PUBLIC_WWADS_AD_BLOCK_DETECT || false, // 是否开启WWADS广告屏蔽插件检测,开启后会在广告位上以文字提示 @see https://github.com/bytegravity/whitelist-wwads
376
+
377
+ // END<----营收相关
378
+
379
+ // 自定义配置notion数据库字段名
380
+ NOTION_PROPERTY_NAME: {
381
+ password: process.env.NEXT_PUBLIC_NOTION_PROPERTY_PASSWORD || 'password',
382
+ type: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE || 'type', // 文章类型,
383
+ type_post: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_POST || 'Post', // 当type文章类型与此值相同时,为博文。
384
+ type_page: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_PAGE || 'Page', // 当type文章类型与此值相同时,为单页。
385
+ type_notice:
386
+ process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_NOTICE || 'Notice', // 当type文章类型与此值相同时,为公告。
387
+ type_menu: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_MENU || 'Menu', // 当type文章类型与此值相同���,为菜单。
388
+ type_sub_menu:
389
+ process.env.NEXT_PUBLIC_NOTION_PROPERTY_TYPE_SUB_MENU || 'SubMenu', // 当type文章类型与此值相同时,为子菜单。
390
+ title: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TITLE || 'title', // 文章标题
391
+ status: process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS || 'status',
392
+ status_publish:
393
+ process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS_PUBLISH || 'Published', // 当status状态值与此相同时为发布,可以为中文
394
+ status_invisible:
395
+ process.env.NEXT_PUBLIC_NOTION_PROPERTY_STATUS_INVISIBLE || 'Invisible', // 当status状态值与此相同时为隐藏发布,可以为中文 , 除此之外其他页面状态不会显示在博客上
396
+ summary: process.env.NEXT_PUBLIC_NOTION_PROPERTY_SUMMARY || 'summary',
397
+ slug: process.env.NEXT_PUBLIC_NOTION_PROPERTY_SLUG || 'slug',
398
+ category: process.env.NEXT_PUBLIC_NOTION_PROPERTY_CATEGORY || 'category',
399
+ date: process.env.NEXT_PUBLIC_NOTION_PROPERTY_DATE || 'date',
400
+ tags: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TAGS || 'tags',
401
+ icon: process.env.NEXT_PUBLIC_NOTION_PROPERTY_ICON || 'icon'
402
+ },
403
+
404
+ // RSS订阅
405
+ ENABLE_RSS: process.env.NEXT_PUBLIC_ENABLE_RSS || true, // 是否开启RSS订阅功能
406
+ MAILCHIMP_LIST_ID: process.env.MAILCHIMP_LIST_ID || null, // 开启mailichimp邮件订阅 客户列表ID ,具体使用方法参阅文档
407
+ MAILCHIMP_API_KEY: process.env.MAILCHIMP_API_KEY || null, // 开启mailichimp邮件订阅 APIkey
408
+
409
+ // 作废配置
410
+ AVATAR: process.env.NEXT_PUBLIC_AVATAR || '/avatar.svg', // 作者头像,被notion中的ICON覆盖。若无ICON则取public目录下的avatar.png
411
+ TITLE: process.env.NEXT_PUBLIC_TITLE || 'NotionNext BLOG', // 站点标题 ,被notion中的页面标题覆盖;此处请勿留空白,否则服务器无法编译
412
+ HOME_BANNER_IMAGE:
413
+ process.env.NEXT_PUBLIC_HOME_BANNER_IMAGE || '/bg_image.jpg', // 首页背景大图, 会被notion中的封面图覆盖,若无封面图则会使用代码中的 /public/bg_image.jpg 文件
414
+ DESCRIPTION:
415
+ process.env.NEXT_PUBLIC_DESCRIPTION || '这是一个由NotionNext生成的站点', // 站点描述,被notion中的页面描述覆盖
416
+
417
+ // 网站图片
418
+ IMG_LAZY_LOAD_PLACEHOLDER: process.env.NEXT_PUBLIC_IMG_LAZY_LOAD_PLACEHOLDER || 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==', // 懒加载占位图片地址,支持base64或url
419
+ 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)
420
+ IMG_SHADOW: process.env.NEXT_PUBLIC_IMG_SHADOW || false, // 文章图片是否自动添加阴影
421
+ IMG_COMPRESS_WIDTH: process.env.NEXT_PUBLIC_IMG_COMPRESS_WIDTH || 800, // Notion图片压缩宽度
422
+
423
+ // 开发相关
424
+ NOTION_ACCESS_TOKEN: process.env.NOTION_ACCESS_TOKEN || '', // Useful if you prefer not to make your database public
425
+ DEBUG: process.env.NEXT_PUBLIC_DEBUG || false, // 是否显示调试按钮
426
+ ENABLE_CACHE: process.env.ENABLE_CACHE || process.env.npm_lifecycle_event === 'build' || process.env.npm_lifecycle_event === 'export', // 在打包过程中默认开启缓存,开发或运行时开启此功能意义不大。
427
+ 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)
428
+ BUNDLE_ANALYZER: process.env.ANALYZE === 'true' || false, // 是否展示编译依赖内容与大小
429
+ VERSION: process.env.NEXT_PUBLIC_VERSION // 版本号
430
+ }
431
+
432
+ module.exports = BLOG
components/AOSAnimation.js ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import AOS from 'aos'
2
+ import { isBrowser } from 'react-notion-x'
3
+
4
+ /**
5
+ * 加载滚动动画
6
+ * https://michalsnik.github.io/aos/
7
+ */
8
+ export default function AOSAnimation() {
9
+ if (isBrowser) {
10
+ AOS.init()
11
+ }
12
+ }
components/Ackee.js ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use strict'
2
+
3
+ import { useEffect } from 'react'
4
+ import { loadExternalResource } from '@/lib/utils'
5
+ import { useRouter } from 'next/router'
6
+ import { siteConfig } from '@/lib/config'
7
+ const Ackee = () => {
8
+ const router = useRouter()
9
+ const server = siteConfig('ANALYTICS_ACKEE_DATA_SERVER')
10
+ const domainId = siteConfig('ANALYTICS_ACKEE_DOMAIN_ID')
11
+
12
+ // 或者使用其他依赖数组,根据需要执行 handleAckee
13
+ useEffect(() => {
14
+ handleAckeeCallback()
15
+ }, [router])
16
+
17
+ // handleAckee 函数
18
+ const handleAckeeCallback = () => {
19
+ handleAckee(
20
+ router.asPath,
21
+ {
22
+ server: server,
23
+ domainId: domainId
24
+ },
25
+ {
26
+ /*
27
+ * Enable or disable tracking of personal data.
28
+ * We recommend to ask the user for permission before turning this option on.
29
+ */
30
+ detailed: true,
31
+ /*
32
+ * Enable or disable tracking when on localhost.
33
+ */
34
+ ignoreLocalhost: false,
35
+ /*
36
+ * Enable or disable the tracking of your own visits.
37
+ * This is enabled by default, but should be turned off when using a wildcard Access-Control-Allow-Origin header.
38
+ * Some browsers strictly block third-party cookies. The option won't have an impact when this is the case.
39
+ */
40
+ ignoreOwnVisits: false
41
+ }
42
+ )
43
+ }
44
+
45
+ return null
46
+ }
47
+
48
+ export default Ackee
49
+
50
+ /**
51
+ * Function to use Ackee.
52
+ * Creates an instance once and a new record every time the pathname changes.
53
+ * Safely no-ops during server-side rendering.
54
+ * @param {?String} pathname - Current path.
55
+ * @param {Object} environment - Object containing the URL of the Ackee server and the domain id.
56
+ * @param {?Object} options - Ackee options.
57
+ */
58
+ const handleAckee = async function (pathname, environment, options = {}) {
59
+ await loadExternalResource(siteConfig('ANALYTICS_ACKEE_TRACKER'), 'js')
60
+ const ackeeTracker = window.ackeeTracker
61
+
62
+ const instance = ackeeTracker?.create(environment.server, options)
63
+
64
+ if (instance == null) {
65
+ console.warn('Skipped record creation because useAckee has been called in a non-browser environment')
66
+ return
67
+ }
68
+
69
+ const hasPathname = (
70
+ pathname != null && pathname !== ''
71
+ )
72
+
73
+ if (hasPathname === false) {
74
+ console.warn('Skipped record creation because useAckee has been called without pathname')
75
+ return
76
+ }
77
+
78
+ const attributes = ackeeTracker?.attributes(options.detailed)
79
+ const url = new URL(pathname, location)
80
+
81
+ return instance.record(environment.domainId, {
82
+ ...attributes,
83
+ siteLocation: url.href
84
+ }).stop
85
+ }
components/AdBlockDetect.js ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect } from 'react'
2
+
3
+ /**
4
+ * 检测广告插件
5
+ * @returns
6
+ */
7
+ export default function AdBlockDetect() {
8
+ useEffect(() => {
9
+ // 如果检测到广告屏蔽插件
10
+ function ABDetected() {
11
+ if (!document) {
12
+ return
13
+ }
14
+ const wwadsCns = document.getElementsByClassName('wwads-cn')
15
+ if (wwadsCns && wwadsCns.length > 0) {
16
+ for (const wwadsCn of wwadsCns) {
17
+ wwadsCn.insertAdjacentHTML('beforeend', "<style>.wwads-horizontal,.wwads-vertical{background-color:#f4f8fa;padding:5px;min-height:120px;margin-top:20px;box-sizing:border-box;border-radius:3px;font-family:sans-serif;display:flex;min-width:150px;position:relative;overflow:hidden;}.wwads-horizontal{flex-wrap:wrap;justify-content:center}.wwads-vertical{flex-direction:column;align-items:center;padding-bottom:32px}.wwads-horizontal a,.wwads-vertical a{text-decoration:none}.wwads-horizontal .wwads-img,.wwads-vertical .wwads-img{margin:5px}.wwads-horizontal .wwads-content,.wwads-vertical .wwads-content{margin:5px}.wwads-horizontal .wwads-content{flex:130px}.wwads-vertical .wwads-content{margin-top:10px}.wwads-horizontal .wwads-text,.wwads-content .wwads-text{font-size:14px;line-height:1.4;color:#0e1011;-webkit-font-smoothing:antialiased}.wwads-horizontal .wwads-poweredby,.wwads-vertical .wwads-poweredby{display:block;font-size:11px;color:#a6b7bf;margin-top:1em}.wwads-vertical .wwads-poweredby{position:absolute;left:10px;bottom:10px}.wwads-horizontal .wwads-poweredby span,.wwads-vertical .wwads-poweredby span{transition:all 0.2s ease-in-out;margin-left:-1em}.wwads-horizontal .wwads-poweredby span:first-child,.wwads-vertical .wwads-poweredby span:first-child{opacity:0}.wwads-horizontal:hover .wwads-poweredby span,.wwads-vertical:hover .wwads-poweredby span{opacity:1;margin-left:0}.wwads-horizontal .wwads-hide,.wwads-vertical .wwads-hide{position:absolute;right:-23px;bottom:-23px;width:46px;height:46px;border-radius:23px;transition:all 0.3s ease-in-out;cursor:pointer;}.wwads-horizontal .wwads-hide:hover,.wwads-vertical .wwads-hide:hover{background:rgb(0 0 0 /0.05)}.wwads-horizontal .wwads-hide svg,.wwads-vertical .wwads-hide svg{position:absolute;left:10px;top:10px;fill:#a6b7bf}.wwads-horizontal .wwads-hide:hover svg,.wwads-vertical .wwads-hide:hover svg{fill:#3E4546}</style><a href='https://wwads.cn/page/whitelist-wwads' class='wwads-img' target='_blank' rel='nofollow'><img src='https://creatives-1301677708.file.myqcloud.com/images/placeholder/wwads-friendly-ads.png' width='130'></a><div class='wwads-content'><a href='https://wwads.cn/page/whitelist-wwads' class='wwads-text' target='_blank' rel='nofollow'>为了本站的长期运营,请将我们的网站加入广告拦截器的白名单,感谢您的支持!</a><a href='https://wwads.cn/page/end-user-privacy' class='wwads-poweredby' title='万维广告 ~ 让广告更优雅,且有用' target='_blank'><span>万维</span><span>广告</span></a></div><a class='wwads-hide' onclick='parentNode.remove()' title='隐藏广告'><svg xmlns='http://www.w3.org/2000/svg' width='6' height='7'><path d='M.879.672L3 2.793 5.121.672a.5.5 0 11.707.707L3.708 3.5l2.12 2.121a.5.5 0 11-.707.707l-2.12-2.12-2.122 2.12a.5.5 0 11-.707-.707l2.121-2.12L.172 1.378A.5.5 0 01.879.672z'></path></svg></a>")
18
+ }
19
+ }
20
+ };
21
+
22
+ // check document ready
23
+ function docReady(t) {
24
+ document.readyState === 'complete' ||
25
+ document.readyState === 'interactive'
26
+ ? setTimeout(t, 1)
27
+ : document.addEventListener('DOMContentLoaded', t)
28
+ }
29
+
30
+ // check if wwads' fire function was blocked after document is ready with 3s timeout (waiting the ad loading)
31
+ docReady(function () {
32
+ setTimeout(function () {
33
+ if (window._AdBlockInit === undefined) {
34
+ ABDetected()
35
+ }
36
+ }, 3000)
37
+ })
38
+ }, [])
39
+ return null
40
+ }
components/AlgoliaSearchModal.js ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useImperativeHandle, useRef } from 'react'
2
+ import algoliasearch from 'algoliasearch'
3
+ import replaceSearchResult from '@/components/Mark'
4
+ import Link from 'next/link'
5
+ import { useGlobal } from '@/lib/global'
6
+ import throttle from 'lodash/throttle'
7
+ import { siteConfig } from '@/lib/config'
8
+
9
+ /**
10
+ * 结合 Algolia 实现的弹出式搜索框
11
+ * 打开方式 cRef.current.openSearch()
12
+ * https://www.algolia.com/doc/api-reference/search-api-parameters/
13
+ */
14
+ export default function AlgoliaSearchModal({ cRef }) {
15
+ const [searchResults, setSearchResults] = useState([])
16
+ const [isModalOpen, setIsModalOpen] = useState(false)
17
+ const [page, setPage] = useState(0)
18
+ const [keyword, setKeyword] = useState(null)
19
+ const [totalPage, setTotalPage] = useState(0)
20
+ const [totalHit, setTotalHit] = useState(0)
21
+ const [useTime, setUseTime] = useState(0)
22
+
23
+ /**
24
+ * 对外暴露方法
25
+ */
26
+ useImperativeHandle(cRef, () => {
27
+ return {
28
+ openSearch: () => {
29
+ setIsModalOpen(true)
30
+ }
31
+ }
32
+ })
33
+
34
+ const client = algoliasearch(siteConfig('ALGOLIA_APP_ID'), siteConfig('ALGOLIA_SEARCH_ONLY_APP_KEY'))
35
+ const index = client.initIndex(siteConfig('ALGOLIA_INDEX'))
36
+
37
+ /**
38
+ * 搜索
39
+ * @param {*} query
40
+ */
41
+ const handleSearch = async (query, page) => {
42
+ setKeyword(query)
43
+ setPage(page)
44
+ setSearchResults([])
45
+ setUseTime(0)
46
+ setTotalPage(0)
47
+ setTotalHit(0)
48
+ if (!query || query === '') {
49
+ return
50
+ }
51
+
52
+ try {
53
+ const res = await index.search(query, { page, hitsPerPage: 10 })
54
+ const { hits, nbHits, nbPages, processingTimeMS } = res
55
+ setUseTime(processingTimeMS)
56
+ setTotalPage(nbPages)
57
+ setTotalHit(nbHits)
58
+ setSearchResults(hits)
59
+
60
+ const doms = document.getElementById('search-wrapper').getElementsByClassName('replace')
61
+
62
+ setTimeout(() => {
63
+ replaceSearchResult({
64
+ doms,
65
+ search: query,
66
+ target: {
67
+ element: 'span',
68
+ className: 'text-blue-600 border-b border-dashed'
69
+ }
70
+ })
71
+ }, 150)
72
+ } catch (error) {
73
+ console.error('Algolia search error:', error)
74
+ }
75
+ }
76
+
77
+ const throttledHandleSearch = useRef(throttle(handleSearch, 300)) // 设置节流延迟时间
78
+
79
+ // 修改input的onChange事件处理函数
80
+ const handleInputChange = (e) => {
81
+ const query = e.target.value
82
+ throttledHandleSearch.current(query, 0)
83
+ }
84
+
85
+ /**
86
+ * 切换页码
87
+ * @param {*} page
88
+ */
89
+ const switchPage = (page) => {
90
+ throttledHandleSearch.current(keyword, page)
91
+ }
92
+
93
+ /**
94
+ * 关闭弹窗
95
+ */
96
+ const closeModal = () => {
97
+ setIsModalOpen(false)
98
+ }
99
+
100
+ if (!siteConfig('ALGOLIA_APP_ID')) {
101
+ return <></>
102
+ }
103
+
104
+ return (
105
+ <div
106
+ id="search-wrapper"
107
+ className={`${
108
+ isModalOpen ? 'opacity-100' : 'invisible opacity-0 pointer-events-none'
109
+ } z-30 fixed h-screen w-screen left-0 top-0 mt-12 flex items-start justify-center`}
110
+ >
111
+ {/* 模态框 */}
112
+ <div
113
+ className={`${
114
+ isModalOpen ? 'opacity-100' : 'invisible opacity-0 translate-y-10'
115
+ } flex flex-col justify-between w-full min-h-[10rem] max-w-xl dark:bg-hexo-black-gray dark:border-gray-800 bg-white dark:bg- p-5 rounded-lg z-50 shadow border hover:border-blue-600 duration-300 transition-all `}
116
+ >
117
+ <div className="flex justify-between items-center">
118
+ <div className="text-2xl text-blue-600 font-bold">搜索</div>
119
+ <div>
120
+ <i
121
+ className="text-gray-600 fa-solid fa-xmark p-1 cursor-pointer hover:text-blue-600"
122
+ onClick={closeModal}
123
+ ></i>
124
+ </div>
125
+ </div>
126
+
127
+ <input
128
+ type="text"
129
+ placeholder="在这里输入搜索关键词..."
130
+ onChange={e => handleInputChange(e)}
131
+ className="text-black dark:text-gray-200 bg-gray-50 dark:bg-gray-600 outline-blue-500 w-full px-4 my-2 py-1 mb-4 border rounded-md"
132
+ />
133
+
134
+ {/* 标签组 */}
135
+ <div className="mb-4">
136
+ <TagGroups />
137
+ </div>
138
+
139
+ <ul>
140
+ {searchResults.map(result => (
141
+ <li key={result.objectID} className="replace my-2">
142
+ <a
143
+ href={`${siteConfig('SUB_PATH', '')}/${result.slug}`}
144
+ className="font-bold hover:text-blue-600 text-black dark:text-gray-200"
145
+ >
146
+ {result.title}
147
+ </a>
148
+ </li>
149
+ ))}
150
+ </ul>
151
+
152
+ <Pagination totalPage={totalPage} page={page} switchPage={switchPage} />
153
+ <div>
154
+ {totalHit > 0 && (
155
+ <div>
156
+ 共搜索到 {totalHit} 条结果,用时 {useTime} 毫秒
157
+ </div>
158
+ )}
159
+ </div>
160
+ <div className="text-gray-600 mt-2">
161
+ <span>
162
+ <i className="fa-brands fa-algolia"></i> Algolia 提供搜索服务
163
+ </span>{' '}
164
+ </div>
165
+ </div>
166
+
167
+ {/* 遮罩 */}
168
+ <div
169
+ onClick={closeModal}
170
+ className="z-30 fixed top-0 left-0 w-full h-full flex items-center justify-center glassmorphism"
171
+ />
172
+ </div>
173
+ )
174
+ }
175
+
176
+ /**
177
+ * 标签组
178
+ */
179
+ function TagGroups(props) {
180
+ const { tagOptions } = useGlobal()
181
+ // 获取tagOptions数组前十个
182
+ const firstTenTags = tagOptions?.slice(0, 10)
183
+
184
+ return <div id='tags-group' className='dark:border-gray-700 space-y-2'>
185
+ {
186
+ firstTenTags?.map((tag, index) => {
187
+ return <Link passHref
188
+ key={index}
189
+ href={`/tag/${encodeURIComponent(tag.name)}`}
190
+ className={'cursor-pointer inline-block whitespace-nowrap'}>
191
+ <div className={' flex items-center text-black dark:text-gray-300 hover:bg-blue-600 dark:hover:bg-yellow-600 hover:scale-110 hover:text-white rounded-lg px-2 py-0.5 duration-150 transition-all'}>
192
+ <div className='text-lg'>{tag.name} </div>{tag.count ? <sup className='relative ml-1'>{tag.count}</sup> : <></>}
193
+ </div>
194
+
195
+ </Link>
196
+ })
197
+ }
198
+ </div>
199
+ }
200
+
201
+ /**
202
+ * 分页
203
+ * @param {*} param0
204
+ */
205
+ function Pagination(props) {
206
+ const { totalPage, page, switchPage } = props
207
+ if (totalPage <= 0) {
208
+ return <></>
209
+ }
210
+ const pagesElement = []
211
+
212
+ for (let i = 0; i < totalPage; i++) {
213
+ const selected = page === i
214
+ pagesElement.push(getPageElement(i, selected, switchPage))
215
+ }
216
+ return <div className='flex space-x-1 w-full justify-center py-1'>
217
+ {pagesElement.map(p => p)}
218
+ </div>
219
+ }
220
+
221
+ /**
222
+ * 获取分页按钮
223
+ * @param {*} i
224
+ * @param {*} selected
225
+ */
226
+ function getPageElement(i, selected, switchPage) {
227
+ return <div onClick={() => switchPage(i)} className={`${selected ? 'font-bold text-white bg-blue-600 rounded' : 'hover:text-blue-600 hover:font-bold'} text-center cursor-pointer w-6 h-6 `}>
228
+ {i + 1}
229
+ </div>
230
+ }
components/Artalk.js ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { siteConfig } from '@/lib/config'
2
+ import { loadExternalResource } from '@/lib/utils'
3
+ import { useEffect } from 'react'
4
+
5
+ /**
6
+ * Artalk 自托管评论系统 @see https://artalk.js.org/
7
+ * @returns {JSX.Element}
8
+ * @constructor
9
+ */
10
+
11
+ const Artalk = ({ siteInfo }) => {
12
+ const artalkCss = siteConfig('COMMENT_ARTALK_CSS')
13
+ const artalkServer = siteConfig('COMMENT_ARTALK_SERVER')
14
+ const artalkLocale = siteConfig('LANG')
15
+ const site = siteConfig('TITLE')
16
+
17
+ useEffect(() => {
18
+ initArtalk()
19
+ }, [])
20
+
21
+ const initArtalk = async () => {
22
+ await loadExternalResource(artalkCss, 'css')
23
+ window?.Artalk?.init({
24
+ server: artalkServer, // 后端地址
25
+ el: '#artalk', // 容器元素
26
+ locale: artalkLocale,
27
+ // pageKey: '/post/1', // 固定链接 (留空自动获取)
28
+ // pageTitle: '关于引入 Artalk 的这档子事', // 页面标题 (留空自动获取)
29
+ site: site // 你的站点名
30
+ })
31
+ }
32
+ return (
33
+ <div id="artalk"></div>
34
+ )
35
+ }
36
+
37
+ export default Artalk
components/Busuanzi.js ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import busuanzi from '@/lib/busuanzi'
2
+ import { useRouter } from 'next/router'
3
+ import { useGlobal } from '@/lib/global'
4
+ // import { useRouter } from 'next/router'
5
+ import { useEffect } from 'react'
6
+
7
+ let path = ''
8
+
9
+ export default function Busuanzi () {
10
+ const { theme } = useGlobal()
11
+ const router = useRouter()
12
+ router.events.on('routeChangeComplete', (url, option) => {
13
+ if (url !== path) {
14
+ path = url
15
+ busuanzi.fetch()
16
+ }
17
+ })
18
+
19
+ // 更换主题时更新
20
+ useEffect(() => {
21
+ if (theme) {
22
+ busuanzi.fetch()
23
+ }
24
+ }, [theme])
25
+ return null
26
+ }
components/ChatBase.js ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { siteConfig } from '@/lib/config'
2
+
3
+ /**
4
+ * 这是一个嵌入组件,可以在任意位置全屏显示您的chat-base对话框
5
+ * 暂时没有页面引用
6
+ * 因为您可以直接用内嵌网页的方式放入您的notion中 https://www.chatbase.co/chatbot-iframe/${siteConfig('CHATBASE_ID')}
7
+ */
8
+ export default function ChatBase() {
9
+ if (!siteConfig('CHATBASE_ID')) {
10
+ return <></>
11
+ }
12
+
13
+ return <iframe
14
+ src={`https://www.chatbase.co/chatbot-iframe/${siteConfig('CHATBASE_ID')}`}
15
+ width="100%"
16
+ style={{ height: '100%', minHeight: '700px' }}
17
+ frameborder="0"
18
+ ></iframe>
19
+ }
components/Collapse.js ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useImperativeHandle, useRef } from 'react'
2
+
3
+ /**
4
+ * 折叠面板组件,支持水平折叠、垂直折叠
5
+ * @param {type:['horizontal','vertical'],isOpen} props
6
+ * @returns
7
+ */
8
+ const Collapse = props => {
9
+ const { collapseRef } = props
10
+ const ref = useRef(null)
11
+ const type = props.type || 'vertical'
12
+
13
+ useImperativeHandle(collapseRef, () => {
14
+ return {
15
+ /**
16
+ * 当子元素高度变化时,可调用此方法更新折叠组件的高度
17
+ * @param {*} param0
18
+ */
19
+ updateCollapseHeight: ({ height, increase }) => {
20
+ if (props.isOpen) {
21
+ ref.current.style.height = ref.current.scrollHeight
22
+ ref.current.style.height = 'auto'
23
+ }
24
+ }
25
+ }
26
+ })
27
+
28
+ /**
29
+ * 折叠
30
+ * @param {*} element
31
+ */
32
+ const collapseSection = element => {
33
+ const sectionHeight = element.scrollHeight
34
+ const sectionWidth = element.scrollWidth
35
+
36
+ requestAnimationFrame(function () {
37
+ switch (type) {
38
+ case 'horizontal':
39
+ element.style.width = sectionWidth + 'px'
40
+ requestAnimationFrame(function () {
41
+ element.style.width = 0 + 'px'
42
+ })
43
+ break
44
+ case 'vertical':
45
+ element.style.height = sectionHeight + 'px'
46
+ requestAnimationFrame(function () {
47
+ element.style.height = 0 + 'px'
48
+ })
49
+ }
50
+ })
51
+ }
52
+
53
+ /**
54
+ * 展开
55
+ * @param {*} element
56
+ */
57
+ const expandSection = element => {
58
+ const sectionHeight = element.scrollHeight
59
+ const sectionWidth = element.scrollWidth
60
+ let clearTime = 0
61
+ switch (type) {
62
+ case 'horizontal':
63
+ element.style.width = sectionWidth + 'px'
64
+ clearTime = setTimeout(() => {
65
+ element.style.width = 'auto'
66
+ }, 400)
67
+ break
68
+ case 'vertical':
69
+ element.style.height = sectionHeight + 'px'
70
+ clearTime = setTimeout(() => {
71
+ element.style.height = 'auto'
72
+ }, 400)
73
+ }
74
+
75
+ clearTimeout(clearTime)
76
+ }
77
+
78
+ useEffect(() => {
79
+ if (props.isOpen) {
80
+ expandSection(ref.current)
81
+ } else {
82
+ collapseSection(ref.current)
83
+ }
84
+ // 通知父组件高度变化
85
+ props?.onHeightChange && props.onHeightChange({ height: ref.current.scrollHeight, increase: props.isOpen })
86
+ }, [props.isOpen])
87
+
88
+ return (
89
+ <div ref={ref} style={type === 'vertical' ? { height: '0px', willChange: 'height' } : { width: '0px', willChange: 'width' }} className={`${props.className || ''} overflow-hidden duration-200 `}>
90
+ {props.children}
91
+ </div>
92
+ )
93
+ }
94
+ Collapse.defaultProps = { isOpen: false }
95
+
96
+ export default Collapse
components/Comment.js ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dynamic from 'next/dynamic'
2
+ import Tabs from '@/components/Tabs'
3
+ import { isBrowser, isSearchEngineBot } from '@/lib/utils'
4
+ import { useRouter } from 'next/router'
5
+ import Artalk from './Artalk'
6
+ import { siteConfig } from '@/lib/config'
7
+
8
+ const WalineComponent = dynamic(
9
+ () => {
10
+ return import('@/components/WalineComponent')
11
+ },
12
+ { ssr: false }
13
+ )
14
+
15
+ const CusdisComponent = dynamic(
16
+ () => {
17
+ return import('@/components/CusdisComponent')
18
+ },
19
+ { ssr: false }
20
+ )
21
+
22
+ const TwikooCompenent = dynamic(
23
+ () => {
24
+ return import('@/components/Twikoo')
25
+ },
26
+ { ssr: false }
27
+ )
28
+
29
+ const GitalkComponent = dynamic(
30
+ () => {
31
+ return import('@/components/Gitalk')
32
+ },
33
+ { ssr: false }
34
+ )
35
+ const UtterancesComponent = dynamic(
36
+ () => {
37
+ return import('@/components/Utterances')
38
+ },
39
+ { ssr: false }
40
+ )
41
+ const GiscusComponent = dynamic(
42
+ () => {
43
+ return import('@/components/Giscus')
44
+ },
45
+ { ssr: false }
46
+ )
47
+ const WebMentionComponent = dynamic(
48
+ () => {
49
+ return import('@/components/WebMention')
50
+ },
51
+ { ssr: false }
52
+ )
53
+
54
+ const ValineComponent = dynamic(() => import('@/components/ValineComponent'), {
55
+ ssr: false
56
+ })
57
+
58
+ /**
59
+ * 评论组件
60
+ * @param {*} param0
61
+ * @returns
62
+ */
63
+ const Comment = ({ siteInfo, frontMatter, className }) => {
64
+ const router = useRouter()
65
+
66
+ const COMMENT_ARTALK_SERVER = siteConfig('COMMENT_ARTALK_SERVER')
67
+ const COMMENT_TWIKOO_ENV_ID = siteConfig('COMMENT_TWIKOO_ENV_ID')
68
+ const COMMENT_WALINE_SERVER_URL = siteConfig('COMMENT_WALINE_SERVER_URL')
69
+ const COMMENT_VALINE_APP_ID = siteConfig('COMMENT_VALINE_APP_ID')
70
+ const COMMENT_GISCUS_REPO = siteConfig('COMMENT_GISCUS_REPO')
71
+ const COMMENT_CUSDIS_APP_ID = siteConfig('COMMENT_CUSDIS_APP_ID')
72
+ const COMMENT_UTTERRANCES_REPO = siteConfig('COMMENT_UTTERRANCES_REPO')
73
+ const COMMENT_GITALK_CLIENT_ID = siteConfig('COMMENT_GITALK_CLIENT_ID')
74
+ const COMMENT_WEBMENTION_ENABLE = siteConfig('COMMENT_WEBMENTION_ENABLE')
75
+
76
+ if (isSearchEngineBot()) {
77
+ return null
78
+ }
79
+
80
+ // 当连接中有特殊参数时跳转到评论区
81
+ if (isBrowser && ('giscus' in router.query || router.query.target === 'comment')) {
82
+ setTimeout(() => {
83
+ const url = router.asPath.replace('?target=comment', '')
84
+ history.replaceState({}, '', url)
85
+ document?.getElementById('comment')?.scrollIntoView({ block: 'start', behavior: 'smooth' })
86
+ }, 1000)
87
+ }
88
+
89
+ if (!frontMatter) {
90
+ return <>Loading...</>
91
+ }
92
+
93
+ return (
94
+ <div key={frontMatter?.id} id='comment' className={`comment mt-5 text-gray-800 dark:text-gray-300 ${className || ''}`}>
95
+ <Tabs>
96
+ {COMMENT_ARTALK_SERVER && (<div key='Artalk'>
97
+ <Artalk />
98
+ </div>)}
99
+
100
+ {COMMENT_TWIKOO_ENV_ID && (<div key='Twikoo'>
101
+ <TwikooCompenent />
102
+ </div>)}
103
+
104
+ {COMMENT_WALINE_SERVER_URL && (<div key='Waline'>
105
+ <WalineComponent />
106
+ </div>)}
107
+
108
+ {COMMENT_VALINE_APP_ID && (<div key='Valine' name='reply'>
109
+ <ValineComponent path={frontMatter.id} />
110
+ </div>)}
111
+
112
+ {COMMENT_GISCUS_REPO && (
113
+ <div key="Giscus">
114
+ <GiscusComponent className="px-2" />
115
+ </div>
116
+ )}
117
+
118
+ {COMMENT_CUSDIS_APP_ID && (<div key='Cusdis'>
119
+ <CusdisComponent frontMatter={frontMatter} />
120
+ </div>)}
121
+
122
+ {COMMENT_UTTERRANCES_REPO && (<div key='Utterance'>
123
+ <UtterancesComponent issueTerm={frontMatter.id} className='px-2' />
124
+ </div>)}
125
+
126
+ {COMMENT_GITALK_CLIENT_ID && (<div key='GitTalk'>
127
+ <GitalkComponent frontMatter={frontMatter} />
128
+ </div>)}
129
+
130
+ {COMMENT_WEBMENTION_ENABLE && (<div key='WebMention'>
131
+ <WebMentionComponent frontMatter={frontMatter} className="px-2" />
132
+ </div>)}
133
+ </Tabs>
134
+ </div>
135
+ )
136
+ }
137
+
138
+ export default Comment
components/CusdisComponent.js ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useGlobal } from '@/lib/global'
2
+ import { useRouter } from 'next/router'
3
+ import { useEffect } from 'react'
4
+ import { loadExternalResource } from '@/lib/utils'
5
+ import { siteConfig } from '@/lib/config'
6
+
7
+ const CusdisComponent = ({ frontMatter }) => {
8
+ const router = useRouter()
9
+ const { isDarkMode, lang } = useGlobal()
10
+ const src = siteConfig('COMMENT_CUSDIS_SCRIPT_SRC')
11
+ const i18nForCusdis = siteConfig('LANG').toLowerCase().indexOf('zh') === 0 ? siteConfig('LANG').toLowerCase() : siteConfig('LANG').toLowerCase().substring(0, 2)
12
+ const langCDN = siteConfig('COMMENT_CUSDIS_LANG_SRC', `https://cusdis.com/js/widget/lang/${i18nForCusdis}.js`)
13
+
14
+ // 处理cusdis主题
15
+ useEffect(() => {
16
+ loadCusdis()
17
+ }, [isDarkMode, lang])
18
+
19
+ const loadCusdis = async () => {
20
+ await loadExternalResource(langCDN, 'js')
21
+ await loadExternalResource(src, 'js')
22
+
23
+ window?.CUSDIS?.initial()
24
+ }
25
+
26
+ return <div id="cusdis_thread"
27
+ lang={lang.toLowerCase()}
28
+ data-host={siteConfig('COMMENT_CUSDIS_HOST')}
29
+ data-app-id={siteConfig('COMMENT_CUSDIS_APP_ID')}
30
+ data-page-id={frontMatter.id}
31
+ data-page-url={siteConfig('LINK') + router.asPath}
32
+ data-page-title={frontMatter.title}
33
+ data-theme={isDarkMode ? 'dark' : 'light'}
34
+ ></div>
35
+ }
36
+
37
+ export default CusdisComponent
components/CustomContextMenu.js ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from 'next/link'
2
+ import { useRouter } from 'next/router'
3
+ import { useEffect, useState, useRef, useLayoutEffect } from 'react'
4
+ import { useGlobal } from '@/lib/global'
5
+ import { saveDarkModeToCookies, THEMES } from '@/themes/theme'
6
+ import useWindowSize from '@/hooks/useWindowSize'
7
+ import { siteConfig } from '@/lib/config'
8
+
9
+ /**
10
+ * 自定义右键菜单
11
+ * @param {*} props
12
+ * @returns
13
+ */
14
+ export default function CustomContextMenu(props) {
15
+ const [position, setPosition] = useState({ x: '0px', y: '0px' })
16
+ const [show, setShow] = useState(false)
17
+ const { isDarkMode, updateDarkMode, locale } = useGlobal()
18
+ const menuRef = useRef(null)
19
+ const windowSize = useWindowSize()
20
+ const [width, setWidth] = useState(0)
21
+ const [height, setHeight] = useState(0)
22
+
23
+ const { latestPosts } = props
24
+ const router = useRouter()
25
+ /**
26
+ * 随机跳转文章
27
+ */
28
+ function handleJumpToRandomPost() {
29
+ const randomIndex = Math.floor(Math.random() * latestPosts.length)
30
+ const randomPost = latestPosts[randomIndex]
31
+ router.push(`${siteConfig('SUB_PATH', '')}/${randomPost?.slug}`)
32
+ }
33
+
34
+ useLayoutEffect(() => {
35
+ setWidth(menuRef.current.offsetWidth)
36
+ setHeight(menuRef.current.offsetHeight)
37
+ }, [])
38
+
39
+ useEffect(() => {
40
+ const handleContextMenu = (event) => {
41
+ event.preventDefault()
42
+ // 计算点击位置加菜单宽高是否超出屏幕,如果超出则贴边弹出
43
+ const x = (event.clientX < windowSize.width - width) ? event.clientX : windowSize.width - width
44
+ const y = (event.clientY < windowSize.height - height) ? event.clientY : windowSize.height - height
45
+ setPosition({ y: `${y}px`, x: `${x}px` })
46
+ setShow(true)
47
+ }
48
+
49
+ const handleClick = (event) => {
50
+ if (menuRef.current && !menuRef.current.contains(event.target)) {
51
+ setShow(false)
52
+ }
53
+ }
54
+
55
+ window.addEventListener('contextmenu', handleContextMenu)
56
+ window.addEventListener('click', handleClick)
57
+
58
+ return () => {
59
+ window.removeEventListener('contextmenu', handleContextMenu)
60
+ window.removeEventListener('click', handleClick)
61
+ }
62
+ }, [windowSize])
63
+
64
+ function handleBack() {
65
+ window.history.back()
66
+ }
67
+
68
+ function handleForward() {
69
+ window.history.forward()
70
+ }
71
+
72
+ function handleRefresh() {
73
+ window.location.reload()
74
+ }
75
+
76
+ function handleScrollTop() {
77
+ window.scrollTo({ top: 0, behavior: 'smooth' })
78
+ setShow(false)
79
+ }
80
+
81
+ function handleCopyLink() {
82
+ const url = window.location.href
83
+ navigator.clipboard.writeText(url)
84
+ .then(() => {
85
+ console.log('页面地址已复制')
86
+ })
87
+ .catch((error) => {
88
+ console.error('复制页面地址失败:', error)
89
+ })
90
+ setShow(false)
91
+ }
92
+
93
+ /**
94
+ * 切换主题
95
+ */
96
+ function handleChangeTheme() {
97
+ const randomTheme = THEMES[Math.floor(Math.random() * THEMES.length)] // 从THEMES数组中 随机取一个主题
98
+ const query = router.query
99
+ query.theme = randomTheme
100
+ router.push({ pathname: router.pathname, query })
101
+ }
102
+
103
+ /**
104
+ * 复制内容
105
+ */
106
+ function handleCopy() {
107
+ const selectedText = document.getSelection().toString();
108
+ if (selectedText) {
109
+ const tempInput = document.createElement('input');
110
+ tempInput.value = selectedText;
111
+ document.body.appendChild(tempInput);
112
+ tempInput.select();
113
+ document.execCommand('copy');
114
+ document.body.removeChild(tempInput);
115
+ // alert("Text copied: " + selectedText);
116
+ } else {
117
+ // alert("Please select some text first.");
118
+ }
119
+
120
+ setShow(false)
121
+ }
122
+
123
+ function handleChangeDarkMode() {
124
+ const newStatus = !isDarkMode
125
+ saveDarkModeToCookies(newStatus)
126
+ updateDarkMode(newStatus)
127
+ const htmlElement = document.getElementsByTagName('html')[0]
128
+ htmlElement.classList?.remove(newStatus ? 'light' : 'dark')
129
+ htmlElement.classList?.add(newStatus ? 'dark' : 'light')
130
+ }
131
+
132
+ return (
133
+ <div
134
+ ref={menuRef}
135
+ style={{ top: position.y, left: position.x }}
136
+ className={`${show ? '' : 'invisible opacity-0'} select-none transition-opacity duration-200 fixed z-50`}
137
+ >
138
+
139
+ {/* 菜单内容 */}
140
+ <div className='rounded-xl w-52 dark:hover:border-yellow-600 bg-white dark:bg-[#040404] dark:text-gray-200 dark:border-gray-600 p-3 border drop-shadow-lg flex-col duration-300 transition-colors'>
141
+ {/* 顶部导航按钮 */}
142
+ <div className='flex justify-between'>
143
+ <i onClick={handleBack} className="hover:bg-blue-600 hover:text-white px-2 py-2 text-center w-8 rounded cursor-pointer fa-solid fa-arrow-left"></i>
144
+ <i onClick={handleForward} className="hover:bg-blue-600 hover:text-white px-2 py-2 text-center w-8 rounded cursor-pointer fa-solid fa-arrow-right"></i>
145
+ <i onClick={handleRefresh} className="hover:bg-blue-600 hover:text-white px-2 py-2 text-center w-8 rounded cursor-pointer fa-solid fa-rotate-right"></i>
146
+ <i onClick={handleScrollTop} className="hover:bg-blue-600 hover:text-white px-2 py-2 text-center w-8 rounded cursor-pointer fa-solid fa-arrow-up"></i>
147
+ </div>
148
+
149
+ <hr className='my-2 border-dashed' />
150
+
151
+ {/* 跳转导航按钮 */}
152
+ <div className='w-full px-2'>
153
+
154
+ <div onClick={handleJumpToRandomPost} title={locale.MENU.WALK_AROUND} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
155
+ <i className="fa-solid fa-podcast mr-2" />
156
+ <div className='whitespace-nowrap'>{locale.MENU.WALK_AROUND}</div>
157
+ </div>
158
+
159
+ <Link href='/category' title={locale.MENU.CATEGORY} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
160
+ <i className="fa-solid fa-square-minus mr-2" />
161
+ <div className='whitespace-nowrap'>{locale.MENU.CATEGORY}</div>
162
+ </Link>
163
+
164
+ <Link href='/tag' title={locale.MENU.TAGS} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
165
+ <i className="fa-solid fa-tag mr-2" />
166
+ <div className='whitespace-nowrap'>{locale.MENU.TAGS}</div>
167
+ </Link>
168
+
169
+ </div>
170
+
171
+ <hr className='my-2 border-dashed' />
172
+
173
+ {/* 功能按钮 */}
174
+ <div className='w-full px-2'>
175
+
176
+ {siteConfig('CAN_COPY') && (
177
+ <div onClick={handleCopy} title={locale.MENU.COPY} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
178
+ <i className="fa-solid fa-copy mr-2" />
179
+ <div className='whitespace-nowrap'>{locale.MENU.COPY}</div>
180
+ </div>
181
+ )}
182
+
183
+ <div onClick={handleCopyLink} title={locale.MENU.SHARE_URL} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
184
+ <i className="fa-solid fa-arrow-up-right-from-square mr-2" />
185
+ <div className='whitespace-nowrap'>{locale.MENU.SHARE_URL}</div>
186
+ </div>
187
+
188
+ <div onClick={handleChangeDarkMode} title={isDarkMode ? locale.MENU.LIGHT_MODE : locale.MENU.DARK_MODE} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
189
+ {isDarkMode ? <i className="fa-regular fa-sun mr-2" /> : <i className="fa-regular fa-moon mr-2" />}
190
+ <div className='whitespace-nowrap'> {isDarkMode ? locale.MENU.LIGHT_MODE : locale.MENU.DARK_MODE}</div>
191
+ </div>
192
+ {siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH') && (
193
+ <div onClick={handleChangeTheme} title={locale.MENU.THEME_SWITCH} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
194
+ <i className="fa-solid fa-palette mr-2" />
195
+ <div className='whitespace-nowrap'>{locale.MENU.THEME_SWITCH}</div>
196
+ </div>
197
+ )}
198
+
199
+ </div>
200
+
201
+ </div>
202
+ </div >
203
+ )
204
+ }
components/DarkModeButton.js ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useGlobal } from '@/lib/global'
2
+ import { Moon, Sun } from './HeroIcons'
3
+ import { useImperativeHandle } from 'react'
4
+
5
+ /**
6
+ * 深色模式按钮
7
+ */
8
+ const DarkModeButton = (props) => {
9
+ const { cRef, className } = props
10
+ const { isDarkMode, toggleDarkMode } = useGlobal()
11
+
12
+ /**
13
+ * 对外暴露方法
14
+ */
15
+ useImperativeHandle(cRef, () => {
16
+ return {
17
+ handleChangeDarkMode: () => {
18
+ toggleDarkMode()
19
+ }
20
+ }
21
+ })
22
+
23
+ return <div onClick={toggleDarkMode} className={`${className || ''} flex justify-center dark:text-gray-200 text-gray-800`}>
24
+ <div id='darkModeButton' className=' hover:scale-110 cursor-pointer transform duration-200 w-5 h-5'> {isDarkMode ? <Sun /> : <Moon />}</div>
25
+ </div>
26
+ }
27
+ export default DarkModeButton
components/DebugPanel.js ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from 'react'
2
+ import Select from './Select'
3
+ import { useGlobal } from '@/lib/global'
4
+ import { THEMES } from '@/themes/theme'
5
+ import { useRouter } from 'next/router'
6
+ import { siteConfigMap } from '@/lib/config'
7
+ import { getQueryParam } from '@/lib/utils'
8
+
9
+ /**
10
+ *
11
+ * @returns 调试面板
12
+ */
13
+ const DebugPanel = () => {
14
+ const [show, setShow] = useState(false)
15
+ const { theme, switchTheme, locale } = useGlobal()
16
+ const router = useRouter()
17
+ const currentTheme = getQueryParam(router.asPath, 'theme') || theme
18
+ const [siteConfig, updateSiteConfig] = useState({})
19
+
20
+ // 主题下拉框
21
+ const themeOptions = THEMES?.map(t => ({ value: t, text: t }))
22
+
23
+ useEffect(() => {
24
+ updateSiteConfig(Object.assign({}, siteConfigMap()))
25
+ }, [])
26
+
27
+ function toggleShow() {
28
+ setShow(!show)
29
+ }
30
+
31
+ function handleChangeDebugTheme() {
32
+ switchTheme()
33
+ }
34
+
35
+ function handleUpdateDebugTheme(newTheme) {
36
+ const query = { ...router.query, theme: newTheme }
37
+ router.push({ pathname: router.pathname, query })
38
+ }
39
+
40
+ function filterResult(text) {
41
+ switch (text) {
42
+ case 'true':
43
+ return <span className='text-green-500'>true</span>
44
+ case 'false':
45
+ return <span className='text-red-500'>false</span>
46
+ case '':
47
+ return '-'
48
+ }
49
+ return text
50
+ }
51
+
52
+ return (
53
+ <>
54
+ {/* 调试按钮 */}
55
+ <div>
56
+ <div
57
+ style={{ writingMode: 'vertical-lr' }}
58
+ className={`bg-black text-xs text-white shadow-2xl p-1.5 rounded-l-xl cursor-pointer ${show ? 'right-96' : 'right-0'} fixed bottom-72 duration-200 z-50`}
59
+ onClick={toggleShow}
60
+ >
61
+ {show
62
+ ? <i className="fas fa-times">&nbsp;{locale.COMMON.DEBUG_CLOSE}</i>
63
+ : <i className="fas fa-tools">&nbsp;{locale.COMMON.DEBUG_OPEN}</i>}
64
+ </div>
65
+ </div>
66
+
67
+ {/* 调试侧拉抽屉 */}
68
+ <div
69
+ className={` ${show ? 'shadow-card w-96 right-0 ' : '-right-96 invisible w-0'} overflow-y-scroll h-full p-5 bg-white fixed bottom-0 z-50 duration-200`}
70
+ >
71
+ <div className="flex justify-between space-x-1 my-5">
72
+ <div className='flex'>
73
+ <Select
74
+ label={locale.COMMON.THEME_SWITCH}
75
+ value={currentTheme}
76
+ options={themeOptions}
77
+ onChange={handleUpdateDebugTheme}
78
+ />
79
+ <div className="p-2 cursor-pointer" onClick={handleChangeDebugTheme}>
80
+ <i className="fas fa-sync" />
81
+ </div>
82
+ </div>
83
+
84
+ <div className='p-2'>
85
+ <i className='fas fa-times' onClick={toggleShow}/>
86
+ </div>
87
+ </div>
88
+
89
+ <div>
90
+ {/* <div>
91
+ <div className="font-bold w-18 border-b my-2">
92
+ 主题配置{`config_${debugTheme}.js`}:
93
+ </div>
94
+ <div className="text-xs">
95
+ {Object.keys(themeConfig).map(k => (
96
+ <div key={k} className="justify-between flex py-1">
97
+ <span className="bg-indigo-500 p-0.5 rounded text-white mr-2">
98
+ {k}
99
+ </span>
100
+ <span className="whitespace-nowrap">
101
+ {filterResult(themeConfig[k] + '')}
102
+ </span>
103
+ </div>
104
+ ))}
105
+ </div>
106
+ </div> */}
107
+ <div className="font-bold w-18 border-b my-2">
108
+ 站点配置[blog.config.js]
109
+ </div>
110
+ <div className="text-xs">
111
+ {siteConfig && Object.keys(siteConfig).map(k => (
112
+ <div key={k} className="justify-between flex py-1">
113
+ <span className="bg-blue-500 p-0.5 rounded text-white mr-2">
114
+ {k}
115
+ </span>
116
+ <span className="whitespace-nowrap">
117
+ {filterResult(siteConfig[k] + '')}
118
+ </span>
119
+ </div>
120
+ ))}
121
+ </div>
122
+ </div>
123
+
124
+ </div>
125
+ </>
126
+ )
127
+ }
128
+ export default DebugPanel
components/DifyChatbot.js ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect } from 'react';
2
+ import { siteConfig } from '@/lib/config';
3
+
4
+ export default function DifyChatbot() {
5
+ useEffect(() => {
6
+ // 这里使用 siteConfig() 函数调用来获取配置值
7
+ if (!siteConfig('DIFY_CHATBOT_ENABLED')) {
8
+ return;
9
+ }
10
+
11
+ // 配置 DifyChatbot,同样需要调用 siteConfig() 获取相应的配置值
12
+ window.difyChatbotConfig = {
13
+ token: siteConfig('DIFY_CHATBOT_TOKEN'),
14
+ baseUrl: siteConfig('DIFY_CHATBOT_BASE_URL')
15
+ };
16
+
17
+ // 加载 DifyChatbot 脚本
18
+ const script = document.createElement('script');
19
+ script.src = `${siteConfig('DIFY_CHATBOT_BASE_URL')}/embed.min.js`; // 注意调用 siteConfig()
20
+ script.id = siteConfig('DIFY_CHATBOT_TOKEN'); // 注意调用 siteConfig()
21
+ script.defer = true;
22
+ document.body.appendChild(script);
23
+
24
+ return () => {
25
+ // 在组件卸载时清理 script 标签
26
+ const existingScript = document.getElementById(siteConfig('DIFY_CHATBOT_TOKEN')); // 注意调用 siteConfig()
27
+ if (existingScript) document.body.removeChild(existingScript);
28
+ };
29
+ }, []); // 注意依赖数组为空,意味着脚本将仅在加载页面时执行一次
30
+
31
+ return null;
32
+ }
components/DisableCopy.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { siteConfig } from '@/lib/config'
2
+ import { useEffect } from 'react'
3
+
4
+ /**
5
+ * 禁止用户拷贝文章的插件
6
+ */
7
+ export default function DisableCopy() {
8
+ useEffect(() => {
9
+ if (!JSON.parse(siteConfig('CAN_COPY'))) {
10
+ // 全栈添加禁止复制的样式
11
+ document.getElementsByTagName('html')[0].classList.add('forbid-copy')
12
+ // 监听复制事件
13
+ document.addEventListener('copy', function (event) {
14
+ event.preventDefault() // 阻止默认复制行为
15
+ alert('抱歉,本网页内容不可复制!')
16
+ })
17
+ }
18
+ }, [])
19
+
20
+ return null
21
+ }
components/Draggable.js ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useRef, useEffect, useState } from 'react'
2
+ /**
3
+ * 可拖拽组件
4
+ */
5
+
6
+ export const Draggable = (props) => {
7
+ const { children } = props
8
+ const draggableRef = useRef(null)
9
+ const rafRef = useRef(null)
10
+ const [moving, setMoving] = useState(false)
11
+ let currentObj, offsetX, offsetY
12
+
13
+ useEffect(() => {
14
+ const draggableElements = document.getElementsByClassName('draggable')
15
+
16
+ // 标准化鼠标事件对象
17
+ function e(event) { // 定义事件对象标准化函数
18
+ if (!event) { // 兼容IE浏览器
19
+ event = window.event
20
+ event.target = event.srcElement
21
+ event.layerX = event.offsetX
22
+ event.layerY = event.offsetY
23
+ }
24
+ // 移动端
25
+ if (event.type === 'touchstart' || event.type === 'touchmove') {
26
+ event.clientX = event.touches[0].clientX
27
+ event.clientY = event.touches[0].clientY
28
+ }
29
+
30
+ event.mx = event.pageX || event.clientX + document.body.scrollLeft
31
+ // 计算鼠标指针的x轴距离
32
+ event.my = event.pageY || event.clientY + document.body.scrollTop
33
+ // 计算鼠标指针的y轴距离
34
+
35
+ return event // 返回标准化的事件对象
36
+ }
37
+
38
+ // 定义鼠标事件处理函数
39
+ // document.pointerdown = start
40
+ document.onmousedown = start
41
+ document.ontouchstart = start
42
+
43
+ function start (event) { // 按下鼠标时,初始化处理
44
+ if (!draggableElements) return
45
+ event = e(event)// 获取标准事件对象
46
+
47
+ for (const drag of draggableElements) {
48
+ // 判断鼠标点击的区域是否是拖拽框内
49
+ if (inDragBox(event, drag)) {
50
+ currentObj = drag.firstElementChild
51
+ }
52
+ }
53
+ if (currentObj) {
54
+ if (event.type === 'touchstart') {
55
+ event.preventDefault() // 阻止默认的滚动行为
56
+ document.documentElement.style.overflow = 'hidden' // 防止页面一起滚动
57
+ }
58
+
59
+ setMoving(true)
60
+ offsetX = event.mx - currentObj.offsetLeft
61
+ offsetY = event.my - currentObj.offsetTop
62
+
63
+ document.onmousemove = move// 注册鼠标移动事件处理函数
64
+ document.ontouchmove = move
65
+ document.onmouseup = stop// 注册松开鼠标事件处理函数
66
+ document.ontouchend = stop
67
+ }
68
+ }
69
+
70
+ function move(event) { // 鼠标移动处理函数
71
+ event = e(event)
72
+ rafRef.current = requestAnimationFrame(() => updatePosition(event))
73
+ }
74
+
75
+ const stop = (event) => {
76
+ event = e(event)
77
+ document.documentElement.style.overflow = 'auto' // 恢复默认的滚动行为
78
+ cancelAnimationFrame(rafRef.current)
79
+ setMoving(false)
80
+ currentObj = document.ontouchmove = document.ontouchend = document.onmousemove = document.onmouseup = null
81
+ }
82
+
83
+ const updatePosition = (event) => {
84
+ if (currentObj) {
85
+ const left = event.mx - offsetX
86
+ const top = event.my - offsetY
87
+ currentObj.style.left = left + 'px'
88
+ currentObj.style.top = top + 'px'
89
+ checkInWindow()
90
+ }
91
+ }
92
+
93
+ /**
94
+ * 鼠标是否在可拖拽区域内
95
+ * @param {*} event
96
+ * @returns
97
+ */
98
+ function inDragBox(event, drag) {
99
+ const { clientX, clientY } = event // 鼠标位置
100
+ const { offsetHeight, offsetWidth, offsetTop, offsetLeft } = drag.firstElementChild // 窗口位置
101
+ const horizontal = clientX > offsetLeft && clientX < offsetLeft + offsetWidth
102
+ const vertical = clientY > offsetTop && clientY < offsetTop + offsetHeight
103
+
104
+ if (horizontal && vertical) {
105
+ return true
106
+ }
107
+
108
+ return false
109
+ }
110
+
111
+ /**
112
+ * 若超出窗口则吸附。
113
+ */
114
+ function checkInWindow() {
115
+ // 检查是否悬浮在窗口内
116
+ for (const drag of draggableElements) {
117
+ // 判断鼠标点击的区域是否是拖拽框内
118
+ const { offsetHeight, offsetWidth, offsetTop, offsetLeft } = drag.firstElementChild
119
+ const { clientHeight, clientWidth } = document.documentElement
120
+ if (offsetTop < 0) {
121
+ drag.firstElementChild.style.top = 0
122
+ }
123
+ if (offsetTop > (clientHeight - offsetHeight)) {
124
+ drag.firstElementChild.style.top = clientHeight - offsetHeight + 'px'
125
+ }
126
+ if (offsetLeft < 0) {
127
+ drag.firstElementChild.style.left = 0
128
+ }
129
+ if (offsetLeft > (clientWidth - offsetWidth)) {
130
+ drag.firstElementChild.style.left = clientWidth - offsetWidth + 'px'
131
+ }
132
+ }
133
+ }
134
+
135
+ window.addEventListener('resize', checkInWindow)
136
+
137
+ return () => {
138
+ return () => {
139
+ window.removeEventListener('resize', checkInWindow)
140
+ cancelAnimationFrame(rafRef.current)
141
+ }
142
+ }
143
+ }, [])
144
+
145
+ return <div className={`draggable ${moving ? 'cursor-grabbing' : 'cursor-grab'} select-none`} ref={draggableRef}>
146
+ {children}
147
+ </div>
148
+ }
149
+
150
+ Draggable.defaultProps = { left: 0, top: 0 }
components/Equation.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+
3
+ import Katex from '@/components/KatexReact'
4
+ import { getBlockTitle } from 'notion-utils'
5
+
6
+ const katexSettings = {
7
+ throwOnError: false,
8
+ strict: false
9
+ }
10
+
11
+ /**
12
+ * 数学公式
13
+ * @param {} param0
14
+ * @returns
15
+ */
16
+ export const Equation = ({ block, math, inline = false, className, ...rest }) => {
17
+ math = math || getBlockTitle(block, null)
18
+ if (!math) return null
19
+
20
+ return (
21
+ <span
22
+ role='button'
23
+ tabIndex={0}
24
+ className={`notion-equation ${inline ? 'notion-equation-inline' : 'notion-equation-block'}`}
25
+ >
26
+ <Katex math={math} settings={katexSettings} {...rest} />
27
+ </span>
28
+ )
29
+ }
components/ExternalPlugins.js ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { siteConfig } from '@/lib/config'
2
+ import dynamic from 'next/dynamic'
3
+ import LA51 from './LA51'
4
+ import WebWhiz from './Webwhiz'
5
+ import TianLiGPT from './TianliGPT'
6
+ import { GlobalStyle } from './GlobalStyle'
7
+
8
+ import { CUSTOM_EXTERNAL_CSS, CUSTOM_EXTERNAL_JS, IMG_SHADOW } from '@/blog.config'
9
+ import { isBrowser, loadExternalResource } from '@/lib/utils'
10
+
11
+ const TwikooCommentCounter = dynamic(() => import('@/components/TwikooCommentCounter'), { ssr: false })
12
+ const DebugPanel = dynamic(() => import('@/components/DebugPanel'), { ssr: false })
13
+ const ThemeSwitch = dynamic(() => import('@/components/ThemeSwitch'), { ssr: false })
14
+ const Fireworks = dynamic(() => import('@/components/Fireworks'), { ssr: false })
15
+ const Nest = dynamic(() => import('@/components/Nest'), { ssr: false })
16
+ const FlutteringRibbon = dynamic(() => import('@/components/FlutteringRibbon'), { ssr: false })
17
+ const Ribbon = dynamic(() => import('@/components/Ribbon'), { ssr: false })
18
+ const Sakura = dynamic(() => import('@/components/Sakura'), { ssr: false })
19
+ const StarrySky = dynamic(() => import('@/components/StarrySky'), { ssr: false })
20
+ const DifyChatbot = dynamic(() => import('@/components/DifyChatbot'), { ssr: false });
21
+ const Analytics = dynamic(() => import('@vercel/analytics/react').then(async (m) => { return m.Analytics }), { ssr: false })
22
+ const MusicPlayer = dynamic(() => import('@/components/Player'), { ssr: false })
23
+ const Ackee = dynamic(() => import('@/components/Ackee'), { ssr: false })
24
+ const Gtag = dynamic(() => import('@/components/Gtag'), { ssr: false })
25
+ const Busuanzi = dynamic(() => import('@/components/Busuanzi'), { ssr: false })
26
+ const GoogleAdsense = dynamic(() => import('@/components/GoogleAdsense'), { ssr: false })
27
+ const Messenger = dynamic(() => import('@/components/FacebookMessenger'), { ssr: false })
28
+ const VConsole = dynamic(() => import('@/components/VConsole'), { ssr: false })
29
+ const CustomContextMenu = dynamic(() => import('@/components/CustomContextMenu'), { ssr: false })
30
+ const DisableCopy = dynamic(() => import('@/components/DisableCopy'), { ssr: false })
31
+ const AdBlockDetect = dynamic(() => import('@/components/AdBlockDetect'), { ssr: false })
32
+ const LoadingProgress = dynamic(() => import('@/components/LoadingProgress'), { ssr: false })
33
+ const AosAnimation = dynamic(() => import('@/components/AOSAnimation'), { ssr: false })
34
+
35
+ /**
36
+ * 各种插件脚本
37
+ * @param {*} props
38
+ * @returns
39
+ */
40
+ const ExternalPlugin = (props) => {
41
+ const DISABLE_PLUGIN = siteConfig('DISABLE_PLUGIN')
42
+ const THEME_SWITCH = siteConfig('THEME_SWITCH')
43
+ const DEBUG = siteConfig('DEBUG')
44
+ const ANALYTICS_ACKEE_TRACKER = siteConfig('ANALYTICS_ACKEE_TRACKER')
45
+ const ANALYTICS_VERCEL = siteConfig('ANALYTICS_VERCEL')
46
+ const ANALYTICS_BUSUANZI_ENABLE = siteConfig('ANALYTICS_BUSUANZI_ENABLE')
47
+ const ADSENSE_GOOGLE_ID = siteConfig('ADSENSE_GOOGLE_ID')
48
+ const FACEBOOK_APP_ID = siteConfig('FACEBOOK_APP_ID')
49
+ const FACEBOOK_PAGE_ID = siteConfig('FACEBOOK_PAGE_ID')
50
+ const FIREWORKS = siteConfig('FIREWORKS')
51
+ const SAKURA = siteConfig('SAKURA')
52
+ const STARRY_SKY = siteConfig('STARRY_SKY')
53
+ const MUSIC_PLAYER = siteConfig('MUSIC_PLAYER')
54
+ const NEST = siteConfig('NEST')
55
+ const FLUTTERINGRIBBON = siteConfig('FLUTTERINGRIBBON')
56
+ const COMMENT_TWIKOO_COUNT_ENABLE = siteConfig('COMMENT_TWIKOO_COUNT_ENABLE')
57
+ const RIBBON = siteConfig('RIBBON')
58
+ const CUSTOM_RIGHT_CLICK_CONTEXT_MENU = siteConfig('CUSTOM_RIGHT_CLICK_CONTEXT_MENU')
59
+ const CAN_COPY = siteConfig('CAN_COPY')
60
+ const WEB_WHIZ_ENABLED = siteConfig('WEB_WHIZ_ENABLED')
61
+ const AD_WWADS_BLOCK_DETECT = siteConfig('AD_WWADS_BLOCK_DETECT')
62
+ const CHATBASE_ID = siteConfig('CHATBASE_ID')
63
+ const COMMENT_DAO_VOICE_ID = siteConfig('COMMENT_DAO_VOICE_ID')
64
+ const AD_WWADS_ID = siteConfig('AD_WWADS_ID')
65
+ const COMMENT_TWIKOO_ENV_ID = siteConfig('COMMENT_TWIKOO_ENV_ID')
66
+ const COMMENT_TWIKOO_CDN_URL = siteConfig('COMMENT_TWIKOO_CDN_URL')
67
+ const COMMENT_ARTALK_SERVER = siteConfig('COMMENT_ARTALK_SERVER')
68
+ const COMMENT_ARTALK_JS = siteConfig('COMMENT_ARTALK_JS')
69
+ const COMMENT_TIDIO_ID = siteConfig('COMMENT_TIDIO_ID')
70
+ const COMMENT_GITTER_ROOM = siteConfig('COMMENT_GITTER_ROOM')
71
+ const ANALYTICS_BAIDU_ID = siteConfig('ANALYTICS_BAIDU_ID')
72
+ const ANALYTICS_CNZZ_ID = siteConfig('ANALYTICS_CNZZ_ID')
73
+ const ANALYTICS_GOOGLE_ID = siteConfig('ANALYTICS_GOOGLE_ID')
74
+ const MATOMO_HOST_URL = siteConfig('MATOMO_HOST_URL')
75
+ const MATOMO_SITE_ID = siteConfig('MATOMO_SITE_ID')
76
+ const ANALYTICS_51LA_ID = siteConfig('ANALYTICS_51LA_ID')
77
+ const ANALYTICS_51LA_CK = siteConfig('ANALYTICS_51LA_CK')
78
+ const DIFY_CHATBOT_ENABLED = siteConfig('DIFY_CHATBOT_ENABLED')
79
+ const TIANLI_KEY = siteConfig('TianliGPT_KEY')
80
+ const GLOBAL_JS = siteConfig('GLOBAL_JS')
81
+ const CLARITY_ID = siteConfig('CLARITY_ID')
82
+
83
+ // 自定义样式css和js引入
84
+ if (isBrowser) {
85
+ // 初始化AOS动画
86
+ // 静态导入本地自定义样式
87
+ loadExternalResource('/css/custom.css', 'css')
88
+ loadExternalResource('/js/custom.js', 'js')
89
+
90
+ // 自动添加图片阴影
91
+ if (IMG_SHADOW) {
92
+ loadExternalResource('/css/img-shadow.css', 'css')
93
+ }
94
+
95
+ // 导入外部自定义脚本
96
+ if (CUSTOM_EXTERNAL_JS && CUSTOM_EXTERNAL_JS.length > 0) {
97
+ for (const url of CUSTOM_EXTERNAL_JS) {
98
+ loadExternalResource(url, 'js')
99
+ }
100
+ }
101
+
102
+ // 导入外部自定义样式
103
+ if (CUSTOM_EXTERNAL_CSS && CUSTOM_EXTERNAL_CSS.length > 0) {
104
+ for (const url of CUSTOM_EXTERNAL_CSS) {
105
+ loadExternalResource(url, 'css')
106
+ }
107
+ }
108
+ }
109
+
110
+ if (DISABLE_PLUGIN) {
111
+ return null
112
+ }
113
+
114
+ return <>
115
+
116
+ {/* 全局样式嵌入 */}
117
+ <GlobalStyle/>
118
+
119
+ {THEME_SWITCH && <ThemeSwitch />}
120
+ {DEBUG && <DebugPanel />}
121
+ {ANALYTICS_ACKEE_TRACKER && <Ackee />}
122
+ {ANALYTICS_GOOGLE_ID && <Gtag />}
123
+ {ANALYTICS_VERCEL && <Analytics />}
124
+ {ANALYTICS_BUSUANZI_ENABLE && <Busuanzi />}
125
+ {ADSENSE_GOOGLE_ID && <GoogleAdsense />}
126
+ {FACEBOOK_APP_ID && FACEBOOK_PAGE_ID && <Messenger />}
127
+ {FIREWORKS && <Fireworks />}
128
+ {SAKURA && <Sakura />}
129
+ {STARRY_SKY && <StarrySky />}
130
+ {MUSIC_PLAYER && <MusicPlayer />}
131
+ {NEST && <Nest />}
132
+ {FLUTTERINGRIBBON && <FlutteringRibbon />}
133
+ {COMMENT_TWIKOO_COUNT_ENABLE && <TwikooCommentCounter {...props} />}
134
+ {RIBBON && <Ribbon />}
135
+ {DIFY_CHATBOT_ENABLED && <DifyChatbot />}
136
+ {CUSTOM_RIGHT_CLICK_CONTEXT_MENU && <CustomContextMenu {...props} />}
137
+ {!CAN_COPY && <DisableCopy />}
138
+ {WEB_WHIZ_ENABLED && <WebWhiz />}
139
+ {AD_WWADS_BLOCK_DETECT && <AdBlockDetect />}
140
+ {TIANLI_KEY && <TianLiGPT/>}
141
+ <VConsole />
142
+ <LoadingProgress />
143
+ <AosAnimation />
144
+ {ANALYTICS_51LA_ID && ANALYTICS_51LA_CK && <LA51/>}
145
+
146
+ {ANALYTICS_51LA_ID && ANALYTICS_51LA_CK && (<>
147
+ <script id="LA_COLLECT" src="//sdk.51.la/js-sdk-pro.min.js" defer/>
148
+ {/* <script async dangerouslySetInnerHTML={{
149
+ __html: `
150
+ LA.init({id:"${ANALYTICS_51LA_ID}",ck:"${ANALYTICS_51LA_CK}",hashMode:true,autoTrack:true})
151
+ `
152
+ }} /> */}
153
+ </>)}
154
+
155
+ {/* 注入JS脚本 */}
156
+ {GLOBAL_JS && <script async dangerouslySetInnerHTML={{
157
+ __html: GLOBAL_JS
158
+ }} />}
159
+
160
+ {CHATBASE_ID && (<>
161
+ <script id={CHATBASE_ID} src="https://www.chatbase.co/embed.min.js" defer />
162
+ <script async dangerouslySetInnerHTML={{
163
+ __html: `
164
+ window.chatbaseConfig = {
165
+ chatbotId: "${CHATBASE_ID}",
166
+ }
167
+ `
168
+ }} />
169
+ </>)}
170
+
171
+ {CLARITY_ID && (<>
172
+ <script async dangerouslySetInnerHTML={{
173
+ __html: `
174
+ (function(c,l,a,r,i,t,y){
175
+ c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
176
+ t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
177
+ y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
178
+ })(window, document, "clarity", "script", "${CLARITY_ID}");
179
+ `
180
+ }} />
181
+ </>)}
182
+
183
+ {COMMENT_DAO_VOICE_ID && (<>
184
+ {/* DaoVoice 反馈 */}
185
+ <script async dangerouslySetInnerHTML={{
186
+ __html: `
187
+ (function(i,s,o,g,r,a,m){i["DaoVoiceObject"]=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;a.charset="utf-8";m.parentNode.insertBefore(a,m)})(window,document,"script",('https:' == document.location.protocol ? 'https:' : 'http:') + "//widget.daovoice.io/widget/daf1a94b.js","daovoice")
188
+ `
189
+ }}
190
+ />
191
+ <script async dangerouslySetInnerHTML={{
192
+ __html: `
193
+ daovoice('init', {
194
+ app_id: "${COMMENT_DAO_VOICE_ID}"
195
+ });
196
+ daovoice('update');
197
+ `
198
+ }}
199
+ />
200
+ </>)}
201
+
202
+ {AD_WWADS_ID && <script type="text/javascript" src="https://cdn.wwads.cn/js/makemoney.js" async></script>}
203
+
204
+ {COMMENT_TWIKOO_ENV_ID && <script defer src={COMMENT_TWIKOO_CDN_URL} />}
205
+
206
+ {COMMENT_ARTALK_SERVER && <script defer src={COMMENT_ARTALK_JS} />}
207
+
208
+ {COMMENT_TIDIO_ID && <script async src={`//code.tidio.co/${COMMENT_TIDIO_ID}.js`} />}
209
+
210
+ {/* gitter聊天室 */}
211
+ {COMMENT_GITTER_ROOM && (<>
212
+ <script src="https://sidecar.gitter.im/dist/sidecar.v1.js" async defer />
213
+ <script async dangerouslySetInnerHTML={{
214
+ __html: `
215
+ ((window.gitter = {}).chat = {}).options = {
216
+ room: '${COMMENT_GITTER_ROOM}'
217
+ };
218
+ `
219
+ }} />
220
+ </>)}
221
+
222
+ {/* 百度统计 */}
223
+ {ANALYTICS_BAIDU_ID && (
224
+ <script async
225
+ dangerouslySetInnerHTML={{
226
+ __html: `
227
+ var _hmt = _hmt || [];
228
+ (function() {
229
+ var hm = document.createElement("script");
230
+ hm.src = "https://hm.baidu.com/hm.js?${ANALYTICS_BAIDU_ID}";
231
+ var s = document.getElementsByTagName("script")[0];
232
+ s.parentNode.insertBefore(hm, s);
233
+ })();
234
+ `
235
+ }}
236
+ />
237
+ )}
238
+
239
+ {/* 站长统计 */}
240
+ {ANALYTICS_CNZZ_ID && (
241
+ <script async
242
+ dangerouslySetInnerHTML={{
243
+ __html: `
244
+ document.write(unescape("%3Cspan style='display:none' id='cnzz_stat_icon_${ANALYTICS_CNZZ_ID}'%3E%3C/span%3E%3Cscript src='https://s9.cnzz.com/z_stat.php%3Fid%3D${ANALYTICS_CNZZ_ID}' type='text/javascript'%3E%3C/script%3E"));
245
+ `
246
+ }}
247
+ />
248
+ )}
249
+
250
+ {/* 谷歌统计 */}
251
+ {ANALYTICS_GOOGLE_ID && (<>
252
+ <script async
253
+ src={`https://www.googletagmanager.com/gtag/js?id=${ANALYTICS_GOOGLE_ID}`}
254
+ />
255
+ <script async
256
+ dangerouslySetInnerHTML={{
257
+ __html: `
258
+ window.dataLayer = window.dataLayer || [];
259
+ function gtag(){dataLayer.push(arguments);}
260
+ gtag('js', new Date());
261
+ gtag('config', '${ANALYTICS_GOOGLE_ID}', {
262
+ page_path: window.location.pathname,
263
+ });
264
+ `
265
+ }}
266
+ />
267
+ </>)}
268
+
269
+ {/* Matomo 统计 */}
270
+ {MATOMO_HOST_URL && MATOMO_SITE_ID && (
271
+ <script async dangerouslySetInnerHTML={{
272
+ __html: `
273
+ var _paq = window._paq = window._paq || [];
274
+ _paq.push(['trackPageView']);
275
+ _paq.push(['enableLinkTracking']);
276
+ (function() {
277
+ var u="//${MATOMO_HOST_URL}/";
278
+ _paq.push(['setTrackerUrl', u+'matomo.php']);
279
+ _paq.push(['setSiteId', '${MATOMO_SITE_ID}']);
280
+ var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
281
+ g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
282
+ })();
283
+ `
284
+ }} />
285
+ )}
286
+
287
+ </>
288
+ }
289
+
290
+ export default ExternalPlugin
components/ExternalScript.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { isBrowser } from '@/lib/utils'
4
+
5
+ /**
6
+ * 自定义外部 script
7
+ * 传入参数将转为 <script>标签。
8
+ * @returns
9
+ */
10
+ const ExternalScript = (props) => {
11
+ const { src } = props
12
+ if (!isBrowser || !src) {
13
+ return null
14
+ }
15
+
16
+ const element = document.querySelector(`script[src="${src}"]`)
17
+ if (element) {
18
+ return null
19
+ }
20
+ const script = document.createElement('script')
21
+ Object.entries(props).forEach(([key, value]) => {
22
+ script.setAttribute(key, value)
23
+ })
24
+ document.head.appendChild(script)
25
+ console.log('加载外部脚本', props, script)
26
+ return null
27
+ }
28
+
29
+ export default ExternalScript
components/FacebookMessenger.js ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, useEffect, useState } from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { siteConfig } from '@/lib/config'
4
+
5
+ export default function Messenger() {
6
+ const pageId = siteConfig('FACEBOOK_PAGE_ID')
7
+ const appId = siteConfig('FACEBOOK_APP_ID')
8
+ const language = siteConfig('LANG').replace('-', '_')
9
+
10
+ // 新增一个状态变量用于追踪是否已经滚动过
11
+ const [showMessenger, setShowMessenger] = useState(false);
12
+
13
+ const showTheComponent = () => {
14
+ window.removeEventListener('scroll', showTheComponent);
15
+ if (!showMessenger) {
16
+ setShowMessenger(true);
17
+ }
18
+ };
19
+
20
+ // 延时7秒,或页面滚动时加载该组件
21
+ useEffect(() => {
22
+ window.addEventListener('scroll', showTheComponent);
23
+ setTimeout(() => {
24
+ showTheComponent()
25
+ }, 7000);
26
+ return () => {
27
+ window.removeEventListener('scroll', showTheComponent);
28
+ };
29
+ }, []);
30
+
31
+ return <>
32
+ {showMessenger && <MessengerCustomerChat
33
+ pageId={pageId}
34
+ appId={appId}
35
+ language={language}
36
+ shouldShowDialog={true}
37
+ />}
38
+ </>
39
+ }
40
+
41
+ /**
42
+ * @see https://github.com/Yoctol/react-messenger-customer-chat
43
+ */
44
+ class MessengerCustomerChat extends Component {
45
+ constructor(props) {
46
+ super(props)
47
+ this.state = {
48
+ fbLoaded: false,
49
+ shouldShowDialog: undefined
50
+ }
51
+ }
52
+
53
+ /**
54
+ * 初始化
55
+ */
56
+ componentDidMount() {
57
+ this.setFbAsyncInit()
58
+ this.reloadSDKAsynchronously()
59
+ }
60
+
61
+ componentDidUpdate(prevProps) {
62
+ if (
63
+ prevProps.pageId !== this.props.pageId ||
64
+ prevProps.appId !== this.props.appId ||
65
+ prevProps.shouldShowDialog !== this.props.shouldShowDialog ||
66
+ prevProps.htmlRef !== this.props.htmlRef ||
67
+ prevProps.minimized !== this.props.minimized ||
68
+ prevProps.themeColor !== this.props.themeColor ||
69
+ prevProps.loggedInGreeting !== this.props.loggedInGreeting ||
70
+ prevProps.loggedOutGreeting !== this.props.loggedOutGreeting ||
71
+ prevProps.greetingDialogDisplay !== this.props.greetingDialogDisplay ||
72
+ prevProps.greetingDialogDelay !== this.props.greetingDialogDelay ||
73
+ prevProps.autoLogAppEvents !== this.props.autoLogAppEvents ||
74
+ prevProps.xfbml !== this.props.xfbml ||
75
+ prevProps.version !== this.props.version ||
76
+ prevProps.language !== this.props.language
77
+ ) {
78
+ this.setFbAsyncInit()
79
+ this.reloadSDKAsynchronously()
80
+ }
81
+ }
82
+
83
+ componentWillUnmount() {
84
+ if (window.FB !== undefined) {
85
+ window.FB.CustomerChat.hide()
86
+ }
87
+ }
88
+
89
+ /**
90
+ * 初始化
91
+ */
92
+ setFbAsyncInit() {
93
+ const { appId, autoLogAppEvents, xfbml, version } = this.props
94
+
95
+ window.fbAsyncInit = () => {
96
+ window.FB.init({
97
+ appId,
98
+ autoLogAppEvents,
99
+ xfbml,
100
+ version: `v${version}`
101
+ })
102
+
103
+ this.setState({ fbLoaded: true })
104
+ }
105
+ }
106
+
107
+ loadSDKAsynchronously() {
108
+ const { language } = this.props;
109
+ /* eslint-disable */
110
+ (function (d, s, id) {
111
+ var js,
112
+ fjs = d.getElementsByTagName(s)[0];
113
+ if (d.getElementById(id)) {
114
+ return;
115
+ }
116
+ js = d.createElement(s);
117
+ js.id = id;
118
+ js.src = `https://connect.facebook.net/${language}/sdk/xfbml.customerchat.js`;
119
+ fjs.parentNode.insertBefore(js, fjs);
120
+ })(document, 'script', 'facebook-jssdk');
121
+ /* eslint-enable */
122
+ }
123
+
124
+ removeFacebookSDK() {
125
+ removeElementByIds(['facebook-jssdk', 'fb-root'])
126
+
127
+ delete window.FB
128
+ }
129
+
130
+ reloadSDKAsynchronously() {
131
+ this.removeFacebookSDK()
132
+ this.loadSDKAsynchronously()
133
+ }
134
+
135
+ controlPlugin() {
136
+ const { shouldShowDialog } = this.props
137
+
138
+ if (shouldShowDialog) {
139
+ window.FB.CustomerChat.showDialog()
140
+ } else {
141
+ window.FB.CustomerChat.hideDialog()
142
+ }
143
+ }
144
+
145
+ subscribeEvents() {
146
+ const { onCustomerChatDialogShow, onCustomerChatDialogHide } = this.props
147
+
148
+ if (onCustomerChatDialogShow) {
149
+ window.FB.Event.subscribe(
150
+ 'customerchat.dialogShow',
151
+ onCustomerChatDialogShow
152
+ )
153
+ }
154
+
155
+ if (onCustomerChatDialogHide) {
156
+ window.FB.Event.subscribe(
157
+ 'customerchat.dialogHide',
158
+ onCustomerChatDialogHide
159
+ )
160
+ }
161
+ }
162
+
163
+ createMarkup() {
164
+ const {
165
+ pageId,
166
+ htmlRef,
167
+ minimized,
168
+ themeColor,
169
+ loggedInGreeting,
170
+ loggedOutGreeting,
171
+ greetingDialogDisplay,
172
+ greetingDialogDelay
173
+ } = this.props
174
+
175
+ const refAttribute = htmlRef !== undefined ? `ref="${htmlRef}"` : ''
176
+ const minimizedAttribute =
177
+ minimized !== undefined ? `minimized="${minimized}"` : ''
178
+ const themeColorAttribute =
179
+ themeColor !== undefined ? `theme_color="${themeColor}"` : ''
180
+ const loggedInGreetingAttribute =
181
+ loggedInGreeting !== undefined
182
+ ? `logged_in_greeting="${loggedInGreeting}"`
183
+ : ''
184
+ const loggedOutGreetingAttribute =
185
+ loggedOutGreeting !== undefined
186
+ ? `logged_out_greeting="${loggedOutGreeting}"`
187
+ : ''
188
+ const greetingDialogDisplayAttribute =
189
+ greetingDialogDisplay !== undefined
190
+ ? `greeting_dialog_display="${greetingDialogDisplay}"`
191
+ : ''
192
+ const greetingDialogDelayAttribute =
193
+ greetingDialogDelay !== undefined
194
+ ? `greeting_dialog_delay="${greetingDialogDelay}"`
195
+ : ''
196
+
197
+ return {
198
+ __html: `<div
199
+ class="fb-customerchat"
200
+ page_id="${pageId}"
201
+ ${refAttribute}
202
+ ${minimizedAttribute}
203
+ ${themeColorAttribute}
204
+ ${loggedInGreetingAttribute}
205
+ ${loggedOutGreetingAttribute}
206
+ ${greetingDialogDisplayAttribute}
207
+ ${greetingDialogDelayAttribute}
208
+ ></div>`
209
+ }
210
+ }
211
+
212
+ render() {
213
+ const { fbLoaded, shouldShowDialog } = this.state
214
+
215
+ if (fbLoaded && shouldShowDialog !== this.props.shouldShowDialog) {
216
+ document.addEventListener(
217
+ 'DOMNodeInserted',
218
+ (event) => {
219
+ const element = event.target
220
+ if (
221
+ element.className &&
222
+ typeof element.className === 'string' &&
223
+ element.className.includes('fb_dialog')
224
+ ) {
225
+ this.controlPlugin()
226
+ }
227
+ },
228
+ false
229
+ )
230
+ this.subscribeEvents()
231
+ }
232
+ // Add a random key to rerender. Reference:
233
+ // https://stackoverflow.com/questions/30242530/dangerouslysetinnerhtml-doesnt-update-during-render
234
+ return <div key={Date()} dangerouslySetInnerHTML={this.createMarkup()} />
235
+ }
236
+ }
237
+
238
+ const removeElementByIds = (ids) => {
239
+ ids.forEach((id) => {
240
+ const element = document.getElementById(id)
241
+ if (element && element.parentNode) {
242
+ element.parentNode.removeChild(element)
243
+ }
244
+ })
245
+ }
246
+
247
+ MessengerCustomerChat.propTypes = {
248
+ pageId: PropTypes.string.isRequired,
249
+ appId: PropTypes.string,
250
+ shouldShowDialog: PropTypes.bool,
251
+ htmlRef: PropTypes.string,
252
+ minimized: PropTypes.bool,
253
+ themeColor: PropTypes.string,
254
+ loggedInGreeting: PropTypes.string,
255
+ loggedOutGreeting: PropTypes.string,
256
+ greetingDialogDisplay: PropTypes.oneOf(['show', 'hide', 'fade']),
257
+ greetingDialogDelay: PropTypes.number,
258
+ autoLogAppEvents: PropTypes.bool,
259
+ xfbml: PropTypes.bool,
260
+ version: PropTypes.string,
261
+ language: PropTypes.string,
262
+ onCustomerChatDialogShow: PropTypes.func,
263
+ onCustomerChatDialogHide: PropTypes.func
264
+ }
265
+
266
+ MessengerCustomerChat.defaultProps = {
267
+ appId: null,
268
+ shouldShowDialog: false,
269
+ htmlRef: undefined,
270
+ minimized: undefined,
271
+ themeColor: undefined,
272
+ loggedInGreeting: undefined,
273
+ loggedOutGreeting: undefined,
274
+ greetingDialogDisplay: undefined,
275
+ greetingDialogDelay: undefined,
276
+ autoLogAppEvents: true,
277
+ xfbml: true,
278
+ version: '11.0',
279
+ language: 'en_US',
280
+ onCustomerChatDialogShow: undefined,
281
+ onCustomerChatDialogHide: undefined
282
+ }
components/FacebookPage.js ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { siteConfig } from '@/lib/config'
2
+ import { FacebookProvider, Page } from 'react-facebook'
3
+ import { FacebookIcon } from 'react-share'
4
+
5
+ /**
6
+ * facebook个人主页
7
+ * @returns
8
+ */
9
+ const FacebookPage = () => {
10
+ if (!siteConfig('FACEBOOK_APP_ID') || !siteConfig('FACEBOOK_PAGE')) {
11
+ return <></>
12
+ }
13
+ return <div className="shadow-md hover:shadow-xl dark:text-gray-300 border dark:border-black rounded-xl px-2 py-4 bg-white dark:bg-hexo-black-gray lg:duration-100 justify-center">
14
+ {siteConfig('FACEBOOK_PAGE') && (
15
+ <div className="flex items-center pb-2">
16
+ <a
17
+ href={siteConfig('FACEBOOK_PAGE')}
18
+ target="_blank"
19
+ rel="noopener noreferrer"
20
+ className="p-1 pr-2 pt-0"
21
+ >
22
+ <FacebookIcon size={28} round />
23
+ </a>
24
+ <a href={siteConfig('FACEBOOK_PAGE')} rel="noopener noreferrer" target="_blank">
25
+ {siteConfig('FACEBOOK_PAGE_TITLE')}
26
+ </a>
27
+ </div>
28
+ )}
29
+ {siteConfig('FACEBOOK_APP_ID') && <FacebookProvider appId={siteConfig('FACEBOOK_APP_ID')}>
30
+ <Page href={siteConfig('FACEBOOK_PAGE')} tabs="timeline" />
31
+ </FacebookProvider>}
32
+ </div>
33
+ }
34
+ export default FacebookPage
components/Fireworks.js ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * https://codepen.io/juliangarnier/pen/gmOwJX
3
+ * custom by hexo-theme-yun @YunYouJun
4
+ */
5
+ import { useEffect } from 'react'
6
+ import anime from 'animejs'
7
+ import { siteConfig } from '@/lib/config'
8
+
9
+ /**
10
+ * 鼠标点击烟花特效
11
+ * @returns
12
+ */
13
+ const Fireworks = () => {
14
+ const fireworksColor = siteConfig('FIREWORKS_COLOR')
15
+
16
+ useEffect(() => {
17
+ createFireworks({ colors: fireworksColor })
18
+ }, [])
19
+ return <canvas id='fireworks' className='fireworks'></canvas>
20
+ }
21
+ export default Fireworks
22
+
23
+ /**
24
+ * 创建烟花
25
+ * @param config
26
+ */
27
+ function createFireworks(config) {
28
+ const defaultConfig = {
29
+ colors: config?.colors,
30
+ numberOfParticules: 20,
31
+ orbitRadius: {
32
+ min: 50,
33
+ max: 100
34
+ },
35
+ circleRadius: {
36
+ min: 10,
37
+ max: 20
38
+ },
39
+ diffuseRadius: {
40
+ min: 50,
41
+ max: 100
42
+ },
43
+ animeDuration: {
44
+ min: 900,
45
+ max: 1500
46
+ }
47
+ }
48
+ config = Object.assign(defaultConfig, config)
49
+
50
+ let pointerX = 0
51
+ let pointerY = 0
52
+
53
+ // sky blue
54
+ const colors = config.colors
55
+
56
+ const canvasEl = document.querySelector('.fireworks')
57
+ const ctx = canvasEl.getContext('2d')
58
+
59
+ /**
60
+ * 设置画布尺寸
61
+ */
62
+ function setCanvasSize(canvasEl) {
63
+ canvasEl.width = window.innerWidth
64
+ canvasEl.height = window.innerHeight
65
+ canvasEl.style.width = `${window.innerWidth}px`
66
+ canvasEl.style.height = `${window.innerHeight}px`
67
+ }
68
+
69
+ /**
70
+ * update pointer
71
+ * @param {TouchEvent} e
72
+ */
73
+ function updateCoords(e) {
74
+ pointerX =
75
+ e.clientX ||
76
+ (e.touches[0] ? e.touches[0].clientX : e.changedTouches[0].clientX)
77
+ pointerY =
78
+ e.clientY ||
79
+ (e.touches[0] ? e.touches[0].clientY : e.changedTouches[0].clientY)
80
+ }
81
+
82
+ function setParticuleDirection(p) {
83
+ const angle = (anime.random(0, 360) * Math.PI) / 180
84
+ const value = anime.random(
85
+ config.diffuseRadius.min,
86
+ config.diffuseRadius.max
87
+ )
88
+ const radius = [-1, 1][anime.random(0, 1)] * value
89
+ return {
90
+ x: p.x + radius * Math.cos(angle),
91
+ y: p.y + radius * Math.sin(angle)
92
+ }
93
+ }
94
+
95
+ /**
96
+ * 在指定位置创建粒子
97
+ * @param {number} x
98
+ * @param {number} y
99
+ * @returns
100
+ */
101
+ function createParticule(x, y) {
102
+ const p = {
103
+ x,
104
+ y,
105
+ color: `rgba(${
106
+ colors[anime.random(0, colors.length - 1)]
107
+ },${
108
+ anime.random(0.2, 0.8)
109
+ })`,
110
+ radius: anime.random(config.circleRadius.min, config.circleRadius.max),
111
+ endPos: null,
112
+ draw() {}
113
+ }
114
+ p.endPos = setParticuleDirection(p)
115
+ p.draw = function() {
116
+ ctx.beginPath()
117
+ ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI, true)
118
+ ctx.fillStyle = p.color
119
+ ctx.fill()
120
+ }
121
+ return p
122
+ }
123
+
124
+ function createCircle(x, y) {
125
+ const p = {
126
+ x,
127
+ y,
128
+ color: '#000',
129
+ radius: 0.1,
130
+ alpha: 0.5,
131
+ lineWidth: 6,
132
+ draw() {}
133
+ }
134
+ p.draw = function() {
135
+ ctx.globalAlpha = p.alpha
136
+ ctx.beginPath()
137
+ ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI, true)
138
+ ctx.lineWidth = p.lineWidth
139
+ ctx.strokeStyle = p.color
140
+ ctx.stroke()
141
+ ctx.globalAlpha = 1
142
+ }
143
+ return p
144
+ }
145
+
146
+ function renderParticule(anim) {
147
+ for (let i = 0; i < anim.animatables.length; i++) { anim.animatables[i].target.draw() }
148
+ }
149
+
150
+ function animateParticules(x, y) {
151
+ const circle = createCircle(x, y)
152
+ const particules = []
153
+ for (let i = 0; i < config.numberOfParticules; i++) { particules.push(createParticule(x, y)) }
154
+
155
+ anime
156
+ .timeline()
157
+ .add({
158
+ targets: particules,
159
+ x(p) {
160
+ return p.endPos.x
161
+ },
162
+ y(p) {
163
+ return p.endPos.y
164
+ },
165
+ radius: 0.1,
166
+ duration: anime.random(
167
+ config.animeDuration.min,
168
+ config.animeDuration.max
169
+ ),
170
+ easing: 'easeOutExpo',
171
+ update: renderParticule
172
+ })
173
+ .add(
174
+ {
175
+ targets: circle,
176
+ radius: anime.random(config.orbitRadius.min, config.orbitRadius.max),
177
+ lineWidth: 0,
178
+ alpha: {
179
+ value: 0,
180
+ easing: 'linear',
181
+ duration: anime.random(600, 800)
182
+ },
183
+ duration: anime.random(1200, 1800),
184
+ easing: 'easeOutExpo',
185
+ update: renderParticule
186
+ },
187
+ 0
188
+ )
189
+ }
190
+
191
+ const render = anime({
192
+ duration: Infinity,
193
+ update: () => {
194
+ ctx.clearRect(0, 0, canvasEl.width, canvasEl.height)
195
+ }
196
+ })
197
+
198
+ document.addEventListener(
199
+ 'mousedown',
200
+ (e) => {
201
+ render.play()
202
+ updateCoords(e)
203
+ animateParticules(pointerX, pointerY)
204
+ },
205
+ false
206
+ )
207
+
208
+ setCanvasSize(canvasEl)
209
+ window.addEventListener(
210
+ 'resize',
211
+ () => {
212
+ setCanvasSize(canvasEl)
213
+ },
214
+ false
215
+ )
216
+ }
components/FlipCard.js ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react'
2
+
3
+ /**
4
+ * 翻转组件
5
+ * @param {*} props
6
+ * @returns
7
+ */
8
+ export default function FlipCard(props) {
9
+ const [isFlipped, setIsFlipped] = useState(false)
10
+
11
+ function handleCardFlip() {
12
+ setIsFlipped(!isFlipped)
13
+ }
14
+
15
+ return (
16
+ <div className={`flip-card ${isFlipped ? 'flipped' : ''}`} >
17
+ <div className={`flip-card-front ${props.className || ''}`} onMouseEnter={handleCardFlip}>
18
+ {props.frontContent}
19
+ </div>
20
+ <div className={`flip-card-back ${props.className || ''}`} onMouseLeave={handleCardFlip}>
21
+ {props.backContent}
22
+ </div>
23
+ <style jsx>{`
24
+ .flip-card {
25
+ width: 100%;
26
+ height: 100%;
27
+ display: inline-block;
28
+ position: relative;
29
+ transform-style: preserve-3d;
30
+ transition: transform 0.2s;
31
+ }
32
+
33
+ .flip-card-front,
34
+ .flip-card-back {
35
+ position: absolute;
36
+ width: 100%;
37
+ height: 100%;
38
+ backface-visibility: hidden;
39
+ }
40
+
41
+ .flip-card-front {
42
+ z-index: 2;
43
+ transform: rotateY(0);
44
+ }
45
+
46
+ .flip-card-back {
47
+ transform: rotateY(180deg);
48
+ }
49
+
50
+ .flip-card.flipped {
51
+ transform: rotateY(180deg);
52
+ }
53
+ `}</style>
54
+ </div>
55
+ )
56
+ }
components/FlutteringRibbon.js ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable */
2
+ import { useEffect } from 'react'
3
+ const id = 'canvasFlutteringRibbon'
4
+ export const FlutteringRibbon = () => {
5
+ const destroyRibbon = ()=>{
6
+ const ribbon = document.getElementById(id)
7
+ if(ribbon && ribbon.parentNode){
8
+ ribbon.parentNode.removeChild(ribbon)
9
+ }
10
+ }
11
+
12
+ useEffect(() => {
13
+ createFlutteringRibbon()
14
+ return () => destroyRibbon()
15
+
16
+ }, [])
17
+ return <></>
18
+ }
19
+
20
+ export default FlutteringRibbon
21
+
22
+
23
+ /**
24
+ * 创建连接点
25
+ * @param config
26
+ */
27
+ function createFlutteringRibbon() {
28
+ 'object' == typeof window &&
29
+ (window.Ribbons = (function () {
30
+ const t = window,
31
+ i = document.body,
32
+ n = document.documentElement
33
+ var o = function () {
34
+ if (1 === arguments.length) {
35
+ if (Array.isArray(arguments[0])) {
36
+ const t = Math.round(o(0, arguments[0].length - 1))
37
+ return arguments[0][t]
38
+ }
39
+ return o(0, arguments[0])
40
+ }
41
+ return 2 === arguments.length
42
+ ? Math.random() * (arguments[1] - arguments[0]) + arguments[0]
43
+ : 0
44
+ }
45
+ const s = function (o) {
46
+ const s = Math.max(
47
+ 0,
48
+ t.innerWidth || n.clientWidth || i.clientWidth || 0
49
+ ),
50
+ e = Math.max(
51
+ 0,
52
+ t.innerHeight || n.clientHeight || i.clientHeight || 0
53
+ )
54
+ return {
55
+ width: s,
56
+ height: e,
57
+ ratio: s / e,
58
+ centerx: s / 2,
59
+ centery: e / 2,
60
+ scrollx:
61
+ Math.max(0, t.pageXOffset || n.scrollLeft || i.scrollLeft || 0) -
62
+ (n.clientLeft || 0),
63
+ scrolly:
64
+ Math.max(0, t.pageYOffset || n.scrollTop || i.scrollTop || 0) -
65
+ (n.clientTop || 0)
66
+ }
67
+ },
68
+ e = function (t, i) {
69
+ ;(this.x = 0), (this.y = 0), this.set(t, i)
70
+ }
71
+ e.prototype = {
72
+ constructor: e,
73
+ set: function (t, i) {
74
+ ;(this.x = t || 0), (this.y = i || 0)
75
+ },
76
+ copy: function (t) {
77
+ return (this.x = t.x || 0), (this.y = t.y || 0), this
78
+ },
79
+ multiply: function (t, i) {
80
+ return (this.x *= t || 1), (this.y *= i || 1), this
81
+ },
82
+ divide: function (t, i) {
83
+ return (this.x /= t || 1), (this.y /= i || 1), this
84
+ },
85
+ add: function (t, i) {
86
+ return (this.x += t || 0), (this.y += i || 0), this
87
+ },
88
+ subtract: function (t, i) {
89
+ return (this.x -= t || 0), (this.y -= i || 0), this
90
+ },
91
+ clampX: function (t, i) {
92
+ return (this.x = Math.max(t, Math.min(this.x, i))), this
93
+ },
94
+ clampY: function (t, i) {
95
+ return (this.y = Math.max(t, Math.min(this.y, i))), this
96
+ },
97
+ flipX: function () {
98
+ return (this.x *= -1), this
99
+ },
100
+ flipY: function () {
101
+ return (this.y *= -1), this
102
+ }
103
+ }
104
+ const h = function (t) {
105
+ ;(this._canvas = null),
106
+ (this._context = null),
107
+ (this._sto = null),
108
+ (this._width = 0),
109
+ (this._height = 0),
110
+ (this._scroll = 0),
111
+ (this._ribbons = []),
112
+ (this._options = {
113
+ colorSaturation: '80%',
114
+ colorBrightness: '60%',
115
+ colorAlpha: 0.65,
116
+ colorCycleSpeed: 6,
117
+ verticalPosition: 'center',
118
+ horizontalSpeed: 150,
119
+ ribbonCount: 5,
120
+ strokeSize: 5,
121
+ parallaxAmount: -0.5,
122
+ animateSections: !0
123
+ }),
124
+ (this._onDraw = this._onDraw.bind(this)),
125
+ (this._onResize = this._onResize.bind(this)),
126
+ (this._onScroll = this._onScroll.bind(this)),
127
+ this.setOptions(t),
128
+ this.init()
129
+ }
130
+ return (
131
+ (h.prototype = {
132
+ constructor: h,
133
+ setOptions: function (t) {
134
+ if ('object' == typeof t)
135
+ for (const i in t)
136
+ t.hasOwnProperty(i) && (this._options[i] = t[i])
137
+ },
138
+ init: function () {
139
+ try {
140
+ ;(this._canvas = document.createElement('canvas')),
141
+ (this._canvas.id = id),
142
+ (this._canvas.style.display = 'block'),
143
+ (this._canvas.style.position = 'fixed'),
144
+ (this._canvas.style.margin = '0'),
145
+ (this._canvas.style.padding = '0'),
146
+ (this._canvas.style.border = '0'),
147
+ (this._canvas.style.outline = '0'),
148
+ (this._canvas.style.left = '0'),
149
+ (this._canvas.style.top = '0'),
150
+ (this._canvas.style.width = '100%'),
151
+ (this._canvas.style.height = '100%'),
152
+ (this._canvas.style['z-index'] = '0'),
153
+ (this._canvas.style['pointer-events'] = 'none'),
154
+ this._onResize(),
155
+ (this._context = this._canvas.getContext('2d')),
156
+ this._context.clearRect(0, 0, this._width, this._height),
157
+ (this._context.globalAlpha = this._options.colorAlpha),
158
+ window.addEventListener('resize', this._onResize),
159
+ window.addEventListener('scroll', this._onScroll),
160
+ document.body.appendChild(this._canvas)
161
+ } catch (t) {
162
+ return void console.warn('Canvas Context Error: ' + t.toString())
163
+ }
164
+ this._onDraw()
165
+ },
166
+ addRibbon: function () {
167
+ const t = Math.round(o(1, 9)) > 5 ? 'right' : 'left'
168
+ let i = 1e3
169
+ const n = 200,
170
+ s = 0 - n,
171
+ h = this._width + n
172
+ let a = 0,
173
+ r = 0
174
+ const l = 'right' === t ? s : h
175
+ let c = Math.round(o(0, this._height))
176
+ ;/^(top|min)$/i.test(this._options.verticalPosition)
177
+ ? (c = 0 + n)
178
+ : /^(middle|center)$/i.test(this._options.verticalPosition)
179
+ ? (c = this._height / 2)
180
+ : /^(bottom|max)$/i.test(this._options.verticalPosition) &&
181
+ (c = this._height - n)
182
+ const p = [],
183
+ _ = new e(l, c),
184
+ d = new e(l, c)
185
+ let u = null,
186
+ b = Math.round(o(0, 360)),
187
+ f = 0
188
+ for (; !(i <= 0); ) {
189
+ if (
190
+ (i--,
191
+ (a = Math.round(
192
+ (1 * Math.random() - 0.2) * this._options.horizontalSpeed
193
+ )),
194
+ (r = Math.round(
195
+ (1 * Math.random() - 0.5) * (0.25 * this._height)
196
+ )),
197
+ (u = new e()),
198
+ u.copy(d),
199
+ 'right' === t)
200
+ ) {
201
+ if ((u.add(a, r), d.x >= h)) break
202
+ } else if ('left' === t && (u.subtract(a, r), d.x <= s)) break
203
+ p.push({
204
+ point1: new e(_.x, _.y),
205
+ point2: new e(d.x, d.y),
206
+ point3: u,
207
+ color: b,
208
+ delay: f,
209
+ dir: t,
210
+ alpha: 0,
211
+ phase: 0
212
+ }),
213
+ _.copy(d),
214
+ d.copy(u),
215
+ (f += 4),
216
+ (b += this._options.colorCycleSpeed)
217
+ }
218
+ this._ribbons.push(p)
219
+ },
220
+ _drawRibbonSection: function (t) {
221
+ if (t) {
222
+ if (t.phase >= 1 && t.alpha <= 0) return !0
223
+ if (t.delay <= 0) {
224
+ if (
225
+ ((t.phase += 0.02),
226
+ (t.alpha = 1 * Math.sin(t.phase)),
227
+ (t.alpha = t.alpha <= 0 ? 0 : t.alpha),
228
+ (t.alpha = t.alpha >= 1 ? 1 : t.alpha),
229
+ this._options.animateSections)
230
+ ) {
231
+ const i = 0.1 * Math.sin(1 + (t.phase * Math.PI) / 2)
232
+ 'right' === t.dir
233
+ ? (t.point1.add(i, 0),
234
+ t.point2.add(i, 0),
235
+ t.point3.add(i, 0))
236
+ : (t.point1.subtract(i, 0),
237
+ t.point2.subtract(i, 0),
238
+ t.point3.subtract(i, 0)),
239
+ t.point1.add(0, i),
240
+ t.point2.add(0, i),
241
+ t.point3.add(0, i)
242
+ }
243
+ } else t.delay -= 0.5
244
+ const i = this._options.colorSaturation,
245
+ n = this._options.colorBrightness,
246
+ o =
247
+ 'hsla(' +
248
+ t.color +
249
+ ', ' +
250
+ i +
251
+ ', ' +
252
+ n +
253
+ ', ' +
254
+ t.alpha +
255
+ ' )'
256
+ this._context.save(),
257
+ 0 !== this._options.parallaxAmount &&
258
+ this._context.translate(
259
+ 0,
260
+ this._scroll * this._options.parallaxAmount
261
+ ),
262
+ this._context.beginPath(),
263
+ this._context.moveTo(t.point1.x, t.point1.y),
264
+ this._context.lineTo(t.point2.x, t.point2.y),
265
+ this._context.lineTo(t.point3.x, t.point3.y),
266
+ (this._context.fillStyle = o),
267
+ this._context.fill(),
268
+ this._options.strokeSize > 0 &&
269
+ ((this._context.lineWidth = this._options.strokeSize),
270
+ (this._context.strokeStyle = o),
271
+ (this._context.lineCap = 'round'),
272
+ this._context.stroke()),
273
+ this._context.restore()
274
+ }
275
+ return !1
276
+ },
277
+ _onDraw: function () {
278
+ for (let t = 0, i = this._ribbons.length; t < i; ++t)
279
+ this._ribbons[t] || this._ribbons.splice(t, 1)
280
+ this._context.clearRect(0, 0, this._width, this._height)
281
+ for (let t = 0; t < this._ribbons.length; ++t) {
282
+ const i = this._ribbons[t],
283
+ n = i.length
284
+ let o = 0
285
+ for (let t = 0; t < n; ++t) this._drawRibbonSection(i[t]) && o++
286
+ o >= n && (this._ribbons[t] = null)
287
+ }
288
+ this._ribbons.length < this._options.ribbonCount &&
289
+ this.addRibbon(),
290
+ requestAnimationFrame(this._onDraw)
291
+ },
292
+ _onResize: function (t) {
293
+ const i = s(t)
294
+ ;(this._width = i.width),
295
+ (this._height = i.height),
296
+ this._canvas &&
297
+ ((this._canvas.width = this._width),
298
+ (this._canvas.height = this._height),
299
+ this._context &&
300
+ (this._context.globalAlpha = this._options.colorAlpha))
301
+ },
302
+ _onScroll: function (t) {
303
+ const i = s(t)
304
+ this._scroll = i.scrolly
305
+ }
306
+ }),
307
+ h
308
+ )
309
+ })())
310
+ new Ribbons({
311
+ colorSaturation: '60%',
312
+ colorBrightness: '50%',
313
+ colorAlpha: 0.5,
314
+ colorCycleSpeed: 5,
315
+ verticalPosition: 'random',
316
+ horizontalSpeed: 200,
317
+ ribbonCount: 3,
318
+ strokeSize: 0,
319
+ parallaxAmount: -0.2,
320
+ animateSections: !0
321
+ })
322
+ }
components/FullScreenButton.js ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { isBrowser } from '@/lib/utils'
2
+ import React, { useState } from 'react'
3
+
4
+ /**
5
+ * 全屏按钮
6
+ * @returns
7
+ */
8
+ const FullScreenButton = () => {
9
+ const [isFullScreen, setIsFullScreen] = useState(false)
10
+
11
+ const handleFullScreenClick = () => {
12
+ if (!isBrowser) {
13
+ return
14
+ }
15
+ const element = document.documentElement
16
+ if (!isFullScreen) {
17
+ if (element.requestFullscreen) {
18
+ element.requestFullscreen()
19
+ } else if (element.webkitRequestFullscreen) {
20
+ element.webkitRequestFullscreen()
21
+ } else if (element.mozRequestFullScreen) {
22
+ element.mozRequestFullScreen()
23
+ } else if (element.msRequestFullscreen) {
24
+ element.msRequestFullscreen()
25
+ }
26
+ setIsFullScreen(true)
27
+ } else {
28
+ if (document.exitFullscreen) {
29
+ document.exitFullscreen()
30
+ } else if (document.webkitExitFullscreen) {
31
+ document.webkitExitFullscreen()
32
+ } else if (document.mozCancelFullScreen) {
33
+ document.mozCancelFullScreen()
34
+ } else if (document.msExitFullscreen) {
35
+ document.msExitFullscreen()
36
+ }
37
+ setIsFullScreen(false)
38
+ }
39
+ }
40
+
41
+ return (
42
+ <button onClick={handleFullScreenClick} className='dark:text-gray-300'>
43
+ {isFullScreen ? '退出全屏' : <i className="fa-solid fa-expand"></i>}
44
+ </button>
45
+ )
46
+ }
47
+
48
+ export default FullScreenButton
components/Giscus.js ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { siteConfig } from '@/lib/config'
2
+ import { useGlobal } from '@/lib/global'
3
+ import Giscus from '@giscus/react'
4
+
5
+ /**
6
+ * Giscus评论 @see https://giscus.app/zh-CN
7
+ * Contribute by @txs https://github.com/txs/NotionNext/commit/1bf7179d0af21fb433e4c7773504f244998678cb
8
+ * @returns {JSX.Element}
9
+ * @constructor
10
+ */
11
+
12
+ const GiscusComponent = () => {
13
+ const { isDarkMode } = useGlobal()
14
+ const theme = isDarkMode ? 'dark' : 'light'
15
+
16
+ return (
17
+ <Giscus
18
+ repo={siteConfig('COMMENT_GISCUS_REPO')}
19
+ repoId={siteConfig('COMMENT_GISCUS_REPO_ID')}
20
+ categoryId={siteConfig('COMMENT_GISCUS_CATEGORY_ID')}
21
+ mapping={siteConfig('COMMENT_GISCUS_MAPPING')}
22
+ reactionsEnabled={siteConfig('COMMENT_GISCUS_REACTIONS_ENABLED')}
23
+ emitMetadata={siteConfig('COMMENT_GISCUS_EMIT_METADATA')}
24
+ theme={theme}
25
+ inputPosition={siteConfig('COMMENT_GISCUS_INPUT_POSITION')}
26
+ lang={siteConfig('COMMENT_GISCUS_LANG')}
27
+ loading={siteConfig('COMMENT_GISCUS_LOADING')}
28
+ crossorigin={siteConfig('COMMENT_GISCUS_CROSSORIGIN')}
29
+ />
30
+ )
31
+ }
32
+
33
+ export default GiscusComponent
components/Gitalk.js ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { siteConfig } from '@/lib/config'
2
+ import { loadExternalResource } from '@/lib/utils'
3
+ import { useEffect } from 'react'
4
+
5
+ /**
6
+ * gitalk评论插件
7
+ * @param {*} param0
8
+ * @returns
9
+ */
10
+ const Gitalk = ({ frontMatter }) => {
11
+ const gitalkCSSCDN = siteConfig('COMMENT_GITALK_CSS_CDN_URL')
12
+ const gitalkJSCDN = siteConfig('COMMENT_GITALK_JS_CDN_URL')
13
+ const clientId = siteConfig('COMMENT_GITALK_CLIENT_ID')
14
+ const clientSecret = siteConfig('COMMENT_GITALK_CLIENT_SECRET')
15
+ const repo = siteConfig('COMMENT_GITALK_REPO')
16
+ const owner = siteConfig('COMMENT_GITALK_OWNER')
17
+ const admin = siteConfig('COMMENT_GITALK_ADMIN').split(',')
18
+ const distractionFreeMode = siteConfig('COMMENT_GITALK_DISTRACTION_FREE_MODE')
19
+
20
+ const loadGitalk = async() => {
21
+ await loadExternalResource(gitalkCSSCDN, 'css')
22
+ await loadExternalResource(gitalkJSCDN, 'js')
23
+ const Gitalk = window.Gitalk
24
+ if (!Gitalk) {
25
+ // 可以加入延时重试
26
+ console.warn('Gitalk 初始化失败')
27
+ return
28
+ }
29
+ const gitalk = new Gitalk({
30
+ clientID: clientId,
31
+ clientSecret: clientSecret,
32
+ repo: repo,
33
+ owner: owner,
34
+ admin: admin,
35
+ id: frontMatter.id, // Ensure uniqueness and length less than 50
36
+ distractionFreeMode: distractionFreeMode // Facebook-like distraction free mode
37
+ })
38
+
39
+ gitalk.render('gitalk-container')
40
+ }
41
+
42
+ useEffect(() => {
43
+ loadGitalk()
44
+ }, [])
45
+
46
+ return <div id="gitalk-container"></div>
47
+ }
48
+
49
+ export default Gitalk
components/GlobalHead.js ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { siteConfig } from '@/lib/config'
2
+ import { useGlobal } from '@/lib/global'
3
+ import Head from 'next/head'
4
+ import { useRouter } from 'next/router'
5
+
6
+ /**
7
+ * 页面的Head头,通常有用于SEO
8
+ * @param {*} param0
9
+ * @returns
10
+ */
11
+ const GlobalHead = (props) => {
12
+ const { children } = props
13
+ let url = siteConfig('PATH')?.length ? `${siteConfig('LINK')}/${siteConfig('SUB_PATH', '')}` : siteConfig('LINK')
14
+ let image
15
+ const meta = getSEOMeta(props, useRouter(), useGlobal())
16
+ if (meta) {
17
+ url = `${url}/${meta.slug}`
18
+ image = meta.image || '/bg_image.jpg'
19
+ }
20
+ const title = meta?.title || siteConfig('TITLE')
21
+ const description = meta?.description || siteConfig('DESCRIPTION')
22
+ const type = meta?.type || 'website'
23
+ const keywords = meta?.tags || siteConfig('KEYWORDS')
24
+ const lang = siteConfig('LANG').replace('-', '_') // Facebook OpenGraph 要 zh_CN 這樣的格式才抓得到語言
25
+ const category = meta?.category || siteConfig('KEYWORDS') // section 主要是像是 category 這樣的分類,Facebook 用這個來抓連結的分類
26
+
27
+ return (
28
+ <Head>
29
+ <title>{title}</title>
30
+ <meta name="theme-color" content={siteConfig('BACKGROUND_DARK')} />
31
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, minimum-scale=1.0" />
32
+ <meta name="robots" content="follow, index" />
33
+ <meta charSet="UTF-8" />
34
+ {siteConfig('SEO_GOOGLE_SITE_VERIFICATION') && (
35
+ <meta
36
+ name="google-site-verification"
37
+ content={siteConfig('SEO_GOOGLE_SITE_VERIFICATION')}
38
+ />
39
+ )}
40
+ {siteConfig('SEO_BAIDU_SITE_VERIFICATION') && (<meta name="baidu-site-verification" content={siteConfig('SEO_BAIDU_SITE_VERIFICATION')} />)}
41
+ <meta name="keywords" content={keywords} />
42
+ <meta name="description" content={description} />
43
+ <meta property="og:locale" content={lang} />
44
+ <meta property="og:title" content={title} />
45
+ <meta property="og:description" content={description} />
46
+ <meta property="og:url" content={url} />
47
+ <meta property="og:image" content={image} />
48
+ <meta property="og:site_name" content={siteConfig('TITLE')} />
49
+ <meta property="og:type" content={type} />
50
+ <meta name="twitter:card" content="summary_large_image" />
51
+ <meta name="twitter:description" content={description} />
52
+ <meta name="twitter:title" content={title} />
53
+
54
+ {siteConfig('COMMENT_WEBMENTION_ENABLE') && (
55
+ <>
56
+ <link rel="webmention" href={`https://webmention.io/${siteConfig('COMMENT_WEBMENTION_HOSTNAME')}/webmention`} />
57
+ <link rel="pingback" href={`https://webmention.io/${siteConfig('COMMENT_WEBMENTION_HOSTNAME')}/xmlrpc`} />
58
+ </>
59
+ )}
60
+
61
+ {siteConfig('COMMENT_WEBMENTION_ENABLE') && siteConfig('COMMENT_WEBMENTION_AUTH') !== '' && (
62
+ <link href={siteConfig('COMMENT_WEBMENTION_AUTH')} rel="me" />
63
+ )}
64
+
65
+ {JSON.parse(siteConfig('ANALYTICS_BUSUANZI_ENABLE')) && <meta name="referrer" content="no-referrer-when-downgrade" />}
66
+ {meta?.type === 'Post' && (
67
+ <>
68
+ <meta
69
+ property="article:published_time"
70
+ content={meta.publishDay}
71
+ />
72
+ <meta property="article:author" content={siteConfig('AUTHOR')} />
73
+ <meta property="article:section" content={category} />
74
+ <meta property="article:publisher" content={siteConfig('FACEBOOK_PAGE')} />
75
+ </>
76
+ )}
77
+ {children}
78
+ </Head>
79
+ )
80
+ }
81
+
82
+ /**
83
+ * 获取SEO信息
84
+ * @param {*} props
85
+ * @param {*} router
86
+ */
87
+ const getSEOMeta = (props, router, global) => {
88
+ const { locale } = global
89
+ const { post, tag, category, page } = props
90
+ const keyword = router?.query?.s
91
+
92
+ switch (router.route) {
93
+ case '/':
94
+ return {
95
+ title: `${siteConfig('TITLE')} | ${siteConfig('DESCRIPTION')}`,
96
+ description: siteConfig('DESCRIPTION'),
97
+ image: siteConfig('HOME_BANNER_IMAGE'),
98
+ slug: '',
99
+ type: 'website'
100
+ }
101
+ case '/archive':
102
+ return {
103
+ title: `${locale.NAV.ARCHIVE} | ${siteConfig('TITLE')}`,
104
+ description: siteConfig('DESCRIPTION'),
105
+ image: siteConfig('HOME_BANNER_IMAGE'),
106
+ slug: 'archive',
107
+ type: 'website'
108
+ }
109
+ case '/page/[page]':
110
+ return {
111
+ title: `${page} | Page | ${siteConfig('TITLE')}`,
112
+ description: siteConfig('DESCRIPTION'),
113
+ image: siteConfig('HOME_BANNER_IMAGE'),
114
+ slug: 'page/' + page,
115
+ type: 'website'
116
+ }
117
+ case '/category/[category]':
118
+ return {
119
+ title: `${category} | ${locale.COMMON.CATEGORY} | ${
120
+ siteConfig('TITLE') || ''
121
+ }`,
122
+ description: siteConfig('DESCRIPTION'),
123
+ slug: 'category/' + category,
124
+ image: siteConfig('HOME_BANNER_IMAGE'),
125
+ type: 'website'
126
+ }
127
+ case '/category/[category]/page/[page]':
128
+ return {
129
+ title: `${category} | ${locale.COMMON.CATEGORY} | ${
130
+ siteConfig('TITLE') || ''
131
+ }`,
132
+ description: siteConfig('DESCRIPTION'),
133
+ slug: 'category/' + category,
134
+ image: siteConfig('HOME_BANNER_IMAGE'),
135
+ type: 'website'
136
+ }
137
+ case '/tag/[tag]':
138
+ case '/tag/[tag]/page/[page]':
139
+ return {
140
+ title: `${tag} | ${locale.COMMON.TAGS} | ${siteConfig('TITLE')}`,
141
+ description: siteConfig('DESCRIPTION'),
142
+ image: siteConfig('HOME_BANNER_IMAGE'),
143
+ slug: 'tag/' + tag,
144
+ type: 'website'
145
+ }
146
+ case '/search':
147
+ return {
148
+ title: `${keyword || ''}${keyword ? ' | ' : ''}${locale.NAV.SEARCH} | ${siteConfig('TITLE')}`,
149
+ description: siteConfig('DESCRIPTION'),
150
+ image: siteConfig('HOME_BANNER_IMAGE'),
151
+ slug: 'search',
152
+ type: 'website'
153
+ }
154
+ case '/search/[keyword]':
155
+ case '/search/[keyword]/page/[page]':
156
+ return {
157
+ title: `${keyword || ''}${keyword ? ' | ' : ''}${locale.NAV.SEARCH} | ${siteConfig('TITLE')}`,
158
+ description: siteConfig('TITLE'),
159
+ image: siteConfig('HOME_BANNER_IMAGE'),
160
+ slug: 'search/' + (keyword || ''),
161
+ type: 'website'
162
+ }
163
+ case '/404':
164
+ return { title: `${siteConfig('TITLE')} | 页面找不到啦`, image: siteConfig('HOME_BANNER_IMAGE') }
165
+ case '/tag':
166
+ return {
167
+ title: `${locale.COMMON.TAGS} | ${siteConfig('TITLE')}`,
168
+ description: siteConfig('DESCRIPTION'),
169
+ image: siteConfig('HOME_BANNER_IMAGE'),
170
+ slug: 'tag',
171
+ type: 'website'
172
+ }
173
+ case '/category':
174
+ return {
175
+ title: `${locale.COMMON.CATEGORY} | ${siteConfig('TITLE')}`,
176
+ description: siteConfig('DESCRIPTION'),
177
+ image: siteConfig('HOME_BANNER_IMAGE'),
178
+ slug: 'category',
179
+ type: 'website'
180
+ }
181
+ default:
182
+ return {
183
+ title: post ? `${post?.title} | ${siteConfig('TITLE')}` : `${siteConfig('TITLE')} | loading`,
184
+ description: post?.summary,
185
+ type: post?.type,
186
+ slug: post?.slug,
187
+ image: post?.pageCoverThumbnail || siteConfig('HOME_BANNER_IMAGE'),
188
+ category: post?.category?.[0],
189
+ tags: post?.tags
190
+ }
191
+ }
192
+ }
193
+
194
+ export default GlobalHead
components/GlobalStyle.js ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable react/no-unknown-property */
2
+
3
+ import { siteConfig } from '@/lib/config'
4
+
5
+ /**
6
+ * 这里的css样式对全局生效
7
+ * 主题客制化css
8
+ * @returns
9
+ */
10
+ const GlobalStyle = () => {
11
+ // 从NotionConfig中读取样式
12
+ const GLOBAL_CSS = siteConfig('GLOBAL_CSS')
13
+ return (<style jsx global>{`
14
+
15
+ ${GLOBAL_CSS}
16
+
17
+ `}</style>)
18
+ }
19
+
20
+ export { GlobalStyle }
components/GoogleAdsense.js ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { siteConfig } from '@/lib/config'
2
+ import { loadExternalResource } from '@/lib/utils'
3
+ import { useRouter } from 'next/router'
4
+ import { useEffect } from 'react'
5
+
6
+ /**
7
+ * 初始化谷歌广告
8
+ * @returns
9
+ */
10
+ export default function GoogleAdsense() {
11
+ const initGoogleAdsense = () => {
12
+ loadExternalResource(`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${siteConfig('ADSENSE_GOOGLE_ID')}`, 'js').then(url => {
13
+ setTimeout(() => {
14
+ const ads = document.getElementsByClassName('adsbygoogle')
15
+ const adsbygoogle = window.adsbygoogle
16
+ if (ads.length > 0) {
17
+ for (let i = 0; i <= ads.length; i++) {
18
+ try {
19
+ adsbygoogle.push(ads[i])
20
+ } catch (e) {
21
+
22
+ }
23
+ }
24
+ }
25
+ }, 100)
26
+ })
27
+ }
28
+
29
+ const router = useRouter()
30
+ useEffect(() => {
31
+ // 延迟3秒加载
32
+ setTimeout(() => {
33
+ initGoogleAdsense()
34
+ }, 3000)
35
+ }, [router])
36
+
37
+ return null
38
+ }
39
+
40
+ /**
41
+ * 文章内嵌广告单元
42
+ * 请在GoogleAdsense后台配置创建对应广告,并且获取相应代码
43
+ * 修改下面广告单元中的 data-ad-slot data-ad-format data-ad-layout-key(如果有)
44
+ * 添加 可以在本地调试
45
+ */
46
+ const AdSlot = ({ type = 'show' }) => {
47
+ if (!siteConfig('ADSENSE_GOOGLE_ID')) {
48
+ return null
49
+ }
50
+ // 文章内嵌广告
51
+ if (type === 'in-article') {
52
+ return <ins className="adsbygoogle"
53
+ style={{ display: 'block', textAlign: 'center' }}
54
+ data-ad-layout="in-article"
55
+ data-ad-format="fluid"
56
+ data-adtest={siteConfig('ADSENSE_GOOGLE_TEST') ? 'on' : 'off'}
57
+ data-ad-client={siteConfig('ADSENSE_GOOGLE_ID')}
58
+ data-ad-slot={siteConfig('ADSENSE_GOOGLE_SLOT_IN_ARTICLE')}></ins>
59
+ }
60
+
61
+ // 信息流广告
62
+ if (type === 'flow') {
63
+ return <ins className="adsbygoogle"
64
+ data-ad-format="fluid"
65
+ data-ad-layout-key="-5j+cz+30-f7+bf"
66
+ style={{ display: 'block' }}
67
+ data-adtest={siteConfig('ADSENSE_GOOGLE_TEST') ? 'on' : 'off'}
68
+ data-ad-client={siteConfig('ADSENSE_GOOGLE_ID')}
69
+ data-ad-slot={siteConfig('ADSENSE_GOOGLE_SLOT_FLOW')}></ins>
70
+ }
71
+
72
+ // 原生广告
73
+ if (type === 'native') {
74
+ return <ins className="adsbygoogle"
75
+ style={{ display: 'block', textAlign: 'center' }}
76
+ data-ad-format="autorelaxed"
77
+ data-adtest={siteConfig('ADSENSE_GOOGLE_TEST') ? 'on' : 'off'}
78
+ data-ad-client={siteConfig('ADSENSE_GOOGLE_ID')}
79
+ data-ad-slot={siteConfig('ADSENSE_GOOGLE_SLOT_NATIVE')}></ins>
80
+ }
81
+
82
+ // 展示广告
83
+ return <ins className="adsbygoogle"
84
+ style={{ display: 'block' }}
85
+ data-ad-client={siteConfig('ADSENSE_GOOGLE_ID')}
86
+ data-adtest={siteConfig('ADSENSE_GOOGLE_TEST') ? 'on' : 'off'}
87
+ data-ad-slot={siteConfig('ADSENSE_GOOGLE_SLOT_AUTO')}
88
+ data-ad-format="auto"
89
+ data-full-width-responsive="true"></ins>
90
+ }
91
+
92
+ export { AdSlot }
components/Gtag.js ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect } from 'react'
2
+ import { useRouter } from 'next/router'
3
+ import * as gtag from '@/lib/gtag'
4
+
5
+ const Gtag = () => {
6
+ const router = useRouter()
7
+ useEffect(() => {
8
+ const gtagRouteChange = url => {
9
+ gtag.pageview(url)
10
+ }
11
+ router.events.on('routeChangeComplete', gtagRouteChange)
12
+ return () => {
13
+ router.events.off('routeChangeComplete', gtagRouteChange)
14
+ }
15
+ }, [router.events])
16
+ return null
17
+ }
18
+ export default Gtag
components/HeroIcons.js ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @see https://heroicons.com/
3
+ * @returns
4
+ */
5
+
6
+ export const Moon = () => {
7
+ return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
8
+ <path strokeLinecap="round" strokeLinejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
9
+ </svg>
10
+ }
11
+
12
+ export const Sun = () => {
13
+ return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
14
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
15
+ </svg>
16
+ }
17
+
18
+ export const Home = ({ className }) => {
19
+ return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
20
+ <path strokeLinecap="round" strokeLinejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
21
+ </svg>
22
+ }
23
+
24
+ export const User = ({ className }) => {
25
+ return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
26
+ <path strokeLinecap="round" strokeLinejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
27
+ </svg>
28
+ }
29
+
30
+ export const ArrowPath = ({ className }) => {
31
+ return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
32
+ <path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
33
+ </svg>
34
+ }
35
+
36
+ export const ChevronLeft = ({ className }) => {
37
+ return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
38
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
39
+ </svg>
40
+ }
41
+
42
+ export const ChevronRight = ({ className }) => {
43
+ return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
44
+ <path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
45
+ </svg>
46
+ }
47
+
48
+ export const ChevronDoubleLeft = ({ className }) => {
49
+ return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
50
+ <path strokeLinecap="round" strokeLinejoin="round" d="M18.75 19.5l-7.5-7.5 7.5-7.5m-6 15L5.25 12l7.5-7.5" />
51
+ </svg>
52
+ }
53
+
54
+ export const ChevronDoubleRight = ({ className }) => {
55
+ return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
56
+ <path strokeLinecap="round" strokeLinejoin="round" d="M11.25 4.5l7.5 7.5-7.5 7.5m-6-15l7.5 7.5-7.5 7.5" />
57
+ </svg>
58
+ }
59
+
60
+ export const InformationCircle = ({ className }) => {
61
+ return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
62
+ <path strokeLinecap="round" strokeLinejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
63
+ </svg>
64
+ }
65
+
66
+ export const HashTag = ({ className }) => {
67
+ return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
68
+ <path strokeLinecap="round" strokeLinejoin="round" d="M5.25 8.25h15m-16.5 7.5h15m-1.8-13.5l-3.9 19.5m-2.1-19.5l-3.9 19.5" />
69
+ </svg>
70
+ }
71
+
72
+ export const GlobeAlt = ({ className }) => {
73
+ return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
74
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418" />
75
+ </svg>
76
+ }
77
+
78
+ export const ArrowRightCircle = ({ className }) => {
79
+ return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
80
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12.75 15l3-3m0 0l-3-3m3 3h-7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
81
+ </svg>
82
+ }
83
+
84
+ export const PlusSmall = ({ className }) => {
85
+ return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
86
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 6v12m6-6H6" />
87
+ </svg>
88
+ }
89
+
90
+ export const ArrowSmallRight = ({ className }) => {
91
+ return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
92
+ <path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75" />
93
+ </svg>
94
+ }
95
+
96
+ export const ArrowSmallUp = ({ className }) => {
97
+ return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
98
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 19.5v-15m0 0l-6.75 6.75M12 4.5l6.75 6.75" />
99
+ </svg>
100
+ }
components/KatexReact.js ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import KaTeX from 'katex'
2
+ import { memo, useEffect, useState } from 'react'
3
+
4
+ /**
5
+ * 数学公式
6
+ * @param {*} param0
7
+ * @returns
8
+ */
9
+ const TeX = ({
10
+ children,
11
+ math,
12
+ block,
13
+ errorColor,
14
+ renderError,
15
+ settings,
16
+ as: asComponent,
17
+ ...props
18
+ }) => {
19
+ const Component = asComponent || (block ? 'div' : 'span')
20
+ const content = (children ?? math)
21
+ const [state, setState] = useState({ innerHtml: '' })
22
+
23
+ useEffect(() => {
24
+ try {
25
+ const innerHtml = KaTeX.renderToString(content, {
26
+ displayMode: true,
27
+ errorColor,
28
+ throwOnError: !!renderError,
29
+ ...settings
30
+ })
31
+
32
+ setState({ innerHtml })
33
+ } catch (error) {
34
+ if (error instanceof KaTeX.ParseError || error instanceof TypeError) {
35
+ if (renderError) {
36
+ setState({ errorElement: renderError(error) })
37
+ } else {
38
+ setState({ innerHtml: error.message })
39
+ }
40
+ } else {
41
+ throw error
42
+ }
43
+ }
44
+ }, [block, content, errorColor, renderError, settings])
45
+
46
+ if ('errorElement' in state) {
47
+ return state.errorElement
48
+ }
49
+
50
+ return (
51
+ <Component
52
+ {...props}
53
+ dangerouslySetInnerHTML={{ __html: state.innerHtml }}
54
+ />
55
+ )
56
+ }
57
+
58
+ export default memo(TeX)
components/LA51.js ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { siteConfig } from '@/lib/config'
2
+ import { useEffect } from 'react'
3
+
4
+ /**
5
+ * 51LA统计
6
+ */
7
+ export default function LA51() {
8
+ const ANALYTICS_51LA_ID = siteConfig('ANALYTICS_51LA_ID')
9
+ const ANALYTICS_51LA_CK = siteConfig('ANALYTICS_51LA_CK')
10
+ useEffect(() => {
11
+ const LA = window.LA
12
+ if (LA) {
13
+ LA.init({ id: `${ANALYTICS_51LA_ID}`, ck: `${ANALYTICS_51LA_CK}`, hashMode: true, autoTrack: true })
14
+ }
15
+ }, [])
16
+
17
+ return <></>
18
+ }
components/LazyImage.js ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { siteConfig } from '@/lib/config'
2
+ import Head from 'next/head'
3
+ import React, { useEffect, useRef, useState } from 'react'
4
+
5
+ /**
6
+ * 图片懒加载
7
+ * @param {*} param0
8
+ * @returns
9
+ */
10
+ export default function LazyImage({
11
+ priority,
12
+ id,
13
+ src,
14
+ alt,
15
+ placeholderSrc,
16
+ className,
17
+ width,
18
+ height,
19
+ title,
20
+ onLoad,
21
+ style
22
+ }) {
23
+ const imageRef = useRef(null)
24
+ const [imageLoaded, setImageLoaded] = useState(false)
25
+ if (!placeholderSrc) {
26
+ placeholderSrc = siteConfig('IMG_LAZY_LOAD_PLACEHOLDER')
27
+ }
28
+
29
+ const handleImageLoad = () => {
30
+ setImageLoaded(true)
31
+ if (typeof onLoad === 'function') {
32
+ onLoad() // 触发传递的onLoad回调函数
33
+ }
34
+ }
35
+
36
+ useEffect(() => {
37
+ const observer = new IntersectionObserver(
38
+ (entries) => {
39
+ entries.forEach((entry) => {
40
+ if (entry.isIntersecting) {
41
+ const lazyImage = entry.target
42
+ lazyImage.src = src
43
+ observer.unobserve(lazyImage)
44
+ }
45
+ })
46
+ },
47
+ { rootMargin: '50px 0px' } // Adjust the rootMargin as needed to trigger the loading earlier or later
48
+ )
49
+
50
+ if (imageRef.current) {
51
+ observer.observe(imageRef.current)
52
+ }
53
+
54
+ return () => {
55
+ if (imageRef.current) {
56
+ observer.unobserve(imageRef.current)
57
+ }
58
+ }
59
+ }, [src])
60
+
61
+ // 动态添加width、height和className属性,仅在它们为有效值时添加
62
+ const imgProps = {
63
+ ref: imageRef,
64
+ src: imageLoaded ? src : placeholderSrc,
65
+ alt: alt,
66
+ onLoad: handleImageLoad
67
+ }
68
+
69
+ if (id) {
70
+ imgProps.id = id
71
+ }
72
+
73
+ if (title) {
74
+ imgProps.title = title
75
+ }
76
+
77
+ if (width && width !== 'auto') {
78
+ imgProps.width = width
79
+ }
80
+
81
+ if (height && height !== 'auto') {
82
+ imgProps.height = height
83
+ }
84
+ if (className) {
85
+ imgProps.className = className
86
+ }
87
+ if (style) {
88
+ imgProps.style = style
89
+ }
90
+ return (<>
91
+ {/* eslint-disable-next-line @next/next/no-img-element */}
92
+ <img {...imgProps} />
93
+ {/* 预加载 */}
94
+ {priority && <Head>
95
+ <link rel='preload' as='image' src={src} />
96
+ </Head>}
97
+ </>)
98
+ }
components/Live2D.js ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable no-undef */
2
+ import { siteConfig } from '@/lib/config'
3
+ import { useGlobal } from '@/lib/global'
4
+ import { isMobile, loadExternalResource } from '@/lib/utils'
5
+ import { useEffect } from 'react'
6
+
7
+ /**
8
+ * 网页动画
9
+ * @returns
10
+ */
11
+ export default function Live2D() {
12
+ const { theme, switchTheme } = useGlobal()
13
+ const showPet = JSON.parse(siteConfig('WIDGET_PET'))
14
+ const petLink = siteConfig('WIDGET_PET_LINK')
15
+
16
+ useEffect(() => {
17
+ if (showPet && !isMobile()) {
18
+ Promise.all([
19
+ loadExternalResource('https://cdn.jsdelivr.net/gh/stevenjoezhang/live2d-widget@latest/live2d.min.js', 'js')
20
+ ]).then((e) => {
21
+ if (typeof window?.loadlive2d !== 'undefined') {
22
+ // https://github.com/xiazeyu/live2d-widget-models
23
+ try {
24
+ loadlive2d('live2d', petLink)
25
+ } catch (error) {
26
+ console.error('读取PET模型', error)
27
+ }
28
+ }
29
+ })
30
+ }
31
+ }, [theme])
32
+
33
+ function handleClick() {
34
+ if (JSON.parse(siteConfig('WIDGET_PET_SWITCH_THEME'))) {
35
+ switchTheme()
36
+ }
37
+ }
38
+
39
+ if (!showPet) {
40
+ return <></>
41
+ }
42
+
43
+ return <canvas id="live2d" width="280" height="250" onClick={handleClick}
44
+ className="cursor-grab"
45
+ onMouseDown={(e) => e.target.classList.add('cursor-grabbing')}
46
+ onMouseUp={(e) => e.target.classList.remove('cursor-grabbing')}
47
+ />
48
+ }
components/Loading.js ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ /**
3
+ * 异步文件加载时的占位符
4
+ * @returns
5
+ */
6
+ const Loading = (props) => {
7
+ return <div id="loading-container" className="-z-10 w-screen h-screen flex justify-center items-center fixed left-0 top-0">
8
+ <div id="loading-wrapper">
9
+ <div className="loading"> <i className="fas fa-spinner animate-spin text-3xl "/></div>
10
+ </div>
11
+ </div>
12
+ }
13
+ export default Loading
components/LoadingProgress.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useRouter } from 'next/router'
2
+ import NProgress from 'nprogress'
3
+ import { useEffect } from 'react'
4
+
5
+ /**
6
+ * 出现页面加载进度条
7
+ */
8
+ export default function LoadingProgress() {
9
+ const router = useRouter()
10
+ // 加载进度条
11
+ useEffect(() => {
12
+ const handleStart = (url) => {
13
+ NProgress.start()
14
+ }
15
+
16
+ const handleStop = () => {
17
+ NProgress.done()
18
+ }
19
+
20
+ router.events.on('routeChangeStart', handleStart)
21
+ router.events.on('routeChangeError', handleStop)
22
+ router.events.on('routeChangeComplete', handleStop)
23
+ return () => {
24
+ router.events.off('routeChangeStart', handleStart)
25
+ router.events.off('routeChangeComplete', handleStop)
26
+ router.events.off('routeChangeError', handleStop)
27
+ }
28
+ }, [router])
29
+ }
components/Mark.js ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { loadExternalResource } from '@/lib/utils'
2
+
3
+ /**
4
+ * 将搜索结果的关键词高亮
5
+ */
6
+ export default async function replaceSearchResult({ doms, search, target }) {
7
+ if (!doms || !search || !target) {
8
+ return
9
+ }
10
+
11
+ try {
12
+ await loadExternalResource('https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/mark.min.js', 'js')
13
+ const Mark = window.Mark
14
+ if (doms instanceof HTMLCollection) {
15
+ for (const container of doms) {
16
+ const re = new RegExp(search, 'gim')
17
+ const instance = new Mark(container)
18
+ instance.markRegExp(re, target)
19
+ }
20
+ } else {
21
+ const re = new RegExp(search, 'gim')
22
+ const instance = new Mark(doms)
23
+ instance.markRegExp(re, target)
24
+ }
25
+ } catch (error) {
26
+ console.error('markjs 加载失败', error)
27
+ }
28
+ }
components/Nest.js ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable */
2
+ import { useEffect } from 'react'
3
+ const id = 'canvasNestCreated'
4
+ const Nest = () => {
5
+ const destroyNest = ()=>{
6
+ const nest = document.getElementById(id)
7
+ if(nest && nest.parentNode){
8
+ nest.parentNode.removeChild(nest)
9
+ }
10
+ }
11
+
12
+ useEffect(() => {
13
+ createNest()
14
+ return () => destroyNest()
15
+ }, [])
16
+ return <></>
17
+ }
18
+
19
+ export default Nest
20
+
21
+ /**
22
+ * 创建连接点
23
+ * @param config
24
+ */
25
+ function createNest() {
26
+ const e = document.getElementById('__next')
27
+ if(!e) return
28
+ function n(e, n, t) {
29
+ return e.getAttribute(n) || t
30
+ }
31
+ function t() {
32
+ ;(u = i.width =
33
+ window.innerWidth ||
34
+ document.documentElement.clientWidth ||
35
+ document.body.clientWidth),
36
+ (d = i.height =
37
+ window.innerHeight ||
38
+ document.documentElement.clientHeight ||
39
+ document.body.clientHeight)
40
+ }
41
+ function o() {
42
+ c.clearRect(0, 0, u, d)
43
+ const e = [s].concat(x)
44
+ let n, t, i, l, r, w
45
+ x.forEach(function (o) {
46
+ for (
47
+ o.x += o.xa,
48
+ o.y += o.ya,
49
+ o.xa *= o.x > u || o.x < 0 ? -1 : 1,
50
+ o.ya *= o.y > d || o.y < 0 ? -1 : 1,
51
+ c.fillRect(o.x - 0.5, o.y - 0.5, 1, 1),
52
+ t = 0;
53
+ t < e.length;
54
+ t++
55
+ )
56
+ (n = e[t]),
57
+ o !== n &&
58
+ null !== n.x &&
59
+ null !== n.y &&
60
+ ((l = o.x - n.x),
61
+ (r = o.y - n.y),
62
+ (w = l * l + r * r),
63
+ w < n.max &&
64
+ (n === s &&
65
+ w >= n.max / 2 &&
66
+ ((o.x -= 0.03 * l), (o.y -= 0.03 * r)),
67
+ (i = (n.max - w) / n.max),
68
+ c.beginPath(),
69
+ (c.lineWidth = i / 2),
70
+ (c.strokeStyle = 'rgba(' + a.c + ',' + (i + 0.2) + ')'),
71
+ c.moveTo(o.x, o.y),
72
+ c.lineTo(n.x, n.y),
73
+ c.stroke()))
74
+ e.splice(e.indexOf(o), 1)
75
+ }),
76
+ m(o)
77
+ }
78
+ var i = document.createElement('canvas')
79
+ i.id = id
80
+ var a = (function () {
81
+ const t = e
82
+ return {
83
+ z: n(t, 'zIndex', 0),
84
+ o: n(t, 'opacity', 0.7),
85
+ c: n(t, 'color', '0,0,0'),
86
+ n: n(t, 'count', 99)
87
+ }
88
+ })(),
89
+ c = i.getContext('2d')
90
+ let u, d
91
+ var m =
92
+ window.requestAnimationFrame ||
93
+ window.webkitRequestAnimationFrame ||
94
+ window.mozRequestAnimationFrame ||
95
+ window.oRequestAnimationFrame ||
96
+ window.msRequestAnimationFrame ||
97
+ function (e) {
98
+ window.setTimeout(e, 1e3 / 45)
99
+ }
100
+ const l = Math.random
101
+ var r,
102
+ s = { x: null, y: null, max: 2e4 }
103
+ ;(i.style.cssText =
104
+ 'position:fixed;top:0;left:0;pointer-events:none;z-index:' + a.z + ';opacity:' + a.o),
105
+ (r = 'body'), e.appendChild(i),
106
+ t(),
107
+ (window.onresize = t),
108
+ (window.onmousemove = function (e) {
109
+ ;(e = e || window.event), (s.x = e.clientX), (s.y = e.clientY)
110
+ }),
111
+ (window.onmouseout = function () {
112
+ ;(s.x = null), (s.y = null)
113
+ })
114
+ for (var x = [], w = 0; a.n > w; w++) {
115
+ const e = l() * u,
116
+ n = l() * d,
117
+ t = 2 * l() - 1,
118
+ o = 2 * l() - 1
119
+ x.push({ x: e, y: n, xa: t, ya: o, max: 6e3 })
120
+ }
121
+ setTimeout(function () {
122
+ o()
123
+ }, 100)
124
+ }
components/NotionIcon.js ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import LazyImage from './LazyImage'
2
+
3
+ /**
4
+ * notion的图标icon
5
+ * 可能是emoji 可能是 svg 也可能是 图片
6
+ * @returns
7
+ */
8
+ const NotionIcon = ({ icon }) => {
9
+ if (!icon) {
10
+ return <></>
11
+ }
12
+
13
+ if (icon.startsWith('http') || icon.startsWith('data:')) {
14
+ return <LazyImage src={icon} className='w-8 h-8 my-auto inline mr-1'/>
15
+ }
16
+
17
+ return <span className='mr-1'>{icon}</span>
18
+ }
19
+
20
+ export default NotionIcon