From dd6efd0c5068f65afbb08ea9d7af467be6077edb Mon Sep 17 00:00:00 2001 From: Victor_Jay Date: Tue, 2 Jun 2026 21:45:00 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20utils.js=20?= =?UTF-8?q?=E4=B8=AD=20showConfirm/showPrompt/showToast=20=E7=9A=84=20CSP?= =?UTF-8?q?=20style-src=20=E8=BF=9D=E8=A7=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - showToast: style.cssText 替换为逐个 style.property 赋值 - showConfirm: 移除 style.cssText 和 innerHTML 中的 style 属性,改用 createElement + setStyles 辅助函数 + addEventListener - showPrompt: 同上,移除所有 inline style 和 onclick - 新增 setStyles 辅助函数,批量设置单个 style 属性以满足 CSP 要求 --- templates/shared/static/js/utils.js | 157 +++++++++++++++++++++++----- 1 file changed, 133 insertions(+), 24 deletions(-) diff --git a/templates/shared/static/js/utils.js b/templates/shared/static/js/utils.js index e5e3296..b6a03f2 100644 --- a/templates/shared/static/js/utils.js +++ b/templates/shared/static/js/utils.js @@ -57,13 +57,25 @@ function showToast(message, type) { if (!container) { container = document.createElement('div'); container.className = 'toast-container'; - container.style.cssText = 'position:fixed;top:20px;right:20px;z-index:9999;display:flex;flex-direction:column;gap:8px;pointer-events:none'; + container.style.position = 'fixed'; + container.style.top = '20px'; + container.style.right = '20px'; + container.style.zIndex = '9999'; + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.gap = '8px'; + container.style.pointerEvents = 'none'; document.body.appendChild(container); } var toast = document.createElement('div'); toast.className = 'toast toast-' + type; toast.textContent = message; - toast.style.cssText = 'background:' + bg + ';color:#fff;padding:10px 24px;border-radius:6px;font-size:14px;pointer-events:auto'; + toast.style.background = bg; + toast.style.color = '#fff'; + toast.style.padding = '10px 24px'; + toast.style.borderRadius = '6px'; + toast.style.fontSize = '14px'; + toast.style.pointerEvents = 'auto'; container.appendChild(toast); setTimeout(function () { toast.style.opacity = '0'; @@ -91,6 +103,15 @@ function api(method, url, data) { return fetch(url, opts).then(function (r) { return r.json(); }); } +/** + * 批量设置元素 style 属性(满足 CSP style-src 限制,避免 cssText 被拦截) + * @param {HTMLElement} el — 目标元素 + * @param {object} styles — 键值对 { property: value } + */ +function setStyles(el, styles) { + Object.keys(styles).forEach(function(k) { el.style[k] = styles[k]; }); +} + /** * 确认弹窗 — 替代原生 confirm(),统一全站风格 * @param {string} title — 弹窗标题 @@ -102,15 +123,59 @@ function showConfirm(title, message) { var safeTitle = escapeHtml(title); var safeMsg = escapeHtml(message).replace(/\n/g, '
'); + // 遮罩层 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 = '
' - + '

' + safeTitle + '

' - + '

' + safeMsg + '

' - + '
' - + '' - + '' - + '
'; + setStyles(overlay, { + position: 'fixed', inset: '0', background: 'rgba(0,0,0,0.4)', + zIndex: '9998', display: 'flex', alignItems: 'center', justifyContent: 'center' + }); + + // 弹窗容器 + var box = document.createElement('div'); + setStyles(box, { + background: '#fff', borderRadius: '8px', padding: '28px 32px', + minWidth: '320px', maxWidth: '440px', + boxShadow: '0 8px 30px rgba(0,0,0,0.15)' + }); + + // 标题 + var h3 = document.createElement('h3'); + setStyles(h3, { margin: '0 0 12px 0', fontSize: '17px', color: '#2d3436' }); + h3.textContent = safeTitle; + box.appendChild(h3); + + // 消息 + var p = document.createElement('p'); + setStyles(p, { margin: '0 0 20px 0', fontSize: '14px', lineHeight: '1.6', color: '#636e72' }); + p.innerHTML = safeMsg; // 已转义,安全 + box.appendChild(p); + + // 按钮容器 + var btnRow = document.createElement('div'); + setStyles(btnRow, { display: 'flex', gap: '10px', justifyContent: 'flex-end' }); + + // 取消按钮 + var btnCancel = document.createElement('button'); + setStyles(btnCancel, { + padding: '8px 20px', border: '1px solid #d1d5db', borderRadius: '6px', + background: '#fff', color: '#374151', fontSize: '14px', cursor: 'pointer' + }); + btnCancel.textContent = '取消'; + btnCancel.addEventListener('click', function() { close(false); }); + btnRow.appendChild(btnCancel); + + // 确认按钮 + var btnConfirm = document.createElement('button'); + setStyles(btnConfirm, { + padding: '8px 20px', border: 'none', borderRadius: '6px', + background: '#dc2626', color: '#fff', fontSize: '14px', cursor: 'pointer' + }); + btnConfirm.textContent = '确认'; + btnConfirm.addEventListener('click', function() { close(true); }); + btnRow.appendChild(btnConfirm); + + box.appendChild(btnRow); + overlay.appendChild(box); document.body.appendChild(overlay); function close(val) { @@ -118,8 +183,6 @@ function showConfirm(title, message) { 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); }); @@ -142,26 +205,72 @@ 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 = '
' - + '

' + safeMsg + '

' - + '' - + '
' - + '' - + '' - + '
'; - document.body.appendChild(overlay); + setStyles(overlay, { + position: 'fixed', inset: '0', background: 'rgba(0,0,0,0.4)', + zIndex: '9998', display: 'flex', alignItems: 'center', justifyContent: 'center' + }); - var input = overlay.querySelector('#mlbPromptInput'); + // 弹窗容器 + var box = document.createElement('div'); + setStyles(box, { + background: '#fff', borderRadius: '8px', padding: '28px 32px', + minWidth: '320px', maxWidth: '440px', + boxShadow: '0 8px 30px rgba(0,0,0,0.15)' + }); + + // 消息 + var p = document.createElement('p'); + setStyles(p, { margin: '0 0 16px 0', fontSize: '14px', lineHeight: '1.6', color: '#2d3436' }); + p.innerHTML = safeMsg; + box.appendChild(p); + + // 输入框 + var input = document.createElement('input'); + input.type = 'text'; + input.id = 'mlbPromptInput'; + setStyles(input, { + width: '100%', padding: '10px 12px', border: '1px solid #d1d5db', + borderRadius: '6px', fontSize: '14px', color: '#2d3436', + outline: 'none', boxSizing: 'border-box' + }); + input.value = escapeHtml(defaultValue || ''); + box.appendChild(input); + + // 按钮容器 + var btnRow = document.createElement('div'); + setStyles(btnRow, { display: 'flex', gap: '10px', justifyContent: 'flex-end', marginTop: '16px' }); + + // 取消按钮 + var btnCancel = document.createElement('button'); + setStyles(btnCancel, { + padding: '8px 20px', border: '1px solid #d1d5db', borderRadius: '6px', + background: '#fff', color: '#374151', fontSize: '14px', cursor: 'pointer' + }); + btnCancel.textContent = '取消'; + btnCancel.addEventListener('click', function() { close(null); }); + btnRow.appendChild(btnCancel); + + // 确认按钮 + var btnConfirm = document.createElement('button'); + setStyles(btnConfirm, { + padding: '8px 20px', border: 'none', borderRadius: '6px', + background: '#0984e3', color: '#fff', fontSize: '14px', cursor: 'pointer' + }); + btnConfirm.textContent = '确认'; + btnConfirm.addEventListener('click', function() { close(input.value); }); + btnRow.appendChild(btnConfirm); + + box.appendChild(btnRow); + overlay.appendChild(box); + document.body.appendChild(overlay); 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); });