From 7045843db8a2c6a1ab937ba6dfc5a7bbeff41f2f Mon Sep 17 00:00:00 2001 From: gouki Date: Sun, 20 Sep 2026 12:53:48 +0000 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=80=BB=E8=A7=88/=E7=9B=91?= =?UTF-8?q?=E6=8E=A7=E4=B8=AD=E5=BF=83=E9=A1=B6=E9=83=A8=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E6=94=AF=E6=8C=81=E7=82=B9=E5=87=BB=E7=AD=9B?= =?UTF-8?q?=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 总览页「总资产」「本年支出」「异常状态」卡片点击跳转资产页并带上对应筛选 (全部/付费资产TAB/异常状态组合筛选);「30天内到期」及「另有N项已过期」 子文案点击跳转监控中心并带上到期区间子筛选 - 监控中心顶部统计卡片(30天/7天/已过期/有到期日)支持点击筛选下方列表, 再点一次取消;SSL 证书卡片(正常/即将到期/已过期或失败)同理 - 资产页状态筛选新增「异常(已停止/已过期)」组合选项:后端 status 只认单值, 该组合作为前端本地筛选哨兵值处理,不发给 API;切到专有视图 TAB 时自动清空 - 顺带修复监控中心统计卡片数字不随类别 TAB 变化的问题(原先 stats 未按 TAB 过滤,与列表数据口径不一致) --- static/js/store.js | 28 +++++++++++- static/js/views/assets.js | 18 +++++--- static/js/views/dashboard.js | 30 ++++++++----- static/js/views/monitor.js | 85 +++++++++++++++++++++++------------- 4 files changed, 114 insertions(+), 47 deletions(-) diff --git a/static/js/store.js b/static/js/store.js index ad7f3eb..05ad213 100644 --- a/static/js/store.js +++ b/static/js/store.js @@ -39,6 +39,8 @@ const store = reactive({ filterProvider: '', // 按平台筛选(slug),从平台页「关联 N 个资产」跳转而来 assetTab: '', // 资产页 TAB:'' 全部 / vps / domain / cloudflare / subscription(付费资产) / ai_agent monitorTab: '', // 监控中心 TAB:同上 + monitorExpiryFilter: '', // 监控中心到期区间子筛选:'' 全部 / soon30 / soon7 / expired + monitorSslStatusFilter: '', // 监控中心 SSL 状态子筛选:'' 全部 / valid / expiring / expired_error dark: localStorage.getItem('vps_dark') === '1', loading: false, error: '', @@ -101,10 +103,13 @@ function buildAssetPayload(f) { } /* ---------------- 数据加载 ---------------- */ +// 后端 status 参数只认单个 AssetStatus 枚举值;「异常」是「已停止 + 已过期」的组合概念, +// 后端不支持多值筛选,这里当作前端本地筛选的哨兵值,不发给 API,由视图层自己过滤。 +const CLIENT_SIDE_STATUS_FILTERS = new Set(['abnormal']); async function loadAssets() { const params = new URLSearchParams(); 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.search) params.set('q', store.search); params.set('sort', 'expiry_date'); params.set('order', 'asc'); @@ -449,6 +454,27 @@ function clearProviderFilter() { 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) { try { diff --git a/static/js/views/assets.js b/static/js/views/assets.js index 5a016f2..e85540c 100644 --- a/static/js/views/assets.js +++ b/static/js/views/assets.js @@ -27,6 +27,7 @@ const AssetsView = {
@@ -114,15 +115,22 @@ const AssetsView = { if (t === 'ai_agent') return AIAccountsView; return null; }); - // 通用列表:全部显示所有,VPS 前端过滤类型 + // 通用列表:全部显示所有,VPS 前端过滤类型;「异常」状态是后端不支持的组合筛选,也走前端过滤 const list = Vue.computed(() => { - if (store.assetTab === 'vps') return store.assets.filter(a => a.asset_type === 'vps'); - return store.assets; + let arr = 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) { store.assetTab = k; - // 专有视图需要全量数据;若此前按类型过滤过则清空重载 - if (k !== '' && k !== 'vps' && store.filterType) { store.filterType = ''; loadAssets(); } + // 专有视图(域名/Cloudflare/订阅/AI)有自己的筛选逻辑,通用的类型/状态筛选切过去后会失效, + // 直接清空避免用户以为筛过了但实际看不出来 + if (k !== '' && k !== 'vps' && (store.filterType || store.filterStatus)) { + store.filterType = ''; + store.filterStatus = ''; + loadAssets(); + } } // 平台筛选标签显示名:filterProvider 存 slug,回退显示原值 function providerName(slug) { diff --git a/static/js/views/dashboard.js b/static/js/views/dashboard.js index 1ae5482..54a0ac8 100644 --- a/static/js/views/dashboard.js +++ b/static/js/views/dashboard.js @@ -3,23 +3,25 @@ const DashboardView = { template: `
-
+ +
+ +
另有 项已过期
-
-
30天内到期
-
{{ store.overview.expiring_30 || 0 }}
-
另有 {{ store.overview.overdue_count }} 项已过期
-
-
+
-
+ +
+
@@ -64,6 +66,12 @@ const DashboardView = { if (a.cost) bits.push(a.cost + (a.currency || '') + '/' + (Fmt.CYCLE_LABELS[a.renewal_cycle] || '未设周期')); 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 }; }, }; diff --git a/static/js/views/monitor.js b/static/js/views/monitor.js index 08bd1fa..e42911b 100644 --- a/static/js/views/monitor.js +++ b/static/js/views/monitor.js @@ -10,44 +10,44 @@ const MonitorView = { 绿=90天内
- +
-
+
-
+ +
-
+ +
-
+ +
+
- +
-
+
-
+ +
-
+ +
-
+ +
+
@@ -135,7 +135,7 @@ const MonitorView = { - 暂无证书监控(在「资产 → 域名」的子域名面板点「探测SSL」,或稍后支持直接添加) + {{ sslEmptyText }} {{ c.hostname }}:{{ c.port }} {{ c.asset_name || '—' }} @@ -156,7 +156,7 @@ const MonitorView = {
-
暂无证书监控
+
{{ sslEmptyText }}
@@ -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 }; }, };