Files
vps-manager/static/js/views/monitor.js
T

247 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 监控中心视图:默认到期倒计时,可按类别筛选 */
const MonitorView = {
template: `
<div class="space-y-4">
<!-- 说明条:让用户明白这个页面是干嘛的 -->
<div class="text-xs text-slate-500 bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-800 rounded-lg px-3 py-2 flex flex-wrap items-center gap-x-3 gap-y-1">
<span>⏳ 所有设置了到期日的资产按剩余天数倒计时排序,越靠前越紧急</span>
<span class="text-xs px-2 py-0.5 rounded-full bg-red-500/10 text-red-600 dark:text-red-400">红=7天内</span>
<span class="text-xs px-2 py-0.5 rounded-full bg-amber-500/10 text-amber-600 dark:text-amber-400">橙=30天内</span>
<span class="text-xs px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">绿=90天内</span>
</div>
<!-- 顶部统计(资产) -->
<div v-if="store.monitorTab!=='ssl'" class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4">
<div class="text-xs text-slate-500">30 天内到期</div>
<div class="text-2xl font-bold mt-1 text-red-600 dark:text-red-400">{{ stats.soon30 }}</div>
</div>
<div class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4">
<div class="text-xs text-slate-500">7 天内到期</div>
<div class="text-2xl font-bold mt-1 text-amber-600 dark:text-amber-400">{{ stats.soon7 }}</div>
</div>
<div class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4">
<div class="text-xs text-slate-500">已过期</div>
<div class="text-2xl font-bold mt-1 text-slate-700 dark:text-slate-200">{{ stats.expired }}</div>
</div>
<div class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4">
<div class="text-xs text-slate-500">有到期日的资产</div>
<div class="text-2xl font-bold mt-1">{{ total }}</div>
</div>
</div>
<!-- 顶部统计(SSL 证书) -->
<div v-if="store.monitorTab==='ssl'" class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4">
<div class="text-xs text-slate-500">监控站点</div>
<div class="text-2xl font-bold mt-1">{{ sslStats.total }}</div>
</div>
<div class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4">
<div class="text-xs text-slate-500">证书正常</div>
<div class="text-2xl font-bold mt-1 text-emerald-600 dark:text-emerald-400">{{ sslStats.valid }}</div>
</div>
<div class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4">
<div class="text-xs text-slate-500">即将到期</div>
<div class="text-2xl font-bold mt-1 text-amber-600 dark:text-amber-400">{{ sslStats.expiring }}</div>
</div>
<div class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4">
<div class="text-xs text-slate-500">已过期 / 失败</div>
<div class="text-2xl font-bold mt-1 text-red-600 dark:text-red-400">{{ sslStats.expired + sslStats.error }}</div>
</div>
</div>
<!-- 类别 TAB -->
<div class="flex flex-wrap gap-1.5">
<button v-for="t in tabs" :key="t.key" @click="switchTab(t.key)" :title="t.tip"
class="text-sm px-3 py-1.5 rounded-full transition"
:class="store.monitorTab===t.key ? 'bg-blue-600 text-white' : 'bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800'">
{{ t.label }}
</button>
</div>
<!-- 资产表格(非 SSL TAB -->
<template v-if="store.monitorTab!=='ssl'">
<!-- 桌面表格 -->
<div class="hidden md:block bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-slate-50 dark:bg-slate-800/50 text-slate-500 text-left text-xs">
<tr>
<th class="px-4 py-2.5 font-medium">名称</th>
<th class="px-4 py-2.5 font-medium">类型</th>
<th class="px-4 py-2.5 font-medium">平台</th>
<th class="px-4 py-2.5 font-medium">到期日</th>
<th class="px-4 py-2.5 font-medium">倒计时</th>
<th class="px-4 py-2.5 font-medium">费用</th>
<th class="px-4 py-2.5 font-medium text-right">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-800">
<tr v-if="!items.length"><td colspan="7" class="px-4 py-10 text-center text-slate-400">{{ emptyText }}</td></tr>
<tr v-for="a in items" :key="a.id" class="hover:bg-slate-50 dark:hover:bg-slate-800/30">
<td class="px-4 py-2.5 font-medium">{{ a.name }}</td>
<td class="px-4 py-2.5"><span class="text-xs px-2 py-0.5 rounded-full" :class="Fmt.typeBadge(a.asset_type)">{{ Fmt.TYPE_LABELS[a.asset_type] }}</span></td>
<td class="px-4 py-2.5 text-slate-500">{{ a.provider_name || a.provider }}</td>
<td class="px-4 py-2.5">{{ a.expiry_date }}</td>
<td class="px-4 py-2.5"><span class="text-xs font-semibold px-2 py-0.5 rounded-full" :class="countdownBadge(a.days_to_expiry)">{{ countdownText(a.days_to_expiry) }}</span></td>
<td class="px-4 py-2.5 text-slate-600 dark:text-slate-300">{{ a.cost }} {{ a.currency }}<span v-if="a.renewal_cycle" class="text-xs text-slate-400 ml-1">/{{ Fmt.CYCLE_LABELS[a.renewal_cycle] || a.renewal_cycle }}</span></td>
<td class="px-4 py-2.5 text-right whitespace-nowrap">
<a v-if="a.renew_url" :href="a.renew_url" target="_blank" class="text-emerald-600 dark:text-emerald-400 hover:underline mr-3 text-xs">续费</a>
<button @click="edit(a)" class="text-blue-600 dark:text-blue-400 hover:underline mr-3 text-xs">编辑</button>
<button @click="del(a)" class="text-red-600 dark:text-red-400 hover:underline text-xs">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 移动卡片 -->
<div class="md:hidden space-y-2">
<div v-if="!items.length" class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-8 text-center text-slate-400 text-sm">{{ emptyText }}</div>
<div v-for="a in items" :key="a.id" class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-3">
<div class="flex justify-between items-start">
<div class="min-w-0">
<div class="font-medium text-sm truncate">{{ a.name }}</div>
<div class="text-xs text-slate-400 mt-0.5">{{ a.provider_name || a.provider }}</div>
</div>
<span class="text-xs px-2 py-0.5 rounded-full shrink-0" :class="countdownBadge(a.days_to_expiry)">{{ countdownText(a.days_to_expiry) }}</span>
</div>
<div class="flex items-center gap-3 mt-2 text-xs">
<span class="text-slate-500">{{ a.expiry_date }}</span>
<span class="text-slate-500">{{ a.cost }}{{ a.currency }}</span>
<span class="text-xs px-2 py-0.5 rounded-full" :class="Fmt.typeBadge(a.asset_type)">{{ Fmt.TYPE_LABELS[a.asset_type] }}</span>
</div>
<div class="flex gap-4 mt-2 pt-2 border-t border-slate-100 dark:border-slate-800 text-xs">
<a v-if="a.renew_url" :href="a.renew_url" target="_blank" class="text-emerald-600 dark:text-emerald-400">续费</a>
<button @click="edit(a)" class="text-blue-600 dark:text-blue-400">编辑</button>
<button @click="del(a)" class="text-red-600 dark:text-red-400">删除</button>
</div>
</div>
</div>
</template>
<!-- SSL 证书表格 -->
<template v-if="store.monitorTab==='ssl'">
<div class="hidden md:block bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-slate-50 dark:bg-slate-800/50 text-slate-500 text-left text-xs">
<tr>
<th class="px-4 py-2.5 font-medium">站点</th>
<th class="px-4 py-2.5 font-medium">所属资产</th>
<th class="px-4 py-2.5 font-medium">签发机构</th>
<th class="px-4 py-2.5 font-medium">到期日</th>
<th class="px-4 py-2.5 font-medium">倒计时</th>
<th class="px-4 py-2.5 font-medium">状态</th>
<th class="px-4 py-2.5 font-medium text-right">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-800">
<tr v-if="!sslList.length"><td colspan="7" class="px-4 py-10 text-center text-slate-400">暂无证书监控(在「资产 → 域名」的子域名面板点「探测SSL」,或稍后支持直接添加)</td></tr>
<tr v-for="c in sslList" :key="c.id" class="hover:bg-slate-50 dark:hover:bg-slate-800/30">
<td class="px-4 py-2.5 font-mono font-medium">{{ c.hostname }}<span v-if="c.port!==443" class="text-xs text-slate-400">:{{ c.port }}</span></td>
<td class="px-4 py-2.5 text-slate-500">{{ c.asset_name || '—' }}</td>
<td class="px-4 py-2.5 text-slate-500 max-w-[220px] truncate" :title="c.issuer || ''">{{ c.issuer || '—' }}</td>
<td class="px-4 py-2.5">{{ c.valid_to || '—' }}</td>
<td class="px-4 py-2.5"><span class="text-xs font-semibold px-2 py-0.5 rounded-full" :class="countdownBadge(c.days_to_expiry)">{{ countdownText(c.days_to_expiry) }}</span></td>
<td class="px-4 py-2.5">
<span v-if="c.status==='error'" class="text-xs text-red-600 dark:text-red-400" :title="c.error || ''">探测失败</span>
<span v-else class="text-xs text-slate-500" :title="c.error || ''">{{ c.last_checked_at ? '已检查 ' + c.last_checked_at.slice(5,16).replace('T',' ') : '未检查' }}</span>
</td>
<td class="px-4 py-2.5 text-right whitespace-nowrap">
<button @click="recheck(c)" class="text-emerald-600 dark:text-emerald-400 hover:underline mr-3 text-xs">重测</button>
<button @click="delCert(c)" class="text-red-600 dark:text-red-400 hover:underline text-xs">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- SSL 移动卡片 -->
<div class="md:hidden space-y-2">
<div v-if="!sslList.length" class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-8 text-center text-slate-400 text-sm">暂无证书监控</div>
<div v-for="c in sslList" :key="c.id" class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-3">
<div class="flex justify-between items-start">
<div class="min-w-0">
<div class="font-mono font-medium text-sm truncate">{{ c.hostname }}</div>
<div class="text-xs text-slate-400 mt-0.5">{{ c.asset_name || '独立站点' }}</div>
</div>
<span class="text-xs px-2 py-0.5 rounded-full shrink-0" :class="countdownBadge(c.days_to_expiry)">{{ countdownText(c.days_to_expiry) }}</span>
</div>
<div class="flex items-center gap-3 mt-2 text-xs text-slate-500">
<span>{{ c.valid_to || '—' }}</span>
<span v-if="c.status==='error'" class="text-red-600 dark:text-red-400" :title="c.error || ''">探测失败</span>
</div>
<div class="flex gap-4 mt-2 pt-2 border-t border-slate-100 dark:border-slate-800 text-xs">
<button @click="recheck(c)" class="text-emerald-600 dark:text-emerald-400">重测</button>
<button @click="delCert(c)" class="text-red-600 dark:text-red-400">删除</button>
</div>
</div>
</div>
</template>
</div>`,
setup() {
const tabs = [
{ key: '', label: '全部', tip: '所有有到期日的资产' },
{ key: 'vps', label: 'VPS', tip: '仅看云服务器' },
{ key: 'domain', label: '域名', tip: '仅看域名' },
{ key: 'cloudflare', label: 'Cloudflare', tip: '仅看 Cloudflare 资源' },
{ key: 'subscription', label: '付费资产', tip: '仅看有费用、需要续费的项目' },
{ key: 'ai_agent', label: 'AI账号', tip: '仅看 AI 服务账号' },
{ key: 'ssl', label: 'SSL证书', tip: '站点 HTTPS 证书到期倒计时(独立于资产)' },
];
const hasExpiry = a => a.expiry_date && a.status !== 'cancelled';
const items = Vue.computed(() => {
if (store.monitorTab === 'ssl') return [];
let list = store.assets.filter(hasExpiry);
const t = store.monitorTab;
if (t === 'subscription') list = list.filter(a => a.cost > 0);
else if (t) list = list.filter(a => a.asset_type === t);
return list.sort((x, y) => (x.days_to_expiry ?? 9999) - (y.days_to_expiry ?? 9999));
});
// SSL 证书:按剩余天数倒计时排序
const sslList = Vue.computed(() =>
[...store.siteCerts].sort((x, y) => (x.days_to_expiry ?? 9999) - (y.days_to_expiry ?? 9999))
);
const sslStats = Vue.computed(() => {
const s = { total: store.siteCerts.length, valid: 0, expiring: 0, expired: 0, error: 0 };
for (const c of store.siteCerts) if (s[c.status] !== undefined) s[c.status]++;
return s;
});
async function recheck(c) {
try { await Api.post('/site-certs/' + c.id + '/check', {}); await loadSiteCerts(); }
catch (e) { alert('重测失败:' + e.message); }
}
async function delCert(c) {
if (!confirm('删除证书监控「' + c.hostname + '」?')) return;
try { await Api.del('/site-certs/' + c.id); await loadSiteCerts(); }
catch (e) { alert('删除失败:' + e.message); }
}
const total = Vue.computed(() => items.value.length);
const stats = Vue.computed(() => {
const s = { soon30: 0, soon7: 0, expired: 0 };
for (const a of store.assets.filter(hasExpiry)) {
const d = a.days_to_expiry;
if (d === null || d === undefined) continue;
if (d < 0) s.expired++;
else if (d <= 7) { s.soon7++; s.soon30++; }
else if (d <= 30) s.soon30++;
}
return s;
});
const emptyText = Vue.computed(() => store.monitorTab ? '该类别暂无到期资产' : '暂无到期资产(资产设置到期日即可出现在这里)');
function switchTab(k) { store.monitorTab = k; }
function countdownText(d) {
if (d === null || d === undefined) return '—';
if (d < 0) return '已过期 ' + Math.abs(d) + ' 天';
if (d === 0) return '今天到期';
return d + ' 天';
}
function countdownBadge(d) {
if (d === null || d === undefined) return 'bg-slate-500/10 text-slate-500';
if (d < 0 || d === 0 || d <= 7) return 'bg-red-500/10 text-red-600 dark:text-red-400';
if (d <= 30) return 'bg-amber-500/10 text-amber-600 dark:text-amber-400';
if (d <= 90) return 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400';
return 'bg-slate-500/10 text-slate-500';
}
return { store, Fmt, tabs, items, total, stats, sslList, sslStats, emptyText, switchTab, countdownText, countdownBadge, recheck, delCert, edit: openAssetEdit, del: deleteAsset };
},
};