自动保存密码,并添加解密通知弹框

This commit is contained in:
tangly1024.com
2024-05-24 16:56:08 +08:00
parent 3b062037ce
commit 2eb95b2bf8
9 changed files with 166 additions and 38 deletions

View 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

View File

@@ -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',

View File

@@ -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: '最終更新日',

View File

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

View File

@@ -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() {

View File

@@ -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>
))} ))}

View File

@@ -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' />

View File

@@ -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>
))}
</>
)
} }
} }