// ==UserScript==
// @name 任务团队分配实时看板(团队/人员双视角)
// @namespace dsh.teamboard
// @version 1.1.0
// @description 实时查看任务分配给了哪些团队、哪些人员,各工序承接与进度如何(纯只读 GET,不修改平台任何数据)
// @author DSH
// @match *://*/v/project/*/project-taskManage*
// @include /^https?:\/\/[^/]+\/v\/project\/[^/]+\/project-taskManage/
// @grant none
// @run-at document-idle
// @noframes
// ==/UserScript==
/*
* 通用版说明
* ─────────────────────────────────────────────────────────────
* @match 使用 *://* 通配,因此**任意域名、任意端口**部署的同一套平台都能直接生效,
* 不再绑定某个固定 IP。项目标识由当前网址自动解析,接口全部走相对路径。
*
* 只调用平台自身的只读查询接口(全部是 HTTP GET):
* GET /v2/projects/{project_key}/task-batches 任务批次列表(含 allocation_info 分配信息)
* GET /v2/guilds 团队(公会)列表
* 鉴权复用页面登录态:localStorage.token → 请求头 access-key。
* 不会发起任何 POST / PUT / DELETE,不会改动平台数据。
*
* 如果要接入的是**另一家标注平台**(接口与字段完全不同),请改用同目录的浏览器
* 扩展版本:它把站点匹配、鉴权、接口路径、字段映射都做成了可视化配置的「站点档案」,
* 不需要改代码。本用户脚本面向的是 /v2 REST 风格的这套平台。
* ─────────────────────────────────────────────────────────────
*/
(function () {
'use strict';
if (window.__DSH_TEAM_BOARD_LOADED__) return;
window.__DSH_TEAM_BOARD_LOADED__ = true;
/* ============================ 配置 ============================ */
var CFG_KEY = 'dsh_team_board_cfg_v1';
var cfg = Object.assign({
autoRefresh: true,
intervalSec: 15,
inlineBadge: true,
panelOpen: true,
panelW: 1000,
panelH: 620,
fontScale: 1.15,
panelX: null,
panelY: null,
tab: 'task',
keyword: ''
}, safeParse(localStorage.getItem(CFG_KEY)));
function saveCfg() {
try { localStorage.setItem(CFG_KEY, JSON.stringify(cfg)); } catch (e) {}
}
function safeParse(s) {
try { return s ? JSON.parse(s) : null; } catch (e) { return null; }
}
/* ============================ 常量 ============================ */
var WORK_TYPES = { '1': '标注', '2': '审核', '3': '质检', '4': '验收' };
var WORK_ORDER = ['1', '2', '3', '4'];
var WORK_COLOR = { '1': '#409eff', '2': '#e6a23c', '3': '#909399', '4': '#67c23a' };
var TASK_TYPES = {
704: '音频标注', 902: '点云标注', 1001: '图像通用标注', 1002: 'OCR文字转写',
1003: 'REID目标跟踪', 1004: '图像语义分割', 1006: '人体关键点', 1103: '文本生成',
1201: '视频标注', 1202: '命名实体识别与关系标注', 1111: '意图及实体标注',
1112: '文章判断', 1113: '相似文本判断'
};
/* ============================ 状态 ============================ */
var state = {
loading: false,
lastUpdate: null,
error: '',
tasks: [], // 归一化后的任务
taskMap: {}, // task_key -> task
guilds: [], // 团队列表
guildMap: {}, // gid -> guild_name
users: [], // 人员列表
userMap: {}, // user_id -> {account, nickname}
source: '', // 数据来源:api / page
rawSample: null
};
var refreshTimer = null;
var domObserver = null;
var badgeTimer = null;
var currentPath = location.pathname;
/* ============================ 工具 ============================ */
function esc(s) {
return String(s === undefined || s === null ? '' : s)
.replace(/&/g, '&').replace(//g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
function num(v) { var n = Number(v); return isFinite(n) ? n : 0; }
function pad2(n) { return n < 10 ? '0' + n : '' + n; }
function fmtTime(d) {
if (!d) return '—';
return pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds());
}
function projectKey() {
var m = location.pathname.match(/\/v\/project\/([^/]+)\//);
return m ? m[1] : '';
}
function getToken() {
try {
var raw = localStorage.getItem('token');
if (!raw) return '';
return String(raw).replace(/^"|"$/g, '');
} catch (e) { return ''; }
}
function getUserInfo() {
try { return JSON.parse(localStorage.getItem('userInfo')) || {}; } catch (e) { return {}; }
}
function isOk(res) {
if (!res) return false;
return res.code === '' || res.code === 0 || res.code === '0';
}
/* ============================ API(只读 GET) ============================ */
function apiGet(path, params) {
var qs = '';
if (params) {
var parts = [];
Object.keys(params).forEach(function (k) {
var v = params[k];
if (v === undefined || v === null) return;
parts.push(encodeURIComponent(k) + '=' + encodeURIComponent(v));
});
if (parts.length) qs = (path.indexOf('?') > -1 ? '&' : '?') + parts.join('&');
}
var url = path + qs;
var headers = { 'access-key': getToken(), 'time': String(Date.now()) };
return fetch(url, {
method: 'GET',
credentials: 'same-origin',
headers: headers,
cache: 'no-store'
}).then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
});
}
/* ============================ 数据加载 ============================ */
function orgId() {
var u = getUserInfo();
return u && u.organize_id ? u.organize_id : '';
}
/**
* 分页拉取全部任务批次(服务端可能限制单页条数,因此按 meta.total_num 翻页)。
* 只读 GET,不会修改任何数据。
*/
function fetchAllTasks(key) {
var PAGE_SIZE = 200;
var MAX_PAGES = 60; // 上限 12000 条,避免异常时无限翻页
var collected = [];
var seen = {};
var 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)) {
if (first) return first; // 翻页中途失败:返回已拿到的部分
return res; // 首页失败:交给上层报错
}
if (!first) first = res;
var items = (res.data && res.data.items) || [];
// 服务端可能把 page_num 截断到一个更小的上限,因此用「新增条数」而不是请求条数判断结束
var added = 0;
items.forEach(function (it) {
var k = it.task_batch_key || it.task_info_id || ('#' + collected.length);
if (seen[k]) return;
seen[k] = 1; collected.push(it); added++;
});
var total = num(res.data && res.data.meta && res.data.meta.total_num);
var noMore = items.length === 0 || added === 0 || page >= MAX_PAGES ||
(total > 0 ? collected.length >= total : items.length < PAGE_SIZE);
if (noMore) {
var out = JSON.parse(JSON.stringify(first));
out.data = out.data || {};
out.data.items = collected;
out.__items = collected;
return out;
}
return step(page + 1);
});
}
return step(1);
}
function loadAll() {
var key = projectKey();
if (!key) return Promise.resolve();
state.loading = true;
state.error = '';
render();
var pTasks = fetchAllTasks(key);
var pGuilds = apiGet('/v2/guilds', {
page: 1, page_num: 99999, guild_name: '', organize_id: orgId()
}).catch(function () { return null; });
// 人员列表:user_items 里一般只有 user_id,需要靠它把「账号」查出来
var pUsers = apiGet('/v2/users', {
page: 1, page_num: 9999, user_name: '', nickname: '', status: '', organize_id: orgId()
}).catch(function () { return null; });
return Promise.all([pTasks, pGuilds, pUsers]).then(function (arr) {
var taskRes = arr[0];
var guildRes = arr[1];
var userRes = arr[2];
if (!isOk(taskRes)) {
throw new Error((taskRes && taskRes.msg) ? taskRes.msg : '任务列表接口返回异常');
}
var items = taskRes.__items || (taskRes.data && taskRes.data.items) || [];
state.rawSample = items[0] || null;
state.tasks = items.map(normalizeTask);
state.taskMap = {};
state.tasks.forEach(function (t) { if (t.key) state.taskMap[t.key] = t; });
if (isOk(guildRes)) {
state.guilds = (guildRes.data && guildRes.data.items) || [];
state.guildMap = {};
state.guilds.forEach(function (g) { state.guildMap[String(g.gid)] = g.guild_name; });
}
if (isOk(userRes)) {
state.users = (userRes.data && userRes.data.items) || [];
state.userMap = {};
state.users.forEach(function (u) {
state.userMap[String(u.user_id)] = { account: u.user_name, nickname: u.nickname };
});
}
state.source = 'api';
state.lastUpdate = new Date();
state.loading = false;
state.error = '';
render();
injectBadges();
}).catch(function (err) {
state.loading = false;
state.error = (err && err.message) ? err.message : String(err);
render();
});
}
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 = {};
var 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 = num(t.accept_num), check = num(t.check_num),
inspect = num(t.inspect_num), done = num(t.done_num);
var total = num(t.total_num);
return {
key: t.task_batch_key,
name: t.task_batch_name,
typeCode: t.task_type,
type: TASK_TYPES[t.task_type] || ('类型' + t.task_type),
total: total,
accept: accept, check: check, inspect: inspect, done: done,
progress: {
'1': accept + check + inspect + done,
'2': accept + inspect + done,
'3': accept + done,
'4': done
},
online: String(t.online_status) === '1',
access: access,
teamsByWt: teamsByWt,
usersByWt: usersByWt,
creator: t.creator_nickname,
createdAt: t.created_at,
raw: t
};
}
/* 某工序是否启用 */
function wtEnabled(task, wt) {
if (!task.access) return false;
return Object.prototype.hasOwnProperty.call(task.access, wt);
}
/* 该任务实际涉及的工序(有启用、或有团队/个人被分配) */
function activeWts(task) {
var set = {};
WORK_ORDER.forEach(function (wt) {
if (wtEnabled(task, wt) ||
(task.teamsByWt[wt] && task.teamsByWt[wt].length) ||
(task.usersByWt[wt] && task.usersByWt[wt].length)) set[wt] = true;
});
var out = WORK_ORDER.filter(function (wt) { return set[wt]; });
return out.length ? out : WORK_ORDER.slice(0, 1);
}
function teamNames(task, wt) {
var list = task.teamsByWt[wt] || [];
return list.map(function (g) {
return g.guild_name || state.guildMap[String(g.gid)] || ('团队' + g.gid);
});
}
function hasAnyTeam(task) {
return WORK_ORDER.some(function (wt) { return (task.teamsByWt[wt] || []).length > 0; });
}
/* ---- 人员维度 ---- */
function userIdOf(u) { return String(u.user_id === undefined || u.user_id === null ? '' : u.user_id); }
/** 账号 / 昵称:条目自带 → 人员列表按 user_id 回填 → 空 */
function userInfoOf(u) {
var uid = userIdOf(u);
var rec = state.userMap[uid] || {};
var account = u.user_name || u.username || u.account || rec.account;
var nick = u.nickname || u.nick_name || u.real_name || rec.nickname;
return {
uid: uid,
account: (account === undefined || account === null) ? '' : String(account),
nickname: (nick === undefined || nick === null) ? '' : String(nick)
};
}
/** 展示用:账号优先,有昵称且不同则括号补充,都没有才退回 用户{id} */
function userLabelOf(u) {
var i = userInfoOf(u);
if (i.account && i.nickname && i.account !== i.nickname) return i.account + '(' + i.nickname + ')';
if (i.account) return i.account;
if (i.nickname) return i.nickname;
return '用户' + i.uid;
}
function userNames(task, wt) {
return (task.usersByWt[wt] || []).map(userLabelOf);
}
function hasAnyUser(task) {
return WORK_ORDER.some(function (wt) { return (task.usersByWt[wt] || []).length > 0; });
}
/** 团队或个人任一维度有分配 */
function hasAnyAssignee(task) {
return hasAnyTeam(task) || hasAnyUser(task);
}
/** 某工序既无团队也无个人 */
function wtEmptyOf(task, wt) {
return teamNames(task, wt).length === 0 && userNames(task, wt).length === 0;
}
/* ============================ 面板 UI ============================ */
var host = null, root = null, shadow = null, bodyEl = null;
function ensurePanel() {
if (host && document.body.contains(host)) return;
host = document.createElement('div');
host.id = 'dsh-team-board-host';
host.style.cssText = 'all:initial;position:fixed;z-index:2147483000;';
document.body.appendChild(host);
shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = [
'',
'
',
'
',
' ',
' 任务团队分配看板 ',
' ',
' 刷新 ',
' 导出CSV ',
' 收起 ',
'
',
'
',
'
'
].join('\n');
bodyEl = shadow.getElementById('body');
applyGeom();
shadow.getElementById('btnRefresh').addEventListener('click', function () { loadAll(); });
shadow.getElementById('btnCsv').addEventListener('click', exportCsv);
shadow.getElementById('btnFold').addEventListener('click', function () {
cfg.panelOpen = !cfg.panelOpen; saveCfg(); applyGeom(); render();
});
makeDraggable(shadow.getElementById('hd'), shadow.getElementById('wrap'));
render();
}
function applyGeom() {
if (!shadow) return;
var w = Math.max(420, Math.min(cfg.panelW, window.innerWidth - 20));
var h = Math.max(160, Math.min(cfg.panelH, window.innerHeight - 20));
var x = cfg.panelX === null ? Math.max(10, window.innerWidth - w - 18) : cfg.panelX;
var y = cfg.panelY === null ? 70 : cfg.panelY;
x = Math.max(0, Math.min(x, window.innerWidth - w));
y = Math.max(0, Math.min(y, window.innerHeight - 40));
var base = 13 * (cfg.fontScale || 1.15);
var wrap = shadow.getElementById('wrap');
wrap.style.fontSize = base + 'px'; // 全局字号,其余尺寸都是 em
wrap.style.width = w + 'px';
wrap.style.height = (cfg.panelOpen ? h : Math.round(base * 2.9)) + 'px';
wrap.style.left = x + 'px';
wrap.style.top = y + 'px';
wrap.classList.toggle('collapsed', !cfg.panelOpen);
shadow.getElementById('btnFold').textContent = cfg.panelOpen ? '收起' : '展开';
}
function makeDraggable(handle, target) {
var sx = 0, sy = 0, ox = 0, oy = 0, dragging = false;
handle.addEventListener('mousedown', function (e) {
if (e.target.tagName === 'BUTTON') return;
dragging = true;
sx = e.clientX; sy = e.clientY;
ox = parseInt(target.style.left, 10) || 0;
oy = parseInt(target.style.top, 10) || 0;
e.preventDefault();
});
window.addEventListener('mousemove', function (e) {
if (!dragging) return;
var nx = Math.max(0, Math.min(ox + e.clientX - sx, window.innerWidth - 80));
var ny = Math.max(0, Math.min(oy + e.clientY - sy, window.innerHeight - 40));
target.style.left = nx + 'px';
target.style.top = ny + 'px';
cfg.panelX = nx; cfg.panelY = ny;
});
window.addEventListener('mouseup', function () {
if (!dragging) return;
dragging = false; saveCfg();
});
}
/* ============================ 渲染 ============================ */
function render() {
if (!shadow) return;
var dot = shadow.getElementById('dot');
dot.className = 'dot' + (state.error ? ' err' : (state.loading ? ' load' : ''));
renderBar();
renderMeta();
renderBody();
renderFoot();
}
var barSig = null;
function renderBar() {
var bar = shadow.getElementById('bar');
var tabs = [['task', '任务视角'], ['team', '团队视角'], ['user', '人员视角'],
['unassigned', '未分配'], ['raw', '诊断']];
var sig = cfg.tab + '|' + cfg.keyword;
// 结构未变化时不重建,避免自动刷新打断正在输入的搜索框
if (barSig !== sig) {
var focused = shadow.activeElement && shadow.activeElement.id === 'srch';
var caret = focused ? shadow.activeElement.selectionStart : 0;
var html = tabs.map(function (t) {
return '' + t[1] + ' ';
}).join('');
html += ' ';
bar.innerHTML = html;
barSig = sig;
bar.querySelectorAll('.tab').forEach(function (el) {
el.addEventListener('click', function () {
cfg.tab = el.getAttribute('data-tab'); saveCfg(); render();
});
});
var srch = bar.querySelector('#srch');
srch.addEventListener('input', function () {
cfg.keyword = srch.value; barSig = null; saveCfg();
renderBody();
});
if (focused) {
srch.focus();
try { srch.setSelectionRange(caret, caret); } catch (e) {}
}
}
}
function renderMeta() {
var meta = shadow.getElementById('meta');
var parts = [];
parts.push('项目:' + esc(projectKey() || '—') + ' ');
parts.push('任务:' + state.tasks.length + ' ');
parts.push('团队:' + state.guilds.length + ' ');
parts.push('人员:' + state.users.length + ' ');
parts.push('更新:' + fmtTime(state.lastUpdate) + ' ');
if (state.loading) parts.push('加载中… ');
if (state.error) parts.push('错误:' + esc(state.error) + ' ');
meta.innerHTML = parts.join('');
}
function matchKeyword(txt) {
if (!cfg.keyword) return true;
return String(txt).toLowerCase().indexOf(cfg.keyword.toLowerCase()) > -1;
}
function renderBody() {
var scroll = shadow.getElementById('scroll');
if (cfg.tab === 'task') scroll.innerHTML = renderTaskTable();
else if (cfg.tab === 'team') scroll.innerHTML = renderTeamTable();
else if (cfg.tab === 'user') scroll.innerHTML = renderUserTable();
else if (cfg.tab === 'unassigned') scroll.innerHTML = renderUnassigned();
else scroll.innerHTML = renderRaw();
}
function userCell(task) {
var wts = activeWts(task);
if (!hasAnyUser(task)) return '全部按团队分配 ';
return wts.map(function (wt) {
var c = WORK_COLOR[wt] || '#9fb2cd';
var names = userNames(task, wt);
var head = '' +
WORK_TYPES[wt] + ' ';
if (!names.length) return head + '— ';
return head + names.map(function (n) {
return '' + esc(n) + ' ';
}).join('');
}).join(' ');
}
function progressCell(task) {
var wts = activeWts(task);
return wts.map(function (wt) {
var done = task.progress[wt] || 0;
var color = WORK_COLOR[wt] || '#9fb2cd';
return '' +
WORK_TYPES[wt] + ' ' + done + '/' + task.total + ' ';
}).join('');
}
function teamCell(task) {
var wts = activeWts(task);
var out = [];
wts.forEach(function (wt) {
var names = teamNames(task, wt);
var color = WORK_COLOR[wt] || '#9fb2cd';
var label = '' + WORK_TYPES[wt] + ' ';
if (names.length) {
out.push(label + names.map(function (n) {
return '' + esc(n) + ' ';
}).join(''));
} else {
out.push(label + '未分配 ');
}
});
return out.join(' ');
}
function renderTaskTable() {
var rows = state.tasks.filter(function (t) {
return matchKeyword(t.name) || matchKeyword(t.key) || matchKeyword(t.type) ||
activeWts(t).some(function (wt) {
return teamNames(t, wt).some(matchKeyword) || userNames(t, wt).some(matchKeyword);
});
});
if (!rows.length) return '' + (state.tasks.length ? '没有匹配的任务' : '暂无数据(点“刷新”重试)') + '
';
var h = '' +
'任务名称 task_key 标注类型 总题数 ' +
'分配团队(按工序) 分配人员(按工序) 进度 状态 ';
rows.forEach(function (t) {
var cls = (hasAnyAssignee(t) ? '' : 'unassigned') + (t.online ? '' : ' offline');
h += '' +
'' + esc(t.name) + ' ' +
'' + esc(t.key) + ' ' +
'' + esc(t.type) + ' ' +
'' + t.total + ' ' +
'' + teamCell(t) + ' ' +
'' + userCell(t) + ' ' +
'' + progressCell(t) + ' ' +
'' + (t.online
? '上线 '
: '下线 ') + ' ' +
' ';
});
return h + '
';
}
function renderTeamTable() {
var agg = {}; // gid -> {name, tasks:[{task, wt}], wtCount:{}}
state.tasks.forEach(function (t) {
WORK_ORDER.forEach(function (wt) {
(t.teamsByWt[wt] || []).forEach(function (g) {
var gid = String(g.gid);
var name = g.guild_name || state.guildMap[gid] || ('团队' + gid);
if (!matchKeyword(name) && !matchKeyword(t.name) && !matchKeyword(t.key)) return;
var a = agg[gid] = agg[gid] || { gid: gid, name: name, tasks: [], wtCount: {} };
a.tasks.push({ task: t, wt: wt });
a.wtCount[wt] = (a.wtCount[wt] || 0) + 1;
});
});
});
var list = Object.keys(agg).map(function (k) { return agg[k]; })
.sort(function (a, b) { return b.tasks.length - a.tasks.length; });
if (!list.length) return '没有团队分配记录
';
var h = '团队 承接任务数 工序分布 任务明细(工序) ';
list.forEach(function (a) {
var wtChips = WORK_ORDER.filter(function (wt) { return a.wtCount[wt]; }).map(function (wt) {
var c = WORK_COLOR[wt];
return '' +
WORK_TYPES[wt] + ' × ' + a.wtCount[wt] + ' ';
}).join('');
var detail = a.tasks.map(function (x) {
var c = WORK_COLOR[x.wt];
return '' + esc(x.task.name) +
' ' + WORK_TYPES[x.wt] + ' ';
}).join(' ');
h += '' + esc(a.name) + ' gid ' + esc(a.gid) + '
' +
'' + a.tasks.length + ' ' + wtChips + ' ' + detail + ' ';
});
return h + '
';
}
function renderUserTable() {
var agg = {};
state.tasks.forEach(function (t) {
WORK_ORDER.forEach(function (wt) {
(t.usersByWt[wt] || []).forEach(function (u) {
var info = userInfoOf(u);
var uid = info.uid || '(无ID)';
if (!matchKeyword(info.account) && !matchKeyword(info.nickname) && !matchKeyword(uid) &&
!matchKeyword(t.name) && !matchKeyword(t.key)) return;
var a = agg[uid] = agg[uid] || {
uid: uid, account: info.account, nickname: info.nickname,
tasks: [], wtCount: {}, teamBacked: 0
};
if (!a.account && info.account) a.account = info.account;
if (!a.nickname && info.nickname) a.nickname = info.nickname;
a.tasks.push({ task: t, wt: wt });
a.wtCount[wt] = (a.wtCount[wt] || 0) + 1;
if (teamNames(t, wt).length) a.teamBacked++;
});
});
});
var list = Object.keys(agg).map(function (k) { return agg[k]; })
.sort(function (a, b) { return b.tasks.length - a.tasks.length; });
if (!list.length) {
return '没有任何按个人分配的记录(当前任务的分配都在团队维度上)
';
}
var h = '人员 承接任务数 工序分布 ' +
'任务明细(工序) 团队覆盖 ';
list.forEach(function (a) {
var wts = WORK_ORDER.filter(function (wt) { return a.wtCount[wt]; }).map(function (wt) {
var c = WORK_COLOR[wt];
return '' +
WORK_TYPES[wt] + ' × ' + a.wtCount[wt] + ' ';
}).join('');
var detail = a.tasks.map(function (x) {
var c = WORK_COLOR[x.wt];
return '' + esc(x.task.name) + ' ' +
'' + WORK_TYPES[x.wt] + ' ';
}).join(' ');
var cover = a.teamBacked === a.tasks.length
? '' + a.tasks.length + '/' + a.tasks.length + ' 有团队 '
: '' + a.teamBacked + '/' + a.tasks.length + ' 有团队 ' +
'' + (a.tasks.length - a.teamBacked) + ' 项属个人直派
';
var title = a.account || a.nickname || ('用户' + a.uid);
var sub = [];
if (a.account) sub.push('账号 ' + a.account);
if (a.nickname) sub.push('昵称 ' + a.nickname);
sub.push('uid ' + a.uid);
h += '' + esc(title) + ' ' + esc(sub.join(' · ')) + '
' +
'' + a.tasks.length + ' ' + wts + ' ' + detail + ' ' + cover + ' ';
});
return h + '
';
}
function renderUnassigned() {
if (!state.tasks.length) return '暂无数据
';
var none = state.tasks.filter(function (t) { return !hasAnyAssignee(t); });
var userOnly = state.tasks.filter(function (t) { return !hasAnyTeam(t) && hasAnyUser(t); });
var partial = state.tasks.filter(function (t) {
return hasAnyAssignee(t) && activeWts(t).some(function (wt) { return wtEmptyOf(t, wt); });
});
var h = '';
h += '① 完全没人承接(无团队也无个人):' + none.length + ' 个
';
if (none.length) {
h += '任务名称 task_key 标注类型 总题数 启用工序 ';
none.forEach(function (t) {
var wts = activeWts(t).map(function (wt) { return WORK_TYPES[wt]; }).join(' / ');
h += '' + esc(t.name) + ' ' + esc(t.key) + ' ' +
esc(t.type) + ' ' + t.total + ' ' + esc(wts) + ' ';
});
h += '
';
}
h += '② 只按个人分配、没有团队承接:' + userOnly.length + ' 个
';
if (userOnly.length) {
h += '任务名称 task_key 人员(按工序) ';
userOnly.forEach(function (t) {
h += '' + esc(t.name) + ' ' + esc(t.key) + ' ' + userCell(t) + ' ';
});
h += '
';
}
h += '③ 部分工序没人(团队与个人都没有):' + partial.length + ' 个
';
if (partial.length) {
h += '任务名称 空缺工序 团队分配 人员分配 ';
partial.forEach(function (t) {
var missing = activeWts(t).filter(function (wt) { return wtEmptyOf(t, wt); })
.map(function (wt) { return WORK_TYPES[wt]; }).join(' / ');
h += '' + esc(t.name) + '' + esc(t.key) + '
' +
'' + esc(missing) + ' ' +
'' + teamCell(t) + ' ' + userCell(t) + ' ';
});
h += '
';
}
return h;
}
function renderRaw() {
var sample = state.rawSample;
var h = '用于核对字段。第一条任务的原始 allocation_info / access_info:
';
if (!sample) return h + '暂无数据
';
var o = {};
['task_batch_key', 'task_batch_name', 'task_type', 'total_num', 'accept_num', 'check_num',
'inspect_num', 'done_num', 'online_status', 'access_info', 'allocation_info'].forEach(function (k) {
o[k] = sample[k];
});
h += '' + esc(JSON.stringify(o, null, 2)) + ' ';
h += '团队列表接口:
';
h += '' + esc(JSON.stringify(state.guilds.slice(0, 5), null, 2)) + ' ';
return h;
}
function renderFoot() {
var foot = shadow.getElementById('foot');
var fs = cfg.fontScale || 1.15;
var fsOpts = [['1', '小'], ['1.15', '标准'], ['1.3', '大'], ['1.5', '特大'], ['1.7', '超大']];
foot.innerHTML =
' 自动刷新 ' +
'间隔 ' +
'秒 ' +
' 表格内标注 ' +
'字号 ' +
fsOpts.map(function (o) {
return '' + o[1] + ' ';
}).join('') + ' ' +
' ' +
'只读 · GET ';
foot.querySelector('#fs').addEventListener('change', function (e) {
cfg.fontScale = Number(e.target.value) || 1.15;
saveCfg(); applyGeom(); renderFoot();
});
foot.querySelector('#chkAuto').addEventListener('change', function (e) {
cfg.autoRefresh = e.target.checked; saveCfg(); setupTimer();
});
foot.querySelector('#intv').addEventListener('change', function (e) {
var v = Math.max(3, Math.min(600, parseInt(e.target.value, 10) || 15));
cfg.intervalSec = v; e.target.value = v; saveCfg(); setupTimer();
});
foot.querySelector('#chkBadge').addEventListener('change', function (e) {
cfg.inlineBadge = e.target.checked; saveCfg();
if (cfg.inlineBadge) injectBadges(); else removeBadges();
});
}
/* ============================ 页面表格内标注 ============================ */
function removeBadges() {
document.querySelectorAll('.dsh-tb-badge').forEach(function (b) { b.remove(); });
}
function badgeHtml(task) {
var wts = activeWts(task);
var teamLine = wts.map(function (wt) {
var names = teamNames(task, wt);
var color = WORK_COLOR[wt] || '#909399';
var txt = names.length ? names.join('、') : '未分配';
var fg = names.length ? color : '#f56c6c';
return '' +
WORK_TYPES[wt] + '·' + esc(txt) + ' ';
}).join('');
if (!hasAnyUser(task)) return teamLine;
var userLine = wts.map(function (wt) {
var names = userNames(task, wt);
if (!names.length) return '';
var color = WORK_COLOR[wt] || '#909399';
return '' +
'👤' + WORK_TYPES[wt] + '·' + esc(names.join('、')) + ' ';
}).join('');
return teamLine + (userLine ? '' + userLine + '
' : '');
}
function injectBadges() {
if (!cfg.inlineBadge) return;
var tables = document.querySelectorAll('.el-table__body-wrapper table');
if (!tables.length) return;
Array.prototype.forEach.call(tables, function (table) {
var ths = table.querySelectorAll('thead th');
if (!ths.length) return;
var headers = Array.prototype.map.call(ths, function (th) { return (th.innerText || '').trim(); });
var keyIdx = headers.indexOf('任务key');
if (keyIdx < 1) {
// 兜底:找哪一列的内容像 task_key
keyIdx = headers.length > 1 ? 1 : -1;
}
if (keyIdx < 1) return;
var trs = table.querySelectorAll('tbody tr');
Array.prototype.forEach.call(trs, function (tr) {
var tds = tr.querySelectorAll('td');
if (tds.length <= keyIdx) return;
var taskKey = (tds[keyIdx].innerText || '').trim();
var task = state.taskMap[taskKey];
var nameCell = tds[keyIdx - 1];
if (!nameCell) return;
var cell = nameCell.querySelector('.cell') || nameCell;
if (!task) {
var old = cell.querySelector('.dsh-tb-badge');
if (old) old.remove();
return;
}
var badge = cell.querySelector('.dsh-tb-badge');
if (!badge) {
badge = document.createElement('div');
badge.className = 'dsh-tb-badge';
badge.style.cssText = 'margin-top:3px;display:flex;flex-wrap:wrap;gap:3px;';
cell.appendChild(badge);
}
var html = badgeHtml(task);
if (badge.getAttribute('data-sig') !== html) {
badge.setAttribute('data-sig', html);
badge.innerHTML = html;
}
});
});
}
function ensureBadgeStyles() {
if (document.getElementById('dsh-tb-badge-style')) return;
var s = document.createElement('style');
s.id = 'dsh-tb-badge-style';
s.textContent = '.dsh-tb-chip{display:inline-block;padding:1px 7px;border-radius:9px;' +
'font-size:12.5px;line-height:19px;border:1px solid transparent;white-space:nowrap;}';
document.head.appendChild(s);
}
/* ============================ 页面 XHR 钩子(同步兜底) ============================ */
function hookXhr() {
try {
var XP = XMLHttpRequest.prototype;
var rawOpen = XP.open, rawSend = XP.send;
XP.open = function (method, url) {
try { this.__dsh_url = url; this.__dsh_method = method; } catch (e) {}
return rawOpen.apply(this, arguments);
};
XP.send = function () {
var xhr = this;
try {
xhr.addEventListener('load', function () {
try {
var u = xhr.__dsh_url || '';
if (!/\/task-batches(\?|$)/.test(u)) return;
if (String(xhr.__dsh_method).toUpperCase() !== 'GET') return;
var res = JSON.parse(xhr.responseText);
if (!isOk(res)) return;
var items = (res.data && res.data.items) || [];
if (!items.length) return;
// 页面自己拉到的数据:仅在接口拉取失败时作为兜底
if (state.error) {
state.tasks = items.map(normalizeTask);
state.taskMap = {};
state.tasks.forEach(function (t) { if (t.key) state.taskMap[t.key] = t; });
state.rawSample = items[0];
state.source = 'page';
state.lastUpdate = new Date();
state.error = '';
render();
}
injectBadges();
} catch (e) {}
});
} catch (e) {}
return rawSend.apply(this, arguments);
};
} catch (e) {}
}
/* ============================ 定时 / 观察 ============================ */
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) * 1000);
}
function setupObserver() {
if (domObserver) domObserver.disconnect();
domObserver = new MutationObserver(function () {
if (badgeTimer) return;
badgeTimer = setTimeout(function () {
badgeTimer = null;
try { injectBadges(); } catch (e) {}
}, 300);
});
domObserver.observe(document.body, { childList: true, subtree: true });
}
function setupRouteWatch() {
setInterval(function () {
if (location.pathname === currentPath) return;
currentPath = location.pathname;
if (/\/v\/project\/[^/]+\/project-taskManage/.test(currentPath)) {
ensurePanel();
if (host) host.style.display = '';
barSig = null;
loadAll();
} else if (host) {
host.style.display = 'none';
}
}, 800);
window.addEventListener('resize', function () { applyGeom(); });
}
/* ============================ 导出 ============================ */
function exportCsv() {
var lines = [];
lines.push(['任务名称', 'task_key', '标注类型', '总题数', '工序', '分配团队', '分配人员', '已完成', '启用'].join(','));
state.tasks.forEach(function (t) {
activeWts(t).forEach(function (wt) {
var names = teamNames(t, wt);
lines.push([
csvCell(t.name), csvCell(t.key), csvCell(t.type), t.total,
WORK_TYPES[wt], csvCell(names.join('、') || '未分配'),
csvCell(userNames(t, wt).join('、')),
(t.progress[wt] || 0) + '/' + t.total,
wtEnabled(t, wt) ? '是' : '否'
].join(','));
});
});
var blob = new Blob(['\ufeff' + lines.join('\r\n')], { type: 'text/csv;charset=utf-8' });
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = '任务团队分配_' + projectKey() + '_' + Date.now() + '.csv';
document.body.appendChild(a);
a.click();
setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
}
function csvCell(v) {
var s = String(v === undefined || v === null ? '' : v);
if (/[",\r\n]/.test(s)) s = '"' + s.replace(/"/g, '""') + '"';
return s;
}
/* ============================ 启动 ============================ */
function boot() {
if (!/\/v\/project\/[^/]+\/project-taskManage/.test(location.pathname)) return;
ensureBadgeStyles();
ensurePanel();
hookXhr();
setupObserver();
setupRouteWatch();
setupTimer();
loadAll();
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
setTimeout(boot, 300);
} else {
window.addEventListener('DOMContentLoaded', function () { setTimeout(boot, 300); });
}
})();