项目初始化

This commit is contained in:
MasonLiu 2026-05-24 02:27:14 +08:00
commit 0f68dfb319
7 changed files with 994 additions and 0 deletions

41
README.md Normal file
View File

@ -0,0 +1,41 @@
## 核云IDC服务商VPS自动监测重启程序
### API接口信息
- 获取登录Token
`https://www.heyunidc.cn/v1/login_api?account={phone number/email acc}&password={API KEY}`
备注POST方法
- 获取VPS列表
`https://www.heyunidc.cn/v1/hosts?page=1&limit=100`
备注GET方法使用"Authorization: JWT {Token}"进行认证
注意若您的VPS数量超过100需要更改limit数量
- 获取VPS状态
`https://www.heyunidc.cn/v1/hosts/{id}/module/status?type=host`
备注GET方法使用"Authorization: JWT {Token}"进行认证
- 操作VPS
`PUT https://www.heyunidc.cn/v1/hosts/:id/module/hard_reboot`
备注PUT方法使用"Authorization: JWT {Token}"进行认证
操作类型hard_reboot - 硬重启on - 开机off - 关机reboot - 重启
### 使用方法
#### 获取API_KEY
如图所示:
![API_KEY](./static/api.png)
#### Web端
部署到服务器上分配域名或直接IP访问即可
首次使用会进入配置页依次输入API_PASSACCOUNTAPI_KEY其中API_PASS是您自定义的网站访问密码支持大小写字母以及数字
随后访问`http://example.com/?pass=API_PASS`即可
#### 监控端
- 安装:
1. chmod +x ./install.sh
2. ./install.sh
系统将自动注册名为idc-monitor的system服务
- 安装:
1. chmod +x ./uninstall.sh
2. ./uninstall.sh
### 配置信息

494
index.php Normal file
View File

