feat(ui): CMA前端UI优化P0 - BSC变量+公共类+统一标题+色值替换+内联style替换

This commit is contained in:
Hermes CI Fix
2026-07-17 16:35:54 +08:00
parent 7f0f74a6e9
commit c82bd5b40f
11 changed files with 93 additions and 54 deletions
+22
View File
@@ -77,6 +77,12 @@ html, body, #app {
--bh-breakpoint-md: 992px;
--bh-breakpoint-lg: 1200px;
--bh-breakpoint-xl: 1920px;
/* BSC四层泳道标准色 */
--bsc-finance: #F56C6C;
--bsc-customer: #409EFF;
--bsc-process: #67C23A;
--bsc-learning: #E6A23C;
}
/* 暗黑模式覆盖 — 只影响内容区,侧边栏保持不变 */
@@ -343,6 +349,22 @@ html.dark .el-drawer__header {
--el-dialog-margin-top: 1vh !important;
}
/* === 页面标准间距与公共布局类 === */
.page-view { padding: 16px; }
.page-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:16px; flex-wrap:wrap; gap:8px; }
.page-title { font-size:18px; font-weight:600; margin:0; }
.page-title h3 { margin:0; }
/* 卡片统计数字 */
.stat-number { font-size:24px; font-weight:600; }
.stat-label { font-size:12px; color:#909399; margin-top:4px; }
/* 通用flex布局 */
.flex-row { display:flex; align-items:center; gap:8px; }
.flex-wrap { flex-wrap:wrap; }
.flex-between { display:flex; justify-content:space-between; align-items:center; }
.section-gap { margin-bottom:16px; }
/* 过渡动画 — 明暗切换平滑过渡 */
html.dark,
html,
+16 -13
View File
@@ -1,7 +1,10 @@
<template>
<div style="padding:0 16px;">
<div class="page-view">
<div class="page-header">
<h3 class="page-title">行动方案库</h3>
</div>
<!-- 顶部tabs -->
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<div class="flex-between" style="margin-bottom:12px;">
<div>
<el-radio-group v-model="activeTab" size="small">
<el-radio-button value="plans">行动方案</el-radio-button>
@@ -17,7 +20,7 @@
<!-- ===== 行动方案列表 ===== -->
<template v-if="activeTab === 'plans'">
<!-- 状态统计卡片 -->
<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap;">
<div class="flex-row flex-wrap" style="margin-bottom:16px;">
<div v-for="card in statCards" :key="card.key"
:class="['stat-card', { active: filterStatus === card.key }]"
@click="filterStatus = card.key; page = 1; loadData()"
@@ -28,7 +31,7 @@
</div>
<!-- 筛选栏 -->
<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap;align-items:center;">
<div class="flex-row" style="gap:12px;margin-bottom:16px;flex-wrap:wrap;align-items:center;">
<el-select v-model="filterDimension" placeholder="维度" clearable style="width:100px;" @change="page=1;loadData()">
<el-option v-for="d in dimensions" :key="d.value" :label="d.label" :value="d.value" />
</el-select>
@@ -92,7 +95,7 @@
<el-table-column label="截止日" width="100" prop="due_date">
<template #default="{ row }">
<span v-if="!row.due_date" style="color:#ccc;"></span>
<span v-else :style="{ color: isOverdue(row) ? '#f56c6c' : '#666', fontWeight: isOverdue(row) ? 600 : 400 }">
<span v-else :style="{ color: isOverdue(row) ? 'var(--bsc-finance)' : '#666', fontWeight: isOverdue(row) ? 600 : 400 }">
{{ row.due_date?.slice(0, 10) }}
<el-tooltip v-if="isOverdue(row)" content="已逾期" placement="top"><span style="margin-left:2px;"></span></el-tooltip>
</span>
@@ -112,7 +115,7 @@
</el-table>
<!-- 批量操作栏 -->
<div v-if="selectedRows.length > 0" style="margin-top:8px;display:flex;gap:8px;align-items:center;padding:8px 12px;background:#f0f9ff;border-radius:6px;">
<div v-if="selectedRows.length > 0" class="flex-row" style="margin-top:8px;padding:8px 12px;background:#f0f9ff;border-radius:6px;">
<span style="font-size:13px;color:#409eff;">已选 {{ selectedRows.length }} </span>
<el-button size="small" @click="batchSetStatus('in_progress')">置为进行中</el-button>
<el-button size="small" type="success" @click="batchSetStatus('completed')">置为已完成</el-button>
@@ -217,7 +220,7 @@
<div style="font-size:12px;color:#999;">截止日期</div>
<div style="margin-top:2px;" :class="{ 'overdue-text': isOverdue(detailPlan) }">
{{ detailPlan.due_date?.slice(0, 10) || '未设置' }}
<span v-if="isOverdue(detailPlan)" style="color:#f56c6c;font-size:12px;margin-left:4px;">已逾期</span>
<span v-if="isOverdue(detailPlan)" style="color:var(--bsc-finance);font-size:12px;margin-left:4px;">已逾期</span>
</div>
</div>
<div>
@@ -279,7 +282,7 @@
<div>
<div style="font-size:12px;color:#909399;">总体内控评分</div>
<div style="font-size:36px;font-weight:700;margin-top:2px;"
:style="{ color: cosoData.total_score < 50 ? '#f56c6c' : cosoData.total_score < 70 ? '#e6a23c' : '#67c23a' }">
:style="{ color: cosoData.total_score < 50 ? 'var(--bsc-finance)' : cosoData.total_score < 70 ? 'var(--bsc-learning)' : 'var(--bsc-process)' }">
{{ cosoData.total_score }}/{{ cosoData.max_score }}
</div>
</div>
@@ -290,7 +293,7 @@
</div>
<div style="flex:1;display:flex;gap:12px;flex-wrap:wrap;">
<div v-for="el in cosoData.elements" :key="el.id" style="text-align:center;min-width:80px;">
<div style="font-size:20px;font-weight:600;" :style="{ color: el.score >= 60 ? '#67c23a' : el.score >= 40 ? '#e6a23c' : '#f56c6c' }">{{ el.score }}</div>
<div style="font-size:20px;font-weight:600;" :style="{ color: el.score >= 60 ? 'var(--bsc-process)' : el.score >= 40 ? 'var(--bsc-learning)' : 'var(--bsc-finance)' }">{{ el.score }}</div>
<div style="font-size:11px;color:#999;">{{ el.name }}</div>
</div>
</div>
@@ -299,14 +302,14 @@
<!-- 五要素折叠面板 -->
<div v-for="el in cosoData.elements" :key="el.id" style="margin-bottom:8px;border:1px solid #ebeef5;border-radius:8px;overflow:hidden;">
<div class="coso-element-header" @click="toggleCosoElement(el.id)" style="cursor:pointer;display:flex;align-items:center;gap:12px;padding:12px 16px;background:#fafafa;border-left:4px solid"
:style="{ borderLeftColor: el.score >= 60 ? '#67c23a' : el.score >= 40 ? '#e6a23c' : '#f56c6c' }">
:style="{ borderLeftColor: el.score >= 60 ? 'var(--bsc-process)' : el.score >= 40 ? 'var(--bsc-learning)' : 'var(--bsc-finance)' }">
<span style="flex:1;font-weight:500;">{{ el.name }}{{ el.name_en }}</span>
<el-progress :percentage="el.score" :stroke-width="14" :status="el.score >= 60 ? 'success' : el.score >= 40 ? 'warning' : 'exception'" style="width:160px;" />
<span :class="{ 'is-open': cosoExpanded[el.id] }" style="color:#999;transition:transform .2s;"></span>
</div>
<div v-show="cosoExpanded[el.id]" style="padding:8px 16px 12px;">
<div v-for="item in el.items" :key="item.id" style="display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid #f5f5f5;">
<span :style="{color: item.passed ? '#67c23a' : '#f56c6c', fontSize: '16px', fontWeight: 'bold'}">{{ item.passed ? '✅' : '❌' }}</span>
<span :style="{color: item.passed ? 'var(--bsc-process)' : 'var(--bsc-finance)', fontSize: '16px', fontWeight: 'bold'}">{{ item.passed ? '✅' : '❌' }}</span>
<span style="flex:1;font-size:14px;">{{ item.text }}</span>
<span style="font-size:12px;color:#666;">{{ item.detail }}</span>
</div>
@@ -601,11 +604,11 @@ onMounted(() => {
background: #fff;
}
.stat-card.active {
border-color: #409eff;
border-color: var(--bsc-customer);
background: #ecf5ff;
}
.overdue-text {
color: #f56c6c;
color: var(--bsc-finance);
font-weight: 600;
}
</style>
+5 -5
View File
@@ -1,7 +1,7 @@
<template>
<div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<h2 style="margin:0;">BI报表中心</h2>
<div class="flex-between section-gap">
<h3 class="page-title">BI报表中心</h3>
<div>
<el-button type="primary" @click="showTemplateDialog = true">从模板创建</el-button>
<el-button @click="showNewReport = true">新建分析报表</el-button>
@@ -9,7 +9,7 @@
</div>
<!-- 预置模板 -->
<el-card shadow="never" style="margin-bottom:16px;">
<el-card shadow="never" class="section-gap">
<template #header>预置报表模板</template>
<el-row :gutter="16">
<el-col v-for="tpl in templates" :key="tpl.id" :span="24 / templates.length || 1">
@@ -31,7 +31,7 @@
</el-card>
<!-- 分析引擎 -->
<el-card v-if="analysisResult" shadow="never" style="margin-bottom:16px;">
<el-card v-if="analysisResult" shadow="never" class="section-gap">
<template #header>
<div style="display:flex;justify-content:space-between;align-items:center;">
<span>{{ currentReportName }}</span>
@@ -61,7 +61,7 @@
</el-card>
<!-- 已保存报表 -->
<el-card shadow="never">
<el-card shadow="never" class="section-gap">
<template #header>已保存报表</template>
<el-table :data="reports" size="small" v-loading="loading">
<el-table-column prop="name" label="报表名称" min-width="200" />
+3
View File
@@ -1,5 +1,8 @@
<template>
<div class="customer-dashboard">
<div class="page-header">
<h3 class="page-title">客户维度KPI看板</h3>
</div>
<KPIListView
ref="kpiListRef"
:dimension="'customer'"
+17 -17
View File
@@ -3,7 +3,7 @@
<!-- 顶部导航条 -->
<div class="dash-header">
<div class="header-left">
<el-tag size="large" :type="roleTagType">{{ roleLabel }}驾驶舱</el-tag>
<h3 class="page-title">{{ roleLabel }}驾驶舱</h3>
<el-radio-group v-model="periodType" size="small" @change="onPeriodChange">
<el-radio-button value="month">本月</el-radio-button>
<el-radio-button value="quarter">本季</el-radio-button>
@@ -112,7 +112,7 @@
<div class="pred-row-val">
<span class="pred-cur">{{ fmtValue(p.last_value, p.kpi_code) }}</span>
<span class="pred-sep"></span>
<span class="pred-fut" :style="{ color: p.trend === 'down' ? '#f56c6c' : '#67c23a' }">
<span class="pred-fut" :style="{ color: p.trend === 'down' ? 'var(--bsc-finance)' : 'var(--bsc-process)' }">
{{ fmtValue(p.predicted_value, p.kpi_code) }}
</span>
</div>
@@ -449,10 +449,10 @@ onMounted(() => { loadData() })
.summary-row { display:grid; grid-template-columns:repeat(auto-fit, minmax(150px, 1fr)); gap:14px; margin-bottom:20px; }
.stat-card { background:#fff; border-radius:10px; padding:16px 20px; box-shadow:0 1px 3px rgba(0,0,0,0.05); border-top:3px solid #ccc; transition:transform .15s; cursor:default; }
.stat-card:hover { transform:translateY(-1px); box-shadow:0 3px 10px rgba(0,0,0,0.08); }
.stat-card.blue { border-top-color:#409eff; }
.stat-card.red { border-top-color:#f56c6c; }
.stat-card.green { border-top-color:#67c23a; }
.stat-card.yellow { border-top-color:#e6a23c; }
.stat-card.blue { border-top-color:var(--bsc-customer); }
.stat-card.red { border-top-color:var(--bsc-finance); }
.stat-card.green { border-top-color:var(--bsc-process); }
.stat-card.yellow { border-top-color:var(--bsc-learning); }
.stat-card.gray { border-top-color:#909399; }
.stat-val { font-size:28px; font-weight:700; color:#1a1a2e; letter-spacing:-1px; }
.stat-label { font-size:13px; color:#888; margin-top:2px; }
@@ -470,11 +470,11 @@ onMounted(() => { loadData() })
.spotlight-list { display:flex; flex-direction:column; gap:6px; }
.spotlight-item { display:flex; align-items:center; gap:10px; padding:10px 12px; border-radius:8px; cursor:pointer; transition:background .15s; font-size:13px; }
.spotlight-item:hover { background:rgba(245,108,108,0.08); }
.spotlight-item.level-red { border-left:3px solid #f56c6c; }
.spotlight-item.level-yellow { border-left:3px solid #e6a23c; }
.spotlight-item.level-red { border-left:3px solid var(--bsc-finance); }
.spotlight-item.level-yellow { border-left:3px solid var(--bsc-learning); }
.spotlight-badge { font-size:11px; font-weight:600; padding:2px 8px; border-radius:4px; white-space:nowrap; }
.spotlight-item.level-red .spotlight-badge { background:#fef0f0; color:#f56c6c; }
.spotlight-item.level-yellow .spotlight-badge { background:#fdf6ec; color:#e6a23c; }
.spotlight-item.level-red .spotlight-badge { background:#fef0f0; color:var(--bsc-finance); }
.spotlight-item.level-yellow .spotlight-badge { background:#fdf6ec; color:var(--bsc-learning); }
.spotlight-msg { flex:1; color:#333; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.spotlight-item .el-icon { color:#ccc; flex-shrink:0; }
@@ -501,22 +501,22 @@ onMounted(() => { loadData() })
.pred-list { display:flex; flex-direction:column; gap:12px; }
.pred-row { padding:12px; border-radius:8px; border:1px solid #f0f0f0; transition:border-color .15s; }
.pred-row:hover { border-color:#d0d0d0; }
.pred-row.trend-down { border-left:3px solid #f56c6c; }
.pred-row.trend-up { border-left:3px solid #67c23a; }
.pred-row.trend-down { border-left:3px solid var(--bsc-finance); }
.pred-row.trend-up { border-left:3px solid var(--bsc-process); }
.pred-row.trend-stable { border-left:3px solid #909399; }
.pred-row-top { display:flex; justify-content:space-between; margin-bottom:6px; }
.pred-name { font-size:13px; font-weight:500; color:#333; }
.pred-arrow { font-size:14px; font-weight:700; color:#999; }
.pred-row.trend-down .pred-arrow { color:#f56c6c; }
.pred-row.trend-up .pred-arrow { color:#67c23a; }
.pred-row.trend-down .pred-arrow { color:var(--bsc-finance); }
.pred-row.trend-up .pred-arrow { color:var(--bsc-process); }
.pred-row-val { display:flex; align-items:center; gap:6px; margin-bottom:4px; }
.pred-cur { font-size:16px; font-weight:600; color:#555; }
.pred-sep { color:#ccc; font-size:12px; }
.pred-fut { font-size:18px; font-weight:700; }
.pred-row-info { font-size:11px; }
.pred-conf { padding:1px 6px; border-radius:3px; }
.pred-conf.high { background:#f0f9eb; color:#67c23a; }
.pred-conf.medium { background:#fdf6ec; color:#e6a23c; }
.pred-row.trend-up .pred-arrow { color:var(--bsc-process); }
.pred-conf.high { background:#f0f9eb; color:var(--bsc-process); }
.pred-conf.medium { background:#fdf6ec; color:var(--bsc-learning); }
.pred-conf.low { background:#f4f4f5; color:#909399; }
/* ========== 业务/财务KPI卡片 ========== */
+7 -7
View File
@@ -1,7 +1,7 @@
<template>
<div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<h2 style="margin:0;">数据质量监控</h2>
<div class="flex-between section-gap">
<span class="page-title">数据质量监控</span>
<div>
<el-button type="primary" @click="runCheck" :loading="checking">执行全量检查</el-button>
<el-button @click="loadLogs">刷新</el-button>
@@ -9,7 +9,7 @@
</div>
<!-- 统计卡片 -->
<el-row :gutter="16" style="margin-bottom:16px;">
<el-row :gutter="16" class="section-gap">
<el-col :span="6">
<el-card shadow="never">
<div style="text-align:center;">
@@ -21,7 +21,7 @@
<el-col :span="6">
<el-card shadow="never">
<div style="text-align:center;">
<div style="font-size:28px;font-weight:600;color:#e6a23c;">{{ stats.open_logs }}</div>
<div style="font-size:28px;font-weight:600;color:var(--bsc-learning);">{{ stats.open_logs }}</div>
<div style="font-size:13px;color:#999;">未解决异常</div>
</div>
</el-card>
@@ -45,7 +45,7 @@
</el-row>
<!-- 异常类型分布 -->
<el-row :gutter="16" style="margin-bottom:16px;">
<el-row :gutter="16" class="section-gap">
<el-col :span="24">
<el-card shadow="never">
<template #header>异常类型分布</template>
@@ -137,8 +137,8 @@ const typeChartOption = computed(() => ({
type: 'pie',
radius: ['40%', '70%'],
data: [
{ name: '数据缺失', value: stats.value.type_counts?.missing_data || 0, itemStyle: { color: '#f56c6c' } },
{ name: '环比骤变', value: stats.value.type_counts?.abnormal_change || 0, itemStyle: { color: '#e6a23c' } },
{ name: '数据缺失', value: stats.value.type_counts?.missing_data || 0, itemStyle: { color: 'var(--bsc-finance)' } },
{ name: '环比骤变', value: stats.value.type_counts?.abnormal_change || 0, itemStyle: { color: 'var(--bsc-learning)' } },
{ name: '连续持平', value: stats.value.type_counts?.flat_data || 0, itemStyle: { color: '#409eff' } },
{ name: '值异常', value: stats.value.type_counts?.value_outlier || 0, itemStyle: { color: '#909399' } },
].filter(d => d.value > 0),
+4 -1
View File
@@ -1,7 +1,10 @@
<template>
<div class="dupont-analysis" v-loading="loading">
<div class="page-header">
<h3 class="page-title">杜邦分析</h3>
</div>
<!-- 顶部控制 -->
<div style="display:flex;gap:12px;margin-bottom:16px;align-items:center;">
<div class="flex-row" style="gap:12px;margin-bottom:16px;align-items:center;">
<span style="font-weight:600;font-size:14px;">分析对象</span>
<el-radio-group v-model="entity" @change="fetchDupont">
<el-radio value="bohai">陕西博海科技IT服务</el-radio>
+1 -1
View File
@@ -14,7 +14,7 @@
<!-- 模式选择器 -->
<div v-if="showSelector" class="align-setup">
<el-alert v-if="!alignConfigured" title="系统尚未配置KPI对齐模式" type="warning" :closable="false" show-icon style="margin-bottom:16px;" />
<el-alert v-if="!alignConfigured" title="系统尚未配置KPI对齐模式" type="warning" :closable="false" show-icon class="section-gap" />
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;">
<el-card v-for="mode in alignModes" :key="mode.key"
:class="{ 'mode-card': true, 'selected-mode': selectedMode === mode.key }"
+7 -4
View File
@@ -1,6 +1,9 @@
<template>
<div>
<el-button text @click="$router.back()">< 返回KPI字典</el-button>
<div class="page-header">
<h3 class="page-title">KPI详情</h3>
</div>
<el-card v-if="kpi" style="margin-top:16px;">
<template #header>
<span style="font-size:16px;font-weight:600;">{{ kpi.kpi_name }}</span>
@@ -99,7 +102,7 @@
<div v-if="upstream.length === 0" style="color:#999;text-align:center;padding:20px;">无上游驱动</div>
<el-timeline v-else>
<el-timeline-item v-for="item in upstream" :key="item.kpi_id"
:color="item.direction === 'positive' ? '#67c23a' : '#f56c6c'"
:color="item.direction === 'positive' ? 'var(--bsc-process)' : 'var(--bsc-finance)'"
:timestamp="'强度:' + item.strength + ' / 滞后期:' + item.lag_months + '月'">
<div><b>{{ item.kpi_name }}</b> ({{ item.kpi_code }})</div>
<div style="font-size:12px;color:#999;">
@@ -116,7 +119,7 @@
<div v-if="downstream.length === 0" style="color:#999;text-align:center;padding:20px;">无下游影响</div>
<el-timeline v-else>
<el-timeline-item v-for="item in downstream" :key="item.kpi_id"
:color="item.direction === 'positive' ? '#67c23a' : '#f56c6c'"
:color="item.direction === 'positive' ? 'var(--bsc-process)' : 'var(--bsc-finance)'"
:timestamp="'强度:' + item.strength + ' / 滞后期:' + item.lag_months + '月'">
<div><b>{{ item.kpi_name }}</b> ({{ item.kpi_code }})</div>
<div style="font-size:12px;color:#999;">
@@ -159,11 +162,11 @@
</el-table-column>
<el-table-column prop="current_value" label="当前值" width="90" />
<el-table-column prop="predicted_value" label="预测值" width="90">
<template #default="{ row }"><span style="color:#409eff;font-weight:600;">{{ row.predicted_value ?? '-' }}</span></template>
<template #default="{ row }"><span style="color:var(--bsc-customer);font-weight:600;">{{ row.predicted_value ?? '-' }}</span></template>
</el-table-column>
<el-table-column prop="change_pct" label="变化率" width="80">
<template #default="{ row }">
<span :style="{ color: row.change_pct > 0 ? '#67c23a' : '#f56c6c' }">{{ row.change_pct > 0 ? '+' : '' }}{{ row.change_pct }}%</span>
<span :style="{ color: row.change_pct > 0 ? 'var(--bsc-process)' : 'var(--bsc-finance)' }">{{ row.change_pct > 0 ? '+' : '' }}{{ row.change_pct }}%</span>
</template>
</el-table-column>
<el-table-column prop="path_strength" label="传递强度" width="80" />
+6 -1
View File
@@ -1,5 +1,9 @@
<template>
<KPIListView
<div>
<div class="page-header">
<h3 class="page-title">学习成长维度KPI看板</h3>
</div>
<KPIListView
:dimension="'learning'"
:title="'学习成长维度KPI看板'"
:icon="'📚'"
@@ -12,6 +16,7 @@
form-title-create="新建学习成长KPI"
form-title-edit="编辑学习成长KPI"
/>
</div>
</template>
<script setup lang="ts">
+5 -5
View File
@@ -2,7 +2,7 @@
<div class="report-page">
<!-- 顶部 -->
<div class="report-header">
<h3 style="margin:0;">📊 CMA管理报表</h3>
<h3 class="page-title">📊 CMA管理报表</h3>
<div class="header-right">
<el-date-picker
v-model="reportPeriod"
@@ -268,8 +268,8 @@ function dimLabel(d: string): string {
}
function bscScoreColor(score: number): string {
if (score >= 80) return '#67c23a'
if (score >= 60) return '#e6a23c'
if (score >= 80) return 'var(--bsc-process)'
if (score >= 60) return 'var(--bsc-learning)'
return '#f56c6c'
}
@@ -398,7 +398,7 @@ onMounted(() => {
.stat-card.blue { border-top-color: #409eff; }
.stat-card.red { border-top-color: #f56c6c; }
.stat-card.green { border-top-color: #67c23a; }
.stat-card.yellow { border-top-color: #e6a23c; }
.stat-card.yellow { border-top-color: var(--bsc-learning); }
.stat-card.gray { border-top-color: #909399; }
.stat-val { font-size: 22px; font-weight: 700; color: #1a1a2e; }
.stat-label { font-size: 12px; color: #888; margin-top: 2px; }
@@ -438,7 +438,7 @@ onMounted(() => {
.bsc-kpi-mini { display: flex; align-items: center; gap: 6px; font-size: 12px; padding: 2px 6px; background: #f9f9f9; border-radius: 4px; }
.bsc-kpi-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
.dot-green { background: #67c23a; }
.dot-yellow { background: #e6a23c; }
.dot-yellow { background: var(--bsc-learning); }
.dot-red { background: #f56c6c; }
.dot-gray { background: #ddd; }
.bsc-kpi-name { flex: 1; color: #666; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }