Files
users/app/data-governance/cleaning/page.tsx
v0 b17b488f8e 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>
2026-01-31 04:32:36 +00:00

349 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState } from "react"
import { Zap, Plus, Play, CheckCircle, Clock, AlertTriangle, FileText, Settings, Code, ArrowRight } 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { CreateCleaningRuleDialog } from "@/components/dialogs/create-cleaning-rule-dialog"
import { CleaningRuleSettingsDialog } from "@/components/dialogs/cleaning-rule-settings-dialog"
import { CleaningRuleLogsDialog } from "@/components/dialogs/cleaning-rule-logs-dialog"
interface CleaningRule {
id: string
name: string
description: string
type: "dedup" | "format" | "mapping" | "validate" | "transform"
sourceTable: string
targetTable: string
status: "active" | "inactive" | "testing"
lastRun: string
processedRecords: number
errorRate: number
}
const MOCK_RULES: CleaningRule[] = [
{
id: "1",
name: "用户手机号标准化",
description: "统一手机号格式,去除+86前缀和空格",
type: "format",
sourceTable: "raw_users",
targetTable: "clean_users",
status: "active",
lastRun: "2025-12-12 14:30:00",
processedRecords: 12895634,
errorRate: 0.02,
},
{
id: "2",
name: "交易记录去重",
description: "基于交易ID和时间戳去除重复记录",
type: "dedup",
sourceTable: "raw_transactions",
targetTable: "clean_transactions",
status: "active",
lastRun: "2025-12-12 14:25:00",
processedRecords: 89234567,
errorRate: 0.01,
},
{
id: "3",
name: "城市编码映射",
description: "将城市名称映射为标准行政区划代码",
type: "mapping",
sourceTable: "raw_users",
targetTable: "clean_users",
status: "active",
lastRun: "2025-12-12 14:20:00",
processedRecords: 45678901,
errorRate: 0.05,
},
{
id: "4",
name: "金额字段校验",
description: "校验金额字段非负且在合理范围内",
type: "validate",
sourceTable: "raw_transactions",
targetTable: "clean_transactions",
status: "testing",
lastRun: "2025-12-12 10:00:00",
processedRecords: 1000000,
errorRate: 0.12,
},
{
id: "5",
name: "时间戳转换",
description: "将各种时间格式统一转换为UTC时间戳",
type: "transform",
sourceTable: "raw_events",
targetTable: "clean_events",
status: "active",
lastRun: "2025-12-12 14:35:00",
processedRecords: 256789012,
errorRate: 0.001,
},
]
const TYPE_CONFIG = {
dedup: { label: "去重", color: "bg-blue-100 text-blue-700" },
format: { label: "格式化", color: "bg-green-100 text-green-700" },
mapping: { label: "映射", color: "bg-purple-100 text-purple-700" },
validate: { label: "校验", color: "bg-yellow-100 text-yellow-700" },
transform: { label: "转换", color: "bg-orange-100 text-orange-700" },
}
const PRESETS = [
{ name: "去除首尾空格", code: "TRIM(column)" },
{ name: "转大写", code: "UPPER(column)" },
{ name: "转小写", code: "LOWER(column)" },
{ name: "日期格式化", code: "DATE_FORMAT(column, '%Y-%m-%d')" },
{ name: "空值填充", code: "COALESCE(column, default_value)" },
{ name: "正则替换", code: "REGEXP_REPLACE(column, pattern, replacement)" },
]
export default function DataCleaningPage() {
const [rules, setRules] = useState<CleaningRule[]>(MOCK_RULES)
const [selectedTab, setSelectedTab] = useState("rules")
const [showCreateDialog, setShowCreateDialog] = useState(false)
const [showSettingsDialog, setShowSettingsDialog] = useState(false)
const [showLogsDialog, setShowLogsDialog] = useState(false)
const [selectedRule, setSelectedRule] = useState<CleaningRule | null>(null)
const stats = {
totalRules: rules.length,
activeRules: rules.filter((r) => r.status === "active").length,
totalProcessed: rules.reduce((sum, r) => sum + r.processedRecords, 0),
avgErrorRate: (rules.reduce((sum, r) => sum + r.errorRate, 0) / rules.length).toFixed(3),
}
const formatNumber = (num: number): string => {
if (num >= 1000000000) return `${(num / 1000000000).toFixed(2)}B`
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`
return num.toString()
}
const handleSettings = (rule: CleaningRule) => {
setSelectedRule(rule)
setShowSettingsDialog(true)
}
const handleViewLogs = (rule: CleaningRule) => {
setSelectedRule(rule)
setShowLogsDialog(true)
}
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>
<Button
className="bg-gradient-to-r from-blue-500 to-purple-500 text-white"
onClick={() => setShowCreateDialog(true)}
>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
{/* Stats */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
<CardContent className="p-4">
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
<FileText className="w-4 h-4" />
</div>
<div className="text-2xl font-bold text-gray-900">{stats.totalRules}</div>
</CardContent>
</Card>
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
<CardContent className="p-4">
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
<CheckCircle className="w-4 h-4 text-green-500" />
</div>
<div className="text-2xl font-bold text-green-600">{stats.activeRules}</div>
</CardContent>
</Card>
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
<CardContent className="p-4">
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
<Zap className="w-4 h-4" />
</div>
<div className="text-2xl font-bold text-gray-900">{formatNumber(stats.totalProcessed)}</div>
</CardContent>
</Card>
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
<CardContent className="p-4">
<div className="flex items-center gap-2 text-gray-500 text-sm mb-1">
<AlertTriangle className="w-4 h-4 text-yellow-500" />
</div>
<div className="text-2xl font-bold text-gray-900">{stats.avgErrorRate}%</div>
</CardContent>
</Card>
</div>
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
<TabsList className="bg-white/60 backdrop-blur-sm">
<TabsTrigger value="rules"></TabsTrigger>
<TabsTrigger value="presets"></TabsTrigger>
<TabsTrigger value="sql">SQL编辑器</TabsTrigger>
</TabsList>
<TabsContent value="rules" className="mt-4">
<div className="space-y-4">
{rules.map((rule) => (
<Card key={rule.id} className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
<CardContent className="p-5">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="font-semibold text-gray-900">{rule.name}</h3>
<Badge className={TYPE_CONFIG[rule.type].color}>{TYPE_CONFIG[rule.type].label}</Badge>
<Badge
variant="outline"
className={
rule.status === "active"
? "border-green-500 text-green-600"
: rule.status === "testing"
? "border-yellow-500 text-yellow-600"
: "border-gray-500 text-gray-600"
}
>
{rule.status === "active" ? "启用" : rule.status === "testing" ? "测试中" : "停用"}
</Badge>
</div>
<p className="text-sm text-gray-500 mb-3">{rule.description}</p>
<div className="flex items-center gap-6 text-sm">
<div className="flex items-center gap-2">
<span className="text-gray-500">:</span>
<code className="bg-gray-100 px-2 py-0.5 rounded text-xs">{rule.sourceTable}</code>
<ArrowRight className="w-3 h-3 text-gray-400" />
<code className="bg-gray-100 px-2 py-0.5 rounded text-xs">{rule.targetTable}</code>
</div>
<div>
<span className="text-gray-500">:</span>
<span className="ml-1 font-medium">{formatNumber(rule.processedRecords)}</span>
</div>
<div>
<span className="text-gray-500">:</span>
<span
className={`ml-1 font-medium ${rule.errorRate > 0.1 ? "text-red-600" : "text-green-600"}`}
>
{rule.errorRate}%
</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm">
<Play className="w-4 h-4 mr-1" />
</Button>
<Button variant="ghost" size="sm" onClick={() => handleSettings(rule)}>
<Settings className="w-4 h-4" />
</Button>
</div>
</div>
<div className="mt-4 pt-4 border-t border-gray-100 flex items-center justify-between text-xs text-gray-500">
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
: {rule.lastRun}
</span>
<Button
variant="link"
size="sm"
className="h-auto p-0 text-blue-500"
onClick={() => handleViewLogs(rule)}
>
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
<TabsContent value="presets" className="mt-4">
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{PRESETS.map((preset, i) => (
<div key={i} className="p-4 rounded-xl bg-gray-50 hover:bg-gray-100 transition-colors cursor-pointer">
<h4 className="font-medium text-gray-900 mb-2">{preset.name}</h4>
<code className="text-xs text-gray-600 bg-white px-2 py-1 rounded block">{preset.code}</code>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="sql" className="mt-4">
<Card className="border-none shadow-sm bg-white/60 backdrop-blur-sm">
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<Code className="w-5 h-5" />
SQL清洗规则编辑器
</CardTitle>
</CardHeader>
<CardContent>
<div className="bg-gray-900 rounded-xl p-4 font-mono text-sm text-green-400 min-h-[200px]">
<pre>{`-- 示例:用户数据清洗
SELECT
user_id,
TRIM(LOWER(email)) as email,
REGEXP_REPLACE(phone, '[^0-9]', '') as phone,
COALESCE(city, '未知') as city,
DATE_FORMAT(created_at, '%Y-%m-%d') as created_date
FROM raw_users
WHERE email IS NOT NULL
AND LENGTH(phone) >= 11;`}</pre>
</div>
<div className="flex justify-end gap-2 mt-4">
<Button variant="outline"></Button>
<Button></Button>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* Create Cleaning Rule Dialog */}
<CreateCleaningRuleDialog open={showCreateDialog} onOpenChange={setShowCreateDialog} />
<CleaningRuleSettingsDialog
open={showSettingsDialog}
onOpenChange={setShowSettingsDialog}
rule={
selectedRule
? {
id: selectedRule.id,
name: selectedRule.name,
description: selectedRule.description,
type: selectedRule.type,
sourceTable: selectedRule.sourceTable,
targetTable: selectedRule.targetTable,
status: selectedRule.status,
}
: undefined
}
/>
<CleaningRuleLogsDialog open={showLogsDialog} onOpenChange={setShowLogsDialog} ruleName={selectedRule?.name} />
</div>
)
}