mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-06-04 23:16:53 +00:00
自动保存密码,并添加解密通知弹框
This commit is contained in:
59
components/Notification.js
Normal file
59
components/Notification.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
const useNotification = () => {
|
||||||
|
const [message, setMessage] = useState('')
|
||||||
|
const [isVisible, setIsVisible] = useState(false)
|
||||||
|
|
||||||
|
const showNotification = msg => {
|
||||||
|
setMessage(msg)
|
||||||
|
setIsVisible(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsVisible(false)
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeNotification = () => {
|
||||||
|
setIsVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试通知效果
|
||||||
|
// const toggleVisible = () => {
|
||||||
|
// setIsVisible(prev => !prev) // 使用函数式更新
|
||||||
|
// }
|
||||||
|
// useEffect(() => {
|
||||||
|
// document?.addEventListener('click', toggleVisible)
|
||||||
|
// return () => {
|
||||||
|
// document?.removeEventListener('click', toggleVisible)
|
||||||
|
// }
|
||||||
|
// }, [])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知组件
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const Notification = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`notification fixed left-0 w-full px-2 z-50 transform transition-all duration-300 ${
|
||||||
|
isVisible ? 'bottom-20 animate__animated animate__fadeIn' : ''
|
||||||
|
} `}>
|
||||||
|
<div className='max-w-3xl mx-auto bg-green-500 flex items-center justify-between px-4 py-2 text-white rounded-lg shadow-lg'>
|
||||||
|
{message}
|
||||||
|
<button
|
||||||
|
onClick={closeNotification}
|
||||||
|
className='ml-4 p-2 cursor-pointer bg-transparent text-white border-none'>
|
||||||
|
<i className='fas fa-times' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
showNotification,
|
||||||
|
closeNotification,
|
||||||
|
Notification
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useNotification
|
||||||
@@ -37,11 +37,13 @@ export default {
|
|||||||
ARTICLE: 'Article',
|
ARTICLE: 'Article',
|
||||||
VISITORS: 'Visitors',
|
VISITORS: 'Visitors',
|
||||||
VIEWS: 'Views',
|
VIEWS: 'Views',
|
||||||
COPYRIGHT_NOTICE: 'All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!',
|
COPYRIGHT_NOTICE:
|
||||||
|
'All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!',
|
||||||
RESULT_OF_SEARCH: 'Results Found',
|
RESULT_OF_SEARCH: 'Results Found',
|
||||||
ARTICLE_DETAIL: 'Article Details',
|
ARTICLE_DETAIL: 'Article Details',
|
||||||
PASSWORD_ERROR: 'Password Error!',
|
PASSWORD_ERROR: 'Password Error!',
|
||||||
ARTICLE_LOCK_TIPS: 'Please Enter the password:',
|
ARTICLE_LOCK_TIPS: 'Please Enter the password:',
|
||||||
|
ARTICLE_UNLOCK_TIPS: 'Article Unlocked',
|
||||||
NO_RESULTS_FOUND: 'No results found.',
|
NO_RESULTS_FOUND: 'No results found.',
|
||||||
SUBMIT: 'Submit',
|
SUBMIT: 'Submit',
|
||||||
POST_TIME: 'Post on',
|
POST_TIME: 'Post on',
|
||||||
|
|||||||
@@ -29,11 +29,14 @@ export default {
|
|||||||
ARTICLE: '記事',
|
ARTICLE: '記事',
|
||||||
VISITORS: '人の訪問者',
|
VISITORS: '人の訪問者',
|
||||||
VIEWS: '回の閲覧',
|
VIEWS: '回の閲覧',
|
||||||
COPYRIGHT_NOTICE: 'この記事はCC BY-NC-SA 4.0 ライセンスの下でライセンスされています。転載する場合には出典を明らかにしてください。',
|
COPYRIGHT_NOTICE:
|
||||||
|
'この記事はCC BY-NC-SA 4.0 ライセンスの下でライセンスされています。転載する場合には出典を明らかにしてください。',
|
||||||
RESULT_OF_SEARCH: '個の検索結果',
|
RESULT_OF_SEARCH: '個の検索結果',
|
||||||
ARTICLE_DETAIL: '記事の詳細',
|
ARTICLE_DETAIL: '記事の詳細',
|
||||||
PASSWORD_ERROR: 'パスワードが違います!',
|
PASSWORD_ERROR: 'パスワードが違います!',
|
||||||
ARTICLE_LOCK_TIPS: 'この記事はロックされています。アクセスパスワードを入力してください。',
|
ARTICLE_LOCK_TIPS:
|
||||||
|
'この記事はロックされています。アクセスパスワードを入力してください。',
|
||||||
|
ARTICLE_UNLOCK_TIPS: '記事がロック解除されました',
|
||||||
SUBMIT: '送信',
|
SUBMIT: '送信',
|
||||||
POST_TIME: '公開日',
|
POST_TIME: '公開日',
|
||||||
LAST_EDITED_TIME: '最終更新日',
|
LAST_EDITED_TIME: '最終更新日',
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export default {
|
|||||||
ARTICLE_DETAIL: '文章详情',
|
ARTICLE_DETAIL: '文章详情',
|
||||||
PASSWORD_ERROR: '密码错误!',
|
PASSWORD_ERROR: '密码错误!',
|
||||||
ARTICLE_LOCK_TIPS: '文章已上锁,请输入访问密码',
|
ARTICLE_LOCK_TIPS: '文章已上锁,请输入访问密码',
|
||||||
|
ARTICLE_UNLOCK_TIPS: '文章已解锁',
|
||||||
SUBMIT: '提交',
|
SUBMIT: '提交',
|
||||||
POST_TIME: '发布于',
|
POST_TIME: '发布于',
|
||||||
LAST_EDITED_TIME: '最后更新',
|
LAST_EDITED_TIME: '最后更新',
|
||||||
|
|||||||
37
lib/password.js
Normal file
37
lib/password.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { isBrowser } from './utils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认密码
|
||||||
|
* 用户可以通过url中拼接参数,输入密码
|
||||||
|
* 输入过一次的密码会被存储在浏览器中,便于下一次免密访问
|
||||||
|
* 返回的是一组历史密码,便于客户端多次尝试
|
||||||
|
*/
|
||||||
|
export const getPasswordQuery = path => {
|
||||||
|
// 使用 URL 对象解析 URL
|
||||||
|
const url = new URL(path, isBrowser ? window.location.origin : '')
|
||||||
|
|
||||||
|
// 获取查询参数
|
||||||
|
const queryParams = Object.fromEntries(url.searchParams.entries())
|
||||||
|
|
||||||
|
// 请求中带着密码
|
||||||
|
if (queryParams.password) {
|
||||||
|
// 将已输入密码作为默认密码存放在 localStorage,便于下次读取并自动尝试
|
||||||
|
localStorage.setItem('password_default', queryParams.password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取路径部分
|
||||||
|
const cleanedPath = url.pathname
|
||||||
|
|
||||||
|
// 从 localStorage 中获取相关密码
|
||||||
|
const storedPassword = localStorage.getItem('password_' + cleanedPath)
|
||||||
|
const defaultPassword = localStorage.getItem('password_default')
|
||||||
|
|
||||||
|
// 将所有密码存储在一个数组中,并过滤掉无效值
|
||||||
|
const passwords = [
|
||||||
|
queryParams.password,
|
||||||
|
storedPassword,
|
||||||
|
defaultPassword
|
||||||
|
].filter(Boolean)
|
||||||
|
|
||||||
|
return passwords
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import BLOG from '@/blog.config'
|
import BLOG from '@/blog.config'
|
||||||
|
import useNotification from '@/components/Notification'
|
||||||
import { siteConfig } from '@/lib/config'
|
import { siteConfig } from '@/lib/config'
|
||||||
import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData'
|
import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData'
|
||||||
|
import { useGlobal } from '@/lib/global'
|
||||||
import { getPageTableOfContents } from '@/lib/notion/getPageTableOfContents'
|
import { getPageTableOfContents } from '@/lib/notion/getPageTableOfContents'
|
||||||
|
import { getPasswordQuery } from '@/lib/password'
|
||||||
import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
|
import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
|
||||||
import { checkSlugHasNoSlash, getRecommendPost } from '@/lib/utils/post'
|
import { checkSlugHasNoSlash, getRecommendPost } from '@/lib/utils/post'
|
||||||
import { getLayoutByTheme } from '@/themes/theme'
|
import { getLayoutByTheme } from '@/themes/theme'
|
||||||
@@ -19,9 +22,11 @@ import { useEffect, useState } from 'react'
|
|||||||
const Slug = props => {
|
const Slug = props => {
|
||||||
const { post } = props
|
const { post } = props
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { locale } = useGlobal()
|
||||||
|
|
||||||
// 文章锁🔐
|
// 文章锁🔐
|
||||||
const [lock, setLock] = useState(post?.password && post?.password !== '')
|
const [lock, setLock] = useState(post?.password && post?.password !== '')
|
||||||
|
const { showNotification, Notification } = useNotification()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证文章密码
|
* 验证文章密码
|
||||||
@@ -36,6 +41,7 @@ const Slug = props => {
|
|||||||
setLock(false)
|
setLock(false)
|
||||||
// 输入密码存入localStorage,下次自动提交
|
// 输入密码存入localStorage,下次自动提交
|
||||||
localStorage.setItem('password_' + router.asPath, passInput)
|
localStorage.setItem('password_' + router.asPath, passInput)
|
||||||
|
showNotification(locale.COMMON.ARTICLE_UNLOCK_TIPS) // 设置解锁成功提示显示
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -56,10 +62,14 @@ const Slug = props => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从localStorage中读取上次记录 自动提交密码
|
// 读取上次记录 自动提交密码
|
||||||
const passInput = localStorage.getItem('password_' + router.asPath)
|
const passInputs = getPasswordQuery(router.asPath)
|
||||||
if (passInput) {
|
if (passInputs.length > 0) {
|
||||||
validPassword(passInput)
|
for (const passInput of passInputs) {
|
||||||
|
if (validPassword(passInput)) {
|
||||||
|
break // 密码验证成功,停止尝试
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [post])
|
}, [post])
|
||||||
|
|
||||||
@@ -83,7 +93,12 @@ const Slug = props => {
|
|||||||
theme: siteConfig('THEME'),
|
theme: siteConfig('THEME'),
|
||||||
router: useRouter()
|
router: useRouter()
|
||||||
})
|
})
|
||||||
return <Layout {...props} />
|
return (
|
||||||
|
<>
|
||||||
|
<Layout {...props} />
|
||||||
|
{!lock && <Notification />}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ const NavPostItem = props => {
|
|||||||
!expanded && <Badge />}
|
!expanded && <Badge />}
|
||||||
</div>
|
</div>
|
||||||
<Collapse isOpen={expanded} onHeightChange={props.onHeightChange}>
|
<Collapse isOpen={expanded} onHeightChange={props.onHeightChange}>
|
||||||
{group?.items?.map(post => (
|
{group?.items?.map((post, index) => (
|
||||||
<div key={post.id} className='ml-3 border-l'>
|
<div key={index} className='ml-3 border-l'>
|
||||||
<BlogPostCard className='text-sm ml-3' post={post} />
|
<BlogPostCard className='text-sm ml-3' post={post} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -51,8 +51,8 @@ const NavPostItem = props => {
|
|||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{group?.items?.map(post => (
|
{group?.items?.map((post, index) => (
|
||||||
<div key={post.id}>
|
<div key={index}>
|
||||||
<BlogPostCard className='text-sm py-2' post={post} />
|
<BlogPostCard className='text-sm py-2' post={post} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { siteConfig } from '@/lib/config'
|
|||||||
import { useGlobal } from '@/lib/global'
|
import { useGlobal } from '@/lib/global'
|
||||||
import { isBrowser } from '@/lib/utils'
|
import { isBrowser } from '@/lib/utils'
|
||||||
import { getShortId } from '@/lib/utils/pageId'
|
import { getShortId } from '@/lib/utils/pageId'
|
||||||
import { Transition } from '@headlessui/react'
|
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
@@ -102,7 +101,7 @@ const LayoutBase = props => {
|
|||||||
slotRight,
|
slotRight,
|
||||||
slotTop
|
slotTop
|
||||||
} = props
|
} = props
|
||||||
const { onLoading, fullWidth } = useGlobal()
|
const { fullWidth } = useGlobal()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [tocVisible, changeTocVisible] = useState(false)
|
const [tocVisible, changeTocVisible] = useState(false)
|
||||||
const [pageNavVisible, changePageNavVisible] = useState(false)
|
const [pageNavVisible, changePageNavVisible] = useState(false)
|
||||||
@@ -174,7 +173,7 @@ const LayoutBase = props => {
|
|||||||
{slotTop}
|
{slotTop}
|
||||||
<WWAds className='w-full' orientation='horizontal' />
|
<WWAds className='w-full' orientation='horizontal' />
|
||||||
|
|
||||||
<Transition
|
{/* <Transition
|
||||||
show={!onLoading}
|
show={!onLoading}
|
||||||
appear={true}
|
appear={true}
|
||||||
enter='transition ease-in-out duration-700 transform order-first'
|
enter='transition ease-in-out duration-700 transform order-first'
|
||||||
@@ -183,9 +182,9 @@ const LayoutBase = props => {
|
|||||||
leave='transition ease-in-out duration-300 transform'
|
leave='transition ease-in-out duration-300 transform'
|
||||||
leaveFrom='opacity-100 translate-y-0'
|
leaveFrom='opacity-100 translate-y-0'
|
||||||
leaveTo='opacity-0 -translate-y-16'
|
leaveTo='opacity-0 -translate-y-16'
|
||||||
unmount={false}>
|
unmount={false}> */}
|
||||||
{children}
|
{children}
|
||||||
</Transition>
|
{/* </Transition> */}
|
||||||
|
|
||||||
{/* Google广告 */}
|
{/* Google广告 */}
|
||||||
<AdSlot type='in-article' />
|
<AdSlot type='in-article' />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import BlogPostCard from './BlogPostCard'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import Collapse from '@/components/Collapse'
|
import Collapse from '@/components/Collapse'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import BlogPostCard from './BlogPostCard'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导航列表
|
* 导航列表
|
||||||
@@ -9,7 +9,7 @@ import Collapse from '@/components/Collapse'
|
|||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
const NavPostItem = (props) => {
|
const NavPostItem = props => {
|
||||||
const { group } = props
|
const { group } = props
|
||||||
const [isOpen, changeIsOpen] = useState(group?.selected)
|
const [isOpen, changeIsOpen] = useState(group?.selected)
|
||||||
|
|
||||||
@@ -20,25 +20,37 @@ const NavPostItem = (props) => {
|
|||||||
console.log(group)
|
console.log(group)
|
||||||
|
|
||||||
if (group?.category) {
|
if (group?.category) {
|
||||||
return <>
|
return (
|
||||||
<div
|
<>
|
||||||
onClick={toggleOpenSubMenu}
|
<div
|
||||||
className='select-none flex justify-between text-sm cursor-pointer p-2 hover:bg-gray-50 rounded-md dark:hover:bg-gray-600' key={group?.category}>
|
onClick={toggleOpenSubMenu}
|
||||||
<span>{group?.category}</span>
|
className='select-none flex justify-between text-sm cursor-pointer p-2 hover:bg-gray-50 rounded-md dark:hover:bg-gray-600'
|
||||||
<div className='inline-flex items-center select-none pointer-events-none '><i className={`px-2 fas fa-chevron-left transition-all duration-200 ${isOpen ? '-rotate-90' : ''}`}></i></div>
|
key={group?.category}>
|
||||||
|
<span>{group?.category}</span>
|
||||||
|
<div className='inline-flex items-center select-none pointer-events-none '>
|
||||||
|
<i
|
||||||
|
className={`px-2 fas fa-chevron-left transition-all duration-200 ${isOpen ? '-rotate-90' : ''}`}></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
|
||||||
|
{group?.items?.map((post, index) => (
|
||||||
|
<div key={index} className='ml-3 border-l'>
|
||||||
|
<BlogPostCard className='text-sm ml-3' post={post} />
|
||||||
</div>
|
</div>
|
||||||
<Collapse isOpen={isOpen} onHeightChange={props.onHeightChange}>
|
))}
|
||||||
{group?.items?.map(post => (<div key={post.id} className='ml-3 border-l'>
|
</Collapse>
|
||||||
<BlogPostCard className='text-sm ml-3' post={post} /></div>))
|
</>
|
||||||
}
|
)
|
||||||
</Collapse>
|
|
||||||
</>
|
|
||||||
} else {
|
} else {
|
||||||
return <>
|
return (
|
||||||
{group?.items?.map(post => (<div key={post.id} >
|
<>
|
||||||
<BlogPostCard className='text-sm py-2' post={post} /></div>))
|
{group?.items?.map((post, index) => (
|
||||||
}
|
<div key={index}>
|
||||||
</>
|
<BlogPostCard className='text-sm py-2' post={post} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user