feat: add comprehensive backend topics and fix build issues
## 新增内容 ### 附录文档扩展 - 扩展前端项目架构文档 (frontend-project-architecture.md) - 扩展后端项目架构文档 (backend-project-architecture.md) - 扩展数据治理文档 (data-governance.md) - 扩展数据可视化文档 (data-visualization.md) - 扩展分布式系统文档 (distributed-systems.md) - 扩展高可用文档 (high-availability.md) - 扩展单体到微服务文档 (monolith-to-microservices.md) - 扩展系统设计方法论文档 (system-design-methodology.md) - 扩展 Docker 容器文档 (docker-containers.md) - 扩展 Kubernetes 文档 (kubernetes.md) - 扩展 Linux 基础文档 (linux-basics.md) - 扩展神经网络文档 (neural-networks.md) ### 新增交互式组件 - 数据治理组件: DataQualityDemo, DataGovernanceFrameworkDemo, DataLineageDemo - 数据可视化组件: ChartTypeSelectorDemo, DashboardLayoutDemo - 分布式系统组件: CAPTheoremDemo, ConsistencyModelsDemo, DistributedChallengesDemo - 高可用组件: AvailabilityCalculatorDemo, FailoverStrategyDemo - 系统设计组件: SystemDesignStepsDemo, CapacityEstimationDemo - Docker 容器组件: DockerArchitectureDemo, DockerLifecycleDemo - Kubernetes 组件: K8sArchitectureDemo, K8sWorkloadsDemo - Linux 基础组件: LinuxFileSystemDemo, LinuxCommandDemo, LinuxPermissionsDemo - 神经网络组件: NeuronDemo, NetworkLayersDemo, NetworkArchitectureDemo - 单体到微服务组件: ArchEvolutionDemo - 项目架构组件: ProjectArchitectureComparisonDemo - 附录导航组件: AppendixFlowMap ### 英文版重构 - 将 en-us 目录重命名为 en - 更新相关配置和组件中的语言代码 ## Bug 修复 - 修复 index.js 中重复的组件导入语句导致的 build 失败 - 恢复被注释的 InvertedIndexDemo 和 SearchRelevanceDemo 导入 - 修复 HomeFeatures.vue 中 en-us 与 config.mjs 中 en 不一致导致的语言切换问题 ## 其他改进 - 添加构建脚本 (scripts/build.mjs) - 更新依赖版本
This commit is contained in:
+130
@@ -0,0 +1,130 @@
|
||||
<!--
|
||||
DataGovernanceFrameworkDemo.vue
|
||||
数据治理框架演示:展示数据治理的核心流程
|
||||
-->
|
||||
<template>
|
||||
<div class="governance-demo">
|
||||
<div class="header">
|
||||
<div class="title">数据治理框架</div>
|
||||
<div class="subtitle">点击各阶段查看详情</div>
|
||||
</div>
|
||||
|
||||
<div class="pipeline">
|
||||
<div
|
||||
v-for="(stage, i) in stages"
|
||||
:key="stage.key"
|
||||
:class="['stage', { active: activeStage === stage.key }]"
|
||||
@click="activeStage = stage.key"
|
||||
>
|
||||
<div class="stage-num">{{ i + 1 }}</div>
|
||||
<div class="stage-name">{{ stage.name }}</div>
|
||||
<div v-if="i < stages.length - 1" class="arrow">→</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="current" class="stage-detail">
|
||||
<div class="detail-title">{{ current.name }}</div>
|
||||
<div class="detail-desc">{{ current.desc }}</div>
|
||||
<div class="activities">
|
||||
<div v-for="(act, i) in current.activities" :key="i" class="activity">
|
||||
<span class="act-icon">{{ act.icon }}</span>
|
||||
<div>
|
||||
<div class="act-name">{{ act.name }}</div>
|
||||
<div class="act-desc">{{ act.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeStage = ref('define')
|
||||
|
||||
const stages = [
|
||||
{
|
||||
key: 'define',
|
||||
name: '定义标准',
|
||||
desc: '制定数据标准、命名规范、数据字典',
|
||||
activities: [
|
||||
{ icon: '📖', name: '数据字典', desc: '定义每个字段的含义、类型、取值范围' },
|
||||
{ icon: '📏', name: '命名规范', desc: '统一字段命名:snake_case、驼峰、前缀约定' },
|
||||
{ icon: '🏷️', name: '分类分级', desc: '按敏感度分级:公开、内部、机密、绝密' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'collect',
|
||||
name: '采集接入',
|
||||
desc: '规范数据采集流程,确保源头质量',
|
||||
activities: [
|
||||
{ icon: '🔌', name: '接入规范', desc: '定义数据接入的格式、协议、频率要求' },
|
||||
{ icon: '✅', name: '入库校验', desc: '数据写入前进行格式、完整性、合规性校验' },
|
||||
{ icon: '📝', name: '血缘记录', desc: '记录数据来源、加工链路、依赖关系' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'store',
|
||||
name: '存储管理',
|
||||
desc: '合理存储数据,控制成本和访问权限',
|
||||
activities: [
|
||||
{ icon: '🗄️', name: '分层存储', desc: 'ODS → DWD → DWS → ADS 数仓分层' },
|
||||
{ icon: '🔒', name: '权限控制', desc: '按角色和数据分级控制读写权限' },
|
||||
{ icon: '♻️', name: '生命周期', desc: '热数据 → 温数据 → 冷数据 → 归档/删除' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'use',
|
||||
name: '使用消费',
|
||||
desc: '让数据安全、高效地被业务使用',
|
||||
activities: [
|
||||
{ icon: '🔍', name: '数据目录', desc: '提供可搜索的数据资产目录,降低找数成本' },
|
||||
{ icon: '🎭', name: '脱敏处理', desc: '对敏感字段进行掩码、加密、泛化处理' },
|
||||
{ icon: '📊', name: '质量监控', desc: '持续监控数据质量指标,异常时告警' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'retire',
|
||||
name: '归档销毁',
|
||||
desc: '按合规要求归档或安全销毁数据',
|
||||
activities: [
|
||||
{ icon: '📦', name: '归档策略', desc: '超过保留期的数据迁移到低成本存储' },
|
||||
{ icon: '🗑️', name: '安全删除', desc: '按 GDPR/个保法要求彻底删除用户数据' },
|
||||
{ icon: '📋', name: '审计日志', desc: '记录数据删除操作,满足合规审计要求' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const current = computed(() => stages.find(s => s.key === activeStage.value))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.governance-demo {
|
||||
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px; padding: 1.5rem; margin: 1.5rem 0;
|
||||
}
|
||||
.header { margin-bottom: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.pipeline { display: flex; align-items: center; gap: 0.25rem; margin-bottom: 1rem; flex-wrap: wrap; }
|
||||
.stage {
|
||||
display: flex; align-items: center; gap: 0.4rem; padding: 0.5rem 0.75rem;
|
||||
border-radius: 8px; cursor: pointer; background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider); transition: all 0.2s; font-size: 0.85rem;
|
||||
}
|
||||
.stage:hover { border-color: var(--vp-c-brand); }
|
||||
.stage.active { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); }
|
||||
.stage-num { width: 20px; height: 20px; border-radius: 50%; background: var(--vp-c-brand); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 0.7rem; font-weight: 700; }
|
||||
.stage-name { font-weight: 600; }
|
||||
.arrow { color: var(--vp-c-text-3); margin-left: 0.25rem; }
|
||||
.stage-detail { background: var(--vp-c-bg); border-radius: 8px; padding: 1rem; border: 1px solid var(--vp-c-divider); }
|
||||
.detail-title { font-weight: 700; font-size: 1rem; margin-bottom: 0.25rem; }
|
||||
.detail-desc { color: var(--vp-c-text-2); font-size: 0.85rem; margin-bottom: 0.75rem; }
|
||||
.activities { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.activity { display: flex; gap: 0.5rem; padding: 0.5rem; border-radius: 6px; background: var(--vp-c-bg-soft); }
|
||||
.act-icon { font-size: 1.2rem; }
|
||||
.act-name { font-weight: 600; font-size: 0.85rem; }
|
||||
.act-desc { font-size: 0.78rem; color: var(--vp-c-text-2); }
|
||||
@media (max-width: 640px) { .pipeline { flex-direction: column; align-items: stretch; } .arrow { display: none; } }
|
||||
</style>
|
||||
@@ -0,0 +1,138 @@
|
||||
<!--
|
||||
DataLineageDemo.vue
|
||||
数据血缘追踪演示:展示数据从源头到消费的流转路径
|
||||
-->
|
||||
<template>
|
||||
<div class="lineage-demo">
|
||||
<div class="header">
|
||||
<div class="title">数据血缘追踪</div>
|
||||
<div class="subtitle">点击任意节点,查看上下游依赖关系</div>
|
||||
</div>
|
||||
|
||||
<div class="lineage-graph">
|
||||
<div v-for="(layer, li) in layers" :key="li" class="layer">
|
||||
<div class="layer-label">{{ layer.label }}</div>
|
||||
<div class="layer-nodes">
|
||||
<div
|
||||
v-for="node in layer.nodes"
|
||||
:key="node.id"
|
||||
:class="['node', { active: activeNode === node.id, upstream: upstreamIds.includes(node.id), downstream: downstreamIds.includes(node.id) }]"
|
||||
@click="selectNode(node.id)"
|
||||
>
|
||||
<div class="node-icon">{{ node.icon }}</div>
|
||||
<div class="node-name">{{ node.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeNode && activeInfo" class="info-panel">
|
||||
<div class="info-title">{{ activeInfo.name }}</div>
|
||||
<div class="info-row"><span class="info-label">上游依赖:</span>{{ activeInfo.upstreamNames || '无(数据源头)' }}</div>
|
||||
<div class="info-row"><span class="info-label">下游消费:</span>{{ activeInfo.downstreamNames || '无(最终消费)' }}</div>
|
||||
<div class="info-row"><span class="info-label">负责人:</span>{{ activeInfo.owner }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeNode = ref(null)
|
||||
|
||||
const nodes = {
|
||||
mysql_user: { name: 'MySQL 用户表', icon: '🗄️', upstream: [], downstream: ['ods_user'], owner: '业务开发组' },
|
||||
mysql_order: { name: 'MySQL 订单表', icon: '🗄️', upstream: [], downstream: ['ods_order'], owner: '业务开发组' },
|
||||
log_click: { name: '点击日志', icon: '📝', upstream: [], downstream: ['ods_click'], owner: '前端团队' },
|
||||
ods_user: { name: 'ODS 用户', icon: '📥', upstream: ['mysql_user'], downstream: ['dwd_user'], owner: '数据工程师' },
|
||||
ods_order: { name: 'ODS 订单', icon: '📥', upstream: ['mysql_order'], downstream: ['dwd_order'], owner: '数据工程师' },
|
||||
ods_click: { name: 'ODS 点击', icon: '📥', upstream: ['log_click'], downstream: ['dwd_click'], owner: '数据工程师' },
|
||||
dwd_user: { name: 'DWD 用户明细', icon: '🔧', upstream: ['ods_user'], downstream: ['dws_user_profile'], owner: '数据开发' },
|
||||
dwd_order: { name: 'DWD 订单明细', icon: '🔧', upstream: ['ods_order'], downstream: ['dws_gmv'], owner: '数据开发' },
|
||||
dwd_click: { name: 'DWD 点击明细', icon: '🔧', upstream: ['ods_click'], downstream: ['dws_user_profile'], owner: '数据开发' },
|
||||
dws_user_profile: { name: 'DWS 用户画像', icon: '📊', upstream: ['dwd_user', 'dwd_click'], downstream: ['ads_report'], owner: '数据分析师' },
|
||||
dws_gmv: { name: 'DWS GMV 汇总', icon: '📊', upstream: ['dwd_order'], downstream: ['ads_report'], owner: '数据分析师' },
|
||||
ads_report: { name: 'ADS 经营报表', icon: '📈', upstream: ['dws_user_profile', 'dws_gmv'], downstream: [], owner: '数据产品' }
|
||||
}
|
||||
|
||||
const layers = [
|
||||
{ label: '数据源', nodes: [{ id: 'mysql_user', ...nodes.mysql_user }, { id: 'mysql_order', ...nodes.mysql_order }, { id: 'log_click', ...nodes.log_click }] },
|
||||
{ label: 'ODS 层', nodes: [{ id: 'ods_user', ...nodes.ods_user }, { id: 'ods_order', ...nodes.ods_order }, { id: 'ods_click', ...nodes.ods_click }] },
|
||||
{ label: 'DWD 层', nodes: [{ id: 'dwd_user', ...nodes.dwd_user }, { id: 'dwd_order', ...nodes.dwd_order }, { id: 'dwd_click', ...nodes.dwd_click }] },
|
||||
{ label: 'DWS 层', nodes: [{ id: 'dws_user_profile', ...nodes.dws_user_profile }, { id: 'dws_gmv', ...nodes.dws_gmv }] },
|
||||
{ label: 'ADS 层', nodes: [{ id: 'ads_report', ...nodes.ads_report }] }
|
||||
]
|
||||
|
||||
function getAllUpstream(id, visited = new Set()) {
|
||||
if (visited.has(id)) return []
|
||||
visited.add(id)
|
||||
const node = nodes[id]
|
||||
if (!node) return []
|
||||
let result = [...node.upstream]
|
||||
node.upstream.forEach(uid => { result = result.concat(getAllUpstream(uid, visited)) })
|
||||
return result
|
||||
}
|
||||
|
||||
function getAllDownstream(id, visited = new Set()) {
|
||||
if (visited.has(id)) return []
|
||||
visited.add(id)
|
||||
const node = nodes[id]
|
||||
if (!node) return []
|
||||
let result = [...node.downstream]
|
||||
node.downstream.forEach(did => { result = result.concat(getAllDownstream(did, visited)) })
|
||||
return result
|
||||
}
|
||||
|
||||
const upstreamIds = computed(() => activeNode.value ? getAllUpstream(activeNode.value) : [])
|
||||
const downstreamIds = computed(() => activeNode.value ? getAllDownstream(activeNode.value) : [])
|
||||
|
||||
const activeInfo = computed(() => {
|
||||
if (!activeNode.value || !nodes[activeNode.value]) return null
|
||||
const n = nodes[activeNode.value]
|
||||
return {
|
||||
...n,
|
||||
upstreamNames: n.upstream.map(id => nodes[id]?.name).join('、'),
|
||||
downstreamNames: n.downstream.map(id => nodes[id]?.name).join('、')
|
||||
}
|
||||
})
|
||||
|
||||
function selectNode(id) {
|
||||
activeNode.value = activeNode.value === id ? null : id
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.lineage-demo {
|
||||
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px; padding: 1.5rem; margin: 1.5rem 0;
|
||||
}
|
||||
.header { margin-bottom: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.lineage-graph { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; }
|
||||
.layer { display: flex; align-items: center; gap: 0.75rem; }
|
||||
.layer-label {
|
||||
min-width: 60px; font-size: 0.75rem; font-weight: 700;
|
||||
color: var(--vp-c-text-3); text-align: right;
|
||||
}
|
||||
.layer-nodes { display: flex; gap: 0.5rem; flex-wrap: wrap; flex: 1; }
|
||||
.node {
|
||||
display: flex; align-items: center; gap: 0.3rem; padding: 0.4rem 0.6rem;
|
||||
border-radius: 6px; cursor: pointer; font-size: 0.78rem;
|
||||
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); transition: all 0.2s;
|
||||
}
|
||||
.node:hover { border-color: var(--vp-c-brand); }
|
||||
.node.active { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.1); font-weight: 700; }
|
||||
.node.upstream { border-color: #f59e0b; background: rgba(245,158,11,0.08); }
|
||||
.node.downstream { border-color: #22c55e; background: rgba(34,197,94,0.08); }
|
||||
.node-icon { font-size: 1rem; }
|
||||
.node-name { white-space: nowrap; }
|
||||
.info-panel {
|
||||
background: var(--vp-c-bg); border-radius: 8px; padding: 0.75rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.info-title { font-weight: 700; font-size: 0.95rem; margin-bottom: 0.5rem; }
|
||||
.info-row { font-size: 0.82rem; margin-bottom: 0.25rem; }
|
||||
.info-label { font-weight: 600; color: var(--vp-c-text-2); }
|
||||
@media (max-width: 640px) { .layer { flex-direction: column; align-items: flex-start; } .layer-label { text-align: left; } }
|
||||
</style>
|
||||
@@ -0,0 +1,266 @@
|
||||
<!--
|
||||
DataQualityDemo.vue
|
||||
数据质量维度演示:展示数据质量的六个核心维度
|
||||
-->
|
||||
<template>
|
||||
<div class="data-quality-demo">
|
||||
<div class="header">
|
||||
<div class="title">数据质量检测器</div>
|
||||
<div class="subtitle">点击不同维度,查看数据质量问题示例</div>
|
||||
</div>
|
||||
|
||||
<div class="dimensions">
|
||||
<div
|
||||
v-for="dim in dimensions"
|
||||
:key="dim.key"
|
||||
:class="['dim-card', { active: activeDim === dim.key }]"
|
||||
@click="activeDim = dim.key"
|
||||
>
|
||||
<div class="dim-icon">{{ dim.icon }}</div>
|
||||
<div class="dim-name">{{ dim.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentDim" class="detail-panel">
|
||||
<div class="detail-header">
|
||||
<span class="detail-icon">{{ currentDim.icon }}</span>
|
||||
<span class="detail-title">{{ currentDim.name }}</span>
|
||||
<span class="detail-desc">{{ currentDim.desc }}</span>
|
||||
</div>
|
||||
|
||||
<div class="example-section">
|
||||
<div class="example bad">
|
||||
<div class="example-label bad-label">问题数据</div>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="col in currentDim.badData.cols" :key="col">{{ col }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, i) in currentDim.badData.rows" :key="i">
|
||||
<td
|
||||
v-for="(cell, j) in row"
|
||||
:key="j"
|
||||
:class="{ 'cell-error': cell.error }"
|
||||
>{{ cell.value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="example good">
|
||||
<div class="example-label good-label">治理后</div>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="col in currentDim.goodData.cols" :key="col">{{ col }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, i) in currentDim.goodData.rows" :key="i">
|
||||
<td v-for="(cell, j) in row" :key="j">{{ cell.value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="quality-score">
|
||||
<div class="score-label">质量评分</div>
|
||||
<div class="score-bar-bg">
|
||||
<div
|
||||
class="score-bar-fill"
|
||||
:style="{ width: currentDim.score + '%', background: scoreColor(currentDim.score) }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="score-value">{{ currentDim.score }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const activeDim = ref('completeness')
|
||||
|
||||
const dimensions = [
|
||||
{
|
||||
key: 'completeness', name: '完整性', icon: '📋',
|
||||
desc: '数据是否存在缺失值',
|
||||
score: 72,
|
||||
badData: {
|
||||
cols: ['用户ID', '姓名', '邮箱', '手机号'],
|
||||
rows: [
|
||||
[{ value: '001' }, { value: '张三' }, { value: 'zhang@mail.com' }, { value: '138xxxx1234' }],
|
||||
[{ value: '002' }, { value: '李四' }, { value: '', error: true }, { value: '', error: true }],
|
||||
[{ value: '003' }, { value: '', error: true }, { value: 'wang@mail.com' }, { value: '139xxxx5678' }]
|
||||
]
|
||||
},
|
||||
goodData: {
|
||||
cols: ['用户ID', '姓名', '邮箱', '手机号'],
|
||||
rows: [
|
||||
[{ value: '001' }, { value: '张三' }, { value: 'zhang@mail.com' }, { value: '138xxxx1234' }],
|
||||
[{ value: '002' }, { value: '李四' }, { value: 'li@mail.com' }, { value: '137xxxx9012' }],
|
||||
[{ value: '003' }, { value: '王五' }, { value: 'wang@mail.com' }, { value: '139xxxx5678' }]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'accuracy', name: '准确性', icon: '🎯',
|
||||
desc: '数据值是否正确反映真实情况',
|
||||
score: 65,
|
||||
badData: {
|
||||
cols: ['订单ID', '金额', '日期', '状态'],
|
||||
rows: [
|
||||
[{ value: 'ORD-101' }, { value: '-50.00', error: true }, { value: '2025-01-15' }, { value: '已完成' }],
|
||||
[{ value: 'ORD-102' }, { value: '299.00' }, { value: '2025-13-01', error: true }, { value: '已发货' }],
|
||||
[{ value: 'ORD-103' }, { value: '1500.00' }, { value: '2025-02-28' }, { value: '已退款', error: true }]
|
||||
]
|
||||
},
|
||||
goodData: {
|
||||
cols: ['订单ID', '金额', '日期', '状态'],
|
||||
rows: [
|
||||
[{ value: 'ORD-101' }, { value: '50.00' }, { value: '2025-01-15' }, { value: '已完成' }],
|
||||
[{ value: 'ORD-102' }, { value: '299.00' }, { value: '2025-01-13' }, { value: '已发货' }],
|
||||
[{ value: 'ORD-103' }, { value: '1500.00' }, { value: '2025-02-28' }, { value: '已完成' }]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'consistency', name: '一致性', icon: '🔗',
|
||||
desc: '同一数据在不同系统中是否一致',
|
||||
score: 58,
|
||||
badData: {
|
||||
cols: ['来源', '用户名', '手机号', '地址'],
|
||||
rows: [
|
||||
[{ value: 'CRM' }, { value: '张三' }, { value: '13812341234' }, { value: '北京市朝阳区' }],
|
||||
[{ value: '订单系统' }, { value: '张三丰', error: true }, { value: '13812341234' }, { value: '北京朝阳', error: true }],
|
||||
[{ value: '客服系统' }, { value: '张三' }, { value: '13899999999', error: true }, { value: '北京市朝阳区' }]
|
||||
]
|
||||
},
|
||||
goodData: {
|
||||
cols: ['来源', '用户名', '手机号', '地址'],
|
||||
rows: [
|
||||
[{ value: 'CRM' }, { value: '张三' }, { value: '13812341234' }, { value: '北京市朝阳区' }],
|
||||
[{ value: '订单系统' }, { value: '张三' }, { value: '13812341234' }, { value: '北京市朝阳区' }],
|
||||
[{ value: '客服系统' }, { value: '张三' }, { value: '13812341234' }, { value: '北京市朝阳区' }]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'timeliness', name: '时效性', icon: '⏰',
|
||||
desc: '数据是否及时更新',
|
||||
score: 80,
|
||||
badData: {
|
||||
cols: ['商品ID', '价格', '库存', '更新时间'],
|
||||
rows: [
|
||||
[{ value: 'SKU-001' }, { value: '¥299' }, { value: '50' }, { value: '2024-06-01', error: true }],
|
||||
[{ value: 'SKU-002' }, { value: '¥599' }, { value: '0', error: true }, { value: '2024-03-15', error: true }],
|
||||
[{ value: 'SKU-003' }, { value: '¥199' }, { value: '200' }, { value: '2025-02-20' }]
|
||||
]
|
||||
},
|
||||
goodData: {
|
||||
cols: ['商品ID', '价格', '库存', '更新时间'],
|
||||
rows: [
|
||||
[{ value: 'SKU-001' }, { value: '¥259' }, { value: '35' }, { value: '2025-02-25' }],
|
||||
[{ value: 'SKU-002' }, { value: '¥549' }, { value: '12' }, { value: '2025-02-25' }],
|
||||
[{ value: 'SKU-003' }, { value: '¥199' }, { value: '180' }, { value: '2025-02-25' }]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'uniqueness', name: '唯一性', icon: '🔑',
|
||||
desc: '数据是否存在重复记录',
|
||||
score: 70,
|
||||
badData: {
|
||||
cols: ['用户ID', '姓名', '邮箱', '注册时间'],
|
||||
rows: [
|
||||
[{ value: '001' }, { value: '张三' }, { value: 'zhang@mail.com' }, { value: '2025-01-01' }],
|
||||
[{ value: '005' }, { value: '张三', error: true }, { value: 'zhang@mail.com', error: true }, { value: '2025-01-15', error: true }],
|
||||
[{ value: '002' }, { value: '李四' }, { value: 'li@mail.com' }, { value: '2025-01-10' }]
|
||||
]
|
||||
},
|
||||
goodData: {
|
||||
cols: ['用户ID', '姓名', '邮箱', '注册时间'],
|
||||
rows: [
|
||||
[{ value: '001' }, { value: '张三' }, { value: 'zhang@mail.com' }, { value: '2025-01-01' }],
|
||||
[{ value: '002' }, { value: '李四' }, { value: 'li@mail.com' }, { value: '2025-01-10' }]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'validity', name: '有效性', icon: '✅',
|
||||
desc: '数据是否符合预定义的格式和规则',
|
||||
score: 75,
|
||||
badData: {
|
||||
cols: ['字段', '值', '规则'],
|
||||
rows: [
|
||||
[{ value: '邮箱' }, { value: 'not-an-email', error: true }, { value: '需包含@' }],
|
||||
[{ value: '年龄' }, { value: '-5', error: true }, { value: '0~150' }],
|
||||
[{ value: '手机号' }, { value: '1234', error: true }, { value: '11位数字' }]
|
||||
]
|
||||
},
|
||||
goodData: {
|
||||
cols: ['字段', '值', '规则'],
|
||||
rows: [
|
||||
[{ value: '邮箱' }, { value: 'user@mail.com' }, { value: '需包含@' }],
|
||||
[{ value: '年龄' }, { value: '28' }, { value: '0~150' }],
|
||||
[{ value: '手机号' }, { value: '13812345678' }, { value: '11位数字' }]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const currentDim = computed(() => dimensions.find(d => d.key === activeDim.value))
|
||||
|
||||
function scoreColor(score) {
|
||||
if (score >= 80) return '#22c55e'
|
||||
if (score >= 60) return '#f59e0b'
|
||||
return '#ef4444'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.data-quality-demo {
|
||||
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft);
|
||||
border-radius: 12px; padding: 1.5rem; margin: 1.5rem 0;
|
||||
}
|
||||
.header { margin-bottom: 1rem; }
|
||||
.title { font-weight: 700; font-size: 1.1rem; }
|
||||
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
|
||||
.dimensions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1rem; }
|
||||
.dim-card {
|
||||
display: flex; align-items: center; gap: 0.4rem; padding: 0.5rem 0.75rem;
|
||||
border-radius: 8px; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
||||
cursor: pointer; font-size: 0.85rem; transition: all 0.2s;
|
||||
}
|
||||
.dim-card:hover { border-color: var(--vp-c-brand); }
|
||||
.dim-card.active { border-color: var(--vp-c-brand); background: rgba(var(--vp-c-brand-rgb), 0.05); }
|
||||
.dim-icon { font-size: 1.1rem; }
|
||||
.dim-name { font-weight: 600; }
|
||||
.detail-panel {
|
||||
padding: 1rem; border-radius: 8px; background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider); margin-bottom: 1rem;
|
||||
}
|
||||
.detail-header { margin-bottom: 0.75rem; display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
|
||||
.detail-icon { font-size: 1.2rem; }
|
||||
.detail-title { font-weight: 700; font-size: 1rem; }
|
||||
.detail-desc { color: var(--vp-c-text-2); font-size: 0.85rem; }
|
||||
.example-section { display: flex; gap: 1rem; margin-bottom: 1rem; }
|
||||
.example { flex: 1; }
|
||||
.example-label { font-weight: 600; font-size: 0.8rem; margin-bottom: 0.4rem; padding: 0.2rem 0.5rem; border-radius: 4px; display: inline-block; }
|
||||
.bad-label { background: rgba(239,68,68,0.1); color: #ef4444; }
|
||||
.good-label { background: rgba(34,197,94,0.1); color: #22c55e; }
|
||||
.data-table { width: 100%; border-collapse: collapse; font-size: 0.75rem; }
|
||||
.data-table th { background: var(--vp-c-bg-soft); padding: 0.3rem 0.4rem; text-align: left; font-weight: 600; border-bottom: 1px solid var(--vp-c-divider); }
|
||||
.data-table td { padding: 0.3rem 0.4rem; border-bottom: 1px solid var(--vp-c-divider); }
|
||||
.cell-error { background: rgba(239,68,68,0.1); color: #ef4444; font-weight: 600; }
|
||||
.quality-score { display: flex; align-items: center; gap: 0.75rem; }
|
||||
.score-label { font-weight: 600; font-size: 0.85rem; white-space: nowrap; }
|
||||
.score-bar-bg { flex: 1; height: 10px; background: var(--vp-c-bg-soft); border-radius: 5px; overflow: hidden; }
|
||||
.score-bar-fill { height: 100%; border-radius: 5px; transition: width 0.4s; }
|
||||
.score-value { font-weight: 700; font-size: 0.9rem; font-family: var(--vp-font-family-mono); min-width: 40px; }
|
||||
@media (max-width: 640px) { .example-section { flex-direction: column; } }
|
||||
</style>
|
||||
Reference in New Issue
Block a user