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
+44 -13
View File
@@ -1,7 +1,7 @@
/* 资产编辑模态框 */
const AssetModal = {
template: `
<div v-if="store.assetModal.show" class="fixed inset-0 bg-black/40 z-50 flex items-end md:items-center justify-center p-0 md:p-4">
<div v-if="store.assetModal.show" class="fixed inset-0 bg-black/40 z-[50] flex items-end md:items-center justify-center p-0 md:p-4">
<div class="bg-white dark:bg-slate-900 w-full md:max-w-lg md:rounded-xl rounded-t-xl max-h-[92vh] overflow-y-auto">
<div class="px-5 py-3 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center sticky top-0 bg-white dark:bg-slate-900">
<h3 class="font-semibold text-sm">{{ store.assetModal.editing ? '编辑资产' : '新增资产' }}</h3>
@@ -109,7 +109,7 @@ const AssetModal = {
<div class="flex justify-end gap-2 pt-1">
<button type="button" @click="store.assetModal.show=false" class="px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-700 text-sm text-slate-600 dark:text-slate-300">取消</button>
<button type="submit" class="px-4 py-2 rounded-lg bg-blue-600 text-white text-sm hover:bg-blue-700">{{ store.assetModal.editing ? '保存' : '创建' }}</button>
<button type="submit" :disabled="store.assetSaving" class="px-4 py-2 rounded-lg bg-blue-600 text-white text-sm hover:bg-blue-700 disabled:opacity-60">{{ store.assetSaving ? '保存中…' : (store.assetModal.editing ? '保存' : '创建') }}</button>
</div>
</form>
</div>
@@ -145,7 +145,7 @@ const AssetModal = {
/* 平台编辑模态框 */
const ProviderModal = {
template: `
<div v-if="store.providerModal.show" class="fixed inset-0 bg-black/40 z-50 flex items-end md:items-center justify-center p-0 md:p-4">
<div v-if="store.providerModal.show" class="fixed inset-0 bg-black/40 z-[50] flex items-end md:items-center justify-center p-0 md:p-4">
<div class="bg-white dark:bg-slate-900 w-full md:max-w-md md:rounded-xl rounded-t-xl max-h-[92vh] overflow-y-auto">
<div class="px-5 py-3 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center">
<h3 class="font-semibold text-sm">{{ store.providerModal.editing ? '编辑平台' : '新增平台' }}</h3>
@@ -169,7 +169,7 @@ const ProviderModal = {
</div>
<div class="flex justify-end gap-2">
<button type="button" @click="store.providerModal.show=false" class="px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-700 text-sm text-slate-600 dark:text-slate-300">取消</button>
<button type="submit" class="px-4 py-2 rounded-lg bg-blue-600 text-white text-sm hover:bg-blue-700">保存</button>
<button type="submit" :disabled="store.providerSaving" class="px-4 py-2 rounded-lg bg-blue-600 text-white text-sm hover:bg-blue-700 disabled:opacity-60">{{ store.providerSaving ? '保存中…' : '保存' }}</button>
</div>
</form>
</div>
@@ -267,7 +267,7 @@ const AccountModal = {
/* 账号查看弹窗:平台 → 账号 → 资产 三层结构,展开账号看名下资产 */
const AccountsViewModal = {
template: `
<div v-if="store.accountsModal.show" class="fixed inset-0 bg-black/40 z-50 flex items-end md:items-center justify-center p-0 md:p-4">
<div v-if="store.accountsModal.show" class="fixed inset-0 bg-black/40 z-[50] flex items-end md:items-center justify-center p-0 md:p-4">
<div class="bg-white dark:bg-slate-900 w-full md:max-w-lg md:rounded-xl rounded-t-xl max-h-[92vh] overflow-y-auto">
<div class="px-5 py-3 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center sticky top-0 bg-white dark:bg-slate-900">
<h3 class="font-semibold text-sm">👤 {{ title }}({{ accountList.length }})</h3>
@@ -315,7 +315,7 @@ const AccountsViewModal = {
</div>
</div>
<!-- 密码查看弹窗:点击账号的 🔑 后展示明文,几秒后自动隐藏 -->
<div v-if="pwd.show" class="absolute inset-0 bg-black/50 flex items-center justify-center p-4" @click.self="pwd.show=false">
<div v-if="pwd.show" class="absolute inset-0 bg-black/50 z-[70] flex items-center justify-center p-4" @click.self="pwd.show=false">
<div class="bg-white dark:bg-slate-800 rounded-xl w-full max-w-xs p-5 shadow-xl">
<h4 class="font-semibold text-sm mb-3">🔑 {{ pwd.name }} 的登录凭证</h4>
<div class="space-y-2 text-sm">
@@ -391,8 +391,8 @@ const AccountsViewModal = {
async function testAcct(a) {
try {
const r = await Api.post('/accounts/' + a.id + '/test', {});
alert(r.ok ? '✅ ' + r.message : '❌ ' + r.message);
} catch (e) { alert('测试失败:' + e.message); }
showResult(r.ok ? '测试成功' : '测试失败', r.message, !r.ok);
} catch (e) { showResult('测试失败', e.message, true); }
}
async function syncAcct(a) {
try {
@@ -402,11 +402,11 @@ const AccountsViewModal = {
if (r.domains) lines.push('域名:新增 ' + r.domains.created + ' / 更新 ' + r.domains.updated);
if (r.vps_error) lines.push('VPS 错误:' + r.vps_error);
if (r.domains_error) lines.push('域名错误:' + r.domains_error);
alert(lines.join('\n'));
showResult('同步完成', lines.join('\n'));
// 同步会新建/更新资产(含到期日),总览的统计卡片也得跟着刷,
// 否则回到总览看到的还是同步前的数字
await Promise.all([loadAccounts(), loadAssets(), loadOverview()]);
} catch (e) { alert('同步失败:' + e.message); }
} catch (e) { showResult('同步失败', e.message, true); }
}
// 每次打开重置展开状态;关闭时连带收掉密码弹窗
// 外层弹窗用 v-if 控制,但本组件实例并不卸载,onUnmounted 不会触发:
@@ -439,7 +439,7 @@ const AccountsViewModal = {
if (pwd.countdown <= 0) closePwd();
}, 1000);
}
} catch (e) { alert('获取密码失败:' + e.message); }
} catch (e) { showResult('获取密码失败', e.message, true); }
}
function closePwd() {
pwd.show = false;
@@ -602,7 +602,7 @@ const CredentialModal = {
* 3. 不支持时如实说明原因,而不是给一个点了才报错的按钮 */
const ActionModal = {
template: `
<div v-if="m.show" class="fixed inset-0 bg-black/40 z-50 flex items-end md:items-center justify-center p-0 md:p-4">
<div v-if="m.show" class="fixed inset-0 bg-black/40 z-[60] flex items-end md:items-center justify-center p-0 md:p-4">
<div class="bg-white dark:bg-slate-900 w-full md:max-w-md md:rounded-xl rounded-t-xl max-h-[92vh] overflow-y-auto">
<div class="px-5 py-3 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center sticky top-0 bg-white dark:bg-slate-900">
<h3 class="font-semibold text-sm">⚡ 云资源操作</h3>
@@ -687,7 +687,7 @@ const ActionModal = {
/* ================= DNS 解析记录管理弹窗 ================= */
const DnsModal = {
template: `
<div v-if="m.show" class="fixed inset-0 bg-black/40 z-50 flex items-end md:items-center justify-center p-0 md:p-4">
<div v-if="m.show" class="fixed inset-0 bg-black/40 z-[60] flex items-end md:items-center justify-center p-0 md:p-4">
<div class="bg-white dark:bg-slate-900 w-full md:max-w-2xl md:rounded-xl rounded-t-xl max-h-[92vh] flex flex-col">
<div class="px-5 py-3 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center">
<h3 class="font-semibold text-sm">🌐 DNS 解析<span v-if="m.zoneId" class="text-slate-400 font-normal"> · {{ m.zoneId }}</span></h3>
@@ -797,3 +797,34 @@ const DnsModal = {
};
},
};
/* ================= 全局确认弹窗(替代原生 confirm) ================= */
const ConfirmModal = {
template: `
<div v-if="store.confirmModal.show" class="fixed inset-0 bg-black/40 z-[70] flex items-center justify-center p-4" @click.self="closeConfirm(false)">
<div class="bg-white dark:bg-slate-900 rounded-xl w-full max-w-sm p-5 shadow-xl">
<h3 class="font-semibold text-sm mb-2">{{ store.confirmModal.title }}</h3>
<p class="text-sm text-slate-600 dark:text-slate-300 whitespace-pre-wrap">{{ store.confirmModal.message }}</p>
<div class="flex gap-2 mt-4 justify-end">
<button @click="closeConfirm(false)" class="px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-700 text-sm text-slate-600 dark:text-slate-300">取消</button>
<button @click="closeConfirm(true)" class="px-4 py-2 rounded-lg text-sm text-white" :class="store.confirmModal.danger ? 'bg-red-600 hover:bg-red-700' : 'bg-blue-600 hover:bg-blue-700'">确认</button>
</div>
</div>
</div>`,
setup() { return { store, closeConfirm }; },
};
/* ================= 全局结果弹窗(替代原生 alert) ================= */
const ResultModal = {
template: `
<div v-if="store.resultModal.show" class="fixed inset-0 bg-black/40 z-[70] flex items-center justify-center p-4" @click.self="closeResult">
<div class="bg-white dark:bg-slate-900 rounded-xl w-full max-w-md p-5 shadow-xl">
<h3 class="font-semibold text-sm mb-2" :class="store.resultModal.isError ? 'text-red-600 dark:text-red-400' : ''">{{ store.resultModal.title }}</h3>
<p class="text-sm text-slate-600 dark:text-slate-300 whitespace-pre-wrap max-h-64 overflow-y-auto">{{ store.resultModal.message }}</p>
<div class="flex gap-2 mt-4 justify-end">
<button @click="closeResult" class="px-4 py-2 rounded-lg bg-blue-600 text-white text-sm hover:bg-blue-700">确定</button>
</div>
</div>
</div>`,
setup() { return { store, closeResult }; },
};