// ==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); });
}
})();