Merge branch 'main' into pr/jxpeng98/1257

This commit is contained in:
tangly1024.com
2023-07-26 18:43:34 +08:00
238 changed files with 7817 additions and 1027 deletions

View File

@@ -1,2 +1,2 @@
# 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables # 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables
NEXT_PUBLIC_VERSION=4.0.0 NEXT_PUBLIC_VERSION=4.0.5

View File

@@ -26,6 +26,7 @@ module.exports = {
} }
}, },
rules: { rules: {
'react/no-unknown-property': 'off', // <style jsx>
'react/prop-types': 'off', 'react/prop-types': 'off',
'space-before-function-paren': 0, 'space-before-function-paren': 0,
'react-hooks/rules-of-hooks': 'error' // Checks rules of Hooks 'react-hooks/rules-of-hooks': 'error' // Checks rules of Hooks

View File

@@ -34,26 +34,31 @@ const BLOG = {
NOTION_HOST: process.env.NEXT_PUBLIC_NOTION_HOST || 'https://www.notion.so', // Notion域名您可以选择用自己的域名进行反向代理如果不懂得什么是反向代理请勿修改此项 NOTION_HOST: process.env.NEXT_PUBLIC_NOTION_HOST || 'https://www.notion.so', // Notion域名您可以选择用自己的域名进行反向代理如果不懂得什么是反向代理请勿修改此项
// 网站字体 BLOG_FAVICON: process.env.NEXT_PUBLIC_FAVICON || '/favicon.ico', // blog favicon 配置, 默认使用 /public/favicon.ico支持在线图片如 https://img.imesong.com/favicon.png
// START ************网站字体*****************
FONT_STYLE: process.env.NEXT_PUBLIC_FONT_STYLE || 'font-sans', // ['font-serif','font-sans'] 两种可选,分别是衬线和无衬线: 参考 https://www.jianshu.com/p/55e410bd2115 FONT_STYLE: process.env.NEXT_PUBLIC_FONT_STYLE || 'font-sans', // ['font-serif','font-sans'] 两种可选,分别是衬线和无衬线: 参考 https://www.jianshu.com/p/55e410bd2115
// 字体CSS 例如 https://npm.elemecdn.com/lxgw-wenkai-webfont@1.6.0/style.css
FONT_URL: [ FONT_URL: [
// 字体CSS 例如 https://npm.elemecdn.com/lxgw-wenkai-webfont@1.6.0/style.css // 'https://npm.elemecdn.com/lxgw-wenkai-webfont@1.6.0/style.css'
'https://fonts.googleapis.com/css?family=Bitter&display=swap', 'https://fonts.googleapis.com/css?family=Bitter&display=swap',
'https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300&display=swap', 'https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300&display=swap',
'https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300&display=swap' 'https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@300&display=swap'
], ],
// 无衬线字体 例如'"LXGW WenKai"'
FONT_SANS: [ FONT_SANS: [
// 无衬线字体 例如'LXGW WenKai' // '"LXGW WenKai"',
'"PingFang SC"', '"PingFang SC"',
'-apple-system', '-apple-system',
'BlinkMacSystemFont', 'BlinkMacSystemFont',
'"Hiragino Sans GB"', '"Hiragino Sans GB"',
'"Microsoft YaHei"',
'"Segoe UI Emoji"', '"Segoe UI Emoji"',
'"Segoe UI Symbol"', '"Segoe UI Symbol"',
'"Segoe UI"', '"Segoe UI"',
'"Noto Sans SC"', '"Noto Sans SC"',
'HarmonyOS_Regular', 'HarmonyOS_Regular',
'"Microsoft YaHei"',
'"Helvetica Neue"', '"Helvetica Neue"',
'Helvetica', 'Helvetica',
'"Source Han Sans SC"', '"Source Han Sans SC"',
@@ -61,8 +66,9 @@ const BLOG = {
'sans-serif', 'sans-serif',
'"Apple Color Emoji"' '"Apple Color Emoji"'
], ],
// 衬线字体 例如'"LXGW WenKai"'
FONT_SERIF: [ FONT_SERIF: [
// 衬线字体 例如'LXGW WenKai' // '"LXGW WenKai"',
'Bitter', 'Bitter',
'"Noto Serif SC"', '"Noto Serif SC"',
'SimSun', 'SimSun',
@@ -73,7 +79,11 @@ const BLOG = {
'"Segoe UI Symbol"', '"Segoe UI Symbol"',
'"Apple Color Emoji"' '"Apple Color Emoji"'
], ],
FONT_AWESOME: process.env.NEXT_PUBLIC_FONT_AWESOME_PATH || '/css/all.min.css', // font-awesome 字体图标地址、默认读取本地; 可选 https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/font-awesome/6.0.0/css/all.min.css FONT_AWESOME: process.env.NEXT_PUBLIC_FONT_AWESOME_PATH || 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css', // font-awesome 字体图标地址; 可选 /css/all.min.css https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/font-awesome/6.0.0/css/all.min.css
// END ************网站字体*****************
CUSTOM_RIGHT_CLICK_CONTEXT_MENU: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU || true, // 自定义右键菜单,覆盖系统菜单
// 自定义外部脚本,外部样式 // 自定义外部脚本,外部样式
CUSTOM_EXTERNAL_JS: [''], // e.g. ['http://xx.com/script.js','http://xx.com/script.js'] CUSTOM_EXTERNAL_JS: [''], // e.g. ['http://xx.com/script.js','http://xx.com/script.js']
@@ -103,10 +113,11 @@ const BLOG = {
CODE_MAC_BAR: process.env.NEXT_PUBLIC_CODE_MAC_BAR || true, // 代码左上角显示mac的红黄绿图标 CODE_MAC_BAR: process.env.NEXT_PUBLIC_CODE_MAC_BAR || true, // 代码左上角显示mac的红黄绿图标
CODE_LINE_NUMBERS: process.env.NEXT_PUBLIC_CODE_LINE_NUMBERS || 'false', // 是否显示行号 CODE_LINE_NUMBERS: process.env.NEXT_PUBLIC_CODE_LINE_NUMBERS || 'false', // 是否显示行号
CODE_COLLAPSE: process.env.NEXT_PUBLIC_CODE_COLLAPSE || true, // 是否折叠代码框
// END********代码相关******** // END********代码相关********
// Mermaid 图表CDN // Mermaid 图表CDN
MERMAID_CDN: process.env.NEXT_PUBLIC_MERMAID_CDN || 'https://cdn.jsdelivr.net/npm/mermaid@10.2.2/dist/mermaid.min.js', // CDN MERMAID_CDN: process.env.NEXT_PUBLIC_MERMAID_CDN || 'https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.2.4/mermaid.min.js', // CDN
BACKGROUND_LIGHT: '#eeeeee', // use hex value, don't forget '#' e.g #fffefc BACKGROUND_LIGHT: '#eeeeee', // use hex value, don't forget '#' e.g #fffefc
BACKGROUND_DARK: '#000000', // use hex value, don't forget '#' BACKGROUND_DARK: '#000000', // use hex value, don't forget '#'
@@ -122,18 +133,25 @@ const BLOG = {
// 支援類似 WP 可自訂文章連結格式的功能https://wordpress.org/documentation/article/customize-permalinks/,目前只先實作 %year%/%month%/%day% // 支援類似 WP 可自訂文章連結格式的功能https://wordpress.org/documentation/article/customize-permalinks/,目前只先實作 %year%/%month%/%day%
// 例:如想連結改成前綴 article + 時間戳記,可變更為: 'article/%year%/%month%/%day%' // 例:如想連結改成前綴 article + 時間戳記,可變更為: 'article/%year%/%month%/%day%'
POST_LIST_STYLE: process.env.NEXT_PUBLIC_PPOST_LIST_STYLE || 'page', // ['page','scroll] 文章列表样式:页码分页、单页滚动加载 POST_LIST_STYLE: process.env.NEXT_PUBLIC_POST_LIST_STYLE || 'page', // ['page','scroll] 文章列表样式:页码分页、单页滚动加载
POST_LIST_PREVIEW: process.env.NEXT_PUBLIC_POST_PREVIEW || 'false', // 是否在列表加载文章预览 POST_LIST_PREVIEW: process.env.NEXT_PUBLIC_POST_PREVIEW || 'false', // 是否在列表加载文章预览
POST_PREVIEW_LINES: 12, // 预览博客行数 POST_PREVIEW_LINES: 12, // 预览博客行数
POST_RECOMMEND_COUNT: 6, // 推荐文章数量 POST_RECOMMEND_COUNT: 6, // 推荐文章数量
POSTS_PER_PAGE: 12, // post counts per page POSTS_PER_PAGE: 12, // post counts per page
POSTS_SORT_BY: process.env.NEXT_PUBLIC_POST_SORT_BY || 'notion', // 排序方式 'date'按时间,'notion'由notion控制 POSTS_SORT_BY: process.env.NEXT_PUBLIC_POST_SORT_BY || 'notion', // 排序方式 'date'按时间,'notion'由notion控制
ALGOLIA_APP_ID: process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || null, // 在这里查看 https://dashboard.algolia.com/account/api-keys/
ALGOLIA_ADMIN_APP_KEY: process.env.ALGOLIA_ADMIN_APP_KEY || null, // 管理后台的KEY不要暴露在代码中在这里查看 https://dashboard.algolia.com/account/api-keys/
ALGOLIA_SEARCH_ONLY_APP_KEY: process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_APP_KEY || null, // 客户端搜索用的KEY
ALGOLIA_INDEX: process.env.NEXT_PUBLIC_ALGOLIA_INDEX || null, // 在Algolia中创建一个index用作数据库
ALGOLIA_RECREATE_DATA: process.env.ALGOLIA_RECREATE_DATA || process.env.npm_lifecycle_event === 'build', // 为true时重新构建索引数据; 默认在build时会构建
PREVIEW_CATEGORY_COUNT: 16, // 首页最多展示的分类数量0为不限制 PREVIEW_CATEGORY_COUNT: 16, // 首页最多展示的分类数量0为不限制
PREVIEW_TAG_COUNT: 16, // 首页最多展示的标签数量0为不限制 PREVIEW_TAG_COUNT: 16, // 首页最多展示的标签数量0为不限制
POST_DISABLE_GALLERY_CLICK: process.env.NEXT_PUBLIC_POST_DISABLE_GALLERY_CLICK || false, // 画册视图禁止点击,方便在友链页面的画册插入链接 POST_DISABLE_GALLERY_CLICK: process.env.NEXT_PUBLIC_POST_DISABLE_GALLERY_CLICK || false, // 画册视图禁止点击,方便在友链页面的画册插入链接
// ********动态特效相关********
// 鼠标点击烟花特效 // 鼠标点击烟花特效
FIREWORKS: process.env.NEXT_PUBLIC_FIREWORKS || false, // 开关 FIREWORKS: process.env.NEXT_PUBLIC_FIREWORKS || false, // 开关
// 烟花色彩,感谢 https://github.com/Vixcity 提交的色彩 // 烟花色彩,感谢 https://github.com/Vixcity 提交的色彩
@@ -146,18 +164,18 @@ const BLOG = {
// 樱花飘落特效 // 樱花飘落特效
SAKURA: process.env.NEXT_PUBLIC_SAKURA || false, // 开关 SAKURA: process.env.NEXT_PUBLIC_SAKURA || false, // 开关
// 漂浮线段特效 // 漂浮线段特效
NEST: process.env.NEXT_PUBLIC_NEST || false, // 开关 NEST: process.env.NEXT_PUBLIC_NEST || false, // 开关
// 动态彩带特效 // 动态彩带特效
FLUTTERINGRIBBON: process.env.NEXT_PUBLIC_FLUTTERINGRIBBON || false, // 开关 FLUTTERINGRIBBON: process.env.NEXT_PUBLIC_FLUTTERINGRIBBON || false, // 开关
// 静态彩带特效 // 静态彩带特效
RIBBON: process.env.NEXT_PUBLIC_RIBBON || false, // 开关 RIBBON: process.env.NEXT_PUBLIC_RIBBON || false, // 开关
// 星空雨特效 黑夜模式才会生效 // 星空雨特效 黑夜模式才会生效
STARRY_SKY: process.env.NEXT_PUBLIC_STARRY_SKY || false, // 开关 STARRY_SKY: process.env.NEXT_PUBLIC_STARRY_SKY || false, // 开关
// ********挂件组件相关********
// Chatbase
CHATBASE_ID: process.env.NEXT_PUBLIC_CHATBASE_ID || null, // 是否显示chatbase机器人 https://www.chatbase.co/
// 悬浮挂件 // 悬浮挂件
WIDGET_PET: process.env.NEXT_PUBLIC_WIDGET_PET || true, // 是否显示宠物挂件 WIDGET_PET: process.env.NEXT_PUBLIC_WIDGET_PET || true, // 是否显示宠物挂件
WIDGET_PET_LINK: WIDGET_PET_LINK:
@@ -200,6 +218,7 @@ const BLOG = {
MUSIC_PLAYER_METING_LRC_TYPE: MUSIC_PLAYER_METING_LRC_TYPE:
process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_LRC_TYPE || '1', // 可选值: 3 | 1 | 00禁用 lrc 歌词1lrc 格式的字符串3lrc 文件 url process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_LRC_TYPE || '1', // 可选值: 3 | 1 | 00禁用 lrc 歌词1lrc 格式的字符串3lrc 文件 url
// ********挂件组件相关********
// ----> 评论互动 可同时开启多个支持 WALINE VALINE GISCUS CUSDIS UTTERRANCES GITALK // ----> 评论互动 可同时开启多个支持 WALINE VALINE GISCUS CUSDIS UTTERRANCES GITALK
// twikoo // twikoo
@@ -297,11 +316,18 @@ const BLOG = {
SEO_GOOGLE_SITE_VERIFICATION: SEO_GOOGLE_SITE_VERIFICATION:
process.env.NEXT_PUBLIC_SEO_GOOGLE_SITE_VERIFICATION || '', // Remove the value or replace it with your own google site verification code process.env.NEXT_PUBLIC_SEO_GOOGLE_SITE_VERIFICATION || '', // Remove the value or replace it with your own google site verification code
SEO_BAIDU_SITE_VERIFICATION:
process.env.NEXT_PUBLIC_SEO_BAIDU_SITE_VERIFICATION || '', // Remove the value or replace it with your own google site verification code
// <---- 站点统计 // <---- 站点统计
// 谷歌广告 // 谷歌广告
ADSENSE_GOOGLE_ID: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_ID || '', // 谷歌广告ID e.g ca-pub-xxxxxxxxxxxxxxxx ADSENSE_GOOGLE_ID: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_ID || '', // 谷歌广告ID e.g ca-pub-xxxxxxxxxxxxxxxx
ADSENSE_GOOGLE_TEST: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_TEST || false, // 谷歌广告ID测试模式这种模式获取假的测试广告用于开发 https://www.tangly1024.com/article/local-dev-google-adsense ADSENSE_GOOGLE_TEST: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_TEST || false, // 谷歌广告ID测试模式这种模式获取假的测试广告用于开发 https://www.tangly1024.com/article/local-dev-google-adsense
ADSENSE_GOOGLE_SLOT_IN_ARTICLE: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_IN_ARTICLE || '3806269138', // Google AdScene>广告>按单元广告>新建文章内嵌广告 粘贴html代码中的data-ad-slot值
ADSENSE_GOOGLE_SLOT_FLOW: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_FLOW || '1510444138', // Google AdScene>广告>按单元广告>新建信息流广告
ADSENSE_GOOGLE_SLOT_NATIVE: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_NATIVE || '4980048999', // Google AdScene>广告>按单元广告>新建原生广告
ADSENSE_GOOGLE_SLOT_AUTO: process.env.NEXT_PUBLIC_ADSENSE_GOOGLE_SLOT_AUTO || '8807314373', // Google AdScene>广告>按单元广告>新建展示广告 (自动广告)
// 自定义配置notion数据库字段名 // 自定义配置notion数据库字段名
NOTION_PROPERTY_NAME: { NOTION_PROPERTY_NAME: {
@@ -342,6 +368,7 @@ const BLOG = {
process.env.NEXT_PUBLIC_DESCRIPTION || '这是一个由NotionNext生成的站点', // 站点描述被notion中的页面描述覆盖 process.env.NEXT_PUBLIC_DESCRIPTION || '这是一个由NotionNext生成的站点', // 站点描述被notion中的页面描述覆盖
// 网站图片 // 网站图片
IMG_LAZY_LOAD_PLACEHOLDER: process.env.NEXT_PUBLIC_IMG_LAZY_LOAD_PLACEHOLDER || 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==', // 懒加载占位图片地址支持base64或url
IMG_URL_TYPE: process.env.NEXT_PUBLIC_IMG_TYPE || 'Notion', // 此配置已失效请勿使用AMAZON方案不再支持仅支持Notion方案。 ['Notion','AMAZON'] 站点图片前缀 默认 Notion:(https://notion.so/images/xx) AMAZON(https://s3.us-west-2.amazonaws.com/xxx) IMG_URL_TYPE: process.env.NEXT_PUBLIC_IMG_TYPE || 'Notion', // 此配置已失效请勿使用AMAZON方案不再支持仅支持Notion方案。 ['Notion','AMAZON'] 站点图片前缀 默认 Notion:(https://notion.so/images/xx) AMAZON(https://s3.us-west-2.amazonaws.com/xxx)
IMG_SHADOW: process.env.NEXT_PUBLIC_IMG_SHADOW || false, // 文章图片是否自动添加阴影 IMG_SHADOW: process.env.NEXT_PUBLIC_IMG_SHADOW || false, // 文章图片是否自动添加阴影

View File

@@ -0,0 +1,92 @@
import { useState, useImperativeHandle } from 'react'
import BLOG from '@/blog.config'
import algoliasearch from 'algoliasearch'
import replaceSearchResult from '@/components/Mark'
/**
* 结合 Algolia 实现的弹出式搜索框
* 打开方式 cRef.current.openSearch()
* https://www.algolia.com/doc/api-reference/search-api-parameters/
*/
export default function AlgoliaSearchModal({ cRef }) {
const [searchResults, setSearchResults] = useState([])
const [isModalOpen, setIsModalOpen] = useState(false)
/**
* 对外暴露方法
*/
useImperativeHandle(cRef, () => {
return {
openSearch: () => {
setIsModalOpen(true)
}
}
})
if (!BLOG.ALGOLIA_APP_ID) {
return <></>
}
const client = algoliasearch(BLOG.ALGOLIA_APP_ID, BLOG.ALGOLIA_SEARCH_ONLY_APP_KEY)
const index = client.initIndex(BLOG.ALGOLIA_INDEX)
const handleSearch = async (query) => {
try {
const res = await index.search(query)
console.log(res)
const { hits } = res
setSearchResults(hits)
const doms = document.getElementById('search-wrapper').getElementsByClassName('replace')
replaceSearchResult({
doms,
search: query,
target: {
element: 'span',
className: 'text-blue-600 border-b border-dashed'
}
})
} catch (error) {
console.error('Algolia search error:', error)
}
}
const closeModal = () => {
setIsModalOpen(false)
}
return (
<div id='search-wrapper' className={`${isModalOpen ? 'opacity-100' : 'invisible opacity-0 pointer-events-none'} fixed h-screen w-screen left-0 top-0 flex items-center justify-center`}>
{/* 内容 */}
<div className={`${isModalOpen ? 'opacity-100' : 'invisible opacity-0 translate-y-10'} flex flex-col justify-between w-full min-h-[10rem] max-w-xl dark:bg-hexo-black-gray dark:border-gray-800 bg-white dark:bg- p-5 rounded-lg z-50 shadow border hover:border-blue-600 duration-300 transition-all `}>
<div className='flex justify-between items-center'>
<div className='text-2xl text-blue-600 font-bold'>搜索</div>
<div><i class="text-gray-600 fa-solid fa-xmark p-1 cursor-pointer hover:text-blue-600" onClick={closeModal} ></i></div>
</div>
<input type="text" placeholder="在这里输入搜索关键词..." onChange={(e) => handleSearch(e.target.value)}
className="bg-gray-50 dark:bg-gray-600 outline-blue-500 w-full px-4 my-2 py-1 mb-4 border rounded-md" />
{/* 标签组 */}
<div>
</div>
<ul>
{searchResults.map((result) => (
<li key={result.objectID} className="replace my-2">
<a href={`${BLOG.SUB_PATH}/${result.slug}`} className="font-bold hover:text-blue-600 ">
{result.title}
</a>
</li>
))}
</ul>
<div className='text-gray-600'><i class="fa-brands fa-algolia"></i> Algolia </div>
</div>
{/* 遮罩 */}
<div onClick={closeModal} className="z-30 fixed top-0 left-0 w-full h-full flex items-center justify-center glassmorphism" />
</div>
)
}

19
components/ChatBase.js Normal file
View File

@@ -0,0 +1,19 @@
import BLOG from '@/blog.config'
/**
* 这是一个嵌入组件可以在任意位置全屏显示您的chat-base对话框
* 暂时没有页面引用
* 因为您可以直接用内嵌网页的方式放入您的notion中 https://www.chatbase.co/chatbot-iframe/${BLOG.CHATBASE_ID}
*/
export default function ChatBase() {
if (!BLOG.CHATBASE_ID) {
return <></>
}
return <iframe
src={`https://www.chatbase.co/chatbot-iframe/${BLOG.CHATBASE_ID}`}
width="100%"
style={{ height: '100%', minHeight: '700px' }}
frameborder="0"
></iframe>
}

View File

@@ -84,7 +84,7 @@ const Collapse = props => {
}, [props.isOpen]) }, [props.isOpen])
return ( return (
<div ref={ref} style={type === 'vertical' ? { height: '0px', willChange: 'height' } : { width: '0px', willChange: 'width' }} className={`${props.className} overflow-hidden duration-200 `}> <div ref={ref} style={type === 'vertical' ? { height: '0px', willChange: 'height' } : { width: '0px', willChange: 'width' }} className={`${props.className || ''} overflow-hidden duration-200 `}>
{props.children} {props.children}
</div> </div>
) )

View File

@@ -54,7 +54,12 @@ const ValineComponent = dynamic(() => import('@/components/ValineComponent'), {
ssr: false ssr: false
}) })
const Comment = ({ frontMatter }) => { /**
* 评论组件
* @param {*} param0
* @returns
*/
const Comment = ({ frontMatter, className }) => {
const router = useRouter() const router = useRouter()
if (isBrowser() && ('giscus' in router.query || router.query.target === 'comment')) { if (isBrowser() && ('giscus' in router.query || router.query.target === 'comment')) {
@@ -70,7 +75,7 @@ const Comment = ({ frontMatter }) => {
} }
return ( return (
<div id='comment' className='comment mt-5 text-gray-800 dark:text-gray-300'> <div key={frontMatter?.id} id='comment' className={`comment mt-5 text-gray-800 dark:text-gray-300 ${className || ''}`}>
<Tabs> <Tabs>
{BLOG.COMMENT_TWIKOO_ENV_ID && (<div key='Twikoo'> {BLOG.COMMENT_TWIKOO_ENV_ID && (<div key='Twikoo'>

View File

@@ -16,55 +16,57 @@ const CommonHead = ({ meta, children }) => {
const category = meta?.category || BLOG.KEYWORDS || '軟體科技' // section 主要是像是 category 這樣的分類Facebook 用這個來抓連結的分類 const category = meta?.category || BLOG.KEYWORDS || '軟體科技' // section 主要是像是 category 這樣的分類Facebook 用這個來抓連結的分類
return ( return (
<Head> <Head>
<title>{title}</title> <title>{title}</title>
<meta name="theme-color" content={BLOG.BACKGROUND_DARK} /> <meta name="theme-color" content={BLOG.BACKGROUND_DARK} />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, minimum-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, minimum-scale=1.0" />
<meta name="robots" content="follow, index" /> <meta name="robots" content="follow, index" />
<meta charSet="UTF-8" /> <meta charSet="UTF-8" />
{BLOG.SEO_GOOGLE_SITE_VERIFICATION && ( {BLOG.SEO_GOOGLE_SITE_VERIFICATION && (
<meta <meta
name="google-site-verification" name="google-site-verification"
content={BLOG.SEO_GOOGLE_SITE_VERIFICATION} content={BLOG.SEO_GOOGLE_SITE_VERIFICATION}
/> />
)} )}
<meta name="keywords" content={keywords} /> {BLOG.SEO_BAIDU_SITE_VERIFICATION && (<meta name="baidu-site-verification" content={BLOG.SEO_BAIDU_SITE_VERIFICATION} />)}
<meta name="description" content={description} /> <meta name="keywords" content={keywords} />
<meta property="og:locale" content={lang} /> <meta name="description" content={description} />
<meta property="og:title" content={title} /> <meta property="og:locale" content={lang} />
<meta property="og:description" content={description} /> <meta property="og:title" content={title} />
<meta property="og:url" content={url} /> <meta property="og:description" content={description} />
<meta property="og:image" content={image} /> <meta property="og:url" content={url} />
<meta property="og:site_name" content={BLOG.TITLE} /> <meta property="og:image" content={image} />
<meta property="og:type" content={type} /> <meta property="og:site_name" content={BLOG.TITLE} />
<meta name="twitter:card" content="summary_large_image" /> <meta property="og:type" content={type} />
<meta name="twitter:description" content={description} /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} /> <meta name="twitter:description" content={description} />
<meta name="twitter:title" content={title} />
{BLOG.COMMENT_WEBMENTION.ENABLE && ( {BLOG.COMMENT_WEBMENTION.ENABLE && (
<> <>
<link rel="webmention" href={`https://webmention.io/${BLOG.COMMENT_WEBMENTION.HOSTNAME}/webmention`} /> <link rel="webmention" href={`https://webmention.io/${BLOG.COMMENT_WEBMENTION.HOSTNAME}/webmention`} />
<link rel="pingback" href={`https://webmention.io/${BLOG.COMMENT_WEBMENTION.HOSTNAME}/xmlrpc`} /> <link rel="pingback" href={`https://webmention.io/${BLOG.COMMENT_WEBMENTION.HOSTNAME}/xmlrpc`} />
</> </>
)} )}
{BLOG.COMMENT_WEBMENTION.ENABLE && BLOG.COMMENT_WEBMENTION.AUTH !== '' && (
<link href={BLOG.COMMENT_WEBMENTION.AUTH} rel="me" />
)}
{JSON.parse(BLOG.ANALYTICS_BUSUANZI_ENABLE) && <meta name="referrer" content="no-referrer-when-downgrade" />} {BLOG.COMMENT_WEBMENTION.ENABLE && BLOG.COMMENT_WEBMENTION.AUTH !== '' && (
{meta?.type === 'Post' && ( <link href={BLOG.COMMENT_WEBMENTION.AUTH} rel="me" />
<> )}
<meta
property="article:published_time" {JSON.parse(BLOG.ANALYTICS_BUSUANZI_ENABLE) && <meta name="referrer" content="no-referrer-when-downgrade" />}
content={meta.publishTime} {meta?.type === 'Post' && (
/> <>
<meta property="article:author" content={BLOG.AUTHOR} /> <meta
<meta property="article:section" content={category} /> property="article:published_time"
<meta property="article:publisher" content={BLOG.FACEBOOK_PAGE} /> content={meta.publishTime}
</> />
)} <meta property="article:author" content={BLOG.AUTHOR} />
{children} <meta property="article:section" content={category} />
</Head> <meta property="article:publisher" content={BLOG.FACEBOOK_PAGE} />
</>
)}
{children}
</Head>
) )
} }

View File

@@ -7,6 +7,18 @@ import BLOG from '@/blog.config'
*/ */
const CommonScript = () => { const CommonScript = () => {
return (<> return (<>
{BLOG.CHATBASE_ID && (<>
<script id={BLOG.CHATBASE_ID} src="https://www.chatbase.co/embed.min.js" defer/>
<script async dangerouslySetInnerHTML={{
__html: `
window.chatbaseConfig = {
chatbotId: "${BLOG.CHATBASE_ID}",
}
`
}}/>
</>)}
{BLOG.COMMENT_DAO_VOICE_ID && (<> {BLOG.COMMENT_DAO_VOICE_ID && (<>
{/* DaoVoice 反馈 */} {/* DaoVoice 反馈 */}
<script async dangerouslySetInnerHTML={{ <script async dangerouslySetInnerHTML={{
@@ -26,10 +38,6 @@ const CommonScript = () => {
/> />
</>)} </>)}
{/* GoogleAdsense 本地开发请加入 data-adbreak-test="on" */}
{BLOG.ADSENSE_GOOGLE_ID && <script async src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${BLOG.ADSENSE_GOOGLE_ID}`}
crossOrigin="anonymous" />}
{BLOG.COMMENT_CUSDIS_APP_ID && <script defer src='https://cusdis.com/js/widget/lang/zh-cn.js' />} {BLOG.COMMENT_CUSDIS_APP_ID && <script defer src='https://cusdis.com/js/widget/lang/zh-cn.js' />}
{BLOG.COMMENT_TIDIO_ID && <script async src={`//code.tidio.co/${BLOG.COMMENT_TIDIO_ID}.js`} />} {BLOG.COMMENT_TIDIO_ID && <script async src={`//code.tidio.co/${BLOG.COMMENT_TIDIO_ID}.js`} />}

View File

@@ -0,0 +1,158 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect, useState, useRef } from 'react'
import { useGlobal } from '@/lib/global'
import { saveDarkModeToCookies, THEMES } from '@/themes/theme'
/**
* 自定义右键菜单
* @param {*} props
* @returns
*/
export default function CustomContextMenu(props) {
const [position, setPosition] = useState({ x: '0px', y: '0px' })
const [show, setShow] = useState(false)
const { isDarkMode, updateDarkMode, locale } = useGlobal()
const menuRef = useRef(null)
const { latestPosts } = props
const router = useRouter()
function handleJumpToRandomPost() {
const randomIndex = Math.floor(Math.random() * latestPosts.length)
const randomPost = latestPosts[randomIndex]
router.push(randomPost.slug)
}
useEffect(() => {
const handleContextMenu = (event) => {
event.preventDefault()
setPosition({ y: `${event.clientY}px`, x: `${event.clientX}px` })
setShow(true)
}
const handleClick = (event) => {
if (menuRef.current && !menuRef.current.contains(event.target)) {
setShow(false)
}
}
window.addEventListener('contextmenu', handleContextMenu)
window.addEventListener('click', handleClick)
return () => {
window.removeEventListener('contextmenu', handleContextMenu)
window.removeEventListener('click', handleClick)
}
}, [])
function handleBack() {
window.history.back()
}
function handleForward() {
window.history.forward()
}
function handleRefresh() {
window.location.reload()
}
function handleScrollTop() {
window.scrollTo({ top: 0, behavior: 'smooth' })
setShow(false)
}
function handleCopyLink() {
const url = window.location.href
navigator.clipboard.writeText(url)
.then(() => {
console.log('页面地址已复制')
})
.catch((error) => {
console.error('复制页面地址失败:', error)
})
setShow(false)
}
/**
* 切换主题
*/
function handeChangeTheme() {
const randomTheme = THEMES[Math.floor(Math.random() * THEMES.length)] // 从THEMES数组中 随机取一个主题
const query = router.query
query.theme = randomTheme
router.push({ pathname: router.pathname, query })
}
function 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 (
<div
ref={menuRef}
style={{ top: position.y, left: position.x }}
className={`${show ? '' : 'invisible opacity-0'} select-none transition-opacity duration-200 fixed z-50`}
>
{/* 菜单内容 */}
<div className='rounded-xl w-52 dark:hover:border-yellow-600 bg-white dark:bg-[#040404] dark:text-gray-200 dark:border-gray-600 p-3 border drop-shadow-lg flex-col duration-300 transition-colors'>
{/* 顶部导航按钮 */}
<div className='flex justify-between'>
<i onClick={handleBack} className="hover:bg-blue-600 hover:text-white px-2 py-2 text-center w-8 rounded cursor-pointer fa-solid fa-arrow-left"></i>
<i onClick={handleForward} className="hover:bg-blue-600 hover:text-white px-2 py-2 text-center w-8 rounded cursor-pointer fa-solid fa-arrow-right"></i>
<i onClick={handleRefresh} className="hover:bg-blue-600 hover:text-white px-2 py-2 text-center w-8 rounded cursor-pointer fa-solid fa-rotate-right"></i>
<i onClick={handleScrollTop} className="hover:bg-blue-600 hover:text-white px-2 py-2 text-center w-8 rounded cursor-pointer fa-solid fa-arrow-up"></i>
</div>
<hr className='my-2 border-dashed' />
{/* 跳转导航按钮 */}
<div className='w-full px-2'>
<div onClick={handleJumpToRandomPost} title={locale.MENU.WALK_AROUND} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className="fa-solid fa-podcast mr-2" />
<div className='whitespace-nowrap'>{locale.MENU.WALK_AROUND}</div>
</div>
<Link href='/category' title={locale.MENU.CATEGORY} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className="fa-solid fa-square-minus mr-2" />
<div className='whitespace-nowrap'>{locale.MENU.CATEGORY}</div>
</Link>
<Link href='/tag' title={locale.MENU.TAGS} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className="fa-solid fa-tag mr-2" />
<div className='whitespace-nowrap'>{locale.MENU.TAGS}</div>
</Link>
</div>
<hr className='my-2 border-dashed' />
{/* 功能按钮 */}
<div className='w-full px-2'>
<div onClick={handleCopyLink} title={locale.MENU.COPY_URL} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className="fa-solid fa-arrow-up-right-from-square mr-2" />
<div className='whitespace-nowrap'>{locale.MENU.COPY_URL}</div>
</div>
<div onClick={handleChangeDarkMode} title={isDarkMode ? locale.MENU.LIGHT_MODE : locale.MENU.DARK_MODE} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
{isDarkMode ? <i className="fa-regular fa-sun mr-2" /> : <i className="fa-regular fa-moon mr-2" />}
<div className='whitespace-nowrap'> {isDarkMode ? locale.MENU.LIGHT_MODE : locale.MENU.DARK_MODE}</div>
</div>
<div onClick={handeChangeTheme} title={locale.MENU.THEME_SWITCH} className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className="fa-solid fa-palette mr-2" />
<div className='whitespace-nowrap'>{locale.MENU.THEME_SWITCH}</div>
</div>
</div>
</div>
</div >
)
}

View File

@@ -1,9 +1,26 @@
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import { saveDarkModeToCookies } from '@/themes/theme' import { saveDarkModeToCookies } from '@/themes/theme'
import { Moon, Sun } from './HeroIcons' import { Moon, Sun } from './HeroIcons'
import { useImperativeHandle } from 'react'
/**
* 深色模式按钮
*/
const DarkModeButton = (props) => { const DarkModeButton = (props) => {
const { cRef, className } = props
const { isDarkMode, updateDarkMode } = useGlobal() const { isDarkMode, updateDarkMode } = useGlobal()
/**
* 对外暴露方法
*/
useImperativeHandle(cRef, () => {
return {
handleChangeDarkMode: () => {
handleChangeDarkMode()
}
}
})
// 用户手动设置主题 // 用户手动设置主题
const handleChangeDarkMode = () => { const handleChangeDarkMode = () => {
const newStatus = !isDarkMode const newStatus = !isDarkMode
@@ -14,7 +31,7 @@ const DarkModeButton = (props) => {
htmlElement.classList?.add(newStatus ? 'dark' : 'light') htmlElement.classList?.add(newStatus ? 'dark' : 'light')
} }
return <div onClick={handleChangeDarkMode} className={`${props.className ? props.className : ''} flex justify-center dark:text-gray-200 text-gray-800`}> return <div onClick={handleChangeDarkMode} className={`${className || ''} flex justify-center dark:text-gray-200 text-gray-800`}>
<div id='darkModeButton' className=' hover:scale-110 cursor-pointer transform duration-200 w-5 h-5'> {isDarkMode ? <Sun /> : <Moon />}</div> <div id='darkModeButton' className=' hover:scale-110 cursor-pointer transform duration-200 w-5 h-5'> {isDarkMode ? <Sun /> : <Moon />}</div>
</div> </div>
} }

View File

@@ -29,6 +29,7 @@ const Busuanzi = dynamic(() => import('@/components/Busuanzi'), { ssr: false })
const GoogleAdsense = dynamic(() => import('@/components/GoogleAdsense'), { ssr: false }) const GoogleAdsense = dynamic(() => import('@/components/GoogleAdsense'), { ssr: false })
const Messenger = dynamic(() => import('@/components/FacebookMessenger'), { ssr: false }) const Messenger = dynamic(() => import('@/components/FacebookMessenger'), { ssr: false })
const VConsole = dynamic(() => import('@/components/VConsole'), { ssr: false }) const VConsole = dynamic(() => import('@/components/VConsole'), { ssr: false })
const CustomContextMenu = dynamic(() => import('@/components/CustomContextMenu'), { ssr: false })
/** /**
* 各种第三方组件 * 各种第三方组件
@@ -53,6 +54,7 @@ const ExternalPlugin = (props) => {
{JSON.parse(BLOG.FLUTTERINGRIBBON) && <FlutteringRibbon />} {JSON.parse(BLOG.FLUTTERINGRIBBON) && <FlutteringRibbon />}
{JSON.parse(BLOG.COMMENT_TWIKOO_COUNT_ENABLE) && <TwikooCommentCounter {...props}/>} {JSON.parse(BLOG.COMMENT_TWIKOO_COUNT_ENABLE) && <TwikooCommentCounter {...props}/>}
{JSON.parse(BLOG.RIBBON) && <Ribbon />} {JSON.parse(BLOG.RIBBON) && <Ribbon />}
{JSON.parse(BLOG.CUSTOM_RIGHT_CLICK_CONTEXT_MENU) && <CustomContextMenu {...props} />}
<VConsole/> <VConsole/>
</> </>
} }

View File

@@ -1,15 +1,15 @@
'use client'
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
import { loadExternalResource } from '@/lib/utils' import { isBrowser, loadExternalResource } from '@/lib/utils'
import { useEffect } from 'react'
/** /**
* 自定义引入外部JS 和 CSS * 自定义引入外部JS 和 CSS
* @returns * @returns
*/ */
const ExternalScript = () => { const ExternalScript = () => {
useEffect(() => { if (isBrowser()) {
// 静态导入本地自定义样式 // 静态导入本地自定义样式
loadExternalResource(BLOG.FONT_AWESOME, 'css')
loadExternalResource('/css/custom.css', 'css') loadExternalResource('/css/custom.css', 'css')
loadExternalResource('/js/custom.js', 'js') loadExternalResource('/js/custom.js', 'js')
@@ -28,12 +28,7 @@ const ExternalScript = () => {
loadExternalResource(url, 'css') loadExternalResource(url, 'css')
} }
} }
// 渲染所有字体 }
BLOG.FONT_URL?.forEach(e => {
loadExternalResource(e, 'css')
})
}, [])
return null return null
} }

56
components/FlipCard.js Normal file
View File

@@ -0,0 +1,56 @@
import React, { useState } from 'react'
/**
* 翻转组件
* @param {*} props
* @returns
*/
export default function FlipCard(props) {
const [isFlipped, setIsFlipped] = useState(false)
function handleCardFlip() {
setIsFlipped(!isFlipped)
}
return (
<div className={`flip-card ${isFlipped ? 'flipped' : ''}`} >
<div className={`flip-card-front ${props.className || ''}`} onMouseEnter={handleCardFlip}>
{props.frontContent}
</div>
<div className={`flip-card-back ${props.className || ''}`} onMouseLeave={handleCardFlip}>
{props.backContent}
</div>
<style jsx>{`
.flip-card {
width: 100%;
height: 100%;
display: inline-block;
position: relative;
transform-style: preserve-3d;
transition: transform 0.2s;
}
.flip-card-front,
.flip-card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
}
.flip-card-front {
z-index: 2;
transform: rotateY(0);
}
.flip-card-back {
transform: rotateY(180deg);
}
.flip-card.flipped {
transform: rotateY(180deg);
}
`}</style>
</div>
)
}

View File

@@ -0,0 +1,48 @@
import { isBrowser } from '@/lib/utils'
import React, { useState } from 'react'
/**
* 全屏按钮
* @returns
*/
const FullScreenButton = () => {
const [isFullScreen, setIsFullScreen] = useState(false)
const handleFullScreenClick = () => {
if (!isBrowser()) {
return
}
const element = document.documentElement
if (!isFullScreen) {
if (element.requestFullscreen) {
element.requestFullscreen()
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen()
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen()
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen()
}
setIsFullScreen(true)
} else {
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen()
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen()
} else if (document.msExitFullscreen) {
document.msExitFullscreen()
}
setIsFullScreen(false)
}
}
return (
<button onClick={handleFullScreenClick} className='dark:text-gray-300'>
{isFullScreen ? '退出全屏' : <i className="fa-solid fa-expand"></i>}
</button>
)
}
export default FullScreenButton

View File

@@ -1,4 +1,5 @@
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
import { loadExternalResource } from '@/lib/utils'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useEffect } from 'react' import { useEffect } from 'react'
@@ -8,25 +9,35 @@ import { useEffect } from 'react'
*/ */
export default function GoogleAdsense() { export default function GoogleAdsense() {
const initGoogleAdsense = () => { const initGoogleAdsense = () => {
setTimeout(() => { // GoogleAdsense 本地开发请加入 data-adbreak-test="on"
const ads = document.getElementsByClassName('adsbygoogle') // {BLOG.ADSENSE_GOOGLE_ID && <script async src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${BLOG.ADSENSE_GOOGLE_ID}`}
const adsbygoogle = window.adsbygoogle // crossOrigin="anonymous" />}
if (ads.length > 0) {
for (let i = 0; i <= ads.length; i++) {
try {
adsbygoogle.push(ads[i])
console.log('adsbygoogle', i, ads[i], adsbygoogle)
} catch (e) {
loadExternalResource(`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${BLOG.ADSENSE_GOOGLE_ID}`, 'js').then(url => {
setTimeout(() => {
const ads = document.getElementsByClassName('adsbygoogle')
const adsbygoogle = window.adsbygoogle
console.log('google-ads', adsbygoogle)
if (ads.length > 0) {
for (let i = 0; i <= ads.length; i++) {
try {
adsbygoogle.push(ads[i])
console.log('adsbygoogle', i, ads[i], adsbygoogle)
} catch (e) {
}
} }
} }
} }, 100)
}, 100) })
} }
const router = useRouter() const router = useRouter()
useEffect(() => { useEffect(() => {
initGoogleAdsense() // 延迟3秒加载
setTimeout(() => {
initGoogleAdsense()
}, 3000)
}, [router]) }, [router])
return null return null
@@ -50,7 +61,7 @@ const AdSlot = ({ type = 'show' }) => {
data-ad-format="fluid" data-ad-format="fluid"
data-adtest={BLOG.ADSENSE_GOOGLE_TEST ? 'on' : 'off'} data-adtest={BLOG.ADSENSE_GOOGLE_TEST ? 'on' : 'off'}
data-ad-client={BLOG.ADSENSE_GOOGLE_ID} data-ad-client={BLOG.ADSENSE_GOOGLE_ID}
data-ad-slot="3806269138"></ins> data-ad-slot={BLOG.ADSENSE_GOOGLE_SLOT_IN_ARTICLE}></ins>
} }
// 信息流广告 // 信息流广告
@@ -61,7 +72,7 @@ const AdSlot = ({ type = 'show' }) => {
style={{ display: 'block' }} style={{ display: 'block' }}
data-adtest={BLOG.ADSENSE_GOOGLE_TEST ? 'on' : 'off'} data-adtest={BLOG.ADSENSE_GOOGLE_TEST ? 'on' : 'off'}
data-ad-client={BLOG.ADSENSE_GOOGLE_ID} data-ad-client={BLOG.ADSENSE_GOOGLE_ID}
data-ad-slot="1510444138"></ins> data-ad-slot={BLOG.ADSENSE_GOOGLE_SLOT_FLOW}></ins>
} }
// 原生广告 // 原生广告
@@ -71,7 +82,7 @@ const AdSlot = ({ type = 'show' }) => {
data-ad-format="autorelaxed" data-ad-format="autorelaxed"
data-adtest={BLOG.ADSENSE_GOOGLE_TEST ? 'on' : 'off'} data-adtest={BLOG.ADSENSE_GOOGLE_TEST ? 'on' : 'off'}
data-ad-client={BLOG.ADSENSE_GOOGLE_ID} data-ad-client={BLOG.ADSENSE_GOOGLE_ID}
data-ad-slot="4980048999"></ins> data-ad-slot={BLOG.ADSENSE_GOOGLE_SLOT_NATIVE}></ins>
} }
// 展示广告 // 展示广告
@@ -79,7 +90,7 @@ const AdSlot = ({ type = 'show' }) => {
style={{ display: 'block' }} style={{ display: 'block' }}
data-ad-client={BLOG.ADSENSE_GOOGLE_ID} data-ad-client={BLOG.ADSENSE_GOOGLE_ID}
data-adtest={BLOG.ADSENSE_GOOGLE_TEST ? 'on' : 'off'} data-adtest={BLOG.ADSENSE_GOOGLE_TEST ? 'on' : 'off'}
data-ad-slot="8807314373" data-ad-slot={BLOG.ADSENSE_GOOGLE_SLOT_AUTO}
data-ad-format="auto" data-ad-format="auto"
data-full-width-responsive="true"></ins> data-full-width-responsive="true"></ins>
} }

View File

@@ -3,27 +3,98 @@
* @returns * @returns
*/ */
const Moon = () => { export const Moon = () => {
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor"> return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
</svg> </svg>
} }
const Sun = () => { export const Sun = () => {
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor"> return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
</svg>
}
export const Home = ({ className }) => {
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
</svg>
}
export const User = ({ className }) => {
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
}
export const ArrowPath = ({ className }) => {
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
}
export const ChevronLeft = ({ className }) => {
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
}
export const ChevronRight = ({ className }) => {
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
}
export const ChevronDoubleLeft = ({ className }) => {
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M18.75 19.5l-7.5-7.5 7.5-7.5m-6 15L5.25 12l7.5-7.5" />
</svg> </svg>
} }
const Home = ({ className }) => { export const ChevronDoubleRight = ({ className }) => {
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}> return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" /> <path strokeLinecap="round" strokeLinejoin="round" d="M11.25 4.5l7.5 7.5-7.5 7.5m-6-15l7.5 7.5-7.5 7.5" />
</svg> </svg>
} }
const User = ({ className }) => { export const InformationCircle = ({ className }) => {
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}> return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
</svg>
}
export const HashTag = ({ className }) => {
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5.25 8.25h15m-16.5 7.5h15m-1.8-13.5l-3.9 19.5m-2.1-19.5l-3.9 19.5" />
</svg>
}
export const GlobeAlt = ({ className }) => {
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418" />
</svg>
}
export const ArrowRightCircle = ({ className }) => {
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12.75 15l3-3m0 0l-3-3m3 3h-7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
}
export const PlusSmall = ({ className }) => {
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v12m6-6H6" />
</svg>
}
export const ArrowSmallRight = ({ className }) => {
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75" />
</svg>
}
export const ArrowSmallUp = ({ className }) => {
return <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 19.5v-15m0 0l-6.75 6.75M12 4.5l6.75 6.75" />
</svg> </svg>
} }
export { Moon, Sun, Home, User }

95
components/LazyImage.js Normal file
View File

@@ -0,0 +1,95 @@
import BLOG from '@/blog.config'
import Head from 'next/head'
import React, { useEffect, useRef, useState } from 'react'
/**
* 图片懒加载
* @param {*} param0
* @returns
*/
export default function LazyImage({
priority,
id,
src,
alt,
placeholderSrc = BLOG.IMG_LAZY_LOAD_PLACEHOLDER,
className,
width,
height,
title,
onLoad,
style
}) {
const imageRef = useRef(null)
const [imageLoaded, setImageLoaded] = useState(false)
const handleImageLoad = () => {
setImageLoaded(true)
if (typeof onLoad === 'function') {
onLoad() // 触发传递的onLoad回调函数
}
}
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const lazyImage = entry.target
lazyImage.src = src
observer.unobserve(lazyImage)
}
})
},
{ rootMargin: '50px 0px' } // Adjust the rootMargin as needed to trigger the loading earlier or later
)
if (imageRef.current) {
observer.observe(imageRef.current)
}
return () => {
if (imageRef.current) {
observer.unobserve(imageRef.current)
}
}
}, [src])
// 动态添加width、height和className属性仅在它们为有效值时添加
const imgProps = {
ref: imageRef,
src: imageLoaded ? src : placeholderSrc,
alt: alt,
onLoad: handleImageLoad
}
if (id) {
imgProps.id = id
}
if (title) {
imgProps.title = title
}
if (width && width !== 'auto') {
imgProps.width = width
}
if (height && height !== 'auto') {
imgProps.height = height
}
if (className) {
imgProps.className = className
}
if (style) {
imgProps.style = style
}
return (<>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img {...imgProps} />
{/* 预加载 */}
{priority && <Head>
<link rel='preload' as='image' src={src} />
</Head>}
</>)
}

View File

@@ -1,14 +0,0 @@
/**
* 加载文件时的全局遮罩
* @returns
*/
const LoadingCover = (props) => {
const { onReading } = props
return <div className={`${onReading ? 'opacity-30' : 'opacity-0'} bg-black text-white shadow-text w-screen h-screen flex justify-center items-center
transition-all fixed top-0 left-0 pointer-events-none duration-1000 z-50 shadow-inner`}>
<i className='text-3xl mr-5 fas fa-spinner animate-spin' />
</div>
}
export default LoadingCover

31
components/Mark.js Normal file
View File

@@ -0,0 +1,31 @@
import { loadExternalResource } from '@/lib/utils'
/**
* 将搜索结果的关键词高亮
*/
export default async function replaceSearchResult({ doms, search, target }) {
if (!doms || !search || !target) {
return
}
try {
const url = await loadExternalResource('https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/mark.min.js', 'js')
console.log('markjs 加载成功', url, window.Mark)
console.log('------', doms)
const Mark = window.Mark
if (doms instanceof HTMLCollection) {
for (const container of doms) {
const re = new RegExp(search, 'gim')
const instance = new Mark(container)
instance.markRegExp(re, target)
}
} else {
const re = new RegExp(search, 'gim')
const instance = new Mark(doms)
instance.markRegExp(re, target)
}
} catch (error) {
console.error('markjs 加载失败', error)
}
}

View File

@@ -1,3 +1,5 @@
import LazyImage from './LazyImage'
/** /**
* notion的图标icon * notion的图标icon
* 可能是emoji 可能是 svg 也可能是 图片 * 可能是emoji 可能是 svg 也可能是 图片
@@ -9,9 +11,7 @@ const NotionIcon = ({ icon }) => {
} }
if (icon.startsWith('http') || icon.startsWith('data:')) { if (icon.startsWith('http') || icon.startsWith('data:')) {
// return <Image src={icon} width={30} height={30}/> return <LazyImage src={icon} className='w-8 h-8 my-auto inline mr-1'/>
// eslint-disable-next-line @next/next/no-img-element
return <img src={icon} className='w-8 h-8 my-auto inline mr-1'/>
} }
return <span className='mr-1'>{icon}</span> return <span className='mr-1'>{icon}</span>

View File

@@ -5,9 +5,9 @@ import React, { useEffect, useRef } from 'react'
// import { Code } from 'react-notion-x/build/third-party/code' // import { Code } from 'react-notion-x/build/third-party/code'
import TweetEmbed from 'react-tweet-embed' import TweetEmbed from 'react-tweet-embed'
import BLOG from '@/blog.config'
import 'katex/dist/katex.min.css' import 'katex/dist/katex.min.css'
import { mapImgUrl } from '@/lib/notion/mapImage' import { mapImgUrl } from '@/lib/notion/mapImage'
import BLOG from '@/blog.config'
import { isBrowser } from '@/lib/utils' import { isBrowser } from '@/lib/utils'
const Code = dynamic(() => const Code = dynamic(() =>
@@ -86,7 +86,7 @@ const NotionPage = ({ post, className }) => {
return <>{post?.summary || ''}</> return <>{post?.summary || ''}</>
} }
return <div id='notion-article' className={`mx-auto ${className || ''}`}> return <div id='notion-article' className={`mx-auto overflow-x-hidden ${className || ''}`}>
<NotionRenderer <NotionRenderer
recordMap={post.blockMap} recordMap={post.blockMap}
mapPageUrl={mapPageUrl} mapPageUrl={mapPageUrl}

View File

@@ -13,68 +13,55 @@ import 'prismjs/plugins/line-numbers/prism-line-numbers.css'
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
import { loadExternalResource } from '@/lib/utils' import { loadExternalResource } from '@/lib/utils'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useGlobal } from '@/lib/global'
/** /**
* 代码美化相关
* @author https://github.com/txs/ * @author https://github.com/txs/
* @returns * @returns
*/ */
const PrismMac = () => { const PrismMac = () => {
const router = useRouter() const router = useRouter()
const { isDarkMode } = useGlobal()
useEffect(() => { useEffect(() => {
const handleDarkModeChange = () => { if (JSON.parse(BLOG.CODE_MAC_BAR)) {
// 加载prism样式 loadExternalResource('/css/prism-mac-style.css', 'css')
loadPrismThemeCSS() }
if (JSON.parse(BLOG.CODE_MAC_BAR)) { // 加载prism样式
loadExternalResource('/css/prism-mac-style.css', 'css') loadPrismThemeCSS(isDarkMode)
// 折叠代码
loadExternalResource(BLOG.PRISM_JS_AUTO_LOADER, 'js').then((url) => {
if (window?.Prism?.plugins?.autoloader) {
window.Prism.plugins.autoloader.languages_path = BLOG.PRISM_JS_PATH
} }
loadExternalResource(BLOG.PRISM_JS_AUTO_LOADER, 'js').then((url) => {
if (window?.Prism?.plugins?.autoloader) {
window.Prism.plugins.autoloader.languages_path = BLOG.PRISM_JS_PATH
}
renderPrismMac()
renderMermaid()
})
}
handleDarkModeChange()
const handleDarkModeToggle = () => { renderPrismMac()
const currentTheme = document.documentElement.className renderMermaid()
handleDarkModeChange() renderCollapseCode()
document.documentElement.className = currentTheme === 'light' ? 'dark' : 'light' })
} }, [router, isDarkMode])
const darkModeSwitchButton = document.getElementById('darkModeButton')
darkModeSwitchButton.addEventListener('click', handleDarkModeToggle)
return () => {
darkModeSwitchButton.removeEventListener('click', handleDarkModeToggle)
}
}, [router])
return <></> return <></>
} }
/** /**
* 加载样式 * 加载样式
*/ */
const loadPrismThemeCSS = () => { const loadPrismThemeCSS = (isDarkMode) => {
let PRISM_THEME let PRISM_THEME
let PRISM_PREVIOUS let PRISM_PREVIOUS
const themeClass = document.documentElement.className
if (JSON.parse(BLOG.PRISM_THEME_SWITCH)) { if (JSON.parse(BLOG.PRISM_THEME_SWITCH)) {
if (themeClass === 'dark') { if (isDarkMode) {
PRISM_THEME = BLOG.PRISM_THEME_DARK_PATH PRISM_THEME = BLOG.PRISM_THEME_DARK_PATH
PRISM_PREVIOUS = BLOG.PRISM_THEME_LIGHT_PATH PRISM_PREVIOUS = BLOG.PRISM_THEME_LIGHT_PATH
const previousTheme = document.querySelector(`link[href="${PRISM_PREVIOUS}"]`)
if (previousTheme) {
previousTheme.parentNode.removeChild(previousTheme)
}
} else { } else {
PRISM_THEME = BLOG.PRISM_THEME_LIGHT_PATH PRISM_THEME = BLOG.PRISM_THEME_LIGHT_PATH
PRISM_PREVIOUS = BLOG.PRISM_THEME_DARK_PATH PRISM_PREVIOUS = BLOG.PRISM_THEME_DARK_PATH
const previousTheme = document.querySelector(`link[href="${PRISM_PREVIOUS}"]`) }
if (previousTheme) { const previousTheme = document.querySelector(`link[href="${PRISM_PREVIOUS}"]`)
previousTheme.parentNode.removeChild(previousTheme) if (previousTheme) {
} previousTheme.parentNode.removeChild(previousTheme)
} }
loadExternalResource(PRISM_THEME, 'css') loadExternalResource(PRISM_THEME, 'css')
} else { } else {
@@ -82,6 +69,52 @@ const loadPrismThemeCSS = () => {
} }
} }
/*
* 将代码块转为可折叠对象
*/
const renderCollapseCode = () => {
if (!JSON.parse(BLOG.CODE_COLLAPSE)) {
return
}
const codeBlocks = document.querySelectorAll('.code-toolbar')
for (const codeBlock of codeBlocks) {
// 判断当前元素是否被包裹
if (codeBlock.closest('.collapse-wrapper')) {
continue // 如果被包裹了,跳过当前循环
}
const code = codeBlock.querySelector('code')
const language = code.getAttribute('class').match(/language-(\w+)/)[1]
const collapseWrapper = document.createElement('div')
collapseWrapper.className = 'collapse-wrapper w-full py-2'
const panelWrapper = document.createElement('div')
panelWrapper.className = 'border dark:border-gray-600 rounded-md hover:border-indigo-500 duration-200 transition-colors'
const header = document.createElement('div')
header.className = 'flex justify-between items-center px-4 py-2 cursor-pointer select-none'
header.innerHTML = `<h3 class="text-lg font-medium">${language}</h3><svg class="transition-all duration-200 w-5 h-5 transform rotate-0" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M6.293 6.293a1 1 0 0 1 1.414 0L10 8.586l2.293-2.293a1 1 0 0 1 1.414 1.414l-3 3a1 1 0 0 1-1.414 0l-3-3a1 1 0 0 1 0-1.414z" clip-rule="evenodd"/></svg>`
const panel = document.createElement('div')
panel.className = 'invisible h-0 transition-transform duration-200 border-t border-gray-300'
panelWrapper.appendChild(header)
panelWrapper.appendChild(panel)
collapseWrapper.appendChild(panelWrapper)
codeBlock.parentNode.insertBefore(collapseWrapper, codeBlock)
panel.appendChild(codeBlock)
header.addEventListener('click', () => {
panel.classList.toggle('invisible')
panel.classList.toggle('h-0')
panel.classList.toggle('h-auto')
header.querySelector('svg').classList.toggle('rotate-180')
panelWrapper.classList.toggle('border-gray-300')
})
}
}
/** /**
* 将mermaid语言 渲染成图片 * 将mermaid语言 渲染成图片
*/ */
@@ -116,13 +149,13 @@ const renderMermaid = async() => {
} }
} }
}) })
if (document.querySelector('#container-inner')) { if (document.querySelector('#notion-article')) {
observer.observe(document.querySelector('#container-inner'), { attributes: true, subtree: true }) observer.observe(document.querySelector('#notion-article'), { attributes: true, subtree: true })
} }
} }
function renderPrismMac() { function renderPrismMac() {
const container = document?.getElementById('container-inner') const container = document?.getElementById('notion-article')
// Add line numbers // Add line numbers
if (BLOG.CODE_LINE_NUMBERS === 'true') { if (BLOG.CODE_LINE_NUMBERS === 'true') {
@@ -179,11 +212,11 @@ const fixCodeLineStyle = () => {
} }
} }
}) })
observer.observe(document.querySelector('#container'), { attributes: true, subtree: true }) observer.observe(document.querySelector('#notion-article'), { attributes: true, subtree: true })
setTimeout(() => { setTimeout(() => {
const preCodes = document.querySelectorAll('pre.notion-code') const preCodes = document.querySelectorAll('pre.notion-code')
for (const preCode of preCodes) { for (const preCode of preCodes) {
console.log('code', preCode) // console.log('code', preCode)
Prism.plugins.lineNumbers.resize(preCode) Prism.plugins.lineNumbers.resize(preCode)
} }
}, 10) }, 10)

