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:
sanbuphy
2026-02-26 04:35:28 +08:00
parent df51f84ab5
commit ef70b1d8e1
84 changed files with 12917 additions and 3477 deletions
@@ -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>