feat(ui): 全局弹窗替代原生 alert/confirm,添加防重复提交与搜索功能
- 新增 ConfirmModal / ResultModal 全局组件,替代原生 alert/confirm/prompt - 资产/平台/账号保存添加防重复提交锁(assetSaving / providerSaving) - 添加 Esc 键盘快捷键关闭模态框 - 平台/监控视图添加搜索过滤功能 - 修复监控中心 SSL TAB 下新增按钮的显示逻辑 - 调整 toast 位置避免遮挡底部导航 - 统一各弹窗 z-index 层级
This commit is contained in:
+49
-7
@@ -48,8 +48,14 @@ const store = reactive({
|
||||
dark: localStorage.getItem('vps_dark') === '1',
|
||||
loading: false,
|
||||
error: '',
|
||||
// 全局确认弹窗
|
||||
confirmModal: { show: false, title: '', message: '', danger: false, resolve: null },
|
||||
// 全局结果弹窗(替代 alert)
|
||||
resultModal: { show: false, title: '', message: '', isError: false },
|
||||
assetModal: { show: false, editing: null, form: null },
|
||||
assetSaving: false, // 资产保存中锁,防止双击/重复提交
|
||||
providerModal: { show: false, editing: null, form: null },
|
||||
providerSaving: false, // 平台保存中锁
|
||||
accountModal: { show: false, editing: null, form: null },
|
||||
accountSaving: false, // 账号保存中锁,防止双击/重复提交触发后端 400 重名
|
||||
accountsModal: { show: false, providerSlug: null }, // 账号查看弹窗:providerSlug 为 null 时看全部
|
||||
@@ -145,6 +151,16 @@ async function loadCredentials() {
|
||||
if (store.vaultSearch) params.set('q', store.vaultSearch);
|
||||
if (store.vaultOtpOnly) params.set('has_otp', 'true');
|
||||
store.credentials = await Api.get('/credentials?' + params.toString());
|
||||
// 前端补充 URL 匹配:后端搜索可能不覆盖 url 字段
|
||||
if (store.vaultSearch) {
|
||||
const kw = store.vaultSearch.trim().toLowerCase();
|
||||
store.credentials = store.credentials.filter(c =>
|
||||
(c.site || '').toLowerCase().includes(kw) ||
|
||||
(c.username || '').toLowerCase().includes(kw) ||
|
||||
(c.note || '').toLowerCase().includes(kw) ||
|
||||
(c.url || '').toLowerCase().includes(kw)
|
||||
);
|
||||
}
|
||||
syncOauthCustom();
|
||||
}
|
||||
async function loadSubdomains() { store.subdomains = await Api.get('/subdomains'); }
|
||||
@@ -184,7 +200,9 @@ function openAssetEdit(a) {
|
||||
store.assetModal = { show: true, editing: a.id, form };
|
||||
}
|
||||
async function saveAsset() {
|
||||
if (store.assetSaving) return;
|
||||
store.error = '';
|
||||
store.assetSaving = true;
|
||||
const f = store.assetModal.form;
|
||||
try {
|
||||
const payload = buildAssetPayload(f);
|
||||
@@ -193,9 +211,10 @@ async function saveAsset() {
|
||||
store.assetModal.show = false;
|
||||
await loadAll();
|
||||
} catch (e) { store.error = e.message; }
|
||||
finally { store.assetSaving = false; }
|
||||
}
|
||||
async function deleteAsset(a) {
|
||||
if (!confirm('确认删除资产「' + a.name + '」?此操作不可恢复。')) return;
|
||||
if (!await showConfirm('删除资产', '确认删除资产「' + a.name + '」?此操作不可恢复。', true)) return;
|
||||
store.error = '';
|
||||
try { await Api.del('/assets/' + a.id); await loadAll(); }
|
||||
catch (e) { store.error = e.message; }
|
||||
@@ -209,7 +228,9 @@ function openProviderEdit(p) {
|
||||
store.providerModal = { show: true, editing: p.id, form: { slug: p.slug, name: p.name, name_en: p.name_en || '', category: p.category, services: p.services || '', website: p.website || '', console_url: p.console_url || '', sdk_type: p.sdk_type || '', enabled: p.enabled, remark: p.remark || '' } };
|
||||
}
|
||||
async function saveProvider() {
|
||||
if (store.providerSaving) return;
|
||||
store.error = '';
|
||||
store.providerSaving = true;
|
||||
const f = store.providerModal.form;
|
||||
// 凭证已下沉到账号层,平台不再携带 api_config
|
||||
const payload = { slug: f.slug, name: f.name, name_en: f.name_en || null, category: f.category, services: f.services || null, website: f.website || null, console_url: f.console_url || null, sdk_type: f.sdk_type || null, enabled: !!f.enabled, remark: f.remark || null };
|
||||
@@ -219,14 +240,15 @@ async function saveProvider() {
|
||||
store.providerModal.show = false;
|
||||
await loadProviders();
|
||||
} catch (e) { store.error = e.message; }
|
||||
finally { store.providerSaving = false; }
|
||||
}
|
||||
async function seedProviders() {
|
||||
store.error = '';
|
||||
try { const r = await Api.post('/providers/seed', {}); await loadProviders(); alert('已初始化预设平台,新增 ' + r.added + ' 个'); }
|
||||
try { const r = await Api.post('/providers/seed', {}); await loadProviders(); showResult('初始化完成', '已初始化预设平台,新增 ' + r.added + ' 个'); }
|
||||
catch (e) { store.error = e.message; }
|
||||
}
|
||||
async function deleteProvider(p) {
|
||||
if (!confirm('确认删除平台「' + p.name + '」?')) return;
|
||||
if (!await showConfirm('删除平台', '确认删除平台「' + p.name + '」?', true)) return;
|
||||
try { await Api.del('/providers/' + p.id); await loadProviders(); }
|
||||
catch (e) { store.error = e.message; }
|
||||
}
|
||||
@@ -266,7 +288,7 @@ async function saveAccount() {
|
||||
async function deleteAccount(a) {
|
||||
let msg = '确认删除账号「' + a.name + '」?';
|
||||
if (a.asset_count) msg += '\n有 ' + a.asset_count + ' 个资产引用该账号(资产中已填的账号名不受影响)。';
|
||||
if (!confirm(msg)) return;
|
||||
if (!await showConfirm('删除账号', msg, true)) return;
|
||||
store.error = '';
|
||||
try { await Api.del('/accounts/' + a.id); await loadAccounts(); }
|
||||
catch (e) { store.error = e.message; }
|
||||
@@ -367,7 +389,7 @@ async function saveCredential() {
|
||||
}
|
||||
async function deleteCredential(c) {
|
||||
const label = c.site + (c.username ? ' · ' + c.username : '');
|
||||
if (!confirm('确认删除凭据「' + label + '」?此操作不可恢复。')) return;
|
||||
if (!await showConfirm('删除凭据', '确认删除凭据「' + label + '」?此操作不可恢复。', true)) return;
|
||||
store.error = '';
|
||||
try {
|
||||
await Api.del('/credentials/' + c.id);
|
||||
@@ -375,7 +397,7 @@ async function deleteCredential(c) {
|
||||
} catch (e) { store.error = e.message; }
|
||||
}
|
||||
async function unbindCredentialOtp(c) {
|
||||
if (!confirm('确认解绑「' + c.site + '」的 2FA?解绑后需重新录入 secret。')) return;
|
||||
if (!await showConfirm('解绑 2FA', '确认解绑「' + c.site + '」的 2FA?解绑后需重新录入 secret。', true)) return;
|
||||
store.error = '';
|
||||
try { await Api.del('/credentials/' + c.id + '/otp'); await loadCredentials(); }
|
||||
catch (e) { store.error = e.message; }
|
||||
@@ -401,6 +423,26 @@ function showToast(text) {
|
||||
if (toast._timer) clearTimeout(toast._timer);
|
||||
toast._timer = setTimeout(() => { toast.show = false; }, 1600);
|
||||
}
|
||||
|
||||
/* ---------------- 全局确认弹窗(替代原生 confirm) ---------------- */
|
||||
function showConfirm(title, message, danger = false) {
|
||||
return new Promise((resolve) => {
|
||||
store.confirmModal = { show: true, title, message, danger, resolve };
|
||||
});
|
||||
}
|
||||
function closeConfirm(result) {
|
||||
store.confirmModal.show = false;
|
||||
if (store.confirmModal.resolve) store.confirmModal.resolve(result);
|
||||
store.confirmModal.resolve = null;
|
||||
}
|
||||
|
||||
/* ---------------- 全局结果弹窗(替代原生 alert) ---------------- */
|
||||
function showResult(title, message, isError = false) {
|
||||
store.resultModal = { show: true, title, message, isError };
|
||||
}
|
||||
function closeResult() {
|
||||
store.resultModal.show = false;
|
||||
}
|
||||
function copyText(text, label) {
|
||||
const done = () => showToast('✓ ' + (label || '内容') + '已复制');
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
@@ -689,7 +731,7 @@ async function saveDnsRecord() {
|
||||
}
|
||||
async function deleteDnsRecord(r) {
|
||||
// 删解析会立刻影响线上访问,必须二次确认而不是直接删
|
||||
if (!window.confirm('确认删除解析记录?\n\n' + r.name + ' ' + r.type + ' ' + r.content + '\n\n删除后该记录立即失效,可能中断线上访问。')) return;
|
||||
if (!await showConfirm('删除解析记录', '确认删除解析记录?\n\n' + r.name + ' ' + r.type + ' ' + r.content + '\n\n删除后该记录立即失效,可能中断线上访问。', true)) return;
|
||||
if (dnsModal.busy) return;
|
||||
dnsModal.busy = true; dnsModal.error = '';
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user