|
|
@@ -24,18 +24,13 @@
|
|
|
|
|
|
<div class="banner-stats">
|
|
|
<div class="stat-item">
|
|
|
- <div class="stat-label">巡检范围</div>
|
|
|
- <div class="stat-value">{{ reportData.totalNodes || 0 }} <small>Nodes</small></div>
|
|
|
+ <div class="stat-label">巡检项</div>
|
|
|
+ <div class="stat-value">{{ cardConfigs.length }}</div>
|
|
|
</div>
|
|
|
<div class="stat-divider"></div>
|
|
|
<div class="stat-item">
|
|
|
- <div class="stat-label">检测耗时</div>
|
|
|
- <div class="stat-value">{{ reportData.checkDuration || '0ms' }}</div>
|
|
|
- </div>
|
|
|
- <div class="stat-divider" v-if="hasAnyIssue"></div>
|
|
|
- <div class="stat-item" v-if="hasAnyIssue">
|
|
|
- <div class="stat-label">风险指数</div>
|
|
|
- <div class="stat-value risk-text">{{ reportData.riskLevel || 'High' }}</div>
|
|
|
+ <div class="stat-label">风险项</div>
|
|
|
+ <div class="stat-value" :class="hasAnyIssue ? 'risk-text' : ''">{{ totalIssuesCount }}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
@@ -53,15 +48,36 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <div class="report-grid">
|
|
|
- <el-row :gutter="20" class="force-row">
|
|
|
+ <div v-for="cat in categories" :key="cat" class="category-group">
|
|
|
+ <div class="category-header">
|
|
|
+ <div class="category-title-box">
|
|
|
+ <span class="category-name">{{ cat }}</span>
|
|
|
+ <el-badge
|
|
|
+ :value="getCategoryIssueCount(cat)"
|
|
|
+ :hidden="getCategoryIssueCount(cat) === 0"
|
|
|
+ type="danger"
|
|
|
+ is-dot
|
|
|
+ class="cat-badge"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="category-line"></div>
|
|
|
+ </div>
|
|
|
|
|
|
- <el-col :span="12" v-for="config in cardConfigs" :key="config.key" class="mb-20">
|
|
|
+ <el-row :gutter="24" class="card-grid-row">
|
|
|
+ <el-col
|
|
|
+ :xs="24" :sm="12" :md="8"
|
|
|
+ v-for="config in getCardsByCategory(cat)"
|
|
|
+ :key="config.key"
|
|
|
+ class="card-col"
|
|
|
+ >
|
|
|
<el-card shadow="hover" class="nice-card" :class="getShadowClass(config)">
|
|
|
<div slot="header" class="card-header">
|
|
|
<div class="title-wrapper">
|
|
|
<i :class="[config.icon, 'icon-accent', config.textColor]"></i>
|
|
|
- <span class="title-text">{{ config.title }} <small>{{ config.subTitle }}</small></span>
|
|
|
+ <div class="text-group">
|
|
|
+ <span class="main-title">{{ config.title }}</span>
|
|
|
+ <span class="sub-title">{{ config.subTitle }}</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
<div class="header-ops">
|
|
|
<el-tooltip content="排查建议" placement="top">
|
|
|
@@ -72,7 +88,7 @@
|
|
|
@click="showAdvice(config)"
|
|
|
></el-button>
|
|
|
</el-tooltip>
|
|
|
- <el-tag v-if="getData(config.key).length > 0" :type="config.tagType" size="mini" effect="dark" class="status-tag">
|
|
|
+ <el-tag v-if="getData(config.key).length > 0" :type="config.tagType" size="mini" effect="dark">
|
|
|
{{ getData(config.key).length }}
|
|
|
</el-tag>
|
|
|
</div>
|
|
|
@@ -80,10 +96,14 @@
|
|
|
|
|
|
<div class="card-body scroll-style">
|
|
|
<template v-if="getData(config.key).length > 0">
|
|
|
- <div v-for="(item, index) in getData(config.key)" :key="index" class="progress-item">
|
|
|
- <div class="info">
|
|
|
- <span class="name text-ellipsis">{{ item.instance || item.name }}</span>
|
|
|
- <span class="val" :class="config.textColor">{{ item.value }}{{ config.unit }}</span>
|
|
|
+ <div v-for="(item, index) in getData(config.key)" :key="index" class="metric-entry">
|
|
|
+ <div class="entry-info">
|
|
|
+ <span class="entry-name" :title="item.name || item.instance">
|
|
|
+ {{ item.name || item.instance }}
|
|
|
+ </span>
|
|
|
+ <span class="entry-val" :class="config.textColor">
|
|
|
+ {{ item.value }}<small>{{ config.unit }}</small>
|
|
|
+ </span>
|
|
|
</div>
|
|
|
<el-progress
|
|
|
:percentage="config.isRatio ? (item.value > 100 ? 100 : item.value) : 100"
|
|
|
@@ -93,8 +113,9 @@
|
|
|
/>
|
|
|
</div>
|
|
|
</template>
|
|
|
- <div v-else class="status-placeholder">
|
|
|
- <i class="el-icon-circle-check"></i><p>{{ config.emptyText }}</p>
|
|
|
+ <div v-else class="empty-state">
|
|
|
+ <i class="el-icon-circle-check"></i>
|
|
|
+ <p>{{ config.emptyText }}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
</el-card>
|
|
|
@@ -105,19 +126,18 @@
|
|
|
<el-dialog
|
|
|
:title="'💡 排查建议: ' + currentAdvice.title"
|
|
|
:visible.sync="dialogVisible"
|
|
|
- width="500px"
|
|
|
+ width="550px"
|
|
|
custom-class="advice-dialog"
|
|
|
append-to-body
|
|
|
>
|
|
|
- <div class="advice-content">
|
|
|
- <div v-if="currentAdvice.text" class="markdown-body">
|
|
|
- <i class="el-icon-guideAdvice"></i>
|
|
|
+ <div class="advice-container">
|
|
|
+ <div v-if="currentAdvice.text" class="advice-box">
|
|
|
<p v-html="formatAdvice(currentAdvice.text)"></p>
|
|
|
</div>
|
|
|
- <el-empty v-else description="暂无排查建议" :image-size="60"></el-empty>
|
|
|
+ <el-empty v-else description="暂无指引" :image-size="60"></el-empty>
|
|
|
</div>
|
|
|
- <span slot="footer" class="dialog-footer">
|
|
|
- <el-button type="primary" size="small" @click="dialogVisible = false">知道了</el-button>
|
|
|
+ <span slot="footer">
|
|
|
+ <el-button type="primary" @click="dialogVisible = false">确 定</el-button>
|
|
|
</span>
|
|
|
</el-dialog>
|
|
|
</div>
|
|
|
@@ -132,50 +152,69 @@ export default {
|
|
|
loading: false,
|
|
|
dialogVisible: false,
|
|
|
currentAdvice: { title: '', text: '' },
|
|
|
+ // 业务分类
|
|
|
+ categories: ['计算资源', '内存指标', '存储文件', '网络协议', '基础环境'],
|
|
|
reportData: {
|
|
|
- cpuThrottled: [], advice_cpuThrottled: '',
|
|
|
- memRisk: [], advice_memRisk: '',
|
|
|
- diskIo: [], advice_diskIo: '',
|
|
|
- diskUsageRisk: [], advice_diskUsageRisk: '',
|
|
|
- inodeRisk: [], advice_inodeRisk: '',
|
|
|
- fdRisk: [], advice_fdRisk: '',
|
|
|
- netEstMax: [], advice_netEstMax: '',
|
|
|
- netTwMax: [], advice_netTwMax: '',
|
|
|
- netOverflows: [], advice_netOverflows: '',
|
|
|
- netDrops: [], advice_netDrops: '',
|
|
|
- advices: {}
|
|
|
+ advices: {},
|
|
|
+ // 动态数据将通过接口覆盖
|
|
|
},
|
|
|
- // 使用配置化方式渲染卡片,减少 HTML 冗余
|
|
|
+ // 完整的 22 个指标配置
|
|
|
cardConfigs: [
|
|
|
- { key: 'cpuThrottled', title: 'CPU 性能节流', subTitle: '24H Throttled', icon: 'el-icon-cpu', textColor: 'warning-text', tagType: 'warning', progColor: '#e6a23c', unit: 's', isRatio: false, emptyText: '无节流限制' },
|
|
|
- { key: 'memRisk', title: '内存水位线', subTitle: '> 85% Usage', icon: 'el-icon-box', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '%', isRatio: true, emptyText: '水位正常' },
|
|
|
- { key: 'diskIo', title: '磁盘 I/O 响应', subTitle: 'Wait > 50ms', icon: 'el-icon-receiving', textColor: 'warning-text', tagType: 'warning', progColor: '#e6a23c', unit: 'ms', isRatio: false, emptyText: 'IO 响应极快' },
|
|
|
- { key: 'diskUsageRisk', title: '根分区空间', subTitle: '( / ) > 85%', icon: 'el-icon-pie-chart', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '%', isRatio: true, emptyText: '磁盘空间充沛' },
|
|
|
- { key: 'inodeRisk', title: 'inode 使用率', subTitle: '> 80% Usage', icon: 'el-icon-files', textColor: 'warning-text', tagType: 'warning', progColor: '#e6a23c', unit: '%', isRatio: true, emptyText: '索引节点充足' },
|
|
|
- { key: 'fdRisk', title: '文件句柄 (FD)', subTitle: 'Usage Ratio', icon: 'el-icon-link', textColor: 'primary-text', tagType: 'primary', progColor: '#409eff', unit: '%', isRatio: true, emptyText: 'FD 占用正常' },
|
|
|
- { key: 'netEstMax', title: 'TCP EST 状态数量', subTitle: 'TCP EST', icon: 'el-icon-warning-outline', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '次', isRatio: false, emptyText: '未超过阈值' },
|
|
|
- { key: 'netTwMax', title: 'TCP TIME_WAIT 状态', subTitle: 'TCP TIME_WAIT', icon: 'el-icon-warning-outline', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '次', isRatio: false, emptyText: '未超过阈值' },
|
|
|
- { key: 'netOverflows', title: 'TCP 全连接队列溢出', subTitle: 'Listen Overflow', icon: 'el-icon-warning-outline', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '次', isRatio: false, emptyText: '无队列溢出' },
|
|
|
- { key: 'netDrops', title: 'TCP 数据包丢弃', subTitle: 'TCP Drops', icon: 'el-icon-circle-close', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '次', isRatio: false, emptyText: '无 TCP 丢弃' }
|
|
|
+ /* 计算资源 */
|
|
|
+ { category: '计算资源', key: 'cpuThrottled', title: 'CPU 性能节流', subTitle: '24H Throttled', icon: 'el-icon-cpu', textColor: 'warning-text', tagType: 'warning', progColor: '#e6a23c', unit: 's', isRatio: false, emptyText: '无节流限制' },
|
|
|
+ { category: '计算资源', key: 'nodeLoadRisk', title: '节点负载饱和度', subTitle: 'Load5 / Cores', icon: 'el-icon-odometer', textColor: 'warning-text', tagType: 'warning', progColor: '#e6a23c', unit: '', isRatio: false, emptyText: '系统负载极低' },
|
|
|
+ { category: '计算资源', key: 'contextSwitchRisk', title: '上下文切换', subTitle: 'Context Switches', icon: 'el-icon-refresh', textColor: 'warning-text', tagType: 'warning', progColor: '#e6a23c', unit: '次/s', isRatio: false, emptyText: '调度平稳' },
|
|
|
+ { category: '计算资源', key: 'zombieRisk', title: '僵尸进程', subTitle: 'Z States', icon: 'el-icon-stopwatch', textColor: 'warning-text', tagType: 'warning', progColor: '#e6a23c', unit: '个', isRatio: false, emptyText: '无僵尸进程' },
|
|
|
+
|
|
|
+ /* 内存指标 */
|
|
|
+ { category: '内存指标', key: 'memRisk', title: '内存水位线', subTitle: '> 85% Usage', icon: 'el-icon-box', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '%', isRatio: true, emptyText: '水位正常' },
|
|
|
+ { category: '内存指标', key: 'memPsiRisk', title: '内存紧缩压力', subTitle: 'PSI Stall %', icon: 'el-icon-timer', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '%', isRatio: true, emptyText: '无访问延迟' },
|
|
|
+ { category: '内存指标', key: 'oomEvents', title: 'OOM Kill 事件', subTitle: 'Kernel OOM', icon: 'el-icon-warning', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '次', isRatio: false, emptyText: '无内存强杀' },
|
|
|
+ { category: '内存指标', key: 'pidLimitRisk', title: 'PID 进程数限制', subTitle: 'Forks / Max', icon: 'el-icon-connection', textColor: 'primary-text', tagType: 'primary', progColor: '#409eff', unit: '%', isRatio: true, emptyText: '池空间充足' },
|
|
|
+
|
|
|
+ /* 存储文件 */
|
|
|
+ { category: '存储文件', key: 'diskUsageRisk', title: '根分区空间', subTitle: '( / ) Usage', icon: 'el-icon-pie-chart', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '%', isRatio: true, emptyText: '空间充沛' },
|
|
|
+ { category: '存储文件', key: 'diskPredictRisk', title: '磁盘存满预测', subTitle: 'Predict 24H', icon: 'el-icon-magic-stick', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '', isRatio: false, emptyText: '无写满风险' },
|
|
|
+ { category: '存储文件', key: 'inodeRisk', title: 'inode 使用率', subTitle: 'Index Nodes', icon: 'el-icon-files', textColor: 'warning-text', tagType: 'warning', progColor: '#e6a23c', unit: '%', isRatio: true, emptyText: '索引节点充足' },
|
|
|
+ { category: '存储文件', key: 'diskIo', title: '磁盘 IO 响应', subTitle: 'IO Wait', icon: 'el-icon-receiving', textColor: 'warning-text', tagType: 'warning', progColor: '#e6a23c', unit: 'ms', isRatio: false, emptyText: '响应极快' },
|
|
|
+ { category: '存储文件', key: 'fdRisk', title: '文件句柄 (FD)', subTitle: 'FD Ratio', icon: 'el-icon-link', textColor: 'primary-text', tagType: 'primary', progColor: '#409eff', unit: '%', isRatio: true, emptyText: '占用正常' },
|
|
|
+ { category: '存储文件', key: 'readOnlyFsRisk', title: '文件系统只读', subTitle: 'RO Status', icon: 'el-icon-lock', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '', isRatio: false, emptyText: '挂载正常' },
|
|
|
+
|
|
|
+ /* 网络协议 */
|
|
|
+ { category: '网络协议', key: 'conntrackRisk', title: '连接跟踪表', subTitle: 'Conntrack', icon: 'el-icon-attract', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '%', isRatio: true, emptyText: '状态健康' },
|
|
|
+ { category: '网络协议', key: 'netEstMax', title: 'TCP EST 数量', subTitle: 'Established', icon: 'el-icon-warning-outline', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '次', isRatio: false, emptyText: '未超限' },
|
|
|
+ { category: '网络协议', key: 'netTwMax', title: 'TCP TIME_WAIT', subTitle: 'Wait Status', icon: 'el-icon-warning-outline', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '次', isRatio: false, emptyText: '未超限' },
|
|
|
+ { category: '网络协议', key: 'netBandwidthRisk', title: '网卡带宽水位', subTitle: 'Traffic Ratio', icon: 'el-icon-sort', textColor: 'warning-text', tagType: 'warning', progColor: '#e6a23c', unit: '%', isRatio: true, emptyText: '流量平稳' },
|
|
|
+ { category: '网络协议', key: 'tcpRetransRisk', title: 'TCP 重传率', subTitle: 'Retrans Ratio', icon: 'el-icon-refresh-left', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '%', isRatio: true, emptyText: '传输稳定' },
|
|
|
+ { category: '网络协议', key: 'netOverflows', title: 'TCP 队列溢出', subTitle: 'Listen Overflow', icon: 'el-icon-warning-outline', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '次', isRatio: false, emptyText: '无溢出' },
|
|
|
+ { category: '网络协议', key: 'netDrops', title: 'TCP 数据包丢弃', subTitle: 'Packet Drops', icon: 'el-icon-circle-close', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '次', isRatio: false, emptyText: '无丢弃' },
|
|
|
+
|
|
|
+ /* 基础环境 */
|
|
|
+ { category: '基础环境', key: 'clockSkewRisk', title: '时钟偏移 (NTP)', subTitle: 'Clock Offset', icon: 'el-icon-alarm-clock', textColor: 'warning-text', tagType: 'warning', progColor: '#e6a23c', unit: 's', isRatio: false, emptyText: '同步精确' }
|
|
|
]
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
- hasAnyIssue() {
|
|
|
- return this.cardConfigs.some(c => this.reportData[c.key]?.length > 0)
|
|
|
+ totalIssuesCount() {
|
|
|
+ return this.cardConfigs.filter(c => this.getData(c.key).length > 0).length
|
|
|
},
|
|
|
+ hasAnyIssue() { return this.totalIssuesCount > 0 },
|
|
|
overallStatus() { return this.hasAnyIssue ? 'warning' : 'success' },
|
|
|
overallDescription() {
|
|
|
- return this.hasAnyIssue ? '系统检测到部分节点异常,请点击卡片问号查看排查建议。' : '所有基础设施运行参数健康。'
|
|
|
+ return this.hasAnyIssue ? `当前环境存在异常项,请优先处理风险卡片。` : '核心基础设施所有参数均在安全范围内。'
|
|
|
}
|
|
|
},
|
|
|
created() { this.loadReport() },
|
|
|
methods: {
|
|
|
+ getCardsByCategory(cat) {
|
|
|
+ return this.cardConfigs.filter(config => config.category === cat)
|
|
|
+ },
|
|
|
+ getCategoryIssueCount(cat) {
|
|
|
+ return this.getCardsByCategory(cat).filter(config => this.getData(config.key).length > 0).length
|
|
|
+ },
|
|
|
getData(key) { return this.reportData[key] || [] },
|
|
|
getShadowClass(config) {
|
|
|
- const hasData = this.getData(config.key).length > 0
|
|
|
- if (!hasData) return ''
|
|
|
- return config.tagType + '-shadow'
|
|
|
+ return this.getData(config.key).length > 0 ? config.tagType + '-shadow' : ''
|
|
|
},
|
|
|
async loadReport() {
|
|
|
this.loading = true
|
|
|
@@ -189,225 +228,210 @@ export default {
|
|
|
showAdvice(config) {
|
|
|
this.currentAdvice = {
|
|
|
title: config.title,
|
|
|
- text: this.reportData.advices[config.key] || '暂无详细排查指引,请联系运维团队。'
|
|
|
+ text: this.reportData.advices[config.key] || '暂无详细指引。'
|
|
|
}
|
|
|
this.dialogVisible = true
|
|
|
},
|
|
|
- formatAdvice(text) {
|
|
|
- // 简单的换行转义,如果后端返回的是带换行的文本
|
|
|
- return text.replace(/\n/g, '<br>')
|
|
|
- }
|
|
|
+ formatAdvice(text) { return text ? text.replace(/\n/g, '<br>') : '' }
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-/* 保持原样式并添加以下内容 */
|
|
|
+/* 1. 基础容器与间距控制 */
|
|
|
+.monitor-report-container {
|
|
|
+ padding: 32px;
|
|
|
+ background-color: #f8fafc;
|
|
|
+ min-height: 100vh;
|
|
|
+}
|
|
|
+
|
|
|
+/* 分类组间距 */
|
|
|
+.category-group {
|
|
|
+ margin-bottom: 40px;
|
|
|
+ background: rgba(255, 255, 255, 0.4);
|
|
|
+ padding: 20px;
|
|
|
+ border-radius: 16px;
|
|
|
+}
|
|
|
|
|
|
-.header-ops {
|
|
|
+/* 分类头部标题 */
|
|
|
+.category-header {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
+ margin-bottom: 24px;
|
|
|
+ padding: 0 4px;
|
|
|
}
|
|
|
|
|
|
-.advice-btn {
|
|
|
- font-size: 18px;
|
|
|
- color: #94a3b8;
|
|
|
- margin-right: 8px;
|
|
|
- padding: 0;
|
|
|
- transition: color 0.2s;
|
|
|
+.category-title-box {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ background: #1e293b;
|
|
|
+ padding: 8px 16px;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
|
}
|
|
|
|
|
|
-.advice-btn:hover {
|
|
|
- color: #409eff;
|
|
|
+.category-name {
|
|
|
+ color: #f8fafc;
|
|
|
+ font-weight: 700;
|
|
|
+ font-size: 15px;
|
|
|
+ letter-spacing: 0.5px;
|
|
|
}
|
|
|
|
|
|
-/* 弹窗样式美化 */
|
|
|
-.advice-dialog >>> .el-dialog__header {
|
|
|
- border-bottom: 1px solid #f1f5f9;
|
|
|
- padding: 20px;
|
|
|
+.category-line {
|
|
|
+ flex: 1;
|
|
|
+ height: 2px;
|
|
|
+ background: linear-gradient(90deg, #cbd5e1 0%, transparent 100%);
|
|
|
+ margin-left: 16px;
|
|
|
}
|
|
|
|
|
|
-.advice-dialog >>> .el-dialog__title {
|
|
|
- font-size: 16px;
|
|
|
- font-weight: bold;
|
|
|
- color: #1e293b;
|
|
|
+/* 卡片容器间距:确保上下卡片不粘连 */
|
|
|
+.card-col {
|
|
|
+ margin-bottom: 24px;
|
|
|
}
|
|
|
|
|
|
-.advice-content {
|
|
|
- padding: 10px 5px;
|
|
|
- line-height: 1.6;
|
|
|
- color: #475569;
|
|
|
- font-size: 14px;
|
|
|
+/* 2. 卡片内部样式优化 */
|
|
|
+.nice-card {
|
|
|
+ height: 300px;
|
|
|
+ border-radius: 12px;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
}
|
|
|
|
|
|
-.markdown-body {
|
|
|
- background: #f8fafc;
|
|
|
- padding: 15px;
|
|
|
- border-radius: 8px;
|
|
|
- border-left: 4px solid #409eff;
|
|
|
+.nice-card:hover {
|
|
|
+ transform: translateY(-4px);
|
|
|
}
|
|
|
|
|
|
-/* 之前已有的样式保持不变 */
|
|
|
-.monitor-report-container { padding: 24px; background-color: #f6f8fb; min-height: 100vh; }
|
|
|
-.force-row { display: flex !important; flex-wrap: wrap !important; }
|
|
|
-.nice-card { height: 320px; border-radius: 12px; display: flex; flex-direction: column; margin-bottom: 20px; overflow: hidden; }
|
|
|
-.nice-card >>> .el-card__header { padding: 15px 20px; background-color: #fafbfd; border-bottom: 1px solid #f1f5f9; flex-shrink: 0; }
|
|
|
-.nice-card >>> .el-card__body { padding: 0; flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
|
|
-.card-header { display: flex; justify-content: space-between; align-items: center; }
|
|
|
-.title-wrapper { display: flex; align-items: center; flex: 1; }
|
|
|
-.status-tag { border-radius: 10px; padding: 0 8px; font-weight: bold; }
|
|
|
-.icon-accent { font-size: 18px; margin-right: 10px; }
|
|
|
-.title-text { font-size: 14px; font-weight: 600; color: #334155; }
|
|
|
-.title-text small { font-weight: normal; font-size: 11px; color: #94a3b8; display: block; }
|
|
|
-.card-body { padding: 15px 20px; flex: 1; overflow-y: auto; }
|
|
|
-.progress-item { margin-bottom: 18px; }
|
|
|
-.progress-item .info { display: flex; justify-content: space-between; margin-bottom: 6px; font-size: 12px; }
|
|
|
-.status-placeholder { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #cbd5e1; }
|
|
|
-.warning-shadow { border-top: 3px solid #e6a23c; }
|
|
|
-.danger-shadow { border-top: 3px solid #f56c6c; }
|
|
|
-.primary-shadow { border-top: 3px solid #409eff; }
|
|
|
-.warning-text { color: #e6a23c; }
|
|
|
-.danger-text { color: #f56c6c; }
|
|
|
-.primary-text { color: #409eff; }
|
|
|
-.text-ellipsis { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 180px; }
|
|
|
-.scroll-style::-webkit-scrollbar { width: 4px; }
|
|
|
-.scroll-style::-webkit-scrollbar-thumb { background: #e2e8f0; border-radius: 10px; }
|
|
|
+.nice-card >>> .el-card__header {
|
|
|
+ padding: 16px 20px;
|
|
|
+ background-color: #ffffff;
|
|
|
+ border-bottom: 1px solid #f1f5f9;
|
|
|
+}
|
|
|
|
|
|
-.status-banner {
|
|
|
+.card-header {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
- padding: 24px 32px;
|
|
|
- background: #ffffff;
|
|
|
- border-radius: 16px;
|
|
|
- margin-bottom: 24px;
|
|
|
- position: relative;
|
|
|
- overflow: hidden;
|
|
|
- box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05);
|
|
|
- border: 1px solid rgba(255, 255, 255, 0.8);
|
|
|
-}
|
|
|
-
|
|
|
-/* 状态颜色背景微调 */
|
|
|
-.banner-success {
|
|
|
- background: linear-gradient(135deg, #f0fdf4 0%, #ffffff 50%);
|
|
|
- border-left: 6px solid #22c55e;
|
|
|
-}
|
|
|
-.banner-warning {
|
|
|
- background: linear-gradient(135deg, #fffbeb 0%, #ffffff 50%);
|
|
|
- border-left: 6px solid #f59e0b;
|
|
|
}
|
|
|
|
|
|
-/* 左侧状态图标与动画 */
|
|
|
-.banner-left {
|
|
|
+.title-wrapper {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- gap: 20px;
|
|
|
+ gap: 12px;
|
|
|
}
|
|
|
|
|
|
-.status-visual {
|
|
|
- position: relative;
|
|
|
- width: 48px;
|
|
|
- height: 48px;
|
|
|
+.text-group {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
}
|
|
|
|
|
|
-.status-icon-wrapper {
|
|
|
- width: 48px;
|
|
|
- height: 48px;
|
|
|
- border-radius: 14px;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- font-size: 24px;
|
|
|
- z-index: 2;
|
|
|
- position: relative;
|
|
|
+.main-title {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #334155;
|
|
|
}
|
|
|
|
|
|
-.banner-success .status-icon-wrapper { background: #dcfce7; color: #16a34a; }
|
|
|
-.banner-warning .status-icon-wrapper { background: #fef3c7; color: #d97706; }
|
|
|
+.sub-title {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #94a3b8;
|
|
|
+ margin-top: 2px;
|
|
|
+}
|
|
|
|
|
|
-.pulse-ring {
|
|
|
- position: absolute;
|
|
|
- top: 0; left: 0; width: 100%; height: 100%;
|
|
|
- border-radius: 14px;
|
|
|
- animation: pulse 2s infinite;
|
|
|
+.card-body {
|
|
|
+ padding: 16px 20px;
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
}
|
|
|
-.banner-success .pulse-ring { background: rgba(34, 197, 94, 0.2); }
|
|
|
-.banner-warning .pulse-ring { background: rgba(245, 158, 11, 0.2); }
|
|
|
|
|
|
-@keyframes pulse {
|
|
|
- 0% { transform: scale(1); opacity: 1; }
|
|
|
- 100% { transform: scale(1.5); opacity: 0; }
|
|
|
+/* 指标条目 */
|
|
|
+.metric-entry {
|
|
|
+ margin-bottom: 16px;
|
|
|
}
|
|
|
|
|
|
-/* 内容文本区 */
|
|
|
-.banner-title {
|
|
|
- font-size: 20px;
|
|
|
- font-weight: 800;
|
|
|
- color: #1e293b;
|
|
|
+.entry-info {
|
|
|
display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 12px;
|
|
|
- margin-bottom: 4px;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 6px;
|
|
|
+ font-size: 12px;
|
|
|
}
|
|
|
|
|
|
-.banner-desc {
|
|
|
- font-size: 14px;
|
|
|
+.entry-name {
|
|
|
color: #64748b;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ max-width: 160px;
|
|
|
}
|
|
|
|
|
|
-.update-time {
|
|
|
- font-size: 12px;
|
|
|
- color: #94a3b8;
|
|
|
+.entry-val {
|
|
|
+ font-weight: 700;
|
|
|
}
|
|
|
+.entry-val small { font-weight: 400; font-size: 10px; margin-left: 2px; }
|
|
|
+
|
|
|
+/* 3. 顶部 Banner 样式 (沿用并微调) */
|
|
|
+.status-banner {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 24px 32px;
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 16px;
|
|
|
+ margin-bottom: 32px;
|
|
|
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.05);
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.banner-success { border-left: 6px solid #22c55e; }
|
|
|
+.banner-warning { border-left: 6px solid #f59e0b; }
|
|
|
|
|
|
-/* 中间指标区 */
|
|
|
.banner-stats {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- background: rgba(248, 250, 252, 0.8);
|
|
|
+ background: #f1f5f9;
|
|
|
padding: 12px 24px;
|
|
|
border-radius: 12px;
|
|
|
gap: 20px;
|
|
|
}
|
|
|
|
|
|
-.stat-item {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- align-items: center;
|
|
|
-}
|
|
|
+.stat-item { text-align: center; }
|
|
|
+.stat-label { font-size: 11px; color: #94a3b8; text-transform: uppercase; }
|
|
|
+.stat-value { font-size: 18px; font-weight: 800; color: #1e293b; }
|
|
|
+.stat-divider { width: 1px; height: 24px; background: #cbd5e1; }
|
|
|
|
|
|
-.stat-label {
|
|
|
- font-size: 11px;
|
|
|
- text-transform: uppercase;
|
|
|
- letter-spacing: 0.5px;
|
|
|
- color: #94a3b8;
|
|
|
- margin-bottom: 2px;
|
|
|
+/* 颜色状态与阴影 */
|
|
|
+.warning-shadow {
|
|
|
+ border-top: 4px solid #e6a23c !important;
|
|
|
+ box-shadow: 0 8px 20px -6px rgba(230, 162, 60, 0.2) !important;
|
|
|
}
|
|
|
-
|
|
|
-.stat-value {
|
|
|
- font-size: 18px;
|
|
|
- font-weight: 700;
|
|
|
- color: #334155;
|
|
|
+.danger-shadow {
|
|
|
+ border-top: 4px solid #f56c6c !important;
|
|
|
+ box-shadow: 0 8px 20px -6px rgba(245, 108, 108, 0.2) !important;
|
|
|
}
|
|
|
-.stat-value small { font-size: 12px; font-weight: normal; }
|
|
|
-.risk-text { color: #ef4444; }
|
|
|
-
|
|
|
-.stat-divider {
|
|
|
- width: 1px;
|
|
|
- height: 24px;
|
|
|
- background: #e2e8f0;
|
|
|
+.primary-shadow {
|
|
|
+ border-top: 4px solid #409eff !important;
|
|
|
+ box-shadow: 0 8px 20px -6px rgba(64, 158, 255, 0.2) !important;
|
|
|
}
|
|
|
|
|
|
-/* 按钮发光效果 */
|
|
|
-.refresh-glow {
|
|
|
- box-shadow: 0 4px 14px 0 rgba(64, 158, 255, 0.3);
|
|
|
- transition: all 0.3s ease;
|
|
|
+.warning-text { color: #d97706; }
|
|
|
+.danger-text { color: #dc2626; }
|
|
|
+.primary-text { color: #2563eb; }
|
|
|
+
|
|
|
+/* 其他辅助类 */
|
|
|
+.empty-state {
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: #cbd5e1;
|
|
|
}
|
|
|
+.empty-state i { font-size: 28px; margin-bottom: 8px; }
|
|
|
+
|
|
|
+.scroll-style::-webkit-scrollbar { width: 4px; }
|
|
|
+.scroll-style::-webkit-scrollbar-thumb { background: #e2e8f0; border-radius: 10px; }
|
|
|
+
|
|
|
.refresh-glow:hover {
|
|
|
- transform: translateY(-1px);
|
|
|
- box-shadow: 0 6px 20px rgba(64, 158, 255, 0.5);
|
|
|
+ box-shadow: 0 0 15px rgba(64, 158, 255, 0.4);
|
|
|
}
|
|
|
</style>
|