Files
users/app/data-ingestion/page.tsx
v0 4eed69520c feat: refactor data asset center for enhanced search and analytics
Refactor homepage for focused search and data display; streamline data platform; enhance user and tag management; focus AI assistant on data analysis and report generation.

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
2025-07-25 06:42:34 +00:00

515 lines
18 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, useEffect } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Progress } from "@/components/ui/progress"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
Database,
Upload,
Download,
RefreshCw,
CheckCircle,
AlertCircle,
Clock,
Settings,
Pause,
BarChart3,
TrendingUp,
Zap,
} from "lucide-react"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { useToast } from "@/components/ui/use-toast"
export default function DataIngestionPage() {
const { toast } = useToast()
const [activeTab, setActiveTab] = useState("overview")
const [ingestionStatus, setIngestionStatus] = useState<any>(null)
const [isManualIngesting, setIsManualIngesting] = useState(false)
// 模拟数据接入状态
const [mockStatus] = useState({
totalIngested: 125678,
todayIngested: 1234,
activeSources: 8,
lastIngestionTime: "2分钟前",
dataQuality: 94.6,
processingQueue: 23,
recentIngestions: [
{
id: "ing_001",
source: "抖音API",
recordsCount: 156,
status: "completed",
startTime: "14:30",
duration: "2分钟",
quality: 96.8,
},
{
id: "ing_002",
source: "触客宝后台",
recordsCount: 89,
status: "completed",
startTime: "14:25",
duration: "1分钟",
quality: 95.2,
},
{
id: "ing_003",
source: "表单提交",
recordsCount: 234,
status: "processing",
startTime: "14:32",
duration: "进行中",
quality: 0,
},
{
id: "ing_004",
source: "小红书API",
recordsCount: 67,
status: "failed",
startTime: "14:20",
duration: "失败",
quality: 0,
},
],
sourceStats: [
{ name: "抖音API", records: 45678, quality: 96.8, status: "active" },
{ name: "小红书API", records: 23456, quality: 92.5, status: "active" },
{ name: "触客宝后台", records: 12345, quality: 95.1, status: "active" },
{ name: "表单提交", records: 34567, quality: 97.2, status: "active" },
{ name: "飞书妙记", records: 5678, quality: 91.3, status: "active" },
{ name: "微信公众号", records: 8901, quality: 93.7, status: "inactive" },
],
})
useEffect(() => {
// 模拟获取接入状态
setIngestionStatus(mockStatus)
}, [])
const handleManualIngest = async (formData: any) => {
setIsManualIngesting(true)
try {
const response = await fetch("/api/ingest", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
})
const result = await response.json()
if (result.success) {
toast({
title: "数据接入成功",
description: `成功处理用户数据用户ID: ${result.data.userId}`,
})
} else {
throw new Error(result.error)
}
} catch (error) {
toast({
title: "数据接入失败",
description: (error as Error).message,
variant: "destructive",
})
} finally {
setIsManualIngesting(false)
}
}
const getStatusColor = (status: string) => {
switch (status) {
case "completed":
return "bg-green-100 text-green-800"
case "processing":
return "bg-yellow-100 text-yellow-800"
case "failed":
return "bg-red-100 text-red-800"
case "active":
return "bg-green-100 text-green-800"
case "inactive":
return "bg-gray-100 text-gray-800"
default:
return "bg-gray-100 text-gray-800"
}
}
const getStatusIcon = (status: string) => {
switch (status) {
case "completed":
return <CheckCircle className="h-4 w-4 text-green-600" />
case "processing":
return <Clock className="h-4 w-4 text-yellow-600 animate-spin" />
case "failed":
return <AlertCircle className="h-4 w-4 text-red-600" />
case "active":
return <CheckCircle className="h-4 w-4 text-green-600" />
case "inactive":
return <Pause className="h-4 w-4 text-gray-600" />
default:
return <AlertCircle className="h-4 w-4 text-gray-600" />
}
}
if (!ingestionStatus) {
return <div className="container mx-auto py-8">...</div>
}
return (
<div className="container mx-auto py-4 md:py-6 space-y-4 md:space-y-6">
{/* 页面标题和工具栏 */}
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
<div>
<h1 className="text-2xl md:text-3xl font-bold"></h1>
<p className="text-sm md:text-base text-muted-foreground mt-1"></p>
</div>
<div className="flex flex-wrap gap-2">
<Dialog>
<DialogTrigger asChild>
<Button>
<Upload className="mr-2 h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<ManualIngestionForm onSubmit={handleManualIngest} isLoading={isManualIngesting} />
</DialogContent>
</Dialog>
<Button variant="outline" size="icon" className="h-9 w-9 bg-transparent">
<RefreshCw className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm" className="hidden md:flex bg-transparent">
<Download className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="overview"></TabsTrigger>
<TabsTrigger value="sources"></TabsTrigger>
<TabsTrigger value="history"></TabsTrigger>
<TabsTrigger value="monitoring"></TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-6">
{/* 接入概览 */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground"></p>
<p className="text-2xl font-bold">{ingestionStatus.totalIngested.toLocaleString()}</p>
</div>
<Database className="h-8 w-8 text-blue-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground"></p>
<p className="text-2xl font-bold">{ingestionStatus.todayIngested.toLocaleString()}</p>
</div>
<TrendingUp className="h-8 w-8 text-green-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground"></p>
<p className="text-2xl font-bold">{ingestionStatus.activeSources}</p>
</div>
<Zap className="h-8 w-8 text-purple-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground"></p>
<p className="text-2xl font-bold">{ingestionStatus.dataQuality}%</p>
</div>
<BarChart3 className="h-8 w-8 text-orange-600" />
</div>
</CardContent>
</Card>
</div>
{/* 实时状态 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<Badge variant="outline">{ingestionStatus.processingQueue} </Badge>
</div>
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm font-medium">{ingestionStatus.lastIngestionTime}</span>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span></span>
<span>{ingestionStatus.dataQuality}%</span>
</div>
<Progress value={ingestionStatus.dataQuality} className="h-2" />
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="sources" className="space-y-6">
{/* 数据源状态 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{ingestionStatus.sourceStats.map((source: any, index: number) => (
<div key={index} className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center gap-4">
{getStatusIcon(source.status)}
<div>
<h3 className="font-medium">{source.name}</h3>
<p className="text-sm text-muted-foreground">{source.records.toLocaleString()} </p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<p className="text-sm font-medium">{source.quality}%</p>
<p className="text-xs text-muted-foreground"></p>
</div>
<Badge className={getStatusColor(source.status)}>
{source.status === "active" ? "活跃" : "非活跃"}
</Badge>
<Button variant="outline" size="sm">
<Settings className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="history" className="space-y-6">
{/* 接入历史 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{ingestionStatus.recentIngestions.map((ingestion: any) => (
<div key={ingestion.id} className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center gap-4">
{getStatusIcon(ingestion.status)}
<div>
<h3 className="font-medium">{ingestion.source}</h3>
<p className="text-sm text-muted-foreground">
{ingestion.recordsCount} {ingestion.startTime} {ingestion.duration}
</p>
</div>
</div>
<div className="flex items-center gap-4">
{ingestion.quality > 0 && (
<div className="text-right">
<p className="text-sm font-medium">{ingestion.quality}%</p>
<p className="text-xs text-muted-foreground"></p>
</div>
)}
<Badge className={getStatusColor(ingestion.status)}>
{ingestion.status === "completed"
? "完成"
: ingestion.status === "processing"
? "处理中"
: "失败"}
</Badge>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="monitoring" className="space-y-6">
{/* 监控统计 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>7</CardDescription>
</CardHeader>
<CardContent>
<div className="h-[200px] flex items-center justify-center text-muted-foreground">
<BarChart3 className="h-12 w-12 mb-2" />
<p></p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{ingestionStatus.sourceStats.slice(0, 5).map((source: any, index: number) => (
<div key={index} className="space-y-1">
<div className="flex justify-between text-sm">
<span>{source.name}</span>
<span>{source.quality}%</span>
</div>
<Progress value={source.quality} className="h-2" />
</div>
))}
</div>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</div>
)
}
// 手动接入表单组件
function ManualIngestionForm({ onSubmit, isLoading }: { onSubmit: (data: any) => void; isLoading: boolean }) {
const [formData, setFormData] = useState({
source: "",
sourceUserId: "",
sourceRecordId: "",
originalData: "",
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
try {
const parsedData = JSON.parse(formData.originalData)
onSubmit({
...formData,
originalData: parsedData,
})
} catch (error) {
alert("原始数据必须是有效的JSON格式")
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="source"></Label>
<Select value={formData.source} onValueChange={(value) => setFormData({ ...formData, source: value })}>
<SelectTrigger>
<SelectValue placeholder="选择数据源" />
</SelectTrigger>
<SelectContent>
<SelectItem value="douyin"></SelectItem>
<SelectItem value="xiaohongshu"></SelectItem>
<SelectItem value="cunkebao_form"></SelectItem>
<SelectItem value="touchkebao_call"></SelectItem>
<SelectItem value="feishu_notes"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="sourceUserId">ID</Label>
<Input
id="sourceUserId"
value={formData.sourceUserId}
onChange={(e) => setFormData({ ...formData, sourceUserId: e.target.value })}
placeholder="可选"
/>
</div>
<div className="space-y-2">
<Label htmlFor="sourceRecordId">ID</Label>
<Input
id="sourceRecordId"
value={formData.sourceRecordId}
onChange={(e) => setFormData({ ...formData, sourceRecordId: e.target.value })}
placeholder="可选"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="originalData"> (JSON格式)</Label>
<Textarea
id="originalData"
value={formData.originalData}
onChange={(e) => setFormData({ ...formData, originalData: e.target.value })}
placeholder='{"name": "张三", "phone": "13800138000", "email": "zhangsan@example.com"}'
rows={8}
required
/>
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" disabled={isLoading}>
</Button>
<Button type="submit" disabled={isLoading || !formData.source || !formData.originalData}>
{isLoading ? (
<>
<Clock className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
</>
)}
</Button>
</div>
</form>
)
}