diff --git a/app/api/routers/dashboard.py b/app/api/routers/dashboard.py index c012514..f57ba2a 100644 --- a/app/api/routers/dashboard.py +++ b/app/api/routers/dashboard.py @@ -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) diff --git a/docs/project_context.md b/docs/project_context.md index 17e2b7e..ceea2cc 100644 --- a/docs/project_context.md +++ b/docs/project_context.md @@ -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` 整体用 `` 包裹,强制看板内所有 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` 通过。 diff --git a/frontend/src/components/CapacityDashboard.vue b/frontend/src/components/CapacityDashboard.vue index a2c3cb8..999cec2 100644 --- a/frontend/src/components/CapacityDashboard.vue +++ b/frontend/src/components/CapacityDashboard.vue @@ -1,88 +1,92 @@ 查询 + + + 导出 + - +
+ +
共 {{ cellTotal }} 个问题小区
- - + - - - - -
-
-
- {{ detailProblem || '正常' }} - {{ detailValue('制式') }} - {{ detailValue('带宽') }} - {{ detailValue('站型') }} - {{ detailValue('频段') }} -
- -
-
-
{{ m.label }}
-
{{ m.value }}
+ + + + +
+
+
+ {{ detailProblem || '正常' }} + {{ detailValue('制式') }} + {{ detailValue('带宽') }} + {{ detailValue('站型') }} + {{ detailValue('频段') }}
-
-
-
优化建议
-

{{ detailSuggestion }}

-
+
+
+
{{ m.label }}
+
{{ m.value }}
+
+
-
-
同扇区同运营商小区({{ detail.siblings.length }})
- -
-
-
- {{ sib.id }} - {{ sib.name }} -
-
- {{ sib.band || '-' }} - {{ sib.freq || '-' }} - 下行 {{ sib.dl }}% - 流量 {{ sib.flow }}GB - {{ sib.problem }} +
+
优化建议
+

{{ detailSuggestion }}

+
+ +
+
同扇区同运营商小区({{ detail.siblings.length }})
+ +
+
+
+ {{ sib.id }} + {{ sib.name }} +
+
+ {{ sib.band || '-' }} + {{ sib.freq || '-' }} + 下行 {{ sib.dl }}% + 流量 {{ sib.flow }}GB + {{ sib.problem }} +
-
- - -
+ + +
+