feat(ui): 全局弹窗替代原生 alert/confirm,添加防重复提交与搜索功能

- 新增 ConfirmModal / ResultModal 全局组件,替代原生 alert/confirm/prompt
- 资产/平台/账号保存添加防重复提交锁(assetSaving / providerSaving)
- 添加 Esc 键盘快捷键关闭模态框
- 平台/监控视图添加搜索过滤功能
- 修复监控中心 SSL TAB 下新增按钮的显示逻辑
- 调整 toast 位置避免遮挡底部导航
- 统一各弹窗 z-index 层级
This commit is contained in:
gouki
2026-10-17 21:17:18 +00:00
parent a63fcb786f
commit eeb05372c4
7 changed files with 167 additions and 48 deletions
+13 -7
View File
@@ -50,13 +50,14 @@ const MonitorView = {
</button>
</div>
<!-- 类别 TAB -->
<div class="flex flex-wrap gap-1.5">
<!-- 类别 TAB + 搜索 -->
<div class="flex flex-wrap gap-1.5 items-center">
<button v-for="t in tabs" :key="t.key" @click="switchTab(t.key)" :title="t.tip"
class="text-sm px-3 py-1.5 rounded-full transition"
:class="store.monitorTab===t.key ? 'bg-blue-600 text-white' : 'bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800'">
{{ t.label }}
</button>
<input v-model="search" placeholder="搜索名称/平台…" class="ml-auto text-sm px-3 py-1.5 rounded-lg border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 w-32 md:w-40">
</div>
<!-- 资产表格(非 SSL TAB) -->
@@ -199,21 +200,26 @@ const MonitorView = {
else if (t) list = list.filter(a => a.asset_type === t);
return list;
});
const search = Vue.ref('');
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);
const kw = search.value.trim().toLowerCase();
if (kw) list = list.filter(a => a.name.toLowerCase().includes(kw) || (a.provider_name || a.provider || '').toLowerCase().includes(kw));
return [...list].sort((x, y) => (x.days_to_expiry ?? 9999) - (y.days_to_expiry ?? 9999));
});
// SSL 证书:按剩余天数倒计时排序,可叠加状态子筛选
// 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');
const kw = search.value.trim().toLowerCase();
if (kw) list = list.filter(c => c.hostname.toLowerCase().includes(kw) || (c.asset_name || '').toLowerCase().includes(kw));
return [...list].sort((x, y) => (x.days_to_expiry ?? 9999) - (y.days_to_expiry ?? 9999));
});
const sslStats = Vue.computed(() => {
@@ -223,12 +229,12 @@ const MonitorView = {
});
async function recheck(c) {
try { await Api.post('/site-certs/' + c.id + '/check', {}); await loadSiteCerts(); }
catch (e) { alert('重测失败:' + e.message); }
catch (e) { showResult('重测失败', e.message, true); }
}
async function delCert(c) {
if (!confirm('删除证书监控「' + c.hostname + '」?')) return;
if (!await showConfirm('删除证书监控', '删除证书监控「' + c.hostname + '」?', true)) return;
try { await Api.del('/site-certs/' + c.id); await loadSiteCerts(); }
catch (e) { alert('删除失败:' + e.message); }
catch (e) { showResult('删除失败', e.message, true); }
}
const total = Vue.computed(() => tabScopedAssets.value.length);
const stats = Vue.computed(() => {
@@ -265,6 +271,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, sslEmptyText, switchTab, toggleExpiryFilter, clearExpiryFilter, toggleSslStatusFilter, clearSslStatusFilter, 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, search };
},
};