// ==UserScript== // @name 日报助手 - ask4help PM系统 // @namespace https://ask4help.bbboc.com // @version 5.1 // @description 多选日期 → 勾选任务 → 填写内容 → 批量提交到已用工时 // @author Claude // @match https://ask4help.bbboc.com:38880/* // @run-at document-start // @grant GM_getValue // @grant GM_setValue // @license MIT // ==/UserScript== (function () { 'use strict'; // ==================== Token 自动捕获 ==================== let _TOKEN = GM_getValue('dr_token', ''); const _origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function (body) { if (!_TOKEN && typeof body === 'string' && body.includes('arg0=')) { const m = body.match(/arg0=%22([a-f0-9]+)%22/); if (m) { _TOKEN = m[1]; GM_setValue('dr_token', m[1]); } } return _origSend.apply(this, arguments); }; const _origWS = window.WebSocket; window.WebSocket = function (url, protocols) { if (!_TOKEN && typeof url === 'string' && url.includes('/event/')) { const m = url.match(/\/event\/([a-f0-9]+)/); if (m) { _TOKEN = m[1]; GM_setValue('dr_token', m[1]); } } return protocols !== undefined ? new _origWS(url, protocols) : new _origWS(url); }; window.WebSocket.prototype = _origWS.prototype; // ==================== 工具函数 ==================== const fmtDate = d => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; const weekDay = d => ['日','一','二','三','四','五','六'][d.getDay()]; const esc = s => s ? String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"') : ''; const isWeekend = d => { const w = d.getDay(); return w === 0 || w === 6; }; async function ensureToken() { if (_TOKEN) return _TOKEN; try { for (const st of [sessionStorage, localStorage]) { for (const key of ['token', 'vuex', 'store']) { const raw = st.getItem(key); if (!raw) continue; if (/^[a-f0-9]{32,36}$/.test(raw)) { _TOKEN = raw; GM_setValue('dr_token', raw); return raw; } try { const obj = JSON.parse(raw); const t = deepFind(obj, 'token'); if (t) { _TOKEN = t; GM_setValue('dr_token', t); return t; } } catch (_) {} } } } catch (_) {} return new Promise((resolve, reject) => { const start = Date.now(); (function check() { if (_TOKEN) { resolve(_TOKEN); return; } if (Date.now() - start > 10000) { reject(new Error('获取Token失败,请在设置中手动填写')); return; } setTimeout(check, 300); })(); }); } function deepFind(obj, key) { if (!obj || typeof obj !== 'object') return null; for (const [k, v] of Object.entries(obj)) { if (k === key && typeof v === 'string' && /^[a-f0-9]{32,36}$/.test(v)) return v; if (typeof v === 'object' && v !== null) { const r = deepFind(v, key); if (r) return r; } } return null; } async function apiCall(method, arg1) { const token = await ensureToken(); let body = 'arg0=' + encodeURIComponent(JSON.stringify(token)); if (arg1 !== undefined) body += '&arg1=' + encodeURIComponent(JSON.stringify(arg1)); const resp = await fetch('/p/api/invoke/' + method, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body, credentials: 'include' }); if (!resp.ok) throw new Error(`${method} ${resp.status}`); const text = await resp.text(); return text && text !== 'null' ? JSON.parse(text) : null; } // ==================== 日期生成 ==================== // 本月所有日期作为默认 function getDefaultDates() { const dates = []; const now = new Date(); const firstDay = new Date(now.getFullYear(), now.getMonth(), 1); const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0); const cur = new Date(firstDay); while (cur <= lastDay) { dates.push(new Date(cur)); cur.setDate(cur.getDate() + 1); } return dates; } function getThisWeek() { const dates = [], now = new Date(); now.setHours(0, 0, 0, 0); const dow = now.getDay(); const mon = new Date(now); mon.setDate(now.getDate() - (dow === 0 ? 6 : dow - 1)); for (let i = 0; i < 7; i++) { const d = new Date(mon); d.setDate(d.getDate() + i); dates.push(d); } return dates; } function getLastWeek() { const dates = [], now = new Date(); now.setHours(0, 0, 0, 0); const dow = now.getDay(); const mon = new Date(now); mon.setDate(now.getDate() - (dow === 0 ? 6 : dow - 1) - 7); for (let i = 0; i < 7; i++) { const d = new Date(mon); d.setDate(d.getDate() + i); dates.push(d); } return dates; } // ==================== UI 样式 ==================== function injectStyle() { const s = document.createElement('style'); s.textContent = ` #dr-w{position:fixed;right:18px;bottom:80px;z-index:99999;display:flex;flex-direction:column;gap:8px;} #dr-w .dr-fab{width:50px;height:50px;border-radius:50%;border:none;cursor:pointer;font-size:22px;color:#fff;box-shadow:0 4px 14px rgba(0,0,0,.3);transition:.15s;text-align:center;line-height:50px;padding:0;} #dr-w .dr-fab:hover{transform:scale(1.12);} #dr-fab-open{background:linear-gradient(135deg,#409EFF,#337ECC);} #dr-fab-set{background:linear-gradient(135deg,#909399,#777);} .dr-overlay{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:100000;display:flex;align-items:center;justify-content:center;} .dr-panel{background:#fff;border-radius:12px;width:960px;max-width:97vw;max-height:92vh;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,.3);} .dr-panel .dr-hd{padding:12px 18px;border-bottom:1px solid #ebeef5;display:flex;justify-content:space-between;align-items:center;gap:10px;flex-shrink:0;} .dr-panel .dr-hd h3{margin:0;font-size:16px;color:#303133;} .dr-panel .dr-bd{padding:14px 18px;overflow-y:auto;flex:1;max-height:calc(92vh - 120px);} .dr-panel .dr-ft{padding:10px 18px;border-top:1px solid #ebeef5;display:flex;gap:8px;justify-content:flex-end;flex-shrink:0;} .dr-btn{padding:8px 16px;border-radius:6px;border:none;cursor:pointer;font-size:13px;transition:.15s;white-space:nowrap;} .dr-btn:hover{opacity:.85;} .dr-btn:disabled{opacity:.35;cursor:not-allowed;} .dr-b-blue{background:#409EFF;color:#fff;} .dr-b-green{background:#67C23A;color:#fff;} .dr-b-orange{background:#E6A23C;color:#fff;} .dr-b-red{background:#F56C6C;color:#fff;} .dr-b-gray{background:#f5f7fa;color:#606266;border:1px solid #dcdfe6;} .dr-b-sm{padding:3px 8px;font-size:11px;} .dr-toast{position:fixed;top:16px;left:50%;transform:translateX(-50%);padding:12px 24px;border-radius:8px;font-size:14px;z-index:100010;box-shadow:0 4px 16px rgba(0,0,0,.22);animation:drFd .3s ease;} .dr-toast.ok{background:#67C23A;color:#fff;} .dr-toast.err{background:#F56C6C;color:#fff;} .dr-toast.info{background:#409EFF;color:#fff;} @keyframes drFd{from{opacity:0;transform:translateX(-50%) translateY(-16px);}to{opacity:1;transform:translateX(-50%) translateY(0);}} .dr-summary{display:flex;gap:16px;flex-wrap:wrap;font-size:12px;color:#606266;margin-bottom:10px;padding:8px 12px;background:#f5f7fa;border-radius:6px;} .dr-summary b{color:#303133;} .dr-date-bar{display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin-bottom:10px;padding:8px 12px;background:#f5f7fa;border-radius:8px;} .dr-date-bar .dr-date-tag{display:inline-flex;align-items:center;gap:4px;padding:4px 9px;border-radius:14px;font-size:11px;cursor:pointer;border:2px solid transparent;transition:.15s;background:#fff;color:#606266;white-space:nowrap;user-select:none;} .dr-date-bar .dr-date-tag.checked{background:#409EFF;color:#fff;border-color:#409EFF;font-weight:600;} .dr-date-bar .dr-date-tag:hover{border-color:#409EFF;} .dr-date-bar .dr-ctrl{display:flex;gap:4px;align-items:center;flex-wrap:wrap;} .dr-date-bar .dr-ctrl button{padding:4px 9px;border-radius:4px;border:1px solid #dcdfe6;background:#fff;cursor:pointer;font-size:11px;white-space:nowrap;} .dr-date-bar .dr-ctrl button:hover{border-color:#409EFF;color:#409EFF;} .dr-task-list{border:1px solid #e8e8e8;border-radius:8px;overflow:hidden;} .dr-task-list .dr-task-hd{padding:8px 12px;background:#fafafa;border-bottom:1px solid #e8e8e8;display:flex;align-items:center;gap:12px;font-size:12px;color:#606266;} .dr-task-row{display:flex;gap:8px;padding:10px 12px;border-bottom:1px solid #f0f0f0;align-items:flex-start;} .dr-task-row:last-child{border-bottom:none;} .dr-task-row input[type=checkbox]{width:16px;height:16px;cursor:pointer;accent-color:#409EFF;flex-shrink:0;margin-top:1px;} .dr-task-info{min-width:210px;max-width:260px;flex-shrink:0;} .dr-task-info .name{font-weight:600;font-size:13px;color:#303133;margin-bottom:2px;word-break:break-all;} .dr-task-info .meta{font-size:11px;color:#909399;display:flex;flex-wrap:wrap;gap:4px;align-items:center;} .dr-tag{display:inline-block;padding:1px 6px;border-radius:3px;color:#fff;font-size:10px;} .dr-task-input{flex:1;display:flex;flex-direction:column;gap:4px;min-width:180px;} .dr-task-input textarea{width:100%;height:50px;border:1px solid #dcdfe6;border-radius:6px;padding:6px 10px;font-size:12px;font-family:inherit;resize:vertical;box-sizing:border-box;} .dr-task-input textarea:focus{outline:none;border-color:#409EFF;} .dr-task-input .sub-row{display:flex;gap:8px;align-items:center;font-size:12px;color:#606266;} .dr-task-input .sub-row input{width:50px;padding:3px 6px;border:1px solid #dcdfe6;border-radius:4px;font-size:12px;text-align:center;} .dr-task-input .sub-row input:focus{outline:none;border-color:#409EFF;} .dr-settings-row{display:flex;align-items:center;gap:10px;margin-bottom:14px;} .dr-settings-row label{width:80px;text-align:right;font-weight:600;font-size:13px;} .dr-settings-row input{flex:1;padding:8px 12px;border:1px solid #dcdfe6;border-radius:6px;font-size:13px;font-family:monospace;} .dr-settings-row input:focus{outline:none;border-color:#409EFF;} .dr-progress{height:4px;background:#ebeef5;border-radius:2px;margin:8px 0;overflow:hidden;} .dr-progress-bar{height:100%;background:#67C23A;transition:width .3s;border-radius:2px;width:0;} .spin{display:inline-block;width:14px;height:14px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:drSpin .8s linear infinite;vertical-align:middle;} @keyframes drSpin{to{transform:rotate(360deg);}} .dr-hint{font-size:12px;color:#E6A23C;margin:4px 0;} `; document.head.appendChild(s); } function injectFab() { const w = document.createElement('div'); w.id = 'dr-w'; w.innerHTML = ` `; document.body.appendChild(w); document.getElementById('dr-fab-open').onclick = openPanel; document.getElementById('dr-fab-set').onclick = openSettings; } function toast(msg, type) { document.querySelectorAll('.dr-toast').forEach(e => e.remove()); const el = document.createElement('div'); el.className = 'dr-toast ' + (type || 'ok'); el.textContent = msg; document.body.appendChild(el); setTimeout(() => el.remove(), type === 'err' ? 6000 : 2200); } // ==================== 主面板 ==================== let _taskList = []; let _userName = ''; let _selDates = getDefaultDates(); async function openPanel() { toast('⏳ 加载中...', 'info'); const overlay = document.createElement('div'); overlay.className = 'dr-overlay'; overlay.id = 'dr-panel'; overlay.innerHTML = `

