feat: 容量看板适配日/夜双主题(Naive/ECharts/CSS 变量随应用主题切换配色)

This commit is contained in:
2026-06-26 08:55:23 +08:00
parent a804c8c480
commit 118e05688b
2 changed files with 171 additions and 106 deletions
+8
View File
@@ -934,3 +934,11 @@ CellData 处理日志细化(`app/services/cell_data.py`):
- **制式分布排除「未知」**:`group_by` 增 `skip_unknown` 参数(`WHERE 制式 IS NOT NULL AND <> ''`),制式分布只剩 TDD/FDD。
- 行间距:统一用 flex `gap:12px`,卡片/图表 panel 内边距收紧,避免叠压。图表高度 `clamp(108px,13vh,160px)` 适配矮窗口。
- 验证:`frontend` `npm run build`(vue-tsc+vite) 通过;后端 `py_compile` 通过;4G/5G 的 avg_ul/total_flow、制式排除空值、上下行直方均经 MCP 对真实数据验证。
## 2026-06-26:容量看板适配日/夜双主题
- 跟随应用主题:复用全局 `composables/theme.ts` 的 `themeName`(`light|dark`,写 `documentElement[data-theme]`)。看板 `isLight = computed(themeName==='light')`。
- Naive 组件:`n-config-provider :theme` 绑定 `lightTheme|darkTheme`;`:theme-overrides` 绑定 `naiveLight|naiveDark`(两套 DataTable/Input/Pagination/Drawer/Empty 覆盖,浅色强调色用 `#0891b2`、深色用 `#22d3ee`)。
- ECharts:新增 `chartPalette` computed(轴文字/轴线/分割线/tooltip 底色与文字/图例/饼描边/柱底色 两套值),所有 option 通过 `axisLine/axisLabel/splitLine/tooltipBase/legendStyle` 辅助函数读取,主题切换时 computed 重算、`EChart.vue` 深度 watch `setOption(opt,true)` 自动重绘。
- CSS:硬编码颜色抽成 `--cap-*` 变量(bg/panel/card/border/shadow/text 系列/soft-bg/tag 等),**定义在 `:global([data-theme='dark'])` 与 `:global([data-theme='light'])`(documentElement)** 上——这样 teleport 到 body 的详情抽屉也能继承同一套变量。浅色:白底玻璃卡片 + 柔和阴影 + 深色文字;深色:原科技风径向辉光。强调色(青/紫/玫红/琥珀)与 `.cap-spark`/分段激活态在两主题保持一致。卡片 icon/底色条改用更深的实色(#06b6d4/#f43f5e 等)以保证浅色下对比度。
- 验证:`frontend` `npm run build`(vue-tsc+vite) 通过。
+163 -106
View File
@@ -1,5 +1,5 @@
<template>
<n-config-provider :theme="darkTheme" :theme-overrides="naiveDark">
<n-config-provider :theme="naiveTheme" :theme-overrides="naiveThemeOverrides">
<div class="cap-dashboard">
<!-- 加载中 -->
<div v-if="loadingStatus" class="cap-center">
@@ -208,7 +208,7 @@
<script setup lang="ts">
import { computed, h, onMounted, reactive, ref, type Component } from 'vue';
import { NTag, darkTheme, useMessage } from 'naive-ui';
import { NTag, darkTheme, lightTheme, useMessage } from 'naive-ui';
import type { DataTableColumns } from 'naive-ui';
import type { EChartsOption } from 'echarts';
import {
@@ -226,6 +226,7 @@ import {
} from '@vicons/ionicons5';
import { apiGet, downloadGet } from '../api/client';
import { themeName } from '../composables/theme';
import EChart from './EChart.vue';
type Rat = '4g' | '5g';
@@ -255,50 +256,58 @@ interface CellDetail {
const message = useMessage();
// 强制看板内 Naive 组件走深色配色,统一科技感主题
// ---------- 主题(跟随应用日/夜切换) ----------
const isLight = computed(() => themeName.value === 'light');
const naiveTheme = computed(() => (isLight.value ? lightTheme : darkTheme));
// 看板内 Naive 组件配色覆盖:青色强调色 + 对应明暗底色
const naiveDark = {
common: {
primaryColor: '#22d3ee',
primaryColorHover: '#38bdf8',
primaryColorPressed: '#0891b2',
primaryColorSuppl: '#38bdf8',
borderRadius: '8px'
},
common: { primaryColor: '#22d3ee', primaryColorHover: '#38bdf8', primaryColorPressed: '#0891b2', primaryColorSuppl: '#38bdf8', borderRadius: '8px' },
DataTable: {
thColor: 'rgba(30, 41, 59, 0.85)',
thColorHover: 'rgba(30, 41, 59, 0.95)',
thTextColor: '#cbd5e1',
thFontWeight: '600',
tdColor: 'transparent',
tdColorHover: 'rgba(56, 189, 248, 0.08)',
tdTextColor: '#dbe4f0',
borderColor: 'rgba(148, 163, 184, 0.12)',
loadingColor: 'rgba(8, 14, 26, 0.6)'
thColor: 'rgba(30, 41, 59, 0.85)', thColorHover: 'rgba(30, 41, 59, 0.95)', thTextColor: '#cbd5e1', thFontWeight: '600',
tdColor: 'transparent', tdColorHover: 'rgba(56, 189, 248, 0.08)', tdTextColor: '#dbe4f0',
borderColor: 'rgba(148, 163, 184, 0.12)', loadingColor: 'rgba(8, 14, 26, 0.6)'
},
Input: {
color: 'rgba(15, 23, 42, 0.45)',
colorFocus: 'rgba(15, 23, 42, 0.7)',
border: '1px solid rgba(148, 163, 184, 0.18)',
borderHover: '1px solid rgba(56, 189, 248, 0.5)',
borderFocus: '1px solid rgba(56, 189, 248, 0.7)',
boxShadowFocus: '0 0 0 2px rgba(56, 189, 248, 0.18)',
textColor: '#e2e8f0',
placeholderColor: '#64748b'
color: 'rgba(15, 23, 42, 0.45)', colorFocus: 'rgba(15, 23, 42, 0.7)',
border: '1px solid rgba(148, 163, 184, 0.18)', borderHover: '1px solid rgba(56, 189, 248, 0.5)', borderFocus: '1px solid rgba(56, 189, 248, 0.7)',
boxShadowFocus: '0 0 0 2px rgba(56, 189, 248, 0.18)', textColor: '#e2e8f0', placeholderColor: '#64748b'
},
Pagination: {
itemColor: 'rgba(15, 23, 42, 0.4)',
itemColorHover: 'rgba(56, 189, 248, 0.12)',
itemColorPressed: 'rgba(56, 189, 248, 0.18)',
itemColorActive: 'rgba(56, 189, 248, 0.16)',
itemBorder: '1px solid rgba(148, 163, 184, 0.16)',
itemBorderActive: '1px solid #22d3ee',
itemTextColor: '#94a3b8',
itemTextColorHover: '#e2e8f0',
itemTextColorActive: '#22d3ee'
itemColor: 'rgba(15, 23, 42, 0.4)', itemColorHover: 'rgba(56, 189, 248, 0.12)', itemColorPressed: 'rgba(56, 189, 248, 0.18)', itemColorActive: 'rgba(56, 189, 248, 0.16)',
itemBorder: '1px solid rgba(148, 163, 184, 0.16)', itemBorderActive: '1px solid #22d3ee',
itemTextColor: '#94a3b8', itemTextColorHover: '#e2e8f0', itemTextColorActive: '#22d3ee'
},
Drawer: { color: '#0c1422' },
Empty: { textColor: '#64748b', iconColor: '#475569' }
};
const naiveLight = {
common: { primaryColor: '#0891b2', primaryColorHover: '#06b6d4', primaryColorPressed: '#0e7490', primaryColorSuppl: '#06b6d4', borderRadius: '8px' },
DataTable: {
thColor: 'rgba(241, 245, 249, 0.9)', thColorHover: 'rgba(226, 232, 240, 0.95)', thTextColor: '#475569', thFontWeight: '600',
tdColor: 'transparent', tdColorHover: 'rgba(8, 145, 178, 0.07)', tdTextColor: '#334155',
borderColor: 'rgba(15, 23, 42, 0.08)', loadingColor: 'rgba(255, 255, 255, 0.6)'
},
Input: {
color: 'rgba(255, 255, 255, 0.85)', colorFocus: '#ffffff',
border: '1px solid rgba(15, 23, 42, 0.12)', borderHover: '1px solid rgba(8, 145, 178, 0.5)', borderFocus: '1px solid rgba(8, 145, 178, 0.7)',
boxShadowFocus: '0 0 0 2px rgba(8, 145, 178, 0.15)', textColor: '#1e293b', placeholderColor: '#94a3b8'
},
Pagination: {
itemColor: 'rgba(255, 255, 255, 0.7)', itemColorHover: 'rgba(8, 145, 178, 0.08)', itemColorPressed: 'rgba(8, 145, 178, 0.14)', itemColorActive: 'rgba(8, 145, 178, 0.12)',
itemBorder: '1px solid rgba(15, 23, 42, 0.12)', itemBorderActive: '1px solid #0891b2',
itemTextColor: '#64748b', itemTextColorHover: '#1e293b', itemTextColorActive: '#0891b2'
},
Drawer: { color: '#ffffff' },
Empty: { textColor: '#94a3b8', iconColor: '#cbd5e1' }
};
const naiveThemeOverrides = computed(() => (isLight.value ? naiveLight : naiveDark));
// ECharts 明暗配色
const chartPalette = computed(() => (isLight.value
? { axisText: '#64748b', axisLine: 'rgba(15,23,42,0.15)', splitLine: 'rgba(15,23,42,0.06)', tooltipBg: 'rgba(255,255,255,0.97)', tooltipText: '#1e293b', tooltipBorder: 'rgba(8,145,178,0.4)', legend: '#64748b', pieBorder: '#ffffff', barTrack: 'rgba(8,145,178,0.18)' }
: { axisText: '#94a3b8', axisLine: 'rgba(148,163,184,0.25)', splitLine: 'rgba(148,163,184,0.12)', tooltipBg: 'rgba(15,23,42,0.92)', tooltipText: '#e2e8f0', tooltipBorder: 'rgba(56,189,248,0.35)', legend: '#94a3b8', pieBorder: 'rgba(2,6,23,0.6)', barTrack: 'rgba(56,189,248,0.18)' }
));
const chartHeight = 'clamp(108px, 13vh, 160px)';
@@ -340,7 +349,7 @@ const PROBLEM_COLOR: Record<string, string> = {
高负荷: '#fb7185',
高流量预警: '#fbbf24',
利用率预警: '#38bdf8',
正常: '#475569'
正常: '#94a3b8'
};
// ---------- 汇总卡片(含数字滚动) ----------
@@ -415,19 +424,26 @@ function baseGrid(): EChartsOption['grid'] {
return { left: 8, right: 16, top: 24, bottom: 6, containLabel: true };
}
function axisLine() {
return { lineStyle: { color: 'rgba(148,163,184,0.25)' } };
return { lineStyle: { color: chartPalette.value.axisLine } };
}
function axisLabel() {
return { color: '#94a3b8', fontSize: 11 };
return { color: chartPalette.value.axisText, fontSize: 11 };
}
function splitLine() {
return { lineStyle: { color: chartPalette.value.splitLine } };
}
function tooltipBase(): EChartsOption['tooltip'] {
const p = chartPalette.value;
return {
backgroundColor: 'rgba(15,23,42,0.92)',
borderColor: 'rgba(56,189,248,0.35)',
backgroundColor: p.tooltipBg,
borderColor: p.tooltipBorder,
borderWidth: 1,
textStyle: { color: '#e2e8f0', fontSize: 12 }
textStyle: { color: p.tooltipText, fontSize: 12 }
};
}
function legendStyle() {
return { color: chartPalette.value.legend, fontSize: 11 };
}
const problemOption = computed<EChartsOption>(() => {
const data = (overview.value?.problem_pie || []).map(d => ({
@@ -436,10 +452,10 @@ const problemOption = computed<EChartsOption>(() => {
}));
return {
tooltip: { ...tooltipBase(), trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: { bottom: 0, textStyle: { color: '#94a3b8', fontSize: 11 }, icon: 'circle', itemWidth: 9, itemHeight: 9 },
legend: { bottom: 0, textStyle: legendStyle(), icon: 'circle', itemWidth: 9, itemHeight: 9 },
series: [{
type: 'pie', radius: ['46%', '72%'], center: ['50%', '42%'], avoidLabelOverlap: true,
itemStyle: { borderColor: 'rgba(2,6,23,0.6)', borderWidth: 2 },
itemStyle: { borderColor: chartPalette.value.pieBorder, borderWidth: 2 },
label: { show: false }, labelLine: { show: false },
data
}]
@@ -451,7 +467,7 @@ function makeHist(buckets: HistBucket[], c0: string, c1: string): EChartsOption
tooltip: { ...tooltipBase(), trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: baseGrid(),
xAxis: { type: 'category', data: buckets.map(b => b.bucket), axisLine: axisLine(), axisLabel: { ...axisLabel(), rotate: 30 }, axisTick: { show: false } },
yAxis: { type: 'value', splitLine: { lineStyle: { color: 'rgba(148,163,184,0.12)' } }, axisLabel: axisLabel() },
yAxis: { type: 'value', splitLine: splitLine(), axisLabel: axisLabel() },
series: [{
type: 'bar', barWidth: '58%', data: buckets.map(b => b.value),
itemStyle: {
@@ -468,12 +484,12 @@ const systemOption = computed<EChartsOption>(() => {
const stats = overview.value?.by_system || [];
return {
tooltip: { ...tooltipBase(), trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: { top: 0, right: 0, textStyle: { color: '#94a3b8', fontSize: 11 }, itemWidth: 10, itemHeight: 10 },
legend: { top: 0, right: 0, textStyle: legendStyle(), itemWidth: 10, itemHeight: 10 },
grid: baseGrid(),
xAxis: { type: 'category', data: stats.map(s => s.name), axisLine: axisLine(), axisLabel: { ...axisLabel(), interval: 0, rotate: stats.length > 5 ? 24 : 0 }, axisTick: { show: false } },
yAxis: { type: 'value', splitLine: { lineStyle: { color: 'rgba(148,163,184,0.12)' } }, axisLabel: axisLabel() },
yAxis: { type: 'value', splitLine: splitLine(), axisLabel: axisLabel() },
series: [
{ name: '总数', type: 'bar', barGap: '-100%', barWidth: '46%', itemStyle: { borderRadius: [3, 3, 0, 0], color: 'rgba(56,189,248,0.18)' }, data: stats.map(s => s.total) },
{ name: '总数', type: 'bar', barGap: '-100%', barWidth: '46%', itemStyle: { borderRadius: [3, 3, 0, 0], color: chartPalette.value.barTrack }, data: stats.map(s => s.total) },
{ name: '高负荷', type: 'bar', barWidth: '46%', itemStyle: { borderRadius: [3, 3, 0, 0], color: '#fb7185' }, data: stats.map(s => s.high) }
]
};
@@ -483,10 +499,10 @@ const stationOption = computed<EChartsOption>(() => {
const data = (overview.value?.by_station || []).map((s, i) => ({ name: s.name, value: s.total, itemStyle: { color: PALETTE[i % PALETTE.length] } }));
return {
tooltip: { ...tooltipBase(), trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: { bottom: 0, textStyle: { color: '#94a3b8', fontSize: 11 }, icon: 'circle', itemWidth: 9, itemHeight: 9 },
legend: { bottom: 0, textStyle: legendStyle(), icon: 'circle', itemWidth: 9, itemHeight: 9 },
series: [{
type: 'pie', radius: ['42%', '70%'], center: ['50%', '42%'],
itemStyle: { borderColor: 'rgba(2,6,23,0.6)', borderWidth: 2 },
itemStyle: { borderColor: chartPalette.value.pieBorder, borderWidth: 2 },
label: { show: false }, labelLine: { show: false }, data
}]
};
@@ -497,7 +513,7 @@ const freqOption = computed<EChartsOption>(() => {
return {
tooltip: { ...tooltipBase(), trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: { left: 8, right: 24, top: 10, bottom: 6, containLabel: true },
xAxis: { type: 'value', splitLine: { lineStyle: { color: 'rgba(148,163,184,0.12)' } }, axisLabel: axisLabel() },
xAxis: { type: 'value', splitLine: splitLine(), axisLabel: axisLabel() },
yAxis: { type: 'category', data: stats.map(s => s.name), axisLine: axisLine(), axisLabel: axisLabel(), axisTick: { show: false } },
series: [{
type: 'bar', barWidth: '56%', data: stats.map(s => s.flagged),
@@ -529,7 +545,7 @@ const columns = computed<DataTableColumns<CellRow>>(() => [
function tagColor(problem: string) {
const c = PROBLEM_COLOR[problem] || '#64748b';
return { color: 'rgba(148,163,184,0.12)', textColor: c, borderColor: 'transparent' };
return { color: isLight.value ? 'rgba(100,116,139,0.1)' : 'rgba(148,163,184,0.14)', textColor: c, borderColor: 'transparent' };
}
function problemClass(problem: string) {
if (problem === '高负荷') return 'is-rose';
@@ -671,6 +687,48 @@ onMounted(loadStatus);
</script>
<style scoped>
/* 主题变量定义在 documentElement 上,使 teleport 到 body 的详情抽屉也能继承 */
:global([data-theme='dark']) {
--cap-bg:
radial-gradient(1200px 600px at 12% -10%, rgba(56, 189, 248, 0.12), transparent 60%),
radial-gradient(1000px 600px at 100% 0%, rgba(129, 140, 248, 0.12), transparent 55%),
linear-gradient(180deg, #0b1220 0%, #070b15 100%);
--cap-panel-bg: linear-gradient(160deg, rgba(30, 41, 59, 0.5), rgba(15, 23, 42, 0.32));
--cap-card-bg: linear-gradient(160deg, rgba(30, 41, 59, 0.55), rgba(15, 23, 42, 0.35));
--cap-panel-border: rgba(148, 163, 184, 0.14);
--cap-shadow: none;
--cap-icon-bg: rgba(148, 163, 184, 0.1);
--cap-control-bg: rgba(15, 23, 42, 0.6);
--cap-control-border: rgba(148, 163, 184, 0.16);
--cap-text: #e2e8f0;
--cap-text-strong: #f1f5f9;
--cap-text-sub: #94a3b8;
--cap-text-muted: #64748b;
--cap-soft-bg: rgba(148, 163, 184, 0.08);
--cap-tag-plain-bg: rgba(148, 163, 184, 0.16);
--cap-glow-opacity: 0.5;
}
:global([data-theme='light']) {
--cap-bg:
radial-gradient(1200px 600px at 12% -10%, rgba(56, 189, 248, 0.16), transparent 60%),
radial-gradient(1000px 600px at 100% 0%, rgba(129, 140, 248, 0.14), transparent 55%),
linear-gradient(180deg, #eef2f9 0%, #e3e9f3 100%);
--cap-panel-bg: rgba(255, 255, 255, 0.82);
--cap-card-bg: rgba(255, 255, 255, 0.9);
--cap-panel-border: rgba(15, 23, 42, 0.08);
--cap-shadow: 0 6px 20px rgba(15, 23, 42, 0.06);
--cap-icon-bg: rgba(15, 23, 42, 0.05);
--cap-control-bg: rgba(255, 255, 255, 0.7);
--cap-control-border: rgba(15, 23, 42, 0.1);
--cap-text: #334155;
--cap-text-strong: #0f172a;
--cap-text-sub: #64748b;
--cap-text-muted: #94a3b8;
--cap-soft-bg: rgba(15, 23, 42, 0.04);
--cap-tag-plain-bg: rgba(15, 23, 42, 0.06);
--cap-glow-opacity: 0.32;
}
.cap-dashboard {
position: relative;
height: calc(100vh - 64px);
@@ -679,11 +737,8 @@ onMounted(loadStatus);
flex-direction: column;
overflow: hidden;
padding: 14px 18px;
color: #e2e8f0;
background:
radial-gradient(1200px 600px at 12% -10%, rgba(56, 189, 248, 0.12), transparent 60%),
radial-gradient(1000px 600px at 100% 0%, rgba(129, 140, 248, 0.12), transparent 55%),
linear-gradient(180deg, #0b1220 0%, #070b15 100%);
color: var(--cap-text);
background: var(--cap-bg);
}
.cap-center {
@@ -709,14 +764,14 @@ onMounted(loadStatus);
font-size: 22px;
font-weight: 700;
letter-spacing: 2px;
color: #e2e8f0;
color: var(--cap-text-strong);
}
.cap-empty-sub {
margin: 0;
max-width: 460px;
text-align: center;
font-size: 13px;
color: #64748b;
color: var(--cap-text-muted);
}
@keyframes pulse {
@@ -740,16 +795,16 @@ onMounted(loadStatus);
background: linear-gradient(180deg, #22d3ee, #818cf8);
box-shadow: 0 0 16px rgba(56, 189, 248, 0.7);
}
.cap-title { margin: 0; font-size: 19px; font-weight: 700; letter-spacing: 1px; color: #f1f5f9; }
.cap-sub { font-size: 12px; color: #64748b; }
.cap-title { margin: 0; font-size: 19px; font-weight: 700; letter-spacing: 1px; color: var(--cap-text-strong); }
.cap-sub { font-size: 12px; color: var(--cap-text-muted); }
.cap-controls { display: flex; align-items: center; gap: 12px; }
.cap-seg {
display: inline-flex;
padding: 3px;
border-radius: 10px;
background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(148, 163, 184, 0.16);
background: var(--cap-control-bg);
border: 1px solid var(--cap-control-border);
}
.cap-seg.sm { padding: 2px; border-radius: 8px; }
.cap-seg-btn {
@@ -757,14 +812,14 @@ onMounted(loadStatus);
border: 0;
border-radius: 8px;
background: transparent;
color: #94a3b8;
color: var(--cap-text-sub);
font: inherit;
font-weight: 600;
cursor: pointer;
transition: all 0.18s ease;
}
.cap-seg.sm .cap-seg-btn { padding: 4px 12px; font-size: 12px; }
.cap-seg-btn:hover { color: #e2e8f0; }
.cap-seg-btn:hover { color: var(--cap-text-strong); }
.cap-seg-btn.active {
color: #061018;
background: linear-gradient(135deg, #22d3ee, #38bdf8);
@@ -773,12 +828,12 @@ onMounted(loadStatus);
.cap-refresh {
display: inline-flex; align-items: center; justify-content: center;
width: 32px; height: 32px; border-radius: 9px;
border: 1px solid rgba(148, 163, 184, 0.16);
background: rgba(15, 23, 42, 0.6);
color: #94a3b8; cursor: pointer; font-size: 16px;
border: 1px solid var(--cap-control-border);
background: var(--cap-control-bg);
color: var(--cap-text-sub); cursor: pointer; font-size: 16px;
transition: all 0.18s ease;
}
.cap-refresh:hover:not(:disabled) { color: #22d3ee; border-color: rgba(56, 189, 248, 0.5); box-shadow: 0 0 16px rgba(56, 189, 248, 0.3); }
.cap-refresh:hover:not(:disabled) { color: #06b6d4; border-color: rgba(56, 189, 248, 0.5); box-shadow: 0 0 16px rgba(56, 189, 248, 0.3); }
.cap-refresh:disabled { opacity: 0.5; cursor: wait; }
/* 概览区(卡片 + 图表),固定高度,由 n-spin 包裹 */
@@ -798,8 +853,9 @@ onMounted(loadStatus);
gap: 10px;
padding: 10px 12px;
border-radius: 12px;
background: linear-gradient(160deg, rgba(30, 41, 59, 0.55), rgba(15, 23, 42, 0.35));
border: 1px solid rgba(148, 163, 184, 0.14);
background: var(--cap-card-bg);
border: 1px solid var(--cap-panel-border);
box-shadow: var(--cap-shadow);
overflow: hidden;
backdrop-filter: blur(8px);
animation: cardIn 0.5s ease both;
@@ -809,22 +865,22 @@ onMounted(loadStatus);
display: flex; align-items: center; justify-content: center;
width: 34px; height: 34px; flex: 0 0 auto;
border-radius: 9px; font-size: 18px;
background: rgba(148, 163, 184, 0.1);
background: var(--cap-icon-bg);
}
.cap-card-body { min-width: 0; }
.cap-card-value { font-size: 19px; font-weight: 800; line-height: 1.15; color: #f8fafc; font-variant-numeric: tabular-nums; white-space: nowrap; }
.cap-card-unit { font-size: 11px; font-weight: 600; margin-left: 2px; color: #94a3b8; }
.cap-card-label { margin-top: 2px; font-size: 11px; color: #94a3b8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cap-card-glow { position: absolute; right: -30px; top: -30px; width: 90px; height: 90px; border-radius: 50%; opacity: 0.5; filter: blur(8px); }
.tone-cyan .cap-card-icon { color: #22d3ee; } .tone-cyan .cap-card-glow { background: rgba(34, 211, 238, 0.35); }
.tone-rose .cap-card-icon { color: #fb7185; } .tone-rose .cap-card-glow { background: rgba(251, 113, 133, 0.35); }
.tone-blue .cap-card-icon { color: #38bdf8; } .tone-blue .cap-card-glow { background: rgba(56, 189, 248, 0.32); }
.tone-amber .cap-card-icon { color: #fbbf24; } .tone-amber .cap-card-glow { background: rgba(251, 191, 36, 0.3); }
.tone-violet .cap-card-icon { color: #c084fc; } .tone-violet .cap-card-glow { background: rgba(192, 132, 252, 0.3); }
.tone-emerald .cap-card-icon { color: #34d399; } .tone-emerald .cap-card-glow { background: rgba(52, 211, 153, 0.3); }
.cap-card::after { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: currentColor; opacity: 0.5; }
.tone-cyan::after { color: #22d3ee; } .tone-rose::after { color: #fb7185; } .tone-blue::after { color: #38bdf8; }
.tone-amber::after { color: #fbbf24; } .tone-violet::after { color: #c084fc; } .tone-emerald::after { color: #34d399; }
.cap-card-value { font-size: 19px; font-weight: 800; line-height: 1.15; color: var(--cap-text-strong); font-variant-numeric: tabular-nums; white-space: nowrap; }
.cap-card-unit { font-size: 11px; font-weight: 600; margin-left: 2px; color: var(--cap-text-sub); }
.cap-card-label { margin-top: 2px; font-size: 11px; color: var(--cap-text-sub); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cap-card-glow { position: absolute; right: -30px; top: -30px; width: 90px; height: 90px; border-radius: 50%; opacity: var(--cap-glow-opacity); filter: blur(8px); }
.tone-cyan .cap-card-icon { color: #06b6d4; } .tone-cyan .cap-card-glow { background: rgba(34, 211, 238, 0.35); }
.tone-rose .cap-card-icon { color: #f43f5e; } .tone-rose .cap-card-glow { background: rgba(251, 113, 133, 0.35); }
.tone-blue .cap-card-icon { color: #0ea5e9; } .tone-blue .cap-card-glow { background: rgba(56, 189, 248, 0.32); }
.tone-amber .cap-card-icon { color: #f59e0b; } .tone-amber .cap-card-glow { background: rgba(251, 191, 36, 0.3); }
.tone-violet .cap-card-icon { color: #8b5cf6; } .tone-violet .cap-card-glow { background: rgba(192, 132, 252, 0.3); }
.tone-emerald .cap-card-icon { color: #10b981; } .tone-emerald .cap-card-glow { background: rgba(52, 211, 153, 0.3); }
.cap-card::after { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: currentColor; opacity: 0.55; }
.tone-cyan::after { color: #06b6d4; } .tone-rose::after { color: #f43f5e; } .tone-blue::after { color: #0ea5e9; }
.tone-amber::after { color: #f59e0b; } .tone-violet::after { color: #8b5cf6; } .tone-emerald::after { color: #10b981; }
/* 图表区:两行三列 */
.cap-charts {
@@ -835,13 +891,14 @@ onMounted(loadStatus);
.cap-panel {
padding: 10px 14px;
border-radius: 12px;
background: linear-gradient(160deg, rgba(30, 41, 59, 0.5), rgba(15, 23, 42, 0.32));
border: 1px solid rgba(148, 163, 184, 0.14);
background: var(--cap-panel-bg);
border: 1px solid var(--cap-panel-border);
box-shadow: var(--cap-shadow);
backdrop-filter: blur(8px);
}
.cap-panel-head {
display: flex; align-items: center; gap: 8px;
margin-bottom: 4px; font-size: 13px; font-weight: 600; color: #cbd5e1;
margin-bottom: 4px; font-size: 13px; font-weight: 600; color: var(--cap-text);
}
.cap-panel-head.sm { font-size: 12px; margin: 14px 0 8px; }
.cap-panel-head .bar { width: 4px; height: 13px; border-radius: 2px; background: linear-gradient(180deg, #22d3ee, #818cf8); }
@@ -855,34 +912,34 @@ onMounted(loadStatus);
.cap-table-wrap { flex: 1 1 0; min-height: 0; }
.cap-table { height: 100%; background: transparent; }
.cap-pager { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px; }
.cap-pager-total { font-size: 12px; color: #64748b; }
.cap-pager-total { font-size: 12px; color: var(--cap-text-muted); }
/* 详情抽屉(强制深色,避免跟随应用浅色主题) */
/* 详情抽屉 */
.cap-detail-head { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
.cap-detail-id { font-weight: 700; font-size: 15px; color: #f1f5f9; }
.cap-detail-name { font-size: 12px; color: #94a3b8; overflow: hidden; text-overflow: ellipsis; }
.cap-detail-id { font-weight: 700; font-size: 15px; color: var(--cap-text-strong); }
.cap-detail-name { font-size: 12px; color: var(--cap-text-sub); overflow: hidden; text-overflow: ellipsis; }
.cap-detail { display: flex; flex-direction: column; gap: 16px; }
.cap-detail-tags { display: flex; flex-wrap: wrap; gap: 8px; }
.cap-tag { padding: 3px 12px; border-radius: 999px; font-size: 12px; font-weight: 600; }
.cap-tag.xs { padding: 1px 8px; font-size: 11px; }
.cap-tag.plain { background: rgba(148, 163, 184, 0.16); color: #94a3b8; }
.cap-tag.is-rose { background: rgba(251, 113, 133, 0.18); color: #fb7185; }
.cap-tag.is-amber { background: rgba(251, 191, 36, 0.18); color: #fbbf24; }
.cap-tag.is-blue { background: rgba(56, 189, 248, 0.18); color: #38bdf8; }
.cap-tag.is-slate { background: rgba(100, 116, 139, 0.18); color: #94a3b8; }
.cap-tag.plain { background: var(--cap-tag-plain-bg); color: var(--cap-text-sub); }
.cap-tag.is-rose { background: rgba(251, 113, 133, 0.18); color: #f43f5e; }
.cap-tag.is-amber { background: rgba(251, 191, 36, 0.18); color: #d97706; }
.cap-tag.is-blue { background: rgba(56, 189, 248, 0.18); color: #0284c7; }
.cap-tag.is-slate { background: rgba(100, 116, 139, 0.18); color: #64748b; }
.cap-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.cap-metric { padding: 10px 12px; border-radius: 10px; background: rgba(148, 163, 184, 0.08); border: 1px solid rgba(148, 163, 184, 0.16); }
.cap-metric-label { font-size: 12px; color: #94a3b8; }
.cap-metric-value { margin-top: 4px; font-size: 16px; font-weight: 700; color: #f1f5f9; }
.cap-metric { padding: 10px 12px; border-radius: 10px; background: var(--cap-soft-bg); border: 1px solid var(--cap-panel-border); }
.cap-metric-label { font-size: 12px; color: var(--cap-text-sub); }
.cap-metric-value { margin-top: 4px; font-size: 16px; font-weight: 700; color: var(--cap-text-strong); }
.cap-suggest { padding: 12px 14px; border-radius: 12px; background: rgba(251, 191, 36, 0.1); border: 1px solid rgba(251, 191, 36, 0.32); }
.cap-suggest-head { display: flex; align-items: center; gap: 6px; font-weight: 700; color: #fbbf24; margin-bottom: 6px; }
.cap-suggest-text { margin: 0; font-size: 13px; line-height: 1.7; color: #e2e8f0; }
.cap-suggest-head { display: flex; align-items: center; gap: 6px; font-weight: 700; color: #d97706; margin-bottom: 6px; }
.cap-suggest-text { margin: 0; font-size: 13px; line-height: 1.7; color: var(--cap-text); }
.cap-sib-list { display: flex; flex-direction: column; gap: 8px; }
.cap-sib-item { padding: 10px 12px; border-radius: 10px; border: 1px solid rgba(148, 163, 184, 0.16); background: rgba(148, 163, 184, 0.05); }
.cap-sib-item { padding: 10px 12px; border-radius: 10px; border: 1px solid var(--cap-panel-border); background: var(--cap-soft-bg); }
.cap-sib-main { display: flex; align-items: baseline; gap: 8px; }
.cap-sib-id { font-weight: 600; font-size: 13px; color: #e2e8f0; }
.cap-sib-name { font-size: 12px; color: #94a3b8; overflow: hidden; text-overflow: ellipsis; }
.cap-sib-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 6px; font-size: 12px; color: #94a3b8; }
.cap-sib-id { font-weight: 600; font-size: 13px; color: var(--cap-text); }
.cap-sib-name { font-size: 12px; color: var(--cap-text-sub); overflow: hidden; text-overflow: ellipsis; }
.cap-sib-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 6px; font-size: 12px; color: var(--cap-text-sub); }
@media (max-width: 1366px) {
.cap-card-value { font-size: 17px; }