feat: 数据治理 — 入库约束+元数据卡片+编码清洗+审计看板

This commit is contained in:
Hermes CI Fix
2026-07-22 12:06:52 +08:00
parent ec6af751a5
commit 748c2da43f
8 changed files with 452 additions and 2 deletions
+81
View File
@@ -44,6 +44,73 @@
</el-col>
</el-row>
<!-- 数据审计看板 -->
<el-row :gutter="16" class="section-gap">
<el-col :span="8">
<el-card shadow="never">
<template #header>📊 KPI完整度评分</template>
<div style="text-align:center;padding:12px 0;">
<div :style="{ fontSize: '36px', fontWeight: 700, color: completenessColor }">
{{ stats.completeness?.score ?? '-' }}%
</div>
<div style="font-size:13px;color:#999;margin-top:4px;">
完整 {{ stats.completeness?.complete ?? 0 }} / 总计 {{ stats.completeness?.total ?? 0 }}
</div>
<div style="font-size:12px;color:#f56c6c;margin-top:8px;">
缺失元数据: {{ stats.completeness?.missing_metadata ?? 0 }} 个KPI
</div>
<el-progress
:percentage="stats.completeness?.score ?? 0"
:stroke-width="12"
:color="completenessColor"
style="margin-top:12px;"
/>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="never">
<template #header>📉 缺失率统计</template>
<div style="text-align:center;padding:12px 0;">
<div style="font-size:36px;font-weight:700;color:#e6a23c;">
{{ stats.data_missing?.rate ?? '-' }}%
</div>
<div style="font-size:13px;color:#999;margin-top:4px;">
无数据值的KPI: {{ stats.data_missing?.count ?? 0 }}
</div>
<div style="font-size:12px;color:#999;margin-top:4px;">
总计 {{ stats.data_missing?.total ?? 0 }} 个KPI
</div>
<el-progress
:percentage="stats.data_missing?.rate ?? 0"
:stroke-width="12"
color="#e6a23c"
style="margin-top:12px;"
/>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="never">
<template #header> 超30天未更新预警</template>
<div style="text-align:center;padding:12px 0;">
<div :style="{ fontSize: '36px', fontWeight: 700, color: staleDataColor }">
{{ stats.stale_data?.count ?? 0 }}
</div>
<div style="font-size:13px;color:#999;margin-top:4px;">
个KPI超过 {{ stats.stale_data?.threshold_days ?? 180 }} 天未更新
</div>
<el-tag v-if="(stats.stale_data?.count ?? 0) > 0" type="danger" size="small" style="margin-top:8px;">
建议立即检查数据源
</el-tag>
<el-tag v-else type="success" size="small" style="margin-top:8px;">
数据更新正常
</el-tag>
</div>
</el-card>
</el-col>
</el-row>
<!-- 异常类型分布 -->
<el-row :gutter="16" class="section-gap">
<el-col :span="24">
@@ -147,6 +214,20 @@ const typeChartOption = computed(() => ({
}],
}))
const completenessColor = computed(() => {
const s = stats.value.completeness?.score ?? 0
if (s >= 80) return '#67c23a'
if (s >= 50) return '#e6a23c'
return '#f56c6c'
})
const staleDataColor = computed(() => {
const c = stats.value.stale_data?.count ?? 0
if (c === 0) return '#67c23a'
if (c <= 5) return '#e6a23c'
return '#f56c6c'
})
async function loadStats() {
try { const r: any = await dataQualityApi.stats(); stats.value = r } catch (e) {}
}
+39
View File
@@ -79,6 +79,37 @@
</el-row>
<el-form-item><el-button type="primary" @click="save">保存</el-button></el-form-item>
</el-form>
<!-- 元数据卡片 -->
<el-card shadow="never" style="margin-top:16px;">
<template #header>
<div style="display:flex;align-items:center;gap:8px;">
<span>📋 元数据卡片</span>
<el-tag v-if="!metadataComplete" type="warning" size="small" effect="dark"> 元数据不完整</el-tag>
<el-tag v-else type="success" size="small" effect="dark"> 完整</el-tag>
</div>
</template>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="计算公式" :span="2">
<span v-if="kpi.formula" style="font-family:monospace;">{{ kpi.formula }}</span>
<el-tag v-else type="warning" size="small">缺失</el-tag>
</el-descriptions-item>
<el-descriptions-item label="数据来源">
<span v-if="kpi.data_source && kpi.data_source !== '待补充'">{{ kpi.data_source }}</span>
<el-tag v-else type="warning" size="small">{{ kpi.data_source || '缺失' }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="数据责任人">
<span v-if="kpi.data_owner && kpi.data_owner !== '待指定'">{{ kpi.data_owner }}</span>
<el-tag v-else type="warning" size="small">{{ kpi.data_owner || '缺失' }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="单位">{{ kpi.unit || '-' }}</el-descriptions-item>
<el-descriptions-item label="目标值">{{ kpi.target_value ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="数据源类型">{{ kpi.data_source_type || '-' }}</el-descriptions-item>
<el-descriptions-item label="维度">{{ dimLabel(kpi.dimension) }}</el-descriptions-item>
<el-descriptions-item label="频率">{{ kpi.frequency || '-' }}</el-descriptions-item>
<el-descriptions-item label="类别" :span="2">{{ catLabel(kpi.category) || '-' }}</el-descriptions-item>
</el-descriptions>
</el-card>
</el-tab-pane>
<el-tab-pane label="历史数据" name="history">
@@ -354,6 +385,14 @@ function dimLabel(d: string) { return ({ finance: '财务', customer: '客户',
function catLabel(c: string) { return CAT_MAP[c] || c }
function dimTagType(d: string) { return ({ finance: '', customer: 'success', process: 'warning', learning: 'info' } as any)[d] || '' }
const metadataComplete = computed(() => {
if (!kpi.value) return false
const k = kpi.value
return !!(k.formula && k.formula.trim() && k.data_source && k.data_source.trim() && k.data_source !== '待补充'
&& k.data_owner && k.data_owner.trim() && k.data_owner !== '待指定'
&& k.unit && k.unit.trim() && k.target_value !== null && k.target_value !== undefined)
})
const chartOption = computed(() => ({
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: values.value.map(v => v.period) },