feat: prompt/confirm 替换为自定义弹窗

- shared/utils.js 新增 showConfirm()/showPrompt() 通用函数(inline CSS,无需额外样式)
- admin/common.js 移除重复 showConfirm(),统一使用 utils.js
- admin/posts.js: 3 处 confirm/prompt 替换(审核/退回/锁定)
- admin/comments.js: 1 处 confirm 替换
- settings/index.html: 4 处 confirm + 1 处 prompt 替换
- studio/posts.html + drafts.html: 3 处 confirm 替换
- post-view.js + login.js + post-favorite.js: 3 处 confirm 替换
This commit is contained in:
2026-06-02 20:11:48 +08:00
parent 2e6e7fc057
commit c73b131b83
10 changed files with 318 additions and 243 deletions

View File

@ -783,9 +783,8 @@
showSettingsToast('请输入密码确认', true); showSettingsToast('请输入密码确认', true);
return; return;
} }
if (!confirm('确定要注销账号吗?\n\n注销后 7 天内重新登录可撤销注销,期满后将永久删除。')) { showConfirm('注销账号', '确定要注销账号吗?\n\n注销后 7 天内重新登录可撤销注销,期满后将永久删除。').then(function(ok) {
return; if (!ok) return;
}
deleteAccountBtn.disabled = true; deleteAccountBtn.disabled = true;
deleteAccountBtn.textContent = '注销中...'; deleteAccountBtn.textContent = '注销中...';
@ -815,6 +814,7 @@
reason: deleteReason ? deleteReason.value : '' reason: deleteReason ? deleteReason.value : ''
})); }));
}); });
});
} }
// ===== 登录管理 Tab ===== // ===== 登录管理 Tab =====
@ -829,7 +829,8 @@
btn.addEventListener('click', function () { btn.addEventListener('click', function () {
var sid = btn.getAttribute('data-sid'); var sid = btn.getAttribute('data-sid');
if (!sid) return; if (!sid) return;
if (!confirm('确定要踢出该设备吗?')) return; showConfirm('确认', '确定要踢出该设备吗?').then(function(ok) {
if (!ok) return;
btn.disabled = true; btn.disabled = true;
btn.textContent = '踢出中...'; btn.textContent = '踢出中...';
@ -861,6 +862,7 @@
xhr.send(); xhr.send();
}); });
}); });
});
// ---- 一键踢出其他设备 ---- // ---- 一键踢出其他设备 ----
if (kickOthersBtn) { if (kickOthersBtn) {
@ -870,7 +872,8 @@
showSettingsToast('没有其他在线设备'); showSettingsToast('没有其他在线设备');
return; return;
} }
if (!confirm('确定要踢出所有其他设备(共 ' + otherCount + ' 台)吗?')) return; showConfirm('确认', '确定要踢出所有其他设备(共 ' + otherCount + ' 台)吗?').then(function(ok) {
if (!ok) return;
kickOthersBtn.disabled = true; kickOthersBtn.disabled = true;
kickOthersBtn.textContent = '处理中...'; kickOthersBtn.textContent = '处理中...';
@ -903,6 +906,7 @@
}; };
xhr.send(); xhr.send();
}); });
});
} }
// ---- 备注保存 ---- // ---- 备注保存 ----
@ -1056,7 +1060,8 @@
listEl.querySelectorAll('.fav-folder-del').forEach(function (btn) { listEl.querySelectorAll('.fav-folder-del').forEach(function (btn) {
btn.addEventListener('click', function () { btn.addEventListener('click', function () {
var id = btn.getAttribute('data-id'); var id = btn.getAttribute('data-id');
if (!confirm('确定要删除此收藏夹?其中的收藏内容也将被清除。')) return; showConfirm('确认', '确定要删除此收藏夹?其中的收藏内容也将被清除。').then(function(ok) {
if (!ok) return;
api('DELETE', '/api/folders/' + id) api('DELETE', '/api/folders/' + id)
.then(function (r) { .then(function (r) {
if (r.success) { if (r.success) {
@ -1071,6 +1076,7 @@
}); });
}); });
}); });
});
}) })
.catch(function () { .catch(function () {
listEl.innerHTML = '<div class="energy-log-empty">加载失败,请刷新页面重试</div>'; listEl.innerHTML = '<div class="energy-log-empty">加载失败,请刷新页面重试</div>';
@ -1079,7 +1085,7 @@
if (createBtn) { if (createBtn) {
createBtn.addEventListener('click', function () { createBtn.addEventListener('click', function () {
var name = prompt('请输入收藏夹名称最多50个字符'); showPrompt('请输入收藏夹名称最多50个字符').then(function(name) {
if (!name || !name.trim()) return; if (!name || !name.trim()) return;
if (name.trim().length > 50) { if (name.trim().length > 50) {
showSettingsToast('收藏夹名称不能超过 50 个字符', true); showSettingsToast('收藏夹名称不能超过 50 个字符', true);
@ -1098,6 +1104,7 @@
showSettingsToast('创建失败', true); showSettingsToast('创建失败', true);
}); });
}); });
});
} }
loadFolders(); loadFolders();

View File

@ -59,8 +59,8 @@
<script nonce="{{.CSPNonce}}"> <script nonce="{{.CSPNonce}}">
function submitPost(id) { function submitPost(id) {
if (!confirm('确定要提交这篇草稿进行审核吗?')) return; showConfirm('确认', '确定要提交这篇草稿进行审核吗?').then(function(ok) {
if (!ok) return;
fetch('/api/studio/posts/' + id + '/submit', { fetch('/api/studio/posts/' + id + '/submit', {
method: 'POST', method: 'POST',
headers: { headers: {
@ -76,11 +76,12 @@ function submitPost(id) {
alert(data.message || '提交失败'); alert(data.message || '提交失败');
} }
}); });
});
} }
function deletePost(id) { function deletePost(id) {
if (!confirm('确定要删除这篇草稿吗?此操作不可恢复。')) return; showConfirm('确认', '确定要删除这篇草稿吗?此操作不可恢复。').then(function(ok) {
if (!ok) return;
fetch('/api/studio/posts/' + id, { fetch('/api/studio/posts/' + id, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
@ -96,6 +97,7 @@ function deletePost(id) {
alert(data.message || '删除失败'); alert(data.message || '删除失败');
} }
}); });
});
} }
// 事件委托代替内联 onclick满足 CSP 无 unsafe-inline // 事件委托代替内联 onclick满足 CSP 无 unsafe-inline

View File

@ -80,8 +80,8 @@
<script nonce="{{.CSPNonce}}"> <script nonce="{{.CSPNonce}}">
function deletePost(id) { function deletePost(id) {
if (!confirm('确定要删除这篇文章吗?')) return; showConfirm('确认', '确定要删除这篇文章吗?').then(function(ok) {
if (!ok) return;
fetch('/api/studio/posts/' + id, { fetch('/api/studio/posts/' + id, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
@ -97,6 +97,7 @@ function deletePost(id) {
alert(data.message || '删除失败'); alert(data.message || '删除失败');
} }
}); });
});
} }
// 事件委托代替内联 onclick满足 CSP 无 unsafe-inline // 事件委托代替内联 onclick满足 CSP 无 unsafe-inline

