This commit is contained in:
Vixcity
2023-03-22 17:04:35 +08:00
143 changed files with 2286 additions and 786 deletions

View File

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

View File

@@ -81,9 +81,11 @@
<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/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 <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>
</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> </tr>

View File

@@ -8,6 +8,10 @@ import React from 'react'
const Collapse = props => { const Collapse = props => {
const collapseRef = React.useRef(null) const collapseRef = React.useRef(null)
const type = props.type || 'vertical' const type = props.type || 'vertical'
/**
* 折叠
* @param {*} element
*/
const collapseSection = element => { const collapseSection = element => {
const sectionHeight = element.scrollHeight const sectionHeight = element.scrollHeight
const sectionWidth = element.scrollWidth const sectionWidth = element.scrollWidth
@@ -54,17 +58,20 @@ const Collapse = props => {
clearTimeout(clearTime) clearTimeout(clearTime)
} }
const updateHeight = () => {
collapseRef.current.style.height = 'auto'
}
React.useEffect(() => { React.useEffect(() => {
const element = collapseRef.current
if (props.isOpen) { if (props.isOpen) {
expandSection(element) expandSection(collapseRef.current)
} else { } else {
collapseSection(element) collapseSection(collapseRef.current)
} }
}, [props.isOpen]) }, [props.isOpen])
return ( 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} {props.children}
</div> </div>
) )

View File

@@ -2,10 +2,31 @@ import { useGlobal } from '@/lib/global'
import { ReactCusdis } from 'react-cusdis' import { ReactCusdis } from 'react-cusdis'
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useEffect } from 'react'
const CusdisComponent = ({ frontMatter }) => { const CusdisComponent = ({ frontMatter }) => {
const { locale } = useGlobal() const { locale } = useGlobal()
const router = useRouter() 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 return <ReactCusdis
lang={locale.LOCALE.toLowerCase()} lang={locale.LOCALE.toLowerCase()}

View File

@@ -13,7 +13,7 @@ const DarkModeButton = (props) => {
htmlElement.classList?.add(newStatus ? 'dark' : 'light') 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'}`} <i id='darkModeButton' className={`hover:scale-125 cursor-pointer transform duration-200 fas ${isDarkMode ? 'fa-sun' : 'fa-moon'}`}
onClick={handleChangeDarkMode} /> onClick={handleChangeDarkMode} />
</div> </div>

View File

@@ -69,7 +69,7 @@ export function DebugPanel() {
{/* 调试侧拉抽屉 */} {/* 调试侧拉抽屉 */}
<div <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 justify-between space-x-1 my-5">
<div className='flex'> <div className='flex'>

View File

@@ -1,16 +1,23 @@
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
import { isBrowser, loadExternalResource } from '@/lib/utils' import { loadExternalResource } from '@/lib/utils'
import { useEffect } from 'react'
/** /**
* 自定义引入外部JS 和 CSS * 自定义引入外部JS 和 CSS
* @returns * @returns
*/ */
const ExternalScript = () => { const ExternalScript = () => {
if (isBrowser()) { useEffect(() => {
// 静态导入本地自定义样式 // 静态导入本地自定义样式
loadExternalResource(BLOG.FONT_AWESOME, 'css') loadExternalResource(BLOG.FONT_AWESOME, 'css')
loadExternalResource('/css/custom.css', 'css') loadExternalResource('/css/custom.css', 'css')
loadExternalResource('/js/custom.js', 'js') loadExternalResource('/js/custom.js', 'js')
// 自动添加图片阴影
if (BLOG.IMG_SHADOW) {
loadExternalResource('/css/img-shadow.css', 'css')
}
if (BLOG.CUSTOM_EXTERNAL_JS && BLOG.CUSTOM_EXTERNAL_JS.length > 0) { if (BLOG.CUSTOM_EXTERNAL_JS && BLOG.CUSTOM_EXTERNAL_JS.length > 0) {
for (const url of BLOG.CUSTOM_EXTERNAL_JS) { for (const url of BLOG.CUSTOM_EXTERNAL_JS) {
loadExternalResource(url, 'js') loadExternalResource(url, 'js')
@@ -25,7 +32,7 @@ const ExternalScript = () => {
BLOG.FONT_URL?.forEach(e => { BLOG.FONT_URL?.forEach(e => {
loadExternalResource(e, 'css') loadExternalResource(e, 'css')
}) })
} }, [])
return null return null
} }

View File

@@ -4,6 +4,7 @@ import mediumZoom from '@fisch0920/medium-zoom'
import React from 'react' import React from 'react'
import { isBrowser } from '@/lib/utils' import { isBrowser } from '@/lib/utils'
import { Code } from 'react-notion-x/build/third-party/code' import { Code } from 'react-notion-x/build/third-party/code'
import TweetEmbed from 'react-tweet-embed'
import 'katex/dist/katex.min.css' import 'katex/dist/katex.min.css'
import { mapImgUrl } from '@/lib/notion/mapImage' 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 } () => 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 NotionPage = ({ post, className }) => {
const zoom = isBrowser() && mediumZoom({ const zoom = isBrowser() && mediumZoom({
container: '.notion-viewport', container: '.notion-viewport',
@@ -79,7 +84,7 @@ const NotionPage = ({ post, className }) => {
return <>{post?.summary || ''}</> 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 <NotionRenderer
recordMap={post.blockMap} recordMap={post.blockMap}
mapPageUrl={mapPageUrl} mapPageUrl={mapPageUrl}
@@ -89,7 +94,8 @@ const NotionPage = ({ post, className }) => {
Collection, Collection,
Equation, Equation,
Modal, Modal,
Pdf Pdf,
Tweet
}} /> }} />
<PrismMac /> <PrismMac />

View File

@@ -7,7 +7,7 @@ export const StarrySky = () => {
}, []) }, [])
return ( return (
<div className="relative"> <div className="relative">
<canvas id="starry-sky-vixcity" className="fixed"></canvas> <canvas id="starry-sky-vixcity" className="fixed pointer-events-none"></canvas>
</div> </div>
) )
} }

View File

@@ -51,8 +51,7 @@ const Tabs = ({ className, children }) => {
{children.map((item, index) => { {children.map((item, index) => {
return <section key={index} return <section key={index}
data-aos="fade-up" data-aos="fade-up"
data-aos-duration="600" data-aos-duration="300"
data-aos-easing="ease-in-out"
data-aos-once="true" data-aos-once="true"
data-aos-anchor-placement="top-bottom"> data-aos-anchor-placement="top-bottom">
{currentTab === index && item} {currentTab === index && item}

View File

@@ -85,18 +85,39 @@ function getCustomNav({ allPages }) {
const customNav = [] const customNav = []
if (allPages && allPages.length > 0) { if (allPages && allPages.length > 0) {
allPages.forEach(p => { allPages.forEach(p => {
if (p?.status === 'Published' && p?.type === 'Page') { if (p?.slug?.indexOf('http') === 0) {
if (p?.slug?.indexOf('http') === 0) { customNav.push({ icon: p.icon || null, name: p.title, to: p.slug, show: true })
customNav.push({ icon: p.icon || null, name: p.title, to: p.slug, show: true }) } else {
} else { customNav.push({ icon: p.icon || null, name: p.title, to: '/' + p.slug, show: true })
customNav.push({ icon: p.icon || null, name: p.title, to: '/' + p.slug, show: true })
}
} }
}) })
} }
return customNav 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 * @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 categoryOptions = getAllCategories({ allPages, categoryOptions: getCategoryOptions(schema) })
const tagOptions = getAllTags({ allPages, tagOptions: getTagOptions(schema) }) const tagOptions = getAllTags({ allPages, tagOptions: getTagOptions(schema) })
const siteInfo = getBlogInfo({ collection, block }) const siteInfo = getBlogInfo({ collection, block })
const customNav = getCustomNav({ allPages: collectionData.filter(post => post.type === 'Page' && post.status === 'Published') }) const customNav = getCustomNav({ allPages: collectionData.filter(post => post.type === 'Page' && post.status === 'Published') })
// 新的菜单
const customMenu = getCustomMenu({ collectionData })
const latestPosts = getLatestPosts({ allPages, from, latestPostCount: 5 }) const latestPosts = getLatestPosts({ allPages, from, latestPostCount: 5 })
return { return {
@@ -236,6 +259,7 @@ async function getPageRecordMapByNotionAPI({ pageId, from }) {
categoryOptions, categoryOptions,
rawMetadata, rawMetadata,
customNav, customNav,
customMenu,
postCount, postCount,
pageIds, pageIds,
latestPosts latestPosts

View File

@@ -76,10 +76,14 @@ export default async function getPageProperties(id, block, schema, authToken, ta
// 映射值用户个性化type和status字段的下拉框选项在此映射回代码的英文标识 // 映射值用户个性化type和status字段的下拉框选项在此映射回代码的英文标识
mapProperties(properties) mapProperties(properties)
if (properties.type === 'Post') { if (properties.type === BLOG.NOTION_PROPERTY_NAME.type_post) {
properties.slug = BLOG.POST_URL_PREFIX ? (BLOG.POST_URL_PREFIX + '/' + (properties.slug ?? properties.id)) : (properties.slug ?? properties.id) properties.slug = (BLOG.POST_URL_PREFIX) ? generateCustomizeUrl(properties) : (properties.slug ?? properties.id)
} else { } else if (properties.type === BLOG.NOTION_PROPERTY_NAME.type_page) {
properties.slug = (properties.slug ?? properties.id) 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' 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)}`
}

View File

@@ -1,5 +1,7 @@
import BLOG from '@/blog.config'
/** /**
* Notion图片映射处理有emjji的图标 * Notion图片映射处理有emoji的图标
* @param {*} img * @param {*} img
* @param {*} value * @param {*} value
* @returns * @returns
@@ -18,7 +20,7 @@ const mapImgUrl = (img, block, type = 'block') => {
} }
// notion永久图床地址 // 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 ret = 'https://www.notion.so/image/' + encodeURIComponent(img) + '?table=' + type + '&id=' + block.id
} }

25
lib/robots.txt.js Normal file
View 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等其他平台这行代码会成功执行
}
}

View File

@@ -5,6 +5,11 @@ import ReactDOMServer from 'react-dom/server'
import { getPostBlocks } from './notion' import { getPostBlocks } from './notion'
import NotionPage from '@/components/NotionPage' import NotionPage from '@/components/NotionPage'
/**
* 生成RSS内容
* @param {*} post
* @returns
*/
const createFeedContent = async post => { const createFeedContent = async post => {
// 加密的文章内容只返回摘要 // 加密的文章内容只返回摘要
if (post.password && post.password !== '') { if (post.password && post.password !== '') {
@@ -15,7 +20,7 @@ const createFeedContent = async post => {
post.blockMap = blockMap post.blockMap = blockMap
const content = ReactDOMServer.renderToString(<NotionPage post={post} />) const content = ReactDOMServer.renderToString(<NotionPage post={post} />)
const regexExp = 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, '') return content.replace(regexExp, '')
} }
} }
@@ -38,7 +43,6 @@ export async function generateRss(posts) {
for (const post of posts) { for (const post of posts) {
feed.addItem({ feed.addItem({
title: post.title, title: post.title,
guid: `${post.id}`,
link: `${BLOG.LINK}/${post.slug}`, link: `${BLOG.LINK}/${post.slug}`,
description: post.summary, description: post.summary,
content: await createFeedContent(post), content: await createFeedContent(post),
@@ -46,8 +50,14 @@ export async function generateRss(posts) {
}) })
} }
fs.mkdirSync('./public/rss', { recursive: true }) try {
fs.writeFileSync('./public/rss/feed.xml', feed.rss2()) fs.mkdirSync('./public/rss', { recursive: true })
fs.writeFileSync('./public/rss/atom.xml', feed.atom1()) fs.writeFileSync('./public/rss/feed.xml', feed.rss2())
fs.writeFileSync('./public/rss/feed.json', feed.json1()) 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
View 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>
`
}

View File

@@ -28,7 +28,8 @@ export const initDarkMode = (isDarkMode, updateDarkMode) => {
*/ */
export const initTheme = (theme, changeTheme) => { export const initTheme = (theme, changeTheme) => {
if (isBrowser()) { 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 let currentTheme = theme
if (queryTheme !== theme && ALL_THEME.indexOf(queryTheme) > -1) { if (queryTheme !== theme && ALL_THEME.indexOf(queryTheme) > -1) {
currentTheme = queryTheme currentTheme = queryTheme

View File

@@ -124,3 +124,32 @@ export const getListByPage = function (list, pageIndex, pageSize) {
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
}

View File

@@ -11,7 +11,9 @@ module.exports = withBundleAnalyzer({
'gravatar.com', 'gravatar.com',
'www.notion.so', 'www.notion.so',
'avatars.githubusercontent.com', 'avatars.githubusercontent.com',
'images.unsplash.com' 'images.unsplash.com',
'source.unsplash.com',
'p1.qhimg.com'
] ]
}, },
// 默认将feed重定向至 /public/rss/feed.xml // 默认将feed重定向至 /public/rss/feed.xml

View File

@@ -1,6 +1,6 @@
{ {
"name": "notion-next", "name": "notion-next",
"version": "3.11.0", "version": "3.12.4",
"homepage": "https://github.com/tangly1024/NotionNext.git", "homepage": "https://github.com/tangly1024/NotionNext.git",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
@@ -50,8 +50,9 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-facebook": "^8.1.4", "react-facebook": "^8.1.4",
"react-messenger-customer-chat": "^0.8.0", "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-share": "^4.4.0",
"react-tweet-embed": "~2.0.0",
"smoothscroll-polyfill": "^0.4.4", "smoothscroll-polyfill": "^0.4.4",
"twikoo": "1.6.9", "twikoo": "1.6.9",
"typed.js": "^2.0.12", "typed.js": "^2.0.12",

View File

@@ -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 } const meta = { title: `${props?.siteInfo?.title || BLOG.TITLE} | loading`, image: siteInfo?.pageCover || BLOG.HOME_BANNER_IMAGE }
return <ThemeComponents.LayoutSlug {...props} showArticleInfo={true} meta={meta} /> return <ThemeComponents.LayoutSlug {...props} showArticleInfo={true} meta={meta} />
} }
@@ -100,7 +100,7 @@ export async function getStaticPaths() {
const from = 'slug-paths' const from = 'slug-paths'
const { allPages } = await getGlobalNotionData({ from }) const { allPages } = await getGlobalNotionData({ from })
return { return {
paths: allPages.map(row => ({ params: { slug: [row.slug] } })), paths: allPages?.map(row => ({ params: { slug: [row.slug] } })),
fallback: true fallback: true
} }
} }

View File

@@ -1,5 +1,5 @@
import BLOG from 'blog.config' import BLOG from 'blog.config'
import React from 'react' import React, { useEffect } from 'react'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import 'animate.css' import 'animate.css'
@@ -20,10 +20,11 @@ import { Sakura } from '@/components/Sakura'
import { StarrySky } from '@/components/StarrySky' import { StarrySky } from '@/components/StarrySky'
import MusicPlayer from '@/components/MusicPlayer' import MusicPlayer from '@/components/MusicPlayer'
import ExternalScript from '@/components/ExternalScript' import ExternalScript from '@/components/ExternalScript'
import { isBrowser } from '@/lib/utils' import smoothscroll from 'smoothscroll-polyfill'
import AOS from 'aos' import AOS from 'aos'
import 'aos/dist/aos.css' // You can also use <link> for styles 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 Ackee = dynamic(() => import('@/components/Ackee'), { ssr: false })
const Gtag = dynamic(() => import('@/components/Gtag'), { ssr: false }) const Gtag = dynamic(() => import('@/components/Gtag'), { ssr: false })
@@ -55,14 +56,17 @@ const MyApp = ({ Component, pageProps }) => {
<ExternalScript/> <ExternalScript/>
</> </>
if (isBrowser()) { useEffect(() => {
AOS.init() AOS.init()
} if (isMobile()) {
smoothscroll.polyfill()
}
}, [])
return ( return (
<GlobalContextProvider> <GlobalContextProvider>
{externalPlugins}
<Component {...pageProps} /> <Component {...pageProps} />
{externalPlugins}
</GlobalContextProvider> </GlobalContextProvider>
) )
} }

View File

@@ -4,6 +4,7 @@ import { getGlobalNotionData } from '@/lib/notion/getNotionData'
import * as ThemeMap from '@/themes' import * as ThemeMap from '@/themes'
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import { generateRss } from '@/lib/rss' import { generateRss } from '@/lib/rss'
import { generateRobotsTxt } from '@/lib/robots.txt'
const Index = props => { const Index = props => {
const { theme } = useGlobal() const { theme } = useGlobal()
const ThemeComponents = ThemeMap[theme] const ThemeComponents = ThemeMap[theme]
@@ -17,7 +18,6 @@ export async function getStaticProps() {
const { siteInfo } = props const { siteInfo } = props
props.posts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published') props.posts = props.allPages.filter(page => page.type === 'Post' && page.status === 'Published')
delete props.allPages
const meta = { const meta = {
title: `${siteInfo?.title} | ${siteInfo?.description}`, title: `${siteInfo?.title} | ${siteInfo?.description}`,
description: siteInfo?.description, description: siteInfo?.description,
@@ -43,8 +43,14 @@ export async function getStaticProps() {
} }
} }
// 异步生成Feed订阅 // 生成robotTxt
generateRss(props?.latestPosts || []) generateRobotsTxt()
// 生成Feed订阅
if (JSON.parse(BLOG.ENABLE_RSS)) {
generateRss(props?.latestPosts || [])
}
delete props.allPages
return { return {
props: { props: {

View File

@@ -122,7 +122,8 @@ async function filterByMemCache(allPosts, keyword) {
const articleInfo = post.title + post.summary + tagContent + categoryContent const articleInfo = post.title + post.summary + tagContent + categoryContent
let hit = articleInfo.toLowerCase().indexOf(keyword) > -1 let hit = articleInfo.toLowerCase().indexOf(keyword) > -1
let indexContent = [post.summary] let indexContent = [post.summary]
if (page && page.block) { // 防止搜到加密文章的内容
if (page && page.block && !post.password) {
const contentIds = Object.keys(page.block) const contentIds = Object.keys(page.block)
contentIds.forEach(id => { contentIds.forEach(id => {
const properties = page?.block[id]?.value?.properties const properties = page?.block[id]?.value?.properties

View File

@@ -38,7 +38,7 @@ export const getServerSideProps = async (ctx) => {
priority: '0.7' priority: '0.7'
} }
] ]
const postFields = allPages?.map(post => { const postFields = allPages?.filter(p => p.status === BLOG.NOTION_PROPERTY_NAME.status_publish)?.map(post => {
return { return {
loc: `${BLOG.LINK}/${post.slug}`, loc: `${BLOG.LINK}/${post.slug}`,
lastmod: new Date(post?.date?.start_date || post?.createdTime).toISOString().split('T')[0], 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) const fields = defaultFields.concat(postFields)
// 缓存 // 缓存
// ctx.res.setHeader( ctx.res.setHeader(
// 'Cache-Control', 'Cache-Control',
// 'public, s-maxage=10, stale-while-revalidate=59' 'public, max-age=3600, stale-while-revalidate=59'
// ) )
return getServerSideSitemap(ctx, fields) return getServerSideSitemap(ctx, fields)
} }

11
public/avatar.svg Normal file
View 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 16 KiB

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

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

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

View File

@@ -167,6 +167,13 @@ nav {
-webkit-box-orient: vertical; -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 { .vcontent .wl-emoji {
display: inline-block; display: inline-block;
vertical-align: baseline; vertical-align: baseline;
@@ -204,7 +211,7 @@ nav {
} }
.nobelium .notion-code{ .nobelium .notion-code{
@apply max-w-2xl; /* @apply max-w-2xl; */
} }
.next #announcement-content *{ .next #announcement-content *{

View File

@@ -180,7 +180,7 @@
} }
.notion-simple-table { .notion-simple-table {
@apply whitespace-nowrap overflow-x-scroll block @apply whitespace-nowrap overflow-x-auto block
} }
.notion-app { .notion-app {
@@ -207,7 +207,6 @@
.medium-zoom-image { .medium-zoom-image {
border-radius: 0; border-radius: 0;
@apply bg-gray-100 dark:bg-black
} }
.medium-zoom-image--opened { .medium-zoom-image--opened {
@@ -939,7 +938,7 @@ code[class*='language-'] {
align-self: flex-start; align-self: flex-start;
width: 24px; width: 24px;
height: 24px; height: 24px;
font-size: 1.3em; font-size: 1em;
line-height: 1em; line-height: 1em;
} }
@@ -1118,10 +1117,11 @@ code[class*='language-'] {
.notion-table-of-contents { .notion-table-of-contents {
width: 100%; width: 100%;
margin: 4px 0; margin: 4px 0;
@apply bg-gray-50 dark:bg-black p-2
} }
.notion-table-of-contents-item { .notion-table-of-contents-item {
color: inherit; /* color: inherit; */
text-decoration: none; text-decoration: none;
user-select: none; user-select: none;
transition: background 20ms ease-in 0s; transition: background 20ms ease-in 0s;
@@ -1137,6 +1137,8 @@ code[class*='language-'] {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@apply text-blue-600 dark:text-blue-200
} }
.notion-table-of-contents-item:hover { .notion-table-of-contents-item:hover {
@@ -2009,3 +2011,10 @@ code.language-mermaid {
.notion-equation-inline .katex-display { .notion-equation-inline .katex-display {
margin: 0 0 !important; margin: 0 0 !important;
} }
.two-line-clamp {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}

View File

@@ -21,9 +21,10 @@ module.exports = {
} }
}, },
maxWidth: { maxWidth: {
side: '14rem' side: '14rem',
'9/10': '90%'
} }
}, }
}, },
variants: { variants: {
extend: {} extend: {}

View File

@@ -16,7 +16,7 @@ import BLOG from '@/blog.config'
const LayoutBase = props => { const LayoutBase = props => {
const { children, meta } = props const { children, meta } = props
return ( 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} /> <CommonHead meta={meta} />
{/* 顶栏LOGO */} {/* 顶栏LOGO */}
<Header {...props} /> <Header {...props} />

View File

@@ -26,7 +26,7 @@ export const ArticleLock = props => {
<div className='text-center space-y-3'> <div className='text-center space-y-3'>
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div> <div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
<div className='flex mx-4'> <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" > <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'} >&nbsp;{locale.COMMON.SUBMIT}</i> <i className={'duration-200 cursor-pointer fas fa-key dark:text-black'} >&nbsp;{locale.COMMON.SUBMIT}</i>
</div> </div>

View File

@@ -78,5 +78,5 @@ export const BlogListScroll = props => {
</div> </div>
</div> </div>
); )
} }

View File

@@ -21,5 +21,5 @@ export const Header = (props) => {
</div> </div>
</div> </div>
</header> </header>
); )
} }

View File

@@ -1,6 +1,6 @@
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import Link from 'next/link' 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 { customNav } = props
const { locale } = useGlobal() const { locale } = useGlobal()
let links = [ let links = [
{ icon: 'fas fa-search', name: locale.NAV.SEARCH, to: '/search', show: CONFIG_EMPTY.MENU_SEARCH }, { 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_EMPTY.MENU_ARCHIVE }, { 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_EMPTY.MENU_CATEGORY }, { 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_EMPTY.MENU_TAG } { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_EXAMPLE.MENU_TAG }
] ]
if (customNav) { if (customNav) {

View File

@@ -62,7 +62,7 @@ const SearchInput = ({ currentTag, currentSearch, cRef }) => {
ref={searchInputRef} ref={searchInputRef}
type='text' type='text'
placeholder={currentTag ? `${locale.SEARCH.TAGS} #${currentTag}` : `${locale.SEARCH.ARTICLES}`} 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} onKeyUp={handleKeyUp}
onCompositionStart={lockSearchInput} onCompositionStart={lockSearchInput}
onCompositionUpdate={lockSearchInput} onCompositionUpdate={lockSearchInput}

View File

@@ -1,8 +1,8 @@
const CONFIG_EMPTY = { const CONFIG_EXAMPLE = {
// 菜单配置 // 菜单配置
MENU_CATEGORY: true, // 显示分类 MENU_CATEGORY: true, // 显示分类
MENU_TAG: true, // 显示标签 MENU_TAG: true, // 显示标签
MENU_ARCHIVE: true, // 显示归档 MENU_ARCHIVE: true, // 显示归档
MENU_SEARCH: true // 显示搜索 MENU_SEARCH: true // 显示搜索
} }
export default CONFIG_EMPTY export default CONFIG_EXAMPLE

View File

@@ -1,4 +1,4 @@
import CONFIG_EMPTY from './config_empty' import CONFIG_EXAMPLE from './config_example'
import { LayoutIndex } from './LayoutIndex' import { LayoutIndex } from './LayoutIndex'
import { LayoutSearch } from './LayoutSearch' import { LayoutSearch } from './LayoutSearch'
import { LayoutArchive } from './LayoutArchive' import { LayoutArchive } from './LayoutArchive'
@@ -11,7 +11,7 @@ import { LayoutTag } from './LayoutTag'
import { LayoutTagIndex } from './LayoutTagIndex' import { LayoutTagIndex } from './LayoutTagIndex'
export { export {
CONFIG_EMPTY as THEME_CONFIG, CONFIG_EXAMPLE as THEME_CONFIG,
LayoutIndex, LayoutIndex,
LayoutSearch, LayoutSearch,
LayoutArchive, LayoutArchive,

View File

@@ -3,6 +3,7 @@ import TopNav from './components/TopNav'
import AsideLeft from './components/AsideLeft' import AsideLeft from './components/AsideLeft'
import Live2D from '@/components/Live2D' import Live2D from '@/components/Live2D'
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
import { isBrowser, loadExternalResource } from '@/lib/utils'
/** /**
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏 * 基础布局 采用左右两侧布局,移动端使用顶部导航栏
@@ -25,7 +26,12 @@ const LayoutBase = (props) => {
meta meta
} = props } = props
const leftAreaSlot = <Live2D/> const leftAreaSlot = <Live2D/>
return (<>
if (isBrowser()) {
loadExternalResource('/css/theme-fukasawa.css', 'css')
}
return (<div id='theme-fukasawa' >
<CommonHead meta={meta} /> <CommonHead meta={meta} />
<TopNav {...props}/> <TopNav {...props}/>
<div className={(BLOG.LAYOUT_SIDEBAR_REVERSE ? 'flex-row-reverse' : '') + ' flex'}> <div className={(BLOG.LAYOUT_SIDEBAR_REVERSE ? 'flex-row-reverse' : '') + ' flex'}>
@@ -38,7 +44,7 @@ const LayoutBase = (props) => {
</main> </main>
</div> </div>
</>) </div>)
} }
export default LayoutBase export default LayoutBase

View File

@@ -30,7 +30,7 @@ export const ArticleLock = props => {
<div className="flex mx-4"> <div className="flex mx-4">
<input <input
id="password" type='password' 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> ></input>
<div <div
onClick={submitPassword} onClick={submitPassword}

View File

@@ -2,9 +2,8 @@ import BLOG from '@/blog.config'
import Link from 'next/link' import Link from 'next/link'
import React from 'react' import React from 'react'
import CONFIG_FUKA from '../config_fuka' 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 const showPreview = CONFIG_FUKA.POST_LIST_PREVIEW && post.blockMap
// matery 主题默认强制显示图片 // matery 主题默认强制显示图片
if (post && !post.page_cover) { if (post && !post.page_cover) {
@@ -13,44 +12,45 @@ const BlogCard = ({ post, showSummary, siteInfo }) => {
const showPageCover = CONFIG_FUKA.POST_LIST_COVER && post?.page_cover const showPageCover = CONFIG_FUKA.POST_LIST_COVER && post?.page_cover
return ( return (
<Card className="w-full lg:max-w-sm p-2 h-full overflow-auto"> <div data-aos="fade-up" data-aos-duration="500" data-aos-once="true"
<div className='w-full lg:max-w-sm p-2 h-full overflow-auto'>
key={post.id} <section className="shadow mb-4 p-2 bg-white dark:bg-hexo-black-gray hover:shadow-lg duration-200">
className="flex flex-col-reverse justify-between duration-300" <div className="flex flex-col-reverse justify-between duration-300">
> <div className="p-2 flex flex-col w-full">
<div className="p-2 flex flex-col w-full"> <Link
<Link href={`${BLOG.SUB_PATH}/${post.slug}`}
href={`${BLOG.SUB_PATH}/${post.slug}`} passHref
passHref className={`break-words cursor-pointer font-bold hover:underline text-xl ${showPreview ? 'justify-center' : 'justify-start'}
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`}>
} 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) && ( {(!showPreview || showSummary) && (
<p className="mt-4 mb-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7 overflow-hidden"> <p className="mt-4 mb-4 text-gray-700 dark:text-gray-300 text-sm font-light leading-7 overflow-hidden">
{post.summary} {post.summary}
</p> </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' /> */}
</div> </div>
</Link>
)} {showPageCover && (
</div> <Link href={`${BLOG.SUB_PATH}/${post.slug}`} passHref legacyBehavior>
</Card> <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>
) )
} }

View File

@@ -1,5 +1,4 @@
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
import { useEffect, useState } from 'react'
import BlogCard from './BlogCard' import BlogCard from './BlogCard'
import BlogPostListEmpty from './BlogListEmpty' import BlogPostListEmpty from './BlogListEmpty'
import PaginationSimple from './PaginationSimple' import PaginationSimple from './PaginationSimple'
@@ -15,25 +14,6 @@ import PaginationSimple from './PaginationSimple'
const BlogListPage = ({ page = 1, posts = [], postCount, siteInfo }) => { const BlogListPage = ({ page = 1, posts = [], postCount, siteInfo }) => {
const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE) const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
const showNext = page < totalPage 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) { if (!posts || posts.length === 0) {
return <BlogPostListEmpty /> return <BlogPostListEmpty />
@@ -41,10 +21,10 @@ const BlogListPage = ({ page = 1, posts = [], postCount, siteInfo }) => {
return ( return (
<div> <div>
{/* 文章列表 */} {/* 文章列表 */}
<div id="container" style={{ columnCount: colCount }}> <div id="container" className='grid-container'>
{posts?.map(post => ( {posts?.map(post => (
<div key={post.id} className='justify-center flex' style={{ breakInside: 'avoid' }}> <div key={post.id} className='grid-item justify-center flex' style={{ breakInside: 'avoid' }}>
<BlogCard key={post.id} post={post} siteInfo={siteInfo} /> <BlogCard index={posts.indexOf(post)} key={post.id} post={post} siteInfo={siteInfo} />
</div> </div>
))} ))}
</div> </div>

View File

@@ -3,7 +3,7 @@ import React from 'react'
import BlogCard from './BlogCard' import BlogCard from './BlogCard'
import BlogPostListEmpty from './BlogListEmpty' import BlogPostListEmpty from './BlogListEmpty'
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import throttle from 'lodash.throttle'
/** /**
* 文章列表分页表格 * 文章列表分页表格
* @param page 当前页 * @param page 当前页
@@ -14,18 +14,7 @@ import throttle from 'lodash.throttle'
*/ */
const BlogListScroll = props => { const BlogListScroll = props => {
const { posts = [], siteInfo } = props const { posts = [], siteInfo } = props
const [colCount, changeCol] = React.useState(1)
const { locale } = useGlobal() 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 targetRef = React.useRef(null)
const [page, updatePage] = React.useState(1) const [page, updatePage] = React.useState(1)
@@ -45,45 +34,41 @@ const BlogListScroll = props => {
} }
// 监听滚动自动分页加载 // 监听滚动自动分页加载
const scrollTrigger = React.useCallback(throttle(() => { const scrollTrigger = () => {
const scrollS = window.scrollY + window.outerHeight requestAnimationFrame(() => {
const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0 const scrollS = window.scrollY + window.outerHeight
if (scrollS > clientHeight + 100) { const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
handleGetMore() if (scrollS > clientHeight + 100) {
} handleGetMore()
}, 500)) }
})
}
React.useEffect(() => { React.useEffect(() => {
updateCol()
window.addEventListener('scroll', scrollTrigger) window.addEventListener('scroll', scrollTrigger)
window.addEventListener('resize', updateCol)
return () => { return () => {
window.removeEventListener('resize', updateCol)
window.removeEventListener('scroll', scrollTrigger) window.removeEventListener('scroll', scrollTrigger)
} }
}) }, [])
if (!posts || posts.length === 0) { if (!posts || posts.length === 0) {
return <BlogPostListEmpty /> return <BlogPostListEmpty />
} else { } else {
return ( return (
<div id="container" ref={targetRef} > <div id="container" ref={targetRef} className='grid-container' >
{/* 文章列表 */} {/* 文章列表 */}
<div style={{ columnCount: colCount }}> {postsToShow?.map(post => (
{postsToShow?.map(post => ( <div key={post.id} className='grid-item justify-center flex' style={{ breakInside: 'avoid' }}>
<div key={post.id} className='justify-center flex' style={{ breakInside: 'avoid' }}> <BlogCard index={posts.indexOf(post)} key={post.id} post={post} siteInfo={siteInfo} />
<BlogCard key={post.id} post={post} siteInfo={siteInfo} /> </div>
</div> ))}
))}
</div>
<div className="w-full my-4 py-4 text-center cursor-pointer " <div className="w-full my-4 py-4 text-center cursor-pointer "
onClick={handleGetMore}> onClick={handleGetMore}>
{' '} {' '}
{hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '} {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
</div> </div>
</div> </div>
) )
} }
} }

View File

@@ -1,11 +1,5 @@
const Card = ({ children, headerSlot, className }) => { const Card = ({ children, headerSlot, className }) => {
return <div className={className} return <div data-aos="fade-in" data-aos-duration="1000" 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"
>
<>{headerSlot}</> <>{headerSlot}</>
<section className="shadow mb-4 p-2 bg-white dark:bg-hexo-black-gray hover:shadow-lg duration-200"> <section className="shadow mb-4 p-2 bg-white dark:bg-hexo-black-gray hover:shadow-lg duration-200">
{children} {children}

View File

@@ -27,7 +27,7 @@ const Catalog = ({ toc }) => {
// 同步选中目录事件 // 同步选中目录事件
const [activeSection, setActiveSection] = React.useState(null) const [activeSection, setActiveSection] = React.useState(null)
const throttleMs = 100 const throttleMs = 200
const actionSectionScrollSpy = React.useCallback(throttle(() => { const actionSectionScrollSpy = React.useCallback(throttle(() => {
const sections = document.getElementsByClassName('notion-h') const sections = document.getElementsByClassName('notion-h')
let prevBBox = null let prevBBox = null

View File

@@ -53,7 +53,7 @@ const SearchInput = ({ currentTag, currentSearch, cRef }) => {
<input <input
ref={searchInputRef} ref={searchInputRef}
type='text' 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} onKeyUp={handleKeyUp}
onCompositionStart={lockSearchInput} onCompositionStart={lockSearchInput}
onCompositionUpdate={lockSearchInput} onCompositionUpdate={lockSearchInput}

View File

@@ -1,11 +1,10 @@
import CommonHead from '@/components/CommonHead' 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 Footer from './components/Footer'
import JumpToTopButton from './components/JumpToTopButton' import JumpToTopButton from './components/JumpToTopButton'
import SideRight from './components/SideRight' import SideRight from './components/SideRight'
import TopNav from './components/TopNav' import TopNav from './components/TopNav'
import smoothscroll from 'smoothscroll-polyfill'
import FloatDarkModeButton from './components/FloatDarkModeButton' import FloatDarkModeButton from './components/FloatDarkModeButton'
import Live2D from '@/components/Live2D' import Live2D from '@/components/Live2D'
import LoadingCover from './components/LoadingCover' import LoadingCover from './components/LoadingCover'
@@ -15,7 +14,13 @@ import dynamic from 'next/dynamic'
const FacebookPage = dynamic( const FacebookPage = dynamic(
() => { () => {
return import('@/components/FacebookPage') let facebook = <></>
try {
facebook = import('@/components/FacebookPage')
} catch (err) {
console.error(err)
}
return facebook
}, },
{ ssr: false } { ssr: false }
) )
@@ -28,7 +33,7 @@ const FacebookPage = dynamic(
*/ */
const LayoutBase = props => { const LayoutBase = props => {
const { children, headerSlot, floatSlot, meta, siteInfo } = props const { children, headerSlot, floatSlot, meta, siteInfo } = props
const [show, switchShow] = useState(false) const [showFloatButton, switchShow] = useState(false)
// const [percent, changePercent] = useState(0) // 页面阅读百分比 // const [percent, changePercent] = useState(0) // 页面阅读百分比
const rightAreaSlot = ( const rightAreaSlot = (
<> <>
@@ -37,8 +42,8 @@ const LayoutBase = props => {
</> </>
) )
const { onLoading } = useGlobal() const { onLoading } = useGlobal()
const throttleMs = 200
const scrollListener = () => { const scrollListener = useCallback(throttle(() => {
const targetRef = document.getElementById('wrapper') const targetRef = document.getElementById('wrapper')
const clientHeight = targetRef?.clientHeight const clientHeight = targetRef?.clientHeight
const scrollY = window.pageYOffset const scrollY = window.pageYOffset
@@ -47,26 +52,24 @@ const LayoutBase = props => {
if (per > 100) per = 100 if (per > 100) per = 100
const shouldShow = scrollY > 100 && per > 0 const shouldShow = scrollY > 100 && per > 0
if (shouldShow !== show) { if (shouldShow !== showFloatButton) {
switchShow(shouldShow) switchShow(shouldShow)
} }
// changePercent(per) }, throttleMs))
}
useEffect(() => { useEffect(() => {
smoothscroll.polyfill()
document.addEventListener('scroll', scrollListener) document.addEventListener('scroll', scrollListener)
return () => document.removeEventListener('scroll', scrollListener) return () => document.removeEventListener('scroll', scrollListener)
}, [show]) }, [])
return ( return (
<div className="bg-hexo-background-gray dark:bg-black"> <div id='theme-hexo'>
<CommonHead meta={meta} siteInfo={siteInfo}/> <CommonHead meta={meta} siteInfo={siteInfo}/>
<TopNav {...props} /> <TopNav {...props} />
{headerSlot} {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 <div
id="container-inner" 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'} 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> </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={(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 <div className={'justify-center flex flex-col items-center cursor-pointer'}>
className={
(show ? 'animate__animated ' : 'hidden') +
' animate__fadeInUp justify-center duration-300 animate__faster flex flex-col items-center cursor-pointer '
}
>
<FloatDarkModeButton /> <FloatDarkModeButton />
{floatSlot} {floatSlot}
<JumpToTopButton /> <JumpToTopButton />

View File

@@ -7,8 +7,8 @@ import LayoutBase from './LayoutBase'
import React from 'react' import React from 'react'
export const LayoutIndex = (props) => { 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} />} {BLOG.POST_LIST_STYLE === 'page' ? <BlogPostListPage {...props} /> : <BlogPostListScroll {...props} />}
</LayoutBase> </LayoutBase>
} }

View File

@@ -46,7 +46,7 @@ export const LayoutSlug = props => {
showTag={false} showTag={false}
floatSlot={floatSlot} 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 && <ArticleLock validPassword={validPassword} />}
{!lock && <div id="container" className="overflow-x-auto flex-grow mx-auto md:w-full md:px-5 "> {!lock && <div id="container" className="overflow-x-auto flex-grow mx-auto md:w-full md:px-5 ">

View File

@@ -6,14 +6,8 @@ const NotionPage = dynamic(() => import('@/components/NotionPage'))
const Announcement = ({ post, className }) => { const Announcement = ({ post, className }) => {
const { locale } = useGlobal() const { locale } = useGlobal()
if (post?.blockMap) { if (post?.blockMap) {
return <div return <div className={className}>
data-aos="fade-up" <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">
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">
<div><i className='mr-2 fas fa-bullhorn' />{locale.COMMON.ANNOUNCEMENT}</div> <div><i className='mr-2 fas fa-bullhorn' />{locale.COMMON.ANNOUNCEMENT}</div>
{post && (<div id="announcement-content"> {post && (<div id="announcement-content">
<NotionPage post={post} className='text-center ' /> <NotionPage post={post} className='text-center ' />

View File

@@ -25,7 +25,7 @@ export const ArticleLock = props => {
<div className='text-center space-y-3'> <div className='text-center space-y-3'>
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div> <div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
<div className='flex mx-4'> <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" > <div onClick={submitPassword} className="px-3 whitespace-nowrap cursor-pointer items-center justify-center py-2 bg-indigo-500 hover:bg-indigo-400 text-white rounded-r duration-300" >
<i className={'duration-200 cursor-pointer fas fa-key'} >&nbsp;{locale.COMMON.SUBMIT}</i> <i className={'duration-200 cursor-pointer fas fa-key'} >&nbsp;{locale.COMMON.SUBMIT}</i>
</div> </div>

View File

@@ -1,107 +1,57 @@
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
import Link from 'next/link' import Link from 'next/link'
import React from 'react' import React from 'react'
import TagItemMini from './TagItemMini'
import CONFIG_HEXO from '../config_hexo' 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 const showPreview = CONFIG_HEXO.POST_LIST_PREVIEW && post.blockMap
if (post && !post.page_cover && CONFIG_HEXO.POST_LIST_COVER_DEFAULT) { if (post && !post.page_cover && CONFIG_HEXO.POST_LIST_COVER_DEFAULT) {
post.page_cover = siteInfo?.pageCover post.page_cover = siteInfo?.pageCover
} }
const showPageCover = CONFIG_HEXO.POST_LIST_COVER && post?.page_cover const showPageCover = CONFIG_HEXO.POST_LIST_COVER && post?.page_cover
const delay = (index % 2) * 200
return ( return (
<div <div
key={post.id}
data-aos="fade-up" data-aos="fade-up"
data-aos-duration="600" data-aos-duration="200"
data-aos-easing="ease-in-out" data-aos-delay={delay}
data-aos-once="false" data-aos-once="true"
data-aos-anchor-placement="top-bottom" data-aos-anchor-placement="top-bottom"
className={`flex md:flex-row flex-col-reverse ${index % 2 === 0 ? 'md:flex-row-reverse' : ''} key={post.id}
w-full md:h-72 h-96 justify-between overflow-hidden drop-shadow-md className={`flex md:flex-row flex-col-reverse ${CONFIG_HEXO.POST_LIST_IMG_CROSSOVER ? 'even:md:flex-row-reverse' : ''}
border dark:border-black rounded-xl bg-white dark:bg-hexo-black-gray `}> 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 <BlogPostCardInfo index={index} post={post} showPageCover={showPageCover} showPreview={showPreview} showSummary={showSummary}/>
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>
{/* 图片封面 */}
{showPageCover && !showPreview && post?.page_cover && ( {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> <Link href={`${BLOG.SUB_PATH}/${post.slug}`} passHref legacyBehavior>
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img {/* <img
src={post?.page_cover} src={post?.page_cover}
alt={post.title} alt={post.title}
className="h-full w-full hover:scale-125 transform object-cover duration-500" loading='lazy'
/> className="w-full relative cursor-pointer object-cover duration-200 hover:scale-125 "
{/* <Image className='hover:scale-125 transform duration-500' src={post?.page_cover} alt={post.title} layout='fill' objectFit='cover' loading='lazy' /> */} /> */}
<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> </Link>
</div> </div>
)} )}

View 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>
}

View File

@@ -22,7 +22,7 @@ const BlogPostListPage = ({ page = 1, posts = [], postCount, siteInfo }) => {
{/* 文章列表 */} {/* 文章列表 */}
<div className="space-y-6 px-2"> <div className="space-y-6 px-2">
{posts.map(post => ( {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> </div>
{showPagination && <PaginationNumber page={page} totalPage={totalPage} />} {showPagination && <PaginationNumber page={page} totalPage={totalPage} />}

View File

@@ -2,7 +2,6 @@ import BLOG from '@/blog.config'
import BlogPostCard from './BlogPostCard' import BlogPostCard from './BlogPostCard'
import BlogPostListEmpty from './BlogPostListEmpty' import BlogPostListEmpty from './BlogPostListEmpty'
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import throttle from 'lodash.throttle'
import React from 'react' import React from 'react'
import CONFIG_HEXO from '../config_hexo' import CONFIG_HEXO from '../config_hexo'
import { getListByPage } from '@/lib/utils' import { getListByPage } from '@/lib/utils'
@@ -31,13 +30,15 @@ const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = CONFIG_HE
} }
// 监听滚动自动分页加载 // 监听滚动自动分页加载
const scrollTrigger = React.useCallback(throttle(() => { const scrollTrigger = () => {
const scrollS = window.scrollY + window.outerHeight requestAnimationFrame(() => {
const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0 const scrollS = window.scrollY + window.outerHeight
if (scrollS > clientHeight + 100) { const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
handleGetMore() if (scrollS > clientHeight + 100) {
} handleGetMore()
}, 500)) }
})
}
// 监听滚动 // 监听滚动
React.useEffect(() => { 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'> <div className='flex flex-wrap space-y-1 lg:space-y-4 px-2'>
{postsToShow.map(post => ( {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> </div>

View File

@@ -1,7 +1,7 @@
const Card = ({ children, headerSlot, className }) => { const Card = ({ children, headerSlot, className }) => {
return <div className={className}> return <div className={className}>
<>{headerSlot}</> <>{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} {children}
</section> </section>
</div> </div>

View File

@@ -28,7 +28,7 @@ const Catalog = ({ toc }) => {
// 同步选中目录事件 // 同步选中目录事件
const [activeSection, setActiveSection] = React.useState(null) const [activeSection, setActiveSection] = React.useState(null)
const throttleMs = 100 const throttleMs = 200
const actionSectionScrollSpy = React.useCallback(throttle(() => { const actionSectionScrollSpy = React.useCallback(throttle(() => {
const sections = document.getElementsByClassName('notion-h') const sections = document.getElementsByClassName('notion-h')
let prevBBox = null let prevBBox = null

View File

@@ -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 Typed from 'typed.js'
import CONFIG_HEXO from '../config_hexo' import CONFIG_HEXO from '../config_hexo'
import NavButtonGroup from './NavButtonGroup' import NavButtonGroup from './NavButtonGroup'
import throttle from 'lodash.throttle'
let wrapperTop = 0 let wrapperTop = 0
let windowTop = 0 let windowTop = 0
let autoScroll = false let autoScroll = false
const enableAutoScroll = false // 是否开启自动吸附滚动
/** /**
* *
@@ -16,6 +19,7 @@ const Header = props => {
const { siteInfo } = props const { siteInfo } = props
useEffect(() => { useEffect(() => {
updateHeaderHeight() updateHeaderHeight()
if (!typed && window && document.getElementById('typed')) { if (!typed && window && document.getElementById('typed')) {
changeType( changeType(
new Typed('#typed', { new Typed('#typed', {
@@ -28,6 +32,7 @@ const Header = props => {
}) })
) )
} }
if (enableAutoScroll) { if (enableAutoScroll) {
scrollTrigger() scrollTrigger()
window.addEventListener('scroll', scrollTrigger) window.addEventListener('scroll', scrollTrigger)
@@ -42,75 +47,75 @@ const Header = props => {
} }
}) })
function updateHeaderHeight () { function updateHeaderHeight() {
setTimeout(() => { requestAnimationFrame(() => {
const wrapperElement = document.getElementById('wrapper') const wrapperElement = document.getElementById('wrapper')
wrapperTop = wrapperElement?.offsetTop 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 ( return (
<header <header
id="header" id="header"
className="duration-500 md:bg-fixed w-full bg-cover bg-center h-screen bg-black text-white relative z-10" className="w-full h-screen bg-black text-white relative"
style={{ >
backgroundImage: <div className={`w-full h-full ${CONFIG_HEXO.HOME_NAV_BACKGROUND_IMG_FIXED ? 'fixed' : ''}`}>
`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}")` {/* <Image src={siteInfo.pageCover} fill
}} style={{ objectFit: 'cover' }}
> className='opacity-70'
<div className="absolute flex flex-col h-full items-center justify-center w-full "> placeholder='blur'
<div className='text-4xl md:text-5xl text-white shadow-text'>{siteInfo?.title}</div> blurDataURL='/bg_image.jpg' /> */}
<div className='mt-2 h-12 items-center text-center shadow-text text-white text-lg'> {/* eslint-disable-next-line @next/next/no-img-element */}
<span id='typed'/> <img src={siteInfo.pageCover} className='h-full w-full object-cover opacity-70 ' />
</div> </div>
{/* 首页导航插件 */} <div className="absolute bottom-0 flex flex-col h-full items-center justify-center w-full ">
{ CONFIG_HEXO.HOME_NAV_BUTTONS && <NavButtonGroup {...props}/>} <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 {CONFIG_HEXO.HOME_NAV_BUTTONS && <NavButtonGroup {...props} />}
onClick={() => {
window.scrollTo({ top: wrapperTop, behavior: 'smooth' }) </div>
}}
className="cursor-pointer w-full text-center py-4 text-3xl absolute bottom-10 text-white" <div
> onClick={() => { window.scrollTo({ top: wrapperTop, behavior: 'smooth' }) }}
<i className='animate-bounce fas fa-angle-down'/> className="cursor-pointer w-full text-center py-4 text-3xl absolute bottom-10 text-white"
</div> >
</header> <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 export default Header

View File

@@ -19,7 +19,7 @@ export default function HeaderArticle({ post, siteInfo }) {
return ( return (
<div <div
id="header" 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 }} 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 "> <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> </div>
</header> </header>
</div> </div>
); )
} }

View File

@@ -11,10 +11,11 @@ import CONFIG_HEXO from '../config_hexo'
* @constructor * @constructor
*/ */
const JumpToTopButton = ({ showPercent = true, percent }) => { const JumpToTopButton = ({ showPercent = true, percent }) => {
const { locale } = useGlobal()
if (!CONFIG_HEXO.WIDGET_TO_TOP) { if (!CONFIG_HEXO.WIDGET_TO_TOP) {
return <></> 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' })} > 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> <div title={locale.POST.TOP} ><i className='fas fa-arrow-up text-xs' /></div>
{showPercent && (<div className='text-xs hidden lg:block'>{percent}</div>)} {showPercent && (<div className='text-xs hidden lg:block'>{percent}</div>)}

View File

@@ -1,5 +1,6 @@
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
// import Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
@@ -19,46 +20,53 @@ const LatestPostsGroup = ({ latestPosts, siteInfo }) => {
} }
return <> return <>
<div className=" mb-2 px-1 flex flex-nowrap justify-between"> <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> <div>
<div className='text-line-2'>{post.title}</div> <i className="mr-2 fas fas fa-history" />
<div className="text-gray-500">{post.lastEditedTime}</div> {locale.COMMON.LATEST_POSTS}
</div> </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 export default LatestPostsGroup

View File

@@ -8,6 +8,7 @@ const MenuButtonGroupTop = (props) => {
const { locale } = useGlobal() const { locale } = useGlobal()
let links = [ 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-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-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 }, // { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_HEXO.MENU_CATEGORY },
@@ -36,12 +37,12 @@ const MenuButtonGroupTop = (props) => {
</div> </div>
</Link> </Link>
); )
} else { } else {
return null return null
} }
})} })}
</nav> </nav>
); )
} }
export default MenuButtonGroupTop export default MenuButtonGroupTop

View File

@@ -25,7 +25,7 @@ const Progress = ({ targetRef, showPercent = true }) => {
useEffect(() => { useEffect(() => {
document.addEventListener('scroll', scrollListener) document.addEventListener('scroll', scrollListener)
return () => document.removeEventListener('scroll', scrollListener) return () => document.removeEventListener('scroll', scrollListener)
}, [percent]) }, [])
return ( return (
<div className="h-4 w-full shadow-2xl bg-gray-400 "> <div className="h-4 w-full shadow-2xl bg-gray-400 ">

View File

@@ -69,7 +69,7 @@ const SearchInput = props => {
ref={searchInputRef} ref={searchInputRef}
type="text" type="text"
className={ 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} onKeyUp={handleKeyUp}
onCompositionStart={lockSearchInput} onCompositionStart={lockSearchInput}

View File

@@ -1,7 +1,6 @@
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import throttle from 'lodash.throttle'
import Link from 'next/link' import Link from 'next/link'
import { useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import CategoryGroup from './CategoryGroup' import CategoryGroup from './CategoryGroup'
import Collapse from './Collapse' import Collapse from './Collapse'
import Logo from './Logo' import Logo from './Logo'
@@ -10,6 +9,7 @@ import TagGroups from './TagGroups'
import MenuButtonGroupTop from './MenuButtonGroupTop' import MenuButtonGroupTop from './MenuButtonGroupTop'
import MenuList from './MenuList' import MenuList from './MenuList'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import throttle from 'lodash.throttle'
let windowTop = 0 let windowTop = 0
@@ -19,17 +19,33 @@ let windowTop = 0
* @returns * @returns
*/ */
const TopNav = props => { const TopNav = props => {
const searchDrawer = useRef()
const { tags, currentTag, categories, currentCategory } = props const { tags, currentTag, categories, currentCategory } = props
const { locale } = useGlobal() const { locale } = useGlobal()
const searchDrawer = useRef()
const { isDarkMode } = useGlobal() const { isDarkMode } = useGlobal()
const router = useRouter() 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 scrollS = window.scrollY
const nav = document.querySelector('#sticky-nav') const nav = document.querySelector('#sticky-nav')
const header = document.querySelector('#header') 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 // 透明导航条的条件 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') nav && nav.classList.replace('transparent', 'dark:bg-hexo-black-gray')
} }
const showNav = scrollS <= windowTop || scrollS < 5 || (header && scrollS <= header.clientHeight * 2)// 非首页无大图时影藏顶部 滚动条置顶时隐藏
if (!showNav) { if (!showNav) {
nav && nav.classList.replace('top-0', '-top-20') nav && nav.classList.replace('top-0', '-top-20')
windowTop = scrollS windowTop = scrollS
@@ -55,7 +72,8 @@ const TopNav = props => {
windowTop = scrollS windowTop = scrollS
} }
navDarkMode() navDarkMode()
}, 200) }, throttleMs)
)
const navDarkMode = () => { const navDarkMode = () => {
const nav = document.getElementById('sticky-nav') 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 = <> const searchDrawerSlot = <>
{ categories && ( {categories && (
<section className='mt-8'> <section className='mt-8'>
<div className='text-sm flex flex-nowrap justify-between font-light px-2'> <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> <div className='text-gray-600 dark:text-gray-200'><i className='mr-2 fas fa-th-list' />{locale.COMMON.CATEGORY}</div>
<Link <Link
href={'/category'} href={'/category'}
passHref passHref
className='mb-3 text-gray-400 hover:text-black dark:text-gray-400 dark:hover:text-white hover:underline cursor-pointer'> 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> </Link>
</div> </div>
<CategoryGroup currentCategory={currentCategory} categories={categories} /> <CategoryGroup currentCategory={currentCategory} categories={categories} />
</section> </section>
) } )}
{ tags && ( {tags && (
<section className='mt-4'> <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-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> <div className='text-gray-600 dark:text-gray-200'><i className='mr-2 fas fa-tag' />{locale.COMMON.TAGS}</div>
<Link <Link
href={'/tag'} href={'/tag'}
passHref passHref
className='text-gray-400 hover:text-black dark:hover:text-white hover:underline cursor-pointer'> 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> </Link>
</div> </div>
<div className='p-2'> <div className='p-2'>
<TagGroups tags={tags} currentTag={currentTag} /> <TagGroups tags={tags} currentTag={currentTag} />
</div> </div>
</section> </section>
) } )}
</> </>
return (<div id='top-nav' className='z-40'> 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 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='w-full flex justify-between items-center px-4 py-2'>
<div className='flex'> <div className='flex'>
<Logo {...props}/> <Logo {...props} />
</div> </div>
{/* 右侧功能 */} {/* 右侧功能 */}
<div className='mr-1 justify-end items-center '> <div className='mr-1 justify-end items-center '>
<div className='hidden lg:flex'> <MenuButtonGroupTop {...props}/></div> <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'> <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'/> } {isOpen ? <i className='fas fa-times' /> : <i className='fas fa-bars' />}
</div> </div>
</div> </div>
</div> </div>
<Collapse type='vertical' isOpen={isOpen} className='shadow-xl'> <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 '> <div className='bg-white dark:bg-hexo-black-gray pt-1 py-2 px-5 lg:hidden '>
<MenuList {...props}/> <MenuList {...props} />
</div>
</Collapse>
</div> </div>
</Collapse> </div>)
</div>
</div>)
} }
export default TopNav export default TopNav

View File

@@ -3,8 +3,10 @@ const CONFIG_HEXO = {
HOME_BANNER_GREETINGS: ['Hi我是一个程序员', 'Hi我是一个打工人', 'Hi我是一个干饭人', '欢迎来到我的博客🎉'], // 首页大图标语文字 HOME_BANNER_GREETINGS: ['Hi我是一个程序员', 'Hi我是一个打工人', 'Hi我是一个干饭人', '欢迎来到我的博客🎉'], // 首页大图标语文字
HOME_NAV_BUTTONS: true, // 首页是否显示分类大图标按钮 HOME_NAV_BUTTONS: true, // 首页是否显示分类大图标按钮
HOME_NAV_BACKGROUND_IMG_FIXED: true, // 首页背景图滚动时是否固定true 则滚动时图片不懂; false则随鼠标滚动
// 菜单配置 // 菜单配置
MENU_INDEX: true, // 显示首页
MENU_CATEGORY: true, // 显示分类 MENU_CATEGORY: true, // 显示分类
MENU_TAG: true, // 显示标签 MENU_TAG: true, // 显示标签
MENU_ARCHIVE: true, // 显示归档 MENU_ARCHIVE: true, // 显示归档
@@ -14,6 +16,7 @@ const CONFIG_HEXO = {
POST_LIST_COVER_DEFAULT: true, // 封面为空时用站点背景做默认封面 POST_LIST_COVER_DEFAULT: true, // 封面为空时用站点背景做默认封面
POST_LIST_SUMMARY: true, // 文章摘要 POST_LIST_SUMMARY: true, // 文章摘要
POST_LIST_PREVIEW: true, // 读取文章预览 POST_LIST_PREVIEW: true, // 读取文章预览
POST_LIST_IMG_CROSSOVER: true, // 博客列表图片左右交错
ARTICLE_ADJACENT: true, // 显示上一篇下一篇文章推荐 ARTICLE_ADJACENT: true, // 显示上一篇下一篇文章推荐
ARTICLE_COPYRIGHT: true, // 显示文章版权声明 ARTICLE_COPYRIGHT: true, // 显示文章版权声明

View File

@@ -8,6 +8,25 @@ import * as medium from './medium'
import * as nobelium from './nobelium' import * as nobelium from './nobelium'
import * as matery from './matery' import * as matery from './matery'
import * as example from './example' import * as example from './example'
import * as simple from './simple'
export const ALL_THEME = ['hexo', 'matery', 'next', 'medium', 'fukasawa', 'nobelium', 'example'] export const ALL_THEME = [
export { hexo, next, medium, fukasawa, nobelium, matery, example } 'hexo',
'matery',
'next',
'medium',
'fukasawa',
'nobelium',
'example',
'simple'
]
export {
hexo,
next,
medium,
fukasawa,
nobelium,
matery,
example,
simple
}

View File

@@ -1,15 +1,15 @@
import CommonHead from '@/components/CommonHead' import CommonHead from '@/components/CommonHead'
import { useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import Footer from './components/Footer' import Footer from './components/Footer'
import JumpToTopButton from './components/JumpToTopButton' import JumpToTopButton from './components/JumpToTopButton'
import TopNav from './components/TopNav' import TopNav from './components/TopNav'
import smoothscroll from 'smoothscroll-polyfill'
import Live2D from '@/components/Live2D' import Live2D from '@/components/Live2D'
import LoadingCover from './components/LoadingCover' import LoadingCover from './components/LoadingCover'
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
import FloatDarkModeButton from './components/FloatDarkModeButton' import FloatDarkModeButton from './components/FloatDarkModeButton'
import throttle from 'lodash.throttle'
/** /**
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏 * 基础布局 采用左右两侧布局,移动端使用顶部导航栏
@@ -22,28 +22,22 @@ const LayoutBase = props => {
const [show, switchShow] = useState(false) const [show, switchShow] = useState(false)
const { onLoading } = useGlobal() const { onLoading } = useGlobal()
const scrollListener = () => { const throttleMs = 200
const targetRef = document.getElementById('wrapper') const scrollListener = useCallback(throttle(() => {
const clientHeight = targetRef?.clientHeight
const scrollY = window.pageYOffset const scrollY = window.pageYOffset
const fullHeight = clientHeight - window.outerHeight const shouldShow = scrollY > 300
let per = parseFloat(((scrollY / fullHeight) * 100).toFixed(0))
if (per > 100) per = 100
const shouldShow = scrollY > 300 && per > 0
if (shouldShow !== show) { if (shouldShow !== show) {
switchShow(shouldShow) switchShow(shouldShow)
} }
// changePercent(per) }, throttleMs))
}
useEffect(() => { useEffect(() => {
smoothscroll.polyfill()
document.addEventListener('scroll', scrollListener) document.addEventListener('scroll', scrollListener)
return () => document.removeEventListener('scroll', scrollListener) return () => document.removeEventListener('scroll', scrollListener)
}, [show]) }, [])
return ( 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} /> <CommonHead meta={meta} siteInfo={siteInfo} />
@@ -67,12 +61,8 @@ const LayoutBase = props => {
</div> </div>
{/* 右下角悬浮 */} {/* 右下角悬浮 */}
<div className="bottom-12 right-2 fixed justify-end z-20"> <div className={ (show ? ' opacity-100 fixed ' : ' hidden opacity-0 ') + ' transition-all duration-200 bottom-12 right-2 justify-end z-20' }>
<div className={ <div className= ' justify-center flex flex-col items-center cursor-pointer '>
(show ? 'animate__animated ' : 'hidden') +
' animate__fadeInUp justify-center duration-300 animate__faster flex flex-col items-center cursor-pointer '
}
>
<JumpToTopButton /> <JumpToTopButton />
</div> </div>
</div> </div>

View File

@@ -1,4 +1,4 @@
import React from 'react' import React, { useCallback, useEffect } from 'react'
import { ArticleLock } from './components/ArticleLock' import { ArticleLock } from './components/ArticleLock'
import HeaderArticle from './components/HeaderArticle' import HeaderArticle from './components/HeaderArticle'
import LayoutBase from './LayoutBase' import LayoutBase from './LayoutBase'
@@ -9,23 +9,25 @@ import ArticleCopyright from './components/ArticleCopyright'
import { ArticleInfo } from './components/ArticleInfo' import { ArticleInfo } from './components/ArticleInfo'
import Catalog from './components/Catalog' import Catalog from './components/Catalog'
import JumpToCommentButton from './components/JumpToCommentButton' import JumpToCommentButton from './components/JumpToCommentButton'
import throttle from 'lodash.throttle'
export const LayoutSlug = props => { export const LayoutSlug = props => {
const { post, lock, validPassword } = props const { post, lock, validPassword } = props
const [show, switchShow] = React.useState(false) const [show, switchShow] = React.useState(false)
const throttleMs = 200
const scrollListener = () => { const scrollListener = useCallback(throttle(() => {
const scrollY = window.pageYOffset const scrollY = window.pageYOffset
const shouldShow = scrollY > 220 && post?.toc?.length > 0 const shouldShow = scrollY > 220 && post?.toc?.length > 0
if (shouldShow !== show) { if (shouldShow !== show) {
switchShow(shouldShow) switchShow(shouldShow)
} }
} }, throttleMs))
React.useEffect(() => { useEffect(() => {
document.addEventListener('scroll', scrollListener) document.addEventListener('scroll', scrollListener)
return () => document.removeEventListener('scroll', scrollListener) return () => document.removeEventListener('scroll', scrollListener)
}, [show]) }, [])
if (!post) { if (!post) {
return <LayoutBase return <LayoutBase
@@ -36,98 +38,92 @@ export const LayoutSlug = props => {
></LayoutBase> ></LayoutBase>
} }
return ( return (<LayoutBase
<LayoutBase headerSlot={<HeaderArticle {...props} />}
headerSlot={<HeaderArticle {...props} />} {...props}
{...props} showCategory={false}
showCategory={false} showTag={false}
showTag={false} >
>
<div id='inner-wrapper'> <div id='inner-wrapper'>
<div className={'drop-shadow-xl w-full lg:max-w-3xl 2xl:max-w-4xl'}> <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"> <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 && <ArticleLock validPassword={validPassword} />}
{!lock && <div id="container" className="overflow-x-auto md:w-full px-3 "> {!lock && <div id="container" className="overflow-x-auto md:w-full px-3 ">
{post?.type === 'Post' && <> {post?.type && post?.type === 'Post' && <>
<div <div
data-aos="fade-down" data-aos="fade-down"
data-aos-duration="500" data-aos-duration="100"
data-aos-easing="ease-in-out" data-aos-once="false"
data-aos-once="false" data-aos-anchor-placement="top-center"
data-aos-anchor-placement="top-center" className='px-10'>
className='px-10'> <ArticleInfo post={post} />
<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>
</div> </div>
<hr />
</>}
</div>} <div className='lg:px-10 subpixel-antialiased'>
</div> <article itemScope >
{post.type === 'Post' && <ArticleAdjacent {...props} />} {/* 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' > <section className="px-1 py-2 my-1 text-sm font-light overflow-auto text-gray-600 dark:text-gray-400">
<div data-aos-delay="200" {/* 文章内嵌广告 */}
data-aos="fade-down" <ins className="adsbygoogle"
data-aos-duration="500" style={{ display: 'block', textAlign: 'center' }}
data-aos-easing="ease-in-out" data-adtest="on"
data-aos-once="false" data-ad-layout="in-article"
data-aos-anchor-placement="top-center" data-ad-format="fluid"
className='relative h-full'> data-ad-client="ca-pub-2708419466378217"
<div className='float-right xl:-mr-72 xl:w-72 w-56 -mr-56 h-full mt-40'> data-ad-slot="3806269138" />
<div className='sticky top-24'> </section>
<Catalog toc={post.toc} />
</div> {/* 文章版权说明 */}
{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>} </div>}
</div> </div>
<div className='fixed bottom-28 right-4'> {/* 文章推荐 */}
<JumpToCommentButton /> {post.type === 'Post' && <ArticleAdjacent {...props} />}
</div>
{/* 文章目录 */}
{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> </div>
</LayoutBase> <div className='fixed bottom-28 right-4'>
<JumpToCommentButton />
</div>
</div>
</LayoutBase>
) )
} }

View File

@@ -25,7 +25,7 @@ export const ArticleLock = props => {
<div className='text-center space-y-3'> <div className='text-center space-y-3'>
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div> <div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
<div className='flex mx-4'> <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" > <div onClick={submitPassword} className="px-3 whitespace-nowrap cursor-pointer items-center justify-center py-2 bg-indigo-500 hover:bg-indigo-400 text-white rounded-r duration-300" >
<i className={'duration-200 cursor-pointer fas fa-key'} >&nbsp;{locale.COMMON.SUBMIT}</i> <i className={'duration-200 cursor-pointer fas fa-key'} >&nbsp;{locale.COMMON.SUBMIT}</i>
</div> </div>

View File

@@ -3,41 +3,45 @@ import Link from 'next/link'
import React from 'react' import React from 'react'
import TagItemMini from './TagItemMini' import TagItemMini from './TagItemMini'
import CONFIG_MATERY from '../config_matery' 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 const showPreview = CONFIG_MATERY.POST_LIST_PREVIEW && post.blockMap
// matery 主题默认强制显示图片 // matery 主题默认强制显示图片
if (post && !post.page_cover) { if (post && !post.page_cover) {
post.page_cover = siteInfo?.pageCover post.page_cover = siteInfo?.pageCover
} }
const showPageCover = CONFIG_MATERY.POST_LIST_COVER && !showPreview && post?.page_cover const showPageCover = CONFIG_MATERY.POST_LIST_COVER && !showPreview && post?.page_cover
const delay = (index % 3) * 300
return ( return (
<div <div
data-aos="zoom-in" data-aos="zoom-in"
data-aos-duration="300" data-aos-duration="500"
data-aos-easing="ease-in-out" data-aos-delay={delay}
data-aos-once="false" data-aos-once="true"
data-aos-anchor-placement="top-bottom" 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 && ( {showPageCover && (
<Link href={`${BLOG.SUB_PATH}/${post.slug}`} passHref legacyBehavior> <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 */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
src={post?.page_cover} src={post?.page_cover}
alt={post.title} alt={post.title}
className="opacity-50 h-full w-full hover:scale-125 rounded-t-md transform object-cover duration-500" 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> </div>
</Link> </Link>
)} )}
{/* 文字描述 */}
<div > <div >
{/* 描述 */} {/* 描述 */}
<div className="px-4 flex flex-col w-full text-gray-700 dark:text-gray-300"> <div className="px-4 flex flex-col w-full text-gray-700 dark:text-gray-300">

View File

@@ -21,9 +21,9 @@ const BlogPostListPage = ({ page = 1, posts = [], postCount, siteInfo }) => {
<div id="container" className='w-full'> <div id="container" className='w-full'>
<div className='pt-6'></div> <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 => ( {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> </div>
{showPagination && <PaginationSimple page={page} totalPage={totalPage} />} {showPagination && <PaginationSimple page={page} totalPage={totalPage} />}

View File

@@ -2,10 +2,10 @@ import BLOG from '@/blog.config'
import BlogPostCard from './BlogPostCard' import BlogPostCard from './BlogPostCard'
import BlogPostListEmpty from './BlogPostListEmpty' import BlogPostListEmpty from './BlogPostListEmpty'
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import throttle from 'lodash.throttle' import React, { useCallback } from 'react'
import React from 'react'
import CONFIG_MATERY from '../config_matery' import CONFIG_MATERY from '../config_matery'
import { getListByPage } from '@/lib/utils' import { getListByPage } from '@/lib/utils'
import throttle from 'lodash.throttle'
/** /**
* 博客列表滚动分页 * 博客列表滚动分页
@@ -30,15 +30,16 @@ const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = CONFIG_MA
updatePage(page + 1) updatePage(page + 1)
} }
// 监听滚动自动分页加载 const throttleMs = 200
const scrollTrigger = React.useCallback(throttle(() => { const scrollTrigger = useCallback(throttle(() => {
const scrollS = window.scrollY + window.outerHeight requestAnimationFrame(() => {
const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0 const scrollS = window.scrollY + window.outerHeight
if (scrollS > clientHeight + 100) { const clientHeight = targetRef ? (targetRef.current ? (targetRef.current.clientHeight) : 0) : 0
handleGetMore() if (scrollS > clientHeight + 100) {
} handleGetMore()
}, 500)) }
})
}, throttleMs))
// 监听滚动 // 监听滚动
React.useEffect(() => { React.useEffect(() => {
window.addEventListener('scroll', scrollTrigger) window.addEventListener('scroll', scrollTrigger)
@@ -56,9 +57,11 @@ const BlogPostListScroll = ({ posts = [], currentSearch, showSummary = CONFIG_MA
return <div id='container' ref={targetRef} className='w-full'> 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 => ( {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> </div>

View File

@@ -27,7 +27,7 @@ const Catalog = ({ toc }) => {
// 同步选中目录事件 // 同步选中目录事件
const [activeSection, setActiveSection] = React.useState(null) const [activeSection, setActiveSection] = React.useState(null)
const throttleMs = 100 const throttleMs = 200
const actionSectionScrollSpy = React.useCallback(throttle(() => { const actionSectionScrollSpy = React.useCallback(throttle(() => {
const sections = document.getElementsByClassName('notion-h') const sections = document.getElementsByClassName('notion-h')
let prevBBox = null let prevBBox = null

View File

@@ -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 Typed from 'typed.js'
import CONFIG_MATERY from '../config_matery' import CONFIG_MATERY from '../config_matery'
import throttle from 'lodash.throttle'
let wrapperTop = 0 let wrapperTop = 0
let windowTop = 0 let windowTop = 0
let autoScroll = false 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) window.addEventListener('resize', updateHeaderHeight)
return () => { return () => {
window.removeEventListener('scroll', scrollTrigger) if (enableAutoScroll) {
window.removeEventListener('resize', updateHeaderHeight) window.removeEventListener('scroll', scrollTrigger)
} window.removeEventListener('resize', updateHeaderHeight)
} }
}) }, [])
const autoScrollEnd = () => { const autoScrollEnd = () => {
if (autoScroll) { if (autoScroll) {
@@ -44,10 +52,10 @@ const Header = props => {
} }
/** /**
* 吸附滚动,移动端关闭 * 吸附滚动,移动端关闭
* @returns * @returns
*/ */
const scrollTrigger = () => { const scrollTrigger = useCallback(throttle(() => {
if (screen.width <= 768) { if (screen.width <= 768) {
return return
} }
@@ -67,36 +75,42 @@ const Header = props => {
setTimeout(autoScrollEnd, 500) setTimeout(autoScrollEnd, 500)
} }
windowTop = scrollS windowTop = scrollS
} }, throttleMs))
function updateHeaderHeight () { function updateHeaderHeight() {
setTimeout(() => { requestAnimationFrame(() => {
const wrapperElement = document.getElementById('wrapper') const wrapperElement = document.getElementById('wrapper')
wrapperTop = wrapperElement?.offsetTop wrapperTop = wrapperElement?.offsetTop
}, 500) })
} }
return ( return (
<header <header
id="header" id="header"
className="duration-500 md:bg-fixed w-full bg-cover bg-center h-screen bg-black text-white relative z-10" className="md:bg-fixed w-full h-screen bg-black text-white relative"
style={{ >
backgroundImage: <div className='w-full h-full absolute'>
`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}")` {/* <Image src={siteInfo.pageCover} fill
}} style={{ objectFit: 'cover' }}
> className='opacity-70'
<div className="absolute flex flex-col h-full items-center justify-center w-full "> placeholder='blur'
<div className='text-4xl md:text-5xl text-white shadow-text'>{siteInfo?.title}</div> blurDataURL='/bg_image.jpg' /> */}
<div className='mt-2 h-12 items-center text-center shadow-text text-white text-lg'> {/* eslint-disable-next-line @next/next/no-img-element */}
<span id='typed'/> <img src={siteInfo.pageCover} className='h-full w-full object-cover opacity-70 ' />
</div> </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> <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>
) )
} }

View File

@@ -1,20 +1,26 @@
import Image from 'next/image'
export default function HeaderArticle({ post, siteInfo }) { export default function HeaderArticle({ post, siteInfo }) {
const headerImage = post?.page_cover ? post?.page_cover : siteInfo?.pageCover const headerImage = post?.page_cover ? post?.page_cover : siteInfo?.pageCover
const title = post?.title const title = post?.title
return ( return (
<div <div
data-aos="fade-down" data-aos="fade-down"
data-aos-duration="500" data-aos-duration="300"
data-aos-easing="ease-in-out"
data-aos-once="false" data-aos-once="false"
data-aos-anchor-placement="top-center" 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"> 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 */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img {/* <img
src={headerImage} src={headerImage}
alt={title} alt={title}
className="opacity-50 dark:opacity-40 h-full w-full object-cover" 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> <span className='absolute text-white p-6 text-3xl'>{title}</span>
</div> </div>
) )

View File

@@ -10,22 +10,24 @@ const Progress = ({ targetRef, showPercent = true }) => {
const currentRef = targetRef?.current || targetRef const currentRef = targetRef?.current || targetRef
const [percent, changePercent] = useState(0) const [percent, changePercent] = useState(0)
const scrollListener = () => { const scrollListener = () => {
const target = currentRef || (isBrowser() && document.getElementById('container')) requestAnimationFrame(() => {
if (target) { const target = currentRef || (isBrowser() && document.getElementById('container'))
const clientHeight = target.clientHeight if (target) {
const scrollY = window.pageYOffset const clientHeight = target.clientHeight
const fullHeight = clientHeight - window.outerHeight const scrollY = window.pageYOffset
let per = parseFloat(((scrollY / fullHeight) * 100).toFixed(0)) const fullHeight = clientHeight - window.outerHeight
if (per > 100) per = 100 let per = parseFloat(((scrollY / fullHeight) * 100).toFixed(0))
if (per < 0) per = 0 if (per > 100) per = 100
changePercent(per) if (per < 0) per = 0
} changePercent(per)
}
})
} }
useEffect(() => { useEffect(() => {
document.addEventListener('scroll', scrollListener) document.addEventListener('scroll', scrollListener)
return () => document.removeEventListener('scroll', scrollListener) return () => document.removeEventListener('scroll', scrollListener)
}, [percent]) }, [])
return ( return (
<div className="h-4 w-full shadow-2xl bg-gray-400 "> <div className="h-4 w-full shadow-2xl bg-gray-400 ">

View File

@@ -69,7 +69,7 @@ const SearchInput = props => {
ref={searchInputRef} ref={searchInputRef}
type="text" type="text"
className={ 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} onKeyUp={handleKeyUp}
onCompositionStart={lockSearchInput} onCompositionStart={lockSearchInput}

View File

@@ -1,7 +1,6 @@
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import throttle from 'lodash.throttle'
import Link from 'next/link' import Link from 'next/link'
import { useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import CategoryGroup from './CategoryGroup' import CategoryGroup from './CategoryGroup'
import Logo from './Logo' import Logo from './Logo'
import SearchDrawer from './SearchDrawer' import SearchDrawer from './SearchDrawer'
@@ -9,6 +8,7 @@ import TagGroups from './TagGroups'
import MenuButtonGroupTop from './MenuButtonGroupTop' import MenuButtonGroupTop from './MenuButtonGroupTop'
import SideBarDrawer from '@/components/SideBarDrawer' import SideBarDrawer from '@/components/SideBarDrawer'
import SideBar from './SideBar' import SideBar from './SideBar'
import throttle from 'lodash.throttle'
let windowTop = 0 let windowTop = 0
@@ -22,36 +22,38 @@ const TopNav = props => {
const { locale } = useGlobal() const { locale } = useGlobal()
const searchDrawer = useRef() const searchDrawer = useRef()
const { isDarkMode } = useGlobal() 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(() => { if (navTransparent) {
const scrollS = window.scrollY nav && nav.classList.replace('bg-indigo-700', 'bg-none')
const nav = document.querySelector('#sticky-nav') nav && nav.classList.replace('text-black', 'text-white')
const header = document.querySelector('#header') nav && nav.classList.replace('drop-shadow-xl', 'shadow-none')
const showNav = scrollS <= windowTop || scrollS < 5 // 非首页无大图时影藏顶部 滚动条置顶时隐藏 nav && nav.classList.replace('dark:bg-hexo-black-gray', 'transparent')
// 是否将导航栏透明 } else {
const navTransparent = header && scrollS < 300 // 透明导航条的条件 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) { if (!showNav) {
nav && nav.classList.replace('bg-indigo-700', 'bg-none') nav && nav.classList.replace('top-0', '-top-20')
nav && nav.classList.replace('text-black', 'text-white') windowTop = scrollS
nav && nav.classList.replace('drop-shadow-xl', 'shadow-none') } else {
nav && nav.classList.replace('dark:bg-hexo-black-gray', 'transparent') nav && nav.classList.replace('-top-20', 'top-0')
} else { windowTop = scrollS
nav && nav.classList.replace('bg-none', 'bg-indigo-700') }
nav && nav.classList.replace('text-white', 'text-black') navDarkMode()
nav && nav.classList.replace('shadow-none', 'drop-shadow-xl') })
nav && nav.classList.replace('transparent', 'dark:bg-hexo-black-gray') }, throttleMs))
}
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)
const navDarkMode = () => { const navDarkMode = () => {
const nav = document.getElementById('sticky-nav') const nav = document.getElementById('sticky-nav')

View File

@@ -28,7 +28,7 @@ const LayoutBase = props => {
return ( return (
<ThemeGlobalMedium.Provider value={{ tocVisible, changeTocVisible }}> <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} /> <CommonHead meta={meta} />
<main id='wrapper' className={(BLOG.LAYOUT_SIDEBAR_REVERSE ? 'flex-row-reverse' : '') + 'relative flex justify-between w-full h-full mx-auto'}> <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 <div
data-aos="fade-up" data-aos="fade-up"
data-aos-duration="300" data-aos-duration="300"
data-aos-easing="ease-in-out"
data-aos-once="false" data-aos-once="false"
data-aos-anchor-placement="top-center" data-aos-anchor-placement="top-center"
className='fixed xl:right-80 right-2 mr-10 bottom-24 hidden lg:block z-20'> className='fixed xl:right-80 right-2 mr-10 bottom-24 hidden lg:block z-20'>

View File

@@ -26,7 +26,7 @@ export const ArticleLock = props => {
<div className='text-center space-y-3'> <div className='text-center space-y-3'>
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div> <div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
<div className='flex mx-4'> <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" > <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'} >&nbsp;{locale.COMMON.SUBMIT}</i> <i className={'duration-200 cursor-pointer fas fa-key'} >&nbsp;{locale.COMMON.SUBMIT}</i>
</div> </div>

View File

@@ -14,8 +14,7 @@ const BlogPostCard = ({ post, showSummary }) => {
<div <div
key={post.id} key={post.id}
data-aos="fade-up" data-aos="fade-up"
data-aos-duration="600" data-aos-duration="300"
data-aos-easing="ease-in-out"
data-aos-once="false" data-aos-once="false"
data-aos-anchor-placement="top-bottom" data-aos-anchor-placement="top-bottom"
className="mb-6 max-w-7xl border-b dark:border-gray-800 " className="mb-6 max-w-7xl border-b dark:border-gray-800 "

View File

@@ -26,7 +26,7 @@ const Catalog = ({ toc }) => {
} }
}, []) }, [])
const throttleMs = 100 const throttleMs = 200
const actionSectionScrollSpy = React.useCallback(throttle(() => { const actionSectionScrollSpy = React.useCallback(throttle(() => {
const sections = document.getElementsByClassName('notion-h') const sections = document.getElementsByClassName('notion-h')
let prevBBox = null let prevBBox = null

View File

@@ -4,7 +4,7 @@ import { useRouter } from 'next/router'
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import CONFIG_MEDIUM from '../config_medium' import CONFIG_MEDIUM from '../config_medium'
function GroupMenu ({ customNav }) { function GroupMenu ({ customMenu, customNav }) {
const { locale } = useGlobal() const { locale } = useGlobal()
const router = useRouter() const router = useRouter()
@@ -39,13 +39,13 @@ function GroupMenu ({ customNav }) {
{link.slot} {link.slot}
</Link> </Link>
); )
} else { } else {
return null return null
} }
})} })}
</nav> </nav>
); )
} }
export default GroupMenu export default GroupMenu

View File

@@ -25,7 +25,7 @@ const Progress = ({ targetRef, showPercent = true }) => {
useEffect(() => { useEffect(() => {
document.addEventListener('scroll', scrollListener) document.addEventListener('scroll', scrollListener)
return () => document.removeEventListener('scroll', scrollListener) return () => document.removeEventListener('scroll', scrollListener)
}, [percent]) }, [])
return ( return (
<div className="h-4 w-full shadow-2xl bg-hexo-light-gray dark:bg-black"> <div className="h-4 w-full shadow-2xl bg-hexo-light-gray dark:bg-black">

View File

@@ -61,7 +61,7 @@ const SearchInput = ({ currentTag, currentSearch, cRef, className }) => {
<input <input
ref={searchInputRef} ref={searchInputRef}
type='text' 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} onKeyUp={handleKeyUp}
onCompositionStart={lockSearchInput} onCompositionStart={lockSearchInput}
onCompositionUpdate={lockSearchInput} onCompositionUpdate={lockSearchInput}

View File

@@ -74,7 +74,7 @@ export default function TopNavBar(props) {
{link.slot} {link.slot}
</Link> </Link>
); )
} else { } else {
return null return null
} }
@@ -82,5 +82,5 @@ export default function TopNavBar(props) {
</div> </div>
</div> </div>
</div> </div>
); )
} }

View File

@@ -10,7 +10,6 @@ import TopNav from './components/TopNav'
import { useGlobal } from '@/lib/global' import { useGlobal } from '@/lib/global'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import smoothscroll from 'smoothscroll-polyfill'
import CONFIG_NEXT from './config_next' import CONFIG_NEXT from './config_next'
import Live2D from '@/components/Live2D' import Live2D from '@/components/Live2D'
import BLOG from '@/blog.config' import BLOG from '@/blog.config'
@@ -45,8 +44,6 @@ const LayoutBase = (props) => {
} }
React.useEffect(() => { React.useEffect(() => {
smoothscroll.polyfill()
// facebook messenger 插件需要调整右下角悬浮按钮的高度 // facebook messenger 插件需要调整右下角悬浮按钮的高度
const fb = document.getElementsByClassName('fb-customerchat') const fb = document.getElementsByClassName('fb-customerchat')
if (fb.length === 0) { if (fb.length === 0) {
@@ -59,7 +56,7 @@ const LayoutBase = (props) => {
return () => document.removeEventListener('scroll', scrollListener) return () => document.removeEventListener('scroll', scrollListener)
}, [show]) }, [show])
return (<> return (<div id='theme-next'>
<CommonHead meta={meta} /> <CommonHead meta={meta} />
@@ -90,7 +87,7 @@ const LayoutBase = (props) => {
</div> </div>
<Footer title={siteInfo?.title}/> <Footer title={siteInfo?.title}/>
</> </div>
) )
} }

View File

@@ -19,32 +19,23 @@ export const LayoutSlug = (props) => {
}} /></div> }} /></div>
: null : null
if (!post) { const rightAreaSlog = CONFIG_NEXT.RIGHT_LATEST_POSTS && <Card><LatestPostsGroup latestPosts={latestPosts} /></Card>
return <LayoutBase
{...props}
rightAreaSlot={
CONFIG_NEXT.RIGHT_LATEST_POSTS && <Card><LatestPostsGroup posts={latestPosts} /></Card>
}
/>
}
return ( return (
<LayoutBase <LayoutBase
{...props} {...props}
floatSlot={floatSlot} floatSlot={floatSlot}
rightAreaSlot={ rightAreaSlot={rightAreaSlog}
CONFIG_NEXT.RIGHT_LATEST_POSTS && <Card><LatestPostsGroup posts={latestPosts} /></Card>
}
> >
{!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} /> <TocDrawer post={post} cRef={drawerRight} targetRef={targetRef} />
</div> </div>}
</LayoutBase> </LayoutBase>
) )

View File

@@ -28,9 +28,8 @@ export default function ArticleDetail(props) {
return ( return (
<div id="container" <div id="container"
data-aos="fade-down" data-aos="fade-down"
data-aos-duration="600" data-aos-duration="300"
data-aos-easing="ease-in-out" data-aos-once="true"
data-aos-once="false"
data-aos-anchor-placement="top-bottom" data-aos-anchor-placement="top-bottom"
className="shadow md:hover:shadow-2xl overflow-x-auto flex-grow mx-auto w-screen md:w-full "> className="shadow md:hover:shadow-2xl overflow-x-auto flex-grow mx-auto w-screen md:w-full ">

View File

@@ -30,7 +30,7 @@ export const ArticleLock = props => {
<div className="flex mx-4"> <div className="flex mx-4">
<input <input
id="password" type='password' 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> ></input>
<div <div
onClick={submitPassword} onClick={submitPassword}

View File

@@ -1,9 +1,8 @@
const Card = ({ children, headerSlot, className }) => { const Card = ({ children, headerSlot, className }) => {
return <div return <div
data-aos="fade-down" data-aos="fade-down"
data-aos-duration="600" data-aos-duration="300"
data-aos-easing="ease-in-out" data-aos-once="true"
data-aos-once="false"
data-aos-anchor-placement="top-bottom" data-aos-anchor-placement="top-bottom"
className={className}> className={className}>
<>{headerSlot}</> <>{headerSlot}</>

View File

@@ -1,5 +1,4 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import smoothscroll from 'smoothscroll-polyfill'
import CONFIG_NEXT from '../config_next' import CONFIG_NEXT from '../config_next'
/** /**
@@ -11,12 +10,14 @@ import CONFIG_NEXT from '../config_next'
* @constructor * @constructor
*/ */
const JumpToBottomButton = ({ showPercent = false }) => { const JumpToBottomButton = ({ showPercent = false }) => {
if (!CONFIG_NEXT.WIDGET_TO_BOTTOM) {
return <></>
}
const [show, switchShow] = useState(false) const [show, switchShow] = useState(false)
const [percent, changePercent] = useState(0) const [percent, changePercent] = useState(0)
useEffect(() => {
document.addEventListener('scroll', scrollListener)
return () => document.removeEventListener('scroll', scrollListener)
}, [show])
const scrollListener = () => { const scrollListener = () => {
const targetRef = document.getElementById('wrapper') const targetRef = document.getElementById('wrapper')
const clientHeight = targetRef?.clientHeight const clientHeight = targetRef?.clientHeight
@@ -36,12 +37,9 @@ const JumpToBottomButton = ({ showPercent = false }) => {
window.scrollTo({ top: targetRef.clientHeight, behavior: 'smooth' }) window.scrollTo({ top: targetRef.clientHeight, behavior: 'smooth' })
} }
useEffect(() => { if (!CONFIG_NEXT.WIDGET_TO_BOTTOM) {
smoothscroll.polyfill() return <></>
}
document.addEventListener('scroll', scrollListener)
return () => document.removeEventListener('scroll', scrollListener)
}, [show])
return (<div className='flex space-x-1 transform hover:scale-105 duration-200 py-2 px-3' onClick={scrollToBottom} > 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' > <div className='dark:text-gray-200' >

View File

@@ -11,10 +11,10 @@ import CONFIG_NEXT from '../config_next'
* @constructor * @constructor
*/ */
const JumpToTopButton = ({ showPercent = true, percent }) => { const JumpToTopButton = ({ showPercent = true, percent }) => {
const { locale } = useGlobal()
if (!CONFIG_NEXT.WIDGET_TO_TOP) { if (!CONFIG_NEXT.WIDGET_TO_TOP) {
return <></> 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' })} > 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} > <div className='dark:text-gray-200' title={locale.POST.TOP} >
<i className='fa-arrow-up fas' /> <i className='fa-arrow-up fas' />

View File

@@ -18,8 +18,7 @@ const PaginationNumber = ({ page, totalPage }) => {
return ( return (
<div <div
data-aos="fade-down" data-aos="fade-down"
data-aos-duration="600" data-aos-duration="300"
data-aos-easing="ease-in-out"
data-aos-once="false" data-aos-once="false"
data-aos-anchor-placement="top-bottom" 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"> 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> </div>
</Link> </Link>
</div> </div>
); )
} }
function getPageElement(pagePrefix, page, currentPage) { function getPageElement(pagePrefix, page, currentPage) {
@@ -81,7 +80,7 @@ function getPageElement(pagePrefix, page, currentPage) {
{page} {page}
</Link>) </Link>)
); )
} }
function generatePages(pagePrefix, page, currentPage, totalPage) { function generatePages(pagePrefix, page, currentPage, totalPage) {
const pages = [] const pages = []

View File

@@ -17,8 +17,7 @@ const PaginationSimple = ({ page, showNext }) => {
return ( return (
<div <div
data-aos="fade-down" data-aos="fade-down"
data-aos-duration="600" data-aos-duration="300"
data-aos-easing="ease-in-out"
data-aos-once="false" data-aos-once="false"
data-aos-anchor-placement="top-bottom" data-aos-anchor-placement="top-bottom"
className="my-10 flex justify-between font-medium text-black dark:text-gray-100 space-x-2"> 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> </button>
</Link> </Link>
</div> </div>
); )
} }
export default PaginationSimple export default PaginationSimple

View File

@@ -25,7 +25,7 @@ const Progress = ({ targetRef, showPercent = true }) => {
useEffect(() => { useEffect(() => {
document.addEventListener('scroll', scrollListener) document.addEventListener('scroll', scrollListener)
return () => document.removeEventListener('scroll', scrollListener) return () => document.removeEventListener('scroll', scrollListener)
}, [percent]) }, [])
return ( return (
<div className="h-4 w-full shadow-2xl bg-gray-400 font-sans"> <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