page1.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. var cpuChart = null;
  2. var memChart = null;
  3. var diskChart = null;
  4. var sysStatusTimer = null;
  5. var resizeHandler = null;
  6. var sysAbortController = null;
  7. function getSystemInfo() {
  8. sysAbortController && sysAbortController.abort();
  9. sysAbortController = new AbortController();
  10. fetch("/api/system/info", { signal: sysAbortController.signal })
  11. .then(function (r) {
  12. if (!r.ok) throw new Error("HTTP " + r.status);
  13. return r.json();
  14. })
  15. .then(function (d) {
  16. dashSetUsage("dk-cpu", d.cpu_usage != null ? Number(d.cpu_usage) : null);
  17. dashSetUsage("dk-mem", d.mem_percent != null ? Number(d.mem_percent) : null);
  18. dashSetUsage("dk-disk", d.disk_percent != null ? Number(d.disk_percent) : null);
  19. if (!cpuChart || !memChart || !diskChart) {
  20. return;
  21. }
  22. cpuChart.setOption({
  23. graphic: [
  24. {
  25. type: "text",
  26. left: "center",
  27. top: "75%",
  28. style: {
  29. text:
  30. (d.cpu_cores || "--") +
  31. "核 " +
  32. (d.cpu_freq || "--") +
  33. "MHz 温度" +
  34. (d.cpu_temp || "--"),
  35. fill: "#aaa",
  36. fontSize: 15,
  37. },
  38. },
  39. ],
  40. series: [
  41. {
  42. data: [
  43. { value: Number((d.cpu_usage != null ? d.cpu_usage : 0).toFixed(1)) },
  44. ],
  45. },
  46. ],
  47. });
  48. memChart.setOption({
  49. graphic: [
  50. {
  51. type: "text",
  52. left: "center",
  53. top: "75%",
  54. style: {
  55. text: (d.mem_used || "--") + " / " + (d.mem_total || "--"),
  56. fill: "#aaa",
  57. fontSize: 15,
  58. },
  59. },
  60. ],
  61. series: [{ data: [{ value: d.mem_percent != null ? d.mem_percent : 0 }] }],
  62. });
  63. diskChart.setOption({
  64. title: {
  65. text: d.disk_percent + "%",
  66. subtext: (d.disk_used || "--") + " / " + (d.disk_total || "--"),
  67. left: "center",
  68. top: "38%",
  69. textStyle: {
  70. fontSize: 38,
  71. fontWeight: "bold",
  72. },
  73. subtextStyle: {
  74. fontSize: 15,
  75. },
  76. },
  77. series: [
  78. {
  79. data: [
  80. { value: d.disk_percent != null ? d.disk_percent : 0, name: "已使用" },
  81. { value: 100 - d.disk_percent, name: "剩余" },
  82. ],
  83. },
  84. ],
  85. });
  86. })
  87. .catch(function (e) {
  88. if (e.name !== "AbortError") {
  89. console.error("system info failed:", e);
  90. }
  91. });
  92. }
  93. function gaugeOption() {
  94. return {
  95. series: [
  96. {
  97. type: "gauge",
  98. radius: "95%",
  99. center: ["50%", "55%"],
  100. min: 0,
  101. max: 100,
  102. startAngle: 210,
  103. endAngle: -30,
  104. axisLine: {
  105. lineStyle: {
  106. width: 22,
  107. },
  108. },
  109. progress: {
  110. show: true,
  111. width: 22,
  112. },
  113. pointer: { show: false },
  114. axisTick: { show: false },
  115. splitLine: { show: false },
  116. axisLabel: { show: false },
  117. detail: {
  118. fontSize: 42,
  119. fontWeight: "bold",
  120. offsetCenter: [0, "0%"],
  121. formatter: "{value}%",
  122. },
  123. title: { show: false },
  124. data: [{ value: 0 }],
  125. },
  126. ],
  127. };
  128. }
  129. function getLoadInfo() {
  130. fetch("/api/system/loadAverage")
  131. .then(function (r) {
  132. if (!r.ok) {
  133. throw new Error("HTTP " + r.status);
  134. }
  135. return r.json();
  136. })
  137. .then(function (data) {
  138. ["load1m", "load5m", "load15m", "runQueue", "bootTime", "uptime"].forEach(
  139. function (id) {
  140. return setText(id, data[id], "page1");
  141. }
  142. );
  143. dashSetUptime(data ? data.uptime : null);
  144. })
  145. .catch(function (e) {
  146. console.error("load info failed:", e);
  147. ["load1m", "load5m", "load15m", "runQueue", "bootTime", "uptime"].forEach(
  148. function (id) {
  149. return setText(id, null, "page1");
  150. }
  151. );
  152. dashSetUptime(null);
  153. });
  154. }
  155. function getServiceInfo() {
  156. fetch("/api/system/serviceStatus")
  157. .then(function (r) {
  158. if (!r.ok) {
  159. throw new Error("HTTP " + r.status);
  160. }
  161. return r.json();
  162. })
  163. .then(function (data) {
  164. [
  165. "var1",
  166. "var2",
  167. "var3",
  168. "var4",
  169. "var5",
  170. "var6",
  171. "var7",
  172. "var8",
  173. "var9",
  174. "var10",
  175. "var11",
  176. "var22",
  177. "var33",
  178. "var44",
  179. "var55",
  180. "var66",
  181. "var77",
  182. "var88",
  183. "var99",
  184. ].forEach(function (id) {
  185. return setText(id, data[id], "page1");
  186. });
  187. dashRenderStatus(data);
  188. })
  189. .catch(function (e) {
  190. console.error("getAllServiceStatus() failed:", e);
  191. [
  192. "var1",
  193. "var2",
  194. "var3",
  195. "var4",
  196. "var5",
  197. "var6",
  198. "var7",
  199. "var8",
  200. "var9",
  201. "var10",
  202. "var11",
  203. "var22",
  204. "var33",
  205. "var44",
  206. "var55",
  207. "var66",
  208. "var77",
  209. "var88",
  210. "var99",
  211. ].forEach(function (id) {
  212. return setText(id, null, "page1");
  213. });
  214. dashRenderStatus(null);
  215. });
  216. }
  217. function startSysStatusRefresh() {
  218. if (sysStatusTimer) return;
  219. getSystemInfo();
  220. getLoadInfo();
  221. getServiceInfo();
  222. sysStatusTimer = setInterval(function () {
  223. getSystemInfo();
  224. getLoadInfo();
  225. getServiceInfo();
  226. }, 5000);
  227. }
  228. function stopSysStatusRefresh() {
  229. if (sysStatusTimer) {
  230. clearInterval(sysStatusTimer);
  231. sysStatusTimer = null;
  232. }
  233. }
  234. function resizeSysCharts() {
  235. cpuChart && cpuChart.resize();
  236. memChart && memChart.resize();
  237. diskChart && diskChart.resize();
  238. }
  239. var serviceNames = {
  240. "yfkj-networkd.service": "设备自动联网",
  241. "yfkj-timesyncd.service": "系统时间同步",
  242. "yfkj-gnss.service": "地理坐标定位",
  243. "yfkj-camera-capture.service": "相机图像采集",
  244. "yfkj-sshd-mqtt-bridge.service": "远程运维通道",
  245. "yfkj-app-install.service": "应用安装卸载",
  246. "yfkj-web-ui.service": "本地配置管理",
  247. "yfkj-frp-client.service": "公网配置管理",
  248. "yfkj-upgrade.service": "开机自动升级",
  249. };
  250. function switchServiceVersion(service, action) {
  251. var name = serviceNames[service] || service;
  252. var msg =
  253. action === "factory"
  254. ? "确认将【" +
  255. name +
  256. "】恢复到出厂版本吗?\n\n警告: 切换过程中服务会自动重启,请勿断开电源!"
  257. : "确认将【" +
  258. name +
  259. "】回退到上一版本吗?\n\n警告: 切换过程中服务会自动重启,请勿断开电源!";
  260. if (!confirm(msg)) {
  261. return;
  262. }
  263. var password = prompt("请输入操作密码:");
  264. if (password === null) {
  265. return;
  266. }
  267. password = password.trim();
  268. fetch("/api/service/version/switch", {
  269. method: "POST",
  270. headers: {
  271. "Content-Type": "application/json",
  272. },
  273. body: JSON.stringify({
  274. service: service,
  275. action: action,
  276. password: password,
  277. }),
  278. })
  279. .then(function (r) {
  280. return r.json();
  281. })
  282. .then(function (d) {
  283. alert(
  284. "【" +
  285. name +
  286. "】\n\n" +
  287. (d.status == "ok" ? "版本切换成功" : "版本切换失败:" + d.message)
  288. );
  289. })
  290. .catch(function (e) {
  291. alert("【" + name + "】\n\n请求失败:" + e.message);
  292. });
  293. }
  294. function switchAutoUpgrade(event, action) {
  295. var enable = action.startsWith("enable");
  296. var msg = enable ? "确认要启用 【开机自动升级】 吗?" : "确认要关闭 【开机自动升级】 吗?";
  297. if (!confirm(msg)) {
  298. return;
  299. }
  300. var password = prompt("请输入操作密码:");
  301. if (password === null) {
  302. return;
  303. }
  304. password = password.trim();
  305. var btn = event.currentTarget;
  306. btn.disabled = true;
  307. fetch("/api/service/control", {
  308. method: "POST",
  309. headers: {
  310. "Content-Type": "application/json",
  311. },
  312. body: JSON.stringify({
  313. name: "yfkj-upgrade.service",
  314. action: action,
  315. secret: password,
  316. }),
  317. })
  318. .then(function (r) {
  319. if (!r.ok) {
  320. return r.text().then(function (t) {
  321. throw new Error(t);
  322. });
  323. }
  324. return r.json();
  325. })
  326. .then(function (d) {
  327. if (d.ok) {
  328. alert("【开机自动升级】\n\n" + (enable ? "已启用" : "已禁用"));
  329. } else {
  330. alert("操作失败");
  331. }
  332. })
  333. .catch(function (e) {
  334. alert("【开机自动升级】\n\n请求失败: " + e.message);
  335. })
  336. .finally(function () {
  337. btn.disabled = false;
  338. });
  339. }
  340. function downloadUpgradeLogFile(event) {
  341. var btn = event.currentTarget;
  342. btn.disabled = true;
  343. fetch("/api/upgrade/logfile")
  344. .then(function (r) {
  345. if (!r.ok) {
  346. throw new Error("HTTP " + r.status);
  347. }
  348. var disposition = r.headers.get("Content-Disposition");
  349. var filename = "upgrade.log.tar.gz";
  350. if (disposition) {
  351. var match = disposition.match(/filename="?([^";]+)"?/);
  352. if (match) {
  353. filename = match[1];
  354. }
  355. }
  356. return r.blob().then(function (blob) {
  357. return { blob: blob, filename: filename };
  358. });
  359. })
  360. .then(function (res) {
  361. downloadBlobFile(res.blob, res.filename);
  362. alert("下载成功");
  363. })
  364. .catch(function (e) {
  365. console.error("download failed:", e);
  366. alert("下载失败: " + e.message);
  367. })
  368. .finally(function () {
  369. btn.disabled = false;
  370. btn.textContent = "下载升级日志";
  371. });
  372. }
  373. function startPublicAccess(event) {
  374. if (!confirm("确定开启公网访问吗?")) {
  375. return;
  376. }
  377. var password = prompt("请输入操作密码:");
  378. if (password === null) {
  379. return;
  380. }
  381. password = password.trim();
  382. var btn = event.currentTarget;
  383. btn.disabled = true;
  384. fetch("/api/service/control", {
  385. method: "POST",
  386. headers: {
  387. "Content-Type": "application/json",
  388. },
  389. body: JSON.stringify({
  390. name: "yfkj-frp-client.service",
  391. action: "start",
  392. secret: password,
  393. }),
  394. })
  395. .then(function (r) {
  396. if (!r.ok) {
  397. return r.text().then(function (t) {
  398. throw new Error(t);
  399. });
  400. }
  401. return r.json();
  402. })
  403. .then(function (d) {
  404. alert(d.ok ? "开启公网访问成功" : "开启公网访问失败");
  405. })
  406. .catch(function (e) {
  407. alert("开启公网访问失败: " + e.message);
  408. })
  409. .finally(function () {
  410. btn.disabled = false;
  411. });
  412. }
  413. function stopPublicAccess(event) {
  414. if (!confirm("确定关闭公网访问吗?")) {
  415. return;
  416. }
  417. var password = prompt("请输入操作密码:");
  418. if (password === null) {
  419. return;
  420. }
  421. password = password.trim();
  422. var btn = event.currentTarget;
  423. btn.disabled = true;
  424. fetch("/api/service/control", {
  425. method: "POST",
  426. headers: {
  427. "Content-Type": "application/json",
  428. },
  429. body: JSON.stringify({
  430. name: "yfkj-frp-client.service",
  431. action: "stop",
  432. secret: password,
  433. }),
  434. })
  435. .then(function (r) {
  436. if (!r.ok) {
  437. return r.text().then(function (t) {
  438. throw new Error(t);
  439. });
  440. }
  441. return r.json();
  442. })
  443. .then(function (d) {
  444. alert(d.ok ? "关闭公网访问成功" : "关闭公网访问失败");
  445. })
  446. .catch(function (e) {
  447. alert("关闭公网访问失败: " + e.message);
  448. })
  449. .finally(function () {
  450. btn.disabled = false;
  451. });
  452. }
  453. function initChart(id, option) {
  454. var dom = document.getElementById(id);
  455. if (!dom) {
  456. return null;
  457. }
  458. var chart = echarts.init(dom);
  459. chart.setOption(option);
  460. return chart;
  461. }
  462. function initPage1() {
  463. dashBuildRows();
  464. cpuChart = initChart("cpu-chart", gaugeOption());
  465. memChart = initChart("mem-chart", gaugeOption());
  466. diskChart = initChart("disk-chart", {
  467. title: {
  468. text: "0%",
  469. subtext: "-- / --",
  470. left: "center",
  471. top: "38%",
  472. textStyle: {
  473. fontSize: 38,
  474. fontWeight: "bold",
  475. },
  476. subtextStyle: {
  477. fontSize: 15,
  478. },
  479. },
  480. series: [
  481. {
  482. type: "pie",
  483. radius: ["65%", "85%"],
  484. label: { show: false },
  485. data: [
  486. { value: 0, name: "已使用" },
  487. { value: 100, name: "剩余" },
  488. ],
  489. },
  490. ],
  491. });
  492. resizeHandler = resizeSysCharts;
  493. window.addEventListener("resize", resizeHandler);
  494. waitChartResize();
  495. startSysStatusRefresh();
  496. }
  497. function waitChartResize(count) {
  498. if (count === undefined) {
  499. count = 0;
  500. }
  501. resizeSysCharts();
  502. var dom = document.getElementById("cpu-chart");
  503. if (dom && dom.clientWidth > 0 && dom.clientHeight > 0) {
  504. return;
  505. }
  506. dom = document.getElementById("mem-chart");
  507. if (dom && dom.clientWidth > 0 && dom.clientHeight > 0) {
  508. return;
  509. }
  510. dom = document.getElementById("disk-chart");
  511. if (dom && dom.clientWidth > 0 && dom.clientHeight > 0) {
  512. return;
  513. }
  514. if (count < 50) {
  515. setTimeout(function () {
  516. waitChartResize(count + 1);
  517. }, 100);
  518. }
  519. }
  520. function exitPage1() {
  521. sysAbortController && sysAbortController.abort();
  522. sysAbortController = null;
  523. stopSysStatusRefresh();
  524. if (resizeHandler) {
  525. window.removeEventListener("resize", resizeHandler);
  526. resizeHandler = null;
  527. }
  528. cpuChart && cpuChart.dispose();
  529. memChart && memChart.dispose();
  530. diskChart && diskChart.dispose();
  531. cpuChart = null;
  532. memChart = null;
  533. diskChart = null;
  534. }
  535. // ===================== 运行总览看板 (仅移动端显示) =====================
  536. // 看板模块: 展示名 / 服务单元 / 状态字段 / 版本字段 (与 /api/system/serviceStatus 一一对应)
  537. var dashModules = [
  538. { name: "设备自动联网", unit: "networkd.service", status: "var1", version: "var11" },
  539. { name: "同步系统时间", unit: "timesyncd.service", status: "var2", version: "var22" },
  540. { name: "地理坐标定位", unit: "gnss.service", status: "var3", version: "var33" },
  541. { name: "远程运维通道", unit: "sshd-mqtt-bridge.service", status: "var4", version: "var44" },
  542. { name: "相机图像采集", unit: "camera-capture.service", status: "var5", version: "var55" },
  543. { name: "应用安装卸载", unit: "app-install.service", status: "var6", version: "var66" },
  544. { name: "本地配置管理", unit: "web-ui.service", status: "var7", version: "var77" },
  545. { name: "公网配置管理", unit: "frp-client.service", status: "var8", version: "var88" },
  546. { name: "设备自动升级", unit: "upgrade.service", status: "var9", version: "var99" },
  547. { name: "开机任务执行", unit: "local-startup.service", status: "var10", version: "var1010" }
  548. ];
  549. // 首次进入页面时生成模块卡片骨架, 之后仅原地更新, 避免每次刷新整块重建造成闪烁
  550. function dashBuildRows() {
  551. var grid = document.getElementById("dash-grid");
  552. if (!grid || grid.children.length) {
  553. return;
  554. }
  555. var html = "";
  556. dashModules.forEach(function (m, i) {
  557. html +=
  558. '<div class="dash-mod" id="dash-mod-' + i + '">' +
  559. '<div class="dash-mod-head"><span class="dash-name">' + m.name + '</span>' +
  560. '<span class="dash-badge" id="dash-badge-' + i + '"><span class="dash-dot" id="dash-dot-' + i + '"></span><span id="dash-text-' + i + '">--</span></span></div>' +
  561. '<div class="dash-unit">' + m.unit + "</div>" +
  562. '<div class="dash-info" id="dash-info-' + i + '">--</div>' +
  563. "</div>";
  564. });
  565. grid.innerHTML = html;
  566. }
  567. // 用 /api/system/serviceStatus 的数据刷新模块状态与"服务正常"计数
  568. // data 为 null 时(接口失败)整块置灰为 "--"
  569. function dashRenderStatus(data) {
  570. var okCount = 0;
  571. dashModules.forEach(function (m, i) {
  572. var v = data ? data[m.status] : null;
  573. var level = "na";
  574. var text = "--";
  575. if (v && typeof v === "object") {
  576. level = v.level || "na";
  577. text = v.text || "--";
  578. }
  579. // ok=运行中, info=一次性任务已完成, 两者都视为正常
  580. if (level === "ok" || level === "info") {
  581. okCount++;
  582. }
  583. var mod = document.getElementById("dash-mod-" + i);
  584. var dot = document.getElementById("dash-dot-" + i);
  585. var badge = document.getElementById("dash-badge-" + i);
  586. var txt = document.getElementById("dash-text-" + i);
  587. var info = document.getElementById("dash-info-" + i);
  588. if (mod) {
  589. mod.className = "dash-mod dash-" + level;
  590. }
  591. if (badge) {
  592. badge.className = "dash-badge dash-" + level;
  593. }
  594. if (dot) {
  595. dot.className = "dash-dot dash-" + level;
  596. }
  597. if (txt) {
  598. txt.textContent = text;
  599. }
  600. if (info) {
  601. var ver = data ? data[m.version] : null;
  602. info.textContent = ver && typeof ver === "object" ? ver.text || "--" : ver || "--";
  603. }
  604. });
  605. setText(
  606. "dk-svc",
  607. {
  608. level: okCount === dashModules.length ? "ok" : "warn",
  609. text: okCount + " / " + dashModules.length
  610. },
  611. "page1"
  612. );
  613. var foot = document.getElementById("dash-updated");
  614. if (foot) {
  615. foot.textContent = data
  616. ? "数据更新于 " + new Date().toTimeString().slice(0, 8)
  617. : "数据获取失败, 稍后自动重试";
  618. }
  619. }
  620. // 更新占用率指标: 数值 + 迷你进度条; >=85% 红色(hot), >=60% 黄色(warm), 其余绿色
  621. function dashSetUsage(id, pct) {
  622. var el = document.getElementById(id);
  623. if (!el) {
  624. return;
  625. }
  626. var bar = document.getElementById(id + "-bar");
  627. if (pct == null || isNaN(pct)) {
  628. el.textContent = "--";
  629. el.className = "dash-kpi-value";
  630. if (bar) {
  631. bar.style.width = "0";
  632. }
  633. return;
  634. }
  635. var tone = pct >= 85 ? " hot" : pct >= 60 ? " warm" : "";
  636. el.textContent = pct.toFixed(1) + "%";
  637. el.className = "dash-kpi-value" + tone;
  638. if (bar) {
  639. bar.style.width = Math.min(100, pct) + "%";
  640. bar.className = "dash-bar-fill" + tone;
  641. }
  642. }
  643. // 更新"持续开机"KPI, 仅显示文本
  644. function dashSetUptime(text) {
  645. var el = document.getElementById("dk-uptime");
  646. if (el) {
  647. el.textContent = text || "--";
  648. }
  649. }