chore: 以本地为准,上传全部并替换 GitHub

This commit is contained in:
卡若
2026-02-03 11:36:53 +08:00
parent 1219166526
commit b404bf546e
131 changed files with 37618 additions and 3930 deletions

View File

@@ -0,0 +1,510 @@
"use client"
import { useState, useEffect } 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 { Switch } from "@/components/ui/switch"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import {
Zap,
Plus,
Search,
RefreshCw,
Play,
CheckCircle2,
AlertTriangle,
Settings,
Trash2,
Copy,
Eye,
Loader2,
Filter,
Shield,
Sparkles,
FileText,
Database,
} from "lucide-react"
// 清洗规则接口
interface CleaningRule {
id: string
name: string
description: string
type: 'format' | 'dedup' | 'validate' | 'enrich' | 'mask'
targetField: string
config: {
pattern?: string
replacement?: string
validation?: string
enrichSource?: string
maskType?: string
}
priority: number
enabled: boolean
createdAt: string
lastRunAt?: string
processedCount?: number
}
// 规则类型配置
const RULE_TYPES = {
format: { label: '格式化', color: 'bg-blue-100 text-blue-700', icon: FileText, desc: '统一数据格式' },
dedup: { label: '去重', color: 'bg-green-100 text-green-700', icon: Copy, desc: '删除重复数据' },
validate: { label: '验证', color: 'bg-yellow-100 text-yellow-700', icon: Shield, desc: '校验数据有效性' },
enrich: { label: '补全', color: 'bg-purple-100 text-purple-700', icon: Sparkles, desc: '补充缺失字段' },
mask: { label: '脱敏', color: 'bg-red-100 text-red-700', icon: Eye, desc: '敏感数据脱敏' },
}
export default function CleaningRulesPage() {
const [rules, setRules] = useState<CleaningRule[]>([])
const [loading, setLoading] = useState(true)
const [executing, setExecuting] = useState<string | null>(null)
const [showAddDialog, setShowAddDialog] = useState(false)
const [searchQuery, setSearchQuery] = useState("")
const [stats, setStats] = useState({
totalRules: 0,
enabledRules: 0,
totalProcessed: 0
})
// 新规则表单
const [newRule, setNewRule] = useState({
name: '',
description: '',
type: 'format' as const,
targetField: 'phone',
pattern: '',
replacement: '',
validation: ''
})
// 加载规则
useEffect(() => {
loadRules()
}, [])
const loadRules = async () => {
setLoading(true)
try {
const [rulesRes, statsRes] = await Promise.all([
fetch('/api/cleaning-rules'),
fetch('/api/cleaning-rules?action=stats')
])
const rulesData = await rulesRes.json()
const statsData = await statsRes.json()
if (rulesData.success) setRules(rulesData.rules)
if (statsData.success) setStats(statsData.stats)
} catch (error) {
console.error('加载清洗规则失败:', error)
} finally {
setLoading(false)
}
}
// 执行单个规则
const executeRule = async (ruleId: string) => {
setExecuting(ruleId)
try {
const res = await fetch('/api/cleaning-rules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'execute', ruleId, limit: 1000 })
})
const data = await res.json()
if (data.success) {
alert(`执行完成: 处理 ${data.result.processed} 条,清洗 ${data.result.cleaned}`)
loadRules()
}
} catch (error) {
console.error('执行失败:', error)
} finally {
setExecuting(null)
}
}
// 执行所有规则
const executeAllRules = async () => {
setExecuting('all')
try {
const res = await fetch('/api/cleaning-rules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'execute_all', limit: 100 })
})
const data = await res.json()
if (data.success) {
alert(`批量执行完成: 执行了 ${data.executed} 条规则`)
loadRules()
}
} catch (error) {
console.error('批量执行失败:', error)
} finally {
setExecuting(null)
}
}
// 切换规则状态
const toggleRule = async (ruleId: string) => {
try {
await fetch('/api/cleaning-rules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'toggle', ruleId })
})
setRules(rules.map(r => r.id === ruleId ? { ...r, enabled: !r.enabled } : r))
} catch (error) {
console.error('切换失败:', error)
}
}
// 添加规则
const handleAddRule = async () => {
try {
const res = await fetch('/api/cleaning-rules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'create',
rule: {
name: newRule.name,
description: newRule.description,
type: newRule.type,
targetField: newRule.targetField,
config: {
pattern: newRule.pattern,
replacement: newRule.replacement,
validation: newRule.validation
}
}
})
})
const data = await res.json()
if (data.success) {
setShowAddDialog(false)
loadRules()
}
} catch (error) {
console.error('添加失败:', error)
}
}
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(0)}K`
return num.toLocaleString()
}
// 筛选规则
const filteredRules = rules.filter(rule => {
if (searchQuery) {
const query = searchQuery.toLowerCase()
return rule.name.toLowerCase().includes(query) ||
rule.description.toLowerCase().includes(query) ||
rule.targetField.toLowerCase().includes(query)
}
return true
})
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">
<Button
variant="outline"
onClick={executeAllRules}
disabled={executing === 'all'}
>
{executing === 'all' ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Play className="h-4 w-4 mr-2" />
)}
</Button>
<Button onClick={() => setShowAddDialog(true)}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500"></p>
<p className="text-2xl font-bold text-gray-900">{stats.totalRules}</p>
</div>
<div className="w-12 h-12 rounded-xl bg-blue-100 flex items-center justify-center">
<Filter className="h-6 w-6 text-blue-600" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500"></p>
<p className="text-2xl font-bold text-green-600">{stats.enabledRules}</p>
</div>
<div className="w-12 h-12 rounded-xl bg-green-100 flex items-center justify-center">
<CheckCircle2 className="h-6 w-6 text-green-600" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500"></p>
<p className="text-2xl font-bold text-gray-900">{formatNumber(stats.totalProcessed)}</p>
</div>
<div className="w-12 h-12 rounded-xl bg-purple-100 flex items-center justify-center">
<Database className="h-6 w-6 text-purple-600" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-white/80 backdrop-blur border-0 shadow-sm">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500"></p>
<p className="text-2xl font-bold text-gray-900">5</p>
</div>
<div className="w-12 h-12 rounded-xl bg-orange-100 flex items-center justify-center">
<Zap className="h-6 w-6 text-orange-600" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* 搜索 */}
<div className="relative max-w-md">
<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 bg-white"
/>
</div>
{/* 规则列表 */}
{loading ? (
<div className="flex items-center justify-center py-20">
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
<span className="ml-3 text-gray-500">...</span>
</div>
) : (
<div className="space-y-4">
{filteredRules.map((rule, index) => {
const typeConfig = RULE_TYPES[rule.type]
const TypeIcon = typeConfig.icon
return (
<Card key={rule.id} className="bg-white/80 backdrop-blur border-0 shadow-sm">
<CardContent className="p-5">
<div className="flex items-start justify-between">
<div className="flex items-start gap-4">
<div className="flex items-center gap-3">
<span className="text-lg font-bold text-gray-300 w-6">{index + 1}</span>
<div className={`p-2.5 rounded-xl ${typeConfig.color.replace('text-', 'bg-').replace('-700', '-100')}`}>
<TypeIcon className={`h-5 w-5 ${typeConfig.color.split(' ')[1]}`} />
</div>
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-semibold text-gray-900">{rule.name}</h3>
<Badge className={typeConfig.color}>{typeConfig.label}</Badge>
{!rule.enabled && (
<Badge variant="outline" className="text-gray-500"></Badge>
)}
</div>
<p className="text-sm text-gray-600 mb-2">{rule.description}</p>
<div className="flex items-center gap-4 text-xs text-gray-500">
<span className="flex items-center gap-1">
<Database className="h-3 w-3" />
: <span className="font-mono text-purple-600">{rule.targetField}</span>
</span>
{rule.lastRunAt && (
<span>: {rule.lastRunAt}</span>
)}
{rule.processedCount !== undefined && (
<span>: {formatNumber(rule.processedCount)} </span>
)}
</div>
</div>
</div>
<div className="flex items-center gap-3">
<Switch
checked={rule.enabled}
onCheckedChange={() => toggleRule(rule.id)}
/>
<Button
variant="outline"
size="sm"
onClick={() => executeRule(rule.id)}
disabled={executing === rule.id || !rule.enabled}
>
{executing === rule.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Play className="h-4 w-4" />
)}
</Button>
<Button variant="ghost" size="sm">
<Settings className="h-4 w-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
)
})}
</div>
)}
{/* 添加规则弹窗 */}
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
<DialogContent className="max-w-xl">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label> *</Label>
<Input
placeholder="例如:手机号格式化"
value={newRule.name}
onChange={(e) => setNewRule({ ...newRule, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label> *</Label>
<Select
value={newRule.type}
onValueChange={(v: any) => setNewRule({ ...newRule, type: v })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(RULE_TYPES).map(([key, config]) => (
<SelectItem key={key} value={key}>
<div className="flex items-center gap-2">
<config.icon className="h-4 w-4" />
{config.label} - {config.desc}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label></Label>
<Textarea
placeholder="描述该规则的作用..."
value={newRule.description}
onChange={(e) => setNewRule({ ...newRule, description: e.target.value })}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label> *</Label>
<Select
value={newRule.targetField}
onValueChange={(v) => setNewRule({ ...newRule, targetField: v })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="phone">phone ()</SelectItem>
<SelectItem value="email">email ()</SelectItem>
<SelectItem value="name">name ()</SelectItem>
<SelectItem value="idcard">idcard ()</SelectItem>
<SelectItem value="address">address ()</SelectItem>
<SelectItem value="province">province ()</SelectItem>
<SelectItem value="city">city ()</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Input
placeholder="例如:^1[3-9]\d{9}$"
value={newRule.pattern}
onChange={(e) => setNewRule({ ...newRule, pattern: e.target.value })}
/>
</div>
</div>
{newRule.type === 'format' && (
<div className="space-y-2">
<Label></Label>
<Input
placeholder="替换后的格式"
value={newRule.replacement}
onChange={(e) => setNewRule({ ...newRule, replacement: e.target.value })}
/>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowAddDialog(false)}>
</Button>
<Button onClick={handleAddRule}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
)
}