Reorganize navigation and module structure based on new requirements. Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
252 lines
9.5 KiB
TypeScript
252 lines
9.5 KiB
TypeScript
"use client"
|
||
|
||
import { useState } from "react"
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { Input } from "@/components/ui/input"
|
||
import { MessageSquare, ArrowLeft, Sparkles, Code, Copy, Play, Clock, CheckCircle2, Table } from "lucide-react"
|
||
import Link from "next/link"
|
||
|
||
const exampleQueries = [
|
||
"查询最近7天新增的高价值用户",
|
||
"统计各项目的用户流失率",
|
||
"找出消费金额前100的用户",
|
||
"分析用户活跃时段分布",
|
||
]
|
||
|
||
const queryHistory = [
|
||
{
|
||
id: 1,
|
||
query: "查询最近30天流失风险大于80的用户数量",
|
||
sql: "SELECT COUNT(*) FROM users WHERE churn_risk > 80 AND last_active >= DATE_SUB(NOW(), INTERVAL 30 DAY)",
|
||
result: "共 2,340 位用户",
|
||
time: "10分钟前",
|
||
},
|
||
{
|
||
id: 2,
|
||
query: "统计各省份的用户分布",
|
||
sql: "SELECT province, COUNT(*) as count FROM users GROUP BY province ORDER BY count DESC",
|
||
result: "返回 31 条记录",
|
||
time: "1小时前",
|
||
},
|
||
{
|
||
id: 3,
|
||
query: "找出RFM评分S级的用户",
|
||
sql: "SELECT * FROM users WHERE rfm_level = 'S' ORDER BY total_value DESC LIMIT 100",
|
||
result: "返回 100 条记录",
|
||
time: "2小时前",
|
||
},
|
||
]
|
||
|
||
export default function NLQPage() {
|
||
const [query, setQuery] = useState("")
|
||
const [isProcessing, setIsProcessing] = useState(false)
|
||
const [result, setResult] = useState<{
|
||
sql: string
|
||
data: { columns: string[]; rows: string[][] }
|
||
summary: string
|
||
} | null>(null)
|
||
|
||
const handleSubmit = () => {
|
||
if (!query.trim()) return
|
||
setIsProcessing(true)
|
||
// 模拟AI处理
|
||
setTimeout(() => {
|
||
setResult({
|
||
sql: `SELECT user_id, name, phone, rfm_score, total_value
|
||
FROM users
|
||
WHERE rfm_level = 'S'
|
||
AND last_active >= DATE_SUB(NOW(), INTERVAL 7 DAY)
|
||
ORDER BY total_value DESC
|
||
LIMIT 10`,
|
||
data: {
|
||
columns: ["用户ID", "姓名", "手机号", "RFM评分", "总价值"],
|
||
rows: [
|
||
["U001", "张伟", "138****8888", "95", "¥125,800"],
|
||
["U002", "李娜", "139****6666", "92", "¥98,500"],
|
||
["U003", "王芳", "137****5555", "90", "¥87,200"],
|
||
["U004", "刘洋", "136****4444", "88", "¥76,800"],
|
||
["U005", "陈静", "135****3333", "86", "¥65,400"],
|
||
],
|
||
},
|
||
summary: "找到 5 位符合条件的S级高价值用户,总价值 ¥453,700",
|
||
})
|
||
setIsProcessing(false)
|
||
}, 1500)
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-emerald-50/30 p-6">
|
||
<div className="max-w-7xl mx-auto space-y-6">
|
||
{/* 页面标题 */}
|
||
<div className="flex items-center gap-4">
|
||
<Link href="/ai-insight">
|
||
<Button variant="ghost" size="icon" className="rounded-full">
|
||
<ArrowLeft className="w-5 h-5" />
|
||
</Button>
|
||
</Link>
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-slate-800">自然语言查询</h1>
|
||
<p className="text-slate-500 mt-1">用自然语言描述需求,AI自动生成SQL并执行</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 查询输入区 */}
|
||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center gap-3">
|
||
<div className="flex-1 relative">
|
||
<MessageSquare className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-400" />
|
||
<Input
|
||
placeholder="用自然语言描述你想查询的数据,例如:查询最近7天新增的高价值用户..."
|
||
value={query}
|
||
onChange={(e) => setQuery(e.target.value)}
|
||
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
|
||
className="pl-12 pr-4 py-6 text-base"
|
||
/>
|
||
</div>
|
||
<Button
|
||
onClick={handleSubmit}
|
||
disabled={isProcessing || !query.trim()}
|
||
className="bg-gradient-to-r from-emerald-500 to-teal-600 hover:from-emerald-600 hover:to-teal-700 px-6 py-6"
|
||
>
|
||
{isProcessing ? (
|
||
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||
) : (
|
||
<>
|
||
<Sparkles className="w-5 h-5 mr-2" />
|
||
AI查询
|
||
</>
|
||
)}
|
||
</Button>
|
||
</div>
|
||
|
||
{/* 示例查询 */}
|
||
<div className="flex items-center gap-2 mt-4 flex-wrap">
|
||
<span className="text-sm text-slate-500">示例:</span>
|
||
{exampleQueries.map((eq, i) => (
|
||
<Button
|
||
key={i}
|
||
variant="outline"
|
||
size="sm"
|
||
className="text-xs bg-transparent"
|
||
onClick={() => setQuery(eq)}
|
||
>
|
||
{eq}
|
||
</Button>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 查询结果 */}
|
||
{result && (
|
||
<div className="space-y-4">
|
||
{/* 生成的SQL */}
|
||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||
<CardHeader className="pb-2">
|
||
<div className="flex items-center justify-between">
|
||
<CardTitle className="text-base flex items-center gap-2">
|
||
<Code className="w-4 h-4 text-slate-500" />
|
||
生成的SQL
|
||
</CardTitle>
|
||
<div className="flex items-center gap-2">
|
||
<Button variant="outline" size="sm">
|
||
<Copy className="w-4 h-4 mr-1" />
|
||
复制
|
||
</Button>
|
||
<Button variant="outline" size="sm">
|
||
<Play className="w-4 h-4 mr-1" />
|
||
重新执行
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<pre className="bg-slate-900 text-slate-100 p-4 rounded-lg text-sm overflow-x-auto">
|
||
<code>{result.sql}</code>
|
||
</pre>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 查询结果表格 */}
|
||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||
<CardHeader className="pb-2">
|
||
<div className="flex items-center justify-between">
|
||
<CardTitle className="text-base flex items-center gap-2">
|
||
<Table className="w-4 h-4 text-slate-500" />
|
||
查询结果
|
||
</CardTitle>
|
||
<Badge className="bg-emerald-100 text-emerald-700">
|
||
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||
{result.summary}
|
||
</Badge>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full">
|
||
<thead>
|
||
<tr className="border-b border-slate-200">
|
||
{result.data.columns.map((col, i) => (
|
||
<th key={i} className="text-left py-3 px-4 text-xs font-medium text-slate-500">
|
||
{col}
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{result.data.rows.map((row, i) => (
|
||
<tr key={i} className="border-b border-slate-100 hover:bg-slate-50/50">
|
||
{row.map((cell, j) => (
|
||
<td key={j} className="py-3 px-4 text-sm text-slate-700">
|
||
{cell}
|
||
</td>
|
||
))}
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)}
|
||
|
||
{/* 查询历史 */}
|
||
<Card className="bg-white/70 backdrop-blur border-slate-200/60">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-base flex items-center gap-2">
|
||
<Clock className="w-4 h-4 text-slate-500" />
|
||
查询历史
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="space-y-3">
|
||
{queryHistory.map((item) => (
|
||
<div key={item.id} className="p-4 bg-slate-50/80 rounded-lg border border-slate-200/60">
|
||
<div className="flex items-start justify-between">
|
||
<div className="flex-1">
|
||
<p className="text-sm font-medium text-slate-800">{item.query}</p>
|
||
<pre className="mt-2 text-xs text-slate-500 bg-slate-100 p-2 rounded overflow-x-auto">
|
||
{item.sql}
|
||
</pre>
|
||
</div>
|
||
<div className="text-right ml-4">
|
||
<Badge variant="outline" className="text-xs">
|
||
{item.result}
|
||
</Badge>
|
||
<p className="text-xs text-slate-500 mt-1">{item.time}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|