📋 日报 - 批量填报

加载中...
`; document.body.appendChild(overlay); overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); }); try { const [loginInfo, todos] = await Promise.all([ apiCall('BizAction.getLoginInfo'), apiCall('BizAction.getTodoTaskInfoList'), ]); _userName = loginInfo ? (loginInfo.name || loginInfo.userName || '未知') : '未知'; _taskList = Array.isArray(todos) ? todos : []; renderPanel(overlay); } catch (e) { overlay.querySelector('.dr-bd').innerHTML = `

❌ ${esc(e.message)}

请确认已登录PM系统,或点击 ⚙ 手动填写Token

`; } } function renderPanel(overlay) { const activeTasks = _taskList.filter(t => !t.isFinish && !t.isDelete); // 日期标签 — 周末默认不勾选 let dateTags = ''; _selDates.forEach((d, i) => { const ds = fmtDate(d); const wd = '周' + weekDay(d); const isToday = fmtDate(new Date()) === ds; const weekend = isWeekend(d); const checked = !weekend ? ' checked' : ''; dateTags += ` ${ds.slice(5)} ${wd}${weekend ? ' 🔴' : ''} `; }); // 任务行 — 带复选框 let tasksHtml = ''; if (activeTasks.length === 0) { tasksHtml = '

暂无进行中的任务

'; } else { activeTasks.forEach((task, i) => { const statusBg = task.statusColor || '#909399'; const priorityBg = task.priorityColor || '#C0C4CC'; const wTime = task.workTime || 0; tasksHtml += `
${esc(task.statusName || '-')} ${esc(task.priorityName || '-')} 📂 ${esc(task.projectName || '-')} ⏱ ${wTime}h
`; }); } // 初始选中统计 const dayCount = overlay.querySelectorAll('.dr-date-tag.checked').length || _selDates.filter(d => !isWeekend(d)).length; const taskCount = activeTasks.length; overlay.querySelector('.dr-panel').innerHTML = `

