(function () { const DEFAULT_UI_MODE = 'debug' const AUTO_CLOSE_SEC_DEBUG = 3 const AUTO_CLOSE_SEC_NORMAL = 2 function readUiMode() { try { const v = localStorage.getItem('mp_ta_ui_mode') if (v === 'debug' || v === 'normal') return v } catch {} return DEFAULT_UI_MODE } function setUiMode(mode) { try { localStorage.setItem('mp_ta_ui_mode', mode) } catch {} } function escapeHtml(s) { if (s === null || s === undefined) return '' return String(s) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, ''') } function showToast(text, type, duration) { type = type || 'info' duration = duration || (type === 'error' ? 3000 : 2000) const colors = { info: { bg: 'rgba(56, 103, 214, 0.92)', border: 'rgba(108, 122, 224, 0.6)', color: '#fff' }, success: { bg: 'rgba(0, 180, 140, 0.92)', border: 'rgba(0, 216, 167, 0.6)', color: '#fff' }, error: { bg: 'rgba(220, 53, 69, 0.92)', border: 'rgba(255, 80, 100, 0.6)', color: '#fff' }, warn: { bg: 'rgba(230, 152, 20, 0.92)', border: 'rgba(255, 193, 7, 0.6)', color: '#fff' } } const c = colors[type] || colors.info const t = document.createElement('div') t.textContent = text Object.assign(t.style, { position: 'fixed', zIndex: 99998, top: '28px', left: '50%', transform: 'translateX(-50%)', padding: '10px 18px', borderRadius: '12px', fontSize: '14px', fontWeight: 500, maxWidth: '90vw', textAlign: 'center', backdropFilter: 'blur(8px)', background: c.bg, border: '1px solid ' + c.border, color: c.color, boxShadow: '0 10px 30px rgba(0,0,0,0.35)' }) document.body.appendChild(t) setTimeout(() => { t.style.transition = 'opacity 0.3s ease, transform 0.3s ease' t.style.opacity = '0' t.style.transform = 'translateX(-50%) translateY(-10px)' setTimeout(() => t.remove(), 320) }, duration) } function confirmDialog(options, onOk, onCancel) { options = options || {} const title = options.title || '请确认操作' const desc = options.desc || '' const okText = options.okText || '确认' const cancelText = options.cancelText || '取消' const danger = options.danger === true const slot = document.getElementById('modal-slot') || document.body const mask = document.createElement('div') mask.className = 'modal-mask' mask.innerHTML = '' slot.appendChild(mask) const close = (result) => { mask.remove() if (result && typeof onOk === 'function') onOk() if (!result && typeof onCancel === 'function') onCancel() } mask.addEventListener('click', (e) => { if (e.target === mask) close(false) const act = e.target.getAttribute && e.target.getAttribute('data-act') if (act === 'ok') close(true) if (act === 'cancel') close(false) }) const okBtn = mask.querySelector('[data-act="ok"]') if (okBtn) okBtn.focus() return { close } } function runTrackedInteraction(options, runner) { const uiMode = readUiMode() options = options || {} const name = options.name || '操作' const toastOnSuccess = options.toastOnSuccess !== false const normalViewAsToast = options.normalViewAsToast !== false const debugMode = uiMode === 'debug' const dlg = document.getElementById('interaction-dialog') || (() => { const d = document.createElement('div') d.id = 'interaction-dialog' d.className = 'interaction-dialog hidden' document.body.appendChild(d) return d })() const startedAt = Date.now() const logs = [] let timer = null let forceStay = false const fmtTime = (ts) => { const d = new Date(ts) const p = (n) => (n < 10 ? '0' + n : '' + n) return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}` } const push = (line) => logs.push(`[${fmtTime(Date.now())}] ${line}`) const render = () => { const elapsedSec = ((Date.now() - startedAt) / 1000).toFixed(1) dlg.innerHTML = '
' + '' + escapeHtml(name) + ' · 交互详情' + '复制' + '
' + '
' + escapeHtml(logs.join('\n') + (timer ? `\n\n倒计时关闭:${remainSec}s` : '') + `\n\n耗时:${elapsedSec}s`) + '
' + '
' + '' + (timer ? `自动关闭中:${remainSec}s` : (forceStay ? '已停留,有错误/异常' : '已完成')) + '' + '
' + (timer ? '' : '') + '' + '
' + '
' const btnCopy = dlg.querySelector('[data-act="copy"]') if (btnCopy) btnCopy.onclick = () => { try { const txt = logs.join('\n') + `\n\n耗时:${elapsedSec}s` if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(txt).then(() => showToast('日志已复制', 'success', 1200)) } else { const ta = document.createElement('textarea') ta.value = txt; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove() showToast('日志已复制', 'success', 1200) } } catch (e) { showToast('复制失败:' + e.message, 'error', 2000) } } const btnStay = dlg.querySelector('[data-act="stay"]') if (btnStay) btnStay.onclick = () => { if (timer) { clearInterval(timer); timer = null; forceStay = true; render() } } const btnClose = dlg.querySelector('[data-act="close"]') if (btnClose) btnClose.onclick = () => { dlg.classList.add('hidden'); if (timer) clearInterval(timer); timer = null } } const showDlg = () => { dlg.classList.remove('hidden'); render() } const closeDlg = () => { dlg.classList.add('hidden'); if (timer) { clearInterval(timer); timer = null } } let remainSec = debugMode ? AUTO_CLOSE_SEC_DEBUG : AUTO_CLOSE_SEC_NORMAL const startCountdown = () => { if (forceStay) return timer = setInterval(() => { remainSec -= 1 if (remainSec <= 0) { closeDlg() } else { render() } }, 1000) } push(`开始:${name}`) push(`HTTP: ${options.method || 'GET'} ${options.path || options.url || '(本地)'}${options.params ? `\n参数:${JSON.stringify(options.params)}` : ''}`) showDlg() const runnerPromise = typeof runner === 'function' ? runner() : Promise.resolve(runner || { ok: true }) return Promise.resolve(runnerPromise).then((res) => { push(`返回:${JSON.stringify(res, null, 2).slice(0, 2000)}`) push(`状态:成功 ✔ 耗时 ${((Date.now()-startedAt)/1000).toFixed(2)}s`) render() if (debugMode) { startCountdown() } else if (normalViewAsToast && toastOnSuccess) { setTimeout(() => closeDlg(), 120); showToast(`${name}成功`, 'success') } else { startCountdown() } return res }).catch((e) => { forceStay = true push(`错误:${e && e.message ? e.message : String(e)}`) push(`状态:失败 ✘ 耗时 ${((Date.now()-startedAt)/1000).toFixed(2)}s`) render() showToast(`${name}失败:${(e && e.message) ? e.message : '未知错误'}`, 'error', 3500) throw e }) } function formatTime(iso) { if (!iso) return '' const d = new Date(iso) if (isNaN(d.getTime())) return String(iso) const p = (n) => (n < 10 ? '0' + n : '' + n) const now = new Date() const sameYear = d.getFullYear() === now.getFullYear() const y = sameYear ? '' : d.getFullYear() + '-' return `${y}${p(d.getMonth()+1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}` } function humanSize(bytes) { const n = Number(bytes || 0) if (n < 1024) return n + ' B' if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB' return (n / 1024 / 1024).toFixed(2) + ' MB' } window.__MPTA_DIALOG__ = { readUiMode, setUiMode, escapeHtml, showToast, confirmDialog, runTrackedInteraction, formatTime, humanSize, AUTO_CLOSE_SEC_DEBUG, AUTO_CLOSE_SEC_NORMAL } })()