feat(ui): 总览/监控中心顶部统计卡片支持点击筛选
- 总览页「总资产」「本年支出」「异常状态」卡片点击跳转资产页并带上对应筛选 (全部/付费资产TAB/异常状态组合筛选);「30天内到期」及「另有N项已过期」 子文案点击跳转监控中心并带上到期区间子筛选 - 监控中心顶部统计卡片(30天/7天/已过期/有到期日)支持点击筛选下方列表, 再点一次取消;SSL 证书卡片(正常/即将到期/已过期或失败)同理 - 资产页状态筛选新增「异常(已停止/已过期)」组合选项:后端 status 只认单值, 该组合作为前端本地筛选哨兵值处理,不发给 API;切到专有视图 TAB 时自动清空 - 顺带修复监控中心统计卡片数字不随类别 TAB 变化的问题(原先 stats 未按 TAB 过滤,与列表数据口径不一致)
This commit is contained in:
+55
-30
@@ -10,44 +10,44 @@ const MonitorView = {
|
||||
<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">
|
||||
<button type="button" @click="toggleExpiryFilter('soon30')" title="点击只看 30 天内到期的资产" class="text-left bg-white dark:bg-slate-900 rounded-xl border p-4 transition" :class="store.monitorExpiryFilter==='soon30' ? 'border-red-500 ring-1 ring-red-500' : 'border-slate-200 dark:border-slate-800 hover:border-red-300 dark:hover:border-red-800'">
|
||||
<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">
|
||||
</button>
|
||||
<button type="button" @click="toggleExpiryFilter('soon7')" title="点击只看 7 天内到期的资产" class="text-left bg-white dark:bg-slate-900 rounded-xl border p-4 transition" :class="store.monitorExpiryFilter==='soon7' ? 'border-amber-500 ring-1 ring-amber-500' : 'border-slate-200 dark:border-slate-800 hover:border-amber-300 dark:hover:border-amber-800'">
|
||||
<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">
|
||||
</button>
|
||||
<button type="button" @click="toggleExpiryFilter('expired')" title="点击只看已过期的资产" class="text-left bg-white dark:bg-slate-900 rounded-xl border p-4 transition" :class="store.monitorExpiryFilter==='expired' ? 'border-slate-500 ring-1 ring-slate-500' : 'border-slate-200 dark:border-slate-800 hover:border-slate-400 dark:hover:border-slate-600'">
|
||||
<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">
|
||||
</button>
|
||||
<button type="button" @click="clearExpiryFilter" title="点击显示当前类别下所有有到期日的资产" class="text-left bg-white dark:bg-slate-900 rounded-xl border p-4 transition" :class="store.monitorExpiryFilter==='' ? 'border-blue-500 ring-1 ring-blue-500' : 'border-slate-200 dark:border-slate-800 hover:border-blue-300 dark:hover:border-blue-800'">
|
||||
<div class="text-xs text-slate-500">有到期日的资产</div>
|
||||
<div class="text-2xl font-bold mt-1">{{ total }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 顶部统计(SSL 证书) -->
|
||||
<!-- 顶部统计(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">
|
||||
<button type="button" @click="clearSslStatusFilter" title="点击显示全部监控站点" class="text-left bg-white dark:bg-slate-900 rounded-xl border p-4 transition" :class="store.monitorSslStatusFilter==='' ? 'border-blue-500 ring-1 ring-blue-500' : 'border-slate-200 dark:border-slate-800 hover:border-blue-300 dark:hover:border-blue-800'">
|
||||
<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">
|
||||
</button>
|
||||
<button type="button" @click="toggleSslStatusFilter('valid')" title="点击只看证书正常的站点" class="text-left bg-white dark:bg-slate-900 rounded-xl border p-4 transition" :class="store.monitorSslStatusFilter==='valid' ? 'border-emerald-500 ring-1 ring-emerald-500' : 'border-slate-200 dark:border-slate-800 hover:border-emerald-300 dark:hover:border-emerald-800'">
|
||||
<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">
|
||||
</button>
|
||||
<button type="button" @click="toggleSslStatusFilter('expiring')" title="点击只看即将到期的证书" class="text-left bg-white dark:bg-slate-900 rounded-xl border p-4 transition" :class="store.monitorSslStatusFilter==='expiring' ? 'border-amber-500 ring-1 ring-amber-500' : 'border-slate-200 dark:border-slate-800 hover:border-amber-300 dark:hover:border-amber-800'">
|
||||
<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">
|
||||
</button>
|
||||
<button type="button" @click="toggleSslStatusFilter('expired_error')" title="点击只看已过期或探测失败的证书" class="text-left bg-white dark:bg-slate-900 rounded-xl border p-4 transition" :class="store.monitorSslStatusFilter==='expired_error' ? 'border-red-500 ring-1 ring-red-500' : 'border-slate-200 dark:border-slate-800 hover:border-red-300 dark:hover:border-red-800'">
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 类别 TAB -->
|
||||
@@ -135,7 +135,7 @@ const MonitorView = {
|
||||
</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-if="!sslList.length"><td colspan="7" class="px-4 py-10 text-center text-slate-400">{{ sslEmptyText }}</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>
|
||||
@@ -156,7 +156,7 @@ const MonitorView = {
|
||||
</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-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">{{ sslEmptyText }}</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">
|
||||
@@ -188,18 +188,32 @@ const MonitorView = {
|
||||
{ key: 'ssl', label: 'SSL证书', tip: '站点 HTTPS 证书到期倒计时(独立于资产)' },
|
||||
];
|
||||
const hasExpiry = a => a.expiry_date && a.status !== 'cancelled';
|
||||
const items = Vue.computed(() => {
|
||||
// 当前 TAB 下、有到期日的资产(未叠加到期区间子筛选),作为统计卡片与列表的共同基础数据集
|
||||
const tabScopedAssets = 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));
|
||||
return list;
|
||||
});
|
||||
const items = Vue.computed(() => {
|
||||
let list = tabScopedAssets.value;
|
||||
const ef = store.monitorExpiryFilter;
|
||||
if (ef === 'soon30') list = list.filter(a => a.days_to_expiry >= 0 && a.days_to_expiry <= 30);
|
||||
else if (ef === 'soon7') list = list.filter(a => a.days_to_expiry >= 0 && a.days_to_expiry <= 7);
|
||||
else if (ef === 'expired') list = list.filter(a => a.days_to_expiry < 0);
|
||||
return [...list].sort((x, y) => (x.days_to_expiry ?? 9999) - (y.days_to_expiry ?? 9999));
|
||||
});
|
||||
// SSL 证书:按剩余天数倒计时排序,可叠加状态子筛选
|
||||
const sslList = Vue.computed(() => {
|
||||
let list = store.siteCerts;
|
||||
const f = store.monitorSslStatusFilter;
|
||||
if (f === 'valid') list = list.filter(c => c.status === 'valid');
|
||||
else if (f === 'expiring') list = list.filter(c => c.status === 'expiring');
|
||||
else if (f === 'expired_error') list = list.filter(c => c.status === 'expired' || c.status === 'error');
|
||||
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]++;
|
||||
@@ -214,10 +228,10 @@ const MonitorView = {
|
||||
try { await Api.del('/site-certs/' + c.id); await loadSiteCerts(); }
|
||||
catch (e) { alert('删除失败:' + e.message); }
|
||||
}
|
||||
const total = Vue.computed(() => items.value.length);
|
||||
const total = Vue.computed(() => tabScopedAssets.value.length);
|
||||
const stats = Vue.computed(() => {
|
||||
const s = { soon30: 0, soon7: 0, expired: 0 };
|
||||
for (const a of store.assets.filter(hasExpiry)) {
|
||||
for (const a of tabScopedAssets.value) {
|
||||
const d = a.days_to_expiry;
|
||||
if (d === null || d === undefined) continue;
|
||||
if (d < 0) s.expired++;
|
||||
@@ -226,8 +240,19 @@ const MonitorView = {
|
||||
}
|
||||
return s;
|
||||
});
|
||||
const emptyText = Vue.computed(() => store.monitorTab ? '该类别暂无到期资产' : '暂无到期资产(资产设置到期日即可出现在这里)');
|
||||
const emptyText = Vue.computed(() => {
|
||||
if (store.monitorExpiryFilter) return '当前到期区间筛选下暂无资产,点上方已选中的卡片可取消筛选';
|
||||
return store.monitorTab ? '该类别暂无到期资产' : '暂无到期资产(资产设置到期日即可出现在这里)';
|
||||
});
|
||||
const sslEmptyText = Vue.computed(() => {
|
||||
if (store.monitorSslStatusFilter) return '当前状态筛选下暂无证书,点上方已选中的卡片可取消筛选';
|
||||
return '暂无证书监控(在「资产 → 域名」的子域名面板点「探测SSL」,或稍后支持直接添加)';
|
||||
});
|
||||
function switchTab(k) { store.monitorTab = k; }
|
||||
function toggleExpiryFilter(k) { store.monitorExpiryFilter = store.monitorExpiryFilter === k ? '' : k; }
|
||||
function clearExpiryFilter() { store.monitorExpiryFilter = ''; }
|
||||
function toggleSslStatusFilter(k) { store.monitorSslStatusFilter = store.monitorSslStatusFilter === k ? '' : k; }
|
||||
function clearSslStatusFilter() { store.monitorSslStatusFilter = ''; }
|
||||
function countdownText(d) {
|
||||
if (d === null || d === undefined) return '—';
|
||||
if (d < 0) return '已过期 ' + Math.abs(d) + ' 天';
|
||||
@@ -241,6 +266,6 @@ const MonitorView = {
|
||||
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 };
|
||||
return { store, Fmt, tabs, items, total, stats, sslList, sslStats, emptyText, sslEmptyText, switchTab, toggleExpiryFilter, clearExpiryFilter, toggleSslStatusFilter, clearSslStatusFilter, countdownText, countdownBadge, recheck, delCert, edit: openAssetEdit, del: deleteAsset };
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user