Reorganize navigation and module structure based on new requirements. Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
266 lines
10 KiB
TypeScript
266 lines
10 KiB
TypeScript
"use client"
|
||
|
||
import { useState } from "react"
|
||
import { Shield, TrendingUp, TrendingDown, RefreshCw, Download } from "lucide-react"
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { Progress } from "@/components/ui/progress"
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||
|
||
interface QualityMetric {
|
||
name: string
|
||
score: number
|
||
trend: "up" | "down" | "stable"
|
||
change: number
|
||
description: string
|
||
}
|
||
|
||
interface TableQuality {
|
||
tableName: string
|
||
completeness: number
|
||
uniqueness: number
|
||
timeliness: number
|
||
accuracy: number
|
||
overallScore: number
|
||
issues: number
|
||
lastCheck: string
|
||
}
|
||
|
||
const QUALITY_METRICS: QualityMetric[] = [
|
||
{ name: "完整性", score: 96.8, trend: "up", change: 0.5, description: "字段非空率" },
|
||
{ name: "唯一性", score: 99.2, trend: "stable", change: 0, description: "主键重复率" },
|
||
{ name: "及时性", score: 94.5, trend: "down", change: -1.2, description: "数据更新延迟" },
|
||
{ name: "准确性", score: 97.3, trend: "up", change: 0.8, description: "格式校验通过率" },
|
||
]
|
||
|
||
const TABLE_QUALITY: TableQuality[] = [
|
||
{
|
||
tableName: "users",
|
||
completeness: 98.5,
|
||
uniqueness: 100,
|
||
timeliness: 96.2,
|
||
accuracy: 99.1,
|
||
overallScore: 98.5,
|
||
issues: 2,
|
||
lastCheck: "2025-12-12 14:30:00",
|
||
},
|
||
{
|
||
tableName: "transactions",
|
||
completeness: 99.8,
|
||
uniqueness: 99.9,
|
||
timeliness: 98.5,
|
||
accuracy: 99.5,
|
||
overallScore: 99.4,
|
||
issues: 0,
|
||
lastCheck: "2025-12-12 14:30:00",
|
||
},
|
||
{
|
||
tableName: "user_behaviors",
|
||
completeness: 92.3,
|
||
uniqueness: 98.5,
|
||
timeliness: 89.2,
|
||
accuracy: 95.6,
|
||
overallScore: 93.9,
|
||
issues: 8,
|
||
lastCheck: "2025-12-12 14:25:00",
|
||
},
|
||
{
|
||
tableName: "accounts",
|
||
completeness: 97.2,
|
||
uniqueness: 100,
|
||
timeliness: 95.8,
|
||
accuracy: 98.3,
|
||
overallScore: 97.8,
|
||
issues: 1,
|
||
lastCheck: "2025-12-12 14:30:00",
|
||
},
|
||
{
|
||
tableName: "tags",
|
||
completeness: 100,
|
||
uniqueness: 100,
|
||
timeliness: 100,
|
||
accuracy: 100,
|
||
overallScore: 100,
|
||
issues: 0,
|
||
lastCheck: "2025-12-12 14:30:00",
|
||
},
|
||
]
|
||
|
||
export default function DataQualityPage() {
|
||
const [selectedPeriod, setSelectedPeriod] = useState("today")
|
||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||
const [overallScore, setOverallScore] = useState(96.95)
|
||
|
||
const getScoreColor = (score: number) => {
|
||
if (score >= 95) return "text-green-600"
|
||
if (score >= 80) return "text-yellow-600"
|
||
return "text-red-600"
|
||
}
|
||
|
||
const getScoreBg = (score: number) => {
|
||
if (score >= 95) return "bg-green-100"
|
||
if (score >= 80) return "bg-yellow-100"
|
||
return "bg-red-100"
|
||
}
|
||
|
||
const refreshData = async () => {
|
||
setIsRefreshing(true)
|
||
await new Promise((r) => setTimeout(r, 1500))
|
||
setOverallScore((prev) => prev + (Math.random() - 0.5) * 0.5)
|
||
setIsRefreshing(false)
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-6 p-6">
|
||
{/* Header */}
|
||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-gray-900">质量监控</h1>
|
||
<p className="text-gray-500 mt-1">实时监控数据质量指标</p>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<Select value={selectedPeriod} onValueChange={setSelectedPeriod}>
|
||
<SelectTrigger className="w-32 bg-white/60">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="today">今日</SelectItem>
|
||
<SelectItem value="week">本周</SelectItem>
|
||
<SelectItem value="month">本月</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
<Button variant="outline" onClick={refreshData} disabled={isRefreshing}>
|
||
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshing ? "animate-spin" : ""}`} />
|
||
刷新
|
||
</Button>
|
||
<Button variant="outline">
|
||
<Download className="w-4 h-4 mr-2" />
|
||
导出报告
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Overall Score */}
|
||
<Card className="border-none shadow-lg bg-gradient-to-br from-green-500 to-emerald-600 text-white">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<div className="flex items-center gap-2 text-green-100 mb-2">
|
||
<Shield className="w-5 h-5" />
|
||
<span>数据质量总分</span>
|
||
</div>
|
||
<div className="text-6xl font-bold">{overallScore.toFixed(1)}</div>
|
||
<p className="text-green-100 mt-2">满分100分,当前处于优秀水平</p>
|
||
</div>
|
||
<div className="hidden md:block">
|
||
<div className="w-32 h-32 rounded-full border-8 border-white/20 flex items-center justify-center">
|
||
<div className="text-center">
|
||
<div className="text-2xl font-bold">A+</div>
|
||
<div className="text-xs text-green-100">质量等级</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Quality Metrics */}
|
||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||
{QUALITY_METRICS.map((metric, i) => (
|
||
<Card key={i} className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||
<CardContent className="p-4">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<span className="text-sm text-gray-500">{metric.name}</span>
|
||
<div className="flex items-center gap-1">
|
||
{metric.trend === "up" ? (
|
||
<TrendingUp className="w-4 h-4 text-green-500" />
|
||
) : metric.trend === "down" ? (
|
||
<TrendingDown className="w-4 h-4 text-red-500" />
|
||
) : null}
|
||
<span
|
||
className={`text-xs ${metric.change > 0 ? "text-green-600" : metric.change < 0 ? "text-red-600" : "text-gray-500"}`}
|
||
>
|
||
{metric.change > 0 ? "+" : ""}
|
||
{metric.change}%
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className={`text-3xl font-bold ${getScoreColor(metric.score)}`}>{metric.score}%</div>
|
||
<p className="text-xs text-gray-400 mt-1">{metric.description}</p>
|
||
<Progress value={metric.score} className="h-2 mt-2" />
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
|
||
{/* Table Quality Details */}
|
||
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
|
||
<CardHeader>
|
||
<CardTitle className="text-lg">数据表质量详情</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full">
|
||
<thead>
|
||
<tr className="border-b border-gray-200">
|
||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-500">数据表</th>
|
||
<th className="text-center py-3 px-4 text-sm font-medium text-gray-500">完整性</th>
|
||
<th className="text-center py-3 px-4 text-sm font-medium text-gray-500">唯一性</th>
|
||
<th className="text-center py-3 px-4 text-sm font-medium text-gray-500">及时性</th>
|
||
<th className="text-center py-3 px-4 text-sm font-medium text-gray-500">准确性</th>
|
||
<th className="text-center py-3 px-4 text-sm font-medium text-gray-500">总分</th>
|
||
<th className="text-center py-3 px-4 text-sm font-medium text-gray-500">问题数</th>
|
||
<th className="text-right py-3 px-4 text-sm font-medium text-gray-500">最后检查</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{TABLE_QUALITY.map((table, i) => (
|
||
<tr key={i} className="border-b border-gray-100 hover:bg-gray-50">
|
||
<td className="py-3 px-4">
|
||
<code className="text-sm font-medium text-gray-900">{table.tableName}</code>
|
||
</td>
|
||
<td className="py-3 px-4 text-center">
|
||
<span className={`text-sm font-medium ${getScoreColor(table.completeness)}`}>
|
||
{table.completeness}%
|
||
</span>
|
||
</td>
|
||
<td className="py-3 px-4 text-center">
|
||
<span className={`text-sm font-medium ${getScoreColor(table.uniqueness)}`}>
|
||
{table.uniqueness}%
|
||
</span>
|
||
</td>
|
||
<td className="py-3 px-4 text-center">
|
||
<span className={`text-sm font-medium ${getScoreColor(table.timeliness)}`}>
|
||
{table.timeliness}%
|
||
</span>
|
||
</td>
|
||
<td className="py-3 px-4 text-center">
|
||
<span className={`text-sm font-medium ${getScoreColor(table.accuracy)}`}>{table.accuracy}%</span>
|
||
</td>
|
||
<td className="py-3 px-4 text-center">
|
||
<Badge className={`${getScoreBg(table.overallScore)} ${getScoreColor(table.overallScore)}`}>
|
||
{table.overallScore}
|
||
</Badge>
|
||
</td>
|
||
<td className="py-3 px-4 text-center">
|
||
{table.issues > 0 ? (
|
||
<Badge variant="destructive">{table.issues}</Badge>
|
||
) : (
|
||
<Badge variant="secondary" className="bg-green-100 text-green-700">
|
||
0
|
||
</Badge>
|
||
)}
|
||
</td>
|
||
<td className="py-3 px-4 text-right text-xs text-gray-500">{table.lastCheck}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|