View File

@@ -1,7 +1,7 @@
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import QRCode from 'qrcode.react' import dynamic from 'next/dynamic'
import { useState } from 'react' import { useState } from 'react'
import { import {
@@ -49,6 +49,13 @@ import {
HatenaIcon HatenaIcon
} from 'react-share' } from 'react-share'
const QRCode = dynamic(
() => {
return import('qrcode.react')
},
{ ssr: false }
)
/** /**
* @author https://github.com/txs * @author https://github.com/txs
* @param {*} param0 * @param {*} param0

View File

@@ -1,8 +1,9 @@
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import React from 'react' import React, { useState } from 'react'
import { Draggable } from './Draggable' import { Draggable } from './Draggable'
import { THEMES } from '@/themes/theme' import { THEMES } from '@/themes/theme'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import DarkModeButton from './DarkModeButton'
/** /**
* *
* @returns 主题切换 * @returns 主题切换
@@ -10,28 +11,40 @@ import { useRouter } from 'next/router'
const ThemeSwitch = () => { const ThemeSwitch = () => {
const { theme } = useGlobal() const { theme } = useGlobal()
const router = useRouter() const router = useRouter()
const [isLoading, setIsLoading] = useState(false)
// 修改当前路径url中的 theme 参数 // 修改当前路径url中的 theme 参数
// 例如 http://localhost?theme=hexo 跳转到 http://localhost?theme=newTheme // 例如 http://localhost?theme=hexo 跳转到 http://localhost?theme=newTheme
const onSelectChange = (e) => { const onSelectChange = (e) => {
setIsLoading(true)
const newTheme = e.target.value const newTheme = e.target.value
const query = router.query const query = router.query
query.theme = newTheme query.theme = newTheme
router.push({ pathname: router.pathname, query }) router.push({ pathname: router.pathname, query }).then(() => {
setIsLoading(false)
})
} }
return (<> return (<>
<Draggable> <Draggable>
<div id="draggableBox" style={{ left: '10px', top: '85vh' }} className="fixed text-white bg-black z-50 rounded-lg shadow-card"> <div id="draggableBox" style={{ left: '10px', top: '80vh' }} className="fixed z-50 dark:text-white bg-gray-50 dark:bg-black rounded-2xl drop-shadow-lg">
<div className="py-2 flex items-center text-sm px-2"> <div className="p-3 w-full flex items-center text-sm group duration-200 transition-all">
<select value={theme} onChange={onSelectChange} name="cars" className='text-white bg-black uppercase cursor-pointer'> <DarkModeButton className='mr-2' />
{THEMES?.map(t => { <div className='w-0 group-hover:w-20 transition-all duration-200 overflow-hidden'>
return <option key={t} value={t}>{t}</option> <select value={theme} onChange={onSelectChange} name="themes" className='appearance-none outline-none dark:text-white bg-gray-50 dark:bg-black uppercase cursor-pointer'>
})} {THEMES?.map(t => {
</select> return <option key={t} value={t}>{t}</option>
<i className='fas fa-palette pl-1' /> })}
</select>
</div>
<i className="fa-solid fa-palette pl-2"></i>
</div> </div>
</div> </div>
{/* 切换主题加载时的全屏遮罩 */}
<div className={`${isLoading ? 'opacity-50 ' : 'opacity-0'} w-screen h-screen bg-black text-white shadow-text flex justify-center items-center
transition-all fixed top-0 left-0 pointer-events-none duration-1000 z-50 shadow-inner`}>
<i className='text-3xl mr-5 fas fa-spinner animate-spin' />
</div>
</Draggable> </Draggable>
</> </>
) )

