-
-
+
+
+
+
+
+
+
+ {/* 切换主题加载时的全屏遮罩 */}
+
+
+
>
)
diff --git a/components/WordCount.js b/components/WordCount.js
new file mode 100644
index 00000000..315c58d3
--- /dev/null
+++ b/components/WordCount.js
@@ -0,0 +1,67 @@
+import { useGlobal } from '@/lib/global'
+import { useEffect } from 'react'
+
+/**
+ * 字数统计
+ * @returns
+ */
+export default function WordCount() {
+ const { locale } = useGlobal()
+ useEffect(() => {
+ countWords()
+ })
+
+ return
+
+
+ 0
+
+
+
+
+ 0 {locale.COMMON.MINUTE}
+
+
+}
+
+/**
+ * 更新字数统计和阅读时间
+ */
+function countWords() {
+ const articleText = deleteHtmlTag(document.getElementById('notion-article')?.innerHTML)
+ const wordCount = fnGetCpmisWords(articleText)
+ // 阅读速度 300-500每分钟
+ document.getElementById('wordCount').innerHTML = wordCount
+ document.getElementById('readTime').innerHTML = Math.floor(wordCount / 400) + 1
+ const wordCountWrapper = document.getElementById('wordCountWrapper')
+ wordCountWrapper.classList.remove('hidden')
+}
+
+// 去除html标签
+function deleteHtmlTag(str) {
+ if (!str) {
+ return ''
+ }
+ str = str.replace(/<[^>]+>|&[^>]+;/g, '').trim()// 去掉所有的html标签和 之类的特殊符合
+ return str
+}
+
+// 用word方式计算正文字数
+function fnGetCpmisWords(str) {
+ if (!str) {
+ return 0
+ }
+ let sLen = 0
+ try {
+ // eslint-disable-next-line no-irregular-whitespace
+ str = str.replace(/(\r\n+|\s+| +)/g, '龘')
+ // eslint-disable-next-line no-control-regex
+ str = str.replace(/[\x00-\xff]/g, 'm')
+ str = str.replace(/m+/g, '*')
+ str = str.replace(/龘+/g, '')
+ sLen = str.length
+ } catch (e) {
+
+ }
+ return sLen
+}
diff --git a/lib/global.js b/lib/global.js
index 95c9614a..9cafd304 100644
--- a/lib/global.js
+++ b/lib/global.js
@@ -21,24 +21,11 @@ export function GlobalContextProvider({ children }) {
const [theme, setTheme] = useState(BLOG.THEME) // 默认博客主题
const [isDarkMode, updateDarkMode] = useState(BLOG.APPEARANCE === 'dark') // 默认深色模式
const [onLoading, setOnLoading] = useState(false) // 抓取文章数据
- // const [onReading, setOnReading] = useState(false) // 网页资源加载
useEffect(() => {
initLocale(lang, locale, updateLang, updateLocale)
initDarkMode(isDarkMode, updateDarkMode)
initTheme()
- if (isBrowser()) {
- // 监听用户刷新页面
- const handleBeforeUnload = (event) => {
- // setOnReading(true)
- }
- // 监听页面元素加载完
- // setOnReading(false)
- window.addEventListener('beforeunload', handleBeforeUnload)
- return () => {
- window.removeEventListener('beforeunload', handleBeforeUnload)
- }
- }
}, [])
useEffect(() => {
@@ -72,7 +59,8 @@ export function GlobalContextProvider({ children }) {
const currentIndex = THEMES.indexOf(theme)
const newIndex = currentIndex < THEMES.length - 1 ? currentIndex + 1 : 0
const newTheme = THEMES[newIndex]
- const query = { ...router.query, theme: newTheme }
+ const query = router.query
+ query.theme = newTheme
router.push({ pathname: router.pathname, query })
return newTheme
}
diff --git a/lib/lang/en-US.js b/lib/lang/en-US.js
index de277434..51e38115 100644
--- a/lib/lang/en-US.js
+++ b/lib/lang/en-US.js
@@ -1,7 +1,16 @@
export default {
LOCALE: 'en-US',
+ MENU: {
+ WALK_AROUND: 'Walk Around',
+ CATEGORY: 'Category',
+ TAGS: 'Tags',
+ COPY_URL: 'Copy URL',
+ DARK_MODE: 'Dark Mode',
+ LIGHT_MODE: 'Light Mode',
+ THEME_SWITCH: 'Theme Switch'
+ },
NAV: {
- INDEX: 'Blog',
+ INDEX: 'Home',
RSS: 'RSS',
SEARCH: 'Search',
ABOUT: 'About',
@@ -35,6 +44,7 @@ export default {
SUBMIT: 'Submit',
POST_TIME: 'Post on',
LAST_EDITED_TIME: 'Last edited',
+ COMMENTS: 'Comments',
RECENT_COMMENTS: 'Recent Comments',
DEBUG_OPEN: 'Debug',
DEBUG_CLOSE: 'Close',
diff --git a/lib/lang/zh-CN.js b/lib/lang/zh-CN.js
index 559afb91..91a4600a 100644
--- a/lib/lang/zh-CN.js
+++ b/lib/lang/zh-CN.js
@@ -1,5 +1,14 @@
export default {
LOCALE: 'zh-CN',
+ MENU: {
+ WALK_AROUND: '随便逛逛',
+ CATEGORY: '博客分类',
+ TAGS: '博客标签',
+ COPY_URL: '复制地址',
+ DARK_MODE: '深色模式',
+ LIGHT_MODE: '浅色模式',
+ THEME_SWITCH: '主题切换'
+ },
NAV: {
INDEX: '首页',
RSS: '订阅',
@@ -12,7 +21,7 @@ export default {
COMMON: {
MORE: '更多',
NO_MORE: '没有更多了',
- LATEST_POSTS: '最新文章',
+ LATEST_POSTS: '最新发布',
TAGS: '标签',
NO_TAG: 'NoTag',
CATEGORY: '分类',
@@ -37,6 +46,7 @@ export default {
SUBMIT: '提交',
POST_TIME: '发布于',
LAST_EDITED_TIME: '最后更新',
+ COMMENTS: '评论',
RECENT_COMMENTS: '最新评论',
DEBUG_OPEN: '开启调试',
DEBUG_CLOSE: '关闭调试',
@@ -47,8 +57,8 @@ export default {
WORD_COUNT: '字数'
},
PAGINATION: {
- PREV: '上一页',
- NEXT: '下一页'
+ PREV: '上页',
+ NEXT: '下页'
},
SEARCH: {
ARTICLES: '搜索文章',
diff --git a/lib/notion/getNotionData.js b/lib/notion/getNotionData.js
index 298795f6..393e3c63 100644
--- a/lib/notion/getNotionData.js
+++ b/lib/notion/getNotionData.js
@@ -309,7 +309,7 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
const customNav = getCustomNav({ allPages: collectionData.filter(post => post?.type === 'Page' && post.status === 'Published') })
// 新的菜单
const customMenu = await getCustomMenu({ collectionData })
- const latestPosts = getLatestPosts({ allPages, from, latestPostCount: 5 })
+ const latestPosts = getLatestPosts({ allPages, from, latestPostCount: 6 })
const allNavPages = getNavPages({ allPages })
return {
diff --git a/lib/notion/getPageProperties.js b/lib/notion/getPageProperties.js
index ab6e5431..dea5c2b1 100644
--- a/lib/notion/getPageProperties.js
+++ b/lib/notion/getPageProperties.js
@@ -100,7 +100,6 @@ export default async function getPageProperties(id, block, schema, authToken, ta
properties.to = properties.slug ?? null
properties.name = properties.title ?? ''
}
- properties.password = properties.password ? md5(properties.slug + properties.password) : ''
// 开启伪静态路径
if (JSON.parse(BLOG.PSEUDO_STATIC)) {
@@ -108,6 +107,7 @@ export default async function getPageProperties(id, block, schema, authToken, ta
properties.slug += '.html'
}
}
+ properties.password = properties.password ? md5(properties.slug + properties.password) : ''
return properties
}
diff --git a/lib/sitemap.xml.js b/lib/sitemap.xml.js
index bb5239f1..e2725f23 100644
--- a/lib/sitemap.xml.js
+++ b/lib/sitemap.xml.js
@@ -21,9 +21,11 @@ export async function generateSitemapXml({ allPages }) {
changefreq: 'daily'
}]
+ // 循环页面生成
allPages?.forEach(post => {
+ const slugWithoutLeadingSlash = post?.slug?.startsWith('/') ? post?.slug?.slice(1) : post.slug
urls.push({
- loc: `${BLOG.LINK}/${post.slug}`,
+ loc: `${BLOG.LINK}/${slugWithoutLeadingSlash}`,
lastmod: new Date(post?.publishTime).toISOString().split('T')[0],
changefreq: 'daily'
})
diff --git a/next.config.js b/next.config.js
index b97ad538..27e33e0f 100644
--- a/next.config.js
+++ b/next.config.js
@@ -98,6 +98,12 @@ module.exports = withBundleAnalyzer({
experimental: {
scrollRestoration: true
},
+ exportPathMap: async function (defaultPathMap, { dev, dir, outDir, distDir, buildId }) {
+ // 导出时 忽略/pages/sitemap.xml.js , 否则报错getServerSideProps
+ const pages = { ...defaultPathMap }
+ delete pages['/sitemap.xml']
+ return pages
+ },
publicRuntimeConfig: { // 这里的配置既可以服务端获取到,也可以在浏览器端获取到
NODE_ENV_API: process.env.NODE_ENV_API || 'prod',
THEMES: themes
diff --git a/package.json b/package.json
index 6203ee96..ca406763 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "notion-next",
- "version": "4.0.0",
+ "version": "4.0.5",
"homepage": "https://github.com/tangly1024/NotionNext.git",
"license": "MIT",
"repository": {
@@ -26,7 +26,6 @@
"@next/bundle-analyzer": "^12.1.1",
"@vercel/analytics": "^1.0.0",
"algoliasearch": "^4.18.0",
- "animate.css": "^4.1.1",
"animejs": "^3.2.1",
"aos": "^3.0.0-beta.6",
"axios": ">=0.21.1",
@@ -36,7 +35,6 @@
"js-md5": "^0.7.3",
"localStorage": "^1.0.4",
"lodash.throttle": "^4.1.1",
- "mark.js": "^8.11.1",
"memory-cache": "^0.2.0",
"mongodb": "^4.6.0",
"next": "13.3.1",
diff --git a/pages/[prefix]/[slug].js b/pages/[prefix]/[slug].js
new file mode 100644
index 00000000..65d025c1
--- /dev/null
+++ b/pages/[prefix]/[slug].js
@@ -0,0 +1,122 @@
+import BLOG from '@/blog.config'
+import { getPostBlocks } from '@/lib/notion'
+import { getGlobalData } from '@/lib/notion/getNotionData'
+import { idToUuid } from 'notion-utils'
+import { getNotion } from '@/lib/notion/getNotion'
+import Slug from '.'
+
+/**
+ * 根据notion的slug访问页面
+ * @param {*} props
+ * @returns
+ */
+const PrefixSlug = props => {
+ return
+}
+
+export async function getStaticPaths() {
+ if (!BLOG.isProd) {
+ return {
+ paths: [],
+ fallback: true
+ }
+ }
+
+ const from = 'slug-paths'
+ const { allPages } = await getGlobalData({ from })
+ return {
+ paths: allPages?.filter(row => row.slug.indexOf('/') > 0).map(row => ({ params: { prefix: row.slug.split('/')[0], slug: row.slug.split('/')[1] } })),
+ fallback: true
+ }
+}
+
+export async function getStaticProps({ params: { prefix, slug } }) {
+ let fullSlug = prefix + '/' + slug
+ if (JSON.parse(BLOG.PSEUDO_STATIC)) {
+ if (!fullSlug.endsWith('.html')) {
+ fullSlug += '.html'
+ }
+ }
+ const from = `slug-props-${fullSlug}`
+ const props = await getGlobalData({ from })
+ // 在列表内查找文章
+ props.post = props?.allPages?.find((p) => {
+ return p.slug === fullSlug || p.id === idToUuid(fullSlug)
+ })
+
+ // 处理非列表内文章的内信息
+ if (!props?.post) {
+ const pageId = slug.slice(-1)[0]
+ if (pageId.length >= 32) {
+ const post = await getNotion(pageId)
+ props.post = post
+ }
+ }
+
+ // 无法获取文章
+ if (!props?.post) {
+ props.post = null
+ return { props, revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND) }
+ }
+
+ // 文章内容加载
+ if (!props?.posts?.blockMap) {
+ props.post.blockMap = await getPostBlocks(props.post.id, from)
+ }
+
+ // 推荐关联文章处理
+ const allPosts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published')
+ if (allPosts && allPosts.length > 0) {
+ const index = allPosts.indexOf(props.post)
+ props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
+ props.next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0]
+ props.recommendPosts = getRecommendPost(props.post, allPosts, BLOG.POST_RECOMMEND_COUNT)
+ } else {
+ props.prev = null
+ props.next = null
+ props.recommendPosts = []
+ }
+
+ delete props.allPages
+ return {
+ props,
+ revalidate: parseInt(BLOG.NEXT_REVALIDATE_SECOND)
+ }
+}
+
+/**
+ * 获取文章的关联推荐文章列表,目前根据标签关联性筛选
+ * @param post
+ * @param {*} allPosts
+ * @param {*} count
+ * @returns
+ */
+function getRecommendPost(post, allPosts, count = 6) {
+ let recommendPosts = []
+ const postIds = []
+ const currentTags = post?.tags || []
+ for (let i = 0; i < allPosts.length; i++) {
+ const p = allPosts[i]
+ if (p.id === post.id || p.type.indexOf('Post') < 0) {
+ continue
+ }
+
+ for (let j = 0; j < currentTags.length; j++) {
+ const t = currentTags[j]
+ if (postIds.indexOf(p.id) > -1) {
+ continue
+ }
+ if (p.tags && p.tags.indexOf(t) > -1) {
+ recommendPosts.push(p)
+ postIds.push(p.id)
+ }
+ }
+ }
+
+ if (recommendPosts.length > count) {
+ recommendPosts = recommendPosts.slice(0, count)
+ }
+ return recommendPosts
+}
+
+export default PrefixSlug
diff --git a/pages/[...slug].js b/pages/[prefix]/index.js
similarity index 94%
rename from pages/[...slug].js
rename to pages/[prefix]/index.js
index 6db02d63..da351672 100644
--- a/pages/[...slug].js
+++ b/pages/[prefix]/index.js
@@ -90,13 +90,14 @@ export async function getStaticPaths() {
const from = 'slug-paths'
const { allPages } = await getGlobalData({ from })
return {
- paths: allPages?.map(row => ({ params: { slug: [row.slug] } })),
+ paths: allPages?.filter(row => row.slug.indexOf('/') < 0).map(row => ({ params: { prefix: row.slug } })),
fallback: true
}
}
-export async function getStaticProps({ params: { slug } }) {
- let fullSlug = slug.join('/')
+export async function getStaticProps({ params: { prefix } }) {
+ // let fullSlug = slug.join('/')
+ let fullSlug = prefix
if (JSON.parse(BLOG.PSEUDO_STATIC)) {
if (!fullSlug.endsWith('.html')) {
fullSlug += '.html'
@@ -111,7 +112,7 @@ export async function getStaticProps({ params: { slug } }) {
// 处理非列表内文章的内信息
if (!props?.post) {
- const pageId = slug.slice(-1)[0]
+ const pageId = prefix.slice(-1)[0]
if (pageId.length >= 32) {
const post = await getNotion(pageId)
props.post = post
diff --git a/pages/_app.js b/pages/_app.js
index e0d11382..a6a11399 100644
--- a/pages/_app.js
+++ b/pages/_app.js
@@ -1,6 +1,6 @@
import { useEffect } from 'react'
-// import 'animate.css'
+import '@/styles/animate.css' // @see https://animate.style/
import '@/styles/globals.css'
import '@/styles/nprogress.css'
import '@/styles/utility-patterns.css'
@@ -28,9 +28,9 @@ const MyApp = ({ Component, pageProps }) => {
return (
-
-
+
+
)
}
diff --git a/pages/_document.js b/pages/_document.js
index c053a632..9c7bd4c8 100644
--- a/pages/_document.js
+++ b/pages/_document.js
@@ -13,11 +13,24 @@ class MyDocument extends Document {
return (
-
-
+
+
+ {/* 预加载字体 */}
+ {BLOG.FONT_AWESOME && <>
+
+
+ >}
+
+ {BLOG.FONT_URL?.map((fontUrl, index) => {
+ if (fontUrl.endsWith('.css')) {
+ return
+ } else {
+ return
+ }
+ })}
-
+
diff --git a/pages/sitemap.xml.js b/pages/sitemap.xml.js
index a613a277..58011385 100644
--- a/pages/sitemap.xml.js
+++ b/pages/sitemap.xml.js
@@ -39,8 +39,9 @@ export const getServerSideProps = async (ctx) => {
}
]
const postFields = allPages?.filter(p => p.status === BLOG.NOTION_PROPERTY_NAME.status_publish)?.map(post => {
+ const slugWithoutLeadingSlash = post?.slug.startsWith('/') ? post?.slug?.slice(1) : post.slug
return {
- loc: `${BLOG.LINK}/${post.slug}`,
+ loc: `${BLOG.LINK}/${slugWithoutLeadingSlash}`,
lastmod: new Date(post?.publishTime).toISOString().split('T')[0],
changefreq: 'daily',
priority: '0.7'
diff --git a/public/css/custom.css b/public/css/custom.css
index 2c08e02e..d9b26a7a 100644
--- a/public/css/custom.css
+++ b/public/css/custom.css
@@ -2,4 +2,7 @@
#theme-fukasawa .sideLeft hr{
opacity: .04;
-}
\ No newline at end of file
+}
+.fa-info:before {
+ content: "\f05a";
+}
diff --git a/public/css/theme-fukasawa.css b/public/css/theme-fukasawa.css
deleted file mode 100644
index d5a56963..00000000
--- a/public/css/theme-fukasawa.css
+++ /dev/null
@@ -1 +0,0 @@
-/* fukasawa的主题相关 */
\ No newline at end of file
diff --git a/public/css/theme-hexo.css b/public/css/theme-hexo.css
deleted file mode 100644
index ff509ec5..00000000
--- a/public/css/theme-hexo.css
+++ /dev/null
@@ -1,30 +0,0 @@
-/* 菜单下划线动画 */
-#theme-hexo .menu-link {
- text-decoration: none;
- background-image: linear-gradient(#928CEE, #928CEE);
- background-repeat: no-repeat;
- background-position: bottom center;
- background-size: 0 2px;
- transition: background-size 100ms ease-in-out;
-}
-
-#theme-hexo .menu-link:hover {
- background-size: 100% 2px;
- color: #928CEE;
-}
-
-/* 设置了从上到下的渐变黑色 */
-#theme-hexo .header-cover::before {
- content: "";
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: linear-gradient(to bottom, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0.2) 10%, rgba(0,0,0,0) 25%, rgba(0,0,0,0.2) 75%, rgba(0,0,0,0.5) 100%);
-}
-
-/* Custem */
-.tk-footer{
- opacity: 0;
-}
\ No newline at end of file
diff --git a/public/css/theme-matery.css b/public/css/theme-matery.css
deleted file mode 100644
index e3a01f01..00000000
--- a/public/css/theme-matery.css
+++ /dev/null
@@ -1,11 +0,0 @@
-
-/* 设置了从上到下的渐变黑色 */
-#theme-matery .header-cover::before {
- content: "";
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: linear-gradient(to bottom, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0.2) 10%, rgba(0,0,0,0) 25%, rgba(0,0,0,0.2) 75%, rgba(0,0,0,0.5) 100%);
-}
\ No newline at end of file
diff --git a/public/css/theme-simple.css b/public/css/theme-simple.css
deleted file mode 100644
index d02b7661..00000000
--- a/public/css/theme-simple.css
+++ /dev/null
@@ -1,34 +0,0 @@
-#theme-simple #announcement-content {
- /* background-color: #f6f6f6; */
-}
-
-#theme-simple .blog-item-title {
- color: #276077;
-}
-
-.dark #theme-simple .blog-item-title {
- color: #d1d5db;
-}
-
-.notion {
- margin-top: 0 !important;
- margin-bottom: 0 !important;
-}
-
-
-/* 菜单下划线动画 */
-#theme-simple .menu-link {
- text-decoration: none;
- background-image: linear-gradient(#dd3333, #dd3333);
- background-repeat: no-repeat;
- background-position: bottom center;
- background-size: 0 2px;
- transition: background-size 100ms ease-in-out;
-}
-
-#theme-simple .menu-link:hover {
- background-size: 100% 2px;
- color: #dd3333;
- cursor: pointer;
-}
-
diff --git a/public/images/heo/20231108a540b2862d26f8850172e4ea58ed075102.webp b/public/images/heo/20231108a540b2862d26f8850172e4ea58ed075102.webp
new file mode 100644
index 00000000..31fa34c8
Binary files /dev/null and b/public/images/heo/20231108a540b2862d26f8850172e4ea58ed075102.webp differ
diff --git a/public/images/heo/20231ca53fa0b09a3ff1df89acd7515e9516173302.webp b/public/images/heo/20231ca53fa0b09a3ff1df89acd7515e9516173302.webp
new file mode 100644
index 00000000..0cde7cf2
Binary files /dev/null and b/public/images/heo/20231ca53fa0b09a3ff1df89acd7515e9516173302.webp differ
diff --git a/public/images/heo/202328bbee0b314297917b327df4a704db5c072402.webp b/public/images/heo/202328bbee0b314297917b327df4a704db5c072402.webp
new file mode 100644
index 00000000..341c67a1
Binary files /dev/null and b/public/images/heo/202328bbee0b314297917b327df4a704db5c072402.webp differ
diff --git a/public/images/heo/20233e777652412247dd57fd9b48cf997c01070702.webp b/public/images/heo/20233e777652412247dd57fd9b48cf997c01070702.webp
new file mode 100644
index 00000000..6da52167
Binary files /dev/null and b/public/images/heo/20233e777652412247dd57fd9b48cf997c01070702.webp differ
diff --git a/public/images/heo/20235c0731cd4c0c95fc136a8db961fdf963071502.webp b/public/images/heo/20235c0731cd4c0c95fc136a8db961fdf963071502.webp
new file mode 100644
index 00000000..c76fccb4
Binary files /dev/null and b/public/images/heo/20235c0731cd4c0c95fc136a8db961fdf963071502.webp differ
diff --git a/public/images/heo/202372b4d760fd8a497d442140c295655426070302.webp b/public/images/heo/202372b4d760fd8a497d442140c295655426070302.webp
new file mode 100644
index 00000000..8f6e6fa5
Binary files /dev/null and b/public/images/heo/202372b4d760fd8a497d442140c295655426070302.webp differ
diff --git a/public/images/heo/20237359d71b45ab77829cee5972e36f8c30073902.webp b/public/images/heo/20237359d71b45ab77829cee5972e36f8c30073902.webp
new file mode 100644
index 00000000..7d9a69ec
Binary files /dev/null and b/public/images/heo/20237359d71b45ab77829cee5972e36f8c30073902.webp differ
diff --git a/public/images/heo/2023786e7fc488f453d5fb2be760c96185c0075502.webp b/public/images/heo/2023786e7fc488f453d5fb2be760c96185c0075502.webp
new file mode 100644
index 00000000..667b0cb7
Binary files /dev/null and b/public/images/heo/2023786e7fc488f453d5fb2be760c96185c0075502.webp differ
diff --git a/public/images/heo/20237c548846044a20dad68a13c0f0e1502f074602.webp b/public/images/heo/20237c548846044a20dad68a13c0f0e1502f074602.webp
new file mode 100644
index 00000000..221d36f2
Binary files /dev/null and b/public/images/heo/20237c548846044a20dad68a13c0f0e1502f074602.webp differ
diff --git a/public/images/heo/20239df3f66615b532ce571eac6d14ff21cf072602.webp b/public/images/heo/20239df3f66615b532ce571eac6d14ff21cf072602.webp
new file mode 100644
index 00000000..8a4c0240
Binary files /dev/null and b/public/images/heo/20239df3f66615b532ce571eac6d14ff21cf072602.webp differ
diff --git a/public/images/heo/2023e0ded7b724a39f12d59c3dc8fbdc7cbe074202.webp b/public/images/heo/2023e0ded7b724a39f12d59c3dc8fbdc7cbe074202.webp
new file mode 100644
index 00000000..53376d3a
Binary files /dev/null and b/public/images/heo/2023e0ded7b724a39f12d59c3dc8fbdc7cbe074202.webp differ
diff --git a/public/images/heo/2023e4058a91608ea41751c4f102b131f267075902.webp b/public/images/heo/2023e4058a91608ea41751c4f102b131f267075902.webp
new file mode 100644
index 00000000..36f479fa
Binary files /dev/null and b/public/images/heo/2023e4058a91608ea41751c4f102b131f267075902.webp differ
diff --git a/public/images/heo/2023f76570d2770c8e84801f7e107cd911b5073202.webp b/public/images/heo/2023f76570d2770c8e84801f7e107cd911b5073202.webp
new file mode 100644
index 00000000..f94947e3
Binary files /dev/null and b/public/images/heo/2023f76570d2770c8e84801f7e107cd911b5073202.webp differ
diff --git a/public/images/heo/2023ffa5707c4e25b6beb3e6a3d286ede4c6071102.webp b/public/images/heo/2023ffa5707c4e25b6beb3e6a3d286ede4c6071102.webp
new file mode 100644
index 00000000..da3e93d1
Binary files /dev/null and b/public/images/heo/2023ffa5707c4e25b6beb3e6a3d286ede4c6071102.webp differ
diff --git a/styles/animate.css b/styles/animate.css
new file mode 100644
index 00000000..43f253c1
--- /dev/null
+++ b/styles/animate.css
@@ -0,0 +1,503 @@
+@charset "UTF-8";/*!
+ * animate.css - https://animate.style/
+ * Version - 4.1.1
+ * Licensed under the MIT license - http://opensource.org/licenses/MIT
+ * https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css
+ * 这里做了精减,后续不再使用animate.css,因为占用体积太大,不如手写动画
+ * Copyright (c) 2020 Animate.css
+ */
+:root {
+ --animate-duration: 1s;
+ --animate-delay: 1s;
+ --animate-repeat: 1;
+}
+.animate__animated {
+ -webkit-animation-duration: 1s;
+ animation-duration: 1s;
+ -webkit-animation-duration: var(--animate-duration);
+ animation-duration: var(--animate-duration);
+ -webkit-animation-fill-mode: both;
+ animation-fill-mode: both;
+}
+
+
+.animate__animated.animate__faster {
+ -webkit-animation-duration: calc(1s / 2);
+ animation-duration: calc(1s / 2);
+ -webkit-animation-duration: calc(var(--animate-duration) / 2);
+ animation-duration: calc(var(--animate-duration) / 2);
+}
+.animate__animated.animate__fast {
+ -webkit-animation-duration: calc(1s * 0.8);
+ animation-duration: calc(1s * 0.8);
+ -webkit-animation-duration: calc(var(--animate-duration) * 0.8);
+ animation-duration: calc(var(--animate-duration) * 0.8);
+}
+
+
+@media print, (prefers-reduced-motion: reduce) {
+ .animate__animated {
+ -webkit-animation-duration: 1ms !important;
+ animation-duration: 1ms !important;
+ -webkit-transition-duration: 1ms !important;
+ transition-duration: 1ms !important;
+ -webkit-animation-iteration-count: 1 !important;
+ animation-iteration-count: 1 !important;
+ }
+
+ .animate__animated[class*='Out'] {
+ opacity: 0;
+ }
+}
+
+
+@-webkit-keyframes shakeX {
+ from,
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ 10%,
+ 30%,
+ 50%,
+ 70%,
+ 90% {
+ -webkit-transform: translate3d(-10px, 0, 0);
+ transform: translate3d(-10px, 0, 0);
+ }
+
+ 20%,
+ 40%,
+ 60%,
+ 80% {
+ -webkit-transform: translate3d(10px, 0, 0);
+ transform: translate3d(10px, 0, 0);
+ }
+}
+@keyframes shakeX {
+ from,
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ 10%,
+ 30%,
+ 50%,
+ 70%,
+ 90% {
+ -webkit-transform: translate3d(-10px, 0, 0);
+ transform: translate3d(-10px, 0, 0);
+ }
+
+ 20%,
+ 40%,
+ 60%,
+ 80% {
+ -webkit-transform: translate3d(10px, 0, 0);
+ transform: translate3d(10px, 0, 0);
+ }
+}
+.animate__shakeX {
+ -webkit-animation-name: shakeX;
+ animation-name: shakeX;
+}
+@-webkit-keyframes shakeY {
+ from,
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ 10%,
+ 30%,
+ 50%,
+ 70%,
+ 90% {
+ -webkit-transform: translate3d(0, -10px, 0);
+ transform: translate3d(0, -10px, 0);
+ }
+
+ 20%,
+ 40%,
+ 60%,
+ 80% {
+ -webkit-transform: translate3d(0, 10px, 0);
+ transform: translate3d(0, 10px, 0);
+ }
+}
+@keyframes shakeY {
+ from,
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ 10%,
+ 30%,
+ 50%,
+ 70%,
+ 90% {
+ -webkit-transform: translate3d(0, -10px, 0);
+ transform: translate3d(0, -10px, 0);
+ }
+
+ 20%,
+ 40%,
+ 60%,
+ 80% {
+ -webkit-transform: translate3d(0, 10px, 0);
+ transform: translate3d(0, 10px, 0);
+ }
+}
+.animate__shakeY {
+ -webkit-animation-name: shakeY;
+ animation-name: shakeY;
+}
+@-webkit-keyframes headShake {
+ 0% {
+ -webkit-transform: translateX(0);
+ transform: translateX(0);
+ }
+
+ 6.5% {
+ -webkit-transform: translateX(-6px) rotateY(-9deg);
+ transform: translateX(-6px) rotateY(-9deg);
+ }
+
+ 18.5% {
+ -webkit-transform: translateX(5px) rotateY(7deg);
+ transform: translateX(5px) rotateY(7deg);
+ }
+
+ 31.5% {
+ -webkit-transform: translateX(-3px) rotateY(-5deg);
+ transform: translateX(-3px) rotateY(-5deg);
+ }
+
+ 43.5% {
+ -webkit-transform: translateX(2px) rotateY(3deg);
+ transform: translateX(2px) rotateY(3deg);
+ }
+
+ 50% {
+ -webkit-transform: translateX(0);
+ transform: translateX(0);
+ }
+}
+@keyframes headShake {
+ 0% {
+ -webkit-transform: translateX(0);
+ transform: translateX(0);
+ }
+
+ 6.5% {
+ -webkit-transform: translateX(-6px) rotateY(-9deg);
+ transform: translateX(-6px) rotateY(-9deg);
+ }
+
+ 18.5% {
+ -webkit-transform: translateX(5px) rotateY(7deg);
+ transform: translateX(5px) rotateY(7deg);
+ }
+
+ 31.5% {
+ -webkit-transform: translateX(-3px) rotateY(-5deg);
+ transform: translateX(-3px) rotateY(-5deg);
+ }
+
+ 43.5% {
+ -webkit-transform: translateX(2px) rotateY(3deg);
+ transform: translateX(2px) rotateY(3deg);
+ }
+
+ 50% {
+ -webkit-transform: translateX(0);
+ transform: translateX(0);
+ }
+}
+.animate__headShake {
+ -webkit-animation-timing-function: ease-in-out;
+ animation-timing-function: ease-in-out;
+ -webkit-animation-name: headShake;
+ animation-name: headShake;
+}
+
+
+@keyframes jello {
+ from,
+ 11.1%,
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ 22.2% {
+ -webkit-transform: skewX(-12.5deg) skewY(-12.5deg);
+ transform: skewX(-12.5deg) skewY(-12.5deg);
+ }
+
+ 33.3% {
+ -webkit-transform: skewX(6.25deg) skewY(6.25deg);
+ transform: skewX(6.25deg) skewY(6.25deg);
+ }
+
+ 44.4% {
+ -webkit-transform: skewX(-3.125deg) skewY(-3.125deg);
+ transform: skewX(-3.125deg) skewY(-3.125deg);
+ }
+
+ 55.5% {
+ -webkit-transform: skewX(1.5625deg) skewY(1.5625deg);
+ transform: skewX(1.5625deg) skewY(1.5625deg);
+ }
+
+ 66.6% {
+ -webkit-transform: skewX(-0.78125deg) skewY(-0.78125deg);
+ transform: skewX(-0.78125deg) skewY(-0.78125deg);
+ }
+
+ 77.7% {
+ -webkit-transform: skewX(0.390625deg) skewY(0.390625deg);
+ transform: skewX(0.390625deg) skewY(0.390625deg);
+ }
+
+ 88.8% {
+ -webkit-transform: skewX(-0.1953125deg) skewY(-0.1953125deg);
+ transform: skewX(-0.1953125deg) skewY(-0.1953125deg);
+ }
+}
+.animate__jello {
+ -webkit-animation-name: jello;
+ animation-name: jello;
+ -webkit-transform-origin: center;
+ transform-origin: center;
+}
+
+
+
+@-webkit-keyframes bounceInRight {
+ from,
+ 60%,
+ 75%,
+ 90%,
+ to {
+ -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ }
+
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(3000px, 0, 0) scaleX(3);
+ transform: translate3d(3000px, 0, 0) scaleX(3);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: translate3d(-25px, 0, 0) scaleX(1);
+ transform: translate3d(-25px, 0, 0) scaleX(1);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(10px, 0, 0) scaleX(0.98);
+ transform: translate3d(10px, 0, 0) scaleX(0.98);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(-5px, 0, 0) scaleX(0.995);
+ transform: translate3d(-5px, 0, 0) scaleX(0.995);
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+@keyframes bounceInRight {
+ from,
+ 60%,
+ 75%,
+ 90%,
+ to {
+ -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ }
+
+ from {
+ opacity: 0;
+ -webkit-transform: translate3d(3000px, 0, 0) scaleX(3);
+ transform: translate3d(3000px, 0, 0) scaleX(3);
+ }
+
+ 60% {
+ opacity: 1;
+ -webkit-transform: translate3d(-25px, 0, 0) scaleX(1);
+ transform: translate3d(-25px, 0, 0) scaleX(1);
+ }
+
+ 75% {
+ -webkit-transform: translate3d(10px, 0, 0) scaleX(0.98);
+ transform: translate3d(10px, 0, 0) scaleX(0.98);
+ }
+
+ 90% {
+ -webkit-transform: translate3d(-5px, 0, 0) scaleX(0.995);
+ transform: translate3d(-5px, 0, 0) scaleX(0.995);
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+.animate__bounceInRight {
+ -webkit-animation-name: bounceInRight;
+ animation-name: bounceInRight;
+}
+
+
+/* Fading entrances */
+@-webkit-keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
+.animate__fadeIn {
+ -webkit-animation-name: fadeIn;
+ animation-name: fadeIn;
+}
+
+
+/* Fading exits */
+/* 删除 */
+
+/* Flippers */
+/* 删除 */
+
+/* Lightspeed */
+/* 删除 */
+
+/* Rotating exits */
+/* 删除 */
+
+/* Zooming entrances */
+/* 删除 */
+
+/* Sliding entrances */
+
+@-webkit-keyframes slideInLeft {
+ from {
+ -webkit-transform: translate3d(-100%, 0, 0);
+ transform: translate3d(-100%, 0, 0);
+ visibility: visible;
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+@keyframes slideInLeft {
+ from {
+ -webkit-transform: translate3d(-100%, 0, 0);
+ transform: translate3d(-100%, 0, 0);
+ visibility: visible;
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+.animate__slideInLeft {
+ -webkit-animation-name: slideInLeft;
+ animation-name: slideInLeft;
+}
+@-webkit-keyframes slideInRight {
+ from {
+ -webkit-transform: translate3d(100%, 0, 0);
+ transform: translate3d(100%, 0, 0);
+ visibility: visible;
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+@keyframes slideInRight {
+ from {
+ -webkit-transform: translate3d(100%, 0, 0);
+ transform: translate3d(100%, 0, 0);
+ visibility: visible;
+ }
+
+ to {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+.animate__slideInRight {
+ -webkit-animation-name: slideInRight;
+ animation-name: slideInRight;
+}
+
+
+@keyframes slideOutLeft {
+ from {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ to {
+ visibility: hidden;
+ -webkit-transform: translate3d(-100%, 0, 0);
+ transform: translate3d(-100%, 0, 0);
+ }
+}
+.animate__slideOutLeft {
+ -webkit-animation-name: slideOutLeft;
+ animation-name: slideOutLeft;
+}
+@-webkit-keyframes slideOutRight {
+ from {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ to {
+ visibility: hidden;
+ -webkit-transform: translate3d(100%, 0, 0);
+ transform: translate3d(100%, 0, 0);
+ }
+}
+@keyframes slideOutRight {
+ from {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+
+ to {
+ visibility: hidden;
+ -webkit-transform: translate3d(100%, 0, 0);
+ transform: translate3d(100%, 0, 0);
+ }
+}
+.animate__slideOutRight {
+ -webkit-animation-name: slideOutRight;
+ animation-name: slideOutRight;
+}
diff --git a/styles/globals.css b/styles/globals.css
index ae4d6aca..dbaa8ef2 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -2,28 +2,6 @@
@tailwind components;
@tailwind utilities;
-html {
- --scrollbarBG: #ffffff00;
- --thumbBG: #b8b8b8;
-}
-body::-webkit-scrollbar {
- width: 5px;
-}
-body {
- scrollbar-width: thin;
- scrollbar-color: var(--thumbBG) var(--scrollbarBG);
-}
-body::-webkit-scrollbar-track {
- background: var(--scrollbarBG);
-}
-body::-webkit-scrollbar-thumb {
- background-color: var(--thumbBG);
-}
-
-::selection {
- background: rgba(45, 170, 219, 0.3);
-}
-
.wrapper {
min-height: 100vh;
display: flex;
@@ -285,57 +263,3 @@ a.avatar-wrapper {
.reply-author-name {
font-weight: 500;
}
-
-.line-clamp-4 {
- display: -webkit-box;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 4;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.line-clamp-3 {
- overflow: hidden;
- display: -webkit-box;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 3;
-}
-
-.line-clamp-2 {
- overflow: hidden;
- display: -webkit-box;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 2;
-}
-
-
-/* fukasawa的首页响应式分栏 */
-#theme-fukasawa .grid-item {
- height: auto;
- break-inside: avoid-column;
- margin-bottom: .5rem;
- }
-
- /* 大屏幕(宽度≥1024px)下显示3列 */
- @media (min-width: 1024px) {
- #theme-fukasawa .grid-container {
- column-count: 3;
- column-gap: .5rem;
- }
- }
-
- /* 小屏幕(宽度≥640px)下显示2列 */
- @media (min-width: 640px) and (max-width: 1023px) {
- #theme-fukasawa .grid-container {
- column-count: 2;
- column-gap: .5rem;
- }
- }
-
- /* 移动端(宽度<640px)下显示1列 */
- @media (max-width: 639px) {
- #theme-fukasawa .grid-container {
- column-count: 1;
- column-gap: .5rem;
- }
- }
\ No newline at end of file
diff --git a/styles/notion.css b/styles/notion.css
index 5fd5b4b8..6e47807b 100644
--- a/styles/notion.css
+++ b/styles/notion.css
@@ -443,6 +443,7 @@ summary > .notion-h {
.notion-h:hover .notion-hash-link {
opacity: 1;
+ @apply dark:fill-gray-200
}
.notion-hash-link {
@@ -1392,6 +1393,10 @@ code[class*='language-'] {
white-space: normal;
}
+.katex-display>.katex>.katex-html>.tag {
+ position: inherit !important;
+}
+
.notion-page-title {
display: inline-flex;
max-width: 100%;
@@ -1943,10 +1948,6 @@ svg + .notion-page-title-text {
display: block !important;
}
-::selection {
- @apply bg-blue-500 text-gray-50 !important;
-}
-
/* https://github.com/kchen0x */
.notion-quote {
display: block;
@@ -2049,4 +2050,9 @@ code.language-mermaid {
.notion-simple-table td{
border: 1px solid var(#eee) !important
-}
\ No newline at end of file
+}
+
+/* 竖屏视频高度bug */
+figure.notion-asset-wrapper.notion-asset-wrapper-video>div {
+ height: 100% !important;
+}
diff --git a/themes/example/components/BlogPostCard.js b/themes/example/components/BlogPostCard.js
index 03f38ec6..faf1753d 100644
--- a/themes/example/components/BlogPostCard.js
+++ b/themes/example/components/BlogPostCard.js
@@ -2,6 +2,7 @@ import BLOG from '@/blog.config'
import CONFIG from '../config'
import Link from 'next/link'
import TwikooCommentCount from '@/components/TwikooCommentCount'
+import LazyImage from '@/components/LazyImage'
const BlogPostCard = ({ post }) => {
const showPageCover = CONFIG.POST_LIST_COVER && post?.pageCoverThumbnail
@@ -41,7 +42,7 @@ const BlogPostCard = ({ post }) => {
{showPageCover && (
)}
diff --git a/themes/example/index.js b/themes/example/index.js
index 51986843..d14bc570 100644
--- a/themes/example/index.js
+++ b/themes/example/index.js
@@ -19,13 +19,14 @@ import NotionPage from '@/components/NotionPage'
import Comment from '@/components/Comment'
import ShareBar from '@/components/ShareBar'
import SearchInput from './components/SearchInput'
-import Mark from 'mark.js'
+import replaceSearchResult from '@/components/Mark'
import { isBrowser } from '@/lib/utils'
import BlogListGroupByDate from './components/BlogListGroupByDate'
import CategoryItem from './components/CategoryItem'
import TagItem from './components/TagItem'
import { useRouter } from 'next/router'
import { Transition } from '@headlessui/react'
+import { Style } from './style'
/**
* 基础布局框架
@@ -50,6 +51,7 @@ const LayoutBase = props => {
{/* 网页SEO信息 */}
+
{/* 页头 */}
@@ -173,21 +175,20 @@ const LayoutSearch = props => {
const slotTop =
const router = useRouter()
useEffect(() => {
- setTimeout(() => {
- if (isBrowser()) {
- // 高亮搜索到的结果
- const container = document.getElementById('posts-wrapper')
- console.log('container', container, keyword)
- if (keyword && container) {
- const re = new RegExp(keyword, 'gim')
- const instance = new Mark(container)
- instance.markRegExp(re, {
+ if (isBrowser()) {
+ // 高亮搜索到的结果
+ const container = document.getElementById('posts-wrapper')
+ if (keyword && container) {
+ replaceSearchResult({
+ doms: container,
+ search: keyword,
+ target: {
element: 'span',
className: 'text-red-500 border-b border-dashed'
- })
- }
+ }
+ })
}
- }, 500)
+ }
}, [router])
return
diff --git a/themes/example/style.js b/themes/example/style.js
new file mode 100644
index 00000000..0708b7b5
--- /dev/null
+++ b/themes/example/style.js
@@ -0,0 +1,17 @@
+/* eslint-disable react/no-unknown-property */
+/**
+ * 此处样式只对当前主题生效
+ * 此处不支持tailwindCSS的 @apply 语法
+ * @returns
+ */
+const Style = () => {
+ return
+}
+
+export { Style }
diff --git a/themes/fukasawa/components/ArticleDetail.js b/themes/fukasawa/components/ArticleDetail.js
index 1ea23429..a1297dd2 100644
--- a/themes/fukasawa/components/ArticleDetail.js
+++ b/themes/fukasawa/components/ArticleDetail.js
@@ -6,6 +6,7 @@ import { useGlobal } from '@/lib/global'
import Link from 'next/link'
import ArticleAround from './ArticleAround'
import { AdSlot } from '@/components/GoogleAdsense'
+import LazyImage from '@/components/LazyImage'
/**
*
@@ -23,8 +24,7 @@ export default function ArticleDetail(props) {
{post?.type && !post?.type !== 'Page' && post?.pageCover && (
- {/* eslint-disable-next-line @next/next/no-img-element */}
-

+
)}
diff --git a/themes/fukasawa/components/BlogCard.js b/themes/fukasawa/components/BlogCard.js
index 9074c7c4..ec9070f4 100644
--- a/themes/fukasawa/components/BlogCard.js
+++ b/themes/fukasawa/components/BlogCard.js
@@ -3,6 +3,7 @@ import Link from 'next/link'
import TagItemMini from './TagItemMini'
import React from 'react'
import CONFIG_FUKA from '../config'
+import LazyImage from '@/components/LazyImage'
const BlogCard = ({ index, post, showSummary, siteInfo }) => {
const showPreview = CONFIG_FUKA.POST_LIST_PREVIEW && post.blockMap
@@ -26,12 +27,11 @@ const BlogCard = ({ index, post, showSummary, siteInfo }) => {
{showPageCover && (
- {/* eslint-disable-next-line @next/next/no-img-element */}
-

+ />
)}
diff --git a/themes/fukasawa/components/SocialButton.js b/themes/fukasawa/components/SocialButton.js
index e3d39317..234182cf 100644
--- a/themes/fukasawa/components/SocialButton.js
+++ b/themes/fukasawa/components/SocialButton.js
@@ -30,7 +30,7 @@ const SocialButton = () => {
{BLOG.CONTACT_EMAIL &&
}
- {BLOG.ENABLE_RSS &&
+ {JSON.parse(BLOG.ENABLE_RSS) &&
}
{BLOG.CONTACT_BILIBILI &&
diff --git a/themes/fukasawa/index.js b/themes/fukasawa/index.js
index af73a189..52b55b5c 100644
--- a/themes/fukasawa/index.js
+++ b/themes/fukasawa/index.js
@@ -5,7 +5,7 @@ import CommonHead from '@/components/CommonHead'
import TopNav from './components/TopNav'
import AsideLeft from './components/AsideLeft'
import BLOG from '@/blog.config'
-import { isBrowser, loadExternalResource } from '@/lib/utils'
+import { isBrowser } from '@/lib/utils'
import { useGlobal } from '@/lib/global'
import BlogListPage from './components/BlogListPage'
import BlogListScroll from './components/BlogListScroll'
@@ -19,9 +19,10 @@ import Link from 'next/link'
import { Transition } from '@headlessui/react'
import dynamic from 'next/dynamic'
import { AdSlot } from '@/components/GoogleAdsense'
+import { Style } from './style'
+import replaceSearchResult from '@/components/Mark'
const Live2D = dynamic(() => import('@/components/Live2D'))
-const Mark = dynamic(() => import('mark.js'))
// 主题全局状态
const ThemeGlobalFukasawa = createContext()
@@ -61,15 +62,12 @@ const LayoutBase = (props) => {
}
}, [isCollapsed])
- if (isBrowser()) {
- loadExternalResource('/css/theme-fukasawa.css', 'css')
- }
-
return (
+
@@ -150,17 +148,16 @@ const LayoutSearch = props => {
const { keyword } = props
const router = useRouter()
useEffect(() => {
- setTimeout(() => {
- const container = isBrowser() && document.getElementById('posts-wrapper')
- if (container && container.innerHTML) {
- const re = new RegExp(keyword, 'gim')
- const instance = new Mark(container)
- instance.markRegExp(re, {
+ if (isBrowser()) {
+ replaceSearchResult({
+ doms: document.getElementById('posts-wrapper'),
+ search: keyword,
+ target: {
element: 'span',
className: 'text-red-500 border-b border-dashed'
- })
- }
- }, 300)
+ }
+ })
+ }
}, [router])
return
}
diff --git a/themes/fukasawa/style.js b/themes/fukasawa/style.js
new file mode 100644
index 00000000..e730d919
--- /dev/null
+++ b/themes/fukasawa/style.js
@@ -0,0 +1,50 @@
+/* eslint-disable react/no-unknown-property */
+/**
+ * 此处样式只对当前主题生效
+ * 此处不支持tailwindCSS的 @apply 语法
+ * @returns
+ */
+const Style = () => {
+ return
+}
+
+export { Style }
diff --git a/themes/gitbook/components/BlogPostCard.js b/themes/gitbook/components/BlogPostCard.js
index bb47fcc2..15ff21ef 100644
--- a/themes/gitbook/components/BlogPostCard.js
+++ b/themes/gitbook/components/BlogPostCard.js
@@ -8,7 +8,7 @@ const BlogPostCard = ({ post, className }) => {
const currentSelected = router.asPath.split('?')[0] === '/' + post.slug
return (
-
+
{post.title}
diff --git a/themes/gitbook/components/FloatTocButton.js b/themes/gitbook/components/FloatTocButton.js
index 3b6c5933..4215f447 100644
--- a/themes/gitbook/components/FloatTocButton.js
+++ b/themes/gitbook/components/FloatTocButton.js
@@ -1,5 +1,8 @@
import { useGitBookGlobal } from '@/themes/gitbook'
+/**
+ * 移动端悬浮目录按钮
+ */
export default function FloatTocButton () {
const { tocVisible, changeTocVisible } = useGitBookGlobal()
@@ -14,7 +17,7 @@ export default function FloatTocButton () {
}
>
diff --git a/themes/gitbook/components/InfoCard.js b/themes/gitbook/components/InfoCard.js
index a18408dc..13fe31c8 100644
--- a/themes/gitbook/components/InfoCard.js
+++ b/themes/gitbook/components/InfoCard.js
@@ -1,4 +1,5 @@
import BLOG from '@/blog.config'
+import LazyImage from '@/components/LazyImage'
import Router from 'next/router'
import React from 'react'
import SocialButton from './SocialButton'
@@ -8,8 +9,7 @@ const InfoCard = (props) => {
return
{ Router.push('/about') }}>
- {/* eslint-disable-next-line @next/next/no-img-element */}
-

+
{BLOG.AUTHOR}
{BLOG.BIO}
diff --git a/themes/gitbook/components/LogoBar.js b/themes/gitbook/components/LogoBar.js
index 803a4fa3..70c96b46 100644
--- a/themes/gitbook/components/LogoBar.js
+++ b/themes/gitbook/components/LogoBar.js
@@ -1,4 +1,5 @@
import BLOG from '@/blog.config'
+import LazyImage from '@/components/LazyImage'
import { useGitBookGlobal } from '@/themes/gitbook'
import Link from 'next/link'
@@ -20,8 +21,7 @@ export default function LogoBar(props) {
- {/* eslint-disable-next-line @next/next/no-img-element */}
-

+
{siteInfo?.title}
diff --git a/themes/gitbook/components/NavPostItem.js b/themes/gitbook/components/NavPostItem.js
index a4cd7178..66ffa6dd 100644
--- a/themes/gitbook/components/NavPostItem.js
+++ b/themes/gitbook/components/NavPostItem.js
@@ -21,7 +21,7 @@ const NavPostItem = (props) => {
return <>
+ className='select-none flex justify-between text-sm font-sans cursor-pointer p-2 hover:bg-gray-50 rounded-md dark:hover:bg-gray-600' key={group?.category}>
{group?.category}
diff --git a/themes/gitbook/components/SocialButton.js b/themes/gitbook/components/SocialButton.js
index 5f51033d..164a71fc 100644
--- a/themes/gitbook/components/SocialButton.js
+++ b/themes/gitbook/components/SocialButton.js
@@ -29,7 +29,7 @@ const SocialButton = () => {
{BLOG.CONTACT_EMAIL &&
}
- {BLOG.ENABLE_RSS &&
+ {JSON.parse(BLOG.ENABLE_RSS) &&
}
diff --git a/themes/gitbook/index.js b/themes/gitbook/index.js
index e5bb0323..2b034195 100644
--- a/themes/gitbook/index.js
+++ b/themes/gitbook/index.js
@@ -30,6 +30,7 @@ import TocDrawer from './components/TocDrawer'
import NotionPage from '@/components/NotionPage'
import { ArticleLock } from './components/ArticleLock'
import { Transition } from '@headlessui/react'
+import { Style } from './style'
// 主题全局变量
const ThemeGlobalGitbook = createContext()
@@ -58,6 +59,7 @@ const LayoutBase = (props) => {
return (
+
{/* 顶部导航栏 */}
@@ -76,7 +78,7 @@ const LayoutBase = (props) => {
-
+
@@ -170,7 +172,7 @@ const LayoutIndex = (props) => {
const article = document.getElementById('notion-article')
if (!article) {
console.log('请检查您的Notion数据库中是否包含此slug页面: ', CONFIG.INDEX_PAGE)
- const containerInner = document.getElementById('container-inner')
+ const containerInner = document.querySelector('#theme-gitbook #container-inner')
const newHTML = `
配置有误
请在您的notion中添加一个slug为${CONFIG.INDEX_PAGE}的文章
`
containerInner?.insertAdjacentHTML('afterbegin', newHTML)
}
@@ -179,7 +181,7 @@ const LayoutIndex = (props) => {
})
}, [])
- return
+ return
}
/**
@@ -189,7 +191,7 @@ const LayoutIndex = (props) => {
* @returns
*/
const LayoutPostList = (props) => {
- return
+ return
}
/**
@@ -213,9 +215,6 @@ const LayoutSlug = (props) => {
{/* Notion文章主体 */}
{post && (
)}
-
-
{/* 分享 */}
@@ -232,7 +231,7 @@ const LayoutSlug = (props) => {
-
+ )}
}
diff --git a/themes/gitbook/style.js b/themes/gitbook/style.js
new file mode 100644
index 00000000..5e8eaa5a
--- /dev/null
+++ b/themes/gitbook/style.js
@@ -0,0 +1,18 @@
+/* eslint-disable react/no-unknown-property */
+/**
+ * 此处样式只对当前主题生效
+ * 此处不支持tailwindCSS的 @apply 语法
+ * @returns
+ */
+const Style = () => {
+ return
+}
+
+export { Style }
diff --git a/themes/heo/components/AnalyticsCard.js b/themes/heo/components/AnalyticsCard.js
new file mode 100644
index 00000000..a9d30477
--- /dev/null
+++ b/themes/heo/components/AnalyticsCard.js
@@ -0,0 +1,43 @@
+import CONFIG from '../config'
+
+/**
+ * 博客统计卡牌
+ * @param {*} props
+ * @returns
+ */
+export function AnalyticsCard(props) {
+ const targetDate = new Date(CONFIG.SITE_CREATE_TIME)
+ const today = new Date()
+ const diffTime = today.getTime() - targetDate.getTime() // 获取两个日期之间的毫秒数差值
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) // 将毫秒数差值转换为天数差值
+
+ const { postCount } = props
+ return <>
+
+ >
+}
diff --git a/themes/heo/components/Announcement.js b/themes/heo/components/Announcement.js
new file mode 100644
index 00000000..6c3feb2a
--- /dev/null
+++ b/themes/heo/components/Announcement.js
@@ -0,0 +1,18 @@
+import dynamic from 'next/dynamic'
+
+const NotionPage = dynamic(() => import('@/components/NotionPage'))
+
+const Announcement = ({ post, className }) => {
+ if (post?.blockMap) {
+ return
+ {post && (
+
+
+
+ )}
+
+ } else {
+ return <>>
+ }
+}
+export default Announcement
diff --git a/themes/heo/components/ArticleAdjacent.js b/themes/heo/components/ArticleAdjacent.js
new file mode 100644
index 00000000..030a5dd7
--- /dev/null
+++ b/themes/heo/components/ArticleAdjacent.js
@@ -0,0 +1,86 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useEffect, useState } from 'react'
+import CONFIG from '../config'
+
+/**
+ * 上一篇,下一篇文章
+ * @param {prev,next} param0
+ * @returns
+ */
+export default function ArticleAdjacent({ prev, next }) {
+ const [isScrollEnd, setIsScrollEnd] = useState(false)
+ const router = useRouter()
+
+ useEffect(() => {
+ setIsScrollEnd(false)
+ }, [router])
+
+ useEffect(() => {
+ // 文章是否已经到了底部
+ const targetElement = document.getElementById('article-end')
+
+ const handleIntersect = (entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ setIsScrollEnd(true)
+ }
+ })
+ }
+
+ const options = {
+ root: null,
+ rootMargin: '0px',
+ threshold: 0.1
+ }
+
+ const observer = new IntersectionObserver(handleIntersect, options)
+ observer.observe(targetElement)
+
+ return () => {
+ observer.disconnect()
+ }
+ }, [])
+
+ if (!prev || !next || !CONFIG.ARTICLE_ADJACENT) {
+ return <>>
+ }
+
+ return (
+
+ {/* 移动端 */}
+
+
+ 上一篇
+ {prev.title}
+
+
+ 下一篇
+ {next.title}
+
+
+
+ {/* 桌面端 */}
+
+
+
+
下一篇
+
+
{next?.title}
+
+
+
+
+ )
+}
diff --git a/themes/heo/components/ArticleCopyright.js b/themes/heo/components/ArticleCopyright.js
new file mode 100644
index 00000000..4664573c
--- /dev/null
+++ b/themes/heo/components/ArticleCopyright.js
@@ -0,0 +1,41 @@
+import BLOG from '@/blog.config'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useEffect, useState } from 'react'
+import CONFIG from '../config'
+
+export default function ArticleCopyright () {
+ if (!CONFIG.ARTICLE_COPYRIGHT) {
+ return <>>
+ }
+ const router = useRouter()
+ const [path, setPath] = useState(BLOG.LINK + router.asPath)
+ useEffect(() => {
+ setPath(window.location.href)
+ })
+
+ const { locale } = useGlobal()
+ return (
+
+
+ -
+ {locale.COMMON.AUTHOR}:
+
+ {BLOG.AUTHOR}
+
+
+ -
+ {locale.COMMON.URL}:
+
+ {path}
+
+
+ -
+ {locale.COMMON.COPYRIGHT}:
+ {locale.COMMON.COPYRIGHT_NOTICE}
+
+
+
+ );
+}
diff --git a/themes/heo/components/ArticleLock.js b/themes/heo/components/ArticleLock.js
new file mode 100644
index 00000000..7f1da728
--- /dev/null
+++ b/themes/heo/components/ArticleLock.js
@@ -0,0 +1,51 @@
+import { useGlobal } from '@/lib/global'
+import { useEffect, useRef } from 'react'
+
+/**
+ * 加密文章校验组件
+ * @param {password, validPassword} props
+ * @param password 正确的密码
+ * @param validPassword(bool) 回调函数,校验正确回调入参为true
+ * @returns
+ */
+export const ArticleLock = props => {
+ const { validPassword } = props
+ const { locale } = useGlobal()
+ const submitPassword = () => {
+ const p = document.getElementById('password')
+ if (!validPassword(p?.value)) {
+ const tips = document.getElementById('tips')
+ if (tips) {
+ tips.innerHTML = ''
+ tips.innerHTML = `${locale.COMMON.PASSWORD_ERROR}
`
+ }
+ }
+ }
+ const passwordInputRef = useRef(null)
+ useEffect(() => {
+ // 选中密码输入框并将其聚焦
+ passwordInputRef.current.focus()
+ }, [])
+
+ return
+
+
{locale.COMMON.ARTICLE_LOCK_TIPS}
+
+
{
+ if (e.key === 'Enter') {
+ submitPassword()
+ }
+ }}
+ ref={passwordInputRef} // 绑定ref到passwordInputRef变量
+ className='outline-none w-full text-sm pl-5 rounded-l transition focus:shadow-lg font-light leading-10 bg-gray-100 dark:bg-gray-500'>
+
+
+ {locale.COMMON.SUBMIT}
+
+
+
+
+
+
+}
diff --git a/themes/heo/components/ArticleRecommend.js b/themes/heo/components/ArticleRecommend.js
new file mode 100644
index 00000000..f7f6c738
--- /dev/null
+++ b/themes/heo/components/ArticleRecommend.js
@@ -0,0 +1,65 @@
+import Link from 'next/link'
+import CONFIG from '../config'
+import BLOG from '@/blog.config'
+import { useGlobal } from '@/lib/global'
+import LazyImage from '@/components/LazyImage'
+
+/**
+ * 关联推荐文章
+ * @param {prev,next} param0
+ * @returns
+ */
+export default function ArticleRecommend({ recommendPosts, siteInfo }) {
+ const { locale } = useGlobal()
+
+ if (
+ !CONFIG.ARTICLE_RECOMMEND ||
+ !recommendPosts ||
+ recommendPosts.length === 0
+ ) {
+ return <>>
+ }
+
+ return (
+
+
+ {/* 推荐文章 */}
+
+
+
+ {locale.COMMON.RELATE_POSTS}
+
+
+
+ {/* 文章列表 */}
+
+
+ {recommendPosts.map(post => {
+ const headerImage = post?.pageCoverThumbnail
+ ? post.pageCoverThumbnail
+ : siteInfo?.pageCover
+
+ return (
+ (
+
+
+
+ )
+ )
+ })}
+
+
+ )
+}
diff --git a/themes/heo/components/BlogPostArchive.js b/themes/heo/components/BlogPostArchive.js
new file mode 100644
index 00000000..4f471444
--- /dev/null
+++ b/themes/heo/components/BlogPostArchive.js
@@ -0,0 +1,87 @@
+import React from 'react'
+import Link from 'next/link'
+import BLOG from '@/blog.config'
+import CONFIG from '../config'
+import TagItemMini from './TagItemMini'
+import LazyImage from '@/components/LazyImage'
+/**
+ * 博客归档列表
+ * @param posts 所有文章
+ * @param archiveTitle 归档标题
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const BlogPostArchive = ({ posts = [], archiveTitle, siteInfo }) => {
+ if (!posts || posts.length === 0) {
+ return <>>
+ } else {
+ return (
+
+ )
+ }
+}
+
+export default BlogPostArchive
diff --git a/themes/heo/components/BlogPostCard.js b/themes/heo/components/BlogPostCard.js
new file mode 100644
index 00000000..d44c5fe4
--- /dev/null
+++ b/themes/heo/components/BlogPostCard.js
@@ -0,0 +1,85 @@
+import Link from 'next/link'
+import CONFIG from '../config'
+import BLOG from '@/blog.config'
+import TagItemMini from './TagItemMini'
+import LazyImage from '@/components/LazyImage'
+
+const BlogPostCard = ({ index, post, showSummary, siteInfo }) => {
+ const showPreview = CONFIG.POST_LIST_PREVIEW && post.blockMap
+ if (post && !post.pageCoverThumbnail && CONFIG.POST_LIST_COVER_DEFAULT) {
+ post.pageCoverThumbnail = siteInfo?.pageCover
+ }
+ const showPageCover = CONFIG.POST_LIST_COVER && post?.pageCoverThumbnail && !showPreview
+ return (
+
+
+
+
+ {/* 图片封面 */}
+ {showPageCover && (
+
+
+
+
+
+ )}
+
+ {/* 文字区块 */}
+
+
+ {/* 分类 */}
+ {post?.category &&
+
+ {post.category}
+
+
}
+
+ {/* 标题 */}
+
+
{post.title}
+
+
+
+ {/* 摘要 */}
+ {(!showPreview || showSummary) && !post.results && (
+
+ {post.summary}
+
+ )}
+
+ {/* 搜索结果 */}
+ {post.results && (
+
+ {post.results.map((r, index) => (
+ {r}
+ ))}
+
+ )}
+
+
+
+ {' '}
+ {post.tagItems?.map(tag => (
+
+ ))}
+
+
+
+
+
+
+
+
+ )
+}
+
+export default BlogPostCard
diff --git a/themes/heo/components/BlogPostListEmpty.js b/themes/heo/components/BlogPostListEmpty.js
new file mode 100644
index 00000000..5f75c3e7
--- /dev/null
+++ b/themes/heo/components/BlogPostListEmpty.js
@@ -0,0 +1,14 @@
+import { useGlobal } from '@/lib/global'
+
+/**
+ * 空白博客 列表
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const BlogPostListEmpty = ({ currentSearch }) => {
+ const { locale } = useGlobal()
+ return
+
{locale.COMMON.NO_MORE} {(currentSearch &&
{currentSearch}
)}
+
+}
+export default BlogPostListEmpty
diff --git a/themes/heo/components/BlogPostListPage.js b/themes/heo/components/BlogPostListPage.js
new file mode 100644
index 00000000..52683984
--- /dev/null
+++ b/themes/heo/components/BlogPostListPage.js
@@ -0,0 +1,35 @@
+import BlogPostCard from './BlogPostCard'
+import PaginationNumber from './PaginationNumber'
+import BLOG from '@/blog.config'
+import BlogPostListEmpty from './BlogPostListEmpty'
+
+/**
+ * 文章列表分页表格
+ * @param page 当前页
+ * @param posts 所有文章
+ * @param tags 所有标签
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const BlogPostListPage = ({ page = 1, posts = [], postCount, siteInfo }) => {
+ const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
+ const showPagination = postCount >= BLOG.POSTS_PER_PAGE
+ if (!posts || posts.length === 0 || page > totalPage) {
+ return
+ } else {
+ return (
+
+ {/* 文章列表 */}
+
+ {posts?.map(post => (
+
+ ))}
+
+
+ {showPagination &&
}
+
+ )
+ }
+}
+
+export default BlogPostListPage
diff --git a/themes/heo/components/BlogPostListScroll.js b/themes/heo/components/BlogPostListScroll.js
new file mode 100644
index 00000000..20202a9f
--- /dev/null
+++ b/themes/heo/components/BlogPostListScroll.js
@@ -0,0 +1,76 @@
+import BLOG from '@/blog.config'
+import BlogPostCard from './BlogPostCard'
+import BlogPostListEmpty from './BlogPostListEmpty'
+import { useGlobal } from '@/lib/global'
+import React, { useEffect, useRef, useState } from 'react'
+import CONFIG from '../config'
+import { getListByPage } from '@/lib/utils'
+
+/**
+ * 博客列表滚动分页
+ * @param posts 所有文章
+ * @param tags 所有标签
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = CONFIG.POST_LIST_SUMMARY, siteInfo }) => {
+ const postsPerPage = BLOG.POSTS_PER_PAGE
+ const [page, updatePage] = useState(1)
+ const postsToShow = getListByPage(posts, page, postsPerPage)
+
+ let hasMore = false
+ if (posts) {
+ const totalCount = posts.length
+ hasMore = page * postsPerPage < totalCount
+ }
+
+ const handleGetMore = () => {
+ if (!hasMore) return
+ updatePage(page + 1)
+ }
+
+ // 监听滚动自动分页加载
+ const scrollTrigger = () => {
+ requestAnimationFrame(() => {
+ const scrollS = window.scrollY + window.outerHeight
+ const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
+ if (scrollS > clientHeight + 100) {
+ handleGetMore()
+ }
+ })
+ }
+
+ // 监听滚动
+ useEffect(() => {
+ window.addEventListener('scroll', scrollTrigger)
+ return () => {
+ window.removeEventListener('scroll', scrollTrigger)
+ }
+ })
+
+ const targetRef = useRef(null)
+ const { locale } = useGlobal()
+
+ if (!postsToShow || postsToShow.length === 0) {
+ return
+ } else {
+ return
+
+ {/* 文章列表 */}
+
+ {postsToShow.map(post => (
+
+ ))}
+
+
+ {/* 更多按钮 */}
+
+
{ handleGetMore() }}
+ className='w-full my-4 py-4 text-center cursor-pointer rounded-xl dark:text-gray-200'
+ > {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE}`}
+
+
+ }
+}
+
+export default BlogPostListScroll
diff --git a/themes/heo/components/Card.js b/themes/heo/components/Card.js
new file mode 100644
index 00000000..0859dec0
--- /dev/null
+++ b/themes/heo/components/Card.js
@@ -0,0 +1,9 @@
+const Card = ({ children, headerSlot, className }) => {
+ return
+ <>{headerSlot}>
+
+
+}
+export default Card
diff --git a/themes/heo/components/Catalog.js b/themes/heo/components/Catalog.js
new file mode 100644
index 00000000..81ea8d1a
--- /dev/null
+++ b/themes/heo/components/Catalog.js
@@ -0,0 +1,90 @@
+import { useCallback, useEffect, useRef, useState } from 'react'
+import throttle from 'lodash.throttle'
+import { uuidToId } from 'notion-utils'
+import { useGlobal } from '@/lib/global'
+
+/**
+ * 目录导航组件
+ * @param toc
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const Catalog = ({ toc }) => {
+ const { locale } = useGlobal()
+ // 监听滚动事件
+ useEffect(() => {
+ window.addEventListener('scroll', actionSectionScrollSpy)
+ actionSectionScrollSpy()
+ return () => {
+ window.removeEventListener('scroll', actionSectionScrollSpy)
+ }
+ }, [])
+
+ // 目录自动滚动
+ const tRef = useRef(null)
+ const tocIds = []
+
+ // 同步选中目录事件
+ const [activeSection, setActiveSection] = useState(null)
+
+ const actionSectionScrollSpy = useCallback(throttle(() => {
+ const sections = document.getElementsByClassName('notion-h')
+ let prevBBox = null
+ let currentSectionId = activeSection
+ for (let i = 0; i < sections.length; ++i) {
+ const section = sections[i]
+ if (!section || !(section instanceof Element)) continue
+ if (!currentSectionId) {
+ currentSectionId = section.getAttribute('data-id')
+ }
+ const bbox = section.getBoundingClientRect()
+ const prevHeight = prevBBox ? bbox.top - prevBBox.bottom : 0
+ const offset = Math.max(150, prevHeight / 4)
+ // GetBoundingClientRect returns values relative to viewport
+ if (bbox.top - offset < 0) {
+ currentSectionId = section.getAttribute('data-id')
+ prevBBox = bbox
+ continue
+ }
+ // No need to continue loop, if last element has been detected
+ break
+ }
+ setActiveSection(currentSectionId)
+ const index = tocIds.indexOf(currentSectionId) || 0
+ tRef?.current?.scrollTo({ top: 28 * index, behavior: 'smooth' })
+ }, 200))
+
+ // 无目录就直接返回空
+ if (!toc || toc.length < 1) {
+ return <>>
+ }
+
+ return
+
{locale.COMMON.TABLE_OF_CONTENTS}
+
+
+
+
+
+}
+
+export default Catalog
diff --git a/themes/heo/components/CategoryBar.js b/themes/heo/components/CategoryBar.js
new file mode 100644
index 00000000..ee0a7a0c
--- /dev/null
+++ b/themes/heo/components/CategoryBar.js
@@ -0,0 +1,61 @@
+import { ChevronDoubleLeft, ChevronDoubleRight } from '@/components/HeroIcons'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useRef, useState } from 'react'
+
+/**
+ * 博客列表上方嵌入条
+ * @param {*} props
+ * @returns
+ */
+export default function CategoryBar(props) {
+ const { categoryOptions, border = true } = props
+ const { locale } = useGlobal()
+ const [scrollRight, setScrollRight] = useState(false)
+ // 创建一个ref引用
+ const categoryBarItemsRef = useRef(null)
+
+ // 点击#right时,滚动#category-bar-items到最右边
+ const handleToggleScroll = () => {
+ if (categoryBarItemsRef.current) {
+ const { scrollWidth, clientWidth } = categoryBarItemsRef.current
+ if (scrollRight) {
+ categoryBarItemsRef.current.scrollLeft = 0
+ } else {
+ categoryBarItemsRef.current.scrollLeft = scrollWidth - clientWidth
+ }
+ setScrollRight(!scrollRight)
+ }
+ }
+ return
+
+
+
+ {categoryOptions?.map((c, index) => )}
+
+
+
+
+ {scrollRight ? : }
+
+
+ {locale.MENU.CATEGORY}
+
+
+
+}
+
+/**
+ * 按钮
+ * @param {*} param0
+ * @returns
+ */
+const MenuItem = ({ href, name }) => {
+ const router = useRouter()
+ const selected = router.pathname === href
+ return
+ {name}
+
+}
diff --git a/themes/heo/components/CategoryGroup.js b/themes/heo/components/CategoryGroup.js
new file mode 100644
index 00000000..811ad9ff
--- /dev/null
+++ b/themes/heo/components/CategoryGroup.js
@@ -0,0 +1,31 @@
+import Link from 'next/link'
+import React from 'react'
+
+const CategoryGroup = ({ currentCategory, categories }) => {
+ if (!categories) {
+ return <>>
+ }
+ return <>
+
+ {categories.map(category => {
+ const selected = currentCategory === category.name
+ return (
+
+
+
{category.name}({category.count})
+
+
+ )
+ })}
+
+ >
+}
+
+export default CategoryGroup
diff --git a/themes/heo/components/DarkModeButton.js b/themes/heo/components/DarkModeButton.js
new file mode 100644
index 00000000..9b56e6da
--- /dev/null
+++ b/themes/heo/components/DarkModeButton.js
@@ -0,0 +1,38 @@
+import { useGlobal } from '@/lib/global'
+import { saveDarkModeToCookies } from '@/themes/theme'
+import { Moon, Sun } from '@/components/HeroIcons'
+import { useImperativeHandle } from 'react'
+
+/**
+ * 深色模式按钮
+ */
+const DarkModeButton = (props) => {
+ const { cRef, className } = props
+ const { isDarkMode, updateDarkMode } = useGlobal()
+
+ /**
+ * 对外暴露方法
+ */
+ useImperativeHandle(cRef, () => {
+ return {
+ handleChangeDarkMode: () => {
+ handleChangeDarkMode()
+ }
+ }
+ })
+
+ // 用户手动设置主题
+ const handleChangeDarkMode = () => {
+ const newStatus = !isDarkMode
+ saveDarkModeToCookies(newStatus)
+ updateDarkMode(newStatus)
+ const htmlElement = document.getElementsByTagName('html')[0]
+ htmlElement.classList?.remove(newStatus ? 'light' : 'dark')
+ htmlElement.classList?.add(newStatus ? 'dark' : 'light')
+ }
+
+ return
+}
+export default DarkModeButton
diff --git a/themes/heo/components/FloatDarkModeButton.js b/themes/heo/components/FloatDarkModeButton.js
new file mode 100644
index 00000000..f693d1f0
--- /dev/null
+++ b/themes/heo/components/FloatDarkModeButton.js
@@ -0,0 +1,31 @@
+import { useGlobal } from '@/lib/global'
+import { saveDarkModeToCookies } from '@/themes/theme'
+import CONFIG from '../config'
+
+export default function FloatDarkModeButton () {
+ const { isDarkMode, updateDarkMode } = useGlobal()
+
+ if (!CONFIG.WIDGET_DARK_MODE) {
+ return <>>
+ }
+
+ // 用户手动设置主题
+ const handleChangeDarkMode = () => {
+ const newStatus = !isDarkMode
+ saveDarkModeToCookies(newStatus)
+ updateDarkMode(newStatus)
+ const htmlElement = document.getElementsByTagName('html')[0]
+ htmlElement.classList?.remove(newStatus ? 'light' : 'dark')
+ htmlElement.classList?.add(newStatus ? 'dark' : 'light')
+ }
+
+ return (
+
+
+
+ )
+}
diff --git a/themes/heo/components/FloatTocButton.js b/themes/heo/components/FloatTocButton.js
new file mode 100644
index 00000000..7ce1eda0
--- /dev/null
+++ b/themes/heo/components/FloatTocButton.js
@@ -0,0 +1,44 @@
+import { useState } from 'react'
+import Catalog from './Catalog'
+
+/**
+ * 移动端悬浮目录按钮
+ */
+export default function FloatTocButton(props) {
+ const [tocVisible, changeTocVisible] = useState(false)
+
+ const { post } = props
+
+ const toggleToc = () => {
+ changeTocVisible(!tocVisible)
+ }
+
+ // 没有目录就隐藏该按钮
+ if (!post || !post.toc || post.toc.length < 1) {
+ return <>>
+ }
+
+ return (
+ {/* 按钮 */}
+
+
+
+
+ {/* 目录Modal */}
+
+ {/* 侧边菜单 */}
+
+ {post && <>
+
+
+
+ >
+ }
+
+
+ {/* 背景蒙版 */}
+
+
)
+}
diff --git a/themes/heo/components/Footer.js b/themes/heo/components/Footer.js
new file mode 100644
index 00000000..fc8f1465
--- /dev/null
+++ b/themes/heo/components/Footer.js
@@ -0,0 +1,58 @@
+import React from 'react'
+import BLOG from '@/blog.config'
+import SocialButton from './SocialButton'
+import { AdSlot } from '@/components/GoogleAdsense'
+// import DarkModeButton from '@/components/DarkModeButton'
+
+const Footer = ({ title }) => {
+ const d = new Date()
+ const currentYear = d.getFullYear()
+ const copyrightDate = (function () {
+ if (Number.isInteger(BLOG.SINCE) && BLOG.SINCE < currentYear) {
+ return BLOG.SINCE + '-' + currentYear
+ }
+ return currentYear
+ })()
+
+ return (
+
+ )
+}
+
+export default Footer
diff --git a/themes/heo/components/Hero.js b/themes/heo/components/Hero.js
new file mode 100644
index 00000000..4d4ea678
--- /dev/null
+++ b/themes/heo/components/Hero.js
@@ -0,0 +1,236 @@
+// import Image from 'next/image'
+
+import BLOG from '@/blog.config'
+import { ArrowSmallRight, PlusSmall } from '@/components/HeroIcons'
+import LazyImage from '@/components/LazyImage'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useImperativeHandle, useRef, useState } from 'react'
+import CONFIG from '../config'
+
+/**
+ * 顶部英雄区
+ * 左右布局,
+ * 左侧:banner组
+ * 右侧:今日卡牌遮罩
+ * @returns
+ */
+const Hero = props => {
+ return (
+
+
+
+
+ {/* 左侧banner组 */}
+
+
+ {/* 右侧置顶文章组 */}
+
+
+
+
+ )
+}
+
+/**
+ * 英雄区左侧banner组
+ * @returns
+ */
+function BannerGroup(props) {
+ return (
+ // 左侧英雄区
+
+ {/* 动图 */}
+
+ {/* 导航分类 */}
+
+
+ )
+}
+
+/**
+ * 英雄区左上角banner动图
+ * @returns
+ */
+function Banner(props) {
+ const router = useRouter()
+ const { latestPosts } = props
+ // 跳转到任意文章
+ function handleClickBanner() {
+ const randomIndex = Math.floor(Math.random() * latestPosts.length)
+ const randomPost = latestPosts[randomIndex]
+ router.push(randomPost.slug)
+ }
+
+ return
+
+
+
{CONFIG.HERO_TITLE_1}
{CONFIG.HERO_TITLE_2}
+
{CONFIG.HERO_TITLE_3}
+
+
+ {/* 斜向滚动的图标 */}
+
+
+ {/* 遮罩 */}
+
+
+
+}
+
+/**
+ * 图标滚动标签组
+ * 英雄区左上角banner条中斜向滚动的图标
+ */
+function TagsGroupBar() {
+ const groupIcons = CONFIG.GROUP_ICONS.concat(CONFIG.GROUP_ICONS)
+
+ return (
+
+
+ {groupIcons?.map((g, index) => {
+ return (
)
+ })}
+
+
+ )
+}
+
+/**
+ * 英雄区左下角3个指定分类按钮
+ * @returns
+ */
+function GroupMenu() {
+ return (
+
+
+
+ {CONFIG.HERO_CATEGORY_1?.title}
+
+
+
+
+
+
+
+
+ {CONFIG.HERO_CATEGORY_2?.title}
+
+
+
+
+
+
+ {/* 第三个标签在小屏上不显示 */}
+
+
+ {CONFIG.HERO_CATEGORY_3?.title}
+
+
+
+
+
+
+
+
+ )
+}
+
+/**
+ * 置顶文章区域
+ */
+function TopGroup(props) {
+ const { latestPosts, siteInfo } = props
+ const todayCardRef = useRef()
+ function handleMouseLeave() {
+ todayCardRef.current.coverUp()
+ }
+ return (
+
+ {/* 置顶最新文章 */}
+
+ {latestPosts?.map((p, index) => {
+ return
+
+
+
{p?.title}
+ {/* hover 悬浮的 ‘荐’ 字 */}
+
+ 荐
+
+
+
+ })}
+
+
+
+ )
+}
+
+/**
+ * 英雄区右侧,今日卡牌
+ * @returns
+ */
+function TodayCard({ cRef }) {
+ const router = useRouter()
+ // 卡牌是否盖住下层
+ const [isCoverUp, setIsCoverUp] = useState(true)
+
+ /**
+ * 外部可以调用此方法
+ */
+ useImperativeHandle(cRef, () => {
+ return {
+ coverUp: () => {
+ setIsCoverUp(true)
+ }
+ }
+ })
+
+ /**
+ * 点击更多
+ * @param {*} e
+ */
+ function handleClickMore(e) {
+ e.stopPropagation()
+ setIsCoverUp(false)
+ }
+
+ /**
+ * 点击卡片跳转的链接
+ * @param {*} e
+ */
+ function handleCardClick(e) {
+ router.push(CONFIG.HERO_TITLE_LINK)
+ }
+
+ return
+
+
+
+
{CONFIG.HERO_TITLE_4}
+
{CONFIG.HERO_TITLE_5}
+
+
+
+
+
+
+}
+
+export default Hero
diff --git a/themes/heo/components/HexoRecentComments.js b/themes/heo/components/HexoRecentComments.js
new file mode 100644
index 00000000..2ebf00c8
--- /dev/null
+++ b/themes/heo/components/HexoRecentComments.js
@@ -0,0 +1,47 @@
+import React from 'react'
+import BLOG from '@/blog.config'
+import Card from '@/themes/hexo/components/Card'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import { RecentComments } from '@waline/client'
+
+/**
+ * @see https://waline.js.org/guide/get-started.html
+ * @param {*} props
+ * @returns
+ */
+const HexoRecentComments = (props) => {
+ const [comments, updateComments] = React.useState([])
+ const { locale } = useGlobal()
+ const [onLoading, changeLoading] = React.useState(true)
+ React.useEffect(() => {
+ RecentComments({
+ serverURL: BLOG.COMMENT_WALINE_SERVER_URL,
+ count: 5
+ }).then(({ comments }) => {
+ changeLoading(false)
+ updateComments(comments)
+ })
+ }, [])
+
+ return (
+
+
+
+ {locale.COMMON.RECENT_COMMENTS}
+
+
+ {onLoading && Loading...
}
+ {!onLoading && comments && comments.length === 0 && No Comments
}
+ {!onLoading && comments && comments.length > 0 && comments.map((comment) =>
+
+
+ --{comment.nick}
+
+
)}
+
+
+ )
+}
+
+export default HexoRecentComments
diff --git a/themes/heo/components/InfoCard.js b/themes/heo/components/InfoCard.js
new file mode 100644
index 00000000..ad4727c8
--- /dev/null
+++ b/themes/heo/components/InfoCard.js
@@ -0,0 +1,87 @@
+import BLOG from '@/blog.config'
+import { ArrowRightCircle, GlobeAlt } from '@/components/HeroIcons'
+import LazyImage from '@/components/LazyImage'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useState } from 'react'
+import CONFIG from '../config'
+import Announcement from './Announcement'
+import Card from './Card'
+
+/**
+ * 社交信息卡
+ * @param {*} props
+ * @returns
+ */
+export function InfoCard(props) {
+ const { siteInfo, notice } = props
+ const router = useRouter()
+ // 在文章详情页特殊处理
+ const isSlugPage = router.pathname === '/[...slug]'
+
+ return (
+
+ {/* 信息卡牌第一行 */}
+
+
+
+ {BLOG.AUTHOR}
+
+
+ {/* 公告栏 */}
+
+
+
+
+ {/* 两个社交按钮 */}
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+/**
+ * 欢迎语
+ */
+function GreetingsWords() {
+ const greetings = CONFIG.INFOCARD_GREETINGS
+ const [greeting, setGreeting] = useState(greetings[0])
+ // 每次点击,随机获取greetings中的一个
+ const handleChangeGreeting = () => {
+ const randomIndex = Math.floor(Math.random() * greetings.length)
+ setGreeting(greetings[randomIndex])
+ }
+
+ return
+ {greeting}
+
+}
+
+/**
+ * 了解更多按鈕
+ * @returns
+ */
+function MoreButton() {
+ return
+
+
+}
diff --git a/themes/heo/components/JumpToCommentButton.js b/themes/heo/components/JumpToCommentButton.js
new file mode 100644
index 00000000..fb007712
--- /dev/null
+++ b/themes/heo/components/JumpToCommentButton.js
@@ -0,0 +1,29 @@
+import React from 'react'
+import CONFIG from '../config'
+
+/**
+ * 跳转到评论区
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const JumpToCommentButton = () => {
+ if (!CONFIG.WIDGET_TO_COMMENT) {
+ return <>>
+ }
+
+ function navToComment() {
+ if (document.getElementById('comment')) {
+ window.scrollTo({ top: document.getElementById('comment').offsetTop, behavior: 'smooth' })
+ }
+ // 兼容性不好
+ // const commentElement = document.getElementById('comment')
+ // if (commentElement) {
+ // commentElement?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' })
+ }
+
+ return (
+
+
)
+}
+
+export default JumpToCommentButton
diff --git a/themes/heo/components/JumpToTopButton.js b/themes/heo/components/JumpToTopButton.js
new file mode 100644
index 00000000..77313f46
--- /dev/null
+++ b/themes/heo/components/JumpToTopButton.js
@@ -0,0 +1,25 @@
+import { useGlobal } from '@/lib/global'
+import React from 'react'
+import CONFIG from '../config'
+
+/**
+ * 跳转到网页顶部
+ * 当屏幕下滑500像素后会出现该控件
+ * @param targetRef 关联高度的目标html标签
+ * @param showPercent 是否显示百分比
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const JumpToTopButton = ({ showPercent = true, percent }) => {
+ const { locale } = useGlobal()
+
+ if (!CONFIG.WIDGET_TO_TOP) {
+ return <>>
+ }
+ return ( window.scrollTo({ top: 0, behavior: 'smooth' })} >
+
+ {showPercent && (
{percent}
)}
+
)
+}
+
+export default JumpToTopButton
diff --git a/themes/heo/components/LatestPostsGroup.js b/themes/heo/components/LatestPostsGroup.js
new file mode 100644
index 00000000..cd836cee
--- /dev/null
+++ b/themes/heo/components/LatestPostsGroup.js
@@ -0,0 +1,49 @@
+import BLOG from '@/blog.config'
+import LazyImage from '@/components/LazyImage'
+import Link from 'next/link'
+
+/**
+ * 最新文章列表
+ * @param posts 所有文章数据
+ * @param sliceCount 截取展示的数量 默认6
+ * @constructor
+ */
+const LatestPostsGroup = ({ latestPosts, siteInfo }) => {
+ // 获取当前路径
+
+ if (!latestPosts) {
+ return <>>
+ }
+
+ return
+ {latestPosts.map(post => {
+ const headerImage = post?.pageCoverThumbnail ? post.pageCoverThumbnail : siteInfo?.pageCover
+
+ return (
+ (
+
+
+
+
+
+
+
+ )
+ )
+ })}
+
+}
+export default LatestPostsGroup
diff --git a/themes/heo/components/LatestPostsGroupMini.js b/themes/heo/components/LatestPostsGroupMini.js
new file mode 100644
index 00000000..bcf4d80a
--- /dev/null
+++ b/themes/heo/components/LatestPostsGroupMini.js
@@ -0,0 +1,63 @@
+import BLOG from '@/blog.config'
+import LazyImage from '@/components/LazyImage'
+import { useGlobal } from '@/lib/global'
+// import Image from 'next/image'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+/**
+ * 最新文章列表
+ * @param posts 所有文章数据
+ * @param sliceCount 截取展示的数量 默认6
+ * @constructor
+ */
+export default function LatestPostsGroupMini ({ latestPosts, siteInfo }) {
+ // 获取当前路径
+ const currentPath = useRouter().asPath
+ const { locale } = useGlobal()
+
+ if (!latestPosts) {
+ return <>>
+ }
+
+ return <>
+
+
+
+ {locale.COMMON.LATEST_POSTS}
+
+
+ {latestPosts.map(post => {
+ const selected = currentPath === `${BLOG.SUB_PATH}/${post.slug}`
+
+ const headerImage = post?.pageCoverThumbnail ? post.pageCoverThumbnail : siteInfo?.pageCover
+
+ return (
+ (
+
+
+
+
+
+
+
{post.title}
+
{post.lastEditedTime}
+
+
+
+ )
+ )
+ })}
+ >
+}
diff --git a/themes/heo/components/LoadingCover.js b/themes/heo/components/LoadingCover.js
new file mode 100644
index 00000000..c6418fad
--- /dev/null
+++ b/themes/heo/components/LoadingCover.js
@@ -0,0 +1,8 @@
+export default function LoadingCover () {
+ return (
+ )
+}
diff --git a/themes/heo/components/Logo.js b/themes/heo/components/Logo.js
new file mode 100644
index 00000000..5e45f4af
--- /dev/null
+++ b/themes/heo/components/Logo.js
@@ -0,0 +1,25 @@
+import BLOG from '@/blog.config'
+import { Home } from '@/components/HeroIcons'
+import LazyImage from '@/components/LazyImage'
+import Link from 'next/link'
+import React from 'react'
+
+const Logo = props => {
+ const { siteInfo } = props
+ return (
+
+
+
+
+
+ {siteInfo?.title || BLOG.TITLE}
+
+
+
+
+
+
+
+ )
+}
+export default Logo
diff --git a/themes/heo/components/MenuGroupCard.js b/themes/heo/components/MenuGroupCard.js
new file mode 100644
index 00000000..89591369
--- /dev/null
+++ b/themes/heo/components/MenuGroupCard.js
@@ -0,0 +1,44 @@
+import React from 'react'
+import Link from 'next/link'
+import { useGlobal } from '@/lib/global'
+import CONFIG from '../config'
+
+const MenuGroupCard = (props) => {
+ const { postCount, categoryOptions, tagOptions } = props
+ const { locale } = useGlobal()
+ const archiveSlot = {postCount}
+ const categorySlot = {categoryOptions?.length}
+ const tagSlot = {tagOptions?.length}
+
+ const links = [
+ { name: locale.COMMON.ARTICLE, to: '/archive', slot: archiveSlot, show: CONFIG.MENU_ARCHIVE },
+ { name: locale.COMMON.CATEGORY, to: '/category', slot: categorySlot, show: CONFIG.MENU_CATEGORY },
+ { name: locale.COMMON.TAGS, to: '/tag', slot: tagSlot, show: CONFIG.MENU_TAG }
+ ]
+
+ return (
+
+ )
+}
+export default MenuGroupCard
diff --git a/themes/heo/components/MenuItemCollapse.js b/themes/heo/components/MenuItemCollapse.js
new file mode 100644
index 00000000..c69ca231
--- /dev/null
+++ b/themes/heo/components/MenuItemCollapse.js
@@ -0,0 +1,54 @@
+import Collapse from '@/components/Collapse'
+import Link from 'next/link'
+import { useState } from 'react'
+
+/**
+ * 折叠菜单
+ * @param {*} param0
+ * @returns
+ */
+export const MenuItemCollapse = ({ link }) => {
+ const [show, changeShow] = useState(false)
+ const hasSubMenu = link?.subMenus?.length > 0
+
+ const [isOpen, changeIsOpen] = useState(false)
+
+ const toggleShow = () => {
+ changeShow(!show)
+ }
+
+ const toggleOpenSubMenu = () => {
+ changeIsOpen(!isOpen)
+ }
+
+ if (!link || !link.show) {
+ return null
+ }
+
+ return <>
+
+ {!hasSubMenu &&
+
{link?.icon && }{link?.name}
+ }
+ {hasSubMenu &&
+ {link?.icon && }{link?.name}
+
+
}
+
+
+ {/* 折叠子菜单 */}
+ {hasSubMenu &&
+ {link.subMenus.map(sLink => {
+ return
+
+ {link?.icon && } {sLink.title}
+
+
+ })}
+ }
+ >
+}
diff --git a/themes/heo/components/MenuItemDrop.js b/themes/heo/components/MenuItemDrop.js
new file mode 100644
index 00000000..7a56aa83
--- /dev/null
+++ b/themes/heo/components/MenuItemDrop.js
@@ -0,0 +1,41 @@
+import Link from 'next/link'
+import { useState } from 'react'
+
+export const MenuItemDrop = ({ link }) => {
+ const [show, changeShow] = useState(false)
+ const hasSubMenu = link?.subMenus?.length > 0
+
+ if (!link || !link.show) {
+ return null
+ }
+
+ return changeShow(true)} onMouseOut={() => changeShow(false)} >
+
+ {/* 不含子菜单 */}
+ {!hasSubMenu &&
+
+ {link?.icon &&
} {link?.name}
+ }
+
+ {/* 含子菜单的按钮 */}
+ {hasSubMenu && <>
+
+ {link?.icon && } {link?.name}
+
+ >}
+
+ {/* 子菜单 */}
+ {hasSubMenu &&
+ {link.subMenus.map((sLink, index) => {
+ return -
+
+ {link?.icon && }{sLink.title}
+
+
+ })}
+
}
+
+
+}
diff --git a/themes/heo/components/MenuListSide.js b/themes/heo/components/MenuListSide.js
new file mode 100644
index 00000000..19f70863
--- /dev/null
+++ b/themes/heo/components/MenuListSide.js
@@ -0,0 +1,37 @@
+import React from 'react'
+import { useGlobal } from '@/lib/global'
+import BLOG from '@/blog.config'
+import { MenuItemCollapse } from './MenuItemCollapse'
+import CONFIG from '../config'
+
+export const MenuListSide = (props) => {
+ const { customNav, customMenu } = props
+ const { locale } = useGlobal()
+
+ let links = [
+ { icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE },
+ { icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH },
+ { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG.MENU_CATEGORY },
+ { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG.MENU_TAG }
+ ]
+
+ if (customNav) {
+ links = customNav.concat(links)
+ }
+
+ // 如果 开启自定义菜单,则覆盖Page生成的菜单
+ if (BLOG.CUSTOM_MENU) {
+ links = customMenu
+ }
+
+ if (!links || links.length === 0) {
+ return null
+ }
+
+ return (
+
+ )
+}
diff --git a/themes/heo/components/MenuListTop.js b/themes/heo/components/MenuListTop.js
new file mode 100644
index 00000000..381b5adb
--- /dev/null
+++ b/themes/heo/components/MenuListTop.js
@@ -0,0 +1,35 @@
+import React from 'react'
+import { useGlobal } from '@/lib/global'
+import CONFIG from '../config'
+import BLOG from '@/blog.config'
+import { MenuItemDrop } from './MenuItemDrop'
+
+export const MenuListTop = (props) => {
+ const { customNav, customMenu } = props
+ const { locale } = useGlobal()
+
+ let links = [
+ { id: 1, icon: 'fa-solid fa-house', name: locale.NAV.INDEX, to: '/', show: CONFIG.MENU_INDEX },
+ { id: 2, icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG.MENU_SEARCH },
+ { id: 3, icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG.MENU_ARCHIVE }
+ ]
+
+ if (customNav) {
+ links = links.concat(customNav)
+ }
+
+ // 如果 开启自定义菜单,则覆盖Page生成的菜单
+ if (BLOG.CUSTOM_MENU) {
+ links = customMenu
+ }
+
+ if (!links || links.length === 0) {
+ return null
+ }
+
+ return (<>
+
+ >)
+}
diff --git a/themes/heo/components/NavBar.js b/themes/heo/components/NavBar.js
new file mode 100644
index 00000000..af69384a
--- /dev/null
+++ b/themes/heo/components/NavBar.js
@@ -0,0 +1,165 @@
+import { useCallback, useEffect, useRef, useState } from 'react'
+import Logo from './Logo'
+import throttle from 'lodash.throttle'
+import RandomPostButton from './RandomPostButton'
+import SearchButton from './SearchButton'
+import DarkModeButton from './DarkModeButton'
+import SlideOver from './SlideOver'
+import ReadingProgress from './ReadingProgress'
+import { MenuListTop } from './MenuListTop'
+import { isBrowser } from '@/lib/utils'
+import BLOG from '@/blog.config'
+/**
+ * 顶部导航
+ * @param {*} param0
+ * @returns
+ */
+const NavBar = props => {
+ const [fixedNav, setFixedNav] = useState(false)
+ const [textWhite, setTextWhite] = useState(false)
+ const [navBgWhite, setBgWhite] = useState(false)
+
+ const [activeIndex, setActiveIndex] = useState(0)
+
+ const slideOverRef = useRef()
+
+ const toggleMenuOpen = () => {
+ slideOverRef?.current?.toggleSlideOvers()
+ }
+
+ /**
+ * 根据滚动条,切换导航栏样式
+ */
+ const scrollTrigger = useCallback(throttle(() => {
+ const scrollS = window.scrollY
+ // 导航栏设置 白色背景
+ if (scrollS <= 0) {
+ setFixedNav(false)
+ setBgWhite(false)
+
+ // 文章详情页特殊处理
+ if (document.querySelector('#post-bg')) {
+ setFixedNav(true)
+ setTextWhite(true)
+ setBgWhite(false)
+ }
+ } else {
+ // 向下滚动后的导航样式
+ setFixedNav(true)
+ setTextWhite(false)
+ setBgWhite(true)
+ }
+ }, 200))
+
+ // 监听滚动
+ useEffect(() => {
+ scrollTrigger()
+ window.addEventListener('scroll', scrollTrigger)
+ return () => {
+ window.removeEventListener('scroll', scrollTrigger)
+ }
+ }, [])
+
+ // 监听导航栏显示文字
+ useEffect(() => {
+ let prevScrollY = 0
+ let ticking = false
+
+ const handleScroll = () => {
+ if (!ticking) {
+ window.requestAnimationFrame(() => {
+ const currentScrollY = window.scrollY
+
+ if (currentScrollY > prevScrollY) {
+ setActiveIndex(1) // 向下滚动时设置activeIndex为1
+ } else {
+ setActiveIndex(0) // 向上滚动时设置activeIndex为0
+ }
+
+ prevScrollY = currentScrollY
+ ticking = false
+ })
+
+ ticking = true
+ }
+ }
+
+ if (isBrowser()) {
+ window.addEventListener('scroll', handleScroll)
+ }
+
+ return () => {
+ if (isBrowser()) {
+ window.removeEventListener('scroll', handleScroll)
+ }
+ }
+ }, [])
+
+ return (<>
+
+
+ {/* 顶部导航菜单栏 */}
+
+ >)
+}
+
+export default NavBar
diff --git a/themes/heo/components/NavButtonGroup.js b/themes/heo/components/NavButtonGroup.js
new file mode 100644
index 00000000..2a3fc898
--- /dev/null
+++ b/themes/heo/components/NavButtonGroup.js
@@ -0,0 +1,33 @@
+
+import React from 'react'
+import Link from 'next/link'
+
+/**
+ * 首页导航大按钮组件
+ * @param {*} props
+ * @returns
+ */
+const NavButtonGroup = (props) => {
+ const { categoryOptions } = props
+ if (!categoryOptions || categoryOptions.length === 0) {
+ return <>>
+ }
+
+ return (
+
+ )
+}
+export default NavButtonGroup
diff --git a/themes/heo/components/NoticeBar.js b/themes/heo/components/NoticeBar.js
new file mode 100644
index 00000000..b4af5086
--- /dev/null
+++ b/themes/heo/components/NoticeBar.js
@@ -0,0 +1,27 @@
+
+import { ArrowRightCircle } from '@/components/HeroIcons'
+import CONFIG from '../config'
+import Swipe from './Swipe'
+
+/**
+ * 通知横幅
+ */
+export function NoticeBar() {
+ const notices = CONFIG.NOTICE_BAR
+
+ if (!notices || notices?.length === 0) {
+ return <>>
+ }
+
+ return (
+
+ )
+}
diff --git a/themes/heo/components/PaginationNumber.js b/themes/heo/components/PaginationNumber.js
new file mode 100644
index 00000000..4a1022ee
--- /dev/null
+++ b/themes/heo/components/PaginationNumber.js
@@ -0,0 +1,191 @@
+import { ChevronDoubleRight } from '@/components/HeroIcons'
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useState } from 'react'
+
+/**
+ * 数字翻页插件
+ * @param page 当前页码
+ * @param showNext 是否有下一页
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const PaginationNumber = ({ page, totalPage }) => {
+ const router = useRouter()
+ const { locale } = useGlobal()
+ const currentPage = +page
+ const showNext = page < totalPage
+ const showPrev = currentPage !== 1
+ const pagePrefix = router.asPath.split('?')[0].replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '')
+ const pages = generatePages(pagePrefix, page, currentPage, totalPage)
+
+ const [value, setValue] = useState('')
+
+ const handleInputChange = (event) => {
+ const newValue = event.target.value.replace(/[^0-9]/g, '')
+ setValue(newValue)
+ }
+
+ /**
+ * 调到指定页
+ */
+ const jumpToPage = () => {
+ if (value) {
+ router.push(value === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${value}`)
+ }
+ }
+
+ return (<>
+
+ {/* pc端分页按钮 */}
+
+ {/* 上一页 */}
+
+
+
+
+ {locale.PAGINATION.PREV}
+
+
+
+
+
+ {/* 分页 */}
+
+ {pages}
+
+ {/* 跳转页码 */}
+
+
+
+ {/* 下一页 */}
+
+
+
+
+
+ {locale.PAGINATION.NEXT}
+
+
+
+
+
+ {/* 移动端分页 */}
+
+
+ {/* 上一页 */}
+
+ {locale.PAGINATION.PREV}
+
+
+ {showPrev && showNext &&
}
+
+ {/* 下一页 */}
+
+ {locale.PAGINATION.NEXT}
+
+
+ >)
+}
+
+/**
+ * 页码按钮
+ * @param {*} page
+ * @param {*} currentPage
+ * @param {*} pagePrefix
+ * @returns
+ */
+function getPageElement(page, currentPage, pagePrefix) {
+ const selected = page + '' === currentPage + ''
+ return (
+ (
+
+ {page}
+
+ )
+ )
+}
+
+/**
+ * 获取所有页码
+ * @param {*} pagePrefix
+ * @param {*} page
+ * @param {*} currentPage
+ * @param {*} totalPage
+ * @returns
+ */
+function generatePages(pagePrefix, page, currentPage, totalPage) {
+ const pages = []
+ const groupCount = 7 // 最多显示页签数
+ if (totalPage <= groupCount) {
+ for (let i = 1; i <= totalPage; i++) {
+ pages.push(getPageElement(i, page, pagePrefix))
+ }
+ } else {
+ pages.push(getPageElement(1, page, pagePrefix))
+ const dynamicGroupCount = groupCount - 2
+ let startPage = currentPage - 2
+ if (startPage <= 1) {
+ startPage = 2
+ }
+ if (startPage + dynamicGroupCount > totalPage) {
+ startPage = totalPage - dynamicGroupCount
+ }
+ if (startPage > 2) {
+ pages.push(...
)
+ }
+
+ for (let i = 0; i < dynamicGroupCount; i++) {
+ if (startPage + i < totalPage) {
+ pages.push(getPageElement(startPage + i, page, pagePrefix))
+ }
+ }
+
+ if (startPage + dynamicGroupCount < totalPage) {
+ pages.push(...
)
+ }
+
+ pages.push(getPageElement(totalPage, page, pagePrefix))
+ }
+ return pages
+}
+export default PaginationNumber
diff --git a/themes/heo/components/PostHeader.js b/themes/heo/components/PostHeader.js
new file mode 100644
index 00000000..55f1a37c
--- /dev/null
+++ b/themes/heo/components/PostHeader.js
@@ -0,0 +1,100 @@
+import Link from 'next/link'
+import BLOG from '@/blog.config'
+import NotionIcon from '@/components/NotionIcon'
+import WavesArea from './WavesArea'
+import { HashTag } from '@/components/HeroIcons'
+import WordCount from '@/components/WordCount'
+import LazyImage from '@/components/LazyImage'
+
+export default function PostHeader({ post, siteInfo }) {
+ if (!post) {
+ return <>>
+ }
+ // 文章头图
+ const headerImage = post?.pageCover ? post.pageCover : siteInfo?.pageCover
+
+ return (
+
+
+
+
+
+ {/* 文章背景图 */}
+
+
+
+
+ {/* 文章文字描述 */}
+
+ {/* 分类+标签 */}
+
+ {post.category && <>
+
+
+ {post.category}
+
+
+ >}
+
+ {post.tagItems && (
+
+ {post.tagItems.map((tag, index) => (
+
+
{tag.name + (tag.count ? `(${tag.count})` : '')}
+
+
+ ))}
+
+ )}
+
+
+ {/* 文章Title */}
+
+ {post.title}
+
+
+ {/* 标题底部补充信息 */}
+
+
+
+
+ {post?.type !== 'Page' && (
+ <>
+
+
{post?.publishTime}
+
+ >
+ )}
+
+
+
+ {BLOG.ANALYTICS_BUSUANZI_ENABLE &&
+
+
}
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/themes/heo/components/RandomPostButton.js b/themes/heo/components/RandomPostButton.js
new file mode 100644
index 00000000..66d6cc85
--- /dev/null
+++ b/themes/heo/components/RandomPostButton.js
@@ -0,0 +1,19 @@
+import { useRouter } from 'next/router'
+
+/**
+ * 随机跳转到一个文章
+ */
+export default function RandomPostButton(props) {
+ const { latestPosts } = props
+ const router = useRouter()
+ function handleClick() {
+ const randomIndex = Math.floor(Math.random() * latestPosts.length)
+ const randomPost = latestPosts[randomIndex]
+ router.push(randomPost.slug)
+ }
+ return (
+
+
+
+ )
+}
diff --git a/themes/heo/components/ReadingProgress.js b/themes/heo/components/ReadingProgress.js
new file mode 100644
index 00000000..0f9c55bb
--- /dev/null
+++ b/themes/heo/components/ReadingProgress.js
@@ -0,0 +1,56 @@
+import { ArrowSmallUp } from '@/components/HeroIcons'
+import { useEffect, useState } from 'react'
+
+/**
+ * 回顶按钮
+ * @returns
+ */
+export default function ReadingProgress() {
+ const [scrollPercentage, setScrollPercentage] = useState(0)
+
+ function handleScroll() {
+ const scrollHeight = document.documentElement.scrollHeight
+ const clientHeight = document.documentElement.clientHeight
+ const scrollY = window.scrollY || window.pageYOffset
+
+ const percent = Math.floor((scrollY / (scrollHeight - clientHeight - 20)) * 100)
+ setScrollPercentage(percent)
+ }
+
+ // 监听滚动事件
+ useEffect(() => {
+ let requestId
+
+ function updateScrollPercentage() {
+ handleScroll()
+ requestId = null
+ }
+
+ function handleAnimationFrame() {
+ if (requestId) {
+ return
+ }
+ requestId = requestAnimationFrame(updateScrollPercentage)
+ }
+
+ window.addEventListener('scroll', handleAnimationFrame)
+ return () => {
+ window.removeEventListener('scroll', handleAnimationFrame)
+ if (requestId) {
+ cancelAnimationFrame(requestId)
+ }
+ }
+ }, [])
+
+ return ( window.scrollTo({ top: 0, behavior: 'smooth' })}
+ className={`${scrollPercentage > 0 ? 'w-10 h-10 ' : 'w-0 h-0 opacity-0'} group cursor-pointer hover:bg-black hover:bg-opacity-10 rounded-full flex justify-center items-center duration-200 transition-all`}
+ >
+
+
+ {scrollPercentage < 100 ? scrollPercentage :
}
+
+
+ )
+}
diff --git a/themes/heo/components/SearchButton.js b/themes/heo/components/SearchButton.js
new file mode 100644
index 00000000..aee73e49
--- /dev/null
+++ b/themes/heo/components/SearchButton.js
@@ -0,0 +1,9 @@
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+
+export default function SearchButton() {
+ const { locale } = useGlobal()
+ return
+
+
+}
diff --git a/themes/heo/components/SearchDrawer.js b/themes/heo/components/SearchDrawer.js
new file mode 100644
index 00000000..c7ec88a7
--- /dev/null
+++ b/themes/heo/components/SearchDrawer.js
@@ -0,0 +1,36 @@
+import { Router } from 'next/router'
+import { useImperativeHandle, useRef } from 'react'
+import SearchInput from './SearchInput'
+const SearchDrawer = ({ cRef, slot }) => {
+ const searchDrawer = useRef()
+ const searchInputRef = useRef()
+ useImperativeHandle(cRef, () => {
+ return {
+ show: () => {
+ searchDrawer?.current?.classList?.remove('hidden')
+ searchInputRef?.current?.focus()
+ }
+ }
+ })
+ const hidden = () => {
+ searchDrawer?.current?.classList?.add('hidden')
+ }
+ Router.events.on('routeChangeComplete', (...args) => {
+ hidden()
+ })
+ return (
+
+ )
+}
+
+export default SearchDrawer
diff --git a/themes/heo/components/SearchInput.js b/themes/heo/components/SearchInput.js
new file mode 100644
index 00000000..6e577bba
--- /dev/null
+++ b/themes/heo/components/SearchInput.js
@@ -0,0 +1,106 @@
+import { useRouter } from 'next/router'
+import { useImperativeHandle, useRef, useState } from 'react'
+import { useGlobal } from '@/lib/global'
+let lock = false
+
+const SearchInput = props => {
+ const { currentSearch, cRef, className } = props
+ const [onLoading, setLoadingState] = useState(false)
+ const router = useRouter()
+ const searchInputRef = useRef()
+ const { locale } = useGlobal()
+ useImperativeHandle(cRef, () => {
+ return {
+ focus: () => {
+ searchInputRef?.current?.focus()
+ }
+ }
+ })
+
+ const handleSearch = () => {
+ const key = searchInputRef.current.value
+ if (key && key !== '') {
+ setLoadingState(true)
+ router.push({ pathname: '/search/' + key }).then(r => {
+ setLoadingState(false)
+ })
+ // location.href = '/search/' + key
+ } else {
+ router.push({ pathname: '/' }).then(r => {})
+ }
+ }
+ const handleKeyUp = e => {
+ if (e.keyCode === 13) {
+ // 回车
+ handleSearch(searchInputRef.current.value)
+ } else if (e.keyCode === 27) {
+ // ESC
+ cleanSearch()
+ }
+ }
+ const cleanSearch = () => {
+ searchInputRef.current.value = ''
+ }
+
+ const [showClean, setShowClean] = useState(false)
+ const updateSearchKey = val => {
+ if (lock) {
+ return
+ }
+ searchInputRef.current.value = val
+
+ if (val) {
+ setShowClean(true)
+ } else {
+ setShowClean(false)
+ }
+ }
+ function lockSearchInput () {
+ lock = true
+ }
+
+ function unLockSearchInput () {
+ lock = false
+ }
+
+ return (
+
+
updateSearchKey(e.target.value)}
+ defaultValue={currentSearch || ''}
+ />
+
+
+
+
+
+ {showClean && (
+
+
+
+ )}
+
+ )
+}
+
+export default SearchInput
diff --git a/themes/heo/components/SearchNav.js b/themes/heo/components/SearchNav.js
new file mode 100644
index 00000000..7be2b8ce
--- /dev/null
+++ b/themes/heo/components/SearchNav.js
@@ -0,0 +1,68 @@
+import { useGlobal } from '@/lib/global'
+import Link from 'next/link'
+import { useEffect, useRef } from 'react'
+import Card from './Card'
+import SearchInput from './SearchInput'
+import TagItemMini from './TagItemMini'
+
+/**
+ * 搜索页面的导航
+ * @param {*} props
+ * @returns
+ */
+export default function SearchNav(props) {
+ const { tagOptions, categoryOptions } = props
+ const cRef = useRef(null)
+ const { locale } = useGlobal()
+ useEffect(() => {
+ // 自动聚焦到搜索框
+ cRef?.current?.focus()
+ }, [])
+
+ return <>
+
+
+ {/* 分类 */}
+
+
+ {locale.COMMON.CATEGORY}:
+
+
+ {categoryOptions?.map(category => {
+ return (
+
+
+
+ {category.name}({category.count})
+
+
+ )
+ })}
+
+
+ {/* 标签 */}
+
+
+ {locale.COMMON.TAGS}:
+
+
+
+
+>
+}
diff --git a/themes/heo/components/SideBar.js b/themes/heo/components/SideBar.js
new file mode 100644
index 00000000..cb7427ac
--- /dev/null
+++ b/themes/heo/components/SideBar.js
@@ -0,0 +1,34 @@
+import BLOG from '@/blog.config'
+import LazyImage from '@/components/LazyImage'
+import { useRouter } from 'next/router'
+import MenuGroupCard from './MenuGroupCard'
+import { MenuListSide } from './MenuListSide'
+
+/**
+ * 侧边抽屉
+ * @param tags
+ * @param currentTag
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const SideBar = (props) => {
+ const { siteInfo } = props
+ const router = useRouter()
+ return (
+
+
+
+
{ router.push('/') }}
+ className='justify-center items-center flex hover:rotate-45 py-6 hover:scale-105 dark:text-gray-100 transform duration-200 cursor-pointer'>
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
+
+
+
+
+
+
+ )
+}
+
+export default SideBar
diff --git a/themes/heo/components/SideBarDrawer.js b/themes/heo/components/SideBarDrawer.js
new file mode 100644
index 00000000..87125c05
--- /dev/null
+++ b/themes/heo/components/SideBarDrawer.js
@@ -0,0 +1,51 @@
+import { useRouter } from 'next/router'
+import { useEffect } from 'react'
+
+/**
+ * 侧边栏抽屉面板,可以从侧面拉出
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const SideBarDrawer = ({ children, isOpen, onOpen, onClose, className }) => {
+ const router = useRouter()
+ useEffect(() => {
+ const sideBarDrawerRouteListener = () => {
+ switchSideDrawerVisible(false)
+ }
+ router.events.on('routeChangeComplete', sideBarDrawerRouteListener)
+ return () => {
+ router.events.off('routeChangeComplete', sideBarDrawerRouteListener)
+ }
+ }, [router.events])
+
+ // 点击按钮更改侧边抽屉状态
+ const switchSideDrawerVisible = (showStatus) => {
+ if (showStatus) {
+ onOpen && onOpen()
+ } else {
+ onClose && onClose()
+ }
+ const sideBarDrawer = window.document.getElementById('sidebar-drawer')
+ const sideBarDrawerBackground = window.document.getElementById('sidebar-drawer-background')
+
+ if (showStatus) {
+ sideBarDrawer?.classList.replace('-mr-72', 'mr-0')
+ sideBarDrawerBackground?.classList.replace('hidden', 'block')
+ } else {
+ sideBarDrawer?.classList.replace('mr-0', '-mr-72')
+ sideBarDrawerBackground?.classList.replace('block', 'hidden')
+ }
+ }
+
+ return
+ )
+}
diff --git a/themes/heo/components/WavesArea.js b/themes/heo/components/WavesArea.js
new file mode 100644
index 00000000..b8f1198a
--- /dev/null
+++ b/themes/heo/components/WavesArea.js
@@ -0,0 +1,65 @@
+import { useGlobal } from '@/lib/global'
+
+/**
+ * 文章波浪动画
+ */
+export default function WavesArea() {
+ const { isDarkMode } = useGlobal()
+ const color = isDarkMode ? '#18171d' : '#f7f9fe'
+
+ return (
+
+
+
+
+ )
+}
diff --git a/themes/heo/config.js b/themes/heo/config.js
new file mode 100644
index 00000000..86ed342e
--- /dev/null
+++ b/themes/heo/config.js
@@ -0,0 +1,130 @@
+const CONFIG = {
+ HOME_BANNER_ENABLE: true,
+
+ SITE_CREATE_TIME: '2021-09-21', // 建站日期,用于计算网站运行的第几天
+
+ // 首页顶部通知条滚动内容,如不需要可以留空 []
+ NOTICE_BAR: [
+ { title: '欢迎来到我的博客', url: 'https://blog.tangly1024.com' },
+ { title: '访问文档中心获取更多帮助', url: 'https://docs.tangly1024.com' }
+ ],
+
+ // 英雄区(首页顶部大卡)
+ HERO_TITLE_1: '分享编程',
+ HERO_TITLE_2: '与思维认知',
+ HERO_TITLE_3: 'TANGLY1024.COM',
+ HERO_TITLE_4: '新版上线',
+ HERO_TITLE_5: 'NotionNext4.0 轻松定制主题',
+ HERO_TITLE_LINK: 'https://tangly1024.com',
+
+ // 英雄区显示三个置顶分类
+ HERO_CATEGORY_1: { title: '必看精选', url: '/tag/必看精选' },
+ HERO_CATEGORY_2: { title: '热门文章', url: '/tag/热门文章' },
+ HERO_CATEGORY_3: { title: '实用教程', url: '/tag/实用教程' },
+
+ // 右侧个人资料卡牌欢迎语,点击可自动切换
+ INFOCARD_GREETINGS: [
+ '你好!我是',
+ '🔍 分享与热心帮助',
+ '🤝 专修交互与设计',
+ '🏃 脚踏实地行动派',
+ '🏠 智能家居小能手',
+ '🤖️ 数码科技爱好者',
+ '🧱 团队小组发动机'
+ ],
+ INFO_CARD_URL: 'https://github.com/tangly1024/NotionNext', // 个人资料底部按钮链接
+
+ // 用户技能图标
+ GROUP_ICONS: [
+ {
+ title_1: 'AfterEffect',
+ img_1: '/images/heo/20239df3f66615b532ce571eac6d14ff21cf072602.webp',
+ color_1: '#989bf8',
+ title_2: 'Sketch',
+ img_2: '/images/heo/2023e0ded7b724a39f12d59c3dc8fbdc7cbe074202.webp',
+ color_2: '#ffffff'
+ },
+ {
+ title_1: 'Docker',
+ img_1: '/images/heo/20231108a540b2862d26f8850172e4ea58ed075102.webp',
+ color_1: '#57b6e6',
+ title_2: 'Photoshop',
+ img_2: '/images/heo/2023e4058a91608ea41751c4f102b131f267075902.webp',
+ color_2: '#4082c3'
+ },
+ {
+ title_1: 'FinalCutPro',
+ img_1: '/images/heo/20233e777652412247dd57fd9b48cf997c01070702.webp',
+ color_1: '#ffffff',
+ title_2: 'Python',
+ img_2: '/images/heo/20235c0731cd4c0c95fc136a8db961fdf963071502.webp',
+ color_2: '#ffffff'
+ },
+ {
+ title_1: 'Swift',
+ img_1: '/images/heo/202328bbee0b314297917b327df4a704db5c072402.webp',
+ color_1: '#eb6840',
+ title_2: 'Principle',
+ img_2: '/images/heo/2023f76570d2770c8e84801f7e107cd911b5073202.webp',
+ color_2: '#8f55ba'
+ },
+ {
+ title_1: 'illustrator',
+ img_1: '/images/heo/20237359d71b45ab77829cee5972e36f8c30073902.webp',
+ color_1: '#f29e39',
+ title_2: 'CSS3',
+ img_2: '/images/heo/20237c548846044a20dad68a13c0f0e1502f074602.webp',
+ color_2: '#2c51db'
+ },
+ {
+ title_1: 'JS',
+ img_1: '/images/heo/2023786e7fc488f453d5fb2be760c96185c0075502.webp',
+ color_1: '#f7cb4f',
+ title_2: 'HTML',
+ img_2: '/images/heo/202372b4d760fd8a497d442140c295655426070302.webp',
+ color_2: '#e9572b'
+ },
+ {
+ title_1: 'Git',
+ img_1: '/images/heo/2023ffa5707c4e25b6beb3e6a3d286ede4c6071102.webp',
+ color_1: '#df5b40',
+ title_2: 'Rhino',
+ img_2: '/images/heo/20231ca53fa0b09a3ff1df89acd7515e9516173302.webp',
+ color_2: '#1f1f1f'
+ }
+ ],
+
+ SOCIAL_CARD: true, // 是否显示右侧,点击加入社群按钮
+ SOCIAL_CARD_TITLE_1: '交流频道',
+ SOCIAL_CARD_TITLE_2: '加入我们的社群讨论分享',
+ SOCIAL_CARD_TITLE_3: '点击加入社群',
+ SOCIAL_CARD_URL: 'https://docs.tangly1024.com/article/how-to-question',
+
+ // ***** 以下配置无效,只是预留开发 ****
+ // 菜单配置
+ MENU_INDEX: true, // 显示首页
+ MENU_CATEGORY: true, // 显示分类
+ MENU_TAG: true, // 显示标签
+ MENU_ARCHIVE: true, // 显示归档
+ MENU_SEARCH: true, // 显示搜索
+
+ POST_LIST_COVER: true, // 列表显示文章封面
+ POST_LIST_COVER_HOVER_ENLARGE: false, // 列表鼠标悬停放大
+
+ POST_LIST_COVER_DEFAULT: true, // 封面为空时用站点背景做默认封面
+ POST_LIST_SUMMARY: true, // 文章摘要
+ POST_LIST_PREVIEW: false, // 读取文章预览
+ POST_LIST_IMG_CROSSOVER: true, // 博客列表图片左右交错
+
+ ARTICLE_ADJACENT: true, // 显示上一篇下一篇文章推荐
+ ARTICLE_COPYRIGHT: true, // 显示文章版权声明
+ ARTICLE_RECOMMEND: true, // 文章关联推荐
+
+ WIDGET_LATEST_POSTS: true, // 显示最新文章卡
+ WIDGET_ANALYTICS: false, // 显示统计卡
+ WIDGET_TO_TOP: true,
+ WIDGET_TO_COMMENT: true, // 跳到评论区
+ WIDGET_DARK_MODE: true, // 夜间模式
+ WIDGET_TOC: true // 移动端悬浮目录
+}
+export default CONFIG
diff --git a/themes/heo/index.js b/themes/heo/index.js
new file mode 100644
index 00000000..60b955b8
--- /dev/null
+++ b/themes/heo/index.js
@@ -0,0 +1,423 @@
+import CONFIG from './config'
+
+import CommonHead from '@/components/CommonHead'
+import { useEffect } from 'react'
+import Footer from './components/Footer'
+import SideRight from './components/SideRight'
+import NavBar from './components/NavBar'
+import { useGlobal } from '@/lib/global'
+import BLOG from '@/blog.config'
+import BlogPostListPage from './components/BlogPostListPage'
+import BlogPostListScroll from './components/BlogPostListScroll'
+import Hero from './components/Hero'
+import { useRouter } from 'next/router'
+import SearchNav from './components/SearchNav'
+import BlogPostArchive from './components/BlogPostArchive'
+import { ArticleLock } from './components/ArticleLock'
+import PostHeader from './components/PostHeader'
+import Comment from '@/components/Comment'
+import NotionPage from '@/components/NotionPage'
+import ArticleAdjacent from './components/ArticleAdjacent'
+import ArticleCopyright from './components/ArticleCopyright'
+import ArticleRecommend from './components/ArticleRecommend'
+import ShareBar from '@/components/ShareBar'
+import Link from 'next/link'
+import CategoryBar from './components/CategoryBar'
+import { Transition } from '@headlessui/react'
+import { Style } from './style'
+import { NoticeBar } from './components/NoticeBar'
+import { HashTag } from '@/components/HeroIcons'
+import LatestPostsGroup from './components/LatestPostsGroup'
+import FloatTocButton from './components/FloatTocButton'
+import replaceSearchResult from '@/components/Mark'
+import LazyImage from '@/components/LazyImage'
+
+/**
+ * 基础布局 采用上中下布局,移动端使用顶部侧边导航栏
+ * @param props
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const LayoutBase = props => {
+ const { children, headerSlot, slotTop, slotRight, meta, siteInfo, className } = props
+
+ return (
+
+ {/* 网页SEO */}
+
+
+
+ {/* 顶部嵌入 导航栏,首页放hero,文章页放文章详情 */}
+ {headerSlot}
+
+ {/* 主区块 */}
+
+
+
+
+ {/* 主区上部嵌入 */}
+ {slotTop}
+ {children}
+
+
+
+ {/* 主区快右侧 */}
+ {slotRight}
+
+
+
+
+
+ {/* 页脚 */}
+
+
+ )
+}
+
+/**
+ * 首页
+ * 是一个博客列表,嵌入一个Hero大图
+ * @param {*} props
+ * @returns
+ */
+const LayoutIndex = (props) => {
+ const headerSlot =
+ {/* 顶部导航 */}
+
+ {/* 通知横幅 */}
+
+
+
+
+ // 右侧栏 用户信息+标签列表
+ const slotRight =
+
+ return
+
+ {/* 文章分类条 */}
+
+ {BLOG.POST_LIST_STYLE === 'page' ? : }
+
+
+}
+
+/**
+ * 博客列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutPostList = (props) => {
+ // 右侧栏
+ const slotRight =
+ const headerSlot =
+
+ return
+
+ {/* 文章分类条 */}
+
+ {BLOG.POST_LIST_STYLE === 'page' ? : }
+
+
+}
+
+/**
+ * 搜索
+ * @param {*} props
+ * @returns
+ */
+const LayoutSearch = props => {
+ const { keyword } = props
+ const router = useRouter()
+ const currentSearch = keyword || router?.query?.s
+ const headerSlot =
+
+ useEffect(() => {
+ // 高亮搜索结果
+ if (currentSearch) {
+ setTimeout(() => {
+ replaceSearchResult({
+ doms: document.getElementsByClassName('replace'),
+ search: currentSearch,
+ target: {
+ element: 'span',
+ className: 'text-red-500 border-b border-dashed'
+ }
+ })
+ }, 100)
+ }
+ }, [])
+ return (
+
+
+ {!currentSearch
+ ?
+ :
{BLOG.POST_LIST_STYLE === 'page' ? : }
}
+
+
+ )
+}
+
+/**
+ * 归档
+ * @param {*} props
+ * @returns
+ */
+const LayoutArchive = (props) => {
+ const { archivePosts } = props
+
+ // 右侧栏
+ const slotRight =
+ const headerSlot =
+
+ // 归档页顶部显示条,如果是默认归档则不显示。分类详情页显示分类列表,标签详情页显示当前标签
+
+ return
+
+ {/* 文章分类条 */}
+
+
+
+ {Object.keys(archivePosts).map(archiveTitle => (
+
+ ))}
+
+
+
+}
+
+/**
+ * 文章详情
+ * @param {*} props
+ * @returns
+ */
+const LayoutSlug = props => {
+ const { post, lock, validPassword } = props
+ const { locale } = useGlobal()
+
+ // 右侧栏
+ const slotRight =
+ const headerSlot =
+
+ return (
+
+
+ {lock &&
}
+
+ {!lock &&
+
+
+ {/* Notion文章主体 */}
+
+
+ {/* 分享 */}
+
+ {post?.type === 'Post' &&
+
+ {/* 版权 */}
+
+ {/* 文章推荐 */}
+
+ {/* 上一篇\下一篇文章 */}
+
+
}
+
+
+
+
+
+ {/* 评论互动 */}
+
+
{locale.COMMON.COMMENTS}
+
+
+
}
+
+
+
+
+ )
+}
+
+/**
+ * 404
+ * @param {*} props
+ * @returns
+ */
+const Layout404 = props => {
+ const { meta, siteInfo } = props
+ const { onLoading } = useGlobal()
+ return (
+
+ {/* 网页SEO */}
+
+
+
+ {/* 顶部嵌入 导航栏,首页放hero,文章页放文章详情 */}
+
+
+ {/* 主区块 */}
+
+
+
+
+
+
+ {/* 404卡牌 */}
+
+ {/* 左侧动图 */}
+
+
+ {/* 右侧文字 */}
+
+
404
+
请尝试站内搜索寻找文章
+
+
+
+
+
+
+ {/* 404页面底部显示最新文章 */}
+
+
+
+
+
+
+
+
+
+ )
+}
+
+/**
+ * 分类列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutCategoryIndex = props => {
+ const { categoryOptions } = props
+ const { locale } = useGlobal()
+ const headerSlot =
+
+ return (
+
+
+
+ {locale.COMMON.CATEGORY}
+
+
+ {categoryOptions.map(category => {
+ return (
+
+
+
+ {category.name}
+
+ {category.count}
+
+
+
+ )
+ })}
+
+
+
+ )
+}
+
+/**
+ * 标签列表
+ * @param {*} props
+ * @returns
+ */
+const LayoutTagIndex = props => {
+ const { tagOptions } = props
+ const { locale } = useGlobal()
+ const headerSlot =
+ return (
+
+
+
+ {locale.COMMON.TAGS}
+
+
+ {tagOptions.map(tag => {
+ return (
+
+
+
+ {tag.name}
+
+ {tag.count}
+
+
+
+ )
+ })}
+
+
+
+ )
+}
+
+export {
+ CONFIG as THEME_CONFIG,
+ LayoutIndex,
+ LayoutSearch,
+ LayoutArchive,
+ LayoutSlug,
+ Layout404,
+ LayoutCategoryIndex,
+ LayoutPostList,
+ LayoutTagIndex
+}
diff --git a/themes/heo/style.js b/themes/heo/style.js
new file mode 100644
index 00000000..b0de7c2d
--- /dev/null
+++ b/themes/heo/style.js
@@ -0,0 +1,64 @@
+/* eslint-disable react/no-unknown-property */
+/**
+ * 此处样式只对当前主题生效
+ * 此处不支持tailwindCSS的 @apply 语法
+ * @returns
+ */
+const Style = () => {
+ return
+}
+
+export { Style }
diff --git a/themes/hexo/components/ArticleRecommend.js b/themes/hexo/components/ArticleRecommend.js
index e4943cf8..dc5ef58e 100644
--- a/themes/hexo/components/ArticleRecommend.js
+++ b/themes/hexo/components/ArticleRecommend.js
@@ -2,6 +2,7 @@ import Link from 'next/link'
import CONFIG from '../config'
import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global'
+import LazyImage from '@/components/LazyImage'
/**
* 关联推荐文章
@@ -13,53 +14,47 @@ export default function ArticleRecommend({ recommendPosts, siteInfo }) {
if (
!CONFIG.ARTICLE_RECOMMEND ||
- !recommendPosts ||
- recommendPosts.length === 0
+ !recommendPosts ||
+ recommendPosts.length === 0
) {
return <>>
}
return (
-
-
-
-
- {locale.COMMON.RELATE_POSTS}
-
-
-
- {recommendPosts.map(post => {
- const headerImage = post?.pageCoverThumbnail
- ? `url("${post.pageCoverThumbnail}")`
- : `url("${siteInfo?.pageCover}")`
-
- return (
- (
-
-
-
-
-
-
- {post.date?.start_date}
-
-
{post.title}
-
+
+
+
+
+ {locale.COMMON.RELATE_POSTS}
-
+
+
+ {recommendPosts.map(post => {
+ const headerImage = post?.pageCoverThumbnail
+ ? post.pageCoverThumbnail
+ : siteInfo?.pageCover
- )
- )
- })}
-
-
+ return (
+ (
+
+
+
+ )
+ )
+ })}
+
+
)
}
diff --git a/themes/hexo/components/BlogPostCard.js b/themes/hexo/components/BlogPostCard.js
index 648ce49e..dc1d9759 100644
--- a/themes/hexo/components/BlogPostCard.js
+++ b/themes/hexo/components/BlogPostCard.js
@@ -3,6 +3,7 @@ import React from 'react'
import CONFIG from '../config'
import { BlogPostCardInfo } from './BlogPostCardInfo'
import BLOG from '@/blog.config'
+import LazyImage from '@/components/LazyImage'
// import Image from 'next/image'
const BlogPostCard = ({ index, post, showSummary, siteInfo }) => {
@@ -16,15 +17,14 @@ const BlogPostCard = ({ index, post, showSummary, siteInfo }) => {
return (
-
{/* 文字内容 */}
@@ -34,7 +34,7 @@ const BlogPostCard = ({ index, post, showSummary, siteInfo }) => {
{showPageCover && (
)}
diff --git a/themes/hexo/components/BlogPostCardInfo.js b/themes/hexo/components/BlogPostCardInfo.js
index 4b7feee6..ffdf0777 100644
--- a/themes/hexo/components/BlogPostCardInfo.js
+++ b/themes/hexo/components/BlogPostCardInfo.js
@@ -51,9 +51,9 @@ export const BlogPostCardInfo = ({ post, showPreview, showPageCover, showSummary
{/* 搜索结果 */}
{post.results && (
- {post.results.map(r => (
- {r}
- ))}
+ {post.results.map((r, index) => (
+ {r}
+ ))}
)}
{/* 预览 */}
diff --git a/themes/hexo/components/BlogPostListScroll.js b/themes/hexo/components/BlogPostListScroll.js
index 830e8177..7646b056 100644
--- a/themes/hexo/components/BlogPostListScroll.js
+++ b/themes/hexo/components/BlogPostListScroll.js
@@ -57,7 +57,7 @@ const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = CONFIG.PO
return
{/* 文章列表 */}
-
+
{postsToShow.map(post => (
))}
diff --git a/themes/hexo/components/Footer.js b/themes/hexo/components/Footer.js
index f67b53f2..8025e7b9 100644
--- a/themes/hexo/components/Footer.js
+++ b/themes/hexo/components/Footer.js
@@ -26,7 +26,7 @@ const Footer = ({ title }) => {
- {title} | {BLOG.BIO}
+ {title} {BLOG.BIO && <>|>} {BLOG.BIO}
Powered by NotionNext {BLOG.VERSION}.
diff --git a/themes/hexo/components/Header.js b/themes/hexo/components/Hero.js
similarity index 90%
rename from themes/hexo/components/Header.js
rename to themes/hexo/components/Hero.js
index 76e86c25..c07a401e 100644
--- a/themes/hexo/components/Header.js
+++ b/themes/hexo/components/Hero.js
@@ -5,6 +5,7 @@ import CONFIG from '../config'
import NavButtonGroup from './NavButtonGroup'
import { useGlobal } from '@/lib/global'
import BLOG from '@/blog.config'
+import LazyImage from '@/components/LazyImage'
let wrapperTop = 0
@@ -72,8 +73,8 @@ const Hero = props => {
-
+
)
diff --git a/themes/hexo/components/InfoCard.js b/themes/hexo/components/InfoCard.js
index 661da999..5f1116e8 100644
--- a/themes/hexo/components/InfoCard.js
+++ b/themes/hexo/components/InfoCard.js
@@ -3,6 +3,7 @@ import { useRouter } from 'next/router'
import Card from './Card'
import SocialButton from './SocialButton'
import MenuGroupCard from './MenuGroupCard'
+import LazyImage from '@/components/LazyImage'
/**
* 社交信息卡
@@ -21,7 +22,7 @@ export function InfoCard(props) {
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
-

+
{BLOG.AUTHOR}
{BLOG.BIO}
diff --git a/themes/hexo/components/LatestPostsGroup.js b/themes/hexo/components/LatestPostsGroup.js
index 22ac3e42..d315367a 100644
--- a/themes/hexo/components/LatestPostsGroup.js
+++ b/themes/hexo/components/LatestPostsGroup.js
@@ -1,4 +1,5 @@
import BLOG from '@/blog.config'
+import LazyImage from '@/components/LazyImage'
import { useGlobal } from '@/lib/global'
// import Image from 'next/image'
import Link from 'next/link'
@@ -40,16 +41,7 @@ const LatestPostsGroup = ({ latestPosts, siteInfo }) => {
className={'my-3 flex'}>
- {/*
*/}
- {/* eslint-disable-next-line @next/next/no-img-element */}
-

+
{
{/* 折叠子菜单 */}
{hasSubMenu &&
- {link.subMenus.map(sLink => {
- return
+ {link.subMenus.map((sLink, index) => {
+ return
{link?.icon && } {sLink.title}
diff --git a/themes/hexo/components/HeaderArticle.js b/themes/hexo/components/PostHeader.js
similarity index 87%
rename from themes/hexo/components/HeaderArticle.js
rename to themes/hexo/components/PostHeader.js
index b5384a1f..9701bb6e 100644
--- a/themes/hexo/components/HeaderArticle.js
+++ b/themes/hexo/components/PostHeader.js
@@ -3,21 +3,20 @@ import TagItemMini from './TagItemMini'
import { useGlobal } from '@/lib/global'
import BLOG from '@/blog.config'
import NotionIcon from '@/components/NotionIcon'
+import LazyImage from '@/components/LazyImage'
-export default function HeaderArticle({ post, siteInfo }) {
+export default function PostHeader({ post, siteInfo }) {
const { locale } = useGlobal()
if (!post) {
return <>>
}
- const headerImage = post?.pageCover ? `url("${post.pageCover}")` : `url("${siteInfo?.pageCover}")`
+ const headerImage = post?.pageCover ? post.pageCover : siteInfo?.pageCover
return (
-