feat: 容量看板配色统一、整页不滚动布局并新增问题小区清单导出
This commit is contained in:
@@ -5,11 +5,17 @@
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
from starlette.background import BackgroundTask
|
||||
|
||||
from app import state
|
||||
from app.config import CACHE_DIR
|
||||
from app.utils.files import remove_file_safely
|
||||
from app.warehouse import make_warehouse
|
||||
|
||||
router = APIRouter(tags=["dashboard"])
|
||||
@@ -228,6 +234,57 @@ def dashboard_cells(
|
||||
return {"items": items, "total": total, "page": page, "page_size": page_size}
|
||||
|
||||
|
||||
@router.get("/api/dashboard/export")
|
||||
def dashboard_export(rat: str = Query("4g"), problem: str = Query(""), keyword: str = Query("")):
|
||||
cfg = _rat(rat)
|
||||
db = _db()
|
||||
if cfg["table"].lower() not in _existing_tables(db):
|
||||
raise HTTPException(status_code=404, detail=f"结果表 {cfg['table']} 不存在,请先进行数据处理")
|
||||
|
||||
t = f"`{cfg['table']}`"
|
||||
ul, dl, flow, users = f"`{cfg['ul']}`", f"`{cfg['dl']}`", f"`{FLOW}`", f"`{cfg['users']}`"
|
||||
wheres = ["`高负荷问题` IS NOT NULL"]
|
||||
if problem in PROBLEMS:
|
||||
wheres = [f"`高负荷问题`='{problem}'"]
|
||||
if keyword.strip():
|
||||
kw = _esc(keyword.strip())
|
||||
wheres.append(f"(`{cfg['id']}` LIKE '%{kw}%' OR `{cfg['name']}` LIKE '%{kw}%')")
|
||||
where_sql = " WHERE " + " AND ".join(wheres)
|
||||
|
||||
rows = _rows(db, (
|
||||
f"SELECT `{cfg['id']}` id, IFNULL(`{cfg['name']}`,'') name, IFNULL(`制式`,'') sys,"
|
||||
f" IFNULL(`带宽`,'') band, IFNULL(`站型`,'') station, IFNULL(`频段`,'') freq,"
|
||||
f" ROUND({ul}*100,1) ul, ROUND({dl}*100,1) dl, ROUND({flow},2) flow, ROUND({users},0) users,"
|
||||
f" IFNULL(`高负荷问题`,'') problem, IFNULL(`优化建议`,'') suggestion"
|
||||
f" FROM {t}{where_sql}"
|
||||
f" ORDER BY FIELD(`高负荷问题`,'高负荷','高流量预警','利用率预警'), {dl} DESC"
|
||||
))
|
||||
|
||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"问题小区清单_{rat.lower()}_{timestamp}.csv"
|
||||
filepath = CACHE_DIR / filename
|
||||
header = [cfg["id"], "小区名称", "制式", "带宽", "站型", "频段",
|
||||
"上行利用率(%)", "下行利用率(%)", "日均流量(GB)", "用户数", "高负荷问题", "优化建议"]
|
||||
try:
|
||||
with filepath.open("w", encoding="utf-8-sig", newline="") as handle:
|
||||
writer = csv.writer(handle)
|
||||
writer.writerow(header)
|
||||
for r in rows:
|
||||
writer.writerow([
|
||||
r.get("id"), r.get("name"), r.get("sys"), r.get("band"), r.get("station"),
|
||||
r.get("freq"), r.get("ul"), r.get("dl"), r.get("flow"), r.get("users"),
|
||||
r.get("problem"), r.get("suggestion"),
|
||||
])
|
||||
except Exception:
|
||||
remove_file_safely(filepath)
|
||||
raise
|
||||
return FileResponse(
|
||||
path=str(filepath), filename=filename, media_type="text/csv",
|
||||
background=BackgroundTask(remove_file_safely, filepath),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/dashboard/cell")
|
||||
def dashboard_cell(rat: str = Query("4g"), id: str = Query(...)):
|
||||
cfg = _rat(rat)
|
||||
|
||||
@@ -917,3 +917,10 @@ CellData 处理日志细化(`app/services/cell_data.py`):
|
||||
- 前端:新增依赖 `echarts`;`components/EChart.vue`(echarts 封装,ResizeObserver 自适应 + dispose);`components/CapacityDashboard.vue`(科技感大屏:4G/5G 分段切换、6 张汇总卡片含数字滚动、6 个图表(环形/直方/分组柱/饼/横向柱)、问题小区清单(筛选+分页+点击行打开)、右侧详情抽屉(关键指标 + 优化建议 + 同扇区兄弟小区))。无结果表时横纵居中「请先进行数据处理」。
|
||||
- 路由 `/dashboard`(name=`dashboard`,懒加载);侧边菜单「容量看板」置于「数据处理」下方(`AppShell` menuKeys/menuOptions + `StatsChartOutline`)。看板自带深色科技风(径向辉光/玻璃拟态/霓虹色),不随应用明暗主题;图表 echarts 自定义深色配色。
|
||||
- 验证:`npm run build`(vue-tsc)通过(CapacityDashboard 懒加载 chunk≈1.16MB 含 echarts);后端 `compileall` 通过;各接口 SQL 经 MCP 对真实数据验证(4G 高负荷 792 / 5G 46,同扇区邻区 6–8 个)。
|
||||
|
||||
## 2026-06-26:容量看板配色统一 + 整页不滚动 + 清单导出
|
||||
|
||||
- 组件配色统一:`CapacityDashboard.vue` 整体用 `<n-config-provider :theme="darkTheme" :theme-overrides="naiveDark">` 包裹,强制看板内所有 Naive 组件(数据表/搜索框/分页/抽屉/Empty/按钮)走深色 + 青色(`#22d3ee`)强调色,解决浅色应用主题下表格/搜索/分页与深色大屏不匹配的问题。`naiveDark` 覆盖 DataTable(透明 td、半透明 th、淡边框)、Input(深色半透明底)、Pagination(深色项+青色激活)、Drawer(`#0c1422`)、Empty。抽屉内自定义 `.cap-*` 样式由 `var(--td-*)` 改为固定深色值,不再跟随应用主题。
|
||||
- 整页不滚动:`.cap-dashboard` 由 `overflow:auto` 改为 `height:100%; overflow:hidden; display:flex; flex-direction:column`(父链 `.content` 高 `calc(100vh-64px)` 为确定值)。布局分三段:顶栏(auto) → `.cap-spin`(flex:0 0 auto,包裹卡片+图表) → `.cap-list`(flex:1,占满剩余)。**清单已移出 `n-spin`** 成为 `.cap-body` 直接 flex 子项以获得确定高度;表格 `n-data-table` 加 `flex-height + height:100%`,由 `.cap-table-wrap`(flex:1 min-height:0) 提供高度,表体内部纵向滚动而非整页滚动。图表改为 2 行 3 列等宽网格,高度 `clamp(136px,16vh,184px)` 随视口自适应;卡片/字号整体缩小。窄屏(<1100px) 回退为 `overflow:auto` 多列换行。
|
||||
- 清单导出:后端新增 `GET /api/dashboard/export?rat&problem&keyword`(`dashboard.py`,sync def 走线程池),WHERE 与 `cells` 完全一致(同 problem/keyword 筛选、同排序、无分页),导出全部命中行为 CSV(`utf-8-sig`,FileResponse + BackgroundTask 清理 CACHE_DIR 临时文件);列含 CGI/NCGI、小区名称、制式/带宽/站型/频段、上下行利用率%、日均流量、用户数、高负荷问题、优化建议。前端清单工具栏「查询」旁加「导出」按钮(`downloadGet`,`exporting` 态,清单为空时禁用)。
|
||||
- 验证:`frontend` `vue-tsc --noEmit` 通过;后端 `py_compile dashboard.py` 通过。
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<template>
|
||||
<n-config-provider :theme="darkTheme" :theme-overrides="naiveDark">
|
||||
<div class="cap-dashboard">
|
||||
<!-- 加载中 -->
|
||||
<div v-if="loadingStatus" class="cap-center">
|
||||
@@ -37,6 +38,7 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="cap-spin">
|
||||
<n-spin :show="loadingOverview">
|
||||
<!-- 汇总卡片 -->
|
||||
<section class="cap-cards">
|
||||
@@ -56,33 +58,35 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 图表区 -->
|
||||
<!-- 图表区:两行三列 -->
|
||||
<section class="cap-charts">
|
||||
<div class="cap-panel span-2">
|
||||
<div class="cap-panel">
|
||||
<div class="cap-panel-head"><span class="bar"></span>负荷问题分布</div>
|
||||
<e-chart :option="problemOption" height="280px" />
|
||||
<e-chart :option="problemOption" :height="chartHeight" />
|
||||
</div>
|
||||
<div class="cap-panel span-4">
|
||||
<div class="cap-panel">
|
||||
<div class="cap-panel-head"><span class="bar"></span>下行利用率分布({{ overview?.labels.dl }})</div>
|
||||
<e-chart :option="histOption" height="280px" />
|
||||
<e-chart :option="histOption" :height="chartHeight" />
|
||||
</div>
|
||||
<div class="cap-panel span-3">
|
||||
<div class="cap-panel-head"><span class="bar"></span>制式分布(总数 / 高负荷)</div>
|
||||
<e-chart :option="systemOption" height="260px" />
|
||||
</div>
|
||||
<div class="cap-panel span-3">
|
||||
<div class="cap-panel-head"><span class="bar"></span>带宽分布(总数 / 高负荷)</div>
|
||||
<e-chart :option="bandOption" height="260px" />
|
||||
</div>
|
||||
<div class="cap-panel span-2">
|
||||
<div class="cap-panel">
|
||||
<div class="cap-panel-head"><span class="bar"></span>站型分布</div>
|
||||
<e-chart :option="stationOption" height="260px" />
|
||||
<e-chart :option="stationOption" :height="chartHeight" />
|
||||
</div>
|
||||
<div class="cap-panel span-4">
|
||||
<div class="cap-panel">
|
||||
<div class="cap-panel-head"><span class="bar"></span>制式分布(总数 / 高负荷)</div>
|
||||
<e-chart :option="systemOption" :height="chartHeight" />
|
||||
</div>
|
||||
<div class="cap-panel">
|
||||
<div class="cap-panel-head"><span class="bar"></span>带宽分布(总数 / 高负荷)</div>
|
||||
<e-chart :option="bandOption" :height="chartHeight" />
|
||||
</div>
|
||||
<div class="cap-panel">
|
||||
<div class="cap-panel-head"><span class="bar"></span>频段标记小区 Top</div>
|
||||
<e-chart :option="freqOption" height="260px" />
|
||||
<e-chart :option="freqOption" :height="chartHeight" />
|
||||
</div>
|
||||
</section>
|
||||
</n-spin>
|
||||
</div>
|
||||
|
||||
<!-- 高负荷 / 预警 清单 -->
|
||||
<section class="cap-panel cap-list">
|
||||
@@ -102,6 +106,7 @@
|
||||
<n-input
|
||||
v-model:value="keyword"
|
||||
class="cap-search"
|
||||
size="small"
|
||||
clearable
|
||||
placeholder="CGI / 小区名称"
|
||||
@keyup.enter="searchCells"
|
||||
@@ -110,28 +115,38 @@
|
||||
<template #prefix><n-icon><SearchOutline /></n-icon></template>
|
||||
</n-input>
|
||||
<n-button size="small" type="primary" ghost @click="searchCells">查询</n-button>
|
||||
<n-button size="small" tertiary :loading="exporting" :disabled="!cellTotal" @click="exportCells">
|
||||
<template #icon><n-icon><DownloadOutline /></n-icon></template>
|
||||
导出
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cap-table-wrap">
|
||||
<n-data-table
|
||||
class="cap-table"
|
||||
size="small"
|
||||
:columns="columns"
|
||||
:data="cells"
|
||||
:loading="loadingCells"
|
||||
:bordered="false"
|
||||
:single-line="false"
|
||||
:scroll-x="1180"
|
||||
:row-props="rowProps"
|
||||
flex-height
|
||||
style="height: 100%"
|
||||
/>
|
||||
</div>
|
||||
<div class="cap-pager">
|
||||
<span class="cap-pager-total">共 {{ cellTotal }} 个问题小区</span>
|
||||
<n-pagination
|
||||
:page="page"
|
||||
:page-count="pageCount"
|
||||
:page-size="pageSize"
|
||||
size="small"
|
||||
@update:page="changePage"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</n-spin>
|
||||
</div>
|
||||
|
||||
<!-- 小区详情抽屉 -->
|
||||
@@ -188,11 +203,12 @@
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
</div>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, reactive, ref, type Component } from 'vue';
|
||||
import { NTag, useMessage } from 'naive-ui';
|
||||
import { NTag, darkTheme, useMessage } from 'naive-ui';
|
||||
import type { DataTableColumns } from 'naive-ui';
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import {
|
||||
@@ -200,6 +216,7 @@ import {
|
||||
AnalyticsOutline,
|
||||
BulbOutline,
|
||||
CellularOutline,
|
||||
DownloadOutline,
|
||||
FlashOutline,
|
||||
PulseOutline,
|
||||
RefreshOutline,
|
||||
@@ -207,7 +224,7 @@ import {
|
||||
TrendingUpOutline
|
||||
} from '@vicons/ionicons5';
|
||||
|
||||
import { apiGet } from '../api/client';
|
||||
import { apiGet, downloadGet } from '../api/client';
|
||||
import EChart from './EChart.vue';
|
||||
|
||||
type Rat = '4g' | '5g';
|
||||
@@ -237,6 +254,53 @@ interface CellDetail {
|
||||
|
||||
const message = useMessage();
|
||||
|
||||
// 强制看板内 Naive 组件走深色配色,统一科技感主题
|
||||
const naiveDark = {
|
||||
common: {
|
||||
primaryColor: '#22d3ee',
|
||||
primaryColorHover: '#38bdf8',
|
||||
primaryColorPressed: '#0891b2',
|
||||
primaryColorSuppl: '#38bdf8',
|
||||
borderRadius: '8px'
|
||||
},
|
||||
DataTable: {
|
||||
thColor: 'rgba(30, 41, 59, 0.55)',
|
||||
thColorHover: 'rgba(30, 41, 59, 0.85)',
|
||||
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'
|
||||
},
|
||||
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'
|
||||
},
|
||||
Drawer: { color: '#0c1422' },
|
||||
Empty: { textColor: '#64748b', iconColor: '#475569' }
|
||||
};
|
||||
|
||||
const chartHeight = 'clamp(136px, 16vh, 184px)';
|
||||
|
||||
const loadingStatus = ref(true);
|
||||
const ready = ref(false);
|
||||
const rat = ref<Rat>('4g');
|
||||
@@ -255,6 +319,7 @@ const pageSize = 20;
|
||||
const loadingCells = ref(false);
|
||||
const problemFilter = ref('');
|
||||
const keyword = ref('');
|
||||
const exporting = ref(false);
|
||||
const problemFilters = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '高负荷', value: '高负荷' },
|
||||
@@ -525,6 +590,21 @@ async function loadCells() {
|
||||
}
|
||||
}
|
||||
|
||||
async function exportCells() {
|
||||
if (!cellTotal.value) return;
|
||||
exporting.value = true;
|
||||
try {
|
||||
const qs = `rat=${rat.value}&problem=${encodeURIComponent(problemFilter.value)}&keyword=${encodeURIComponent(keyword.value.trim())}`;
|
||||
const filename = `问题小区清单_${rat.value.toUpperCase()}.csv`;
|
||||
const result = await downloadGet(`/api/dashboard/export?${qs}`, filename);
|
||||
if (result.saved) message.success('问题小区清单已导出');
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '导出失败');
|
||||
} finally {
|
||||
exporting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function openDetail(id: string) {
|
||||
detailVisible.value = true;
|
||||
loadingDetail.value = true;
|
||||
@@ -573,8 +653,10 @@ onMounted(loadStatus);
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 20px 24px 32px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 14px 18px;
|
||||
color: #e2e8f0;
|
||||
background:
|
||||
radial-gradient(1200px 600px at 12% -10%, rgba(56, 189, 248, 0.12), transparent 60%),
|
||||
@@ -587,8 +669,8 @@ onMounted(loadStatus);
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 320px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
@@ -620,9 +702,10 @@ onMounted(loadStatus);
|
||||
50% { transform: scale(1.12); opacity: 1; }
|
||||
}
|
||||
|
||||
.cap-body { display: flex; flex-direction: column; gap: 18px; }
|
||||
.cap-body { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 12px; }
|
||||
|
||||
.cap-topbar {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -631,11 +714,11 @@ onMounted(loadStatus);
|
||||
}
|
||||
.cap-title-wrap { display: flex; align-items: center; gap: 12px; }
|
||||
.cap-spark {
|
||||
width: 6px; height: 26px; border-radius: 3px;
|
||||
width: 6px; height: 24px; border-radius: 3px;
|
||||
background: linear-gradient(180deg, #22d3ee, #818cf8);
|
||||
box-shadow: 0 0 16px rgba(56, 189, 248, 0.7);
|
||||
}
|
||||
.cap-title { margin: 0; font-size: 20px; font-weight: 700; letter-spacing: 1px; color: #f1f5f9; }
|
||||
.cap-title { margin: 0; font-size: 19px; font-weight: 700; letter-spacing: 1px; color: #f1f5f9; }
|
||||
.cap-sub { font-size: 12px; color: #64748b; }
|
||||
.cap-controls { display: flex; align-items: center; gap: 12px; }
|
||||
|
||||
@@ -648,7 +731,7 @@ onMounted(loadStatus);
|
||||
}
|
||||
.cap-seg.sm { padding: 2px; border-radius: 8px; }
|
||||
.cap-seg-btn {
|
||||
padding: 6px 18px;
|
||||
padding: 5px 16px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
@@ -667,28 +750,33 @@ onMounted(loadStatus);
|
||||
}
|
||||
.cap-refresh {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 34px; height: 34px; border-radius: 9px;
|
||||
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: 17px;
|
||||
color: #94a3b8; 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:disabled { opacity: 0.5; cursor: wait; }
|
||||
|
||||
/* n-spin 包裹卡片与图表(固定高度区域) */
|
||||
.cap-spin { flex: 0 0 auto; }
|
||||
.cap-spin :deep(.n-spin-content) { display: flex; flex-direction: column; gap: 12px; }
|
||||
|
||||
/* 汇总卡片 */
|
||||
.cap-cards {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
gap: 12px;
|
||||
}
|
||||
.cap-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 14px;
|
||||
gap: 11px;
|
||||
padding: 11px 13px;
|
||||
border-radius: 13px;
|
||||
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);
|
||||
overflow: hidden;
|
||||
@@ -698,14 +786,14 @@ onMounted(loadStatus);
|
||||
@keyframes cardIn { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: translateY(0); } }
|
||||
.cap-card-icon {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
width: 42px; height: 42px; flex: 0 0 auto;
|
||||
border-radius: 11px; font-size: 22px;
|
||||
width: 38px; height: 38px; flex: 0 0 auto;
|
||||
border-radius: 10px; font-size: 20px;
|
||||
background: rgba(148, 163, 184, 0.1);
|
||||
}
|
||||
.cap-card-body { min-width: 0; }
|
||||
.cap-card-value { font-size: 26px; font-weight: 800; line-height: 1.1; color: #f8fafc; font-variant-numeric: tabular-nums; }
|
||||
.cap-card-unit { font-size: 13px; font-weight: 600; margin-left: 3px; color: #94a3b8; }
|
||||
.cap-card-label { margin-top: 4px; font-size: 12px; color: #94a3b8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.cap-card-value { font-size: 22px; font-weight: 800; line-height: 1.15; color: #f8fafc; font-variant-numeric: tabular-nums; }
|
||||
.cap-card-unit { font-size: 12px; font-weight: 600; margin-left: 3px; color: #94a3b8; }
|
||||
.cap-card-label { margin-top: 2px; font-size: 12px; 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); }
|
||||
@@ -717,71 +805,76 @@ onMounted(loadStatus);
|
||||
.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-charts {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.cap-panel {
|
||||
padding: 14px 16px;
|
||||
border-radius: 14px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 13px;
|
||||
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);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.span-2 { grid-column: span 2; } .span-3 { grid-column: span 3; } .span-4 { grid-column: span 4; }
|
||||
.cap-panel-head {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
margin-bottom: 10px; font-size: 13px; font-weight: 600; color: #cbd5e1;
|
||||
margin-bottom: 6px; font-size: 13px; font-weight: 600; color: #cbd5e1;
|
||||
}
|
||||
.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); }
|
||||
|
||||
/* 清单 */
|
||||
.cap-list { display: flex; flex-direction: column; }
|
||||
.cap-list-head { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; margin-bottom: 10px; }
|
||||
/* 清单:占据剩余高度,表格内部滚动 */
|
||||
.cap-list { flex: 1 1 0; min-height: 0; display: flex; flex-direction: column; padding-bottom: 10px; }
|
||||
.cap-list-head { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; margin-bottom: 8px; }
|
||||
.cap-list-head .cap-panel-head { margin-bottom: 0; }
|
||||
.cap-list-filters { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||
.cap-search { width: 220px; }
|
||||
.cap-table { background: transparent; margin-top: 4px; }
|
||||
.cap-pager { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 12px; }
|
||||
.cap-search { width: 200px; }
|
||||
.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-detail-head { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
|
||||
.cap-detail-id { font-weight: 700; font-size: 15px; }
|
||||
.cap-detail-name { font-size: 12px; color: var(--td-text-color-secondary, #94a3b8); overflow: hidden; text-overflow: ellipsis; }
|
||||
.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 { 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: var(--td-text-color-secondary, #64748b); }
|
||||
.cap-tag.is-rose { background: rgba(251, 113, 133, 0.16); color: #f43f5e; }
|
||||
.cap-tag.is-amber { background: rgba(251, 191, 36, 0.16); color: #d97706; }
|
||||
.cap-tag.is-blue { background: rgba(56, 189, 248, 0.16); color: #0284c7; }
|
||||
.cap-tag.is-slate { background: rgba(100, 116, 139, 0.16); color: #64748b; }
|
||||
.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-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
||||
.cap-metric { padding: 10px 12px; border-radius: 10px; background: var(--td-bg-color-secondarycontainer, rgba(148, 163, 184, 0.08)); border: 1px solid var(--td-border-color-light, rgba(148, 163, 184, 0.16)); }
|
||||
.cap-metric-label { font-size: 12px; color: var(--td-text-color-secondary, #94a3b8); }
|
||||
.cap-metric-value { margin-top: 4px; font-size: 16px; font-weight: 700; }
|
||||
.cap-suggest { padding: 12px 14px; border-radius: 12px; background: rgba(251, 191, 36, 0.08); border: 1px solid rgba(251, 191, 36, 0.3); }
|
||||
.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(--td-text-color-primary, #334155); }
|
||||
.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-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-sib-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.cap-sib-item { padding: 10px 12px; border-radius: 10px; border: 1px solid var(--td-border-color-light, rgba(148, 163, 184, 0.16)); }
|
||||
.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-main { display: flex; align-items: baseline; gap: 8px; }
|
||||
.cap-sib-id { font-weight: 600; font-size: 13px; }
|
||||
.cap-sib-name { font-size: 12px; color: var(--td-text-color-secondary, #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: var(--td-text-color-secondary, #94a3b8); }
|
||||
.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; }
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.cap-cards { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.span-2, .span-3, .span-4 { grid-column: span 3; }
|
||||
.cap-charts { grid-template-columns: repeat(6, minmax(0, 1fr)); }
|
||||
@media (max-width: 1366px) {
|
||||
.cap-card-value { font-size: 20px; }
|
||||
.cap-card-icon { width: 34px; height: 34px; font-size: 18px; }
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.cap-dashboard { padding: 14px; }
|
||||
@media (max-width: 1100px) {
|
||||
.cap-dashboard { overflow: auto; }
|
||||
.cap-cards { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.cap-charts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.cap-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.span-2, .span-3, .span-4 { grid-column: span 6; }
|
||||
.cap-charts { grid-template-columns: minmax(0, 1fr); }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user