Merge branch 'main' into pr/jxpeng98/1257
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 | 0(0:禁用 lrc 歌词,1:lrc 格式的字符串,3:lrc 文件 url)
|
process.env.NEXT_PUBLIC_MUSIC_PLAYER_METING_LRC_TYPE || '1', // 可选值: 3 | 1 | 0(0:禁用 lrc 歌词,1:lrc 格式的字符串,3:lrc 文件 url)
|
||||||
|
|
||||||
|
// ********挂件组件相关********
|
||||||
// ----> 评论互动 可同时开启多个支持 WALINE VALINE GISCUS CUSDIS UTTERRANCES GITALK
|
// ----> 评论互动 可同时开启多个支持 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, // 文章图片是否自动添加阴影
|
||||||
|
|
||||||
|
|||||||
92
components/AlgoliaSearchModal.js
Normal 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
@@ -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>
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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'>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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`} />}
|
||||||
|
|||||||
158
components/CustomContextMenu.js
Normal 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 >
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
48
components/FullScreenButton.js
Normal 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
|
||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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>}
|
||||||
|
</>)
|
||||||
|
}
|
||||||
@@ -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
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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> {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标签和 之类的特殊符合
|
||||||
|
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
@@ -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 }
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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: '搜索文章',
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
@@ -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 || []
|
||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -2,4 +2,7 @@
|
|||||||
|
|
||||||
#theme-fukasawa .sideLeft hr{
|
#theme-fukasawa .sideLeft hr{
|
||||||
opacity: .04;
|
opacity: .04;
|
||||||
}
|
}
|
||||||
|
.fa-info:before {
|
||||||
|
content: "\f05a";
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
/* fukasawa的主题相关 */
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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%);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
503
styles/animate.css
vendored
Normal 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;
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
@@ -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 }
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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} >
|
||||||
|
|||||||
@@ -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
@@ -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 }
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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 }
|
||||||
43
themes/heo/components/AnalyticsCard.js
Normal 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>
|
||||||
|
</>
|
||||||
|
}
|
||||||
18
themes/heo/components/Announcement.js
Normal 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
|
||||||
86
themes/heo/components/ArticleAdjacent.js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
41
themes/heo/components/ArticleCopyright.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
themes/heo/components/ArticleLock.js
Normal 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'} > {locale.COMMON.SUBMIT}</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id='tips'>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
65
themes/heo/components/ArticleRecommend.js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
87
themes/heo/components/BlogPostArchive.js
Normal 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
|
||||||
85
themes/heo/components/BlogPostCard.js
Normal 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
|
||||||
14
themes/heo/components/BlogPostListEmpty.js
Normal 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
|
||||||
35
themes/heo/components/BlogPostListPage.js
Normal 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
|
||||||
76
themes/heo/components/BlogPostListScroll.js
Normal 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
|
||||||
9
themes/heo/components/Card.js
Normal 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
|
||||||
90
themes/heo/components/Catalog.js
Normal 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
|
||||||
61
themes/heo/components/CategoryBar.js
Normal 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>
|
||||||
|
}
|
||||||