View File

@ -95,9 +95,9 @@
loginInProgress = false; loginInProgress = false;
submitBtn.disabled = false; submitBtn.disabled = false;
submitBtn.textContent = '登录'; submitBtn.textContent = '登录';
if (confirm(resp.message + '\n\n确认登录将撤销注销并恢复账号。')) { showConfirm('确认', resp.message + '\n\n确认登录将撤销注销并恢复账号。').then(function(ok) {
doConfirmRestore(email, password); if (ok) doConfirmRestore(email, password);
} });
} else if (xhr.status === 200 && resp && resp.success) { } else if (xhr.status === 200 && resp && resp.success) {
window.MetaLab.toast(toast, '登录成功', false); window.MetaLab.toast(toast, '登录成功', false);
setTimeout(function () { setTimeout(function () {

View File

@ -119,7 +119,8 @@
function removeFromFolder(folderId) { function removeFromFolder(folderId) {
if (loading) return; if (loading) return;
if (!confirm('确定取消收藏吗?')) return; showConfirm('确认', '确定取消收藏吗?').then(function(ok) {
if (!ok) return;
loading = true; loading = true;
api('DELETE', '/api/folders/' + folderId + '/posts/' + postId) api('DELETE', '/api/folders/' + folderId + '/posts/' + postId)
.then(function(d) { .then(function(d) {
@ -134,6 +135,7 @@
} }
}) })
.catch(function() { loading = false; }); .catch(function() { loading = false; });
});
} }
function createAndAdd(name) { function createAndAdd(name) {

View File

@ -29,7 +29,8 @@
var submitBtn = document.getElementById('submitAuditBtn'); var submitBtn = document.getElementById('submitAuditBtn');
if (submitBtn) { if (submitBtn) {
submitBtn.addEventListener('click', function() { submitBtn.addEventListener('click', function() {
if (!confirm('确认提交审核?审核通过后将公开发布。')) return; showConfirm('确认', '确认提交审核?审核通过后将公开发布。').then(function(ok) {
if (!ok) return;
fetch('/api/posts/' + submitBtn.dataset.id + '/submit', { fetch('/api/posts/' + submitBtn.dataset.id + '/submit', {
method: 'POST', method: 'POST',
headers: { headers: {
@ -44,5 +45,6 @@
}) })
.catch(function() { alert('请求失败'); }); .catch(function() { alert('请求失败'); });
}); });
});
} }
})(); })();

View File

@ -55,9 +55,10 @@
for (var i = 0; i < btns.length; i++) { for (var i = 0; i < btns.length; i++) {
if (btns[i]._bound) continue; if (btns[i]._bound) continue;
btns[i]._bound = true; btns[i]._bound = true;
btns[i].addEventListener('click', function() { btns[i].addEventListener('click', async function() {
var id = this.dataset.id; var id = this.dataset.id;
if (!confirm('确认删除此评论?')) return; var ok = await showConfirm('确认', '确认删除此评论?');
if (!ok) return;
fetch('/api/admin/comments/' + id, { fetch('/api/admin/comments/' + id, {
method: 'DELETE', method: 'DELETE',
headers: { 'X-CSRF-Token': csrfToken } headers: { 'X-CSRF-Token': csrfToken }

View File

@ -37,36 +37,6 @@ const api = {
} }
}; };
// --- 确认弹窗 ---
function showConfirm(title, message) {
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.className = 'modal-overlay show';
overlay.innerHTML = `
<div class="modal-box">
<h3>${escapeHtml(title)}</h3>
<p>${escapeHtml(message)}</p>
<div class="modal-actions">
<button class="btn btn-outline" id="modalCancel">取消</button>
<button class="btn btn-danger" id="modalConfirm">确认</button>
</div>
</div>`;
document.body.appendChild(overlay);
overlay.querySelector('#modalCancel').onclick = () => {
overlay.remove();
resolve(false);
};
overlay.querySelector('#modalConfirm').onclick = () => {
overlay.remove();
resolve(true);
};
overlay.addEventListener('click', (e) => {
if (e.target === overlay) { overlay.remove(); resolve(false); }
});
});
}
// --- 角色标签 --- // --- 角色标签 ---
// 数据源由服务端 model.RoleDisplayNames 注入为 window.__ROLE_NAMES // 数据源由服务端 model.RoleDisplayNames 注入为 window.__ROLE_NAMES
function roleLabel(role) { function roleLabel(role) {

View File

@ -1,48 +1,53 @@
(function() { (function() {
// CSRF token 由 shared/utils.js 的 getCSRFToken() 统一提供 // CSRF token 由 shared/utils.js 的 getCSRFToken() 统一提供
// showConfirm / showPrompt / escapeHtml 由 shared/utils.js 统一提供
// api 对象 (api.get/api.post/api.put) 由 admin/common.js 统一提供 // api 对象 (api.get/api.post/api.put) 由 admin/common.js 统一提供
function refreshPage() { window.location.reload(); } function refreshPage() { window.location.reload(); }
function handleClick(sel, actionFn) { function handleClick(sel, confirmMsg, actionFn) {
var btns = document.querySelectorAll(sel); var btns = document.querySelectorAll(sel);
btns.forEach(function(btn) { btns.forEach(function(btn) {
btn.addEventListener('click', function() { btn.addEventListener('click', async function() {
var id = btn.getAttribute('data-id'); var id = btn.getAttribute('data-id');
if (confirm('确定执行此操作吗?')) { if (confirmMsg) {
actionFn(id); var ok = await showConfirm('确认', confirmMsg);
if (!ok) return;
} }
actionFn(id);
}); });
}); });
} }
// 审核通过 // 审核通过
handleClick('.post-approve', function(id) { handleClick('.post-approve', '确定审核通过此文章吗?', function(id) {
api.post('/api/admin/posts/' + id + '/approve') api.post('/api/admin/posts/' + id + '/approve')
.then(function(d) { if (d.success) refreshPage(); else alert(d.message); }) .then(function(d) { if (d.success) refreshPage(); else alert(d.message); })
.catch(function() { alert('操作失败'); }); .catch(function() { alert('操作失败'); });
}); });
// 退回 // 退回
handleClick('.post-reject', function(id) { handleClick('.post-reject', null, function(id) {
var reason = prompt('请输入退回理由(必填):'); showPrompt('请输入退回理由(必填):').then(function(reason) {
if (!reason || !reason.trim()) return; if (!reason || !reason.trim()) return;
api.post('/api/admin/posts/' + id + '/reject', { reason: reason.trim() }) api.post('/api/admin/posts/' + id + '/reject', { reason: reason.trim() })
.then(function(d) { if (d.success) refreshPage(); else alert(d.message); }) .then(function(d) { if (d.success) refreshPage(); else alert(d.message); })
.catch(function() { alert('操作失败'); }); .catch(function() { alert('操作失败'); });
}); });
});
// 锁定 // 锁定
handleClick('.post-lock', function(id) { handleClick('.post-lock', null, function(id) {
var reason = prompt('请输入锁定理由(必填):'); showPrompt('请输入锁定理由(必填):').then(function(reason) {
if (!reason || !reason.trim()) return; if (!reason || !reason.trim()) return;
api.post('/api/admin/posts/' + id + '/lock', { reason: reason.trim() }) api.post('/api/admin/posts/' + id + '/lock', { reason: reason.trim() })
.then(function(d) { if (d.success) refreshPage(); else alert(d.message); }) .then(function(d) { if (d.success) refreshPage(); else alert(d.message); })
.catch(function() { alert('操作失败'); }); .catch(function() { alert('操作失败'); });
}); });
});
// 解锁 // 解锁
handleClick('.post-unlock', function(id) { handleClick('.post-unlock', '确定解锁此文章吗?', function(id) {
api.post('/api/admin/posts/' + id + '/unlock') api.post('/api/admin/posts/' + id + '/unlock')
.then(function(d) { if (d.success) refreshPage(); else alert(d.message); }) .then(function(d) { if (d.success) refreshPage(); else alert(d.message); })
.catch(function() { alert('操作失败'); }); .catch(function() { alert('操作失败'); });

View File

@ -91,6 +91,91 @@ function api(method, url, data) {
return fetch(url, opts).then(function (r) { return r.json(); }); return fetch(url, opts).then(function (r) { return r.json(); });
} }
/**
* 确认弹窗 — 替代原生 confirm(),统一全站风格
* @param {string} title — 弹窗标题
* @param {string} message — 提示消息(支持 \n 换行)
* @returns {Promise<boolean>} 用户确认返回 true取消返回 false
*/
function showConfirm(title, message) {
return new Promise(function(resolve) {
var safeTitle = escapeHtml(title);
var safeMsg = escapeHtml(message).replace(/\n/g, '<br>');
var overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:9998;display:flex;align-items:center;justify-content:center;';
overlay.innerHTML = '<div style="background:#fff;border-radius:8px;padding:28px 32px;min-width:320px;max-width:440px;box-shadow:0 8px 30px rgba(0,0,0,0.15);">'
+ '<h3 style="margin:0 0 12px 0;font-size:17px;color:#2d3436;">' + safeTitle + '</h3>'
+ '<p style="margin:0 0 20px 0;font-size:14px;line-height:1.6;color:#636e72;">' + safeMsg + '</p>'
+ '<div style="display:flex;gap:10px;justify-content:flex-end;">'
+ '<button id="mlbModalCancel" style="padding:8px 20px;border:1px solid #d1d5db;border-radius:6px;background:#fff;color:#374151;font-size:14px;cursor:pointer;">取消</button>'
+ '<button id="mlbModalConfirm" style="padding:8px 20px;border:none;border-radius:6px;background:#dc2626;color:#fff;font-size:14px;cursor:pointer;">确认</button>'
+ '</div></div>';
document.body.appendChild(overlay);
function close(val) {
overlay.remove();
resolve(val);
}
overlay.querySelector('#mlbModalCancel').onclick = function() { close(false); };
overlay.querySelector('#mlbModalConfirm').onclick = function() { close(true); };
overlay.addEventListener('click', function(e) {
if (e.target === overlay) close(false);
});
// Enter 键确认
var onKey = function(e) {
if (e.key === 'Enter') { document.removeEventListener('keydown', onKey); close(true); }
if (e.key === 'Escape') { document.removeEventListener('keydown', onKey); close(false); }
};
document.addEventListener('keydown', onKey);
});
}
/**
* 输入弹窗 — 替代原生 prompt(),统一全站风格
* @param {string} message — 提示文本
* @param {string} [defaultValue=''] — 输入框默认值
* @returns {Promise<string|null>} 用户输入的值,取消返回 null
*/
function showPrompt(message, defaultValue) {
return new Promise(function(resolve) {
var safeMsg = escapeHtml(message);
var overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:9998;display:flex;align-items:center;justify-content:center;';
overlay.innerHTML = '<div style="background:#fff;border-radius:8px;padding:28px 32px;min-width:320px;max-width:440px;box-shadow:0 8px 30px rgba(0,0,0,0.15);">'
+ '<p style="margin:0 0 16px 0;font-size:14px;line-height:1.6;color:#2d3436;">' + safeMsg + '</p>'
+ '<input id="mlbPromptInput" type="text" style="width:100%;padding:10px 12px;border:1px solid #d1d5db;border-radius:6px;font-size:14px;color:#2d3436;outline:none;box-sizing:border-box;" value="' + escapeHtml(defaultValue || '') + '" placeholder="">'
+ '<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:16px;">'
+ '<button id="mlbPromptCancel" style="padding:8px 20px;border:1px solid #d1d5db;border-radius:6px;background:#fff;color:#374151;font-size:14px;cursor:pointer;">取消</button>'
+ '<button id="mlbPromptConfirm" style="padding:8px 20px;border:none;border-radius:6px;background:#0984e3;color:#fff;font-size:14px;cursor:pointer;">确认</button>'
+ '</div></div>';
document.body.appendChild(overlay);
var input = overlay.querySelector('#mlbPromptInput');
function close(val) {
overlay.remove();
resolve(val);
}
overlay.querySelector('#mlbPromptCancel').onclick = function() { close(null); };
overlay.querySelector('#mlbPromptConfirm').onclick = function() { close(input.value); };
overlay.addEventListener('click', function(e) {
if (e.target === overlay) close(null);
});
// Enter 确认Escape 取消
var onKey = function(e) {
if (e.key === 'Enter') { document.removeEventListener('keydown', onKey); close(input.value); }
if (e.key === 'Escape') { document.removeEventListener('keydown', onKey); close(null); }
};
document.addEventListener('keydown', onKey);
// 自动聚焦输入框
setTimeout(function() { input.focus(); input.select(); }, 50);
});
}
// 全局头像图片加载失败回退(替代 onerror 内联事件,满足 CSP 无 unsafe-inline // 全局头像图片加载失败回退(替代 onerror 内联事件,满足 CSP 无 unsafe-inline
document.addEventListener('error', function(e) { document.addEventListener('error', function(e) {
var img = e.target; var img = e.target;