449 lines
21 KiB
Vue
449 lines
21 KiB
Vue
<template>
|
||
<div class="report-page">
|
||
<!-- 顶部 -->
|
||
<div class="report-header">
|
||
<h3 class="page-title">📊 CMA管理报表</h3>
|
||
<div class="header-right">
|
||
<el-date-picker
|
||
v-model="reportPeriod"
|
||
type="month"
|
||
placeholder="选择月份"
|
||
value-format="YYYY-MM"
|
||
size="small"
|
||
style="width:140px"
|
||
@change="onPeriodChange"
|
||
/>
|
||
<el-button text size="small" :loading="loading" @click="loadAll">刷新</el-button>
|
||
<el-button type="primary" size="small" @click="openDupont">📊 杜邦分析</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 四个标签页 -->
|
||
<el-tabs v-model="activeTab" type="border-card" class="report-tabs">
|
||
|
||
<!-- ════ 报表1:管理利润表 ════ -->
|
||
<el-tab-pane label="💰 管理利润表" name="profit">
|
||
<div v-loading="loading">
|
||
<div class="report-desc">基于管理会计视角的利润结构分析:收入 → 变动成本 → 边际贡献 → 固定成本 → 息税前利润</div>
|
||
<el-table :data="profitItems" border stripe size="small" style="width:100%;margin-top:12px;" :summary-method="profitSummary" show-summary>
|
||
<el-table-column prop="name" label="项目" width="200">
|
||
<template #default="{ row }">
|
||
<span :style="{ fontWeight: row.is_total ? 700 : row.is_subtotal ? 600 : 400 }">{{ row.name }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="本期金额" width="160" align="right">
|
||
<template #default="{ row }">
|
||
<span :style="{ fontWeight: row.is_total ? 700 : 400, color: (row.name?.includes('利润') || row.name?.includes('贡献')) && row.value != null && row.value < 0 ? '#f56c6c' : '#333' }">
|
||
{{ row.value != null ? '¥ ' + row.value.toLocaleString() : '-' }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="上期金额" width="160" align="right">
|
||
<template #default="{ row }">
|
||
<span style="color:#999;">{{ row.prev_value != null ? '¥ ' + row.prev_value.toLocaleString() : '-' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="环比变化" width="120" align="right">
|
||
<template #default="{ row }">
|
||
<span v-if="row.change_rate != null" :style="{ color: row.change_rate > 0 ? '#f56c6c' : row.change_rate < 0 ? '#67c23a' : '#999', fontWeight: 600 }">
|
||
{{ row.change_rate > 0 ? '↑' : '↓' }}{{ Math.abs(row.change_rate).toFixed(1) }}%
|
||
</span>
|
||
<span v-else style="color:#ccc;">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="占比" width="100" align="right">
|
||
<template #default="{ row }">
|
||
<span v-if="row.ratio != null" style="color:#666;">{{ row.ratio.toFixed(1) }}%</span>
|
||
<span v-else style="color:#ccc;">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<!-- ════ 报表2:预算执行报告 ════ -->
|
||
<el-tab-pane label="📋 预算执行报告" name="budget">
|
||
<div v-loading="loading">
|
||
<div class="report-desc">各KPI预算 vs 实际差异分析。正值=超预算,负值=节约。</div>
|
||
<div class="summary-row" style="margin-top:12px;">
|
||
<div class="stat-card blue"><div class="stat-val">{{ budgetSummary.total }}</div><div class="stat-label">KPI总数</div></div>
|
||
<div class="stat-card green"><div class="stat-val">{{ budgetSummary.with_budget }}</div><div class="stat-label">有预算</div></div>
|
||
<div class="stat-card red"><div class="stat-val">{{ budgetSummary.over_budget }}</div><div class="stat-label">超预算</div></div>
|
||
<div class="stat-card gray"><div class="stat-val">{{ budgetSummary.under_budget }}</div><div class="stat-label">节约</div></div>
|
||
<div class="stat-card yellow"><div class="stat-val">{{ budgetSummary.normal }}</div><div class="stat-label">正常</div></div>
|
||
</div>
|
||
<div style="display:flex;gap:8px;margin:12px 0;">
|
||
<el-select v-model="budgetDim" placeholder="维度" clearable size="small" style="width:120px;" @change="loadBudget">
|
||
<el-option v-for="d in dimensions" :key="d.value" :label="d.label" :value="d.value" />
|
||
</el-select>
|
||
<el-select v-model="budgetLevel" placeholder="状态" clearable size="small" style="width:120px;" @change="loadBudget">
|
||
<el-option label="超预算" value="red" />
|
||
<el-option label="预警" value="yellow" />
|
||
<el-option label="正常" value="normal" />
|
||
</el-select>
|
||
</div>
|
||
<el-table :data="budgetItems" border stripe size="small" style="width:100%;" @sort-change="onBudgetSort">
|
||
<el-table-column prop="kpi_name" label="KPI名称" min-width="150" sortable="custom" />
|
||
<el-table-column prop="dimension" label="维度" width="90">
|
||
<template #default="{ row }">{{ dimLabel(row.dimension) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="实际值" width="120" align="right" prop="actual_value" sortable="custom">
|
||
<template #default="{ row }">{{ row.actual_value != null ? row.actual_value.toLocaleString() : '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="预算值" width="120" align="right" prop="budget_value" sortable="custom">
|
||
<template #default="{ row }">{{ row.budget_value != null ? row.budget_value.toLocaleString() : '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="差异额" width="120" align="right" prop="deviation_amount" sortable="custom">
|
||
<template #default="{ row }">
|
||
<span :style="{ color: row.deviation_amount > 0 ? '#f56c6c' : row.deviation_amount < 0 ? '#67c23a' : '#999' }">
|
||
{{ row.deviation_amount != null ? (row.deviation_amount > 0 ? '+' : '') + row.deviation_amount.toLocaleString() : '-' }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="差异率" width="100" align="right" prop="deviation_rate" sortable="custom">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.deviation_rate != null" :type="row.alert_level === 'red' ? 'danger' : row.alert_level === 'yellow' ? 'warning' : 'success'" size="small">
|
||
{{ row.deviation_rate > 0 ? '+' : '' }}{{ row.deviation_rate.toFixed(1) }}%
|
||
</el-tag>
|
||
<span v-else style="color:#ccc;">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<!-- ════ 报表3:KPI趋势报告 ════ -->
|
||
<el-tab-pane label="📈 KPI趋势报告" name="trends">
|
||
<div v-loading="loading">
|
||
<div class="report-desc">选定KPI的历史趋势分析,含目标线标记。</div>
|
||
<div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap;">
|
||
<el-select v-model="trendDim" placeholder="维度" clearable size="small" style="width:120px;" @change="loadTrends">
|
||
<el-option v-for="d in dimensions" :key="d.value" :label="d.label" :value="d.value" />
|
||
</el-select>
|
||
<el-select v-model="trendKpiId" placeholder="选择KPI" clearable filterable size="small" style="width:200px;" @change="loadTrends">
|
||
<el-option v-for="k in kpiOptions" :key="k.id" :label="k.kpi_name" :value="k.id" />
|
||
</el-select>
|
||
<el-select v-model="trendMonths" size="small" style="width:100px;" @change="loadTrends">
|
||
<el-option label="12期" :value="12" />
|
||
<el-option label="24期" :value="24" />
|
||
<el-option label="30期" :value="30" />
|
||
</el-select>
|
||
</div>
|
||
<!-- 趋势图表 -->
|
||
<div v-if="trendData.length > 0" class="trend-grid">
|
||
<el-card v-for="t in trendData" :key="t.kpi_id" class="trend-card-2">
|
||
<template #header>
|
||
<div class="trend-hd">
|
||
<span class="trend-name">{{ t.kpi_name }}</span>
|
||
<span class="trend-unit">{{ t.unit }}</span>
|
||
<span class="trend-stat">
|
||
均值 {{ t.avg }} | 最高 {{ t.max }} | 最低 {{ t.min }}
|
||
</span>
|
||
<span class="trend-dir" :class="t.trend_dir">
|
||
{{ t.trend_dir === 'up' ? '↑ 上升' : t.trend_dir === 'down' ? '↓ 下降' : '→ 平稳' }}
|
||
</span>
|
||
</div>
|
||
</template>
|
||
<div :ref="el => setTrendChartRef(t.kpi_id, el)" style="height:180px;"></div>
|
||
</el-card>
|
||
</div>
|
||
<el-empty v-else description="请选择KPI查看趋势" />
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<!-- ════ 报表4:BSC评分卡 ════ -->
|
||
<el-tab-pane label="🎯 四维度绩效评分卡" name="bsc">
|
||
<div v-loading="loading">
|
||
<div class="report-desc">基于BSC框架的四维度绩效评分,从已发布战略地图构建。</div>
|
||
<div v-if="bscData.overall_score != null" style="margin-top:12px;">
|
||
<!-- 总分 -->
|
||
<div class="bsc-overall">
|
||
<div class="bsc-score-ring" :style="{ borderColor: bscScoreColor(bscData.overall_score) }">
|
||
<span class="bsc-score-val">{{ bscData.overall_score }}</span>
|
||
<span class="bsc-score-label">综合评分</span>
|
||
</div>
|
||
<div class="bsc-map-info" v-if="bscData.map_title">
|
||
基于:{{ bscData.map_title }} | 周期:{{ bscData.period }}
|
||
</div>
|
||
</div>
|
||
<!-- 四维度 -->
|
||
<div class="bsc-dims">
|
||
<div v-for="dim in bscData.dimensions" :key="dim.key" class="bsc-dim-card">
|
||
<div class="bsc-dim-head" :style="{ borderLeft: '4px solid ' + dim.color }">
|
||
<span class="bsc-dim-icon">{{ dim.icon }}</span>
|
||
<span class="bsc-dim-name">{{ dim.name }}</span>
|
||
<span class="bsc-dim-score" :style="{ color: bscScoreColor(dim.score) }">{{ dim.score }}</span>
|
||
</div>
|
||
<div class="bsc-dim-body">
|
||
<div v-for="obj in dim.objectives" :key="obj.name" class="bsc-obj-row">
|
||
<div class="bsc-obj-name">{{ obj.name }}</div>
|
||
<div class="bsc-obj-kpis" v-if="obj.kpis && obj.kpis.length > 0">
|
||
<div v-for="k in obj.kpis.slice(0, 3)" :key="k.code" class="bsc-kpi-mini">
|
||
<span class="bsc-kpi-dot" :class="'dot-' + k.level"></span>
|
||
<span class="bsc-kpi-name">{{ k.name }}</span>
|
||
<span class="bsc-kpi-score">{{ k.score ?? '-' }}</span>
|
||
</div>
|
||
<div v-if="obj.kpis.length > 3" class="bsc-kpi-more">+{{ obj.kpis.length - 3 }} 更多</div>
|
||
</div>
|
||
<div v-else class="bsc-obj-empty">暂无KPI数据</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<el-empty v-else description="暂无已发布的战略地图,请先在战略地图中发布" />
|
||
</div>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, onMounted, nextTick } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import axios from 'axios'
|
||
|
||
// ── 状态 ──
|
||
const loading = ref(false)
|
||
const activeTab = ref('profit')
|
||
const reportPeriod = ref('2026-06')
|
||
|
||
// API 配置
|
||
const api = axios.create({ baseURL: '/api/cma', timeout: 30000 })
|
||
api.interceptors.request.use((config: any) => {
|
||
const token = localStorage.getItem('cma_token')
|
||
if (token) config.headers.Authorization = `'Bearer ' + token`
|
||
return config
|
||
})
|
||
|
||
// ── 报表1:管理利润表 ──
|
||
const profitItems = ref<any[]>([])
|
||
|
||
function profitSummary(param: any) {
|
||
const sums: string[] = ['合计']
|
||
const last = profitItems.value[profitItems.value.length - 1]
|
||
if (last) {
|
||
sums.push(last.value != null ? '¥ ' + last.value.toLocaleString() : '-')
|
||
sums.push(last.prev_value != null ? '¥ ' + last.prev_value.toLocaleString() : '-')
|
||
sums.push(last.change_rate != null ? (last.change_rate > 0 ? '↑' : '↓') + Math.abs(last.change_rate).toFixed(1) + '%' : '-')
|
||
sums.push(last.ratio != null ? last.ratio.toFixed(1) + '%' : '-')
|
||
} else {
|
||
sums.push('-', '-', '-', '-')
|
||
}
|
||
return sums
|
||
}
|
||
|
||
// ── 报表2:预算执行报告 ──
|
||
const budgetSummary = ref({ total: 0, with_budget: 0, over_budget: 0, under_budget: 0, normal: 0 })
|
||
const budgetItems = ref<any[]>([])
|
||
const budgetDim = ref('')
|
||
const budgetLevel = ref('')
|
||
|
||
// ── 报表3:KPI趋势报告 ──
|
||
const trendDim = ref('')
|
||
const trendKpiId = ref<number | null>(null)
|
||
const trendMonths = ref(24)
|
||
const trendData = ref<any[]>([])
|
||
const kpiOptions = ref<any[]>([])
|
||
const trendChartRefs: Record<string, HTMLElement> = {}
|
||
|
||
function setTrendChartRef(id: number, el: any) {
|
||
if (el) trendChartRefs['chart-' + id] = el
|
||
}
|
||
|
||
// ── 报表4:BSC评分卡 ──
|
||
const bscData = ref<any>({})
|
||
|
||
// ── 公共 ──
|
||
const dimensions = [
|
||
{ value: 'finance', label: '财务维度' },
|
||
{ value: 'customer', label: '客户维度' },
|
||
{ value: 'process', label: '内部流程' },
|
||
{ value: 'learning', label: '学习成长' },
|
||
]
|
||
|
||
function dimLabel(d: string): string {
|
||
const map: Record<string, string> = { finance: '财务', customer: '客户', process: '内部流程', learning: '学习成长' }
|
||
return map[d] || d
|
||
}
|
||
|
||
function bscScoreColor(score: number): string {
|
||
if (score >= 80) return 'var(--bsc-process)'
|
||
if (score >= 60) return 'var(--bsc-learning)'
|
||
return '#f56c6c'
|
||
}
|
||
|
||
function onPeriodChange() {
|
||
loadAll()
|
||
}
|
||
|
||
// ── 加载 ──
|
||
async function loadProfit() {
|
||
try {
|
||
const r = await api.get('/reports/profit-summary', { params: { period: reportPeriod.value } })
|
||
profitItems.value = (r as any).data?.items || []
|
||
} catch { profitItems.value = [] }
|
||
}
|
||
|
||
async function loadBudget() {
|
||
try {
|
||
const params: any = { period: reportPeriod.value }
|
||
if (budgetDim.value) params.dimension = budgetDim.value
|
||
if (budgetLevel.value) params.alert_level = budgetLevel.value
|
||
const r = await api.get('/reports/budget-execution', { params })
|
||
const d = (r as any).data || {}
|
||
budgetSummary.value = d.summary || {}
|
||
budgetItems.value = d.items || []
|
||
} catch { budgetItems.value = [] }
|
||
}
|
||
|
||
async function loadTrends() {
|
||
try {
|
||
const params: any = { months: trendMonths.value }
|
||
if (trendKpiId.value) params.kpi_id = trendKpiId.value
|
||
else if (trendDim.value) params.dimension = trendDim.value
|
||
const r = await api.get('/reports/kpi-trends', { params })
|
||
trendData.value = (r as any).data?.data || []
|
||
nextTick(() => renderTrendCharts())
|
||
} catch { trendData.value = [] }
|
||
}
|
||
|
||
async function loadBsc() {
|
||
try {
|
||
const r = await api.get('/reports/bsc-scorecard', { params: { period: reportPeriod.value } })
|
||
bscData.value = (r as any).data || {}
|
||
} catch { bscData.value = {} }
|
||
}
|
||
|
||
async function loadKpiOptions() {
|
||
try {
|
||
const r = await api.get('/kpis', { params: { page_size: 100 } })
|
||
kpiOptions.value = (r as any).data?.data || []
|
||
} catch {}
|
||
}
|
||
|
||
function loadAll() {
|
||
loading.value = true
|
||
Promise.all([loadProfit(), loadBudget(), loadTrends(), loadBsc()]).finally(() => { loading.value = false })
|
||
}
|
||
|
||
function openDupont() {
|
||
window.open('/dupont-analysis', '_blank')
|
||
}
|
||
|
||
// ── ECharts 渲染 ──
|
||
function renderTrendCharts() {
|
||
import('echarts').then(echarts => {
|
||
trendData.value.forEach((t: any) => {
|
||
const el = trendChartRefs['chart-' + t.kpi_id]
|
||
if (!el) return
|
||
const existing = echarts.getInstanceByDom(el)
|
||
if (existing) existing.dispose()
|
||
const chart = echarts.init(el)
|
||
const dates = t.trend.map((d: any) => d.period)
|
||
const values = t.trend.map((d: any) => d.value)
|
||
const series: any[] = [{
|
||
type: 'line', data: values, smooth: true, lineStyle: { width: 2 },
|
||
areaStyle: { opacity: 0.1 }, symbol: 'circle', symbolSize: 4,
|
||
name: '实际值',
|
||
}]
|
||
// 目标线
|
||
if (t.target_value != null) {
|
||
series.push({
|
||
type: 'line', data: Array(dates.length).fill(t.target_value),
|
||
lineStyle: { type: 'dashed', width: 1, color: '#f56c6c' },
|
||
symbol: 'none', name: '目标值',
|
||
})
|
||
}
|
||
chart.setOption({
|
||
grid: { left: 50, right: 15, top: 10, bottom: 28 },
|
||
xAxis: { type: 'category', data: dates, axisLabel: { fontSize: 10, rotate: 45 } },
|
||
yAxis: { type: 'value', splitLine: { lineStyle: { type: 'dashed', color: '#eee' } } },
|
||
series,
|
||
tooltip: { trigger: 'axis' },
|
||
legend: { show: true, bottom: 0, icon: 'circle', itemWidth: 8, itemHeight: 8 },
|
||
})
|
||
})
|
||
})
|
||
}
|
||
|
||
// ── 排序 ──
|
||
function onBudgetSort(sort: any) {
|
||
if (!sort.prop || !sort.order) return
|
||
const items = [...budgetItems.value]
|
||
items.sort((a: any, b: any) => {
|
||
const av = a[sort.prop] ?? 0
|
||
const bv = b[sort.prop] ?? 0
|
||
return sort.order === 'ascending' ? av - bv : bv - av
|
||
})
|
||
budgetItems.value = items
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadKpiOptions()
|
||
loadAll()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.report-page { max-width: 1400px; margin: 0 auto; }
|
||
.report-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||
.header-right { display: flex; gap: 8px; align-items: center; }
|
||
.report-tabs { margin-bottom: 20px; }
|
||
.report-desc { font-size: 13px; color: #888; padding: 8px 0; }
|
||
|
||
/* 统计卡片 */
|
||
.summary-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; }
|
||
.stat-card { background: #fff; border-radius: 8px; padding: 12px 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); border-top: 3px solid #ccc; }
|
||
.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: 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; }
|
||
|
||
/* 趋势卡片 */
|
||
.trend-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 14px; margin-top: 12px; }
|
||
.trend-card-2 { border-radius: 10px; }
|
||
.trend-hd { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||
.trend-name { font-weight: 600; font-size: 14px; flex: 1; }
|
||
.trend-unit { color: #999; font-size: 12px; }
|
||
.trend-stat { font-size: 11px; color: #999; }
|
||
.trend-dir { font-size: 11px; padding: 2px 8px; border-radius: 4px; }
|
||
.trend-dir.up { background: #fff1f0; color: #cf1322; }
|
||
.trend-dir.down { background: #f6ffed; color: #389e0d; }
|
||
.trend-dir.stable { background: #f5f5f5; color: #999; }
|
||
|
||
/* BSC评分卡 */
|
||
.bsc-overall { text-align: center; padding: 20px 0; }
|
||
.bsc-score-ring {
|
||
display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
|
||
width: 120px; height: 120px; border-radius: 50%; border: 6px solid #67c23a;
|
||
background: #fff; box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
||
}
|
||
.bsc-score-val { font-size: 36px; font-weight: 700; color: #1a1a2e; line-height: 1.1; }
|
||
.bsc-score-label { font-size: 12px; color: #999; margin-top: 2px; }
|
||
.bsc-map-info { font-size: 12px; color: #999; margin-top: 8px; }
|
||
.bsc-dims { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 14px; margin-top: 16px; }
|
||
.bsc-dim-card { background: #fff; border-radius: 10px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); overflow: hidden; }
|
||
.bsc-dim-head { display: flex; align-items: center; gap: 8px; padding: 12px 14px; background: #fafafa; }
|
||
.bsc-dim-icon { font-size: 18px; }
|
||
.bsc-dim-name { font-weight: 600; font-size: 14px; flex: 1; }
|
||
.bsc-dim-score { font-size: 24px; font-weight: 700; }
|
||
.bsc-dim-body { padding: 10px 14px; }
|
||
.bsc-obj-row { margin-bottom: 10px; }
|
||
.bsc-obj-name { font-size: 13px; font-weight: 500; color: #555; margin-bottom: 4px; }
|
||
.bsc-obj-kpis { display: flex; flex-direction: column; gap: 3px; }
|
||
.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: 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; }
|
||
.bsc-kpi-score { font-weight: 600; color: #333; }
|
||
.bsc-kpi-more { font-size: 11px; color: #409eff; text-align: center; padding: 2px; }
|
||
.bsc-obj-empty { font-size: 11px; color: #ccc; text-align: center; padding: 4px; }
|
||
</style>
|