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
+35 -3
View File
@@ -61,9 +61,13 @@ const AppRoot = {
<div class="flex-1 flex flex-col min-w-0">
<header class="sticky top-0 z-30 bg-white/80 dark:bg-slate-900/80 backdrop-blur border-b border-slate-200 dark:border-slate-800 px-4 py-3 flex items-center gap-3">
<h1 class="font-semibold text-slate-900 dark:text-white md:text-lg">{{ currentNav.label }}</h1>
<div v-if="store.loading" class="flex items-center gap-1.5 text-xs text-slate-400">
<span class="inline-block w-3 h-3 border-2 border-blue-500 border-t-transparent rounded-full animate-spin"></span>
<span class="hidden sm:inline">加载中…</span>
</div>
<div class="ml-auto flex items-center gap-2">
<input v-if="store.view==='assets'" v-model="store.search" @input="reload" placeholder="搜索…" class="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-48">
<button @click="quickAdd" class="text-sm px-3 py-1.5 rounded-lg bg-blue-600 text-white hover:bg-blue-700 whitespace-nowrap">+ 新增</button>
<button v-if="store.view!=='monitor' || store.monitorTab==='ssl'" @click="quickAdd" class="text-sm px-3 py-1.5 rounded-lg bg-blue-600 text-white hover:bg-blue-700 whitespace-nowrap">+ 新增</button>
</div>
</header>
@@ -89,9 +93,11 @@ const AppRoot = {
<credential-modal></credential-modal>
<action-modal></action-modal>
<dns-modal></dns-modal>
<confirm-modal></confirm-modal>
<result-modal></result-modal>
<!-- 复制等操作的轻提示(底部导航上方,不遮内容) -->
<div v-if="toast.show" class="fixed bottom-20 md:bottom-6 left-1/2 -translate-x-1/2 z-[80] px-4 py-2 rounded-full bg-slate-900/90 dark:bg-slate-100/90 text-white dark:text-slate-900 text-xs shadow-lg pointer-events-none whitespace-nowrap">{{ toast.text }}</div>
<div v-if="toast.show" class="fixed bottom-24 md:bottom-6 left-1/2 -translate-x-1/2 z-[80] px-4 py-2 rounded-full bg-slate-900/90 dark:bg-slate-100/90 text-white dark:text-slate-900 text-xs shadow-lg pointer-events-none whitespace-nowrap">{{ toast.text }}</div>
</div>`,
setup() {
const appName = Api.CFG.appName;
@@ -104,7 +110,12 @@ const AppRoot = {
function quickAdd() {
if (store.view === 'providers') return openProviderCreate();
if (store.view === 'vault') return openCredentialCreate();
// 资产页:按当前 TAB 预设类型;监控中心/总览/设置:默认 VPS
// 监控中心:SSL TAB 下添加证书监控,其他情况隐藏新增按钮(模板已控制)
if (store.view === 'monitor') {
if (store.monitorTab === 'ssl') return showResult('提示', '请在「资产 → 域名」的子域名面板点「探测SSL」添加证书监控');
return; // 非 SSL TAB 不响应(模板已隐藏按钮)
}
// 总览/设置:默认创建 VPS
let preset = 'vps';
if (store.view === 'assets') {
const t = store.assetTab;
@@ -124,6 +135,24 @@ const AppRoot = {
},
};
/* ================= 键盘快捷键 ================= */
function initKeyboard() {
document.addEventListener('keydown', (e) => {
// Esc 关闭最上层模态框(按 z-index 从高到低检查)
if (e.key === 'Escape') {
if (store.resultModal.show) { closeResult(); return; }
if (store.confirmModal.show) { closeConfirm(false); return; }
if (store.credentialModal.show) { store.credentialModal.show = false; return; }
if (store.accountModal.show) { store.accountModal.show = false; return; }
if (store.assetModal.show) { store.assetModal.show = false; return; }
if (store.providerModal.show) { store.providerModal.show = false; return; }
if (store.accountsModal.show) { store.accountsModal.show = false; return; }
if (actionModal.show) { closeActions(); return; }
if (dnsModal.show) { closeDns(); return; }
}
});
}
/* ================= 挂载 ================= */
const app = Vue.createApp(AppRoot);
app.component('dashboard-view', DashboardView);
@@ -140,7 +169,10 @@ app.component('accounts-view-modal', AccountsViewModal);
app.component('credential-modal', CredentialModal);
app.component('action-modal', ActionModal);
app.component('dns-modal', DnsModal);
app.component('confirm-modal', ConfirmModal);
app.component('result-modal', ResultModal);
applyDark();
initRouter();
initKeyboard();
loadAll();
app.mount('#app');