refactor: overhaul UI for streamlined user experience

Redesign navigation, home overview, user portrait, and valuation pages
with improved functionality and responsive design.

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
v0
2025-07-18 13:47:12 +00:00
parent 440b310c6f
commit 2408d50cb0
316 changed files with 55785 additions and 0 deletions

View File

@@ -0,0 +1,923 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { Checkbox } from "@/components/ui/checkbox"
import { Badge } from "@/components/ui/badge"
import { Separator } from "@/components/ui/separator"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import {
Brain,
Lightbulb,
BarChart,
PieChart,
Users,
Tag,
Play,
Settings,
Search,
Filter,
Copy,
Check,
Zap,
} from "lucide-react"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { useToast } from "@/components/ui/use-toast"
interface AIAnalysisTask {
id: string
name: string
type: string
status: "pending" | "running" | "completed" | "failed"
progress: number
createdAt: string
completedAt: string | null
dataSource: string
parameters: Record<string, any>
result: any | null
}
interface AIModel {
id: string
name: string
provider: string
type: string
capabilities: string[]
apiEndpoint: string
apiKey: string
status: "active" | "inactive"
}
export function AIAnalysisTools() {
const { toast } = useToast()
const [activeTab, setActiveTab] = useState("analysis-tasks")
const [isCreatingTask, setIsCreatingTask] = useState(false)
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null)
const [isAddingModel, setIsAddingModel] = useState(false)
const [copiedText, setCopiedText] = useState<string | null>(null)
// 模拟AI分析任务
const [aiTasks, setAiTasks] = useState<AIAnalysisTask[]>([
{
id: "task-1",
name: "用户行为分析",
type: "behavior-analysis",
status: "completed",
progress: 100,
createdAt: "2023-07-20 10:15",
completedAt: "2023-07-20 10:25",
dataSource: "行为分析平台",
parameters: {
userSegment: "高价值用户",
timeRange: "近30天",
analysisDepth: "深度",
},
result: {
summary: "高价值用户在过去30天内主要活跃在晚间时段偏好浏览科技和旅游类内容平均会话时长较长。",
insights: [
"用户主要在晚上9点至11点活跃",
"科技类内容点击率高于平均水平58%",
"平均会话时长为8.5分钟高于普通用户3.2分钟",
"移动端访问占比87%其中iOS设备占65%",
],
recommendations: [
"在晚间时段推送个性化内容",
"增加科技和旅游类内容的推荐权重",
"针对长会话时长优化页面加载速度",
"优化iOS端用户体验",
],
},
},
{
id: "task-2",
name: "用户分群自动发现",
type: "segment-discovery",
status: "completed",
progress: 100,
createdAt: "2023-07-19 14:30",
completedAt: "2023-07-19 15:10",
dataSource: "用户数据库",
parameters: {
clusteringMethod: "K-Means",
features: ["消费行为", "活跃度", "内容偏好"],
maxClusters: 8,
},
result: {
summary: "系统自动发现了5个明显的用户分群其中包括高消费低活跃、高活跃低消费等特征明显的群体。",
clusters: [
{
name: "高消费科技爱好者",
size: 12500,
characteristics: ["高消费", "科技内容偏好", "中等活跃度"],
},
{
name: "活跃社交用户",
size: 28900,
characteristics: ["高活跃度", "社交内容偏好", "中等消费"],
},
{
name: "奢侈品偶尔购买者",
size: 8700,
characteristics: ["低活跃度", "高单次消费", "奢侈品偏好"],
},
{
name: "日常必需品购买者",
size: 45600,
characteristics: ["高频次低金额消费", "中等活跃度", "生活类内容偏好"],
},
{
name: "休眠用户",
size: 15400,
characteristics: ["极低活跃度", "低消费", "无明显内容偏好"],
},
],
},
},
{
id: "task-3",
name: "用户流失预测",
type: "churn-prediction",
status: "running",
progress: 65,
createdAt: "2023-07-21 09:45",
completedAt: null,
dataSource: "用户数据库 & 行为分析平台",
parameters: {
predictionHorizon: "30天",
modelType: "随机森林",
features: ["活跃度", "消费频率", "客服交互", "产品使用情况"],
},
result: null,
},
{
id: "task-4",
name: "营销策略生成",
type: "strategy-generation",
status: "pending",
progress: 0,
createdAt: "2023-07-21 10:30",
completedAt: null,
dataSource: "用户画像 & 行为分析",
parameters: {
targetSegment: "流失风险用户",
campaignGoal: "提高留存率",
budgetConstraint: "中等",
channelPreference: ["短信", "应用内推送", "邮件"],
},
result: null,
},
{
id: "task-5",
name: "自动标签生成",
type: "tag-generation",
status: "completed",
progress: 100,
createdAt: "2023-07-18 11:20",
completedAt: "2023-07-18 12:05",
dataSource: "用户数据库 & 行为分析平台",
parameters: {
tagCategories: ["兴趣", "消费习惯", "活跃模式"],
minConfidence: 0.7,
maxTagsPerUser: 10,
},
result: {
summary: "系统为85%的用户生成了新标签平均每用户5.3个标签。",
tagCategories: [
{
name: "兴趣标签",
count: 28,
coverage: "92%",
examples: ["科技爱好者", "旅游达人", "美食家", "运动健身"],
},
{
name: "消费习惯",
count: 15,
coverage: "88%",
examples: ["奢侈品偏好", "理性消费", "冲动购物", "价格敏感"],
},
{
name: "活跃模式",
count: 12,
coverage: "95%",
examples: ["工作时间活跃", "夜间活跃", "周末活跃", "低频高质"],
},
],
},
},
])
// 模拟AI模型
const [aiModels, setAiModels] = useState<AIModel[]>([
{
id: "model-1",
name: "GPT-4",
provider: "OpenAI",
type: "大语言模型",
capabilities: ["文本生成", "内容分析", "情感分析", "摘要生成"],
apiEndpoint: "https://api.openai.com/v1/chat/completions",
apiKey: "sk-***********",
status: "active",
},
{
id: "model-2",
name: "Claude 2",
provider: "Anthropic",
type: "大语言模型",
capabilities: ["文本生成", "内容分析", "代码生成", "问答"],
apiEndpoint: "https://api.anthropic.com/v1/complete",
apiKey: "sk-ant-***********",
status: "active",
},
{
id: "model-3",
name: "MCP SERVER AI",
provider: "内部服务",
type: "专用分析模型",
capabilities: ["用户行为分析", "分群发现", "标签生成", "预测模型"],
apiEndpoint: "https://mcp.example.com/api/analyze",
apiKey: "mcp-***********",
status: "active",
},
{
id: "model-4",
name: "DALL-E 3",
provider: "OpenAI",
type: "图像生成模型",
capabilities: ["图像生成", "图像编辑", "风格迁移"],
apiEndpoint: "https://api.openai.com/v1/images/generations",
apiKey: "sk-***********",
status: "inactive",
},
])
// 模拟分析模板
const analysisTemplates = [
{
id: "template-1",
name: "用户行为分析",
description: "分析用户的行为模式、偏好和习惯",
type: "behavior-analysis",
parameters: {
userSegment: "所有用户",
timeRange: "近30天",
analysisDepth: "标准",
},
},
{
id: "template-2",
name: "用户分群发现",
description: "自动发现数据中的用户分群",
type: "segment-discovery",
parameters: {
clusteringMethod: "K-Means",
features: ["消费行为", "活跃度", "内容偏好"],
maxClusters: 8,
},
},
{
id: "template-3",
name: "用户流失预测",
description: "预测未来可能流失的用户",
type: "churn-prediction",
parameters: {
predictionHorizon: "30天",
modelType: "随机森林",
features: ["活跃度", "消费频率", "客服交互", "产品使用情况"],
},
},
{
id: "template-4",
name: "营销策略生成",
description: "为特定用户分群生成营销策略",
type: "strategy-generation",
parameters: {
targetSegment: "所有用户",
campaignGoal: "提高转化率",
budgetConstraint: "中等",
channelPreference: ["短信", "应用内推送", "邮件"],
},
},
{
id: "template-5",
name: "自动标签生成",
description: "基于用户数据自动生成用户标签",
type: "tag-generation",
parameters: {
tagCategories: ["兴趣", "消费习惯", "活跃模式"],
minConfidence: 0.7,
maxTagsPerUser: 10,
},
},
]
const getStatusBadge = (status: string) => {
switch (status) {
case "pending":
return <Badge className="bg-gray-100 text-gray-800"></Badge>
case "running":
return <Badge className="bg-blue-100 text-blue-800"></Badge>
case "completed":
return <Badge className="bg-green-100 text-green-800"></Badge>
case "failed":
return <Badge className="bg-red-100 text-red-800"></Badge>
case "active":
return <Badge className="bg-green-100 text-green-800"></Badge>
case "inactive":
return <Badge className="bg-gray-100 text-gray-800"></Badge>
default:
return <Badge></Badge>
}
}
const getTaskTypeIcon = (type: string) => {
switch (type) {
case "behavior-analysis":
return <Users className="h-5 w-5 text-blue-500" />
case "segment-discovery":
return <PieChart className="h-5 w-5 text-purple-500" />
case "churn-prediction":
return <BarChart className="h-5 w-5 text-orange-500" />
case "strategy-generation":
return <Lightbulb className="h-5 w-5 text-yellow-500" />
case "tag-generation":
return <Tag className="h-5 w-5 text-green-500" />
default:
return <Brain className="h-5 w-5" />
}
}
const selectedTask = selectedTaskId ? aiTasks.find((task) => task.id === selectedTaskId) : null
const handleCopyText = (text: string) => {
navigator.clipboard.writeText(text)
setCopiedText(text)
toast({
title: "已复制到剪贴板",
description: "文本已成功复制到剪贴板",
})
setTimeout(() => setCopiedText(null), 2000)
}
const toggleModelStatus = (id: string) => {
setAiModels(
aiModels.map((model) => {
if (model.id === id) {
return {
...model,
status: model.status === "active" ? "inactive" : "active",
}
}
return model
}),
)
}
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold">AI分析工具</h2>
<div className="flex items-center gap-2">
<Button variant="outline">
<Settings className="mr-2 h-4 w-4" />
AI设置
</Button>
<Button onClick={() => setIsCreatingTask(true)}>
<Brain className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
<TabsList>
<TabsTrigger value="analysis-tasks"></TabsTrigger>
<TabsTrigger value="ai-models">AI模型</TabsTrigger>
<TabsTrigger value="templates"></TabsTrigger>
<TabsTrigger value="settings">AI设置</TabsTrigger>
</TabsList>
<TabsContent value="analysis-tasks" className="space-y-4">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle>AI分析任务</CardTitle>
<div className="flex items-center gap-2">
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input type="text" placeholder="搜索任务..." className="pl-8 w-[200px]" />
</div>
<Select defaultValue="all">
<SelectTrigger className="w-[150px]">
<Filter className="mr-2 h-4 w-4" />
<SelectValue placeholder="任务状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="pending"></SelectItem>
<SelectItem value="running"></SelectItem>
<SelectItem value="completed"></SelectItem>
<SelectItem value="failed"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[50px]"></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{aiTasks.map((task) => (
<TableRow key={task.id}>
<TableCell>{getTaskTypeIcon(task.type)}</TableCell>
<TableCell className="font-medium">{task.name}</TableCell>
<TableCell>{task.dataSource}</TableCell>
<TableCell>{task.createdAt}</TableCell>
<TableCell>{getStatusBadge(task.status)}</TableCell>
<TableCell>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className={`h-2.5 rounded-full ${
task.status === "completed"
? "bg-green-600"
: task.status === "running"
? "bg-blue-600"
: task.status === "failed"
? "bg-red-600"
: "bg-gray-400"
}`}
style={{ width: `${task.progress}%` }}
></div>
</div>
</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm" onClick={() => setSelectedTaskId(task.id)}>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="ai-models" className="space-y-4">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle>AI模型管理</CardTitle>
<Button onClick={() => setIsAddingModel(true)}></Button>
</div>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>API端点</TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{aiModels.map((model) => (
<TableRow key={model.id}>
<TableCell className="font-medium">{model.name}</TableCell>
<TableCell>{model.provider}</TableCell>
<TableCell>{model.type}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{model.capabilities.slice(0, 2).map((capability, index) => (
<Badge key={index} variant="outline">
{capability}
</Badge>
))}
{model.capabilities.length > 2 && (
<Badge variant="outline">+{model.capabilities.length - 2}</Badge>
)}
</div>
</TableCell>
<TableCell className="max-w-[200px] truncate">{model.apiEndpoint}</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<Switch
checked={model.status === "active"}
onCheckedChange={() => toggleModelStatus(model.id)}
aria-label="Toggle model"
/>
<span>{getStatusBadge(model.status)}</span>
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end space-x-2">
<Button variant="ghost" size="sm">
</Button>
<Button variant="ghost" size="sm">
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>MCP SERVER配置</CardTitle>
<CardDescription>MCP SERVER AI工具的分析参数</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="mcp-server-url"></Label>
<Input id="mcp-server-url" defaultValue="https://mcp.example.com/api" />
</div>
<div className="space-y-2">
<Label htmlFor="mcp-api-key">API密钥</Label>
<Input id="mcp-api-key" type="password" defaultValue="••••••••••••••••" />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="mcp-model">AI模型选择</Label>
<Select defaultValue="gpt-4">
<SelectTrigger id="mcp-model">
<SelectValue placeholder="选择AI模型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="gpt-4">GPT-4</SelectItem>
<SelectItem value="gpt-3.5-turbo">GPT-3.5 Turbo</SelectItem>
<SelectItem value="claude-2">Claude 2</SelectItem>
<SelectItem value="llama-2">Llama 2</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<div className="grid grid-cols-2 gap-2">
<div className="flex items-center space-x-2">
<Switch id="behavior-analysis" defaultChecked />
<Label htmlFor="behavior-analysis"></Label>
</div>
<div className="flex items-center space-x-2">
<Switch id="segment-discovery" defaultChecked />
<Label htmlFor="segment-discovery"></Label>
</div>
<div className="flex items-center space-x-2">
<Switch id="churn-prediction" defaultChecked />
<Label htmlFor="churn-prediction"></Label>
</div>
<div className="flex items-center space-x-2">
<Switch id="tag-generation" defaultChecked />
<Label htmlFor="tag-generation"></Label>
</div>
<div className="flex items-center space-x-2">
<Switch id="strategy-generation" defaultChecked />
<Label htmlFor="strategy-generation"></Label>
</div>
<div className="flex items-center space-x-2">
<Switch id="content-generation" defaultChecked />
<Label htmlFor="content-generation"></Label>
</div>
</div>
</div>
<div className="flex justify-end">
<Button>MCP配置</Button>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="templates" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{analysisTemplates.map((template) => (
<Card key={template.id} className="border shadow-sm">
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{getTaskTypeIcon(template.type)}
<CardTitle className="text-lg">{template.name}</CardTitle>
</div>
<Button variant="ghost" size="icon">
<Settings className="h-4 w-4" />
</Button>
</div>
<CardDescription>{template.description}</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
<h4 className="text-sm font-medium"></h4>
<div className="text-sm">
{Object.entries(template.parameters).map(([key, value]) => (
<div key={key} className="flex justify-between py-1 border-b border-dashed border-gray-200">
<span className="text-muted-foreground">{key}:</span>
<span className="font-medium">
{Array.isArray(value) ? value.join(", ") : value.toString()}
</span>
</div>
))}
</div>
</div>
<div className="mt-4 flex justify-end">
<Button onClick={() => setIsCreatingTask(true)}>
<Play className="mr-2 h-4 w-4" />
使
</Button>
</div>
</CardContent>
</Card>
))}
<Card className="border shadow-sm border-dashed">
<CardContent className="p-6 flex flex-col items-center justify-center h-full">
<div className="rounded-full bg-muted p-3 mb-4">
<Plus className="h-6 w-6 text-muted-foreground" />
</div>
<h3 className="text-lg font-medium mb-2"></h3>
<p className="text-sm text-muted-foreground text-center mb-4">
</p>
<Button variant="outline"></Button>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="settings" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>AI分析设置</CardTitle>
<CardDescription>AI分析的全局设置</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="enable-ai">AI分析</Label>
<Switch id="enable-ai" defaultChecked />
</div>
<p className="text-sm text-muted-foreground">AI分析功能将暂停</p>
</div>
<div className="space-y-2">
<Label htmlFor="default-model">AI模型</Label>
<Select defaultValue="model-3">
<SelectTrigger id="default-model">
<SelectValue placeholder="选择默认模型" />
</SelectTrigger>
<SelectContent>
{aiModels
.filter((model) => model.status === "active")
.map((model) => (
<SelectItem key={model.id} value={model.id}>
{model.name} ({model.provider})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="analysis-concurrency"></Label>
<Select defaultValue="3">
<SelectTrigger id="analysis-concurrency">
<SelectValue placeholder="选择并发任务数" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1</SelectItem>
<SelectItem value="3">3</SelectItem>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
AI分析任务数量
</p>
</div>
<Separator />
<div className="space-y-2">
<Label>访</Label>
<div className="grid grid-cols-2 gap-2">
<div className="flex items-center space-x-2">
<Checkbox id="access-user-db" defaultChecked />
<Label htmlFor="access-user-db"></Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="access-transaction" defaultChecked />
<Label htmlFor="access-transaction"></Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="access-behavior" defaultChecked />
<Label htmlFor="access-behavior"></Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="access-user-value" defaultChecked />
<Label htmlFor="access-user-value"></Label>
</div>
</div>
</div>
<div className="space-y-2">
<Label></Label>
<div className="grid grid-cols-2 gap-2">
<div className="flex items-center space-x-2">
<Checkbox id="anonymize-data" defaultChecked />
<Label htmlFor="anonymize-data"></Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="cache-results" defaultChecked />
<Label htmlFor="cache-results"></Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="auto-update" defaultChecked />
<Label htmlFor="auto-update"></Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="save-history" defaultChecked />
<Label htmlFor="save-history"></Label>
</div>
</div>
</div>
<div className="space-y-2">
<Label></Label>
<div className="grid grid-cols-2 gap-2">
<div className="flex items-center space-x-2">
<Checkbox id="notify-completion" defaultChecked />
<Label htmlFor="notify-completion"></Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="notify-error" defaultChecked />
<Label htmlFor="notify-error"></Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="notify-insight" defaultChecked />
<Label htmlFor="notify-insight"></Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="notify-email" defaultChecked />
<Label htmlFor="notify-email"></Label>
</div>
</div>
</div>
<div className="flex justify-end">
<Button></Button>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* 任务详情对话框 */}
<Dialog open={!!selectedTaskId} onOpenChange={(open) => !open && setSelectedTaskId(null)}>
<DialogContent className="sm:max-w-[700px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>AI分析任务的详细信息和结果</DialogDescription>
</DialogHeader>
{selectedTask && (
<div className="space-y-4 py-4">
<div className="flex items-center gap-4">
{getTaskTypeIcon(selectedTask.type)}
<div>
<h3 className="font-medium text-lg">{selectedTask.name}</h3>
<p className="text-sm text-muted-foreground">
{selectedTask.dataSource} | {selectedTask.createdAt}
</p>
</div>
<div className="ml-auto">{getStatusBadge(selectedTask.status)}</div>
</div>
<Separator />
<div className="space-y-2">
<h4 className="text-sm font-medium"></h4>
<div className="grid grid-cols-2 gap-2">
{Object.entries(selectedTask.parameters).map(([key, value]) => (
<div key={key} className="flex justify-between py-1 border-b border-dashed border-gray-200">
<span className="text-sm text-muted-foreground">{key}:</span>
<span className="text-sm font-medium">
{Array.isArray(value) ? value.join(", ") : value.toString()}
</span>
</div>
))}
</div>
</div>
{selectedTask.status === "running" && (
<div className="space-y-2">
<div className="flex justify-between items-center">
<h4 className="text-sm font-medium"></h4>
<span className="text-sm font-medium">{selectedTask.progress}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-blue-600 h-2.5 rounded-full"
style={{ width: `${selectedTask.progress}%` }}
></div>
</div>
<p className="text-xs text-muted-foreground">
...: {Math.round((100 - selectedTask.progress) / 10)}
</p>
</div>
)}
{selectedTask.result && (
<div className="space-y-4">
<Separator />
<div className="space-y-2">
<div className="flex justify-between items-center">
<h4 className="text-sm font-medium"></h4>
<Button
variant="ghost"
size="sm"
onClick={() => handleCopyText(selectedTask.result.summary)}
className="h-7"
>
{copiedText === selectedTask.result.summary ? (
<Check className="h-3 w-3 mr-1" />
) : (
<Copy className="h-3 w-3 mr-1" />
)}
</Button>
</div>
<div className="p-3 bg-muted rounded-md text-sm">{selectedTask.result.summary}</div>
</div>
{selectedTask.result.insights && (
<div className="space-y-2">
<h4 className="text-sm font-medium"></h4>
<div className="space-y-1">
{selectedTask.result.insights.map((insight: string, index: number) => (
<div key={index} className="flex items-start gap-2">
<Lightbulb className="h-4 w-4 text-yellow-500 mt-0.5" />
<p className="text-sm">{insight}</p>
</div>
))}
</div>
</div>
)}
{selectedTask.result.recommendations && (
<div className="space-y-2">
<h4 className="text-sm font-medium"></h4>
<div className="space-y-1">
{selectedTask.result.recommendations.map((recommendation: string, index: number) => (
<div key={index} className="flex items-start gap-2">
<Zap className="h-4 w-4 text-blue-500 mt-0.5" />
<p className="text-sm">{recommendation}</p>
</div>
))}
</div>
</div>
)}
{selectedTask.result.clusters && (
<div className="space-y-2">
<h4 className="text-sm font-medium"></h4>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{selectedTask.result.clusters.map((cluster: any, index: number) => (
<TableRow key={

View File

@@ -0,0 +1,664 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
import { Copy, Check, Code, FileJson, Play } from "lucide-react"
import { useToast } from "@/components/ui/use-toast"
interface ApiEndpoint {
id: string
name: string
method: "GET" | "POST" | "PUT" | "DELETE"
path: string
description: string
parameters: {
name: string
type: string
required: boolean
description: string
}[]
requestBody?: {
type: string
example: string
}
responses: {
code: string
description: string
example: string
}[]
authentication: "API Key" | "OAuth 2.0" | "None"
}
export function ApiDocumentation() {
const { toast } = useToast()
const [activeTab, setActiveTab] = useState("user-data")
const [copiedEndpoint, setCopiedEndpoint] = useState<string | null>(null)
// 模拟API端点数据
const apiEndpoints: Record<string, ApiEndpoint[]> = {
"user-data": [
{
id: "get-users",
name: "获取用户列表",
method: "GET",
path: "/api/users",
description: "获取系统中的用户列表,支持分页、筛选和排序",
parameters: [
{
name: "page",
type: "number",
required: false,
description: "页码默认为1",
},
{
name: "limit",
type: "number",
required: false,
description: "每页数量默认为20最大为100",
},
{
name: "sort",
type: "string",
required: false,
description: "排序字段例如name,-createdAt-表示降序)",
},
{
name: "filter",
type: "string",
required: false,
description: "筛选条件例如status=active",
},
],
responses: [
{
code: "200",
description: "成功",
example: `{
"data": [
{
"id": "user_123",
"name": "张三",
"phoneNumber": "138****1234",
"registrationDate": "2023-01-15T08:30:00Z",
"lastActiveTime": "2023-07-20T14:25:30Z",
"tags": ["高价值", "活跃用户"]
},
// 更多用户...
],
"pagination": {
"page": 1,
"limit": 20,
"total": 156,
"pages": 8
}
}`,
},
{
code: "400",
description: "请求参数错误",
example: `{
"error": "Bad Request",
"message": "Invalid filter format",
"code": "INVALID_FILTER"
}`,
},
{
code: "401",
description: "未授权",
example: `{
"error": "Unauthorized",
"message": "API key is invalid or expired",
"code": "INVALID_API_KEY"
}`,
},
],
authentication: "API Key",
},
{
id: "get-user",
name: "获取用户详情",
method: "GET",
path: "/api/users/{id}",
description: "根据用户ID获取用户详细信息",
parameters: [
{
name: "id",
type: "string",
required: true,
description: "用户ID",
},
],
responses: [
{
code: "200",
description: "成功",
example: `{
"id": "user_123",
"name": "张三",
"phoneNumber": "138****1234",
"identityNumber": "310******1234",
"email": "zhangsan@example.com",
"registrationDate": "2023-01-15T08:30:00Z",
"lastActiveTime": "2023-07-20T14:25:30Z",
"tags": ["高价值", "活跃用户"],
"devices": [
{
"id": "device_456",
"type": "mobile",
"model": "iPhone 13",
"imei": "123456789012345",
"lastActiveTime": "2023-07-20T14:25:30Z"
}
],
"userValue": {
"rfm": {
"recency": 5,
"frequency": 4,
"monetary": 5,
"score": 4.7
},
"lifetimeValue": 12500
}
}`,
},
{
code: "404",
description: "用户不存在",
example: `{
"error": "Not Found",
"message": "User with ID user_999 not found",
"code": "USER_NOT_FOUND"
}`,
},
],
authentication: "API Key",
},
],
"user-portrait": [
{
id: "get-user-portrait",
name: "获取用户画像",
method: "GET",
path: "/api/user-portrait/{id}",
description: "根据用户ID获取用户画像数据",
parameters: [
{
name: "id",
type: "string",
required: true,
description: "用户ID",
},
],
responses: [
{
code: "200",
description: "成功",
example: `{
"userId": "user_123",
"basicInfo": {
"name": "张三",
"age": 28,
"gender": "male",
"location": "上海市"
},
"behaviorTags": ["夜间活跃", "周末购物", "高频App使用"],
"interestTags": ["科技", "旅游", "美食"],
"consumptionPattern": {
"averageOrderValue": 320,
"purchaseFrequency": "每周2-3次",
"preferredCategories": ["电子产品", "服装"]
},
"rfmAnalysis": {
"recency": 5,
"frequency": 4,
"monetary": 5,
"score": 4.7,
"segment": "高价值用户"
},
"deviceInfo": [
{
"type": "mobile",
"model": "iPhone 13",
"osVersion": "iOS 16.5",
"usageFrequency": "高"
}
],
"channelPreference": ["App", "微信小程序"],
"riskScore": 0.2
}`,
},
],
authentication: "API Key",
},
],
"ai-analysis": [
{
id: "analyze-user-behavior",
name: "用户行为分析",
method: "POST",
path: "/api/ai/analyze-behavior",
description: "使用AI分析用户行为数据生成洞察报告",
parameters: [],
requestBody: {
type: "application/json",
example: `{
"userId": "user_123",
"timeRange": {
"start": "2023-01-01T00:00:00Z",
"end": "2023-07-20T23:59:59Z"
},
"analysisDepth": "deep",
"includeTags": true,
"includeRecommendations": true
}`,
},
responses: [
{
code: "200",
description: "成功",
example: `{
"userId": "user_123",
"analysisTime": "2023-07-21T10:15:30Z",
"timeRange": {
"start": "2023-01-01T00:00:00Z",
"end": "2023-07-20T23:59:59Z"
},
"behaviorPatterns": [
{
"pattern": "夜间活跃",
"confidence": 0.92,
"description": "用户主要在晚上9点至凌晨1点活跃",
"supportingData": {
"activeTimeDistribution": {
"morning": 0.15,
"afternoon": 0.25,
"evening": 0.60
}
}
},
// 更多行为模式...
],
"insights": [
{
"type": "preference",
"description": "用户对科技类产品有强烈兴趣,尤其是智能家居设备",
"confidence": 0.85
},
// 更多洞察...
],
"recommendations": [
{
"type": "marketing",
"description": "建议在晚间时段推送智能家居相关促销信息",
"expectedImpact": "高"
},
// 更多建议...
]
}`,
},
],
authentication: "API Key",
},
],
}
const handleCopyCode = (code: string, endpointId: string) => {
navigator.clipboard.writeText(code)
setCopiedEndpoint(endpointId)
toast({
title: "已复制到剪贴板",
description: "代码已成功复制到剪贴板",
})
setTimeout(() => setCopiedEndpoint(null), 2000)
}
return (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle>API文档</CardTitle>
<CardDescription>API接口文档和使用说明</CardDescription>
</CardHeader>
<CardContent>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
<TabsList className="grid grid-cols-3 w-full max-w-md">
<TabsTrigger value="user-data"></TabsTrigger>
<TabsTrigger value="user-portrait"></TabsTrigger>
<TabsTrigger value="ai-analysis">AI分析</TabsTrigger>
</TabsList>
{Object.entries(apiEndpoints).map(([category, endpoints]) => (
<TabsContent key={category} value={category} className="space-y-4">
{endpoints.map((endpoint) => (
<Card key={endpoint.id} className="border shadow-sm">
<CardHeader className="pb-2">
<div className="flex justify-between items-start">
<div>
<CardTitle className="text-lg">{endpoint.name}</CardTitle>
<CardDescription>{endpoint.description}</CardDescription>
</div>
<Badge
className={
endpoint.method === "GET"
? "bg-blue-100 text-blue-800"
: endpoint.method === "POST"
? "bg-green-100 text-green-800"
: endpoint.method === "PUT"
? "bg-yellow-100 text-yellow-800"
: "bg-red-100 text-red-800"
}
>
{endpoint.method}
</Badge>
</div>
</CardHeader>
<CardContent className="pb-2">
<div className="flex items-center gap-2 mb-4">
<code className="bg-muted px-2 py-1 rounded text-sm font-mono">{endpoint.path}</code>
<Button
variant="ghost"
size="icon"
onClick={() => handleCopyCode(endpoint.path, `path-${endpoint.id}`)}
>
{copiedEndpoint === `path-${endpoint.id}` ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
<Accordion type="single" collapsible className="w-full">
{endpoint.parameters.length > 0 && (
<AccordionItem value="parameters">
<AccordionTrigger></AccordionTrigger>
<AccordionContent>
<div className="rounded-md border">
<table className="min-w-full divide-y divide-border">
<thead>
<tr className="bg-muted/50">
<th className="px-4 py-2 text-left text-sm font-medium"></th>
<th className="px-4 py-2 text-left text-sm font-medium"></th>
<th className="px-4 py-2 text-left text-sm font-medium"></th>
<th className="px-4 py-2 text-left text-sm font-medium"></th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{endpoint.parameters.map((param, index) => (
<tr key={index}>
<td className="px-4 py-2 text-sm font-mono">{param.name}</td>
<td className="px-4 py-2 text-sm">{param.type}</td>
<td className="px-4 py-2 text-sm">
{param.required ? (
<Badge className="bg-red-100 text-red-800"></Badge>
) : (
<Badge className="bg-gray-100 text-gray-800"></Badge>
)}
</td>
<td className="px-4 py-2 text-sm">{param.description}</td>
</tr>
))}
</tbody>
</table>
</div>
</AccordionContent>
</AccordionItem>
)}
{endpoint.requestBody && (
<AccordionItem value="request-body">
<AccordionTrigger></AccordionTrigger>
<AccordionContent>
<div className="space-y-2">
<div className="flex items-center gap-2">
<Badge className="bg-purple-100 text-purple-800">{endpoint.requestBody.type}</Badge>
<Button
variant="ghost"
size="sm"
className="h-7"
onClick={() => handleCopyCode(endpoint.requestBody!.example, `req-${endpoint.id}`)}
>
{copiedEndpoint === `req-${endpoint.id}` ? (
<Check className="h-3 w-3 mr-1" />
) : (
<Copy className="h-3 w-3 mr-1" />
)}
</Button>
</div>
<pre className="bg-muted p-4 rounded-md overflow-x-auto text-sm font-mono">
{endpoint.requestBody.example}
</pre>
</div>
</AccordionContent>
</AccordionItem>
)}
<AccordionItem value="responses">
<AccordionTrigger></AccordionTrigger>
<AccordionContent>
<div className="space-y-4">
{endpoint.responses.map((response, index) => (
<div key={index} className="space-y-2">
<div className="flex items-center gap-2">
<Badge
className={
response.code.startsWith("2")
? "bg-green-100 text-green-800"
: response.code.startsWith("4")
? "bg-red-100 text-red-800"
: "bg-yellow-100 text-yellow-800"
}
>
{response.code}
</Badge>
<span className="text-sm font-medium">{response.description}</span>
<Button
variant="ghost"
size="sm"
className="h-7"
onClick={() => handleCopyCode(response.example, `res-${endpoint.id}-${index}`)}
>
{copiedEndpoint === `res-${endpoint.id}-${index}` ? (
<Check className="h-3 w-3 mr-1" />
) : (
<Copy className="h-3 w-3 mr-1" />
)}
</Button>
</div>
<pre className="bg-muted p-4 rounded-md overflow-x-auto text-sm font-mono">
{response.example}
</pre>
</div>
))}
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="authentication">
<AccordionTrigger></AccordionTrigger>
<AccordionContent>
<div className="p-2">
<Badge className="bg-blue-100 text-blue-800">{endpoint.authentication}</Badge>
{endpoint.authentication === "API Key" && (
<div className="mt-2 text-sm">
<p></p>
<code className="bg-muted px-2 py-1 rounded text-sm font-mono mt-1 block">
X-API-Key: your_api_key_here
</code>
</div>
)}
{endpoint.authentication === "OAuth 2.0" && (
<div className="mt-2 text-sm">
<p></p>
<code className="bg-muted px-2 py-1 rounded text-sm font-mono mt-1 block">
Authorization: Bearer your_access_token_here
</code>
</div>
)}
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="code-examples">
<AccordionTrigger></AccordionTrigger>
<AccordionContent>
<div className="space-y-4">
<div>
<div className="flex items-center gap-2 mb-2">
<Badge className="bg-blue-100 text-blue-800">JavaScript</Badge>
<Button
variant="ghost"
size="sm"
className="h-7"
onClick={() => handleCopyCode(jsExample, `js-${endpoint.id}`)}
>
{copiedEndpoint === `js-${endpoint.id}` ? (
<Check className="h-3 w-3 mr-1" />
) : (
<Copy className="h-3 w-3 mr-1" />
)}
</Button>
</div>
<pre className="bg-muted p-4 rounded-md overflow-x-auto text-sm font-mono">
{jsExample}
</pre>
</div>
<div>
<div className="flex items-center gap-2 mb-2">
<Badge className="bg-green-100 text-green-800">Python</Badge>
<Button
variant="ghost"
size="sm"
className="h-7"
onClick={() => handleCopyCode(pythonExample, `py-${endpoint.id}`)}
>
{copiedEndpoint === `py-${endpoint.id}` ? (
<Check className="h-3 w-3 mr-1" />
) : (
<Copy className="h-3 w-3 mr-1" />
)}
</Button>
</div>
<pre className="bg-muted p-4 rounded-md overflow-x-auto text-sm font-mono">
{pythonExample}
</pre>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</CardContent>
</Card>
))}
</TabsContent>
))}
</Tabs>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>API使用指南</CardTitle>
<CardDescription>使API</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<h3 className="text-lg font-medium">1. API密钥</h3>
<p className="text-sm text-muted-foreground">
使API之前API密钥API密钥
</p>
</div>
<div className="space-y-2">
<h3 className="text-lg font-medium">2. </h3>
<p className="text-sm text-muted-foreground">API请求都需要进行认证API密钥</p>
<pre className="bg-muted p-4 rounded-md overflow-x-auto text-sm font-mono">
X-API-Key: your_api_key_here
</pre>
</div>
<div className="space-y-2">
<h3 className="text-lg font-medium">3. </h3>
<p className="text-sm text-muted-foreground">API支持JSON格式的请求和响应</p>
<pre className="bg-muted p-4 rounded-md overflow-x-auto text-sm font-mono">
Content-Type: application/json Accept: application/json
</pre>
</div>
<div className="space-y-2">
<h3 className="text-lg font-medium">4. </h3>
<p className="text-sm text-muted-foreground">
API使用标准的HTTP状态码表示请求的结果
</p>
<pre className="bg-muted p-4 rounded-md overflow-x-auto text-sm font-mono">
{`{
"error": "错误类型",
"message": "错误详细信息",
"code": "错误代码"
}`}
</pre>
</div>
<div className="space-y-2">
<h3 className="text-lg font-medium">5. </h3>
<p className="text-sm text-muted-foreground">
API有速率限制100429
</p>
</div>
<div className="flex justify-between items-center mt-4">
<Button variant="outline" className="gap-2">
<FileJson className="h-4 w-4" />
OpenAPI规范
</Button>
<Button variant="outline" className="gap-2">
<Code className="h-4 w-4" />
SDK
</Button>
<Button className="gap-2">
<Play className="h-4 w-4" />
API测试工具
</Button>
</div>
</CardContent>
</Card>
</div>
)
}
// 示例代码
const jsExample = `// 使用fetch API调用
const apiKey = 'your_api_key_here';
fetch('https://api.example.com/api/users', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey
}
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));`
const pythonExample = `# 使用requests库调用
import requests
api_key = 'your_api_key_here'
headers = {
'Content-Type': 'application/json',
'X-API-Key': api_key
}
response = requests.get('https://api.example.com/api/users', headers=headers)
data = response.json()
print(data)`

View File

@@ -0,0 +1,698 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Badge } from "@/components/ui/badge"
import { Separator } from "@/components/ui/separator"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Plus, Trash2, Clock, RefreshCw, Database, Server, FileText, Settings, HelpCircle } from "lucide-react"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
interface CollectionTask {
id: string
name: string
source: string
type: string
schedule: string
lastRun: string | null
nextRun: string | null
status: "active" | "paused" | "error" | "completed"
recordsCollected: number
}
export function DataCollectionSettings() {
const [activeTab, setActiveTab] = useState("tasks")
const [isAddingTask, setIsAddingTask] = useState(false)
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null)
// 模拟数据采集任务
const [collectionTasks, setCollectionTasks] = useState<CollectionTask[]>([
{
id: "1",
name: "用户基本信息采集",
source: "CRM系统",
type: "数据库",
schedule: "每日 00:00",
lastRun: "2023-07-20 00:00",
nextRun: "2023-07-21 00:00",
status: "active",
recordsCollected: 125678,
},
{
id: "2",
name: "用户行为数据采集",
source: "行为分析平台",
type: "API",
schedule: "每小时",
lastRun: "2023-07-20 15:00",
nextRun: "2023-07-20 16:00",
status: "active",
recordsCollected: 458921,
},
{
id: "3",
name: "社交媒体数据采集",
source: "MCP SERVER",
type: "AI工具",
schedule: "每日 02:00",
lastRun: "2023-07-20 02:00",
nextRun: "2023-07-21 02:00",
status: "active",
recordsCollected: 87452,
},
{
id: "4",
name: "历史交易数据导入",
source: "交易系统",
type: "文件",
schedule: "手动",
lastRun: "2023-07-15 10:30",
nextRun: null,
status: "completed",
recordsCollected: 254789,
},
{
id: "5",
name: "设备信息采集",
source: "设备管理系统",
type: "API",
schedule: "每日 03:00",
lastRun: "2023-07-20 03:00",
nextRun: "2023-07-21 03:00",
status: "error",
recordsCollected: 12547,
},
])
// 模拟数据源
const dataSources = [
{ id: "crm", name: "CRM系统", type: "数据库" },
{ id: "behavior", name: "行为分析平台", type: "API" },
{ id: "mcp", name: "MCP SERVER", type: "AI工具" },
{ id: "transaction", name: "交易系统", type: "文件" },
{ id: "device", name: "设备管理系统", type: "API" },
{ id: "social", name: "社交媒体平台", type: "API" },
{ id: "ecommerce", name: "电商平台", type: "API" },
]
// 模拟采集类型
const collectionTypes = [
{ id: "full", name: "全量采集", description: "采集所有数据" },
{ id: "incremental", name: "增量采集", description: "仅采集新增或变更的数据" },
{ id: "scheduled", name: "定时采集", description: "按照设定的时间计划采集数据" },
{ id: "event", name: "事件触发采集", description: "在特定事件发生时采集数据" },
{ id: "manual", name: "手动采集", description: "手动触发数据采集" },
]
const getStatusBadge = (status: CollectionTask["status"]) => {
switch (status) {
case "active":
return <Badge className="bg-green-100 text-green-800"></Badge>
case "paused":
return <Badge className="bg-yellow-100 text-yellow-800"></Badge>
case "error":
return <Badge className="bg-red-100 text-red-800"></Badge>
case "completed":
return <Badge className="bg-blue-100 text-blue-800"></Badge>
}
}
const getSourceIcon = (type: string) => {
switch (type) {
case "数据库":
return <Database className="h-4 w-4 text-blue-500" />
case "API":
return <Server className="h-4 w-4 text-purple-500" />
case "文件":
return <FileText className="h-4 w-4 text-green-500" />
case "AI工具":
return <Settings className="h-4 w-4 text-orange-500" />
default:
return <Database className="h-4 w-4" />
}
}
const addTask = (task: Omit<CollectionTask, "id" | "lastRun" | "recordsCollected">) => {
const newTask: CollectionTask = {
id: `task-${Date.now()}`,
...task,
lastRun: null,
recordsCollected: 0,
}
setCollectionTasks([...collectionTasks, newTask])
setIsAddingTask(false)
}
const deleteTask = (id: string) => {
setCollectionTasks(collectionTasks.filter((task) => task.id !== id))
}
const toggleTaskStatus = (id: string) => {
setCollectionTasks(
collectionTasks.map((task) => {
if (task.id === id) {
return {
...task,
status: task.status === "active" ? "paused" : "active",
}
}
return task
}),
)
}
const selectedTask = selectedTaskId ? collectionTasks.find((task) => task.id === selectedTaskId) : null
return (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
<TabsList>
<TabsTrigger value="tasks"></TabsTrigger>
<TabsTrigger value="sources"></TabsTrigger>
<TabsTrigger value="settings"></TabsTrigger>
</TabsList>
<TabsContent value="tasks" className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<Badge className="bg-blue-100 text-blue-800"> {collectionTasks.length} </Badge>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon">
<HelpCircle className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p className="max-w-xs">
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Button onClick={() => setIsAddingTask(true)}>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{collectionTasks.map((task) => (
<TableRow key={task.id}>
<TableCell className="font-medium">{task.name}</TableCell>
<TableCell>
<div className="flex items-center gap-1">
{getSourceIcon(task.type)}
<span>{task.source}</span>
</div>
</TableCell>
<TableCell>{task.schedule}</TableCell>
<TableCell>{task.lastRun || "从未运行"}</TableCell>
<TableCell>{task.nextRun || "无计划"}</TableCell>
<TableCell>{getStatusBadge(task.status)}</TableCell>
<TableCell>{task.recordsCollected.toLocaleString()}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end space-x-2">
<Button
variant="ghost"
size="icon"
onClick={() => toggleTaskStatus(task.id)}
disabled={task.status === "completed"}
>
{task.status === "active" ? (
<Clock className="h-4 w-4 text-yellow-500" />
) : (
<RefreshCw className="h-4 w-4 text-green-500" />
)}
</Button>
<Button variant="ghost" size="icon" onClick={() => setSelectedTaskId(task.id)}>
<Settings className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" onClick={() => deleteTask(task.id)}>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</TabsContent>
<TabsContent value="sources" className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-medium"></h3>
<Button variant="outline"></Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{dataSources.map((source) => (
<Card key={source.id} className="border shadow-sm">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{getSourceIcon(source.type)}
<div>
<h4 className="font-medium">{source.name}</h4>
<p className="text-sm text-muted-foreground">{source.type}</p>
</div>
</div>
<Button variant="ghost" size="icon">
<Settings className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
<Card className="mt-4">
<CardHeader>
<CardTitle>MCP SERVER </CardTitle>
<CardDescription>MCP SERVER AI工具的采集参数</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="mcp-server-url"></Label>
<Input id="mcp-server-url" defaultValue="https://mcp.example.com/api" />
</div>
<div className="space-y-2">
<Label htmlFor="mcp-api-key">API密钥</Label>
<Input id="mcp-api-key" type="password" defaultValue="••••••••••••••••" />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="mcp-model">AI模型选择</Label>
<Select defaultValue="gpt-4">
<SelectTrigger id="mcp-model">
<SelectValue placeholder="选择AI模型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="gpt-4">GPT-4</SelectItem>
<SelectItem value="gpt-3.5-turbo">GPT-3.5 Turbo</SelectItem>
<SelectItem value="claude-2">Claude 2</SelectItem>
<SelectItem value="llama-2">Llama 2</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="mcp-auto-process"></Label>
<Switch id="mcp-auto-process" defaultChecked />
</div>
<p className="text-sm text-muted-foreground">
使AI处理采集的数据
</p>
</div>
<Separator />
<div className="space-y-2">
<Label></Label>
<div className="grid grid-cols-2 gap-2">
<div className="flex items-center space-x-2">
<Switch id="collect-profile" defaultChecked />
<Label htmlFor="collect-profile"></Label>
</div>
<div className="flex items-center space-x-2">
<Switch id="collect-posts" defaultChecked />
<Label htmlFor="collect-posts"></Label>
</div>
<div className="flex items-center space-x-2">
<Switch id="collect-interactions" defaultChecked />
<Label htmlFor="collect-interactions"></Label>
</div>
<div className="flex items-center space-x-2">
<Switch id="collect-followers" defaultChecked />
<Label htmlFor="collect-followers"></Label>
</div>
</div>
</div>
<div className="flex justify-end">
<Button>MCP配置</Button>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="settings" className="space-y-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="enable-collection"></Label>
<Switch id="enable-collection" defaultChecked />
</div>
<p className="text-sm text-muted-foreground"></p>
</div>
<div className="space-y-2">
<Label htmlFor="collection-threads">线</Label>
<Select defaultValue="5">
<SelectTrigger id="collection-threads">
<SelectValue placeholder="选择并发线程数" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1</SelectItem>
<SelectItem value="3">3</SelectItem>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="20">20</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
</p>
</div>
<div className="space-y-2">
<Label htmlFor="retry-attempts"></Label>
<Select defaultValue="3">
<SelectTrigger id="retry-attempts">
<SelectValue placeholder="选择重试次数" />
</SelectTrigger>
<SelectContent>
<SelectItem value="0"></SelectItem>
<SelectItem value="1">1</SelectItem>
<SelectItem value="3">3</SelectItem>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="retry-delay"></Label>
<Input id="retry-delay" type="number" defaultValue="60" />
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="data-deduplication"></Label>
<Switch id="data-deduplication" defaultChecked />
</div>
<p className="text-sm text-muted-foreground"></p>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="data-validation"></Label>
<Switch id="data-validation" defaultChecked />
</div>
<p className="text-sm text-muted-foreground"></p>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="notification"></Label>
<Switch id="notification" defaultChecked />
</div>
<p className="text-sm text-muted-foreground"></p>
</div>
<div className="flex justify-end">
<Button></Button>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</CardContent>
</Card>
{/* 添加采集任务对话框 */}
<Dialog open={isAddingTask} onOpenChange={setIsAddingTask}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="task-name" className="text-right">
</Label>
<Input id="task-name" className="col-span-3" placeholder="例如:用户基本信息采集" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="data-source" className="text-right">
</Label>
<Select>
<SelectTrigger id="data-source" className="col-span-3">
<SelectValue placeholder="选择数据源" />
</SelectTrigger>
<SelectContent>
{dataSources.map((source) => (
<SelectItem key={source.id} value={source.id}>
{source.name} ({source.type})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="collection-type" className="text-right">
</Label>
<Select>
<SelectTrigger id="collection-type" className="col-span-3">
<SelectValue placeholder="选择采集类型" />
</SelectTrigger>
<SelectContent>
{collectionTypes.map((type) => (
<SelectItem key={type.id} value={type.id}>
{type.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="schedule" className="text-right">
</Label>
<Select>
<SelectTrigger id="schedule" className="col-span-3">
<SelectValue placeholder="选择采集频率" />
</SelectTrigger>
<SelectContent>
<SelectItem value="hourly"></SelectItem>
<SelectItem value="daily"></SelectItem>
<SelectItem value="weekly"></SelectItem>
<SelectItem value="monthly"></SelectItem>
<SelectItem value="manual"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="time" className="text-right">
</Label>
<Input id="time" type="time" className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<div className="text-right">
<Label htmlFor="auto-process"></Label>
</div>
<div className="flex items-center space-x-2 col-span-3">
<Switch id="auto-process" />
<Label htmlFor="auto-process"></Label>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddingTask(false)}>
</Button>
<Button
onClick={() =>
addTask({
name: "新建采集任务",
source: "CRM系统",
type: "数据库",
schedule: "每日 00:00",
nextRun: "2023-07-21 00:00",
status: "active",
})
}
>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 任务详情对话框 */}
<Dialog open={!!selectedTaskId} onOpenChange={(open) => !open && setSelectedTaskId(null)}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{selectedTask && (
<div className="space-y-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-sm text-muted-foreground"></Label>
<p className="font-medium">{selectedTask.name}</p>
</div>
<div>
<Label className="text-sm text-muted-foreground"></Label>
<p className="font-medium">{selectedTask.source}</p>
</div>
<div>
<Label className="text-sm text-muted-foreground"></Label>
<p className="font-medium">{selectedTask.schedule}</p>
</div>
<div>
<Label className="text-sm text-muted-foreground"></Label>
<div>{getStatusBadge(selectedTask.status)}</div>
</div>
<div>
<Label className="text-sm text-muted-foreground"></Label>
<p className="font-medium">{selectedTask.lastRun || "从未运行"}</p>
</div>
<div>
<Label className="text-sm text-muted-foreground"></Label>
<p className="font-medium">{selectedTask.nextRun || "无计划"}</p>
</div>
<div>
<Label className="text-sm text-muted-foreground"></Label>
<p className="font-medium">{selectedTask.recordsCollected.toLocaleString()}</p>
</div>
</div>
<Separator />
<div className="space-y-2">
<Label></Label>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>ID</TableCell>
<TableCell></TableCell>
<TableCell>
<Switch defaultChecked />
</TableCell>
</TableRow>
<TableRow>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell>
<Switch defaultChecked />
</TableCell>
</TableRow>
<TableRow>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell>
<Switch defaultChecked />
</TableCell>
</TableRow>
<TableRow>
<TableCell>IMEI</TableCell>
<TableCell></TableCell>
<TableCell>
<Switch defaultChecked />
</TableCell>
</TableRow>
<TableRow>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell>
<Switch defaultChecked />
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<div className="space-y-2">
<Label></Label>
<div className="grid grid-cols-2 gap-2">
<div className="flex items-center space-x-2">
<Switch defaultChecked />
<Label></Label>
</div>
<div className="flex items-center space-x-2">
<Switch defaultChecked />
<Label></Label>
</div>
<div className="flex items-center space-x-2">
<Switch defaultChecked />
<Label></Label>
</div>
<div className="flex items-center space-x-2">
<Switch defaultChecked />
<Label></Label>
</div>
</div>
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setSelectedTaskId(null)}>
</Button>
<Button variant="outline" onClick={() => {}}>
</Button>
<Button onClick={() => setSelectedTaskId(null)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -0,0 +1,950 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Checkbox } from "@/components/ui/checkbox"
import { Badge } from "@/components/ui/badge"
import { Separator } from "@/components/ui/separator"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Search, Filter, Download, RefreshCw, Play, ArrowRight, Settings } from "lucide-react"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
// 模拟图表组件
const NetworkGraph = ({ data, title }: { data: any; title: string }) => (
<div className="w-full h-96 bg-muted/30 rounded-md flex items-center justify-center">
<div className="text-center">
<p className="text-muted-foreground">{title}</p>
<p className="text-sm text-muted-foreground"></p>
</div>
</div>
)
const ScatterPlot = ({ data, title }: { data: any; title: string }) => (
<div className="w-full h-64 bg-muted/30 rounded-md flex items-center justify-center">
<div className="text-center">
<p className="text-muted-foreground">{title}</p>
<p className="text-sm text-muted-foreground"></p>
</div>
</div>
)
const HeatMap = ({ data, title }: { data: any; title: string }) => (
<div className="w-full h-64 bg-muted/30 rounded-md flex items-center justify-center">
<div className="text-center">
<p className="text-muted-foreground">{title}</p>
<p className="text-sm text-muted-foreground"></p>
</div>
</div>
)
interface CorrelationResult {
id: string
field1: string
field2: string
correlationCoefficient: number
significance: number
sampleSize: number
createdAt: string
dataSource: string
}
interface UserSegmentCorrelation {
id: string
segment1: string
segment2: string
overlapPercentage: number
uniqueUsers1: number
uniqueUsers2: number
commonUsers: number
}
export function DataCorrelationAnalysis() {
const [activeTab, setActiveTab] = useState("field-correlation")
const [isCreatingAnalysis, setIsCreatingAnalysis] = useState(false)
const [selectedFields, setSelectedFields] = useState<string[]>([])
const [selectedDataSources, setSelectedDataSources] = useState<string[]>([])
// 模拟字段列表
const fields = [
{ id: "userId", name: "用户ID", type: "string", dataSource: "所有数据源" },
{ id: "phoneNumber", name: "手机号", type: "string", dataSource: "所有数据源" },
{ id: "identityNumber", name: "身份证号", type: "string", dataSource: "所有数据源" },
{ id: "deviceImei", name: "IMEI设备号", type: "string", dataSource: "所有数据源" },
{ id: "registrationDate", name: "注册时间", type: "datetime", dataSource: "用户数据库" },
{ id: "lastActiveTime", name: "最后活跃时间", type: "datetime", dataSource: "行为分析平台" },
{ id: "age", name: "年龄", type: "number", dataSource: "用户数据库" },
{ id: "gender", name: "性别", type: "string", dataSource: "用户数据库" },
{ id: "location", name: "地区", type: "string", dataSource: "用户数据库" },
{ id: "totalSpent", name: "消费总额", type: "number", dataSource: "交易系统" },
{ id: "purchaseFrequency", name: "购买频率", type: "number", dataSource: "交易系统" },
{ id: "averageOrderValue", name: "平均订单金额", type: "number", dataSource: "交易系统" },
{ id: "preferredCategory", name: "偏好类别", type: "string", dataSource: "交易系统" },
{ id: "loginFrequency", name: "登录频率", type: "number", dataSource: "行为分析平台" },
{ id: "timeSpentPerSession", name: "单次会话时长", type: "number", dataSource: "行为分析平台" },
{ id: "clickThroughRate", name: "点击率", type: "number", dataSource: "行为分析平台" },
{ id: "conversionRate", name: "转化率", type: "number", dataSource: "行为分析平台" },
{ id: "rfmScore", name: "RFM评分", type: "number", dataSource: "用户价值模型" },
{ id: "lifetimeValue", name: "生命周期价值", type: "number", dataSource: "用户价值模型" },
{ id: "churnRisk", name: "流失风险", type: "number", dataSource: "用户价值模型" },
]
// 模拟数据源
const dataSources = [
{ id: "all", name: "所有数据源" },
{ id: "user-db", name: "用户数据库" },
{ id: "transaction", name: "交易系统" },
{ id: "behavior", name: "行为分析平台" },
{ id: "user-value", name: "用户价值模型" },
]
// 模拟用户分群
const userSegments = [
{ id: "high-value", name: "高价值用户", count: 12500 },
{ id: "new-users", name: "新注册用户", count: 45600 },
{ id: "inactive", name: "非活跃用户", count: 28900 },
{ id: "potential", name: "潜在高转化用户", count: 18700 },
{ id: "loyal", name: "忠诚用户", count: 9800 },
{ id: "risk", name: "流失风险用户", count: 15400 },
]
// 模拟相关性分析结果
const correlationResults: CorrelationResult[] = [
{
id: "corr-1",
field1: "年龄",
field2: "消费总额",
correlationCoefficient: 0.72,
significance: 0.001,
sampleSize: 45678,
createdAt: "2023-07-20 15:30",
dataSource: "用户数据库 & 交易系统",
},
{
id: "corr-2",
field1: "登录频率",
field2: "购买频率",
correlationCoefficient: 0.85,
significance: 0.001,
sampleSize: 38945,
createdAt: "2023-07-20 15:30",
dataSource: "行为分析平台 & 交易系统",
},
{
id: "corr-3",
field1: "单次会话时长",
field2: "转化率",
correlationCoefficient: 0.65,
significance: 0.01,
sampleSize: 42156,
createdAt: "2023-07-20 15:30",
dataSource: "行为分析平台",
},
{
id: "corr-4",
field1: "地区",
field2: "偏好类别",
correlationCoefficient: 0.58,
significance: 0.05,
sampleSize: 45678,
createdAt: "2023-07-20 15:30",
dataSource: "用户数据库 & 交易系统",
},
{
id: "corr-5",
field1: "RFM评分",
field2: "生命周期价值",
correlationCoefficient: 0.92,
significance: 0.001,
sampleSize: 45678,
createdAt: "2023-07-20 15:30",
dataSource: "用户价值模型",
},
]
// 模拟用户分群相关性
const segmentCorrelations: UserSegmentCorrelation[] = [
{
id: "seg-corr-1",
segment1: "高价值用户",
segment2: "忠诚用户",
overlapPercentage: 68.5,
uniqueUsers1: 12500,
uniqueUsers2: 9800,
commonUsers: 6713,
},
{
id: "seg-corr-2",
segment1: "新注册用户",
segment2: "潜在高转化用户",
overlapPercentage: 12.3,
uniqueUsers1: 45600,
uniqueUsers2: 18700,
commonUsers: 5609,
},
{
id: "seg-corr-3",
segment1: "非活跃用户",
segment2: "流失风险用户",
overlapPercentage: 75.2,
uniqueUsers1: 28900,
uniqueUsers2: 15400,
commonUsers: 11581,
},
{
id: "seg-corr-4",
segment1: "高价值用户",
segment2: "潜在高转化用户",
overlapPercentage: 8.7,
uniqueUsers1: 12500,
uniqueUsers2: 18700,
commonUsers: 1088,
},
{
id: "seg-corr-5",
segment1: "忠诚用户",
segment2: "流失风险用户",
overlapPercentage: 2.1,
uniqueUsers1: 9800,
uniqueUsers2: 15400,
commonUsers: 206,
},
]
const getCorrelationStrength = (coefficient: number) => {
const absCoefficient = Math.abs(coefficient)
if (absCoefficient >= 0.8) return "强"
if (absCoefficient >= 0.5) return "中"
return "弱"
}
const getCorrelationBadge = (coefficient: number) => {
const absCoefficient = Math.abs(coefficient)
if (absCoefficient >= 0.8) {
return <Badge className="bg-green-100 text-green-800"></Badge>
}
if (absCoefficient >= 0.5) {
return <Badge className="bg-yellow-100 text-yellow-800"></Badge>
}
return <Badge className="bg-gray-100 text-gray-800"></Badge>
}
const getSignificanceBadge = (significance: number) => {
if (significance <= 0.001) {
return <Badge className="bg-green-100 text-green-800"> (p0.001)</Badge>
}
if (significance <= 0.01) {
return <Badge className="bg-green-100 text-green-800"> (p0.01)</Badge>
}
if (significance <= 0.05) {
return <Badge className="bg-yellow-100 text-yellow-800"> (p0.05)</Badge>
}
return <Badge className="bg-red-100 text-red-800"> (p>0.05)</Badge>
}
const toggleFieldSelection = (fieldId: string) => {
if (selectedFields.includes(fieldId)) {
setSelectedFields(selectedFields.filter((id) => id !== fieldId))
} else {
setSelectedFields([...selectedFields, fieldId])
}
}
const toggleDataSourceSelection = (sourceId: string) => {
if (selectedDataSources.includes(sourceId)) {
setSelectedDataSources(selectedDataSources.filter((id) => id !== sourceId))
} else {
setSelectedDataSources([...selectedDataSources, sourceId])
}
}
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold"></h2>
<div className="flex items-center gap-2">
<Button variant="outline">
<Download className="mr-2 h-4 w-4" &gt;
导出分析结果
</Button>
<Button onClick={() => setIsCreatingAnalysis(true)}>
<Play className="mr-2 h-4 w-4" &gt;
创建新分析
</Button>
</div>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
<TabsList>
<TabsTrigger value="field-correlation">字段相关性</TabsTrigger>
<TabsTrigger value="segment-correlation">分群相关性</TabsTrigger>
<TabsTrigger value="user-network">用户关联网络</TabsTrigger>
<TabsTrigger value="path-analysis">用户路径分析</TabsTrigger>
</TabsList>
<TabsContent value="field-correlation" className="space-y-4">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle>字段相关性分析</CardTitle>
<div className="flex items-center gap-2">
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input type="text" placeholder="搜索字段..." className="pl-8 w-[200px]" />
</div>
<Select defaultValue="all">
<SelectTrigger className="w-[150px]">
<Filter className="mr-2 h-4 w-4" />
<SelectValue placeholder="相关性强度" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">所有强度</SelectItem>
<SelectItem value="strong">强相关</SelectItem>
<SelectItem value="medium">中等相关</SelectItem>
<SelectItem value="weak">弱相关</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>字段1</TableHead>
<TableHead>字段2</TableHead>
<TableHead>相关系数</TableHead>
<TableHead>相关强度</TableHead>
<TableHead>显著性</TableHead>
<TableHead>样本量</TableHead>
<TableHead>数据源</TableHead>
<TableHead className="text-right">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{correlationResults.map((result) => (
<TableRow key={result.id}>
<TableCell className="font-medium">{result.field1}</TableCell>
<TableCell className="font-medium">{result.field2}</TableCell>
<TableCell>{result.correlationCoefficient.toFixed(2)}</TableCell>
<TableCell>{getCorrelationBadge(result.correlationCoefficient)}</TableCell>
<TableCell>{getSignificanceBadge(result.significance)}</TableCell>
<TableCell>{result.sampleSize.toLocaleString()}</TableCell>
<TableCell>{result.dataSource}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm">
查看详情
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle>相关性热力图</CardTitle>
<CardDescription>字段间相关性强度可视化</CardDescription>
</CardHeader>
<CardContent>
<HeatMap data={{}} title="字段相关性热力图" />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>散点图分析</CardTitle>
<CardDescription>选择两个字段查看其相关性散点图</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex gap-4">
<div className="flex-1">
<Label>X轴字段</Label>
<Select defaultValue="age">
<SelectTrigger>
<SelectValue placeholder="选择字段" />
</SelectTrigger>
<SelectContent>
{fields
.filter((field) => field.type === "number")
.map((field) => (
<SelectItem key={field.id} value={field.id}>
{field.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex-1">
<Label>Y轴字段</Label>
<Select defaultValue="totalSpent">
<SelectTrigger>
<SelectValue placeholder="选择字段" />
</SelectTrigger>
<SelectContent>
{fields
.filter((field) => field.type === "number")
.map((field) => (
<SelectItem key={field.id} value={field.id}>
{field.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<ScatterPlot data={{}} title="年龄 vs 消费总额" />
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="segment-correlation" className="space-y-4">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle></CardTitle>
<Button></Button>
</div>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>1</TableHead>
<TableHead>2</TableHead>
<TableHead></TableHead>
<TableHead>1</TableHead>
<TableHead>2</TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{segmentCorrelations.map((correlation) => (
<TableRow key={correlation.id}>
<TableCell className="font-medium">{correlation.segment1}</TableCell>
<TableCell className="font-medium">{correlation.segment2}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<span>{correlation.overlapPercentage.toFixed(1)}%</span>
{correlation.overlapPercentage > 50 ? (
<Badge className="bg-green-100 text-green-800"></Badge>
) : correlation.overlapPercentage > 20 ? (
<Badge className="bg-yellow-100 text-yellow-800"></Badge>
) : (
<Badge className="bg-gray-100 text-gray-800"></Badge>
)}
</div>
</TableCell>
<TableCell>{correlation.uniqueUsers1.toLocaleString()}</TableCell>
<TableCell>{correlation.uniqueUsers2.toLocaleString()}</TableCell>
<TableCell>{correlation.commonUsers.toLocaleString()}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm">
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex gap-4">
<div className="flex-1">
<Label>1</Label>
<Select defaultValue="high-value">
<SelectTrigger>
<SelectValue placeholder="选择分群" />
</SelectTrigger>
<SelectContent>
{userSegments.map((segment) => (
<SelectItem key={segment.id} value={segment.id}>
{segment.name} ({segment.count.toLocaleString()})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex-1">
<Label>2</Label>
<Select defaultValue="loyal">
<SelectTrigger>
<SelectValue placeholder="选择分群" />
</SelectTrigger>
<SelectContent>
{userSegments.map((segment) => (
<SelectItem key={segment.id} value={segment.id}>
{segment.name} ({segment.count.toLocaleString()})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="p-6 border rounded-md">
<div className="flex justify-center items-center">
<div className="w-32 h-32 rounded-full bg-blue-100 flex items-center justify-center text-blue-800 font-bold text-lg">
</div>
<div className="w-24 h-24 bg-blue-200 mx-4 flex items-center justify-center text-blue-800 font-bold">
<br />
68.5%
</div>
<div className="w-32 h-32 rounded-full bg-green-100 flex items-center justify-center text-green-800 font-bold text-lg">
</div>
</div>
<div className="mt-4 text-center">
<p className="text-sm text-muted-foreground">
共同用户: 6,713 | 高价值用户: 12,500 | 忠诚用户: 9,800
</p>
</div>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="user-network" className="space-y-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex gap-2">
<Select defaultValue="all">
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="关联维度" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="id">ID</SelectItem>
<SelectItem value="phone"></SelectItem>
<SelectItem value="identity"></SelectItem>
<SelectItem value="imei">IMEI设备号</SelectItem>
</SelectContent>
</Select>
<Select defaultValue="high-value">
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="用户分群" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{userSegments.map((segment) => (
<SelectItem key={segment.id} value={segment.id}>
{segment.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm">
<Settings className="mr-2 h-4 w-4" />
</Button>
<Button variant="outline" size="sm">
<RefreshCw className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
<NetworkGraph data={{}} title="用户关联网络图" />
<div className="flex justify-between items-center">
<div>
<h3 className="text-sm font-medium"></h3>
<p className="text-xs text-muted-foreground">节点: 1,245 | 连接: 3,567 | 平均连接度: 2.87</p>
</div>
<div className="flex gap-2">
<Badge className="bg-blue-100 text-blue-800">ID</Badge>
<Badge className="bg-green-100 text-green-800"></Badge>
<Badge className="bg-purple-100 text-purple-800"></Badge>
<Badge className="bg-orange-100 text-orange-800">IMEI设备号</Badge>
</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium">-001</TableCell>
<TableCell>125</TableCell>
<TableCell>
<Badge className="bg-green-100 text-green-800"></Badge>
</TableCell>
<TableCell>, IMEI设备号</TableCell>
<TableCell>2023-07-20 15:30</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm">
</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">-002</TableCell>
<TableCell>87</TableCell>
<TableCell>
<Badge className="bg-green-100 text-green-800"></Badge>
</TableCell>
<TableCell>, ID</TableCell>
<TableCell>2023-07-20 15:30</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm">
</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">-003</TableCell>
<TableCell>215</TableCell>
<TableCell>
<Badge className="bg-yellow-100 text-yellow-800"></Badge>
</TableCell>
<TableCell>, ID</TableCell>
<TableCell>2023-07-20 15:30</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm">
</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">-004</TableCell>
<TableCell>56</TableCell>
<TableCell>
<Badge className="bg-gray-100 text-gray-800"></Badge>
</TableCell>
<TableCell>IMEI设备号</TableCell>
<TableCell>2023-07-20 15:30</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm">
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="path-analysis" className="space-y-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex gap-2">
<Select defaultValue="registration-to-purchase">
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="选择路径类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="registration-to-purchase"></SelectItem>
<SelectItem value="browse-to-purchase"></SelectItem>
<SelectItem value="multi-purchase"></SelectItem>
<SelectItem value="churn-path"></SelectItem>
</SelectContent>
</Select>
<Select defaultValue="all">
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="用户分群" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{userSegments.map((segment) => (
<SelectItem key={segment.id} value={segment.id}>
{segment.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button variant="outline">
<RefreshCw className="mr-2 h-4 w-4" />
</Button>
</div>
<div className="p-4 border rounded-md">
<div className="flex justify-center items-center">
<div className="flex flex-col items-center">
<div className="w-32 h-12 bg-blue-100 rounded-md flex items-center justify-center text-blue-800 font-medium">
</div>
<div className="text-xs text-muted-foreground mt-1">100%</div>
</div>
<div className="flex flex-col items-center mx-2">
<ArrowRight className="h-6 w-6 text-muted-foreground" />
<div className="text-xs text-muted-foreground">85%</div>
</div>
<div className="flex flex-col items-center">
<div className="w-32 h-12 bg-blue-100 rounded-md flex items-center justify-center text-blue-800 font-medium">
</div>
<div className="text-xs text-muted-foreground mt-1">85%</div>
</div>
<div className="flex flex-col items-center mx-2">
<ArrowRight className="h-6 w-6 text-muted-foreground" />
<div className="text-xs text-muted-foreground">65%</div>
</div>
<div className="flex flex-col items-center">
<div className="w-32 h-12 bg-blue-100 rounded-md flex items-center justify-center text-blue-800 font-medium">
</div>
<div className="text-xs text-muted-foreground mt-1">55%</div>
</div>
<div className="flex flex-col items-center mx-2">
<ArrowRight className="h-6 w-6 text-muted-foreground" />
<div className="text-xs text-muted-foreground">40%</div>
</div>
<div className="flex flex-col items-center">
<div className="w-32 h-12 bg-green-100 rounded-md flex items-center justify-center text-green-800 font-medium">
</div>
<div className="text-xs text-muted-foreground mt-1">22%</div>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<h3 className="text-sm font-medium mb-2"></h3>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm">85%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-green-600 h-1.5 rounded-full" style={{ width: "85%" }}></div>
</div>
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm">65%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-green-600 h-1.5 rounded-full" style={{ width: "65%" }}></div>
</div>
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm">40%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-yellow-600 h-1.5 rounded-full" style={{ width: "40%" }}></div>
</div>
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm">22%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-yellow-600 h-1.5 rounded-full" style={{ width: "22%" }}></div>
</div>
</div>
</div>
<div>
<h3 className="text-sm font-medium mb-2"></h3>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm">15%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-red-600 h-1.5 rounded-full" style={{ width: "15%" }}></div>
</div>
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm">35%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-red-600 h-1.5 rounded-full" style={{ width: "35%" }}></div>
</div>
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm">60%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-red-600 h-1.5 rounded-full" style={{ width: "60%" }}></div>
</div>
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm">78%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-red-600 h-1.5 rounded-full" style={{ width: "78%" }}></div>
</div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* 创建分析对话框 */}
<Dialog open={isCreatingAnalysis} onOpenChange={setIsCreatingAnalysis}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label></Label>
<div className="grid grid-cols-2 gap-2">
{dataSources.map((source) => (
<div key={source.id} className="flex items-center space-x-2">
<Checkbox
id={`source-${source.id}`}
checked={selectedDataSources.includes(source.id)}
onCheckedChange={() => toggleDataSourceSelection(source.id)}
/>
<Label htmlFor={`source-${source.id}`}>{source.name}</Label>
</div>
))}
</div>
</div>
<Separator />
<div className="space-y-2">
<Label></Label>
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input type="text" placeholder="搜索字段..." className="pl-8" />
</div>
<div className="h-60 overflow-y-auto border rounded-md p-2">
{fields.map((field) => (
<div key={field.id} className="flex items-center space-x-2 py-1">
<Checkbox
id={`field-${field.id}`}
checked={selectedFields.includes(field.id)}
onCheckedChange={() => toggleFieldSelection(field.id)}
/>
<Label htmlFor={`field-${field.id}`} className="flex-1">
{field.name}
</Label>
<span className="text-xs text-muted-foreground">{field.type}</span>
<Badge variant="outline">{field.dataSource}</Badge>
</div>
))}
</div>
</div>
<div className="space-y-2">
<Label></Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="correlation-method"></Label>
<Select defaultValue="pearson">
<SelectTrigger id="correlation-method">
<SelectValue placeholder="选择方法" />
</SelectTrigger>
<SelectContent>
<SelectItem value="pearson">Pearson相关系数</SelectItem>
<SelectItem value="spearman">Spearman等级相关</SelectItem>
<SelectItem value="kendall">Kendall's Tau</SelectItem>
<SelectItem value="chi-square"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="sample-size"></Label>
<Select defaultValue="all">
<SelectTrigger id="sample-size">
<SelectValue placeholder="选择样本大小" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="10000">10,000</SelectItem>
<SelectItem value="50000">50,000</SelectItem>
<SelectItem value="100000">100,000</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox id="save-result" defaultChecked />
<Label htmlFor="save-result"></Label>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsCreatingAnalysis(false)}>
</Button>
<Button onClick={() => setIsCreatingAnalysis(false)}>
<Play className="mr-2 h-4 w-4" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -0,0 +1,449 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Button } from "@/components/ui/button"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Badge } from "@/components/ui/badge"
import { ArrowUp, ArrowDown, Download, RefreshCw, Calendar } from "lucide-react"
import { getGrowthClass } from "@/lib/utils"
// 模拟图表组件
const LineChart = ({ data, title }: { data: any; title: string }) => (
<div className="w-full h-64 bg-muted/30 rounded-md flex items-center justify-center">
<div className="text-center">
<p className="text-muted-foreground">{title}</p>
<p className="text-sm text-muted-foreground"></p>
</div>
</div>
)
const BarChart = ({ data, title }: { data: any; title: string }) => (
<div className="w-full h-64 bg-muted/30 rounded-md flex items-center justify-center">
<div className="text-center">
<p className="text-muted-foreground">{title}</p>
<p className="text-sm text-muted-foreground"></p>
</div>
</div>
)
const PieChart = ({ data, title }: { data: any; title: string }) => (
<div className="w-full h-64 bg-muted/30 rounded-md flex items-center justify-center">
<div className="text-center">
<p className="text-muted-foreground">{title}</p>
<p className="text-sm text-muted-foreground"></p>
</div>
</div>
)
export function DataDashboard() {
const [timeRange, setTimeRange] = useState("7d")
const [activeTab, setActiveTab] = useState("overview")
// 模拟数据
const overviewData = {
totalUsers: 1245678,
userGrowth: 12.5,
totalDataSources: 8,
dataSourceGrowth: 33.3,
totalRecords: 45678921,
recordsGrowth: 8.7,
dataQuality: 92,
dataQualityGrowth: 3.2,
}
const dataSourceStats = [
{ name: "CRM系统", records: 12500000, growth: 5.2, quality: 95 },
{ name: "交易系统", records: 8750000, growth: 12.3, quality: 90 },
{ name: "行为分析平台", records: 15800000, growth: 8.7, quality: 88 },
{ name: "社交媒体", records: 4500000, growth: 15.2, quality: 85 },
{ name: "设备管理系统", records: 3200000, growth: -2.1, quality: 93 },
]
const dataQualityIssues = [
{ type: "缺失值", count: 12500, percentage: 0.027, severity: "medium" },
{ type: "格式错误", count: 8750, percentage: 0.019, severity: "high" },
{ type: "重复数据", count: 23800, percentage: 0.052, severity: "low" },
{ type: "异常值", count: 4500, percentage: 0.01, severity: "high" },
{ type: "不一致数据", count: 9200, percentage: 0.02, severity: "medium" },
]
const formatNumber = (num: number) => {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + "M"
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + "K"
}
return num.toString()
}
const getSeverityBadge = (severity: string) => {
switch (severity) {
case "high":
return <Badge className="bg-red-100 text-red-800"></Badge>
case "medium":
return <Badge className="bg-yellow-100 text-yellow-800"></Badge>
case "low":
return <Badge className="bg-green-100 text-green-800"></Badge>
default:
return <Badge></Badge>
}
}
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold"></h2>
<div className="flex items-center gap-2">
<Select value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger className="w-[180px]">
<Calendar className="mr-2 h-4 w-4" />
<SelectValue placeholder="选择时间范围" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1d"></SelectItem>
<SelectItem value="7d">7</SelectItem>
<SelectItem value="30d">30</SelectItem>
<SelectItem value="90d">90</SelectItem>
<SelectItem value="1y">1</SelectItem>
<SelectItem value="custom"></SelectItem>
</SelectContent>
</Select>
<Button variant="outline" size="icon">
<RefreshCw className="h-4 w-4" />
</Button>
<Button variant="outline">
<Download className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardContent className="p-6">
<div className="flex flex-col space-y-2">
<p className="text-sm text-muted-foreground"></p>
<div className="flex items-end justify-between">
<h3 className="text-2xl font-bold">{formatNumber(overviewData.totalUsers)}</h3>
<div className={`flex items-center ${getGrowthClass(overviewData.userGrowth)}`}>
{overviewData.userGrowth > 0 ? (
<ArrowUp className="h-4 w-4 mr-1" />
) : (
<ArrowDown className="h-4 w-4 mr-1" />
)}
<span>{Math.abs(overviewData.userGrowth)}%</span>
</div>
</div>
<p className="text-xs text-muted-foreground"></p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex flex-col space-y-2">
<p className="text-sm text-muted-foreground"></p>
<div className="flex items-end justify-between">
<h3 className="text-2xl font-bold">{overviewData.totalDataSources}</h3>
<div className={`flex items-center ${getGrowthClass(overviewData.dataSourceGrowth)}`}>
{overviewData.dataSourceGrowth > 0 ? (
<ArrowUp className="h-4 w-4 mr-1" />
) : (
<ArrowDown className="h-4 w-4 mr-1" />
)}
<span>{Math.abs(overviewData.dataSourceGrowth)}%</span>
</div>
</div>
<p className="text-xs text-muted-foreground"></p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex flex-col space-y-2">
<p className="text-sm text-muted-foreground"></p>
<div className="flex items-end justify-between">
<h3 className="text-2xl font-bold">{formatNumber(overviewData.totalRecords)}</h3>
<div className={`flex items-center ${getGrowthClass(overviewData.recordsGrowth)}`}>
{overviewData.recordsGrowth > 0 ? (
<ArrowUp className="h-4 w-4 mr-1" />
) : (
<ArrowDown className="h-4 w-4 mr-1" />
)}
<span>{Math.abs(overviewData.recordsGrowth)}%</span>
</div>
</div>
<p className="text-xs text-muted-foreground"></p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex flex-col space-y-2">
<p className="text-sm text-muted-foreground"></p>
<div className="flex items-end justify-between">
<h3 className="text-2xl font-bold">{overviewData.dataQuality}/100</h3>
<div className={`flex items-center ${getGrowthClass(overviewData.dataQualityGrowth)}`}>
{overviewData.dataQualityGrowth > 0 ? (
<ArrowUp className="h-4 w-4 mr-1" />
) : (
<ArrowDown className="h-4 w-4 mr-1" />
)}
<span>{Math.abs(overviewData.dataQualityGrowth)}%</span>
</div>
</div>
<p className="text-xs text-muted-foreground"></p>
</div>
</CardContent>
</Card>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
<TabsList>
<TabsTrigger value="overview"></TabsTrigger>
<TabsTrigger value="sources"></TabsTrigger>
<TabsTrigger value="quality"></TabsTrigger>
<TabsTrigger value="integration"></TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<LineChart data={{}} title="用户数据增长趋势图" />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<PieChart data={{}} title="数据源分布图" />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<LineChart data={{}} title="数据质量趋势图" />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<BarChart data={{}} title="数据整合进度图" />
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="sources" className="space-y-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<table className="min-w-full divide-y divide-border">
<thead>
<tr className="bg-muted/50">
<th className="px-4 py-3 text-left text-sm font-medium"></th>
<th className="px-4 py-3 text-left text-sm font-medium"></th>
<th className="px-4 py-3 text-left text-sm font-medium"></th>
<th className="px-4 py-3 text-left text-sm font-medium"></th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{dataSourceStats.map((source, index) => (
<tr key={index}>
<td className="px-4 py-3 text-sm font-medium">{source.name}</td>
<td className="px-4 py-3 text-sm">{formatNumber(source.records)}</td>
<td className="px-4 py-3 text-sm">
<div className={`flex items-center ${getGrowthClass(source.growth)}`}>
{source.growth > 0 ? (
<ArrowUp className="h-4 w-4 mr-1" />
) : (
<ArrowDown className="h-4 w-4 mr-1" />
)}
<span>{Math.abs(source.growth)}%</span>
</div>
</td>
<td className="px-4 py-3 text-sm">{source.quality}/100</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<BarChart data={{}} title="数据源记录数对比图" />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<BarChart data={{}} title="数据源质量对比图" />
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="quality" className="space-y-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<table className="min-w-full divide-y divide-border">
<thead>
<tr className="bg-muted/50">
<th className="px-4 py-3 text-left text-sm font-medium"></th>
<th className="px-4 py-3 text-left text-sm font-medium"></th>
<th className="px-4 py-3 text-left text-sm font-medium"></th>
<th className="px-4 py-3 text-left text-sm font-medium"></th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{dataQualityIssues.map((issue, index) => (
<tr key={index}>
<td className="px-4 py-3 text-sm font-medium">{issue.type}</td>
<td className="px-4 py-3 text-sm">{formatNumber(issue.count)}</td>
<td className="px-4 py-3 text-sm">{(issue.percentage * 100).toFixed(2)}%</td>
<td className="px-4 py-3 text-sm">{getSeverityBadge(issue.severity)}</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<PieChart data={{}} title="数据质量问题分布图" />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<LineChart data={{}} title="数据质量趋势图" />
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="integration" className="space-y-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="font-medium">ID整合率</span>
<span className="text-green-600 font-medium">95%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div className="bg-green-600 h-2.5 rounded-full" style={{ width: "95%" }}></div>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="font-medium"></span>
<span className="text-green-600 font-medium">87%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div className="bg-green-600 h-2.5 rounded-full" style={{ width: "87%" }}></div>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="font-medium"></span>
<span className="text-yellow-600 font-medium">72%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div className="bg-yellow-600 h-2.5 rounded-full" style={{ width: "72%" }}></div>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="font-medium">IMEI设备号整合率</span>
<span className="text-yellow-600 font-medium">68%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div className="bg-yellow-600 h-2.5 rounded-full" style={{ width: "68%" }}></div>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="font-medium"></span>
<span className="text-green-600 font-medium">82%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div className="bg-green-600 h-2.5 rounded-full" style={{ width: "82%" }}></div>
</div>
</div>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<LineChart data={{}} title="数据整合趋势图" />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<BarChart data={{}} title="标识符覆盖率图" />
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</div>
)
}

View File

@@ -0,0 +1,321 @@
"use client"
import { useState } from "react"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { ArrowRight, Plus, Save, Trash2, HelpCircle } from "lucide-react"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
interface FieldMapping {
id: string
sourceField: string
targetField: string
dataType: string
required: boolean
transformation: string
primaryKey: boolean
}
export function DataFieldMapping() {
// 模拟数据
const [mappings, setMappings] = useState<FieldMapping[]>([
{
id: "1",
sourceField: "user_id",
targetField: "userId",
dataType: "string",
required: true,
transformation: "none",
primaryKey: true,
},
{
id: "2",
sourceField: "phone",
targetField: "phoneNumber",
dataType: "string",
required: true,
transformation: "formatPhoneNumber",
primaryKey: false,
},
{
id: "3",
sourceField: "id_card",
targetField: "identityNumber",
dataType: "string",
required: true,
transformation: "mask",
primaryKey: false,
},
{
id: "4",
sourceField: "imei",
targetField: "deviceImei",
dataType: "string",
required: false,
transformation: "none",
primaryKey: false,
},
{
id: "5",
sourceField: "name",
targetField: "fullName",
dataType: "string",
required: true,
transformation: "none",
primaryKey: false,
},
{
id: "6",
sourceField: "register_time",
targetField: "registrationDate",
dataType: "datetime",
required: true,
transformation: "parseDateTime",
primaryKey: false,
},
{
id: "7",
sourceField: "last_login",
targetField: "lastActiveTime",
dataType: "datetime",
required: false,
transformation: "parseDateTime",
primaryKey: false,
},
])
const addNewMapping = () => {
const newMapping: FieldMapping = {
id: `new-${Date.now()}`,
sourceField: "",
targetField: "",
dataType: "string",
required: false,
transformation: "none",
primaryKey: false,
}
setMappings([...mappings, newMapping])
}
const updateMapping = (id: string, field: keyof FieldMapping, value: any) => {
setMappings(mappings.map((mapping) => (mapping.id === id ? { ...mapping, [field]: value } : mapping)))
}
const deleteMapping = (id: string) => {
setMappings(mappings.filter((mapping) => mapping.id !== id))
}
return (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>
IDIMEI设备号为标准整合客户数据
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<Badge className="bg-blue-100 text-blue-800"></Badge>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon">
<HelpCircle className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p className="max-w-xs">
便
IDIMEI设备号
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Button onClick={addNewMapping}>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mappings.map((mapping) => (
<TableRow key={mapping.id}>
<TableCell>
<Input
value={mapping.sourceField}
onChange={(e) => updateMapping(mapping.id, "sourceField", e.target.value)}
placeholder="源字段名"
/>
</TableCell>
<TableCell>
<ArrowRight className="h-4 w-4 text-muted-foreground" />
</TableCell>
<TableCell>
<Input
value={mapping.targetField}
onChange={(e) => updateMapping(mapping.id, "targetField", e.target.value)}
placeholder="目标字段名"
/>
</TableCell>
<TableCell>
<Select
value={mapping.dataType}
onValueChange={(value) => updateMapping(mapping.id, "dataType", value)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="选择数据类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="string"></SelectItem>
<SelectItem value="number"></SelectItem>
<SelectItem value="boolean"></SelectItem>
<SelectItem value="datetime"></SelectItem>
<SelectItem value="array"></SelectItem>
<SelectItem value="object"></SelectItem>
</SelectContent>
</Select>
</TableCell>
<TableCell>
<div className="flex items-center justify-center">
<Switch
checked={mapping.required}
onCheckedChange={(checked) => updateMapping(mapping.id, "required", checked)}
/>
</div>
</TableCell>
<TableCell>
<div className="flex items-center justify-center">
<Switch
checked={mapping.primaryKey}
onCheckedChange={(checked) => updateMapping(mapping.id, "primaryKey", checked)}
/>
</div>
</TableCell>
<TableCell>
<Select
value={mapping.transformation}
onValueChange={(value) => updateMapping(mapping.id, "transformation", value)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="选择转换方式" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
<SelectItem value="formatPhoneNumber"></SelectItem>
<SelectItem value="mask"></SelectItem>
<SelectItem value="parseDateTime"></SelectItem>
<SelectItem value="uppercase"></SelectItem>
<SelectItem value="lowercase"></SelectItem>
<SelectItem value="trim"></SelectItem>
<SelectItem value="custom"></SelectItem>
</SelectContent>
</Select>
</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="icon" onClick={() => deleteMapping(mapping.id)}>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<div className="flex justify-end space-x-2">
<Button variant="outline"></Button>
<Button>
<Save className="mr-2 h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium">userId</TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">phoneNumber</TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">identityNumber</TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">deviceImei</TableCell>
<TableCell></TableCell>
<TableCell>IMEI号码</TableCell>
<TableCell></TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">fullName</TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">registrationDate</TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">lastActiveTime</TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,210 @@
"use client"
import { useState } from "react"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { ArrowRight, Plus, Save, Trash2 } from "lucide-react"
interface FieldMapping {
id: string
sourceField: string
targetField: string
dataType: string
required: boolean
transformation: string
}
export function DataMappingConfig() {
// 模拟数据
const [mappings, setMappings] = useState<FieldMapping[]>([
{
id: "1",
sourceField: "phone",
targetField: "phoneNumber",
dataType: "string",
required: true,
transformation: "formatPhoneNumber",
},
{
id: "2",
sourceField: "id_card",
targetField: "identityNumber",
dataType: "string",
required: true,
transformation: "mask",
},
{
id: "3",
sourceField: "imei",
targetField: "deviceImei",
dataType: "string",
required: false,
transformation: "none",
},
{
id: "4",
sourceField: "address",
targetField: "userAddress",
dataType: "string",
required: false,
transformation: "none",
},
{
id: "5",
sourceField: "name",
targetField: "fullName",
dataType: "string",
required: true,
transformation: "none",
},
{
id: "6",
sourceField: "register_time",
targetField: "registrationDate",
dataType: "datetime",
required: true,
transformation: "parseDateTime",
},
{
id: "7",
sourceField: "last_login",
targetField: "lastActiveTime",
dataType: "datetime",
required: false,
transformation: "parseDateTime",
},
])
const addNewMapping = () => {
const newMapping: FieldMapping = {
id: `new-${Date.now()}`,
sourceField: "",
targetField: "",
dataType: "string",
required: false,
transformation: "none",
}
setMappings([...mappings, newMapping])
}
const updateMapping = (id: string, field: keyof FieldMapping, value: any) => {
setMappings(mappings.map((mapping) => (mapping.id === id ? { ...mapping, [field]: value } : mapping)))
}
const deleteMapping = (id: string) => {
setMappings(mappings.filter((mapping) => mapping.id !== id))
}
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-medium"></h3>
<Button onClick={addNewMapping}>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mappings.map((mapping) => (
<TableRow key={mapping.id}>
<TableCell>
<Input
value={mapping.sourceField}
onChange={(e) => updateMapping(mapping.id, "sourceField", e.target.value)}
placeholder="源字段名"
/>
</TableCell>
<TableCell>
<ArrowRight className="h-4 w-4 text-muted-foreground" />
</TableCell>
<TableCell>
<Input
value={mapping.targetField}
onChange={(e) => updateMapping(mapping.id, "targetField", e.target.value)}
placeholder="目标字段名"
/>
</TableCell>
<TableCell>
<Select
value={mapping.dataType}
onValueChange={(value) => updateMapping(mapping.id, "dataType", value)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="选择数据类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="string"></SelectItem>
<SelectItem value="number"></SelectItem>
<SelectItem value="boolean"></SelectItem>
<SelectItem value="datetime"></SelectItem>
<SelectItem value="array"></SelectItem>
<SelectItem value="object"></SelectItem>
</SelectContent>
</Select>
</TableCell>
<TableCell>
<div className="flex items-center justify-center">
<Switch
checked={mapping.required}
onCheckedChange={(checked) => updateMapping(mapping.id, "required", checked)}
/>
</div>
</TableCell>
<TableCell>
<Select
value={mapping.transformation}
onValueChange={(value) => updateMapping(mapping.id, "transformation", value)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="选择转换方式" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"></SelectItem>
<SelectItem value="formatPhoneNumber"></SelectItem>
<SelectItem value="mask"></SelectItem>
<SelectItem value="parseDateTime"></SelectItem>
<SelectItem value="uppercase"></SelectItem>
<SelectItem value="lowercase"></SelectItem>
<SelectItem value="trim"></SelectItem>
<SelectItem value="custom"></SelectItem>
</SelectContent>
</Select>
</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="icon" onClick={() => deleteMapping(mapping.id)}>
<Trash2 className="h-4 w-4 text-red-500" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<div className="flex justify-end space-x-2">
<Button variant="outline"></Button>
<Button>
<Save className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
)
}

View File

@@ -0,0 +1,235 @@
"use client"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
export function DataPreview() {
// 模拟数据
const previewData = [
{
id: "1",
name: "张三",
phone: "13812345678",
idCard: "3101********1234",
imei: "86723*****5432",
address: "上海市浦东新区张江高科技园区",
registerDate: "2023-01-15",
lastActive: "2023-07-12",
source: "CRM系统",
status: "matched",
},
{
id: "2",
name: "李四",
phone: "13987654321",
idCard: "4401********5678",
imei: "35871*****7890",
address: "广州市天河区珠江新城",
registerDate: "2022-11-20",
lastActive: "2023-07-10",
source: "电商平台",
status: "matched",
},
{
id: "3",
name: "王五",
phone: "13765432198",
idCard: "1101********7890",
imei: "",
address: "北京市海淀区中关村",
registerDate: "2023-03-05",
lastActive: "2023-07-15",
source: "营销活动",
status: "partial",
},
{
id: "4",
name: "赵六",
phone: "13698765432",
idCard: "",
imei: "86723*****1234",
address: "深圳市南山区科技园",
registerDate: "2023-05-18",
lastActive: "2023-07-14",
source: "APP注册",
status: "partial",
},
{
id: "5",
name: "钱七",
phone: "",
idCard: "",
imei: "",
address: "杭州市西湖区",
registerDate: "2023-06-30",
lastActive: "2023-07-01",
source: "社交媒体",
status: "unmatched",
},
]
const getStatusBadge = (status: string) => {
switch (status) {
case "matched":
return <Badge className="bg-green-100 text-green-800"></Badge>
case "partial":
return <Badge className="bg-yellow-100 text-yellow-800"></Badge>
case "unmatched":
return <Badge className="bg-red-100 text-red-800"></Badge>
}
}
return (
<div className="space-y-4">
<Tabs defaultValue="preview">
<TabsList>
<TabsTrigger value="preview"></TabsTrigger>
<TabsTrigger value="matched"></TabsTrigger>
<TabsTrigger value="partial"></TabsTrigger>
<TabsTrigger value="unmatched"></TabsTrigger>
</TabsList>
<TabsContent value="preview" className="pt-4">
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>IMEI</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{previewData.map((item) => (
<TableRow key={item.id}>
<TableCell className="font-medium">{item.name}</TableCell>
<TableCell>{item.phone || "-"}</TableCell>
<TableCell>{item.idCard || "-"}</TableCell>
<TableCell>{item.imei || "-"}</TableCell>
<TableCell>{item.address}</TableCell>
<TableCell>{item.registerDate}</TableCell>
<TableCell>{item.lastActive}</TableCell>
<TableCell>{item.source}</TableCell>
<TableCell>{getStatusBadge(item.status)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</TabsContent>
<TabsContent value="matched" className="pt-4">
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>IMEI</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{previewData
.filter((item) => item.status === "matched")
.map((item) => (
<TableRow key={item.id}>
<TableCell className="font-medium">{item.name}</TableCell>
<TableCell>{item.phone || "-"}</TableCell>
<TableCell>{item.idCard || "-"}</TableCell>
<TableCell>{item.imei || "-"}</TableCell>
<TableCell>{item.address}</TableCell>
<TableCell>{item.registerDate}</TableCell>
<TableCell>{item.lastActive}</TableCell>
<TableCell>{item.source}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</TabsContent>
<TabsContent value="partial" className="pt-4">
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>IMEI</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{previewData
.filter((item) => item.status === "partial")
.map((item) => (
<TableRow key={item.id}>
<TableCell className="font-medium">{item.name}</TableCell>
<TableCell>{item.phone || "-"}</TableCell>
<TableCell>{item.idCard || "-"}</TableCell>
<TableCell>{item.imei || "-"}</TableCell>
<TableCell>{item.address}</TableCell>
<TableCell>{item.registerDate}</TableCell>
<TableCell>{item.lastActive}</TableCell>
<TableCell>{item.source}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</TabsContent>
<TabsContent value="unmatched" className="pt-4">
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>IMEI</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{previewData
.filter((item) => item.status === "unmatched")
.map((item) => (
<TableRow key={item.id}>
<TableCell className="font-medium">{item.name}</TableCell>
<TableCell>{item.phone || "-"}</TableCell>
<TableCell>{item.idCard || "-"}</TableCell>
<TableCell>{item.imei || "-"}</TableCell>
<TableCell>{item.address}</TableCell>
<TableCell>{item.registerDate}</TableCell>
<TableCell>{item.lastActive}</TableCell>
<TableCell>{item.source}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</TabsContent>
</Tabs>
</div>
)
}

View File

@@ -0,0 +1,987 @@
"use client"
import { Textarea } from "@/components/ui/textarea"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { Badge } from "@/components/ui/badge"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Separator } from "@/components/ui/separator"
import { AlertTriangle, CheckCircle, Bell, RefreshCw, Filter, Download, Search, Clock } from "lucide-react"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
interface DataQualityIssue {
id: string
type: string
description: string
affectedRecords: number
affectedFields: string[]
severity: "high" | "medium" | "low"
status: "open" | "in_progress" | "resolved"
createdAt: string
dataSource: string
}
interface DataQualityRule {
id: string
name: string
description: string
type: string
enabled: boolean
severity: "high" | "medium" | "low"
dataSource: string
lastRun: string
issuesDetected: number
}
export function DataQualityMonitor() {
const [activeTab, setActiveTab] = useState("issues")
const [selectedIssueId, setSelectedIssueId] = useState<string | null>(null)
const [isAddingRule, setIsAddingRule] = useState(false)
const [filterSeverity, setFilterSeverity] = useState<string>("all")
const [filterStatus, setFilterStatus] = useState<string>("all")
// 模拟数据质量问题
const [dataQualityIssues, setDataQualityIssues] = useState<DataQualityIssue[]>([
{
id: "issue-1",
type: "缺失值",
description: "用户表中存在大量手机号字段为空的记录",
affectedRecords: 1250,
affectedFields: ["phoneNumber"],
severity: "high",
status: "open",
createdAt: "2023-07-20 10:15",
dataSource: "CRM系统",
},
{
id: "issue-2",
type: "格式错误",
description: "部分用户的身份证号格式不正确",
affectedRecords: 458,
affectedFields: ["identityNumber"],
severity: "medium",
status: "in_progress",
createdAt: "2023-07-19 15:30",
dataSource: "用户数据库",
},
{
id: "issue-3",
type: "重复数据",
description: "存在多条使用相同手机号的用户记录",
affectedRecords: 789,
affectedFields: ["userId", "phoneNumber"],
severity: "medium",
status: "open",
createdAt: "2023-07-18 09:45",
dataSource: "交易系统",
},
{
id: "issue-4",
type: "异常值",
description: "部分用户的年龄字段值异常超过150岁",
affectedRecords: 23,
affectedFields: ["age"],
severity: "low",
status: "resolved",
createdAt: "2023-07-17 14:20",
dataSource: "用户数据库",
},
{
id: "issue-5",
type: "不一致数据",
description: "用户在不同系统中的姓名不一致",
affectedRecords: 567,
affectedFields: ["fullName"],
severity: "high",
status: "open",
createdAt: "2023-07-16 11:05",
dataSource: "多系统",
},
])
// 模拟数据质量规则
const [dataQualityRules, setDataQualityRules] = useState<DataQualityRule[]>([
{
id: "rule-1",
name: "手机号格式检查",
description: "检查手机号是否符合中国大陆手机号格式",
type: "格式验证",
enabled: true,
severity: "high",
dataSource: "所有数据源",
lastRun: "2023-07-20 00:00",
issuesDetected: 458,
},
{
id: "rule-2",
name: "身份证号验证",
description: "验证身份证号的格式和校验码",
type: "格式验证",
enabled: true,
severity: "high",
dataSource: "所有数据源",
lastRun: "2023-07-20 00:00",
issuesDetected: 789,
},
{
id: "rule-3",
name: "必填字段检查",
description: "检查关键字段是否有值",
type: "完整性检查",
enabled: true,
severity: "medium",
dataSource: "所有数据源",
lastRun: "2023-07-20 00:00",
issuesDetected: 1250,
},
{
id: "rule-4",
name: "重复数据检测",
description: "检测并标记重复的用户记录",
type: "唯一性检查",
enabled: true,
severity: "medium",
dataSource: "所有数据源",
lastRun: "2023-07-20 00:00",
issuesDetected: 567,
},
{
id: "rule-5",
name: "数据一致性检查",
description: "检查用户在不同系统中的数据是否一致",
type: "一致性检查",
enabled: true,
severity: "high",
dataSource: "所有数据源",
lastRun: "2023-07-20 00:00",
issuesDetected: 567,
},
])
// 模拟数据质量统计
const qualityStats = {
totalIssues: 3087,
openIssues: 2606,
resolvedIssues: 481,
highSeverity: 1245,
mediumSeverity: 1356,
lowSeverity: 486,
dataQualityScore: 87,
completenessScore: 92,
accuracyScore: 85,
consistencyScore: 88,
validityScore: 84,
}
const getSeverityBadge = (severity: string) => {
switch (severity) {
case "high":
return <Badge className="bg-red-100 text-red-800"></Badge>
case "medium":
return <Badge className="bg-yellow-100 text-yellow-800"></Badge>
case "low":
return <Badge className="bg-green-100 text-green-800"></Badge>
default:
return <Badge></Badge>
}
}
const getStatusBadge = (status: string) => {
switch (status) {
case "open":
return <Badge className="bg-red-100 text-red-800"></Badge>
case "in_progress":
return <Badge className="bg-yellow-100 text-yellow-800"></Badge>
case "resolved":
return <Badge className="bg-green-100 text-green-800"></Badge>
default:
return <Badge></Badge>
}
}
const getStatusIcon = (status: string) => {
switch (status) {
case "open":
return <AlertTriangle className="h-5 w-5 text-red-500" />
case "in_progress":
return <RefreshCw className="h-5 w-5 text-yellow-500" />
case "resolved":
return <CheckCircle className="h-5 w-5 text-green-500" />
default:
return <AlertTriangle className="h-5 w-5" />
}
}
const filteredIssues = dataQualityIssues.filter((issue) => {
if (filterSeverity !== "all" && issue.severity !== filterSeverity) return false
if (filterStatus !== "all" && issue.status !== filterStatus) return false
return true
})
const selectedIssue = selectedIssueId ? dataQualityIssues.find((issue) => issue.id === selectedIssueId) : null
const updateIssueStatus = (id: string, status: "open" | "in_progress" | "resolved") => {
setDataQualityIssues(
dataQualityIssues.map((issue) => {
if (issue.id === id) {
return { ...issue, status }
}
return issue
}),
)
}
const toggleRuleStatus = (id: string) => {
setDataQualityRules(
dataQualityRules.map((rule) => {
if (rule.id === id) {
return { ...rule, enabled: !rule.enabled }
}
return rule
}),
)
}
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold"></h2>
<div className="flex items-center gap-2">
<Button variant="outline">
<Bell className="mr-2 h-4 w-4" />
</Button>
<Button>
<RefreshCw className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardContent className="p-6">
<div className="flex flex-col space-y-2">
<p className="text-sm text-muted-foreground"></p>
<div className="flex items-end justify-between">
<h3 className="text-2xl font-bold">{qualityStats.dataQualityScore}/100</h3>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-green-600 h-2.5 rounded-full"
style={{ width: `${qualityStats.dataQualityScore}%` }}
></div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex flex-col space-y-2">
<p className="text-sm text-muted-foreground"></p>
<div className="flex items-center justify-between">
<h3 className="text-2xl font-bold">{qualityStats.openIssues}</h3>
<AlertTriangle className="h-5 w-5 text-red-500" />
</div>
<p className="text-xs text-muted-foreground">: {qualityStats.highSeverity} </p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex flex-col space-y-2">
<p className="text-sm text-muted-foreground"></p>
<div className="flex items-center justify-between">
<h3 className="text-2xl font-bold">
{dataQualityIssues.filter((issue) => issue.status === "in_progress").length}
</h3>
<RefreshCw className="h-5 w-5 text-yellow-500" />
</div>
<p className="text-xs text-muted-foreground"></p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex flex-col space-y-2">
<p className="text-sm text-muted-foreground"></p>
<div className="flex items-center justify-between">
<h3 className="text-2xl font-bold">{qualityStats.resolvedIssues}</h3>
<CheckCircle className="h-5 w-5 text-green-500" />
</div>
<p className="text-xs text-muted-foreground">
: {((qualityStats.resolvedIssues / qualityStats.totalIssues) * 100).toFixed(1)}%
</p>
</div>
</CardContent>
</Card>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
<TabsList>
<TabsTrigger value="issues"></TabsTrigger>
<TabsTrigger value="rules"></TabsTrigger>
<TabsTrigger value="metrics"></TabsTrigger>
<TabsTrigger value="reports"></TabsTrigger>
</TabsList>
<TabsContent value="issues" className="space-y-4">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle></CardTitle>
<div className="flex items-center gap-2">
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input type="text" placeholder="搜索问题..." className="pl-8 w-[200px]" />
</div>
<Select value={filterSeverity} onValueChange={setFilterSeverity}>
<SelectTrigger className="w-[130px]">
<Filter className="mr-2 h-4 w-4" />
<SelectValue placeholder="严重性" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="high"></SelectItem>
<SelectItem value="medium"></SelectItem>
<SelectItem value="low"></SelectItem>
</SelectContent>
</Select>
<Select value={filterStatus} onValueChange={setFilterStatus}>
<SelectTrigger className="w-[130px]">
<Filter className="mr-2 h-4 w-4" />
<SelectValue placeholder="状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="open"></SelectItem>
<SelectItem value="in_progress"></SelectItem>
<SelectItem value="resolved"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[50px]"></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredIssues.map((issue) => (
<TableRow key={issue.id}>
<TableCell>{getStatusIcon(issue.status)}</TableCell>
<TableCell className="font-medium">{issue.type}</TableCell>
<TableCell>{issue.description}</TableCell>
<TableCell>{issue.affectedRecords.toLocaleString()}</TableCell>
<TableCell>{getSeverityBadge(issue.severity)}</TableCell>
<TableCell>{issue.dataSource}</TableCell>
<TableCell>{issue.createdAt}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm" onClick={() => setSelectedIssueId(issue.id)}>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="rules" className="space-y-4">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle></CardTitle>
<Button onClick={() => setIsAddingRule(true)}></Button>
</div>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{dataQualityRules.map((rule) => (
<TableRow key={rule.id}>
<TableCell className="font-medium">{rule.name}</TableCell>
<TableCell>{rule.type}</TableCell>
<TableCell>{getSeverityBadge(rule.severity)}</TableCell>
<TableCell>{rule.dataSource}</TableCell>
<TableCell>{rule.lastRun}</TableCell>
<TableCell>{rule.issuesDetected.toLocaleString()}</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<Switch
checked={rule.enabled}
onCheckedChange={() => toggleRuleStatus(rule.id)}
aria-label="Toggle rule"
/>
<span>{rule.enabled ? "启用" : "禁用"}</span>
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end space-x-2">
<Button variant="ghost" size="sm">
</Button>
<Button variant="ghost" size="sm">
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="metrics" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="font-medium"></span>
<span className="text-green-600 font-medium">{qualityStats.completenessScore}/100</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-green-600 h-2.5 rounded-full"
style={{ width: `${qualityStats.completenessScore}%` }}
></div>
</div>
<div className="space-y-2 mt-4">
<h4 className="text-sm font-medium"></h4>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm">ID</span>
<span className="text-sm">100%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-green-600 h-1.5 rounded-full" style={{ width: "100%" }}></div>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm">87%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-green-600 h-1.5 rounded-full" style={{ width: "87%" }}></div>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm">72%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-yellow-600 h-1.5 rounded-full" style={{ width: "72%" }}></div>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm">IMEI设备号</span>
<span className="text-sm">68%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-yellow-600 h-1.5 rounded-full" style={{ width: "68%" }}></div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="font-medium"></span>
<span className="text-green-600 font-medium">{qualityStats.accuracyScore}/100</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-green-600 h-2.5 rounded-full"
style={{ width: `${qualityStats.accuracyScore}%` }}
></div>
</div>
<div className="space-y-2 mt-4">
<h4 className="text-sm font-medium"></h4>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm">92%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-green-600 h-1.5 rounded-full" style={{ width: "92%" }}></div>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm">85%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-green-600 h-1.5 rounded-full" style={{ width: "85%" }}></div>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm">95%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-green-600 h-1.5 rounded-full" style={{ width: "95%" }}></div>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm">78%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-yellow-600 h-1.5 rounded-full" style={{ width: "78%" }}></div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="font-medium"></span>
<span className="text-green-600 font-medium">{qualityStats.consistencyScore}/100</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-green-600 h-2.5 rounded-full"
style={{ width: `${qualityStats.consistencyScore}%` }}
></div>
</div>
<div className="space-y-2 mt-4">
<h4 className="text-sm font-medium"></h4>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm">CRM与交易系统</span>
<span className="text-sm">90%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-green-600 h-1.5 rounded-full" style={{ width: "90%" }}></div>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm">85%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-green-600 h-1.5 rounded-full" style={{ width: "85%" }}></div>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm">78%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-yellow-600 h-1.5 rounded-full" style={{ width: "78%" }}></div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="font-medium"></span>
<span className="text-green-600 font-medium">{qualityStats.validityScore}/100</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-green-600 h-2.5 rounded-full"
style={{ width: `${qualityStats.validityScore}%` }}
></div>
</div>
<div className="space-y-2 mt-4">
<h4 className="text-sm font-medium"></h4>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm">92%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-green-600 h-1.5 rounded-full" style={{ width: "92%" }}></div>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm">88%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-green-600 h-1.5 rounded-full" style={{ width: "88%" }}></div>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm"></span>
<span className="text-sm">75%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div className="bg-yellow-600 h-1.5 rounded-full" style={{ width: "75%" }}></div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="reports" className="space-y-4">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle></CardTitle>
<div className="flex items-center gap-2">
<Select defaultValue="weekly">
<SelectTrigger className="w-[150px]">
<Clock className="mr-2 h-4 w-4" />
<SelectValue placeholder="报告频率" />
</SelectTrigger>
<SelectContent>
<SelectItem value="daily"></SelectItem>
<SelectItem value="weekly"></SelectItem>
<SelectItem value="monthly"></SelectItem>
</SelectContent>
</Select>
<Button variant="outline">
<Download className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>2023-07-21</TableCell>
<TableCell>87/100</TableCell>
<TableCell>156</TableCell>
<TableCell>23</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm">
</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>2023-07-14</TableCell>
<TableCell>85/100</TableCell>
<TableCell>178</TableCell>
<TableCell>31</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm">
</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>2023-07-07</TableCell>
<TableCell>82/100</TableCell>
<TableCell>203</TableCell>
<TableCell>45</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm">
</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium"></TableCell>
<TableCell>2023-06-30</TableCell>
<TableCell>80/100</TableCell>
<TableCell>245</TableCell>
<TableCell>52</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm">
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* 问题详情对话框 */}
<Dialog open={!!selectedIssueId} onOpenChange={(open) => !open && setSelectedIssueId(null)}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
{selectedIssue && (
<div className="space-y-4 py-4">
<div className="flex items-center gap-4">
{getStatusIcon(selectedIssue.status)}
<div>
<h3 className="font-medium text-lg">{selectedIssue.type}</h3>
<p className="text-sm text-muted-foreground">{selectedIssue.description}</p>
</div>
</div>
<Separator />
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-sm text-muted-foreground"></Label>
<p className="font-medium">{getSeverityBadge(selectedIssue.severity)}</p>
</div>
<div>
<Label className="text-sm text-muted-foreground"></Label>
<p className="font-medium">{getStatusBadge(selectedIssue.status)}</p>
</div>
<div>
<Label className="text-sm text-muted-foreground"></Label>
<p className="font-medium">{selectedIssue.dataSource}</p>
</div>
<div>
<Label className="text-sm text-muted-foreground"></Label>
<p className="font-medium">{selectedIssue.createdAt}</p>
</div>
<div>
<Label className="text-sm text-muted-foreground"></Label>
<p className="font-medium">{selectedIssue.affectedRecords.toLocaleString()}</p>
</div>
</div>
<div>
<Label className="text-sm text-muted-foreground"></Label>
<div className="flex flex-wrap gap-2 mt-1">
{selectedIssue.affectedFields.map((field, index) => (
<Badge key={index} variant="outline">
{field}
</Badge>
))}
</div>
</div>
<Separator />
<div className="space-y-2">
<Label></Label>
<Select
value={selectedIssue.status}
onValueChange={(value) =>
updateIssueStatus(selectedIssue.id, value as "open" | "in_progress" | "resolved")
}
>
<SelectTrigger>
<SelectValue placeholder="选择状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="open"></SelectItem>
<SelectItem value="in_progress"></SelectItem>
<SelectItem value="resolved"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Textarea placeholder="输入处理备注..." rows={3} />
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setSelectedIssueId(null)}>
</Button>
<Button onClick={() => setSelectedIssueId(null)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 添加规则对话框 */}
<Dialog open={isAddingRule} onOpenChange={setIsAddingRule}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="rule-name" className="text-right">
</Label>
<Input id="rule-name" className="col-span-3" placeholder="例如:手机号格式检查" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="rule-type" className="text-right">
</Label>
<Select>
<SelectTrigger id="rule-type" className="col-span-3">
<SelectValue placeholder="选择规则类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="format"></SelectItem>
<SelectItem value="completeness"></SelectItem>
<SelectItem value="uniqueness"></SelectItem>
<SelectItem value="consistency"></SelectItem>
<SelectItem value="range"></SelectItem>
<SelectItem value="custom"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="rule-severity" className="text-right">
</Label>
<Select>
<SelectTrigger id="rule-severity" className="col-span-3">
<SelectValue placeholder="选择严重性" />
</SelectTrigger>
<SelectContent>
<SelectItem value="high"></SelectItem>
<SelectItem value="medium"></SelectItem>
<SelectItem value="low"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="rule-datasource" className="text-right">
</Label>
<Select>
<SelectTrigger id="rule-datasource" className="col-span-3">
<SelectValue placeholder="选择数据源" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="crm">CRM系统</SelectItem>
<SelectItem value="transaction"></SelectItem>
<SelectItem value="user-db"></SelectItem>
<SelectItem value="behavior"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-start gap-4">
<Label htmlFor="rule-description" className="text-right pt-2">
</Label>
<Textarea
id="rule-description"
className="col-span-3"
placeholder="描述规则的用途和检查内容..."
rows={3}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<div className="text-right">
<Label htmlFor="rule-enabled"></Label>
</div>
<div className="flex items-center space-x-2 col-span-3">
<Switch id="rule-enabled" defaultChecked />
<Label htmlFor="rule-enabled"></Label>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddingRule(false)}>
</Button>
<Button onClick={() => setIsAddingRule(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -0,0 +1,185 @@
"use client"
import { useState } from "react"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Edit, ExternalLink, MoreHorizontal, Search, RefreshCw } from "lucide-react"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
interface DataSource {
id: string
name: string
type: "api" | "file" | "database" | "third-party"
status: "active" | "inactive" | "error" | "pending"
lastSync: Date | null
recordCount: number
createdAt: Date
}
export function DataSourceList() {
const [searchQuery, setSearchQuery] = useState("")
// 模拟数据
const dataSources: DataSource[] = [
{
id: "1",
name: "CRM系统用户数据",
type: "api",
status: "active",
lastSync: new Date(2023, 6, 15, 14, 30),
recordCount: 45892,
createdAt: new Date(2023, 3, 10),
},
{
id: "2",
name: "电商平台订单数据",
type: "api",
status: "active",
lastSync: new Date(2023, 6, 15, 10, 15),
recordCount: 128456,
createdAt: new Date(2023, 2, 5),
},
{
id: "3",
name: "营销活动参与用户",
type: "file",
status: "active",
lastSync: new Date(2023, 6, 14, 9, 45),
recordCount: 8754,
createdAt: new Date(2023, 5, 20),
},
{
id: "4",
name: "APP用户行为数据",
type: "api",
status: "error",
lastSync: new Date(2023, 6, 10, 16, 20),
recordCount: 65432,
createdAt: new Date(2023, 1, 15),
},
{
id: "5",
name: "社交媒体用户数据",
type: "third-party",
status: "active",
lastSync: new Date(2023, 6, 15, 8, 0),
recordCount: 32145,
createdAt: new Date(2023, 4, 25),
},
]
// 过滤数据源
const filteredDataSources = dataSources.filter(
(source) =>
source.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
source.type.toLowerCase().includes(searchQuery.toLowerCase()),
)
const formatDate = (date: Date | null) => {
if (!date) return "从未同步"
return date.toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
})
}
const getStatusBadge = (status: DataSource["status"]) => {
switch (status) {
case "active":
return <Badge className="bg-green-100 text-green-800"></Badge>
case "inactive":
return <Badge className="bg-gray-100 text-gray-800"></Badge>
case "error":
return <Badge className="bg-red-100 text-red-800"></Badge>
case "pending":
return <Badge className="bg-yellow-100 text-yellow-800"></Badge>
}
}
const getTypeIcon = (type: DataSource["type"]) => {
switch (type) {
case "api":
return <Badge className="bg-blue-100 text-blue-800">API接口</Badge>
case "file":
return <Badge className="bg-purple-100 text-purple-800"></Badge>
case "database":
return <Badge className="bg-green-100 text-green-800"></Badge>
case "third-party":
return <Badge className="bg-orange-100 text-orange-800"></Badge>
}
}
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<div className="relative w-64">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="搜索数据源..."
className="pl-8"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<Button variant="outline" size="sm">
<RefreshCw className="h-4 w-4 mr-2" />
</Button>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredDataSources.map((source) => (
<TableRow key={source.id}>
<TableCell className="font-medium">{source.name}</TableCell>
<TableCell>{getTypeIcon(source.type)}</TableCell>
<TableCell>{getStatusBadge(source.status)}</TableCell>
<TableCell>{formatDate(source.lastSync)}</TableCell>
<TableCell>{source.recordCount.toLocaleString()}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end space-x-2">
<Button variant="ghost" size="icon">
<Edit className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon">
<ExternalLink className="h-4 w-4" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem></DropdownMenuItem>
<DropdownMenuItem></DropdownMenuItem>
<DropdownMenuItem></DropdownMenuItem>
<DropdownMenuItem className="text-red-600"></DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)
}

View File

@@ -0,0 +1,293 @@
"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
import { Database, TableIcon, Key, Download, RefreshCw } from "lucide-react"
import { Skeleton } from "@/components/ui/skeleton"
interface TableField {
Field: string
Type: string
Null: string
Key: string
Default: string | null
Extra: string
}
interface DatabaseStructure {
[tableName: string]: TableField[]
}
export function DatabaseStructureViewer() {
const [databases, setDatabases] = useState<string[]>([])
const [selectedDatabase, setSelectedDatabase] = useState<string>("")
const [structure, setStructure] = useState<DatabaseStructure | null>(null)
const [loading, setLoading] = useState<boolean>(false)
const [error, setError] = useState<string | null>(null)
// 获取数据库列表
useEffect(() => {
const fetchDatabases = async () => {
try {
setLoading(true)
const response = await fetch("/api/database-structure")
const result = await response.json()
if (result.success) {
const dbList = result.data.map((item: any) => Object.values(item)[0] as string)
setDatabases(dbList)
// 默认选择第一个非系统数据库
const nonSystemDbs = dbList.filter(
(db: string) => !["information_schema", "mysql", "performance_schema", "sys"].includes(db),
)
if (nonSystemDbs.length > 0) {
setSelectedDatabase(nonSystemDbs[0])
}
} else {
setError(result.message || "获取数据库列表失败")
}
} catch (err) {
setError("获取数据库列表失败")
console.error(err)
} finally {
setLoading(false)
}
}
fetchDatabases()
}, [])
// 获取数据库结构
useEffect(() => {
if (!selectedDatabase) return
const fetchDatabaseStructure = async () => {
try {
setLoading(true)
setStructure(null)
const response = await fetch(`/api/database-structure?database=${selectedDatabase}`)
const result = await response.json()
if (result.success) {
setStructure(result.data)
setError(null)
} else {
setError(result.message || "获取数据库结构失败")
}
} catch (err) {
setError("获取数据库结构失败")
console.error(err)
} finally {
setLoading(false)
}
}
fetchDatabaseStructure()
}, [selectedDatabase])
// 获取字段类型的简化描述
const getSimplifiedType = (type: string) => {
if (type.includes("int")) return "Integer"
if (type.includes("varchar") || type.includes("text") || type.includes("char")) return "String"
if (type.includes("datetime") || type.includes("timestamp")) return "DateTime"
if (type.includes("date")) return "Date"
if (type.includes("decimal") || type.includes("float") || type.includes("double")) return "Decimal"
if (type.includes("bool")) return "Boolean"
if (type.includes("json")) return "JSON"
if (type.includes("blob")) return "Binary"
return type
}
// 获取字段类型的标签颜色
const getTypeColor = (type: string) => {
if (type.includes("int")) return "bg-blue-100 text-blue-800"
if (type.includes("varchar") || type.includes("text") || type.includes("char")) return "bg-green-100 text-green-800"
if (type.includes("datetime") || type.includes("timestamp") || type.includes("date"))
return "bg-purple-100 text-purple-800"
if (type.includes("decimal") || type.includes("float") || type.includes("double"))
return "bg-yellow-100 text-yellow-800"
if (type.includes("bool")) return "bg-orange-100 text-orange-800"
if (type.includes("json")) return "bg-indigo-100 text-indigo-800"
if (type.includes("blob")) return "bg-red-100 text-red-800"
return "bg-gray-100 text-gray-800"
}
// 导出数据库结构为JSON
const exportDatabaseStructure = () => {
if (!structure) return
const dataStr = JSON.stringify(structure, null, 2)
const dataUri = `data:application/json;charset=utf-8,${encodeURIComponent(dataStr)}`
const exportFileDefaultName = `${selectedDatabase}-structure.json`
const linkElement = document.createElement("a")
linkElement.setAttribute("href", dataUri)
linkElement.setAttribute("download", exportFileDefaultName)
linkElement.click()
}
// 刷新数据库结构
const refreshDatabaseStructure = async () => {
if (!selectedDatabase) return
try {
setLoading(true)
setStructure(null)
const response = await fetch(`/api/database-structure?database=${selectedDatabase}`)
const result = await response.json()
if (result.success) {
setStructure(result.data)
setError(null)
} else {
setError(result.message || "刷新数据库结构失败")
}
} catch (err) {
setError("刷新数据库结构失败")
console.error(err)
} finally {
setLoading(false)
}
}
return (
<Card className="w-full">
<CardHeader>
<div className="flex justify-between items-center">
<div>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</div>
<div className="flex space-x-2">
<Button
variant="outline"
size="sm"
onClick={refreshDatabaseStructure}
disabled={loading || !selectedDatabase}
>
<RefreshCw className="h-4 w-4 mr-2" />
</Button>
<Button variant="outline" size="sm" onClick={exportDatabaseStructure} disabled={!structure}>
<Download className="h-4 w-4 mr-2" />
JSON
</Button>
</div>
</div>
</CardHeader>
<CardContent>
{loading && (
<div className="space-y-4">
<Skeleton className="h-8 w-full" />
<Skeleton className="h-64 w-full" />
</div>
)}
{error && (
<div className="p-4 bg-red-50 text-red-800 rounded-md">
<p>{error}</p>
</div>
)}
{!loading && !error && (
<div className="space-y-4">
<div className="flex space-x-2 overflow-x-auto pb-2">
{databases.map((db) => (
<Button
key={db}
variant={selectedDatabase === db ? "default" : "outline"}
size="sm"
onClick={() => setSelectedDatabase(db)}
>
<Database className="h-4 w-4 mr-2" />
{db}
</Button>
))}
</div>
{structure && (
<div>
<h3 className="text-lg font-medium mb-2">: {selectedDatabase}</h3>
<p className="text-sm text-muted-foreground mb-4"> {Object.keys(structure).length} </p>
<Accordion type="multiple" className="w-full">
{Object.entries(structure).map(([tableName, fields]) => (
<AccordionItem key={tableName} value={tableName}>
<AccordionTrigger className="hover:bg-muted/50 px-4">
<div className="flex items-center">
<TableIcon className="h-4 w-4 mr-2" />
<span>{tableName}</span>
<Badge className="ml-2 bg-gray-100 text-gray-800">{fields.length} </Badge>
</div>
</AccordionTrigger>
<AccordionContent>
<div className="rounded-md border overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[200px]"></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{fields.map((field, index) => (
<TableRow key={index}>
<TableCell className="font-medium">
{field.Field}
{field.Key === "PRI" && <Key className="h-3 w-3 ml-1 inline text-amber-500" />}
</TableCell>
<TableCell>
<Badge className={getTypeColor(field.Type)}>{getSimplifiedType(field.Type)}</Badge>
<span className="text-xs text-muted-foreground ml-2">{field.Type}</span>
</TableCell>
<TableCell>
{field.Null === "YES" ? (
<Badge variant="outline"></Badge>
) : (
<Badge className="bg-red-100 text-red-800"></Badge>
)}
</TableCell>
<TableCell>
{field.Key === "PRI" && <Badge></Badge>}
{field.Key === "UNI" && <Badge className="bg-blue-100 text-blue-800"></Badge>}
{field.Key === "MUL" && (
<Badge className="bg-purple-100 text-purple-800"></Badge>
)}
</TableCell>
<TableCell>
{field.Default !== null ? (
field.Default
) : (
<span className="text-muted-foreground">NULL</span>
)}
</TableCell>
<TableCell>{field.Extra}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
)}
</div>
)}
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,240 @@
"use client"
import { useState } from "react"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Search, FileDown, Eye, RefreshCw } from "lucide-react"
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
interface IntegrationRecord {
id: string
sourceId: string
sourceName: string
startTime: Date
endTime: Date
status: "success" | "failed" | "partial" | "running"
totalRecords: number
successRecords: number
failedRecords: number
notes: string
}
export function IntegrationHistory() {
const [searchQuery, setSearchQuery] = useState("")
const [statusFilter, setStatusFilter] = useState("all")
// 模拟数据
const integrationRecords: IntegrationRecord[] = [
{
id: "1",
sourceId: "1",
sourceName: "CRM系统用户数据",
startTime: new Date(2023, 6, 15, 14, 30),
endTime: new Date(2023, 6, 15, 14, 45),
status: "success",
totalRecords: 5280,
successRecords: 5280,
failedRecords: 0,
notes: "完全同步成功",
},
{
id: "2",
sourceId: "2",
sourceName: "电商平台订单数据",
startTime: new Date(2023, 6, 15, 10, 15),
endTime: new Date(2023, 6, 15, 10, 40),
status: "partial",
totalRecords: 12500,
successRecords: 12350,
failedRecords: 150,
notes: "部分记录格式错误",
},
{
id: "3",
sourceId: "3",
sourceName: "营销活动参与用户",
startTime: new Date(2023, 6, 14, 9, 45),
endTime: new Date(2023, 6, 14, 9, 50),
status: "success",
totalRecords: 875,
successRecords: 875,
failedRecords: 0,
notes: "同步成功",
},
{
id: "4",
sourceId: "4",
sourceName: "APP用户行为数据",
startTime: new Date(2023, 6, 10, 16, 20),
endTime: new Date(2023, 6, 10, 16, 35),
status: "failed",
totalRecords: 8500,
successRecords: 0,
failedRecords: 8500,
notes: "API连接超时",
},
{
id: "5",
sourceId: "5",
sourceName: "社交媒体用户数据",
startTime: new Date(2023, 6, 15, 8, 0),
endTime: new Date(2023, 6, 15, 8, 15),
status: "success",
totalRecords: 3200,
successRecords: 3200,
failedRecords: 0,
notes: "同步成功",
},
{
id: "6",
sourceId: "7",
sourceName: "新用户注册数据",
startTime: new Date(2023, 6, 15, 16, 0),
endTime: null,
status: "running",
totalRecords: 0,
successRecords: 0,
failedRecords: 0,
notes: "首次同步进行中",
},
]
// 过滤记录
const filteredRecords = integrationRecords.filter((record) => {
const matchesSearch = record.sourceName.toLowerCase().includes(searchQuery.toLowerCase())
const matchesStatus = statusFilter === "all" || record.status === statusFilter
return matchesSearch && matchesStatus
})
const formatDate = (date: Date | null) => {
if (!date) return "进行中"
return date.toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
})
}
const getStatusBadge = (status: IntegrationRecord["status"]) => {
switch (status) {
case "success":
return <Badge className="bg-green-100 text-green-800"></Badge>
case "failed":
return <Badge className="bg-red-100 text-red-800"></Badge>
case "partial":
return <Badge className="bg-yellow-100 text-yellow-800"></Badge>
case "running":
return <Badge className="bg-blue-100 text-blue-800"></Badge>
}
}
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex space-x-2">
<div className="relative w-64">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="搜索数据源..."
className="pl-8"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="状态筛选" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="success"></SelectItem>
<SelectItem value="failed"></SelectItem>
<SelectItem value="partial"></SelectItem>
<SelectItem value="running"></SelectItem>
</SelectContent>
</Select>
</div>
<Button variant="outline">
<RefreshCw className="mr-2 h-4 w-4" />
</Button>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredRecords.map((record) => (
<TableRow key={record.id}>
<TableCell className="font-medium">{record.sourceName}</TableCell>
<TableCell>{formatDate(record.startTime)}</TableCell>
<TableCell>{formatDate(record.endTime)}</TableCell>
<TableCell>{getStatusBadge(record.status)}</TableCell>
<TableCell>{record.totalRecords.toLocaleString()}</TableCell>
<TableCell>{record.successRecords.toLocaleString()}</TableCell>
<TableCell>{record.failedRecords.toLocaleString()}</TableCell>
<TableCell>{record.notes}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end space-x-2">
<Button variant="ghost" size="icon">
<Eye className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon">
<FileDown className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="#" />
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">1</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="#" isActive>
2
</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="#">3</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext href="#" />
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
)
}