190 lines
5.8 KiB
TypeScript
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>
|
|
</>
|
|
)
|
|
}
|