mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-14 07:26:52 +00:00
提交部分页面
This commit is contained in:
BIN
public/bg_image.jpg
Executable file → Normal file
BIN
public/bg_image.jpg
Executable file → Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 261 KiB After Width: | Height: | Size: 167 KiB |
@@ -188,4 +188,8 @@ nav {
|
||||
width: 100%;
|
||||
background-image: linear-gradient(-180deg,rgba(255,255,255,0) 0%,#fff 70%);
|
||||
padding-bottom: 34px;
|
||||
}
|
||||
|
||||
.shadow-text{
|
||||
text-shadow: 0.1em 0.1em 0.2em black;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import 'prismjs'
|
||||
import 'prismjs/components/prism-bash'
|
||||
import 'prismjs/components/prism-javascript'
|
||||
@@ -5,6 +6,7 @@ import 'prismjs/components/prism-markup'
|
||||
import 'prismjs/components/prism-python'
|
||||
import 'prismjs/components/prism-typescript'
|
||||
import { Code, Collection, CollectionRow, Equation, NotionRenderer } from 'react-notion-x'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
const mapPageUrl = id => {
|
||||
return 'https://www.notion.so/' + id.replace(/-/g, '')
|
||||
@@ -12,7 +14,14 @@ const mapPageUrl = id => {
|
||||
|
||||
export const LayoutSlug = (props) => {
|
||||
const { post } = props
|
||||
return <div>
|
||||
const meta = {
|
||||
title: `${post.title} | ${BLOG.TITLE}`,
|
||||
description: post.summary,
|
||||
type: 'article',
|
||||
tags: post.tags
|
||||
}
|
||||
|
||||
return <LayoutBase {...props} meta={meta}>
|
||||
<h1>Slug - {post?.title}</h1>
|
||||
<p>
|
||||
{/* Notion文章主体 */}
|
||||
@@ -32,5 +41,5 @@ export const LayoutSlug = (props) => {
|
||||
</section>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</LayoutBase>
|
||||
}
|
||||
|
||||
@@ -1,41 +1,34 @@
|
||||
import CommonHead from '@/components/CommonHead'
|
||||
|
||||
import Footer from './components/Footer'
|
||||
import SideRight from './components/SideRight'
|
||||
|
||||
/**
|
||||
* 基础布局 采用左右两侧布局,移动端使用顶部导航栏
|
||||
* @param children
|
||||
* @param layout
|
||||
* @param tags
|
||||
* @param meta
|
||||
* @param post
|
||||
* @param currentSearch
|
||||
* @param currentCategory
|
||||
* @param currentTag
|
||||
* @param categories
|
||||
* @param props
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const LayoutBase = ({
|
||||
children,
|
||||
headerSlot,
|
||||
tags,
|
||||
meta,
|
||||
post,
|
||||
postCount,
|
||||
sideBarSlot,
|
||||
floatSlot,
|
||||
rightAreaSlot,
|
||||
currentSearch,
|
||||
currentCategory,
|
||||
currentTag,
|
||||
categories
|
||||
}) => {
|
||||
return (<>
|
||||
const LayoutBase = (props) => {
|
||||
const { children, headerSlot, meta } = props
|
||||
|
||||
return (<div className='bg-white'>
|
||||
<CommonHead meta={meta} />
|
||||
|
||||
{headerSlot}
|
||||
<main id='wrapper' className='flex justify-center flex-1 pb-12'>
|
||||
{children}
|
||||
|
||||
<main id='wrapper' className='flex w-full justify-center py-8 min-h-screen'>
|
||||
|
||||
<div id='container-inner' className='w-full mx-auto flex justify-between max-w-6xl'>
|
||||
{children}
|
||||
<SideRight {...props}/>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</>)
|
||||
|
||||
<Footer title={meta.title}/>
|
||||
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default LayoutBase
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import BlogPostListPage from './components/BlogPostListPage'
|
||||
import Header from './components/Header'
|
||||
import CONFIG_HEXO from './config_hexo'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutIndex = (props) => {
|
||||
return <LayoutBase {...props} headerSlot={CONFIG_HEXO.HOME_BANNER_ENABLE && <Header/>}>
|
||||
|
||||
<BlogPostListPage {...props}/>
|
||||
</LayoutBase>
|
||||
}
|
||||
|
||||
@@ -1,21 +1,84 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import formatDate from '@/lib/formatDate'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faFolderOpen } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import Link from 'next/link'
|
||||
import 'prismjs'
|
||||
import 'prismjs/components/prism-bash'
|
||||
import 'prismjs/components/prism-java'
|
||||
import 'prismjs/components/prism-javascript'
|
||||
import 'prismjs/components/prism-markup'
|
||||
import 'prismjs/components/prism-python'
|
||||
import 'prismjs/components/prism-typescript'
|
||||
import CONFIG_NEXT from '../NEXT/config_next'
|
||||
import ArticleDetail from './components/ArticleDetail'
|
||||
import Card from './components/Card'
|
||||
import LayoutBase from './LayoutBase'
|
||||
|
||||
export const LayoutSlug = ({
|
||||
post,
|
||||
tags,
|
||||
prev,
|
||||
next,
|
||||
recommendPosts,
|
||||
categories,
|
||||
postCount,
|
||||
latestPosts
|
||||
}) => {
|
||||
return <div>
|
||||
Slug
|
||||
</div>
|
||||
export const LayoutSlug = props => {
|
||||
const { post } = props
|
||||
const meta = {
|
||||
title: `${post.title} | ${BLOG.TITLE}`,
|
||||
description: post.summary,
|
||||
type: 'article',
|
||||
tags: post.tags
|
||||
}
|
||||
|
||||
const { locale } = useGlobal()
|
||||
const date = formatDate(
|
||||
post?.date?.start_date || post.createdTime,
|
||||
locale.LOCALE
|
||||
)
|
||||
|
||||
const headerSlot = (
|
||||
<div className="w-full h-96 relative md:flex-shrink-0 overflow-hidden bg-cover bg-center bg-no-repeat"
|
||||
style={{ backgroundImage: `url("/${CONFIG_NEXT.HOME_BANNER_IMAGE}")` }}>
|
||||
|
||||
<header className="animate__slideInDown animate__animated bg-black bg-opacity-50 absolute top-0 w-full h-96 py-10 flex justify-center items-center font-sans">
|
||||
<div>
|
||||
{/* 文章Title */}
|
||||
<div className="font-bold text-3xl shadow-text flex justify-center text-white dark:text-white font-sans">
|
||||
{post.title}
|
||||
</div>
|
||||
|
||||
<section className="flex-wrap shadow-text flex justify-center mt-2 text-white dark:text-gray-400 font-light leading-8">
|
||||
<div>
|
||||
<Link href={`/category/${post.category}`} passHref>
|
||||
<a className="cursor-pointer text-md mr-2 dark:hover:text-white border-b dark:border-gray-500 border-dashed">
|
||||
<FontAwesomeIcon icon={faFolderOpen} className="mr-1" />
|
||||
{post.category}
|
||||
</a>
|
||||
</Link>
|
||||
<span className="mr-2">|</span>
|
||||
|
||||
{post.type[0] !== 'Page' && (<>
|
||||
<Link
|
||||
href={`/archive#${post?.date?.start_date?.substr(0, 7)}`}
|
||||
passHref
|
||||
>
|
||||
<a className="pl-1 mr-2 cursor-pointer hover:underline border-b dark:border-gray-500 border-dashed">
|
||||
{date}
|
||||
</a>
|
||||
</Link>
|
||||
</>)}
|
||||
|
||||
<div className="hidden busuanzi_container_page_pv font-light mr-2">
|
||||
<span className="mr-2">|</span>
|
||||
<span className="mr-2 busuanzi_value_page_pv" />次访问
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<LayoutBase headerSlot={headerSlot} {...props} meta={meta}>
|
||||
<Card className="w-full">
|
||||
<ArticleDetail {...props} />
|
||||
</Card>
|
||||
</LayoutBase>
|
||||
)
|
||||
}
|
||||
|
||||
98
themes/Hexo/components/ArticleDetail.js
Normal file
98
themes/Hexo/components/ArticleDetail.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import Comment from '@/components/Comment'
|
||||
import mediumZoom from 'medium-zoom'
|
||||
import 'prismjs'
|
||||
import 'prismjs/components/prism-bash'
|
||||
import 'prismjs/components/prism-javascript'
|
||||
import 'prismjs/components/prism-markup'
|
||||
import 'prismjs/components/prism-python'
|
||||
import 'prismjs/components/prism-typescript'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { Code, Collection, CollectionRow, Equation, NotionRenderer } from 'react-notion-x'
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function ArticleDetail ({ post, recommendPosts, prev, next }) {
|
||||
const zoom = typeof window !== 'undefined' && mediumZoom({
|
||||
container: '.notion-viewport',
|
||||
background: 'rgba(0, 0, 0, 0.2)',
|
||||
margin: getMediumZoomMargin()
|
||||
})
|
||||
const zoomRef = useRef(zoom ? zoom.clone() : null)
|
||||
|
||||
useEffect(() => {
|
||||
// 将所有container下的所有图片添加medium-zoom
|
||||
const container = document?.getElementById('container')
|
||||
const imgList = container?.getElementsByTagName('img')
|
||||
if (imgList && zoomRef.current) {
|
||||
for (let i = 0; i < imgList.length; i++) {
|
||||
(zoomRef.current).attach(imgList[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return (<div id="container" className="max-w-5xl overflow-x-auto flex-grow mx-auto w-screen md:w-full md:px-5 ">
|
||||
<article itemScope itemType="https://schema.org/Movie"
|
||||
className="subpixel-antialiased dark:border-gray-700 bg-white dark:bg-gray-800"
|
||||
>
|
||||
|
||||
{/* Notion文章主体 */}
|
||||
<section id='notion-article' className='px-1'>
|
||||
{post.blockMap && (
|
||||
<NotionRenderer
|
||||
recordMap={post.blockMap}
|
||||
mapPageUrl={mapPageUrl}
|
||||
components={{
|
||||
equation: Equation,
|
||||
code: Code,
|
||||
collectionRow: CollectionRow,
|
||||
collection: Collection
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
|
||||
</article>
|
||||
|
||||
{/* 评论互动 */}
|
||||
<div className="duration-200 px-12 w-screen md:w-full overflow-x-auto bg-white dark:bg-gray-800">
|
||||
<div className='text-2xl mt-8 mx-8'>发表评论</div>
|
||||
<Comment frontMatter={post} />
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
|
||||
const mapPageUrl = id => {
|
||||
return 'https://www.notion.so/' + id.replace(/-/g, '')
|
||||
}
|
||||
|
||||
function getMediumZoomMargin () {
|
||||
const width = window.innerWidth
|
||||
|
||||
if (width < 500) {
|
||||
return 8
|
||||
} else if (width < 800) {
|
||||
return 20
|
||||
} else if (width < 1280) {
|
||||
return 30
|
||||
} else if (width < 1600) {
|
||||
return 40
|
||||
} else if (width < 1920) {
|
||||
return 48
|
||||
} else {
|
||||
return 72
|
||||
}
|
||||
}
|
||||
83
themes/Hexo/components/BlogPostCard.js
Normal file
83
themes/Hexo/components/BlogPostCard.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { faFolder } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
import { Code, Collection, CollectionRow, Equation, NotionRenderer } from 'react-notion-x'
|
||||
import TagItemMini from './TagItemMini'
|
||||
import CONFIG_HEXO from '../config_hexo'
|
||||
|
||||
const BlogPostCard = ({ post, showSummary }) => {
|
||||
const showPreview = CONFIG_HEXO.POST_LIST_PREVIEW && post.blockMap
|
||||
return (
|
||||
<div className='w-full shadow-xl hover:shadow-2xl border border-gray-100 rounded-xl bg-white dark:bg-gray-800 duration-300'>
|
||||
<div key={post.id} className='animate__animated animate__fadeIn flex flex-col-reverse lg:flex-row justify-between duration-300'>
|
||||
|
||||
<div className='lg:p-8 p-4 flex flex-col w-full'>
|
||||
<Link href={`${BLOG.PATH}/article/${post.slug}`} passHref>
|
||||
<a className={`cursor-pointer font-bold hover:underline text-3xl flex ${showPreview ? 'justify-center' : 'justify-start'} leading-tight text-gray-700 dark:text-gray-100 hover:text-blue-500 dark:hover:text-blue-400`}>
|
||||
{post.title}
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<div className={`flex mt-2 items-center ${showPreview ? 'justify-center' : 'justify-start'} flex-wrap dark:text-gray-500 text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 `}>
|
||||
<div>
|
||||
<Link href={`/archive#${post?.date?.start_date?.substr(0, 7)}`} passHref>
|
||||
<a className='font-light hover:underline cursor-pointer text-sm leading-4 mr-3'>{post.date.start_date}</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(!showPreview || showSummary) && <p className='mt-4 mb-24 text-gray-700 dark:text-gray-300 text-sm font-light leading-7'>
|
||||
{post.summary}
|
||||
</p>}
|
||||
|
||||
{showPreview && post?.blockMap && <div className='overflow-ellipsis truncate'>
|
||||
<NotionRenderer
|
||||
bodyClassName='max-h-full'
|
||||
recordMap={post.blockMap}
|
||||
mapPageUrl={mapPageUrl}
|
||||
components={{
|
||||
equation: Equation,
|
||||
code: Code,
|
||||
collectionRow: CollectionRow,
|
||||
collection: Collection
|
||||
}}
|
||||
/>
|
||||
</div> }
|
||||
|
||||
<div className='text-gray-400 justify-between flex'>
|
||||
|
||||
<Link href={`/category/${post.category}`} passHref>
|
||||
<a className='cursor-pointer font-light text-sm hover:underline transform'>
|
||||
<FontAwesomeIcon icon={faFolder} className='mr-1' />{post.category}
|
||||
</a>
|
||||
</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>
|
||||
|
||||
{CONFIG_HEXO.POST_LIST_COVER && post?.page_cover && (
|
||||
<Link href={`${BLOG.PATH}/article/${post.slug}`} passHref>
|
||||
<a className='w-full relative duration-200 rounded-t-xl lg:rounded-r-xl lg:rounded-t-none cursor-pointer transform overflow-hidden'>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
{/* <img src={post?.page_cover} alt={post.title} className='h-full object-cover'></img> */}
|
||||
<Image className='hover:scale-125 rounded-t-xl lg:rounded-r-xl lg:rounded-t-none transform duration-500' src={post?.page_cover} alt={post.title} layout='fill' objectFit='cover' loading='lazy' />
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</div >
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
const mapPageUrl = id => {
|
||||
return 'https://www.notion.so/' + id.replace(/-/g, '')
|
||||
}
|
||||
|
||||
export default BlogPostCard
|
||||
12
themes/Hexo/components/BlogPostListEmpty.js
Normal file
12
themes/Hexo/components/BlogPostListEmpty.js
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
/**
|
||||
* 空白博客 列表
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const BlogPostListEmpty = ({ currentSearch }) => {
|
||||
return <div className='flex items-center justify-center min-h-screen mx-auto md:-mt-20'>
|
||||
<p className='text-gray-500 dark:text-gray-300'>没有找到文章 {(currentSearch && <div>{currentSearch}</div>)}</p>
|
||||
</div>
|
||||
}
|
||||
export default BlogPostListEmpty
|
||||
34
themes/Hexo/components/BlogPostListPage.js
Normal file
34
themes/Hexo/components/BlogPostListPage.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import BlogPostCard from './BlogPostCard'
|
||||
import PaginationNumber from './PaginationNumber'
|
||||
import BLOG from '@/blog.config'
|
||||
import BlogPostListEmpty from './BlogPostListEmpty'
|
||||
|
||||
/**
|
||||
* 文章列表分页表格
|
||||
* @param page 当前页
|
||||
* @param posts 所有文章
|
||||
* @param tags 所有标签
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const BlogPostListPage = ({ page = 1, posts = [], postCount }) => {
|
||||
const totalPage = Math.ceil(postCount / BLOG.POSTS_PER_PAGE)
|
||||
|
||||
if (!posts || posts.length === 0) {
|
||||
return <BlogPostListEmpty />
|
||||
} else {
|
||||
return (
|
||||
<div id="container" className='w-full'>
|
||||
{/* 文章列表 */}
|
||||
<div className="flex lg:space-y-4 space-y-1">
|
||||
{posts.map(post => (
|
||||
<BlogPostCard key={post.id} post={post} />
|
||||
))}
|
||||
</div>
|
||||
<PaginationNumber page={page} totalPage={totalPage} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default BlogPostListPage
|
||||
9
themes/Hexo/components/Card.js
Normal file
9
themes/Hexo/components/Card.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const Card = ({ children, headerSlot, className }) => {
|
||||
return <div className={className}>
|
||||
<>{headerSlot}</>
|
||||
<section className="shadow-xl hover:shadow-2xl border border-gray-100 rounded-xl px-2 py-4 bg-white dark:bg-gray-800 duration-300">
|
||||
{children}
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
export default Card
|
||||
24
themes/Hexo/components/CategoryGroup.js
Normal file
24
themes/Hexo/components/CategoryGroup.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { faFolder, faFolderOpen } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
const CategoryGroup = ({ currentCategory, categories }) => {
|
||||
return <>
|
||||
<div id='category-list' className='dark:border-gray-600 flex flex-wrap font-sans mx-4'>
|
||||
{Object.keys(categories).map(category => {
|
||||
const selected = currentCategory === category
|
||||
return <Link key={category} href={`/category/${category}`} passHref>
|
||||
<a className={(selected
|
||||
? 'hover:text-white dark:hover:text-white bg-blue-600 text-white '
|
||||
: 'dark:text-gray-400 text-gray-500 hover:text-white dark:hover:text-white hover:bg-blue-600') +
|
||||
' text-sm w-full items-center duration-300 px-2 cursor-pointer py-1 font-light'}>
|
||||
<div> <FontAwesomeIcon icon={selected ? faFolderOpen : faFolder} className={'mr-2'} />{category}({categories[category]})</div>
|
||||
</a>
|
||||
</Link>
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
export default CategoryGroup
|
||||
30
themes/Hexo/components/Footer.js
Normal file
30
themes/Hexo/components/Footer.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { faCopyright, faEye, faShieldAlt, faUsers, faHeart } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import React from 'react'
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
const Footer = ({ title }) => {
|
||||
const d = new Date()
|
||||
const currentYear = d.getFullYear()
|
||||
const startYear = BLOG.SINCE && BLOG.SINCE !== currentYear && BLOG.SINCE + '-'
|
||||
return (
|
||||
<footer
|
||||
className='dark:bg-gray-900 flex-shrink-0 justify-center text-center m-auto w-full leading-6 text-gray-400 text-sm p-6'
|
||||
>
|
||||
<FontAwesomeIcon icon={faCopyright} /> {`${startYear}${currentYear}`} <span><FontAwesomeIcon icon={faHeart} className='mx-1 animate-pulse'/> <a href={BLOG.LINK} className='underline font-bold text-gray-500 dark:text-gray-300 '>{BLOG.AUTHOR}</a>.
|
||||
<br/>
|
||||
|
||||
<span>Powered by <a href='https://notion.so' className='underline font-bold text-gray-500 dark:text-gray-300'>Notion</a> & <a href='https://github.com/tangly1024/NotionNext' className='underline font-bold text-gray-500 dark:text-gray-300'>NotionNext</a>.</span></span>
|
||||
|
||||
{BLOG.BEI_AN && <><br /><FontAwesomeIcon icon={faShieldAlt} /> <a href='https://beian.miit.gov.cn/' className='mr-2'>{BLOG.BEI_AN}</a><br/></>}
|
||||
<span className='hidden busuanzi_container_site_pv'>
|
||||
<FontAwesomeIcon icon={faEye}/><span className='px-1 busuanzi_value_site_pv'> </span> </span>
|
||||
<span className='pl-2 hidden busuanzi_container_site_uv'>
|
||||
<FontAwesomeIcon icon={faUsers}/> <span className='px-1 busuanzi_value_site_uv'> </span> </span>
|
||||
<br/>
|
||||
<h1>{title}</h1>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer
|
||||
@@ -77,7 +77,7 @@ export default function Header () {
|
||||
setTimeout(() => {
|
||||
if (window) {
|
||||
const wrapperElement = document.getElementById('wrapper')
|
||||
wrapperTop = wrapperElement.offsetTop
|
||||
wrapperTop = wrapperElement?.offsetTop
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
@@ -103,7 +103,7 @@ export default function Header () {
|
||||
}}
|
||||
>
|
||||
<div className="absolute flex h-full items-center lg:-mt-14 justify-center w-full text-4xl md:text-7xl text-white">
|
||||
<div id='typed' className='flex text-center font-serif'/>
|
||||
<div id='typed' className='flex text-center font-sans shadow-text'/>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
|
||||
42
themes/Hexo/components/LatestPostsGroup.js
Normal file
42
themes/Hexo/components/LatestPostsGroup.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { faArchive, faFileAlt } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
/**
|
||||
* 最新文章列表
|
||||
* @param posts 所有文章数据
|
||||
* @param sliceCount 截取展示的数量 默认6
|
||||
* @constructor
|
||||
*/
|
||||
const LatestPostsGroup = ({ posts }) => {
|
||||
if (!posts) {
|
||||
return <></>
|
||||
}
|
||||
// 获取当前路径
|
||||
const currentPath = useRouter().asPath
|
||||
const { locale } = useGlobal()
|
||||
|
||||
return <>
|
||||
<div className='text-xs mb-2 px-1 flex flex-nowrap justify-between'>
|
||||
<div className='font-light text-gray-600 dark:text-gray-200'><FontAwesomeIcon icon={faArchive} className='mr-2' />{locale.COMMON.LATEST_POSTS}</div>
|
||||
</div>
|
||||
{posts.map(post => {
|
||||
const selected = currentPath === `${BLOG.PATH}/article/${post.slug}`
|
||||
return (
|
||||
<Link key={post.id} title={post.title} href={`${BLOG.PATH}/article/${post.slug}`} passHref>
|
||||
<a className={ 'my-1 mx-4 flex font-light'}>
|
||||
<div className={ (selected ? 'text-white bg-blue-600 ' : 'text-gray-500 dark:text-gray-400 ') + ' text-xs py-1.5 flex overflow-x-hidden whitespace-nowrap hover:bg-blue-600 px-2 duration-200 w-full ' +
|
||||
'hover:text-white dark:hover:text-white cursor-pointer' }>
|
||||
<FontAwesomeIcon icon={faFileAlt} className='mr-2'/>
|
||||
<div className='truncate'>{post.title}</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
}
|
||||
export default LatestPostsGroup
|
||||
41
themes/Hexo/components/MenuButtonGroup.js
Normal file
41
themes/Hexo/components/MenuButtonGroup.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faArchive, faHome, faTag, faTh, faUser } from '@fortawesome/free-solid-svg-icons'
|
||||
import CONFIG_HEXO from '../config_hexo'
|
||||
|
||||
const MenuButtonGroup = ({ postCount }) => {
|
||||
const { locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
const archiveSlot = <div className='bg-blue-300 dark:bg-blue-500 rounded-md text-gray-50 px-1 text-xs'>{postCount}</div>
|
||||
|
||||
const links = [
|
||||
{ id: 0, icon: faHome, name: locale.NAV.INDEX, to: '/' || '/', show: true },
|
||||
{ id: 1, icon: faTh, name: locale.COMMON.CATEGORY, to: '/category', show: CONFIG_HEXO.MENU_CATEGORY },
|
||||
{ id: 2, icon: faTag, name: locale.COMMON.TAGS, to: '/tag', show: CONFIG_HEXO.MENU_TAG },
|
||||
{ id: 3, icon: faArchive, name: locale.NAV.ARCHIVE, to: '/archive', slot: archiveSlot, show: CONFIG_HEXO.MENU_ARCHIVE },
|
||||
{ id: 4, icon: faUser, name: locale.NAV.ABOUT, to: '/about', show: CONFIG_HEXO.MENU_ABOUT }
|
||||
]
|
||||
return <nav id='nav' className='leading-8 text-gray-500 dark:text-gray-400 font-sans w-full'>
|
||||
{links.map(link => {
|
||||
if (link.show) {
|
||||
const selected = (router.pathname === link.to) || (router.asPath === link.to)
|
||||
return <Link key={`${link.id}-${link.to}`} title={link.to} href={link.to} >
|
||||
<a className={'py-1.5 my-1 px-5 duration-300 text-base justify-between hover:bg-blue-600 rounded-lg hover:text-white hover:shadow-lg cursor-pointer font-light flex flex-nowrap items-center ' +
|
||||
(selected ? 'bg-blue-600 text-white' : ' ')} >
|
||||
<div className='my-auto items-center justify-center flex '>
|
||||
<FontAwesomeIcon icon={link.icon} />
|
||||
<div className={'ml-4'}>{link.name}</div>
|
||||
</div>
|
||||
{link.slot}
|
||||
</a>
|
||||
</Link>
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})}
|
||||
</nav>
|
||||
}
|
||||
export default MenuButtonGroup
|
||||
94
themes/Hexo/components/PaginationNumber.js
Normal file
94
themes/Hexo/components/PaginationNumber.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faAngleLeft, faAngleRight } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
/**
|
||||
* 数字翻页插件
|
||||
* @param page 当前页码
|
||||
* @param showNext 是否有下一页
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const PaginationNumber = ({ page, totalPage }) => {
|
||||
const router = useRouter()
|
||||
const currentPage = +page
|
||||
const showNext = page !== totalPage
|
||||
const pages = generatePages(page, currentPage, totalPage)
|
||||
|
||||
return (
|
||||
<div className='my-5 flex justify-center items-end font-medium text-black duration-500 bg-white dark:bg-blue-700 dark:text-gray-300 py-3 space-x-2'>
|
||||
|
||||
{/* 上一页 */}
|
||||
<Link
|
||||
href={ {
|
||||
pathname: (currentPage - 1 === 1 ? `${BLOG.PATH || '/'}` : `/page/${currentPage - 1}`), query: router.query.s ? { s: router.query.s } : {}
|
||||
} } passHref >
|
||||
<div
|
||||
rel='prev'
|
||||
className={`${currentPage === 1 ? 'invisible' : 'block'} border-white dark:border-gray-700 hover:border-gray-400 dark:hover:border-gray-400 w-6 text-center cursor-pointer duration-200 hover:font-bold`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAngleLeft}/>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{pages}
|
||||
|
||||
{/* 下一页 */}
|
||||
<Link href={ { pathname: `/page/${currentPage + 1}`, query: router.query.s ? { s: router.query.s } : {} } } passHref>
|
||||
<div
|
||||
rel='next'
|
||||
className={`${+showNext ? 'block' : 'invisible'} border-t-2 border-white dark:border-gray-700 hover:border-gray-400 dark:hover:border-gray-400 w-6 text-center cursor-pointer duration-500 hover:font-bold`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAngleRight}/>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getPageElement (page, currentPage) {
|
||||
return <Link href={page === 1 ? '/' : `/page/${page}`} key={page} passHref>
|
||||
<a className={(page + '' === currentPage + '' ? 'font-bold bg-blue-500 dark:bg-blue-400 text-white ' : 'border-t-2 duration-500 border-white hover:border-gray-400 ') +
|
||||
' border-white dark:border-gray-700 dark:hover:border-gray-400 cursor-pointer w-6 text-center font-light hover:font-bold'}>
|
||||
{page}
|
||||
</a>
|
||||
</Link>
|
||||
}
|
||||
function generatePages (page, currentPage, totalPage) {
|
||||
const pages = []
|
||||
const groupCount = 7 // 最多显示页签数
|
||||
if (totalPage <= groupCount) {
|
||||
for (let i = 1; i <= totalPage; i++) {
|
||||
pages.push(getPageElement(i, page))
|
||||
}
|
||||
} else {
|
||||
pages.push(getPageElement(1, page))
|
||||
const dynamicGroupCount = groupCount - 2
|
||||
let startPage = currentPage - 2
|
||||
if (startPage <= 1) {
|
||||
startPage = 2
|
||||
}
|
||||
if (startPage + dynamicGroupCount > totalPage) {
|
||||
startPage = totalPage - dynamicGroupCount
|
||||
}
|
||||
if (startPage > 2) {
|
||||
pages.push(<div key={-1}>... </div>)
|
||||
}
|
||||
|
||||
for (let i = 0; i < dynamicGroupCount; i++) {
|
||||
if (startPage + i < totalPage) {
|
||||
pages.push(getPageElement(startPage + i, page))
|
||||
}
|
||||
}
|
||||
|
||||
if (startPage + dynamicGroupCount < totalPage) {
|
||||
pages.push(<div key={-2}>... </div>)
|
||||
}
|
||||
|
||||
pages.push(getPageElement(totalPage, page))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
export default PaginationNumber
|
||||
68
themes/Hexo/components/SearchInput.js
Normal file
68
themes/Hexo/components/SearchInput.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useImperativeHandle, useRef, useState } from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faSearch, faSpinner, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
const SearchInput = ({ currentTag, currentSearch, cRef }) => {
|
||||
const [searchKey, setSearchKey] = useState(currentSearch || '')
|
||||
const [onLoading, setLoadingState] = useState(false)
|
||||
const router = useRouter()
|
||||
const searchInputRef = useRef()
|
||||
useImperativeHandle(cRef, () => {
|
||||
return {
|
||||
focus: () => {
|
||||
searchInputRef?.current?.focus()
|
||||
}
|
||||
}
|
||||
})
|
||||
const handleSearch = (key) => {
|
||||
if (key && key !== '') {
|
||||
setLoadingState(true)
|
||||
router.push({ pathname: '/search', query: { s: key } }).then(r => {
|
||||
setLoadingState(false)
|
||||
})
|
||||
} else {
|
||||
router.push({ pathname: '/' }).then(r => {
|
||||
})
|
||||
}
|
||||
}
|
||||
const handleKeyUp = (e) => {
|
||||
if (e.keyCode === 13) { // 回车
|
||||
handleSearch(searchInputRef.current.value)
|
||||
} else if (e.keyCode === 27) { // ESC
|
||||
cleanSearch()
|
||||
}
|
||||
}
|
||||
const cleanSearch = () => {
|
||||
searchInputRef.current.value = ''
|
||||
setSearchKey('')
|
||||
}
|
||||
|
||||
const updateSearchKey = (val) => {
|
||||
setSearchKey(val)
|
||||
}
|
||||
|
||||
return <div className='flex'>
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type='text'
|
||||
className={'w-full rounded-lg 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}
|
||||
onChange={e => updateSearchKey(e.target.value)}
|
||||
defaultValue={searchKey}
|
||||
/>
|
||||
|
||||
<div className='-ml-8 cursor-pointer dark:bg-gray-600 dark:hover:bg-gray-800 float-right items-center justify-center py-2'
|
||||
onClick={() => { handleSearch(searchKey) }}>
|
||||
<FontAwesomeIcon spin={onLoading} icon={onLoading ? faSpinner : faSearch} className='hover:text-black transform duration-200 text-gray-500 cursor-pointer' />
|
||||
</div>
|
||||
|
||||
{(searchKey && searchKey.length &&
|
||||
<div className='-ml-12 cursor-pointer dark:bg-gray-600 dark:hover:bg-gray-800 float-right items-center justify-center py-2'>
|
||||
<FontAwesomeIcon icon={faTimes} className='hover:text-black transform duration-200 text-gray-400 cursor-pointer' onClick={cleanSearch} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default SearchInput
|
||||
64
themes/Hexo/components/SideRight.js
Normal file
64
themes/Hexo/components/SideRight.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import Router from 'next/router'
|
||||
import Image from 'next/image'
|
||||
import BLOG from '@/blog.config'
|
||||
import Card from './Card'
|
||||
import MenuButtonGroup from './MenuButtonGroup'
|
||||
import SearchInput from './SearchInput'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faChartArea, faTh } from '@fortawesome/free-solid-svg-icons'
|
||||
import CategoryGroup from './CategoryGroup'
|
||||
import LatestPostsGroup from './LatestPostsGroup'
|
||||
import TagGroups from './TagGroups'
|
||||
import SocialButton from './SocialButton'
|
||||
|
||||
export default function SideRight (props) {
|
||||
const { postCount, currentCategory, categories, latestPosts, tags, currentTag } = props
|
||||
return <div id='left' className='w-96 mx-4 space-y-4 hidden lg:block'>
|
||||
<Card>
|
||||
<div className='justify-center items-center flex hover:rotate-45 py-6 hover:scale-105 transform duration-200 cursor-pointer' onClick={ () => { Router.push('/') }}>
|
||||
<Image
|
||||
alt={BLOG.AUTHOR}
|
||||
width={120}
|
||||
height={120}
|
||||
loading='lazy'
|
||||
src='/avatar.jpg'
|
||||
className='rounded-full'
|
||||
/>
|
||||
</div>
|
||||
<div className='text-center text-xl pb-4'>{BLOG.TITLE}</div>
|
||||
<SocialButton/>
|
||||
</Card>
|
||||
<Card>
|
||||
<MenuButtonGroup/>
|
||||
<SearchInput/>
|
||||
</Card>
|
||||
<Card>
|
||||
<div className='text-xs font-light ml-2 mb-3 font-sans'>
|
||||
<FontAwesomeIcon icon={faChartArea}/> 统计
|
||||
</div>
|
||||
<div className='text-xs font-sans font-light justify-center mx-6'>
|
||||
<div className='inline'>
|
||||
<div className='flex justify-between'><div>文章数:</div> <div>{postCount}</div></div>
|
||||
</div>
|
||||
<div className="hidden busuanzi_container_page_pv ml-2">
|
||||
<div className='flex justify-between'><div>访问量:</div><div className="busuanzi_value_page_pv"/></div>
|
||||
</div>
|
||||
<div className="hidden busuanzi_container_site_uv ml-2">
|
||||
<div className='flex justify-between'><div>访客数:</div><div className="busuanzi_value_site_uv"/></div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div className='text-xs font-light ml-2 mb-1 font-sans'>
|
||||
<FontAwesomeIcon icon={faTh}/> 分类
|
||||
</div>
|
||||
<CategoryGroup currentCategory={currentCategory} categories={categories}/>
|
||||
</Card>
|
||||
<Card>
|
||||
<TagGroups tags={tags} currentTag={currentTag}/>
|
||||
</Card>
|
||||
<Card>
|
||||
<LatestPostsGroup posts={latestPosts}/>
|
||||
</Card>
|
||||
</div>
|
||||
}
|
||||
36
themes/Hexo/components/SocialButton.js
Normal file
36
themes/Hexo/components/SocialButton.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { faGithub, faTelegram, faTwitter, faWeibo } from '@fortawesome/free-brands-svg-icons'
|
||||
import { faEnvelope, faRss } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* 社交联系方式按钮组
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const SocialButton = () => {
|
||||
return <div className='w-full justify-center flex-wrap flex'>
|
||||
<div className='space-x-3 text-xl text-gray-600 dark:text-gray-400 '>
|
||||
{BLOG.CONTACT_GITHUB && <a target='_blank' rel='noreferrer' title={'github'} href={BLOG.CONTACT_GITHUB} >
|
||||
<FontAwesomeIcon icon={faGithub} className='transform hover:scale-125 duration-150'/>
|
||||
</a>}
|
||||
{BLOG.CONTACT_TWITTER && <a target='_blank' rel='noreferrer' title={'twitter'} href={BLOG.CONTACT_TWITTER} >
|
||||
<FontAwesomeIcon icon={faTwitter} className='transform hover:scale-125 duration-150'/>
|
||||
</a>}
|
||||
{BLOG.CONTACT_TELEGRAM && <a target='_blank' rel='noreferrer' href={BLOG.CONTACT_TELEGRAM} title={'telegram'} >
|
||||
<FontAwesomeIcon icon={faTelegram} className='transform hover:scale-125 duration-150'/>
|
||||
</a>}
|
||||
{BLOG.CONTACT_WEIBO && <a target='_blank' rel='noreferrer' title={'weibo'} href={BLOG.CONTACT_WEIBO} >
|
||||
<FontAwesomeIcon icon={faWeibo} className='transform hover:scale-125 duration-150'/>
|
||||
</a>}
|
||||
{BLOG.CONTACT_EMAIL && <a target='_blank' rel='noreferrer' title={'email'} href={`mailto:${BLOG.CONTACT_EMAIL}`} >
|
||||
<FontAwesomeIcon icon={faEnvelope} className='transform hover:scale-125 duration-150'/>
|
||||
</a>}
|
||||
<a target='_blank' rel='noreferrer' title={'RSS'} href={'/feed'} >
|
||||
<FontAwesomeIcon icon={faRss} className='transform hover:scale-125 duration-150'/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
export default SocialButton
|
||||
29
themes/Hexo/components/TagGroups.js
Normal file
29
themes/Hexo/components/TagGroups.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { faTag } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import TagItemMini from './TagItemMini'
|
||||
|
||||
/**
|
||||
* 标签组
|
||||
* @param tags
|
||||
* @param currentTag
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const TagGroups = ({ tags, currentTag }) => {
|
||||
if (!tags) return <></>
|
||||
return (
|
||||
<div id='tags-group' className='dark:border-gray-600 space-y-2'>
|
||||
<div className='font-light text-xs ml-2 mb-2'><FontAwesomeIcon icon={faTag} className='mr-1' />标签</div>
|
||||
<div className='px-4'>
|
||||
{
|
||||
tags.map(tag => {
|
||||
const selected = tag.name === currentTag
|
||||
return <TagItemMini key={tag.name} tag={tag} selected={selected} />
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagGroups
|
||||
17
themes/Hexo/components/TagItemMini.js
Normal file
17
themes/Hexo/components/TagItemMini.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { faTag } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import Link from 'next/link'
|
||||
|
||||
const TagItemMini = ({ tag, selected = false }) => {
|
||||
return <Link key={tag} href={selected ? '/' : `/tag/${encodeURIComponent(tag.name)}`} passHref>
|
||||
<a className={`cursor-pointer inline-block rounded hover:bg-blue-500 hover:text-white duration-200
|
||||
mr-2 py-0.5 px-1 text-xs whitespace-nowrap dark:hover:text-white
|
||||
${selected
|
||||
? 'text-white dark:text-gray-300 bg-black dark:bg-black dark:hover:bg-blue-900'
|
||||
: `text-gray-600 hover:shadow-xl dark:border-gray-400 notion-${tag.color}_background dark:bg-blue-800`}` }>
|
||||
<div className='font-light dark:text-gray-400'>{selected && <FontAwesomeIcon icon={faTag} className='mr-1'/>} {tag.name + (tag.count ? `(${tag.count})` : '')} </div>
|
||||
</a>
|
||||
</Link>
|
||||
}
|
||||
|
||||
export default TagItemMini
|
||||
@@ -1,6 +1,15 @@
|
||||
const CONFIG_HEXO = {
|
||||
HOME_BANNER_ENABLE: true,
|
||||
HOME_BANNER_GREETINGS: ['Hi,我是一个程序员', 'Hi,我是一个打工人', 'Hi,我是一个干饭人', '欢迎来到我的博客🎉'], // 首页大图标语文字
|
||||
HOME_BANNER_IMAGE: './bg_image.jpg' // see /public/bg_image.jpg
|
||||
HOME_BANNER_IMAGE: './bg_image.jpg', // see /public/bg_image.jpg
|
||||
|
||||
// 菜单
|
||||
MENU_ABOUT: false, // 显示关于
|
||||
MENU_CATEGORY: true, // 显示分类
|
||||
MENU_TAG: true, // 显示标签
|
||||
MENU_ARCHIVE: true, // 显示归档
|
||||
MENU_SEARCH: true, // 显示搜索
|
||||
|
||||
POST_LIST_COVER: true // 文章封面
|
||||
}
|
||||
export default CONFIG_HEXO
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 修改 from 后面的路径,实现主题切换
|
||||
*/
|
||||
|
||||
// export * from './NEXT' // 切换主题
|
||||
// export * from './Empty' // 空主题
|
||||
// export * from './NEXT'
|
||||
// export * from './Fukasawa'
|
||||
// export * from './Empty'
|
||||
export * from './Hexo'
|
||||
export * from './Hexo' //
|
||||
|
||||
Reference in New Issue
Block a user