// ==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 = `
❌ ${esc(e.message)}
请确认已登录PM系统,或点击 ⚙ 手动填写Token
暂无进行中的任务
'; } else { activeTasks.forEach((task, i) => { const statusBg = task.statusColor || '#909399'; const priorityBg = task.priorityColor || '#C0C4CC'; const wTime = task.workTime || 0; tasksHtml += `
获取方法:F12 → Network → 点任意菜单 → 找 /p/api/invoke/ 请求 → Payload → arg0
${token ? '✅ Token: ' + token.slice(0, 8) + '...' : '❌ 暂无Token'}