主要更新: - 后台菜单精简(9项→6项) - 新增搜索功能(敏感信息过滤) - 分销绑定和提现系统完善 - 数据库初始化API(自动修复表结构) - 用户管理:显示绑定关系详情 - 小程序:上下章导航优化、匹配页面重构 - 修复hydration和数据类型问题
213 lines
7.6 KiB
TypeScript
213 lines
7.6 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect, useRef } from "react"
|
|
import { useRouter } from "next/navigation"
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Search, X, ChevronRight, FileText, Hash } from "lucide-react"
|
|
|
|
interface SearchResult {
|
|
id: string
|
|
title: string
|
|
partTitle: string
|
|
chapterTitle: string
|
|
price: number
|
|
isFree: boolean
|
|
matchType: 'title' | 'content'
|
|
snippet?: string
|
|
score: number
|
|
}
|
|
|
|
interface SearchModalProps {
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
}
|
|
|
|
export function SearchModal({ open, onOpenChange }: SearchModalProps) {
|
|
const router = useRouter()
|
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
const [query, setQuery] = useState("")
|
|
const [results, setResults] = useState<SearchResult[]>([])
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
const [keywords, setKeywords] = useState<string[]>([])
|
|
|
|
// 热门搜索词
|
|
const hotKeywords = ["私域", "流量", "赚钱", "电商", "AI", "社群"]
|
|
|
|
// 搜索防抖
|
|
useEffect(() => {
|
|
if (!query.trim()) {
|
|
setResults([])
|
|
setKeywords([])
|
|
return
|
|
}
|
|
|
|
const timer = setTimeout(async () => {
|
|
setIsLoading(true)
|
|
try {
|
|
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}&type=all`)
|
|
const data = await res.json()
|
|
|
|
if (data.success) {
|
|
setResults(data.data.results || [])
|
|
setKeywords(data.data.keywords || [])
|
|
}
|
|
} catch (error) {
|
|
console.error("Search error:", error)
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}, 300)
|
|
|
|
return () => clearTimeout(timer)
|
|
}, [query])
|
|
|
|
// 打开时聚焦输入框
|
|
useEffect(() => {
|
|
if (open && inputRef.current) {
|
|
setTimeout(() => inputRef.current?.focus(), 100)
|
|
}
|
|
}, [open])
|
|
|
|
const handleResultClick = (result: SearchResult) => {
|
|
onOpenChange(false)
|
|
router.push(`/read/${result.id}`)
|
|
}
|
|
|
|
const handleKeywordClick = (keyword: string) => {
|
|
setQuery(keyword)
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="bg-[#1c1c1e] border-white/10 text-white max-w-lg p-0 gap-0 overflow-hidden">
|
|
{/* 搜索输入 */}
|
|
<div className="p-4 border-b border-white/5">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
|
<Input
|
|
ref={inputRef}
|
|
value={query}
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
placeholder="搜索章节标题或内容..."
|
|
className="pl-10 pr-10 bg-[#2c2c2e] border-white/5 text-white placeholder:text-gray-500 focus:border-[#00CED1]/50"
|
|
/>
|
|
{query && (
|
|
<button
|
|
onClick={() => setQuery("")}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-white"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 搜索结果 */}
|
|
<div className="max-h-[60vh] overflow-y-auto">
|
|
{/* 热门搜索 */}
|
|
{!query && (
|
|
<div className="p-4">
|
|
<p className="text-gray-500 text-xs mb-3">热门搜索</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{hotKeywords.map((keyword) => (
|
|
<button
|
|
key={keyword}
|
|
onClick={() => handleKeywordClick(keyword)}
|
|
className="px-3 py-1.5 text-xs rounded-full bg-[#2c2c2e] text-gray-300 hover:bg-[#3c3c3e] transition-colors"
|
|
>
|
|
{keyword}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 加载状态 */}
|
|
{isLoading && (
|
|
<div className="p-8 text-center">
|
|
<div className="w-5 h-5 border-2 border-[#00CED1] border-t-transparent rounded-full animate-spin mx-auto" />
|
|
<p className="text-gray-500 text-sm mt-2">搜索中...</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* 搜索结果列表 */}
|
|
{!isLoading && query && results.length > 0 && (
|
|
<div>
|
|
<p className="px-4 py-2 text-gray-500 text-xs border-b border-white/5">
|
|
找到 {results.length} 个结果
|
|
</p>
|
|
{results.map((result) => (
|
|
<button
|
|
key={result.id}
|
|
onClick={() => handleResultClick(result)}
|
|
className="w-full p-4 text-left hover:bg-[#2c2c2e] transition-colors border-b border-white/5 last:border-0"
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<div className="w-8 h-8 rounded-lg bg-[#00CED1]/10 flex items-center justify-center shrink-0">
|
|
<FileText className="w-4 h-4 text-[#00CED1]" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-[#00CED1] text-xs font-mono">{result.id}</span>
|
|
{result.isFree && (
|
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-[#00CED1]/10 text-[#00CED1]">
|
|
免费
|
|
</span>
|
|
)}
|
|
{result.matchType === 'content' && (
|
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-purple-500/10 text-purple-400">
|
|
内容匹配
|
|
</span>
|
|
)}
|
|
</div>
|
|
<h4 className="text-white font-medium text-sm mt-1 truncate">{result.title}</h4>
|
|
{result.snippet && (
|
|
<p className="text-gray-500 text-xs mt-1 line-clamp-2">{result.snippet}</p>
|
|
)}
|
|
<p className="text-gray-600 text-xs mt-1">
|
|
{result.partTitle} · {result.chapterTitle}
|
|
</p>
|
|
</div>
|
|
<ChevronRight className="w-4 h-4 text-gray-600 shrink-0 mt-1" />
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* 相关关键词 */}
|
|
{!isLoading && query && keywords.length > 0 && (
|
|
<div className="p-4 border-t border-white/5">
|
|
<p className="text-gray-500 text-xs mb-2">
|
|
<Hash className="w-3 h-3 inline mr-1" />
|
|
相关标签
|
|
</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{keywords.slice(0, 8).map((keyword) => (
|
|
<button
|
|
key={keyword}
|
|
onClick={() => handleKeywordClick(keyword)}
|
|
className="px-2 py-1 text-xs rounded bg-[#2c2c2e] text-gray-400 hover:text-[#00CED1] transition-colors"
|
|
>
|
|
#{keyword}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 无结果 */}
|
|
{!isLoading && query && results.length === 0 && (
|
|
<div className="p-8 text-center">
|
|
<Search className="w-10 h-10 text-gray-600 mx-auto mb-3" />
|
|
<p className="text-gray-500 text-sm">未找到相关内容</p>
|
|
<p className="text-gray-600 text-xs mt-1">试试其他关键词</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|