get_expiring/get_overview 去掉 expiry_date>=today 下界(仅排除已注销)并新增 overdue_count;renewal_service 续费提醒纳入已过期、文案区分已过期N天/今天到期/N天后;dashboard 用到期日历逐条显示具体到期日与倒计时;api.js 新增 Fmt.countdownText 统一文案。
70 lines
4.4 KiB
JavaScript
70 lines
4.4 KiB
JavaScript
/* 总览视图 */
|
||
const DashboardView = {
|
||
template: `
|
||
<div class="space-y-5">
|
||
<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">
|
||
<div class="text-xs text-slate-500">总资产</div>
|
||
<div class="text-2xl font-bold mt-1">{{ store.overview.total || 0 }}</div>
|
||
</div>
|
||
<div class="bg-white dark:bg-slate-900 rounded-xl p-4 border border-red-200 dark:border-red-900/50">
|
||
<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-2xl font-bold mt-1">{{ store.overview.year_cost || 0 }}</div>
|
||
</div>
|
||
<div class="bg-white dark:bg-slate-900 rounded-xl p-4 border border-amber-200 dark:border-amber-900/50">
|
||
<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>
|
||
</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(' · ');
|
||
}
|
||
return { store, Fmt, expiryList, noExpiryCount, meta };
|
||
},
|
||
};
|