feat(ui): 总览/监控中心顶部统计卡片支持点击筛选

- 总览页「总资产」「本年支出」「异常状态」卡片点击跳转资产页并带上对应筛选
  (全部/付费资产TAB/异常状态组合筛选);「30天内到期」及「另有N项已过期」
  子文案点击跳转监控中心并带上到期区间子筛选
- 监控中心顶部统计卡片(30天/7天/已过期/有到期日)支持点击筛选下方列表,
  再点一次取消;SSL 证书卡片(正常/即将到期/已过期或失败)同理
- 资产页状态筛选新增「异常(已停止/已过期)」组合选项:后端 status 只认单值,
  该组合作为前端本地筛选哨兵值处理,不发给 API;切到专有视图 TAB 时自动清空
- 顺带修复监控中心统计卡片数字不随类别 TAB 变化的问题(原先 stats 未按 TAB
  过滤,与列表数据口径不一致)
This commit is contained in:
gouki
2026-09-20 12:53:48 +00:00
parent cbb32ea3b0
commit 7045843db8
4 changed files with 114 additions and 47 deletions
+27 -1
View File
@@ -39,6 +39,8 @@ const store = reactive({
filterProvider: '', // 按平台筛选(slug),从平台页「关联 N 个资产」跳转而来 filterProvider: '', // 按平台筛选(slug),从平台页「关联 N 个资产」跳转而来
assetTab: '', // 资产页 TAB:'' 全部 / vps / domain / cloudflare / subscription(付费资产) / ai_agent assetTab: '', // 资产页 TAB:'' 全部 / vps / domain / cloudflare / subscription(付费资产) / ai_agent
monitorTab: '', // 监控中心 TAB:同上 monitorTab: '', // 监控中心 TAB:同上
monitorExpiryFilter: '', // 监控中心到期区间子筛选:'' 全部 / soon30 / soon7 / expired
monitorSslStatusFilter: '', // 监控中心 SSL 状态子筛选:'' 全部 / valid / expiring / expired_error
dark: localStorage.getItem('vps_dark') === '1', dark: localStorage.getItem('vps_dark') === '1',
loading: false, loading: false,
error: '', error: '',
@@ -101,10 +103,13 @@ function buildAssetPayload(f) {
} }
/* ---------------- 数据加载 ---------------- */ /* ---------------- 数据加载 ---------------- */
// 后端 status 参数只认单个 AssetStatus 枚举值;「异常」是「已停止 + 已过期」的组合概念,
// 后端不支持多值筛选,这里当作前端本地筛选的哨兵值,不发给 API,由视图层自己过滤。
const CLIENT_SIDE_STATUS_FILTERS = new Set(['abnormal']);
async function loadAssets() { async function loadAssets() {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (store.filterType) params.set('asset_type', store.filterType); if (store.filterType) params.set('asset_type', store.filterType);
if (store.filterStatus) params.set('status', store.filterStatus); if (store.filterStatus && !CLIENT_SIDE_STATUS_FILTERS.has(store.filterStatus)) params.set('status', store.filterStatus);
if (store.filterProvider) params.set('provider', store.filterProvider); if (store.filterProvider) params.set('provider', store.filterProvider);
if (store.search) params.set('q', store.search); if (store.search) params.set('q', store.search);
params.set('sort', 'expiry_date'); params.set('order', 'asc'); params.set('sort', 'expiry_date'); params.set('order', 'asc');
@@ -449,6 +454,27 @@ function clearProviderFilter() {
loadAssets(); loadAssets();
} }
/* ---------------- 总览页卡片跳转:带预设筛选条件切到资产页 ---------------- */
// 未显式传入的筛选项一律重置,避免带着上一页的旧筛选条件跳过去后数字对不上
function openAssetsWithFilter({ assetTab = '', filterStatus = '', filterType = '', filterProvider = '', search = '' } = {}) {
store.assetTab = assetTab;
store.filterStatus = filterStatus;
store.filterType = filterType;
store.filterProvider = filterProvider;
store.search = search;
if (window.location.hash !== '#assets') window.location.hash = '#assets';
else store.view = 'assets';
loadAssets();
}
/* ---------------- 总览页卡片跳转:带预设到期区间筛选切到监控中心 ---------------- */
function openMonitorWithExpiryFilter(filter) {
store.monitorTab = '';
store.monitorExpiryFilter = filter || '';
if (window.location.hash !== '#monitor') window.location.hash = '#monitor';
else store.view = 'monitor';
}
/* ---------------- 服务器监控 ---------------- */ /* ---------------- 服务器监控 ---------------- */
async function viewServer(a) { async function viewServer(a) {
try { try {
+13 -5
View File
@@ -27,6 +27,7 @@ const AssetsView = {
<div class="flex flex-wrap gap-2 items-center"> <div class="flex flex-wrap gap-2 items-center">
<select v-model="store.filterStatus" @change="reload" class="text-sm px-3 py-1.5 rounded-lg border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900"> <select v-model="store.filterStatus" @change="reload" class="text-sm px-3 py-1.5 rounded-lg border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900">
<option value="">全部状态</option> <option value="">全部状态</option>
<option value="abnormal">异常(已停止/已过期)</option>
<option v-for="(l,k) in Fmt.STATUS_LABELS" :key="k" :value="k">{{ l }}</option> <option v-for="(l,k) in Fmt.STATUS_LABELS" :key="k" :value="k">{{ l }}</option>
</select> </select>
<button v-if="store.assetTab===''" @click="syncAllAI" class="text-sm px-3 py-1.5 rounded-lg border border-violet-300 dark:border-violet-700 text-violet-600 dark:text-violet-400 hover:bg-violet-50 dark:hover:bg-violet-900/20">同步AI余额</button> <button v-if="store.assetTab===''" @click="syncAllAI" class="text-sm px-3 py-1.5 rounded-lg border border-violet-300 dark:border-violet-700 text-violet-600 dark:text-violet-400 hover:bg-violet-50 dark:hover:bg-violet-900/20">同步AI余额</button>
@@ -114,15 +115,22 @@ const AssetsView = {
if (t === 'ai_agent') return AIAccountsView; if (t === 'ai_agent') return AIAccountsView;
return null; return null;
}); });
// 通用列表:全部显示所有,VPS 前端过滤类型 // 通用列表:全部显示所有,VPS 前端过滤类型;「异常」状态是后端不支持的组合筛选,也走前端过滤
const list = Vue.computed(() => { const list = Vue.computed(() => {
if (store.assetTab === 'vps') return store.assets.filter(a => a.asset_type === 'vps'); let arr = store.assets;
return store.assets; if (store.assetTab === 'vps') arr = arr.filter(a => a.asset_type === 'vps');
if (store.filterStatus === 'abnormal') arr = arr.filter(a => a.status === 'stopped' || a.status === 'expired');
return arr;
}); });
function switchTab(k) { function switchTab(k) {
store.assetTab = k; store.assetTab = k;
// 专有视图需要全量数据;若此前按类型过滤过则清空重载 // 专有视图(域名/Cloudflare/订阅/AI)有自己的筛选逻辑,通用的类型/状态筛选切过去后会失效,
if (k !== '' && k !== 'vps' && store.filterType) { store.filterType = ''; loadAssets(); } // 直接清空避免用户以为筛过了但实际看不出来
if (k !== '' && k !== 'vps' && (store.filterType || store.filterStatus)) {
store.filterType = '';
store.filterStatus = '';
loadAssets();
}
} }
// 平台筛选标签显示名:filterProvider 存 slug,回退显示原值 // 平台筛选标签显示名:filterProvider 存 slug,回退显示原值
function providerName(slug) { function providerName(slug) {
+19 -11
View File
@@ -3,23 +3,25 @@ const DashboardView = {
template: ` template: `
<div class="space-y-5"> <div class="space-y-5">
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3"> <div class="grid grid-cols-2 lg:grid-cols-4 gap-3">
<div class="bg-white dark:bg-slate-900 rounded-xl p-4 border border-slate-200 dark:border-slate-800"> <button type="button" @click="goTotal" title="点击查看全部资产" class="text-left bg-white dark:bg-slate-900 rounded-xl p-4 border border-slate-200 dark:border-slate-800 hover:border-blue-400 dark:hover:border-blue-600 transition">
<div class="text-xs text-slate-500">总资产</div> <div class="text-xs text-slate-500">总资产</div>
<div class="text-2xl font-bold mt-1">{{ store.overview.total || 0 }}</div> <div class="text-2xl font-bold mt-1">{{ store.overview.total || 0 }}</div>
</button>
<div class="bg-white dark:bg-slate-900 rounded-xl p-4 border border-red-200 dark:border-red-900/50 hover:border-red-400 dark:hover:border-red-700 transition">
<button type="button" @click="goExpiring30" title="点击到监控中心查看 30 天内到期的资产" class="w-full text-left group">
<div class="text-xs text-slate-500">30天内到期</div>
<div class="text-2xl font-bold mt-1 text-red-600 dark:text-red-400 group-hover:underline">{{ store.overview.expiring_30 || 0 }}</div>
</button>
<div v-if="store.overview.overdue_count" class="text-xs mt-1 font-medium text-red-600 dark:text-red-400">另有 <button type="button" @click="goOverdue" title="点击到监控中心查看已过期资产" class="underline hover:no-underline">{{ store.overview.overdue_count }}</button> 项已过期</div>
</div> </div>
<div class="bg-white dark:bg-slate-900 rounded-xl p-4 border border-red-200 dark:border-red-900/50"> <button type="button" @click="goYearCost" title="点击查看付费资产汇总" class="text-left bg-white dark:bg-slate-900 rounded-xl p-4 border border-slate-200 dark:border-slate-800 hover:border-blue-400 dark:hover:border-blue-600 transition">
<div class="text-xs text-slate-500">30天内到期</div>
<div class="text-2xl font-bold mt-1 text-red-600 dark:text-red-400">{{ store.overview.expiring_30 || 0 }}</div>
<div v-if="store.overview.overdue_count" class="text-xs mt-1 font-medium text-red-600 dark:text-red-400">另有 {{ store.overview.overdue_count }} 项已过期</div>
</div>
<div class="bg-white dark:bg-slate-900 rounded-xl p-4 border border-slate-200 dark:border-slate-800">
<div class="text-xs text-slate-500">本年支出</div> <div class="text-xs text-slate-500">本年支出</div>
<div class="text-2xl font-bold mt-1">{{ store.overview.year_cost || 0 }}</div> <div class="text-2xl font-bold mt-1">{{ store.overview.year_cost || 0 }}</div>
</div> </button>
<div class="bg-white dark:bg-slate-900 rounded-xl p-4 border border-amber-200 dark:border-amber-900/50"> <button type="button" @click="goAbnormal" title="点击查看已停止/已过期的资产" class="text-left bg-white dark:bg-slate-900 rounded-xl p-4 border border-amber-200 dark:border-amber-900/50 hover:border-amber-400 dark:hover:border-amber-700 transition">
<div class="text-xs text-slate-500">异常状态</div> <div class="text-xs text-slate-500">异常状态</div>
<div class="text-2xl font-bold mt-1 text-amber-600 dark:text-amber-400">{{ store.overview.abnormal_count || 0 }}</div> <div class="text-2xl font-bold mt-1 text-amber-600 dark:text-amber-400">{{ store.overview.abnormal_count || 0 }}</div>
</div> </button>
</div> </div>
<!-- 到期日历:不限制在 30 天窗口内,每个有到期日的产品都列出“几号到期 / 还剩多久” --> <!-- 到期日历:不限制在 30 天窗口内,每个有到期日的产品都列出“几号到期 / 还剩多久” -->
@@ -64,6 +66,12 @@ const DashboardView = {
if (a.cost) bits.push(a.cost + (a.currency || '') + '/' + (Fmt.CYCLE_LABELS[a.renewal_cycle] || '未设周期')); if (a.cost) bits.push(a.cost + (a.currency || '') + '/' + (Fmt.CYCLE_LABELS[a.renewal_cycle] || '未设周期'));
return bits.filter(Boolean).join(' · '); return bits.filter(Boolean).join(' · ');
} }
return { store, Fmt, expiryList, noExpiryCount, meta }; // 卡片点击:带上对应筛选条件跳到目标视图,数字点一下就能看到背后的具体清单
function goTotal() { openAssetsWithFilter({}); }
function goExpiring30() { openMonitorWithExpiryFilter('soon30'); }
function goOverdue() { openMonitorWithExpiryFilter('expired'); }
function goYearCost() { openAssetsWithFilter({ assetTab: 'subscription' }); }
function goAbnormal() { openAssetsWithFilter({ filterStatus: 'abnormal' }); }
return { store, Fmt, expiryList, noExpiryCount, meta, goTotal, goExpiring30, goOverdue, goYearCost, goAbnormal };
}, },
}; };
+55 -30
View File
@@ -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> <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>
<!-- 顶部统计(资产) --> <!-- 顶部统计(资产):点卡片按到期区间筛选下方列表,再点一次取消 -->
<div v-if="store.monitorTab!=='ssl'" class="grid grid-cols-2 md:grid-cols-4 gap-3"> <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-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 class="text-2xl font-bold mt-1 text-red-600 dark:text-red-400">{{ stats.soon30 }}</div>
</div> </button>
<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('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-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 class="text-2xl font-bold mt-1 text-amber-600 dark:text-amber-400">{{ stats.soon7 }}</div>
</div> </button>
<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('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-xs text-slate-500">已过期</div>
<div class="text-2xl font-bold mt-1 text-slate-700 dark:text-slate-200">{{ stats.expired }}</div> <div class="text-2xl font-bold mt-1 text-slate-700 dark:text-slate-200">{{ stats.expired }}</div>
</div> </button>
<div class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4"> <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-xs text-slate-500">有到期日的资产</div>
<div class="text-2xl font-bold mt-1">{{ total }}</div> <div class="text-2xl font-bold mt-1">{{ total }}</div>
</div> </button>
</div> </div>
<!-- 顶部统计(SSL 证书) --> <!-- 顶部统计(SSL 证书):点卡片按证书状态筛选下方列表 -->
<div v-if="store.monitorTab==='ssl'" class="grid grid-cols-2 md:grid-cols-4 gap-3"> <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-xs text-slate-500">监控站点</div>
<div class="text-2xl font-bold mt-1">{{ sslStats.total }}</div> <div class="text-2xl font-bold mt-1">{{ sslStats.total }}</div>
</div> </button>
<div class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4"> <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-xs text-slate-500">证书正常</div>
<div class="text-2xl font-bold mt-1 text-emerald-600 dark:text-emerald-400">{{ sslStats.valid }}</div> <div class="text-2xl font-bold mt-1 text-emerald-600 dark:text-emerald-400">{{ sslStats.valid }}</div>
</div> </button>
<div class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4"> <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-xs text-slate-500">即将到期</div>
<div class="text-2xl font-bold mt-1 text-amber-600 dark:text-amber-400">{{ sslStats.expiring }}</div> <div class="text-2xl font-bold mt-1 text-amber-600 dark:text-amber-400">{{ sslStats.expiring }}</div>
</div> </button>
<div class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4"> <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-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 class="text-2xl font-bold mt-1 text-red-600 dark:text-red-400">{{ sslStats.expired + sslStats.error }}</div>
</div> </button>
</div> </div>
<!-- 类别 TAB --> <!-- 类别 TAB -->
@@ -135,7 +135,7 @@ const MonitorView = {
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-800"> <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"> <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 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">{{ c.asset_name || '—' }}</td>
@@ -156,7 +156,7 @@ const MonitorView = {
</div> </div>
<!-- SSL 移动卡片 --> <!-- SSL 移动卡片 -->
<div class="md:hidden space-y-2"> <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 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="flex justify-between items-start">
<div class="min-w-0"> <div class="min-w-0">
@@ -188,18 +188,32 @@ const MonitorView = {
{ key: 'ssl', label: 'SSL证书', tip: '站点 HTTPS 证书到期倒计时(独立于资产)' }, { key: 'ssl', label: 'SSL证书', tip: '站点 HTTPS 证书到期倒计时(独立于资产)' },
]; ];
const hasExpiry = a => a.expiry_date && a.status !== 'cancelled'; const hasExpiry = a => a.expiry_date && a.status !== 'cancelled';
const items = Vue.computed(() => { // 当前 TAB 下、有到期日的资产(未叠加到期区间子筛选),作为统计卡片与列表的共同基础数据集
const tabScopedAssets = Vue.computed(() => {
if (store.monitorTab === 'ssl') return []; if (store.monitorTab === 'ssl') return [];
let list = store.assets.filter(hasExpiry); let list = store.assets.filter(hasExpiry);
const t = store.monitorTab; const t = store.monitorTab;
if (t === 'subscription') list = list.filter(a => a.cost > 0); if (t === 'subscription') list = list.filter(a => a.cost > 0);
else if (t) list = list.filter(a => a.asset_type === t); 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 sslStats = Vue.computed(() => {
const s = { total: store.siteCerts.length, valid: 0, expiring: 0, expired: 0, error: 0 }; 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]++; 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(); } try { await Api.del('/site-certs/' + c.id); await loadSiteCerts(); }
catch (e) { alert('删除失败:' + e.message); } catch (e) { alert('删除失败:' + e.message); }
} }
const total = Vue.computed(() => items.value.length); const total = Vue.computed(() => tabScopedAssets.value.length);
const stats = Vue.computed(() => { const stats = Vue.computed(() => {
const s = { soon30: 0, soon7: 0, expired: 0 }; 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; const d = a.days_to_expiry;
if (d === null || d === undefined) continue; if (d === null || d === undefined) continue;
if (d < 0) s.expired++; if (d < 0) s.expired++;
@@ -226,8 +240,19 @@ const MonitorView = {
} }
return s; 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 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) { function countdownText(d) {
if (d === null || d === undefined) return '—'; if (d === null || d === undefined) return '—';
if (d < 0) return '已过期 ' + Math.abs(d) + ' 天'; 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'; if (d <= 90) return 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400';
return 'bg-slate-500/10 text-slate-500'; 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 };
}, },
}; };