feat(mp, web, calendar): 新增微信收藏夹与日历批量生成功能
- 新增微信小程序版个人收藏夹全套功能,包含聚合首页、收藏列表、编辑、详情页面与配套后端API - 新增web版时光助手门户与个人收藏夹工具,支持与小程序共享后端数据 - 优化日历提醒工具:替换本地bootstrap资源为公共CDN版本,新增批量生成循环提醒的前端界面与后端逻辑,新增数据库batch_id字段,更新鉴权配置文件 - 补充项目配置、站点地图、调试诊断脚本等配套文件 - 完善各模块的页面配置与样式文件
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
(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
|
||||
}
|
||||
})()
|
||||
Reference in New Issue
Block a user