Upload 699 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .dockerignore +1 -0
- .env.local +174 -0
- .eslintrc.js +38 -0
- .gitattributes +1 -0
- .gitignore +52 -0
- .prettierrc.json +6 -0
- CONTRIBUTING.md +52 -0
- Dockerfile +27 -0
- blog.config.js +432 -0
- components/AOSAnimation.js +12 -0
- components/Ackee.js +85 -0
- components/AdBlockDetect.js +40 -0
- components/AlgoliaSearchModal.js +230 -0
- components/Artalk.js +37 -0
- components/Busuanzi.js +26 -0
- components/ChatBase.js +19 -0
- components/Collapse.js +96 -0
- components/Comment.js +138 -0
- components/CusdisComponent.js +37 -0
- components/CustomContextMenu.js +204 -0
- components/DarkModeButton.js +27 -0
- components/DebugPanel.js +128 -0
- components/DifyChatbot.js +32 -0
- components/DisableCopy.js +21 -0
- components/Draggable.js +150 -0
- components/Equation.js +29 -0
- components/ExternalPlugins.js +290 -0
- components/ExternalScript.js +29 -0
- components/FacebookMessenger.js +282 -0
- components/FacebookPage.js +34 -0
- components/Fireworks.js +216 -0
- components/FlipCard.js +56 -0
- components/FlutteringRibbon.js +322 -0
- components/FullScreenButton.js +48 -0
- components/Giscus.js +33 -0
- components/Gitalk.js +49 -0
- components/GlobalHead.js +194 -0
- components/GlobalStyle.js +20 -0
- components/GoogleAdsense.js +92 -0
- components/Gtag.js +18 -0
- components/HeroIcons.js +100 -0
- components/KatexReact.js +58 -0
- components/LA51.js +18 -0
- components/LazyImage.js +98 -0
- components/Live2D.js +48 -0
- components/Loading.js +13 -0
- components/LoadingProgress.js +29 -0
- components/Mark.js +28 -0
- components/Nest.js +124 -0
- 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"> {locale.COMMON.DEBUG_CLOSE}</i>
|
63 |
+
: <i className="fas fa-tools"> {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
|