完成前端缓存监控适配
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
# 前端移除 Redis 实施记录
|
||||||
|
|
||||||
|
## 实施时间
|
||||||
|
- 2026-03-28
|
||||||
|
|
||||||
|
## 修改内容
|
||||||
|
- 将缓存概览页从 Redis 专属字段改为本地缓存统计字段展示
|
||||||
|
- 将第二张图表改为本地缓存统计柱状图,并保留原有页面布局
|
||||||
|
- 为缓存概览页补充空数据保护、命中率计算和监控采样时间展示
|
||||||
|
- 为缓存列表页补充空列表兜底、清理后的详情重置和全部清理后的界面清空
|
||||||
|
- 复核缓存监控 API 路径不变,仅调整接口注释为缓存语义
|
||||||
|
|
||||||
|
## 文档路径
|
||||||
|
- `doc/2026-03-28-remove-redis-frontend-plan.md`
|
||||||
|
- `doc/implementation-report-2026-03-28-remove-redis-frontend.md`
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
- 已两次验证 `npm --prefix ruoyi-ui run build:prod` 通过,输出为 `Build complete.`
|
||||||
|
- 已验证 `npm --prefix ruoyi-ui run dev` 可成功启动,前端开发服务地址为 `http://localhost:1025`
|
||||||
|
- 已在浏览器中完成登录并打开 `/monitor/cache`、`/monitor/cacheList` 路由,确认页面可正常进入
|
||||||
|
- 当前环境下真实接口 `/dev-api/monitor/cache`、`/dev-api/monitor/cache/getNames`、`/dev-api/monitor/cache/getKeys/*` 请求均出现 10 秒超时,未能完成真实后端联调
|
||||||
|
- 已使用与后端实现一致的返回结构进行浏览器侧 mock 验证,确认缓存概览字段映射、图表渲染、缓存名称到键名联动、缓存内容展示以及清理后表单清空行为正常
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
- 按仓库要求,本次前端开发直接在当前分支进行,未使用 `git worktree`
|
||||||
|
- 计划明确不引入新的前端测试框架,因此本次以前端生产构建与本地联调作为主要验证手段
|
||||||
|
- 联调与验证结束后,已主动停止本次任务启动的 Node 前端进程
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
|
||||||
// 查询缓存详细
|
// 查询缓存概览
|
||||||
export function getCache() {
|
export function getCache() {
|
||||||
return request({
|
return request({
|
||||||
url: '/monitor/cache',
|
url: '/monitor/cache',
|
||||||
@@ -32,7 +32,7 @@ export function getCacheValue(cacheName, cacheKey) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理指定名称缓存
|
// 清理指定名称下的缓存
|
||||||
export function clearCacheName(cacheName) {
|
export function clearCacheName(cacheName) {
|
||||||
return request({
|
return request({
|
||||||
url: '/monitor/cache/clearCacheName/' + cacheName,
|
url: '/monitor/cache/clearCacheName/' + cacheName,
|
||||||
|
|||||||
271
ruoyi-ui/src/views/monitor/cache/index.vue
vendored
271
ruoyi-ui/src/views/monitor/cache/index.vue
vendored
@@ -8,34 +8,34 @@
|
|||||||
<table cellspacing="0" style="width: 100%">
|
<table cellspacing="0" style="width: 100%">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">Redis版本</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">缓存类型</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.redis_version }}</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">{{ cacheTypeText }}</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">运行模式</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">运行模式</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.redis_mode == "standalone" ? "单机" : "集群" }}</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">{{ cacheModeText }}</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">端口</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">总键数</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.tcp_port }}</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">{{ displayDbSize }}</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">客户端数</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">写入次数</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.connected_clients }}</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">{{ cache.info.write_count }}</div></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">运行时间(天)</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">命中次数</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.uptime_in_days }}</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">{{ cache.info.hit_count }}</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">使用内存</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">未命中次数</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.used_memory_human }}</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">{{ cache.info.miss_count }}</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">使用CPU</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">过期清理次数</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ parseFloat(cache.info.used_cpu_user_children).toFixed(2) }}</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">{{ cache.info.expired_count }}</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">内存配置</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">命中率</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.maxmemory_human }}</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">{{ hitRateText }}</div></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">AOF是否开启</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">统计说明</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.aof_enabled == "0" ? "否" : "是" }}</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">进程内累计统计</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">RDB是否成功</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">监控范围</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.rdb_last_bgsave_status }}</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">当前应用实例</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">Key数量</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">图表数据项</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.dbSize">{{ cache.dbSize }} </div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">{{ commandStats.length }}</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell">网络入口/出口</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">监控采样时间</div></td>
|
||||||
<td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.instantaneous_input_kbps }}kps/{{cache.info.instantaneous_output_kbps}}kps</div></td>
|
<td class="el-table__cell is-leaf"><div class="cell">{{ sampleTime }}</div></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
|
|
||||||
<el-col :span="12" class="card-box">
|
<el-col :span="12" class="card-box">
|
||||||
<el-card>
|
<el-card>
|
||||||
<div slot="header"><span><i class="el-icon-odometer"></i> 内存信息</span></div>
|
<div slot="header"><span><i class="el-icon-odometer"></i> 缓存命中概览</span></div>
|
||||||
<div class="el-table el-table--enable-row-hover el-table--medium">
|
<div class="el-table el-table--enable-row-hover el-table--medium">
|
||||||
<div ref="usedmemory" style="height: 420px" />
|
<div ref="usedmemory" style="height: 420px" />
|
||||||
</div>
|
</div>
|
||||||
@@ -74,74 +74,197 @@ export default {
|
|||||||
return {
|
return {
|
||||||
// 统计命令信息
|
// 统计命令信息
|
||||||
commandstats: null,
|
commandstats: null,
|
||||||
// 使用内存
|
// 缓存统计
|
||||||
usedmemory: null,
|
usedmemory: null,
|
||||||
// cache信息
|
// cache信息
|
||||||
cache: []
|
cache: {
|
||||||
|
info: {
|
||||||
|
cache_type: "IN_MEMORY",
|
||||||
|
cache_mode: "single-instance",
|
||||||
|
key_size: 0,
|
||||||
|
hit_count: 0,
|
||||||
|
miss_count: 0,
|
||||||
|
expired_count: 0,
|
||||||
|
write_count: 0
|
||||||
|
},
|
||||||
|
dbSize: 0,
|
||||||
|
commandStats: []
|
||||||
|
},
|
||||||
|
sampleTime: "-",
|
||||||
|
resizeHandler: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.getList()
|
|
||||||
this.openLoading()
|
this.openLoading()
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.resizeHandler) {
|
||||||
|
window.removeEventListener("resize", this.resizeHandler)
|
||||||
|
}
|
||||||
|
if (this.commandstats) {
|
||||||
|
this.commandstats.dispose()
|
||||||
|
}
|
||||||
|
if (this.usedmemory) {
|
||||||
|
this.usedmemory.dispose()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
cacheTypeText() {
|
||||||
|
return this.formatCacheType(this.cache.info.cache_type)
|
||||||
|
},
|
||||||
|
cacheModeText() {
|
||||||
|
return this.formatCacheMode(this.cache.info.cache_mode)
|
||||||
|
},
|
||||||
|
displayDbSize() {
|
||||||
|
return this.toNumber(this.cache.dbSize || this.cache.info.key_size)
|
||||||
|
},
|
||||||
|
hitRateText() {
|
||||||
|
const hitCount = this.toNumber(this.cache.info.hit_count)
|
||||||
|
const missCount = this.toNumber(this.cache.info.miss_count)
|
||||||
|
const total = hitCount + missCount
|
||||||
|
if (!total) {
|
||||||
|
return "0.00%"
|
||||||
|
}
|
||||||
|
return ((hitCount / total) * 100).toFixed(2) + "%"
|
||||||
|
},
|
||||||
|
commandStats() {
|
||||||
|
return this.normalizeCommandStats(this.cache.commandStats)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
/** 查缓存询信息 */
|
/** 查缓存询信息 */
|
||||||
getList() {
|
getList() {
|
||||||
getCache().then((response) => {
|
getCache().then((response) => {
|
||||||
this.cache = response.data
|
this.cache = this.normalizeCacheData(response.data)
|
||||||
|
this.sampleTime = this.formatSampleTime(new Date())
|
||||||
this.$modal.closeLoading()
|
this.$modal.closeLoading()
|
||||||
|
this.$nextTick(() => {
|
||||||
this.commandstats = echarts.init(this.$refs.commandstats, "macarons")
|
this.renderCharts()
|
||||||
this.commandstats.setOption({
|
|
||||||
tooltip: {
|
|
||||||
trigger: "item",
|
|
||||||
formatter: "{a} <br/>{b} : {c} ({d}%)",
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: "命令",
|
|
||||||
type: "pie",
|
|
||||||
roseType: "radius",
|
|
||||||
radius: [15, 95],
|
|
||||||
center: ["50%", "38%"],
|
|
||||||
data: response.data.commandStats,
|
|
||||||
animationEasing: "cubicInOut",
|
|
||||||
animationDuration: 1000,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
this.usedmemory = echarts.init(this.$refs.usedmemory, "macarons")
|
|
||||||
this.usedmemory.setOption({
|
|
||||||
tooltip: {
|
|
||||||
formatter: "{b} <br/>{a} : " + this.cache.info.used_memory_human,
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: "峰值",
|
|
||||||
type: "gauge",
|
|
||||||
min: 0,
|
|
||||||
max: 1000,
|
|
||||||
detail: {
|
|
||||||
formatter: this.cache.info.used_memory_human,
|
|
||||||
},
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
value: parseFloat(this.cache.info.used_memory_human),
|
|
||||||
name: "内存消耗",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
window.addEventListener("resize", () => {
|
|
||||||
this.commandstats.resize()
|
|
||||||
this.usedmemory.resize()
|
|
||||||
})
|
})
|
||||||
|
}).catch(() => {
|
||||||
|
this.cache = this.normalizeCacheData()
|
||||||
|
this.sampleTime = "-"
|
||||||
|
this.$modal.closeLoading()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
// 打开加载层
|
// 打开加载层
|
||||||
openLoading() {
|
openLoading() {
|
||||||
this.$modal.loading("正在加载缓存监控数据,请稍候!")
|
this.$modal.loading("正在加载缓存监控数据,请稍候!")
|
||||||
|
},
|
||||||
|
normalizeCacheData(data) {
|
||||||
|
const source = data || {}
|
||||||
|
const info = source.info || {}
|
||||||
|
return {
|
||||||
|
info: {
|
||||||
|
cache_type: info.cache_type || "IN_MEMORY",
|
||||||
|
cache_mode: info.cache_mode || "single-instance",
|
||||||
|
key_size: this.toNumber(info.key_size),
|
||||||
|
hit_count: this.toNumber(info.hit_count),
|
||||||
|
miss_count: this.toNumber(info.miss_count),
|
||||||
|
expired_count: this.toNumber(info.expired_count),
|
||||||
|
write_count: this.toNumber(info.write_count)
|
||||||
|
},
|
||||||
|
dbSize: this.toNumber(source.dbSize || info.key_size),
|
||||||
|
commandStats: source.commandStats || []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
normalizeCommandStats(commandStats) {
|
||||||
|
const stats = Array.isArray(commandStats) ? commandStats : []
|
||||||
|
return stats.map((item) => ({
|
||||||
|
name: this.statLabel(item.name),
|
||||||
|
value: this.toNumber(item.value)
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
statLabel(name) {
|
||||||
|
const labelMap = {
|
||||||
|
hit_count: "命中次数",
|
||||||
|
miss_count: "未命中次数",
|
||||||
|
expired_count: "过期清理次数",
|
||||||
|
write_count: "写入次数"
|
||||||
|
}
|
||||||
|
return labelMap[name] || name || "未命名统计"
|
||||||
|
},
|
||||||
|
formatCacheType(type) {
|
||||||
|
const typeMap = {
|
||||||
|
IN_MEMORY: "进程内缓存"
|
||||||
|
}
|
||||||
|
return typeMap[type] || type || "-"
|
||||||
|
},
|
||||||
|
formatCacheMode(mode) {
|
||||||
|
const modeMap = {
|
||||||
|
"single-instance": "单实例"
|
||||||
|
}
|
||||||
|
return modeMap[mode] || mode || "-"
|
||||||
|
},
|
||||||
|
formatSampleTime(date) {
|
||||||
|
const current = date || new Date()
|
||||||
|
const pad = (value) => String(value).padStart(2, "0")
|
||||||
|
return `${current.getFullYear()}-${pad(current.getMonth() + 1)}-${pad(current.getDate())} ${pad(current.getHours())}:${pad(current.getMinutes())}:${pad(current.getSeconds())}`
|
||||||
|
},
|
||||||
|
toNumber(value) {
|
||||||
|
const parsedValue = Number(value)
|
||||||
|
return Number.isFinite(parsedValue) ? parsedValue : 0
|
||||||
|
},
|
||||||
|
renderCharts() {
|
||||||
|
if (!this.commandstats) {
|
||||||
|
this.commandstats = echarts.init(this.$refs.commandstats, "macarons")
|
||||||
|
}
|
||||||
|
if (!this.usedmemory) {
|
||||||
|
this.usedmemory = echarts.init(this.$refs.usedmemory, "macarons")
|
||||||
|
}
|
||||||
|
this.commandstats.setOption({
|
||||||
|
tooltip: {
|
||||||
|
trigger: "item",
|
||||||
|
formatter: "{a} <br/>{b} : {c} ({d}%)"
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "缓存统计",
|
||||||
|
type: "pie",
|
||||||
|
roseType: "radius",
|
||||||
|
radius: [15, 95],
|
||||||
|
center: ["50%", "38%"],
|
||||||
|
data: this.commandStats,
|
||||||
|
animationEasing: "cubicInOut",
|
||||||
|
animationDuration: 1000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
this.usedmemory.setOption({
|
||||||
|
tooltip: {
|
||||||
|
trigger: "axis",
|
||||||
|
axisPointer: {
|
||||||
|
type: "shadow"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: "category",
|
||||||
|
data: this.commandStats.map((item) => item.name),
|
||||||
|
axisTick: {
|
||||||
|
alignWithLabel: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "value",
|
||||||
|
minInterval: 1
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "统计次数",
|
||||||
|
type: "bar",
|
||||||
|
barMaxWidth: 48,
|
||||||
|
data: this.commandStats.map((item) => item.value)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
if (!this.resizeHandler) {
|
||||||
|
this.resizeHandler = () => {
|
||||||
|
this.commandstats && this.commandstats.resize()
|
||||||
|
this.usedmemory && this.usedmemory.resize()
|
||||||
|
}
|
||||||
|
window.addEventListener("resize", this.resizeHandler)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
ruoyi-ui/src/views/monitor/cache/list.vue
vendored
20
ruoyi-ui/src/views/monitor/cache/list.vue
vendored
@@ -175,7 +175,10 @@ export default {
|
|||||||
getCacheNames() {
|
getCacheNames() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
listCacheName().then(response => {
|
listCacheName().then(response => {
|
||||||
this.cacheNames = response.data
|
this.cacheNames = response.data || []
|
||||||
|
this.loading = false
|
||||||
|
}).catch(() => {
|
||||||
|
this.cacheNames = []
|
||||||
this.loading = false
|
this.loading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -188,7 +191,7 @@ export default {
|
|||||||
handleClearCacheName(row) {
|
handleClearCacheName(row) {
|
||||||
clearCacheName(row.cacheName).then(response => {
|
clearCacheName(row.cacheName).then(response => {
|
||||||
this.$modal.msgSuccess("清理缓存名称[" + row.cacheName + "]成功")
|
this.$modal.msgSuccess("清理缓存名称[" + row.cacheName + "]成功")
|
||||||
this.getCacheKeys()
|
this.getCacheKeys(row)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
/** 查询缓存键名列表 */
|
/** 查询缓存键名列表 */
|
||||||
@@ -199,7 +202,13 @@ export default {
|
|||||||
}
|
}
|
||||||
this.subLoading = true
|
this.subLoading = true
|
||||||
listCacheKey(cacheName).then(response => {
|
listCacheKey(cacheName).then(response => {
|
||||||
this.cacheKeys = response.data
|
this.cacheKeys = response.data || []
|
||||||
|
this.cacheForm = {}
|
||||||
|
this.subLoading = false
|
||||||
|
this.nowCacheName = cacheName
|
||||||
|
}).catch(() => {
|
||||||
|
this.cacheKeys = []
|
||||||
|
this.cacheForm = {}
|
||||||
this.subLoading = false
|
this.subLoading = false
|
||||||
this.nowCacheName = cacheName
|
this.nowCacheName = cacheName
|
||||||
})
|
})
|
||||||
@@ -227,13 +236,16 @@ export default {
|
|||||||
/** 查询缓存内容详细 */
|
/** 查询缓存内容详细 */
|
||||||
handleCacheValue(cacheKey) {
|
handleCacheValue(cacheKey) {
|
||||||
getCacheValue(this.nowCacheName, cacheKey).then(response => {
|
getCacheValue(this.nowCacheName, cacheKey).then(response => {
|
||||||
this.cacheForm = response.data
|
this.cacheForm = response.data || {}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
/** 清理全部缓存 */
|
/** 清理全部缓存 */
|
||||||
handleClearCacheAll() {
|
handleClearCacheAll() {
|
||||||
clearCacheAll().then(response => {
|
clearCacheAll().then(response => {
|
||||||
this.$modal.msgSuccess("清理全部缓存成功")
|
this.$modal.msgSuccess("清理全部缓存成功")
|
||||||
|
this.cacheKeys = []
|
||||||
|
this.cacheForm = {}
|
||||||
|
this.nowCacheName = ""
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user