67
components/WordCount.js Normal file
View File

@@ -0,0 +1,67 @@
import { useGlobal } from '@/lib/global'
import { useEffect } from 'react'
/**
* 字数统计
* @returns
*/
export default function WordCount() {
const { locale } = useGlobal()
useEffect(() => {
countWords()
})
return <span id='wordCountWrapper' className='flex gap-3 font-light'>
<span className='flex whitespace-nowrap items-center'>
<i className='pl-1 pr-2 fas fa-file-word' />
<span id='wordCount'>0</span>
</span>
<span className='flex whitespace-nowrap items-center'>
<i className='mr-1 fas fa-clock' />
<span></span>
<span id='readTime'>0</span>&nbsp;{locale.COMMON.MINUTE}
</span>
</span>
}
/**
* 更新字数统计和阅读时间
*/
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标签和&nbsp;之类的特殊符合
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
}

42
lib/algolia.js Normal file
View File

@@ -0,0 +1,42 @@
import BLOG from '@/blog.config'
import { getPageContentText } from '@/pages/search/[keyword]'
import algoliasearch from 'algoliasearch'
/**
* 生成全文索引
* @param {*} allPages
*/
const generateAlgoliaSearch = async({ allPages, force = false }) => {
allPages?.forEach(p => {
// 判断这篇文章是否需要重新创建索引
if (p && !p.password) {
uploadDataToAlgolia(p)
}
})
}
/**
* 上传数据
*/
const uploadDataToAlgolia = (post) => {
// Connect and authenticate with your Algolia app
const client = algoliasearch(BLOG.ALGOLIA_APP_ID, BLOG.ALGOLIA_ADMIN_APP_KEY)
// Create a new index and add a record
const index = client.initIndex(BLOG.ALGOLIA_INDEX)
const record = {
objectID: post.id,
title: post.title,
category: post.category,
tags: post.tags,
pageCover: post.pageCover,
slug: post.slug,
summary: post.summary,
content: getPageContentText(post, post.blockMap)
}
index.saveObject(record).wait().then(r => {
console.log('Algolia索引', r, record)
})
}
export { uploadDataToAlgolia, generateAlgoliaSearch }

