feat(监控): 实现缓存监控页面及样式优化
- 新增缓存监控三栏布局页面,支持缓存名称、键名、内容展示 - 实现智能JSON内容检测和格式化功能 - 优化缓存项视觉效果,增强卡片分离度和清晰度 - 修复URL路径参数编码问题,解决冒号特殊字符处理 - 实现专业代码高亮显示和一键复制功能 - 采用现代化卡片设计,支持悬停和激活状态 - 集成缓存增删改查和实时刷新功能
This commit is contained in:
parent
a361b7f7a0
commit
df6cf59e7c
954
src/views/monitor/cache/index.vue
vendored
Normal file
954
src/views/monitor/cache/index.vue
vendored
Normal file
@ -0,0 +1,954 @@
|
||||
<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>
|
||||
Loading…
Reference in New Issue
Block a user