feat(ui): 全局弹窗替代原生 alert/confirm,添加防重复提交与搜索功能
- 新增 ConfirmModal / ResultModal 全局组件,替代原生 alert/confirm/prompt - 资产/平台/账号保存添加防重复提交锁(assetSaving / providerSaving) - 添加 Esc 键盘快捷键关闭模态框 - 平台/监控视图添加搜索过滤功能 - 修复监控中心 SSL TAB 下新增按钮的显示逻辑 - 调整 toast 位置避免遮挡底部导航 - 统一各弹窗 z-index 层级
This commit is contained in:
+44
-13
@@ -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 }; },
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user