refactor: restructure navigation and module layout

Reorganize navigation and module structure based on new requirements.

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
v0
2026-01-31 04:32:36 +00:00
parent 22e725887a
commit b17b488f8e
105 changed files with 20530 additions and 3622 deletions

View File

@@ -0,0 +1,270 @@
"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>
)
}