feat(ui): 总览/监控中心顶部统计卡片支持点击筛选
- 总览页「总资产」「本年支出」「异常状态」卡片点击跳转资产页并带上对应筛选 (全部/付费资产TAB/异常状态组合筛选);「30天内到期」及「另有N项已过期」 子文案点击跳转监控中心并带上到期区间子筛选 - 监控中心顶部统计卡片(30天/7天/已过期/有到期日)支持点击筛选下方列表, 再点一次取消;SSL 证书卡片(正常/即将到期/已过期或失败)同理 - 资产页状态筛选新增「异常(已停止/已过期)」组合选项:后端 status 只认单值, 该组合作为前端本地筛选哨兵值处理,不发给 API;切到专有视图 TAB 时自动清空 - 顺带修复监控中心统计卡片数字不随类别 TAB 变化的问题(原先 stats 未按 TAB 过滤,与列表数据口径不一致)
This commit is contained in:
+27
-1
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
</div>
|
</button>
|
||||||
<div class="bg-white dark:bg-slate-900 rounded-xl p-4 border border-red-200 dark:border-red-900/50">
|
<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-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 class="text-2xl font-bold mt-1 text-red-600 dark:text-red-400 group-hover:underline">{{ 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>
|
</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-slate-200 dark:border-slate-800">
|
<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">本年支出</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
@@ -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 };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user