db7648eb4d
- 新增微信小程序版个人收藏夹全套功能,包含聚合首页、收藏列表、编辑、详情页面与配套后端API - 新增web版时光助手门户与个人收藏夹工具,支持与小程序共享后端数据 - 优化日历提醒工具:替换本地bootstrap资源为公共CDN版本,新增批量生成循环提醒的前端界面与后端逻辑,新增数据库batch_id字段,更新鉴权配置文件 - 补充项目配置、站点地图、调试诊断脚本等配套文件 - 完善各模块的页面配置与样式文件
213 lines
8.9 KiB
JavaScript
213 lines
8.9 KiB
JavaScript
(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, '"').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 =
|
|
'<div class="modal-card">' +
|
|
'<div class="modal-title' + (danger ? ' danger' : '') + '">' + escapeHtml(title) + '</div>' +
|
|
'<div class="modal-desc">' + escapeHtml(desc) + '</div>' +
|
|
'<div class="modal-actions">' +
|
|
'<button type="button" class="btn secondary btn-sm" data-act="cancel">' + escapeHtml(cancelText) + '</button>' +
|
|
'<button type="button" class="btn ' + (danger ? 'danger' : 'primary') + ' btn-sm" data-act="ok">' + escapeHtml(okText) + '</button>' +
|
|
'</div>' +
|
|
'</div>'
|
|
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 =
|
|
'<div class="id-title">' +
|
|
'<span>' + escapeHtml(name) + ' · 交互详情</span>' +
|
|
'<span style="cursor:pointer;color:#8a8aa8;" data-act="copy">复制</span>' +
|
|
'</div>' +
|
|
'<div class="id-body">' + escapeHtml(logs.join('\n') + (timer ? `\n\n倒计时关闭:${remainSec}s` : '') + `\n\n耗时:${elapsedSec}s`) + '</div>' +
|
|
'<div class="id-actions">' +
|
|
'<span class="id-countdown">' + (timer ? `自动关闭中:${remainSec}s` : (forceStay ? '已停留,有错误/异常' : '已完成')) + '</span>' +
|
|
'<div style="display:flex;gap:8px;">' +
|
|
(timer ? '<button type="button" class="btn ghost btn-sm" data-act="stay">停留</button>' : '') +
|
|
'<button type="button" class="btn secondary btn-sm" data-act="close">关闭</button>' +
|
|
'</div>' +
|
|
'</div>'
|
|
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
|
|
}
|
|
})()
|