// ==UserScript== // @name 任务团队分配看板(任务名旁显示标注团队) // @namespace dsh.teamboard // @version 5.0.1 // @description 在任务管理页的每个任务名称右边显示该任务「标注」工序的团队;按人员分配的可点 ✎ 手填团队名,永久记住 // @author DSH // @match *://*/v/project/*/project-taskManage* // @include /^https?:\/\/[^/]+\/v\/project\/[^/]+\/project-taskManage/ // @grant none // @run-at document-idle // @noframes // ==/UserScript== /* * 显示规则(只关心「标注」工序) * ───────────────────────────────────────────────────────────── * ① 接口里标注工序的真实团队 → 蓝色 例:极智 * ② 你手填的 / 按人员推断的团队名 → 橙色 例:栗镇 * ③ 标注工序承接的个人账号 → 紫色 人员· 仅在 ①② 都没有时显示 * * ① 有就显示;② 只补充 ① 里**没有**的名字(不会出现「极智 极智」); * ③ 只在 ① ② 都为空时才显示,所以你手填之后人员账号会自动收起。 * * 手填 * ───────────────────────────────────────────────────────────── * 每行任务名后面有个小 ✎: * · 点一下 → 就地输入团队名,回车保存(Esc 取消,留空回车 = 清除本行) * · Shift + 点击 → 清除全部手填记录 * 记录存在浏览器本地,关页面/重启浏览器都还在;保存时会顺手记住 * 「这些人属于这个团队」,别的任务里出现同一批人就自动带出来,不用一条条填。 * 只往页面写显示内容,不调用任何写接口,平台数据一个字节都不会变。 * * 想看别的工序:把下面的 ONLY_WT 改成 '2'(审核) / '3'(质检) / '4'(验收),'' = 全部。 * * 通用性 * ───────────────────────────────────────────────────────────── * @match 用 *://* 通配;表格不靠平台类名识别(先找有 tbody 行的表,再配对它的表头, * 已处理 Element-UI 表头/表体分属两张 的情况)。 * 接口(全部只读 GET):/v2/projects/{project_key}/task-batches、/v2/guilds * ───────────────────────────────────────────────────────────── */ (function () { 'use strict'; if (window.__DSH_TEAM_BOARD_LOADED__) return; window.__DSH_TEAM_BOARD_LOADED__ = true; var ONLY_WT = '1'; // '1'=标注(默认只看这个);'2'=审核 '3'=质检 '4'=验收;''=全部 var CFG_KEY = 'dsh_team_board_cfg_v5'; var cfg = Object.assign({ autoRefresh: true, intervalSec: 15, manual: { task: {}, person: {} }, // 手填的团队名:按任务 / 按人员记住 bannerDismissed: false }, safeParse(localStorage.getItem(CFG_KEY))); var WORK_ORDER = ['1', '2', '3', '4']; var WORK_TYPES = { '1': '标注', '2': '审核', '3': '质检', '4': '验收' }; var WORK_COLOR = { '1': '#409eff', '2': '#e6a23c', '3': '#b0b8c4', '4': '#67c23a' }; var ALL_COLOR = '#4da3ff'; var USER_COLOR = '#b37feb'; // 个人承接:紫色 var MANUAL_COLOR = '#e6a23c'; // 手填的团队名:橙色 var state = { tasks: [], taskMap: {}, guilds: [], guildMap: {}, error: '', lastUpdate: null, _logSig: '' }; var refreshTimer = null, domObserver = null, badgeTimer = null, routeTimer = null; var currentPath = location.pathname; /* ============================================================ 工具 */ function safeParse(s) { try { return s ? JSON.parse(s) : null; } catch (e) { return null; } } function saveCfg() { try { localStorage.setItem(CFG_KEY, JSON.stringify(cfg)); } catch (e) {} } function esc(s) { return String(s === undefined || s === null ? '' : s) .replace(/&/g, '&').replace(//g, '>'); } function projectKey() { var m = location.pathname.match(/\/v\/project\/([^/]+)\//); return m ? m[1] : ''; } function getToken() { try { return String(localStorage.getItem('token') || '').replace(/^"+|"+$/g, ''); } catch (e) { return ''; } } function orgId() { try { return JSON.parse(localStorage.getItem('userInfo') || '{}').organize_id || ''; } catch (e) { return ''; } } function isOk(res) { if (!res || typeof res !== 'object') return false; var c = res.code; return c === '' || c === 0 || c === '0' || c === undefined || c === null; } function workLabel(wt) { return WORK_TYPES[wt] || ('工序' + wt); } function colorOf(wt) { return wt ? (WORK_COLOR[wt] || ALL_COLOR) : ALL_COLOR; } /** 行内输入框 / 铅笔按钮的样式(注入到页面,只影响自己的元素) */ function ensureStyles() { if (document.getElementById('dsh-tb-style')) return; var s = document.createElement('style'); s.id = 'dsh-tb-style'; s.textContent = '.dsh-tb-edit{display:inline-block;margin-left:.3em;padding:0 .25em;border:0;background:transparent;' + ' cursor:pointer;color:#7d90ad;font-size:.9em;line-height:1.2;opacity:.5;vertical-align:baseline;}' + '.dsh-tb-edit:hover{opacity:1;color:#e6a23c;}' + '.dsh-tb-input{margin-left:.4em;width:9em;font-size:.94em;font-family:inherit;padding:.05em .35em;' + ' border:1px solid #3b7cf0;border-radius:3px;background:#0d1626;color:#e3ebf8;outline:none;}' + '.dsh-tb-badge{display:inline;}' + '.dsh-tb-badge>span{white-space:nowrap;}'; document.head.appendChild(s); } /* ============================================================ 接口(只读 GET) */ function apiGet(path, params) { var qs = []; Object.keys(params || {}).forEach(function (k) { var v = params[k]; if (v === undefined || v === null) return; qs.push(encodeURIComponent(k) + '=' + encodeURIComponent(v)); }); var url = path + (qs.length ? (path.indexOf('?') > -1 ? '&' : '?') + qs.join('&') : ''); return fetch(url, { method: 'GET', credentials: 'same-origin', headers: { 'access-key': getToken(), 'time': String(Date.now()) }, cache: 'no-store' }).then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }); } /** 分页拉全量任务;服务端可能把 page_num 截断,因此用「新增条数」判断结束 */ function fetchAllTasks(key) { var PAGE_SIZE = 200, MAX_PAGES = 60; var collected = [], seen = {}, first = null; function one(page) { return apiGet('/v2/projects/' + encodeURIComponent(key) + '/task-batches', { task_name: '', task_key: '', line_status: '', page: page, page_num: PAGE_SIZE, creator_ids: '', file_name: '', seg_dir_name: '' }); } function step(page) { return one(page).then(function (res) { if (!isOk(res)) return { res: res, items: collected.length ? collected : null }; if (!first) first = res; var items = (res.data && res.data.items) || []; var added = 0; items.forEach(function (it) { var k = it.task_batch_key || ('#' + collected.length); if (seen[k]) return; seen[k] = 1; collected.push(it); added++; }); var total = Number(res.data && res.data.meta && res.data.meta.total_num) || 0; var noMore = items.length === 0 || added === 0 || page >= MAX_PAGES || (total > 0 ? collected.length >= total : items.length < PAGE_SIZE); return noMore ? { res: first, items: collected } : step(page + 1); }); } return step(1); } /* ============================================================ 数据 */ function normalizeTask(t) { var access = {}; (t.access_info || []).forEach(function (a) { if (a && a.work_type !== undefined) access[String(a.work_type)] = a.access; }); var alloc = t.allocation_info || {}; var teamsByWt = {}, usersByWt = {}; (alloc.guild_items || []).forEach(function (g) { var wt = String(g.work_type); (teamsByWt[wt] = teamsByWt[wt] || []).push(g); }); (alloc.user_items || []).forEach(function (u) { var wt = String(u.work_type); (usersByWt[wt] = usersByWt[wt] || []).push(u); }); var accept = Number(t.accept_num) || 0, check = Number(t.check_num) || 0; var inspect = Number(t.inspect_num) || 0, done = Number(t.done_num) || 0; return { key: String(t.task_batch_key || ''), name: t.task_batch_name || '', total: Number(t.total_num) || 0, progress: { '1': accept + check + inspect + done, '2': accept + inspect + done, '3': accept + done, '4': done }, access: access, teamsByWt: teamsByWt, usersByWt: usersByWt }; } function teamNames(task, wt) { return ((task.teamsByWt || {})[wt] || []).map(function (g) { return g.guild_name || (state.guildMap[String(g.gid)]) || ('团队' + g.gid); }); } /** 人员名:账号 → 昵称 → 用户{id} */ function userLabelOf(u) { var n = u.user_name || u.username || u.account; if (!n) n = u.nickname || u.nick_name || u.real_name || u.name; if (n === undefined || n === null || n === '') n = '用户' + (u.user_id === undefined ? '?' : u.user_id); return String(n); } function userNames(task, wt) { return ((task.usersByWt || {})[wt] || []).map(userLabelOf); } function hasAnyTeam(task, wt) { return teamNames(task, wt).length > 0; } function hasAnyUser(task, wt) { return userNames(task, wt).length > 0; } /** 当前关心的工序集合:ONLY_WT 有值就只看它,否则看全部 */ function scopedWts() { return ONLY_WT ? [ONLY_WT] : WORK_ORDER; } function scopedTeams(task) { var out = [], seen = {}; scopedWts().forEach(function (wt) { teamNames(task, wt).forEach(function (n) { if (!seen[n]) { seen[n] = 1; out.push(n); } }); }); return out; } function scopedUsers(task) { var out = [], seen = {}; scopedWts().forEach(function (wt) { userNames(task, wt).forEach(function (n) { if (!seen[n]) { seen[n] = 1; out.push(n); } }); }); return out; } /* ---- 手填团队名(只显示,不改平台数据;存 localStorage) ---- */ function manualStore() { if (!cfg.manual || typeof cfg.manual !== 'object') cfg.manual = { task: {}, person: {} }; if (!cfg.manual.task) cfg.manual.task = {}; if (!cfg.manual.person) cfg.manual.person = {}; return cfg.manual; } function manualOf(taskKey) { return manualStore().task[taskKey] || ''; } /** 按「这些人属于哪个团队」的记住的映射,推断当前任务的团队 */ function inferredOf(task) { var m = manualStore().person, out = [], seen = {}; scopedWts().forEach(function (wt) { ((task.usersByWt || {})[wt] || []).forEach(function (u) { var id = (u.user_id === undefined || u.user_id === null) ? '' : String(u.user_id); var t = id ? m[id] : ''; if (t && !seen[t]) { seen[t] = 1; out.push(t); } }); }); return out; } /** 手填优先,其次按人员推断;返回名字数组 + 来源 */ function manualLabelOf(task) { var own = manualOf(task.key); if (own) return { names: [own], source: '手填' }; var inf = inferredOf(task); if (inf.length) return { names: inf, source: '按人员所属团队推断' }; return null; } /** * 任务名右边要显示的内容,按段返回(每段一个颜色)。 * 前缀一律用文字,不用 emoji:有些 Windows 上 emoji 会渲染成空白或方框。 */ function displaySegments(task) { var segs = []; var teams = scopedTeams(task); if (teams.length) { segs.push({ txt: teams.join('、'), color: colorOf(ONLY_WT), kind: 'team' }); } var man = manualLabelOf(task); if (man) { // 已经由真实团队显示过的名字不再重复(否则会出现「极智 极智」) var extra = man.names.filter(function (n) { return teams.indexOf(n) < 0; }); if (extra.length) { segs.push({ txt: extra.join('、'), color: MANUAL_COLOR, kind: 'manual', src: man.source }); } } var users = scopedUsers(task); if (users.length && !teams.length && !man) { segs.push({ txt: '人员·' + users.join('、'), color: USER_COLOR, kind: 'user' }); } return segs; } function scopeLabel() { return ONLY_WT ? (workLabel(ONLY_WT) + '工序') : '全部工序'; } function teamTip(task) { var lines = []; var t = scopedTeams(task), u = scopedUsers(task); if (t.length) lines.push(scopeLabel() + '团队:' + t.join('、')); if (u.length) lines.push(scopeLabel() + '人员:' + u.join('、')); if (!lines.length) lines.push(scopeLabel() + ':未分配'); var man = manualLabelOf(task); if (man) lines.push(man.source + ':' + man.names.join('、')); return lines.join('\n'); } function loadAll() { var key = projectKey(); if (!key) return Promise.resolve(); state.error = ''; return Promise.all([ fetchAllTasks(key), apiGet('/v2/guilds', { page: 1, page_num: 9999, guild_name: '', organize_id: orgId() }) .catch(function () { return null; }) ]).then(function (arr) { var taskRes = arr[0], guildRes = arr[1]; if (!taskRes.items) throw new Error(taskRes.res && taskRes.res.msg ? taskRes.res.msg : '任务列表接口返回异常'); state.guilds = (isOk(guildRes) && guildRes.data && guildRes.data.items) || []; state.guildMap = {}; state.guilds.forEach(function (g) { state.guildMap[String(g.gid)] = g.guild_name; }); state.tasks = taskRes.items.map(normalizeTask); state.taskMap = {}; state.tasks.forEach(function (t) { if (t.key) state.taskMap[t.key] = t; }); state.lastUpdate = new Date(); refreshAndReport(); }).catch(function (err) { state.error = (err && err.message) ? err.message : String(err); console.warn('[团队看板] 取数失败:' + state.error); refreshAndReport(); // 提示内容统一由 report() 决定,避免被后续重判覆盖 }); } /* ============================================================ 找任务表格 */ /** * ⚠ 关键:Element-UI 把表头和数据放在**两张不同的
** 里 * (.el-table__header-wrapper table 和 .el-table__body-wrapper table), * 表头那张没有 tbody。所以必须分别取「有数据的表」和「它的表头」再配对。 */ function findTables() { var pairs = []; var all = []; try { all = Array.prototype.slice.call(document.querySelectorAll('table')); } catch (e) { return pairs; } all.forEach(function (t) { var rows = t.querySelectorAll('tbody tr'); if (!rows.length) return; var head = t.querySelector('thead') || headerOf(t); pairs.push({ head: head, body: t, rows: rows }); }); return pairs; } function headerOf(bodyTable) { var el = bodyTable; for (var up = 0; up < 5 && el; up++) { el = el.parentElement; if (!el) break; var heads = el.querySelectorAll('thead'); for (var i = 0; i < heads.length; i++) { if (!bodyTable.contains(heads[i])) return heads[i]; } } return null; } function headTexts(head) { if (!head) return []; return Array.prototype.map.call(head.querySelectorAll('th'), function (th) { return (th.textContent || '').trim(); // 不用 innerText:它依赖布局,取不到值会整个失配 }); } function isTaskTable(pair) { var hs = headTexts(pair.head); if (!hs.length) return false; return hs.some(function (h) { return h.indexOf('任务名称') > -1; }) || hs.some(function (h) { return /任务\s*key/i.test(h); }); } function taskTables() { var pairs = findTables(); var hit = pairs.filter(isTaskTable); if (hit.length) return hit; return pairs.length === 1 ? pairs : []; } function nameHeaderCell(pair) { var ths = pair.head ? pair.head.querySelectorAll('th') : []; for (var i = 0; i < ths.length; i++) { if ((ths[i].textContent || '').indexOf('任务名称') > -1) { return ths[i].querySelector('.cell') || ths[i]; } } return null; } /** 平台这一列带 nowrap + ellipsis,会把追加的内容裁掉,必须解开 */ function unclip(el) { if (!el || !el.style) return; el.style.whiteSpace = 'normal'; el.style.textOverflow = 'clip'; } /** * 就地编辑:在任务名旁边弹一个输入框,回车保存。 * 只写 localStorage,不碰平台任何数据;关掉页面再打开仍在。 */ function openEditor(cell, task) { if (cell.querySelector('.dsh-tb-input')) return; var btn = cell.querySelector('.dsh-tb-edit'); if (btn) btn.style.display = 'none'; var inp = document.createElement('input'); inp.className = 'dsh-tb-input'; inp.type = 'text'; inp.placeholder = '团队名,回车保存'; inp.value = manualOf(task.key) || ''; inp.title = '只显示用,不会修改平台数据;留空回车 = 清除本行'; cell.appendChild(inp); inp.focus(); inp.select(); var done = false; function finish(save) { if (done) return; done = true; var v = (inp.value || '').trim(); if (save) { var m = manualStore(); if (v) { m.task[task.key] = v; // 顺手记住「这些人属于这个团队」,别的任务里出现同一批人就能自动带出来 scopedWts().forEach(function (wt) { ((task.usersByWt || {})[wt] || []).forEach(function (u) { if (u.user_id !== undefined && u.user_id !== null && u.user_id !== '') { m.person[String(u.user_id)] = v; } }); }); } else { delete m.task[task.key]; } saveCfg(); } inp.remove(); if (btn) btn.style.display = ''; refreshAndReport(); } inp.addEventListener('keydown', function (e) { e.stopPropagation(); // 别让平台的快捷键抢走 if (e.key === 'Enter') { e.preventDefault(); finish(true); } else if (e.key === 'Escape') { e.preventDefault(); finish(false); } }); inp.addEventListener('blur', function () { finish(true); }); ['click', 'mousedown', 'dblclick'].forEach(function (ev) { inp.addEventListener(ev, function (e) { e.stopPropagation(); }); }); } function clearAllManual() { var n = Object.keys(manualStore().task).length; if (!n) return; if (!window.confirm('清除全部手填的团队名(共 ' + n + ' 条)?\n只影响本脚本的记录,不影响平台数据。')) return; cfg.manual = { task: {}, person: {} }; saveCfg(); refreshAndReport(); } /* ============================================================ 注入团队名 */ /** 返回 {tables, rows, matched, shown, withTeam, withUser} 供上层判断是否要提示 */ function injectBadges() { var stat = { tables: 0, rows: 0, matched: 0, shown: 0, withTeam: 0, withUser: 0 }; var pairs = taskTables(); stat.tables = pairs.length; pairs.forEach(function (pair) { var headers = headTexts(pair.head); var keyIdx = headers.indexOf('任务key'); if (keyIdx < 1) keyIdx = headers.indexOf('任务 key'); if (keyIdx < 1) { for (var i = 0; i < headers.length; i++) { if (headers[i].indexOf('任务名称') > -1) { keyIdx = i + 1; break; } } } if (keyIdx < 1) keyIdx = headers.length > 1 ? 1 : -1; if (keyIdx < 1) return; Array.prototype.forEach.call(pair.rows, function (tr) { var tds = tr.querySelectorAll('td'); if (tds.length <= keyIdx) return; stat.rows++; var taskKey = (tds[keyIdx].textContent || '').trim(); var task = state.taskMap[taskKey]; var nameCell = tds[keyIdx - 1]; if (task) stat.matched++; if (task) { if (hasAnyTeam(task, ONLY_WT)) stat.withTeam++; if (hasAnyUser(task, ONLY_WT)) stat.withUser++; } if (!nameCell) return; var cell = nameCell.querySelector('.cell') || nameCell; // 正在编辑这一行时别动它,否则输入框会被冲掉 if (cell.querySelector('.dsh-tb-input')) return; var segs = task ? displaySegments(task) : []; var badge = cell.querySelector('.dsh-tb-badge'); var editBtn = cell.querySelector('.dsh-tb-edit'); if (!segs.length) { if (badge) badge.remove(); if (editBtn) editBtn.remove(); return; } stat.shown++; unclip(cell); var sig = segs.map(function (s) { return s.txt + s.color; }).join('|'); if (!badge) { badge = document.createElement('span'); badge.className = 'dsh-tb-badge'; badge.style.cssText = 'margin-left:.55em;font-size:.94em;white-space:normal;'; cell.appendChild(badge); } if (badge.getAttribute('data-sig') !== sig) { badge.setAttribute('data-sig', sig); badge.innerHTML = segs.map(function (s) { return '' + esc(s.txt) + ''; }).join(''); badge.setAttribute('title', teamTip(task)); } // 每行一个小铅笔:点开手填;Shift+点击 = 清除全部 if (task && !editBtn) { editBtn = document.createElement('button'); editBtn.className = 'dsh-tb-edit'; editBtn.type = 'button'; editBtn.textContent = '✎'; editBtn.title = '点击:手填这个任务的团队名(只显示,不改平台数据)\nShift+点击:清除全部手填记录'; editBtn.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); if (e.shiftKey) { clearAllManual(); return; } openEditor(cell, task); }); editBtn.addEventListener('mousedown', function (e) { e.stopPropagation(); }); cell.appendChild(editBtn); } }); }); return stat; } /* ============================================================ 出问题时的提示 */ function domReport() { var lines = ['URL: ' + location.href]; var tables = document.querySelectorAll('table'); lines.push('页面
数量: ' + tables.length); for (var i = 0; i < Math.min(tables.length, 8); i++) { var t = tables[i]; var head = t.querySelector('thead'); var ht = head ? (head.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 60) : '(无 thead)'; lines.push(' #' + i + ' class="' + (t.className || '') + '" 行数=' + t.querySelectorAll('tbody tr').length + ' thead=' + ht); } lines.push('候选任务表数: ' + taskTables().length); return lines.join('\n'); } function diagText() { return [ '【团队看板诊断】', '项目: ' + projectKey(), '范围: ' + scopeLabel(), '任务: ' + state.tasks.length + ' 团队: ' + state.guilds.length, '手填: ' + Object.keys(manualStore().task).length + ' 条', '错误: ' + (state.error || '无'), domReport() ].join('\n'); } /** 只在异常 / 无数据时,在「任务名称」表头上挂一句提示;正常时完全消失 */ function setNote(text, detail, info) { taskTables().forEach(function (pair) { var cell = nameHeaderCell(pair); if (!cell) return; var note = cell.querySelector('.dsh-tb-note'); if (!text) { if (note) note.remove(); return; } if (!note) { note = document.createElement('span'); note.className = 'dsh-tb-note'; note.addEventListener('click', copyDiag); cell.appendChild(note); } note.style.cssText = 'margin-left:.6em;font-weight:400;font-size:.86em;cursor:pointer;color:' + (info ? '#7fb3ff' : '#e6a23c') + ';'; var title = (detail ? detail + '\n\n' : '') + '点击复制诊断信息'; if (note.textContent !== text) note.textContent = text; if (note.title !== title) note.title = title; unclip(cell); }); } var bannerEl = null, bannerTimer = null; function showBanner(text, detail) { if (cfg.bannerDismissed) return; if (!bannerEl) { bannerEl = document.createElement('div'); bannerEl.style.cssText = 'position:fixed;right:14px;bottom:14px;z-index:2147483200;max-width:460px;' + 'background:#2a2113;color:#ffd591;border:1px solid #7a5a1e;border-radius:8px;' + 'padding:10px 30px 10px 12px;font:13px/1.6 "Microsoft YaHei",sans-serif;' + 'box-shadow:0 8px 24px rgba(0,0,0,.5);white-space:pre-wrap;cursor:pointer;'; bannerEl.title = '点击复制诊断信息'; var close = document.createElement('span'); close.textContent = '✕'; close.style.cssText = 'position:absolute;right:9px;top:7px;cursor:pointer;color:#c9a86a;font-size:14px;'; close.addEventListener('click', function (e) { e.stopPropagation(); cfg.bannerDismissed = true; saveCfg(); hideBanner(); }); var body = document.createElement('span'); body.className = 'dsh-tb-banner-body'; bannerEl.appendChild(close); bannerEl.appendChild(body); bannerEl.addEventListener('click', copyDiag); document.body.appendChild(bannerEl); bannerTimer = setTimeout(hideBanner, 25000); } var msg = '【团队看板】' + text + (detail ? '\n' + detail + '\n(点击复制诊断信息)' : ''); var body = bannerEl.querySelector('.dsh-tb-banner-body'); if (body.textContent !== msg) body.textContent = msg; if (bannerTimer) { clearTimeout(bannerTimer); bannerTimer = setTimeout(hideBanner, 25000); } } function hideBanner() { if (bannerTimer) { clearTimeout(bannerTimer); bannerTimer = null; } if (bannerEl) { bannerEl.remove(); bannerEl = null; } } function copyDiag() { var txt = diagText(); try { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(txt); if (bannerEl) { var body = bannerEl.querySelector('.dsh-tb-banner-body'); body.textContent = '【团队看板】诊断信息已复制到剪贴板,可直接粘贴给我'; if (bannerTimer) clearTimeout(bannerTimer); bannerTimer = setTimeout(hideBanner, 8000); } return; } } catch (e) {} window.prompt('诊断信息(Ctrl+C 复制):', txt); } /* ============================================================ 每次判定 */ /** * 注入团队名 + 重新判定提示。 * ⚠ 页面翻页 / 筛选 / 重渲染都要走这里:只重注入而不再判定的话, * 表头那句提示会停在旧状态。 */ function refreshAndReport() { report(injectBadges()); } function report(st) { var key = projectKey(); var sig = [state.tasks.length, state.guilds.length, st.tables, st.rows, st.matched, st.shown, st.withTeam, st.withUser].join('/'); if (state._logSig !== sig) { state._logSig = sig; console.log('[团队看板] 项目=' + key + ' 范围=' + scopeLabel() + ' 任务=' + state.tasks.length + ' 团队=' + state.guilds.length + ' 任务表=' + st.tables + ' 页面行数=' + st.rows + ' 匹配上=' + st.matched + ' 有团队=' + st.withTeam + ' 有个人=' + st.withUser + ' 已显示=' + st.shown + ' 手填=' + Object.keys(manualStore().task).length); } var note = '', tip = '', info = false; if (state.error) { note = '⚠ 取数失败'; tip = state.error; } else if (!st.tables) { note = '⚠ 没找到任务表格'; tip = '脚本没在当前页面找到任务表格。'; console.warn('[团队看板] 没找到表格\n' + domReport()); } else if (!state.tasks.length) { note = '⚠ 接口没取到任务'; tip = '任务列表接口返回 0 条,可能是接口路径/参数不对,或当前账号没有该项目的权限。'; } else if (st.rows && !st.matched) { note = '⚠ 任务 key 对不上'; tip = '页面有 ' + st.rows + ' 行,接口取到 ' + state.tasks.length + ' 个任务,但没有一行匹配上。点这里复制诊断信息。'; } else if (st.rows && !st.shown) { note = '本页任务均未分配「' + workLabel(ONLY_WT) + '」'; tip = '接口取到 ' + state.tasks.length + ' 个任务,当前页 ' + st.rows + ' 行在' + scopeLabel() + '上既没有团队也没有个人承接。\n' + '如果是分配给了个人但接口没返回,可以点每行的 ✎ 手填一个团队名。'; } else if (st.rows && !st.withTeam && st.withUser) { // 不是问题,只是这批任务是按个人派的,解释一下免得误会 info = true; note = '本页任务按个人分配(无团队)'; tip = '这些任务没有分配给任何团队,直接派给了个人。\n' + '点每一行任务名后面的 ✎ 手填一个团队名,会自动记住,关掉页面再打开还在。'; } if (note) { setNote(note, tip, info); if (!info) showBanner(note, tip); else hideBanner(); } else { setNote(''); hideBanner(); } } /* ============================================================ 定时 / 观察 */ function setupTimer() { if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; } if (!cfg.autoRefresh) return; refreshTimer = setInterval(function () { if (document.hidden) return; loadAll(); }, Math.max(3, cfg.intervalSec || 15) * 1000); } function setupObserver() { if (domObserver) domObserver.disconnect(); domObserver = new MutationObserver(function () { if (badgeTimer) return; badgeTimer = setTimeout(function () { badgeTimer = null; try { refreshAndReport(); } catch (e) {} // 注入 + 重新判定,保证提示不过期 }, 200); }); domObserver.observe(document.body, { childList: true, subtree: true }); } function setupRouteWatch() { routeTimer = setInterval(function () { if (location.pathname === currentPath) return; currentPath = location.pathname; state._logSig = ''; if (/\/v\/project\/[^/]+\/project-taskManage/.test(currentPath)) loadAll(); }, 800); } /* ============================================================ 启动 */ function boot() { if (!/\/v\/project\/[^/]+\/project-taskManage/.test(location.pathname)) return; ensureStyles(); setupObserver(); setupRouteWatch(); setupTimer(); loadAll(); } if (document.readyState === 'complete' || document.readyState === 'interactive') { setTimeout(boot, 300); } else { window.addEventListener('DOMContentLoaded', function () { setTimeout(boot, 300); }); } })();