diff --git a/README.md b/README.md index 9e9c42a..67d77de 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,14 @@ ![API_KEY](./static/api.png) #### Web端 -部署到服务器上,分配域名或直接IP访问即可 +将web目录部分部署到服务器上,分配域名或直接IP访问即可 首次使用会进入配置页,依次输入API_PASS,ACCOUNT,API_KEY(其中API_PASS是您自定义的网站访问密码,支持大小写字母以及数字) 随后访问`http://example.com/?pass=API_PASS`即可 +切记:切勿将app部分放置于网站目录下 + #### 监控端 **安装:** diff --git a/app/install.sh b/app/install.sh new file mode 100644 index 0000000..a2bfe0b --- /dev/null +++ b/app/install.sh @@ -0,0 +1,191 @@ +#!/bin/bash + +# 核云IDC服务商VPS自动监测重启程序 - 安装脚本 +# 该脚本会创建一个systemd服务,持续化运行当前目录下的python main.py命令 + +# 获取当前脚本所在目录的绝对路径 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +# 定义服务名称 +SERVICE_NAME="idc-monitor" +SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service" + +echo "正在安装 ${SERVICE_NAME} 服务..." + +# 检查是否以root权限运行 +if [ "$EUID" -ne 0 ]; then + echo "错误: 请以root权限运行此脚本 (sudo ./install.sh)" + exit 1 +fi + +# 检查Python是否已安装 +if ! command -v python3 &> /dev/null; then + if command -v python &> /dev/null; then + PYTHON_CMD="python" + else + echo "错误: 未找到Python,请先安装Python" + exit 1 + fi +else + PYTHON_CMD="python3" +fi + +echo "检测到Python: ${PYTHON_CMD}" + +# 检查并安装pip +install_pip() { + echo "正在安装pip..." + + # 尝试使用系统包管理器安装pip + if command -v apt-get &> /dev/null; then + apt-get update && apt-get install -y python3-pip + PIP_CMD="pip3" + elif command -v yum &> /dev/null; then + yum install -y python3-pip + PIP_CMD="pip3" + elif command -v dnf &> /dev/null; then + dnf install -y python3-pip + PIP_CMD="pip3" + else + # 如果包管理器不可用,使用get-pip.py + echo "未检测到常用包管理器,尝试使用get-pip.py安装..." + curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py + ${PYTHON_CMD} get-pip.py + rm -f get-pip.py + PIP_CMD="pip" + fi + + # 验证pip安装 + if command -v pip3 &> /dev/null; then + PIP_CMD="pip3" + elif command -v pip &> /dev/null; then + PIP_CMD="pip" + else + echo "错误: pip安装失败" + return 1 + fi + + echo "✅ pip安装成功: ${PIP_CMD}" + return 0 +} + +# 检查pip是否存在 +if command -v pip3 &> /dev/null; then + PIP_CMD="pip3" + echo "检测到pip: ${PIP_CMD}" +elif command -v pip &> /dev/null; then + PIP_CMD="pip" + echo "检测到pip: ${PIP_CMD}" +else + echo "警告: 未检测到pip" + read -p "是否现在安装pip?(Y/n): " -n 1 -r + echo + if [[ $REPLY =~ ^[Nn]$ ]]; then + echo "跳过pip安装" + PIP_CMD="" + else + if ! install_pip; then + echo "错误: pip安装失败,无法继续" + exit 1 + fi + fi +fi + +# 安装Python依赖 +if [ -f "${PROJECT_DIR}/requirements.txt" ]; then + if [ -n "${PIP_CMD}" ]; then + echo "正在安装Python依赖包..." + cd ${PROJECT_DIR} + if ${PIP_CMD} install -r requirements.txt; then + echo "✅ Python依赖包安装成功" + else + echo "❌ Python依赖包安装失败" + read -p "是否继续安装服务?(y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "安装已取消" + exit 1 + fi + fi + else + echo "警告: 跳过依赖包安装(pip不可用)" + fi +else + echo "警告: ${PROJECT_DIR}/requirements.txt 文件不存在,跳过依赖安装" +fi + +# 检查main.py是否存在 +if [ ! -f "${PROJECT_DIR}/main.py" ]; then + echo "警告: ${PROJECT_DIR}/main.py 文件不存在" + echo "请确保main.py文件位于项目根目录下" + read -p "是否继续安装?(y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "安装已取消" + exit 1 + fi +fi + +# ... existing code ... + +# 创建systemd服务文件 +cat > ${SERVICE_FILE} << EOF +[Unit] +Description=Heyun IDC Monitor Service +After=network.target +Wants=network-online.target + +[Service] +Type=simple +User=root +WorkingDirectory=${PROJECT_DIR} +ExecStart=${PYTHON_CMD} ${PROJECT_DIR}/main.py +Restart=always +RestartSec=10 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=${SERVICE_NAME} + +# 资源限制 +LimitNOFILE=65536 + +# 安全设置 +NoNewPrivileges=true +ProtectSystem=strict +ReadWritePaths=${PROJECT_DIR} + +[Install] +WantedBy=multi-user.target +EOF + +# ... existing code ... + +# 重新加载systemd配置 +systemctl daemon-reload + +# 启用服务(开机自启) +systemctl enable ${SERVICE_NAME} + +# 启动服务 +systemctl start ${SERVICE_NAME} + +# 检查服务状态 +if systemctl is-active --quiet ${SERVICE_NAME}; then + echo "✅ ${SERVICE_NAME} 服务已成功安装并启动" + echo "服务状态: 运行中" +else + echo "❌ ${SERVICE_NAME} 服务启动失败" + echo "请检查日志: journalctl -u ${SERVICE_NAME} -f" + exit 1 +fi + +echo "" +echo "常用命令:" +echo " 查看服务状态: systemctl status ${SERVICE_NAME}" +echo " 查看实时日志: journalctl -u ${SERVICE_NAME} -f" +echo " 停止服务: systemctl stop ${SERVICE_NAME}" +echo " 重启服务: systemctl restart ${SERVICE_NAME}" +echo " 卸载服务: ./uninstall.sh" +echo "" +echo "安装完成!" \ No newline at end of file diff --git a/app/monitor.py b/app/monitor.py new file mode 100644 index 0000000..e69de29 diff --git a/app/uninstall.sh b/app/uninstall.sh new file mode 100644 index 0000000..ea413fc --- /dev/null +++ b/app/uninstall.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +# 核云IDC服务商VPS自动监测重启程序 - 卸载脚本 +# 该脚本会移除systemd服务并清理相关文件 + +# 定义服务名称 +SERVICE_NAME="idc-monitor" +SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service" + +echo "正在卸载 ${SERVICE_NAME} 服务..." + +# 检查是否以root权限运行 +if [ "$EUID" -ne 0 ]; then + echo "错误: 请以root权限运行此脚本 (sudo ./uninstall.sh)" + exit 1 +fi + +# 检查服务是否存在 +if [ ! -f "${SERVICE_FILE}" ]; then + echo "警告: 服务文件 ${SERVICE_FILE} 不存在" + echo "可能服务未安装或已被卸载" + exit 0 +fi + +# 停止服务 +echo "正在停止服务..." +if systemctl is-active --quiet ${SERVICE_NAME}; then + systemctl stop ${SERVICE_NAME} + echo "✅ 服务已停止" +else + echo "服务未运行,跳过停止步骤" +fi + +# 禁用服务(取消开机自启) +echo "正在禁用服务..." +if systemctl is-enabled --quiet ${SERVICE_NAME} 2>/dev/null; then + systemctl disable ${SERVICE_NAME} + echo "✅ 服务已禁用" +else + echo "服务未启用,跳过禁用步骤" +fi + +# 重新加载systemd配置 +echo "正在清理systemd配置..." +systemctl daemon-reload +systemctl reset-failed ${SERVICE_NAME} 2>/dev/null + +# 删除服务文件 +echo "正在删除服务文件..." +rm -f ${SERVICE_FILE} +echo "✅ 服务文件已删除: ${SERVICE_FILE}" + +# 清理journal日志(可选) +echo "" +read -p "是否同时清理该服务的历史日志?(y/N): " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + journalctl --rotate + journalctl --vacuum-time=1s 2>/dev/null + echo "✅ 日志已清理" +else + echo "跳过日志清理" +fi + +echo "" +echo "✅ ${SERVICE_NAME} 服务已成功卸载" +echo "" +echo "注意:" +echo " - Python依赖包未被卸载,如需清理请手动执行: pip uninstall -r requirements.txt" +echo " - 项目文件未被删除,如需删除请手动清理项目目录" +echo "" +echo "卸载完成!" \ No newline at end of file diff --git a/index.php b/index.php deleted file mode 100644 index 4c4f95b..0000000 --- a/index.php +++ /dev/null @@ -1,494 +0,0 @@ - - - - - - - - - 初始配置 - VPS管理面板 - - -
-
-