@ -0,0 +1,494 @@
<?php
// ==================== 全局配置区域 ====================
define('CONFIG_FILE', __DIR__ . '/config.php'); // 配置文件路径
define('TOKEN_CACHE_FILE', __DIR__ . '/token_cache.php'); // Token缓存文件路径
define('TOKEN_EXPIRE_TIME', 3600); // Token过期时间默认1小时
// 加载配置文件
if (file_exists(CONFIG_FILE)) {
require_once CONFIG_FILE;
}
// 检查是否需要显示配置页面
$needConfig = !defined('API_PASS') || !defined('ACCOUNT') || !defined('API_KEY') ||
empty(API_PASS) || empty(ACCOUNT) || empty(API_KEY);
// 处理配置提交
if ($needConfig && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['setup_config'])) {
$apiPass = trim($_POST['api_pass']);
$account = trim($_POST['account']);
$apiKey = trim($_POST['api_key']);
if (empty($apiPass) || empty($account) || empty($apiKey)) {
$configError = '所有字段都不能为空!';
} else {
// 生成配置文件内容
$configContent = "<?php\n";
$configContent .= "// 核云IDC VPS管理面板配置文件\n";
$configContent .= "// 生成时间: " . date('Y-m-d H:i:s') . "\n\n";
$configContent .= "define('API_PASS', '" . addslashes($apiPass) . "'); // API访问密码\n";
$configContent .= "define('ACCOUNT', '" . addslashes($account) . "'); // 核云IDC账号手机号或邮箱\n";
$configContent .= "define('API_KEY', '" . addslashes($apiKey) . "'); // 核云IDC API KEY\n";
$configContent .= "define('BASE_URL', 'https://www.heyunidc.cn/v1'); // API基础URL\n";
// 写入配置文件
if (file_put_contents(CONFIG_FILE, $configContent)) {
// 重新加载配置
require_once CONFIG_FILE;
$needConfig = false;
} else {
$configError = '配置文件写入失败,请检查目录权限!';
}
}
}
// 如果仍需配置,显示配置页面
if ($needConfig) {
header('Content-Type: text/html; charset=utf-8');
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="./static/favicon.ico" type="image/x-icon">
<link href="./static/initial.css" rel="stylesheet" type="text/css">
<title>初始配置 - VPS管理面板</title>
</head>
<body>
<div class="config-container">
<div class="config-header">
<h1>🔧 初始配置</h1>
<p>请填写以下信息以开始使用</p>
</div>
<?php if (isset($configError)): ?>
<div class="error-message">
<?php echo htmlspecialchars($configError); ?>
</div>
<?php endif; ?>
<div class="info-box">
💡 提示:这些信息将保存在 <code>config.php</code> 文件中,请妥善保管。<br>
若您需要重置或更改配置,请删除 <code>config.php</code> 文件并重新运行。<br>
配置完成后请以该格式访问服务example.com/?pass=API_PASS
</div>
<form method="POST" onsubmit="return validatePassword()">
<div class="form-group">
<label for="api_pass">访问密码 (API_PASS)</label>
<input type="text" id="api_pass" name="api_pass" required placeholder="设置一个安全的访问密码" pattern="[a-zA-Z0-9]+" title="只允许字母和数字">
<div class="help-text">用于保护管理面板的访问权限<br>可选:字母(a-z,A-Z)和数字(0-9)</div>
</div>
<div class="form-group">
<label for="account">核云IDC账号 (ACCOUNT)</label>
<input type="text" id="account" name="account" required placeholder="手机号或邮箱">
<div class="help-text">您的核云IDC登录账号</div>
</div>
<div class="form-group">
<label for="api_key">API密钥 (API_KEY)</label>
<input type="password" id="api_key" name="api_key" required placeholder="输入API KEY">
<div class="help-text">在核云IDC控制台获取的API密钥</div>
</div>
<button type="submit" name="setup_config" class="btn-submit">
保存配置并开始使用
</button>
</form>
</div>
<script>
function validatePassword() {
var password = document.getElementById('api_pass').value;
var pattern = /^[a-zA-Z0-9]+$/;
if (!pattern.test(password)) {
alert('格式不正确!\n\n密码仅允许包含\n• 字母 (a-z, A-Z)\n• 数字 (0-9)\n\n请勿使用特殊符号或空格');
return false;
}
if (password.length < 6) {
alert('至少输入6个字符');
return false;
}
return true;
}
</script>
</body>
</html>
<?php
exit;
}
// ==================== Token管理函数 ====================
/**
* 从PHP缓存文件读取Token
*/
function getCachedToken() {
if (!file_exists(TOKEN_CACHE_FILE)) {
return null;
}
// 包含文件获取变量
include TOKEN_CACHE_FILE;
if (!isset($cached_token) || !isset($cached_timestamp)) {
return null;
}
// 检查Token是否过期1小时内有效
$currentTime = time();
$tokenAge = $currentTime - $cached_timestamp;
if ($tokenAge < TOKEN_EXPIRE_TIME) {
return $cached_token;
}
// Token已过期清除缓存
@unlink(TOKEN_CACHE_FILE);
return null;
}
/**
* 保存Token到PHP缓存文件
*/
function saveTokenToCache($token) {
$timestamp = time();
$expireAt = date('Y-m-d H:i:s', $timestamp + TOKEN_EXPIRE_TIME);
$content = "<?php\n";
$content .= "// Token缓存文件 - 自动生成\n";
$content .= "// 生成时间: " . date('Y-m-d H:i:s', $timestamp) . "\n";
$content .= "// 过期时间: {$expireAt}\n";
$content .= "// 警告: 不要手动修改此文件\n\n";
$content .= "\$cached_token = '" . addslashes($token) . "';\n";
$content .= "\$cached_timestamp = {$timestamp};\n";
file_put_contents(TOKEN_CACHE_FILE, $content);
// 设置文件权限(如果可能)
if (function_exists('chmod')) {
@chmod(TOKEN_CACHE_FILE, 0600);
}
}
/**
* 获取登录Token带缓存
*/
function getLoginToken() {
// 首先尝试从缓存获取
$cachedToken = getCachedToken();
if ($cachedToken) {
return $cachedToken;
}
// 缓存不存在或已过期重新获取Token
$url = BASE_URL . '/login_api';
$data = [
'account' => ACCOUNT,
'password' => API_KEY
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
return null;
}
$result = json_decode($response, true);
$token = isset($result['jwt']) ? $result['jwt'] : null;
// 保存新Token到缓存
if ($token) {
saveTokenToCache($token);
}
return $token;
}
// ==================== 功能实现区域 ====================
/**
* 发送API请求
*/
function sendApiRequest($endpoint, $method = 'GET', $data = []) {
$token = getLoginToken();
if (!$token) {
return ['status' => 500, 'msg' => '获取Token失败请检查账号和密码配置'];
}
$url = BASE_URL . $endpoint;
$headers = [
'Authorization: JWT ' . $token,
'Content-Type: application/json'
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
} elseif ($method === 'PUT') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return json_decode($response, true);
}
/**
* 获取VPS列表
*/
function getHostList($page = 1, $limit = 100) {
return sendApiRequest("/hosts?page={$page}&limit={$limit}", 'GET');
}
/**
* 获取VPS状态
*/
function getHostStatus($hostId) {
return sendApiRequest("/hosts/{$hostId}/module/status?type=host", 'GET');
}
/**
* 操作VPS
*/
function operateHost($hostId, $operation) {
return sendApiRequest("/hosts/{$hostId}/module/{$operation}", 'PUT');
}
// ==================== 主逻辑区域 ====================
// 验证pass参数
$pass = isset($_GET['pass']) ? $_GET['pass'] : '';
if ($pass !== API_PASS) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['status' => 403, 'msg' => '访问密码错误或者未输入密码,请拼接?pass=API_PASS后再尝试访问'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
exit;
}
// 处理操作请求
$action = isset($_POST['action']) ? $_POST['action'] : '';
$hostId = isset($_POST['host_id']) ? intval($_POST['host_id']) : 0;
$result = null;
if ($action && $hostId > 0) {
if ($action === 'reboot') {
$result = operateHost($hostId, 'hard_reboot');
} elseif ($action === 'on') {
$result = operateHost($hostId, 'on');
} elseif ($action === 'off') {
$result = operateHost($hostId, 'off');
}
}
// 获取VPS列表和状态
$hosts = [];
$statusMap = [];
$domainstatus = [];
$totalCount = 0;
$errorMsg = '';
$listResult = getHostList();
if (isset($listResult['status']) && $listResult['status'] === 200) {
$data = $listResult['data'];
$hosts = $data['host'];
$domainstatus = $data['domainstatus'];
$totalCount = $data['total'];
// 批量获取所有VPS的状态
foreach ($hosts as $host) {
$statusResult = getHostStatus($host['id']);
if (isset($statusResult['status']) && $statusResult['status'] === 200) {
$statusMap[$host['id']] = $statusResult['data'];
} else {
$statusMap[$host['id']] = ['status' => 'unknown', 'des' => '未知'];
}
}
} else {
$errorMsg = isset($listResult['msg']) ? $listResult['msg'] : '获取VPS列表失败';
}
header('Content-Type: text/html; charset=utf-8');
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="./static/favicon.ico" type="image/x-icon">
<link href="./static/style.css" rel="stylesheet" type="text/css">
<title>VPS管理面板</title>
</head>
<body>
<div class="container">
<div class="header">
<h1>🖥️ 核云IDC VPS管理面板</h1>
<p>实时查看和管理您的云服务器</p>
</div>
<div class="content">
<?php if (!empty($errorMsg)): ?>
<div class="error-message">
<h3> 加载失败</h3>
<p><?php echo htmlspecialchars($errorMsg); ?></p>
</div>
<?php else: ?>
<?php if (!empty($result)): ?>
<?php if (isset($result['status']) && $result['status'] === 200): ?>
<div class="success-message">
<h3> 操作成功</h3>
<p>VPS #<?php echo $hostId; ?> 已成功执行操作</p>
</div>
<?php else: ?>
<div class="error-message">
<h3> 操作失败</h3>
<p><?php echo isset($result['msg']) ? htmlspecialchars($result['msg']) : '未知错误'; ?></p>
</div>
<?php endif; ?>
<?php endif; ?>
<?php
$cachedToken = getCachedToken();
if ($cachedToken):
// 从PHP文件读取时间戳
include TOKEN_CACHE_FILE;
$remainingTime = TOKEN_EXPIRE_TIME - (time() - $cached_timestamp);
$remainingMinutes = floor($remainingTime / 60);
?>
<div class="token-info">
<strong>Token缓存生效中</strong> | 剩余有效期:约 <?php echo $remainingMinutes; ?> 分钟
</div>
<?php endif; ?>
<div class="stats-bar">
<div class="stats-item">
总计:<strong><?php echo $totalCount; ?></strong> 台服务器
</div>
<div class="stats-item">
运行中:<strong><?php
$runningCount = 0;
foreach ($statusMap as $status) {
if ($status['status'] === 'on') $runningCount++;
}
echo $runningCount;
?></strong> 台
</div>
<div class="stats-item">
已关机:<strong><?php
$offCount = 0;
foreach ($statusMap as $status) {
if ($status['status'] === 'off') $offCount++;
}
echo $offCount;
?></strong> 台
</div>
</div>
<div class="card-grid">
<?php foreach ($hosts as $host):
$statusKey = $host['domainstatus'];
$statusInfo = isset($domainstatus[$statusKey]) ? $domainstatus[$statusKey] : ['name' => $statusKey, 'color' => '#999'];
$powerStatus = isset($statusMap[$host['id']]) ? $statusMap[$host['id']] : ['status' => 'unknown', 'des' => '未知'];
$regDate = date('Y-m-d', $host['regdate']);
$nextDueDate = date('Y-m-d', $host['nextduedate']);
$powerClass = 'power-unknown';
if ($powerStatus['status'] === 'on') {
$powerClass = 'power-on';
} elseif ($powerStatus['status'] === 'off') {
$powerClass = 'power-off';
}
?>
<div class="vps-card">
<div class="card-header">
<div class="vps-id">#<?php echo $host['id']; ?></div>
<span class="status-badge" style="background-color: <?php echo $statusInfo['color']; ?>">
<?php echo htmlspecialchars($statusInfo['name']); ?>
</span>
</div>
<div class="card-body">
<div class="info-row">
<span class="info-label">域名</span>
<span class="info-value"><?php echo htmlspecialchars($host['domain']); ?></span>
</div>
<div class="info-row">
<span class="info-label">IP地址</span>
<span class="info-value"><?php echo htmlspecialchars($host['dedicatedip']); ?></span>
</div>
<div class="info-row">
<span class="info-label">产品名称</span>
<span class="info-value"><?php echo htmlspecialchars($host['product_name']); ?></span>
</div>
<div class="info-row">
<span class="info-label">注册日期</span>
<span class="info-value"><?php echo $regDate; ?></span>
</div>
<div class="info-row">
<span class="info-label">到期日期</span>
<span class="info-value"><?php echo $nextDueDate; ?></span>
</div>
<div class="info-row">
<span class="info-label">金额</span>
<span class="info-value">¥<?php echo htmlspecialchars($host['amount']); ?></span>
</div>
<div class="info-row">
<span class="info-label">计费周期</span>
<span class="info-value"><?php echo htmlspecialchars($host['billingcycle']); ?></span>
</div>
</div>
<div class="power-status">
<span class="power-indicator <?php echo $powerClass; ?>"></span>
<span class="power-text"><?php echo htmlspecialchars($powerStatus['des']); ?></span>
</div>
<form method="POST" style="margin: 0;">
<input type="hidden" name="host_id" value="<?php echo $host['id']; ?>">
<div class="card-actions">
<button type="submit" name="action" value="on" class="btn btn-success" onclick="return confirm('确认开机?')">
开机
</button>
<button type="submit" name="action" value="off" class="btn btn-danger" onclick="return confirm('确认关机?')">
🔴 关机
</button>
<button type="submit" name="action" value="reboot" class="btn btn-warning" onclick="return confirm('确认硬重启?')" style="grid-column: span 2;">
🔄 硬重启
</button>
</div>
</form>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</body>
</html>

