2025-05-22 20:42:10 +08:00

190 lines
5.8 KiB
TypeScript

"use client"
import type React from "react"
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Search, X } from "lucide-react"
import { useRouter, useSearchParams } from "next/navigation"
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
import { fetchWithAuth } from "@/lib/api"
interface SearchResult {
bookId: number
title: string
author: string[]
price: number
coverImage?: string
}
export function SearchBar() {
const [searchTerm, setSearchTerm] = useState("")
const [isLoading, setIsLoading] = useState(false)
const [open, setOpen] = useState(false)
const [results, setResults] = useState<SearchResult[]>([])
const router = useRouter()
const searchParams = useSearchParams()
// 从 URL 参数中获取搜索词
useEffect(() => {
const query = searchParams?.get("search")
if (query) {
setSearchTerm(query)
}
}, [searchParams])
// 监听快捷键打开搜索
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen((open) => !open)
}
}
document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down)
}, [])
// 当搜索对话框打开时执行搜索
useEffect(() => {
if (open && searchTerm.trim().length > 0) {
performSearch(searchTerm)
}
}, [open, searchTerm])
const performSearch = async (term: string) => {
if (!term.trim()) return
setIsLoading(true)
try {
const response = await fetchWithAuth(`book/search/title?title=${encodeURIComponent(term)}&pageNum=1&pageSize=5`)
const result = await response.json()
if (result.code === 0) {
setResults(
result.data.data.map((book: any) => ({
bookId: book.bookId,
title: book.title,
author: book.author,
price: book.price,
coverImage: book.coverImage,
})),
)
} else {
setResults([])
}
} catch (error) {
console.error("搜索失败", error)
setResults([])
} finally {
setIsLoading(false)
}
}
const handleSearch = (e: React.FormEvent) => {
e.preventDefault()
if (searchTerm.trim()) {
router.push(`/books?search=${encodeURIComponent(searchTerm)}`)
setOpen(false)
}
}
const handleSelectResult = (bookId: number) => {
router.push(`/books/${bookId}`)
setOpen(false)
}
const clearSearch = () => {
setSearchTerm("")
}
return (
<>
<div className="relative w-full max-w-sm">
<Button
variant="outline"
className="relative h-9 w-full justify-start text-sm text-muted-foreground sm:pr-12 md:w-40 lg:w-64"
onClick={() => setOpen(true)}
>
<span className="hidden lg:inline-flex">...</span>
<span className="inline-flex lg:hidden">...</span>
<kbd className="pointer-events-none absolute right-1.5 top-1.5 hidden h-6 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
<span className="text-xs"></span>K
</kbd>
</Button>
</div>
<CommandDialog open={open} onOpenChange={setOpen}>
<form onSubmit={handleSearch}>
<div className="flex items-center border-b px-3">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandInput
value={searchTerm}
onValueChange={setSearchTerm}
placeholder="搜索图书..."
className="flex-1 border-0 focus:ring-0"
/>
{searchTerm && (
<Button type="button" variant="ghost" size="icon" className="h-6 w-6" onClick={clearSearch}>
<X className="h-4 w-4" />
</Button>
)}
</div>
</form>
<CommandList>
{isLoading ? (
<div className="py-6 text-center text-sm">...</div>
) : (
<>
<CommandEmpty></CommandEmpty>
<CommandGroup heading="搜索结果">
{results.map((book) => (
<CommandItem
key={book.bookId}
onSelect={() => handleSelectResult(book.bookId)}
className="flex items-center gap-2 py-2"
>
<div className="h-10 w-8 overflow-hidden rounded">
<img
src={book.coverImage || "/placeholder.svg?height=40&width=30"}
alt={book.title}
className="h-full w-full object-cover"
/>
</div>
<div className="flex-1 overflow-hidden">
<p className="truncate font-medium">{book.title}</p>
<p className="text-xs text-muted-foreground">
{book.author.join(", ")} · ¥{book.price.toFixed(2)}
</p>
</div>
</CommandItem>
))}
</CommandGroup>
{searchTerm.trim() && (
<CommandGroup>
<CommandItem
onSelect={() => {
router.push(`/books?search=${encodeURIComponent(searchTerm)}`)
setOpen(false)
}}
className="justify-center font-medium"
>
"{searchTerm}"
</CommandItem>
</CommandGroup>
)}
</>
)}
</CommandList>
</CommandDialog>
</>
)
}