mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-17 07:26:46 +00:00
提交
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
# 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables
|
||||
NEXT_PUBLIC_VERSION=3.11.0
|
||||
NEXT_PUBLIC_VERSION=3.12.4
|
||||
|
||||
@@ -81,10 +81,12 @@
|
||||
|
||||
<td align="center"><a href="https://github.com/Pylogmon"><img src="https://avatars.githubusercontent.com/u/59004461" width="64px;" alt="Pylogmon"/><br/><sub><b>派了个萌 </b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=Pylogmon" title="Pylogmon" >🔧 🐛</a></td>
|
||||
|
||||
<td align="center"><a href="https://github.com/SkysCrystal"><img src="https://avatars.githubusercontent.com/u/49473463" width="64px;" alt="SkysCrystal"/><br/><sub><b>Simon Shi
|
||||
</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=SkysCrystal" title="SkysCrystal" >🔧 🐛</a></td>
|
||||
|
||||
<td align="center"><a href="https://github.com/SkysCrystal"><img src="https://avatars.githubusercontent.com/u/49473463" width="64px;" alt="SkysCrystal"/><br/><sub><b>Simon Shi</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=SkysCrystal" title="SkysCrystal" >🔧 🐛</a></td>
|
||||
|
||||
<td align="center"><a href="https://github.com/siygle"><img src="https://avatars.githubusercontent.com/u/173408" width="64px;" alt="S.Y. Lee"/><br/><sub><b>S.Y. Lee</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=siygle" title="siygle" >🔧 🐛</a></td>
|
||||
|
||||
<td align="center"><a href="https://github.com/fighting-bug"><img src="https://avatars.githubusercontent.com/u/56441589" width="64px;" alt="fighting-buf"/><br/><sub><b>fighting-buf</b></sub></a><br/><a href="https://github.com/tangly1024/NotionNext/commits?author=fighting-bug" title="fighting-buf" >🔧 🐛</a></td>
|
||||
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -8,6 +8,10 @@ import React from 'react'
|
||||
const Collapse = props => {
|
||||
const collapseRef = React.useRef(null)
|
||||
const type = props.type || 'vertical'
|
||||
/**
|
||||
* 折叠
|
||||
* @param {*} element
|
||||
*/
|
||||
const collapseSection = element => {
|
||||
const sectionHeight = element.scrollHeight
|
||||
const sectionWidth = element.scrollWidth
|
||||
@@ -54,17 +58,20 @@ const Collapse = props => {
|
||||
clearTimeout(clearTime)
|
||||
}
|
||||
|
||||
const updateHeight = () => {
|
||||
collapseRef.current.style.height = 'auto'
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
const element = collapseRef.current
|
||||
if (props.isOpen) {
|
||||
expandSection(element)
|
||||
expandSection(collapseRef.current)
|
||||
} else {
|
||||
collapseSection(element)
|
||||
collapseSection(collapseRef.current)
|
||||
}
|
||||
}, [props.isOpen])
|
||||
|
||||
return (
|
||||
<div ref={collapseRef} style={type === 'vertical' ? { height: '0px' } : { width: '0px' }} className={'overflow-hidden duration-200 ' + props.className }>
|
||||
<div ref={collapseRef} onClick={updateHeight} style={type === 'vertical' ? { height: '0px' } : { width: '0px' }} className={'overflow-hidden duration-200 ' + props.className }>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,10 +2,31 @@ import { useGlobal } from '@/lib/global'
|
||||
import { ReactCusdis } from 'react-cusdis'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
const CusdisComponent = ({ frontMatter }) => {
|
||||
const { locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
const { isDarkMode } = useGlobal()
|
||||
|
||||
// 处理cusdis主题
|
||||
useEffect(() => {
|
||||
const cusdisThread = document?.getElementById('cusdis_thread')
|
||||
const cusdisIframe = cusdisThread?.getElementsByTagName('iframe')
|
||||
if (cusdisIframe) {
|
||||
const cusdisWrapper = cusdisIframe[0]?.contentDocument?.getElementById('root')
|
||||
if (isDarkMode) {
|
||||
cusdisWrapper?.classList?.remove('light')
|
||||
cusdisWrapper?.classList?.add('dark')
|
||||
} else {
|
||||
cusdisWrapper?.classList?.remove('dark')
|
||||
cusdisWrapper?.classList?.add('light')
|
||||
}
|
||||
if (!cusdisWrapper?.firstElementChild?.classList?.contains('dark:text-gray-100')) {
|
||||
cusdisWrapper?.firstElementChild?.classList?.add('dark:text-gray-100')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return <ReactCusdis
|
||||
lang={locale.LOCALE.toLowerCase()}
|
||||
|
||||
@@ -13,7 +13,7 @@ const DarkModeButton = (props) => {
|
||||
htmlElement.classList?.add(newStatus ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
return <div className={'z-10 duration-200 text-xl py-2 ' + props.className}>
|
||||
return <div className={'text-white z-10 duration-200 text-xl py-2 ' + props.className}>
|
||||
<i id='darkModeButton' className={`hover:scale-125 cursor-pointer transform duration-200 fas ${isDarkMode ? 'fa-sun' : 'fa-moon'}`}
|
||||
onClick={handleChangeDarkMode} />
|
||||
</div>
|
||||
|
||||
@@ -69,7 +69,7 @@ export function DebugPanel() {
|
||||
|
||||
{/* 调试侧拉抽屉 */}
|
||||
<div
|
||||
className={` ${show ? 'shadow-card w-96 right-0 ' : '-right-96 w-0'} overflow-y-scroll h-full p-5 bg-white fixed bottom-0 z-50 duration-200`}
|
||||
className={` ${show ? 'shadow-card w-96 right-0 ' : '-right-96 invisible w-0'} overflow-y-scroll h-full p-5 bg-white fixed bottom-0 z-50 duration-200`}
|
||||
>
|
||||
<div className="flex justify-between space-x-1 my-5">
|
||||
<div className='flex'>
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { isBrowser, loadExternalResource } from '@/lib/utils'
|
||||
import { loadExternalResource } from '@/lib/utils'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* 自定义引入外部JS 和 CSS
|
||||
* @returns
|
||||
*/
|
||||
const ExternalScript = () => {
|
||||
if (isBrowser()) {
|
||||
useEffect(() => {
|
||||
// 静态导入本地自定义样式
|
||||
loadExternalResource(BLOG.FONT_AWESOME, 'css')
|
||||
loadExternalResource('/css/custom.css', 'css')
|
||||
loadExternalResource('/js/custom.js', 'js')
|
||||
|
||||
// 自动添加图片阴影
|
||||
if (BLOG.IMG_SHADOW) {
|
||||
loadExternalResource('/css/img-shadow.css', 'css')
|
||||
}
|
||||
|
||||
if (BLOG.CUSTOM_EXTERNAL_JS && BLOG.CUSTOM_EXTERNAL_JS.length > 0) {
|
||||
for (const url of BLOG.CUSTOM_EXTERNAL_JS) {
|
||||
loadExternalResource(url, 'js')
|
||||
@@ -25,7 +32,7 @@ const ExternalScript = () => {
|
||||
BLOG.FONT_URL?.forEach(e => {
|
||||
loadExternalResource(e, 'css')
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import mediumZoom from '@fisch0920/medium-zoom'
|
||||
import React from 'react'
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
import { Code } from 'react-notion-x/build/third-party/code'
|
||||
import TweetEmbed from 'react-tweet-embed'
|
||||
|
||||
import 'katex/dist/katex.min.css'
|
||||
import { mapImgUrl } from '@/lib/notion/mapImage'
|
||||
@@ -36,6 +37,10 @@ const Modal = dynamic(
|
||||
() => import('react-notion-x/build/third-party/modal').then((m) => m.Modal), { ssr: false }
|
||||
)
|
||||
|
||||
const Tweet = ({ id }) => {
|
||||
return <TweetEmbed tweetId={id} />
|
||||
}
|
||||
|
||||
const NotionPage = ({ post, className }) => {
|
||||
const zoom = isBrowser() && mediumZoom({
|
||||
container: '.notion-viewport',
|
||||
@@ -79,7 +84,7 @@ const NotionPage = ({ post, className }) => {
|
||||
return <>{post?.summary || ''}</>
|
||||
}
|
||||
|
||||
return <div id='container' className={`max-w-5xl font-medium mx-auto ${className}`}>
|
||||
return <div id='container' className={`font-medium mx-auto ${className}`}>
|
||||
<NotionRenderer
|
||||
recordMap={post.blockMap}
|
||||
mapPageUrl={mapPageUrl}
|
||||
@@ -89,7 +94,8 @@ const NotionPage = ({ post, className }) => {
|
||||
Collection,
|
||||
Equation,
|
||||
Modal,
|
||||
Pdf
|
||||
Pdf,
|
||||
Tweet
|
||||
}} />
|
||||
|
||||
<PrismMac />
|
||||
|
||||
@@ -7,7 +7,7 @@ export const StarrySky = () => {
|
||||
}, [])
|
||||
return (
|
||||
<div className="relative">
|
||||
<canvas id="starry-sky-vixcity" className="fixed"></canvas>
|
||||
<canvas id="starry-sky-vixcity" className="fixed pointer-events-none"></canvas>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,8 +51,7 @@ const Tabs = ({ className, children }) => {
|
||||
{children.map((item, index) => {
|
||||
return <section key={index}
|
||||
data-aos="fade-up"
|
||||
data-aos-duration="600"
|
||||
data-aos-easing="ease-in-out"
|
||||
data-aos-duration="300"
|
||||
data-aos-once="true"
|
||||
data-aos-anchor-placement="top-bottom">
|
||||
{currentTab === index && item}
|
||||
|
||||
@@ -85,18 +85,39 @@ function getCustomNav({ allPages }) {
|
||||
const customNav = []
|
||||
if (allPages && allPages.length > 0) {
|
||||
allPages.forEach(p => {
|
||||
if (p?.status === 'Published' && p?.type === 'Page') {
|
||||
if (p?.slug?.indexOf('http') === 0) {
|
||||
customNav.push({ icon: p.icon || null, name: p.title, to: p.slug, show: true })
|
||||
} else {
|
||||
customNav.push({ icon: p.icon || null, name: p.title, to: '/' + p.slug, show: true })
|
||||
}
|
||||
if (p?.slug?.indexOf('http') === 0) {
|
||||
customNav.push({ icon: p.icon || null, name: p.title, to: p.slug, show: true })
|
||||
} else {
|
||||
customNav.push({ icon: p.icon || null, name: p.title, to: '/' + p.slug, show: true })
|
||||
}
|
||||
})
|
||||
}
|
||||
return customNav
|
||||
}
|
||||
|
||||
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 menus = []
|
||||
if (menuPages && menuPages.length > 0) {
|
||||
menuPages.forEach(e => {
|
||||
e.show = true
|
||||
if (e.type === BLOG.NOTION_PROPERTY_NAME.type_menu) {
|
||||
menus.push(e)
|
||||
} else if (e.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu) {
|
||||
const parentMenu = menus[menus.length - 1]
|
||||
if (parentMenu) {
|
||||
if (parentMenu.subMenus) {
|
||||
parentMenu.subMenus.push(e)
|
||||
} else {
|
||||
parentMenu.subMenus = [e]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return menus
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取标签选项
|
||||
* @param schema
|
||||
@@ -214,11 +235,13 @@ async function getPageRecordMapByNotionAPI({ pageId, from }) {
|
||||
})
|
||||
}
|
||||
|
||||
const notice = await getNotice(collectionData.filter(post => { return post?.type === 'Notice' && post.status === 'Published' })?.[0])
|
||||
const notice = await getNotice(collectionData.filter(post => { return post && post?.type && post?.type === 'Notice' && post.status === 'Published' })?.[0])
|
||||
const categoryOptions = getAllCategories({ allPages, categoryOptions: getCategoryOptions(schema) })
|
||||
const tagOptions = getAllTags({ allPages, tagOptions: getTagOptions(schema) })
|
||||
const siteInfo = getBlogInfo({ collection, block })
|
||||
const customNav = getCustomNav({ allPages: collectionData.filter(post => post.type === 'Page' && post.status === 'Published') })
|
||||
// 新的菜单
|
||||
const customMenu = getCustomMenu({ collectionData })
|
||||
const latestPosts = getLatestPosts({ allPages, from, latestPostCount: 5 })
|
||||
|
||||
return {
|
||||
@@ -236,6 +259,7 @@ async function getPageRecordMapByNotionAPI({ pageId, from }) {
|
||||
categoryOptions,
|
||||
rawMetadata,
|
||||
customNav,
|
||||
customMenu,
|
||||
postCount,
|
||||
pageIds,
|
||||
latestPosts
|
||||
|
||||
@@ -76,10 +76,14 @@ export default async function getPageProperties(id, block, schema, authToken, ta
|
||||
// 映射值:用户个性化type和status字段的下拉框选项,在此映射回代码的英文标识
|
||||
mapProperties(properties)
|
||||
|
||||
if (properties.type === 'Post') {
|
||||
properties.slug = BLOG.POST_URL_PREFIX ? (BLOG.POST_URL_PREFIX + '/' + (properties.slug ?? properties.id)) : (properties.slug ?? properties.id)
|
||||
} else {
|
||||
properties.slug = (properties.slug ?? properties.id)
|
||||
if (properties.type === BLOG.NOTION_PROPERTY_NAME.type_post) {
|
||||
properties.slug = (BLOG.POST_URL_PREFIX) ? generateCustomizeUrl(properties) : (properties.slug ?? properties.id)
|
||||
} else if (properties.type === BLOG.NOTION_PROPERTY_NAME.type_page) {
|
||||
properties.slug = properties.slug ?? properties.id
|
||||
} else if (properties.type === BLOG.NOTION_PROPERTY_NAME.type_menu || properties.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu) {
|
||||
// 菜单路径为空、作为可展开菜单使用
|
||||
properties.to = properties.slug ?? null
|
||||
properties.name = properties.title ?? ''
|
||||
}
|
||||
|
||||
// 开启伪静态路径
|
||||
@@ -143,3 +147,30 @@ function mapProperties(properties) {
|
||||
properties.status = 'Invisible'
|
||||
}
|
||||
}
|
||||
|
||||
function generateCustomizeUrl(postProperties) {
|
||||
let fullSlug = ''
|
||||
const allSlugPatterns = BLOG.POST_URL_PREFIX.split('/')
|
||||
allSlugPatterns.forEach((pattern, idx) => {
|
||||
if (pattern === '%year%' && postProperties?.date?.start_date) {
|
||||
const formatPostCreatedDate = new Date(postProperties?.date?.start_date)
|
||||
fullSlug += formatPostCreatedDate.getUTCFullYear()
|
||||
} else if (pattern === '%month%' && postProperties?.date?.start_date) {
|
||||
const formatPostCreatedDate = new Date(postProperties?.date?.start_date)
|
||||
fullSlug += String(formatPostCreatedDate.getUTCMonth() + 1).padStart(2, 0)
|
||||
} else if (pattern === '%day%' && postProperties?.date?.start_date) {
|
||||
const formatPostCreatedDate = new Date(postProperties?.date?.start_date)
|
||||
fullSlug += String(formatPostCreatedDate.getUTCDate()).padStart(2, 0)
|
||||
} else if (pattern === '%slug%') {
|
||||
fullSlug += (postProperties.slug ?? postProperties.id)
|
||||
} else if (!pattern.includes('%')) {
|
||||
fullSlug += pattern
|
||||
} else {
|
||||
return
|
||||
}
|
||||
if (idx !== allSlugPatterns.length - 1) {
|
||||
fullSlug += '/'
|
||||
}
|
||||
})
|
||||
return `${fullSlug}/${(postProperties.slug ?? postProperties.id)}`
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
/**
|
||||
* Notion图片映射处理有emjji的图标
|
||||
* Notion图片映射处理有emoji的图标
|
||||
* @param {*} img
|
||||
* @param {*} value
|
||||
* @returns
|
||||
@@ -18,7 +20,7 @@ const mapImgUrl = (img, block, type = 'block') => {
|
||||
}
|
||||
|
||||
// notion永久图床地址
|
||||
if (!ret && img.indexOf('secure.notion-static.com') > 0) {
|
||||
if (!ret && img.indexOf('secure.notion-static.com') > 0 && (BLOG.IMG_URL_TYPE === 'Notion' || type !== 'block')) {
|
||||
ret = 'https://www.notion.so/image/' + encodeURIComponent(img) + '?table=' + type + '&id=' + block.id
|
||||
}
|
||||
|
||||
|
||||
25
lib/robots.txt.js
Normal file
25
lib/robots.txt.js
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
import fs from 'fs'
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
export async function generateRobotsTxt() {
|
||||
const content = `
|
||||
# *
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# Host
|
||||
Host: ${BLOG.LINK}
|
||||
|
||||
# Sitemaps
|
||||
Sitemap: ${BLOG.LINK}/sitemap.xml
|
||||
|
||||
`
|
||||
try {
|
||||
fs.mkdirSync('./public', { recursive: true })
|
||||
fs.writeFileSync('./public/robots.txt', content)
|
||||
} catch (error) {
|
||||
// 在vercel运行环境是只读的,这里会报错;
|
||||
// 但在vercel编译阶段、或VPS等其他平台这行代码会成功执行
|
||||
}
|
||||
}
|
||||
22
lib/rss.js
22
lib/rss.js
@@ -5,6 +5,11 @@ import ReactDOMServer from 'react-dom/server'
|
||||
import { getPostBlocks } from './notion'
|
||||
import NotionPage from '@/components/NotionPage'
|
||||
|
||||
/**
|
||||
* 生成RSS内容
|
||||
* @param {*} post
|
||||
* @returns
|
||||
*/
|
||||
const createFeedContent = async post => {
|
||||
// 加密的文章内容只返回摘要
|
||||
if (post.password && post.password !== '') {
|
||||
@@ -15,7 +20,7 @@ const createFeedContent = async post => {
|
||||
post.blockMap = blockMap
|
||||
const content = ReactDOMServer.renderToString(<NotionPage post={post} />)
|
||||
const regexExp =
|
||||
/<div class="notion-collection-row"><div class="notion-collection-row-body"><div class="notion-collection-row-property"><div class="notion-collection-column-title"><svg.*?class="notion-collection-column-title-icon">.*?<\/svg><div class="notion-collection-column-title-body">.*?<\/div><\/div><div class="notion-collection-row-value">.*?<\/div><\/div><\/div><\/div>/g
|
||||
/<div class="notion-collection-row"><div class="notion-collection-row-body"><div class="notion-collection-row-property"><div class="notion-collection-column-title"><svg.*?class="notion-collection-column-title-icon">.*?<\/svg><div class="notion-collection-column-title-body">.*?<\/div><\/div><div class="notion-collection-row-value">.*?<\/div><\/div><\/div><\/div>/g
|
||||
return content.replace(regexExp, '')
|
||||
}
|
||||
}
|
||||
@@ -38,7 +43,6 @@ export async function generateRss(posts) {
|
||||
for (const post of posts) {
|
||||
feed.addItem({
|
||||
title: post.title,
|
||||
guid: `${post.id}`,
|
||||
link: `${BLOG.LINK}/${post.slug}`,
|
||||
description: post.summary,
|
||||
content: await createFeedContent(post),
|
||||
@@ -46,8 +50,14 @@ export async function generateRss(posts) {
|
||||
})
|
||||
}
|
||||
|
||||
fs.mkdirSync('./public/rss', { recursive: true })
|
||||
fs.writeFileSync('./public/rss/feed.xml', feed.rss2())
|
||||
fs.writeFileSync('./public/rss/atom.xml', feed.atom1())
|
||||
fs.writeFileSync('./public/rss/feed.json', feed.json1())
|
||||
try {
|
||||
fs.mkdirSync('./public/rss', { recursive: true })
|
||||
fs.writeFileSync('./public/rss/feed.xml', feed.rss2())
|
||||
fs.writeFileSync('./public/rss/atom.xml', feed.atom1())
|
||||
fs.writeFileSync('./public/rss/feed.json', feed.json1())
|
||||
} catch (error) {
|
||||
// 在vercel运行环境是只读的,这里会报错;
|
||||
// 但在vercel编译阶段、或VPS等其他平台这行代码会成功执行
|
||||
// RSS被高频词访问将大量消耗服务端资源,故作为静态文件
|
||||
}
|
||||
}
|
||||
|
||||
59
lib/sitemap.xml.js
Normal file
59
lib/sitemap.xml.js
Normal file
@@ -0,0 +1,59 @@
|
||||
|
||||
import fs from 'fs'
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
export async function generateSitemapXml({ allPages }) {
|
||||
const urls = [{
|
||||
loc: `${BLOG.LINK}`,
|
||||
lastmod: new Date().toISOString().split('T')[0],
|
||||
changefreq: 'daily'
|
||||
}, {
|
||||
loc: `${BLOG.LINK}/archive`,
|
||||
lastmod: new Date().toISOString().split('T')[0],
|
||||
changefreq: 'daily'
|
||||
}, {
|
||||
loc: `${BLOG.LINK}/category`,
|
||||
lastmod: new Date().toISOString().split('T')[0],
|
||||
changefreq: 'daily'
|
||||
}, {
|
||||
loc: `${BLOG.LINK}/tag`,
|
||||
lastmod: new Date().toISOString().split('T')[0],
|
||||
changefreq: 'daily'
|
||||
}]
|
||||
|
||||
allPages?.forEach(post => {
|
||||
urls.push({
|
||||
loc: `${BLOG.LINK}/${post.slug}`,
|
||||
lastmod: new Date(post?.date?.start_date || post?.createdTime).toISOString().split('T')[0],
|
||||
changefreq: 'daily'
|
||||
})
|
||||
})
|
||||
const xml = createSitemapXml(urls)
|
||||
try {
|
||||
fs.writeFileSync('sitemap.xml', xml)
|
||||
fs.writeFileSync('./public/sitemap.xml', xml)
|
||||
} catch (error) {
|
||||
console.warn('无法写入文件', error)
|
||||
}
|
||||
}
|
||||
function createSitemapXml(urls) {
|
||||
let urlsXml = ''
|
||||
urls.forEach(u => {
|
||||
urlsXml += `<url>
|
||||
<loc>${u.loc}</loc>
|
||||
<lastmod>${u.lastmod}</lastmod>
|
||||
<changefreq>${u.changefreq}</changefreq>
|
||||
</url>
|
||||
`
|
||||
})
|
||||
|
||||
return `
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"
|
||||
xmlns:xhtml="http://www.w3.org/1999/xhtml"
|
||||
xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0"
|
||||
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
|
||||
${urlsXml}
|
||||
</urlset>
|
||||
`
|
||||
}
|
||||
@@ -28,7 +28,8 @@ export const initDarkMode = (isDarkMode, updateDarkMode) => {
|
||||
*/
|
||||
export const initTheme = (theme, changeTheme) => {
|
||||
if (isBrowser()) {
|
||||
const queryTheme = getQueryVariable('theme') || loadThemeFromCookies() || BLOG.THEME
|
||||
// const queryTheme = getQueryVariable('theme') || loadThemeFromCookies() || BLOG.THEME
|
||||
const queryTheme = getQueryVariable('theme') || BLOG.THEME
|
||||
let currentTheme = theme
|
||||
if (queryTheme !== theme && ALL_THEME.indexOf(queryTheme) > -1) {
|
||||
currentTheme = queryTheme
|
||||
|
||||
29
lib/utils.js
29
lib/utils.js
@@ -124,3 +124,32 @@ export const getListByPage = function (list, pageIndex, pageSize) {
|
||||
pageIndex * pageSize
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否移动设备
|
||||
*/
|
||||
export const isMobile = () => {
|
||||
let isMobile = false
|
||||
if (!isBrowser()) {
|
||||
return isMobile
|
||||
}
|
||||
|
||||
// 这个判断会引发 TypeError: navigator.userAgentData.mobile is undefined 问题,导致博客无法正常工作
|
||||
// if (!isMobile && navigator.userAgentData.mobile) {
|
||||
// isMobile = true
|
||||
// }
|
||||
|
||||
if (!isMobile && (/Mobi|Android|iPhone/i.test(navigator.userAgent))) {
|
||||
isMobile = true
|
||||
}
|
||||
|
||||
if (/Android|iPhone|iPad|iPod/i.test(navigator.platform)) {
|
||||
isMobile = true
|
||||
}
|
||||
|
||||
if (typeof window.orientation !== 'undefined') {
|
||||
isMobile = true
|
||||
}
|
||||
|
||||
return isMobile
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ module.exports = withBundleAnalyzer({
|
||||
'gravatar.com',
|
||||
'www.notion.so',
|
||||
'avatars.githubusercontent.com',
|
||||
'images.unsplash.com'
|
||||
'images.unsplash.com',
|
||||
'source.unsplash.com',
|
||||
'p1.qhimg.com'
|
||||
]
|
||||
},
|
||||
// 默认将feed重定向至 /public/rss/feed.xml
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "notion-next",
|
||||
"version": "3.11.0",
|
||||
"version": "3.12.4",
|
||||
"homepage": "https://github.com/tangly1024/NotionNext.git",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
@@ -50,8 +50,9 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-facebook": "^8.1.4",
|
||||
"react-messenger-customer-chat": "^0.8.0",
|
||||
"react-notion-x": "6.15.8",
|
||||
"react-notion-x": "6.16.0",
|
||||
"react-share": "^4.4.0",
|
||||
"react-tweet-embed": "~2.0.0",
|
||||
"smoothscroll-polyfill": "^0.4.4",
|
||||
"twikoo": "1.6.9",
|
||||
"typed.js": "^2.0.12",
|
||||
|
||||
@@ -49,7 +49,7 @@ const Slug = props => {
|
||||
})
|
||||
}
|
||||
}
|
||||
}, 20 * 1000)
|
||||
}, 8 * 1000) // 404时长
|
||||
const meta = { title: `${props?.siteInfo?.title || BLOG.TITLE} | loading`, image: siteInfo?.pageCover || BLOG.HOME_BANNER_IMAGE }
|
||||
return <ThemeComponents.LayoutSlug {...props} showArticleInfo={true} meta={meta} />
|
||||
}
|
||||
@@ -100,7 +100,7 @@ export async function getStaticPaths() {
|
||||
const from = 'slug-paths'
|
||||
const { allPages } = await getGlobalNotionData({ from })
|
||||
return {
|
||||
paths: allPages.map(row => ({ params: { slug: [row.slug] } })),
|
||||
paths: allPages?.map(row => ({ params: { slug: [row.slug] } })),
|
||||
fallback: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import BLOG from 'blog.config'
|
||||
import React from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
import 'animate.css'
|
||||
@@ -20,10 +20,11 @@ import { Sakura } from '@/components/Sakura'
|
||||
import { StarrySky } from '@/components/StarrySky'
|
||||
import MusicPlayer from '@/components/MusicPlayer'
|
||||
import ExternalScript from '@/components/ExternalScript'
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
import smoothscroll from 'smoothscroll-polyfill'
|
||||
|
||||
import AOS from 'aos'
|
||||
import 'aos/dist/aos.css' // You can also use <link> for styles
|
||||
import { isMobile } from '@/lib/utils'
|
||||
|
||||
const Ackee = dynamic(() => import('@/components/Ackee'), { ssr: false })
|
||||
const Gtag = dynamic(() => import('@/components/Gtag'), { ssr: false })
|
||||
@@ -55,14 +56,17 @@ const MyApp = ({ Component, pageProps }) => {
|
||||
<ExternalScript/>
|
||||
</>
|
||||
|
||||
if (isBrowser()) {
|
||||
useEffect(() => {
|
||||
AOS.init()
|
||||
}
|
||||
if (isMobile()) {
|
||||
smoothscroll.polyfill()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<GlobalContextProvider>
|
||||
{externalPlugins}
|
||||
<Component {...pageProps} />
|
||||
{externalPlugins}
|
||||
</GlobalContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getGlobalNotionData } from '@/lib/notion/getNotionData'
|
||||
import * as ThemeMap from '@/themes'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { generateRss } from '@/lib/rss'
|
||||
import { generateRobotsTxt } from '@/lib/robots.txt'
|
||||
const Index = props => {
|
||||
const { theme } = useGlobal()
|
||||
const ThemeComponents = ThemeMap[theme]
|
||||
@@ -17,7 +18,6 @@ export async function getStaticProps() {
|
||||
const { siteInfo } = props
|
||||
props.posts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published')
|
||||
|
||||
delete props.allPages
|
||||
const meta = {
|
||||
title: `${siteInfo?.title} | ${siteInfo?.description}`,
|
||||
description: siteInfo?.description,
|
||||
@@ -43,8 +43,14 @@ export async function getStaticProps() {
|
||||
}
|
||||
}
|
||||
|
||||
// 异步生成Feed订阅
|
||||
generateRss(props?.latestPosts || [])
|
||||
// 生成robotTxt
|
||||
generateRobotsTxt()
|
||||
// 生成Feed订阅
|
||||
if (JSON.parse(BLOG.ENABLE_RSS)) {
|
||||
generateRss(props?.latestPosts || [])
|
||||
}
|
||||
|
||||
delete props.allPages
|
||||
|
||||
return {
|
||||
props: {
|
||||
|
||||
@@ -122,7 +122,8 @@ async function filterByMemCache(allPosts, keyword) {
|
||||
const articleInfo = post.title + post.summary + tagContent + categoryContent
|
||||
let hit = articleInfo.toLowerCase().indexOf(keyword) > -1
|
||||
let indexContent = [post.summary]
|
||||
if (page && page.block) {
|
||||
// 防止搜到加密文章的内容
|
||||
if (page && page.block && !post.password) {
|
||||
const contentIds = Object.keys(page.block)
|
||||
contentIds.forEach(id => {
|
||||
const properties = page?.block[id]?.value?.properties
|
||||
|
||||
@@ -38,7 +38,7 @@ export const getServerSideProps = async (ctx) => {
|
||||
priority: '0.7'
|
||||
}
|
||||
]
|
||||
const postFields = allPages?.map(post => {
|
||||
const postFields = allPages?.filter(p => p.status === BLOG.NOTION_PROPERTY_NAME.status_publish)?.map(post => {
|
||||
return {
|
||||
loc: `${BLOG.LINK}/${post.slug}`,
|
||||
lastmod: new Date(post?.date?.start_date || post?.createdTime).toISOString().split('T')[0],
|
||||
@@ -49,10 +49,10 @@ export const getServerSideProps = async (ctx) => {
|
||||
const fields = defaultFields.concat(postFields)
|
||||
|
||||
// 缓存
|
||||
// ctx.res.setHeader(
|
||||
// 'Cache-Control',
|
||||
// 'public, s-maxage=10, stale-while-revalidate=59'
|
||||
// )
|
||||
ctx.res.setHeader(
|
||||
'Cache-Control',
|
||||
'public, max-age=3600, stale-while-revalidate=59'
|
||||
)
|
||||
|
||||
return getServerSideSitemap(ctx, fields)
|
||||
}
|
||||
|
||||
11
public/avatar.svg
Normal file
11
public/avatar.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<rect width="200" height="200" fill="#111111"/>
|
||||
<path d="M200.563 83.7392H169V144.594H168.092L126.765 83.7392H99.9706V200H131.534V138.918H132.215L174.223 200H200.563V83.7392Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="200" height="200" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 426 B |
BIN
public/bg_image.jpg
Normal file → Executable file
BIN
public/bg_image.jpg
Normal file → Executable file
Binary file not shown.
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 16 KiB |
5
public/css/img-shadow.css
Normal file
5
public/css/img-shadow.css
Normal file
@@ -0,0 +1,5 @@
|
||||
/* 图片阴影 */
|
||||
#notion-article img{
|
||||
box-shadow: rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
29
public/css/theme-fukasawa.css
Normal file
29
public/css/theme-fukasawa.css
Normal file
@@ -0,0 +1,29 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
33
public/css/theme-simple.css
Normal file
33
public/css/theme-simple.css
Normal file
@@ -0,0 +1,33 @@
|
||||
#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;
|
||||
}
|
||||
|
||||
|
||||
/* 菜单下划线动画 */
|
||||
.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;
|
||||
}
|
||||
|
||||
.menu-link:hover {
|
||||
background-size: 100% 2px;
|
||||
color: #dd3333;
|
||||
}
|
||||
|
||||
@@ -167,6 +167,13 @@ nav {
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.waline-recent-content .wl-emoji {
|
||||
height: 1.1rem !important;
|
||||
display: inline-block !important;
|
||||
line-height: 1.25rem !important;
|
||||
vertical-align: text-bottom !important;
|
||||
}
|
||||
|
||||
.vcontent .wl-emoji {
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
@@ -204,7 +211,7 @@ nav {
|
||||
}
|
||||
|
||||
.nobelium .notion-code{
|
||||
@apply max-w-2xl;
|
||||
/* @apply max-w-2xl; */
|
||||
}
|
||||
|
||||
.next #announcement-content *{
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
}
|
||||
|
||||
.notion-simple-table {
|
||||
@apply whitespace-nowrap overflow-x-scroll block
|
||||
@apply whitespace-nowrap overflow-x-auto block
|
||||
}
|
||||
|
||||
.notion-app {
|
||||
@@ -207,7 +207,6 @@
|
||||
|
||||
.medium-zoom-image {
|
||||
border-radius: 0;
|
||||
@apply bg-gray-100 dark:bg-black
|
||||
}
|
||||
|
||||
.medium-zoom-image--opened {
|
||||
@@ -939,7 +938,7 @@ code[class*='language-'] {
|
||||
align-self: flex-start;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 1.3em;
|
||||
font-size: 1em;
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
@@ -1118,10 +1117,11 @@ code[class*='language-'] {
|
||||
.notion-table-of-contents {
|
||||
width: 100%;
|
||||
margin: 4px 0;
|
||||
@apply bg-gray-50 dark:bg-black p-2
|
||||
}
|
||||
|
||||
.notion-table-of-contents-item {
|
||||
color: inherit;
|
||||
/* color: inherit; */
|
||||
text-decoration: none;
|
||||
user-select: none;
|
||||
transition: background 20ms ease-in 0s;
|
||||
@@ -1137,6 +1137,8 @@ code[class*='language-'] {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@apply text-blue-600 dark:text-blue-200
|
||||
}
|
||||
|
||||
.notion-table-of-contents-item:hover {
|
||||
@@ -2008,4 +2010,11 @@ code.language-mermaid {
|
||||
|
||||
.notion-equation-inline .katex-display {
|
||||
margin: 0 0 !important;
|
||||
}
|
||||
|
||||
.two-line-clamp {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -21,9 +21,10 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
maxWidth: {
|
||||
side: '14rem'
|
||||
side: '14rem',
|
||||
'9/10': '90%'
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
variants: {
|
||||
extend: {}
|
||||
|
||||
@@ -16,7 +16,7 @@ import BLOG from '@/blog.config'
|
||||
const LayoutBase = props => {
|
||||
const { children, meta } = props
|
||||
return (
|
||||
<div className='dark:text-gray-300 bg-white dark:bg-black'>
|
||||
<div id='theme-example' className='dark:text-gray-300 bg-white dark:bg-black'>
|
||||
<CommonHead meta={meta} />
|
||||
{/* 顶栏LOGO */}
|
||||
<Header {...props} />
|
||||
|
||||
@@ -26,7 +26,7 @@ export const ArticleLock = props => {
|
||||
<div className='text-center space-y-3'>
|
||||
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
|
||||
<div className='flex mx-4'>
|
||||
<input id="password" type='password' className='w-full text-sm pl-5 rounded-l transition font-light leading-10 text-black dark:bg-gray-500 bg-gray-50'></input>
|
||||
<input id="password" type='password' className='outline-none w-full text-sm pl-5 rounded-l transition font-light leading-10 text-black dark:bg-gray-500 bg-gray-50'></input>
|
||||
<div onClick={submitPassword} className="px-3 whitespace-nowrap cursor-pointer items-center justify-center py-2 rounded-r duration-300 bg-gray-300" >
|
||||
<i className={'duration-200 cursor-pointer fas fa-key dark:text-black'} > {locale.COMMON.SUBMIT}</i>
|
||||
</div>
|
||||
|
||||
@@ -78,5 +78,5 @@ export const BlogListScroll = props => {
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,5 +21,5 @@ export const Header = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import CONFIG_EMPTY from '../config_empty'
|
||||
import CONFIG_EXAMPLE from '../config_example'
|
||||
|
||||
/**
|
||||
* 菜单导航
|
||||
@@ -11,10 +11,10 @@ export const Nav = (props) => {
|
||||
const { customNav } = props
|
||||
const { locale } = useGlobal()
|
||||
let links = [
|
||||
{ icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG_EMPTY.MENU_SEARCH },
|
||||
{ icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG_EMPTY.MENU_ARCHIVE },
|
||||
{ icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_EMPTY.MENU_CATEGORY },
|
||||
{ icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_EMPTY.MENU_TAG }
|
||||
{ icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG_EXAMPLE.MENU_SEARCH },
|
||||
{ icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG_EXAMPLE.MENU_ARCHIVE },
|
||||
{ icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_EXAMPLE.MENU_CATEGORY },
|
||||
{ icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_EXAMPLE.MENU_TAG }
|
||||
]
|
||||
|
||||
if (customNav) {
|
||||
|
||||
@@ -62,7 +62,7 @@ const SearchInput = ({ currentTag, currentSearch, cRef }) => {
|
||||
ref={searchInputRef}
|
||||
type='text'
|
||||
placeholder={currentTag ? `${locale.SEARCH.TAGS} #${currentTag}` : `${locale.SEARCH.ARTICLES}`}
|
||||
className={'w-full text-sm pl-4 transition focus:shadow-lg font-light leading-10 text-black bg-gray-100 dark:bg-gray-900 dark:text-white'}
|
||||
className={'outline-none w-full text-sm pl-4 transition focus:shadow-lg font-light leading-10 text-black bg-gray-100 dark:bg-gray-900 dark:text-white'}
|
||||
onKeyUp={handleKeyUp}
|
||||
onCompositionStart={lockSearchInput}
|
||||
onCompositionUpdate={lockSearchInput}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const CONFIG_EMPTY = {
|
||||
const CONFIG_EXAMPLE = {
|
||||
// 菜单配置
|
||||
MENU_CATEGORY: true, // 显示分类
|
||||
MENU_TAG: true, // 显示标签
|
||||
MENU_ARCHIVE: true, // 显示归档
|
||||
MENU_SEARCH: true // 显示搜索
|
||||
}
|
||||
export default CONFIG_EMPTY
|
||||
export default CONFIG_EXAMPLE
|
||||
@@ -1,4 +1,4 @@
|
||||
import CONFIG_EMPTY from './config_empty'
|
||||
import CONFIG_EXAMPLE from './config_example'
|
||||
import { LayoutIndex } from './LayoutIndex'
|
||||
import { LayoutSearch } from './LayoutSearch'
|
||||
import { LayoutArchive } from './LayoutArchive'
|
||||
@@ -11,7 +11,7 @@ import { LayoutTag } from './LayoutTag'
|
||||
import { LayoutTagIndex } from './LayoutTagIndex'
|
||||
|
||||
export {
|
||||
CONFIG_EMPTY as THEME_CONFIG,
|
||||
CONFIG_EXAMPLE as THEME_CONFIG,
|
||||
LayoutIndex,
|
||||
LayoutSearch,
|
||||
LayoutArchive,
|
||||
|
||||
@@ -3,6 +3,7 @@ import TopNav from './components/TopNav'
|
||||
import AsideLeft from './components/AsideLeft'
|
||||
import Live2D from '@/components/Live2D'
|
||||
import BLOG from '@/blog.config'
|
||||
import { isBrowser, loadExternalResource } from '@/lib/utils'
|
||||
|
||||
/**
|
||||
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
|
||||
@@ -25,7 +26,12 @@ const LayoutBase = (props) => {
|
||||
meta
|
||||
} = props
|
||||
const leftAreaSlot = <Live2D/>
|
||||
return (<>
|
||||
|
||||
if (isBrowser()) {
|
||||
loadExternalResource('/css/theme-fukasawa.css', 'css')
|
||||
}
|
||||
|
||||
return (<div id='theme-fukasawa' >
|
||||
<CommonHead meta={meta} />
|
||||
<TopNav {...props}/>
|
||||
<div className={(BLOG.LAYOUT_SIDEBAR_REVERSE ? 'flex-row-reverse' : '') + ' flex'}>
|
||||
@@ -38,7 +44,7 @@ const LayoutBase = (props) => {
|
||||
</main>
|
||||
</div>
|
||||
|
||||
</>)
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default LayoutBase
|
||||
|
||||
@@ -30,7 +30,7 @@ export const ArticleLock = props => {
|
||||
<div className="flex mx-4">
|
||||
<input
|
||||
id="password" type='password'
|
||||
className="w-full text-sm pl-5 rounded-l transition focus:shadow-lg dark:text-gray-300 font-light leading-10 text-black bg-gray-100 dark:bg-gray-500"
|
||||
className="outline-none w-full text-sm pl-5 rounded-l transition focus:shadow-lg dark:text-gray-300 font-light leading-10 text-black bg-gray-100 dark:bg-gray-500"
|
||||
></input>
|
||||
<div
|
||||
onClick={submitPassword}
|
||||
|
||||
@@ -2,9 +2,8 @@ import BLOG from '@/blog.config'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import CONFIG_FUKA from '../config_fuka'
|
||||
import Card from './Card'
|
||||
|
||||
const BlogCard = ({ post, showSummary, siteInfo }) => {
|
||||
const BlogCard = ({ index, post, showSummary, siteInfo }) => {
|
||||
const showPreview = CONFIG_FUKA.POST_LIST_PREVIEW && post.blockMap
|
||||
// matery 主题默认强制显示图片
|
||||
if (post && !post.page_cover) {
|
||||
@@ -13,44 +12,45 @@ const BlogCard = ({ post, showSummary, siteInfo }) => {
|
||||
const showPageCover = CONFIG_FUKA.POST_LIST_COVER && post?.page_cover
|
||||
|
||||
return (
|
||||
<Card className="w-full lg:max-w-sm p-2 h-full overflow-auto">
|
||||
<div
|
||||
key={post.id}
|
||||
className="flex flex-col-reverse justify-between duration-300"
|
||||
>
|
||||
<div className="p-2 flex flex-col w-full">
|
||||
<Link
|
||||
href={`${BLOG.SUB_PATH}/${post.slug}`}
|
||||
passHref
|
||||
className={`cursor-pointer font-bold hover:underline text-xl ${showPreview ? 'justify-center' : 'justify-start'
|
||||
} leading-tight text-gray-700 dark:text-gray-100 hover:text-blue-500 dark:hover:text-blue-400`}>
|
||||
<div data-aos="fade-up" data-aos-duration="500" data-aos-once="true"
|
||||
className='w-full lg:max-w-sm p-2 h-full overflow-auto'>
|
||||
<section className="shadow mb-4 p-2 bg-white dark:bg-hexo-black-gray hover:shadow-lg duration-200">
|
||||
<div className="flex flex-col-reverse justify-between duration-300">
|
||||
<div className="p-2 flex flex-col w-full">
|
||||
<Link
|
||||
href={`${BLOG.SUB_PATH}/${post.slug}`}
|
||||
passHref
|
||||
className={`break-words cursor-pointer font-bold hover:underline text-xl ${showPreview ? 'justify-center' : 'justify-start'}
|
||||
leading-tight text-gray-700 dark:text-gray-100 hover:text-blue-500 dark:hover:text-blue-400`}>
|
||||
|
||||
{post.title}
|
||||
{post.title}
|
||||
|
||||
</Link>
|
||||
</Link>
|
||||
|
||||
{(!showPreview || showSummary) && (
|
||||
<p className="mt-4 mb-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7 overflow-hidden">
|
||||
{post.summary}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showPageCover && (
|
||||
<Link href={`${BLOG.SUB_PATH}/${post.slug}`} passHref legacyBehavior>
|
||||
<div className="h-40 w-full relative duration-200 cursor-pointer transform overflow-hidden">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={post?.page_cover}
|
||||
alt={post.title}
|
||||
className="w-full hover:scale-125 transform duration-500"
|
||||
></img>
|
||||
{/* <Image className='hover:scale-105 transform duration-500' src={post?.page_cover} alt={post.title} layout='fill' objectFit='cover' loading='lazy' /> */}
|
||||
{(!showPreview || showSummary) && (
|
||||
<p className="mt-4 mb-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7 overflow-hidden">
|
||||
{post.summary}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{showPageCover && (
|
||||
<Link href={`${BLOG.SUB_PATH}/${post.slug}`} passHref legacyBehavior>
|
||||
<div className="h-40 w-full relative duration-200 cursor-pointer transform overflow-hidden">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={post?.page_cover}
|
||||
alt={post.title}
|
||||
className="w-full hover:scale-125 transform duration-500"
|
||||
></img>
|
||||
{/* <Image className='hover:scale-105 transform duration-500' src={post?.page_cover} alt={post.title} layout='fill' objectFit='cover' loading='lazy' /> */}
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useEffect, useState } from 'react'
|
||||
import BlogCard from './BlogCard'
|
||||
import BlogPostListEmpty from './BlogListEmpty'
|
||||
import PaginationSimple from './PaginationSimple'
|
||||
@@ -15,25 +14,6 @@ import PaginationSimple from './PaginationSimple'
|
||||
const BlogListPage = ({ page = 1, posts = [], postCount, siteInfo }) => {
|
||||
const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
|
||||
const showNext = page < totalPage
|
||||
const [colCount, changeCol] = useState(1)
|
||||
|
||||
function updateCol() {
|
||||
if (window.outerWidth > 1200) {
|
||||
changeCol(3)
|
||||
} else if (window.outerWidth > 900) {
|
||||
changeCol(2)
|
||||
} else {
|
||||
changeCol(1)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateCol()
|
||||
window.addEventListener('resize', updateCol)
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateCol)
|
||||
}
|
||||
})
|
||||
|
||||
if (!posts || posts.length === 0) {
|
||||
return <BlogPostListEmpty />
|
||||
@@ -41,10 +21,10 @@ const BlogListPage = ({ page = 1, posts = [], postCount, siteInfo }) => {
|
||||
return (
|
||||
<div>
|
||||
{/* 文章列表 */}
|
||||
<div id="container" style={{ columnCount: colCount }}>
|
||||
<div id="container" className='grid-container'>
|
||||
{posts?.map(post => (
|
||||
<div key={post.id} className='justify-center flex' style={{ breakInside: 'avoid' }}>
|
||||
<BlogCard key={post.id} post={post} siteInfo={siteInfo} />
|
||||
<div key={post.id} className='grid-item justify-center flex' style={{ breakInside: 'avoid' }}>
|
||||
<BlogCard index={posts.indexOf(post)} key={post.id} post={post} siteInfo={siteInfo} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react'
|
||||
import BlogCard from './BlogCard'
|
||||
import BlogPostListEmpty from './BlogListEmpty'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import throttle from 'lodash.throttle'
|
||||
|
||||
/**
|
||||
* 文章列表分页表格
|
||||
* @param page 当前页
|
||||
@@ -14,18 +14,7 @@ import throttle from 'lodash.throttle'
|
||||
*/
|
||||
const BlogListScroll = props => {
|
||||
const { posts = [], siteInfo } = props
|
||||
const [colCount, changeCol] = React.useState(1)
|
||||
const { locale } = useGlobal()
|
||||
|
||||
function updateCol() {
|
||||
if (window.outerWidth > 1200) {
|
||||
changeCol(3)
|
||||
} else if (window.outerWidth > 900) {
|
||||
changeCol(2)
|
||||
} else {
|
||||
changeCol(1)
|
||||
}
|
||||
}
|
||||
const targetRef = React.useRef(null)
|
||||
|
||||
const [page, updatePage] = React.useState(1)
|
||||
@@ -45,45 +34,41 @@ const BlogListScroll = props => {
|
||||
}
|
||||
|
||||
// 监听滚动自动分页加载
|
||||
const scrollTrigger = React.useCallback(throttle(() => {
|
||||
const scrollS = window.scrollY + window.outerHeight
|
||||
const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
|
||||
if (scrollS > clientHeight + 100) {
|
||||
handleGetMore()
|
||||
}
|
||||
}, 500))
|
||||
const scrollTrigger = () => {
|
||||
requestAnimationFrame(() => {
|
||||
const scrollS = window.scrollY + window.outerHeight
|
||||
const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
|
||||
if (scrollS > clientHeight + 100) {
|
||||
handleGetMore()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
updateCol()
|
||||
window.addEventListener('scroll', scrollTrigger)
|
||||
|
||||
window.addEventListener('resize', updateCol)
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateCol)
|
||||
window.removeEventListener('scroll', scrollTrigger)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (!posts || posts.length === 0) {
|
||||
return <BlogPostListEmpty />
|
||||
} else {
|
||||
return (
|
||||
<div id="container" ref={targetRef} >
|
||||
{/* 文章列表 */}
|
||||
<div style={{ columnCount: colCount }}>
|
||||
{postsToShow?.map(post => (
|
||||
<div key={post.id} className='justify-center flex' style={{ breakInside: 'avoid' }}>
|
||||
<BlogCard key={post.id} post={post} siteInfo={siteInfo} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div id="container" ref={targetRef} className='grid-container' >
|
||||
{/* 文章列表 */}
|
||||
{postsToShow?.map(post => (
|
||||
<div key={post.id} className='grid-item justify-center flex' style={{ breakInside: 'avoid' }}>
|
||||
<BlogCard index={posts.indexOf(post)} key={post.id} post={post} siteInfo={siteInfo} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="w-full my-4 py-4 text-center cursor-pointer "
|
||||
onClick={handleGetMore}>
|
||||
{' '}
|
||||
{hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full my-4 py-4 text-center cursor-pointer "
|
||||
onClick={handleGetMore}>
|
||||
{' '}
|
||||
{hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
const Card = ({ children, headerSlot, className }) => {
|
||||
return <div className={className}
|
||||
data-aos="fade-up"
|
||||
data-aos-duration="600"
|
||||
data-aos-easing="ease-in-out"
|
||||
data-aos-once="false"
|
||||
data-aos-anchor-placement="top-bottom"
|
||||
>
|
||||
return <div data-aos="fade-in" data-aos-duration="1000" className={className}>
|
||||
<>{headerSlot}</>
|
||||
<section className="shadow mb-4 p-2 bg-white dark:bg-hexo-black-gray hover:shadow-lg duration-200">
|
||||
{children}
|
||||
|
||||
@@ -27,7 +27,7 @@ const Catalog = ({ toc }) => {
|
||||
|
||||
// 同步选中目录事件
|
||||
const [activeSection, setActiveSection] = React.useState(null)
|
||||
const throttleMs = 100
|
||||
const throttleMs = 200
|
||||
const actionSectionScrollSpy = React.useCallback(throttle(() => {
|
||||
const sections = document.getElementsByClassName('notion-h')
|
||||
let prevBBox = null
|
||||
|
||||
@@ -53,7 +53,7 @@ const SearchInput = ({ currentTag, currentSearch, cRef }) => {
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type='text'
|
||||
className={'w-full text-sm pl-2 transition focus:shadow-lg font-light leading-10 text-black bg-gray-100 dark:bg-gray-800 dark:text-white'}
|
||||
className={'outline-none w-full text-sm pl-2 transition focus:shadow-lg font-light leading-10 text-black bg-gray-100 dark:bg-gray-800 dark:text-white'}
|
||||
onKeyUp={handleKeyUp}
|
||||
onCompositionStart={lockSearchInput}
|
||||
onCompositionUpdate={lockSearchInput}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import CommonHead from '@/components/CommonHead'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import throttle from 'lodash.throttle'
|
||||
import Footer from './components/Footer'
|
||||
import JumpToTopButton from './components/JumpToTopButton'
|
||||
import SideRight from './components/SideRight'
|
||||
import TopNav from './components/TopNav'
|
||||
import smoothscroll from 'smoothscroll-polyfill'
|
||||
import FloatDarkModeButton from './components/FloatDarkModeButton'
|
||||
import Live2D from '@/components/Live2D'
|
||||
import LoadingCover from './components/LoadingCover'
|
||||
@@ -15,7 +14,13 @@ import dynamic from 'next/dynamic'
|
||||
|
||||
const FacebookPage = dynamic(
|
||||
() => {
|
||||
return import('@/components/FacebookPage')
|
||||
let facebook = <></>
|
||||
try {
|
||||
facebook = import('@/components/FacebookPage')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
return facebook
|
||||
},
|
||||
{ ssr: false }
|
||||
)
|
||||
@@ -28,7 +33,7 @@ const FacebookPage = dynamic(
|
||||
*/
|
||||
const LayoutBase = props => {
|
||||
const { children, headerSlot, floatSlot, meta, siteInfo } = props
|
||||
const [show, switchShow] = useState(false)
|
||||
const [showFloatButton, switchShow] = useState(false)
|
||||
// const [percent, changePercent] = useState(0) // 页面阅读百分比
|
||||
const rightAreaSlot = (
|
||||
<>
|
||||
@@ -37,8 +42,8 @@ const LayoutBase = props => {
|
||||
</>
|
||||
)
|
||||
const { onLoading } = useGlobal()
|
||||
|
||||
const scrollListener = () => {
|
||||
const throttleMs = 200
|
||||
const scrollListener = useCallback(throttle(() => {
|
||||
const targetRef = document.getElementById('wrapper')
|
||||
const clientHeight = targetRef?.clientHeight
|
||||
const scrollY = window.pageYOffset
|
||||
@@ -47,26 +52,24 @@ const LayoutBase = props => {
|
||||
if (per > 100) per = 100
|
||||
const shouldShow = scrollY > 100 && per > 0
|
||||
|
||||
if (shouldShow !== show) {
|
||||
if (shouldShow !== showFloatButton) {
|
||||
switchShow(shouldShow)
|
||||
}
|
||||
// changePercent(per)
|
||||
}
|
||||
}, throttleMs))
|
||||
useEffect(() => {
|
||||
smoothscroll.polyfill()
|
||||
document.addEventListener('scroll', scrollListener)
|
||||
return () => document.removeEventListener('scroll', scrollListener)
|
||||
}, [show])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="bg-hexo-background-gray dark:bg-black">
|
||||
<div id='theme-hexo'>
|
||||
<CommonHead meta={meta} siteInfo={siteInfo}/>
|
||||
|
||||
<TopNav {...props} />
|
||||
|
||||
{headerSlot}
|
||||
|
||||
<main id="wrapper" className="w-full py-8 md:px-8 lg:px-24 min-h-screen relative">
|
||||
<main id="wrapper" className="bg-hexo-background-gray dark:bg-black w-full py-8 md:px-8 lg:px-24 min-h-screen relative">
|
||||
<div
|
||||
id="container-inner"
|
||||
className={(BLOG.LAYOUT_SIDEBAR_REVERSE ? 'flex-row-reverse' : '') + ' pt-14 w-full mx-auto lg:flex lg:space-x-4 justify-center relative z-10'}
|
||||
@@ -79,13 +82,8 @@ const LayoutBase = props => {
|
||||
</main>
|
||||
|
||||
{/* 右下角悬浮 */}
|
||||
<div className="bottom-12 right-1 fixed justify-end z-20 text-white bg-indigo-500 dark:bg-hexo-black-gray rounded-sm">
|
||||
<div
|
||||
className={
|
||||
(show ? 'animate__animated ' : 'hidden') +
|
||||
' animate__fadeInUp justify-center duration-300 animate__faster flex flex-col items-center cursor-pointer '
|
||||
}
|
||||
>
|
||||
<div className={(showFloatButton ? 'opacity-100 ' : 'invisible opacity-0') + ' duration-300 transition-all bottom-12 right-1 fixed justify-end z-20 text-white bg-indigo-500 dark:bg-hexo-black-gray rounded-sm'}>
|
||||
<div className={'justify-center flex flex-col items-center cursor-pointer'}>
|
||||
<FloatDarkModeButton />
|
||||
{floatSlot}
|
||||
<JumpToTopButton />
|
||||
|
||||
@@ -7,8 +7,8 @@ import LayoutBase from './LayoutBase'
|
||||
import React from 'react'
|
||||
|
||||
export const LayoutIndex = (props) => {
|
||||
return <LayoutBase {...props} headerSlot={CONFIG_HEXO.HOME_BANNER_ENABLE && <Header {...props} />}>
|
||||
|
||||
const headerSlot = CONFIG_HEXO.HOME_BANNER_ENABLE && <Header {...props} />
|
||||
return <LayoutBase {...props} headerSlot={headerSlot}>
|
||||
{BLOG.POST_LIST_STYLE === 'page' ? <BlogPostListPage {...props} /> : <BlogPostListScroll {...props} />}
|
||||
</LayoutBase>
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export const LayoutSlug = props => {
|
||||
showTag={false}
|
||||
floatSlot={floatSlot}
|
||||
>
|
||||
<div className="w-full lg:shadow-sm lg:hover:shadow lg:border lg:rounded-xl lg:px-2 lg:py-4 bg-white dark:bg-hexo-black-gray dark:border-black">
|
||||
<div className="w-full lg:hover:shadow lg:border lg:rounded-xl lg:px-2 lg:py-4 bg-white dark:bg-hexo-black-gray dark:border-black">
|
||||
{lock && <ArticleLock validPassword={validPassword} />}
|
||||
|
||||
{!lock && <div id="container" className="overflow-x-auto flex-grow mx-auto md:w-full md:px-5 ">
|
||||
|
||||
@@ -6,14 +6,8 @@ const NotionPage = dynamic(() => import('@/components/NotionPage'))
|
||||
const Announcement = ({ post, className }) => {
|
||||
const { locale } = useGlobal()
|
||||
if (post?.blockMap) {
|
||||
return <div
|
||||
data-aos="fade-up"
|
||||
data-aos-duration="600"
|
||||
data-aos-easing="ease-in-out"
|
||||
data-aos-once="false"
|
||||
data-aos-anchor-placement="top-bottom"
|
||||
className={className}>
|
||||
<section id='announcement-wrapper' className="hover:shadow-md dark:text-gray-300 border dark:border-black rounded-xl px-2 py-4 bg-white dark:bg-hexo-black-gray">
|
||||
return <div className={className}>
|
||||
<section id='announcement-wrapper' className="dark:text-gray-300 border dark:border-black rounded-xl px-2 py-4 bg-white dark:bg-hexo-black-gray">
|
||||
<div><i className='mr-2 fas fa-bullhorn' />{locale.COMMON.ANNOUNCEMENT}</div>
|
||||
{post && (<div id="announcement-content">
|
||||
<NotionPage post={post} className='text-center ' />
|
||||
|
||||
@@ -25,7 +25,7 @@ export const ArticleLock = props => {
|
||||
<div className='text-center space-y-3'>
|
||||
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
|
||||
<div className='flex mx-4'>
|
||||
<input id="password" type='password' className='w-full text-sm pl-5 rounded-l transition focus:shadow-lg dark:text-gray-300 font-light leading-10 text-black bg-gray-100 dark:bg-gray-500'></input>
|
||||
<input id="password" type='password' className='outline-none w-full text-sm pl-5 rounded-l transition focus:shadow-lg dark:text-gray-300 font-light leading-10 text-black 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>
|
||||
|
||||
@@ -1,107 +1,57 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import TagItemMini from './TagItemMini'
|
||||
import CONFIG_HEXO from '../config_hexo'
|
||||
import NotionPage from '@/components/NotionPage'
|
||||
import { BlogPostCardInfo } from './BlogPostCardInfo'
|
||||
// import Image from 'next/image'
|
||||
|
||||
const BlogPostCard = ({ post, showSummary, index, siteInfo }) => {
|
||||
const BlogPostCard = ({ index, post, showSummary, siteInfo }) => {
|
||||
const showPreview = CONFIG_HEXO.POST_LIST_PREVIEW && post.blockMap
|
||||
if (post && !post.page_cover && CONFIG_HEXO.POST_LIST_COVER_DEFAULT) {
|
||||
post.page_cover = siteInfo?.pageCover
|
||||
}
|
||||
const showPageCover = CONFIG_HEXO.POST_LIST_COVER && post?.page_cover
|
||||
const delay = (index % 2) * 200
|
||||
|
||||
return (
|
||||
<div
|
||||
key={post.id}
|
||||
data-aos="fade-up"
|
||||
data-aos-duration="600"
|
||||
data-aos-easing="ease-in-out"
|
||||
data-aos-once="false"
|
||||
data-aos-duration="200"
|
||||
data-aos-delay={delay}
|
||||
data-aos-once="true"
|
||||
data-aos-anchor-placement="top-bottom"
|
||||
className={`flex md:flex-row flex-col-reverse ${index % 2 === 0 ? 'md:flex-row-reverse' : ''}
|
||||
w-full md:h-72 h-96 justify-between overflow-hidden drop-shadow-md
|
||||
border dark:border-black rounded-xl bg-white dark:bg-hexo-black-gray `}>
|
||||
key={post.id}
|
||||
className={`flex md:flex-row flex-col-reverse ${CONFIG_HEXO.POST_LIST_IMG_CROSSOVER ? 'even:md:flex-row-reverse' : ''}
|
||||
w-full justify-between overflow-hidden
|
||||
border dark:border-black rounded-xl bg-white dark:bg-hexo-black-gray`}>
|
||||
|
||||
<div className={`lg:p-8 p-4 flex flex-col ${showPageCover ? 'md:w-7/12 w-full' : 'w-full'}`}>
|
||||
<Link
|
||||
href={`${BLOG.SUB_PATH}/${post.slug}`}
|
||||
passHref
|
||||
className={`replace cursor-pointer hover:underline text-2xl ${showPreview ? 'text-center' : ''
|
||||
} leading-tight text-gray-600 dark:text-gray-100 hover:text-indigo-700 dark:hover:text-indigo-400`}>
|
||||
|
||||
{post.title}
|
||||
|
||||
</Link>
|
||||
|
||||
<div
|
||||
className={`flex mt-2 items-center ${showPreview ? 'justify-center' : 'justify-start'
|
||||
} flex-wrap dark:text-gray-500 text-gray-400 hover:text-indigo-700 dark:hover:text-indigo-400`}
|
||||
>
|
||||
<Link
|
||||
href={`/archive#${post?.date?.start_date?.substr(0, 7)}`}
|
||||
passHref
|
||||
className="font-light hover:underline cursor-pointer text-sm leading-4 mr-3">
|
||||
|
||||
<i className="far fa-calendar-alt mr-1" />
|
||||
{post.date?.start_date || post.lastEditedTime}
|
||||
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{(!showPreview || showSummary) && !post.results && (
|
||||
<p style={{ overflow: 'hidden', textOverflow: 'ellipsis', display: '-webkit-box', WebkitLineClamp: '4', WebkitBoxOrient: 'vertical' }}
|
||||
className="replace h-full max-h-32 my-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7">
|
||||
{post.summary}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* 搜索结果 */}
|
||||
{post.results && (
|
||||
<p className="mt-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7">
|
||||
{post.results.map(r => (
|
||||
<span key={r}>{r}</span>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{showPreview && (
|
||||
<div className="overflow-ellipsis truncate">
|
||||
<NotionPage post={post} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-gray-400 justify-between flex">
|
||||
<Link
|
||||
href={`/category/${post.category}`}
|
||||
passHref
|
||||
className="cursor-pointer font-light text-sm hover:underline hover:text-indigo-700 dark:hover:text-indigo-400 transform">
|
||||
|
||||
<i className="mr-1 far fa-folder" />
|
||||
{post.category}
|
||||
|
||||
</Link>
|
||||
<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>
|
||||
{/* 文字内容 */}
|
||||
<BlogPostCardInfo index={index} post={post} showPageCover={showPageCover} showPreview={showPreview} showSummary={showSummary}/>
|
||||
|
||||
{/* 图片封面 */}
|
||||
{showPageCover && !showPreview && post?.page_cover && (
|
||||
<div className="flex relative duration-200 cursor-pointer transform overflow-hidden md:w-5/12 ">
|
||||
<div className="h-auto md:w-5/12">
|
||||
<Link href={`${BLOG.SUB_PATH}/${post.slug}`} passHref legacyBehavior>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
{/* <img
|
||||
src={post?.page_cover}
|
||||
alt={post.title}
|
||||
className="h-full w-full hover:scale-125 transform object-cover duration-500"
|
||||
/>
|
||||
{/* <Image className='hover:scale-125 transform duration-500' src={post?.page_cover} alt={post.title} layout='fill' objectFit='cover' loading='lazy' /> */}
|
||||
loading='lazy'
|
||||
className="w-full relative cursor-pointer object-cover duration-200 hover:scale-125 "
|
||||
/> */}
|
||||
<div className='bg-center bg-cover md:h-full h-52' style={{ backgroundImage: `url('${post?.page_cover}')` }}/>
|
||||
|
||||
{/* <div className='relative w-full h-full'>
|
||||
<Image
|
||||
className='hover:scale-125 transition cursor-pointer duration-500'
|
||||
src={post?.page_cover}
|
||||
alt={post.title}
|
||||
quality={30}
|
||||
placeholder='blur'
|
||||
blurDataURL='/bg_image.jpg'
|
||||
style={{ objectFit: 'cover' }}
|
||||
fill/>
|
||||
</div> */}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
89
themes/hexo/components/BlogPostCardInfo.js
Normal file
89
themes/hexo/components/BlogPostCardInfo.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import NotionPage from '@/components/NotionPage'
|
||||
import Link from 'next/link'
|
||||
import TagItemMini from './TagItemMini'
|
||||
|
||||
/**
|
||||
* 博客列表的文字内容
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export const BlogPostCardInfo = ({ post, showPreview, showPageCover, showSummary }) => {
|
||||
return <div className={`h-56 flex flex-col justify-between lg:p-6 p-4 md:max-h-60 ${showPageCover ? 'md:w-7/12 w-full ' : 'w-full'}`}>
|
||||
|
||||
<div>
|
||||
{/* 标题 */}
|
||||
<Link
|
||||
href={`${BLOG.SUB_PATH}/${post.slug}`}
|
||||
passHref
|
||||
className={`replace cursor-pointer hover:underline text-2xl ${showPreview ? 'text-center' : ''
|
||||
} leading-tight text-gray-600 dark:text-gray-100 hover:text-indigo-700 dark:hover:text-indigo-400`}>
|
||||
|
||||
{post.title}
|
||||
|
||||
</Link>
|
||||
|
||||
{/* 日期 */}
|
||||
<div
|
||||
className={`flex mt-2 items-center ${showPreview ? 'justify-center' : 'justify-start'
|
||||
} flex-wrap dark:text-gray-500 text-gray-400 hover:text-indigo-700 dark:hover:text-indigo-400`}
|
||||
>
|
||||
<Link
|
||||
href={`/archive#${post?.date?.start_date?.substr(0, 7)}`}
|
||||
passHref
|
||||
className="font-light hover:underline cursor-pointer text-sm leading-4 mr-3">
|
||||
|
||||
<i className="far fa-calendar-alt mr-1" />
|
||||
{post.date?.start_date || post.lastEditedTime}
|
||||
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* 摘要 */}
|
||||
{(!showPreview || showSummary) && !post.results && (
|
||||
<p className="two-line-clamp replace my-3 text-gray-700 dark:text-gray-300 text-sm font-light leading-7">
|
||||
{post.summary}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* 搜索结果 */}
|
||||
{post.results && (
|
||||
<p className="two-line-clamp mt-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7">
|
||||
{post.results.map(r => (
|
||||
<span key={r}>{r}</span>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
{/* 预览 */}
|
||||
{showPreview && (
|
||||
<div className="overflow-ellipsis truncate">
|
||||
<NotionPage post={post} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{/* 分类标签 */}
|
||||
<div className="text-gray-400 justify-between flex">
|
||||
<Link
|
||||
href={`/category/${post.category}`}
|
||||
passHref
|
||||
className="cursor-pointer font-light text-sm hover:underline hover:text-indigo-700 dark:hover:text-indigo-400 transform">
|
||||
|
||||
<i className="mr-1 far fa-folder" />
|
||||
{post.category}
|
||||
|
||||
</Link>
|
||||
<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>
|
||||
}
|
||||
@@ -22,7 +22,7 @@ const BlogPostListPage = ({ page = 1, posts = [], postCount, siteInfo }) => {
|
||||
{/* 文章列表 */}
|
||||
<div className="space-y-6 px-2">
|
||||
{posts.map(post => (
|
||||
<BlogPostCard key={post.id} post={post} index={posts.indexOf(post)} siteInfo={siteInfo}/>
|
||||
<BlogPostCard index={posts.indexOf(post)} key={post.id} post={post} siteInfo={siteInfo}/>
|
||||
))}
|
||||
</div>
|
||||
{showPagination && <PaginationNumber page={page} totalPage={totalPage} />}
|
||||
|
||||
@@ -2,7 +2,6 @@ import BLOG from '@/blog.config'
|
||||
import BlogPostCard from './BlogPostCard'
|
||||
import BlogPostListEmpty from './BlogPostListEmpty'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import throttle from 'lodash.throttle'
|
||||
import React from 'react'
|
||||
import CONFIG_HEXO from '../config_hexo'
|
||||
import { getListByPage } from '@/lib/utils'
|
||||
@@ -31,13 +30,15 @@ const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = CONFIG_HE
|
||||
}
|
||||
|
||||
// 监听滚动自动分页加载
|
||||
const scrollTrigger = React.useCallback(throttle(() => {
|
||||
const scrollS = window.scrollY + window.outerHeight
|
||||
const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
|
||||
if (scrollS > clientHeight + 100) {
|
||||
handleGetMore()
|
||||
}
|
||||
}, 500))
|
||||
const scrollTrigger = () => {
|
||||
requestAnimationFrame(() => {
|
||||
const scrollS = window.scrollY + window.outerHeight
|
||||
const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
|
||||
if (scrollS > clientHeight + 100) {
|
||||
handleGetMore()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 监听滚动
|
||||
React.useEffect(() => {
|
||||
@@ -58,7 +59,7 @@ const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = CONFIG_HE
|
||||
{/* 文章列表 */}
|
||||
<div className='flex flex-wrap space-y-1 lg:space-y-4 px-2'>
|
||||
{postsToShow.map(post => (
|
||||
<BlogPostCard key={post.id} post={post} index={posts.indexOf(post)} showSummary={showSummary} siteInfo={siteInfo}/>
|
||||
<BlogPostCard key={post.id} post={post} showSummary={showSummary} siteInfo={siteInfo}/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const Card = ({ children, headerSlot, className }) => {
|
||||
return <div className={className}>
|
||||
<>{headerSlot}</>
|
||||
<section className=" drop-shadow-md hover:shadow-md dark:text-gray-300 border dark:border-black rounded-xl px-2 py-4 bg-white dark:bg-hexo-black-gray lg:duration-100">
|
||||
<section className="shadow-md hover:shadow-md dark:text-gray-300 border dark:border-black rounded-xl px-2 py-4 bg-white dark:bg-hexo-black-gray lg:duration-100">
|
||||
{children}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@ const Catalog = ({ toc }) => {
|
||||
// 同步选中目录事件
|
||||
const [activeSection, setActiveSection] = React.useState(null)
|
||||
|
||||
const throttleMs = 100
|
||||
const throttleMs = 200
|
||||
const actionSectionScrollSpy = React.useCallback(throttle(() => {
|
||||
const sections = document.getElementsByClassName('notion-h')
|
||||
let prevBBox = null
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
// import Image from 'next/image'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import Typed from 'typed.js'
|
||||
import CONFIG_HEXO from '../config_hexo'
|
||||
import NavButtonGroup from './NavButtonGroup'
|
||||
import throttle from 'lodash.throttle'
|
||||
|
||||
let wrapperTop = 0
|
||||
let windowTop = 0
|
||||
let autoScroll = false
|
||||
const enableAutoScroll = false // 是否开启自动吸附滚动
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -16,6 +19,7 @@ const Header = props => {
|
||||
const { siteInfo } = props
|
||||
useEffect(() => {
|
||||
updateHeaderHeight()
|
||||
|
||||
if (!typed && window && document.getElementById('typed')) {
|
||||
changeType(
|
||||
new Typed('#typed', {
|
||||
@@ -28,6 +32,7 @@ const Header = props => {
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (enableAutoScroll) {
|
||||
scrollTrigger()
|
||||
window.addEventListener('scroll', scrollTrigger)
|
||||
@@ -42,75 +47,75 @@ const Header = props => {
|
||||
}
|
||||
})
|
||||
|
||||
function updateHeaderHeight () {
|
||||
setTimeout(() => {
|
||||
function updateHeaderHeight() {
|
||||
requestAnimationFrame(() => {
|
||||
const wrapperElement = document.getElementById('wrapper')
|
||||
wrapperTop = wrapperElement?.offsetTop
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
const autoScrollEnd = () => {
|
||||
if (autoScroll) {
|
||||
windowTop = window.scrollY
|
||||
autoScroll = false
|
||||
}
|
||||
}
|
||||
const throttleMs = 200
|
||||
const scrollTrigger = useCallback(throttle(() => {
|
||||
if (screen.width <= 768) {
|
||||
return
|
||||
}
|
||||
|
||||
const scrollS = window.scrollY
|
||||
// 自动滚动
|
||||
if ((scrollS > windowTop) & (scrollS < window.innerHeight) && !autoScroll
|
||||
) {
|
||||
autoScroll = true
|
||||
window.scrollTo({ top: wrapperTop, behavior: 'smooth' })
|
||||
autoScrollEnd()
|
||||
}
|
||||
if ((scrollS < windowTop) && (scrollS < window.innerHeight) && !autoScroll) {
|
||||
autoScroll = true
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
autoScrollEnd()
|
||||
}
|
||||
windowTop = scrollS
|
||||
}, throttleMs))
|
||||
|
||||
return (
|
||||
<header
|
||||
id="header"
|
||||
className="duration-500 md:bg-fixed w-full bg-cover bg-center h-screen bg-black text-white relative z-10"
|
||||
style={{
|
||||
backgroundImage:
|
||||
`linear-gradient(rgba(0, 0, 0, 0.9), rgba(0,0,0,0.5), rgba(0,0,0,0.3), rgba(0,0,0,0.5), rgba(0, 0, 0, 0.9) ),url("${siteInfo?.pageCover}")`
|
||||
}}
|
||||
>
|
||||
<div className="absolute flex flex-col h-full items-center justify-center w-full ">
|
||||
<div className='text-4xl md:text-5xl text-white shadow-text'>{siteInfo?.title}</div>
|
||||
<div className='mt-2 h-12 items-center text-center shadow-text text-white text-lg'>
|
||||
<span id='typed'/>
|
||||
</div>
|
||||
<header
|
||||
id="header"
|
||||
className="w-full h-screen bg-black text-white relative"
|
||||
>
|
||||
<div className={`w-full h-full ${CONFIG_HEXO.HOME_NAV_BACKGROUND_IMG_FIXED ? 'fixed' : ''}`}>
|
||||
{/* <Image src={siteInfo.pageCover} fill
|
||||
style={{ objectFit: 'cover' }}
|
||||
className='opacity-70'
|
||||
placeholder='blur'
|
||||
blurDataURL='/bg_image.jpg' /> */}
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={siteInfo.pageCover} className='h-full w-full object-cover opacity-70 ' />
|
||||
</div>
|
||||
|
||||
{/* 首页导航插件 */}
|
||||
{ CONFIG_HEXO.HOME_NAV_BUTTONS && <NavButtonGroup {...props}/>}
|
||||
<div className="absolute bottom-0 flex flex-col h-full items-center justify-center w-full ">
|
||||
<div className='text-4xl md:text-5xl text-white shadow-text'>{siteInfo?.title}</div>
|
||||
<div className='mt-2 h-12 items-center text-center shadow-text text-white text-lg'>
|
||||
<span id='typed' />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
window.scrollTo({ top: wrapperTop, behavior: 'smooth' })
|
||||
}}
|
||||
className="cursor-pointer w-full text-center py-4 text-3xl absolute bottom-10 text-white"
|
||||
>
|
||||
<i className='animate-bounce fas fa-angle-down'/>
|
||||
</div>
|
||||
</header>
|
||||
{/* 首页导航插件 */}
|
||||
{CONFIG_HEXO.HOME_NAV_BUTTONS && <NavButtonGroup {...props} />}
|
||||
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={() => { window.scrollTo({ top: wrapperTop, behavior: 'smooth' }) }}
|
||||
className="cursor-pointer w-full text-center py-4 text-3xl absolute bottom-10 text-white"
|
||||
>
|
||||
<i className='animate-bounce fas fa-angle-down' />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
const enableAutoScroll = false // 是否开启自动吸附滚动
|
||||
|
||||
const autoScrollEnd = () => {
|
||||
if (autoScroll) {
|
||||
windowTop = window.scrollY
|
||||
autoScroll = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动吸附滚动,移动端体验不好暂时关闭
|
||||
*/
|
||||
const scrollTrigger = () => {
|
||||
if (screen.width <= 768) {
|
||||
return
|
||||
}
|
||||
|
||||
const scrollS = window.scrollY
|
||||
// 自动滚动
|
||||
if ((scrollS > windowTop) & (scrollS < window.innerHeight) && !autoScroll
|
||||
) {
|
||||
autoScroll = true
|
||||
window.scrollTo({ top: wrapperTop, behavior: 'smooth' })
|
||||
setTimeout(autoScrollEnd, 500)
|
||||
}
|
||||
if ((scrollS < windowTop) && (scrollS < window.innerHeight) && !autoScroll) {
|
||||
autoScroll = true
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
setTimeout(autoScrollEnd, 500)
|
||||
}
|
||||
windowTop = scrollS
|
||||
}
|
||||
|
||||
export default Header
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function HeaderArticle({ post, siteInfo }) {
|
||||
return (
|
||||
<div
|
||||
id="header"
|
||||
className="w-full h-96 relative md:flex-shrink-0 overflow-hidden bg-cover bg-center bg-no-repeat animate__animated animate__fadeIn relative"
|
||||
className="w-full h-96 relative md:flex-shrink-0 overflow-hidden bg-cover bg-center bg-no-repeat animate__animated animate__fadeIn"
|
||||
style={{ backgroundImage: headerImage }}
|
||||
>
|
||||
<header className="animate__slideInDown animate__animated bg-black bg-opacity-70 absolute top-0 w-full h-96 py-10 flex justify-center items-center ">
|
||||
@@ -65,5 +65,5 @@ export default function HeaderArticle({ post, siteInfo }) {
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,10 +11,11 @@ import CONFIG_HEXO from '../config_hexo'
|
||||
* @constructor
|
||||
*/
|
||||
const JumpToTopButton = ({ showPercent = true, percent }) => {
|
||||
const { locale } = useGlobal()
|
||||
|
||||
if (!CONFIG_HEXO.WIDGET_TO_TOP) {
|
||||
return <></>
|
||||
}
|
||||
const { locale } = useGlobal()
|
||||
return (<div className='space-x-1 items-center justify-center transform hover:scale-105 duration-200 w-7 h-auto pb-1 text-center' onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })} >
|
||||
<div title={locale.POST.TOP} ><i className='fas fa-arrow-up text-xs' /></div>
|
||||
{showPercent && (<div className='text-xs hidden lg:block'>{percent}</div>)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
// import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
@@ -19,46 +20,53 @@ const LatestPostsGroup = ({ latestPosts, siteInfo }) => {
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className=" mb-2 px-1 flex flex-nowrap justify-between">
|
||||
<div>
|
||||
<i className="mr-2 fas fas fa-history" />
|
||||
{locale.COMMON.LATEST_POSTS}
|
||||
</div>
|
||||
</div>
|
||||
{latestPosts.map(post => {
|
||||
const selected = currentPath === `${BLOG.SUB_PATH}/${post.slug}`
|
||||
const headerImage = post?.page_cover
|
||||
? `url("${post.page_cover}")`
|
||||
: `url("${siteInfo?.pageCover}")`
|
||||
|
||||
return (
|
||||
(<Link
|
||||
key={post.id}
|
||||
title={post.title}
|
||||
href={`${BLOG.SUB_PATH}/${post.slug}`}
|
||||
passHref
|
||||
className={'my-2 flex'}>
|
||||
|
||||
<div
|
||||
className="w-20 h-16 bg-cover bg-center bg-no-repeat"
|
||||
style={{ backgroundImage: headerImage }}
|
||||
/>
|
||||
<div
|
||||
className={
|
||||
(selected ? ' text-indigo-400 ' : 'dark:text-gray-400 ') +
|
||||
' text-sm overflow-x-hidden hover:text-indigo-600 px-2 duration-200 w-full rounded ' +
|
||||
' hover:text-indigo-400 cursor-pointer items-center flex'
|
||||
}
|
||||
>
|
||||
<div className=" mb-2 px-1 flex flex-nowrap justify-between">
|
||||
<div>
|
||||
<div className='text-line-2'>{post.title}</div>
|
||||
<div className="text-gray-500">{post.lastEditedTime}</div>
|
||||
<i className="mr-2 fas fas fa-history" />
|
||||
{locale.COMMON.LATEST_POSTS}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{latestPosts.map(post => {
|
||||
const selected = currentPath === `${BLOG.SUB_PATH}/${post.slug}`
|
||||
|
||||
</Link>)
|
||||
)
|
||||
})}
|
||||
</>
|
||||
const headerImage = post?.page_cover ? post.page_cover : siteInfo?.pageCover
|
||||
|
||||
return (
|
||||
(<Link
|
||||
key={post.id}
|
||||
title={post.title}
|
||||
href={`${BLOG.SUB_PATH}/${post.slug}`}
|
||||
passHref
|
||||
className={'my-2 flex'}>
|
||||
|
||||
<div className="w-20 h-16 overflow-hidden relative">
|
||||
{/* <Image
|
||||
src={headerImage}
|
||||
fill
|
||||
style={{ objectFit: 'cover' }}
|
||||
placeholder='blur'
|
||||
blurDataURL='/bg_image.jpg'
|
||||
quality={10}
|
||||
alt={post.title} /> */}
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={headerImage} className='object-cover w-full h-full'/>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
(selected ? ' text-indigo-400 ' : 'dark:text-gray-400 ') +
|
||||
' text-sm overflow-x-hidden hover:text-indigo-600 px-2 duration-200 w-full rounded ' +
|
||||
' hover:text-indigo-400 cursor-pointer items-center flex'
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div className='text-line-2'>{post.title}</div>
|
||||
<div className="text-gray-500">{post.lastEditedTime}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Link>)
|
||||
)
|
||||
})}
|
||||
</>
|
||||
}
|
||||
export default LatestPostsGroup
|
||||
|
||||
@@ -8,6 +8,7 @@ const MenuButtonGroupTop = (props) => {
|
||||
const { locale } = useGlobal()
|
||||
|
||||
let links = [
|
||||
{ icon: 'fa-solid fa-house', name: locale.NAV.INDEX, to: '/', show: CONFIG_HEXO.MENU_INDEX },
|
||||
{ icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG_HEXO.MENU_SEARCH },
|
||||
{ icon: 'fas fa-archive', name: locale.NAV.ARCHIVE, to: '/archive', show: CONFIG_HEXO.MENU_ARCHIVE }
|
||||
// { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_HEXO.MENU_CATEGORY },
|
||||
@@ -36,12 +37,12 @@ const MenuButtonGroupTop = (props) => {
|
||||
</div>
|
||||
|
||||
</Link>
|
||||
);
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
)
|
||||
}
|
||||
export default MenuButtonGroupTop
|
||||
|
||||
@@ -25,7 +25,7 @@ const Progress = ({ targetRef, showPercent = true }) => {
|
||||
useEffect(() => {
|
||||
document.addEventListener('scroll', scrollListener)
|
||||
return () => document.removeEventListener('scroll', scrollListener)
|
||||
}, [percent])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="h-4 w-full shadow-2xl bg-gray-400 ">
|
||||
|
||||
@@ -69,7 +69,7 @@ const SearchInput = props => {
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
className={
|
||||
'w-full text-sm pl-5 rounded-lg transition focus:shadow-lg dark:text-gray-300 font-light leading-10 text-black bg-gray-100 dark:bg-gray-500'
|
||||
'outline-none w-full text-sm pl-5 rounded-lg transition focus:shadow-lg dark:text-gray-300 font-light leading-10 text-black bg-gray-100 dark:bg-gray-500'
|
||||
}
|
||||
onKeyUp={handleKeyUp}
|
||||
onCompositionStart={lockSearchInput}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import throttle from 'lodash.throttle'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import CategoryGroup from './CategoryGroup'
|
||||
import Collapse from './Collapse'
|
||||
import Logo from './Logo'
|
||||
@@ -10,6 +9,7 @@ import TagGroups from './TagGroups'
|
||||
import MenuButtonGroupTop from './MenuButtonGroupTop'
|
||||
import MenuList from './MenuList'
|
||||
import { useRouter } from 'next/router'
|
||||
import throttle from 'lodash.throttle'
|
||||
|
||||
let windowTop = 0
|
||||
|
||||
@@ -19,17 +19,33 @@ let windowTop = 0
|
||||
* @returns
|
||||
*/
|
||||
const TopNav = props => {
|
||||
const searchDrawer = useRef()
|
||||
const { tags, currentTag, categories, currentCategory } = props
|
||||
const { locale } = useGlobal()
|
||||
const searchDrawer = useRef()
|
||||
const { isDarkMode } = useGlobal()
|
||||
const router = useRouter()
|
||||
|
||||
const scrollTrigger = throttle(() => {
|
||||
const [isOpen, changeShow] = useState(false)
|
||||
|
||||
const toggleMenuOpen = () => {
|
||||
changeShow(!isOpen)
|
||||
}
|
||||
|
||||
// 监听滚动
|
||||
useEffect(() => {
|
||||
scrollTrigger()
|
||||
window.addEventListener('scroll', scrollTrigger)
|
||||
return () => {
|
||||
window.removeEventListener('scroll', scrollTrigger)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const throttleMs = 200
|
||||
|
||||
const scrollTrigger = useCallback(throttle(() => {
|
||||
const scrollS = window.scrollY
|
||||
const nav = document.querySelector('#sticky-nav')
|
||||
const header = document.querySelector('#header')
|
||||
const showNav = scrollS <= windowTop || scrollS < 5 || (header && scrollS <= header.clientHeight)// 非首页无大图时影藏顶部 滚动条置顶时隐藏
|
||||
// 是否将导航栏透明
|
||||
const navTransparent = (scrollS < document.documentElement.clientHeight - 12 && router.route === '/') || scrollS < 300 // 透明导航条的条件
|
||||
|
||||
@@ -47,6 +63,7 @@ const TopNav = props => {
|
||||
nav && nav.classList.replace('transparent', 'dark:bg-hexo-black-gray')
|
||||
}
|
||||
|
||||
const showNav = scrollS <= windowTop || scrollS < 5 || (header && scrollS <= header.clientHeight * 2)// 非首页无大图时影藏顶部 滚动条置顶时隐藏
|
||||
if (!showNav) {
|
||||
nav && nav.classList.replace('top-0', '-top-20')
|
||||
windowTop = scrollS
|
||||
@@ -55,7 +72,8 @@ const TopNav = props => {
|
||||
windowTop = scrollS
|
||||
}
|
||||
navDarkMode()
|
||||
}, 200)
|
||||
}, throttleMs)
|
||||
)
|
||||
|
||||
const navDarkMode = () => {
|
||||
const nav = document.getElementById('sticky-nav')
|
||||
@@ -69,86 +87,70 @@ const TopNav = props => {
|
||||
}
|
||||
}
|
||||
|
||||
// 监听滚动
|
||||
useEffect(() => {
|
||||
scrollTrigger()
|
||||
|
||||
window.addEventListener('scroll', scrollTrigger)
|
||||
return () => {
|
||||
window.removeEventListener('scroll', scrollTrigger)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [isOpen, changeShow] = useState(false)
|
||||
|
||||
const toggleMenuOpen = () => {
|
||||
changeShow(!isOpen)
|
||||
}
|
||||
|
||||
const searchDrawerSlot = <>
|
||||
{ categories && (
|
||||
<section className='mt-8'>
|
||||
<div className='text-sm flex flex-nowrap justify-between font-light px-2'>
|
||||
<div className='text-gray-600 dark:text-gray-200'><i className='mr-2 fas fa-th-list' />{locale.COMMON.CATEGORY}</div>
|
||||
<Link
|
||||
href={'/category'}
|
||||
passHref
|
||||
className='mb-3 text-gray-400 hover:text-black dark:text-gray-400 dark:hover:text-white hover:underline cursor-pointer'>
|
||||
{categories && (
|
||||
<section className='mt-8'>
|
||||
<div className='text-sm flex flex-nowrap justify-between font-light px-2'>
|
||||
<div className='text-gray-600 dark:text-gray-200'><i className='mr-2 fas fa-th-list' />{locale.COMMON.CATEGORY}</div>
|
||||
<Link
|
||||
href={'/category'}
|
||||
passHref
|
||||
className='mb-3 text-gray-400 hover:text-black dark:text-gray-400 dark:hover:text-white hover:underline cursor-pointer'>
|
||||
|
||||
{locale.COMMON.MORE} <i className='fas fa-angle-double-right' />
|
||||
{locale.COMMON.MORE} <i className='fas fa-angle-double-right' />
|
||||
|
||||
</Link>
|
||||
</div>
|
||||
<CategoryGroup currentCategory={currentCategory} categories={categories} />
|
||||
</section>
|
||||
) }
|
||||
</Link>
|
||||
</div>
|
||||
<CategoryGroup currentCategory={currentCategory} categories={categories} />
|
||||
</section>
|
||||
)}
|
||||
|
||||
{ tags && (
|
||||
<section className='mt-4'>
|
||||
<div className='text-sm py-2 px-2 flex flex-nowrap justify-between font-light dark:text-gray-200'>
|
||||
<div className='text-gray-600 dark:text-gray-200'><i className='mr-2 fas fa-tag'/>{locale.COMMON.TAGS}</div>
|
||||
<Link
|
||||
href={'/tag'}
|
||||
passHref
|
||||
className='text-gray-400 hover:text-black dark:hover:text-white hover:underline cursor-pointer'>
|
||||
{tags && (
|
||||
<section className='mt-4'>
|
||||
<div className='text-sm py-2 px-2 flex flex-nowrap justify-between font-light dark:text-gray-200'>
|
||||
<div className='text-gray-600 dark:text-gray-200'><i className='mr-2 fas fa-tag' />{locale.COMMON.TAGS}</div>
|
||||
<Link
|
||||
href={'/tag'}
|
||||
passHref
|
||||
className='text-gray-400 hover:text-black dark:hover:text-white hover:underline cursor-pointer'>
|
||||
|
||||
{locale.COMMON.MORE} <i className='fas fa-angle-double-right' />
|
||||
{locale.COMMON.MORE} <i className='fas fa-angle-double-right' />
|
||||
|
||||
</Link>
|
||||
</div>
|
||||
<div className='p-2'>
|
||||
<TagGroups tags={tags} currentTag={currentTag} />
|
||||
</div>
|
||||
</section>
|
||||
) }
|
||||
</Link>
|
||||
</div>
|
||||
<div className='p-2'>
|
||||
<TagGroups tags={tags} currentTag={currentTag} />
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
|
||||
return (<div id='top-nav' className='z-40'>
|
||||
<SearchDrawer cRef={searchDrawer} slot={searchDrawerSlot}/>
|
||||
<SearchDrawer cRef={searchDrawer} slot={searchDrawerSlot} />
|
||||
|
||||
{/* 导航栏 */}
|
||||
<div id='sticky-nav' className={'top-0 shadow-none fixed bg-none dark:bg-hexo-black-gray dark:text-gray-200 text-black w-full z-20 transform transition-all duration-200 border-transparent dark:border-transparent'}>
|
||||
<div className='w-full flex justify-between items-center px-4 py-2'>
|
||||
<div className='flex'>
|
||||
<Logo {...props}/>
|
||||
</div>
|
||||
{/* 导航栏 */}
|
||||
<div id='sticky-nav' className={'top-0 duration-200 transition-all shadow-none fixed bg-none dark:bg-hexo-black-gray dark:text-gray-200 text-black w-full z-20 transform border-transparent dark:border-transparent'}>
|
||||
<div className='w-full flex justify-between items-center px-4 py-2'>
|
||||
<div className='flex'>
|
||||
<Logo {...props} />
|
||||
</div>
|
||||
|
||||
{/* 右侧功能 */}
|
||||
<div className='mr-1 justify-end items-center '>
|
||||
<div className='hidden lg:flex'> <MenuButtonGroupTop {...props}/></div>
|
||||
<div onClick={toggleMenuOpen} className='w-8 justify-center items-center h-8 cursor-pointer flex lg:hidden'>
|
||||
{ isOpen ? <i className='fas fa-times'/> : <i className='fas fa-bars'/> }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 右侧功能 */}
|
||||
<div className='mr-1 justify-end items-center '>
|
||||
<div className='hidden lg:flex'> <MenuButtonGroupTop {...props} /></div>
|
||||
<div onClick={toggleMenuOpen} className='w-8 justify-center items-center h-8 cursor-pointer flex lg:hidden'>
|
||||
{isOpen ? <i className='fas fa-times' /> : <i className='fas fa-bars' />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Collapse type='vertical' isOpen={isOpen} className='shadow-xl'>
|
||||
<div className='bg-white dark:bg-hexo-black-gray pt-1 py-2 px-5 lg:hidden '>
|
||||
<MenuList {...props}/>
|
||||
<Collapse type='vertical' isOpen={isOpen} className='shadow-xl'>
|
||||
<div className='bg-white dark:bg-hexo-black-gray pt-1 py-2 px-5 lg:hidden '>
|
||||
<MenuList {...props} />
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
</div>)
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default TopNav
|
||||
|
||||
@@ -3,8 +3,10 @@ const CONFIG_HEXO = {
|
||||
HOME_BANNER_GREETINGS: ['Hi,我是一个程序员', 'Hi,我是一个打工人', 'Hi,我是一个干饭人', '欢迎来到我的博客🎉'], // 首页大图标语文字
|
||||
|
||||
HOME_NAV_BUTTONS: true, // 首页是否显示分类大图标按钮
|
||||
HOME_NAV_BACKGROUND_IMG_FIXED: true, // 首页背景图滚动时是否固定,true 则滚动时图片不懂; false则随鼠标滚动
|
||||
|
||||
// 菜单配置
|
||||
MENU_INDEX: true, // 显示首页
|
||||
MENU_CATEGORY: true, // 显示分类
|
||||
MENU_TAG: true, // 显示标签
|
||||
MENU_ARCHIVE: true, // 显示归档
|
||||
@@ -14,6 +16,7 @@ const CONFIG_HEXO = {
|
||||
POST_LIST_COVER_DEFAULT: true, // 封面为空时用站点背景做默认封面
|
||||
POST_LIST_SUMMARY: true, // 文章摘要
|
||||
POST_LIST_PREVIEW: true, // 读取文章预览
|
||||
POST_LIST_IMG_CROSSOVER: true, // 博客列表图片左右交错
|
||||
|
||||
ARTICLE_ADJACENT: true, // 显示上一篇下一篇文章推荐
|
||||
ARTICLE_COPYRIGHT: true, // 显示文章版权声明
|
||||
|
||||
@@ -8,6 +8,25 @@ import * as medium from './medium'
|
||||
import * as nobelium from './nobelium'
|
||||
import * as matery from './matery'
|
||||
import * as example from './example'
|
||||
import * as simple from './simple'
|
||||
|
||||
export const ALL_THEME = ['hexo', 'matery', 'next', 'medium', 'fukasawa', 'nobelium', 'example']
|
||||
export { hexo, next, medium, fukasawa, nobelium, matery, example }
|
||||
export const ALL_THEME = [
|
||||
'hexo',
|
||||
'matery',
|
||||
'next',
|
||||
'medium',
|
||||
'fukasawa',
|
||||
'nobelium',
|
||||
'example',
|
||||
'simple'
|
||||
]
|
||||
export {
|
||||
hexo,
|
||||
next,
|
||||
medium,
|
||||
fukasawa,
|
||||
nobelium,
|
||||
matery,
|
||||
example,
|
||||
simple
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import CommonHead from '@/components/CommonHead'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import Footer from './components/Footer'
|
||||
import JumpToTopButton from './components/JumpToTopButton'
|
||||
import TopNav from './components/TopNav'
|
||||
import smoothscroll from 'smoothscroll-polyfill'
|
||||
import Live2D from '@/components/Live2D'
|
||||
import LoadingCover from './components/LoadingCover'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import BLOG from '@/blog.config'
|
||||
import FloatDarkModeButton from './components/FloatDarkModeButton'
|
||||
import throttle from 'lodash.throttle'
|
||||
|
||||
/**
|
||||
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
|
||||
@@ -22,28 +22,22 @@ const LayoutBase = props => {
|
||||
const [show, switchShow] = useState(false)
|
||||
const { onLoading } = useGlobal()
|
||||
|
||||
const scrollListener = () => {
|
||||
const targetRef = document.getElementById('wrapper')
|
||||
const clientHeight = targetRef?.clientHeight
|
||||
const throttleMs = 200
|
||||
const scrollListener = useCallback(throttle(() => {
|
||||
const scrollY = window.pageYOffset
|
||||
const fullHeight = clientHeight - window.outerHeight
|
||||
let per = parseFloat(((scrollY / fullHeight) * 100).toFixed(0))
|
||||
if (per > 100) per = 100
|
||||
const shouldShow = scrollY > 300 && per > 0
|
||||
|
||||
const shouldShow = scrollY > 300
|
||||
if (shouldShow !== show) {
|
||||
switchShow(shouldShow)
|
||||
}
|
||||
// changePercent(per)
|
||||
}
|
||||
}, throttleMs))
|
||||
|
||||
useEffect(() => {
|
||||
smoothscroll.polyfill()
|
||||
document.addEventListener('scroll', scrollListener)
|
||||
return () => document.removeEventListener('scroll', scrollListener)
|
||||
}, [show])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div id="outer-wrapper" className="min-h-screen flex flex-col justify-between bg-hexo-background-gray dark:bg-black w-full">
|
||||
<div id='theme-matery' className="min-h-screen flex flex-col justify-between bg-hexo-background-gray dark:bg-black w-full">
|
||||
|
||||
<CommonHead meta={meta} siteInfo={siteInfo} />
|
||||
|
||||
@@ -67,12 +61,8 @@ const LayoutBase = props => {
|
||||
</div>
|
||||
|
||||
{/* 右下角悬浮 */}
|
||||
<div className="bottom-12 right-2 fixed justify-end z-20">
|
||||
<div className={
|
||||
(show ? 'animate__animated ' : 'hidden') +
|
||||
' animate__fadeInUp justify-center duration-300 animate__faster flex flex-col items-center cursor-pointer '
|
||||
}
|
||||
>
|
||||
<div className={ (show ? ' opacity-100 fixed ' : ' hidden opacity-0 ') + ' transition-all duration-200 bottom-12 right-2 justify-end z-20' }>
|
||||
<div className= ' justify-center flex flex-col items-center cursor-pointer '>
|
||||
<JumpToTopButton />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { ArticleLock } from './components/ArticleLock'
|
||||
import HeaderArticle from './components/HeaderArticle'
|
||||
import LayoutBase from './LayoutBase'
|
||||
@@ -9,23 +9,25 @@ import ArticleCopyright from './components/ArticleCopyright'
|
||||
import { ArticleInfo } from './components/ArticleInfo'
|
||||
import Catalog from './components/Catalog'
|
||||
import JumpToCommentButton from './components/JumpToCommentButton'
|
||||
import throttle from 'lodash.throttle'
|
||||
|
||||
export const LayoutSlug = props => {
|
||||
const { post, lock, validPassword } = props
|
||||
|
||||
const [show, switchShow] = React.useState(false)
|
||||
const throttleMs = 200
|
||||
|
||||
const scrollListener = () => {
|
||||
const scrollListener = useCallback(throttle(() => {
|
||||
const scrollY = window.pageYOffset
|
||||
const shouldShow = scrollY > 220 && post?.toc?.length > 0
|
||||
if (shouldShow !== show) {
|
||||
switchShow(shouldShow)
|
||||
}
|
||||
}
|
||||
React.useEffect(() => {
|
||||
}, throttleMs))
|
||||
useEffect(() => {
|
||||
document.addEventListener('scroll', scrollListener)
|
||||
return () => document.removeEventListener('scroll', scrollListener)
|
||||
}, [show])
|
||||
}, [])
|
||||
|
||||
if (!post) {
|
||||
return <LayoutBase
|
||||
@@ -36,98 +38,92 @@ export const LayoutSlug = props => {
|
||||
></LayoutBase>
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutBase
|
||||
headerSlot={<HeaderArticle {...props} />}
|
||||
{...props}
|
||||
showCategory={false}
|
||||
showTag={false}
|
||||
>
|
||||
return (<LayoutBase
|
||||
headerSlot={<HeaderArticle {...props} />}
|
||||
{...props}
|
||||
showCategory={false}
|
||||
showTag={false}
|
||||
>
|
||||
|
||||
<div id='inner-wrapper'>
|
||||
<div className={'drop-shadow-xl w-full lg:max-w-3xl 2xl:max-w-4xl'}>
|
||||
<div className="-mt-32 rounded-md mx-3 lg:border lg:rounded-xl lg:py-4 bg-white dark:bg-hexo-black-gray dark:border-black">
|
||||
{lock && <ArticleLock validPassword={validPassword} />}
|
||||
<div id='inner-wrapper'>
|
||||
<div className={'w-full lg:max-w-3xl 2xl:max-w-4xl'}>
|
||||
<div className="-mt-32 rounded-md mx-3 lg:border lg:rounded-xl lg:py-4 bg-white dark:bg-hexo-black-gray dark:border-black">
|
||||
{lock && <ArticleLock validPassword={validPassword} />}
|
||||
|
||||
{!lock && <div id="container" className="overflow-x-auto md:w-full px-3 ">
|
||||
{post?.type === 'Post' && <>
|
||||
<div
|
||||
data-aos="fade-down"
|
||||
data-aos-duration="500"
|
||||
data-aos-easing="ease-in-out"
|
||||
data-aos-once="false"
|
||||
data-aos-anchor-placement="top-center"
|
||||
className='px-10'>
|
||||
<ArticleInfo post={post} />
|
||||
</div>
|
||||
<hr />
|
||||
</>}
|
||||
|
||||
<div className='lg:px-10 '>
|
||||
<article itemScope itemType="https://schema.org/Movie" className="subpixel-antialiased overflow-y-hidden" >
|
||||
{/* Notion文章主体 */}
|
||||
<section id='notion-article'
|
||||
data-aos-delay="200"
|
||||
data-aos="fade-down"
|
||||
data-aos-duration="500"
|
||||
data-aos-easing="ease-in-out"
|
||||
data-aos-once="false"
|
||||
data-aos-anchor-placement="top-bottom"
|
||||
className='justify-center mx-auto max-w-2xl lg:max-w-full'>
|
||||
{post && <NotionPage post={post} />}
|
||||
</section>
|
||||
|
||||
<section className="px-1 py-2 my-1 text-sm font-light overflow-auto text-gray-600 dark:text-gray-400">
|
||||
{/* 文章内嵌广告 */}
|
||||
<ins className="adsbygoogle"
|
||||
style={{ display: 'block', textAlign: 'center' }}
|
||||
data-adtest="on"
|
||||
data-ad-layout="in-article"
|
||||
data-ad-format="fluid"
|
||||
data-ad-client="ca-pub-2708419466378217"
|
||||
data-ad-slot="3806269138" />
|
||||
</section>
|
||||
|
||||
{post.type === 'Post' && <ArticleCopyright {...props} />}
|
||||
|
||||
</article>
|
||||
|
||||
<hr className='border-dashed' />
|
||||
|
||||
{/* 评论互动 */}
|
||||
<div className="duration-200 overflow-x-auto dark:bg-hexo-black-gray px-3">
|
||||
<Comment frontMatter={post} />
|
||||
</div>
|
||||
{!lock && <div id="container" className="overflow-x-auto md:w-full px-3 ">
|
||||
{post?.type && post?.type === 'Post' && <>
|
||||
<div
|
||||
data-aos="fade-down"
|
||||
data-aos-duration="100"
|
||||
data-aos-once="false"
|
||||
data-aos-anchor-placement="top-center"
|
||||
className='px-10'>
|
||||
<ArticleInfo post={post} />
|
||||
</div>
|
||||
<hr />
|
||||
</>}
|
||||
|
||||
</div>}
|
||||
</div>
|
||||
{post.type === 'Post' && <ArticleAdjacent {...props} />}
|
||||
<div className='lg:px-10 subpixel-antialiased'>
|
||||
<article itemScope >
|
||||
{/* Notion文章主体 */}
|
||||
<section id='notion-article' className='justify-center mx-auto max-w-2xl lg:max-w-full'>
|
||||
{post && <NotionPage post={post} />}
|
||||
</section>
|
||||
|
||||
{post?.toc?.length > 0 && <div id='toc-wrapper' style={{ zIndex: '-1' }} className='absolute top-0 w-full h-full xl:block hidden' >
|
||||
<div data-aos-delay="200"
|
||||
data-aos="fade-down"
|
||||
data-aos-duration="500"
|
||||
data-aos-easing="ease-in-out"
|
||||
data-aos-once="false"
|
||||
data-aos-anchor-placement="top-center"
|
||||
className='relative h-full'>
|
||||
<div className='float-right xl:-mr-72 xl:w-72 w-56 -mr-56 h-full mt-40'>
|
||||
<div className='sticky top-24'>
|
||||
<Catalog toc={post.toc} />
|
||||
</div>
|
||||
<section className="px-1 py-2 my-1 text-sm font-light overflow-auto text-gray-600 dark:text-gray-400">
|
||||
{/* 文章内嵌广告 */}
|
||||
<ins className="adsbygoogle"
|
||||
style={{ display: 'block', textAlign: 'center' }}
|
||||
data-adtest="on"
|
||||
data-ad-layout="in-article"
|
||||
data-ad-format="fluid"
|
||||
data-ad-client="ca-pub-2708419466378217"
|
||||
data-ad-slot="3806269138" />
|
||||
</section>
|
||||
|
||||
{/* 文章版权说明 */}
|
||||
{post.type === 'Post' && <ArticleCopyright {...props} />}
|
||||
|
||||
</article>
|
||||
|
||||
<hr className='border-dashed' />
|
||||
|
||||
{/* 评论互动 */}
|
||||
<div className="overflow-x-auto dark:bg-hexo-black-gray px-3">
|
||||
<Comment frontMatter={post} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
|
||||
<div className='fixed bottom-28 right-4'>
|
||||
<JumpToCommentButton />
|
||||
</div>
|
||||
{/* 文章推荐 */}
|
||||
{post.type === 'Post' && <ArticleAdjacent {...props} />}
|
||||
|
||||
{/* 文章目录 */}
|
||||
{post?.toc?.length > 0 && <div id='toc-wrapper' style={{ zIndex: '-1' }} className='absolute top-0 w-full h-full xl:block hidden lg:max-w-3xl 2xl:max-w-4xl' >
|
||||
<div data-aos-delay="200"
|
||||
data-aos="fade-down"
|
||||
data-aos-duration="200"
|
||||
data-aos-once="true"
|
||||
data-aos-anchor-placement="top-center"
|
||||
className='relative h-full'>
|
||||
<div className='float-right xl:-mr-72 xl:w-72 w-56 -mr-56 h-full mt-40'>
|
||||
<div className='sticky top-24'>
|
||||
<Catalog toc={post.toc} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
|
||||
</LayoutBase>
|
||||
<div className='fixed bottom-28 right-4'>
|
||||
<JumpToCommentButton />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export const ArticleLock = props => {
|
||||
<div className='text-center space-y-3'>
|
||||
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
|
||||
<div className='flex mx-4'>
|
||||
<input id="password" type='password' className='w-full text-sm pl-5 rounded-l transition focus:shadow-lg dark:text-gray-300 font-light leading-10 text-black bg-gray-100 dark:bg-gray-500'></input>
|
||||
<input id="password" type='password' className='outline-none w-full text-sm pl-5 rounded-l transition focus:shadow-lg dark:text-gray-300 font-light leading-10 text-black 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>
|
||||
|
||||
@@ -3,41 +3,45 @@ import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import TagItemMini from './TagItemMini'
|
||||
import CONFIG_MATERY from '../config_matery'
|
||||
// import Image from 'next/image'
|
||||
|
||||
const BlogPostCard = ({ post, showSummary, siteInfo }) => {
|
||||
const BlogPostCard = ({ index, post, showSummary, siteInfo }) => {
|
||||
const showPreview = CONFIG_MATERY.POST_LIST_PREVIEW && post.blockMap
|
||||
// matery 主题默认强制显示图片
|
||||
if (post && !post.page_cover) {
|
||||
post.page_cover = siteInfo?.pageCover
|
||||
}
|
||||
const showPageCover = CONFIG_MATERY.POST_LIST_COVER && !showPreview && post?.page_cover
|
||||
const delay = (index % 3) * 300
|
||||
return (
|
||||
<div
|
||||
data-aos="zoom-in"
|
||||
data-aos-duration="300"
|
||||
data-aos-easing="ease-in-out"
|
||||
data-aos-once="false"
|
||||
data-aos-duration="500"
|
||||
data-aos-delay={delay}
|
||||
data-aos-once="true"
|
||||
data-aos-anchor-placement="top-bottom"
|
||||
className="w-full mb-4 h-full overflow-auto drop-shadow-md border dark:border-black rounded-xl bg-white dark:bg-hexo-black-gray">
|
||||
className="w-full mb-4 overflow-auto shadow-md border dark:border-black rounded-xl bg-white dark:bg-hexo-black-gray">
|
||||
|
||||
{/* 固定高度 ,空白用图片拉升填充 */}
|
||||
<div key={post.id} className="flex flex-col h-96 justify-between">
|
||||
<div className="flex flex-col h-80 justify-between">
|
||||
|
||||
{/* 头部图片 填充卡片 */}
|
||||
{showPageCover && (
|
||||
<Link href={`${BLOG.SUB_PATH}/${post.slug}`} passHref legacyBehavior>
|
||||
<div className="flex flex-grow w-full relative duration-200 bg-black rounded-t-md cursor-pointer transform overflow-hidden">
|
||||
<div
|
||||
className="flex flex-grow w-full relative duration-200 bg-black rounded-t-md cursor-pointer transform overflow-hidden">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={post?.page_cover}
|
||||
alt={post.title}
|
||||
className="opacity-50 h-full w-full hover:scale-125 rounded-t-md transform object-cover duration-500"
|
||||
/>
|
||||
<span className='absolute bottom-0 left-0 text-white p-6 text-2xl replace' > {post.title}</span>
|
||||
<span className='absolute bottom-0 left-0 text-white p-6 text-2xl replace break-words w-full' > {post.title}</span>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* 文字描述 */}
|
||||
<div >
|
||||
{/* 描述 */}
|
||||
<div className="px-4 flex flex-col w-full text-gray-700 dark:text-gray-300">
|
||||
|
||||
@@ -21,9 +21,9 @@ const BlogPostListPage = ({ page = 1, posts = [], postCount, siteInfo }) => {
|
||||
<div id="container" className='w-full'>
|
||||
<div className='pt-6'></div>
|
||||
{/* 文章列表 */}
|
||||
<div className="px-4 pt-4 xl:columns-3 md:columns-2 pb-24" >
|
||||
<div className="px-4 pt-4 flex flex-wrap pb-24" >
|
||||
{posts.map(post => (
|
||||
<BlogPostCard key={post.id} post={post} siteInfo={siteInfo} />
|
||||
<div key={post.id} className='xl:w-1/3 md:w-1/2 w-full p-4'> <BlogPostCard index={posts.indexOf(post)} post={post} siteInfo={siteInfo} /></div>
|
||||
))}
|
||||
</div>
|
||||
{showPagination && <PaginationSimple page={page} totalPage={totalPage} />}
|
||||
|
||||
@@ -2,10 +2,10 @@ import BLOG from '@/blog.config'
|
||||
import BlogPostCard from './BlogPostCard'
|
||||
import BlogPostListEmpty from './BlogPostListEmpty'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import throttle from 'lodash.throttle'
|
||||
import React from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import CONFIG_MATERY from '../config_matery'
|
||||
import { getListByPage } from '@/lib/utils'
|
||||
import throttle from 'lodash.throttle'
|
||||
|
||||
/**
|
||||
* 博客列表滚动分页
|
||||
@@ -30,15 +30,16 @@ const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = CONFIG_MA
|
||||
updatePage(page + 1)
|
||||
}
|
||||
|
||||
// 监听滚动自动分页加载
|
||||
const scrollTrigger = React.useCallback(throttle(() => {
|
||||
const scrollS = window.scrollY + window.outerHeight
|
||||
const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
|
||||
if (scrollS > clientHeight + 100) {
|
||||
handleGetMore()
|
||||
}
|
||||
}, 500))
|
||||
|
||||
const throttleMs = 200
|
||||
const scrollTrigger = useCallback(throttle(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const scrollS = window.scrollY + window.outerHeight
|
||||
const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
|
||||
if (scrollS > clientHeight + 100) {
|
||||
handleGetMore()
|
||||
}
|
||||
})
|
||||
}, throttleMs))
|
||||
// 监听滚动
|
||||
React.useEffect(() => {
|
||||
window.addEventListener('scroll', scrollTrigger)
|
||||
@@ -56,9 +57,11 @@ const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = CONFIG_MA
|
||||
return <div id='container' ref={targetRef} className='w-full'>
|
||||
|
||||
{/* 文章列表 */}
|
||||
<div className='flex flex-wrap space-y-1 lg:space-y-4 px-2'>
|
||||
<div className="px-4 pt-4 flex flex-wrap pb-24" >
|
||||
{postsToShow.map(post => (
|
||||
<BlogPostCard key={post.id} post={post} showSummary={showSummary} siteInfo={siteInfo}/>
|
||||
<div key={post.id} className='xl:w-1/3 md:w-1/2 w-full p-4'>
|
||||
<BlogPostCard index={posts.indexOf(post)} post={post} siteInfo={siteInfo} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ const Catalog = ({ toc }) => {
|
||||
|
||||
// 同步选中目录事件
|
||||
const [activeSection, setActiveSection] = React.useState(null)
|
||||
const throttleMs = 100
|
||||
const throttleMs = 200
|
||||
const actionSectionScrollSpy = React.useCallback(throttle(() => {
|
||||
const sections = document.getElementsByClassName('notion-h')
|
||||
let prevBBox = null
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
// import Image from 'next/image'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import Typed from 'typed.js'
|
||||
import CONFIG_MATERY from '../config_matery'
|
||||
import throttle from 'lodash.throttle'
|
||||
|
||||
let wrapperTop = 0
|
||||
let windowTop = 0
|
||||
let autoScroll = false
|
||||
const enableAutoScroll = false // 是否开启自动吸附滚动
|
||||
const throttleMs = 200
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -28,13 +32,17 @@ const Header = props => {
|
||||
})
|
||||
)
|
||||
}
|
||||
window.addEventListener('scroll', scrollTrigger)
|
||||
if (enableAutoScroll) {
|
||||
scrollTrigger()
|
||||
window.addEventListener('scroll', scrollTrigger)
|
||||
}
|
||||
window.addEventListener('resize', updateHeaderHeight)
|
||||
return () => {
|
||||
window.removeEventListener('scroll', scrollTrigger)
|
||||
window.removeEventListener('resize', updateHeaderHeight)
|
||||
if (enableAutoScroll) {
|
||||
window.removeEventListener('scroll', scrollTrigger)
|
||||
} window.removeEventListener('resize', updateHeaderHeight)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const autoScrollEnd = () => {
|
||||
if (autoScroll) {
|
||||
@@ -44,10 +52,10 @@ const Header = props => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 吸附滚动,移动端关闭
|
||||
* @returns
|
||||
*/
|
||||
const scrollTrigger = () => {
|
||||
* 吸附滚动,移动端关闭
|
||||
* @returns
|
||||
*/
|
||||
const scrollTrigger = useCallback(throttle(() => {
|
||||
if (screen.width <= 768) {
|
||||
return
|
||||
}
|
||||
@@ -67,36 +75,42 @@ const Header = props => {
|
||||
setTimeout(autoScrollEnd, 500)
|
||||
}
|
||||
windowTop = scrollS
|
||||
}
|
||||
}, throttleMs))
|
||||
|
||||
function updateHeaderHeight () {
|
||||
setTimeout(() => {
|
||||
function updateHeaderHeight() {
|
||||
requestAnimationFrame(() => {
|
||||
const wrapperElement = document.getElementById('wrapper')
|
||||
wrapperTop = wrapperElement?.offsetTop
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<header
|
||||
id="header"
|
||||
className="duration-500 md:bg-fixed w-full bg-cover bg-center h-screen bg-black text-white relative z-10"
|
||||
style={{
|
||||
backgroundImage:
|
||||
`linear-gradient(rgba(0, 0, 0, 0.9), rgba(0,0,0,0.5), rgba(0,0,0,0.3), rgba(0,0,0,0.5), rgba(0, 0, 0, 0.9) ),url("${siteInfo?.pageCover}")`
|
||||
}}
|
||||
>
|
||||
<div className="absolute flex flex-col h-full items-center justify-center w-full ">
|
||||
<div className='text-4xl md:text-5xl text-white shadow-text'>{siteInfo?.title}</div>
|
||||
<div className='mt-2 h-12 items-center text-center shadow-text text-white text-lg'>
|
||||
<span id='typed'/>
|
||||
</div>
|
||||
<div onClick={() => { window.scrollTo({ top: wrapperTop, behavior: 'smooth' }) }}
|
||||
className="mt-12 border cursor-pointer w-40 text-center pt-4 pb-3 text-md text-white hover:bg-orange-600 duration-300 rounded-3xl">
|
||||
<i className='animate-bounce fas fa-angle-double-down'/> <span>开始阅读</span>
|
||||
</div>
|
||||
</div>
|
||||
<header
|
||||
id="header"
|
||||
className="md:bg-fixed w-full h-screen bg-black text-white relative"
|
||||
>
|
||||
<div className='w-full h-full absolute'>
|
||||
{/* <Image src={siteInfo.pageCover} fill
|
||||
style={{ objectFit: 'cover' }}
|
||||
className='opacity-70'
|
||||
placeholder='blur'
|
||||
blurDataURL='/bg_image.jpg' /> */}
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={siteInfo.pageCover} className='h-full w-full object-cover opacity-70 ' />
|
||||
</div>
|
||||
|
||||
</header>
|
||||
<div className="absolute flex flex-col h-full items-center justify-center w-full ">
|
||||
<div className='text-4xl md:text-5xl text-white shadow-text'>{siteInfo?.title}</div>
|
||||
<div className='mt-2 h-12 items-center text-center shadow-text text-white text-lg'>
|
||||
<span id='typed' />
|
||||
</div>
|
||||
<div onClick={() => { window.scrollTo({ top: wrapperTop, behavior: 'smooth' }) }}
|
||||
className="mt-12 border cursor-pointer w-40 text-center pt-4 pb-3 text-md text-white hover:bg-orange-600 duration-300 rounded-3xl">
|
||||
<i className='animate-bounce fas fa-angle-double-down' /> <span>开始阅读</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
export default function HeaderArticle({ post, siteInfo }) {
|
||||
const headerImage = post?.page_cover ? post?.page_cover : siteInfo?.pageCover
|
||||
const title = post?.title
|
||||
return (
|
||||
<div
|
||||
data-aos="fade-down"
|
||||
data-aos-duration="500"
|
||||
data-aos-easing="ease-in-out"
|
||||
data-aos-duration="300"
|
||||
data-aos-once="false"
|
||||
data-aos-anchor-placement="top-center"
|
||||
id='header' className="flex h-96 justify-center align-middle items-center w-full relative duration-200 bg-black">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
{/* <img
|
||||
src={headerImage}
|
||||
alt={title}
|
||||
className="opacity-50 dark:opacity-40 h-full w-full object-cover"
|
||||
/>
|
||||
/> */}
|
||||
<Image alt={title} src={headerImage} fill
|
||||
style={{ objectFit: 'cover' }}
|
||||
className='opacity-50'
|
||||
placeholder='blur'
|
||||
blurDataURL='/bg_image.jpg' />
|
||||
<span className='absolute text-white p-6 text-3xl'>{title}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -10,22 +10,24 @@ const Progress = ({ targetRef, showPercent = true }) => {
|
||||
const currentRef = targetRef?.current || targetRef
|
||||
const [percent, changePercent] = useState(0)
|
||||
const scrollListener = () => {
|
||||
const target = currentRef || (isBrowser() && document.getElementById('container'))
|
||||
if (target) {
|
||||
const clientHeight = target.clientHeight
|
||||
const scrollY = window.pageYOffset
|
||||
const fullHeight = clientHeight - window.outerHeight
|
||||
let per = parseFloat(((scrollY / fullHeight) * 100).toFixed(0))
|
||||
if (per > 100) per = 100
|
||||
if (per < 0) per = 0
|
||||
changePercent(per)
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
const target = currentRef || (isBrowser() && document.getElementById('container'))
|
||||
if (target) {
|
||||
const clientHeight = target.clientHeight
|
||||
const scrollY = window.pageYOffset
|
||||
const fullHeight = clientHeight - window.outerHeight
|
||||
let per = parseFloat(((scrollY / fullHeight) * 100).toFixed(0))
|
||||
if (per > 100) per = 100
|
||||
if (per < 0) per = 0
|
||||
changePercent(per)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('scroll', scrollListener)
|
||||
return () => document.removeEventListener('scroll', scrollListener)
|
||||
}, [percent])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="h-4 w-full shadow-2xl bg-gray-400 ">
|
||||
|
||||
@@ -69,7 +69,7 @@ const SearchInput = props => {
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
className={
|
||||
'w-full text-sm pl-5 rounded-lg transition focus:shadow-lg dark:text-gray-300 font-light leading-10 text-black bg-gray-100 dark:bg-gray-500'
|
||||
'outline-none w-full text-sm pl-5 rounded-lg transition focus:shadow-lg dark:text-gray-300 font-light leading-10 text-black bg-gray-100 dark:bg-gray-500'
|
||||
}
|
||||
onKeyUp={handleKeyUp}
|
||||
onCompositionStart={lockSearchInput}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import throttle from 'lodash.throttle'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import CategoryGroup from './CategoryGroup'
|
||||
import Logo from './Logo'
|
||||
import SearchDrawer from './SearchDrawer'
|
||||
@@ -9,6 +8,7 @@ import TagGroups from './TagGroups'
|
||||
import MenuButtonGroupTop from './MenuButtonGroupTop'
|
||||
import SideBarDrawer from '@/components/SideBarDrawer'
|
||||
import SideBar from './SideBar'
|
||||
import throttle from 'lodash.throttle'
|
||||
|
||||
let windowTop = 0
|
||||
|
||||
@@ -22,36 +22,38 @@ const TopNav = props => {
|
||||
const { locale } = useGlobal()
|
||||
const searchDrawer = useRef()
|
||||
const { isDarkMode } = useGlobal()
|
||||
const throttleMs = 200
|
||||
const scrollTrigger = useCallback(throttle(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const scrollS = window.scrollY
|
||||
const nav = document.querySelector('#sticky-nav')
|
||||
const header = document.querySelector('#header')
|
||||
const showNav = scrollS <= windowTop || scrollS < 5 || (header && scrollS <= header.clientHeight * 2)// 非首页无大图时影藏顶部 滚动条置顶时隐藏// 非首页无大图时影藏顶部 滚动条置顶时隐藏
|
||||
// 是否将导航栏透明
|
||||
const navTransparent = header && scrollS < 300 // 透明导航条的条件
|
||||
|
||||
const scrollTrigger = throttle(() => {
|
||||
const scrollS = window.scrollY
|
||||
const nav = document.querySelector('#sticky-nav')
|
||||
const header = document.querySelector('#header')
|
||||
const showNav = scrollS <= windowTop || scrollS < 5 // 非首页无大图时影藏顶部 滚动条置顶时隐藏
|
||||
// 是否将导航栏透明
|
||||
const navTransparent = header && scrollS < 300 // 透明导航条的条件
|
||||
if (navTransparent) {
|
||||
nav && nav.classList.replace('bg-indigo-700', 'bg-none')
|
||||
nav && nav.classList.replace('text-black', 'text-white')
|
||||
nav && nav.classList.replace('drop-shadow-xl', 'shadow-none')
|
||||
nav && nav.classList.replace('dark:bg-hexo-black-gray', 'transparent')
|
||||
} else {
|
||||
nav && nav.classList.replace('bg-none', 'bg-indigo-700')
|
||||
nav && nav.classList.replace('text-white', 'text-black')
|
||||
nav && nav.classList.replace('shadow-none', 'drop-shadow-xl')
|
||||
nav && nav.classList.replace('transparent', 'dark:bg-hexo-black-gray')
|
||||
}
|
||||
|
||||
if (navTransparent) {
|
||||
nav && nav.classList.replace('bg-indigo-700', 'bg-none')
|
||||
nav && nav.classList.replace('text-black', 'text-white')
|
||||
nav && nav.classList.replace('drop-shadow-xl', 'shadow-none')
|
||||
nav && nav.classList.replace('dark:bg-hexo-black-gray', 'transparent')
|
||||
} else {
|
||||
nav && nav.classList.replace('bg-none', 'bg-indigo-700')
|
||||
nav && nav.classList.replace('text-white', 'text-black')
|
||||
nav && nav.classList.replace('shadow-none', 'drop-shadow-xl')
|
||||
nav && nav.classList.replace('transparent', 'dark:bg-hexo-black-gray')
|
||||
}
|
||||
|
||||
if (!showNav) {
|
||||
nav && nav.classList.replace('top-0', '-top-20')
|
||||
windowTop = scrollS
|
||||
} else {
|
||||
nav && nav.classList.replace('-top-20', 'top-0')
|
||||
windowTop = scrollS
|
||||
}
|
||||
navDarkMode()
|
||||
}, 200)
|
||||
if (!showNav) {
|
||||
nav && nav.classList.replace('top-0', '-top-20')
|
||||
windowTop = scrollS
|
||||
} else {
|
||||
nav && nav.classList.replace('-top-20', 'top-0')
|
||||
windowTop = scrollS
|
||||
}
|
||||
navDarkMode()
|
||||
})
|
||||
}, throttleMs))
|
||||
|
||||
const navDarkMode = () => {
|
||||
const nav = document.getElementById('sticky-nav')
|
||||
|
||||
@@ -28,7 +28,7 @@ const LayoutBase = props => {
|
||||
|
||||
return (
|
||||
<ThemeGlobalMedium.Provider value={{ tocVisible, changeTocVisible }}>
|
||||
<div className='bg-white dark:bg-hexo-black-gray w-full h-full min-h-screen justify-center dark:text-gray-300'>
|
||||
<div id='theme-medium' className='bg-white dark:bg-hexo-black-gray w-full h-full min-h-screen justify-center dark:text-gray-300'>
|
||||
<CommonHead meta={meta} />
|
||||
|
||||
<main id='wrapper' className={(BLOG.LAYOUT_SIDEBAR_REVERSE ? 'flex-row-reverse' : '') + 'relative flex justify-between w-full h-full mx-auto'}>
|
||||
@@ -46,7 +46,6 @@ const LayoutBase = props => {
|
||||
<div
|
||||
data-aos="fade-up"
|
||||
data-aos-duration="300"
|
||||
data-aos-easing="ease-in-out"
|
||||
data-aos-once="false"
|
||||
data-aos-anchor-placement="top-center"
|
||||
className='fixed xl:right-80 right-2 mr-10 bottom-24 hidden lg:block z-20'>
|
||||
|
||||
@@ -26,7 +26,7 @@ export const ArticleLock = props => {
|
||||
<div className='text-center space-y-3'>
|
||||
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
|
||||
<div className='flex mx-4'>
|
||||
<input id="password" type='password' className='w-full text-sm pl-5 rounded-l transition focus:shadow-lg dark:text-gray-300 font-light leading-10 text-black bg-gray-100 dark:bg-gray-500'></input>
|
||||
<input id="password" type='password' className='outline-none w-full text-sm pl-5 rounded-l transition focus:shadow-lg dark:text-gray-300 font-light leading-10 text-black 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-green-500 hover:bg-green-400 text-white rounded-r duration-300" >
|
||||
<i className={'duration-200 cursor-pointer fas fa-key'} > {locale.COMMON.SUBMIT}</i>
|
||||
</div>
|
||||
|
||||
@@ -14,8 +14,7 @@ const BlogPostCard = ({ post, showSummary }) => {
|
||||
<div
|
||||
key={post.id}
|
||||
data-aos="fade-up"
|
||||
data-aos-duration="600"
|
||||
data-aos-easing="ease-in-out"
|
||||
data-aos-duration="300"
|
||||
data-aos-once="false"
|
||||
data-aos-anchor-placement="top-bottom"
|
||||
className="mb-6 max-w-7xl border-b dark:border-gray-800 "
|
||||
|
||||
@@ -26,7 +26,7 @@ const Catalog = ({ toc }) => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const throttleMs = 100
|
||||
const throttleMs = 200
|
||||
const actionSectionScrollSpy = React.useCallback(throttle(() => {
|
||||
const sections = document.getElementsByClassName('notion-h')
|
||||
let prevBBox = null
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRouter } from 'next/router'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import CONFIG_MEDIUM from '../config_medium'
|
||||
|
||||
function GroupMenu ({ customNav }) {
|
||||
function GroupMenu ({ customMenu, customNav }) {
|
||||
const { locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -39,13 +39,13 @@ function GroupMenu ({ customNav }) {
|
||||
{link.slot}
|
||||
|
||||
</Link>
|
||||
);
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default GroupMenu
|
||||
|
||||
@@ -25,7 +25,7 @@ const Progress = ({ targetRef, showPercent = true }) => {
|
||||
useEffect(() => {
|
||||
document.addEventListener('scroll', scrollListener)
|
||||
return () => document.removeEventListener('scroll', scrollListener)
|
||||
}, [percent])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="h-4 w-full shadow-2xl bg-hexo-light-gray dark:bg-black">
|
||||
|
||||
@@ -61,7 +61,7 @@ const SearchInput = ({ currentTag, currentSearch, cRef, className }) => {
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type='text'
|
||||
className={'w-full text-sm pl-2 transition focus:shadow-lg font-light leading-10 text-black bg-gray-100 dark:bg-gray-900 dark:text-white'}
|
||||
className={'outline-none w-full text-sm pl-2 transition focus:shadow-lg font-light leading-10 text-black bg-gray-100 dark:bg-gray-900 dark:text-white'}
|
||||
onKeyUp={handleKeyUp}
|
||||
onCompositionStart={lockSearchInput}
|
||||
onCompositionUpdate={lockSearchInput}
|
||||
|
||||
@@ -74,7 +74,7 @@ export default function TopNavBar(props) {
|
||||
{link.slot}
|
||||
|
||||
</Link>
|
||||
);
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
@@ -82,5 +82,5 @@ export default function TopNavBar(props) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import TopNav from './components/TopNav'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import smoothscroll from 'smoothscroll-polyfill'
|
||||
import CONFIG_NEXT from './config_next'
|
||||
import Live2D from '@/components/Live2D'
|
||||
import BLOG from '@/blog.config'
|
||||
@@ -45,8 +44,6 @@ const LayoutBase = (props) => {
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
smoothscroll.polyfill()
|
||||
|
||||
// facebook messenger 插件需要调整右下角悬浮按钮的高度
|
||||
const fb = document.getElementsByClassName('fb-customerchat')
|
||||
if (fb.length === 0) {
|
||||
@@ -59,7 +56,7 @@ const LayoutBase = (props) => {
|
||||
return () => document.removeEventListener('scroll', scrollListener)
|
||||
}, [show])
|
||||
|
||||
return (<>
|
||||
return (<div id='theme-next'>
|
||||
|
||||
<CommonHead meta={meta} />
|
||||
|
||||
@@ -90,7 +87,7 @@ const LayoutBase = (props) => {
|
||||
</div>
|
||||
|
||||
<Footer title={siteInfo?.title}/>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,32 +19,23 @@ export const LayoutSlug = (props) => {
|
||||
}} /></div>
|
||||
: null
|
||||
|
||||
if (!post) {
|
||||
return <LayoutBase
|
||||
{...props}
|
||||
rightAreaSlot={
|
||||
CONFIG_NEXT.RIGHT_LATEST_POSTS && <Card><LatestPostsGroup posts={latestPosts} /></Card>
|
||||
}
|
||||
/>
|
||||
}
|
||||
const rightAreaSlog = CONFIG_NEXT.RIGHT_LATEST_POSTS && <Card><LatestPostsGroup latestPosts={latestPosts} /></Card>
|
||||
|
||||
return (
|
||||
<LayoutBase
|
||||
{...props}
|
||||
floatSlot={floatSlot}
|
||||
rightAreaSlot={
|
||||
CONFIG_NEXT.RIGHT_LATEST_POSTS && <Card><LatestPostsGroup posts={latestPosts} /></Card>
|
||||
}
|
||||
rightAreaSlot={rightAreaSlog}
|
||||
>
|
||||
|
||||
{!lock && <ArticleDetail {...props} />}
|
||||
{post && !lock && <ArticleDetail {...props} />}
|
||||
|
||||
{lock && <ArticleLock validPassword={validPassword} />}
|
||||
{post && lock && <ArticleLock validPassword={validPassword} />}
|
||||
|
||||
{/* 悬浮目录按钮 */}
|
||||
<div className='block lg:hidden'>
|
||||
{post && <div className='block lg:hidden'>
|
||||
<TocDrawer post={post} cRef={drawerRight} targetRef={targetRef} />
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
</LayoutBase>
|
||||
)
|
||||
|
||||
@@ -28,9 +28,8 @@ export default function ArticleDetail(props) {
|
||||
return (
|
||||
<div id="container"
|
||||
data-aos="fade-down"
|
||||
data-aos-duration="600"
|
||||
data-aos-easing="ease-in-out"
|
||||
data-aos-once="false"
|
||||
data-aos-duration="300"
|
||||
data-aos-once="true"
|
||||
data-aos-anchor-placement="top-bottom"
|
||||
|
||||
className="shadow md:hover:shadow-2xl overflow-x-auto flex-grow mx-auto w-screen md:w-full ">
|
||||
|
||||
@@ -30,7 +30,7 @@ export const ArticleLock = props => {
|
||||
<div className="flex mx-4">
|
||||
<input
|
||||
id="password" type='password'
|
||||
className="w-full text-sm pl-5 transition focus:shadow-lg dark:text-gray-300 font-light leading-10 text-black bg-gray-100 dark:bg-gray-500"
|
||||
className="outline-none w-full text-sm pl-5 transition focus:shadow-lg dark:text-gray-300 font-light leading-10 text-black bg-gray-100 dark:bg-gray-500"
|
||||
></input>
|
||||
<div
|
||||
onClick={submitPassword}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
const Card = ({ children, headerSlot, className }) => {
|
||||
return <div
|
||||
data-aos="fade-down"
|
||||
data-aos-duration="600"
|
||||
data-aos-easing="ease-in-out"
|
||||
data-aos-once="false"
|
||||
data-aos-duration="300"
|
||||
data-aos-once="true"
|
||||
data-aos-anchor-placement="top-bottom"
|
||||
className={className}>
|
||||
<>{headerSlot}</>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import smoothscroll from 'smoothscroll-polyfill'
|
||||
import CONFIG_NEXT from '../config_next'
|
||||
|
||||
/**
|
||||
@@ -11,12 +10,14 @@ import CONFIG_NEXT from '../config_next'
|
||||
* @constructor
|
||||
*/
|
||||
const JumpToBottomButton = ({ showPercent = false }) => {
|
||||
if (!CONFIG_NEXT.WIDGET_TO_BOTTOM) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const [show, switchShow] = useState(false)
|
||||
const [percent, changePercent] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('scroll', scrollListener)
|
||||
return () => document.removeEventListener('scroll', scrollListener)
|
||||
}, [show])
|
||||
|
||||
const scrollListener = () => {
|
||||
const targetRef = document.getElementById('wrapper')
|
||||
const clientHeight = targetRef?.clientHeight
|
||||
@@ -36,12 +37,9 @@ const JumpToBottomButton = ({ showPercent = false }) => {
|
||||
window.scrollTo({ top: targetRef.clientHeight, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
smoothscroll.polyfill()
|
||||
|
||||
document.addEventListener('scroll', scrollListener)
|
||||
return () => document.removeEventListener('scroll', scrollListener)
|
||||
}, [show])
|
||||
if (!CONFIG_NEXT.WIDGET_TO_BOTTOM) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (<div className='flex space-x-1 transform hover:scale-105 duration-200 py-2 px-3' onClick={scrollToBottom} >
|
||||
<div className='dark:text-gray-200' >
|
||||
|
||||
@@ -11,10 +11,10 @@ import CONFIG_NEXT from '../config_next'
|
||||
* @constructor
|
||||
*/
|
||||
const JumpToTopButton = ({ showPercent = true, percent }) => {
|
||||
const { locale } = useGlobal()
|
||||
if (!CONFIG_NEXT.WIDGET_TO_TOP) {
|
||||
return <></>
|
||||
}
|
||||
const { locale } = useGlobal()
|
||||
return (<div className='flex space-x-1 items-center transform hover:scale-105 duration-200 py-2 px-3' onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })} >
|
||||
<div className='dark:text-gray-200' title={locale.POST.TOP} >
|
||||
<i className='fa-arrow-up fas' />
|
||||
|
||||
@@ -18,8 +18,7 @@ const PaginationNumber = ({ page, totalPage }) => {
|
||||
return (
|
||||
<div
|
||||
data-aos="fade-down"
|
||||
data-aos-duration="600"
|
||||
data-aos-easing="ease-in-out"
|
||||
data-aos-duration="300"
|
||||
data-aos-once="false"
|
||||
data-aos-anchor-placement="top-bottom"
|
||||
className="my-5 flex justify-center items-end font-medium text-black hover:shadow-xl duration-500 bg-white dark:bg-hexo-black-gray dark:text-gray-300 py-3 shadow space-x-2">
|
||||
@@ -62,7 +61,7 @@ const PaginationNumber = ({ page, totalPage }) => {
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function getPageElement(pagePrefix, page, currentPage) {
|
||||
@@ -81,7 +80,7 @@ function getPageElement(pagePrefix, page, currentPage) {
|
||||
{page}
|
||||
|
||||
</Link>)
|
||||
);
|
||||
)
|
||||
}
|
||||
function generatePages(pagePrefix, page, currentPage, totalPage) {
|
||||
const pages = []
|
||||
|
||||
@@ -17,8 +17,7 @@ const PaginationSimple = ({ page, showNext }) => {
|
||||
return (
|
||||
<div
|
||||
data-aos="fade-down"
|
||||
data-aos-duration="600"
|
||||
data-aos-easing="ease-in-out"
|
||||
data-aos-duration="300"
|
||||
data-aos-once="false"
|
||||
data-aos-anchor-placement="top-bottom"
|
||||
className="my-10 flex justify-between font-medium text-black dark:text-gray-100 space-x-2">
|
||||
@@ -58,7 +57,7 @@ const PaginationSimple = ({ page, showNext }) => {
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default PaginationSimple
|
||||
|
||||
@@ -25,7 +25,7 @@ const Progress = ({ targetRef, showPercent = true }) => {
|
||||
useEffect(() => {
|
||||
document.addEventListener('scroll', scrollListener)
|
||||
return () => document.removeEventListener('scroll', scrollListener)
|
||||
}, [percent])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="h-4 w-full shadow-2xl bg-gray-400 font-sans">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user