BIN
static/api.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
static/heyunlogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

121
static/initial.css Normal file
View File

@ -0,0 +1,121 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.config-container {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 40px;
max-width: 500px;
width: 100%;
}
.config-header {
text-align: center;
margin-bottom: 30px;
}
.config-header h1 {
color: #2d3748;
font-size: 24px;
margin-bottom: 10px;
}
.config-header p {
color: #718096;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
color: #4a5568;
font-weight: 600;
margin-bottom: 8px;
font-size: 14px;
}
.form-group input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
transition: all 0.3s;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-group .help-text {
color: #a0aec0;
font-size: 12px;
margin-top: 6px;
}
.error-message {
background: #fed7d7;
border-left: 4px solid #f56565;
color: #c53030;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
}
.btn-submit {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-submit:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
}
.btn-submit:active {
transform: translateY(0);
}
.info-box {
background: #ebf8ff;
border-left: 4px solid #4299e1;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 13px;
color: #2c5282;
}
@media (max-width: 480px) {
.config-container {
padding: 30px 20px;
}
}

338
static/style.css Normal file
View File

@ -0,0 +1,338 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
min-height: 100vh;
padding: 10px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 25px 20px;
text-align: center;
}
.header h1 {
font-size: 24px;
margin-bottom: 8px;
font-weight: 600;
}
.header p {
opacity: 0.9;
font-size: 13px;
}
.content {
padding: 20px;
}
.token-info {
background: linear-gradient(135deg, #e0f7fa 0%, #b2ebf2 100%);
border-left: 4px solid #00bcd4;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 13px;
color: #006064;
}
.token-info strong {
color: #00838f;
}
.stats-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 10px;
}
.stats-item {
font-size: 14px;
color: #2d3748;
}
.stats-item strong {
color: #667eea;
font-size: 18px;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
}
.vps-card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
border: 1px solid #e2e8f0;
}
.vps-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 12px;
border-bottom: 2px solid #f0f0f0;
}
.vps-id {
font-size: 18px;
font-weight: 600;
color: #2d3748;
}
.status-badge {
display: inline-block;
padding: 6px 14px;
border-radius: 20px;
color: white;
font-size: 12px;
font-weight: 600;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.card-body {
margin-bottom: 15px;
}
.info-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #f7fafc;
font-size: 13px;
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
color: #718096;
font-weight: 500;
}
.info-value {
color: #2d3748;
font-weight: 600;
text-align: right;
max-width: 60%;
word-break: break-all;
}
.power-status {
display: flex;
align-items: center;
gap: 8px;
padding: 10px;
background: #f7fafc;
border-radius: 8px;
margin-bottom: 15px;
}
.power-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
}
.power-on {
background: #48bb78;
box-shadow: 0 0 8px #48bb78;
}
.power-off {
background: #e53e3e;
box-shadow: 0 0 8px #e53e3e;
}
.power-unknown {
background: #a0aec0;
}
.power-text {
font-size: 14px;
font-weight: 600;
color: #2d3748;
}
.card-actions {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.btn {
padding: 10px 16px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
text-decoration: none;
color: white;
transition: all 0.3s;
text-align: center;
display: block;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.btn:active {
transform: translateY(0);
}
.btn-success {
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
}
.btn-warning {
background: linear-gradient(135deg, #ed8936 0%, #dd6b20 100%);
}
.btn-danger {
background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%);
}
.btn-info {
background: linear-gradient(135deg, #4299e1 0%, #3182ce 100%);
}
.error-message {
background: linear-gradient(135deg, #fed7d7 0%, #feb2b2 100%);
border-left: 4px solid #f56565;
color: #c53030;
padding: 20px;
border-radius: 8px;
text-align: center;
}
.success-message {
background: linear-gradient(135deg, #c6f6d5 0%, #9ae6b4 100%);
border-left: 4px solid #48bb78;
color: #22543d;
padding: 20px;
border-radius: 8px;
text-align: center;
margin-bottom: 20px;
}
.loading {
text-align: center;
padding: 40px;
color: #718096;
}
.loading-spinner {
display: inline-block;
width: 40px;
height: 40px;
border: 4px solid #e2e8f0;
border-top-color: #667eea;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 15px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 响应式设计 */
@media (max-width: 768px) {
body {
padding: 5px;
}
.header {
padding: 20px 15px;
}
.header h1 {
font-size: 20px;
}
.content {
padding: 15px;
}
.card-grid {
grid-template-columns: 1fr;
gap: 15px;
}
.vps-card {
padding: 15px;
}
.card-actions {
grid-template-columns: 1fr;
}
.stats-bar {
flex-direction: column;
align-items: flex-start;
}
.info-row {
flex-direction: column;
gap: 4px;
}
.info-value {
max-width: 100%;
text-align: left;
}
}
@media (max-width: 480px) {
.header h1 {
font-size: 18px;
}
.vps-id {
font-size: 16px;
}
.btn {
padding: 8px 12px;
font-size: 12px;
}
}