mirror of
https://github.com/Kori1c/ecs-controller.git
synced 2026-05-07 22:27:22 +08:00
973 lines
56 KiB
HTML
973 lines
56 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN" class="antialiased">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>阿里云 CDT 监控控制台</title>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
||
<script>
|
||
tailwind.config = {
|
||
theme: {
|
||
extend: {
|
||
fontFamily: {
|
||
sans: ['Inter', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
|
||
},
|
||
colors: {
|
||
ios: {
|
||
bg: '#F2F2F7',
|
||
card: 'rgba(255, 255, 255, 0.72)',
|
||
blue: '#007AFF',
|
||
black: '#1C1C1E',
|
||
red: '#FF3B30',
|
||
green: '#34C759',
|
||
gray: '#8E8E93',
|
||
}
|
||
},
|
||
backdropBlur: {
|
||
xs: '2px',
|
||
},
|
||
animation: {
|
||
'spin-slow': 'spin 1.5s linear infinite',
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
<style>
|
||
[v-cloak] { display: none !important; }
|
||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||
|
||
.glass-panel {
|
||
background: rgba(255, 255, 255, 0.65);
|
||
backdrop-filter: blur(40px);
|
||
-webkit-backdrop-filter: blur(40px);
|
||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||
box-shadow: 0 1px 2px rgba(0,0,0,0.02), 0 8px 32px rgba(0,0,0,0.04), inset 0 0 0 1px rgba(255,255,255,0.2);
|
||
}
|
||
|
||
.glass-input {
|
||
background: rgba(242, 242, 247, 0.5);
|
||
box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);
|
||
transition: all 0.2s;
|
||
}
|
||
.glass-input:focus {
|
||
background: #fff;
|
||
box-shadow: 0 0 0 2px rgba(0,0,0,0.05);
|
||
}
|
||
|
||
.tap-effect:active {
|
||
transform: scale(0.96);
|
||
transition: transform 0.1s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||
}
|
||
|
||
.tooltip-content {
|
||
visibility: hidden;
|
||
opacity: 0;
|
||
left: -2rem;
|
||
transform: translateY(10px);
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
.group:hover .tooltip-content {
|
||
visibility: visible;
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
@media (min-width: 768px) {
|
||
.tooltip-content { left: 50%; transform: translateY(10px) translateX(-50%); }
|
||
.group:hover .tooltip-content { transform: translateY(0) translateX(-50%); }
|
||
}
|
||
|
||
.dropdown-enter-active, .dropdown-leave-active { transition: all 0.2s ease; }
|
||
.dropdown-enter-from, .dropdown-leave-to { opacity: 0; transform: translateY(-10px); }
|
||
|
||
body { background-color: #F2F2F7; }
|
||
</style>
|
||
</head>
|
||
<body class="text-[#1C1C1E]">
|
||
|
||
<div id="app" v-cloak class="min-h-screen p-6 md:p-12 flex flex-col items-center">
|
||
|
||
<!-- 警告条 -->
|
||
<div v-if="cronWarning && initialized && !criticalError" class="w-full max-w-5xl mb-6 bg-yellow-50 border-l-4 border-yellow-400 p-4 rounded-r-xl shadow-sm animate-fade-in">
|
||
<div class="flex">
|
||
<div class="flex-shrink-0">
|
||
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
|
||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||
</svg>
|
||
</div>
|
||
<div class="ml-3">
|
||
<p class="text-sm text-yellow-700">
|
||
<span class="font-bold">监控心跳异常:</span>
|
||
计划任务可能未运行,请检查 Crontab 设置 (超过 3 分钟无响应)。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<header class="w-full max-w-5xl flex justify-between items-center mb-10 px-2">
|
||
<div>
|
||
<h1 class="text-2xl font-bold tracking-tight text-[#1C1C1E]">CDT Monitor</h1>
|
||
<p class="text-xs font-semibold tracking-widest text-gray-400 uppercase mt-1">Status & Control</p>
|
||
</div>
|
||
|
||
<button v-if="initialized && !checkingLogin && !criticalError" @click="toggleAdmin"
|
||
class="tap-effect px-5 py-2 rounded-full text-sm font-medium transition-all duration-300"
|
||
:class="isAdmin ? 'bg-[#1C1C1E] text-white shadow-lg' : 'bg-white text-gray-600 shadow-sm border border-gray-200'">
|
||
{{ isAdmin ? '退出管理' : '管理员登录' }}
|
||
</button>
|
||
</header>
|
||
|
||
<main v-if="initialized && !loading && !criticalError" class="w-full max-w-5xl grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 animate-fade-in-up">
|
||
<div v-for="(item, index) in statusData" :key="index"
|
||
class="glass-panel rounded-[32px] p-6 flex flex-col justify-between relative overflow-hidden group hover:shadow-2xl transition-all duration-500">
|
||
<div class="absolute top-0 right-0 p-6 opacity-50 flex gap-2">
|
||
<!-- 图表按钮 (新增) -->
|
||
<button v-if="isAdmin" @click="openChart(item.id, item.account)"
|
||
class="p-1 rounded-full hover:bg-white/50 text-gray-400 hover:text-blue-500 transition-colors mr-1">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" />
|
||
</svg>
|
||
</button>
|
||
|
||
<!-- 刷新按钮 -->
|
||
<button v-if="isAdmin" @click="refreshSingle(item.id, index)"
|
||
class="p-1 rounded-full hover:bg-white/50 text-gray-400 hover:text-gray-600 transition-colors"
|
||
:class="{'animate-spin': refreshingMap[index]}"
|
||
:disabled="refreshingMap[index]">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
|
||
</svg>
|
||
</button>
|
||
<div class="w-2 h-2 rounded-full transition-colors duration-500 mt-1.5"
|
||
:class="item.rate95 ? 'bg-red-500 shadow-[0_0_10px_rgba(255,59,48,0.5)]' : 'bg-green-500 shadow-[0_0_10px_rgba(52,199,89,0.5)]'"></div>
|
||
</div>
|
||
|
||
<div>
|
||
<div class="flex items-center gap-2 mb-4">
|
||
<span class="px-2 py-1 bg-gray-100/50 rounded-lg text-[10px] font-bold tracking-wider text-gray-500 uppercase border border-white/40">{{ item.regionName }}</span>
|
||
<span class="text-[10px] text-gray-400">{{ item.lastUpdated ? item.lastUpdated.split(' ')[1] : '' }} 更新</span>
|
||
</div>
|
||
<h3 class="text-3xl font-extrabold tracking-tighter text-[#1C1C1E]">
|
||
{{ item.flow_used }} <span class="text-base font-medium text-gray-400">GB</span>
|
||
</h3>
|
||
<div class="flex justify-between items-end mt-1">
|
||
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wide">已用流量</p>
|
||
<p class="text-xs font-medium text-gray-400">{{ item.percentageOfUse }}% / {{ item.flow_total }}G</p>
|
||
</div>
|
||
<div class="w-full h-1.5 bg-gray-200/50 rounded-full mt-3 overflow-hidden">
|
||
<div class="h-full rounded-full transition-all duration-1000 ease-out"
|
||
:class="item.rate95 ? 'bg-[#FF3B30]' : 'bg-[#1C1C1E]'"
|
||
:style="{ width: Math.min(item.percentageOfUse, 100) + '%' }"></div>
|
||
</div>
|
||
</div>
|
||
<div class="mt-6 pt-6 border-t border-gray-200/40 flex flex-col gap-2">
|
||
<div class="flex justify-between items-center">
|
||
<span class="text-sm font-medium text-gray-500">运行状态</span>
|
||
<span class="text-sm font-bold" :class="getInstanceStatusColor(item.instanceStatus)">{{ item.instanceStatus }}</span>
|
||
</div>
|
||
<div class="flex justify-between items-center">
|
||
<span class="text-sm font-medium text-gray-500">账号</span>
|
||
<span class="text-sm font-medium text-[#1C1C1E] font-mono">{{ item.account }}</span>
|
||
</div>
|
||
<div class="flex justify-between items-center">
|
||
<span class="text-xs font-medium text-gray-400">当前阈值</span>
|
||
<span class="text-xs font-medium text-gray-400">{{ item.threshold }}%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="statusData.length === 0" class="col-span-full py-20 text-center text-gray-400">
|
||
<p>暂无监控账号,请点击右上角登录管理员添加。</p>
|
||
</div>
|
||
</main>
|
||
|
||
<div v-if="initialized && loading && !criticalError" class="w-full h-64 flex items-center justify-center">
|
||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-800"></div>
|
||
</div>
|
||
|
||
<!-- Chart Modal (New) -->
|
||
<div v-if="showChartModal" class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||
<div class="absolute inset-0 bg-gray-200/30 backdrop-blur-md" @click="closeChart"></div>
|
||
<div class="glass-panel w-full max-w-4xl p-6 rounded-[40px] relative z-10 shadow-2xl bg-white/90">
|
||
<div class="flex justify-between items-center mb-6">
|
||
<div>
|
||
<h3 class="text-xl font-bold text-[#1C1C1E]">流量趋势</h3>
|
||
<p class="text-xs text-gray-400 font-mono mt-1">{{ currentChartAccount }}</p>
|
||
</div>
|
||
<div class="flex bg-gray-100 rounded-lg p-1">
|
||
<button @click="switchChartMode('30d')"
|
||
class="px-3 py-1 text-xs font-medium rounded-md transition-all"
|
||
:class="chartMode === '30d' ? 'bg-white shadow text-black' : 'text-gray-500'">30天</button>
|
||
<button @click="switchChartMode('24h')"
|
||
class="px-3 py-1 text-xs font-medium rounded-md transition-all"
|
||
:class="chartMode === '24h' ? 'bg-white shadow text-black' : 'text-gray-500'">24小时</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="chartLoading" class="h-80 flex items-center justify-center">
|
||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-800"></div>
|
||
</div>
|
||
<div v-show="!chartLoading" id="echarts-container" class="w-full h-80"></div>
|
||
|
||
<div class="mt-4 text-center">
|
||
<button @click="closeChart" class="text-sm text-gray-500 hover:text-gray-800 transition">关闭</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Critical Error Modal -->
|
||
<div v-if="criticalError" class="fixed inset-0 z-50 flex items-center justify-center p-6 bg-red-50/50 backdrop-blur-sm">
|
||
<div class="glass-panel w-full max-w-md p-8 rounded-[40px] shadow-xl border-red-200 bg-red-50/80">
|
||
<div class="text-center">
|
||
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4">
|
||
<svg class="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||
</svg>
|
||
</div>
|
||
<h3 class="text-lg font-medium leading-6 text-red-900">系统初始化错误</h3>
|
||
<div class="mt-2 px-2 py-3">
|
||
<p class="text-sm text-red-700 break-words font-mono bg-red-100/50 p-2 rounded-lg">{{ criticalError }}</p>
|
||
</div>
|
||
<p class="mt-4 text-xs text-gray-500">请检查服务器目录权限,确保 Web 用户对当前目录有写入权限。</p>
|
||
<div class="mt-6">
|
||
<button @click="location.reload()" class="w-full inline-flex justify-center rounded-xl border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none sm:text-sm">重试</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Login Modal -->
|
||
<div v-if="showLoginModal" class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||
<div class="absolute inset-0 bg-gray-200/30 backdrop-blur-md" @click="showLoginModal = false"></div>
|
||
<div class="glass-panel w-full max-w-md p-8 rounded-[40px] relative z-10 transform transition-all shadow-2xl">
|
||
<h2 class="text-2xl font-bold mb-6 text-center">管理员验证</h2>
|
||
<input type="password" v-model="passwordInput" placeholder="请输入配置密码"
|
||
class="w-full glass-input px-4 py-3 rounded-xl border border-transparent focus:outline-none transition-all text-center text-lg mb-6">
|
||
<button @click="performLogin" class="w-full bg-[#1C1C1E] text-white py-3 rounded-2xl font-semibold tap-effect shadow-lg">解锁控制台</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Setup Wizard -->
|
||
<div v-if="!initialized && !loadingCheckInit && !criticalError" class="fixed inset-0 z-50 flex flex-col items-center justify-center p-6 bg-[#F2F2F7]">
|
||
<!-- ... (Setup Wizard content remains same) ... -->
|
||
<div class="glass-panel w-full max-w-lg p-10 rounded-[48px] shadow-2xl border border-white/80">
|
||
<div class="text-center mb-8">
|
||
<h1 class="text-3xl font-extrabold text-[#1C1C1E] mb-2">欢迎使用 CDT Monitor</h1>
|
||
<p class="text-gray-500 text-sm">初次使用,请先配置核心参数。</p>
|
||
</div>
|
||
<div class="space-y-6">
|
||
<div class="space-y-2">
|
||
<label class="text-xs font-bold text-gray-500 ml-2 uppercase tracking-wider">设置管理员密码 <span class="text-red-500">*</span></label>
|
||
<input type="text" v-model="setupData.admin_password" placeholder="用于登录后台管理" class="w-full glass-input rounded-2xl px-5 py-3 text-base focus:ring-2 focus:ring-gray-200 focus:bg-white transition-all">
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-4">
|
||
<div class="space-y-2">
|
||
<label class="text-xs font-bold text-gray-500 ml-2 uppercase tracking-wider">流量阈值 (%)</label>
|
||
<input type="number" v-model.number="setupData.traffic_threshold" class="w-full glass-input rounded-2xl px-5 py-3 text-base">
|
||
</div>
|
||
<div class="space-y-2">
|
||
<label class="text-xs font-bold text-gray-500 ml-2 uppercase tracking-wider">停机模式</label>
|
||
<select v-model="setupData.shutdown_mode" class="w-full glass-input rounded-2xl px-5 py-3 text-base appearance-none bg-transparent">
|
||
<option value="KeepCharging">普通停机</option>
|
||
<option value="StopCharging">节省停机</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<button @click="performSetup" :disabled="!setupData.admin_password" class="w-full bg-[#1C1C1E] text-white py-4 rounded-2xl font-bold text-lg shadow-xl hover:shadow-2xl hover:scale-[1.02] active:scale-95 transition-all disabled:opacity-50 disabled:cursor-not-allowed mt-4">初始化系统</button>
|
||
<p class="text-center text-[10px] text-gray-400">点击后将创建本地数据库并自动登录。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Admin Config Editor -->
|
||
<div v-if="isAdmin && config" class="w-full max-w-5xl mt-12 mb-20 animate-fade-in-up space-y-8">
|
||
<div class="flex items-center gap-4">
|
||
<h2 class="text-xl font-bold">系统配置</h2>
|
||
<div class="h-px bg-gray-300 flex-1"></div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||
<!-- Left Column -->
|
||
<div class="lg:col-span-1 space-y-8">
|
||
<!-- Global Settings -->
|
||
<div class="glass-panel rounded-[32px] p-6 overflow-visible z-20">
|
||
<h3 class="text-sm font-bold uppercase tracking-widest text-gray-400 mb-4">全局设置</h3>
|
||
<div class="space-y-4">
|
||
<div class="space-y-1">
|
||
<label class="text-xs font-medium text-gray-500 ml-2">管理员密码</label>
|
||
<input v-model="config.admin_password" type="text" class="w-full glass-input rounded-xl px-4 py-2 text-sm">
|
||
</div>
|
||
<div class="space-y-1">
|
||
<label class="text-xs font-medium text-gray-500 ml-2">告警阈值 (%)</label>
|
||
<input v-model.number="config.traffic_threshold" type="number" min="1" max="100" class="w-full glass-input rounded-xl px-4 py-2 text-sm">
|
||
</div>
|
||
|
||
<!-- 接口调用频率 -->
|
||
<div class="space-y-1">
|
||
<label class="text-xs font-medium text-gray-500 ml-2">接口调用频率</label>
|
||
<select v-model.number="config.api_interval" class="w-full glass-input rounded-xl px-4 py-2 text-sm appearance-none cursor-pointer">
|
||
<option value="60">1 分钟 (高频)</option>
|
||
<option value="180">3 分钟</option>
|
||
<option value="300">5 分钟</option>
|
||
<option value="600">10 分钟 (默认)</option>
|
||
<option value="1800">30 分钟 (低频)</option>
|
||
</select>
|
||
<p class="text-[10px] text-gray-400 px-2 mt-1">控制更新状态和流量的频率。开关机操作触发时会自动加速至 1 分钟。</p>
|
||
</div>
|
||
|
||
<!-- 停机模式 (带 Tooltip) -->
|
||
<div class="space-y-1 relative">
|
||
<div class="flex items-center gap-1">
|
||
<label class="text-xs font-medium text-gray-500 ml-2">停机模式</label>
|
||
<div class="group relative flex items-center">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3.5 h-3.5 text-gray-400 cursor-help hover:text-gray-600 transition-colors">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
||
</svg>
|
||
<div class="tooltip-content absolute bottom-full mb-2 w-64 md:w-80 p-4 bg-[#1C1C1E]/95 backdrop-blur-md text-white text-xs rounded-2xl shadow-[0_8px_30px_rgba(0,0,0,0.3)] z-50 border border-gray-700 pointer-events-none text-left leading-relaxed">
|
||
<div class="space-y-3">
|
||
<div>
|
||
<h4 class="text-orange-400 font-bold mb-1">普通停机模式 (KeepCharging)</h4>
|
||
<p class="text-gray-300">停止实例后保留实例的资源并继续收费。</p>
|
||
</div>
|
||
<div class="w-full h-px bg-gray-700/50"></div>
|
||
<div>
|
||
<h4 class="text-green-400 font-bold mb-1">节省停机模式 (StopCharging)</h4>
|
||
<p class="text-gray-300">计算资源(CPU/内存)、固定公网IP带宽暂停计费。</p>
|
||
</div>
|
||
</div>
|
||
<div class="absolute top-full left-10 md:left-1/2 md:-translate-x-1/2 -mt-2 border-8 border-transparent border-t-[#1C1C1E]/95"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<select v-model="config.shutdown_mode" class="w-full glass-input rounded-xl px-4 py-2 text-sm appearance-none cursor-pointer">
|
||
<option value="KeepCharging">普通停机 (保留IP/收费)</option>
|
||
<option value="StopCharging">节省停机 (回收IP/免费)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="space-y-1">
|
||
<label class="text-xs font-medium text-gray-500 ml-2">流量阈值动作</label>
|
||
<select v-model="config.threshold_action" class="w-full glass-input rounded-xl px-4 py-2 text-sm appearance-none cursor-pointer">
|
||
<option value="stop_and_notify">自动关机并告警 (默认)</option>
|
||
<option value="notify_only">仅发送告警 (不关机)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 实例保活 (新增) -->
|
||
<div class="flex items-center justify-between pt-2">
|
||
<div class="flex items-center gap-1">
|
||
<label class="text-xs font-medium text-gray-500 ml-2">实例保活 (抢占式)</label>
|
||
<!-- Tooltip Trigger -->
|
||
<div class="group relative flex items-center">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3.5 h-3.5 text-gray-400 cursor-help hover:text-gray-600 transition-colors">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
||
</svg>
|
||
<div class="tooltip-content absolute bottom-full mb-2 w-64 md:w-72 p-4 bg-[#1C1C1E]/95 backdrop-blur-md text-white text-xs rounded-2xl shadow-[0_8px_30px_rgba(0,0,0,0.3)] z-50 border border-gray-700 pointer-events-none text-left leading-relaxed">
|
||
<p class="text-gray-300">防止抢占式实例在<span class="text-green-400 font-bold">非关机时间段</span>被意外释放。</p>
|
||
<div class="absolute top-full left-10 md:left-1/2 md:-translate-x-1/2 -mt-2 border-8 border-transparent border-t-[#1C1C1E]/95"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="relative inline-block w-10 mr-2">
|
||
<input type="checkbox" v-model="config.keep_alive" id="toggle-keep-alive" class="peer sr-only"/>
|
||
<label for="toggle-keep-alive" class="block overflow-hidden h-5 w-10 rounded-full bg-gray-300 cursor-pointer transition-colors peer-checked:bg-[#1C1C1E]"></label>
|
||
<div class="absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full transition-transform peer-checked:translate-x-5"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex items-center justify-between pt-2">
|
||
<label class="text-xs font-medium text-gray-500 ml-2">定时任务邮件通知</label>
|
||
<div class="relative inline-block w-10 mr-2">
|
||
<input type="checkbox" v-model="config.enable_schedule_email" id="toggle-sched" class="peer sr-only"/>
|
||
<label for="toggle-sched" class="block overflow-hidden h-5 w-10 rounded-full bg-gray-300 cursor-pointer transition-colors peer-checked:bg-[#1C1C1E]"></label>
|
||
<div class="absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full transition-transform peer-checked:translate-x-5"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Notification Settings -->
|
||
<div class="glass-panel rounded-[32px] p-6 z-10">
|
||
<h3 class="text-sm font-bold uppercase tracking-widest text-gray-400 mb-4">邮件服务器配置</h3>
|
||
<div class="space-y-3">
|
||
<div class="space-y-1">
|
||
<label class="text-xs font-medium text-gray-500 ml-2">SMTP Host</label>
|
||
<input v-model="config.Notification.host" class="w-full glass-input rounded-xl px-4 py-2 text-sm">
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div class="space-y-1">
|
||
<label class="text-xs font-medium text-gray-500 ml-2">Port</label>
|
||
<input v-model="config.Notification.port" type="number" class="w-full glass-input rounded-xl px-4 py-2 text-sm">
|
||
</div>
|
||
<div class="space-y-1">
|
||
<label class="text-xs font-medium text-gray-500 ml-2">Secure</label>
|
||
<select v-model="config.Notification.secure" class="w-full glass-input rounded-xl px-4 py-2 text-sm appearance-none">
|
||
<option value="ssl">SSL</option>
|
||
<option value="tls">TLS</option>
|
||
<option value="">无 (None)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="space-y-1">
|
||
<label class="text-xs font-medium text-gray-500 ml-2">Username</label>
|
||
<input v-model="config.Notification.username" class="w-full glass-input rounded-xl px-4 py-2 text-sm">
|
||
</div>
|
||
<div class="space-y-1">
|
||
<label class="text-xs font-medium text-gray-500 ml-2">Password</label>
|
||
<input v-model="config.Notification.password" type="password" class="w-full glass-input rounded-xl px-4 py-2 text-sm">
|
||
</div>
|
||
<div class="space-y-1">
|
||
<label class="text-xs font-medium text-gray-500 ml-2">Receiver Email</label>
|
||
<input v-model="config.Notification.email" class="w-full glass-input rounded-xl px-4 py-2 text-sm">
|
||
</div>
|
||
|
||
<div class="pt-4">
|
||
<button @click="sendTestEmail" :disabled="sendingEmail" class="w-full border border-gray-300 text-gray-600 py-2 rounded-xl text-xs font-semibold hover:bg-gray-50 transition disabled:opacity-50">
|
||
{{ sendingEmail ? '发送中...' : '发送测试邮件' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right Column (Accounts) -->
|
||
<div class="lg:col-span-2 space-y-8">
|
||
<div class="glass-panel rounded-[40px] p-8 h-fit z-0">
|
||
<div class="space-y-8">
|
||
<div v-for="(acc, idx) in config.Accounts" :key="idx" class="p-6 bg-white/40 rounded-[28px] border border-white/60 relative hover:bg-white/60 transition-colors">
|
||
<button @click="removeAccount(idx)" class="absolute top-4 right-4 text-xs text-red-500 font-bold hover:bg-red-50 p-2 rounded-lg transition">移除</button>
|
||
|
||
<h3 class="text-sm font-bold uppercase tracking-widest text-gray-400 mb-4">账号 {{ idx + 1 }}</h3>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div class="space-y-1">
|
||
<label class="text-xs font-medium text-gray-500 ml-2">AccessKey ID</label>
|
||
<input v-model="acc.AccessKeyId" class="w-full glass-input rounded-xl px-4 py-2 text-sm">
|
||
</div>
|
||
<div class="space-y-1">
|
||
<label class="text-xs font-medium text-gray-500 ml-2">AccessKey Secret</label>
|
||
<input v-model="acc.AccessKeySecret" type="password" class="w-full glass-input rounded-xl px-4 py-2 text-sm">
|
||
</div>
|
||
<div class="space-y-1">
|
||
<label class="text-xs font-medium text-gray-500 ml-2">Instance ID</label>
|
||
<input v-model="acc.instanceId" placeholder="实例 ID (必填)" class="w-full glass-input rounded-xl px-4 py-2 text-sm">
|
||
</div>
|
||
<div class="space-y-1 relative">
|
||
<label class="text-xs font-medium text-gray-500 ml-2">Region ID</label>
|
||
<input v-model="acc.regionId"
|
||
@focus="regionSearchFocus = idx"
|
||
@blur="setTimeout(() => regionSearchFocus = -1, 200)"
|
||
placeholder="输入地域名称或代码..."
|
||
class="w-full glass-input rounded-xl px-4 py-2 text-sm">
|
||
|
||
<transition name="dropdown">
|
||
<div v-show="regionSearchFocus === idx" class="absolute left-0 right-0 top-full mt-2 bg-white/90 backdrop-blur-md border border-gray-200 rounded-xl shadow-xl z-50 max-h-60 overflow-y-auto no-scrollbar">
|
||
<ul>
|
||
<li v-for="region in aliyunRegions.filter(r => !acc.regionId || r.id.toLowerCase().includes(acc.regionId.toLowerCase()) || r.name.includes(acc.regionId))"
|
||
@click="acc.regionId = region.id; regionSearchFocus = -1"
|
||
class="px-4 py-2 text-xs hover:bg-gray-100 cursor-pointer border-b border-gray-100/50 last:border-0 flex justify-between items-center group">
|
||
<span class="font-medium text-gray-800">{{ region.name }}</span>
|
||
<span class="text-[10px] text-gray-400 group-hover:text-gray-600 font-mono">{{ region.id }}</span>
|
||
</li>
|
||
<li v-if="aliyunRegions.filter(r => !acc.regionId || r.id.toLowerCase().includes(acc.regionId.toLowerCase()) || r.name.includes(acc.regionId)).length === 0" class="px-4 py-3 text-xs text-gray-400 text-center">
|
||
无匹配地域
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</transition>
|
||
</div>
|
||
<div class="space-y-1">
|
||
<label class="text-xs font-medium text-gray-500 ml-2">Max Traffic (GB)</label>
|
||
<input v-model.number="acc.maxTraffic" type="number" class="w-full glass-input rounded-xl px-4 py-2 text-sm">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-6 pt-4 border-t border-gray-200/50">
|
||
<div class="flex items-center gap-3 mb-3">
|
||
<div class="relative inline-block w-8 mr-1">
|
||
<input type="checkbox" v-model="acc.schedule.enabled" :id="'sched-'+idx" class="peer sr-only"/>
|
||
<label :for="'sched-'+idx" class="block overflow-hidden h-4 w-8 rounded-full bg-gray-300 cursor-pointer transition-colors peer-checked:bg-[#1C1C1E]"></label>
|
||
<div class="absolute left-0.5 top-0.5 w-3 h-3 bg-white rounded-full transition-transform peer-checked:translate-x-4"></div>
|
||
</div>
|
||
<span class="text-sm font-bold text-gray-700">启用定时开关机</span>
|
||
</div>
|
||
|
||
<div v-if="acc.schedule && acc.schedule.enabled" class="flex flex-wrap gap-4 items-center animate-fade-in">
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-xs text-gray-500">开机时间</span>
|
||
<input type="time" v-model="acc.schedule.startTime" class="bg-white/80 border border-gray-200 rounded-lg px-2 py-1 text-sm">
|
||
</div>
|
||
<div class="w-4 h-px bg-gray-300"></div>
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-xs text-gray-500">关机时间</span>
|
||
<input type="time" v-model="acc.schedule.stopTime" class="bg-white/80 border border-gray-200 rounded-lg px-2 py-1 text-sm">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button @click="addAccount" class="w-full py-4 rounded-2xl border border-dashed border-gray-300 text-gray-400 hover:bg-white/40 hover:text-gray-600 transition font-medium">+ 添加账号</button>
|
||
|
||
<div class="flex justify-end pt-6">
|
||
<button @click="saveConfig" class="bg-[#1C1C1E] text-white px-10 py-4 rounded-2xl font-bold shadow-xl tap-effect hover:shadow-2xl transition-all">保存所有配置</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- System Logs (New) -->
|
||
<div class="glass-panel rounded-[40px] p-8 relative overflow-hidden">
|
||
<h3 class="text-lg font-bold mb-6 text-[#1C1C1E] flex items-center justify-between">
|
||
系统执行日志
|
||
<button @click="fetchLogs" class="text-xs font-normal text-blue-500 hover:text-blue-600 transition">刷新日志</button>
|
||
</h3>
|
||
<div class="overflow-y-auto max-h-96 pr-2 no-scrollbar">
|
||
<table class="w-full text-left border-collapse">
|
||
<thead>
|
||
<tr>
|
||
<th class="text-xs font-bold text-gray-400 uppercase tracking-wider pb-3 pl-2">时间</th>
|
||
<th class="text-xs font-bold text-gray-400 uppercase tracking-wider pb-3">类型</th>
|
||
<th class="text-xs font-bold text-gray-400 uppercase tracking-wider pb-3">内容</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="text-sm text-gray-600">
|
||
<tr v-for="(log, i) in systemLogs" :key="i" class="border-t border-gray-100/50 hover:bg-white/40 transition-colors">
|
||
<td class="py-3 pl-2 text-gray-500 font-mono text-xs whitespace-nowrap">{{ log.time_str }}</td>
|
||
<td class="py-3">
|
||
<span class="px-2 py-0.5 rounded-md text-[10px] font-bold uppercase tracking-wide"
|
||
:class="{
|
||
'bg-blue-100 text-blue-700': log.type === 'info',
|
||
'bg-yellow-100 text-yellow-700': log.type === 'warning',
|
||
'bg-red-100 text-red-700': log.type === 'error'
|
||
}">
|
||
{{ log.type }}
|
||
</span>
|
||
</td>
|
||
<td class="py-3 pr-2">{{ log.message }}</td>
|
||
</tr>
|
||
<tr v-if="systemLogs.length === 0">
|
||
<td colspan="3" class="py-8 text-center text-gray-400 text-xs">暂无日志记录</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||
<script>
|
||
const { createApp, ref, onMounted } = Vue;
|
||
|
||
createApp({
|
||
setup() {
|
||
const statusData = ref([]);
|
||
const config = ref(null);
|
||
const loading = ref(true);
|
||
const isAdmin = ref(false);
|
||
const checkingLogin = ref(true);
|
||
const showLoginModal = ref(false);
|
||
const passwordInput = ref('');
|
||
const sendingEmail = ref(false);
|
||
const criticalError = ref(null);
|
||
|
||
const initialized = ref(true);
|
||
const loadingCheckInit = ref(true);
|
||
const regionSearchFocus = ref(-1);
|
||
const refreshingMap = ref({});
|
||
const systemLogs = ref([]);
|
||
const cronWarning = ref(false);
|
||
|
||
// 图表相关
|
||
const showChartModal = ref(false);
|
||
const chartLoading = ref(false);
|
||
const chartMode = ref('30d');
|
||
const currentChartId = ref(0);
|
||
const currentChartAccount = ref('');
|
||
let chartInstance = null;
|
||
let currentChartData = {}; // 缓存当前请求的图表数据
|
||
|
||
// 阿里云地域数据 (...省略...)
|
||
const aliyunRegions = [
|
||
{ id: 'cn-hongkong', name: '中国香港' },
|
||
{ id: 'ap-southeast-1', name: '新加坡' },
|
||
{ id: 'ap-northeast-1', name: '日本 (东京)' },
|
||
{ id: 'us-west-1', name: '美国 (硅谷)' },
|
||
{ id: 'us-east-1', name: '美国 (弗吉尼亚)' },
|
||
{ id: 'eu-central-1', name: '德国 (法兰克福)' },
|
||
{ id: 'eu-west-1', name: '英国 (伦敦)' },
|
||
{ id: 'ap-southeast-2', name: '澳大利亚 (悉尼)' },
|
||
{ id: 'ap-southeast-3', name: '马来西亚 (吉隆坡)' },
|
||
{ id: 'ap-southeast-5', name: '印度尼西亚 (雅加达)' },
|
||
{ id: 'ap-southeast-6', name: '菲律宾 (马尼拉)' },
|
||
{ id: 'ap-southeast-7', name: '泰国 (曼谷)' },
|
||
{ id: 'ap-northeast-2', name: '韩国 (首尔)' },
|
||
{ id: 'me-east-1', name: '阿联酋 (迪拜)' },
|
||
{ id: 'cn-hangzhou', name: '华东1 (杭州)' },
|
||
{ id: 'cn-shanghai', name: '华东2 (上海)' },
|
||
{ id: 'cn-qingdao', name: '华北1 (青岛)' },
|
||
{ id: 'cn-beijing', name: '华北2 (北京)' },
|
||
{ id: 'cn-zhangjiakou', name: '华北3 (张家口)' },
|
||
{ id: 'cn-huhehaote', name: '华北5 (呼和浩特)' },
|
||
{ id: 'cn-wulanchabu', name: '华北6 (乌兰察布)' },
|
||
{ id: 'cn-shenzhen', name: '华南1 (深圳)' },
|
||
{ id: 'cn-heyuan', name: '华南2 (河源)' },
|
||
{ id: 'cn-guangzhou', name: '华南3 (广州)' },
|
||
{ id: 'cn-chengdu', name: '西南1 (成都)' }
|
||
];
|
||
|
||
const setupData = ref({
|
||
admin_password: '',
|
||
traffic_threshold: 95,
|
||
shutdown_mode: 'KeepCharging',
|
||
threshold_action: 'stop_and_notify',
|
||
keep_alive: false,
|
||
enable_schedule_email: false,
|
||
Notification: {
|
||
email: '', host: '', port: 465, secure: 'ssl', username: '', password: ''
|
||
},
|
||
Accounts: []
|
||
});
|
||
|
||
// ... (checkInitStatus, performSetup, checkLoginStatus remain same) ...
|
||
|
||
const checkInitStatus = async () => {
|
||
loadingCheckInit.value = true;
|
||
try {
|
||
const res = await fetch('index.php?action=check_init');
|
||
const data = await res.json();
|
||
|
||
if (data.error) {
|
||
criticalError.value = data.error;
|
||
loading.value = false;
|
||
return;
|
||
}
|
||
|
||
initialized.value = data.initialized;
|
||
|
||
if (data.initialized) {
|
||
checkLoginStatus();
|
||
} else {
|
||
loading.value = false;
|
||
}
|
||
} catch (e) {
|
||
console.error('Init check failed', e);
|
||
criticalError.value = "无法连接到服务器或响应格式错误。";
|
||
loading.value = false;
|
||
} finally {
|
||
loadingCheckInit.value = false;
|
||
}
|
||
};
|
||
|
||
const performSetup = async () => {
|
||
try {
|
||
const res = await fetch('index.php?action=setup', {
|
||
method: 'POST',
|
||
body: JSON.stringify(setupData.value)
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
alert('系统初始化成功!');
|
||
location.reload();
|
||
} else {
|
||
alert('初始化失败: ' + (data.message || '未知错误'));
|
||
}
|
||
} catch(e) {
|
||
alert('网络请求失败');
|
||
}
|
||
};
|
||
|
||
const checkLoginStatus = async () => {
|
||
checkingLogin.value = true;
|
||
try {
|
||
const res = await fetch('index.php?action=check_login');
|
||
const data = await res.json();
|
||
if (data.logged_in) {
|
||
isAdmin.value = true;
|
||
fetchConfig();
|
||
fetchLogs();
|
||
}
|
||
fetchData();
|
||
} catch (e) {
|
||
console.error('Session check failed', e);
|
||
} finally {
|
||
checkingLogin.value = false;
|
||
}
|
||
};
|
||
|
||
const fetchData = async () => {
|
||
// 首次加载才显示全局 loading
|
||
if (statusData.value.length === 0) loading.value = true;
|
||
try {
|
||
const res = await fetch('index.php?action=get_status');
|
||
const json = await res.json();
|
||
|
||
if (json.error) {
|
||
criticalError.value = json.error;
|
||
} else {
|
||
statusData.value = json.data || [];
|
||
|
||
// 检查心跳
|
||
if (json.system_last_run) {
|
||
const now = Math.floor(Date.now() / 1000);
|
||
const diff = now - json.system_last_run;
|
||
// 改为 180 秒 (3分钟)
|
||
cronWarning.value = diff > 180;
|
||
} else {
|
||
// 如果从来没运行过,也视为异常 (初始化阶段除外,这里简化判断)
|
||
// 如果有数据但没心跳记录,说明可能是旧版升级上来第一次运行
|
||
if (statusData.value.length > 0) cronWarning.value = true;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error(e);
|
||
statusData.value = [];
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
const refreshSingle = async (id, index) => {
|
||
refreshingMap.value[index] = true;
|
||
try {
|
||
const res = await fetch('index.php?action=refresh_account', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ id: id })
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
await fetchData();
|
||
} else {
|
||
console.error('Refresh failed');
|
||
}
|
||
} catch (e) {
|
||
console.error(e);
|
||
} finally {
|
||
refreshingMap.value[index] = false;
|
||
}
|
||
};
|
||
|
||
// 图表功能
|
||
const openChart = async (id, accountName) => {
|
||
showChartModal.value = true;
|
||
chartLoading.value = true;
|
||
currentChartId.value = id;
|
||
currentChartAccount.value = accountName;
|
||
chartMode.value = '30d'; // 默认显示30天
|
||
|
||
try {
|
||
const res = await fetch(`index.php?action=get_history&id=${id}`);
|
||
const json = await res.json();
|
||
currentChartData = json.data || {};
|
||
|
||
// 等待 DOM 渲染后初始化图表
|
||
setTimeout(() => {
|
||
initChart();
|
||
renderChart();
|
||
chartLoading.value = false;
|
||
}, 100);
|
||
|
||
} catch(e) {
|
||
console.error('Fetch history failed', e);
|
||
chartLoading.value = false;
|
||
}
|
||
};
|
||
|
||
const initChart = () => {
|
||
const dom = document.getElementById('echarts-container');
|
||
if (chartInstance) {
|
||
chartInstance.dispose();
|
||
}
|
||
chartInstance = echarts.init(dom);
|
||
window.addEventListener('resize', () => chartInstance && chartInstance.resize());
|
||
};
|
||
|
||
const switchChartMode = (mode) => {
|
||
chartMode.value = mode;
|
||
renderChart();
|
||
};
|
||
|
||
const renderChart = () => {
|
||
if (!chartInstance) return;
|
||
|
||
let xData = [];
|
||
let sData = [];
|
||
let is30d = chartMode.value === '30d';
|
||
|
||
if (is30d && currentChartData.history_30d) {
|
||
xData = currentChartData.history_30d.map(i => i.date);
|
||
sData = currentChartData.history_30d.map(i => i.value);
|
||
} else if (!is30d && currentChartData.history_24h) {
|
||
xData = currentChartData.history_24h.map(i => i.time);
|
||
sData = currentChartData.history_24h.map(i => i.value);
|
||
}
|
||
|
||
// ECharts 配置
|
||
const option = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
formatter: '{b} <br/> 流量: {c} GB'
|
||
},
|
||
grid: {
|
||
left: '3%', right: '4%', bottom: '3%', top: '10%',
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
boundaryGap: is30d, // 30天用柱状图,留间隙;24h用折线图,不留
|
||
data: xData,
|
||
axisLine: { lineStyle: { color: '#ccc' } },
|
||
axisLabel: { color: '#666' }
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
name: 'GB',
|
||
splitLine: { lineStyle: { type: 'dashed', color: '#eee' } }
|
||
},
|
||
series: [{
|
||
data: sData,
|
||
type: is30d ? 'bar' : 'line',
|
||
smooth: true,
|
||
itemStyle: { color: is30d ? '#1C1C1E' : '#007AFF' },
|
||
areaStyle: is30d ? null : {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: 'rgba(0,122,255,0.3)' },
|
||
{ offset: 1, color: 'rgba(0,122,255,0.01)' }
|
||
])
|
||
},
|
||
showSymbol: false
|
||
}]
|
||
};
|
||
|
||
// 如果是30天,增加折线趋势辅助
|
||
if (is30d) {
|
||
option.series.push({
|
||
data: sData,
|
||
type: 'line',
|
||
smooth: true,
|
||
lineStyle: { color: '#FF3B30', width: 2, type: 'dashed' },
|
||
itemStyle: { opacity: 0 }, // 隐藏点
|
||
tooltip: { show: false } // 不重复显示tooltip
|
||
});
|
||
}
|
||
|
||
chartInstance.setOption(option);
|
||
};
|
||
|
||
const closeChart = () => {
|
||
showChartModal.value = false;
|
||
if(chartInstance) {
|
||
chartInstance.dispose();
|
||
chartInstance = null;
|
||
}
|
||
};
|
||
|
||
const toggleAdmin = () => {
|
||
if (isAdmin.value) {
|
||
fetch('index.php?action=logout').then(() => {
|
||
isAdmin.value = false;
|
||
config.value = null;
|
||
systemLogs.value = [];
|
||
});
|
||
} else {
|
||
showLoginModal.value = true;
|
||
}
|
||
};
|
||
|
||
const performLogin = async () => {
|
||
try {
|
||
const res = await fetch('index.php?action=login', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ password: passwordInput.value })
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
isAdmin.value = true;
|
||
showLoginModal.value = false;
|
||
passwordInput.value = '';
|
||
fetchConfig();
|
||
fetchLogs();
|
||
} else {
|
||
// 修复:显示后端返回的具体错误信息,而不是硬编码的“密码错误”
|
||
alert(data.message || '登录失败');
|
||
}
|
||
} catch(e) {
|
||
alert('登录请求失败');
|
||
}
|
||
};
|
||
|
||
const fetchConfig = async () => {
|
||
try {
|
||
const res = await fetch('index.php?action=get_config');
|
||
const data = await res.json();
|
||
if (!data.Accounts) data.Accounts = [];
|
||
data.Accounts.forEach(acc => {
|
||
if(!acc.schedule) acc.schedule = { enabled: false, startTime: '', stopTime: '' };
|
||
if(typeof acc.maxTraffic === 'undefined') acc.maxTraffic = 200;
|
||
});
|
||
if (!data.Notification) data.Notification = { email: '', host: '', port: 465, secure: 'ssl', username: '', password: '' };
|
||
config.value = data;
|
||
} catch(e) { console.error(e); }
|
||
};
|
||
|
||
const saveConfig = async () => {
|
||
if (!config.value) return;
|
||
try {
|
||
const res = await fetch('index.php?action=save_config', { method: 'POST', body: JSON.stringify(config.value) });
|
||
const data = await res.json();
|
||
if (data.success) { alert('配置已保存'); fetchData(); } else { alert('保存失败'); }
|
||
} catch(e) { alert('保存请求失败'); }
|
||
};
|
||
|
||
// 新增:获取系统日志
|
||
const fetchLogs = async () => {
|
||
if (!isAdmin.value) return;
|
||
try {
|
||
const res = await fetch('index.php?action=get_logs');
|
||
const data = await res.json();
|
||
if (data.data) {
|
||
systemLogs.value = data.data;
|
||
}
|
||
} catch (e) { console.error('Fetch logs failed', e); }
|
||
};
|
||
|
||
const sendTestEmail = async () => {
|
||
if (!config.value || !config.value.Notification.email) { alert('请先填写接收邮箱并保存配置'); return; }
|
||
if(!confirm('发送测试邮件将使用已保存的配置。继续发送吗?')) { return; }
|
||
sendingEmail.value = true;
|
||
try {
|
||
const res = await fetch('index.php?action=send_test_email', { method: 'POST', body: JSON.stringify({ email: config.value.Notification.email }) });
|
||
const data = await res.json();
|
||
if (data.success) { alert('邮件发送成功!'); } else { alert('邮件发送失败'); }
|
||
} catch(e) { alert('发送请求失败'); } finally { sendingEmail.value = false; }
|
||
};
|
||
|
||
const addAccount = () => {
|
||
if(!config.value.Accounts) config.value.Accounts = [];
|
||
config.value.Accounts.push({ AccessKeyId: '', AccessKeySecret: '', maxTraffic: 200, regionId: '', instanceId: '', schedule: { enabled: false, startTime: '', stopTime: '' } });
|
||
};
|
||
|
||
const removeAccount = (index) => { if(confirm('确定删除该账号配置?')) config.value.Accounts.splice(index, 1); };
|
||
|
||
const getInstanceStatusColor = (status) => {
|
||
const map = { 'Running': 'text-green-500', 'Stopped': 'text-red-500', 'Starting': 'text-yellow-500', 'Stopping': 'text-yellow-500' };
|
||
return map[status] || 'text-gray-400';
|
||
};
|
||
|
||
onMounted(() => {
|
||
checkInitStatus();
|
||
});
|
||
|
||
return {
|
||
statusData, loading, isAdmin, checkingLogin, showLoginModal, passwordInput, config, initialized, loadingCheckInit, setupData, criticalError,
|
||
regionSearchFocus, aliyunRegions, refreshingMap, systemLogs, cronWarning,
|
||
showChartModal, chartLoading, chartMode, currentChartAccount, openChart, closeChart, switchChartMode,
|
||
toggleAdmin, performLogin, saveConfig, sendTestEmail, sendingEmail, addAccount, removeAccount, getInstanceStatusColor, performSetup, refreshSingle, fetchLogs
|
||
};
|
||
}
|
||
}).mount('#app');
|
||
</script>
|
||
</body>
|
||
</html> |