From c8fb389aca428e7dd5e326d1e3e7daa17f146434 Mon Sep 17 00:00:00 2001 From: Nixevol Date: Fri, 26 Jun 2026 07:51:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=AE=B9=E9=87=8F?= =?UTF-8?q?=E7=9C=8B=E6=9D=BF=EF=BC=884G/5G=20=E9=AB=98=E8=B4=9F=E8=8D=B7?= =?UTF-8?q?=E5=88=86=E6=9E=90=E5=A4=A7=E5=B1=8F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端 dashboard 路由:status/overview/cells/cell,基于 4G/5G 结果表聚合分析 - 前端 CapacityDashboard:4G/5G 切换、汇总卡片(数字滚动)、echarts 图表(环形/直方/柱/饼)、问题小区清单、详情抽屉(指标+优化建议+同扇区邻区) - 侧边菜单「容量看板」置于数据处理下方;无结果表时居中提示「请先进行数据处理」 - 新增依赖 echarts;封装 EChart 组件 --- app/api/routers/dashboard.py | 281 +++++++ app/main.py | 2 + docs/project_context.md | 12 + frontend/package-lock.json | 43 +- frontend/package.json | 1 + frontend/src/AppShell.vue | 4 +- frontend/src/components/CapacityDashboard.vue | 787 ++++++++++++++++++ frontend/src/components/EChart.vue | 43 + frontend/src/router.ts | 2 + 9 files changed, 1157 insertions(+), 18 deletions(-) create mode 100644 app/api/routers/dashboard.py create mode 100644 frontend/src/components/CapacityDashboard.vue create mode 100644 frontend/src/components/EChart.vue diff --git a/app/api/routers/dashboard.py b/app/api/routers/dashboard.py new file mode 100644 index 0000000..118f3fc --- /dev/null +++ b/app/api/routers/dashboard.py @@ -0,0 +1,281 @@ +"""容量看板分析接口。 + +基于 4G/5G 结果表(含富集 + 高负荷判定 + 优化建议列)做聚合分析,供前端容量看板展示。 +读经 `make_warehouse`(直连 MySQL 或 Metrix 平台),分析对象为“主仓库”里的结果表。 +""" +from __future__ import annotations + +from typing import Any + +from fastapi import APIRouter, HTTPException, Query + +from app import state +from app.warehouse import make_warehouse + +router = APIRouter(tags=["dashboard"]) + +# 每个制式(结果表)的关键列映射 +RAT = { + "4g": { + "table": "4G_结果表", + "id": "CGI", + "name": "小区名称", + "ul": "上行PUSCH利用率", + "dl": "下行PDSCH利用率", + "ul_label": "上行PUSCH利用率", + "dl_label": "下行PDSCH利用率", + "users": "YY-RRC连接建立最大用户数", + }, + "5g": { + "table": "5G_结果表", + "id": "NCGI", + "name": "CU小区配置名称", + "ul": "上行PRB平均利用率", + "dl": "下行PRB平均利用率", + "ul_label": "上行PRB利用率", + "dl_label": "下行PRB利用率", + "users": "RRC连接平均连接用户数", + }, +} +FLOW = "日均流量(GB)" +PROBLEMS = ["高负荷", "利用率预警", "高流量预警"] + + +def _db(): + return make_warehouse(state.current_config()) + + +def _esc(value: str) -> str: + return str(value).replace("\\", "\\\\").replace("'", "''") + + +def _rows(db, sql: str) -> list[dict[str, Any]]: + ok, result = db.execute_sql(sql) + if not ok: + raise HTTPException(status_code=500, detail=str(result)) + return result if isinstance(result, list) else [] + + +def _num(value: Any, digits: int | None = None) -> float | int: + try: + num = float(value) + except (TypeError, ValueError): + return 0 + if digits is None: + return int(num) + return round(num, digits) + + +def _rat(rat: str) -> dict[str, str]: + cfg = RAT.get((rat or "").lower()) + if not cfg: + raise HTTPException(status_code=400, detail="rat 仅支持 4g / 5g") + return cfg + + +def _existing_tables(db) -> set[str]: + try: + return {str(t).lower() for t in db.get_tables()} + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=f"读取数据表失败: {exc}") from exc + + +@router.get("/api/dashboard/status") +def dashboard_status(): + tables = _existing_tables(_db()) + has_4g = RAT["4g"]["table"].lower() in tables + has_5g = RAT["5g"]["table"].lower() in tables + return {"has_4g": has_4g, "has_5g": has_5g, "ready": has_4g and has_5g} + + +@router.get("/api/dashboard/overview") +def dashboard_overview(rat: str = Query("4g")): + 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']}`" + + summary = _rows(db, ( + f"SELECT COUNT(*) total," + f" SUM(`高负荷问题`='高负荷') high_load," + f" SUM(`高负荷问题`='利用率预警') util_warn," + f" SUM(`高负荷问题`='高流量预警') flow_warn," + f" SUM(`高负荷问题` IS NULL) normal," + f" ROUND(AVG({dl})*100,1) avg_dl," + f" ROUND(MAX({dl})*100,1) max_dl," + f" ROUND(AVG({flow}),2) avg_flow," + f" ROUND(SUM({flow}),1) total_flow" + f" FROM {t}" + )) + s = summary[0] if summary else {} + summary_out = { + "total": _num(s.get("total")), + "high_load": _num(s.get("high_load")), + "util_warn": _num(s.get("util_warn")), + "flow_warn": _num(s.get("flow_warn")), + "normal": _num(s.get("normal")), + "avg_dl": _num(s.get("avg_dl"), 1), + "max_dl": _num(s.get("max_dl"), 1), + "avg_flow": _num(s.get("avg_flow"), 2), + "total_flow": _num(s.get("total_flow"), 1), + } + + problem_pie = [ + {"name": "高负荷", "value": summary_out["high_load"]}, + {"name": "利用率预警", "value": summary_out["util_warn"]}, + {"name": "高流量预警", "value": summary_out["flow_warn"]}, + {"name": "正常", "value": summary_out["normal"]}, + ] + + def group_by(col: str, limit: int = 0) -> list[dict[str, Any]]: + limit_sql = f" LIMIT {int(limit)}" if limit else "" + rows = _rows(db, ( + f"SELECT IFNULL(`{col}`,'未知') name, COUNT(*) total," + f" SUM(`是否高负荷小区`='是') high," + f" SUM(`高负荷问题` IS NOT NULL) flagged" + f" FROM {t} GROUP BY `{col}` ORDER BY flagged DESC, total DESC{limit_sql}" + )) + return [ + {"name": str(r.get("name")), "total": _num(r.get("total")), + "high": _num(r.get("high")), "flagged": _num(r.get("flagged"))} + for r in rows + ] + + # 下行利用率分布(0-10%...90-100%) + hist_rows = _rows(db, ( + f"SELECT LEAST(FLOOR({dl}*10),9) b, COUNT(*) c FROM {t}" + f" WHERE {dl} IS NOT NULL GROUP BY b ORDER BY b" + )) + hist_map = {int(_num(r.get("b"))): _num(r.get("c")) for r in hist_rows} + util_hist = [ + {"bucket": f"{i*10}-{i*10+10}%", "value": hist_map.get(i, 0)} for i in range(10) + ] + + top_rows = _rows(db, ( + f"SELECT `{cfg['id']}` id, IFNULL(`{cfg['name']}`,'') name, IFNULL(`制式`,'') system," + f" IFNULL(`带宽`,'') band, IFNULL(`站型`,'') station," + f" ROUND({ul}*100,1) ul, ROUND({dl}*100,1) dl, ROUND({flow},2) flow, ROUND({users},0) users," + f" IFNULL(`高负荷问题`,'') problem" + f" FROM {t} WHERE `高负荷问题`='高负荷' ORDER BY {dl} DESC, {flow} DESC LIMIT 10" + )) + top_cells = [ + {"id": str(r.get("id")), "name": str(r.get("name")), "system": str(r.get("system")), + "band": str(r.get("band")), "station": str(r.get("station")), + "ul": _num(r.get("ul"), 1), "dl": _num(r.get("dl"), 1), + "flow": _num(r.get("flow"), 2), "users": _num(r.get("users")), "problem": str(r.get("problem"))} + for r in top_rows + ] + + return { + "rat": rat.lower(), + "labels": {"ul": cfg["ul_label"], "dl": cfg["dl_label"]}, + "summary": summary_out, + "problem_pie": problem_pie, + "by_system": group_by("制式"), + "by_band": group_by("带宽"), + "by_station": group_by("站型"), + "by_freq": group_by("频段", limit=12), + "util_hist": util_hist, + "top_cells": top_cells, + } + + +@router.get("/api/dashboard/cells") +def dashboard_cells( + rat: str = Query("4g"), + problem: str = Query(""), + keyword: str = Query(""), + page: int = Query(1, ge=1), + page_size: int = Query(20, ge=1, le=200), +): + 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) + + total = _num(_rows(db, f"SELECT COUNT(*) c FROM {t}{where_sql}")[0].get("c")) + offset = (page - 1) * page_size + rows = _rows(db, ( + f"SELECT `{cfg['id']}` id, IFNULL(`{cfg['name']}`,'') name, IFNULL(`制式`,'') system," + 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(`是否高负荷小区`,'否') is_high" + f" FROM {t}{where_sql}" + f" ORDER BY FIELD(`高负荷问题`,'高负荷','高流量预警','利用率预警'), {dl} DESC" + f" LIMIT {int(page_size)} OFFSET {int(offset)}" + )) + items = [ + {"id": str(r.get("id")), "name": str(r.get("name")), "system": str(r.get("system")), + "band": str(r.get("band")), "station": str(r.get("station")), "freq": str(r.get("freq")), + "ul": _num(r.get("ul"), 1), "dl": _num(r.get("dl"), 1), "flow": _num(r.get("flow"), 2), + "users": _num(r.get("users")), "problem": str(r.get("problem")), "is_high": str(r.get("is_high"))} + for r in rows + ] + return {"items": items, "total": total, "page": page, "page_size": page_size} + + +@router.get("/api/dashboard/cell") +def dashboard_cell(rat: str = Query("4g"), id: 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']}`" + cell_id = _esc(id) + rows = _rows(db, f"SELECT * FROM {t} WHERE `{cfg['id']}`='{cell_id}' LIMIT 1") + if not rows: + raise HTTPException(status_code=404, detail="未找到该小区") + row = rows[0] + + # 同扇区同 PLMN 的兄弟小区(用于详情页的均衡上下文) + sector = str(row.get("扇区") or "") + siblings: list[dict[str, Any]] = [] + if sector: + plmn = _esc("-".join(str(id).split("-")[:2])) + sector_e = _esc(sector) + dl = f"`{cfg['dl']}`" + sib_rows = _rows(db, ( + f"SELECT `{cfg['id']}` id, IFNULL(`{cfg['name']}`,'') name, IFNULL(`带宽`,'') band," + f" IFNULL(`频段`,'') freq, ROUND(`{cfg['ul']}`*100,1) ul, ROUND({dl}*100,1) dl," + f" ROUND(`{FLOW}`,2) flow, IFNULL(`高负荷问题`,'正常') problem" + f" FROM {t} WHERE `扇区`='{sector_e}' AND SUBSTRING_INDEX(`{cfg['id']}`,'-',2)='{plmn}'" + f" AND `{cfg['id']}`<>'{cell_id}' ORDER BY {dl} DESC LIMIT 30" + )) + siblings = [ + {"id": str(r.get("id")), "name": str(r.get("name")), "band": str(r.get("band")), + "freq": str(r.get("freq")), "ul": _num(r.get("ul"), 1), "dl": _num(r.get("dl"), 1), + "flow": _num(r.get("flow"), 2), "problem": str(r.get("problem"))} + for r in sib_rows + ] + + # 原始行转为字符串友好的 dict(数值保留,None→空) + detail = {str(k): (v if v is not None else "") for k, v in row.items()} + return { + "rat": rat.lower(), + "id": str(row.get(cfg["id"]) or id), + "name": str(row.get(cfg["name"]) or ""), + "labels": {"ul": cfg["ul_label"], "dl": cfg["dl_label"]}, + "id_field": cfg["id"], + "name_field": cfg["name"], + "ul_field": cfg["ul"], + "dl_field": cfg["dl"], + "users_field": cfg["users"], + "flow_field": FLOW, + "detail": detail, + "siblings": siblings, + } diff --git a/app/main.py b/app/main.py index f5b2192..4cf50b1 100644 --- a/app/main.py +++ b/app/main.py @@ -14,6 +14,7 @@ from app.api.routers import ( cache, cell_data, config, + dashboard, database, health, history, @@ -112,6 +113,7 @@ def register_routes(app: FastAPI) -> None: config.router, cache.router, cell_data.router, + dashboard.router, script.router, ] for router in routers: diff --git a/docs/project_context.md b/docs/project_context.md index f02c569..17e2b7e 100644 --- a/docs/project_context.md +++ b/docs/project_context.md @@ -905,3 +905,15 @@ CellData 处理日志细化(`app/services/cell_data.py`): 附带修复:`frontend/src/vite-env.d.ts` 增加 `declare module 'monaco-editor/esm/vs/basic-languages/mysql/mysql.js'`,修复 ScriptPanel SQL 补全的深层导入缺类型声明导致 `vue-tsc`(`npm run build`)失败(TS7016)。 - 验证:`python -m compileall -q app` 通过;`frontend` `npm run build`(vue-tsc)通过(DatabasePanel chunk≈124KB)。 + +## 2026-06-26:容量看板(4G/5G 高负荷分析大屏) + +- 后端 `app/api/routers/dashboard.py`(`main.py` 注册):基于「主仓库」(`make_warehouse`,直连 MySQL 或 Metrix) 的 4G/5G 结果表聚合分析,4 个接口: + - `GET /api/dashboard/status` → `{has_4g,has_5g,ready}`(两表都在才 ready); + - `GET /api/dashboard/overview?rat=4g|5g` → 汇总(总数/高负荷/利用率预警/高流量预警/正常/平均下行利用率/总流量) + 问题分布饼 + 制式/带宽/站型/频段 分组统计 + 下行利用率 10 档直方 + Top10 高负荷小区; + - `GET /api/dashboard/cells?rat&problem&keyword&page&page_size` → 问题小区清单(按 高负荷>高流量预警>利用率预警 + 下行利用率 排序,可筛选); + - `GET /api/dashboard/cell?rat&id` → 单小区全字段 + 优化建议 + 同扇区同 PLMN 兄弟小区。 + - 4G 指标用 PUSCH/PDSCH 利用率与 YY-RRC,5G 用 PRB 利用率与 RRC 平均;execute_sql 原始 SQL,用户输入(id/keyword) 转义。 +- 前端:新增依赖 `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 个)。 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index dbef47d..e7e9778 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,9 +10,9 @@ "dependencies": { "@tauri-apps/api": "^2.11.0", "@vicons/ionicons5": "^0.13.0", + "echarts": "^6.1.0", "monaco-editor": "^0.55.1", "naive-ui": "^2.42.0", - "swagger-ui-dist": "^5.32.6", "vue": "^3.5.0", "vue-router": "^5.0.7" }, @@ -1008,13 +1008,6 @@ "win32" ] }, - "node_modules/@scarf/scarf": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", - "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", - "hasInstallScript": true, - "license": "Apache-2.0" - }, "node_modules/@tauri-apps/api": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz", @@ -1419,6 +1412,16 @@ "@types/trusted-types": "^2.0.7" } }, + "node_modules/echarts": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.1.0.tgz", + "integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.1.0" + } + }, "node_modules/entities": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", @@ -1937,15 +1940,6 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/swagger-ui-dist": { - "version": "5.32.6", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.6.tgz", - "integrity": "sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==", - "license": "Apache-2.0", - "dependencies": { - "@scarf/scarf": "=1.4.0" - } - }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -1968,6 +1962,12 @@ "integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==", "license": "MIT" }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -2379,6 +2379,15 @@ "funding": { "url": "https://github.com/sponsors/eemeli" } + }, + "node_modules/zrender": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.1.0.tgz", + "integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } } } } diff --git a/frontend/package.json b/frontend/package.json index a6deeed..ec86b4f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "dependencies": { "@tauri-apps/api": "^2.11.0", "@vicons/ionicons5": "^0.13.0", + "echarts": "^6.1.0", "monaco-editor": "^0.55.1", "naive-ui": "^2.42.0", "vue": "^3.5.0", diff --git a/frontend/src/AppShell.vue b/frontend/src/AppShell.vue index f37a932..aec5fb7 100644 --- a/frontend/src/AppShell.vue +++ b/frontend/src/AppShell.vue @@ -128,6 +128,7 @@ import { MoonOutline, ServerOutline, SettingsOutline, + StatsChartOutline, SunnyOutline } from '@vicons/ionicons5'; @@ -147,12 +148,13 @@ const loginLoading = ref(false); const sidebarCollapsed = ref(localStorage.getItem('sidebarCollapsed') === 'true'); const licenseModalVisible = ref(false); const brandClickCount = ref(0); -const menuKeys = ['workflow', 'history', 'database', 'script', 'settings'] as const; +const menuKeys = ['workflow', 'dashboard', 'history', 'database', 'script', 'settings'] as const; type MenuKey = (typeof menuKeys)[number]; let brandClickResetTimer: number | undefined; const menuOptions: MenuOption[] = [ { label: '数据处理', key: 'workflow', icon: renderIcon(CloudUploadOutline) }, + { label: '容量看板', key: 'dashboard', icon: renderIcon(StatsChartOutline) }, { label: '处理历史', key: 'history', icon: renderIcon(FileTrayFullOutline) }, { label: '数据管理', key: 'database', icon: renderIcon(ServerOutline) }, { label: '脚本编辑', key: 'script', icon: renderIcon(ConstructOutline) }, diff --git a/frontend/src/components/CapacityDashboard.vue b/frontend/src/components/CapacityDashboard.vue new file mode 100644 index 0000000..a2c3cb8 --- /dev/null +++ b/frontend/src/components/CapacityDashboard.vue @@ -0,0 +1,787 @@ + + + + + diff --git a/frontend/src/components/EChart.vue b/frontend/src/components/EChart.vue new file mode 100644 index 0000000..09e0d1f --- /dev/null +++ b/frontend/src/components/EChart.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/frontend/src/router.ts b/frontend/src/router.ts index 20f3a1d..0ba6146 100644 --- a/frontend/src/router.ts +++ b/frontend/src/router.ts @@ -1,6 +1,7 @@ import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'; const FileWorkflow = () => import('./components/FileWorkflow.vue'); +const CapacityDashboard = () => import('./components/CapacityDashboard.vue'); const HistoryPanel = () => import('./components/HistoryPanel.vue'); const DatabasePanel = () => import('./components/DatabasePanel.vue'); const SettingsPanel = () => import('./components/SettingsPanel.vue'); @@ -9,6 +10,7 @@ const ScriptPanel = () => import('./components/ScriptPanel.vue'); export const routes: RouteRecordRaw[] = [ { path: '/', redirect: { name: 'workflow' } }, { path: '/upload', name: 'workflow', component: FileWorkflow, meta: { title: '数据处理' } }, + { path: '/dashboard', name: 'dashboard', component: CapacityDashboard, meta: { title: '容量看板' } }, { path: '/history', name: 'history', component: HistoryPanel, meta: { title: '处理历史' } }, { path: '/database', name: 'database', component: DatabasePanel, meta: { title: '数据管理' } }, { path: '/script', name: 'script', component: ScriptPanel, meta: { title: '脚本编辑' } },