feat: 完善后台管理+搜索功能+分销系统
主要更新: - 后台菜单精简(9项→6项) - 新增搜索功能(敏感信息过滤) - 分销绑定和提现系统完善 - 数据库初始化API(自动修复表结构) - 用户管理:显示绑定关系详情 - 小程序:上下章导航优化、匹配页面重构 - 修复hydration和数据类型问题
This commit is contained in:
212
components/search-modal.tsx
Normal file
212
components/search-modal.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user