import replaceSearchResult from '@/components/Mark' import { siteConfig } from '@/lib/config' import { useGlobal } from '@/lib/global' import algoliasearch from 'algoliasearch' import throttle from 'lodash/throttle' import Link from 'next/link' import { useRouter } from 'next/router' import { Fragment, useEffect, useImperativeHandle, useRef, useState } from 'react' import { useHotkeys } from 'react-hotkeys-hook' const ShortCutActions = [ { key: '↑ ↓', action: '选择' }, { key: 'Enter', action: '跳转' }, { key: 'Esc', action: '关闭' } ] /** * 结合 Algolia 实现的弹出式搜索框 * 打开方式 cRef.current.openSearch() * https://www.algolia.com/doc/api-reference/search-api-parameters/ */ export default function AlgoliaSearchModal({ cRef }) { const [searchResults, setSearchResults] = useState([]) const [isModalOpen, setIsModalOpen] = useState(false) const [page, setPage] = useState(0) const [keyword, setKeyword] = useState(null) const [totalPage, setTotalPage] = useState(0) const [totalHit, setTotalHit] = useState(0) const [useTime, setUseTime] = useState(0) const [activeIndex, setActiveIndex] = useState(0) const [isLoading, setIsLoading] = useState(false) const [isInputFocused, setIsInputFocused] = useState(false) const inputRef = useRef(null) const router = useRouter() /** * 快捷键设置 */ useHotkeys('ctrl+k', e => { e.preventDefault() setIsModalOpen(true) }) // 修改快捷键的使用逻辑 useHotkeys( 'down', e => { if (isInputFocused) { // 只有在聚焦时才触发 e.preventDefault() if (activeIndex < searchResults.length - 1) { setActiveIndex(activeIndex + 1) } } }, { enableOnFormTags: true } ) useHotkeys( 'up', e => { if (isInputFocused) { e.preventDefault() if (activeIndex > 0) { setActiveIndex(activeIndex - 1) } } }, { enableOnFormTags: true } ) useHotkeys( 'esc', e => { if (isInputFocused) { e.preventDefault() setIsModalOpen(false) } }, { enableOnFormTags: true } ) useHotkeys( 'enter', e => { if (isInputFocused && searchResults.length > 0) { onJumpSearchResult(index) } }, { enableOnFormTags: true } ) // 跳转Search结果 const onJumpSearchResult = () => { if (searchResults.length > 0) { window.location.href = `${siteConfig('SUB_PATH', '')}/${searchResults[activeIndex].slug}` } } const resetSearch = () => { setActiveIndex(0) setKeyword('') setSearchResults([]) setUseTime(0) setTotalPage(0) setTotalHit(0) if (inputRef.current) inputRef.current.value = '' } /** * 页面路径变化后,自动关闭此modal */ useEffect(() => { setIsModalOpen(false) }, [router]) /** * 自动聚焦搜索框 */ useEffect(() => { if (isModalOpen) { setTimeout(() => { inputRef.current?.focus() }, 100) } else { resetSearch() } }, [isModalOpen]) /** * 对外暴露方法 **/ useImperativeHandle(cRef, () => { return { openSearch: () => { setIsModalOpen(true) } } }) const client = algoliasearch( siteConfig('ALGOLIA_APP_ID'), siteConfig('ALGOLIA_SEARCH_ONLY_APP_KEY') ) const index = client.initIndex(siteConfig('ALGOLIA_INDEX')) /** * 搜索 * @param {*} query */ const handleSearch = async (query, page) => { setKeyword(query) setPage(page) setSearchResults([]) setUseTime(0) setTotalPage(0) setTotalHit(0) setActiveIndex(0) if (!query || query === '') { return } setIsLoading(true) try { const res = await index.search(query, { page, hitsPerPage: 10 }) const { hits, nbHits, nbPages, processingTimeMS } = res setUseTime(processingTimeMS) setTotalPage(nbPages) setTotalHit(nbHits) setSearchResults(hits) setIsLoading(false) const doms = document .getElementById('search-wrapper') .getElementsByClassName('replace') setTimeout(() => { replaceSearchResult({ doms, search: query, target: { element: 'span', className: 'font-bold border-b border-dashed' } }) }, 200) // 延时高亮 } catch (error) { console.error('Algolia search error:', error) } } // 定义节流函数,确保在用户停止输入一段时间后才会调用处理搜索的方法 const throttledHandleInputChange = useRef( throttle((query, page = 0) => { handleSearch(query, page) }, 1000) ) // 用于存储搜索延迟的计时器 const searchTimer = useRef(null) // 修改input的onChange事件处理函数 const handleInputChange = e => { const query = e.target.value // 如果已经有计时器在等待搜索,先清除之前的计时器 if (searchTimer.current) { clearTimeout(searchTimer.current) } // 设置新的计时器,在用户停止输入一段时间后触发搜索 searchTimer.current = setTimeout(() => { throttledHandleInputChange.current(query) }, 800) } /** * 切换页码 * @param {*} page */ const switchPage = page => { throttledHandleInputChange.current(keyword, page) } /** * 关闭弹窗 */ const closeModal = () => { setIsModalOpen(false) } if (!siteConfig('ALGOLIA_APP_ID')) { return <> } return (
{/* 模态框 */}
搜索
handleInputChange(e)} onFocus={() => setIsInputFocused(true)} // 聚焦时 onBlur={() => setIsInputFocused(false)} // 失去焦点时 className='text-black dark:text-gray-200 bg-gray-50 dark:bg-gray-600 outline-blue-500 w-full px-4 my-2 py-1 mb-4 border rounded-md' ref={inputRef} /> {/* 标签组 */}
{searchResults.length === 0 && keyword && !isLoading && (

{' '} 无法找到相关结果 "{keyword}"

)}
{totalHit === 0 && (
{ShortCutActions.map((action, index) => { return (
{action.key}
{action.action}
) })}
)}
{totalHit > 0 && (

共搜索到 {totalHit} 条结果,用时 {useTime} 毫秒

)}
Algolia 提供搜索服务
{/* 遮罩 */}
) } /** * 标签组 */ function TagGroups() { const { tagOptions } = useGlobal() // 获取tagOptions数组前十个 const firstTenTags = tagOptions?.slice(0, 10) return (
{firstTenTags?.map((tag, index) => { return (
{tag.name}
{tag.count ? ( {tag.count} ) : ( <> )}
) })}
) } /** * 分页 * @param {*} param0 */ function Pagination(props) { const { totalPage, page, switchPage } = props if (totalPage <= 0) { return <> } return (
{Array.from({ length: totalPage }, (_, i) => { const classNames = page === i ? 'font-bold text-white bg-blue-600 dark:bg-yellow-600 rounded' : 'hover:text-blue-600 hover:font-bold dark:text-gray-300' return (
switchPage(i)} className={`text-center cursor-pointer w-6 h-6 ${classNames}`} key={i}> {i + 1}
) })}
) }