Files
users/app/intelligent-search/page.tsx
v0 f0a6a364f2 feat: sync Sidebar and BottomNav, standardize user profile API
Align Sidebar & BottomNav menus, remove "Search", add user profile mock data, implement /api/users, add FilterDrawer, complete Section, ProfileHeader, MetricsRFM components

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
2025-08-08 07:00:12 +00:00

582 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import type React from "react"
import { useState, useCallback } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Avatar } from "@/components/ui/avatar"
import { Progress } from "@/components/ui/progress"
import { Switch } from "@/components/ui/switch"
import { Label } from "@/components/ui/label"
import { Search, Sparkles, Clock, TrendingUp, Users, Brain, Zap, Download, RefreshCw, Eye, BarChart3, Lightbulb } from 'lucide-react'
import { TooltipHelp, TooltipProvider } from "@/components/ui/tooltip-help"
interface SearchResult {
id: string
type: "user" | "traffic" | "insight"
title: string
description: string
tags: string[]
relevanceScore: number
updatedAt: string
metadata?: Record<string, any>
}
interface SearchResponse {
results: SearchResult[]
stats: {
totalResults: number
queryTime: number
suggestions: string[]
filters: Record<string, any>
}
hasMore: boolean
}
export default function IntelligentSearchPage() {
const [searchQuery, setSearchQuery] = useState("")
const [searchType, setSearchType] = useState<"all" | "user" | "traffic">("all")
const [useAI, setUseAI] = useState(true)
const [includeInsights, setIncludeInsights] = useState(true)
const [isSearching, setIsSearching] = useState(false)
const [searchResults, setSearchResults] = useState<SearchResponse | null>(null)
const [searchHistory, setSearchHistory] = useState<string[]>([])
const [activeTab, setActiveTab] = useState("search")
const [rfmHighValueOnly, setRfmHighValueOnly] = useState(false);
const detectQueryType = (q: string) => {
const s = q.trim()
if (/^\d{5,11}$/.test(s)) {
if (/^1[3-9]\d{9}$/.test(s) || /^\d{3}\*{4}\d{4}$/.test(s)) return "手机号"
return "QQ号"
}
if (/^[A-F0-9]{14}$/.test(s)) return "MEID"
if (/^0{6,}$/.test(s) || s.includes("000000")) return "流量词"
return "通用"
}
const queryType = detectQueryType(searchQuery)
// 执行搜索
const performSearch = useCallback(
async (query: string) => {
if (!query.trim()) return
setIsSearching(true)
try {
const response = await fetch("/api/search", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query,
type: searchType,
options: {
useAI,
includeInsights,
rfmTag: includeInsights ? "高" : undefined,
limit: 50,
},
}),
})
if (!response.ok) {
throw new Error("搜索失败")
}
const data: SearchResponse = await response.json()
setSearchResults(data)
// 添加到搜索历史
setSearchHistory((prev) => {
const newHistory = [query, ...prev.filter((h) => h !== query)].slice(0, 10)
return newHistory
})
} catch (error) {
console.error("搜索错误:", error)
} finally {
setIsSearching(false)
}
},
[searchType, useAI, includeInsights],
)
// 处理搜索输入
const handleSearch = () => {
performSearch(searchQuery)
}
// 处理回车键搜索
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
handleSearch()
}
}
// 获取类型图标
const getTypeIcon = (type: string) => {
switch (type) {
case "user":
return <Users className="h-4 w-4 text-blue-500" />
case "traffic":
return <TrendingUp className="h-4 w-4 text-green-500" />
case "insight":
return <Lightbulb className="h-4 w-4 text-purple-500" />
default:
return <Search className="h-4 w-4 text-gray-500" />
}
}
// 获取类型标签颜色
const getTypeBadgeColor = (type: string) => {
switch (type) {
case "user":
return "bg-blue-100 text-blue-800"
case "traffic":
return "bg-green-100 text-green-800"
case "insight":
return "bg-purple-100 text-purple-800"
default:
return "bg-gray-100 text-gray-800"
}
}
return (
<TooltipProvider>
<div className="container mx-auto p-6 space-y-6">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold flex items-center gap-2">
<Brain className="h-8 w-8 text-purple-600" />
</h1>
<p className="text-muted-foreground mt-2">AI驱动的亚秒级数据搜索与洞察分析</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm">
<Download className="h-4 w-4 mr-2" />
</Button>
<Button variant="outline" size="sm">
<BarChart3 className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
{/* 搜索配置卡片 */}
<Card className="bg-gradient-to-r from-purple-50 to-blue-50 border-none shadow-lg">
<CardContent className="p-6">
<div className="space-y-4">
{/* 主搜索框 */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-muted-foreground" />
<Input
type="text"
placeholder="输入搜索关键词,支持自然语言查询..."
className="pl-10 h-12 text-base border-2 border-purple-200 focus:border-purple-400"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyPress={handleKeyPress}
/>
<Button
onClick={handleSearch}
disabled={isSearching || !searchQuery.trim()}
className="absolute right-2 top-1/2 transform -translate-y-1/2"
size="sm"
>
{isSearching ? <RefreshCw className="h-4 w-4 animate-spin" /> : <Search className="h-4 w-4" />}
</Button>
</div>
<div className="flex items-center gap-2 mt-2">
<Badge variant="secondary" className="text-xs">: {queryType}</Badge>
{queryType === "流量词" && <Badge className="text-xs"></Badge>}
</div>
{/* 搜索选项 */}
<div className="flex flex-wrap items-center gap-6">
<div className="flex items-center gap-2">
<Label htmlFor="search-type">:</Label>
<select
id="search-type"
value={searchType}
onChange={(e) => setSearchType(e.target.value as any)}
className="px-3 py-1 border rounded-md text-sm"
>
<option value="all"></option>
<option value="user"></option>
<option value="traffic"></option>
</select>
</div>
<div className="flex items-center gap-2">
<Switch id="use-ai" checked={useAI} onCheckedChange={setUseAI} />
<Label htmlFor="use-ai" className="flex items-center gap-1">
<Sparkles className="h-4 w-4 text-purple-500" />
AI增强搜索
<TooltipHelp content="启用AI增强搜索提供更智能的查询理解和结果优化" />
</Label>
</div>
<div className="flex items-center gap-2">
<Switch id="include-insights" checked={includeInsights} onCheckedChange={setIncludeInsights} />
<Label htmlFor="include-insights" className="flex items-center gap-1">
<Brain className="h-4 w-4 text-blue-500" />
AI洞察
<TooltipHelp content="在搜索结果中包含AI生成的业务洞察和分析建议" />
</Label>
</div>
<div className="flex items-center gap-2">
<Switch id="rfm-high" checked={rfmHighValueOnly} onCheckedChange={setRfmHighValueOnly} />
<Label htmlFor="rfm-high" className="flex items-center gap-1">
RFM高价值
</Label>
</div>
</div>
{/* 搜索历史 */}
{searchHistory.length > 0 && (
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">:</span>
<div className="flex flex-wrap gap-1">
{searchHistory.slice(0, 5).map((query, index) => (
<Button
key={index}
variant="outline"
size="sm"
className="h-6 px-2 text-xs bg-transparent"
onClick={() => {
setSearchQuery(query)
performSearch(query)
}}
>
{query}
</Button>
))}
</div>
</div>
)}
</div>
</CardContent>
</Card>
{/* 搜索结果 */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="search"></TabsTrigger>
<TabsTrigger value="insights">AI洞察</TabsTrigger>
<TabsTrigger value="analytics"></TabsTrigger>
<TabsTrigger value="history"></TabsTrigger>
</TabsList>
{/* 搜索结果标签页 */}
<TabsContent value="search" className="space-y-4">
{searchResults && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2">
<Search className="h-5 w-5" />
<Badge variant="outline">{searchResults.stats.totalResults} </Badge>
</CardTitle>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Zap className="h-4 w-4" />
: {searchResults.stats.queryTime}ms
</div>
</div>
{searchResults.stats.suggestions.length > 0 && (
<CardDescription>
<div className="flex items-center gap-2 mt-2">
<span>:</span>
<div className="flex flex-wrap gap-1">
{searchResults.stats.suggestions.map((suggestion, index) => (
<Button
key={index}
variant="ghost"
size="sm"
className="h-6 px-2 text-xs text-blue-600 hover:text-blue-800"
onClick={() => {
setSearchQuery(suggestion)
performSearch(suggestion)
}}
>
{suggestion}
</Button>
))}
</div>
</div>
</CardDescription>
)}
</CardHeader>
<CardContent>
{searchResults.results.length > 0 ? (
<div className="space-y-4">
{searchResults.results.map((result) => (
<div
key={result.id}
className="flex items-start gap-4 p-4 border rounded-lg hover:bg-gray-50 transition-colors cursor-pointer"
>
{/* 类型图标 */}
<div className="flex-shrink-0 mt-1">
{result.type === "user" ? (
<Avatar className="h-10 w-10">
<div className="w-full h-full bg-blue-100 flex items-center justify-center">
<Users className="h-5 w-5 text-blue-600" />
</div>
</Avatar>
) : (
<div className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center">
{getTypeIcon(result.type)}
</div>
)}
</div>
{/* 内容 */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<h3 className="font-semibold text-lg truncate">{result.title}</h3>
<Badge className={getTypeBadgeColor(result.type)}>
{result.type === "user" ? "用户" : result.type === "traffic" ? "流量" : "洞察"}
</Badge>
<Badge variant="outline" className="text-xs">
: {(result.relevanceScore * 100).toFixed(1)}%
</Badge>
</div>
<p className="text-sm text-muted-foreground mb-2 line-clamp-2">{result.description}</p>
{/* 标签 */}
<div className="flex flex-wrap gap-1 mb-2">
{result.tags.slice(0, 5).map((tag, index) => (
<Badge key={index} variant="secondary" className="text-xs">
{tag}
</Badge>
))}
</div>
{/* 元数据 */}
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{new Date(result.updatedAt).toLocaleString("zh-CN")}
</span>
{result.metadata?.searchType && <span>: {result.metadata.searchType}</span>}
</div>
</div>
{/* 操作按钮 */}
<div className="flex-shrink-0">
<Button variant="outline" size="sm">
<Eye className="h-4 w-4 mr-1" />
</Button>
</div>
</div>
))}
{/* 加载更多 */}
{searchResults.hasMore && (
<div className="text-center pt-4">
<Button variant="outline"></Button>
</div>
)}
</div>
) : (
<div className="text-center py-12">
<Search className="h-12 w-12 mx-auto mb-4 text-muted-foreground opacity-50" />
<h3 className="text-lg font-medium mb-2"></h3>
<p className="text-muted-foreground">使</p>
</div>
)}
</CardContent>
</Card>
)}
{/* 空状态 */}
{!searchResults && (
<Card>
<CardContent className="text-center py-12">
<Brain className="h-16 w-16 mx-auto mb-4 text-purple-400" />
<h3 className="text-xl font-medium mb-2"></h3>
<p className="text-muted-foreground mb-4">AI增强分析</p>
<div className="flex flex-wrap justify-center gap-2">
{["高价值用户", "流量趋势", "用户行为分析", "RFM分群"].map((example) => (
<Button
key={example}
variant="outline"
size="sm"
onClick={() => {
setSearchQuery(example)
performSearch(example)
}}
>
{example}
</Button>
))}
</div>
</CardContent>
</Card>
)}
</TabsContent>
{/* AI洞察标签页 */}
<TabsContent value="insights" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-purple-600" />
AI智能洞察
<TooltipHelp content="基于搜索结果生成的AI洞察和业务建议" />
</CardTitle>
<CardDescription>AI分析搜索数据</CardDescription>
</CardHeader>
<CardContent>
{searchResults?.results.filter((r) => r.type === "insight").length > 0 ? (
<div className="space-y-4">
{searchResults.results
.filter((r) => r.type === "insight")
.map((insight) => (
<div key={insight.id} className="p-4 border rounded-lg bg-purple-50">
<div className="flex items-start gap-3">
<Lightbulb className="h-5 w-5 text-purple-600 mt-1" />
<div className="flex-1">
<h4 className="font-medium mb-2">{insight.title}</h4>
<p className="text-sm text-muted-foreground mb-2">{insight.description}</p>
<div className="flex items-center gap-2">
<Badge variant="secondary" className="text-xs">
: {(insight.relevanceScore * 100).toFixed(1)}%
</Badge>
<span className="text-xs text-muted-foreground">
{new Date(insight.updatedAt).toLocaleString("zh-CN")}
</span>
</div>
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8">
<Sparkles className="h-12 w-12 mx-auto mb-4 text-purple-400 opacity-50" />
<p className="text-muted-foreground">AI将为您生成智能洞察</p>
</div>
)}
</CardContent>
</Card>
</TabsContent>
{/* 搜索分析标签页 */}
<TabsContent value="analytics" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold mb-2">{searchResults?.stats.queryTime || 0}ms</div>
<Progress value={Math.min((searchResults?.stats.queryTime || 0) / 10, 100)} className="h-2" />
<p className="text-xs text-muted-foreground mt-2">: &lt;1000ms</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold mb-2">
{searchResults
? Math.round(
(searchResults.results.reduce((sum, r) => sum + r.relevanceScore, 0) /
searchResults.results.length) *
100,
)
: 0}
%
</div>
<Progress
value={
searchResults
? Math.round(
(searchResults.results.reduce((sum, r) => sum + r.relevanceScore, 0) /
searchResults.results.length) *
100,
)
: 0
}
className="h-2"
/>
<p className="text-xs text-muted-foreground mt-2"></p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base">AI增强率</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold mb-2">{useAI ? "100" : "0"}%</div>
<Progress value={useAI ? 100 : 0} className="h-2" />
<p className="text-xs text-muted-foreground mt-2">AI功能启用状态</p>
</CardContent>
</Card>
</div>
</TabsContent>
{/* 搜索历史标签页 */}
<TabsContent value="history" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Clock className="h-5 w-5" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
{searchHistory.length > 0 ? (
<div className="space-y-2">
{searchHistory.map((query, index) => (
<div
key={index}
className="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50 cursor-pointer"
onClick={() => {
setSearchQuery(query)
performSearch(query)
}}
>
<div className="flex items-center gap-3">
<Search className="h-4 w-4 text-muted-foreground" />
<span>{query}</span>
</div>
<Button variant="ghost" size="sm">
</Button>
</div>
))}
</div>
) : (
<div className="text-center py-8">
<Clock className="h-12 w-12 mx-auto mb-4 text-muted-foreground opacity-50" />
<p className="text-muted-foreground"></p>
</div>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
</TooltipProvider>
)
}