🔧 初始配置

-

请填写以下信息以开始使用

-
- - -
- ❌ -
- - -
- 💡 提示:这些信息将保存在 config.php 文件中,请妥善保管。
- 若您需要重置或更改配置,请删除 config.php 文件并重新运行。
- 配置完成后请以该格式访问服务:example.com/?pass=API_PASS -
- -
-
- - -
用于保护管理面板的访问权限
可选:字母(a-z,A-Z)和数字(0-9)
-
- -
- - -
您的核云IDC登录账号
-
- -
- - -
在核云IDC控制台获取的API密钥
-
- - -
-
- - - - - 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'); -?> - - - - - - - - VPS管理面板 - - -
-
-

🖥️ 核云IDC VPS管理面板

-

实时查看和管理您的云服务器

-
- -
- -
-

❌ 加载失败

-

-
- - - - -
-

✅ 操作成功

-

VPS # 已成功执行操作

-
- -
-

❌ 操作失败

-

-
- - - - -
- ✅ Token缓存生效中 | 剩余有效期:约 分钟 -
- - -
-
- 总计: 台服务器 -
-
- 运行中: 台 -
-
- 已关机: 台 -
-
- -
- $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'; - } - ?> -
-
-
#
- - - -
- -
-
- 域名 - -
-
- IP地址 - -
-
- 产品名称 - -
-
- 注册日期 - -
-
- 到期日期 - -
-
- 金额 - ¥ -
-
- 计费周期 - -
-
- -
- - -
- -
- -
- - - -
-
-
- -
- -
-
- - \ No newline at end of file diff --git a/static/api.png b/static/api.png deleted file mode 100644 index 48caf5f..0000000 Binary files a/static/api.png and /dev/null differ diff --git a/static/favicon.ico b/static/favicon.ico deleted file mode 100644 index edab233..0000000 Binary files a/static/favicon.ico and /dev/null differ diff --git a/static/heyunlogo.png b/static/heyunlogo.png deleted file mode 100644 index b3d88fb..0000000 Binary files a/static/heyunlogo.png and /dev/null differ diff --git a/static/initial.css b/static/initial.css deleted file mode 100644 index 70d47b1..0000000 --- a/static/initial.css +++ /dev/null @@ -1,121 +0,0 @@ -* { - 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; - } -} \ No newline at end of file diff --git a/static/style.css b/static/style.css deleted file mode 100644 index 9e955c0..0000000 --- a/static/style.css +++ /dev/null @@ -1,338 +0,0 @@ -* { - 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; - } -} \ No newline at end of file diff --git a/web b/web new file mode 160000 index 0000000..28cb40a --- /dev/null +++ b/web @@ -0,0 +1 @@ +Subproject commit 28cb40ad280dbca46ae1fe0254f8910eb178ce6f