Files
vps-manager/static/js/views/dashboard.js
T
gouki 7045843db8 feat(ui): 总览/监控中心顶部统计卡片支持点击筛选
- 总览页「总资产」「本年支出」「异常状态」卡片点击跳转资产页并带上对应筛选
  (全部/付费资产TAB/异常状态组合筛选);「30天内到期」及「另有N项已过期」
  子文案点击跳转监控中心并带上到期区间子筛选
- 监控中心顶部统计卡片(30天/7天/已过期/有到期日)支持点击筛选下方列表,
  再点一次取消;SSL 证书卡片(正常/即将到期/已过期或失败)同理
- 资产页状态筛选新增「异常(已停止/已过期)」组合选项:后端 status 只认单值,
  该组合作为前端本地筛选哨兵值处理,不发给 API;切到专有视图 TAB 时自动清空
- 顺带修复监控中心统计卡片数字不随类别 TAB 变化的问题(原先 stats 未按 TAB
  过滤,与列表数据口径不一致)
2026-09-20 12:53:48 +00:00

78 lines
5.8 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 DashboardView = {
template: `
<div class="space-y-5">
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3">
<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-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>
<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-2xl font-bold mt-1">{{ store.overview.year_cost || 0 }}</div>
</button>
<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-2xl font-bold mt-1 text-amber-600 dark:text-amber-400">{{ store.overview.abnormal_count || 0 }}</div>
</button>
</div>
<!-- 到期日历:不限制在 30 天窗口内,每个有到期日的产品都列出“几号到期 / 还剩多久” -->
<div class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4">
<div class="flex items-center justify-between mb-3">
<h2 class="text-sm font-semibold">到期日历({{ expiryList.length }})</h2>
<a href="#monitor" class="text-xs text-blue-600 dark:text-blue-400 hover:underline">全部倒计时 →</a>
</div>
<div v-if="expiryList.length" class="space-y-2">
<div v-for="a in expiryList" :key="a.id" class="flex justify-between items-center gap-3 px-3 py-2 rounded-lg bg-slate-50 dark:bg-slate-800/50">
<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 truncate">{{ meta(a) }}</div>
</div>
<div class="text-right shrink-0">
<div class="text-sm" :class="Fmt.expiryText(a.days_to_expiry)">{{ a.expiry_date }}</div>
<span class="text-xs px-2 py-0.5 rounded-full" :class="Fmt.expiryBadge(a.days_to_expiry)">{{ Fmt.countdownText(a.days_to_expiry) }}</span>
</div>
</div>
</div>
<p v-else class="text-xs text-slate-400 py-4 text-center">没有资产设置了到期日,算不出倒计时</p>
<p v-if="noExpiryCount" class="text-xs text-slate-400 mt-2">另 {{ noExpiryCount }} 个资产未填到期日,不会出现在这里(到资产页编辑补上)</p>
</div>
<div class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 p-4">
<h2 class="text-sm font-semibold mb-3">按类型分布</h2>
<div class="flex flex-wrap gap-2">
<span v-for="(n, t) in store.overview.by_type" :key="t" class="text-xs px-3 py-1.5 rounded-lg" :class="Fmt.typeBadge(t)">{{ Fmt.TYPE_LABELS[t] }} {{ n }}</span>
<span v-if="!store.overview.by_type || !Object.keys(store.overview.by_type).length" class="text-xs text-slate-400">暂无数据</span>
</div>
</div>
</div>`,
setup() {
// 到期日历:只看未注销且有到期日的资产,最紧急(含已过期)排最前
const expiryList = Vue.computed(() => store.assets
.filter(a => a.expiry_date && a.status !== 'cancelled')
.sort((x, y) => (x.days_to_expiry ?? 10 ** 9) - (y.days_to_expiry ?? 10 ** 9)));
const noExpiryCount = Vue.computed(() => store.assets.filter(a => !a.expiry_date && a.status !== 'cancelled').length);
// 副标题:类型 · 平台 · 费用/周期,缺失的字段不占位(避免出现 "VPS · · 3.5USD")
function meta(a) {
const bits = [Fmt.TYPE_LABELS[a.asset_type] || a.asset_type, a.provider_name || a.provider];
if (a.cost) bits.push(a.cost + (a.currency || '') + '/' + (Fmt.CYCLE_LABELS[a.renewal_cycle] || '未设周期'));
return bits.filter(Boolean).join(' · ');
}
// 卡片点击:带上对应筛选条件跳到目标视图,数字点一下就能看到背后的具体清单
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 };
},
};