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:
923
components/data-integration/ai-analysis-tools.tsx
Normal file
923
components/data-integration/ai-analysis-tools.tsx
Normal 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={
|
||||
664
components/data-integration/api-documentation.tsx
Normal file
664
components/data-integration/api-documentation.tsx
Normal 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有速率限制,默认为每分钟100个请求。超过限制会返回429状态码。
|
||||
</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)`
|
||||
698
components/data-integration/data-collection-settings.tsx
Normal file
698
components/data-integration/data-collection-settings.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
950
components/data-integration/data-correlation-analysis.tsx
Normal file
950
components/data-integration/data-correlation-analysis.tsx
Normal 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">高度显著 (p≤0.001)</Badge>
|
||||
}
|
||||
if (significance <= 0.01) {
|
||||
return <Badge className="bg-green-100 text-green-800">显著 (p≤0.01)</Badge>
|
||||
}
|
||||
if (significance <= 0.05) {
|
||||
return <Badge className="bg-yellow-100 text-yellow-800">边际显著 (p≤0.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" >
|
||||
导出分析结果
|
||||
</Button>
|
||||
<Button onClick={() => setIsCreatingAnalysis(true)}>
|
||||
<Play className="mr-2 h-4 w-4" >
|
||||
创建新分析
|
||||
</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>
|
||||
)
|
||||
}
|
||||
449
components/data-integration/data-dashboard.tsx
Normal file
449
components/data-integration/data-dashboard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
321
components/data-integration/data-field-mapping.tsx
Normal file
321
components/data-integration/data-field-mapping.tsx
Normal 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>
|
||||
配置源数据字段与目标数据字段的映射关系,以ID、手机号、身份证号和IMEI设备号为标准整合客户数据
|
||||
</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">
|
||||
标准字段映射用于将不同数据源的字段映射到统一的标准字段,以便于数据整合和分析。
|
||||
主键字段用于唯一标识用户,通常为用户ID、手机号、身份证号或IMEI设备号。
|
||||
</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>
|
||||
)
|
||||
}
|
||||
210
components/data-integration/data-mapping-config.tsx
Normal file
210
components/data-integration/data-mapping-config.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
235
components/data-integration/data-preview.tsx
Normal file
235
components/data-integration/data-preview.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
987
components/data-integration/data-quality-monitor.tsx
Normal file
987
components/data-integration/data-quality-monitor.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
185
components/data-integration/data-source-list.tsx
Normal file
185
components/data-integration/data-source-list.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
293
components/data-integration/database-structure-viewer.tsx
Normal file
293
components/data-integration/database-structure-viewer.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
240
components/data-integration/integration-history.tsx
Normal file
240
components/data-integration/integration-history.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user