View File

@@ -21,24 +21,11 @@ export function GlobalContextProvider({ children }) {
const [theme, setTheme] = useState(BLOG.THEME) // 默认博客主题 const [theme, setTheme] = useState(BLOG.THEME) // 默认博客主题
const [isDarkMode, updateDarkMode] = useState(BLOG.APPEARANCE === 'dark') // 默认深色模式 const [isDarkMode, updateDarkMode] = useState(BLOG.APPEARANCE === 'dark') // 默认深色模式
const [onLoading, setOnLoading] = useState(false) // 抓取文章数据 const [onLoading, setOnLoading] = useState(false) // 抓取文章数据
// const [onReading, setOnReading] = useState(false) // 网页资源加载
useEffect(() => { useEffect(() => {
initLocale(lang, locale, updateLang, updateLocale) initLocale(lang, locale, updateLang, updateLocale)
initDarkMode(isDarkMode, updateDarkMode) initDarkMode(isDarkMode, updateDarkMode)
initTheme() initTheme()
if (isBrowser()) {
// 监听用户刷新页面
const handleBeforeUnload = (event) => {
// setOnReading(true)
}
// 监听页面元素加载完
// setOnReading(false)
window.addEventListener('beforeunload', handleBeforeUnload)
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload)
}
}
}, []) }, [])
useEffect(() => { useEffect(() => {
@@ -72,7 +59,8 @@ export function GlobalContextProvider({ children }) {
const currentIndex = THEMES.indexOf(theme) const currentIndex = THEMES.indexOf(theme)
const newIndex = currentIndex < THEMES.length - 1 ? currentIndex + 1 : 0 const newIndex = currentIndex < THEMES.length - 1 ? currentIndex + 1 : 0
const newTheme = THEMES[newIndex] const newTheme = THEMES[newIndex]
const query = { ...router.query, theme: newTheme } const query = router.query
query.theme = newTheme
router.push({ pathname: router.pathname, query }) router.push({ pathname: router.pathname, query })
return newTheme return newTheme
} }

View File

@@ -1,7 +1,16 @@
export default { export default {
LOCALE: 'en-US', 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: { NAV: {
INDEX: 'Blog', INDEX: 'Home',
RSS: 'RSS', RSS: 'RSS',
SEARCH: 'Search', SEARCH: 'Search',
ABOUT: 'About', ABOUT: 'About',
@@ -35,6 +44,7 @@ export default {
SUBMIT: 'Submit', SUBMIT: 'Submit',
POST_TIME: 'Post on', POST_TIME: 'Post on',
LAST_EDITED_TIME: 'Last edited', LAST_EDITED_TIME: 'Last edited',
COMMENTS: 'Comments',
RECENT_COMMENTS: 'Recent Comments', RECENT_COMMENTS: 'Recent Comments',
DEBUG_OPEN: 'Debug', DEBUG_OPEN: 'Debug',
DEBUG_CLOSE: 'Close', DEBUG_CLOSE: 'Close',

View File

@@ -1,5 +1,14 @@
export default { export default {
LOCALE: 'zh-CN', LOCALE: 'zh-CN',
MENU: {
WALK_AROUND: '随便逛逛',
CATEGORY: '博客分类',
TAGS: '博客标签',
COPY_URL: '复制地址',
DARK_MODE: '深色模式',
LIGHT_MODE: '浅色模式',
THEME_SWITCH: '主题切换'
},
NAV: { NAV: {
INDEX: '首页', INDEX: '首页',
RSS: '订阅', RSS: '订阅',
@@ -12,7 +21,7 @@ export default {
COMMON: { COMMON: {
MORE: '更多', MORE: '更多',
NO_MORE: '没有更多了', NO_MORE: '没有更多了',
LATEST_POSTS: '最新文章', LATEST_POSTS: '最新发布',
TAGS: '标签', TAGS: '标签',
NO_TAG: 'NoTag', NO_TAG: 'NoTag',
CATEGORY: '分类', CATEGORY: '分类',
@@ -37,6 +46,7 @@ export default {
SUBMIT: '提交', SUBMIT: '提交',
POST_TIME: '发布于', POST_TIME: '发布于',
LAST_EDITED_TIME: '最后更新', LAST_EDITED_TIME: '最后更新',
COMMENTS: '评论',
RECENT_COMMENTS: '最新评论', RECENT_COMMENTS: '最新评论',
DEBUG_OPEN: '开启调试', DEBUG_OPEN: '开启调试',
DEBUG_CLOSE: '关闭调试', DEBUG_CLOSE: '关闭调试',
@@ -47,8 +57,8 @@ export default {
WORD_COUNT: '字数' WORD_COUNT: '字数'
}, },
PAGINATION: { PAGINATION: {
PREV: '上页', PREV: '上页',
NEXT: '下页' NEXT: '下页'
}, },
SEARCH: { SEARCH: {
ARTICLES: '搜索文章', ARTICLES: '搜索文章',

View File

@@ -102,7 +102,7 @@ function getCustomNav({ allPages }) {
* @returns * @returns
*/ */
function getCustomMenu({ collectionData }) { function getCustomMenu({ collectionData }) {
const menuPages = collectionData.filter(post => (post.type === BLOG.NOTION_PROPERTY_NAME.type_menu || post.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu) && post.status === 'Published') const menuPages = collectionData.filter(post => (post?.type === BLOG.NOTION_PROPERTY_NAME.type_menu || post?.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu) && post.status === 'Published')
const menus = [] const menus = []
if (menuPages && menuPages.length > 0) { if (menuPages && menuPages.length > 0) {
menuPages.forEach(e => { menuPages.forEach(e => {
@@ -185,10 +185,10 @@ export function getNavPages({ allPages }) {
const result = allNavPages.map(item => ({ id: item.id, title: item.title || '', category: item.category || null, tags: item.tags || null, summary: item.summary || null, slug: item.slug })) const result = allNavPages.map(item => ({ id: item.id, title: item.title || '', category: item.category || null, tags: item.tags || null, summary: item.summary || null, slug: item.slug }))
const groupedArray = result.reduce((groups, item) => { const groupedArray = result.reduce((groups, item) => {
const categoryName = item.category ? item.category.join('/') : '' // 将category转换为字符串 const categoryName = item?.category ? item?.category : '' // 将category转换为字符串
const lastGroup = groups[groups.length - 1] // 获取最后一个分组 const lastGroup = groups[groups.length - 1] // 获取最后一个分组
if (!lastGroup || lastGroup.category !== categoryName) { // 如果当前元素的category与上一个元素不同则创建新分组 if (!lastGroup || lastGroup?.category !== categoryName) { // 如果当前元素的category与上一个元素不同则创建新分组
groups.push({ category: categoryName, items: [] }) groups.push({ category: categoryName, items: [] })
} }
@@ -287,7 +287,7 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
let postCount = 0 let postCount = 0
// 查找所有的Post和Page // 查找所有的Post和Page
const allPages = collectionData.filter(post => { const allPages = collectionData.filter(post => {
if (post.type === 'Post' && post.status === 'Published') { if (post?.type === 'Post' && post.status === 'Published') {
postCount++ postCount++
} }
return post && post?.slug && return post && post?.slug &&
@@ -306,10 +306,10 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
const categoryOptions = getAllCategories({ allPages, categoryOptions: getCategoryOptions(schema) }) const categoryOptions = getAllCategories({ allPages, categoryOptions: getCategoryOptions(schema) })
const tagOptions = getAllTags({ allPages, tagOptions: getTagOptions(schema) }) const tagOptions = getAllTags({ allPages, tagOptions: getTagOptions(schema) })
// 旧的菜单 // 旧的菜单
const customNav = getCustomNav({ allPages: collectionData.filter(post => post.type === 'Page' && post.status === 'Published') }) const customNav = getCustomNav({ allPages: collectionData.filter(post => post?.type === 'Page' && post.status === 'Published') })
// 新的菜单 // 新的菜单
const customMenu = await getCustomMenu({ collectionData }) const customMenu = await getCustomMenu({ collectionData })
const latestPosts = getLatestPosts({ allPages, from, latestPostCount: 5 }) const latestPosts = getLatestPosts({ allPages, from, latestPostCount: 6 })
const allNavPages = getNavPages({ allPages }) const allNavPages = getNavPages({ allPages })
return { return {

View File

@@ -69,9 +69,10 @@ export default async function getPageProperties(id, block, schema, authToken, ta
}) })
} }
// type\status下拉框 取数组第一个 // type\status\category 是单选下拉框 取数组第一个
properties.type = properties.type?.[0] properties.type = properties.type?.[0] || ''
properties.status = properties.status?.[0] properties.status = properties.status?.[0] || ''
properties.category = properties.category?.[0] || ''
// 映射值用户个性化type和status字段的下拉框选项在此映射回代码的英文标识 // 映射值用户个性化type和status字段的下拉框选项在此映射回代码的英文标识
mapProperties(properties) mapProperties(properties)
@@ -99,7 +100,6 @@ export default async function getPageProperties(id, block, schema, authToken, ta
properties.to = properties.slug ?? null properties.to = properties.slug ?? null
properties.name = properties.title ?? '' properties.name = properties.title ?? ''
} }
properties.password = properties.password ? md5(properties.slug + properties.password) : ''
// 开启伪静态路径 // 开启伪静态路径
if (JSON.parse(BLOG.PSEUDO_STATIC)) { if (JSON.parse(BLOG.PSEUDO_STATIC)) {
@@ -107,6 +107,7 @@ export default async function getPageProperties(id, block, schema, authToken, ta
properties.slug += '.html' properties.slug += '.html'
} }
} }
properties.password = properties.password ? md5(properties.slug + properties.password) : ''
return properties return properties
} }

View File

@@ -6,7 +6,7 @@ import BLOG from '@/blog.config'
* 2. UnPlash 图片可以通过api q=50 控制压缩质量 width=400 控制图片尺寸 * 2. UnPlash 图片可以通过api q=50 控制压缩质量 width=400 控制图片尺寸
* @param {*} image * @param {*} image
*/ */
const compressImage = (image, width = 300) => { const compressImage = (image, width = 400, quality = 50, fmt = 'webp') => {
if (!image) { if (!image) {
return null return null
} }
@@ -20,11 +20,12 @@ const compressImage = (image, width = 300) => {
// 获取URL参数 // 获取URL参数
const params = new URLSearchParams(urlObj.search) const params = new URLSearchParams(urlObj.search)
// 将q参数的值替换 // 将q参数的值替换
params.set('q', '50') params.set('q', quality)
// 尺寸 // 尺寸
params.set('width', width) params.set('width', width)
// 格式 // 格式
params.set('fmt', 'webp') params.set('fmt', fmt)
params.set('fm', fmt)
// 生成新的URL // 生成新的URL
urlObj.search = params.toString() urlObj.search = params.toString()
return urlObj.toString() return urlObj.toString()

View File

@@ -21,9 +21,11 @@ export async function generateSitemapXml({ allPages }) {
changefreq: 'daily' changefreq: 'daily'
}] }]
// 循环页面生成
allPages?.forEach(post => { allPages?.forEach(post => {
const slugWithoutLeadingSlash = post?.slug?.startsWith('/') ? post?.slug?.slice(1) : post.slug
urls.push({ urls.push({
loc: `${BLOG.LINK}/${post.slug}`, loc: `${BLOG.LINK}/${slugWithoutLeadingSlash}`,
lastmod: new Date(post?.publishTime).toISOString().split('T')[0], lastmod: new Date(post?.publishTime).toISOString().split('T')[0],
changefreq: 'daily' changefreq: 'daily'
}) })
@@ -36,6 +38,12 @@ export async function generateSitemapXml({ allPages }) {
console.warn('无法写入文件', error) console.warn('无法写入文件', error)
} }
} }
/**
* 生成站点地图
* @param {*} urls
* @returns
*/
function createSitemapXml(urls) { function createSitemapXml(urls) {
let urlsXml = '' let urlsXml = ''
urls.forEach(u => { urls.forEach(u => {

View File

@@ -98,6 +98,12 @@ module.exports = withBundleAnalyzer({
experimental: { experimental: {
scrollRestoration: true 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: { // 这里的配置既可以服务端获取到,也可以在浏览器端获取到 publicRuntimeConfig: { // 这里的配置既可以服务端获取到,也可以在浏览器端获取到
NODE_ENV_API: process.env.NODE_ENV_API || 'prod', NODE_ENV_API: process.env.NODE_ENV_API || 'prod',
THEMES: themes THEMES: themes

View File

@@ -1,6 +1,6 @@
{ {
"name": "notion-next", "name": "notion-next",
"version": "4.0.0", "version": "4.0.5",
"homepage": "https://github.com/tangly1024/NotionNext.git", "homepage": "https://github.com/tangly1024/NotionNext.git",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
@@ -25,7 +25,7 @@
"@headlessui/react": "^1.7.15", "@headlessui/react": "^1.7.15",
"@next/bundle-analyzer": "^12.1.1", "@next/bundle-analyzer": "^12.1.1",
"@vercel/analytics": "^1.0.0", "@vercel/analytics": "^1.0.0",
"animate.css": "^4.1.1", "algoliasearch": "^4.18.0",
"animejs": "^3.2.1", "animejs": "^3.2.1",
"aos": "^3.0.0-beta.6", "aos": "^3.0.0-beta.6",
"axios": ">=0.21.1", "axios": ">=0.21.1",
@@ -35,7 +35,6 @@
"js-md5": "^0.7.3", "js-md5": "^0.7.3",
"localStorage": "^1.0.4", "localStorage": "^1.0.4",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"mark.js": "^8.11.1",
"memory-cache": "^0.2.0", "memory-cache": "^0.2.0",
"mongodb": "^4.6.0", "mongodb": "^4.6.0",
"next": "13.3.1", "next": "13.3.1",
@@ -69,7 +68,7 @@
"eslint-plugin-react": "^7.23.2", "eslint-plugin-react": "^7.23.2",
"next-sitemap": "^1.6.203", "next-sitemap": "^1.6.203",
"postcss": "^8.4.20", "postcss": "^8.4.20",
"tailwindcss": "^3.2.4", "tailwindcss": "^3.3.2",
"webpack-bundle-analyzer": "^4.5.0" "webpack-bundle-analyzer": "^4.5.0"
}, },
"resolutions": { "resolutions": {

87
pages/[prefix]/[slug].js Normal file
View File

@@ -0,0 +1,87 @@
import BLOG from '@/blog.config'
import { getPostBlocks } from '@/lib/notion'
import { getGlobalData } from '@/lib/notion/getNotionData'
import { idToUuid } from 'notion-utils'
import { getNotion } from '@/lib/notion/getNotion'
import Slug, { getRecommendPost } from '.'
/**
* 根据notion的slug访问页面
* @param {*} props
* @returns
*/
const PrefixSlug = props => {
return <Slug {...props}/>
}
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)
}
}
export default PrefixSlug

View File

@@ -9,6 +9,7 @@ import { getPageTableOfContents } from '@/lib/notion/getPageTableOfContents'
import { getLayoutByTheme } from '@/themes/theme' import { getLayoutByTheme } from '@/themes/theme'
import md5 from 'js-md5' import md5 from 'js-md5'
import { isBrowser } from '@/lib/utils' import { isBrowser } from '@/lib/utils'
import { uploadDataToAlgolia } from '@/lib/algolia'
/** /**
* 根据notion的slug访问页面 * 根据notion的slug访问页面
@@ -89,13 +90,13 @@ export async function getStaticPaths() {
const from = 'slug-paths' const from = 'slug-paths'
const { allPages } = await getGlobalData({ from }) const { allPages } = await getGlobalData({ from })
return { 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 fallback: true
} }
} }
export async function getStaticProps({ params: { slug } }) { export async function getStaticProps({ params: { prefix } }) {
let fullSlug = slug.join('/') let fullSlug = prefix
if (JSON.parse(BLOG.PSEUDO_STATIC)) { if (JSON.parse(BLOG.PSEUDO_STATIC)) {
if (!fullSlug.endsWith('.html')) { if (!fullSlug.endsWith('.html')) {
fullSlug += '.html' fullSlug += '.html'
@@ -110,7 +111,7 @@ export async function getStaticProps({ params: { slug } }) {
// 处理非列表内文章的内信息 // 处理非列表内文章的内信息
if (!props?.post) { if (!props?.post) {
const pageId = slug.slice(-1)[0] const pageId = prefix.slice(-1)[0]
if (pageId.length >= 32) { if (pageId.length >= 32) {
const post = await getNotion(pageId) const post = await getNotion(pageId)
props.post = post props.post = post
@@ -128,6 +129,10 @@ export async function getStaticProps({ params: { slug } }) {
props.post.blockMap = await getPostBlocks(props.post.id, from) props.post.blockMap = await getPostBlocks(props.post.id, from)
} }
if (BLOG.ALGOLIA_APP_ID && BLOG.ALGOLIA_APP_KEY) {
uploadDataToAlgolia(props?.post)
}
// 推荐关联文章处理 // 推荐关联文章处理
const allPosts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published') const allPosts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published')
if (allPosts && allPosts.length > 0) { if (allPosts && allPosts.length > 0) {
@@ -155,7 +160,7 @@ export async function getStaticProps({ params: { slug } }) {
* @param {*} count * @param {*} count
* @returns * @returns
*/ */
function getRecommendPost(post, allPosts, count = 6) { export function getRecommendPost(post, allPosts, count = 6) {
let recommendPosts = [] let recommendPosts = []
const postIds = [] const postIds = []
const currentTags = post?.tags || [] const currentTags = post?.tags || []

View File

@@ -1,6 +1,6 @@
import { useEffect } from 'react' import { useEffect } from 'react'
// import 'animate.css' import '@/styles/animate.css' // @see https://animate.style/
import '@/styles/globals.css' import '@/styles/globals.css'
import '@/styles/nprogress.css' import '@/styles/nprogress.css'
import '@/styles/utility-patterns.css' import '@/styles/utility-patterns.css'
@@ -28,9 +28,9 @@ const MyApp = ({ Component, pageProps }) => {
return ( return (
<GlobalContextProvider> <GlobalContextProvider>
<Component {...pageProps}/>
<ExternalPlugins {...pageProps} />
<ExternalScript /> <ExternalScript />
<Component {...pageProps} />
<ExternalPlugins {...pageProps} />
</GlobalContextProvider> </GlobalContextProvider>
) )
} }

View File

@@ -13,11 +13,24 @@ class MyDocument extends Document {
return ( return (
<Html lang={BLOG.LANG}> <Html lang={BLOG.LANG}>
<Head> <Head>
<link rel='icon' href='/favicon.ico' /> <link rel='icon' href= {`${BLOG.BLOG_FAVICON}`} />
<CommonScript /> <CommonScript />
{/* 预加载字体 */}
{BLOG.FONT_AWESOME && <>
<link rel='preload' href={BLOG.FONT_AWESOME} as="style" crossOrigin="anonymous" />
<link rel="stylesheet" href={BLOG.FONT_AWESOME} crossOrigin="anonymous" referrerPolicy="no-referrer" />
</>}
{BLOG.FONT_URL?.map((fontUrl, index) => {
if (fontUrl.endsWith('.css')) {
return <link key={index} rel="stylesheet" href={fontUrl} />
} else {
return <link key={index} rel="preload" href={fontUrl} as="font" type="font/woff2" />
}
})}
</Head> </Head>
<body className={`${BLOG.FONT_STYLE} font-light bg-day dark:bg-night`}> <body className={`${BLOG.FONT_STYLE} font-light scroll-smooth`}>
<Main /> <Main />
<NextScript /> <NextScript />
</body> </body>

View File

@@ -6,6 +6,8 @@ import { generateRobotsTxt } from '@/lib/robots.txt'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { getLayoutByTheme } from '@/themes/theme' import { getLayoutByTheme } from '@/themes/theme'
import { generateAlgoliaSearch } from '@/lib/algolia'
/** /**
* 首页布局 * 首页布局
* @param {*} props * @param {*} props
@@ -61,6 +63,11 @@ export async function getStaticProps() {
generateRss(props?.latestPosts || []) generateRss(props?.latestPosts || [])
} }
// 生成全文索引 - 仅在 yarn build 时执行 && process.env.npm_lifecycle_event === 'build'
if (BLOG.ALGOLIA_APP_ID && JSON.parse(BLOG.ALGOLIA_RECREATE_DATA)) {
generateAlgoliaSearch({ allPages: props.allPages })
}
delete props.allPages delete props.allPages
return { return {

View File

@@ -117,20 +117,11 @@ async function filterByMemCache(allPosts, keyword) {
for (const post of allPosts) { for (const post of allPosts) {
const cacheKey = 'page_block_' + post.id const cacheKey = 'page_block_' + post.id
const page = await getDataFromCache(cacheKey, true) const page = await getDataFromCache(cacheKey, true)
const tagContent = post.tags && Array.isArray(post.tags) ? post.tags.join(' ') : '' const tagContent = post?.tags && Array.isArray(post?.tags) ? post?.tags.join(' ') : ''
const categoryContent = post.category && Array.isArray(post.category) ? post.category.join(' ') : '' const categoryContent = post.category && Array.isArray(post.category) ? post.category.join(' ') : ''
const articleInfo = post.title + post.summary + tagContent + categoryContent const articleInfo = post.title + post.summary + tagContent + categoryContent
let hit = articleInfo.toLowerCase().indexOf(keyword) > -1 let hit = articleInfo.toLowerCase().indexOf(keyword) > -1
let indexContent = [post.summary] const indexContent = getPageContentText(post, page)
// 防止搜到加密文章的内容
if (page && page.block && !post.password) {
const contentIds = Object.keys(page.block)
contentIds.forEach(id => {
const properties = page?.block[id]?.value?.properties
indexContent = appendText(indexContent, properties, 'title')
indexContent = appendText(indexContent, properties, 'caption')
})
}
// console.log('全文搜索缓存', cacheKey, page != null) // console.log('全文搜索缓存', cacheKey, page != null)
post.results = [] post.results = []
let hitCount = 0 let hitCount = 0
@@ -157,4 +148,18 @@ async function filterByMemCache(allPosts, keyword) {
return filterPosts return filterPosts
} }
export function getPageContentText(post, pageBlockMap) {
let indexContent = []
// 防止搜到加密文章的内容
if (pageBlockMap && pageBlockMap.block && !post.password) {
const contentIds = Object.keys(pageBlockMap.block)
contentIds.forEach(id => {
const properties = pageBlockMap?.block[id]?.value?.properties
indexContent = appendText(indexContent, properties, 'title')
indexContent = appendText(indexContent, properties, 'caption')
})
}
return indexContent.join('')
}
export default Index export default Index

View File

@@ -115,7 +115,7 @@ async function filterByMemCache(allPosts, keyword) {
for (const post of allPosts) { for (const post of allPosts) {
const cacheKey = 'page_block_' + post.id const cacheKey = 'page_block_' + post.id
const page = await getDataFromCache(cacheKey, true) const page = await getDataFromCache(cacheKey, true)
const tagContent = post.tags && Array.isArray(post.tags) ? post.tags.join(' ') : '' const tagContent = post?.tags && Array.isArray(post?.tags) ? post?.tags.join(' ') : ''
const categoryContent = post.category && Array.isArray(post.category) ? post.category.join(' ') : '' const categoryContent = post.category && Array.isArray(post.category) ? post.category.join(' ') : ''
const articleInfo = post.title + post.summary + tagContent + categoryContent const articleInfo = post.title + post.summary + tagContent + categoryContent
let hit = articleInfo.indexOf(keyword) > -1 let hit = articleInfo.indexOf(keyword) > -1

View File

@@ -23,7 +23,7 @@ const Search = props => {
// 静态过滤 // 静态过滤
if (keyword) { if (keyword) {
filteredPosts = posts.filter(post => { filteredPosts = posts.filter(post => {
const tagContent = post.tags ? post.tags.join(' ') : '' const tagContent = post?.tags ? post?.tags.join(' ') : ''
const categoryContent = post.category ? post.category.join(' ') : '' const categoryContent = post.category ? post.category.join(' ') : ''
const searchContent = const searchContent =
post.title + post.summary + tagContent + categoryContent post.title + post.summary + tagContent + categoryContent

View File

@@ -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 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 { return {
loc: `${BLOG.LINK}/${post.slug}`, loc: `${BLOG.LINK}/${slugWithoutLeadingSlash}`,
lastmod: new Date(post?.publishTime).toISOString().split('T')[0], lastmod: new Date(post?.publishTime).toISOString().split('T')[0],
changefreq: 'daily', changefreq: 'daily',
priority: '0.7' priority: '0.7'

View File

@@ -33,7 +33,7 @@ export async function getStaticProps({ params: { tag } }) {
const props = await getGlobalData({ from }) const props = await getGlobalData({ from })
// 过滤状态 // 过滤状态
props.posts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post.tags && post.tags.includes(tag)) props.posts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post?.tags && post?.tags.includes(tag))
// 处理文章页数 // 处理文章页数
props.postCount = props.posts.length props.postCount = props.posts.length

View File

@@ -27,7 +27,7 @@ export async function getStaticProps({ params: { tag, page } }) {
const from = 'tag-page-props' const from = 'tag-page-props'
const props = await getGlobalData({ from }) const props = await getGlobalData({ from })
// 过滤状态、标签 // 过滤状态、标签
props.posts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post.tags && post.tags.includes(tag)) props.posts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post?.tags && post?.tags.includes(tag))
// 处理文章数 // 处理文章数
props.postCount = props.posts.length props.postCount = props.posts.length
// 处理分页 // 处理分页
@@ -48,7 +48,7 @@ export async function getStaticPaths() {
const paths = [] const paths = []
tagOptions?.forEach(tag => { tagOptions?.forEach(tag => {
// 过滤状态类型 // 过滤状态类型
const tagPosts = allPages.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post.tags && post.tags.includes(tag.name)) const tagPosts = allPages.filter(page => page.type === 'Post' && page.status === 'Published').filter(post => post && post?.tags && post?.tags.includes(tag.name))
// 处理文章页数 // 处理文章页数
const postCount = tagPosts.length const postCount = tagPosts.length
const totalPages = Math.ceil(postCount / BLOG.POSTS_PER_PAGE) const totalPages = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)

View File

@@ -2,4 +2,7 @@
#theme-fukasawa .sideLeft hr{ #theme-fukasawa .sideLeft hr{
opacity: .04; opacity: .04;
} }
.fa-info:before {
content: "\f05a";
}

View File

@@ -11,6 +11,10 @@
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.collapse-wrapper .code-toolbar {
margin-bottom: 0;
}
.toolbar-item{ .toolbar-item{
white-space: nowrap; white-space: nowrap;
} }
@@ -21,7 +25,7 @@
pre[class*='language-'] { pre[class*='language-'] {
margin-top: 0rem !important; margin-top: 0rem !important;
margin-bottom: 0rem !important; // margin-bottom: 0rem !important;
padding-top: 1.5rem !important; padding-top: 1.5rem !important;
} }

View File

@@ -1 +0,0 @@
/* fukasawa的主题相关 */

View File

@@ -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;
}

View File

@@ -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%);
}

View File

@@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

503
styles/animate.css vendored Normal file
View File

@@ -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;
}

View File

@@ -2,28 +2,6 @@
@tailwind components; @tailwind components;
@tailwind utilities; @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 { .wrapper {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
@@ -285,57 +263,3 @@ a.avatar-wrapper {
.reply-author-name { .reply-author-name {
font-weight: 500; 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;
}
}

View File

@@ -179,9 +179,6 @@
color: var(--select-color-2) !important; color: var(--select-color-2) !important;
} }
.notion-simple-table {
@apply whitespace-nowrap overflow-x-auto block
}
.notion-app { .notion-app {
position: relative; position: relative;
@@ -446,6 +443,7 @@ summary > .notion-h {
.notion-h:hover .notion-hash-link { .notion-h:hover .notion-hash-link {
opacity: 1; opacity: 1;
@apply dark:fill-gray-200
} }
.notion-hash-link { .notion-hash-link {
@@ -748,6 +746,8 @@ svg.notion-page-icon {
.notion .notion-code { .notion .notion-code {
font-size: 85%; font-size: 85%;
margin-top: 0;
margin-bottom: 0.5em;
} }
pre[class*='language-'] { pre[class*='language-'] {
@@ -1395,6 +1395,10 @@ code[class*='language-'] {
white-space: normal; white-space: normal;
} }
.katex-display>.katex>.katex-html>.tag {
position: inherit !important;
}
.notion-page-title { .notion-page-title {
display: inline-flex; display: inline-flex;
max-width: 100%; max-width: 100%;
@@ -1939,25 +1943,13 @@ svg + .notion-page-title-text {
} }
.notion-simple-table { .notion-simple-table {
width: 100% !important; @apply whitespace-nowrap overflow-x-auto block w-full border-0 !important;
} }
.notion-asset-wrapper-pdf > div { .notion-asset-wrapper-pdf > div {
display: block !important; display: block !important;
} }
::selection {
@apply bg-blue-500 text-gray-50 !important;
}
.dark img{
@apply opacity-80
}
.dark #live2d {
@apply opacity-80
}
/* https://github.com/kchen0x */ /* https://github.com/kchen0x */
.notion-quote { .notion-quote {
display: block; display: block;
@@ -2053,11 +2045,16 @@ code.language-mermaid {
/* 表格头 */ /* 表格头 */
.notion-simple-table tr:first-child td{ .notion-simple-table tr:first-child td{
background-color: #f5f6f8 !important; background-color: #f5f6f8;
@apply text-center font-bold !important; @apply text-center font-bold dark:bg-gray-800 !important;
} }
.notion-simple-table td{ .notion-simple-table td{
border: 1px solid var(#eee) !important border: 1px solid var(#eee) !important
} }
/* 竖屏视频高度bug */
figure.notion-asset-wrapper.notion-asset-wrapper-video>div {
height: 100% !important;
}

View File

@@ -2,6 +2,7 @@ import BLOG from '@/blog.config'
import CONFIG from '../config' import CONFIG from '../config'
import Link from 'next/link' import Link from 'next/link'
import TwikooCommentCount from '@/components/TwikooCommentCount' import TwikooCommentCount from '@/components/TwikooCommentCount'
import LazyImage from '@/components/LazyImage'
const BlogPostCard = ({ post }) => { const BlogPostCard = ({ post }) => {
const showPageCover = CONFIG.POST_LIST_COVER && post?.pageCoverThumbnail const showPageCover = CONFIG.POST_LIST_COVER && post?.pageCoverThumbnail
@@ -41,7 +42,7 @@ const BlogPostCard = ({ post }) => {
{showPageCover && ( {showPageCover && (
<div className="md:w-5/12 w-full overflow-hidden p-1"> <div className="md:w-5/12 w-full overflow-hidden p-1">
<Link href={`${BLOG.SUB_PATH}/${post.slug}`} passHref legacyBehavior> <Link href={`${BLOG.SUB_PATH}/${post.slug}`} passHref legacyBehavior>
<div className='h-44 bg-center bg-cover hover:scale-110 duration-200' style={{ backgroundImage: `url('${post?.pageCoverThumbnail}')` }} /> <LazyImage src={post?.pageCoverThumbnail} className='h-44 bg-center bg-cover hover:scale-110 duration-200' />
</Link> </Link>
</div> </div>
)} )}

View File

@@ -19,13 +19,14 @@ import NotionPage from '@/components/NotionPage'
import Comment from '@/components/Comment' import Comment from '@/components/Comment'
import ShareBar from '@/components/ShareBar' import ShareBar from '@/components/ShareBar'
import SearchInput from './components/SearchInput' import SearchInput from './components/SearchInput'
import Mark from 'mark.js' import replaceSearchResult from '@/components/Mark'
import { isBrowser } from '@/lib/utils' import { isBrowser } from '@/lib/utils'
import BlogListGroupByDate from './components/BlogListGroupByDate' import BlogListGroupByDate from './components/BlogListGroupByDate'
import CategoryItem from './components/CategoryItem' import CategoryItem from './components/CategoryItem'
import TagItem from './components/TagItem' import TagItem from './components/TagItem'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { Transition } from '@headlessui/react' import { Transition } from '@headlessui/react'
import { Style } from './style'
/** /**
* 基础布局框架 * 基础布局框架
@@ -50,6 +51,7 @@ const LayoutBase = props => {
<div id='theme-example' className='dark:text-gray-300 bg-white dark:bg-black'> <div id='theme-example' className='dark:text-gray-300 bg-white dark:bg-black'>
{/* 网页SEO信息 */} {/* 网页SEO信息 */}
<CommonHead meta={meta} /> <CommonHead meta={meta} />
<Style/>
{/* 页头 */} {/* 页头 */}
<Header {...props} /> <Header {...props} />
@@ -173,21 +175,20 @@ const LayoutSearch = props => {
const slotTop = <div className='pb-12'><SearchInput {...props} /></div> const slotTop = <div className='pb-12'><SearchInput {...props} /></div>
const router = useRouter() const router = useRouter()
useEffect(() => { useEffect(() => {
setTimeout(() => { if (isBrowser()) {
if (isBrowser()) { // 高亮搜索到的结果
// 高亮搜索到的结果 const container = document.getElementById('posts-wrapper')
const container = document.getElementById('posts-wrapper') if (keyword && container) {
console.log('container', container, keyword) replaceSearchResult({
if (keyword && container) { doms: container,
const re = new RegExp(keyword, 'gim') search: keyword,
const instance = new Mark(container) target: {
instance.markRegExp(re, {
element: 'span', element: 'span',
className: 'text-red-500 border-b border-dashed' className: 'text-red-500 border-b border-dashed'
}) }
} })
} }
}, 500) }
}, [router]) }, [router])
return <LayoutPostList slotTop={slotTop} {...props} /> return <LayoutPostList slotTop={slotTop} {...props} />

17
themes/example/style.js Normal file
View File

@@ -0,0 +1,17 @@
/* eslint-disable react/no-unknown-property */
/**
* 此处样式只对当前主题生效
* 此处不支持tailwindCSS的 @apply 语法
* @returns
*/
const Style = () => {
return <style jsx global>{`
// 底色
.dark body{
background-color: black;
}
`}</style>
}
export { Style }

View File

@@ -6,6 +6,7 @@ import { useGlobal } from '@/lib/global'
import Link from 'next/link' import Link from 'next/link'
import ArticleAround from './ArticleAround' import ArticleAround from './ArticleAround'
import { AdSlot } from '@/components/GoogleAdsense' import { AdSlot } from '@/components/GoogleAdsense'
import LazyImage from '@/components/LazyImage'
/** /**
* *
@@ -23,8 +24,7 @@ export default function ArticleDetail(props) {
<div id="container" className="max-w-5xl overflow-x-auto flex-grow mx-auto w-screen md:w-full "> <div id="container" className="max-w-5xl overflow-x-auto flex-grow mx-auto w-screen md:w-full ">
{post?.type && !post?.type !== 'Page' && post?.pageCover && ( {post?.type && !post?.type !== 'Page' && post?.pageCover && (
<div className="w-full relative md:flex-shrink-0 overflow-hidden"> <div className="w-full relative md:flex-shrink-0 overflow-hidden">
{/* eslint-disable-next-line @next/next/no-img-element */} <LazyImage alt={post.title} src={post?.pageCover} className='object-center w-full' />
<img alt={post.title} src={post?.pageCover} className='object-center w-full' />
</div> </div>
)} )}
@@ -96,7 +96,7 @@ export default function ArticleDetail(props) {
</article> </article>
{post.type === 'Post' && <ArticleAround prev={prev} next={next} /> } {post?.type === 'Post' && <ArticleAround prev={prev} next={next} /> }
{/* 评论互动 */} {/* 评论互动 */}
<div className="duration-200 shadow py-6 px-12 w-screen md:w-full overflow-x-auto dark:border-gray-700 bg-white dark:bg-hexo-black-gray"> <div className="duration-200 shadow py-6 px-12 w-screen md:w-full overflow-x-auto dark:border-gray-700 bg-white dark:bg-hexo-black-gray">

View File

@@ -3,6 +3,7 @@ import Link from 'next/link'
import TagItemMini from './TagItemMini' import TagItemMini from './TagItemMini'
import React from 'react' import React from 'react'
import CONFIG_FUKA from '../config' import CONFIG_FUKA from '../config'
import LazyImage from '@/components/LazyImage'
const BlogCard = ({ index, post, showSummary, siteInfo }) => { const BlogCard = ({ index, post, showSummary, siteInfo }) => {
const showPreview = CONFIG_FUKA.POST_LIST_PREVIEW && post.blockMap const showPreview = CONFIG_FUKA.POST_LIST_PREVIEW && post.blockMap
@@ -15,8 +16,9 @@ const BlogCard = ({ index, post, showSummary, siteInfo }) => {
return ( return (
<div <div
data-aos="fade-up" data-aos="fade-up"
data-aos-duration="500" data-aos-duration="600"
data-aos-once="true" data-aos-once="true"
data-aos-anchor-placement="top-bottom"
style={{ maxHeight: '60rem' }} style={{ maxHeight: '60rem' }}
className="w-full lg:max-w-sm p-3 shadow mb-4 mx-2 bg-white dark:bg-hexo-black-gray hover:shadow-lg duration-200" className="w-full lg:max-w-sm p-3 shadow mb-4 mx-2 bg-white dark:bg-hexo-black-gray hover:shadow-lg duration-200"
> >
@@ -25,12 +27,11 @@ const BlogCard = ({ index, post, showSummary, siteInfo }) => {
{showPageCover && ( {showPageCover && (
<div className="flex-grow mb-3 w-full duration-200 cursor-pointer transform overflow-hidden"> <div className="flex-grow mb-3 w-full duration-200 cursor-pointer transform overflow-hidden">
<Link href={`${BLOG.SUB_PATH}/${post.slug}`} passHref legacyBehavior> <Link href={`${BLOG.SUB_PATH}/${post.slug}`} passHref legacyBehavior>
{/* eslint-disable-next-line @next/next/no-img-element */} <LazyImage
<img
src={post?.pageCoverThumbnail} src={post?.pageCoverThumbnail}
alt={post?.title || BLOG.TITLE} alt={post?.title || BLOG.TITLE}
className="object-cover w-full h-full hover:scale-125 transform duration-500" className="object-cover w-full h-full hover:scale-125 transform duration-500"
></img> />
</Link> </Link>
</div> </div>
)} )}
@@ -54,7 +55,7 @@ const BlogCard = ({ index, post, showSummary, siteInfo }) => {
{post.category && <Link {post.category && <Link
href={`/category/${post.category}`} href={`/category/${post.category}`}
passHref passHref
className="cursor-pointer font-light text-sm hover:underline hover:text-indigo-700 dark:hover:text-indigo-400 transform" className="cursor-pointer dark:text-gray-300 font-light text-sm hover:underline hover:text-indigo-700 dark:hover:text-indigo-400 transform"
> >
<i className="mr-1 far fa-folder" /> <i className="mr-1 far fa-folder" />
{post.category} {post.category}

View File

@@ -30,7 +30,7 @@ const SocialButton = () => {
{BLOG.CONTACT_EMAIL && <a target='_blank' rel='noreferrer' title={'email'} href={`mailto:${BLOG.CONTACT_EMAIL}`} > {BLOG.CONTACT_EMAIL && <a target='_blank' rel='noreferrer' title={'email'} href={`mailto:${BLOG.CONTACT_EMAIL}`} >
<i className='fas fa-envelope transform hover:scale-125 duration-150'/> <i className='fas fa-envelope transform hover:scale-125 duration-150'/>
</a>} </a>}
{BLOG.ENABLE_RSS && <a target='_blank' rel='noreferrer' title={'RSS'} href={'/feed'} > {JSON.parse(BLOG.ENABLE_RSS) && <a target='_blank' rel='noreferrer' title={'RSS'} href={'/feed'} >
<i className='fas fa-rss transform hover:scale-125 duration-150'/> <i className='fas fa-rss transform hover:scale-125 duration-150'/>
</a>} </a>}
{BLOG.CONTACT_BILIBILI && <a target='_blank' rel='noreferrer' title={'bilibili'} href={BLOG.CONTACT_BILIBILI} > {BLOG.CONTACT_BILIBILI && <a target='_blank' rel='noreferrer' title={'bilibili'} href={BLOG.CONTACT_BILIBILI} >

View File

@@ -5,7 +5,7 @@ import CommonHead from '@/components/CommonHead'
import TopNav from './components/TopNav' import TopNav from './components/TopNav'
import AsideLeft from './components/AsideLeft' import AsideLeft from './components/AsideLeft'
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
import { isBrowser, loadExternalResource } from '@/lib/utils' import { isBrowser } from '@/lib/utils'
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import BlogListPage from './components/BlogListPage' import BlogListPage from './components/BlogListPage'
import BlogListScroll from './components/BlogListScroll' import BlogListScroll from './components/BlogListScroll'
@@ -19,9 +19,10 @@ import Link from 'next/link'
import { Transition } from '@headlessui/react' import { Transition } from '@headlessui/react'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import { AdSlot } from '@/components/GoogleAdsense' import { AdSlot } from '@/components/GoogleAdsense'
import { Style } from './style'
import replaceSearchResult from '@/components/Mark'
const Live2D = dynamic(() => import('@/components/Live2D')) const Live2D = dynamic(() => import('@/components/Live2D'))
const Mark = dynamic(() => import('mark.js'))
// 主题全局状态 // 主题全局状态
const ThemeGlobalFukasawa = createContext() const ThemeGlobalFukasawa = createContext()
@@ -61,15 +62,12 @@ const LayoutBase = (props) => {
} }
}, [isCollapsed]) }, [isCollapsed])
if (isBrowser()) {
loadExternalResource('/css/theme-fukasawa.css', 'css')
}
return ( return (
<ThemeGlobalFukasawa.Provider value={{ isCollapsed, setIsCollapse }}> <ThemeGlobalFukasawa.Provider value={{ isCollapsed, setIsCollapse }}>
<div id='theme-fukasawa'> <div id='theme-fukasawa'>
<CommonHead meta={meta} /> <CommonHead meta={meta} />
<Style/>
<TopNav {...props} /> <TopNav {...props} />
@@ -150,17 +148,16 @@ const LayoutSearch = props => {
const { keyword } = props const { keyword } = props
const router = useRouter() const router = useRouter()
useEffect(() => { useEffect(() => {
setTimeout(() => { if (isBrowser()) {
const container = isBrowser() && document.getElementById('posts-wrapper') replaceSearchResult({
if (container && container.innerHTML) { doms: document.getElementById('posts-wrapper'),
const re = new RegExp(keyword, 'gim') search: keyword,
const instance = new Mark(container) target: {
instance.markRegExp(re, {
element: 'span', element: 'span',
className: 'text-red-500 border-b border-dashed' className: 'text-red-500 border-b border-dashed'
}) }
} })
}, 300) }
}, [router]) }, [router])
return <LayoutPostList {...props} /> return <LayoutPostList {...props} />
} }

50
themes/fukasawa/style.js Normal file
View File

@@ -0,0 +1,50 @@
/* eslint-disable react/no-unknown-property */
/**
* 此处样式只对当前主题生效
* 此处不支持tailwindCSS的 @apply 语法
* @returns
*/
const Style = () => {
return <style jsx global>{`
// 底色
body{
background-color: #eeedee;
}
.dark body{
background-color: black;
}
/* 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;
}
}
`}</style>
}
export { Style }

View File

@@ -8,7 +8,7 @@ const BlogPostCard = ({ post, className }) => {
const currentSelected = router.asPath.split('?')[0] === '/' + post.slug const currentSelected = router.asPath.split('?')[0] === '/' + post.slug
return ( return (
<div key={post.id} className={`${className} py-1 cursor-pointer px-2 hover:bg-gray-50 rounded-md dark:hover:bg-gray-600 ${currentSelected ? 'bg-green-50 text-green-500' : ''}`}> <div key={post.id} className={`${className} py-1 cursor-pointer px-2 hover:bg-gray-50 rounded-md dark:hover:bg-gray-600 ${currentSelected ? 'bg-green-50 text-green-500' : ''}`}>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full select-none">
<Link href={`${BLOG.SUB_PATH}/${post.slug}`} passHref> <Link href={`${BLOG.SUB_PATH}/${post.slug}`} passHref>
{post.title} {post.title}
</Link> </Link>

View File

@@ -1,5 +1,8 @@
import { useGitBookGlobal } from '@/themes/gitbook' import { useGitBookGlobal } from '@/themes/gitbook'
/**
* 移动端悬浮目录按钮
*/
export default function FloatTocButton () { export default function FloatTocButton () {
const { tocVisible, changeTocVisible } = useGitBookGlobal() const { tocVisible, changeTocVisible } = useGitBookGlobal()
@@ -14,7 +17,7 @@ export default function FloatTocButton () {
} }
> >
<a <a
id="darkModeButton" id="toc-button"
className={'fa-list-ol cursor-pointer fas hover:scale-150 transform duration-200'} className={'fa-list-ol cursor-pointer fas hover:scale-150 transform duration-200'}
/> />
</div> </div>

View File

@@ -1,4 +1,5 @@
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
import LazyImage from '@/components/LazyImage'
import Router from 'next/router' import Router from 'next/router'
import React from 'react' import React from 'react'
import SocialButton from './SocialButton' import SocialButton from './SocialButton'
@@ -8,8 +9,7 @@ const InfoCard = (props) => {
return <div id='info-card' className='py-4'> return <div id='info-card' className='py-4'>
<div className='items-center justify-center'> <div className='items-center justify-center'>
<div className='hover:scale-105 transform duration-200 cursor-pointer flex justify-center' onClick={ () => { Router.push('/about') }}> <div className='hover:scale-105 transform duration-200 cursor-pointer flex justify-center' onClick={ () => { Router.push('/about') }}>
{/* eslint-disable-next-line @next/next/no-img-element */} <LazyImage src={siteInfo?.icon} className='rounded-full' width={120} alt={BLOG.AUTHOR}/>
<img src={siteInfo?.icon} className='rounded-full' width={120} alt={BLOG.AUTHOR}/>
</div> </div>
<div className='text-xl py-2 hover:scale-105 transform duration-200 flex justify-center dark:text-gray-300'>{BLOG.AUTHOR}</div> <div className='text-xl py-2 hover:scale-105 transform duration-200 flex justify-center dark:text-gray-300'>{BLOG.AUTHOR}</div>
<div className='font-light text-gray-600 mb-2 hover:scale-105 transform duration-200 flex justify-center dark:text-gray-400'>{BLOG.BIO}</div> <div className='font-light text-gray-600 mb-2 hover:scale-105 transform duration-200 flex justify-center dark:text-gray-400'>{BLOG.BIO}</div>

View File

@@ -1,4 +1,5 @@
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
import LazyImage from '@/components/LazyImage'
import { useGitBookGlobal } from '@/themes/gitbook' import { useGitBookGlobal } from '@/themes/gitbook'
import Link from 'next/link' import Link from 'next/link'
@@ -20,8 +21,7 @@ export default function LogoBar(props) {
<i className={`fa-solid ${pageNavVisible ? 'fa-align-justify' : 'fa-indent'}`}></i> <i className={`fa-solid ${pageNavVisible ? 'fa-align-justify' : 'fa-indent'}`}></i>
</div> </div>
<Link href='/' className='flex text-md md:text-xl dark:text-gray-200'> <Link href='/' className='flex text-md md:text-xl dark:text-gray-200'>
{/* eslint-disable-next-line @next/next/no-img-element */} <LazyImage src={siteInfo?.icon} width={24} height={24} alt={BLOG.AUTHOR} className='mr-2 hidden md:block' />
<img src={siteInfo?.icon} width={24} height={24} alt={BLOG.AUTHOR} className='mr-2 hidden md:block' />
{siteInfo?.title} {siteInfo?.title}
</Link> </Link>
</div> </div>

View File

@@ -21,7 +21,7 @@ const NavPostItem = (props) => {
return <> return <>
<div <div
onClick={toggleOpenSubMenu} onClick={toggleOpenSubMenu}
className='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}> 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}>
<span>{group?.category}</span> <span>{group?.category}</span>
<div className='inline-flex items-center select-none pointer-events-none '><i className={`px-2 fas fa-chevron-left transition-all duration-200 ${isOpen ? '-rotate-90' : ''}`}></i></div> <div className='inline-flex items-center select-none pointer-events-none '><i className={`px-2 fas fa-chevron-left transition-all duration-200 ${isOpen ? '-rotate-90' : ''}`}></i></div>
</div> </div>

View File

@@ -29,7 +29,7 @@ const SocialButton = () => {
{BLOG.CONTACT_EMAIL && <a target='_blank' rel='noreferrer' title={'email'} href={`mailto:${BLOG.CONTACT_EMAIL}`} > {BLOG.CONTACT_EMAIL && <a target='_blank' rel='noreferrer' title={'email'} href={`mailto:${BLOG.CONTACT_EMAIL}`} >
<i className='fas fa-envelope transform hover:scale-125 duration-150 hover:text-green-600'/> <i className='fas fa-envelope transform hover:scale-125 duration-150 hover:text-green-600'/>
</a>} </a>}
{BLOG.ENABLE_RSS && <a target='_blank' rel='noreferrer' title={'RSS'} href={'/feed'} > {JSON.parse(BLOG.ENABLE_RSS) && <a target='_blank' rel='noreferrer' title={'RSS'} href={'/feed'} >
<i className='fas fa-rss transform hover:scale-125 duration-150 hover:text-green-600'/> <i className='fas fa-rss transform hover:scale-125 duration-150 hover:text-green-600'/>
</a>} </a>}
</div> </div>

View File

@@ -30,6 +30,7 @@ import TocDrawer from './components/TocDrawer'
import NotionPage from '@/components/NotionPage' import NotionPage from '@/components/NotionPage'
import { ArticleLock } from './components/ArticleLock' import { ArticleLock } from './components/ArticleLock'
import { Transition } from '@headlessui/react' import { Transition } from '@headlessui/react'
import { Style } from './style'
// 主题全局变量 // 主题全局变量
const ThemeGlobalGitbook = createContext() const ThemeGlobalGitbook = createContext()
@@ -58,6 +59,7 @@ const LayoutBase = (props) => {
return ( return (
<ThemeGlobalGitbook.Provider value={{ tocVisible, changeTocVisible, filteredPostGroups, setFilteredPostGroups, allNavPages, pageNavVisible, changePageNavVisible }}> <ThemeGlobalGitbook.Provider value={{ tocVisible, changeTocVisible, filteredPostGroups, setFilteredPostGroups, allNavPages, pageNavVisible, changePageNavVisible }}>
<CommonHead meta={meta} /> <CommonHead meta={meta} />
<Style/>
<div id='theme-gitbook' className='bg-white dark:bg-hexo-black-gray w-full h-full min-h-screen justify-center dark:text-gray-300'> <div id='theme-gitbook' className='bg-white dark:bg-hexo-black-gray w-full h-full min-h-screen justify-center dark:text-gray-300'>
{/* 顶部导航栏 */} {/* 顶部导航栏 */}
@@ -76,7 +78,7 @@ const LayoutBase = (props) => {
</div> </div>
<div className='w-72 fixed left-0 bottom-0 z-20 bg-white'> <div className='w-72 fixed left-0 bottom-0 z-20 bg-white'>
<Footer {...props}/> <Footer {...props} />
</div> </div>
</div> </div>
@@ -170,7 +172,7 @@ const LayoutIndex = (props) => {
const article = document.getElementById('notion-article') const article = document.getElementById('notion-article')
if (!article) { if (!article) {
console.log('请检查您的Notion数据库中是否包含此slug页面 ', CONFIG.INDEX_PAGE) console.log('请检查您的Notion数据库中是否包含此slug页面 ', CONFIG.INDEX_PAGE)
const containerInner = document.getElementById('container-inner') const containerInner = document.querySelector('#theme-gitbook #container-inner')
const newHTML = `<h1 class="text-3xl pt-12 dark:text-gray-300">配置有误</h1><blockquote class="notion-quote notion-block-ce76391f3f2842d386468ff1eb705b92"><div>请在您的notion中添加一个slug为${CONFIG.INDEX_PAGE}的文章</div></blockquote>` const newHTML = `<h1 class="text-3xl pt-12 dark:text-gray-300">配置有误</h1><blockquote class="notion-quote notion-block-ce76391f3f2842d386468ff1eb705b92"><div>请在您的notion中添加一个slug为${CONFIG.INDEX_PAGE}的文章</div></blockquote>`
containerInner?.insertAdjacentHTML('afterbegin', newHTML) containerInner?.insertAdjacentHTML('afterbegin', newHTML)
} }
@@ -179,7 +181,7 @@ const LayoutIndex = (props) => {
}) })
}, []) }, [])
return <LayoutBase {...props}/> return <LayoutBase {...props} />
} }
/** /**
@@ -189,7 +191,7 @@ const LayoutIndex = (props) => {
* @returns * @returns
*/ */
const LayoutPostList = (props) => { const LayoutPostList = (props) => {
return <LayoutBase {...props}/> return <LayoutBase {...props} />
} }
/** /**
@@ -213,9 +215,6 @@ const LayoutSlug = (props) => {
{/* Notion文章主体 */} {/* Notion文章主体 */}
{post && (<section id="article-wrapper" className="px-1"> {post && (<section id="article-wrapper" className="px-1">
<NotionPage post={post} /> <NotionPage post={post} />
</section>)}
<section>
{/* 分享 */} {/* 分享 */}
<ShareBar post={post} /> <ShareBar post={post} />
@@ -232,7 +231,7 @@ const LayoutSlug = (props) => {
<AdSlot /> <AdSlot />
<Comment frontMatter={post} /> <Comment frontMatter={post} />
</section> </section>)}
<TocDrawer {...props} /> <TocDrawer {...props} />
</div>} </div>}

18
themes/gitbook/style.js Normal file
View File

@@ -0,0 +1,18 @@
/* eslint-disable react/no-unknown-property */
/**
* 此处样式只对当前主题生效
* 此处不支持tailwindCSS的 @apply 语法
* @returns
*/
const Style = () => {
return <style jsx global>{`
// 底色
.dark body{
background-color: black;
}
`}</style>
}
export { Style }

View File

@@ -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 <>
<div className='text-md flex flex-col space-y-1 justify-center px-3'>
<div className='inline'>
<div className='flex justify-between'>
<div>文章数:</div>
<div>{postCount}</div>
</div>
</div>
<div className='inline'>
<div className='flex justify-between'>
<div>建站天数:</div>
<div>{diffDays} </div>
</div>
</div>
<div className='hidden busuanzi_container_page_pv'>
<div className='flex justify-between'>
<div>访问量:</div>
<div className='busuanzi_value_page_pv' />
</div>
</div>
<div className='hidden busuanzi_container_site_uv'>
<div className='flex justify-between'>
<div>访客数:</div>
<div className='busuanzi_value_site_uv' />
</div>
</div>
</div>
</>
}

View File

@@ -0,0 +1,18 @@
import dynamic from 'next/dynamic'
const NotionPage = dynamic(() => import('@/components/NotionPage'))
const Announcement = ({ post, className }) => {
if (post?.blockMap) {
return <div >
{post && (
<div id="announcement-content">
<NotionPage post={post} />
</div>
)}
</div>
} else {
return <></>
}
}
export default Announcement

View File

@@ -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 (
<div id='article-end'>
{/* 移动端 */}
<section className='lg:hidden pt-8 text-gray-800 items-center text-xs md:text-sm flex flex-col m-1 '>
<Link
href={`/${prev.slug}`}
passHref
className='cursor-pointer justify-between space-y-1 px-5 py-6 rounded-t-xl dark:bg-[#1e1e1e] border dark:border-gray-600 border-b-0 items-center dark:text-white flex flex-col w-full h-18 duration-200'
>
<div className='flex justify-start items-center w-full'>上一篇</div>
<div className='flex justify-center items-center text-lg font-bold'>{prev.title}</div>
</Link>
<Link
href={`/${next.slug}`}
passHref
className='cursor-pointer justify-between space-y-1 px-5 py-6 rounded-b-xl dark:bg-[#1e1e1e] border dark:border-gray-600 items-center dark:text-white flex flex-col w-full h-18 duration-200'
>
<div className='flex justify-start items-center w-full'>下一篇</div>
<div className='flex justify-center items-center text-lg font-bold'>{next.title}</div>
</Link>
</section>
{/* 桌面端 */}
<div id='pc-next-post' className={`hidden md:block fixed z-20 right-16 bottom-4 duration-200 transition-all ${isScrollEnd ? 'mb-0 opacity-100' : '-mb-24 opacity-0'}`}>
<Link
href={`/${next.slug}`}
className='cursor-pointer duration transition-all h-24 dark:bg-[#1e1e1e] border dark:border-gray-600 p-3 bg-white dark:text-gray-300 dark:hover:text-yellow-600 hover:text-white hover:font-bold hover:bg-gray-400 rounded-lg flex flex-col justify-between'
>
<div className='text-xs'>下一篇</div>
<hr />
<div>{next?.title}</div>
</Link>
</div>
</div>
)
}

View File

@@ -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 (
<section className="dark:text-gray-300 mt-6 mx-1 ">
<ul className="overflow-x-auto whitespace-nowrap text-sm dark:bg-gray-900 bg-gray-100 p-5 leading-8 border-l-2 border-indigo-500">
<li>
<strong className='mr-2'>{locale.COMMON.AUTHOR}:</strong>
<Link href={'/about'} className="hover:underline">
{BLOG.AUTHOR}
</Link>
</li>
<li>
<strong className='mr-2'>{locale.COMMON.URL}:</strong>
<a className="whitespace-normal break-words hover:underline" href={path}>
{path}
</a>
</li>
<li>
<strong className='mr-2'>{locale.COMMON.COPYRIGHT}:</strong>
{locale.COMMON.COPYRIGHT_NOTICE}
</li>
</ul>
</section>
);
}

View File

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

View File

@@ -0,0 +1,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 (
<div className="pt-8 hidden md:block">
{/* 推荐文章 */}
<div className=" mb-2 px-1 flex flex-nowrap justify-between">
<div className='dark:text-gray-300 text-lg font-bold'>
<i className="mr-2 fas fa-thumbs-up" />
{locale.COMMON.RELATE_POSTS}
</div>
</div>
{/* 文章列表 */}
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{recommendPosts.map(post => {
const headerImage = post?.pageCoverThumbnail
? post.pageCoverThumbnail
: siteInfo?.pageCover
return (
(<Link
key={post.id}
title={post.title}
href={`${BLOG.SUB_PATH}/${post.slug}`}
passHref
className="flex h-40 cursor-pointer overflow-hidden rounded-2xl">
<div className="h-full w-full relative group">
<div className="flex items-center justify-center w-full h-full duration-300 ">
<div className="z-10 text-lg px-4 font-bold text-white text-center shadow-text select-none">
{post.title}
</div>
</div>
<LazyImage src={headerImage} className='absolute top-0 w-full h-full object-cover object-center group-hover:scale-110 group-hover:brightness-50 transform duration-200'/>
</div>
</Link>)
)
})}
</div>
</div>
)
}

View File

@@ -0,0 +1,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 (
<div className=''>
<div
className="pb-4 dark:text-gray-300"
id={archiveTitle}
>
{archiveTitle}
</div>
<ul>
{posts?.map(post => {
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 <div key={post.id} className={'cursor-pointer flex flex-row mb-4 h-24 md:flex-row group w-full dark:border-gray-600 hover:border-indigo-600 dark:hover:border-yellow-600 duration-300 transition-colors justify-between overflow-hidden'}>
{/* 图片封面 */}
{showPageCover && (
<div>
<Link href={`${BLOG.SUB_PATH}/${post.slug}`} passHref legacyBehavior>
<LazyImage className={'rounded-xl bg-center bg-cover w-40 h-24'} src={post?.pageCoverThumbnail}/>
</Link>
</div>
)}
{/* 文字区块 */}
<div className={'flex px-2 flex-col justify-between w-full'}>
<div>
{/* 分类 */}
{post?.category && <div className={`flex items-center ${showPreview ? 'justify-center' : 'justify-start'} hidden md:block flex-wrap dark:text-gray-500 text-gray-600 `}>
<Link passHref href={`/category/${post.category}`}
className="cursor-pointer text-xs font-normal menu-link hover:text-indigo-700 dark:text-gray-600 transform">
{post.category}
</Link>
</div>}
{/* 标题 */}
<Link
href={`${BLOG.SUB_PATH}/${post.slug}`}
passHref
className={' group-hover:text-indigo-700 group-hover:dark:text-indigo-400 text-black dark:text-gray-100 dark:group-hover:text-yellow-600 line-clamp-2 replace cursor-pointer text-xl font-extrabold leading-tight'}>
<span className='menu-link '>{post.title}</span>
</Link>
</div>
{/* 摘要 */}
{/* <p className="line-clamp-1 replace my-3 2xl:my-0 text-gray-700 dark:text-gray-300 text-xs font-light leading-tight">
{post.summary}
</p> */}
<div className="md:flex-nowrap flex-wrap md:justify-start inline-block">
<div>
{' '}
{post.tagItems?.map(tag => (
<TagItemMini key={tag.name} tag={tag} />
))}
</div>
</div>
</div>
</div>
})}
</ul>
</div>
)
}
}
export default BlogPostArchive

View File

@@ -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 (
<div className={` ${CONFIG.POST_LIST_COVER_HOVER_ENLARGE ? ' hover:scale-110 transition-all duration-150' : ''}`} >
<div
data-aos="fade-up"
data-aos-duration="200"
data-aos-once="false"
data-aos-anchor-placement="top-bottom"
className={'border bg-white dark:bg-[#1e1e1e] flex mb-4 flex-col h-[23rem] md:h-52 md:flex-row 2xl:h-96 2xl:flex-col group w-full dark:border-gray-600 hover:border-indigo-600 dark:hover:border-yellow-600 duration-300 transition-colors justify-between overflow-hidden rounded-xl'}>
{/* 图片封面 */}
{showPageCover && (
<Link href={`${BLOG.SUB_PATH}/${post.slug}`} passHref legacyBehavior>
<div className="w-full md:w-5/12 2xl:w-full overflow-hidden">
<LazyImage priority={index === 0} src={post?.pageCoverThumbnail} alt={post?.title} className='h-60 w-full object-cover group-hover:scale-105 group-hover:brightness-75 transition-all duration-300' />
</div>
</Link>
)}
{/* 文字区块 */}
<div className={'flex p-6 2xl:p-4 flex-col justify-between h-48 md:h-full 2xl:h-48 w-full md:w-7/12 2xl:w-full'}>
<div>
{/* 分类 */}
{post?.category && <div className={`flex mb-1 items-center ${showPreview ? 'justify-center' : 'justify-start'} hidden md:block flex-wrap dark:text-gray-500 text-gray-600 `}>
<Link passHref href={`/category/${post.category}`}
className="cursor-pointer text-xs font-normal menu-link hover:text-indigo-700 dark:hover:text-yellow-700 dark:text-gray-600 transform">
{post.category}
</Link>
</div>}
{/* 标题 */}
<Link
href={`${BLOG.SUB_PATH}/${post.slug}`}
passHref
className={' group-hover:text-indigo-700 dark:hover:text-yellow-700 dark:group-hover:text-yellow-600 text-black dark:text-gray-100 line-clamp-2 replace cursor-pointer text-xl font-extrabold leading-tight'}>
<span className='menu-link '>{post.title}</span>
</Link>
</div>
{/* 摘要 */}
{(!showPreview || showSummary) && !post.results && (
<p className="line-clamp-2 replace my-3 2xl:my-1 text-gray-700 dark:text-gray-300 text-sm font-light leading-tight">
{post.summary}
</p>
)}
{/* 搜索结果 */}
{post.results && (
<p className="line-clamp-2 mt-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7">
{post.results.map((r, index) => (
<span key={index}>{r}</span>
))}
</p>
)}
<div className="md:flex-nowrap flex-wrap md:justify-start inline-block">
<div>
{' '}
{post.tagItems?.map(tag => (
<TagItemMini key={tag.name} tag={tag} />
))}
</div>
</div>
</div>
</div>
</div>
)
}
export default BlogPostCard

View File

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

View File

@@ -0,0 +1,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 <BlogPostListEmpty />
} else {
return (
<div id="container" className='w-full'>
{/* 文章列表 */}
<div className="2xl:grid 2xl:grid-cols-2 grid-cols-1 gap-5">
{posts?.map(post => (
<BlogPostCard index={posts.indexOf(post)} key={post.id} post={post} siteInfo={siteInfo} />
))}
</div>
{showPagination && <PaginationNumber page={page} totalPage={totalPage} />}
</div>
)
}
}
export default BlogPostListPage

View File

@@ -0,0 +1,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 <BlogPostListEmpty currentSearch={currentSearch} />
} else {
return <div id='container' ref={targetRef} className='w-full'>
{/* 文章列表 */}
<div className="2xl:grid 2xl:grid-cols-2 grid-cols-1 gap-5">
{postsToShow.map(post => (
<BlogPostCard key={post.id} post={post} showSummary={showSummary} siteInfo={siteInfo}/>
))}
</div>
{/* 更多按钮 */}
<div>
<div onClick={() => { handleGetMore() }}
className='w-full my-4 py-4 text-center cursor-pointer rounded-xl dark:text-gray-200'
> {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE}`} </div>
</div>
</div>
}
}
export default BlogPostListScroll

View File

@@ -0,0 +1,9 @@
const Card = ({ children, headerSlot, className }) => {
return <div className={`${className || ''} card border dark:border-gray-700 rounded-xl lg:p-6 p-4`}>
<>{headerSlot}</>
<section>
{children}
</section>
</div>
}
export default Card

View File

@@ -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 <div className='px-3 py-1 dark:text-white text-black'>
<div className='w-full'><i className='mr-1 fas fa-stream' />{locale.COMMON.TABLE_OF_CONTENTS}</div>
<div className='overflow-y-auto max-h-36 lg:max-h-96 overscroll-none scroll-hidden' ref={tRef}>
<nav className='h-full'>
{toc.map((tocItem) => {
const id = uuidToId(tocItem.id)
tocIds.push(id)
return (
<a
key={id}
href={`#${id}`}
className={`notion-table-of-contents-item duration-300 transform font-light dark:text-gray-200
notion-table-of-contents-item-indent-level-${tocItem.indentLevel} `}
>
<span style={{ display: 'inline-block', marginLeft: tocItem.indentLevel * 16 }}
className={`${activeSection === id && ' font-bold text-indigo-600'}`}
>
{tocItem.text}
</span>
</a>
)
})}
</nav>
</div>
</div>
}
export default Catalog

View File

@@ -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 <div id='category-bar' className={`flex flex-nowrap justify-between items-center h-12 mb-4 space-x-2 w-full lg:bg-white dark:lg:bg-[#1e1e1e]
${border ? 'lg:border lg:hover:border dark:lg:border-gray-800 hover:border-indigo-600 dark:hover:border-yellow-600 ' : ''} py-2 lg:px-2 rounded-xl transition-colors duration-200`}>
<div id='category-bar-items' ref={categoryBarItemsRef} className='scroll-smooth max-w-4xl rounded-lg scroll-hidden flex justify-start flex-nowrap items-center overflow-x-scroll'>
<MenuItem href='/' name={locale.NAV.INDEX} />
{categoryOptions?.map((c, index) => <MenuItem key={index} href={`/category/${c.name}`} name={c.name} />)}
</div>
<div id='category-bar-next' className='flex items-center justify-center'>
<div id='right' className='cursor-pointer mx-2' onClick={handleToggleScroll}>
{scrollRight ? <ChevronDoubleLeft className={'w-5 h-5'} /> : <ChevronDoubleRight className={'w-5 h-5'} /> }
</div>
<Link href='/category' className='whitespace-nowrap font-bold text-gray-900 dark:text-white transition-colors duration-200 hover:text-indigo-600'>
{locale.MENU.CATEGORY}
</Link>
</div>
</div>
}
/**
* 按钮
* @param {*} param0
* @returns
*/
const MenuItem = ({ href, name }) => {
const router = useRouter()
const selected = router.pathname === href
return <div className={`whitespace-nowrap mr-2 duration-200 transition-all font-bold px-2 py-0.5 rounded-md text-gray-900 dark:text-white hover:text-white hover:bg-indigo-600 dark:hover:bg-yellow-600 ${selected ? 'text-white bg-indigo-600 dark:bg-yellow-600' : ''}`}>
<Link href={href}>{name}</Link>
</div>
}

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