feat(admin): 添加系统健康检测功能
新增管理员健康检测面板,可检测以下配置项: - 安全配置:JWT密钥、CORS、HTTPS、管理员账号、登录防爆破 - 服务状态:SMTP邮件、数据库连接、存储目录 - 运行配置:反向代理支持、Node环境 修改文件: - backend/auth.js: 新增 isJwtSecretSecure() 函数 - backend/server.js: 新增 /api/admin/health-check API - frontend/app.js: 新增健康检测数据和方法 - frontend/app.html: 新增健康检测UI界面 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -132,9 +132,15 @@ function adminMiddleware(req, res, next) {
|
|||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查JWT密钥是否安全
|
||||||
|
function isJwtSecretSecure() {
|
||||||
|
return !DEFAULT_SECRETS.includes(JWT_SECRET) && JWT_SECRET.length >= 32;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
JWT_SECRET,
|
JWT_SECRET,
|
||||||
generateToken,
|
generateToken,
|
||||||
authMiddleware,
|
authMiddleware,
|
||||||
adminMiddleware
|
adminMiddleware,
|
||||||
|
isJwtSecretSecure
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const execAsync = util.promisify(exec);
|
|||||||
const execFileAsync = util.promisify(execFile);
|
const execFileAsync = util.promisify(execFile);
|
||||||
|
|
||||||
const { db, UserDB, ShareDB, SettingsDB, VerificationDB, PasswordResetTokenDB } = require('./database');
|
const { db, UserDB, ShareDB, SettingsDB, VerificationDB, PasswordResetTokenDB } = require('./database');
|
||||||
const { generateToken, authMiddleware, adminMiddleware } = require('./auth');
|
const { generateToken, authMiddleware, adminMiddleware, isJwtSecretSecure } = require('./auth');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 40001;
|
const PORT = process.env.PORT || 40001;
|
||||||
@@ -3382,6 +3382,184 @@ app.post('/api/admin/settings/test-smtp', authMiddleware, adminMiddleware, async
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 系统健康检测API
|
||||||
|
app.get('/api/admin/health-check', authMiddleware, adminMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const checks = [];
|
||||||
|
let overallStatus = 'healthy'; // healthy, warning, critical
|
||||||
|
|
||||||
|
// 1. JWT密钥安全检查
|
||||||
|
const jwtSecure = isJwtSecretSecure();
|
||||||
|
checks.push({
|
||||||
|
name: 'JWT密钥',
|
||||||
|
category: 'security',
|
||||||
|
status: jwtSecure ? 'pass' : 'fail',
|
||||||
|
message: jwtSecure ? 'JWT密钥已正确配置(随机生成)' : 'JWT密钥使用默认值或长度不足,存在安全风险!',
|
||||||
|
suggestion: jwtSecure ? null : '请在.env中设置随机生成的JWT_SECRET,至少32字符'
|
||||||
|
});
|
||||||
|
if (!jwtSecure) overallStatus = 'critical';
|
||||||
|
|
||||||
|
// 2. CORS配置检查
|
||||||
|
const allowedOrigins = process.env.ALLOWED_ORIGINS;
|
||||||
|
const corsConfigured = allowedOrigins && allowedOrigins.trim().length > 0;
|
||||||
|
checks.push({
|
||||||
|
name: 'CORS跨域配置',
|
||||||
|
category: 'security',
|
||||||
|
status: corsConfigured ? 'pass' : 'warning',
|
||||||
|
message: corsConfigured
|
||||||
|
? `已配置允许的域名: ${allowedOrigins}`
|
||||||
|
: 'CORS未配置,允许所有来源(仅适合开发环境)',
|
||||||
|
suggestion: corsConfigured ? null : '生产环境建议配置ALLOWED_ORIGINS环境变量'
|
||||||
|
});
|
||||||
|
if (!corsConfigured && overallStatus === 'healthy') overallStatus = 'warning';
|
||||||
|
|
||||||
|
// 3. HTTPS/Cookie安全配置
|
||||||
|
const enforceHttps = process.env.ENFORCE_HTTPS === 'true';
|
||||||
|
const cookieSecure = process.env.COOKIE_SECURE === 'true';
|
||||||
|
const httpsConfigured = enforceHttps && cookieSecure;
|
||||||
|
checks.push({
|
||||||
|
name: 'HTTPS安全配置',
|
||||||
|
category: 'security',
|
||||||
|
status: httpsConfigured ? 'pass' : 'warning',
|
||||||
|
message: httpsConfigured
|
||||||
|
? 'HTTPS强制开启,Cookie安全标志已设置'
|
||||||
|
: `ENFORCE_HTTPS=${enforceHttps}, COOKIE_SECURE=${cookieSecure}`,
|
||||||
|
suggestion: httpsConfigured ? null : '生产环境建议开启ENFORCE_HTTPS和COOKIE_SECURE'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. 管理员密码强度检查(检查是否为默认值)
|
||||||
|
const adminUsername = process.env.ADMIN_USERNAME;
|
||||||
|
const adminConfigured = adminUsername && adminUsername !== 'admin';
|
||||||
|
checks.push({
|
||||||
|
name: '管理员账号配置',
|
||||||
|
category: 'security',
|
||||||
|
status: adminConfigured ? 'pass' : 'warning',
|
||||||
|
message: adminConfigured
|
||||||
|
? '管理员用户名已自定义'
|
||||||
|
: '管理员使用默认用户名"admin"',
|
||||||
|
suggestion: adminConfigured ? null : '建议使用自定义管理员用户名'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. SMTP邮件配置检查
|
||||||
|
const smtpHost = SettingsDB.get('smtp_host') || process.env.SMTP_HOST;
|
||||||
|
const smtpUser = SettingsDB.get('smtp_user') || process.env.SMTP_USER;
|
||||||
|
const smtpPassword = SettingsDB.get('smtp_password') || process.env.SMTP_PASSWORD;
|
||||||
|
const smtpConfigured = smtpHost && smtpUser && smtpPassword;
|
||||||
|
checks.push({
|
||||||
|
name: 'SMTP邮件服务',
|
||||||
|
category: 'service',
|
||||||
|
status: smtpConfigured ? 'pass' : 'warning',
|
||||||
|
message: smtpConfigured
|
||||||
|
? `已配置: ${smtpHost}`
|
||||||
|
: '未配置SMTP,邮箱验证和密码重置功能不可用',
|
||||||
|
suggestion: smtpConfigured ? null : '配置SMTP以启用邮箱验证功能'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. 数据库连接检查
|
||||||
|
let dbStatus = 'pass';
|
||||||
|
let dbMessage = '数据库连接正常';
|
||||||
|
try {
|
||||||
|
const testQuery = db.prepare('SELECT 1').get();
|
||||||
|
if (!testQuery) throw new Error('查询返回空');
|
||||||
|
} catch (dbError) {
|
||||||
|
dbStatus = 'fail';
|
||||||
|
dbMessage = '数据库连接异常: ' + dbError.message;
|
||||||
|
overallStatus = 'critical';
|
||||||
|
}
|
||||||
|
checks.push({
|
||||||
|
name: '数据库连接',
|
||||||
|
category: 'service',
|
||||||
|
status: dbStatus,
|
||||||
|
message: dbMessage,
|
||||||
|
suggestion: dbStatus === 'fail' ? '检查数据库文件权限和路径配置' : null
|
||||||
|
});
|
||||||
|
|
||||||
|
// 7. 存储目录检查
|
||||||
|
const storageRoot = process.env.STORAGE_ROOT || path.join(__dirname, 'storage');
|
||||||
|
let storageStatus = 'pass';
|
||||||
|
let storageMessage = `存储目录正常: ${storageRoot}`;
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(storageRoot)) {
|
||||||
|
fs.mkdirSync(storageRoot, { recursive: true });
|
||||||
|
storageMessage = `存储目录已创建: ${storageRoot}`;
|
||||||
|
}
|
||||||
|
// 检查写入权限
|
||||||
|
const testFile = path.join(storageRoot, '.health-check-test');
|
||||||
|
fs.writeFileSync(testFile, 'test');
|
||||||
|
fs.unlinkSync(testFile);
|
||||||
|
} catch (storageError) {
|
||||||
|
storageStatus = 'fail';
|
||||||
|
storageMessage = '存储目录不可写: ' + storageError.message;
|
||||||
|
overallStatus = 'critical';
|
||||||
|
}
|
||||||
|
checks.push({
|
||||||
|
name: '存储目录',
|
||||||
|
category: 'service',
|
||||||
|
status: storageStatus,
|
||||||
|
message: storageMessage,
|
||||||
|
suggestion: storageStatus === 'fail' ? '检查存储目录权限,确保Node进程有写入权限' : null
|
||||||
|
});
|
||||||
|
|
||||||
|
// 8. 限流器状态
|
||||||
|
const rateLimiterActive = typeof loginLimiter !== 'undefined' && loginLimiter !== null;
|
||||||
|
checks.push({
|
||||||
|
name: '登录防爆破',
|
||||||
|
category: 'security',
|
||||||
|
status: rateLimiterActive ? 'pass' : 'warning',
|
||||||
|
message: rateLimiterActive
|
||||||
|
? '限流器已启用(5次/15分钟,封锁30分钟)'
|
||||||
|
: '限流器未正常初始化',
|
||||||
|
suggestion: null
|
||||||
|
});
|
||||||
|
|
||||||
|
// 9. 信任代理配置(反向代理环境)
|
||||||
|
const trustProxy = app.get('trust proxy');
|
||||||
|
checks.push({
|
||||||
|
name: '反向代理支持',
|
||||||
|
category: 'config',
|
||||||
|
status: trustProxy ? 'pass' : 'info',
|
||||||
|
message: trustProxy
|
||||||
|
? '已启用trust proxy,支持X-Forwarded-For'
|
||||||
|
: '未启用trust proxy,直接暴露时无影响',
|
||||||
|
suggestion: null
|
||||||
|
});
|
||||||
|
|
||||||
|
// 10. Node环境
|
||||||
|
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||||
|
checks.push({
|
||||||
|
name: '运行环境',
|
||||||
|
category: 'config',
|
||||||
|
status: nodeEnv === 'production' ? 'pass' : 'info',
|
||||||
|
message: `当前环境: ${nodeEnv}`,
|
||||||
|
suggestion: nodeEnv !== 'production' ? '生产部署建议设置NODE_ENV=production' : null
|
||||||
|
});
|
||||||
|
|
||||||
|
// 统计
|
||||||
|
const summary = {
|
||||||
|
total: checks.length,
|
||||||
|
pass: checks.filter(c => c.status === 'pass').length,
|
||||||
|
warning: checks.filter(c => c.status === 'warning').length,
|
||||||
|
fail: checks.filter(c => c.status === 'fail').length,
|
||||||
|
info: checks.filter(c => c.status === 'info').length
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
overallStatus,
|
||||||
|
summary,
|
||||||
|
checks,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
version: process.env.npm_package_version || '1.1.0'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('健康检测失败:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '健康检测失败: ' + error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 获取服务器存储统计信息
|
// 获取服务器存储统计信息
|
||||||
app.get('/api/admin/storage-stats', authMiddleware, adminMiddleware, async (req, res) => {
|
app.get('/api/admin/storage-stats', authMiddleware, adminMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -654,6 +654,12 @@
|
|||||||
background: rgba(102, 126, 234, 0.05);
|
background: rgba(102, 126, 234, 0.05);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 健康检测状态颜色 */
|
||||||
|
.text-green-600 { color: #16a34a; }
|
||||||
|
.text-yellow-600 { color: #ca8a04; }
|
||||||
|
.text-red-600 { color: #dc2626; }
|
||||||
|
.text-blue-600 { color: #2563eb; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -1747,6 +1753,127 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 健康检测 -->
|
||||||
|
<div class="card" style="margin-bottom: 30px;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||||
|
<h3 style="margin: 0;">
|
||||||
|
<i class="fas fa-heartbeat"></i> 系统健康检测
|
||||||
|
</h3>
|
||||||
|
<button class="btn btn-primary" @click="loadHealthCheck" :disabled="healthCheck.loading">
|
||||||
|
<i class="fas" :class="healthCheck.loading ? 'fa-spinner fa-spin' : 'fa-sync'"></i>
|
||||||
|
{{ healthCheck.loading ? '检测中...' : '刷新检测' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 整体状态 -->
|
||||||
|
<div v-if="healthCheck.overallStatus" style="margin-bottom: 20px;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 15px; flex-wrap: wrap;">
|
||||||
|
<div style="font-size: 18px; font-weight: bold;" :class="getOverallStatusColor(healthCheck.overallStatus)">
|
||||||
|
<i class="fas" :class="{
|
||||||
|
'fa-check-circle': healthCheck.overallStatus === 'healthy',
|
||||||
|
'fa-exclamation-triangle': healthCheck.overallStatus === 'warning',
|
||||||
|
'fa-times-circle': healthCheck.overallStatus === 'critical'
|
||||||
|
}"></i>
|
||||||
|
{{ getOverallStatusText(healthCheck.overallStatus) }}
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 12px; font-size: 13px;">
|
||||||
|
<span style="color: #28a745;"><i class="fas fa-check"></i> 通过: {{ healthCheck.summary.pass }}</span>
|
||||||
|
<span style="color: #ffc107;"><i class="fas fa-exclamation"></i> 警告: {{ healthCheck.summary.warning }}</span>
|
||||||
|
<span style="color: #dc3545;"><i class="fas fa-times"></i> 失败: {{ healthCheck.summary.fail }}</span>
|
||||||
|
<span style="color: #17a2b8;"><i class="fas fa-info"></i> 信息: {{ healthCheck.summary.info }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="healthCheck.lastCheck" style="font-size: 12px; color: #888; margin-top: 8px;">
|
||||||
|
上次检测: {{ new Date(healthCheck.lastCheck).toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 检测项列表 -->
|
||||||
|
<div v-if="healthCheck.checks.length > 0">
|
||||||
|
<!-- 按分类分组 -->
|
||||||
|
<div style="margin-bottom: 20px;">
|
||||||
|
<h4 style="margin-bottom: 12px; color: #333;"><i class="fas fa-shield-alt"></i> 安全配置</h4>
|
||||||
|
<div style="display: grid; gap: 10px;">
|
||||||
|
<div v-for="check in healthCheck.checks.filter(c => c.category === 'security')" :key="check.name"
|
||||||
|
style="display: flex; align-items: flex-start; gap: 12px; padding: 12px; background: #f8f9fa; border-radius: 8px;">
|
||||||
|
<span style="font-size: 18px; width: 24px; text-align: center;"
|
||||||
|
:class="{
|
||||||
|
'text-green-600': check.status === 'pass',
|
||||||
|
'text-yellow-600': check.status === 'warning',
|
||||||
|
'text-red-600': check.status === 'fail',
|
||||||
|
'text-blue-600': check.status === 'info'
|
||||||
|
}">
|
||||||
|
{{ getHealthStatusIcon(check.status) }}
|
||||||
|
</span>
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<div style="font-weight: 500; margin-bottom: 4px;">{{ check.name }}</div>
|
||||||
|
<div style="font-size: 13px; color: #666;">{{ check.message }}</div>
|
||||||
|
<div v-if="check.suggestion" style="font-size: 12px; color: #e67e22; margin-top: 4px;">
|
||||||
|
<i class="fas fa-lightbulb"></i> {{ check.suggestion }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 20px;">
|
||||||
|
<h4 style="margin-bottom: 12px; color: #333;"><i class="fas fa-server"></i> 服务状态</h4>
|
||||||
|
<div style="display: grid; gap: 10px;">
|
||||||
|
<div v-for="check in healthCheck.checks.filter(c => c.category === 'service')" :key="check.name"
|
||||||
|
style="display: flex; align-items: flex-start; gap: 12px; padding: 12px; background: #f8f9fa; border-radius: 8px;">
|
||||||
|
<span style="font-size: 18px; width: 24px; text-align: center;"
|
||||||
|
:class="{
|
||||||
|
'text-green-600': check.status === 'pass',
|
||||||
|
'text-yellow-600': check.status === 'warning',
|
||||||
|
'text-red-600': check.status === 'fail',
|
||||||
|
'text-blue-600': check.status === 'info'
|
||||||
|
}">
|
||||||
|
{{ getHealthStatusIcon(check.status) }}
|
||||||
|
</span>
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<div style="font-weight: 500; margin-bottom: 4px;">{{ check.name }}</div>
|
||||||
|
<div style="font-size: 13px; color: #666;">{{ check.message }}</div>
|
||||||
|
<div v-if="check.suggestion" style="font-size: 12px; color: #e67e22; margin-top: 4px;">
|
||||||
|
<i class="fas fa-lightbulb"></i> {{ check.suggestion }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 style="margin-bottom: 12px; color: #333;"><i class="fas fa-cog"></i> 运行配置</h4>
|
||||||
|
<div style="display: grid; gap: 10px;">
|
||||||
|
<div v-for="check in healthCheck.checks.filter(c => c.category === 'config')" :key="check.name"
|
||||||
|
style="display: flex; align-items: flex-start; gap: 12px; padding: 12px; background: #f8f9fa; border-radius: 8px;">
|
||||||
|
<span style="font-size: 18px; width: 24px; text-align: center;"
|
||||||
|
:class="{
|
||||||
|
'text-green-600': check.status === 'pass',
|
||||||
|
'text-yellow-600': check.status === 'warning',
|
||||||
|
'text-red-600': check.status === 'fail',
|
||||||
|
'text-blue-600': check.status === 'info'
|
||||||
|
}">
|
||||||
|
{{ getHealthStatusIcon(check.status) }}
|
||||||
|
</span>
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<div style="font-weight: 500; margin-bottom: 4px;">{{ check.name }}</div>
|
||||||
|
<div style="font-size: 13px; color: #666;">{{ check.message }}</div>
|
||||||
|
<div v-if="check.suggestion" style="font-size: 12px; color: #e67e22; margin-top: 4px;">
|
||||||
|
<i class="fas fa-lightbulb"></i> {{ check.suggestion }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 未检测提示 -->
|
||||||
|
<div v-else style="text-align: center; padding: 40px; color: #888;">
|
||||||
|
<i class="fas fa-stethoscope" style="font-size: 48px; margin-bottom: 15px;"></i>
|
||||||
|
<p>点击"刷新检测"按钮开始系统健康检测</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 style="margin-bottom: 20px;">用户管理</h3>
|
<h3 style="margin-bottom: 20px;">用户管理</h3>
|
||||||
<div style="overflow-x: auto;">
|
<div style="overflow-x: auto;">
|
||||||
<table style="width: 100%; border-collapse: collapse; table-layout: fixed; min-width: 900px;">
|
<table style="width: 100%; border-collapse: collapse; table-layout: fixed; min-width: 900px;">
|
||||||
|
|||||||
@@ -148,6 +148,15 @@ createApp({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 健康检测
|
||||||
|
healthCheck: {
|
||||||
|
loading: false,
|
||||||
|
lastCheck: null,
|
||||||
|
overallStatus: null, // healthy, warning, critical
|
||||||
|
summary: { total: 0, pass: 0, warning: 0, fail: 0, info: 0 },
|
||||||
|
checks: []
|
||||||
|
},
|
||||||
|
|
||||||
// Toast通知
|
// Toast通知
|
||||||
toasts: [],
|
toasts: [],
|
||||||
toastIdCounter: 0,
|
toastIdCounter: 0,
|
||||||
@@ -2041,10 +2050,11 @@ handleDragLeave(e) {
|
|||||||
this.loadShares();
|
this.loadShares();
|
||||||
break;
|
break;
|
||||||
case 'admin':
|
case 'admin':
|
||||||
// 切换到管理后台时,重新加载用户列表
|
// 切换到管理后台时,重新加载用户列表和健康检测
|
||||||
if (this.user && this.user.is_admin) {
|
if (this.user && this.user.is_admin) {
|
||||||
this.loadUsers();
|
this.loadUsers();
|
||||||
this.loadServerStorageStats();
|
this.loadServerStorageStats();
|
||||||
|
this.loadHealthCheck();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'settings':
|
case 'settings':
|
||||||
@@ -2260,6 +2270,67 @@ handleDragLeave(e) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ===== 健康检测 =====
|
||||||
|
|
||||||
|
async loadHealthCheck() {
|
||||||
|
this.healthCheck.loading = true;
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${this.apiBase}/api/admin/health-check`, {
|
||||||
|
headers: { Authorization: `Bearer ${this.token}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
this.healthCheck.overallStatus = response.data.overallStatus;
|
||||||
|
this.healthCheck.summary = response.data.summary;
|
||||||
|
this.healthCheck.checks = response.data.checks;
|
||||||
|
this.healthCheck.lastCheck = response.data.timestamp;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('健康检测失败:', error);
|
||||||
|
this.showToast('error', '错误', '健康检测失败');
|
||||||
|
} finally {
|
||||||
|
this.healthCheck.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getHealthStatusColor(status) {
|
||||||
|
const colors = {
|
||||||
|
pass: 'bg-green-100 text-green-800',
|
||||||
|
warning: 'bg-yellow-100 text-yellow-800',
|
||||||
|
fail: 'bg-red-100 text-red-800',
|
||||||
|
info: 'bg-blue-100 text-blue-800'
|
||||||
|
};
|
||||||
|
return colors[status] || 'bg-gray-100 text-gray-800';
|
||||||
|
},
|
||||||
|
|
||||||
|
getHealthStatusIcon(status) {
|
||||||
|
const icons = {
|
||||||
|
pass: '✓',
|
||||||
|
warning: '⚠',
|
||||||
|
fail: '✗',
|
||||||
|
info: 'ℹ'
|
||||||
|
};
|
||||||
|
return icons[status] || '?';
|
||||||
|
},
|
||||||
|
|
||||||
|
getOverallStatusColor(status) {
|
||||||
|
const colors = {
|
||||||
|
healthy: 'text-green-600',
|
||||||
|
warning: 'text-yellow-600',
|
||||||
|
critical: 'text-red-600'
|
||||||
|
};
|
||||||
|
return colors[status] || 'text-gray-600';
|
||||||
|
},
|
||||||
|
|
||||||
|
getOverallStatusText(status) {
|
||||||
|
const texts = {
|
||||||
|
healthy: '系统健康',
|
||||||
|
warning: '存在警告',
|
||||||
|
critical: '存在问题'
|
||||||
|
};
|
||||||
|
return texts[status] || '未知';
|
||||||
|
},
|
||||||
|
|
||||||
// ===== 上传工具管理 =====
|
// ===== 上传工具管理 =====
|
||||||
|
|
||||||
// 检测上传工具是否存在
|
// 检测上传工具是否存在
|
||||||
|
|||||||
Reference in New Issue
Block a user