Reorganize navigation and module structure based on new requirements. Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
271 lines
11 KiB
TypeScript
271 lines
11 KiB
TypeScript
"use client"
|
||
|
||
import { useState } from "react"
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { Input } from "@/components/ui/input"
|
||
import {
|
||
GitBranch,
|
||
Search,
|
||
ZoomIn,
|
||
ZoomOut,
|
||
Maximize2,
|
||
Database,
|
||
Table,
|
||
ArrowRight,
|
||
Filter,
|
||
Download,
|
||
} from "lucide-react"
|
||
|
||
// 第二部分:数据接入 - 数据血缘
|
||
export default function DataLineagePage() {
|
||
const [searchQuery, setSearchQuery] = useState("")
|
||
const [selectedNode, setSelectedNode] = useState<string | null>(null)
|
||
|
||
// 数据血缘节点
|
||
const lineageNodes = [
|
||
// 数据源层
|
||
{ id: "source-1", type: "source", name: "存客宝-MySQL", level: 0, x: 50, y: 100 },
|
||
{ id: "source-2", type: "source", name: "触客宝-MySQL", level: 0, x: 50, y: 200 },
|
||
{ id: "source-3", type: "source", name: "数智员工-API", level: 0, x: 50, y: 300 },
|
||
// 原始表层
|
||
{ id: "raw-1", type: "table", name: "raw_users", level: 1, x: 250, y: 100 },
|
||
{ id: "raw-2", type: "table", name: "raw_transactions", level: 1, x: 250, y: 200 },
|
||
{ id: "raw-3", type: "table", name: "raw_behaviors", level: 1, x: 250, y: 300 },
|
||
// 清洗层
|
||
{ id: "clean-1", type: "table", name: "clean_users", level: 2, x: 450, y: 150 },
|
||
{ id: "clean-2", type: "table", name: "clean_transactions", level: 2, x: 450, y: 250 },
|
||
// 标签层
|
||
{ id: "tag-1", type: "table", name: "user_tags", level: 3, x: 650, y: 150 },
|
||
{ id: "tag-2", type: "table", name: "user_portraits", level: 3, x: 650, y: 250 },
|
||
// 输出层
|
||
{ id: "output-1", type: "output", name: "流量包-高价值用户", level: 4, x: 850, y: 150 },
|
||
{ id: "output-2", type: "output", name: "API-用户画像", level: 4, x: 850, y: 250 },
|
||
]
|
||
|
||
// 血缘关系
|
||
const lineageEdges = [
|
||
{ from: "source-1", to: "raw-1" },
|
||
{ from: "source-1", to: "raw-2" },
|
||
{ from: "source-2", to: "raw-3" },
|
||
{ from: "source-3", to: "raw-1" },
|
||
{ from: "raw-1", to: "clean-1" },
|
||
{ from: "raw-2", to: "clean-2" },
|
||
{ from: "raw-3", to: "clean-1" },
|
||
{ from: "clean-1", to: "tag-1" },
|
||
{ from: "clean-2", to: "tag-1" },
|
||
{ from: "clean-1", to: "tag-2" },
|
||
{ from: "tag-1", to: "output-1" },
|
||
{ from: "tag-2", to: "output-2" },
|
||
]
|
||
|
||
const getNodeColor = (type: string) => {
|
||
switch (type) {
|
||
case "source":
|
||
return "bg-blue-100 border-blue-300 text-blue-700"
|
||
case "table":
|
||
return "bg-green-100 border-green-300 text-green-700"
|
||
case "output":
|
||
return "bg-purple-100 border-purple-300 text-purple-700"
|
||
default:
|
||
return "bg-gray-100 border-gray-300 text-gray-700"
|
||
}
|
||
}
|
||
|
||
const getNodeIcon = (type: string) => {
|
||
switch (type) {
|
||
case "source":
|
||
return <Database className="h-4 w-4" />
|
||
case "table":
|
||
return <Table className="h-4 w-4" />
|
||
case "output":
|
||
return <ArrowRight className="h-4 w-4" />
|
||
default:
|
||
return <GitBranch className="h-4 w-4" />
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20">
|
||
<div className="p-6 space-y-6">
|
||
{/* 顶部标题 */}
|
||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-gray-900">数据血缘</h1>
|
||
<p className="text-sm text-gray-500 mt-1">可视化展示数据从源头到输出的完整流转路径</p>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<div className="relative">
|
||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||
<Input
|
||
placeholder="搜索表名或数据源..."
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
className="pl-9 w-64 bg-white"
|
||
/>
|
||
</div>
|
||
<Button variant="outline">
|
||
<Filter className="h-4 w-4 mr-2" />
|
||
筛选
|
||
</Button>
|
||
<Button variant="outline">
|
||
<Download className="h-4 w-4 mr-2" />
|
||
导出
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 图例 */}
|
||
<div className="flex items-center gap-6">
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-4 h-4 rounded bg-blue-100 border border-blue-300" />
|
||
<span className="text-sm text-gray-600">数据源</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-4 h-4 rounded bg-green-100 border border-green-300" />
|
||
<span className="text-sm text-gray-600">数据表</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-4 h-4 rounded bg-purple-100 border border-purple-300" />
|
||
<span className="text-sm text-gray-600">输出服务</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 血缘图 */}
|
||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||
<CardHeader className="pb-2">
|
||
<div className="flex items-center justify-between">
|
||
<CardTitle className="text-base font-semibold">血缘关系图</CardTitle>
|
||
<div className="flex items-center gap-2">
|
||
<Button variant="outline" size="icon">
|
||
<ZoomOut className="h-4 w-4" />
|
||
</Button>
|
||
<Button variant="outline" size="icon">
|
||
<ZoomIn className="h-4 w-4" />
|
||
</Button>
|
||
<Button variant="outline" size="icon">
|
||
<Maximize2 className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="relative h-[500px] bg-gray-50 rounded-lg overflow-hidden">
|
||
{/* SVG连线 */}
|
||
<svg className="absolute inset-0 w-full h-full pointer-events-none">
|
||
{lineageEdges.map((edge, index) => {
|
||
const fromNode = lineageNodes.find((n) => n.id === edge.from)
|
||
const toNode = lineageNodes.find((n) => n.id === edge.to)
|
||
if (!fromNode || !toNode) return null
|
||
return (
|
||
<line
|
||
key={index}
|
||
x1={fromNode.x + 80}
|
||
y1={fromNode.y + 20}
|
||
x2={toNode.x}
|
||
y2={toNode.y + 20}
|
||
stroke="#94a3b8"
|
||
strokeWidth="2"
|
||
markerEnd="url(#arrowhead)"
|
||
/>
|
||
)
|
||
})}
|
||
<defs>
|
||
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||
<polygon points="0 0, 10 3.5, 0 7" fill="#94a3b8" />
|
||
</marker>
|
||
</defs>
|
||
</svg>
|
||
|
||
{/* 节点 */}
|
||
{lineageNodes.map((node) => (
|
||
<div
|
||
key={node.id}
|
||
className={`absolute px-3 py-2 rounded-lg border-2 cursor-pointer transition-all hover:shadow-md ${getNodeColor(node.type)} ${
|
||
selectedNode === node.id ? "ring-2 ring-blue-500 ring-offset-2" : ""
|
||
}`}
|
||
style={{ left: node.x, top: node.y }}
|
||
onClick={() => setSelectedNode(node.id)}
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
{getNodeIcon(node.type)}
|
||
<span className="text-sm font-medium whitespace-nowrap">{node.name}</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
|
||
{/* 层级标签 */}
|
||
{[
|
||
{ label: "数据源", x: 50 },
|
||
{ label: "原始层", x: 250 },
|
||
{ label: "清洗层", x: 450 },
|
||
{ label: "标签层", x: 650 },
|
||
{ label: "输出层", x: 850 },
|
||
].map((level, index) => (
|
||
<div
|
||
key={index}
|
||
className="absolute top-2 text-xs text-gray-400 font-medium"
|
||
style={{ left: level.x }}
|
||
>
|
||
{level.label}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 节点详情 */}
|
||
{selectedNode && (
|
||
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-base font-semibold">
|
||
节点详情 - {lineageNodes.find((n) => n.id === selectedNode)?.name}
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||
<div>
|
||
<h4 className="text-sm font-medium text-gray-500 mb-2">上游依赖</h4>
|
||
<div className="space-y-2">
|
||
{lineageEdges
|
||
.filter((e) => e.to === selectedNode)
|
||
.map((edge, index) => (
|
||
<Badge key={index} variant="secondary" className="mr-2">
|
||
{lineageNodes.find((n) => n.id === edge.from)?.name}
|
||
</Badge>
|
||
))}
|
||
{lineageEdges.filter((e) => e.to === selectedNode).length === 0 && (
|
||
<span className="text-sm text-gray-400">无上游依赖</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<h4 className="text-sm font-medium text-gray-500 mb-2">下游影响</h4>
|
||
<div className="space-y-2">
|
||
{lineageEdges
|
||
.filter((e) => e.from === selectedNode)
|
||
.map((edge, index) => (
|
||
<Badge key={index} variant="secondary" className="mr-2">
|
||
{lineageNodes.find((n) => n.id === edge.to)?.name}
|
||
</Badge>
|
||
))}
|
||
{lineageEdges.filter((e) => e.from === selectedNode).length === 0 && (
|
||
<span className="text-sm text-gray-400">无下游影响</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<h4 className="text-sm font-medium text-gray-500 mb-2">更新信息</h4>
|
||
<p className="text-sm text-gray-700">最后更新:2分钟前</p>
|
||
<p className="text-sm text-gray-700">记录数:1,234,567</p>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|