Compare commits
No commits in common. "df6cf59e7c6c67a43ae5014bb11d2398ceb01b8b" and "e5ad68f1ff5adaee239396c08f5b7f6ce79701c4" have entirely different histories.
df6cf59e7c
...
e5ad68f1ff
55
src/service/api/monitor/cache/index.ts
vendored
55
src/service/api/monitor/cache/index.ts
vendored
@ -1,55 +0,0 @@
|
||||
import { request } from '@/service/http'
|
||||
import type { DeleteCacheKeyBo, GetCacheValueBo, SysCacheVo } from './types'
|
||||
|
||||
/**
|
||||
* 查询Redis缓存所有Key
|
||||
*/
|
||||
export function getRedisCache() {
|
||||
return request.Get<Service.ResponseResult<SysCacheVo[]>>('/coder/monitor/cache/getRedisCache')
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询Redis缓存键名列表
|
||||
*/
|
||||
export function getCacheKeys(cacheName: string) {
|
||||
// 对缓存名称进行URL编码,处理冒号等特殊字符
|
||||
const encodedCacheName = encodeURIComponent(cacheName)
|
||||
return request.Get<Service.ResponseResult<string[]>>(`/coder/monitor/cache/getCacheKeys/${encodedCacheName}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Redis缓存内容
|
||||
*/
|
||||
export function getCacheValue(data: GetCacheValueBo) {
|
||||
return request.Post<Service.ResponseResult<SysCacheVo>>('/coder/monitor/cache/getValue', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Redis指定名称缓存
|
||||
*/
|
||||
export function deleteCacheName(cacheName: string) {
|
||||
// 对缓存名称进行URL编码,处理冒号等特殊字符
|
||||
const encodedCacheName = encodeURIComponent(cacheName)
|
||||
return request.Post<Service.ResponseResult<void>>(`/coder/monitor/cache/deleteCacheName/${encodedCacheName}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Redis指定键名缓存
|
||||
*/
|
||||
export function deleteCacheKey(data: DeleteCacheKeyBo) {
|
||||
return request.Post<Service.ResponseResult<void>>('/coder/monitor/cache/deleteCacheKey', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Redis所有信息
|
||||
*/
|
||||
export function deleteCacheAll() {
|
||||
return request.Post<Service.ResponseResult<void>>('/coder/monitor/cache/deleteCacheAll')
|
||||
}
|
||||
|
||||
// 重新导出类型供外部使用
|
||||
export type {
|
||||
DeleteCacheKeyBo,
|
||||
GetCacheValueBo,
|
||||
SysCacheVo,
|
||||
} from './types'
|
||||
33
src/service/api/monitor/cache/types.ts
vendored
33
src/service/api/monitor/cache/types.ts
vendored
@ -1,33 +0,0 @@
|
||||
/**
|
||||
* 缓存管理相关类型定义
|
||||
*/
|
||||
|
||||
// 缓存信息
|
||||
export interface SysCacheVo {
|
||||
/** 缓存名称 */
|
||||
cacheName: string
|
||||
/** 缓存键名 */
|
||||
cacheKey?: string
|
||||
/** 缓存内容 */
|
||||
cacheValue?: string
|
||||
/** 缓存过期时间 */
|
||||
expireTime?: string
|
||||
/** 备注信息 */
|
||||
remark?: string
|
||||
}
|
||||
|
||||
// 获取缓存内容请求参数
|
||||
export interface GetCacheValueBo {
|
||||
/** 缓存名称 */
|
||||
cacheName: string
|
||||
/** 缓存键名 */
|
||||
cacheKey: string
|
||||
}
|
||||
|
||||
// 删除缓存键请求参数
|
||||
export interface DeleteCacheKeyBo {
|
||||
/** 缓存名称 */
|
||||
cacheName?: string
|
||||
/** 缓存键名 */
|
||||
cacheKey: string
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
import { request } from '@/service/http'
|
||||
import type { RedisInfoVo } from './types'
|
||||
|
||||
/**
|
||||
* 获取Redis监控信息
|
||||
*/
|
||||
export function getRedisInformation() {
|
||||
return request.Get<Service.ResponseResult<RedisInfoVo>>('/coder/monitor/redis/getRedisInformation')
|
||||
}
|
||||
|
||||
// 重新导出类型供外部使用
|
||||
export type {
|
||||
RedisCommandStatVo,
|
||||
RedisInfoVo,
|
||||
} from './types'
|
||||
@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Redis监控相关类型定义
|
||||
*/
|
||||
|
||||
// Redis命令统计项
|
||||
export interface RedisCommandStatVo {
|
||||
/** 命令名称 */
|
||||
name: string
|
||||
/** 调用次数 */
|
||||
value: string
|
||||
}
|
||||
|
||||
// Redis监控信息
|
||||
export interface RedisInfoVo {
|
||||
/** Redis基本信息 */
|
||||
info: Record<string, any>
|
||||
/** 数据库大小 */
|
||||
dbSize: number
|
||||
/** 命令统计 */
|
||||
commandStats: RedisCommandStatVo[]
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
import { request } from '@/service/http'
|
||||
import type { ServerVo } from './types'
|
||||
|
||||
/**
|
||||
* 获取服务器监控信息
|
||||
*/
|
||||
export function getServerInformation() {
|
||||
return request.Get<Service.ResponseResult<ServerVo>>('/coder/monitor/server/getServerInformation')
|
||||
}
|
||||
|
||||
// 重新导出类型供外部使用
|
||||
export type {
|
||||
CpuVo,
|
||||
JvmVo,
|
||||
MemVo,
|
||||
ServerVo,
|
||||
SysFileVo,
|
||||
SysVo,
|
||||
} from './types'
|
||||
@ -1,123 +0,0 @@
|
||||
/**
|
||||
* 服务器监控相关类型定义
|
||||
*/
|
||||
|
||||
// CPU信息
|
||||
export interface CpuVo {
|
||||
/** 核心数 */
|
||||
cpuNum: number
|
||||
/** CPU总的使用率 */
|
||||
total: number
|
||||
/** CPU系统使用率 */
|
||||
sys: number
|
||||
/** CPU用户使用率 */
|
||||
used: number
|
||||
/** CPU当前等待率 */
|
||||
wait: number
|
||||
/** CPU当前空闲率 */
|
||||
free: number
|
||||
/** CPU使用率百分比 */
|
||||
cpuUsage: number
|
||||
/** CPU系统使用率百分比 */
|
||||
sysUsage: number
|
||||
/** CPU用户使用率百分比 */
|
||||
userUsage: number
|
||||
/** CPU等待率百分比 */
|
||||
waitUsage: number
|
||||
/** CPU空闲率百分比 */
|
||||
freeUsage: number
|
||||
}
|
||||
|
||||
// 内存信息
|
||||
export interface MemVo {
|
||||
/** 内存总量 */
|
||||
total: number
|
||||
/** 已用内存 */
|
||||
used: number
|
||||
/** 剩余内存 */
|
||||
free: number
|
||||
/** 内存使用率 */
|
||||
usage: number
|
||||
/** 总内存(格式化) */
|
||||
totalStr: string
|
||||
/** 已用内存(格式化) */
|
||||
usedStr: string
|
||||
/** 剩余内存(格式化) */
|
||||
freeStr: string
|
||||
}
|
||||
|
||||
// JVM信息
|
||||
export interface JvmVo {
|
||||
/** 当前JVM占用的内存总数(M) */
|
||||
total: number
|
||||
/** JVM最大可用内存总数(M) */
|
||||
max: number
|
||||
/** JVM空闲内存(M) */
|
||||
free: number
|
||||
/** JDK版本 */
|
||||
version: string
|
||||
/** JDK路径 */
|
||||
home: string
|
||||
/** JVM已用内存 */
|
||||
used: number
|
||||
/** JVM内存使用率 */
|
||||
usage: number
|
||||
/** 总内存(格式化) */
|
||||
totalStr: string
|
||||
/** 已用内存(格式化) */
|
||||
usedStr: string
|
||||
/** 剩余内存(格式化) */
|
||||
freeStr: string
|
||||
/** 最大内存(格式化) */
|
||||
maxStr: string
|
||||
/** JVM启动时间 */
|
||||
startTime: string
|
||||
/** JVM运行时间 */
|
||||
runTime: string
|
||||
}
|
||||
|
||||
// 系统信息
|
||||
export interface SysVo {
|
||||
/** 服务器名称 */
|
||||
computerName: string
|
||||
/** 服务器IP */
|
||||
computerIp: string
|
||||
/** 项目路径 */
|
||||
userDir: string
|
||||
/** 操作系统 */
|
||||
osName: string
|
||||
/** 系统架构 */
|
||||
osArch: string
|
||||
}
|
||||
|
||||
// 磁盘文件信息
|
||||
export interface SysFileVo {
|
||||
/** 盘符路径 */
|
||||
dirName: string
|
||||
/** 盘符类型 */
|
||||
sysTypeName: string
|
||||
/** 文件类型 */
|
||||
typeName: string
|
||||
/** 总大小 */
|
||||
total: string
|
||||
/** 剩余大小 */
|
||||
free: string
|
||||
/** 已经使用量 */
|
||||
used: string
|
||||
/** 资源的使用率 */
|
||||
usage: number
|
||||
}
|
||||
|
||||
// 服务器信息
|
||||
export interface ServerVo {
|
||||
/** CPU相关信息 */
|
||||
cpu: CpuVo
|
||||
/** 内存相关信息 */
|
||||
mem: MemVo
|
||||
/** JVM相关信息 */
|
||||
jvm: JvmVo
|
||||
/** 服务器相关信息 */
|
||||
sys: SysVo
|
||||
/** 磁盘相关信息 */
|
||||
sysFiles: SysFileVo[]
|
||||
}
|
||||
954
src/views/monitor/cache/index.vue
vendored
954
src/views/monitor/cache/index.vue
vendored
@ -1,954 +0,0 @@
|
||||
<template>
|
||||
<div class="cache-monitor-container">
|
||||
<!-- 三栏布局 -->
|
||||
<div class="cache-layout">
|
||||
<!-- 左侧:缓存名称列表 -->
|
||||
<n-card title="缓存名称" class="cache-names-panel">
|
||||
<template #header-extra>
|
||||
<n-space size="small">
|
||||
<n-button text size="small" :loading="loading" @click="refreshCacheNames">
|
||||
<template #icon>
|
||||
<n-icon><IconParkOutlineRefresh /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-popconfirm
|
||||
trigger="click"
|
||||
:show-icon="false"
|
||||
placement="bottom"
|
||||
@positive-click="clearAllCache"
|
||||
>
|
||||
<template #trigger>
|
||||
<n-button text size="small" type="error" :loading="clearingAll">
|
||||
<template #icon>
|
||||
<n-icon><IconParkOutlineDelete /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
<div class="confirm-content">
|
||||
<p class="confirm-title">
|
||||
确定要清空所有缓存吗?
|
||||
</p>
|
||||
<p class="confirm-desc">
|
||||
此操作将删除Redis中的所有数据,且不可恢复!
|
||||
</p>
|
||||
</div>
|
||||
</n-popconfirm>
|
||||
</n-space>
|
||||
</template>
|
||||
<div class="cache-names-list">
|
||||
<div
|
||||
v-for="(cache, index) in cacheNames"
|
||||
:key="index"
|
||||
class="cache-name-item"
|
||||
:class="{ active: selectedCacheName === cache.cacheName }"
|
||||
@click="selectCacheName(cache.cacheName)"
|
||||
>
|
||||
<div class="cache-name-content">
|
||||
<div class="cache-name">
|
||||
{{ cache.cacheName }}
|
||||
</div>
|
||||
<div class="cache-remark">
|
||||
{{ cache.remark }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="cache-actions">
|
||||
<n-popconfirm
|
||||
trigger="click"
|
||||
:show-icon="false"
|
||||
placement="right"
|
||||
@positive-click="deleteCacheByName(cache.cacheName)"
|
||||
@click.stop
|
||||
>
|
||||
<template #trigger>
|
||||
<n-button type="error" size="small" text>
|
||||
<template #icon>
|
||||
<n-icon><IconParkOutlineDelete /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
确定删除此缓存分类的所有数据吗?
|
||||
</n-popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 中间:缓存键名列表 -->
|
||||
<n-card :title="`缓存键名 (${selectedCacheName || '请选择缓存类型'})`" class="cache-keys-panel">
|
||||
<template #header-extra>
|
||||
<n-space size="small">
|
||||
<n-button
|
||||
text
|
||||
size="small"
|
||||
:loading="loadingKeys"
|
||||
:disabled="!selectedCacheName"
|
||||
@click="refreshCacheKeys"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><IconParkOutlineRefresh /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
<div v-if="!selectedCacheName" class="empty-state">
|
||||
<n-empty description="请先选择左侧的缓存类型">
|
||||
<template #icon>
|
||||
<n-icon><IconParkOutlineKey /></n-icon>
|
||||
</template>
|
||||
</n-empty>
|
||||
</div>
|
||||
<div v-else-if="loadingKeys" class="loading-state">
|
||||
<n-spin size="medium">
|
||||
<template #description>
|
||||
加载缓存键名中...
|
||||
</template>
|
||||
</n-spin>
|
||||
</div>
|
||||
<div v-else class="cache-keys-list">
|
||||
<div v-if="cacheKeys.length === 0" class="empty-state">
|
||||
<n-empty description="该缓存类型下暂无数据">
|
||||
<template #icon>
|
||||
<n-icon><IconParkOutlineFileQuestion /></n-icon>
|
||||
</template>
|
||||
</n-empty>
|
||||
</div>
|
||||
<div
|
||||
v-for="(key, index) in cacheKeys"
|
||||
:key="index"
|
||||
class="cache-key-item"
|
||||
:class="{ active: selectedCacheKey === key }"
|
||||
@click="selectCacheKey(key)"
|
||||
>
|
||||
<div class="cache-key-content">
|
||||
<div class="cache-key">
|
||||
{{ key }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="cache-key-actions">
|
||||
<n-popconfirm
|
||||
trigger="click"
|
||||
:show-icon="false"
|
||||
placement="right"
|
||||
@positive-click="deleteCacheByKey(key)"
|
||||
@click.stop
|
||||
>
|
||||
<template #trigger>
|
||||
<n-button type="error" size="small" text>
|
||||
<template #icon>
|
||||
<n-icon><IconParkOutlineDelete /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
确定删除此缓存键吗?
|
||||
</n-popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 右侧:缓存内容详情 -->
|
||||
<n-card :title="`缓存内容 (${selectedCacheKey || '请选择缓存键'})`" class="cache-content-panel">
|
||||
<template #header-extra>
|
||||
<n-space size="small">
|
||||
<n-button
|
||||
text
|
||||
size="small"
|
||||
:loading="loadingContent"
|
||||
:disabled="!selectedCacheKey"
|
||||
@click="refreshCacheContent"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><IconParkOutlineRefresh /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
<div v-if="!selectedCacheKey" class="empty-state">
|
||||
<n-empty description="请先选择左侧的缓存键">
|
||||
<template #icon>
|
||||
<n-icon><IconParkOutlineFileText /></n-icon>
|
||||
</template>
|
||||
</n-empty>
|
||||
</div>
|
||||
<div v-else-if="loadingContent" class="loading-state">
|
||||
<n-spin size="medium">
|
||||
<template #description>
|
||||
加载缓存内容中...
|
||||
</template>
|
||||
</n-spin>
|
||||
</div>
|
||||
<div v-else class="cache-content">
|
||||
<!-- 优化的元信息展示 -->
|
||||
<div class="content-meta-card">
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">
|
||||
缓存键名
|
||||
</div>
|
||||
<div class="meta-value key-name">
|
||||
{{ cacheContent?.cacheKey || 'N/A' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">
|
||||
过期时间
|
||||
</div>
|
||||
<div class="meta-value">
|
||||
<n-tag v-if="cacheContent?.expireTime && cacheContent.expireTime !== '不过期'" type="warning" size="small" :bordered="false">
|
||||
<template #icon>
|
||||
<n-icon><IconParkOutlineTime /></n-icon>
|
||||
</template>
|
||||
{{ cacheContent.expireTime }}
|
||||
</n-tag>
|
||||
<n-tag v-else type="success" size="small" :bordered="false">
|
||||
<template #icon>
|
||||
<n-icon><IconParkOutlineCheck /></n-icon>
|
||||
</template>
|
||||
永不过期
|
||||
</n-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 优化的缓存内容展示 -->
|
||||
<div class="content-section">
|
||||
<div class="section-header">
|
||||
<h4 class="section-title">
|
||||
缓存内容
|
||||
</h4>
|
||||
<n-space size="small">
|
||||
<n-button size="tiny" text @click="formatJson">
|
||||
<template #icon>
|
||||
<n-icon><IconParkOutlineCode /></n-icon>
|
||||
</template>
|
||||
格式化
|
||||
</n-button>
|
||||
<n-button size="tiny" text @click="copyContent">
|
||||
<template #icon>
|
||||
<n-icon><IconParkOutlineCopy /></n-icon>
|
||||
</template>
|
||||
复制
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<div class="content-display">
|
||||
<div v-if="isJsonContent" class="json-viewer">
|
||||
<pre class="json-content">{{ formattedContent }}</pre>
|
||||
</div>
|
||||
<div v-else class="text-content">
|
||||
<n-input
|
||||
:value="cacheContent?.cacheValue || ''"
|
||||
type="textarea"
|
||||
readonly
|
||||
:rows="12"
|
||||
placeholder="缓存内容为空"
|
||||
class="content-textarea"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
|
||||
import {
|
||||
deleteCacheAll,
|
||||
deleteCacheKey,
|
||||
deleteCacheName,
|
||||
getCacheKeys,
|
||||
getCacheValue,
|
||||
getRedisCache,
|
||||
} from '@/service/api/monitor/cache'
|
||||
import type { SysCacheVo } from '@/service/api/monitor/cache'
|
||||
import IconParkOutlineRefresh from '~icons/icon-park-outline/refresh'
|
||||
import IconParkOutlineDelete from '~icons/icon-park-outline/delete'
|
||||
import IconParkOutlineKey from '~icons/icon-park-outline/key'
|
||||
import IconParkOutlineFileQuestion from '~icons/icon-park-outline/file-question'
|
||||
import IconParkOutlineFileText from '~icons/icon-park-outline/file-text'
|
||||
import IconParkOutlineTime from '~icons/icon-park-outline/time'
|
||||
import IconParkOutlineCheck from '~icons/icon-park-outline/check'
|
||||
import IconParkOutlineCode from '~icons/icon-park-outline/code'
|
||||
import IconParkOutlineCopy from '~icons/icon-park-outline/copy'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const loadingKeys = ref(false)
|
||||
const loadingContent = ref(false)
|
||||
const clearingAll = ref(false)
|
||||
|
||||
const cacheNames = ref<SysCacheVo[]>([])
|
||||
const cacheKeys = ref<string[]>([])
|
||||
const cacheContent = ref<SysCacheVo>()
|
||||
|
||||
const selectedCacheName = ref<string>('')
|
||||
const selectedCacheKey = ref<string>('')
|
||||
|
||||
// 内容展示相关
|
||||
const isJsonFormatted = ref(false)
|
||||
|
||||
// 计算属性
|
||||
const isJsonContent = computed(() => {
|
||||
const content = cacheContent.value?.cacheValue
|
||||
if (!content)
|
||||
return false
|
||||
try {
|
||||
JSON.parse(content)
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const formattedContent = computed(() => {
|
||||
const content = cacheContent.value?.cacheValue
|
||||
if (!content || !isJsonContent.value)
|
||||
return content
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(content), null, 2)
|
||||
}
|
||||
catch {
|
||||
return content
|
||||
}
|
||||
})
|
||||
|
||||
// 格式化JSON
|
||||
function formatJson() {
|
||||
isJsonFormatted.value = !isJsonFormatted.value
|
||||
}
|
||||
|
||||
// 复制内容到剪贴板
|
||||
async function copyContent() {
|
||||
const content = cacheContent.value?.cacheValue
|
||||
if (!content) {
|
||||
coiMsgWarning('没有可复制的内容')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(content)
|
||||
coiMsgSuccess('内容已复制到剪贴板')
|
||||
}
|
||||
catch {
|
||||
coiMsgError('复制失败,请手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取缓存名称列表
|
||||
async function fetchCacheNames() {
|
||||
try {
|
||||
loading.value = true
|
||||
const { data, isSuccess } = await getRedisCache()
|
||||
|
||||
if (isSuccess && data) {
|
||||
cacheNames.value = data
|
||||
}
|
||||
else {
|
||||
coiMsgError('获取缓存名称列表失败')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取缓存名称列表异常:', error)
|
||||
coiMsgError('获取缓存名称列表异常')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取指定缓存名称的键列表
|
||||
async function fetchCacheKeys(cacheName: string) {
|
||||
try {
|
||||
loadingKeys.value = true
|
||||
console.warn('正在获取缓存键列表,缓存名称:', cacheName)
|
||||
const { data, isSuccess, message } = await getCacheKeys(cacheName)
|
||||
|
||||
if (isSuccess && data) {
|
||||
cacheKeys.value = data
|
||||
console.warn('获取缓存键列表成功,数据量:', data.length)
|
||||
if (data.length === 0) {
|
||||
coiMsgWarning(`缓存类型"${cacheName}"下暂无数据`)
|
||||
}
|
||||
}
|
||||
else {
|
||||
cacheKeys.value = []
|
||||
console.warn('获取缓存键列表失败:', message || '未知错误')
|
||||
coiMsgWarning(message || `缓存类型"${cacheName}"下暂无数据`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取缓存键列表异常:', error)
|
||||
cacheKeys.value = []
|
||||
coiMsgError('获取缓存键列表异常,请检查网络连接或权限配置')
|
||||
}
|
||||
finally {
|
||||
loadingKeys.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取缓存内容
|
||||
async function fetchCacheContent(cacheName: string, cacheKey: string) {
|
||||
try {
|
||||
loadingContent.value = true
|
||||
const { data, isSuccess } = await getCacheValue({
|
||||
cacheName,
|
||||
cacheKey,
|
||||
})
|
||||
|
||||
if (isSuccess && data) {
|
||||
cacheContent.value = data
|
||||
}
|
||||
else {
|
||||
cacheContent.value = undefined
|
||||
coiMsgError('获取缓存内容失败')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取缓存内容异常:', error)
|
||||
cacheContent.value = undefined
|
||||
coiMsgError('获取缓存内容异常')
|
||||
}
|
||||
finally {
|
||||
loadingContent.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 选择缓存名称
|
||||
async function selectCacheName(cacheName: string) {
|
||||
selectedCacheName.value = cacheName
|
||||
selectedCacheKey.value = ''
|
||||
cacheContent.value = undefined
|
||||
|
||||
await fetchCacheKeys(cacheName)
|
||||
}
|
||||
|
||||
// 选择缓存键
|
||||
async function selectCacheKey(cacheKey: string) {
|
||||
selectedCacheKey.value = cacheKey
|
||||
|
||||
if (selectedCacheName.value) {
|
||||
await fetchCacheContent(selectedCacheName.value, cacheKey)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除指定名称的缓存
|
||||
async function deleteCacheByName(cacheName: string) {
|
||||
try {
|
||||
const { isSuccess } = await deleteCacheName(cacheName)
|
||||
|
||||
if (isSuccess) {
|
||||
coiMsgSuccess('删除缓存成功')
|
||||
|
||||
// 如果删除的是当前选中的缓存,清空选择
|
||||
if (selectedCacheName.value === cacheName) {
|
||||
selectedCacheName.value = ''
|
||||
selectedCacheKey.value = ''
|
||||
cacheKeys.value = []
|
||||
cacheContent.value = undefined
|
||||
}
|
||||
|
||||
// 刷新缓存名称列表
|
||||
await fetchCacheNames()
|
||||
}
|
||||
else {
|
||||
coiMsgError('删除缓存失败')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('删除缓存异常:', error)
|
||||
coiMsgError('删除缓存异常')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除指定键的缓存
|
||||
async function deleteCacheByKey(cacheKey: string) {
|
||||
try {
|
||||
const { isSuccess } = await deleteCacheKey({
|
||||
cacheName: selectedCacheName.value,
|
||||
cacheKey,
|
||||
})
|
||||
|
||||
if (isSuccess) {
|
||||
coiMsgSuccess('删除缓存键成功')
|
||||
|
||||
// 如果删除的是当前选中的键,清空选择
|
||||
if (selectedCacheKey.value === cacheKey) {
|
||||
selectedCacheKey.value = ''
|
||||
cacheContent.value = undefined
|
||||
}
|
||||
|
||||
// 刷新当前缓存名称的键列表
|
||||
if (selectedCacheName.value) {
|
||||
await fetchCacheKeys(selectedCacheName.value)
|
||||
}
|
||||
}
|
||||
else {
|
||||
coiMsgError('删除缓存键失败')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('删除缓存键异常:', error)
|
||||
coiMsgError('删除缓存键异常')
|
||||
}
|
||||
}
|
||||
|
||||
// 清空所有缓存
|
||||
async function clearAllCache() {
|
||||
try {
|
||||
clearingAll.value = true
|
||||
const { isSuccess } = await deleteCacheAll()
|
||||
|
||||
if (isSuccess) {
|
||||
coiMsgSuccess('清空所有缓存成功')
|
||||
|
||||
// 清空所有选择和数据
|
||||
selectedCacheName.value = ''
|
||||
selectedCacheKey.value = ''
|
||||
cacheKeys.value = []
|
||||
cacheContent.value = undefined
|
||||
|
||||
// 刷新缓存名称列表
|
||||
await fetchCacheNames()
|
||||
}
|
||||
else {
|
||||
coiMsgError('清空所有缓存失败')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('清空所有缓存异常:', error)
|
||||
coiMsgError('清空所有缓存异常')
|
||||
}
|
||||
finally {
|
||||
clearingAll.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新缓存名称列表
|
||||
async function refreshCacheNames() {
|
||||
await fetchCacheNames()
|
||||
coiMsgSuccess('刷新缓存成功')
|
||||
}
|
||||
|
||||
// 刷新缓存键列表
|
||||
async function refreshCacheKeys() {
|
||||
if (selectedCacheName.value) {
|
||||
await fetchCacheKeys(selectedCacheName.value)
|
||||
coiMsgSuccess('刷新缓存键成功')
|
||||
}
|
||||
else {
|
||||
coiMsgWarning('请先选择缓存类型')
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新缓存内容
|
||||
async function refreshCacheContent() {
|
||||
if (selectedCacheName.value && selectedCacheKey.value) {
|
||||
await fetchCacheContent(selectedCacheName.value, selectedCacheKey.value)
|
||||
coiMsgSuccess('刷新缓存内容成功')
|
||||
}
|
||||
else {
|
||||
coiMsgWarning('请先选择缓存类型和缓存键')
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchCacheNames()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cache-monitor-container {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: var(--body-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.confirm-content {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.confirm-title {
|
||||
font-weight: 600;
|
||||
color: var(--text-color-1);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.confirm-desc {
|
||||
font-size: 13px;
|
||||
color: var(--text-color-3);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cache-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 360px 1fr;
|
||||
gap: 20px;
|
||||
height: calc(100vh - 140px);
|
||||
padding: 20px 20px 20px 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cache-names-panel,
|
||||
.cache-keys-panel,
|
||||
.cache-content-panel {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cache-names-list,
|
||||
.cache-keys-list {
|
||||
height: calc(100% - 20px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.cache-name-item,
|
||||
.cache-key-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 18px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--divider-color);
|
||||
margin-bottom: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: var(--card-color);
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.08),
|
||||
0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cache-name-item:hover,
|
||||
.cache-key-item:hover {
|
||||
background: var(--hover-color);
|
||||
border-color: var(--primary-color);
|
||||
box-shadow:
|
||||
0 4px 12px rgba(var(--primary-color-rgb), 0.12),
|
||||
0 2px 6px rgba(var(--primary-color-rgb), 0.08);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.cache-name-item.active,
|
||||
.cache-key-item.active {
|
||||
background: var(--primary-color-suppl);
|
||||
border-color: var(--primary-color);
|
||||
box-shadow:
|
||||
0 6px 20px rgba(var(--primary-color-rgb), 0.2),
|
||||
0 3px 10px rgba(var(--primary-color-rgb), 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.cache-name-item.active::before,
|
||||
.cache-key-item.active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 4px;
|
||||
height: 60%;
|
||||
background: var(--primary-color);
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.cache-name-content,
|
||||
.cache-key-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cache-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color-1);
|
||||
margin-bottom: 6px;
|
||||
word-break: break-all;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.cache-remark {
|
||||
font-size: 12px;
|
||||
color: var(--text-color-2);
|
||||
font-weight: 500;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.cache-key {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color-1);
|
||||
word-break: break-all;
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
line-height: 1.5;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.cache-actions,
|
||||
.cache-key-actions {
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.empty-state,
|
||||
.loading-state {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cache-content {
|
||||
height: calc(100% - 20px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content-meta {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 新的内容展示样式 */
|
||||
.content-meta-card {
|
||||
background: linear-gradient(135deg, var(--card-color) 0%, rgba(var(--primary-color-rgb), 0.03) 100%);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color-2);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-color-1);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.key-name {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
background: rgba(var(--primary-color-rgb), 0.1);
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
border: 1px solid rgba(var(--primary-color-rgb), 0.2);
|
||||
}
|
||||
|
||||
.content-section {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color-1);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.content-display {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.json-viewer {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background: var(--code-block-color, #1e1e1e);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.json-content {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #d4d4d4;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
background: linear-gradient(135deg, #1e1e1e 0%, #2d2d30 100%);
|
||||
border-radius: 8px;
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.text-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content-textarea {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content-textarea :deep(.n-input__textarea-el) {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
background: var(--input-color);
|
||||
border: none;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
:deep(.n-card) {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid rgba(var(--border-color-rgb, 239, 239, 245), 0.6);
|
||||
background: var(--card-color);
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
:deep(.n-card:hover) {
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
:deep(.n-card .n-card__header) {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
background: linear-gradient(135deg, var(--card-color) 0%, rgba(var(--primary-color-rgb), 0.02) 100%);
|
||||
border-radius: 16px 16px 0 0;
|
||||
}
|
||||
|
||||
:deep(.n-card .n-card__content) {
|
||||
padding: 24px;
|
||||
height: calc(100% - 70px);
|
||||
}
|
||||
|
||||
:deep(.n-card .n-card-header__main) {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: var(--text-color-1);
|
||||
}
|
||||
|
||||
/* 优化标签样式 */
|
||||
:deep(.n-tag) {
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
/* 优化空状态样式 */
|
||||
.empty-state,
|
||||
.loading-state {
|
||||
height: 300px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-color-3);
|
||||
}
|
||||
|
||||
/* 响应式设计优化 */
|
||||
@media (max-width: 1400px) {
|
||||
.cache-layout {
|
||||
grid-template-columns: 280px 320px 1fr;
|
||||
gap: 16px;
|
||||
padding: 16px 16px 16px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.cache-layout {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
height: auto;
|
||||
padding: 16px 16px 16px 0;
|
||||
}
|
||||
|
||||
.cache-names-panel,
|
||||
.cache-keys-panel,
|
||||
.cache-content-panel {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.content-meta-card {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.cache-layout {
|
||||
gap: 12px;
|
||||
padding: 12px 12px 12px 0;
|
||||
}
|
||||
|
||||
:deep(.n-card .n-card__header) {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
:deep(.n-card .n-card__content) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content-meta-card {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 清理重复样式,已在上面定义 */
|
||||
|
||||
/* 滚动条样式 */
|
||||
.cache-names-list::-webkit-scrollbar,
|
||||
.cache-keys-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.cache-names-list::-webkit-scrollbar-track,
|
||||
.cache-keys-list::-webkit-scrollbar-track {
|
||||
background: var(--scrollbar-track-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.cache-names-list::-webkit-scrollbar-thumb,
|
||||
.cache-keys-list::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.cache-names-list::-webkit-scrollbar-thumb:hover,
|
||||
.cache-keys-list::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--scrollbar-thumb-hover-color);
|
||||
}
|
||||
</style>
|
||||
@ -1,332 +0,0 @@
|
||||
<template>
|
||||
<div class="pie-chart-container">
|
||||
<div v-if="!data || data.length === 0" class="empty-state">
|
||||
<n-empty description="暂无命令统计数据">
|
||||
<template #icon>
|
||||
<n-icon><IconParkOutlineChartPie /></n-icon>
|
||||
</template>
|
||||
</n-empty>
|
||||
</div>
|
||||
<div v-else class="chart-content">
|
||||
<!-- SVG饼图 -->
|
||||
<svg class="pie-chart" :width="chartSize" :height="chartSize" :viewBox="`0 0 ${chartSize} ${chartSize}`">
|
||||
<!-- 饼图扇形 -->
|
||||
<g :transform="`translate(${chartSize / 2}, ${chartSize / 2})`">
|
||||
<path
|
||||
v-for="(segment, index) in pieSegments"
|
||||
:key="index"
|
||||
:d="segment.path"
|
||||
:fill="segment.color"
|
||||
stroke="var(--card-color)"
|
||||
:stroke-width="2"
|
||||
class="pie-segment"
|
||||
@mouseenter="hoveredIndex = index"
|
||||
@mouseleave="hoveredIndex = -1"
|
||||
/>
|
||||
</g>
|
||||
|
||||
<!-- 中心文本 -->
|
||||
<g :transform="`translate(${chartSize / 2}, ${chartSize / 2})`">
|
||||
<text
|
||||
class="center-text-title"
|
||||
text-anchor="middle"
|
||||
:y="-8"
|
||||
fill="var(--text-color-1)"
|
||||
>
|
||||
命令统计
|
||||
</text>
|
||||
<text
|
||||
class="center-text-value"
|
||||
text-anchor="middle"
|
||||
:y="12"
|
||||
fill="var(--text-color-2)"
|
||||
>
|
||||
{{ totalCommands.toLocaleString() }}
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<!-- 图例 -->
|
||||
<div class="chart-legend">
|
||||
<div
|
||||
v-for="(item, index) in topCommands"
|
||||
:key="index"
|
||||
class="legend-item"
|
||||
:class="{ active: hoveredIndex === index }"
|
||||
@mouseenter="hoveredIndex = index"
|
||||
@mouseleave="hoveredIndex = -1"
|
||||
>
|
||||
<div class="legend-color" :style="{ backgroundColor: colors[index] }" />
|
||||
<div class="legend-content">
|
||||
<div class="legend-name">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div class="legend-value">
|
||||
{{ Number(item.value).toLocaleString() }} ({{ item.percentage }}%)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="otherCommands > 0" class="legend-item">
|
||||
<div class="legend-color" :style="{ backgroundColor: colors[topCommands.length] }" />
|
||||
<div class="legend-content">
|
||||
<div class="legend-name">
|
||||
其他
|
||||
</div>
|
||||
<div class="legend-value">
|
||||
{{ otherCommands.toLocaleString() }} ({{ otherPercentage }}%)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import type { RedisCommandStatVo } from '@/service/api/monitor/redis'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
data: RedisCommandStatVo[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// 响应式数据
|
||||
const hoveredIndex = ref(-1)
|
||||
const chartSize = 240
|
||||
const radius = 80
|
||||
|
||||
// 颜色配置
|
||||
const colors = [
|
||||
'#18a058',
|
||||
'#2080f0',
|
||||
'#f0a020',
|
||||
'#d03050',
|
||||
'#722ed1',
|
||||
'#eb2f96',
|
||||
'#fa8c16',
|
||||
'#52c41a',
|
||||
'#13c2c2',
|
||||
'#1890ff',
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const totalCommands = computed(() => {
|
||||
return props.data.reduce((sum, item) => sum + Number(item.value), 0)
|
||||
})
|
||||
|
||||
// 取前8个命令,其余归为"其他"
|
||||
const topCommands = computed(() => {
|
||||
const sorted = [...props.data]
|
||||
.sort((a, b) => Number(b.value) - Number(a.value))
|
||||
.slice(0, 8)
|
||||
|
||||
return sorted.map(item => ({
|
||||
...item,
|
||||
percentage: totalCommands.value > 0
|
||||
? ((Number(item.value) / totalCommands.value) * 100).toFixed(1)
|
||||
: '0.0',
|
||||
}))
|
||||
})
|
||||
|
||||
const otherCommands = computed(() => {
|
||||
if (props.data.length <= 8)
|
||||
return 0
|
||||
|
||||
const topTotal = topCommands.value.reduce((sum, item) => sum + Number(item.value), 0)
|
||||
return totalCommands.value - topTotal
|
||||
})
|
||||
|
||||
const otherPercentage = computed(() => {
|
||||
if (otherCommands.value === 0 || totalCommands.value === 0)
|
||||
return '0.0'
|
||||
return ((otherCommands.value / totalCommands.value) * 100).toFixed(1)
|
||||
})
|
||||
|
||||
// 饼图扇形数据
|
||||
const pieSegments = computed(() => {
|
||||
const segments = []
|
||||
let currentAngle = -Math.PI / 2 // 从12点钟方向开始
|
||||
|
||||
// 添加主要命令扇形
|
||||
topCommands.value.forEach((item, index) => {
|
||||
const percentage = Number(item.value) / totalCommands.value
|
||||
const angle = percentage * 2 * Math.PI
|
||||
|
||||
if (angle > 0) {
|
||||
const path = createArcPath(currentAngle, currentAngle + angle, radius)
|
||||
segments.push({
|
||||
path,
|
||||
color: colors[index],
|
||||
percentage: item.percentage,
|
||||
})
|
||||
currentAngle += angle
|
||||
}
|
||||
})
|
||||
|
||||
// 添加"其他"扇形
|
||||
if (otherCommands.value > 0) {
|
||||
const percentage = otherCommands.value / totalCommands.value
|
||||
const angle = percentage * 2 * Math.PI
|
||||
|
||||
if (angle > 0) {
|
||||
const path = createArcPath(currentAngle, currentAngle + angle, radius)
|
||||
segments.push({
|
||||
path,
|
||||
color: colors[topCommands.value.length],
|
||||
percentage: otherPercentage.value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return segments
|
||||
})
|
||||
|
||||
// 创建弧形路径
|
||||
function createArcPath(startAngle: number, endAngle: number, radius: number): string {
|
||||
const x1 = Math.cos(startAngle) * radius
|
||||
const y1 = Math.sin(startAngle) * radius
|
||||
const x2 = Math.cos(endAngle) * radius
|
||||
const y2 = Math.sin(endAngle) * radius
|
||||
|
||||
const largeArcFlag = endAngle - startAngle > Math.PI ? 1 : 0
|
||||
|
||||
return [
|
||||
'M',
|
||||
0,
|
||||
0,
|
||||
'L',
|
||||
x1,
|
||||
y1,
|
||||
'A',
|
||||
radius,
|
||||
radius,
|
||||
0,
|
||||
largeArcFlag,
|
||||
1,
|
||||
x2,
|
||||
y2,
|
||||
'Z',
|
||||
].join(' ')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pie-chart-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pie-chart {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pie-segment {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.pie-segment:hover {
|
||||
filter: brightness(1.1);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.center-text-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.center-text-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
flex: 1;
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.legend-item:hover,
|
||||
.legend-item.active {
|
||||
background-color: var(--hover-color);
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.legend-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.legend-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-color-1);
|
||||
margin-bottom: 2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.legend-value {
|
||||
font-size: 12px;
|
||||
color: var(--text-color-3);
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.chart-legend::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.chart-legend::-webkit-scrollbar-track {
|
||||
background: var(--scrollbar-track-color);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.chart-legend::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb-color);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.chart-legend::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--scrollbar-thumb-hover-color);
|
||||
}
|
||||
</style>
|
||||
@ -1,272 +0,0 @@
|
||||
<template>
|
||||
<div class="memory-chart-container">
|
||||
<div class="chart-content">
|
||||
<!-- 圆环图表 -->
|
||||
<div class="ring-chart-wrapper">
|
||||
<svg class="ring-chart" :width="chartSize" :height="chartSize" :viewBox="`0 0 ${chartSize} ${chartSize}`">
|
||||
<!-- 背景圆环 -->
|
||||
<circle
|
||||
:cx="centerX"
|
||||
:cy="centerY"
|
||||
:r="radius"
|
||||
fill="none"
|
||||
stroke="var(--border-color)"
|
||||
:stroke-width="strokeWidth"
|
||||
/>
|
||||
|
||||
<!-- 进度圆环 -->
|
||||
<circle
|
||||
:cx="centerX"
|
||||
:cy="centerY"
|
||||
:r="radius"
|
||||
fill="none"
|
||||
:stroke="progressColor"
|
||||
:stroke-width="strokeWidth"
|
||||
:stroke-dasharray="circumference"
|
||||
:stroke-dashoffset="strokeDashoffset"
|
||||
stroke-linecap="round"
|
||||
class="progress-ring"
|
||||
:transform="`rotate(-90 ${centerX} ${centerY})`"
|
||||
/>
|
||||
|
||||
<!-- 中心内容 -->
|
||||
<g :transform="`translate(${centerX}, ${centerY})`">
|
||||
<text
|
||||
class="usage-percentage"
|
||||
text-anchor="middle"
|
||||
:y="-8"
|
||||
fill="var(--text-color-1)"
|
||||
>
|
||||
{{ formattedUsage }}
|
||||
</text>
|
||||
<text
|
||||
class="usage-label"
|
||||
text-anchor="middle"
|
||||
:y="12"
|
||||
fill="var(--text-color-3)"
|
||||
>
|
||||
内存使用率
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- 内存统计信息 -->
|
||||
<div class="memory-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">
|
||||
已用内存
|
||||
</div>
|
||||
<div class="stat-value" :style="{ color: progressColor }">
|
||||
{{ formatBytes(usedMemory) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">
|
||||
总内存
|
||||
</div>
|
||||
<div class="stat-value">
|
||||
{{ formatBytes(maxMemory) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">
|
||||
可用内存
|
||||
</div>
|
||||
<div class="stat-value">
|
||||
{{ formatBytes(availableMemory) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">
|
||||
状态
|
||||
</div>
|
||||
<div class="stat-value" :style="{ color: statusColor }">
|
||||
{{ memoryStatus }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
// Props
|
||||
interface Props {
|
||||
usedMemory: number
|
||||
maxMemory: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// 图表配置
|
||||
const chartSize = 280
|
||||
const centerX = chartSize / 2
|
||||
const centerY = chartSize / 2
|
||||
const radius = 90
|
||||
const strokeWidth = 16
|
||||
|
||||
// 计算属性
|
||||
const memoryUsage = computed(() => {
|
||||
if (props.maxMemory === 0 || props.usedMemory === 0)
|
||||
return 0
|
||||
const usage = (props.usedMemory / props.maxMemory) * 100
|
||||
return Math.min(Math.max(usage, 0), 100) // 确保在0-100之间
|
||||
})
|
||||
|
||||
const formattedUsage = computed(() => {
|
||||
return `${memoryUsage.value.toFixed(1)}%`
|
||||
})
|
||||
|
||||
const availableMemory = computed(() => {
|
||||
return Math.max(props.maxMemory - props.usedMemory, 0)
|
||||
})
|
||||
|
||||
// 圆环图相关计算
|
||||
const circumference = computed(() => 2 * Math.PI * radius)
|
||||
|
||||
const strokeDashoffset = computed(() => {
|
||||
const progress = memoryUsage.value / 100
|
||||
return circumference.value * (1 - progress)
|
||||
})
|
||||
|
||||
const progressColor = computed(() => {
|
||||
const usage = memoryUsage.value
|
||||
if (usage < 50)
|
||||
return '#18a058' // 绿色
|
||||
if (usage < 80)
|
||||
return '#f0a020' // 黄色
|
||||
return '#d03050' // 红色
|
||||
})
|
||||
|
||||
const statusColor = computed(() => {
|
||||
return progressColor.value
|
||||
})
|
||||
|
||||
const memoryStatus = computed(() => {
|
||||
const usage = memoryUsage.value
|
||||
if (usage === 0)
|
||||
return '未知'
|
||||
if (usage < 50)
|
||||
return '良好'
|
||||
if (usage < 80)
|
||||
return '警告'
|
||||
return '危险'
|
||||
})
|
||||
|
||||
// 格式化字节
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes === 0)
|
||||
return '0 B'
|
||||
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.memory-chart-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.ring-chart-wrapper {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ring-chart {
|
||||
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
.progress-ring {
|
||||
transition: stroke-dashoffset 1.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
stroke 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-ring:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.usage-percentage {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.usage-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.memory-stats {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(var(--primary-color-rgb), 0.05);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.stat-item:hover {
|
||||
background: rgba(var(--primary-color-rgb), 0.08);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-color-3);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text-color-1);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.chart-content {
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.memory-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,316 +0,0 @@
|
||||
<template>
|
||||
<div class="redis-monitor-container">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header mb-6">
|
||||
<div class="header-left">
|
||||
<h2 class="page-title">
|
||||
Redis监控
|
||||
</h2>
|
||||
<p class="page-subtitle">
|
||||
实时监控Redis服务器状态和性能指标
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<n-button type="primary" :loading="loading" @click="refreshData">
|
||||
<template #icon>
|
||||
<n-icon><IconParkOutlineRefresh /></n-icon>
|
||||
</template>
|
||||
刷新数据
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Redis基本信息 -->
|
||||
<n-card title="基本信息" class="mb-6">
|
||||
<template #header-extra>
|
||||
<n-icon><IconParkOutlineServer /></n-icon>
|
||||
</template>
|
||||
<n-grid :x-gap="16" :y-gap="16">
|
||||
<n-gi :span="8">
|
||||
<n-descriptions :column="1" label-placement="left" :label-style="{ width: '100px' }">
|
||||
<n-descriptions-item label="Redis版本">
|
||||
{{ getRedisInfo('redis_version') || 'N/A' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="运行模式">
|
||||
{{ getRedisMode() }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="端口">
|
||||
{{ getRedisInfo('tcp_port') || 'N/A' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="客户端连接">
|
||||
{{ getRedisInfo('connected_clients') || '0' }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</n-gi>
|
||||
<n-gi :span="8">
|
||||
<n-descriptions :column="1" label-placement="left" :label-style="{ width: '120px' }">
|
||||
<n-descriptions-item label="运行时间(天)">
|
||||
{{ formatUptime(getRedisInfo('uptime_in_days')) }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="使用内存">
|
||||
{{ formatMemory(getRedisInfo('used_memory_human')) }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="最大内存">
|
||||
{{ getRedisInfo('maxmemory') === '0' ? '无限制' : formatBytes(getRedisInfo('maxmemory')) }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="使用CPU">
|
||||
{{ getRedisInfo('used_cpu_user_children') || '0' }}%
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="Key数量">
|
||||
{{ redisData?.dbSize || 0 }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</n-gi>
|
||||
<n-gi :span="8">
|
||||
<n-descriptions :column="1" label-placement="left" :label-style="{ width: '120px' }">
|
||||
<n-descriptions-item label="网络输入/输出">
|
||||
{{ formatNetwork() }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="AOF是否开启">
|
||||
{{ getRedisInfo('aof_enabled') === '1' ? '是' : '否' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="RDB是否成功">
|
||||
{{ getRedisInfo('rdb_last_save_time') ? '是' : '否' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="Key命中率">
|
||||
{{ getKeyHitRate() }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<n-grid :x-gap="16" :y-gap="16">
|
||||
<!-- 命令统计饼图 -->
|
||||
<n-gi :span="12">
|
||||
<n-card title="命令统计" class="chart-card">
|
||||
<template #header-extra>
|
||||
<n-icon><IconParkOutlineChartPie /></n-icon>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<CommandStatsPieChart :data="redisData?.commandStats || []" />
|
||||
</div>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
|
||||
<!-- 内存消耗仪表盘 -->
|
||||
<n-gi :span="12">
|
||||
<n-card title="内存消耗" class="chart-card">
|
||||
<template #header-extra>
|
||||
<n-icon><IconParkOutlineData /></n-icon>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<MemoryGaugeChart :used-memory="getUsedMemory()" :max-memory="getMaxMemory()" />
|
||||
</div>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { coiMsgError, coiMsgSuccess } from '@/utils/coi'
|
||||
import { getRedisInformation } from '@/service/api/monitor/redis'
|
||||
import type { RedisInfoVo } from '@/service/api/monitor/redis'
|
||||
import CommandStatsPieChart from './components/CommandStatsPieChart.vue'
|
||||
import MemoryGaugeChart from './components/MemoryGaugeChart.vue'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const redisData = ref<RedisInfoVo>()
|
||||
|
||||
// 获取Redis信息的辅助函数
|
||||
function getRedisInfo(key: string): string {
|
||||
return redisData.value?.info?.[key] || ''
|
||||
}
|
||||
|
||||
// 获取Redis运行模式
|
||||
function getRedisMode(): string {
|
||||
const mode = getRedisInfo('redis_mode')
|
||||
if (mode === 'standalone')
|
||||
return '单机'
|
||||
if (mode === 'cluster')
|
||||
return '集群'
|
||||
if (mode === 'sentinel')
|
||||
return '哨兵'
|
||||
return mode || '未知'
|
||||
}
|
||||
|
||||
// 格式化运行时间
|
||||
function formatUptime(days: string): string {
|
||||
const numDays = Number(days) || 0
|
||||
return `${numDays}天`
|
||||
}
|
||||
|
||||
// 格式化内存
|
||||
function formatMemory(memory: string): string {
|
||||
return memory || '0B'
|
||||
}
|
||||
|
||||
// 格式化网络输入输出
|
||||
function formatNetwork(): string {
|
||||
const inputKb = getRedisInfo('total_net_input_bytes')
|
||||
const outputKb = getRedisInfo('total_net_output_bytes')
|
||||
return `${formatBytes(inputKb)}/${formatBytes(outputKb)}`
|
||||
}
|
||||
|
||||
// 格式化字节
|
||||
function formatBytes(bytes: string): string {
|
||||
const num = Number(bytes) || 0
|
||||
if (num === 0)
|
||||
return '0B'
|
||||
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(num) / Math.log(k))
|
||||
|
||||
return `${Number.parseFloat((num / k ** i).toFixed(1))}${sizes[i]}`
|
||||
}
|
||||
|
||||
// 获取Key命中率
|
||||
function getKeyHitRate(): string {
|
||||
const hits = Number(getRedisInfo('keyspace_hits')) || 0
|
||||
const misses = Number(getRedisInfo('keyspace_misses')) || 0
|
||||
const total = hits + misses
|
||||
|
||||
if (total === 0)
|
||||
return '0.00%'
|
||||
|
||||
const rate = (hits / total) * 100
|
||||
return `${rate.toFixed(2)}%`
|
||||
}
|
||||
|
||||
// 获取已用内存(数值)
|
||||
function getUsedMemory(): number {
|
||||
const used = getRedisInfo('used_memory')
|
||||
return Number(used) || 0
|
||||
}
|
||||
|
||||
// 获取最大内存(数值)
|
||||
function getMaxMemory(): number {
|
||||
const max = getRedisInfo('maxmemory')
|
||||
const maxMemory = Number(max) || 0
|
||||
|
||||
// 如果Redis没有设置maxmemory(值为0),尝试获取系统总内存作为参考
|
||||
if (maxMemory === 0) {
|
||||
const systemMemory = getRedisInfo('total_system_memory')
|
||||
if (systemMemory) {
|
||||
return Number(systemMemory)
|
||||
}
|
||||
|
||||
// 如果没有系统内存信息,使用一个合理的默认值
|
||||
// 基于已用内存估算一个合理的总内存(已用内存的2-4倍作为参考)
|
||||
const used = getUsedMemory()
|
||||
if (used > 0) {
|
||||
return Math.max(used * 3, 1024 * 1024 * 1024) // 至少1GB
|
||||
}
|
||||
|
||||
// 最后的兜底值:2GB
|
||||
return 2 * 1024 * 1024 * 1024
|
||||
}
|
||||
|
||||
return maxMemory
|
||||
}
|
||||
|
||||
// 获取Redis监控数据
|
||||
async function fetchRedisData() {
|
||||
try {
|
||||
loading.value = true
|
||||
const { data, isSuccess } = await getRedisInformation()
|
||||
|
||||
if (isSuccess && data) {
|
||||
redisData.value = data
|
||||
}
|
||||
else {
|
||||
coiMsgError('获取Redis监控数据失败')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取Redis监控数据异常:', error)
|
||||
coiMsgError('获取Redis监控数据异常')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
async function refreshData() {
|
||||
await fetchRedisData()
|
||||
coiMsgSuccess('数据刷新成功')
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchRedisData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.redis-monitor-container {
|
||||
padding: 20px;
|
||||
background-color: var(--body-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color-1);
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--text-color-3);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 420px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:deep(.n-card) {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:deep(.n-card .n-card__header) {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
:deep(.n-card .n-card__content) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
:deep(.n-descriptions .n-descriptions-item-label) {
|
||||
font-weight: 500;
|
||||
color: var(--text-color-2);
|
||||
}
|
||||
</style>
|
||||
@ -1,182 +0,0 @@
|
||||
<template>
|
||||
<n-card class="stat-card" :style="cardStyle" hoverable>
|
||||
<!-- 主要内容区域 -->
|
||||
<n-space justify="space-between" align="center" class="mb-2">
|
||||
<!-- 左侧数据区域 -->
|
||||
<div class="stat-content">
|
||||
<div class="stat-title">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="stat-value">
|
||||
{{ value }}
|
||||
</div>
|
||||
<div class="stat-subtitle">
|
||||
{{ subtitle }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧图标区域 -->
|
||||
<div class="stat-icon" :style="iconStyle">
|
||||
<n-icon :size="28">
|
||||
<IconParkOutlineServer v-if="icon === 'server'" />
|
||||
<IconParkOutlineComputer v-else-if="icon === 'system'" />
|
||||
<IconParkOutlineNetworkTree v-else-if="icon === 'network'" />
|
||||
<IconParkOutlineChip v-else-if="icon === 'chip'" />
|
||||
<IconParkOutlineCpu v-else-if="icon === 'cpu'" />
|
||||
<IconParkOutlineData v-else-if="icon === 'memory'" />
|
||||
<IconParkOutlineCode v-else-if="icon === 'java'" />
|
||||
<IconParkOutlineServer v-else />
|
||||
</n-icon>
|
||||
</div>
|
||||
</n-space>
|
||||
|
||||
<!-- 底部信息区域 -->
|
||||
<div v-if="trend || extraInfo" class="stat-footer">
|
||||
<n-space justify="space-between" align="center">
|
||||
<div class="stat-extra">
|
||||
{{ extraInfo }}
|
||||
</div>
|
||||
<div v-if="trend" class="stat-trend" :class="trendClass">
|
||||
<n-icon :size="12" class="trend-icon">
|
||||
<IconParkOutlineUp v-if="trendUp" />
|
||||
<IconParkOutlineDown v-else />
|
||||
</n-icon>
|
||||
<span class="trend-text">{{ trend }}</span>
|
||||
</div>
|
||||
</n-space>
|
||||
</div>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
// 定义Props接口
|
||||
interface StatCardProps {
|
||||
title: string
|
||||
value: string | number
|
||||
subtitle: string
|
||||
icon?: string
|
||||
color?: string
|
||||
trend?: string
|
||||
trendUp?: boolean
|
||||
extraInfo?: string
|
||||
}
|
||||
|
||||
// 定义Props
|
||||
const props = withDefaults(defineProps<StatCardProps>(), {
|
||||
icon: 'server',
|
||||
color: '#18a058',
|
||||
trendUp: true,
|
||||
})
|
||||
|
||||
// 卡片样式
|
||||
const cardStyle = computed(() => ({
|
||||
background:
|
||||
'linear-gradient(135deg, var(--card-color) 0%, rgba(255, 255, 255, 0.6) 100%)',
|
||||
border: '1px solid var(--border-color)',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.08), 0 1px 4px rgba(0, 0, 0, 0.05)',
|
||||
transition: 'all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)',
|
||||
padding: '18px',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
minHeight: '140px',
|
||||
}))
|
||||
|
||||
// 图标样式
|
||||
const iconStyle = computed(() => ({
|
||||
color: props.color,
|
||||
background: `${props.color}12`,
|
||||
borderRadius: '10px',
|
||||
padding: '10px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}))
|
||||
|
||||
// 趋势样式类
|
||||
const trendClass = computed(() => ({
|
||||
'trend-up': props.trendUp,
|
||||
'trend-down': !props.trendUp,
|
||||
}))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-card {
|
||||
height: 100%;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-color-3);
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--text-color-1);
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.2;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.stat-subtitle {
|
||||
font-size: 12px;
|
||||
color: var(--text-color-3);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
flex-shrink: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-footer {
|
||||
margin-top: 12px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.stat-extra {
|
||||
font-size: 12px;
|
||||
color: var(--text-color-3);
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.trend-up {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.trend-down {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.trend-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trend-text {
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
@ -1,327 +0,0 @@
|
||||
<template>
|
||||
<div class="server-monitor-container">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header mb-6">
|
||||
<div class="header-left">
|
||||
<h2 class="page-title">
|
||||
服务监控
|
||||
</h2>
|
||||
<p class="page-subtitle">
|
||||
实时监控服务器运行状态和系统资源使用情况
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<n-button type="primary" :loading="loading" @click="refreshData">
|
||||
<template #icon>
|
||||
<n-icon><IconParkOutlineRefresh /></n-icon>
|
||||
</template>
|
||||
刷新数据
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统信息统计卡片 -->
|
||||
<n-grid :x-gap="16" :y-gap="16" class="mb-6">
|
||||
<n-gi :span="6">
|
||||
<StatCard
|
||||
title="服务器名称"
|
||||
:value="serverData?.sys?.computerName || 'N/A'"
|
||||
subtitle="主机名"
|
||||
icon="server"
|
||||
color="#18a058"
|
||||
/>
|
||||
</n-gi>
|
||||
<n-gi :span="6">
|
||||
<StatCard
|
||||
title="操作系统"
|
||||
:value="serverData?.sys?.osName || 'N/A'"
|
||||
subtitle="系统版本"
|
||||
icon="system"
|
||||
color="#2080f0"
|
||||
/>
|
||||
</n-gi>
|
||||
<n-gi :span="6">
|
||||
<StatCard
|
||||
title="服务器IP"
|
||||
:value="serverData?.sys?.computerIp || 'N/A'"
|
||||
subtitle="主机地址"
|
||||
icon="network"
|
||||
color="#f0a020"
|
||||
/>
|
||||
</n-gi>
|
||||
<n-gi :span="6">
|
||||
<StatCard
|
||||
title="系统架构"
|
||||
:value="serverData?.sys?.osArch || 'N/A'"
|
||||
subtitle="处理器架构"
|
||||
icon="chip"
|
||||
color="#d03050"
|
||||
/>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<!-- CPU和内存信息 -->
|
||||
<n-grid :x-gap="16" :y-gap="16" class="mb-6">
|
||||
<n-gi :span="8">
|
||||
<StatCard
|
||||
title="CPU使用率"
|
||||
:value="formatPercentage(serverData?.cpu?.cpuUsage)"
|
||||
subtitle="处理器负载"
|
||||
icon="cpu"
|
||||
color="#18a058"
|
||||
:trend="formatPercentage(serverData?.cpu?.cpuUsage)"
|
||||
:trend-up="(serverData?.cpu?.cpuUsage || 0) < 80"
|
||||
/>
|
||||
</n-gi>
|
||||
<n-gi :span="8">
|
||||
<StatCard
|
||||
title="内存使用率"
|
||||
:value="formatPercentage(serverData?.mem?.usage)"
|
||||
subtitle="内存负载"
|
||||
icon="memory"
|
||||
color="#2080f0"
|
||||
:trend="formatPercentage(serverData?.mem?.usage)"
|
||||
:trend-up="(serverData?.mem?.usage || 0) < 80"
|
||||
/>
|
||||
</n-gi>
|
||||
<n-gi :span="8">
|
||||
<StatCard
|
||||
title="JVM使用率"
|
||||
:value="formatPercentage(serverData?.jvm?.usage)"
|
||||
subtitle="Java虚拟机"
|
||||
icon="java"
|
||||
color="#f0a020"
|
||||
:trend="formatPercentage(serverData?.jvm?.usage)"
|
||||
:trend-up="(serverData?.jvm?.usage || 0) < 80"
|
||||
/>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<!-- Java环境信息 -->
|
||||
<n-card title="Java环境信息" class="mb-6">
|
||||
<template #header-extra>
|
||||
<n-icon><IconParkOutlineCode /></n-icon>
|
||||
</template>
|
||||
<n-descriptions :column="3" label-placement="left" :label-style="{ width: '120px' }">
|
||||
<n-descriptions-item label="Java版本">
|
||||
{{ serverData?.jvm?.version || 'N/A' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="启动时间">
|
||||
{{ serverData?.jvm?.startTime || 'N/A' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="运行时间">
|
||||
{{ serverData?.jvm?.runTime || 'N/A' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="安装路径">
|
||||
{{ serverData?.jvm?.home || 'N/A' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="项目路径">
|
||||
{{ serverData?.sys?.userDir || 'N/A' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="运行参数">
|
||||
<n-tag type="info" size="small">
|
||||
-Djava.version={{ serverData?.jvm?.version || 'N/A' }}
|
||||
</n-tag>
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
|
||||
<!-- JVM内存详情 -->
|
||||
<n-divider>JVM内存详情</n-divider>
|
||||
<n-space>
|
||||
<n-statistic label="总内存" :value="serverData?.jvm?.totalStr || '0 MB'" />
|
||||
<n-statistic label="已用内存" :value="serverData?.jvm?.usedStr || '0 MB'" />
|
||||
<n-statistic label="剩余内存" :value="serverData?.jvm?.freeStr || '0 MB'" />
|
||||
<n-statistic label="最大内存" :value="serverData?.jvm?.maxStr || '0 MB'" />
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<!-- 磁盘状态 -->
|
||||
<n-card title="磁盘状态">
|
||||
<template #header-extra>
|
||||
<n-icon><IconParkOutlineHdd /></n-icon>
|
||||
</template>
|
||||
<n-data-table
|
||||
:columns="diskColumns"
|
||||
:data="serverData?.sysFiles || []"
|
||||
:loading="loading"
|
||||
size="small"
|
||||
:bordered="false"
|
||||
/>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { coiMsgError, coiMsgSuccess } from '@/utils/coi'
|
||||
import { getServerInformation } from '@/service/api/monitor/server'
|
||||
import type { ServerVo, SysFileVo } from '@/service/api/monitor/server'
|
||||
import StatCard from './components/StatCard.vue'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const serverData = ref<ServerVo>()
|
||||
|
||||
// 磁盘状态表格列定义
|
||||
const diskColumns: DataTableColumns<SysFileVo> = [
|
||||
{
|
||||
title: '盘符路径',
|
||||
key: 'dirName',
|
||||
width: 120,
|
||||
ellipsis: {
|
||||
tooltip: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '文件系统',
|
||||
key: 'sysTypeName',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '盘符类型',
|
||||
key: 'typeName',
|
||||
width: 200,
|
||||
ellipsis: {
|
||||
tooltip: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '总大小',
|
||||
key: 'total',
|
||||
width: 100,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: '可用大小',
|
||||
key: 'free',
|
||||
width: 100,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: '已用大小',
|
||||
key: 'used',
|
||||
width: 100,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: '已用百分比',
|
||||
key: 'usage',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
render: (row) => {
|
||||
const usage = row.usage || 0
|
||||
const color = usage > 80 ? '#d03050' : usage > 60 ? '#f0a020' : '#18a058'
|
||||
return h('div', { style: { color } }, `${usage.toFixed(1)}%`)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// 格式化百分比
|
||||
function formatPercentage(value?: number): string {
|
||||
if (value === undefined || value === null)
|
||||
return '0.0%'
|
||||
return `${value.toFixed(1)}%`
|
||||
}
|
||||
|
||||
// 获取服务器监控数据
|
||||
async function fetchServerData() {
|
||||
try {
|
||||
loading.value = true
|
||||
const { data, isSuccess } = await getServerInformation()
|
||||
|
||||
if (isSuccess && data) {
|
||||
serverData.value = data
|
||||
}
|
||||
else {
|
||||
coiMsgError('获取服务器监控数据失败')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取服务器监控数据异常:', error)
|
||||
coiMsgError('获取服务器监控数据异常')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
async function refreshData() {
|
||||
await fetchServerData()
|
||||
coiMsgSuccess('数据刷新成功')
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchServerData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.server-monitor-container {
|
||||
padding: 20px;
|
||||
background-color: var(--body-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color-1);
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--text-color-3);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
:deep(.n-card) {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:deep(.n-card .n-card__header) {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
:deep(.n-card .n-card__content) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
:deep(.n-descriptions .n-descriptions-item-label) {
|
||||
font-weight: 500;
|
||||
color: var(--text-color-2);
|
||||
}
|
||||
|
||||
:deep(.n-divider) {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
:deep(.n-statistic) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(.n-data-table) {
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user