📋 日报填报

👤 ${esc(_userName)} | ${taskCount}个任务
📅 日期:${dayCount}/${_selDates.length} 天 📊 任务:${taskCount}/${taskCount} 个 📝 提交:${dayCount * taskCount}
日期: ${dateTags}
💡 勾选任务 + 勾选日期 = 提交到已用工时
${tasksHtml}
`; // ===== 事件绑定 ===== overlay.querySelector('#dr-close').onclick = () => overlay.remove(); // 日期标签点击 overlay.querySelectorAll('.dr-date-tag').forEach(tag => { tag.onclick = () => { tag.classList.toggle('checked'); updateCounts(overlay); }; }); // 日期快捷按钮 overlay.querySelector('#dr-sel-all').onclick = () => { overlay.querySelectorAll('.dr-date-tag').forEach(t => t.classList.add('checked')); updateCounts(overlay); }; overlay.querySelector('#dr-sel-workday').onclick = () => { overlay.querySelectorAll('.dr-date-tag').forEach(t => { const d = new Date(parseInt(t.dataset.ts)); d.getDay() === 0 || d.getDay() === 6 ? t.classList.remove('checked') : t.classList.add('checked'); }); updateCounts(overlay); }; overlay.querySelector('#dr-sel-none').onclick = () => { overlay.querySelectorAll('.dr-date-tag').forEach(t => t.classList.remove('checked')); updateCounts(overlay); }; overlay.querySelector('#dr-sel-thismonth').onclick = () => { _selDates = getDefaultDates(); renderPanel(overlay); }; overlay.querySelector('#dr-sel-thisweek').onclick = () => { _selDates = getThisWeek(); renderPanel(overlay); }; overlay.querySelector('#dr-sel-lastweek').onclick = () => { _selDates = getLastWeek(); renderPanel(overlay); }; overlay.querySelector('#dr-sel-custom').onclick = () => { const start = prompt('起始日期 (YYYY-MM-DD):', fmtDate(new Date(new Date().getFullYear(), new Date().getMonth(), 1))); if (!start) return; const end = prompt('结束日期 (YYYY-MM-DD):', fmtDate(new Date())); if (!end) return; const s = new Date(start), e = new Date(end); if (isNaN(s.getTime()) || isNaN(e.getTime())) { toast('日期格式错误', 'err'); return; } _selDates = []; const cur = new Date(s); while (cur <= e) { _selDates.push(new Date(cur)); cur.setDate(cur.getDate() + 1); } renderPanel(overlay); }; // 全选任务 overlay.querySelector('#dr-task-all-cb').onchange = function () { overlay.querySelectorAll('.dr-task-cb').forEach(cb => { cb.checked = this.checked; }); updateCounts(overlay); }; // 每个任务复选框 overlay.querySelectorAll('.dr-task-cb').forEach(cb => { cb.onchange = () => updateCounts(overlay); }); // 功能按钮 overlay.querySelector('#dr-preview').onclick = () => preview(activeTasks, overlay); overlay.querySelector('#dr-copy').onclick = () => copyText(activeTasks, overlay); overlay.querySelector('#dr-submit').onclick = () => batchSubmit(activeTasks, overlay); } function updateCounts(overlay) { const selDays = overlay.querySelectorAll('.dr-date-tag.checked').length; const totalDays = overlay.querySelectorAll('.dr-date-tag').length; const selTasks = overlay.querySelectorAll('.dr-task-cb:checked').length; const totalTasks = overlay.querySelectorAll('.dr-task-cb').length; const total = selDays * selTasks; const elD = overlay.querySelector('#dr-sum-dates'); const elT = overlay.querySelector('#dr-sum-tasks'); const elTo = overlay.querySelector('#dr-sum-total'); const elBtn = overlay.querySelector('#dr-sub-total'); if (elD) elD.textContent = selDays; if (elT) elT.textContent = selTasks; if (elTo) elTo.textContent = total; if (elBtn) elBtn.textContent = total; } // ==================== 提交 ==================== async function batchSubmit(activeTasks, overlay) { const selDateTags = overlay.querySelectorAll('.dr-date-tag.checked'); if (selDateTags.length === 0) { toast('⚠️ 请至少选择1个日期', 'err'); return; } const selDates = Array.from(selDateTags).map(t => new Date(parseInt(t.dataset.ts))); const taskEntries = []; for (const task of activeTasks) { const cb = overlay.querySelector('#dr-cb-' + task.id); if (!cb || !cb.checked) continue; const contentEl = overlay.querySelector('#dr-c-' + task.id); const hourEl = overlay.querySelector('#dr-h-' + task.id); const content = contentEl ? contentEl.value.trim() : ''; const hour = hourEl ? parseFloat(hourEl.value) || 8 : 8; if (!content) { toast(`⚠️ "${task.name}" 已勾选但未填写内容`, 'err'); return; } taskEntries.push({ taskId: task.id, taskName: task.name, content, hour }); } if (taskEntries.length === 0) { toast('⚠️ 请勾选任务并填写内容', 'err'); return; } const totalOps = selDates.length * taskEntries.length; const dateList = selDates.map(d => fmtDate(d)).join(', '); if (!confirm(`确认批量提交?\n\n📅 ${selDates.length} 天:${dateList}\n📊 ${taskEntries.length} 个任务\n📝 共 ${totalOps} 条\n\n${taskEntries.map(t => ' ✅ ' + t.taskName + ' (' + t.hour + 'h)').join('\n')}`)) return; const btn = overlay.querySelector('#dr-submit'); if (btn) { btn.disabled = true; btn.innerHTML = ' 0/' + totalOps; } let progDiv = overlay.querySelector('.dr-progress-bar'); if (!progDiv) { const bar = document.createElement('div'); bar.className = 'dr-progress'; bar.innerHTML = '
'; overlay.querySelector('.dr-bd').insertBefore(bar, overlay.querySelector('.dr-bd').firstChild); progDiv = overlay.querySelector('.dr-progress-bar'); } else { progDiv.style.width = '0%'; } let done = 0, errors = []; for (const date of selDates) { for (const entry of taskEntries) { try { await apiCall('BizAction.addTaskWorkTimeLog', { taskId: entry.taskId, startTime: date.getTime(), hour: entry.hour, content: entry.content }); done++; if (btn) btn.innerHTML = ' ' + done + '/' + totalOps; if (progDiv) progDiv.style.width = (done / totalOps * 100) + '%'; } catch (e) { errors.push(fmtDate(date) + ' ' + entry.taskName + ': ' + e.message); done++; } } } if (btn) { btn.disabled = false; btn.textContent = '🚀 批量提交'; } if (progDiv) setTimeout(() => { progDiv.style.width = '0%'; }, 3000); if (errors.length === 0) { toast('✅ ' + totalOps + ' 条全部成功!'); } else { toast('⚠️ 成功 ' + (done - errors.length) + '/' + totalOps + ',失败 ' + errors.length, 'err'); } } // ==================== 预览 / 复制 ==================== function getCheckedTasks(activeTasks, overlay) { return activeTasks.filter(task => { const cb = overlay.querySelector('#dr-cb-' + task.id); return cb && cb.checked; }); } function preview(activeTasks, overlay) { const selDateTags = overlay.querySelectorAll('.dr-date-tag.checked'); const selDates = Array.from(selDateTags).map(t => new Date(parseInt(t.dataset.ts))); const selTasks = getCheckedTasks(activeTasks, overlay); let text = '【预览】' + selDates.length + ' 天 × ' + selTasks.length + ' 个任务\n\n'; for (const date of selDates) { text += '📅 ' + fmtDate(date) + ' 周' + weekDay(date) + '\n'; for (const task of selTasks) { const el = overlay.querySelector('#dr-c-' + task.id); const hEl = overlay.querySelector('#dr-h-' + task.id); const c = el ? el.value.trim() : ''; const h = hEl ? hEl.value : '8'; text += c ? ' ✅ ' + task.name + ' [' + h + 'h] ' + c.slice(0, 80) + '\n' : ' ⬜ ' + task.name + '(未填写)\n'; } text += '\n'; } alert(text.slice(0, 3000)); } function copyText(activeTasks, overlay) { const selDateTags = overlay.querySelectorAll('.dr-date-tag.checked'); const selDates = Array.from(selDateTags).map(t => new Date(parseInt(t.dataset.ts))); const selTasks = getCheckedTasks(activeTasks, overlay); const today = fmtDate(new Date()), wd = weekDay(new Date()); let text = '📋 日报 ' + today + ' 周' + wd + '\n👤 ' + _userName + '\n\n'; text += '【填报日期】' + selDates.map(d => fmtDate(d)).join(', ') + '\n\n'; text += '【工作任务 · ' + selTasks.length + '项】\n'; selTasks.forEach((task, i) => { const el = overlay.querySelector('#dr-c-' + task.id); const c = el ? el.value.trim() : ''; text += (i + 1) + '. ' + task.name + ' [' + task.statusName + '] [' + (task.projectName || '') + ']\n'; if (c) text += ' 📝 ' + c.replace(/\n/g, '\n ') + '\n'; }); text += '\n【遇到的问题】\n无\n\n【明日计划】\n待定\n'; navigator.clipboard.writeText(text).then(() => toast('✅ 已复制')); } // ==================== 设置 ==================== function openSettings() { const token = _TOKEN || GM_getValue('dr_token', ''); const overlay = document.createElement('div'); overlay.className = 'dr-overlay'; overlay.innerHTML = `

⚙ 设置

获取方法:F12 → Network → 点任意菜单 → 找 /p/api/invoke/ 请求 → Payload → arg0
${token ? '✅ Token: ' + token.slice(0, 8) + '...' : '❌ 暂无Token'}

`; document.body.appendChild(overlay); overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); }); overlay.querySelector('#dr-s-close').onclick = () => overlay.remove(); overlay.querySelector('#dr-s-save').onclick = () => { _TOKEN = overlay.querySelector('#dr-s-token').value.trim(); if (_TOKEN) GM_setValue('dr_token', _TOKEN); else GM_setValue('dr_token', ''); toast(_TOKEN ? '✅ 已保存' : '已清空'); overlay.remove(); }; overlay.querySelector('#dr-s-clear').onclick = () => { _TOKEN = ''; GM_setValue('dr_token', ''); toast('已清除'); overlay.remove(); }; } // ==================== 启动 ==================== function init() { injectStyle(); injectFab(); console.log('✅ 日报助手 v5.1'); console.log(' 📋 蓝按钮 → 勾选日期+任务 → 填内容 → 批量提交'); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();