From b3a43e63d2c2fa854d676676d3f8072c0d943d13 Mon Sep 17 00:00:00 2001 From: 潘志宝 <979469083@qq.com> Date: 星期三, 26 二月 2025 15:36:11 +0800 Subject: [PATCH] Merge branch 'master' of http://dlindusit.com:53929/r/iailab-plat-ui-vue3 --- src/components/MonitorDiskPie/PieChart.vue | 35 + /dev/null | 161 ----- src/views/infra/monitor/components/MonitorDisk.vue | 487 +++++++++++++++++ src/views/infra/monitor/components/MonitorMem.vue | 478 +++++++++++++++++ src/api/infra/monitordisk/index.ts | 57 ++ src/views/infra/monitor/index.vue | 20 src/api/infra/monitormem/index.ts | 56 ++ src/store/modules/mall/kefu.ts | 81 ++ src/views/infra/monitor/components/index.ts | 3 src/views/infra/monitor/components/MonitorDiskForm.vue | 128 ++++ src/views/infra/monitor/components/MonitorMemForm.vue | 148 +++++ src/views/system/loginlog/LoginLogDetail.vue | 2 12 files changed, 1,494 insertions(+), 162 deletions(-) diff --git a/src/api/infra/monitordisk/index.ts b/src/api/infra/monitordisk/index.ts new file mode 100644 index 0000000..e9a2512 --- /dev/null +++ b/src/api/infra/monitordisk/index.ts @@ -0,0 +1,57 @@ +import request from '@/config/axios' + +// 磁盘监控日志 VO +export interface MonitorDiskVO { + id: number // 访问ID + hostName: string // 主机名称 + hostIp: string // 服务器ip + disk: string // 盘符 + diskName: string // 磁盘名 + spaceTotal: number // 总空间 + spaceUsed: number // 已用空间 + spaceUsable: number // 可用空间 + spaceRatio: number // 空间使用比例 +} + +// 磁盘监控日志 API +export const MonitorDiskApi = { + // 查询磁盘监控日志分页 + getMonitorDiskPage: async (params: any) => { + return await request.get({ url: `/infra/monitor-disk/page`, params }) + }, + + // 查询磁盘监控日志列表 + getMonitorDiskList: async (params: any) => { + return await request.get({ url: `/infra/monitor-disk/getMonitorDiskList`, params }) + }, + + // 查询磁盘监控日志信息 + getMonitorDiskInfo: async (params: any) => { + return await request.get({ url: `/infra/monitor-disk/getMonitorDiskInfo`, params }) + }, + + // 查询磁盘监控日志详情 + getMonitorDisk: async (id: number) => { + return await request.get({ url: `/infra/monitor-disk/get?id=` + id }) + }, + + // 新增磁盘监控日志 + createMonitorDisk: async (data: MonitorDiskVO) => { + return await request.post({ url: `/infra/monitor-disk/create`, data }) + }, + + // 修改磁盘监控日志 + updateMonitorDisk: async (data: MonitorDiskVO) => { + return await request.put({ url: `/infra/monitor-disk/update`, data }) + }, + + // 删除磁盘监控日志 + deleteMonitorDisk: async (id: number) => { + return await request.delete({ url: `/infra/monitor-disk/delete?id=` + id }) + }, + + // 导出磁盘监控日志 Excel + exportMonitorDisk: async (params) => { + return await request.download({ url: `/infra/monitor-disk/export-excel`, params }) + }, +} diff --git a/src/api/infra/monitormem/index.ts b/src/api/infra/monitormem/index.ts new file mode 100644 index 0000000..c402272 --- /dev/null +++ b/src/api/infra/monitormem/index.ts @@ -0,0 +1,56 @@ +import request from '@/config/axios' + +// 内存监控日志 VO +export interface MonitorMemVO { + id: number // 访问ID + hostName: string // 主机名称 + hostIp: string // 服务器ip + serverName: string // 服务名 + physicalTotal: number // 总物理内存 + physicalUsed: number // 已用物理内存 + physicalFree: number // 剩余物理内存 + physicalUsage: number // 物理内存使用率 + runtimeTotal: number // jvm运行总内存 + runtimeMax: number // jvm最大内存 + runtimeUsed: number // jvm已用内存 + runtimeFree: number // jvm空闲内存 + runtimeUsage: number // jvm内存使用率 +} + +// 内存监控日志 API +export const MonitorMemApi = { + // 查询内存监控日志分页 + getMonitorMemPage: async (params: any) => { + return await request.get({ url: `/infra/monitor-mem/page`, params }) + }, + + // 查询统计数据列表 + getMonitorMemList: async (params: any) => { + return await request.get({ url: `/infra/monitor-mem/getMonitorMemList`, params }) + }, + + // 查询内存监控日志详情 + getMonitorMem: async (id: number) => { + return await request.get({ url: `/infra/monitor-mem/get?id=` + id }) + }, + + // 新增内存监控日志 + createMonitorMem: async (data: MonitorMemVO) => { + return await request.post({ url: `/infra/monitor-mem/create`, data }) + }, + + // 修改内存监控日志 + updateMonitorMem: async (data: MonitorMemVO) => { + return await request.put({ url: `/infra/monitor-mem/update`, data }) + }, + + // 删除内存监控日志 + deleteMonitorMem: async (id: number) => { + return await request.delete({ url: `/infra/monitor-mem/delete?id=` + id }) + }, + + // 导出内存监控日志 Excel + exportMonitorMem: async (params) => { + return await request.download({ url: `/infra/monitor-mem/export-excel`, params }) + }, +} diff --git a/src/assets/svgs/member_balance.svg b/src/assets/svgs/member_balance.svg deleted file mode 100644 index 5395b23..0000000 --- a/src/assets/svgs/member_balance.svg +++ /dev/null @@ -1 +0,0 @@ -<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1693028338187" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="22985" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M983.8 312.7C958 251.7 921 197 874 150c-47-47-101.8-83.9-162.7-109.7C648.2 13.5 581.1 0 512 0S375.8 13.5 312.7 40.2C251.7 66 197 102.9 150 150c-47 47-83.9 101.8-109.7 162.7C13.5 375.8 0 442.9 0 512s13.5 136.2 40.2 199.3C66 772.3 102.9 827 150 874c47 47 101.8 83.9 162.7 109.7 63.1 26.7 130.2 40.2 199.3 40.2s136.2-13.5 199.3-40.2C772.3 958 827 921 874 874c47-47 83.9-101.8 109.7-162.7 26.7-63.1 40.2-130.2 40.2-199.3s-13.4-136.2-40.1-199.3z m-55.3 375.2c-22.8 53.8-55.4 102.2-96.9 143.7s-89.9 74.1-143.7 96.9C632.2 952.1 573 964 512 964s-120.2-11.9-175.9-35.5c-53.8-22.8-102.2-55.4-143.7-96.9s-74.1-89.9-96.9-143.7C71.9 632.2 60 573 60 512s11.9-120.2 35.5-175.9c22.8-53.8 55.4-102.2 96.9-143.7s89.9-74.1 143.7-96.9C391.8 71.9 451 60 512 60s120.2 11.9 175.9 35.5c53.8 22.8 102.2 55.4 143.7 96.9s74.1 89.9 96.9 143.7C952.1 391.8 964 451 964 512s-11.9 120.2-35.5 175.9z" fill="#000000" p-id="22986"></path><path d="M706 469.1H574.7l84.2-180.6c7-15 0.4-32.9-14.5-39.9-15-7-32.9-0.4-39.9 14.5L512 461.5l-92.5-198.3c-7-15-24.9-21.5-39.9-14.5s-21.5 24.9-14.5 39.9l84.2 180.6H318c-16.5 0-30 13.5-30 30s13.5 30 30 30h164v64h-92.5c-20.6 0-37.5 13.5-37.5 30s16.9 30 37.5 30H482v95c0 16.5 13.5 30 30 30s30-13.5 30-30v-95h92.5c20.6 0 37.5-13.5 37.5-30s-16.9-30-37.5-30H542v-64h164c16.5 0 30-13.5 30-30 0-16.6-13.5-30.1-30-30.1z" fill="#000000" p-id="22987"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/member_expenditure_balance.svg b/src/assets/svgs/member_expenditure_balance.svg deleted file mode 100644 index 02d498c..0000000 --- a/src/assets/svgs/member_expenditure_balance.svg +++ /dev/null @@ -1 +0,0 @@ -<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1693028553383" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="28918" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M510.72 962.56C262.4 960 61.44 757.76 64 509.44 66.56 263.68 264.96 65.28 510.72 62.72c17.92 0 34.56 14.08 34.56 32s-14.08 34.56-32 34.56h-2.56C299.52 130.56 128 300.8 128 512s171.52 382.72 382.72 382.72S893.44 723.2 893.44 512c0-17.92 16.64-33.28 34.56-32 17.92 0 32 15.36 32 32 0 248.32-200.96 450.56-449.28 450.56z" fill="#000000" p-id="28919"></path><path d="M645.12 480H375.04c-17.92 0-34.56-14.08-34.56-32s14.08-34.56 32-34.56h272.64c17.92 0 33.28 16.64 32 34.56 0 17.92-14.08 32-32 32z m0 130.56H375.04c-17.92 0-33.28-16.64-32-34.56 0-17.92 15.36-32 32-32h270.08c17.92 0 33.28 16.64 32 34.56 0 16.64-14.08 32-32 32z" fill="#000000" p-id="28920"></path><path d="M510.72 746.24c-17.92 0-33.28-15.36-33.28-33.28V441.6c0-17.92 16.64-33.28 34.56-32 17.92 0 32 15.36 32 32v270.08c0 19.2-15.36 34.56-33.28 34.56z" fill="#000000" p-id="28921"></path><path d="M510.72 458.24c-8.96 0-17.92-3.84-24.32-10.24l-111.36-111.36c-14.08-12.8-15.36-33.28-2.56-47.36s33.28-15.36 47.36-2.56l2.56 2.56 111.36 111.36c12.8 12.8 12.8 34.56 0 47.36-6.4 6.4-15.36 10.24-23.04 10.24z" fill="#000000" p-id="28922"></path><path d="M510.72 458.24c-8.96 0-17.92-3.84-24.32-10.24-12.8-12.8-12.8-34.56 0-47.36l111.36-111.36c14.08-12.8 35.84-10.24 47.36 2.56 11.52 12.8 11.52 32 0 44.8L533.76 448c-6.4 6.4-15.36 10.24-23.04 10.24zM925.44 241.92c17.92 0 33.28-15.36 33.28-33.28 0-8.96-3.84-17.92-10.24-24.32l-111.36-111.36c-12.8-14.08-33.28-14.08-47.36-1.28s-14.08 33.28-1.28 47.36l1.28 1.28 111.36 111.36c7.68 6.4 15.36 10.24 24.32 10.24z" fill="#000000" p-id="28923"></path><path d="M815.36 353.28c8.96 0 17.92-3.84 24.32-10.24l111.36-111.36c12.8-14.08 10.24-35.84-2.56-47.36-12.8-11.52-32-11.52-44.8 0l-111.36 111.36c-12.8 12.8-12.8 34.56 0 47.36 5.12 6.4 14.08 10.24 23.04 10.24z" fill="#000000" p-id="28924"></path><path d="M920.32 241.92c17.92 0 34.56-14.08 34.56-32s-14.08-34.56-32-34.56H695.04c-17.92 0-33.28 16.64-32 34.56 0 17.92 15.36 32 32 32h225.28z" fill="#000000" p-id="28925"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/member_level.svg b/src/assets/svgs/member_level.svg deleted file mode 100644 index cbcc686..0000000 --- a/src/assets/svgs/member_level.svg +++ /dev/null @@ -1 +0,0 @@ -<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1693027700643" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8876" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M936.96 385.877333l-203.434667-204.8-18.090667-7.68L308.565333 173.397333l-18.090667 7.68L87.04 385.877333c-9.728 9.898667-9.898667 25.941333-0.170667 35.84l406.869333 421.034667c4.778667 4.949333 11.434667 7.850667 18.432 7.850667 6.997333 0 13.653333-2.901333 18.432-7.850667l406.869333-421.034667C946.858667 411.648 946.688 395.776 936.96 385.877333zM868.522667 389.632l-141.994667 0-163.84-165.034667 141.994667 0L868.522667 389.632zM319.317333 224.768l143.018667 0-163.84 165.034667L155.477333 389.802667 319.317333 224.768zM176.469333 440.832l132.608 0 18.090667-7.509333 185.173333-186.538667 185.173333 186.538667 18.090667 7.509333 131.584 0L512 787.968 176.469333 440.832z" p-id="8877" fill="#000000"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/member_point.svg b/src/assets/svgs/member_point.svg deleted file mode 100644 index b849ddb..0000000 --- a/src/assets/svgs/member_point.svg +++ /dev/null @@ -1 +0,0 @@ -<svg t="1693027780777" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10083" width="128" height="128"><path d="M509.091764 501.653351c241.775532 0 424.086741-78.085426 424.086741-181.63992 0-103.543238-182.311209-181.628664-424.086741-181.628664S84.993766 216.471217 84.993766 320.014454C84.993766 423.568948 267.316232 501.653351 509.091764 501.653351zM509.091764 184.220698c222.908836 0 378.251833 71.561849 378.251833 135.793756S732.001623 455.818443 509.091764 455.818443c-222.920092 0-378.26309-71.573105-378.26309-135.803989S286.171672 184.220698 509.091764 184.220698z" fill="#000000" p-id="10084"></path><path d="M509.083577 694.061522c241.1155 0 422.937568-77.598332 422.937568-180.482561 0-27.169803-13.127995-52.453652-36.241412-75.131141-0.148379-0.153496-0.26606-0.320295-0.418532-0.468674-0.170892-0.166799-0.285502-0.345877-0.456395-0.51063l-0.11461 0.125867c-3.717671-3.40761-8.576329-5.608741-14.017248-5.608741-11.542894 0-20.898982 9.356089-20.898982 20.898982 0 6.110161 2.721994 11.481496 6.901177 15.302521l-0.082888 0.091074c13.948687 14.024411 21.809725 31.154557 21.809725 45.300742 0 64.785515-155.813718 136.966465-379.419426 136.966465-223.595474 0-379.410216-72.180949-379.410216-136.966465 0-16.139585 4.53734-29.952172 22.323425-45.670156 0.213871-0.204661 0.429789-0.381693 0.635473-0.594541 0.137123-0.118704 0.240477-0.233314 0.378623-0.354064l-0.084934-0.080841c3.416819-3.719718 5.623068-8.588609 5.623068-14.037714 0-11.542894-9.356089-20.898982-20.898982-20.898982-5.770424 0-10.993378 2.340301-14.773472 6.119371l-0.122797-0.118704c-23.408129 22.797215-36.594453 48.27754-36.594453 75.635631C86.158289 616.462167 267.979334 694.061522 509.083577 694.061522z" fill="#000000" p-id="10085"></path><path d="M895.577119 629.529787c-0.168846-0.164752-0.282433-0.342808-0.453325-0.50756l-0.11461 0.124843c-3.717671-3.40761-8.577353-5.608741-14.018272-5.608741-11.540847 0-20.897959 9.356089-20.897959 20.898982 0 6.110161 2.720971 11.482519 6.901177 15.302521l-0.083911 0.091074c13.94971 14.024411 21.810748 31.154557 21.810748 45.300742 0 64.787562-155.813718 136.966465-379.419426 136.966465-223.595474 0-379.410216-72.179926-379.410216-136.966465 0-16.139585 4.53734-29.952172 22.321378-45.670156 0.213871-0.202615 0.429789-0.381693 0.635473-0.594541 0.137123-0.118704 0.240477-0.233314 0.378623-0.354064l-0.084934-0.080841c3.416819-3.719718 5.623068-8.588609 5.623068-14.037714 0-11.542894-9.356089-20.898982-20.897959-20.898982-5.770424 0-10.993378 2.340301-14.773472 6.119371l-0.122797-0.118704c-23.410176 22.797215-36.594453 48.278563-36.594453 75.635631 0 102.884228 181.821045 180.482561 422.926312 180.482561 241.114476 0 422.935522-77.598332 422.935522-180.482561 0-27.166733-13.125949-52.452629-36.235272-75.127048C895.851365 629.847012 895.730615 629.681236 895.577119 629.529787z" fill="#000000" p-id="10086"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/member_recharge_balance.svg b/src/assets/svgs/member_recharge_balance.svg deleted file mode 100644 index 7519bb2..0000000 --- a/src/assets/svgs/member_recharge_balance.svg +++ /dev/null @@ -1 +0,0 @@ -<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1693028440322" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="25843" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M512 750.509317c-19.080745 0-31.801242-12.720497-31.801242-31.801242L480.198758 432.496894c0-19.080745 12.720497-31.801242 31.801242-31.801242s31.801242 12.720497 31.801242 31.801242l0 286.21118C537.440994 737.78882 524.720497 750.509317 512 750.509317z" fill="#000000" p-id="25844"></path><path d="M651.925466 534.26087 365.714286 534.26087c-19.080745 0-31.801242-12.720497-31.801242-31.801242 0-19.080745 12.720497-31.801242 31.801242-31.801242l286.21118 0c19.080745 0 31.801242 12.720497 31.801242 31.801242C683.726708 521.540373 671.006211 534.26087 651.925466 534.26087z" fill="#000000" p-id="25845"></path><path d="M651.925466 648.745342 365.714286 648.745342c-19.080745 0-31.801242-12.720497-31.801242-31.801242 0-19.080745 12.720497-31.801242 31.801242-31.801242l286.21118 0c19.080745 0 31.801242 12.720497 31.801242 31.801242C683.726708 636.024845 671.006211 648.745342 651.925466 648.745342z" fill="#000000" p-id="25846"></path><path d="M512 464.298137c-6.360248 0-19.080745 0-25.440994-6.360248L352.993789 324.372671c-12.720497-12.720497-12.720497-31.801242 0-44.521739 12.720497-12.720497 31.801242-12.720497 44.521739 0l133.565217 133.565217c12.720497 12.720497 12.720497 31.801242 0 44.521739C524.720497 464.298137 518.360248 464.298137 512 464.298137z" fill="#000000" p-id="25847"></path><path d="M512 464.298137c-6.360248 0-19.080745 0-25.440994-6.360248-12.720497-12.720497-12.720497-31.801242 0-44.521739l133.565217-133.565217c12.720497-12.720497 31.801242-12.720497 44.521739 0 12.720497 12.720497 12.720497 31.801242 0 44.521739L531.080745 457.937888C524.720497 464.298137 518.360248 464.298137 512 464.298137z" fill="#000000" p-id="25848"></path><path d="M512 1017.639752c-279.850932 0-508.819876-228.968944-508.819876-508.819876s228.968944-508.819876 508.819876-508.819876 508.819876 228.968944 508.819876 508.819876c0 25.440994 0 50.881988-6.360248 82.68323 0 19.080745-19.080745 31.801242-38.161491 25.440994-19.080745 0-31.801242-19.080745-25.440994-38.161491 6.360248-25.440994 6.360248-44.521739 6.360248-69.962733 0-248.049689-197.167702-445.217391-445.217391-445.217391S66.782609 267.130435 66.782609 515.180124s197.167702 445.217391 445.217391 445.217391c25.440994 0 57.242236 0 82.68323-6.360248 19.080745-6.360248 31.801242 6.360248 38.161491 25.440994 6.360248 19.080745-6.360248 31.801242-25.440994 38.161491C575.602484 1017.639752 543.801242 1017.639752 512 1017.639752z" fill="#000000" p-id="25849"></path><path d="M989.018634 864.993789l-318.012422 0c-19.080745 0-31.801242-12.720497-31.801242-31.801242s12.720497-31.801242 31.801242-31.801242l318.012422 0c19.080745 0 31.801242 12.720497 31.801242 31.801242S1001.73913 864.993789 989.018634 864.993789z" fill="#000000" p-id="25850"></path><path d="M830.012422 1024c-19.080745 0-31.801242-12.720497-31.801242-31.801242l0-318.012422c0-19.080745 12.720497-31.801242 31.801242-31.801242s31.801242 12.720497 31.801242 31.801242l0 318.012422C861.813665 1004.919255 842.732919 1024 830.012422 1024z" fill="#000000" p-id="25851"></path></svg> \ No newline at end of file diff --git a/src/assets/svgs/money.svg b/src/assets/svgs/money.svg deleted file mode 100644 index c1580de..0000000 --- a/src/assets/svgs/money.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M54.122 127.892v-28.68H7.513V87.274h46.609v-12.4H7.513v-12.86h38.003L.099 0h22.6l32.556 45.07c3.617 5.144 6.44 9.611 8.487 13.385 1.788-3.05 4.89-7.779 9.301-14.186L103.93 0h24.01L82.385 62.013h38.34v12.862h-46.41v12.4h46.41v11.937h-46.41v28.68H54.123z"/></svg> \ No newline at end of file diff --git a/src/assets/svgs/shopping.svg b/src/assets/svgs/shopping.svg deleted file mode 100644 index f395bc7..0000000 --- a/src/assets/svgs/shopping.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M42.913 101.36c1.642 0 3.198.332 4.667.996a12.28 12.28 0 013.89 2.772c1.123 1.184 1.987 2.582 2.592 4.193.605 1.612.908 3.318.908 5.118 0 1.8-.303 3.507-.908 5.118-.605 1.611-1.469 3.01-2.593 4.194a13.3 13.3 0 01-3.889 2.843 10.582 10.582 0 01-4.667 1.066c-1.729 0-3.306-.355-4.732-1.066a13.604 13.604 0 01-3.825-2.843c-1.123-1.185-1.988-2.583-2.593-4.194a14.437 14.437 0 01-.907-5.118c0-1.8.302-3.506.907-5.118.605-1.61 1.47-3.009 2.593-4.193a12.515 12.515 0 013.825-2.772c1.426-.664 3.003-.996 4.732-.996zm53.932.285c1.643 0 3.22.331 4.733.995a11.386 11.386 0 013.889 2.772c1.08 1.185 1.945 2.583 2.593 4.194.648 1.61.972 3.317.972 5.118 0 1.8-.324 3.506-.972 5.117-.648 1.611-1.513 3.01-2.593 4.194a12.253 12.253 0 01-3.89 2.843 11 11 0 01-4.732 1.066 10.58 10.58 0 01-4.667-1.066 12.478 12.478 0 01-3.824-2.843c-1.08-1.185-1.945-2.583-2.593-4.194a13.581 13.581 0 01-.973-5.117c0-1.801.325-3.507.973-5.118.648-1.611 1.512-3.01 2.593-4.194a11.559 11.559 0 013.824-2.772 11.212 11.212 0 014.667-.995zm21.781-80.747c2.42 0 4.3.355 5.64 1.066 1.34.71 2.29 1.587 2.852 2.63a6.427 6.427 0 01.778 3.34c-.044 1.185-.195 2.204-.454 3.057-.26.853-.8 2.606-1.62 5.26a589.268 589.268 0 01-2.788 8.743 1236.373 1236.373 0 00-3.047 9.453c-.994 3.128-1.75 5.592-2.269 7.393-1.123 3.79-2.55 6.42-4.278 7.89-1.728 1.469-3.846 2.203-6.352 2.203H39.023l1.945 12.795h65.342c4.148 0 6.223 1.943 6.223 5.828 0 1.896-.41 3.53-1.232 4.905-.821 1.374-2.442 2.061-4.862 2.061H38.505c-1.729 0-3.176-.426-4.343-1.28-1.167-.852-2.14-1.966-2.917-3.34a21.277 21.277 0 01-1.88-4.478 44.128 44.128 0 01-1.102-4.55c-.087-.568-.324-1.942-.713-4.122-.39-2.18-.865-4.904-1.426-8.174l-1.88-10.947c-.692-4.027-1.383-8.079-2.075-12.154-1.642-9.572-3.5-20.234-5.574-31.986H6.87c-1.296 0-2.377-.356-3.24-1.067a9.024 9.024 0 01-2.14-2.558 10.416 10.416 0 01-1.167-3.2C.108 8.53 0 7.488 0 6.54c0-1.896.583-3.46 1.75-4.69C2.917.615 4.494 0 6.482 0h13.095c1.728 0 3.111.284 4.148.853 1.037.569 1.858 1.28 2.463 2.132a8.548 8.548 0 011.297 2.701c.26.948.475 1.754.648 2.417.173.758.346 1.825.519 3.199.173 1.374.345 2.772.518 4.193.26 1.706.519 3.507.778 5.403h88.678z"/></svg> \ No newline at end of file diff --git a/src/components/MonitorDiskPie/PieChart.vue b/src/components/MonitorDiskPie/PieChart.vue new file mode 100644 index 0000000..b7ba35e --- /dev/null +++ b/src/components/MonitorDiskPie/PieChart.vue @@ -0,0 +1,35 @@ +<template> + <svg :width="size" :height="size" viewBox="0 0 100 100"> + <!-- 背景圆 --> + <circle cx="50" cy="50" r="50" fill="#eee"/> + <!-- 使用率扇形 --> + <path :d="arcPath" fill="#1C134B"/> + </svg> +</template> + +<script setup> +import { computed } from 'vue'; + +const props = defineProps({ + used: { type: Number, required: true }, + total: { type: Number, required: true }, + size: { type: Number, default: 150 } +}); + +const percentage = computed(() => { + if (props.total === 0) return 0; + return (props.used / props.total) * 100; +}); + +const arcPath = computed(() => { + if (percentage.value >= 100) return ''; + + const angle = (percentage.value * 360) / 100; + const radians = (angle - 90) * Math.PI / 180; + const x = 50 + 50 * Math.cos(radians); + const y = 50 + 50 * Math.sin(radians); + const largeArc = angle > 180 ? 1 : 0; + + return `M 50 50 L 50 0 A 50 50 0 ${largeArc} 1 ${x} ${y} L 50 50 Z`; +}); +</script> diff --git a/src/store/modules/mall/kefu.ts b/src/store/modules/mall/kefu.ts new file mode 100644 index 0000000..2aecee0 --- /dev/null +++ b/src/store/modules/mall/kefu.ts @@ -0,0 +1,81 @@ +import { store } from '@/store' +import { defineStore } from 'pinia' +import { KeFuConversationApi, KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation' +import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message' +import { isEmpty } from '@/utils/is' + +interface MallKefuInfoVO { + conversationList: KeFuConversationRespVO[] // 会话列表 + conversationMessageList: Map<number, KeFuMessageRespVO[]> // 会话消息 +} + +export const useMallKefuStore = defineStore('mall-kefu', { + state: (): MallKefuInfoVO => ({ + conversationList: [], + conversationMessageList: new Map<number, KeFuMessageRespVO[]>() // key 会话,value 会话消息列表 + }), + getters: { + getConversationList(): KeFuConversationRespVO[] { + return this.conversationList + }, + getConversationMessageList(): (conversationId: number) => KeFuMessageRespVO[] | undefined { + return (conversationId: number) => this.conversationMessageList.get(conversationId) + } + }, + actions: { + // ======================= 会话消息相关 ======================= + /** 缓存历史消息 */ + saveMessageList(conversationId: number, messageList: KeFuMessageRespVO[]) { + this.conversationMessageList.set(conversationId, messageList) + }, + + // ======================= 会话相关 ======================= + /** 加载会话缓存列表 */ + async setConversationList() { + this.conversationList = await KeFuConversationApi.getConversationList() + this.conversationSort() + }, + /** 更新会话缓存已读 */ + async updateConversationStatus(conversationId: number) { + if (isEmpty(this.conversationList)) { + return + } + const conversation = this.conversationList.find((item) => item.id === conversationId) + conversation && (conversation.adminUnreadMessageCount = 0) + }, + /** 更新会话缓存 */ + async updateConversation(conversationId: number) { + if (isEmpty(this.conversationList)) { + return + } + + const conversation = await KeFuConversationApi.getConversation(conversationId) + this.deleteConversation(conversationId) + conversation && this.conversationList.push(conversation) + this.conversationSort() + }, + /** 删除会话缓存 */ + deleteConversation(conversationId: number) { + const index = this.conversationList.findIndex((item) => item.id === conversationId) + // 存在则删除 + if (index > -1) { + this.conversationList.splice(index, 1) + } + }, + conversationSort() { + // 按置顶属性和最后消息时间排序 + this.conversationList.sort((a, b) => { + // 按照置顶排序,置顶的会在前面 + if (a.adminPinned !== b.adminPinned) { + return a.adminPinned ? -1 : 1 + } + // 按照最后消息时间排序,最近的会在前面 + return (b.lastMessageTime as unknown as number) - (a.lastMessageTime as unknown as number) + }) + } + } +}) + +export const useMallKefuStoreWithOut = () => { + return useMallKefuStore(store) +} diff --git a/src/views/infra/monitor/components/MonitorDisk.vue b/src/views/infra/monitor/components/MonitorDisk.vue new file mode 100644 index 0000000..ffec629 --- /dev/null +++ b/src/views/infra/monitor/components/MonitorDisk.vue @@ -0,0 +1,487 @@ +<template> + <ContentWrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > + <!-- <el-form-item label="主机名称" prop="hostName">--> + <!-- <el-input--> + <!-- v-model="queryParams.hostName"--> + <!-- placeholder="请输入主机名称"--> + <!-- clearable--> + <!-- @keyup.enter="handleQuery"--> + <!-- class="!w-240px"--> + <!-- />--> + <!-- </el-form-item>--> + <el-form-item label="服务器IP" prop="hostIp"> + <el-input + v-model="queryParams.hostIp" + placeholder="请输入服务器IP" + clearable + @keyup.enter="handleQuery" + class="!w-120px" + /> + </el-form-item> + <!-- <el-form-item label="盘符" prop="disk">--> + <!-- <el-input--> + <!-- v-model="queryParams.disk"--> + <!-- placeholder="请输入盘符"--> + <!-- clearable--> + <!-- @keyup.enter="handleQuery"--> + <!-- class="!w-240px"--> + <!-- />--> + <!-- </el-form-item>--> + <el-form-item label="磁盘名" prop="diskName"> + <el-input + v-model="queryParams.diskName" + placeholder="请输入磁盘名" + clearable + @keyup.enter="handleQuery" + class="!w-120px" + /> + </el-form-item> + <el-form-item label="创建时间" prop="createTime"> + <el-date-picker + v-model="queryParams.createTime" + value-format="YYYY-MM-DD HH:mm:ss" + type="datetimerange" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-360px" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"> + <Icon icon="ep:search" class="mr-5px"/> + 搜索 + </el-button> + <el-button @click="resetQuery"> + <Icon icon="ep:refresh" class="mr-5px"/> + 重置 + </el-button> + <el-button + type="primary" + plain + @click="openForm('create')" + v-hasPermi="['infra:monitor-disk:create']" + > + <Icon icon="ep:plus" class="mr-5px"/> + 新增 + </el-button> + <el-button + type="success" + plain + @click="handleExport" + :loading="exportLoading" + v-hasPermi="['infra:monitor-disk:export']" + > + <Icon icon="ep:download" class="mr-5px"/> + 导出 + </el-button> + </el-form-item> + <el-form-item style="float: right"> + <el-button + v-if="showType == 'chart'" + type="warning" + style="font-weight: bold" + plain + @click="switchShow('data')"> + <Icon icon="fa-solid:th-list" class="mr-5px"/> + 列表展示 + </el-button> + <el-button + v-if="showType == 'data'" + type="danger" + style="font-weight: bold" + plain + @click="switchShow('chart')"> + <Icon icon="fa-solid:chart-pie" class="mr-5px"/> + 图例展示 + </el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <ContentWrap v-if="showType == 'chart'"> + <!-- 磁盘使用率折线图 --> + <el-skeleton :loading="echartsLoading" animated> + <Echart :height="320" :options="diskChartOptions"/> + </el-skeleton> + <!-- 磁盘使用率饼图 --> + <h3 style="margin-top: 20px; margin-bottom: 10px">主机磁盘使用率</h3> + <div v-for="host in hostList" :key="host.name" class="host"> + <div class="host-child"> + <h4>主机名:{{ host.name }} 主机IP:{{ host.ip }}</h4> + <el-skeleton :loading="echartsLoading" animated> + <div class="disks"> + <div v-for="disk in host.disks" :key="disk.name" class="disk"> + <h4 id="diskTitle">{{ disk.disk }}</h4> + <PieChart :used="disk.used" :total="disk.total" /> + <div class="disk-info"> + <div style="margin-bottom: 6px; font-size: 16px"><span style="color: #b9292b ;font-weight: bolder">{{ disk.total != 0 ? ((disk.used / disk.total) * 100).toFixed(1) : 0.0 }}% </span>已使用</div> + <div style="font-weight: bolder">{{ disk.used }}GB / {{ disk.total }}GB</div> + </div> + </div> + </div> + </el-skeleton> + </div> + </div> +<!-- <div v-for="(host, hostIndex) in hostList" :key="hostIndex">--> +<!-- <div style="margin-top: 10px">--> +<!-- <el-skeleton :loading="echartsLoading" animated>--> +<!-- {{ hostIndex }} 主机名: {{ host.name }} --> +<!-- 服务器IP:{{ host.ip }}--> +<!-- <div v-for="(disk, diskIndex) in host.disks" :key="diskIndex">--> +<!-- <h3>{{ disk.name }}</h3>--> +<!-- <div :ref="el => chartRefs[hostIndex][diskIndex] = el"--> +<!-- :style="{ width: '300px', height: '300px' }"></div>--> +<!-- </div>--> +<!-- </el-skeleton>--> +<!-- </div>--> +<!-- </div>--> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap v-if="showType == 'data'"> + <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> + <el-table-column label="主机名称" align="center" prop="hostName"/> + <el-table-column label="服务器ip" align="center" prop="hostIp"/> + <el-table-column label="盘符" align="center" prop="disk"/> + <el-table-column label="磁盘名" align="center" prop="diskName"/> + <el-table-column label="总空间" align="center" prop="spaceTotal"/> + <el-table-column label="已用空间" align="center" prop="spaceUsed"/> + <el-table-column label="可用空间" align="center" prop="spaceUsable"/> + <el-table-column label="空间使用比例" align="center" prop="spaceRatio"/> + <el-table-column + label="创建时间" + align="center" + prop="createTime" + :formatter="dateFormatter" + width="180px" + /> + <el-table-column label="操作" align="center"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openForm('update', scope.row.id)" + v-hasPermi="['infra:monitor-disk:query']" + > + 详情 + </el-button> + <el-button + link + type="danger" + @click="handleDelete(scope.row.id)" + v-hasPermi="['infra:monitor-disk:delete']" + > + 删除 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + + <!-- 表单弹窗:添加/修改 --> + <MonitorDiskForm ref="formRef" @success="getList"/> +</template> + +<script setup lang="ts"> +import {dateFormatter} from '@/utils/formatTime' +import download from '@/utils/download' +import {MonitorDiskApi, MonitorDiskVO} from '@/api/infra/monitordisk' +import MonitorDiskForm from './MonitorDiskForm.vue' +import {EChartsOption} from "echarts"; +import * as echarts from 'echarts'; +import {formatTime} from "@/utils"; +import {formatDate} from "@vueuse/core"; +import PieChart from '@/components/MonitorDiskPie/PieChart.vue'; + +/** 磁盘监控日志 列表 */ +defineOptions({name: 'MonitorDisk'}) + +const message = useMessage() // 消息弹窗 +const {t} = useI18n() // 国际化 + +const loading = ref(true) // 列表的加载中 +const list = ref<MonitorDiskVO[]>([]) // 列表的数据 +const total = ref(0) // 列表的总页数 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + hostName: undefined, + hostIp: undefined, + disk: undefined, + diskName: undefined, + spaceTotal: undefined, + spaceUsed: undefined, + spaceUsable: undefined, + spaceRatio: undefined, + createTime: [], +}) +const queryFormRef = ref() // 搜索的表单 +const exportLoading = ref(false) // 导出的加载中 +const echartsLoading = ref(true) // 图表加载中 +const showType = ref() //展示类型(chart-图例,data-数据) + +const hostList = ref([ + { + name: 'Thinkpad-E14', + ip: '172.16.216.133', + disks: [ + {disk: '磁盘C', used: 70, total: 200}, + {disk: '磁盘D', used: 40, total: 60} + ] + }, + { + name: 'Thinkpad-E16', + ip: '172.16.216.133', + disks: [ + {disk: '磁盘C', used: 80, total: 500}, + {disk: '磁盘D', used: 20, total: 500} + ] + } +]); + +const chartRefs = ref([]); + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await MonitorDiskApi.getMonitorDiskPage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } +} + +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + if (showType.value == 'data') { + getList() + } else { + getMonitorDiskDataList() + usedDiskInstance() + } +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() +} + +/** 添加/修改操作 */ +const formRef = ref() +const openForm = (type: string, id?: number) => { + formRef.value.open(type, id) +} + +/** 删除按钮操作 */ +const handleDelete = async (id: number) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await MonitorDiskApi.deleteMonitorDisk(id) + message.success(t('common.delSuccess')) + // 刷新列表 + await getList() + } catch { + } +} + +/** 导出按钮操作 */ +const handleExport = async () => { + try { + // 导出的二次确认 + await message.exportConfirm() + // 发起导出 + exportLoading.value = true + const data = await MonitorDiskApi.exportMonitorDisk(queryParams) + download.excel(data, '磁盘监控日志.xls') + } catch { + } finally { + exportLoading.value = false + } +} + +/** 堆叠面积图配置 */ +const diskChartOptions = reactive<EChartsOption>({ + title: { + text: '磁盘空间折线图' + }, + dataset: { + dimensions: [], + source: [] + }, + grid: { + left: 30, + right: 20, + bottom: 10, + top: 70, + containLabel: true + }, + legend: { + top: 0 + }, + series: [ + { + name: 'disk', type: 'line', + emphasis: { + focus: 'series' + }, smooth: false + } + ], + toolbox: { + feature: { + // 数据区域缩放 + dataZoom: { + yAxisIndex: false // Y轴不缩放 + }, + brush: { + type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮 + }, + saveAsImage: {show: true, name: '物理内存日志图片'} // 保存为图片 + } + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'cross', + label: { + backgroundColor: '#6a7985' + } + }, + padding: [5, 10] + }, + xAxis: { + type: 'category', + boundaryGap: false, + axisTick: { + show: false + } + }, + yAxis: { + name: "单位(百分比)", + nameTextStyle: { + color: "#aaa", + nameLocation: "start", + }, + }, +}) as EChartsOption + +/** 查询统计数据列表 */ +const getMonitorDiskDataList = async () => { + const list = await MonitorDiskApi.getMonitorDiskList(queryParams) + if (list != null && list != undefined && list.length > 0) { + diskChartOptions.dataset['dimensions'] = Object.keys(list[0]) + diskChartOptions.series = diskChartOptions.dataset['dimensions'].map(item => ({ + name: item.name, + type: 'line', + emphasis: { + focus: 'series' + }, + smooth: false + })); + diskChartOptions.series.splice(0, 1) + for (let item of list) { + item.createTime = formatTime(item.createTime, 'yyyy-MM-dd HH:mm:ss') + } + } + // 更新 Echarts 数据 + diskChartOptions.dataset['source'] = list + echartsLoading.value = false +} + +const usedDiskInstance = async () => { + const list = await MonitorDiskApi.getMonitorDiskInfo(queryParams) + hostList.value = list + // 仪表盘详情,用于显示数据。 +} + +/** 切换展示方式 */ +const switchShow = (type: String) => { + showType.value = type + if (showType.value == 'data') { + getList() + } else { + getMonitorDiskDataList() + usedDiskInstance() + } +} + +let intervalId; +/** 初始化 **/ +onMounted(() => { + showType.value = 'data'; + const currentDate = new Date(); + const previousDate = new Date(currentDate); + previousDate.setDate(currentDate.getDate() - 1); + queryParams.createTime[0] = formatDate(previousDate, 'YYYY-MM-DD HH:mm:ss'); + queryParams.createTime[1] = formatDate(currentDate, 'YYYY-MM-DD HH:mm:ss'); + intervalId = setInterval(() => { + if (showType.value == 'data') { + getList() + } else { + getMonitorDiskDataList() + usedDiskInstance() + } + }, 30000); +}) + +onUnmounted(() => { + clearInterval(intervalId); +}); +</script> +<style> + .host { + margin-bottom: 20px; + margin-right: 20px; + border-radius: 8px; + } + .host-child { + background: rgba(200, 200, 200, 0.3); + border-radius: 8px; + padding: 10px; + } + .disks { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); + gap: 20px; + margin-top: 20px; + } + .disk { + width: 250px; + background: rgba(100, 100, 150, 0.2); + padding: 15px; + border-radius: 16px; + text-align: center; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + } + + #diskTitle { + margin-bottom: 10px + } + + .disk-info { + margin-top: 10px; + font-size: 0.9em; + color: #666; + } +</style> diff --git a/src/views/infra/monitor/components/MonitorDiskForm.vue b/src/views/infra/monitor/components/MonitorDiskForm.vue new file mode 100644 index 0000000..aeedc9c --- /dev/null +++ b/src/views/infra/monitor/components/MonitorDiskForm.vue @@ -0,0 +1,128 @@ +<template> + <Dialog :title="dialogTitle" v-model="dialogVisible"> + <el-form + ref="formRef" + :model="formData" + :rules="formRules" + label-width="100px" + v-loading="formLoading" + > + <el-form-item label="主机名称" prop="hostName"> + <el-input v-model="formData.hostName" placeholder="请输入主机名称" /> + </el-form-item> + <el-form-item label="服务器ip" prop="hostIp"> + <el-input v-model="formData.hostIp" placeholder="请输入服务器ip" /> + </el-form-item> + <el-form-item label="盘符" prop="disk"> + <el-input v-model="formData.disk" placeholder="请输入盘符" /> + </el-form-item> + <el-form-item label="磁盘名" prop="diskName"> + <el-input v-model="formData.diskName" placeholder="请输入磁盘名" /> + </el-form-item> + <el-form-item label="总空间" prop="spaceTotal"> + <el-input v-model="formData.spaceTotal" placeholder="请输入总空间" /> + </el-form-item> + <el-form-item label="已用空间" prop="spaceUsed"> + <el-input v-model="formData.spaceUsed" placeholder="请输入已用空间" /> + </el-form-item> + <el-form-item label="可用空间" prop="spaceUsable"> + <el-input v-model="formData.spaceUsable" placeholder="请输入可用空间" /> + </el-form-item> + <el-form-item label="空间使用比例" prop="spaceRatio"> + <el-input v-model="formData.spaceRatio" placeholder="请输入空间使用比例" /> + </el-form-item> + </el-form> + <template #footer> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="dialogVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import { MonitorDiskApi, MonitorDiskVO } from '@/api/infra/monitordisk' + +/** 磁盘监控日志 表单 */ +defineOptions({ name: 'MonitorDiskForm' }) + +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const dialogVisible = ref(false) // 弹窗的是否展示 +const dialogTitle = ref('') // 弹窗的标题 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formType = ref('') // 表单的类型:create - 新增;update - 修改 +const formData = ref({ + id: undefined, + hostName: undefined, + hostIp: undefined, + disk: undefined, + diskName: undefined, + spaceTotal: undefined, + spaceUsed: undefined, + spaceUsable: undefined, + spaceRatio: undefined, +}) +const formRules = reactive({ + hostName: [{ required: true, message: '主机名称不能为空', trigger: 'blur' }], + hostIp: [{ required: true, message: '服务器ip不能为空', trigger: 'blur' }], +}) +const formRef = ref() // 表单 Ref + +/** 打开弹窗 */ +const open = async (type: string, id?: number) => { + dialogVisible.value = true + dialogTitle.value = t('action.' + type) + formType.value = type + resetForm() + // 修改时,设置数据 + if (id) { + formLoading.value = true + try { + formData.value = await MonitorDiskApi.getMonitorDisk(id) + } finally { + formLoading.value = false + } + } +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + await formRef.value.validate() + // 提交请求 + formLoading.value = true + try { + const data = formData.value as unknown as MonitorDiskVO + if (formType.value === 'create') { + await MonitorDiskApi.createMonitorDisk(data) + message.success(t('common.createSuccess')) + } else { + await MonitorDiskApi.updateMonitorDisk(data) + message.success(t('common.updateSuccess')) + } + dialogVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + id: undefined, + hostName: undefined, + hostIp: undefined, + disk: undefined, + diskName: undefined, + spaceTotal: undefined, + spaceUsed: undefined, + spaceUsable: undefined, + spaceRatio: undefined, + } + formRef.value?.resetFields() +} +</script> \ No newline at end of file diff --git a/src/views/infra/monitor/components/MonitorMem.vue b/src/views/infra/monitor/components/MonitorMem.vue new file mode 100644 index 0000000..768af0e --- /dev/null +++ b/src/views/infra/monitor/components/MonitorMem.vue @@ -0,0 +1,478 @@ +<template> + <ContentWrap> + <!-- 搜索工作栏 --> + <el-form + class="-mb-15px" + :model="queryParams" + ref="queryFormRef" + :inline="true" + label-width="68px" + > +<!-- <el-form-item label="主机名称" prop="hostName">--> +<!-- <el-input--> +<!-- v-model="queryParams.hostName"--> +<!-- placeholder="请输入主机名称"--> +<!-- clearable--> +<!-- @keyup.enter="handleQuery"--> +<!-- class="!w-120px"--> +<!-- />--> +<!-- </el-form-item>--> + <el-form-item label="服务器IP" prop="hostIp"> + <el-input + v-model="queryParams.hostIp" + placeholder="请输入服务器IP" + clearable + @keyup.enter="handleQuery" + class="!w-120px" + /> + </el-form-item> + <el-form-item label="服务名" prop="serverName"> + <el-input + v-model="queryParams.serverName" + placeholder="请输入服务名" + clearable + @keyup.enter="handleQuery" + class="!w-120px" + /> + </el-form-item> + <el-form-item label="创建时间" prop="createTime"> + <el-date-picker + v-model="queryParams.createTime" + value-format="YYYY-MM-DD HH:mm:ss" + type="datetimerange" + start-placeholder="开始日期" + end-placeholder="结束日期" + :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" + class="!w-360px" + /> + </el-form-item> + <el-form-item> + <el-button @click="handleQuery"> + <Icon icon="ep:search" class="mr-5px"/> + 搜索 + </el-button> + <el-button @click="resetQuery"> + <Icon icon="ep:refresh" class="mr-5px"/> + 重置 + </el-button> + <el-button + type="primary" + plain + @click="openForm('create')" + v-hasPermi="['infra:monitor-mem:create']" + > + <Icon icon="ep:plus" class="mr-5px"/> + 新增 + </el-button> + <el-button + type="success" + plain + @click="handleExport" + :loading="exportLoading" + v-hasPermi="['infra:monitor-mem:export']" + > + <Icon icon="ep:download" class="mr-5px"/> + 导出 + </el-button> + </el-form-item> + <el-form-item style="float: right"> + <el-button + v-if="showType == 'chart'" + type="warning" + style="font-weight: bold" + plain + @click="switchShow('data')"> + <Icon icon="fa-solid:th-list" class="mr-5px"/> + 列表展示 + </el-button> + <el-button + v-if="showType == 'data'" + type="danger" + style="font-weight: bold" + plain + @click="switchShow('chart')"> + <Icon icon="fa-solid:chart-pie" class="mr-5px"/> + 图例展示 + </el-button> + </el-form-item> + </el-form> + </ContentWrap> + + <ContentWrap v-if="showType == 'chart'"> + <!-- 物理内存折线图 --> + <el-skeleton :loading="echartsLoading" animated> + <Echart :height="320" :options="physicalChartOptions"/> + </el-skeleton> + <!-- JVM内存折线图 --> + <el-skeleton :loading="echartsLoading" animated> + <Echart style="margin-top: 20px" :height="320" :options="JVMChartOptions"/> + </el-skeleton> + </ContentWrap> + + <!-- 列表 --> + <ContentWrap v-if="showType == 'data'"> + <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> + <el-table-column label="主机名称" align="center" prop="hostName"/> + <el-table-column label="服务器ip" align="center" prop="hostIp"/> + <el-table-column label="服务名" align="center" prop="serverName" width="120"/> + <el-table-column label="总内存" align="center" prop="physicalTotal"/> + <el-table-column label="已用内存" align="center" prop="physicalUsed"/> + <el-table-column label="空闲内存" align="center" prop="physicalFree"/> + <el-table-column label="内存使用率" align="center" prop="physicalUsage" width="100"/> + <el-table-column label="JVM占用内存" align="center" prop="runtimeTotal"/> + <el-table-column label="JVM最大内存" align="center" prop="runtimeMax"/> + <el-table-column label="JVM可用内存" align="center" prop="runtimeUsed"/> + <el-table-column label="JVM空闲内存" align="center" prop="runtimeFree"/> + <el-table-column label="JVM内存使用率" align="center" prop="runtimeUsage"/> + <el-table-column + label="创建时间" + align="center" + prop="createTime" + :formatter="dateFormatter" + width="180px" + /> + <el-table-column label="操作" align="center"> + <template #default="scope"> + <el-button + link + type="primary" + @click="openForm('update', scope.row.id)" + v-hasPermi="['infra:monitor-mem:query']" + > + 详情 + </el-button> + <el-button + link + type="danger" + @click="handleDelete(scope.row.id)" + v-hasPermi="['infra:monitor-mem:delete']" + > + 删除 + </el-button> + </template> + </el-table-column> + </el-table> + <!-- 分页 --> + <Pagination + :total="total" + v-model:page="queryParams.pageNo" + v-model:limit="queryParams.pageSize" + @pagination="getList" + /> + </ContentWrap> + + <!-- 表单弹窗:添加/修改 --> + <MonitorMemForm ref="formRef" @success="getList"/> +</template> + +<script setup lang="ts"> +import {dateFormatter} from '@/utils/formatTime' +import download from '@/utils/download' +import {MonitorMemApi, MonitorMemVO} from '@/api/infra/monitormem' +import MonitorMemForm from './MonitorMemForm.vue' +import {EChartsOption} from "echarts"; +import {formatTime} from "@/utils"; +import {formatDate} from "@vueuse/core"; + +/** 内存监控日志 列表 */ +defineOptions({name: 'MonitorMem'}) + +const message = useMessage() // 消息弹窗 +const {t} = useI18n() // 国际化 + +const loading = ref(true) // 列表的加载中 +const list = ref<MonitorMemVO[]>([]) // 列表的数据 +const total = ref(0) // 列表的总页数 +const queryParams = reactive({ + pageNo: 1, + pageSize: 10, + hostName: undefined, + hostIp: undefined, + serverName: undefined, + physicalTotal: undefined, + physicalUsed: undefined, + physicalFree: undefined, + physicalUsage: undefined, + runtimeTotal: undefined, + runtimeMax: undefined, + runtimeUsed: undefined, + runtimeFree: undefined, + runtimeUsage: undefined, + createTime: [], +}) +const queryFormRef = ref() // 搜索的表单 +const exportLoading = ref(false) // 导出的加载中 +const echartsLoading = ref(true) // 图表加载中 +const showType = ref() //展示类型(chart-图例,data-数据) + +/** 查询列表 */ +const getList = async () => { + loading.value = true + try { + const data = await MonitorMemApi.getMonitorMemPage(queryParams) + list.value = data.list + total.value = data.total + } finally { + loading.value = false + } +} + +/** 搜索按钮操作 */ +const handleQuery = () => { + queryParams.pageNo = 1 + if(showType.value == 'data') { + getList() + } else { + getMonitorMemDataList() + } +} + +/** 重置按钮操作 */ +const resetQuery = () => { + queryFormRef.value.resetFields() + handleQuery() +} + +/** 添加/修改操作 */ +const formRef = ref() +const openForm = (type: string, id?: number) => { + formRef.value.open(type, id) +} + +/** 删除按钮操作 */ +const handleDelete = async (id: number) => { + try { + // 删除的二次确认 + await message.delConfirm() + // 发起删除 + await MonitorMemApi.deleteMonitorMem(id) + message.success(t('common.delSuccess')) + // 刷新列表 + await getList() + } catch { + } +} + +/** 堆叠面积图配置 */ +const physicalChartOptions = reactive<EChartsOption>({ + title: { + text: '物理内存折线图' + }, + dataset: { + dimensions: ['createTime', 'physicalTotal', 'physicalUsed', 'physicalFree'], + source: [] + }, + grid: { + left: 20, + right: 20, + bottom: 10, + top: 70, + containLabel: true + }, + legend: { + top: 0 + }, + series: [ + { + name: '总物理内存', type: 'line', + emphasis: { + focus: 'series' + }, smooth: false + }, + { + name: '已用物理内存', type: 'line', areaStyle: {}, + emphasis: { + focus: 'series' + }, smooth: false + }, + { + name: '剩余物理内存', type: 'line', areaStyle: {}, + emphasis: { + focus: 'series' + }, smooth: false + } + ], + toolbox: { + feature: { + // 数据区域缩放 + dataZoom: { + yAxisIndex: false // Y轴不缩放 + }, + brush: { + type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮 + }, + saveAsImage: {show: true, name: '物理内存日志图片'} // 保存为图片 + } + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'cross', + label: { + backgroundColor: '#6a7985' + } + }, + padding: [5, 10] + }, + xAxis: { + type: 'category', + boundaryGap: false, + axisTick: { + show: false + } + }, + yAxis: { + name: "单位(MB)", + nameTextStyle: { + color: "#aaa", + nameLocation: "start", + }, + }, +}) as EChartsOption + +/** 堆叠面积图配置 */ +const JVMChartOptions = reactive<EChartsOption>({ + title: { + text: 'JVM内存折线图' + }, + dataset: { + dimensions: ['createTime', 'runtimeMax', 'runtimeTotal', 'runtimeUsed', 'runtimeFree'], + source: [] + }, + grid: { + left: 20, + right: 20, + bottom: 0, + top: 70, + containLabel: true + }, + legend: { + top: 0 + }, + series: [ + { + name: 'JVM最大内存', type: 'line', + emphasis: { + focus: 'series' + }, smooth: false + }, + { + name: 'JVM占用内存', type: 'line', + emphasis: { + focus: 'series' + }, smooth: false + }, + { + name: 'JVM可用内存', type: 'line', areaStyle: {}, + emphasis: { + focus: 'series' + }, smooth: false + }, + { + name: 'JVM空闲内存', type: 'line', areaStyle: {}, + emphasis: { + focus: 'series' + }, smooth: false + } + ], + toolbox: { + feature: { + // 数据区域缩放 + dataZoom: { + yAxisIndex: false // Y轴不缩放 + }, + brush: { + type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮 + }, + saveAsImage: {show: true, name: 'JVM内存日志图片'} // 保存为图片 + } + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'cross', + label: { + backgroundColor: '#6a7985' + } + }, + padding: [5, 10] + }, + xAxis: { + type: 'category', + boundaryGap: false, + axisTick: { + show: false + } + }, + yAxis: { + name: "单位(MB)", + nameTextStyle: { + color: "#aaa", + nameLocation: "start", + }, + }, +}) as EChartsOption + +/** 查询统计数据列表 */ +const getMonitorMemDataList = async () => { + const list = await MonitorMemApi.getMonitorMemList(queryParams) + for (let item of list) { + item.createTime = formatTime(item.createTime, 'yyyy-MM-dd HH:mm:ss') + } + // 更新 Echarts 数据 + if (physicalChartOptions.dataset && physicalChartOptions.dataset['source']) { + physicalChartOptions.dataset['source'] = list + } + if (JVMChartOptions.dataset && JVMChartOptions.dataset['source']) { + JVMChartOptions.dataset['source'] = list + } + echartsLoading.value = false +} + +/** 切换展示方式 */ +const switchShow = (type: String) => { + showType.value = type + if(showType.value == 'data') { + getList() + } else { + getMonitorMemDataList() + } +} + +/** 导出按钮操作 */ +const handleExport = async () => { + try { + // 导出的二次确认 + await message.exportConfirm() + // 发起导出 + exportLoading.value = true + const data = await MonitorMemApi.exportMonitorMem(queryParams) + download.excel(data, '内存监控日志.xls') + } catch { + } finally { + exportLoading.value = false + } +} + +let intervalId; +/** 初始化 **/ +onMounted(() => { + showType.value = 'data'; + const currentDate = new Date(); + const previousDate = new Date(currentDate); + previousDate.setDate(currentDate.getDate() - 1); + queryParams.createTime[0] = formatDate(previousDate, 'YYYY-MM-DD HH:mm:ss'); + queryParams.createTime[1] = formatDate(currentDate, 'YYYY-MM-DD HH:mm:ss'); + intervalId = setInterval(() => { + if(showType.value == 'data') { + getList() + } else { + getMonitorMemDataList() + } + }, 60000); +}) + +onUnmounted(() => { + clearInterval(intervalId); +}); +</script> diff --git a/src/views/infra/monitor/components/MonitorMemForm.vue b/src/views/infra/monitor/components/MonitorMemForm.vue new file mode 100644 index 0000000..92d889b --- /dev/null +++ b/src/views/infra/monitor/components/MonitorMemForm.vue @@ -0,0 +1,148 @@ +<template> + <Dialog :title="dialogTitle" v-model="dialogVisible"> + <el-form + ref="formRef" + :model="formData" + :rules="formRules" + label-width="100px" + v-loading="formLoading" + > + <el-form-item label="主机名称" prop="hostName"> + <el-input v-model="formData.hostName" placeholder="请输入主机名称" /> + </el-form-item> + <el-form-item label="服务器ip" prop="hostIp"> + <el-input v-model="formData.hostIp" placeholder="请输入服务器ip" /> + </el-form-item> + <el-form-item label="服务名" prop="serverName"> + <el-input v-model="formData.serverName" placeholder="请输入服务名" /> + </el-form-item> + <el-form-item label="总内存" prop="physicalTotal"> + <el-input v-model="formData.physicalTotal" placeholder="请输入总物理内存" /> + </el-form-item> + <el-form-item label="已用内存" prop="physicalUsed"> + <el-input v-model="formData.physicalUsed" placeholder="请输入已用物理内存" /> + </el-form-item> + <el-form-item label="空闲内存" prop="physicalFree"> + <el-input v-model="formData.physicalFree" placeholder="请输入空闲内存" /> + </el-form-item> + <el-form-item label="内存使用率" prop="physicalUsage"> + <el-input v-model="formData.physicalUsage" placeholder="请输入物理内存使用率" /> + </el-form-item> + <el-form-item label="jvm运行总内存" prop="runtimeTotal"> + <el-input v-model="formData.runtimeTotal" placeholder="请输入jvm运行总内存" /> + </el-form-item> + <el-form-item label="jvm最大内存" prop="runtimeMax"> + <el-input v-model="formData.runtimeMax" placeholder="请输入jvm最大内存" /> + </el-form-item> + <el-form-item label="jvm已用内存" prop="runtimeUsed"> + <el-input v-model="formData.runtimeUsed" placeholder="请输入jvm已用内存" /> + </el-form-item> + <el-form-item label="jvm空闲内存" prop="runtimeFree"> + <el-input v-model="formData.runtimeFree" placeholder="请输入jvm空闲内存" /> + </el-form-item> + <el-form-item label="jvm内存使用率" prop="runtimeUsage"> + <el-input v-model="formData.runtimeUsage" placeholder="请输入jvm内存使用率" /> + </el-form-item> + </el-form> + <template #footer> + <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> + <el-button @click="dialogVisible = false">取 消</el-button> + </template> + </Dialog> +</template> +<script setup lang="ts"> +import { MonitorMemApi, MonitorMemVO } from '@/api/infra/monitormem' + +/** 内存监控日志 表单 */ +defineOptions({ name: 'MonitorMemForm' }) + +const { t } = useI18n() // 国际化 +const message = useMessage() // 消息弹窗 + +const dialogVisible = ref(false) // 弹窗的是否展示 +const dialogTitle = ref('') // 弹窗的标题 +const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 +const formType = ref('') // 表单的类型:create - 新增;update - 修改 +const formData = ref({ + id: undefined, + hostName: undefined, + hostIp: undefined, + serverName: undefined, + physicalTotal: undefined, + physicalUsed: undefined, + physicalFree: undefined, + physicalUsage: undefined, + runtimeTotal: undefined, + runtimeMax: undefined, + runtimeUsed: undefined, + runtimeFree: undefined, + runtimeUsage: undefined, +}) +const formRules = reactive({ + hostName: [{ required: true, message: '主机名称不能为空', trigger: 'blur' }], + hostIp: [{ required: true, message: '服务器ip不能为空', trigger: 'blur' }], +}) +const formRef = ref() // 表单 Ref + +/** 打开弹窗 */ +const open = async (type: string, id?: number) => { + dialogVisible.value = true + dialogTitle.value = t('action.' + type) + formType.value = type + resetForm() + // 修改时,设置数据 + if (id) { + formLoading.value = true + try { + formData.value = await MonitorMemApi.getMonitorMem(id) + } finally { + formLoading.value = false + } + } +} +defineExpose({ open }) // 提供 open 方法,用于打开弹窗 + +/** 提交表单 */ +const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 +const submitForm = async () => { + // 校验表单 + await formRef.value.validate() + // 提交请求 + formLoading.value = true + try { + const data = formData.value as unknown as MonitorMemVO + if (formType.value === 'create') { + await MonitorMemApi.createMonitorMem(data) + message.success(t('common.createSuccess')) + } else { + await MonitorMemApi.updateMonitorMem(data) + message.success(t('common.updateSuccess')) + } + dialogVisible.value = false + // 发送操作成功的事件 + emit('success') + } finally { + formLoading.value = false + } +} + +/** 重置表单 */ +const resetForm = () => { + formData.value = { + id: undefined, + hostName: undefined, + hostIp: undefined, + serverName: undefined, + physicalTotal: undefined, + physicalUsed: undefined, + physicalFree: undefined, + physicalUsage: undefined, + runtimeTotal: undefined, + runtimeMax: undefined, + runtimeUsed: undefined, + runtimeFree: undefined, + runtimeUsage: undefined, + } + formRef.value?.resetFields() +} +</script> diff --git a/src/views/infra/monitor/components/index.ts b/src/views/infra/monitor/components/index.ts new file mode 100644 index 0000000..f329dca --- /dev/null +++ b/src/views/infra/monitor/components/index.ts @@ -0,0 +1,3 @@ +import MonitorMem from './MonitorMem.vue' +import MonitorDisk from './MonitorDisk.vue' +export { MonitorMem, MonitorDisk } diff --git a/src/views/infra/monitor/index.vue b/src/views/infra/monitor/index.vue new file mode 100644 index 0000000..dc29b06 --- /dev/null +++ b/src/views/infra/monitor/index.vue @@ -0,0 +1,20 @@ +<template> + <ContentWrap> + <el-tabs v-model="activeName"> + <el-tab-pane label="内存监控日志" name="monitorMem"> + <monitor-mem ref="memInfoRef" /> + </el-tab-pane> + <el-tab-pane label="硬盘监控日志" name="colum"> + <monitor-disk ref="diskInfoRef" /> + </el-tab-pane> + </el-tabs> + </ContentWrap> +</template> +<script lang="ts" setup> +import { MonitorMem, MonitorDisk } from './components' + +defineOptions({ name: 'InfraMonitor' }) + +const activeName = ref('monitorMem') // Tag 激活的窗口 + +</script> diff --git a/src/views/infra/storage/index_rec.vue b/src/views/infra/storage/index_rec.vue deleted file mode 100644 index f6dfc23..0000000 --- a/src/views/infra/storage/index_rec.vue +++ /dev/null @@ -1,161 +0,0 @@ -<template> - <el-scrollbar height="calc(100vh - 88px - 40px - 50px)"> - <el-row> - <!-- 磁盘使用量统计 --> - <el-col :span="12" class="mt-3"> - <el-card class="ml-3" :gutter="12" shadow="hover"> -<!-- <div ref="chartRef" style="width: 100%; height: 90%"></div>--> - <Echart :options="usedDiskEchartChika" :height="420" /> -<!-- <Echart :options="usedDiskEchartChika" :height="420" />--> - </el-card> - </el-col> - </el-row> - </el-scrollbar> -</template> -<script lang="ts" setup> -import { ref, onMounted } from "vue"; -import * as StorageApi from '@/api/infra/storage' -import { StorageMonitorInfoVO } from '@/api/infra/storage/types' -const disks = ref<StorageMonitorInfoVO>() -const disk = ref<StorageMonitorInfoVO>() - -// 基本信息 -const readDiskInfo = async () => { - const data = await StorageApi.getDiskInfo() - disks.value = data - disk.value = data[0] -} - -// 内存使用情况 -const usedDiskEchartChika = reactive<any>({ - title: { - // 仪表盘标题。 - text: '磁盘使用情况', - left: 'center', - show: true, // 是否显示标题,默认 true。 - offsetCenter: [0, '20%'], //相对于仪表盘中心的偏移位置,数组第一项是水平方向的偏移,第二项是垂直方向的偏移。可以是绝对的数值,也可以是相对于仪表盘半径的百分比。 - color: 'yellow', // 文字的颜色,默认 #333。 - fontSize: 20 // 文字的字体大小,默认 15。 - }, - toolbox: { - show: false, - feature: { - restore: { show: true }, - saveAsImage: { show: true } - } - }, - series: [ - { - name: '峰值', - type: 'gauge', - min: 0, - max: 500, - splitNumber: 10, - //这是指针的颜色 - color: '#F5C74E', - radius: '85%', - center: ['50%', '50%'], - startAngle: 225, - endAngle: -45, - axisLine: { - // 坐标轴线 - lineStyle: { - // 属性lineStyle控制线条样式 - color: [ - [0.2, '#7FFF00'], - [0.8, '#00FFFF'], - [1, '#FF0000'] - ], - //width: 6 外框的大小(环的宽度) - width: 10 - } - }, - axisTick: { - // 坐标轴小标记 - //里面的线长是5(短线) - length: 5, // 属性length控制线长 - lineStyle: { - // 属性lineStyle控制线条样式 - color: '#76D9D7' - } - }, - splitLine: { - // 分隔线 - length: 20, // 属性length控制线长 - lineStyle: { - // 属性lineStyle(详见lineStyle)控制线条样式 - color: '#76D9D7' - } - }, - axisLabel: { - color: '#76D9D7', - distance: 15, - fontSize: 15 - }, - pointer: { - // 指针的大小 - width: 7, - show: true - }, - detail: { - textStyle: { - fontWeight: 'normal', - // 里面文字下的数值大小(50) - fontSize: 15, - color: '#FFFFFF' - }, - valueAnimation: true - }, - progress: { - show: true - } - } - ] -}) - - -/** 加载数据 */ -const getSummary = () => { - // 初始化命令图表 - usedDiskInstance() -} - -const usedDiskInstance = async () => { - try { - const data = await StorageApi.getDiskInfo() - disks.value = data - disk.value = data[0] - // data.forEach((disk) => { - console.log(disk.value) - console.log(disk.value.name) - console.log(disk.value!.restPPT) - // 仪表盘详情,用于显示数据。 - usedDiskEchartChika.series[0].detail = { - show: true, // 是否显示详情,默认 true。 - offsetCenter: [0, '50%'], // 相对于仪表盘中心的偏移位置,数组第一项是水平方向的偏移,第二项是垂直方向的偏移。可以是绝对的数值,也可以是相对于仪表盘半径的百分比。 - color: 'auto', // 文字的颜色,默认 auto。 - fontSize: 30, // 文字的字体大小,默认 15。 - formatter: disk.value!.restPPT // 格式化函数或者字符串 - } - console.log(disk.value.restPPT) - usedDiskEchartChika.series[0].data[0] = { - value: disk.value!.restPPT, - name: '磁盘消耗' - } - console.log(disk.value) - usedDiskEchartChika.tooltip = { - formatter: '{b} <br/>{a} : ' + disk.value!.restPPT - } - // }) - } catch {} -} - -/** 初始化 **/ -onMounted(() => { - readDiskInfo() - // 读取 redis 信息 - // readDiskInfo() - // // 加载数据 - getSummary() -}) -</script> diff --git a/src/views/system/loginlog/LoginLogDetail.vue b/src/views/system/loginlog/LoginLogDetail.vue index ff49453..7d58978 100644 --- a/src/views/system/loginlog/LoginLogDetail.vue +++ b/src/views/system/loginlog/LoginLogDetail.vue @@ -16,7 +16,7 @@ <el-descriptions-item label="浏览器"> {{ detailData.userAgent }} </el-descriptions-item> - <el-descriptions-item label="登陆结果"> + <el-descriptions-item label="登录结果"> <dict-tag :type="DICT_TYPE.SYSTEM_LOGIN_RESULT" :value="detailData.result" /> </el-descriptions-item> <el-descriptions-item label="登录日期"> -- Gitblit v1.9.3